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

@ -184,6 +184,15 @@ enum AgentVerb {
/// Queues and returns, the same way `agent create` does — watch the
/// swarm UI's job view for the outcome.
MintIdentity(AgentMintIdentityArgs),
/// Check one agent's forge token, and mint it if it's missing or stale.
///
/// swarm-controller does this for every agent with a store identity at
/// start and every five minutes; this is for when waiting isn't an
/// option. It leaves a current token alone.
///
/// Queues and returns, the same way `agent create` does — watch the
/// swarm UI's job view for the outcome.
MintForgeToken(AgentMintForgeTokenArgs),
}
#[derive(Args)]
@ -210,6 +219,19 @@ struct AgentCreateArgs {
controller_socket: Option<PathBuf>,
}
#[derive(Args)]
struct AgentMintForgeTokenArgs {
/// Name of an agent that already exists.
name: String,
/// swarm-controller's unix socket.
///
/// Supplied by the nix module that installs this binary, from the same
/// `socketPath` option the daemon binds; falls back to
/// `SWARM_CONTROLLER_SOCKET`.
#[arg(long, value_name = "PATH")]
controller_socket: Option<PathBuf>,
}
#[derive(Args)]
struct AgentMintIdentityArgs {
/// Name of an agent that already exists.
@ -308,6 +330,13 @@ fn main() -> Result<()> {
let socket = path_from(args.controller_socket, "SWARM_CONTROLLER_SOCKET")?;
agent::mint_identity(&socket, &args.name, &args.hive)
}
// Same socket-resolution reasoning as `Create` above.
Verb::Agent {
command: AgentVerb::MintForgeToken(args),
} => {
let socket = path_from(args.controller_socket, "SWARM_CONTROLLER_SOCKET")?;
agent::mint_forge_token(&socket, &args.name)
}
// Resolved lazily, inside the one arm that actually touches the
// deployment env vars — see the `MarkdownDocs` doc comment above
// for why an unconditional resolve up front would be wrong.
@ -697,6 +726,31 @@ mod tests {
);
}
#[test]
fn the_forge_token_verb_takes_an_agent_and_no_hive() {
let cli = Cli::try_parse_from(["swarmctl", "agent", "mint-forge-token", "scribe"])
.expect("the minimal form parses");
let Verb::Agent {
command: AgentVerb::MintForgeToken(args),
} = cli.command
else {
panic!("expected `agent mint-forge-token`");
};
assert_eq!(args.name, "scribe");
assert!(
Cli::try_parse_from([
"swarmctl",
"agent",
"mint-forge-token",
"scribe",
"--hive",
"a"
])
.is_err(),
"the token has no hive, so the verb must not take one"
);
}
#[test]
fn parses_authelia_hash_output() {
let out = "Random Password: hunter2\nDigest: $argon2id$v=19$m=65536$abc\n";