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:
parent
a0a3f9a70c
commit
7dd147adb4
3 changed files with 469 additions and 32 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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` |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue