Skip to content

feat(runtime): pod-side Claude login relay — mint the subscription credential on the engine pod (PRODUCT-1136) - #1272

Open
cravenceiling wants to merge 2 commits into
mainfrom
product-1136-pod-side-login-relay
Open

feat(runtime): pod-side Claude login relay — mint the subscription credential on the engine pod (PRODUCT-1136)#1272
cravenceiling wants to merge 2 commits into
mainfrom
product-1136-pod-side-login-relay

Conversation

@cravenceiling

Copy link
Copy Markdown
Collaborator

Fixes PRODUCT-1136.

Why

Connecting a Claude subscription previously required minting the OAuth credential on the user's machine: the desktop spawns a bundled claude helper (which SIGILLs outright on pre-AVX2 CPUs — Sentry HOUSTON-APP-543, 103 events) and then pushes the credential to the pod, a handoff that fails in transit for real users (push_claude_oauth_credential family: ~101 events / ~50 users in 4 days, with HOUSTON-APP-564's 19 events landing users in a paste flow whose instructions require installing the Claude CLI). The product requirement is: users install nothing.

What

The runtime now mints the credential on the engine pod by driving the pod's own claude auth login --claudeai — the same binary every turn already resolves, and the same piped-readline child contract the desktop runner has proven in production.

  • backends/claude/login-cli.ts — subprocess + parsing mechanics: spawn with CLAUDE_CONFIG_DIR pointed at a mkdtemp mint dir (outside the store-sync tree — the sync excludes are exact-key, so a mint under HOUSTON_HOME would leak to the object store on older hosts), visit:-marker URL extraction with OSC-8 stripping, envelope read of <mintDir>/.credentials.json.
  • auth/anthropic-cli-login.ts — the flow driver: the authorize URL rides the existing auth_code LoginInfo (zero protocol change, zero cloud-repo change), the pasted approval code answers the CLI's Paste code here if prompted > readline, and the minted credential is sunk exactly where the sanctioned flows expect it: a full entry in the acting scope's auth.json (so the client's existing connect-once capture → central store put → refresh scrub makes the gateway the family's single rotator, trap Landing page refresh: hero animation, Mission Control, nav polish #4), plus the team-scope materialized file exactly like a desktop push (access-only in serve mode; personal scope never touches the pod-shared file, HOU-976). The mint dir is scrubbed win or lose.
  • auth/login.ts — the anthropic seam now runs the relay and degrades to the setup-token paste flow only when the relay is unavailable on this pod: binary absent (the cloud per-turn image strips it), macOS (Keychain mint, unreadable), HOUSTON_CLAUDE_POD_LOGIN=0 (kill switch), or a child that dies/goes silent before printing its URL — which also deterministically catches the feared Ink-TUI piped-stdio deadlock. Every unavailability is pre-URL by construction, so the dialog never switches under the user; post-URL failures (declined approval, bad code) surface as real sign-in errors. Cancellation is never misclassified as fallback.
  • Dialog copy (provider-login-dialog.tsx + en/es/pt locales) — the auth_code header goes flow-neutral and the link is labeled by destination host: it is the primary action for the relay, still the docs reference for the fallback.

No client routing changes needed: web "Connect Claude" and the PRODUCT-1135 desktop degrade path already call providerLogin → runtime startLogin, and pollProviderConnect already captures + scrubs on completion.

Net UX on any hardware

Click Connect → approve in the browser → paste the one code claude.ai shows. Nothing installed.

Tests

  • 21 new tests: login-cli.test.ts (URL/OSC-8/envelope parsing) and anthropic-cli-login.test.ts (fake-child driver: happy path, ENOENT/silence/pre-URL-exit fallbacks, post-URL decline/signal as real errors, abort never falls back, mint scrub on every path, sink serve/self-host/personal-scope matrix).
  • login.test.ts pins its anthropic tests to the setup-token path via the kill switch so no machine's real claude is spawned.
  • Full runs green: runtime 1044 tests, app 2767 tests, tsgo typecheck both packages, biome clean on touched files.

Before / after

Reproduce the gap before this change (as a cloud/web user, or any desktop where the helper can't run):

  1. Open Houston web against a managed pod → Settings → AI Models → Connect Claude.
  2. The dialog instructs: "Run claude setup-token in your terminal…" — i.e. install something. That is the entire fallback surface for pre-AVX2 desktops (HOUSTON-APP-543) and for credential-push failures (HOUSTON-APP-564).

Verify after deploy (engine image containing this change):

  1. Same click path → the dialog now shows "Open the link below and approve the sign-in in your browser…" with an Open claude.com link and a paste box.
  2. Approve in the browser; claude.ai shows a code; paste it. The card flips to Connected without any local CLI.
  3. On the pod: <HOUSTON_HOME>/claude-login/.credentials.json exists (refresh-stripped under serve mode), auth.json's anthropic refresh token is scrubbed after capture, and no claude-login-mint-* dir remains in /tmp.
  4. Degrade check: set HOUSTON_CLAUDE_POD_LOGIN=0 on the runtime (or use an image without the SDK binary) → the same click lands on the setup-token dialog, previous behavior.

…dential on the engine pod (PRODUCT-1136)

Connect Claude with zero local install: the runtime spawns the pod's own
`claude auth login --claudeai` (the desktop runner's production-proven piped
readline contract), relays the authorize URL over the existing auth_code
LoginInfo, feeds the pasted approval code back to the CLI's stdin, and sinks
the minted credential where the sanctioned flows expect it — auth.json for the
connect-once capture (gateway stays the family's single rotator) plus the
team-scope materialized file (access-only in serve mode). The mint happens in
a throwaway temp dir outside the store-sync tree, scrubbed win or lose.

Where the CLI cannot run (binary stripped from the cloud per-turn image,
macOS Keychain mint, HOUSTON_CLAUDE_POD_LOGIN=0 kill switch, or a child that
dies/goes silent before printing its URL — the probed Ink-TUI deadlock shape)
the flow degrades to the setup-token paste dialog, which stays the terminal
fallback. Post-URL failures surface as real sign-in errors; the dialog never
switches under the user.

The auth_code dialog copy goes flow-neutral and the link is labeled by its
destination host — it is the primary action for the relay, still the docs
reference for the fallback.
…-1136)

- A clean CLI exit that never printed an authorize URL is now
  fallback-eligible instead of a false success: with a fresh mint dir there
  is no cached session, so exit 0 without a URL cannot be a mint — resolving
  stored nothing while startLogin's info race timed out on a dialog that
  never opened.
- The pasted-code stdin write is guarded against a synchronous throw racing
  the child's death, so the floating then-chain can never carry an unhandled
  rejection.
- A failed mint-dir removal now logs the path loudly (never contents): a
  self-host /tmp outlives the process, and a lingering mint holds a refresh
  token.
@cravenceiling

Copy link
Copy Markdown
Collaborator Author

Follow-up commit from an independent review pass: pre-URL clean exits are now fallback-eligible instead of a false success, the stdin paste write can't float an unhandled rejection, and a failed mint-dir removal logs loudly. Two new tests pin the first two.

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