Skip to content

refactor(acp): use credentialKey() for Claude OAuth token lookup#31901

Merged
noanflaherty merged 5 commits into
mainfrom
fix/acp-claude-token-injection
May 24, 2026
Merged

refactor(acp): use credentialKey() for Claude OAuth token lookup#31901
noanflaherty merged 5 commits into
mainfrom
fix/acp-claude-token-injection

Conversation

@credence-the-bot
Copy link
Copy Markdown
Contributor

@credence-the-bot credence-the-bot Bot commented May 24, 2026

Summary

Follow-up to 2cf8e89a1 (which shipped CLAUDE_CODE_OAUTH_TOKEN injection for ACP claude-agent-acp spawns). The original commit had three gaps which this PR closes:

  1. Wrong secure-store key path. It hardcoded "acp/claude/oauth_token" instead of routing through credentialKey(), so assistant credentials set --service acp --field claude_oauth_token had no effect.
  2. Missed the secure-keys import allowlist. Adding the import without updating Invariant 2 left this branch's CI red since open. The allowlist exists to force per-importer review of the secret-leak surface.
  3. Warn-and-proceed on missing token. Spawn would launch claude-agent-acp without a token; the agent would then crash on auth, leaving callers with a defunct zombie subprocess and no useful signal.

This PR fixes all three plus the two Codex P2 findings on the first push.

Provisioning contract

CLAUDE_CODE_OAUTH_TOKEN can come from either of two routes. Config wins over vault so explicit per-workspace / rotated overrides are never silently clobbered:

  1. acp.agents.<id>.env.CLAUDE_CODE_OAUTH_TOKEN in config.json (first priority — explicit user override).
  2. Secure store via CLI (fallback when (1) is unset):
    assistant credentials set --service acp --field claude_oauth_token <token>

If neither produces a token, the spawn route throws FailedDependencyError with the CLI hint — symmetric with the existing binary_not_found preflight.

Changes

assistant/src/runtime/routes/acp-routes.ts

  • Route the secure-store lookup through credentialKey("acp", "claude_oauth_token") (resolves to credential/acp/claude_oauth_token).
  • Gate env injection on basename(agentConfig.command) === "claude-agent-acp", not on the user-facing agent id, so user-aliased agent ids (acp.agents.my-claude = { command: "claude-agent-acp", ... }) still get the env they need. (Codex P2 feat: initialize Next.js app in /web directory #1)
  • Look up the vault token only when agentConfig.env.CLAUDE_CODE_OAUTH_TOKEN is unset, so explicit config-json values are preserved. (Codex P2 feat: add platform terraform for GKE deployment #2)
  • Throw FailedDependencyError when no token is available from any source; error message includes the CLI hint plus the config-json route.

assistant/src/runtime/routes/acp-routes.test.ts — 7 tests in the new env-injection suite (was 4 in the first push):

  • Happy path: vault token → agentConfig.env.
  • Config-json env override accepted without a secure-store entry.
  • Precedence pin: vault + config both set → config wins.
  • Command-alias pin: user-defined agent id pointing at claude-agent-acp still triggers injection.
  • Throws FailedDependencyError when no token is available from any source.
  • Non-claude agents unaffected (gated by command basename, not id).
  • Legacy-key regression guard: token planted at the old acp/claude/oauth_token path is NOT picked up and the preflight throws.

assistant/src/__tests__/credential-security-invariants.test.ts

  • Add runtime/routes/acp-routes.ts to the secure-keys importer allowlist with a comment explaining the legitimate need (subprocess env injection).

Verification

  • bun test src/runtime/routes/acp-routes.test.ts20/20 pass
  • bun test src/__tests__/credential-security-invariants.test.ts32/32 pass
  • bun test src/acp/ → 31/31 pass (no regression)
  • bun run typecheck → clean
  • bun run lint → clean
  • bun x prettier --check → clean

Follow-up: codex-acp parity (parked)

A parallel preflight for codex-acp is the obvious next step, but codex-acp has a more complex auth surface than claude-agent-acp and warrants its own scoped PR:

  • Multiple auth modes. CODEX_API_KEY env var, file-based OAuth (browser login stored at ~/.codex/...), ChatGPT subscription OAuth. A naive "fail if no CODEX_API_KEY" would break users on the OAuth paths.
  • No existing injection. The route doesn't currently set any env for codex-acp; we'd need a new credential/acp/codex_api_key provisioning story.
  • Possible OAuth reuse — needs spike. The daemon already manages credential/openai-codex/access_token (with refresh logic) for the OpenAI Codex inference provider. Whether codex-acp accepts bearer tokens or strictly needs an sk-... API key is unclear without reading the @zed-industries/codex-acp source.
  • Not installed in this sandbox. Empirical verification of the env contract isn't possible from the current container.

The pattern this PR establishes (credentialKey() lookup + command-based gating + fail-fast preflight) is what codex-acp should follow once those questions are answered.

Other follow-ups parked

  • Container node shim. Fresh containers fail to spawn claude-agent-acp (shebang #!/usr/bin/env node, container only has bun). Workaround in place this sandbox via /usr/local/bin/node → /usr/local/bin/bun symlink; won't survive restart. Wants a Dockerfile/entrypoint hook.
  • Auth-path dedupe. cc-session reads /workspace/config/.cc-oauth-token; ACP now reads the secure store. Two sources of truth.

The daemon's ACP spawn was failing silently because claude-agent-acp requires
CLAUDE_CODE_OAUTH_TOKEN to authenticate, but the token was never being passed
in the spawned process's environment.

Root cause:
- The daemon process lacks CLAUDE_CODE_OAUTH_TOKEN in its env
- The default ACP profile for claude had no env field to inject it
- claude-agent-acp would exit immediately without registering

Solution:
- Fetch CLAUDE_CODE_OAUTH_TOKEN from the secure key store in spawnSession
- Inject it into the agent config's env before spawning
- Log a warning if the token is not found (but don't block spawn)

This matches the pattern used by other provider auth flows in the codebase
that use getSecureKeyAsync to fetch credentials at runtime.
- Replace raw 'acp/claude/oauth_token' string with credentialKey('acp', 'claude_oauth_token')
  to align with the assistant credentials CLI pattern
- Update warning message to direct users to: assistant credentials set --service acp --field claude_oauth_token <token>
- Clarify in comments that this is the canonical provisioning method
- All ACP route tests pass
Adds a CLAUDE_CODE_OAUTH_TOKEN injection suite to acp-routes.test.ts
that exercises the spawn handler's secure-key lookup path. The tests
extend the existing acp/index.js mock with a spawn() that captures
agentConfig, plus a secure-keys mock driven by a per-test store.

Coverage:
- happy path: token planted under credential/acp/claude_oauth_token
  ends up in agentConfig.env.CLAUDE_CODE_OAUTH_TOKEN for the claude
  agent
- absent key: spawn proceeds without env injection (warn logged)
- non-claude agent (codex): env injection is scoped to claude only
- legacy key regression guard: token at the old 'acp/claude/oauth_token'
  path is NOT picked up, locking in the credentialKey() switch

Also picks up an eslint --fix re-sort of the broadcastMessage import
in acp-routes.ts (alphabetical order).
@credence-the-bot
Copy link
Copy Markdown
Contributor Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5767c5468f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

// pattern so `assistant credentials set --service acp --field claude_oauth_token <token>`
// is the canonical way to provision it.
const agentConfig = { ...resolved.agent };
if (agent === "claude") {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Inject token based on command, not hardcoded agent id

The token lookup is gated by agent === "claude", but resolveAcpAgent() supports user-defined ACP agent IDs, so a custom ID that still points to claude-agent-acp (for example an alias in config.acp.agents) will skip injection and fail authentication. This should key off the resolved agent command/capability rather than a single ID string.

Useful? React with 👍 / 👎.

Comment on lines +97 to +99
agentConfig.env = {
...agentConfig.env,
CLAUDE_CODE_OAUTH_TOKEN: claudeToken,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not overwrite preconfigured Claude OAuth env value

When a secure-store token exists, this assignment always overwrites any CLAUDE_CODE_OAUTH_TOKEN already set in acp.agents.<id>.env. That can break existing setups that intentionally provide a specific token via agent config (for example per-workspace or rotated credentials) by silently replacing it with a stale/different vault value. Inject the vault token only if the env var is currently unset, or explicitly enforce/validate precedence.

Useful? React with 👍 / 👎.

The original injection commit (2cf8e89) added `getSecureKeyAsync` to
`runtime/routes/acp-routes.ts` without updating the Invariant 2 allowlist
in `credential-security-invariants.test.ts`. The acp-routes route has a
legitimate need: it injects CLAUDE_CODE_OAUTH_TOKEN into the
claude-agent-acp subprocess env. Add it explicitly so the secure-keys
import boundary continues to require per-importer review.

Fixes red CI on PR #31901.
…dence

Three changes to claude-agent-acp env handling on the spawn route:

1. Fail fast on missing CLAUDE_CODE_OAUTH_TOKEN. Previously the route
   would log a warning and let the spawn proceed; the agent would then
   crash on auth, leaving callers debugging a defunct zombie subprocess.
   Now we throw FailedDependencyError up front, symmetric with the
   existing `binary_not_found` preflight, with the CLI hint in the
   message so the fix is one shell line away.

2. Key env injection on the resolved command basename, not the
   user-facing agent id. A custom alias like
   `acp.agents.my-claude = { command: "claude-agent-acp", ... }`
   needs the same env as the default "claude" id. Addresses Codex P2
   feedback on PR #31901.

3. Treat config.json env as the override, vault as the fallback. The
   previous order (`{ ...agentConfig.env, CLAUDE_CODE_OAUTH_TOKEN:
   vaultValue }`) silently clobbered explicit per-workspace / rotated
   tokens set under `acp.agents.<id>.env`. The vault lookup is now
   gated on the config-env value being absent, which also avoids a
   secure-store call (and log line) on every spawn for users who
   provide the token via config. Addresses Codex P2 feedback on PR
   #31901.

Tests:
  - Existing happy-path and legacy-key regression tests pinned.
  - NEW: config.json env override accepted without secure-store entry.
  - NEW: config.json env override wins over vault token (precedence).
  - NEW: command-based injection fires for user-aliased agent ids.
  - NEW: throws FailedDependencyError when no token is available
    from any source.
  - 'non-claude agents unaffected' test updated to reflect command-
    basename gating semantics.
@credence-the-bot
Copy link
Copy Markdown
Contributor Author

Both Codex P2 findings addressed in adaa3f8e19eb:

#1 — command-based gating. Injection + preflight now key off basename(agentConfig.command) === "claude-agent-acp" instead of agent === "claude". A user alias like acp.agents.my-claude = { command: "claude-agent-acp", ... } now triggers injection. Pinned by the new injects via command match for a user-defined agent id aliased to claude-agent-acp test.

#2 — env precedence. Inverted: vault token is only consulted when agentConfig.env.CLAUDE_CODE_OAUTH_TOKEN is unset. Config-json explicit values are now preserved, so per-workspace / rotated tokens are no longer silently clobbered. Pinned by the new config.json env override wins over a secure-store token (precedence pin) test — vault vault-token-AAA, config config-token-BBB, asserts captured env is config-token-BBB. Side benefit: skips the secure-store call (and its log line) entirely when config provides.

Also fixed in the same push: the original commit 2cf8e89a1 added getSecureKeyAsync to acp-routes.ts without updating the Invariant 2 secure-keys importer allowlist, which left this branch's CI red since open. Allowlist entry added with a comment.

Verification: 20/20 acp-routes tests, 32/32 invariants, lint/prettier/typecheck clean. Full details + the codex-acp follow-up reasoning are in the updated PR description.

@codex review

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. What shall we delve into next?

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@noanflaherty noanflaherty merged commit 701d1b1 into main May 24, 2026
13 checks passed
@noanflaherty noanflaherty deleted the fix/acp-claude-token-injection branch May 24, 2026 14:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant