120 lines
7.6 KiB
Markdown
120 lines
7.6 KiB
Markdown
# Claude login: the minter, the harness's version rule and metrics
|
|
|
|
How the `swarm-claude-login` service runs a login, why the agent's harness takes
|
|
only a newer stored login, the minter's grants and its metrics. The operator's
|
|
view is [the claude login page](../../2-handbook/swarm/claude-login.md).
|
|
|
|
The service runs the `claude-code` that its host's
|
|
`services.hyperhive.deploy.hive-controller.claudeCodePackage`
|
|
pins for agents, and its container's own nixpkgs' where the host pins none.
|
|
|
|
## What happens on a login
|
|
|
|
1. The agent publishes `needs_login` in its turn-state header, and its card
|
|
shows **login needed** with a **log in** button.
|
|
2. **log in** opens a dialog, which calls
|
|
`POST /api/agents/<agent>/claude-login`. swarm-controller relays the start as
|
|
a request on the queue subject `$SWARM.claude-login.request`.
|
|
3. The minter runs `claude auth login` with `HOME` and `CLAUDE_CONFIG_DIR` in a
|
|
fresh scratch directory, over plain stdio pipes, and answers with the first
|
|
`https://` URL it prints. The dialog shows that URL.
|
|
4. The operator opens the URL, signs in to the agent's own claude account, and
|
|
pastes the code the page shows into the dialog, which posts it to
|
|
`POST /api/agents/<agent>/claude-login/code`.
|
|
5. The minter writes the code to claude's stdin and waits for it to exit. It
|
|
stores the `.credentials.json` claude wrote at
|
|
`swarm/agents/<agent>/claude-credentials` (field `value`), as a new version
|
|
over any stored before, and removes the scratch directory.
|
|
6. The agent's harness reads that leaf under the agent's own certificate: every
|
|
30 seconds while it waits for a login, every 5 minutes while it has a
|
|
session, and once as a paused agent resumes. When the stored version is
|
|
newer than the one it installed last, it writes the file to
|
|
`~/.claude/.credentials.json`, records the version, and, for an agent that
|
|
waited, resumes its turn loop as it does after a login from the agent's web
|
|
UI.
|
|
|
|
A started login waits 15 minutes for its code, then the minter stops it.
|
|
Starting another login for the same agent replaces the running one. Each step
|
|
has a minute to finish; a refused code fails the login, and the dialog shows
|
|
what claude printed.
|
|
|
|
## The stored login's routes
|
|
|
|
The agent's card shows when the swarm stored its login for it, or that none is.
|
|
Two more routes serve that:
|
|
|
|
- `GET /api/agents/<agent>/claude-login` answers the card's "stored at" line.
|
|
The minter answers from the leaf's metadata: it can say the login exists and
|
|
when it changed, and never reads the credential.
|
|
- `DELETE /api/agents/<agent>/claude-login/stored` is **delete login** on the
|
|
card. The minter removes the leaf and every version of it, not just the newest:
|
|
an older version is still a session. An agent that already holds a session logs
|
|
out too, the next time it reads the store: within 5 minutes, or when a paused
|
|
agent resumes (see [the harness's rule](#why-the-harness-takes-only-a-newer-login)).
|
|
|
|
## Why the harness takes only a newer login
|
|
|
|
`claude` rotates the refresh token in `.credentials.json` as it uses the
|
|
session, and the agent can't write the store. After the first refresh the
|
|
stored copy is stale. A harness that installed it whenever the bytes differed
|
|
would replace the agent's live session with a stale copy and log the agent
|
|
out. The harness therefore compares the store's version with the last one it
|
|
installed (`claude-credentials-version` under the harness dir), by the time
|
|
the store wrote each. A delete restarts the KV number but not the time, so a
|
|
login right after a delete, version 1 again, still counts as newer. A record
|
|
from before the harness kept the time falls back to the number, and gets its
|
|
time the first time it reads the same number.
|
|
|
|
- it installs a newer write, that is, a new login;
|
|
- it leaves an equal or older one alone, even when the agent needs a login
|
|
again, so it never puts back a session that has gone bad;
|
|
- it logs the agent out when the store holds nothing although a version was
|
|
installed: the operator deleted the stored login. A read that failed never
|
|
does it, since an unreachable store says nothing about the login. The
|
|
logout is the one `/api/logout` does, and the recorded version is
|
|
forgotten so the next login installs.
|
|
|
|
## Credentials
|
|
|
|
| Principal | Grant |
|
|
| -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
| `swarm-claude-login`, in its container | `create`, `update` on `swarm/agents/+/claude-credentials`; `read`, `delete` on that leaf's metadata; `read` on `swarm/services/<minter client id>/oidc/client` |
|
|
| the agent, under its own certificate | `read` on its own `swarm/agents/<agent>/*`, which covers the leaf |
|
|
|
|
The minter has no read on the leaf's value and nothing else under an agent. Every
|
|
hive's policy reads `swarm/agents/*`, so every hive can read every agent's
|
|
claude session, as it can read the GitHub token.
|
|
|
|
The `operatorsOnly` authelia rule on the swarm domain, not the controller,
|
|
keeps these routes to operators: the controller checks only that the gateway
|
|
named a user. Without that rule any account authelia admits could store its
|
|
own claude session as any agent's, so no agent account may exist before the
|
|
rule reaches the swarm.
|
|
|
|
The minter refuses a step whose agent name isn't an identifier before it
|
|
makes a directory or runs `claude`.
|
|
|
|
## Caveat
|
|
|
|
The installed-version record lives under the harness dir and the credentials
|
|
file under `~/.claude`. If only the record survives a rebuild, the agent
|
|
ignores the stored version until the operator logs it in again, which writes a
|
|
newer one.
|
|
|
|
## Observability
|
|
|
|
The minter pushes OTLP metrics through its container's forwarder to the swarm
|
|
collector, labelled `service.name=hyperhive-claude-login`. With no
|
|
`OTEL_EXPORTER_OTLP_ENDPOINT` it exports nothing. The client-certificate
|
|
gauges (`bao.client_cert.*`) come with them.
|
|
|
|
| Metric | Unit | Kind | Attributes |
|
|
| ----------------------------------------- | ---- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
| `hyperhive.claude_login.steps` | — | counter | `step`: `start`, `code`, `cancel`, `status`, `delete`, `invalid`; `outcome`: `started`, `stored`, `cancelled`, `login`, `deleted`, `failed` |
|
|
| `hyperhive.claude_login.step.duration` | `s` | histogram | `step` |
|
|
| `hyperhive.claude_login.sessions.expired` | — | counter | — |
|
|
| `hyperhive.claude_login.sessions.active` | — | gauge | logins waiting for a code, as of the last step |
|
|
|
|
`invalid` is a request that didn't decode or named no agent. A `start` spawns
|
|
`claude auth login` and a `code` stores the credentials, so those two are the
|
|
slow steps. `expired` counts logins stopped because their code never came.
|