The agent's settings get a claude login row: when the swarm stored the login, read by the minter from the leaf's metadata, or that none is, with a delete that removes every version after a confirmation. New minter steps (status, delete), controller routes (GET claude-login, DELETE claude-login/stored) and a read and delete grant on the leaf's metadata, pinned in the module-eval check.
118 lines
7 KiB
Markdown
118 lines
7 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, while it waits for a login, reads that leaf every 30
|
|
seconds under the agent's own certificate. When the stored version is newer
|
|
than the one it installed last, it writes the file to
|
|
`~/.claude/.credentials.json`, records the version, and 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. It doesn't log out an agent that already holds a session; that agent
|
|
keeps running on what's in its own `~/.claude`.
|
|
|
|
## Why the harness takes only a newer version
|
|
|
|
`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 KV version with the last
|
|
version it installed (`claude-credentials-version` under the harness dir):
|
|
|
|
- it installs a newer version, that is, a new login;
|
|
- it leaves an equal or older version alone, even when the agent needs a
|
|
login again, so it never puts back a session that has gone bad;
|
|
- it reads nothing while the agent has a session.
|
|
|
|
## 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.
|