diff --git a/docs/integrations/forge.md b/docs/integrations/forge.md index 4d1ff43e..d7186857 100644 --- a/docs/integrations/forge.md +++ b/docs/integrations/forge.md @@ -58,12 +58,13 @@ creates what's missing and re-mints a token that's missing or no longer matches the forge. A pass whose store or forge reads fail queues nothing, so an outage never rotates every hive's token. -The account name isn't reserved, so an agent may carry the name `hive-` too. -swarm-controller creates the hive's account with the email -`hive-@hives.hyperhive.local` and refuses an account by that name with -any other email: it never adds an agent's account to the team or mints a -token for it. An agent's own creation refuses the hive's account the same -way. +swarm-controller refuses to create an agent whose name starts with `hive-`: +these accounts own that prefix. An agent that already had such a +name keeps it, so the accounts are also told apart by email. swarm-controller +creates the hive's account with the email `hive-@hives.hyperhive.local` +and refuses an account by that name with any other email: it never adds an +agent's account to the team or mints a token for it. An agent's own forge +account setup refuses the hive's account the same way. hive-c0re reads the token with `forge::reader_token()`, under the hive's own store certificate, every time it reads the forge. Where the store holds none, diff --git a/hive-types/src/lib.rs b/hive-types/src/lib.rs index dc123baf..961e557d 100644 --- a/hive-types/src/lib.rs +++ b/hive-types/src/lib.rs @@ -93,6 +93,24 @@ pub fn forge_username_violation(name: &str) -> Option<&'static str> { } } +/// The prefix no new agent's name may start with. `hive-` is each hive's +/// own forge account (`swarm_secret_client::forge::hive_login`), and every +/// agent's forge account takes the agent's name, so an agent called +/// `hive-` would claim hive ``'s account. +pub const RESERVED_AGENT_PREFIX: &str = "hive-"; + +/// The reserved-prefix rule `name` breaks as a new agent's name, or `None`. +/// +/// Creation sites only, for the reason given at [`is_reserved_name`]. Agent +/// names only: hive names are kept off `hive` by +/// `nix/reserved-hive-fragments.nix`, and the other [`Ident`]s never become a +/// forge user. +#[must_use] +pub fn reserved_agent_prefix_violation(name: &str) -> Option<&'static str> { + name.starts_with(RESERVED_AGENT_PREFIX) + .then_some("must not start with 'hive-': that prefix is reserved for hive forge accounts") +} + /// A validated hive identifier: 1-63 chars of `[a-z0-9-]`. /// /// The single ident type for agent names, forge labels, and matrix / github @@ -187,7 +205,7 @@ impl<'de> serde::Deserialize<'de> for Ident { mod ident_tests { use super::{ FORGE_USERNAME_MAX_LEN, Ident, forge_username_violation, is_reserved_name, - parse_reserved_names, + parse_reserved_names, reserved_agent_prefix_violation, }; #[test] @@ -302,6 +320,20 @@ mod ident_tests { } } + #[test] + fn the_hive_prefix_is_refused_for_an_agent() { + for bad in ["hive-x", "hive-alpha", "hive-"] { + assert!( + reserved_agent_prefix_violation(bad).is_some_and(|r| r.contains("hive forge")), + "{bad:?}" + ); + } + // The accept arm: `hive` elsewhere in the name, or without the dash. + for ok in ["hivemind", "xhive-y", "hive", "my-hive", "hives-x"] { + assert_eq!(reserved_agent_prefix_violation(ok), None, "{ok:?}"); + } + } + #[test] fn round_trips_and_serde_validates() { let id = Ident::parse("damocles").unwrap(); diff --git a/swarm-controller/src/forge/hive_token.rs b/swarm-controller/src/forge/hive_token.rs index 971885b9..d2e0867f 100644 --- a/swarm-controller/src/forge/hive_token.rs +++ b/swarm-controller/src/forge/hive_token.rs @@ -10,9 +10,12 @@ //! token the forge still lists with the same last eight characters and scopes //! is kept, and a pass whose reads fail queues nothing. //! -//! ⚠️ The account name is not reserved: an agent may be called `hive-`. -//! Each side refuses an account carrying the other's email, so neither ever -//! locks down, joins the team with, or mints a token for the other's account. +//! A new agent may not be called `hive-` +//! (`hive_types::reserved_agent_prefix_violation`, checked at agent creation), +//! which is the primary guard against an agent claiming a hive's account. An +//! agent created before that rule can still hold such a name, so each side +//! also refuses an account carrying the other's email: neither ever locks +//! down, joins the team with, or mints a token for the other's account. use std::sync::Arc; diff --git a/swarm-controller/src/main.rs b/swarm-controller/src/main.rs index 0485e43f..17691787 100644 --- a/swarm-controller/src/main.rs +++ b/swarm-controller/src/main.rs @@ -1882,6 +1882,9 @@ fn broken_name_rules( "agent name {agent:?} {rule}: the forge refuses it as a username" )); } + if let Some(rule) = hive_types::reserved_agent_prefix_violation(agent) { + broken.push(format!("agent name {agent:?} {rule}")); + } broken } @@ -3892,6 +3895,32 @@ mod tests { } } + /// The `hive-` prefix belongs to hives' forge accounts, so a new agent + /// may not take it. `hivemind` and `xhive-y` are the control. + #[test] + fn a_new_agent_with_the_hive_prefix_is_refused() { + let raw = nix_reserved(); + let mut warnings = Vec::new(); + let broken = super::broken_name_rules("hive-x", Some(&raw), &mut warnings); + let err = + super::name_verdict("hive-x", &broken, Ok(false), &mut warnings).expect_err("hive-x"); + assert!(err.contains("reserved for hive forge accounts"), "{err}"); + for ok in ["hivemind", "xhive-y"] { + let broken = super::broken_name_rules(ok, Some(&raw), &mut warnings); + assert!(broken.is_empty(), "{ok:?}: {broken:?}"); + } + assert!(warnings.is_empty(), "{warnings:?}"); + } + + /// The prefix the rule reserves is the one hive accounts are named with. + #[test] + fn the_reserved_prefix_is_the_hive_account_prefix() { + assert!( + swarm_secret_client::forge::hive_login("alpha") + .starts_with(hive_types::RESERVED_AGENT_PREFIX) + ); + } + /// Re-creating an agent under its own name must keep working: a name /// already in the roster is warned about, never refused. #[test] diff --git a/swarm-secret-client/src/forge.rs b/swarm-secret-client/src/forge.rs index d6de4a07..cb43aab4 100644 --- a/swarm-secret-client/src/forge.rs +++ b/swarm-secret-client/src/forge.rs @@ -40,9 +40,9 @@ pub fn agent_token_path(agent: &str) -> Result { /// basic-auth header that token travels in. /// /// The `hive-` prefix keeps the account reading as a hive's rather than an -/// agent's, as [`crate::matrix::hive_localpart`] does on the homeserver. It -/// does not make the name unique: an agent may be called `hive-` too, and -/// `swarm-controller` tells the two apart by the account's email. +/// agent's, as [`crate::matrix::hive_localpart`] does on the homeserver. +/// `swarm-controller` refuses a new agent named with this prefix, and tells an +/// older one apart from the hive's account by the account's email. #[must_use] pub fn hive_login(hive: &str) -> String { format!("hive-{hive}")