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
166 lines
8 KiB
TOML
166 lines
8 KiB
TOML
[package]
|
|
name = "swarm-controller"
|
|
version.workspace = true
|
|
readme = "README.md"
|
|
edition.workspace = true
|
|
|
|
[[bin]]
|
|
name = "swarm-controller"
|
|
path = "src/main.rs"
|
|
|
|
[dependencies]
|
|
anyhow.workspace = true
|
|
backon.workspace = true
|
|
# `kv` (which pulls `jetstream`) on top of the workspace's feature set: the
|
|
# queue is this daemon's *store*, not just its transport - a hive's last
|
|
# status snapshot is read out of a JetStream KV bucket. Declared here rather
|
|
# than in the workspace entry so the auth-callout responder, which speaks
|
|
# neither, does not claim to need them.
|
|
async-nats = { workspace = true, features = ["kv"] }
|
|
axum.workspace = true
|
|
# swarm-controller's own forge client (`forge.rs`) — self-contained,
|
|
# deliberately not sharing code with `hive-c0re::forge` across the crate
|
|
# boundary (see #3306's design discussion: forcing that split now, over a
|
|
# few idempotent CRUD-ish calls, is premature plumbing).
|
|
forgejo-api.workspace = true
|
|
# `forge.rs` encodes file content for its `repo_change_files` calls and
|
|
# decodes blobs it reads: forgejo's content and blob APIs carry base64,
|
|
# never raw bytes.
|
|
base64.workspace = true
|
|
# `message_stream.rs` renders the history cutoff in the spelling of a row's `ts`.
|
|
chrono.workspace = true
|
|
# `agent_identity.rs` mints the per-agent queue secret, the one value in this
|
|
# tree this daemon invents rather than receives. Straight from the kernel's
|
|
# CSPRNG — see the workspace entry for why this and not `rand`.
|
|
getrandom.workspace = true
|
|
futures-util.workspace = true
|
|
# RFC 9457 `application/problem+json` error bodies. Same version + `axum`
|
|
# feature as hive-c0re: the two daemons answer the same operator UIs, so a
|
|
# reader that handles one's failures has to handle the other's.
|
|
problem_details = { version = "0.9.0", features = ["axum"] }
|
|
# The graph itself, held directly rather than behind a c0re-style wrapper
|
|
# module — that layering (`hive-c0re::job_queue`) is partially legacy (predates
|
|
# `hive-jobq`'s extraction into its own crate) and this daemon does not need it
|
|
# repeated. Driven by `hive_jobq::scheduler::Scheduler` (`spawn_jobq_worker`),
|
|
# same shape `hive-c0re/src/job_queue/scheduler.rs` uses over its own graph.
|
|
hive-jobq.workspace = true
|
|
hive-jobq-wire.workspace = true
|
|
hive-log.workspace = true
|
|
# The jobq-rollup OTEL exporter, wired up in `main` via
|
|
# `hive_jobq_metrics::spawn_exporter` — moved to its own crate (rather than
|
|
# living here as `jobq_metrics.rs`) specifically so a future second caller
|
|
# (e.g. hive-c0re, for its own per-hive job graph) doesn't have to depend on
|
|
# this whole binary to reuse it.
|
|
hive-jobq-metrics.workspace = true
|
|
# Direct OTEL SDK use in `vcs_metrics.rs` — sync counters recorded off
|
|
# webhook deliveries, a different shape from `hive-jobq-metrics`'s
|
|
# observable-gauge rollup, so it isn't a fit for that crate's API and lives
|
|
# here instead. Same three crates that pairing already pulls in transitively,
|
|
# named directly since this module builds its own `SdkMeterProvider`.
|
|
opentelemetry.workspace = true
|
|
opentelemetry_sdk.workspace = true
|
|
opentelemetry-otlp.workspace = true
|
|
# The forge webhook HMAC (`webhook.rs`). Kept in this crate rather than
|
|
# shared with hive-c0re's equivalent: c0re's copy is scheduled to be deleted
|
|
# with its webhook routes once registration moves here, so the second holder
|
|
# is departing, not arriving — see that module's docs.
|
|
hmac.workspace = true
|
|
sha2.workspace = true
|
|
# `forge/pull_diff.rs` diffs a PR's changed files itself, at a context width
|
|
# the forge's own `.diff` does not offer.
|
|
similar.workspace = true
|
|
# The `Permission` names `/api/agents/{name}/permissions` accepts, the same
|
|
# enum the hive enforces.
|
|
# The send and turn row markers `message_stream.rs` reads a message back out
|
|
# of.
|
|
# Validates `POST /api/agents`' `name` before it becomes `agent`/`repo`
|
|
# everywhere downstream — see `create_agent`'s doc comment for why this is
|
|
# defense-in-depth, not the only gate (per an argus review finding).
|
|
hive-types.workspace = true
|
|
# `auth`'s bridge client — same crate the bridge itself uses to define the
|
|
# request/response shape, so the two ends cannot drift. `forge.rs` also
|
|
# uses this directly for `StatusCode` in its error-classification helpers.
|
|
#
|
|
# `blocking` on top of the workspace default: `vcs_metrics`'s OTLP exporter
|
|
# runs on a thread with no tokio reactor (see that module's doc), so the
|
|
# `reqwest::blocking::Client` it hands to `AuthenticatedHttpClient` has to
|
|
# come from the blocking half of this crate, not the async one every other
|
|
# consumer here uses.
|
|
reqwest = { workspace = true, features = ["blocking"] }
|
|
serde.workspace = true
|
|
serde_json.workspace = true
|
|
serde_norway = "0.9.42"
|
|
strum.workspace = true
|
|
swarm-authelia-bridge-sock.workspace = true
|
|
# `schema`: the forge and schedule routes describe these bodies in the spec.
|
|
swarm-controller-api = { workspace = true, features = ["schema"] }
|
|
# The queue connect (token mint + auth callback + reconnect) is shared with
|
|
# every other participant - a hive publishing its own status runs the same
|
|
# code with a different client id. Two copies of credential handling is one
|
|
# token-refresh fix that has to be found twice.
|
|
#
|
|
# `kv` for the same reason one level in: the status bucket's name and
|
|
# creation config are shared with the hive that writes it, so this end does
|
|
# not get to declare them privately.
|
|
#
|
|
# `subagent-term`: this daemon creates every agent's subagent stream, with the
|
|
# config the crate spells.
|
|
# `agent-term`: likewise every agent's terminal stream, plus the consumer it
|
|
# replays that stream through.
|
|
# `forge-events`: the stream it keeps verified forge deliveries in, for the
|
|
# notification router.
|
|
swarm-queue-client = { workspace = true, features = [
|
|
"kv",
|
|
"agent-permissions",
|
|
"subagent-term",
|
|
"agent-term",
|
|
"forge-events",
|
|
] }
|
|
# `matrix_account.rs` writes the credential this daemon's route accepts. Same
|
|
# crate the hive reads it back with, which is the point: the path, the field
|
|
# name and the object's shape are agreements between the two ends, and a
|
|
# second spelling here would be a store this hive could not read.
|
|
swarm-secret-client.workspace = true
|
|
# The builder, claude login and review classifier subjects, requests and
|
|
# replies it sends. `schema`: the review classifier try-out API serves the
|
|
# same shapes, so they derive `ToSchema`.
|
|
swarm-wire = { workspace = true, features = ["schema"] }
|
|
# The appservice calls `matrix_account::agent_token` mints agents' accounts
|
|
# with — shared with `swarm-matrix-ctl`, which pins the same device id.
|
|
swarm-matrix-client.workspace = true
|
|
# `otel_http_client.rs`'s `AuthenticatedHttpClient` — an
|
|
# `opentelemetry_http::HttpClient` impl authenticated with this crate's own
|
|
# `swarm-queue-client` identity. Lives in this crate rather than
|
|
# `swarm-queue-client` (mara's own call during review) since this daemon is
|
|
# its only caller; see that module's doc for the full rationale. `opentelemetry-http`
|
|
# is also needed directly (not just transitively) so `main.rs` can name
|
|
# `Box<dyn opentelemetry_http::HttpClient>` when handing
|
|
# `vcs_metrics::authenticated_http_client()`'s output to
|
|
# `hive_jobq_metrics::spawn_exporter`.
|
|
opentelemetry-http.workspace = true
|
|
async-trait.workspace = true
|
|
bytes.workspace = true
|
|
http.workspace = true
|
|
tokio.workspace = true
|
|
tracing.workspace = true
|
|
url.workspace = true
|
|
utoipa.workspace = true
|
|
utoipa-axum.workspace = true
|
|
# `agent_renewal.rs` reads an agent certificate's validity window.
|
|
x509-cert.workspace = true
|
|
|
|
# `test-util` for `#[tokio::test(start_paused = true)]`: `agent_renewal`'s
|
|
# tests wait out its five-minute bound on paused time.
|
|
[dev-dependencies]
|
|
tokio = { workspace = true, features = ["test-util"] }
|
|
# A fresh directory per test `nats-server`, for its store and ports file.
|
|
tempfile = "3"
|
|
# `ServiceExt::oneshot`, to drive a route through the real router in tests.
|
|
tower = { version = "0.5", features = ["util"] }
|
|
# A layer that captures `review_classifier::runs`'s log event in a test.
|
|
tracing-subscriber.workspace = true
|
|
# `operator_profiles::read`, the agent's read, run against what `sync` wrote.
|
|
swarm-queue-client = { workspace = true, features = ["operator-profiles"] }
|
|
|
|
[lints]
|
|
workspace = true
|