Skip to content

Claude Code signed in from chat: a card with Anthropic's link and its code for the computer (FIN-353) - #920

Merged
futurepaul merged 8 commits into
masterfrom
claude/claude-sign-in
Nov 1, 2026
Merged

futurepaul merged 8 commits into
masterfrom
claude/claude-sign-in

Conversation

@futurepaul

@futurepaul futurepaul commented Nov 1, 2026 •

Copy link
Copy Markdown
Collaborator

FIN-353, PR 3 of 3 (Paul: "(a) first message, (b) inside the card"). Stacked on #911, which adds the generic answer-for-the-computer card field; GitHub retargets this PR to master once #911 merges.

What

When a Claude agent's Claude Code is signed out, its turn asks its person to sign in on a card:

  • Signed out: before a session's claude process starts, the bridge runs claude auth status. If no one is signed in, it starts claude auth login with no display (so it opens no browser on the computer). The login prints Anthropic's sign-in URL, and the card links it for the person's own browser on any device: "Claude Code needs you to sign in to your Claude account. Open the sign-in in your own browser, then paste the code it shows you here."
  • The code goes back through the card's masked field. That is A card's answer for the agent's computer, never a record (FIN-353, PR 2) #911's answer for the computer: chat page → shell (owner frames only) → the computer's /answer → the bridge. It is written to the login's input and is never a record or a log line.
  • Signed in: the login exits 0 and auth status agrees. The turn says "Signed in to Claude", then does what was asked.
  • Invalid code: when the login says "Invalid code", a new card of the same turn says "That code didn't work. Copy the whole code and try again." After 3 tries the turn ends saying so.
  • Stop, let-go or the card expiring ends the turn, and the login's whole process group is killed.
  • A restart ends the sign-in. The PKCE verifier lives only in that login process. A code posted afterwards is refused (409, "This ended before your answer came: send a message to start again."), and the next message asks anew. This is the "answers after a restart" rule from A Claude agent's turn asks to install Claude Code, and installs it (FIN-353, PR 1) #901, designed in from the start.
  • After an install (both the live card and the late LateCard path), the session drops its hold mark before the sign-in waits. A save's hold is never stuck behind a person reading their email.
  • The stub image's Claude Code (fake-claude) now starts signed out and signs in with a fixed test code, so the chat e2e lane goes install → sign-in → reply through the real shell.

Tests

  • tests/claude.rs:
    • a_turn_asks_its_person_to_sign_in_and_runs_once_signed_in (valid, invalid code asked again, no record holds either code, replay asks nothing);
    • a_sign_in_ends_with_its_turn_and_the_next_message_asks_anew (restart: login killed, old card's code refused with words, next message signs in; invalid: 3 wrong codes end the turn; a Stop ends it; no login left running);
    • an_install_then_a_sign_in_which_no_hold_waits_on (a hold during the sign-in is answered at once; signs in after; replay asks neither).
    • a_stop_anywhere_in_a_sign_in_ends_it_and_no_session_begins (Alex): a Stop before the URL, during a code's exchange, and during the final status check each end the turn stopped, leave no login, and Claude Code first hears the next message;
    • unit a_sign_in_url_is_taken_only_once_said_whole (Alex): the URL is taken only once said whole, at every split point of the real login's words.
  • e2e chat lane claude_install: now in the shell. Install card → Install → sign-in card (link, masked field) → wrong code asked again → right code → "Signed in to Claude" + the reply; no record holds either code.
  • Counterfactuals:
    • skipping the signed-in check fails both sign-in tests;
    • leaving the login running on a Stop fails the restart/invalid test (the fake's login now outlives a closed input, so only a kill ends it);
    • keeping the hold mark through the sign-in fails the hold test.
  • cargo test --workspace and clippy -D warnings in images/; cargo xtask check on the merged head.
  • Chat e2e lane locally: 214 passed, 0 failed. The first run failed once at framing_shell (the fresh chat's frame never got its hello). That is the known local-only ERR_NETWORK_CHANGED module stall: this box still has disable_ipv6=0, which CI sets since ci: e2e shards give new links no IPv6, so Chrome sees no network change (the module stalls) #796. Its later imports never reached the node while the computer churned for the new agent.
  • tests/docker.rs real-Claude test, run by name (FRAGMENT_DOCKER_REAL_CLAUDE=1, no model call, no real code): passed locally against Docker lane: a Claude agent's first turn asks to install (after #901) #913's version. Anthropic's installer runs (46 s). rowan's turn shows the sign-in card linking the real claude auth login URL (claude.com/cai/oauth/authorize, PKCE S256). The real login calls a fake code invalid, and the card asks again. A Stop ends the turn and leaves no login running. The same card appears after a restart and after an image update.

Complexity ledger

  • Added:
    • sign_in, ensure_signed_in, uncut (one cancel guard for every wait) and sign_in_url (~230 lines) in runtime/claude.rs, three constants and the card's words;
    • fake-claude's auth status and auth login (~45 lines);
    • three bridge tests, the extended e2e step, docs.
  • Reused, not added: A card's answer for the agent's computer, never a record (FIN-353, PR 2) #911's computer-answer path, today's cards, the install's process-group guard. There is no new cell route, record kind or wire field.
  • Cut:
    • as_person now builds the session's environment for both spawn and the login (−9 lines in spawn);
    • docs no longer say an all-Claude computer must sign in from another agent's desktop.
  • Cost: one claude auth status process per session process start: about 70–100 ms (Claude Code 2.1.314, measured locally).

Screenshots of the real cards go on FIN-353 before auto-merge.

🤖 Generated with Claude Code

futurepaul and others added 2 commits October 31, 2026 21:44
… code for the computer (FIN-353)

Signed out (`claude auth status`, before a session's process starts), a
Claude agent's turn asks its person on a card: `claude auth login`, run
with no display, says Anthropic's sign-in URL, which the card links for
their own browser; the code that page shows goes back on the card as an
answer for the computer (#911), never a record, and into the login's
input. A code it calls invalid is asked again (at most 3); a Stop, a
let-go or the card's expiry ends the turn and its login; a restart ends
it too (its verifier lived only in that process), and the next message
asks anew. A sign-in after an install is never waited on by a hold.

The stub image's Claude Code starts signed out, so the chat e2e lane
signs in from the card (screenshots for FIN-353).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@alexlwn123 alexlwn123 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Fail for now at 8f9b887059f5b4aba35215508f6f4faaa2550559, reviewed against #911 at 43cca3b6c1972f06bd75356fca82a7b60898a6af.

This layer adds Claude sign-in through the existing computer-answer card. That reuse keeps the scope appropriate, but two subprocess-boundary failures need fixing.

Blocking:

  1. Wait for a complete sign-in URL before publishing the card (images/bridge/src/runtime/claude.rs:734–740). The reader emits arbitrary 1024-byte chunks, but split_whitespace() treats the current buffer's trailing fragment as a complete URL. When a read ends inside the URL, the card permanently gets a truncated OAuth link. I reproduced this against sign_in with a disposable child writing https://claude.com/cai/oauth/authorize?client_id= then, 100 ms later, test&state=complete : the card contained only the first part. Pipe read boundaries are not message boundaries. Buffer each stream until the URL token is terminated (or that stream reaches EOF); test splits inside the scheme, path and query, including the read-buffer boundary.

  2. Honor cancellation throughout sign-in, including the code exchange (images/bridge/src/runtime/claude.rs:772–798). After a code is submitted, this select never receives ToSession::Stop or Forget. A slow exchange therefore keeps running after Stop; successful completion returns Done and the caller invokes begin before processing the queued cancellation, allowing the original request to start after cancellation. I reproduced this with a child that exits successfully 300 ms after receiving the code and a Stop sent at 50 ms: sign_in returned success instead of Outcome::Stopped. The initial auth-status check, URL wait and final auth-status check have the same cancellation gap. Keep Stop/Forget/channel closure observable across those phases and kill the login group before returning; cover cancellation before the URL, during exchange, and during final status verification, asserting that no session begins afterward.

Validation: both temporary regression tests failed on this head; removed them afterward. The existing Claude integration suite had 15 passes and 3 failures caused by its Linux /proc process-count assertions on this macOS host, not attributed to this PR. GitHub check and images check passed; real-Hermes Docker was still running. Real-Claude Docker and browser e2e were not run locally. The required external engineering-style file was unavailable at its documented path.

…sked again, a Stop ends its login

After the install, rowan's turn asks to sign in on a card linking
Anthropic's sign-in as the real `claude auth login` says it; a code that
is not one is called invalid and asked again; a Stop ends the turn and
leaves no login running. The same card after a restart and after an
image update. By name only (FRAGMENT_DOCKER_REAL_CLAUDE=1): no model is
called and no real code is used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@alexlwn123 alexlwn123 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Fail for now at 7fce8a7968eb4a26d755c442bc2effa33b65da64, against #911 at 43cca3b6c1972f06bd75356fca82a7b60898a6af.

Both blockers from my previous review remain. The runtime implementation and Claude integration tests are unchanged from that reviewed head; the added real-Claude Docker coverage exercises Stop while awaiting a card answer, not cancellation during the exchange.

Blocking:

  1. Partial OAuth URLs still become permanent card links (images/bridge/src/runtime/claude.rs:734–740). The readers deliver arbitrary 1024-byte chunks, and split_whitespace() accepts an unterminated trailing URL fragment. A pipe read ending inside the URL publishes a truncated link before the remainder arrives. Preserve stream boundaries and wait for a terminated URL token (or EOF). Cover splits inside the scheme, path and query, including the buffer boundary.

  2. Cancellation still cannot interrupt the code exchange (images/bridge/src/runtime/claude.rs:772–798). After submitting the code, the select observes process exit, output and timeout, but never Stop/Forget/channel closure. A Stop during a slow successful exchange remains queued while sign_in returns Done and its caller invokes begin, starting the original request after cancellation. The initial status check, URL wait and final status verification likewise do not observe cancellation. Make cancellation observable throughout these phases, terminate the login group, and test that cancellation before the URL, during exchange and during final verification prevents session start.

The existing computer-answer path remains an appropriate, contained implementation choice; no additional abstraction is needed to fix these boundaries.

Validation: inspected the current head and the changes since my previous verdict, and traced both unchanged failure paths and their callers. The earlier review records reproductions; I did not rerun those tests in this pass. Current GitHub check, images check and real-Hermes Docker are pending. Local Docker/browser suites were not run; the external engineering-style file is unavailable at its documented path.

… waits (Alex on #920)

1. The login's output is kept per stream, and its URL taken only once
   whitespace (or the stream's end) follows it: a pipe's reads are not
   its lines. Tested at every split point of the real login's words;
   fake-claude now says its URL in two writes, apart.
2. Every wait of a sign-in (the first `auth status`, the URL, a code's
   exchange, the final `auth status`) runs under `uncut`: a Stop or a
   let-go already waiting wins, the login's process group is killed, and
   no session begins. Tested with fake-claude slowed in each phase: the
   turn ends stopped, no login is left, and Claude Code first hears the
   next message.

The Stop test now waits for the tool's call before its Stop: a Stop in
the sign-in check is honoured before Claude Code starts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@alexlwn123 alexlwn123 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Pass for this stack layer at dfeb5f5c2717df8f3b170c308b92786356d8779c, against #911 at 43cca3b6c1972f06bd75356fca82a7b60898a6af.

Both earlier blockers are fixed: each output stream now buffers the OAuth URL until whitespace or EOF, and uncut observes Stop/Forget/channel closure during the initial status check, URL wait, code exchange and final status check. Cancellation returns without starting the requested session and drops the active login's process-group guard.

The fixes stay within the image runtime and reuse the existing computer-answer path. The small shared cancellation helper is appropriate for these waits; I found no new blocking regression in the changes since my previous verdict.

Validation: the URL test covering every split point passed locally. The sign-in integration test reached its card but failed at its Linux /proc process-count assertion on this macOS host (claude.rs:448); this does not establish a runtime failure. The new cancellation integration test also relies on /proc, so I inspected it rather than running it here. GitHub check, images check and real-Hermes Docker are still pending. Docker/browser e2e were not run locally, and the external engineering-style file is unavailable at its documented path.

@futurepaul

Copy link
Copy Markdown
Collaborator Author

Thanks, Alex. Both points were real. Fixed at dfeb5f5.

  1. The URL only once said whole.

    • The login's output is kept per stream (Said, each stream's first 64 KiB, with whether it ended). Its URL is taken only once whitespace follows https://…, or once that stream ends (sign_in_url). A buffer that ends inside the URL yields nothing yet.
    • a_sign_in_url_is_taken_only_once_said_whole splits the real login's words (including the … and the no-newline prompt) into two reads at every byte: inside the scheme, the path, the query and the trailing newline. The first read alone never yields anything but the whole URL; both reads always yield it. It also covers a stream that ended mid-URL, one that said none, and the per-stream cap.
    • fake-claude now always writes its URL in two writes 50 ms apart, so every sign-in test (and the e2e lane) goes through a split live.
    • Counterfactual: taking the trailing fragment as before fails at split 73 (Some("https://")).
  2. Cancellation everywhere.

    • Every wait of a sign-in now runs under uncut: the first auth status, the URL, the code's exchange, and the final auth status (inside the exchange). It's a biased select over the session's channel, so a Stop already waiting wins over work that finished.
    • A Stop ends the turn stopped. A Forget, or the channel closing, lets it go. The work is dropped with the login's process group (killed by Group, plus kill_on_drop), and ensure_signed_in returns false, so begin never runs.
    • a_stop_anywhere_in_a_sign_in_ends_it_and_no_session_begins slows fake-claude in each phase in turn (before its URL, in the exchange, in the signed-in status check). Each Stop is sent once the slow phase has started; for the exchange, that's once the login has read the code (a marker), since the code reaches it only after the card's closure posts. Each time the turn ends stopped, no claude auth login is left (checked in /proc), and Claude Code's first heard message is the next one ("Again"), never the stopped turn's.
    • Counterfactuals: with uncut doing nothing, the test fails (a card is posted for the stopped turn). With only the exchange's uncut removed, it fails in the exchange case (the exchange finishes after the Stop and signs in).
    • One knock-on: stop_interrupts_the_running_tool (replay) used to send its Stop at turn.start, which now lands in the sign-in check and stops the turn before Claude Code starts. That is correct, but it isn't what the recording plays, so the test now waits for the tool's turn.call, as its recording mode already waits for the tool.

Also since your review: after an install, the session drops its hold mark before the sign-in waits, so a save is never held by a card (an_install_then_a_sign_in_which_no_hold_waits_on, with a counterfactual). The real-Claude Docker test (by name, no model, no real code) passed locally on #913's version. It shows the real login's URL on the card, the real login calling a fake code invalid, and a Stop leaving no login running, after the install, a restart and an image update.

Validation: cargo test --workspace and clippy -D warnings in images/ (the Claude suite 5× in a row), cargo xtask check, and the chat e2e lane (214/0).

On macOS: the three /proc counts (running, logging_in) are Linux-only, as before.

@futurepaul
futurepaul requested a review from alexlwn123 November 1, 2026 03:21

@alexlwn123 alexlwn123 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Pass for this stack layer at b225a9eadca42f0e0b6851dcf67a9b5e06d12c43, against #911 at 104558d850f43b590dd18ad23c6e654848b2e841.

Both earlier blockers remain fixed: OAuth URLs wait for a complete token per output stream, and cancellation is observed through the status checks, URL wait and code exchange. This head merges the updated base; the Claude runtime, sign-in tests and chat e2e code are unchanged from my approved head dfeb5f5c2717df8f3b170c308b92786356d8779c. I checked the merged context and found no new blocking regression.

The layer remains appropriately confined to the image runtime and reuses the existing computer-answer card path; the merge adds no sign-in machinery.

Validation: the URL test covering every split point passed locally on this head. GitHub check, images check and real-Hermes Docker are still pending. Linux /proc-dependent integration tests and Docker/browser e2e were not rerun on this Mac. The external engineering-style file remains unavailable at its documented path.

@alexlwn123 alexlwn123 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Pass for this stack layer at 7fef814bba04d9201b064a9978434bb0784dd819, against #911 at 8c6d32ec00d3d0df002d54f13b9262221ec63f67.

Both earlier blockers remain fixed: OAuth URLs wait for a complete token per stream, and cancellation is observed through the status checks, URL wait and code exchange. The Claude runtime, sign-in integration tests and fake Claude are unchanged from my approved head 95084b0d84670d1c8a68dcef5d2c79e8bec75cc1. I checked the merged card path, including the base's script command rename to code-card, and found no new blocking regression.

The layer continues to reuse the generic computer-answer path and confines vendor-specific behavior to the image; this merge adds no sign-in machinery.

Validation: the complete-URL unit test and a_cards_answer_for_the_computer_reaches_its_runtime_alone integration test both passed locally on this head. GitHub check, images check and real-Hermes Docker remain pending. Linux /proc-dependent sign-in integration tests and Docker/browser e2e were not rerun on this Mac. The external engineering-style file is unavailable at its documented path.

@futurepaul
futurepaul changed the base branch from claude/card-to-computer to master November 1, 2026 09:24
Merged via the queue into master with commit b2cacdc Nov 1, 2026
13 checks passed
@futurepaul
futurepaul added this pull request to the merge queue Nov 1, 2026
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.

2 participants