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:
parent
e7ed28b382
commit
fefc565794
11 changed files with 558 additions and 69 deletions
|
|
@ -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(),
|
||||
] {
|
||||
|
|
|
|||
Loading…
Reference in a new issue