agents: pull the forge token from bao; drop tea-login

forge-token.nix fetches swarm/agents/<agent>/forge-token under the
agent's own store identity into /run/hive-agent-forge-token/token, and
re-fetches on a timer so a rotation lands. hive-forge, the git
credential helper, hive-forge-notify, forge-avatar-sync and the web UI
read that file first and fall back to <state>/forge-token.

tea-login is deleted: it copied the token into ~/.config/tea, which
docs/swarm/credentials.md forbids for a store secret. hive-forge covers
the same verbs. swarmctl gains agent mint-forge-token.

Refs #3782
This commit is contained in:
atlas 2026-09-24 16:35:40 +02:00 • committed by mara
commit dd32a395f7
16 changed files with 501 additions and 156 deletions

View file

@ -1,5 +1,5 @@
//! `swarmctl agent create` and `swarmctl agent mint-identity` — queue work
//! on the swarm-controller's job graph.
//! `swarmctl agent create`, `swarmctl agent mint-identity` and `swarmctl agent
//! mint-forge-token` — queue work on the swarm-controller's job graph.
//!
//! Each POSTs and returns as soon as the work is *inserted*. Both verbs
//! print the queued node id and stop: creation's last node only *publishes*
@ -64,6 +64,12 @@ struct MintIdentityResponse {
node_id: u64,
}
/// Success body of `POST /api/agents/{name}/forge-token`, which takes no body.
#[derive(Deserialize)]
struct MintForgeTokenResponse {
node_id: u64,
}
/// Run `swarmctl agent create`.
///
/// Synchronous on purpose: every other verb in this crate is, and this is
@ -137,6 +143,33 @@ pub(crate) fn mint_identity(socket: &Path, name: &str, hive: &str) -> Result<()>
Ok(())
}
/// Run `swarmctl agent mint-forge-token`.
///
/// Synchronous for the same reason [`create`] is, and built on the same
/// round trip. No `--hive`: the token's store path has no hive in it.
pub(crate) fn mint_forge_token(socket: &Path, name: &str) -> Result<()> {
let name = parse_ident(name, "agent name")?;
let rt = tokio::runtime::Builder::new_current_thread()
.enable_io()
.build()
.context("starting a tokio runtime for the controller request")?;
let resp: MintForgeTokenResponse = rt.block_on(post(
socket,
&format!("/api/agents/{name}/forge-token"),
&serde_json::json!({}),
"mint-forge-token",
))?;
println!("queued: job node {}", resp.node_id);
println!(
"agent {name:?}'s forge token will be checked once the job graph runs, and minted \
only if it is missing or stale; `swarmctl` does not wait for it. The agent picks a \
new token up on its next fetch"
);
Ok(())
}
/// One `POST /api/agents` round trip over the controller's unix socket.
async fn post_create(socket: &Path, name: &str, hive: &str) -> Result<CreateAgentResponse> {
post(