A new swarm service, swarm-notify-router, in its own container on the host
that sets deploy.notify-router.enable (defaulted on by allSwarmServices where
the host has the router's store leaf, like the claude login minter).
- swarm-controller registers instance-wide hooks for `issues` +
`issue_assign` and `pull_request` + `pull_request_assign`, updates the
event list of a hook already registered with fewer (leaving its active flag as it is), and keeps each
verified body unchanged in the SWARM_EVENTS stream on
$SWARM.events.forge.<X-Forgejo-Event-Type>, deduped by X-Forgejo-Delivery.
A delivery the stream did not keep answers 503.
- swarm-notify-router drains its durable consumer on that stream, runs its
flows (source -> match -> todo sink, stored as nodes plus edges and checked
to be a tree) and queues each op on $SWARM.router.todo.<agent> in the
work-queue stream ROUTER_TODOS. Built-in flows: assigned, unassigned (same
key, rewritten) and closed (prefix-clears every holder's router todos). A
per-issue snapshot in its notify-router-state bucket holds the last
assignees and the todo holders; only agents in the agent-status bucket get
ops, and an assignee that was not one is left out of the snapshot so the
next assign event retries it. After the first event that must be retried,
the rest of the batch is nakked so redelivery keeps stream order.
- hive-agent creates its own durable consumer todo-<agent> and applies each
op to its todo store as subsystem `router`, then acks; a refused op stops
the rest of its batch the same way. The store's (subsystem, key) dedup
makes a redelivery harmless. New Todos::clear_prefix.
- swarm-nats-auth: a --notify-router-client arm; the controller may publish
$SWARM.events.forge.*; each agent gets three {agent} templates (extended
consumer create whose filter the server checks, MSG.NEXT, ACK).
- Credentials: a notify-router store leaf minted by glue-bao-tls, a
swarm-notify-router bao policy (read on its own queue client secret), an
authelia machine client, the secret publisher entry, a journal forwarder
route. No manual step.
hive-forge-notify is unchanged, so agents also still get forge's own
notifications.
Refs #5571
87 lines
6.1 KiB
Markdown
87 lines
6.1 KiB
Markdown
# Notification router
|
|
|
|
The swarm's `swarm-notify-router` service turns forge events into todos on the
|
|
agents they concern. No hive is in the path, and the agent can be on any hive:
|
|
swarm-controller keeps the events, the router decides, and each agent's harness
|
|
takes its own todos off the queue.
|
|
|
|
Where it runs and how it's enabled: [`services.md`](services.md#notification-router).
|
|
|
|
It runs beside `hive-forge-notify`, which still delivers forge notifications
|
|
the way it did. An agent can get the same news from both.
|
|
|
|
## What an agent gets
|
|
|
|
| Flow | Event | The agent's todo |
|
|
| ------------ | ------------------------------------------ | ------------------------------------------------------------------------------------------- |
|
|
| `assigned` | someone assigns an agent to an issue or PR | `you are assigned to issue <repo>#<n>: <title> (<url>)`, key `forge:<repo>#<n>:assignment` |
|
|
| `unassigned` | someone takes an agent off an issue or PR | the same key rewritten: `you are not assigned to issue <repo>#<n> anymore: <title> (<url>)` |
|
|
| `closed` | someone closes or merges the issue or PR | the router clears every router todo it queued for it, on every agent that holds one |
|
|
|
|
The todos are subsystem `router` in the agent's todo list. A todo under the
|
|
same key replaces the one before, so an issue gives an agent one assignment
|
|
todo however often it changes hands. A new event reopens a todo the agent
|
|
marked done.
|
|
|
|
Only agents get todos. An assignee with no agent of that name gets nothing.
|
|
An agent assigned before it first reports its status gets its todo on the
|
|
issue's next assign event.
|
|
|
|
Forgejo's assign events carry the assignees after the change, not which one
|
|
changed. The router keeps the assignees it saw on the last assign event per
|
|
issue and takes the difference. With no earlier assign event for an issue,
|
|
every current assignee counts as newly assigned and nobody as removed: an
|
|
router notices an unassign only on an issue it has seen an assign event for.
|
|
|
|
## The path
|
|
|
|
1. swarm-controller registers two instance-wide hooks, one for the `issues`
|
|
and `issue_assign` events at `/webhook/forge/issues` and one for
|
|
`pull_request` and `pull_request_assign` at `/webhook/forge/pull-request`.
|
|
At each start it adds any of these events a registered hook lacks.
|
|
2. On each delivery the controller checks the HMAC and publishes the body,
|
|
unchanged, to the `SWARM_EVENTS` stream on
|
|
`$SWARM.events.forge.<X-Forgejo-Event-Type>`, such as
|
|
`$SWARM.events.forge.issue_assign`. The stream keeps a delivery 7 days and
|
|
drops a redelivery by its `X-Forgejo-Delivery` id. A delivery the stream did
|
|
not keep answers 503, so the forge's delivery log shows it.
|
|
3. The router drains its durable consumer, `notify-router`, in order. For each
|
|
event it runs every flow, queues the todo ops the flows emit, writes the
|
|
issue's snapshot to its `notify-router-state` bucket, then acks the event. A
|
|
failed step stops the batch: that event and the rest come back in stream
|
|
order after 30 seconds, and the stream drops the ops queued twice by their
|
|
message id.
|
|
4. Each op goes to the `ROUTER_TODOS` stream on the agent's own subject,
|
|
`$SWARM.router.todo.<agent>`. Work-queue retention: an op stays until its
|
|
agent acks it, up to 30 days and 1000 per agent.
|
|
5. The agent's harness creates its own durable consumer, `todo-<agent>`,
|
|
applies each op to its todo list and acks it. An agent that's down, or
|
|
whose harness predates this, leaves its ops waiting.
|
|
|
|
The router holds no forge token and no webhook secret, and registers no hook.
|
|
|
|
## Flows
|
|
|
|
A flow is a graph of nodes joined by edges: one `source` node naming the forge
|
|
event types it starts from, `match` nodes that pass when every condition
|
|
holds, and `sink` nodes that emit one todo op per recipient. A match node can
|
|
have more than one child, and each one runs, so one event can reach several
|
|
sinks. A flow must be a tree: one source, one parent for every other node,
|
|
every node reachable, no children under a sink.
|
|
|
|
The router ships the three flows above, each a source → match on `action` →
|
|
todo sink.
|
|
|
|
## Credentials and grants
|
|
|
|
| Principal | Grant |
|
|
| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
| `swarm-notify-router`, in its container | store: `read` on `swarm/services/<router client id>/oidc/client`. Queue: its own consumer on `SWARM_EVENTS` (create, pull, ack), publish on `$SWARM.router.todo.*`, create `ROUTER_TODOS`, its own `notify-router-state` bucket, and `STREAM.INFO` on the agent-status bucket, which lists the agents |
|
|
| swarm-controller | publish on `$SWARM.events.forge.*` |
|
|
| an agent, under its own credential | create its own consumer `todo-<agent>` in the form whose filter is `$SWARM.router.todo.<agent>`, pull from it, and ack what it delivered |
|
|
|
|
No other client may publish on `$SWARM.router.todo.*` or
|
|
`$SWARM.events.forge.*`, so each has one writer. The agent's create subject
|
|
carries the consumer's filter, and the queue server checks it against the
|
|
request, so an agent can't create a consumer that delivers another agent's
|
|
todos.
|