strong-ice-84145
07/16/2026, 5:00 PMstrong-ice-84145
07/16/2026, 5:01 PMstrong-ice-84145
07/16/2026, 5:02 PMcalm-artist-7206
07/16/2026, 5:18 PMapp-server and make it a supported backend for agent-canvas . Ref: Isolated containers for each conversation
◦ (we have two PRs for it)
• the OpenHands PR seems to be Saas-only
• afaik, ACP is supported (?) in app-server backend, or isn’t it?
• it seems to me that this concurrency bug would happen there too?
Please tell me what I’m missing!calm-artist-7206
07/16/2026, 5:27 PMapp-server. And Christopher has worked on fixing WS for it.
So this is a fair question, it seems to me: if we are maintaining the app-server as a backend, then, well, we cannot solve bugs only on the Saas, right… But, I’m sorry for not keeping tabs on this, maybe we should start with the beginning: is ACP supported on the app-server alone? (without enterprise code)limited-student-10747
07/16/2026, 5:32 PMapp-server (probably renamed to sandbox-server). Let me write up a summary of the current thinking in a momentcalm-artist-7206
07/16/2026, 5:36 PMapp-server , in the wild hope that it’s maybe not a lot on top of sandbox orchestration, but it turns out that it is quite a lot:
https://github.com/enyst/app-server
Because then it needs to manage settings and all stuff that is shared between sandboxes… doesn’t look quite “minimal” 😭limited-student-10747
07/16/2026, 5:40 PMlimited-student-10747
07/20/2026, 1:51 PMcalm-artist-7206
07/20/2026, 2:28 PMagent-server case, it means the same concurrency issue exists in the Open Source project, even if we consider it unsupported for the legacy app-server
Could we make an issue on it?
https://github.com/OpenHands/OpenHands/pull/15287#discussion_r3603163578calm-artist-7206
07/20/2026, 2:31 PMcalm-artist-7206
07/20/2026, 2:32 PMcalm-artist-7206
07/20/2026, 2:33 PMstrong-ice-84145
07/20/2026, 3:38 PMstrong-ice-84145
07/20/2026, 3:58 PMConclusion: do not point concurrent local-Docker conversations at one writable CODEX_HOME for ChatGPT auth.
Why:
- Every conversation launches a separate Codex app-server process. Codex's refresh semaphore is process-local, so it cannot coordinate conversations.
- Codex 0.144.6 reads auth.json directly and saves it using truncate(true) followed by write_all, without an inter-process lock or atomic replacement. Exact pinned source
(<https://github.com/openai/codex/blob/rust-v0.144.6/codex-rs/login/src/auth/storage.rs#L191-L223>)
- The OpenHands broker correctly serializes upstream token rotation, but it cannot serialize the subsequent writes performed independently by each Codex process.
- With synthetic ChatGPT OAuth credentials and eight concurrent Codex 0.144.6 processes, I observed the shared auth.json becoming empty during refresh writes. A separate stress run left it persistently malformed.
- OpenAI documents that local clients share cached login state, but it does not promise concurrent file-write safety. Normal interactive CLI usage has much less contention than an agent server running many simultaneous conversations.
Authentication documentation (<https://developers.openai.com/codex/auth>)
The best design for #15287 remains:
- One authoritative brokered credential per user.
- One disposable, writable CODEX_HOME working copy per conversation.
- Propagate rotations back through the broker with CAS, as the PR now does.
Those duplicate files are therefore isolation copies, not duplicate sources of truth. A shared home would only become defensible after Codex adds an inter-process transaction lock around load/modify/save/delete plus atomic file replacement—or
if OpenHands multiplexes every conversation through one shared Codex app-server, which is a substantially larger architectural change.calm-artist-7206
07/20/2026, 4:17 PMcalm-artist-7206
07/20/2026, 4:18 PMstrong-ice-84145
07/20/2026, 4:18 PMIt is one Codex process versus several Codex processes.
Direct Codex app-server:
Conversation A ─┐
Conversation B ─┼─> one Codex app-server
Conversation C ─┘ └─ one AuthManager
└─ one refresh lock
└─ auth.json
All conversations use the same in-memory authentication manager. If A refreshes, B waits; afterward B reloads the new credentials and skips its own refresh. Only one process writes auth.json.
Local Docker with OpenHands ACP:
Conversation A -> codex-acp A -> Codex app-server A -> refresh lock A ┐
Conversation B -> codex-acp B -> Codex app-server B -> refresh lock B ├-> shared auth.json
Conversation C -> codex-acp C -> Codex app-server C -> refresh lock C ┘
Each OpenHands conversation launches a separate codex-acp, and each adapter launches its own codex app-server. The locks are therefore independent. codex-acp process creation
(<https://github.com/agentclientprotocol/codex-acp/blob/v1.1.2/src/CodexJsonRpcConnection.ts#L15-L42>)
Even with the OpenHands broker:
1. A and B both load refresh token r0.
2. The broker safely rotates r0 → r1 only once.
3. Both Codex processes receive r1.
4. Both independently truncate and rewrite the same auth.json.
5. One process can read the file while it is empty or partially written.
The broker protects the logical token rotation, but not the subsequent filesystem writes. Codex currently performs those writes without an inter-process lock or atomic replacement. Codex file-auth implementation
(<https://github.com/openai/codex/blob/rust-v0.144.6/codex-rs/login/src/auth/storage.rs#L191-L223>)
One important qualification: if by "backend agent-server directly" you mean an OpenHands agent-server started natively that still creates one ACP subprocess per conversation, then it is not safer. It has the same problem. Sharing is safe only
when one Codex app-server process owns the home and multiplexes all conversations—and no separate CLI/login/logout process writes there concurrently.strong-ice-84145
07/20/2026, 4:20 PMcalm-artist-7206
07/20/2026, 4:20 PMstrong-ice-84145
07/20/2026, 4:54 PMstrong-ice-84145
07/20/2026, 4:55 PMcalm-artist-7206
07/20/2026, 4:55 PMcalm-artist-7206
07/20/2026, 4:56 PMstrong-ice-84145
07/20/2026, 4:59 PMCurrent topology:
Default local Docker
conversation A container
OpenHands agent-server → ACPAgent A → codex-acp A → Codex app-server A
conversation B container
OpenHands agent-server → ACPAgent B → codex-acp B → Codex app-server B
Direct OpenHands agent-server
one OpenHands agent-server
ACPAgent A → codex-acp A → Codex app-server A
ACPAgent B → codex-acp B → Codex app-server B
Local Docker defaults to NO_GROUPING, which deliberately creates a new sandbox/container for every conversation. The setting defaults to NO_GROUPING
(<https://github.com/OpenHands/OpenHands/blob/7db8bd4d92d6db33816cb534799b0bb618bbb7de/openhands/app_server/settings/settings_models.py#L605-L693>), and the Docker sandbox service then starts a separate container
(<https://github.com/OpenHands/OpenHands/blob/7db8bd4d92d6db33816cb534799b0bb618bbb7de/openhands/app_server/sandbox/docker_sandbox_service.py#L385-L495>).
A directly run OpenHands agent-server can host multiple conversations, but that does not mean one Codex process. Every conversation gets its own ACPAgent, and each ACPAgent owns and spawns its own ACP subprocess
(<https://github.com/OpenHands/software-agent-sdk/blob/68aa583ebf07efefbb9219b63859c1aacecaf7b3/openhands-sdk/openhands/sdk/agent/acp_agent.py#L1668-L1674>). The launch happens once per agent instance
(<https://github.com/OpenHands/software-agent-sdk/blob/68aa583ebf07efefbb9219b63859c1aacecaf7b3/openhands-sdk/openhands/sdk/agent/acp_agent.py#L2333-L2470>). Therefore, sharing ~/.codex from a direct agent-server is also unsafe when multiple
Codex conversations run concurrently.
Could Docker use one Codex process? Yes, architecturally. The upstream adapter already supports it:
- One codex-acp process starts exactly one Codex app-server (<https://github.com/agentclientprotocol/codex-acp/blob/v1.1.2/src/index.ts#L71-L127>).
- It maintains multiple session states keyed by session ID (<https://github.com/agentclientprotocol/codex-acp/blob/v1.1.2/src/CodexAcpServer.ts#L145-L173>).
- It explicitly tests independent sessions and concurrent prompts (<https://github.com/agentclientprotocol/codex-acp/blob/v1.1.2/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts#L917-L1022>).
But simply enabling sandbox grouping or pointing every conversation at one CODEX_HOME does not accomplish that. OpenHands would need a sandbox-owned Codex/ACP manager instead of conversation-owned subprocesses. It must also route events,
approvals, cancellation, masking and teardown by session. More importantly, OpenHands currently injects each conversation's entire secret registry into that conversation's subprocess environment. A shared process would mix conversation-
specific secrets unless that mechanism were redesigned.
Default Docker isolation creates another constraint: a Codex process inside container A cannot operate on container B's filesystem and runtime. A shared process therefore requires grouped conversations in the same sandbox, or a much larger
remote-execution design.
There is also a subtle Codex 0.144.6 caveat: one app-server shares its main AuthManager across threads and serializes its refreshes, but it also constructs a separate auth manager for its background cloud-config loader. Meanwhile, file
storage rewrites auth.json using truncate-and-write without a file lock (<https://github.com/openai/codex/blob/rust-v0.144.6/codex-rs/login/src/auth/storage.rs#L191-L218>). So one process substantially reduces the conversation-driven race, but
it is not yet a complete storage-safety guarantee.
My conclusion:
- Do not point current local-Docker conversations at a shared CODEX_HOME.
- Do not consider direct OpenHands agent-server sharing safe either; it currently launches multiple Codex processes.
- Keep the per-conversation homes and broker work for the current PR.
- A future simplification should be a sandbox-scoped, multi-session Codex process for grouped, same-user conversations—with session-scoped secrets and lifecycle routing. That is a separate architectural change, not a small replacement for the
broker.calm-artist-7206
07/20/2026, 5:01 PMcalm-artist-7206
07/20/2026, 5:02 PMcalm-artist-7206
07/20/2026, 5:04 PMcalm-artist-7206
07/20/2026, 5:05 PMstrong-ice-84145
07/20/2026, 5:06 PMcalm-artist-7206
07/20/2026, 5:06 PMstrong-ice-84145
07/20/2026, 5:06 PMcalm-artist-7206
07/20/2026, 5:08 PMcalm-artist-7206
07/20/2026, 5:08 PMstrong-ice-84145
07/20/2026, 5:09 PMstrong-ice-84145
07/20/2026, 5:09 PMcalm-artist-7206
07/20/2026, 5:09 PMstrong-ice-84145
07/20/2026, 5:10 PMstrong-ice-84145
07/20/2026, 5:10 PMcalm-artist-7206
07/20/2026, 5:11 PMcalm-artist-7206
07/20/2026, 5:11 PMcalm-artist-7206
07/20/2026, 5:13 PMstrong-ice-84145
07/20/2026, 5:13 PMcalm-artist-7206
07/20/2026, 5:13 PMstrong-ice-84145
07/20/2026, 5:16 PMcalm-artist-7206
07/20/2026, 5:21 PMstrong-ice-84145
07/20/2026, 5:31 PMlimited-student-10747
07/20/2026, 5:32 PMstrong-ice-84145
07/20/2026, 5:33 PMstrong-ice-84145
07/20/2026, 5:34 PM## Why a shared CODEX_HOME is unsafe
Codex 0.144.6 writes auth.json by opening it with truncate(true) and writing directly. It does not use atomic replacement or an interprocess file lock. Exact Codex source
(<https://github.com/openai/codex/blob/rust-v0.144.6/codex-rs/login/src/auth/storage.rs#L202-L218>).
Codex has a refresh semaphore, but it belongs to one AuthManager inside one Codex process. It does not coordinate the separate Codex app-server processes launched by separate OpenHands conversations. Refresh locking source
(<https://github.com/openai/codex/blob/rust-v0.144.6/codex-rs/login/src/auth/manager.rs#L2362-L2413>).calm-artist-7206
07/20/2026, 5:40 PMFixing this by having one ground truth somewhere and a credential broker handling sync with several processes , whether its local / docker / saas, seems like the only way forwardThis sounds to me like, we already added a lot of complexity, the only way forward is to add more ?
calm-artist-7206
07/20/2026, 5:41 PMI’m a little bit confused — is it not possible to have two codex CLIs open at the same time through the same authentication? I’ve done this before and it seems to work fineYes… Maybe this is a problem in theory, but the probability is very low?
calm-artist-7206
07/20/2026, 5:41 PMstrong-ice-84145
07/20/2026, 5:43 PMThis sounds to me like, we already added a lot of complexity, the only way forward is to add more ?If this fits with this github.com/OpenHands/OpenHands/issues/15298 we could be OK
strong-ice-84145
07/20/2026, 5:43 PMBut Codex 0.144.6 has a much better mechanism we could technically use: external ChatGPT auth.
The flow would be:
1. Start Codex with no auth.json.
2. Initialize app-server with experimentalApi: true.
3. Call account/login/start with type: "chatgptAuthTokens", supplying only the access token, account ID, and plan.
4. When Codex receives a 401, it calls the host via account/chatgptAuthTokens/refresh.
5. Our broker rotates the authoritative refresh token and returns only the new access token.
Codex keeps these externally supplied tokens in memory rather than persisting them. After login, it also replaces the cloud-config loader with the primary auth manager, so the runtime and cloud-config path use the same manager. Protocol
(<https://github.com/openai/codex/blob/rust-v0.144.6/codex-rs/app-server-protocol/src/protocol/v2/account.rs#L90-L106>), in-memory auth mode
(<https://github.com/openai/codex/blob/rust-v0.144.6/codex-rs/app-server-protocol/src/protocol/common.rs#L26-L33>), manager rebinding
(<https://github.com/openai/codex/blob/rust-v0.144.6/codex-rs/app-server/src/request_processors/account_processor.rs#L658-L674>).
That would remove most of our current complexity:
- No duplicated auth.json.
- No refresh tokens inside conversation containers.
- No file PUT/CAS reconciliation or final credential synchronization.
- No reason to share CODEX_HOME for authentication.
- The broker still owns encryption, capability lifecycle, and cross-conversation refresh serialization.
The blocker is codex-acp. Our pinned 1.1.2 initializes Codex without experimental capabilities and only implements normal file/browser ChatGPT auth; it does not handle the refresh callback. codex-acp 1.1.2 source
(<https://github.com/agentclientprotocol/codex-acp/blob/8aff492d4b033ff2c02ad3b9d591994d57617463/src/CodexAcpClient.ts#L70-L104>). We would need an upstream change or fork.
The other serious caveat is that Codex explicitly labels this API "FOR OPENAI INTERNAL USE ONLY — DO NOT USE."strong-ice-84145
07/20/2026, 5:44 PMFOR OPENAI INTERNAL USE ONLY — DO NOT USE. huhstrong-ice-84145
07/20/2026, 5:47 PMcalm-artist-7206
07/20/2026, 5:49 PMcalm-artist-7206
07/20/2026, 5:50 PMcalm-artist-7206
07/20/2026, 5:51 PMcalm-artist-7206
07/20/2026, 5:51 PMcalm-artist-7206
07/20/2026, 5:56 PMstrong-ice-84145
07/20/2026, 6:01 PMstrong-ice-84145
07/20/2026, 6:01 PMcalm-artist-7206
07/20/2026, 6:05 PMcalm-artist-7206
07/20/2026, 6:22 PM> This sounds to me like, we already added a lot of complexity, the only way forward is to add more ?
If this fits with this github.com/OpenHands/OpenHands/issues/15298 we could be OKMultiple codex auth.json files in conversations logs… doesn’t fit with the goal of 15298 🤔 I think maybe we are making this too complicated. If this works:
one place, same as regular codex CLIthen it should work the same • inside docker (slightly different “home”) • on a remote vm • in isolated cloud sandboxes (same as docker) • in shared configurations (one per each box) What if we clean up the per-conversation credential locations?
calm-artist-7206
07/20/2026, 7:17 PMperhaps what we need is a happy recovery path if one day this concurrency bug happens?Maybe like • catch the exception, poke codex to refresh... if that's possible • or, not do anything, we can cross that bridge when we come to it?
strong-ice-84145
07/21/2026, 9:28 AMMultiple codex auth.json files in conversations logs… doesn’t fit with the goal of 15298 🤔It does. Ground truth current codex auth secret can be stored in a Cred Store that safely allows concurrent secret rewrite. Codex ACP still owns the detection of new secret in the auth.json + the responsability of rewriting each auth.json for each conversation Still thinking of simplest fix to go forward
strong-ice-84145
07/21/2026, 3:12 PMstrong-ice-84145
07/21/2026, 3:17 PMcalm-artist-7206
07/21/2026, 3:31 PMcalm-artist-7206
07/21/2026, 3:41 PM