request the review classifier's review when a PR's head moves
The router gets a request_reviewer node and a pull_request_sync source; the built-in head-classifier flow asks swarm-controller, over $SWARM.router.sink.request-reviewer, to request review-classifier on every push, unless its review of the head already stands. The controller subscribes to pull_request_sync on its pull-request hook and answers the router; nats-auth grants the router the publish and the controller the subscribe.
This commit is contained in:
parent
463c5cbfcc
commit
0b1669a364
20 changed files with 1231 additions and 64 deletions
|
|
@ -293,6 +293,10 @@ impl Policy {
|
|||
/// controller creates the bucket and is its one writer.
|
||||
/// - **The roster.** `STREAM.INFO` on the agent-status bucket, which lists
|
||||
/// its keys (`<hive>.<agent>`) and writes none.
|
||||
/// - **Review requests.** Publish on the one sink subject
|
||||
/// `swarm_wire::router_sink::REQUEST_REVIEWER_SUBJECT` and hear the answer
|
||||
/// on its own inbox. The controller is the subject's one subscriber and
|
||||
/// holds the forge token; no wildcard over `$SWARM.router.sink`.
|
||||
fn notify_router_permissions(client_id: &str) -> Permissions {
|
||||
use swarm_wire::{forge_events, notify_flows, notify_router, router_todo};
|
||||
|
||||
|
|
@ -320,6 +324,7 @@ impl Policy {
|
|||
format!("$JS.API.STREAM.INFO.{flows}"),
|
||||
format!("$JS.API.DIRECT.GET.{flows}.>"),
|
||||
format!("$JS.API.STREAM.INFO.{}", Self::agent_status_stream()),
|
||||
swarm_wire::router_sink::REQUEST_REVIEWER_SUBJECT.to_owned(),
|
||||
Self::inbox(client_id),
|
||||
]);
|
||||
Permissions {
|
||||
|
|
@ -347,7 +352,8 @@ impl Policy {
|
|||
|
||||
/// What a reader may subscribe to: every agent's terminal, its
|
||||
/// subagents' terminals and its turn state, which the controller relays
|
||||
/// to swarm-ui, every hive's permissions import, and its own reply inbox.
|
||||
/// to swarm-ui, every hive's permissions import, the notification router's
|
||||
/// review requests, and its own reply inbox.
|
||||
///
|
||||
/// The three families are the publishers' (`hive-agent`'s `swarm_term`
|
||||
/// and `swarm_agent_state`, `swarm_wire::subagent_term`), spelled
|
||||
|
|
@ -359,6 +365,7 @@ impl Policy {
|
|||
swarm_wire::subagent_term::subject_wildcard(),
|
||||
swarm_queue_client::agent_state_subject_wildcard(),
|
||||
swarm_wire::agent_permissions::import_subject_wildcard(),
|
||||
swarm_wire::router_sink::REQUEST_REVIEWER_SUBJECT.to_owned(),
|
||||
Self::inbox(client_id),
|
||||
]
|
||||
}
|
||||
|
|
@ -1737,7 +1744,7 @@ mod tests {
|
|||
#[test]
|
||||
fn the_notify_router_drains_its_own_consumer_and_writes_only_its_own_state() {
|
||||
use swarm_queue_client::client_inbox_prefix;
|
||||
use swarm_wire::{forge_events, notify_flows, notify_router, router_todo};
|
||||
use swarm_wire::{forge_events, notify_flows, notify_router, router_sink, router_todo};
|
||||
|
||||
let p = with_router()
|
||||
.permissions(NOTIFY_ROUTER)
|
||||
|
|
@ -1758,6 +1765,7 @@ mod tests {
|
|||
router_todo::subject("atlas"),
|
||||
format!("$KV.{state}.42.5571"),
|
||||
format!("$JS.API.DIRECT.GET.KV_{state}.$KV.{state}.42.5571"),
|
||||
router_sink::REQUEST_REVIEWER_SUBJECT.to_owned(),
|
||||
"$JS.API.STREAM.INFO.KV_agent-status".to_owned(),
|
||||
// The flows document: read it.
|
||||
format!("$JS.API.STREAM.INFO.KV_{flows}"),
|
||||
|
|
@ -1783,6 +1791,8 @@ mod tests {
|
|||
forge_events::subject("issues"),
|
||||
"$KV.agent-status.alpha.atlas".to_owned(),
|
||||
"$KV.agent-schedules.atlas".to_owned(),
|
||||
"$SWARM.router.sink.other".to_owned(),
|
||||
"$SWARM.router.sink.>".to_owned(),
|
||||
// The flows document: not write it, nor make the bucket.
|
||||
format!("$KV.{flows}.{}", notify_flows::KEY),
|
||||
format!("$JS.API.STREAM.CREATE.KV_{flows}"),
|
||||
|
|
@ -1798,6 +1808,48 @@ mod tests {
|
|||
assert_eq!(policy().permissions(NOTIFY_ROUTER), None);
|
||||
}
|
||||
|
||||
/// The router asks and the controller answers: nobody else may send the
|
||||
/// request, and nobody else may hear it, since the answer acts under the
|
||||
/// controller's forge token.
|
||||
#[test]
|
||||
fn only_the_router_may_send_a_review_request_and_only_the_reader_may_hear_it() {
|
||||
use swarm_wire::router_sink::{REPLY_DEADLINE, REQUEST_REVIEWER_SUBJECT};
|
||||
|
||||
let p = with_router();
|
||||
let grants = [
|
||||
(
|
||||
"the reader",
|
||||
p.permissions("swarm-controller").expect("reader"),
|
||||
),
|
||||
("a hive", p.permissions("hive-alpha").expect("a hive")),
|
||||
("the router", p.permissions(NOTIFY_ROUTER).expect("router")),
|
||||
("the minter", p.permissions(CLAUDE_LOGIN).expect("minter")),
|
||||
("the builder", p.permissions(BUILDER).expect("builder")),
|
||||
(
|
||||
"the classifier",
|
||||
p.permissions(CLASSIFIER).expect("classifier"),
|
||||
),
|
||||
("atlas", p.agent_token_permissions("atlas").expect("atlas")),
|
||||
];
|
||||
for (who, grant) in &grants {
|
||||
let sends = grant
|
||||
.publish
|
||||
.iter()
|
||||
.any(|g| covers(g, REQUEST_REVIEWER_SUBJECT));
|
||||
let hears = may_subscribe(grant, REQUEST_REVIEWER_SUBJECT);
|
||||
assert_eq!(sends, *who == "the router", "{who} sends: {sends}");
|
||||
assert_eq!(hears, *who == "the reader", "{who} hears: {hears}");
|
||||
}
|
||||
// The controller answers on the reply subject for as long as the
|
||||
// router waits.
|
||||
let reader = &grants[0].1;
|
||||
assert!(
|
||||
reader.responses >= Some(REPLY_DEADLINE),
|
||||
"{:?}",
|
||||
reader.responses
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_the_router_may_queue_an_agents_todo_and_only_the_reader_may_keep_a_forge_event() {
|
||||
use swarm_wire::{forge_events, router_todo};
|
||||
|
|
|
|||
Loading…
Reference in a new issue