diff --git a/swarm-nats-auth/src/policy.rs b/swarm-nats-auth/src/policy.rs index 8c1fea29..43cfff04 100644 --- a/swarm-nats-auth/src/policy.rs +++ b/swarm-nats-auth/src/policy.rs @@ -278,6 +278,12 @@ impl Policy { // about a change, with nothing in the controller's log to say a // permission was the reason. swarm_queue_client::KNOWLEDGE_SUBJECT.to_owned(), + // The deploy event, same shape and the same failure mode as the + // knowledge event above — one literal subject, one writer. Also + // deliberately not a per-hive family: this responder scopes + // publish only, so a per-hive subject would not stop a hive + // reading another's. See the const's own doc. + swarm_queue_client::DEPLOY_SUBJECT.to_owned(), ]); subjects } @@ -373,6 +379,39 @@ mod tests { ); } + #[test] + fn a_reader_may_publish_the_deploy_event() { + let p = policy().permissions("swarm-controller").expect("a reader"); + assert!( + p.publish + .contains(&swarm_queue_client::DEPLOY_SUBJECT.to_owned()), + "the controller is the only publisher of this event; without the \ + grant its publish is refused, and a refusal arrives as a timeout" + ); + } + + #[test] + fn a_hive_may_not_publish_the_deploy_event_to_anyone_including_itself() { + // Same arm as the knowledge event's, and it matters more here: a forged + // knowledge event makes a hive re-read a repo, while a forged deploy + // event makes it rebuild and restart a named agent. One shared subject + // means a single forged message reaches every hive in the swarm. + // + // Note this is the half `sub` scoping would not fix even once it lands: + // reading another hive's deploy message is a confidentiality question, + // *sending* one is this. + let p = policy() + .permissions("hive-alpha") + .expect("a hive is admitted"); + assert!( + !p.publish + .iter() + .any(|s| s == swarm_queue_client::DEPLOY_SUBJECT), + "a hive must not publish the deploy event: {:?}", + p.publish + ); + } + #[test] fn a_hive_grant_never_includes_the_jetstream_wildcard() { // `$JS.API.>` also covers `$JS.API.STREAM.DELETE.KV_hive-status`, with diff --git a/swarm-queue-client/src/lib.rs b/swarm-queue-client/src/lib.rs index c7a9143f..a9e839ee 100644 --- a/swarm-queue-client/src/lib.rs +++ b/swarm-queue-client/src/lib.rs @@ -173,6 +173,40 @@ pub mod status; /// permitted at all — speaks neither `jetstream` nor `kv`. pub const KNOWLEDGE_SUBJECT: &str = "$SWARM.knowledge"; +/// The subject the swarm controller publishes on to ask a hive to rebuild one +/// of its agents. Same shape as [`KNOWLEDGE_SUBJECT`]: one writer, every hive +/// subscribes, and it lives here for the same three-crate reason. +/// +/// # Swarm-wide, not per-hive, and that is deliberate +/// +/// A `$SWARM.deploy.` family would *look* like isolation and provide +/// none: the auth-callout responder scopes **publish** only, leaving `sub` +/// unrestricted, so any hive could subscribe to another's subject just as +/// easily as to its own. Until `sub` is scoped, the per-hive split costs a +/// wider grant and buys nothing — so the addressing lives in the payload and +/// each hive filters on its own name, exactly as the knowledge event fans out. +pub const DEPLOY_SUBJECT: &str = "$SWARM.deploy"; + +/// What a [`DEPLOY_SUBJECT`] message carries. +/// +/// The knowledge event has no payload — every hive does the same thing on +/// receipt. This one is addressed, so both ends have to agree on the fields, +/// which is why the type lives beside the subject rather than in whichever +/// crate happened to need it first. +/// +/// ⚠️ **A trigger, not the config.** The hive already tracks the agent's +/// config repo; putting desired state on the wire would make this message a +/// second source of truth for something git already owns, and a hive that +/// missed a message would then be wrong rather than merely late. +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct DeployRequest { + /// Which hive should act. Every hive receives the message; the one whose + /// own name this matches is the one that rebuilds. + pub hive: String, + /// The agent to rebuild, as the swarm knows it. + pub agent: String, +} + /// The hive-notices stream, shared by the hive that publishes and /// whatever eventually consumes it. Behind the `notices` feature, same /// reason `status` is behind `kv` — see the module doc.