Watch
1
0
Fork
You've already forked SouveraineOS
0

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:
Fimeg 2026-08-18 10:33:36 -04:00
commit 1c565ce0e2
2 changed files with 105 additions and 0 deletions

View 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).

View file

@ -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). |