Watch
1
0
Fork
You've already forked SouveraineOS
0
SouveraineOS/docs/tasks/78-claude-login-owned-by-souveraine.md
Fimeg 1c565ce0e2 task 78: the claude login belongs to souveraine, not to claude
Scoped from the 2026-08-17/18 revoked-token incident: a day of 401s healed only by claude code rewriting the spare. Souveraine can refresh, rotate, and adopt logins but cannot birth one — so it mint its own PKCE grant, outside the phone/laptop sync, and say so in the Panel when every login is dead. Full Providers subsection is a later expansion.
2026-08-18 10:33:36 -04:00

5.1 KiB

TASK-78 — the claude login belongs to souveraine, not to claude

Status: scoped 2026-08-18, nothing built. Everything below read from source, the live config, or the incident's own logs. Repos: souveraine. Connects to: TASK-67 (provider/cache split), saf/surface/04-agent-pane.md (the Panel), saf/identity/02-agent-principal.md (provider health reporting), the 2026-08-17/18 revoked-token incident.

The incident this exists to end

2026-08-17 21:51 a re-login elsewhere revoked the server's token family. From 22:00 the provider answered every turn with

401 Unauthorized ... "OAuth access token has been revoked."

for a day straight, on both logins, in different ways:

  • primary (~/.claude.anthropic/.credentials.json) — the whole family was revoked by the re-login; the file token that replaced it was alive (probed: /v1/models 200, /v1/messages 429) yet the server kept 401-ing on its in-memory copy, and nothing in the provider refreshes on a 401 — only on expiry;
  • spare (~/.claude.b/.credentials.json) — grant dead since 2026-08-13; rotation correctly refused it (can_authenticate, commit 0398f9f).

The only thing that healed it was Casey's open claude code session rewriting the spare at 10:14. Rotation, adoption, and refresh all worked; nothing in souveraine can birth a login. Refresh-on-401 is not the fix — a revoked family's refresh token dies with it. The fix is that souveraine mints its own grant, so a re-login on the phone (or anywhere) cannot revoke what it holds.

What to build

  1. souveraine login — the same headless PKCE platform-code flow claude code itself uses, verified against third-party implementations of it:

    • authorize URL https://claude.ai/oauth/authorize with code=true, client_id=9d1c250a-e61b-44d9-88ed-5944d1962f5e (PROD_CLIENT_ID is already a constant in claude_subscription.rs), response_type=code, redirect_uri=https://platform.claude.com/oauth/code/callback, the provider's SCOPES, S256 code_challenge, and state;
    • print the URL, wait for the pasted code — the platform page shows a 92-char string, <code>#<fragment>; only the part before # is the authorization code;
    • exchange at TOKEN_URL (https://platform.claude.com/v1/oauth/token, already a constant) as application/x-www-form-urlencoded — JSON returns invalid_grant — with grant_type=authorization_code, code, redirect_uri, client_id, code_verifier, state; codes are single-use and PKCE is mandatory;
    • persist through the existing write_back_credentials CAS machinery. The login subcommand slots into the Commands enum in src/main.rs beside init/server.
  2. A souveraine-owned credential file the sync does not touch. .claude.anthropic and .claude.b sync between the phone and the laptop, so a login on one device revokes the other's — that is the machine behind the incident. Souveraine's grant must live outside the synced set (e.g. ~/.souveraine/claude-oauth.json, 0600) and be added to the provider's credential_files in the live config.

  3. The Panel says it when no login can speak. The Panel is the primary surface; provider health already has a reporting path (saf/identity/02-agent-principal.md, the agent-pane doc). Minimal scope: when every login fails authentication, the server emits the fact and the Panel renders it with the souveraine login command. The full Providers subsection — a Panel popup directing to a settings page with a lot more options built out — is explicitly a later expansion, scoped and built as its own task.

Open questions

  • Does a fresh login elsewhere revoke other active grants for the account? Today's incident says yes for the synced file's family, but that may be file-sync clobbering rather than Anthropic revoking grants. Empirical test after the build: log in on the phone, watch the laptop grant survive.
  • Where the notice renders in the Panel's existing surfaces (settings page, footer chip, or a turn-failure banner).

Acceptance

  1. souveraine login completes end-to-end on the laptop with a real login: URL → paste code → tokens persisted.
  2. The souveraine-owned grant lives outside the synced files, and the provider speaks through it with no claude binary involved at any step.
  3. With every login dead, the server surfaces the souveraine login command in the Panel without a restart.
  4. Rotation still spans the souveraine-owned login and the synced ones.
  5. A login's next turn goes through with no manual intervention.

Notes for whoever picks this up

  • The refresh exchange already exists (refresh_token in claude_subscription.rs, JSON, works); only the code exchange needs the form-encoded path.
  • Reuse TOKEN_URL, PROD_CLIENT_ID, SCOPES, write_back_credentials, TokenState, the accounts list, rotation, and adoption. A login is just another credential file.
  • Probe history from the incident is in the 2026-08-18 session; the token endpoint constants were verified against the running file (claudeAiOauth shape, expiresAt in ms).