Watch
0
0
Fork
You've already forked hyperhive
0

swarm: cancel an agent's turn from the swarm queue

A separate $SWARM.control.<agent> subject carries commands, apart from
the text subject. swarm-controller exposes POST /api/agents/{name}/cancel
as a NATS request; the harness answers whether a turn was actually
stopped, through the same cancel path as the dashboard button. The agent
credential gets subscribe on its control subject and allow_responses to
answer; only the controller may publish there.

Refs #3803
This commit is contained in:
damocles 2026-10-14 23:08:57 +02:00 • committed by mara
commit fefc565794
11 changed files with 558 additions and 69 deletions

View file

@ -174,8 +174,16 @@ impl Policy {
subscribe: Some(vec![
format!("{}.>", swarm_queue_client::agent_inbox_prefix(agent)),
swarm_queue_client::agent_input_subject(agent),
swarm_queue_client::agent_control_subject(agent),
]),
responses: None,
// The controller sends a control command as a request, and the
// harness answers it. `allow_responses` lets it reply to that one
// request without a publish grant that would name the
// controller's inbox, which the agent has no business reaching.
// Operator text input is sent the same way but never answered
// (see `hive-agent`'s `swarm_input`), so the reply right is
// unused there.
responses: Some(swarm_queue_client::AGENT_CONTROL_REPLY_DEADLINE),
})
}
@ -585,6 +593,11 @@ impl Policy {
// `from` as given, which holds only while this client is the
// family's sole publisher: no hive or agent grant may name it.
swarm_queue_client::AGENT_INPUT_SUBJECT_WILDCARD.to_owned(),
// Operator control commands to an agent (cancel its turn). A
// family of its own so the text-input grant never implies the
// power to stop a turn; same sole-publisher property, so a
// command needs no sender field to be trusted.
swarm_queue_client::AGENT_CONTROL_SUBJECT_WILDCARD.to_owned(),
// A review classifier try-out, sent as a request: the reply comes
// back on this client's inbox, which it already subscribes to.
swarm_queue_client::review_classifier::SUBJECT.to_owned(),
@ -922,6 +935,49 @@ mod tests {
);
}
#[test]
fn a_reader_may_publish_control_to_any_agent() {
let p = policy().permissions("swarm-controller").expect("a reader");
assert!(
p.publish
.contains(&swarm_queue_client::AGENT_CONTROL_SUBJECT_WILDCARD.to_owned()),
"the controller is the only publisher of control commands; without \
the grant its request is refused, and a refusal arrives as a timeout"
);
}
#[test]
fn nothing_but_the_controller_may_publish_control() {
// A command carries no sender: the harness acts on it because only
// the controller can publish it. A hive or an agent able to publish
// here could cancel any agent's turn.
let hive = policy().permissions("hive-alpha").expect("admitted");
let agent = policy()
.with_agent_token_subjects(vec!["$SWARM.term.{agent}".to_owned()])
.expect("a per-agent template is valid")
.agent_token_permissions("alpha")
.expect("configured");
for (who, p) in [("a hive", &hive), ("an agent", &agent)] {
assert!(
!p.publish.iter().any(|s| s.starts_with("$SWARM.control")),
"{who} must not publish control: {:?}",
p.publish
);
}
}
#[test]
fn an_agent_may_answer_the_command_it_was_sent() {
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_eq!(
atlas.responses,
Some(swarm_queue_client::AGENT_CONTROL_REPLY_DEADLINE)
);
}
#[test]
fn a_hive_may_not_publish_input_to_an_agent() {
// The harness turns this message into a turn from the operator named in
@ -1082,8 +1138,8 @@ mod tests {
}
#[test]
fn an_agent_token_may_subscribe_to_its_own_inbox_and_input_only() {
use swarm_queue_client::{agent_inbox_prefix, agent_input_subject};
fn an_agent_token_may_subscribe_to_its_own_inbox_input_and_control_only() {
use swarm_queue_client::{agent_control_subject, agent_inbox_prefix, agent_input_subject};
let p = policy()
.with_agent_token_subjects(vec!["$SWARM.term.{agent}".to_owned()])
@ -1091,13 +1147,16 @@ mod tests {
let atlas = p.agent_token_permissions("atlas").expect("configured");
assert!(may_subscribe(&atlas, &reply(&agent_inbox_prefix("atlas"))));
assert!(may_subscribe(&atlas, &agent_input_subject("atlas")));
assert!(may_subscribe(&atlas, &agent_control_subject("atlas")));
for refused in [
">".to_owned(),
"$SWARM.term.>".to_owned(),
"$SWARM.term.argus".to_owned(),
"$SWARM.term.atlas".to_owned(),
"$SWARM.input.*".to_owned(),
"$SWARM.control.*".to_owned(),
agent_input_subject("argus"),
agent_control_subject("argus"),
reply(&agent_inbox_prefix("argus")),
"_INBOX.nuid123.token456".to_owned(),
] {