swarm UI: log an agent into claude from its card; the credentials go to bao and the agent picks them up
A new swarm service, `swarm-claude-login`, in its own container on the host
that sets `deploy.claude-login.enable` (defaulted on by `allSwarmServices`
where the host has the minter's store leaf, like the review classifier).
- swarm-ui: an agent card showing "login needed" gets a "log in" button.
Its dialog starts the login, shows the URL claude printed, and takes the
pasted code.
- swarm-controller: `POST/DELETE /api/agents/{name}/claude-login` and
`POST /api/agents/{name}/claude-login/code`, behind the swarm UI's
authelia-gated `/api/`, relay the steps to the minter as requests on
`$SWARM.claude-login.request`.
- swarm-claude-login: runs `claude auth login` over piped stdio with HOME
and CLAUDE_CONFIG_DIR in a per-login scratch dir, scrapes the first
https URL, writes the code to stdin, and stores the `.credentials.json`
at `swarm/agents/<agent>/claude-credentials` with a KV v2 write, a new
version over any earlier one. The scratch dir goes on every exit path; a
login waits 15 minutes for its code.
- hive-agent: while parked in `wait_for_login`, reads that leaf every 30 s
under the agent's own cert and installs it only when its KV version is
newer than the one recorded in `<harness>/claude-credentials-version`.
An equal or older version is never put back, because claude rotates the
refresh token and the stored copy is stale after the first refresh.
- swarm-secret-client: `claude_login` paths and `SecretStore::read_versioned`.
- Credentials: a `claude-login` store leaf minted by glue-bao-tls, a
`swarm-claude-login` policy (create/update on
`swarm/agents/+/claude-credentials`, read on its own queue client
secret), an authelia machine client, and a `--claude-login-client`
grant in the queue's auth-callout responder. The unit runs as a static
system user.
The hive's own web-UI login is unchanged.
Refs #5268
This commit is contained in:
parent
248df20164
commit
d5de1e4943
48 changed files with 2680 additions and 42 deletions
93
swarm-queue-client/src/claude_login.rs
Normal file
93
swarm-queue-client/src/claude_login.rs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
//! The claude login minter's requests: the subject swarm-controller sends an
|
||||
//! operator's login steps on, and what travels each way.
|
||||
//!
|
||||
//! Three crates name these: the controller sends the requests, the minter
|
||||
//! answers them, and the auth-callout responder grants the one subject to
|
||||
//! each.
|
||||
//!
|
||||
//! A login is two requests for one agent:
|
||||
//! [`Request::Start`](crate::claude_login::Request::Start) runs
|
||||
//! `claude auth login` and answers with the URL it printed, and
|
||||
//! [`Request::Code`](crate::claude_login::Request::Code) hands it the code the
|
||||
//! operator pasted and answers once the credentials are in the agent's store
|
||||
//! leaf. The running login lives in the minter's memory between the two, so
|
||||
//! the swarm runs one minter.
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// The subject every request goes to. The controller is its one publisher
|
||||
/// and the minter its one subscriber; the minter answers on the request's
|
||||
/// reply subject.
|
||||
pub const SUBJECT: &str = "$SWARM.claude-login.request";
|
||||
|
||||
/// The longest the minter waits for `claude` to print the login URL, and
|
||||
/// then for it to finish once the code is in.
|
||||
pub const STEP_DEADLINE: Duration = Duration::from_mins(1);
|
||||
|
||||
/// How long the controller waits for a reply, and the queue lets the minter
|
||||
/// answer: a step at its deadline, plus the store write.
|
||||
pub const REPLY_DEADLINE: Duration = Duration::from_secs(90);
|
||||
|
||||
/// How long a started login waits for its code before the minter stops it
|
||||
/// and deletes its scratch directory.
|
||||
pub const SESSION_DEADLINE: Duration = Duration::from_mins(15);
|
||||
|
||||
/// One step of an operator's login of one agent.
|
||||
///
|
||||
/// No `Debug` derive: [`Request::Code`] carries the one-time code.
|
||||
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(tag = "step", rename_all = "kebab-case")]
|
||||
pub enum Request {
|
||||
/// Start a login for `agent`, replacing one already running for it.
|
||||
Start {
|
||||
agent: String,
|
||||
/// The swarm UI user, as the gateway named them.
|
||||
operator: String,
|
||||
},
|
||||
/// Hand the running login for `agent` the code the operator pasted.
|
||||
Code { agent: String, code: String },
|
||||
/// Stop the running login for `agent`, if any.
|
||||
Cancel { agent: String },
|
||||
}
|
||||
|
||||
/// The minter's answer to one [`Request`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(tag = "state", rename_all = "kebab-case")]
|
||||
pub enum Reply {
|
||||
/// The login is running; the operator opens `url` and pastes the code it
|
||||
/// shows back.
|
||||
Started { url: String },
|
||||
/// The credentials are stored in the agent's leaf, as its newest
|
||||
/// version.
|
||||
Stored,
|
||||
/// The running login, if there was one, is stopped.
|
||||
Cancelled,
|
||||
/// The step failed: why, with the last lines `claude` printed where it
|
||||
/// printed any.
|
||||
Failed { error: String },
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn the_wire_tags_the_request_by_step_and_the_reply_by_state() {
|
||||
let json = serde_json::to_value(Request::Code {
|
||||
agent: "atlas".to_owned(),
|
||||
code: "c".to_owned(),
|
||||
})
|
||||
.expect("encodes");
|
||||
assert_eq!(json["step"], "code");
|
||||
assert_eq!(json["agent"], "atlas");
|
||||
let json = serde_json::to_value(Reply::Stored).expect("encodes");
|
||||
assert_eq!(json["state"], "stored");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_reply_deadline_outlasts_a_step() {
|
||||
assert!(REPLY_DEADLINE > STEP_DEADLINE);
|
||||
}
|
||||
}
|
||||
|
|
@ -462,6 +462,10 @@ pub mod stored_secret;
|
|||
/// The builder's build subject and the request and reply it carries.
|
||||
pub mod builder;
|
||||
|
||||
/// The claude login minter's subject and the requests and replies it
|
||||
/// carries.
|
||||
pub mod claude_login;
|
||||
|
||||
/// Only the fields this needs; authelia returns several.
|
||||
#[derive(serde::Deserialize)]
|
||||
struct TokenResponse {
|
||||
|
|
|
|||
Loading…
Reference in a new issue