Watch
0
0
Fork
You've already forked hyperhive
0

swarm-controller: create every agent's subagent stream

swarm-controller now creates `term-sub-<agent>` for every agent a hive is
declared to run, at start and every minute after, with the config
`swarm_queue_client::subagent_term::open_or_create` spells (subjects
`$SWARM.term.<agent>.sub.>`, max_age 24h). An existing stream is opened as
it is, as the controller does for its other streams and buckets, under the
`$JS.API.STREAM.CREATE.*` grant it already holds.

The agent no longer creates the stream: its token is granted publish on
`$SWARM.term.<agent>.sub.>` and no `$JS.API.STREAM.CREATE|INFO` subject,
and the subagent daemon only publishes. A `CREATE` carries the stream's
config in its payload, which no subject grant narrows, so the agent could
otherwise pick the stream's subjects and limits.
This commit is contained in:
atlas 2026-10-02 23:34:27 +02:00 • committed by mara
commit 318f67cda9
13 changed files with 135 additions and 102 deletions

View file

@ -363,16 +363,12 @@ mod tests {
"$KV.agent-icons.{agent}",
"--agent-token-publish-subject",
"$SWARM.term.{agent}.sub.>",
"--agent-token-publish-subject",
"$JS.API.STREAM.CREATE.term-sub-{agent}",
"--agent-token-publish-subject",
"$JS.API.STREAM.INFO.term-sub-{agent}",
"--store-cert-role",
"swarm-nats-auth",
])
.expect("the unit's own argument vector must parse");
assert_eq!(args.agent_client_suffix, "-agent");
assert_eq!(args.agent_token_publish_subjects.len(), 6);
assert_eq!(args.agent_token_publish_subjects.len(), 4);
}
/// The control for the case above: an ordinary value parses through the

View file

@ -1219,34 +1219,32 @@ mod tests {
);
}
/// The subagent templates as `swarm-nats.nix` spells them expand to the
/// subjects and stream name the subagent daemon uses, and to `CREATE` and
/// `INFO` on that one stream only.
/// The subagent template as `swarm-nats.nix` spells it expands to the
/// subjects the subagent daemon publishes on, publish only. The stream is
/// the controller's to create, which its own grant covers.
#[test]
fn an_agents_subagent_grant_is_its_own_stream_and_nothing_wider() {
fn an_agents_subagent_grant_is_publish_on_its_own_family_only() {
use swarm_queue_client::subagent_term::{stream_name, stream_subjects, subject};
let p = policy_with_agent_subject()
.with_agent_token_subjects(vec![
"$SWARM.term.{agent}.sub.>".to_owned(),
"$JS.API.STREAM.CREATE.term-sub-{agent}".to_owned(),
"$JS.API.STREAM.INFO.term-sub-{agent}".to_owned(),
])
.expect("per-agent templates are valid");
.with_agent_token_subjects(vec!["$SWARM.term.{agent}.sub.>".to_owned()])
.expect("a per-agent template is valid");
let g = p.agent_token_permissions("atlas").expect("configured");
assert_eq!(
g.publish,
vec![
stream_subjects("atlas"),
format!("$JS.API.STREAM.CREATE.{}", stream_name("atlas")),
format!("$JS.API.STREAM.INFO.{}", stream_name("atlas")),
]
);
assert_eq!(g.publish, vec![stream_subjects("atlas")]);
assert!(subject("atlas", "scout").starts_with(g.publish[0].trim_end_matches('>')));
let argus = p.agent_token_permissions("argus").expect("configured");
assert_eq!(argus.publish, vec![stream_subjects("argus")]);
assert_ne!(g.publish, argus.publish);
let stream = stream_name("atlas");
assert!(
g.publish.iter().all(|s| !argus.publish.contains(s)),
"atlas and argus share a subject: {g:?} {argus:?}"
!stream.contains('.'),
"{stream} is one token, so `*` covers it"
);
assert!(
policy()
.reader_subjects()
.contains(&"$JS.API.STREAM.CREATE.*".to_owned())
);
}