Watch
0
0
Fork
You've already forked hyperhive
0

publish operators' forge-profile pronouns to kv and list them in the agent prompt

swarm-controller reads each member of authelia's operator group's forge
profile every five minutes and keeps one key per operator in the new
operator-profiles bucket. Every agent may list and read that bucket, and
hive-agent renders it into the system prompt at harness start, falling
back to today's prompt when the bucket is empty or doesn't answer within
five seconds.

Fixes #5383
This commit is contained in:
atlas 2026-10-17 11:17:07 +02:00 • committed by mara
commit 6942f8fd27
16 changed files with 681 additions and 35 deletions

View file

@ -118,6 +118,10 @@ mod tests {
"$SWARM.term.atlas".to_owned(),
"$SWARM.agent-state.atlas".to_owned(),
"$KV.agent-icons.atlas".to_owned(),
"$JS.API.STREAM.INFO.KV_operator-profiles".to_owned(),
"$JS.API.CONSUMER.CREATE.KV_operator-profiles".to_owned(),
"$JS.API.CONSUMER.CREATE.KV_operator-profiles.>".to_owned(),
"$JS.API.DIRECT.GET.KV_operator-profiles.>".to_owned(),
]
);
}

View file

@ -173,12 +173,16 @@ impl Policy {
/// must already be a single `[A-Za-z0-9_-]` segment, which the token parse
/// guarantees, so it cannot widen a subject with `.`, `*` or `>`.
pub fn agent_token_permissions(&self, agent: &str) -> Option<Permissions> {
let publish: Vec<String> = self
let mut publish: Vec<String> = self
.agent_token_subjects
.iter()
.map(|s| s.replace(AGENT_PLACEHOLDER, agent))
.collect();
(!publish.is_empty()).then(|| Permissions {
if publish.is_empty() {
return None;
}
publish.extend(Self::operator_profiles_read());
Some(Permissions {
publish,
subscribe: Some(vec![
format!("{}.>", swarm_queue_client::agent_inbox_prefix(agent)),
@ -196,6 +200,26 @@ impl Policy {
})
}
/// Reading every key of the operator-profile bucket
/// (`swarm_queue_client::operator_profiles::read_all`), which an agent
/// renders into its system prompt.
///
/// The whole bucket, since every agent reads every operator. Not an
/// `{agent}` template, so it is granted here rather than through
/// `--agent-token-publish-subject`. `STREAM.INFO` opens the bucket, both
/// `CONSUMER.CREATE` forms are the `keys()` listing (an ephemeral consumer
/// carries no name, and `>` never matches zero tokens), and `DIRECT.GET`
/// reads each key. No `$KV.` subject: the controller is the only writer.
fn operator_profiles_read() -> [String; 4] {
let stream = swarm_queue_client::operator_profiles::STREAM;
[
format!("$JS.API.STREAM.INFO.{stream}"),
format!("$JS.API.CONSUMER.CREATE.{stream}"),
format!("$JS.API.CONSUMER.CREATE.{stream}.>"),
format!("$JS.API.DIRECT.GET.{stream}.>"),
]
}
/// The permissions for `client_id`, or `None` when no rule matches.
///
/// `None` is a denial. It is not "grant nothing and let them connect":
@ -724,6 +748,18 @@ mod tests {
.expect("the default policy is valid")
}
/// An agent grant's publish list without the operator-profile read every
/// agent holds, so a test can compare the templated part exactly.
fn templated(grant: &Permissions) -> Vec<String> {
let shared = Policy::operator_profiles_read();
grant
.publish
.iter()
.filter(|s| !shared.contains(s))
.cloned()
.collect()
}
#[test]
fn the_reader_can_open_and_list_the_agent_status_bucket() {
let subjects = policy().reader_subjects();
@ -1878,6 +1914,10 @@ mod tests {
vec![
"$SWARM.term.atlas".to_owned(),
"$SWARM.agent-state.atlas".to_owned(),
"$JS.API.STREAM.INFO.KV_operator-profiles".to_owned(),
"$JS.API.CONSUMER.CREATE.KV_operator-profiles".to_owned(),
"$JS.API.CONSUMER.CREATE.KV_operator-profiles.>".to_owned(),
"$JS.API.DIRECT.GET.KV_operator-profiles.>".to_owned(),
]
);
}
@ -1891,12 +1931,12 @@ mod tests {
.with_agent_token_subjects(vec!["$KV.agent-icons.{agent}".to_owned()])
.expect("a per-agent template is valid");
let atlas = p.agent_token_permissions("atlas").expect("configured");
assert_eq!(atlas.publish, vec![subject("atlas")]);
assert_eq!(templated(&atlas), vec![subject("atlas")]);
assert!(!atlas.publish.contains(&subject("argus")));
// Control: the same template does grant argus its own key, so atlas
// lacking it is atlas's grant being scoped.
let argus = p.agent_token_permissions("argus").expect("configured");
assert_eq!(argus.publish, vec![subject("argus")]);
assert_eq!(templated(&argus), vec![subject("argus")]);
}
/// The subagent template as `swarm-nats.nix` spells it expands to the
@ -1910,10 +1950,10 @@ mod tests {
.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")]);
assert_eq!(templated(&g), 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_eq!(templated(&argus), vec![stream_subjects("argus")]);
assert_ne!(g.publish, argus.publish);
let stream = stream_name("atlas");
@ -1941,11 +1981,11 @@ mod tests {
])
.expect("a per-agent template is valid");
let atlas = p.agent_token_permissions("atlas").expect("configured");
assert_eq!(atlas.publish, vec![direct_get_subject("atlas")]);
assert_eq!(templated(&atlas), vec![direct_get_subject("atlas")]);
// Control: argus's own expansion is a different subject, so atlas
// lacking it is the grant being scoped, not the template being empty.
let argus = p.agent_token_permissions("argus").expect("configured");
assert_eq!(argus.publish, vec![direct_get_subject("argus")]);
assert_eq!(templated(&argus), vec![direct_get_subject("argus")]);
assert!(!atlas.publish.contains(&direct_get_subject("argus")));
for refused in [
@ -1989,9 +2029,9 @@ mod tests {
])
.expect("a per-agent template is valid");
let atlas = p.agent_token_permissions("atlas").expect("configured");
assert_eq!(atlas.publish, vec![direct_get_subject("atlas")]);
assert_eq!(templated(&atlas), vec![direct_get_subject("atlas")]);
let argus = p.agent_token_permissions("argus").expect("configured");
assert_eq!(argus.publish, vec![direct_get_subject("argus")]);
assert_eq!(templated(&argus), vec![direct_get_subject("argus")]);
for refused in [
direct_get_subject("argus"),
@ -2022,4 +2062,44 @@ mod tests {
fn with_no_agent_token_subject_configured_an_agent_token_is_refused() {
assert!(policy().agent_token_permissions("atlas").is_none());
}
/// Every agent reads every operator's profile, and only reads it: the
/// bucket opens, lists and answers a direct get, and nothing writes,
/// reshapes or deletes it.
#[test]
fn an_agent_may_list_and_read_every_operator_profile_and_write_none() {
use swarm_queue_client::operator_profiles::{BUCKET, STREAM};
let p = policy()
.with_agent_token_subjects(vec!["$SWARM.term.{agent}".to_owned()])
.expect("a per-agent template is valid");
let atlas = p.agent_token_permissions("atlas").expect("configured");
let may = |subject: &str| atlas.publish.iter().any(|g| covers(g, subject));
for allowed in [
format!("$JS.API.STREAM.INFO.{STREAM}"),
format!("$JS.API.CONSUMER.CREATE.{STREAM}"),
format!("$JS.API.CONSUMER.CREATE.{STREAM}.c1.$KV.{BUCKET}.>"),
format!("$JS.API.DIRECT.GET.{STREAM}.$KV.{BUCKET}.mara"),
] {
assert!(may(&allowed), "atlas may not publish {allowed}");
}
for refused in [
format!("$KV.{BUCKET}.mara"),
format!("$JS.API.STREAM.CREATE.{STREAM}"),
format!("$JS.API.STREAM.UPDATE.{STREAM}"),
format!("$JS.API.STREAM.DELETE.{STREAM}"),
format!("$JS.API.STREAM.PURGE.{STREAM}"),
"$JS.API.DIRECT.GET.KV_agent-inference.$KV.agent-inference.argus".to_owned(),
] {
assert!(!may(&refused), "atlas may publish {refused}");
}
// The hive's own client is not the reader and gets none of it.
let hive = p.permissions("hive-alpha").expect("admitted");
assert!(
!hive
.publish
.iter()
.any(|g| covers(g, &format!("$JS.API.STREAM.INFO.{STREAM}")))
);
}
}