Watch
0
0
Fork
You've already forked hyperhive
0

docs: the claude login page keeps the operator steps and moves the minter detail out

The queue subjects, scratch directory and stdio steps of a login, the harness's
newer-version rule, the minter's grants, the caveat and the metrics move to
4-implementation/swarm/claude-login.md. The handbook page keeps how to log an agent
in, the stored login and who may do it. Design pointers in the nix module, the
crate README and AGENTS.md name the new page.
This commit is contained in:
iris 2026-11-11 02:49:46 +01:00 • committed by mara
commit aa1ae2729f
6 changed files with 142 additions and 116 deletions

View file

@ -171,7 +171,7 @@ Swarm-level:
[`docs/4-implementation/swarm/ci-metrics.md`](docs/4-implementation/swarm/ci-metrics.md).
- **`swarm-claude-login/`** — runs `claude auth login` for one agent and
stores the credentials at `swarm/agents/<agent>/claude-credentials`.
Design: [`docs/2-handbook/swarm/claude-login.md`](docs/2-handbook/swarm/claude-login.md).
Design: [`docs/4-implementation/swarm/claude-login.md`](docs/4-implementation/swarm/claude-login.md).
- **`swarm-notify-router/`** — forge events from swarm-controller's
webhooks to todos on the agents they concern, through each agent's own
consumer on the `ROUTER_TODOS` stream. Design:

View file

@ -7,123 +7,42 @@ 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.
## Logging an agent in
## What happens on a login
1. The agent's card shows **login needed** with a **log in** button when its
claude has no session.
2. **log in** opens a dialog with a link. Open it, sign in to the agent's own
claude account, and paste the code the page shows into the dialog.
3. When the secret store has the login, the dialog closes and the agent picks the
login up within a few minutes, then resumes its turns.
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 stops. Starting another login
for the same agent replaces the running one. A refused code fails the login, and
the dialog shows what claude printed.
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 agent's own web UI login (`/login`) still works, unchanged. It goes away once
you have confirmed the swarm login works.
## 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.
The agent's card shows when the swarm stored its login for it, or that none is.
**delete login** on the card removes the stored login, every version of it, after
asking first. 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.
**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)).
An agent installs a stored login only when it's newer than the one it installed
last, so a login that has since gone bad is never put back. The rule and its
reasons are in
[the reference](../../4-implementation/swarm/claude-login.md#why-the-harness-takes-only-a-newer-login).
## Why the harness takes only a newer login
## Who may log an agent in
`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.
The `operatorsOnly` authelia rule on the swarm domain keeps the login 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. Every
hive's policy reads `swarm/agents/*`, so every hive can read every agent's claude
session, as it can read the GitHub token.
- 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.
The minter's grants, the steps of a login and its metrics are in
[the reference](../../4-implementation/swarm/claude-login.md).

View file

@ -0,0 +1,106 @@
# 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.
## 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.

View file

@ -61,9 +61,10 @@ The services that exist once per swarm. Start at
twice after downtime?** → [`2-handbook/swarm/schedules.md`](2-handbook/swarm/schedules.md).
- **How to create an agent from a ready-made set of permissions and
settings?** → [`2-handbook/swarm/archetypes.md`](2-handbook/swarm/archetypes.md).
- **How to log an agent in to claude from the swarm UI, and why doesn't a
stale stored copy log it out again?** →
[`2-handbook/swarm/claude-login.md`](2-handbook/swarm/claude-login.md).
- **How to log an agent in to claude from the swarm UI?** →
[`2-handbook/swarm/claude-login.md`](2-handbook/swarm/claude-login.md). Why a
stale stored copy does not log the agent out again, and the minter's grants:
[`4-implementation/swarm/claude-login.md`](4-implementation/swarm/claude-login.md).
- **Where do the hives back up each agent's state, how long does it stay, and how to
check it?** → [`2-handbook/swarm/archive.md`](2-handbook/swarm/archive.md).

View file

@ -4,7 +4,7 @@
# an agent in from its card in the swarm UI: swarm-controller relays the start
# and the pasted code over the queue, and the daemon runs `claude auth login`
# and writes the credentials to that agent's store leaf.
# docs/2-handbook/swarm/claude-login.md has the design.
# docs/4-implementation/swarm/claude-login.md has the design.
#
# Its own network namespace on the bridge, as ./swarm-review-classifier.nix
# has: it reaches the swarm services by name and nothing on the host's

View file

@ -2,7 +2,7 @@
The claude login minter, in the `swarm-claude-login` container on the host
that sets `deploy.claude-login.enable`. The design is
[`docs/2-handbook/swarm/claude-login.md`](../docs/2-handbook/swarm/claude-login.md).
[`docs/4-implementation/swarm/claude-login.md`](../docs/4-implementation/swarm/claude-login.md).
swarm-controller relays an operator's login of one agent as two requests on
`$SWARM.claude-login.request`: a start, answered with the URL