Skip to content

fix: forward Claude guided login verification code#1678

Open
simple-agent-manager[bot] wants to merge 12 commits into
mainfrom
sam/fix-claude-code-guided-gh57hs
Open

fix: forward Claude guided login verification code#1678
simple-agent-manager[bot] wants to merge 12 commits into
mainfrom
sam/fix-claude-code-guided-gh57hs

Conversation

@simple-agent-manager

@simple-agent-manager simple-agent-manager Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Summary

  • forward Claude's browser-displayed short-lived verification code into the still-running sandbox CLI instead of asking for the impossible final OAuth token
  • pipe PTY stdin, constrain the code file to the setup home, inject exact normalized bytes plus carriage return, harden wrapped token parsing, and fail fast on driver rejection/exit
  • add the exchanging state across DO/D1/UI, preserve server-side token capture/encrypted save/teardown, and keep one-active-session uniqueness through an additive index migration
  • PR fix: describe native Codex verification flow #1667 is independent Codex-only helper copy; this PR supersedes none of its touched text

Alarm candidate volume remains one session per DO. Each tick adds one sandbox state-file read before the existing credential-file check; worst case is bounded by one configured capture poll per active session and TTL teardown.

Validation

  • pnpm lint (0 errors; repository baseline warnings)
  • pnpm typecheck
  • affected API/web tests (64+ focused assertions) and full suite; one unrelated Vultr timing test flaked once and passed isolated rerun
  • pnpm build
  • migration safety/ordering, AST, file-size, source-contract, and DO wall-time gates
  • candidate-volume and cost stated above

Staging Verification (REQUIRED for all code changes — merge-blocking)

  • Staging deployment green — run 30193502104 succeeded from c3505f311
  • Live app verified via Playwright
  • Existing workflows confirmed working
  • New feature/fix verified on staging
  • Infrastructure verification completed — N/A: no cloud-init, VM-agent, DNS, TLS, or deploy-script changes
  • Mobile and desktop verification notes added for UI changes

Staging Verification Evidence

Deployment: https://github.com/raphaeltm/simple-agent-manager/actions/runs/30193502104 (success). Authenticated staging Playwright session 01KYEQ1PNJVAADQPMXMMG4FX1V reached waiting_for_user with a trusted Claude URL in 12.5s and no browser token; garbage#rejected-code transitioned to exchanging, then the real CLI rejection surfaced as sanitized code_rejected in 5.0s. Cleanup returned 200 and read-only D1 verification found zero active setup-session rows. Local component/visual audits passed at 375x667 and 1280x800. The successful real-subscription exchange remains Raphaël’s explicit manual gate.

UI Compliance Checklist (Required for UI changes)

  • Mobile-first layout verified
  • Accessibility checks completed
  • Shared UI components used
  • Playwright audit passed at 375x667 and 1280x800 with no horizontal overflow; screenshots in .codex/tmp/playwright-screenshots/

End-to-End Verification (Required for multi-component changes)

  • Data flow traced from user input to final outcome
  • Capability coverage exercises route/DO/sandbox boundaries plus driver exact stdin and server-side capture/save
  • Existing behavior verified against code and the reviewed SAM idea
  • Real successful provider E2E is explicitly left for Raphaël's Claude subscription

Data Flow Trace

AgentCredentialConnectModal.handleSubmitVerificationCodesubmitAgentCredentialSetupVerificationCode → owned POST /:id/verification-codesubmitSetupSessionVerificationCodeCredentialSetupSession.submitVerificationCode → sandbox verification-code.txtrunClaudeSetupToken PTY stdin → CLI OAuth exchange → server-only claude-oauth-token.txtattemptCapturesaveAgentCredentialForUser → teardown.

Untested Gaps

A successful real Claude subscription exchange cannot be automated without Raphaël's account. Staging verifies the complete no-account path, including rejected-code failure and cleanup; Raphaël will perform the final fresh-code success test.

Post-Mortem (Required for bug fix PRs)

What broke

Guided Claude login displayed the authorization URL but could never complete; it hung until TTL. The follow-up branch then asked users for a final token the browser never displays.

Root cause

PR #1671 launched claude setup-token with stdin ignored. Validation stopped at URL display, and the later fix modeled an imagined browser-token flow instead of the documented container flow.

Class of bug

Flow shipped with a completion step that could not physically complete; mocks stopped before the real cross-boundary exchange.

Why it wasn't caught

There was no exact-stdin regression or full route-to-sandbox/state-file sequence, and staging did not exercise token capture.

Process fix included in this PR

Driver exact-byte regression, DO failure-fast tests, route/DO boundary coverage, component behavior, and two-viewport Playwright audit. The durable task records the incident and manual provider gate.

Post-mortem file

tasks/active/2026-07-26-claude-guided-verification-code.md

Specialist Review Evidence (Required for agent-authored PRs)

  • All local reviewers completed and findings addressed before merge
  • If any reviewer did NOT complete: needs-human-review label added and merge deferred to human
Reviewer Status Outcome
task-completion-validator ADDRESSED Main-agent skill audit caught the missing exchanging-state D1 uniqueness index; fixed in 93cfa51
cloudflare-specialist + security-auditor PASS Main-agent skill review covered DO concurrency, ownership, sandbox path constraints, secret non-persistence, teardown, and alarm cost
ui-ux-specialist + test-engineer PASS Main-agent skill review plus component tests and screenshot-backed 375/1280 Playwright audit found no blocking issue
constitution-validator PASS Protocol constants reviewed; existing env-configured session TTL/capture polling retained and no deployment-specific URLs added

Exceptions (If any)

  • Scope: Real successful Claude OAuth exchange
  • Rationale: Requires Raphaël's personal paid Claude subscription login
  • Expiration: Before merge, after Raphaël performs the documented staging test

Agent Preflight (Required)

  • Preflight completed before code changes

Classification

  • external-api-change
  • cross-component-change
  • business-logic-change
  • public-surface-change
  • docs-sync-change
  • security-sensitive-change
  • ui-change
  • infra-change

External References

Reviewed the official documentation for Claude authentication (https://code.claude.com/docs/en/authentication) and anthropics/claude-code issues #47773/#47699 via reviewed SAM idea 01KYEHTF6BA3ZPTN2RBRYBH462.

Codebase Impact Analysis

apps/api/scripts/claude-setup-token.mjs and the sandbox image input contract; apps/api/src/durable-objects/credential-setup-session/ plus its route/service and D1 index; apps/web/src/components/CodexConnectModal.tsx and API client; API unit/vertical-boundary tests and web component/Playwright tests.

Documentation & Specs

No public docs page needs a change because this corrects an existing guided-login flow without adding configuration. The reviewed contract and manual gate are synchronized in tasks/active/2026-07-26-claude-guided-verification-code.md, component copy, and behavioral tests.

Constitution & Risk Check

Checked Principle XI and credential-boundary rules. The browser receives/submits only short-lived authorization material; the OAuth token remains server-side. Paths are fixed protocol filenames under a validated per-session home; session timing remains env-configured.

@simple-agent-manager simple-agent-manager Bot added needs-human-review Agent could not complete all review gates — human must approve before merge and removed needs-human-review Agent could not complete all review gates — human must approve before merge labels Jul 26, 2026
@codspeed-hq

codspeed-hq Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 6 untouched benchmarks


Comparing sam/fix-claude-code-guided-gh57hs (b2ca1eb) with main (44adc7e)

Open in CodSpeed

raphaeltm and others added 2 commits July 26, 2026 08:17
The Claude Code prompt treats one large stdin chunk as a paste and absorbs an
inline trailing carriage return: realistic ~100-char codes were typed into the
field but never submitted, so the guided login sat in 'exchanging' until the
session TTL (reproduced against claude v2.1.220; 21-char test codes submit,
which is why staging verification passed). Write the code, then send Enter as
a separate write after a settle delay.

Also fail visibly instead of stalling: any post-forward 'OAuth error:' screen
is terminal (the previous pattern required a 'status code 4xx' wording that
401/state-mismatch/network failures never print), and a bounded exchange
deadline reports 'exchange_timeout' distinctly through the DO when the CLI
produces no recognizable outcome. Both knobs are env-configurable
(CLAUDE_SETUP_ENTER_DELAY_MS, CLAUDE_SETUP_EXCHANGE_TIMEOUT_MS).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@simple-agent-manager

Copy link
Copy Markdown
Contributor Author

Post-verification fix: real codes never submitted inside the CLI (commit adc7c4f)

Raphaël's real-subscription E2E failed: the modal sat on "Completing sign-in…" until the 15-min TTL (staging session 01KYESZPQZ41TFTBSSB88KHNWG, expired 09:07 UTC).

Root cause (reproduced locally against claude v2.1.220, the same version as the staging sandbox image): the Claude Code prompt treats one large stdin chunk as a paste and absorbs an inline trailing \r — the code is typed into the field (masked asterisks visible in PTY capture) but never submitted. Realistic code#state blobs are ~100+ chars and always hit this; the 21-char garbage#rejected-code used for the original staging verification stays below the paste threshold and submits fine, which is why verification passed while every real login hung.

Changes:

  • Driver writes the code, then sends Enter as a separate write after a settle delay (CLAUDE_SETUP_ENTER_DELAY_MS, default 1000ms) — verified against the real CLI with 21/99/210-char codes.
  • Broadened terminal-failure detection: any post-forward OAuth error: screen is terminal. The old pattern required a "status code 4xx" wording that the CLI's 401 ("Authentication failed: Invalid authorization code"), state-mismatch, and network failures never print — those would still have hung forever.
  • Bounded exchange deadline (CLAUDE_SETUP_EXCHANGE_TIMEOUT_MS, default 120s): any unrecognized outcome now fails visibly as exchange_timeout (mapped distinctly by the DO) instead of stalling until TTL.
  • Tests: rewrote the single-write stdin contract (rule 42), added a discriminating paste-absorption regression (fails on the pre-fix driver — verified: 5 tests red pre-fix), 401-wording rejection, exchange-timeout, and DO timeout-mapping tests. 39/39 pass.

Staging verification (run 30197296704, deployed): session 01KYEYE9KBR79YRWPMZG6HTXYJ reached waiting_for_user in 12s; a 99-char garbage code transitioned to exchanging and produced a visible code_rejected failure in 5s (pre-fix: infinite hang — this exact scenario). No active setup-session rows remain.

Follow-ups noted (not blocking): DO-side watchdog for exchanging if the driver process dies (container sleep/OOM); modal UX — Cancel affordance during exchanging, hide the sign-in button once the code is submitted, immediate poll on visibilitychange for backgrounded mobile tabs; disable the manual credential Save button when the field is empty ("Credential cannot be empty").

Still do not merge — awaiting Raphaël's real-subscription code-paste E2E on staging.

…etail

The driver flattened every post-forward 'OAuth error:' render into 'Claude
rejected the verification code', discarding the CLI line that distinguishes
an incomplete paste (missing #state half — instant local failure), a server
rejection (status-code wording), and a sandbox network failure. Reproduced
each wording live against claude v2.1.220; renders arrive with characters
dropped by Ink redraws, so classification tolerates that mangling.

- driver: extract the last OAuth error line after a settle window
  (CLAUDE_SETUP_REJECTION_SETTLE_MS, default 400ms), redact token-like runs,
  classify into code_incomplete / code_rejected / exchange_network_error,
  and publish a bounded detail field alongside the failure state
- DO: map the new failure classes to accurate guidance and append the
  sanitized detail as '[CLI: ...]' (printable ASCII, capped, sk-ant
  redacted); the driver's free-form error field is still never surfaced
- modal: block a claude-code paste missing its '#' half before burning the
  setup session, with copy-the-entire-code guidance

Verified end-to-end against the real CLI: no-# paste -> code_incomplete
with the CLI's own 'full code was copied' advice; full-format fake code ->
code_rejected with 'status code 400' detail.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…low-up

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@simple-agent-manager

Copy link
Copy Markdown
Contributor Author

Follow-up: real-code failure diagnosed — error detail is no longer discarded (b26de657f, b2ca1ebed)

Raphaël's first real-code attempt after the paste fix (session 01KYGZBWGAF1Y7Q4HREJS22XPB, 2026-07-27 05:04 UTC) failed with "Claude rejected the verification code" despite a correctly copied code.

What the investigation established (live evidence)

  1. The paste/submit fix works — exchanges now reach a terminal outcome in seconds.

  2. Sandbox egress is healthy. A Node-fetch token exchange from inside a staging sandbox received a genuine 400 invalid_grant verdict from platform.claude.com/v1/oauth/token. (curl-shaped probes get 429 rate_limit_error from the same egress — TLS-fingerprint bot scoring; a red herring. The Bun-based CLI is unaffected.)

  3. The real bug: the driver flattened every OAuth error: render into "code rejected". Live against claude v2.1.220 the CLI distinguishes:

    • paste missing the #state half → instant local Invalid code. Please make sure the full code was copied (no network!)
    • full-format bad code/state/PKCE → server Request failed with status code 400
    • network failure → connect ECONNREFUSED …

    The most likely cause of the real-code failure is an incomplete mobile copy (missing # half) or a code issued against a different PKCE challenge — but the driver discarded the only signal that could tell.

The fix

  • Driver: extracts the last OAuth-error line after a settle window (CLAUDE_SETUP_REJECTION_SETTLE_MS, default 400 ms), tolerates Ink overwrite-mangling, redacts sk-ant*, and classifies into code_incomplete / code_rejected / exchange_network_error + bounded detail.
  • DO: maps each class to accurate guidance and appends [CLI: …] (printable-ASCII sanitized, capped 160; the driver's free-form error is still never surfaced).
  • Modal: blocks a claude-code paste with no # before burning the setup session.

Verification

  • 50 unit tests green across driver/DO/modal suites; new tests use the live-captured mangled renders.
  • Real-CLI E2E (patched driver + claude 2.1.220): no-# paste → code_incomplete in 2 s; full-format fake → code_rejected + status code 400 detail.
  • Staging (deploy 30240838919, success incl. smoke tests), through the real product flow:
    • Probe A full-format fake code → code_rejected, message now ends [CLI: Requstfailed withstatus code 400] — proving the sandbox exchange gets genuine server verdicts.
    • Probe B code missing #code_incomplete: "The pasted code was incomplete. Copy the entire code Claude shows — it has a # in the middle — then start again. [CLI: Invalidcode. Please makesure the fullcde wascopied]"
    • Zero active setup sessions / orphan sandboxes after both probes.

PR stays unmerged — the remaining gate is Raphaël's real-subscription retest; whatever happens, the failure message will now carry the true reason.

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