Watch
0
0
Fork
You've already forked hyperhive
0

swarm ui: show when an agent's claude login was stored, and delete it

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.
This commit is contained in:
iris 2026-11-09 21:39:17 +01:00 • committed by mara
commit 7dd147adb4
3 changed files with 469 additions and 32 deletions

View file

@ -39,6 +39,20 @@ 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
@ -55,12 +69,12 @@ version it installed (`claude-credentials-version` under the harness dir):
## Credentials
| Principal | Grant |
| -------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `swarm-claude-login`, in its container | `create`, `update` on `swarm/agents/+/claude-credentials`; `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 |
| 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 and nothing else under an agent. Every
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.
@ -92,12 +106,12 @@ 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`, `invalid`; `outcome`: `started`, `stored`, `cancelled`, `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 |
| 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

View file

@ -177,7 +177,7 @@ login minter's, the forge host's and Grafana's, which reach more than one:
| `swarm-bao-forwarder-oidc` | `forwarderOidcClientCertFile` / `forwarderOidcClientKeyFile` | `swarm/services/<store forwarder client id>/oidc/client` |
| `swarm-bao-ci-runner`, and the runner's journal forwarder in its container | `ciRunnerClientCertFile` / `ciRunnerClientKeyFile` | `swarm/services/ci-runner/registration-token` and `swarm/services/<runner forwarder client id>/oidc/client` |
| `swarm-review-classifier`, in its container | `reviewClassifierClientCertFile` / `reviewClassifierClientKeyFile` | `swarm/services/review-classifier/anthropic-api-key`, `swarm/services/review-classifier/acp-provider` and `swarm/services/<classifier client id>/oidc/client` |
| `swarm-claude-login`, in its container | `claudeLoginClientCertFile` / `claudeLoginClientKeyFile` | creates and updates `swarm/agents/+/claude-credentials`; reads `swarm/services/<minter client id>/oidc/client` |
| `swarm-claude-login`, in its container | `claudeLoginClientCertFile` / `claudeLoginClientKeyFile` | creates and updates `swarm/agents/+/claude-credentials`, reads and deletes its metadata; reads `swarm/services/<minter client id>/oidc/client` |
| `swarm-notify-router`, in its container | `notifyRouterClientCertFile` / `notifyRouterClientKeyFile` | `swarm/services/<router client id>/oidc/client` |
| `swarm-builder`, in its container | `builderClientCertFile` / `builderClientKeyFile` | `swarm/services/builder/forge/token`, `swarm/services/attic/push-token` and `swarm/services/<builder client id>/oidc/client` |
| `swarm-bao-attic-secret` and `swarm-bao-attic-tokens` | `atticClientCertFile` / `atticClientKeyFile` | creates and reads `swarm/services/attic/jwt-secret`, `swarm/services/attic/push-token` and `swarm/services/attic/pull-token` |

View file

@ -70,7 +70,9 @@ that runs the claude login from a dialog: open the URL it shows, then paste
the code back. When the secret store has the login, the dialog closes and a
short toast in the top-right corner says so. The credentials go to the agent through the secret store
([`claude-login.md`](claude-login.md)). The agent's own web UI login still
works too.
works too. The settings section of an opened card has a **claude login** row: when
the swarm stored the agent's login, or that none is, with a **delete login**
action that asks first.
### The agents page