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:
parent
c0eb6780ec
commit
aa1ae2729f
3 changed files with 142 additions and 116 deletions
|
|
@ -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).
|
||||
|
|
|
|||
106
docs/4-implementation/swarm/claude-login.md
Normal file
106
docs/4-implementation/swarm/claude-login.md
Normal 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.
|
||||
|
|
@ -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).
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue