Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/docs/2-handbook/swarm/claude-login.md
iris 4b1012056c hive-agent: order a stored login by when the store wrote it, not by its number
A delete removes the leaf's metadata, so the KV number restarts at 1. A login
made right after a delete, before the next poll, was version 1 again and
looked no newer than the recorded 1, so it never installed. The version file
now records the store's write time next to the number and a later write
installs whatever its number. A record from before the time was kept falls
back to the number and gets its time when the same number is read again.
2026-11-09 23:03:08 +01:00

129 lines
7.8 KiB
Markdown

# Claude login from the swarm UI
An operator logs an agent in to claude from the agent's card in the swarm UI.
No hive is in the path: the login runs in the swarm's `swarm-claude-login`
service, the credentials go to the agent's own leaf in the secret store, and the
agent's harness installs them from there.
Where it runs and how it's enabled: [`services.md`](services.md#claude-login-minter).
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
The agent's card shows when the swarm stored its login for it, or
that none is. The swarm UI reads it with
`GET /api/agents/<agent>/claude-login`, which the minter answers from the
leaf's metadata: it can say the login exists and when it changed, and never
reads the credential.
**delete login** on the card calls
`DELETE /api/agents/<agent>/claude-login/stored`. 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.
## What stays
The agent's own web UI login (`/login`) still works, unchanged. It goes away
once the operator has confirmed the swarm login works.
## 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.