Watch
0
0
Fork
You've already forked hyperhive
0

keep the operator list under one key and read it with one direct get

The agent read the operator-profiles bucket with a keys() listing, an
ordered consumer whose grant (STREAM.INFO, CONSUMER.CREATE) and flow-control
reply had never run against a permissioned queue. The controller now writes
the whole list as one JSON array under one key, only when it changed, and
the agent reads it with one DIRECT.GET the way agent_inference::read_own
reads its key. The agent grant shrinks to that one subject.
This commit is contained in:
atlas 2026-10-17 12:19:43 +02:00 • committed by mara
commit 49015eb553
12 changed files with 254 additions and 201 deletions

View file

@ -118,10 +118,8 @@ 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(),
"$JS.API.DIRECT.GET.KV_operator-profiles.$KV.operator-profiles.operators"
.to_owned(),
]
);
}

View file

@ -181,7 +181,13 @@ impl Policy {
if publish.is_empty() {
return None;
}
publish.extend(Self::operator_profiles_read());
// The operator list (`swarm_queue_client::operator_profiles`), which
// every agent renders into its system prompt: one direct get on the
// bucket's one key, so no `STREAM.INFO`, no `CONSUMER.CREATE` and no
// `$KV.` write. Every agent reads the same key, so it is no `{agent}`
// template and is granted here rather than through
// `--agent-token-publish-subject`.
publish.push(swarm_queue_client::operator_profiles::direct_get_subject());
Some(Permissions {
publish,
subscribe: Some(vec![
@ -200,26 +206,6 @@ 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":
@ -751,11 +737,11 @@ mod tests {
/// 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();
let shared = swarm_queue_client::operator_profiles::direct_get_subject();
grant
.publish
.iter()
.filter(|s| !shared.contains(s))
.filter(|s| **s != shared)
.cloned()
.collect()
}
@ -1914,10 +1900,8 @@ 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(),
"$JS.API.DIRECT.GET.KV_operator-profiles.$KV.operator-profiles.operators"
.to_owned(),
]
);
}
@ -2063,43 +2047,46 @@ mod tests {
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.
/// Every agent reads the operator list with one direct get on its one key,
/// and nothing else of the bucket: no lookup, no listing, no other key and
/// no write.
#[test]
fn an_agent_may_list_and_read_every_operator_profile_and_write_none() {
use swarm_queue_client::operator_profiles::{BUCKET, STREAM};
fn an_agent_may_read_only_the_operator_list_key() {
use swarm_queue_client::operator_profiles::{
BUCKET, STREAM, direct_get_subject, key_subject,
};
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");
assert!(atlas.publish.contains(&direct_get_subject()));
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"),
key_subject(),
format!("$JS.API.DIRECT.GET.{STREAM}"),
format!("$JS.API.DIRECT.GET.{STREAM}.$KV.{BUCKET}.mara"),
format!("$JS.API.STREAM.INFO.{STREAM}"),
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(),
format!("$JS.API.STREAM.MSG.GET.{STREAM}"),
format!("$JS.API.CONSUMER.CREATE.{STREAM}"),
format!("$JS.API.CONSUMER.CREATE.{STREAM}.c1.$KV.{BUCKET}.>"),
] {
assert!(!may(&refused), "atlas may publish {refused}");
}
// Control: the matcher does match, so the refusals above are not
// vacuous.
assert!(may(&direct_get_subject()));
// 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}")))
.any(|g| covers(g, &direct_get_subject()))
);
}
}