Watch
0
0
Fork
You've already forked hyperhive
0

swarm-queue-client: one agent-token spelling, and no hive in AgentCredential

`swarm_queue_client::agent_token::format_agent_token` / `parse_agent_token`
are the spelling an agent presents its own queue secret in,
`swarm-agent.<agent>.<secret>`, and the one the auth-callout responder
reads back. The prefix is what separates it from an OIDC access token,
which may itself contain `.`. Parsing distinguishes "not an agent token"
(no prefix) from "a malformed one"; the error names the problem and never
the value. The module is store-free, so the agent formats its token
without linking the secret-store client.

`swarm_secret_client::queue::AgentCredential` loses `hive`: an agent's
identity is not tied to a hive, and nothing reads the field. Objects
already in the store carry it and still decode, since unknown fields are
ignored; a test parses one. The controller stops writing it.

With the credential no longer naming a hive, and the agent's policy
naming none since #4762, nothing in the mint consumes one. `hive` goes
from `mint_and_verify`, from the `MintAgentIdentity` node, and from
`POST /api/agents/{name}/identity`, which now takes no body and no longer
checks a hive against the roster; a caller that still sends one is not
refused, the body is ignored. `swarmctl agent mint-identity` loses
`--hive`, so passing it is now a usage error.
This commit is contained in:
atlas 2026-09-26 01:05:53 +02:00
commit fc97c237dc
10 changed files with 234 additions and 201 deletions

View file

@ -22,6 +22,9 @@
//! reaching the store and nothing else; deriving a queue identity from it would
//! couple the two credentials' lifetimes, so that renewing one would mean
//! renewing the other.
//!
//! The token an agent presents that secret in at the queue is spelled by
//! `swarm_queue_client::agent_token`, which needs no store client.
use serde::{Deserialize, Serialize};
@ -57,14 +60,11 @@ pub fn agent_queue_path(agent: &str) -> Result<String, Error> {
Ok(format!("{prefix}/queue"))
}
/// What [`agent_queue_path`] holds: the secret, and the principal it proves.
/// What [`agent_queue_path`] holds: the secret, and the agent it proves.
///
/// Both names ride **in the object** rather than being parsed back out of a
/// composite principal string. Hive and agent names draw from the same
/// alphabet (`hive_types::Ident`, `[a-z0-9-]`), so a principal spelled
/// `hive-<hive>-agent-<agent>` parses two ways for a name containing `-agent-`
/// — and an ambiguous principal parse in an authorisation path is a caller that
/// authenticates fine and is handed somebody else's grant.
/// No hive: an agent's identity is not tied to one, and the subjects the
/// verifier grants are keyed on the agent alone. Objects written with a `hive`
/// field still decode, because unknown fields are ignored.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AgentCredential {
/// The secret itself. Named to match [`Credential::value`] and
@ -74,13 +74,6 @@ pub struct AgentCredential {
/// The agent this secret authenticates.
pub agent: String,
/// The hive that agent belongs to.
///
/// Here because the verifying end has no roster to look it up in, and
/// because the subjects an agent is granted are hive-templated — without
/// this field the verifier would know *who* is connecting and not *where*.
pub hive: String,
}
/// What the path holds: the client secret, plus the client id it belongs to.
@ -198,7 +191,6 @@ mod tests {
let c = AgentCredential {
value: "s3cr3t".to_owned(),
agent: "atlas".to_owned(),
hive: "alpha".to_owned(),
};
let json = serde_json::to_string(&c).expect("serialises");
assert_eq!(
@ -212,29 +204,39 @@ mod tests {
let json = serde_json::to_value(AgentCredential {
value: "s3cr3t".to_owned(),
agent: "atlas".to_owned(),
hive: "alpha".to_owned(),
})
.expect("serialises");
assert_eq!(json["value"], "s3cr3t");
assert_eq!(json["agent"], "atlas");
assert_eq!(json["hive"], "alpha");
assert!(json.get("hive").is_none(), "{json}");
}
/// Neither name is optional. An object missing one is not a usable
/// credential — a verifier holding `None` for the hive can only guess at
/// the subjects to grant, and guessing is the failure this shape exists to
/// prevent.
/// A stored object may carry a `hive` field. It is ignored, and the object
/// decodes.
#[test]
fn an_agent_object_missing_a_principal_does_not_decode() {
assert!(
serde_json::from_str::<AgentCredential>(r#"{"value":"s","agent":"atlas"}"#).is_err()
fn a_stored_agent_object_that_still_names_a_hive_decodes() {
let c: AgentCredential =
serde_json::from_str(r#"{"value":"s3cr3t","agent":"atlas","hive":"alpha"}"#)
.expect("a stored object carrying `hive` decodes");
assert_eq!(
c,
AgentCredential {
value: "s3cr3t".to_owned(),
agent: "atlas".to_owned(),
}
);
}
/// The agent is required: it is what the verifier checks the presented
/// name against.
#[test]
fn an_agent_object_missing_its_agent_does_not_decode() {
assert!(serde_json::from_str::<AgentCredential>(r#"{"value":"s"}"#).is_err());
assert!(
serde_json::from_str::<AgentCredential>(r#"{"value":"s","hive":"alpha"}"#).is_err()
);
}
/// The two kinds are different objects at different paths, and neither
/// decodes as the other — the property that keeps a reader from picking up
/// a hive-shared credential where a per-agent one was meant.
#[test]
@ -249,7 +251,6 @@ mod tests {
let agent_json = serde_json::to_string(&AgentCredential {
value: "s".to_owned(),
agent: "atlas".to_owned(),
hive: "alpha".to_owned(),
})
.expect("serialises");
assert!(serde_json::from_str::<Credential>(&agent_json).is_err());