OpenHands / OpenHands/software-agent-sdk

[Bug]: Codex ACP on a headless host: `authenticate("chat-gpt")` does not complete within `ACP_AUTH_TIMEOUT` although Codex is logged in ("ChatGPT authentication did not complete in time")

Open
#5,167 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

acp bug priority:medium
Dominant language
Python
Stars
1.1k
Forks
539
Avg merge
1d 19h
Merged PRs (30d)
137

Description

Is there an existing issue for the same bug?
  • I have searched existing issues and this is not a duplicate.

Closest related items: #3629 (closed; added the startup/auth timeout that now produces the error below). In codex-acp: agentclientprotocol/codex-acp#243 and agentclientprotocol/codex-acp#247 (early return when already logged in), agentclientprotocol/codex-acp#464 (headless fail-fast, closed unmerged), agentclientprotocol/codex-acp#495 (expired-credential error mapping). None of them covers this case: a ChatGPT login that is already usable, where authenticate("chat-gpt") still does not complete on a headless host.

Bug Description

In a containerized, headless Agent Canvas 1.20.0 deployment, starting a Codex (ACP) conversation fails after ~30 s with:

ChatGPT authentication did not complete in time. Please sign in again.

The Codex ChatGPT subscription login in the container is usable. The Codex CLI bundled with codex-acp reports Logged in using ChatGPT, and driving codex-acp directly over ACP stdio without the authenticate call gives a successful session/new and real inference. The failure happens only because the SDK sends authenticate(methodId: "chat-gpt") before session/new. On this host that request did not respond within the SDK's 30 s budget, nor within a ~90 s direct-probe observation window.

Reading codex-acp 1.10.0 suggests why, as the leading inference (the same function is unchanged in v1.12.0). authenticateWithChatGpt() returns early only if account/read {refreshToken: true} reports a chatgpt account. Otherwise it calls account/login {type: "chatgpt"}, tries to open() the auth URL in a browser and awaits account/login/completed. With no display there is no browser and no loopback callback, so no completion of that branch was observed. The SDK's 30 s ACP_AUTH_TIMEOUT then converts that into ACPFileCredentialNeedsReauthError.

We have not established why account/read {refreshToken: true} did not report a ChatGPT account in this environment (see "Open question" below).

Expected Behavior

When Codex already has usable ChatGPT subscription authentication state, the Agent Canvas / ACP integration should be able to establish a Codex session without initiating a redundant interactive authentication flow that cannot complete in the headless runtime.

At minimum, when no interactive login is possible on a headless host, the session should fail fast with an actionable error. It should not wait on a browser flow that has no observed way to complete on that host.

Actual Behavior

Agent Canvas selects the ChatGPT authentication method and invokes the ACP authenticate("chat-gpt") flow. In the observed headless deployment this did not complete, so the 30 s Canvas/SDK authentication timeout fired. This happened even though the same Codex installation reports that it is logged in using ChatGPT, and raw ACP session creation performs genuine inference when the authenticate call is not made.

User-facing error after ~30 s:

ChatGPT authentication did not complete in time. Please sign in again.
Steps to Reproduce

These steps consistently reproduced the failure in the affected deployment after the Codex profile was registered. One earlier conversation on the same versions succeeded (see "Open question"), so the trigger is conditional. The failure is expected when codex-acp's account/read {refreshToken: true} does not report a ChatGPT account.

  1. Run the Agent Canvas 1.20.0 all-in-one image on a headless Linux host (no DISPLAY / WAYLAND_DISPLAY).
  2. Provide a ChatGPT-mode Codex login at ~/.codex/auth.json in the container. Running the Codex CLI bundled with codex-acp (@openai/codex under codex-acp's node_modules; it is not on PATH) as codex login status prints Logged in using ChatGPT. Leave OPENAI_API_KEY / CODEX_API_KEY unset.
  3. Create an agent profile with acp_server: codex and make it active, or create a conversation with an ACPAgent whose acp_server is codex.
  4. Start a New Chat and send any message.
Acceptance Criteria
  • With a usable ChatGPT Codex login present on a headless host, a Codex ACP conversation starts without waiting on an interactive browser login.
  • When an interactive login would be required but cannot complete (headless), the user receives a fast, actionable error rather than a 30 s timeout that says "sign in again".
  • Credential validity is still enforced by codex-acp/Codex (see negative tests below); an auth.json-shaped file alone is not treated as authentication.
Installation Method

Agent Canvas all-in-one Docker image ghcr.io/openhands/agent-canvas:1.20.0 (the SDK is embedded in the frozen agent-server binary). A thin local layer only remaps the container UID/GID and makes no code changes.

SDK Version

openhands-sdk 1.49.1 (agent server /server_info: version 1.49.1, sdk_version 1.49.1, build_git_ref refs/tags/v1.49.1)

Version Confirmation

Not independently re-tested on v1.49.2 (Canvas 1.20.0 bundles 1.49.1). The relevant code (_select_auth_method and the authenticate timeout handling in openhands/sdk/agent/acp_agent.py) is unchanged from 1.49.1 through main as of 2026-09-17, so the failure is expected to reproduce there; codex-acp's authenticateWithChatGpt() is unchanged in v1.12.0 (latest).

Python Version

3.13.13 (frozen agent server)

Model Name (if applicable)

Codex ACP with ChatGPT subscription auth; profile acp_model: null (Codex default).

Operating System

Linux (Debian 13 container, headless)

Environment
Component Version / detail
Agent Canvas (all-in-one image) 1.20.0 (image revision 9737f713616a1e452f822c2967f0e2c8bf2dc308, refs/tags/v1.20.0)
Agent server (serving, frozen/PyInstaller binary) openhands-agent-server 1.49.1
openhands-sdk 1.49.1
@agentclientprotocol/codex-acp 1.10.0 (image-pinned)
Bundled Codex CLI (@openai/codex) 0.153.4
Deployment Docker, headless Linux host, no display server; container runs non-root with no-new-privileges and all capabilities dropped
Agent selection 1.20.0 agent profile: agent_kind: acp, acp_server: codex, acp_command: null, acp_model: null
Codex data dir acp_isolate_data_dir: false, CODEX_HOME unset, no CODEX_AUTH_JSON secret, so Codex uses ~/.codex/auth.json
Authentication Model
  • ChatGPT subscription only. No API-key auth: OPENAI_API_KEY, OPENAI_BASE_URL and CODEX_API_KEY are unset.
  • The host operator's Codex login file is bind-mounted read-only, as a single file, at ~/.codex/auth.json inside the container. The rest of ~/.codex is a writable container directory, where Codex writes its caches and SQLite state.
  • Because the file is read-only, Codex inside the container cannot persist a token refresh. Refreshes happen on the host. This is a deliberate deployment choice, and it may be relevant to the open question below.
Logs and Error Messages

Agent-server log sequence (1.49.1; line numbers as reported by the frozen bundle):

acp_agent.py:3086  Authenticating with ACP method: chat-gpt
acp_agent.py:2271  Failed to start ACP server: ChatGPT authentication did not complete in time. Please sign in again.
-> ACPFileCredentialNeedsReauthError

Code path (openhands/sdk/agent/acp_agent.py, 1.49.1; same on main at the time of writing):

# _select_auth_method
if "chat-gpt" in method_ids and codex_auth_file_is_chatgpt(env):
    return "chat-gpt"
...
# ACP start-up
await asyncio.wait_for(conn.authenticate(method_id=method_id, **auth_kwargs),
                       timeout=_ACP_AUTH_TIMEOUT)          # default 30.0
# TimeoutError + method_id == "chat-gpt" -> ACPFileCredentialNeedsReauthError(
#     "ChatGPT authentication did not complete in time. Please sign in again.")
Working control: raw ACP path

Same container, same read-only auth.json, codex-acp 1.10.0 driven directly over ACP stdio:

  1. codex login status (bundled CLI 0.153.4) prints Logged in using ChatGPT.
  2. initialize returns authMethods: ["api-key", "chat-gpt"] and agentInfo: {name: "@agentclientprotocol/codex-acp", version: "1.10.0"}.
  3. Skip authenticate. session/new {cwd, mcpServers: []} succeeds and returns a session id.
  4. session/prompt (a trivial "reply with a token" prompt) returns the correct reply, stopReason: end_turn, with real token usage reported. Round trip ~3.7 s.

So the credential, the adapter and the model access all work. Only the authenticate step fails.

Direct authenticate("chat-gpt") probe

Same container, raw ACP stdio against codex-acp 1.10.0:

-> initialize
<- result: authMethods ["api-key", "chat-gpt"], agentInfo codex-acp 1.10.0
<- notification _auth/status_update {"authStatus": {"kind": "none", "label": "Not logged in"}}
-> authenticate {"methodId": "chat-gpt"}
   (no response, no error, no login URL on stdout/stderr, until the probe was killed by an outer ~90 s timeout)

Note the _auth/status_update "Not logged in" notification after initialize, even though codex login status against the same file reports Logged in using ChatGPT.

The relevant codex-acp code (1.10.0 src/CodexAcpClient.ts; unchanged in v1.12.0):

private async authenticateWithChatGpt(): Promise<Boolean> {
    const accountResponse = await this.codexClient.accountRead({refreshToken: true});
    if (accountResponse.account?.type === "chatgpt") {
        return true;                                   // early return added by codex-acp#247
    }
    const loginCompletedPromise = this.awaitNextLoginCompleted();
    const loginResponse = await this.codexClient.accountLogin({type: "chatgpt"});
    if (loginResponse.type == "chatgpt") {
        await open(loginResponse.authUrl);             // no browser on a headless host
    }
    const result = await loginCompletedPromise;        // no completion observed headlessly (inferred branch)
    return result.success;
}

Inference, not proven: account/read {refreshToken: true} did not report a chatgpt account here, so the code fell through to the browser login and blocked. This matches the observed "Not logged in" status notification and the authenticate request that did not respond within the observation window.

Open question: what changes the account/read result

On the same image and versions, one SDK-mediated Codex conversation did succeed earlier the same day, about one hour before the failure was first seen. It was created through the agent-server REST API with an explicit acp_command: ["codex-acp"] and acp_model: "gpt-5.6-sol", in a freshly created container. Every later SDK-mediated attempt failed as described. We have not determined what distinguishes the two. Candidates we could not verify without touching the credential:

  • the age / expiry state of the access token in the read-only auth.json (the host had not rewritten the file in between);
  • the forced refresh requested by account/read {refreshToken: true} failing, or not being persistable, against a read-only auth.json;
  • container-local Codex state under ~/.codex that existed on later attempts but not in the fresh container.

The explicit acp_command / acp_model difference is unlikely to matter: both resolve to the same binary, and neither is an input to _select_auth_method. It is listed for completeness.

Negative authentication safety evidence

We checked that skipping the redundant authenticate would not become an auth bypass, because codex-acp enforces authentication itself at session/new:

Credential state codex_auth_file_is_chatgpt() SDK selection session/new with authenticate skipped
absent False None rejected {"code": -32000, "message": "Authentication required"}
well-formed but bogus (synthetic placeholder values) True "chat-gpt" rejected {"code": -32000, "message": "Authentication required"}, no inference
real, usable ChatGPT login True "chat-gpt" accepted, real inference
absent, OPENAI_API_KEY set (isolated test) False "api-key" (API-key fallback selection unchanged)

The diagnostic result does not imply that possession of an auth.json-shaped file should be trusted as authentication. codex_auth_file_is_chatgpt() is a structural check of the file, and it does not check expiry or revocation. A bogus but structurally well-formed credential was rejected by codex-acp at session creation, so session/new is the real enforcement point. We are not asking for auth.json to be trusted blindly.

Why this appears to be an integration defect
  • The SDK calls authenticate("chat-gpt") on the assumption that it is cheap or idempotent when a ChatGPT auth.json exists. agentclientprotocol/codex-acp#247 made it idempotent only when account/read reports a ChatGPT account.
  • When that check does not pass, codex-acp falls back to a browser login with no headless failure path. The SDK cannot tell "login in progress" from "login impossible here", so it waits the full 30 s and then tells the user to "sign in again". Signing in again is not expected to help, because the control test shows the existing login is already usable (a fresh re-login was not tested).
  • An unmerged codex-acp PR (agentclientprotocol/codex-acp#464, "hide browser ChatGPT auth on headless hosts and fail fast") describes the same headless hang from a different client. It was closed without merge, and v1.12.0 still has the fall-through.
Local workarounds investigated

For maintainers' context only. None of these is proposed as the fix.

  • Skipping the chat-gpt selection in _select_auth_method (a local diagnostic patch). It is semantically safe according to the negative tests above. It could not take effect: the serving agent server is a PyInstaller binary with an embedded copy of openhands.sdk, so patching the installed site-packages changes nothing. It is not deployed.
  • Running python3 -m openhands.agent_server from site-packages instead. Rejected: the installed openhands-agent-server in the 1.20.0 image is 1.27.1, which lacks the /api/agent-profiles router that Canvas 1.20.0 relies on. The serving 1.49.1 agent server exists only inside the frozen binary.
  • Raising ACP_AUTH_TIMEOUT is not expected to help. The direct probe saw no response in ~90 s, three times the default budget, and the inferred browser flow has no observed completion path headlessly. There is no supported setting to skip the authenticate call.
Impact
  • Codex can become unusable through Agent Canvas 1.20.0 on headless/containerized deployments that use a ChatGPT subscription login, even though Codex itself works. In the affected deployment it failed on every attempt after the first successful conversation.
  • The error message points users at re-authentication, which is not expected to resolve it, because the existing login already works (a fresh re-login was not tested).
  • Claude Code ACP on the same deployment is unaffected.
Suggested upstream investigation

For maintainers to decide the right architecture. These are suggestions only:

  1. SDK: avoid starting a fresh interactive authenticate("chat-gpt") when a ChatGPT login file is already present. For example, attempt session/new first and authenticate only on -32000 Authentication required, or use an ACP auth-status mechanism if one becomes available (discussed in agentclientprotocol/codex-acp#243).
  2. SDK: in the error shown to users, distinguish "interactive login impossible here (headless)" from "credential needs re-auth".
  3. codex-acp: on headless hosts, do not block in authenticateWithChatGpt(). Fail fast, or do not advertise browser chat-gpt (the approach sketched in the unmerged agentclientprotocol/codex-acp#464).
  4. codex-acp / Codex: check why account/read {refreshToken: true} does not report a ChatGPT account when the CLI reports Logged in using ChatGPT, and whether a read-only auth.json (refresh not persistable) affects it.
Security / privacy notes
  • No credentials, tokens, session keys or real auth.json contents are included. The negative test used a synthetic file with placeholder values.
  • Any fix should keep codex-acp (or Codex) as the enforcement point for credential validity. The SDK's codex_auth_file_is_chatgpt() is a shape check and should not become a validity oracle.
  • Full logs are available on request, with secrets redacted.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in openhands/sdk/agent/acp_agent.py at _select_auth_method and the authenticate timeout handling, then compare the raw ACP path that skips authenticate with the failing chat-gpt flow. Reproduce the headless case and inspect the referenced codex-acp src/CodexAcpClient.ts account-read and login behavior. Done means usable ChatGPT credentials start a session without an interactive wait, while required authentication fails fast with an actionable error and invalid credentials remain rejected.

Written by the indexing model from the issue text.

Assessment

Tech stack
python, typescript
Domain
api, authentication, backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.