hive-c0re: read each agent's swarm caps from agent-limits
Every 30 seconds `workers::swarm_limits` reads the `agent-limits` key of each agent this hive has and stores the result in swarm-resource-limits.json, where the drop-in, the heap ceiling and the dashboard read it. A cap therefore follows its agent to whichever hive runs it. Each value goes through the hive's own validate_cpu_quota / validate_memory_max before it is stored, since hive-priv renders it verbatim into the container's drop-in, and one bad value refuses the whole set (argus on #4268, carried over from the wanted-state consumer this replaces). A failed read, or a bucket that does not answer, leaves the stored caps as they are; three failures in a row raise a banner. swarm-nats-auth grants every hive `$JS.API.DIRECT.GET.KV_agent-limits.>`: read-only, and every agent's key, not just the hive's own. Scoping it to the hive's agents would need a roster in the auth responder, which policy.rs argues against. mara chose this on #4268 (c95293); narrowing it is a possible low-priority follow-up, not planned. A test pins that the grant reads every key, writes none, and reaches no other bucket. Contributes to #3865.
This commit is contained in:
parent
ebeef4b84d
commit
fccf2ab892
8 changed files with 320 additions and 4 deletions
|
|
@ -490,6 +490,17 @@ impl Policy {
|
|||
format!("$JS.API.STREAM.CREATE.{agent_status}"),
|
||||
format!("$KV.{}.{hive}.*", swarm_queue_client::agent_status::BUCKET),
|
||||
]);
|
||||
// Reading the caps the swarm sets for the agents this hive runs
|
||||
// (`swarm_queue_client::agent_limits`): a direct get, so read-only, and
|
||||
// no `STREAM.INFO` because the reader skips the lookup.
|
||||
//
|
||||
// Every agent's key, not just this hive's: the bucket is keyed by agent
|
||||
// alone, and naming one hive's agents here needs the roster
|
||||
// `hive_name`'s doc argues against.
|
||||
subjects.push(format!(
|
||||
"$JS.API.DIRECT.GET.{}.>",
|
||||
swarm_queue_client::agent_limits::STREAM
|
||||
));
|
||||
subjects.extend(
|
||||
self.extra_hive_subjects
|
||||
.iter()
|
||||
|
|
@ -712,6 +723,44 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
/// A hive reads any agent's caps and can change none of them, and the
|
||||
/// grant reaches no other bucket.
|
||||
#[test]
|
||||
fn a_hive_may_read_every_agents_caps_and_write_none() {
|
||||
use swarm_queue_client::agent_limits::{STREAM, direct_get_subject, key_subject};
|
||||
|
||||
let p = policy()
|
||||
.permissions("hive-alpha")
|
||||
.expect("a hive is admitted");
|
||||
let may = |subject: &str| p.publish.iter().any(|g| covers(g, subject));
|
||||
|
||||
// Its own agents and agents running elsewhere alike.
|
||||
for agent in ["atlas", "iris"] {
|
||||
assert!(may(&direct_get_subject(agent)), "cannot read {agent}");
|
||||
}
|
||||
// A KV put or delete is a publish to the key's subject; the rest
|
||||
// reshape or empty the stream.
|
||||
for forbidden in [
|
||||
key_subject("atlas"),
|
||||
format!("$JS.API.STREAM.CREATE.{STREAM}"),
|
||||
format!("$JS.API.STREAM.UPDATE.{STREAM}"),
|
||||
format!("$JS.API.STREAM.DELETE.{STREAM}"),
|
||||
format!("$JS.API.STREAM.PURGE.{STREAM}"),
|
||||
format!("$JS.API.STREAM.MSG.DELETE.{STREAM}"),
|
||||
format!("$JS.API.CONSUMER.CREATE.{STREAM}"),
|
||||
] {
|
||||
assert!(!may(&forbidden), "hive alpha may publish {forbidden}");
|
||||
}
|
||||
// The read reaches only this bucket.
|
||||
for other in [
|
||||
swarm_queue_client::agent_inference::direct_get_subject("atlas"),
|
||||
swarm_queue_client::agent_schedules::direct_get_subject("atlas"),
|
||||
"$JS.API.DIRECT.GET.KV_hive-wanted-beta.$KV.hive-wanted-beta.beta".to_owned(),
|
||||
] {
|
||||
assert!(!may(&other), "hive alpha may publish {other}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_extra_subject_without_the_placeholder_is_refused() {
|
||||
// 🩸 The option exists to put a second stream inside ONE hive's
|
||||
|
|
|
|||
Loading…
Reference in a new issue