swarm UI: send a message to an agent, without its hive
The agent view in the swarm UI gets a message box. The text reaches the
agent's harness straight from swarm-controller over the swarm queue and
becomes an inbox turn from the operator's authelia username. hive-c0re is
not in the path.
- gateway: `/api/` forwards authelia's `Remote-User`, replacing any the
client sent.
- swarm-controller: `POST /api/agents/{name}/input` takes that header as
the sender (401 without it) and sends `{from, body}` as a NATS request
on `$SWARM.input.<agent>`. No subscriber is a 503.
- swarm-nats-auth: the controller is granted publish on `$SWARM.input.*`;
no hive or agent grant names the family, so the harness can trust
`from`.
- hive-agent: a task subscribes to the agent's own input subject and
pushes each message into the serve loop's existing reminder channel,
renamed `local_msg_*` since it now carries both.
The harness does not reply to the request: no agent grant covers a reply
subject. The controller therefore answers 202 once the request times out
(2s) and 503 on the server's no-responders status.
Closes #4828
This commit is contained in:
parent
5971b51e65
commit
48ac4ac423
13 changed files with 664 additions and 24 deletions
|
|
@ -498,6 +498,11 @@ impl Policy {
|
|||
// admission). Same failure mode as the knowledge event above: a
|
||||
// refused publish reaches the client as a timeout.
|
||||
swarm_queue_client::DEPLOY_SUBJECT_WILDCARD.to_owned(),
|
||||
// Operator input to an agent, one subject per agent, wildcard for
|
||||
// the same no-roster reason. The harness takes the message's
|
||||
// `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(),
|
||||
// The wanted-state buckets — one per hive, all created and written
|
||||
// by this single client.
|
||||
//
|
||||
|
|
@ -774,6 +779,43 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_reader_may_publish_input_to_any_agent() {
|
||||
let p = policy().permissions("swarm-controller").expect("a reader");
|
||||
assert!(
|
||||
p.publish
|
||||
.contains(&swarm_queue_client::AGENT_INPUT_SUBJECT_WILDCARD.to_owned()),
|
||||
"the controller is the only publisher of operator input; without \
|
||||
the grant its request is refused, and a refusal arrives as a timeout"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn neither_a_hive_nor_its_agents_may_publish_input_to_an_agent() {
|
||||
// The harness turns this message into a turn from the operator named in
|
||||
// it. A hive or agent able to publish here could speak as the operator
|
||||
// to any agent in the swarm.
|
||||
let policy = policy_with_agent_subject();
|
||||
for client in ["hive-alpha", "hive-alpha-agent"] {
|
||||
let p = policy.permissions(client).expect("admitted");
|
||||
for forbidden in [
|
||||
swarm_queue_client::agent_input_subject("alpha"),
|
||||
swarm_queue_client::AGENT_INPUT_SUBJECT_WILDCARD.to_owned(),
|
||||
] {
|
||||
assert!(
|
||||
!p.publish.contains(&forbidden),
|
||||
"{client} must not publish {forbidden}: {:?}",
|
||||
p.publish
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
!p.publish.iter().any(|s| s.starts_with("$SWARM.input")),
|
||||
"{client}: {:?}",
|
||||
p.publish
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_hive_grant_never_includes_the_jetstream_wildcard() {
|
||||
// `$JS.API.>` also covers `$JS.API.STREAM.DELETE.KV_hive-status`, with
|
||||
|
|
|
|||
Loading…
Reference in a new issue