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.
This commit is contained in:
parent
3bdfba8468
commit
1c565ce0e2
2 changed files with 105 additions and 0 deletions
103
docs/tasks/78-claude-login-owned-by-souveraine.md
Normal file
103
docs/tasks/78-claude-login-owned-by-souveraine.md
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
# 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).
|
||||
|
|
@ -65,6 +65,8 @@ Four that are cheap relative to what they unblock:
|
|||
|
||||
| # | Task | What's left |
|
||||
|---|------|-------------|
|
||||
| 78 | [The claude login belongs to souveraine, not to `claude`](78-claude-login-owned-by-souveraine.md) | Scoped 2026-08-18, nothing built. Headless PKCE `souveraine login` (flow verified), a souveraine-owned credential file outside the phone/laptop sync, and a Panel notice when no login can speak. Full Providers subsection in the Panel is a later expansion. |
|
||||
|---|------|-------------|
|
||||
| 74 | [LibrePods: an admitted accessory](74-librepods-as-an-admitted-accessory.md) | **Scoped 2026-08-13.** AirPods protocol stays in LibrePods; sessiond must admit/audit presentation events and Quickshell projects the card. First gate: capture a redacted case-open fixture — connection is not proof of opening. |
|
||||
| 73 | [One spine, two skins: the self-dashboard](73-self-dashboard.md) | Designed 2026-08-11, nothing built. RedFlag Desktop (Tauri, public) + house Gatehouse (wry + QML glance, lens skeleton), spine is agent-side pacman closure resolution that retires souveraine-updater's `pkexec --noconfirm`. Name and home-repo decisions are Casey's. |
|
||||
| 71 | [QmlMobileScanner as a components app candidate](71-qml-mobile-scanner-candidate.md) | Recon 2026-08-11, verdict owed. GPL-3.0 Qt Quick scanner+writer (zxing-cpp/QZXing/zint, SQLite history). Compatible inside AGPL; camera gate is TASK-46; ffmpeg backend needs a freedreno check; fontawesome-qml likely **not** needed (no FontLoader in the tree — unverified). |
|
||||
|
|
|
|||
Loading…
Reference in a new issue