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
281 lines
10 KiB
TOML
281 lines
10 KiB
TOML
[workspace]
|
|
resolver = "3"
|
|
members = [
|
|
"hive-agent",
|
|
"hive-agent-mcp",
|
|
"hive-agent-sock",
|
|
"hive-core-agent-sock",
|
|
"hive-bash-mcp",
|
|
"hive-c0re",
|
|
"hive-screen-mcp",
|
|
"hive-forge",
|
|
"hive-forge-notify",
|
|
"hive-host-sock",
|
|
"hive-jobq",
|
|
"hive-jobq-metrics",
|
|
"hive-jobq-wire",
|
|
"hive-log",
|
|
"hive-matrix-mcp",
|
|
"hive-metric",
|
|
"hive-priv",
|
|
"hive-priv-sock",
|
|
"hive-runtime",
|
|
"hive-sh4re",
|
|
"hive-sock-client",
|
|
"hive-subagent-mcp",
|
|
"hive-types",
|
|
"hivectl",
|
|
"swarm-agent-verify",
|
|
"swarm-authelia-bridge",
|
|
"swarm-builder",
|
|
"swarm-ci-metrics",
|
|
"swarm-claude-login",
|
|
"swarm-authelia-bridge-sock",
|
|
"swarm-controller",
|
|
"swarm-controller-api",
|
|
"swarm-matrix-client",
|
|
"swarm-matrix-ctl",
|
|
"swarm-nats-auth",
|
|
"swarm-notify-router",
|
|
"swarm-queue-client",
|
|
"swarm-review-classifier",
|
|
"swarm-logs",
|
|
"swarm-metrics",
|
|
"swarm-logs-auth",
|
|
"swarm-metrics-auth",
|
|
"swarm-secret-client",
|
|
"swarm-wire",
|
|
"swarmctl",
|
|
]
|
|
|
|
[workspace.package]
|
|
edition = "2024"
|
|
version = "0.1.0"
|
|
|
|
[workspace.lints.clippy]
|
|
# Pedantic is a hard error (locally + in CI): we want pedantic lints
|
|
# gated, so a toolchain bump that adds a new one reds the build until the
|
|
# code is updated rather than sliding in unnoticed. Priority -1 keeps the
|
|
# specific allows below winning over the group.
|
|
pedantic = { level = "deny", priority = -1 }
|
|
# Tolerated stylistic pedantic lints (noisy, not actionable).
|
|
missing_errors_doc = "allow"
|
|
missing_panics_doc = "allow"
|
|
module_name_repetitions = "allow"
|
|
|
|
[workspace.lints.rustdoc]
|
|
# Doc-link rot has no other discoverer: clippy does not read intra-doc
|
|
# links, `cargo test` does not, and nothing else builds docs. A `[`Foo`]`
|
|
# pointing at a renamed, moved or deleted item renders as plain text and
|
|
# misleads the next reader — worse than no link, since it names something
|
|
# and so sends them looking.
|
|
#
|
|
# Here rather than in `RUSTDOCFLAGS` on the CI check, so a plain local
|
|
# `cargo doc` fails the same way CI does. A gate you only meet in CI is a
|
|
# gate you meet too late.
|
|
broken_intra_doc_links = "deny"
|
|
private_intra_doc_links = "deny"
|
|
invalid_html_tags = "deny"
|
|
redundant_explicit_links = "deny"
|
|
bare_urls = "deny"
|
|
unescaped_backticks = "deny"
|
|
|
|
[workspace.dependencies]
|
|
anyhow = "1"
|
|
libc = "0.2"
|
|
axum = { version = "0.8", features = ["ws"] }
|
|
backon = "1"
|
|
base64 = "0.22"
|
|
chrono = { version = "0.4", default-features = false, features = [
|
|
"clock",
|
|
"serde",
|
|
"std",
|
|
] }
|
|
clap = { version = "4", features = ["derive"] }
|
|
clap_complete = "4"
|
|
enumflags2 = { version = "0.7.12", features = ["serde"] }
|
|
# The OS CSPRNG, for the one thing in this tree that generates a secret rather
|
|
# than receiving one (`swarm-controller::agent_identity`). `getrandom` rather
|
|
# than `rand`: the whole need is "fill these bytes from the kernel", and `rand`
|
|
# would add `rand_core` + `rand_chacha` to do it through a userspace generator
|
|
# this has no use for.
|
|
getrandom = "0.3"
|
|
indicatif = "0.18"
|
|
hive-sh4re = { path = "hive-sh4re" }
|
|
hive-agent-sock = { path = "hive-agent-sock" }
|
|
hive-jobq = { path = "hive-jobq" }
|
|
hive-jobq-metrics = { path = "hive-jobq-metrics" }
|
|
hive-jobq-wire = { path = "hive-jobq-wire" }
|
|
hive-log = { path = "hive-log" }
|
|
hive-core-agent-sock = { path = "hive-core-agent-sock" }
|
|
hive-claude = "0.1.4"
|
|
hive-host-sock = { path = "hive-host-sock" }
|
|
hive-priv-sock = { path = "hive-priv-sock" }
|
|
hive-runtime = { path = "hive-runtime" }
|
|
hive-sock-client = { path = "hive-sock-client" }
|
|
hive-types = { path = "hive-types" }
|
|
swarm-agent-verify = { path = "swarm-agent-verify" }
|
|
swarm-authelia-bridge-sock = { path = "swarm-authelia-bridge-sock" }
|
|
swarm-controller-api = { path = "swarm-controller-api" }
|
|
swarm-matrix-client = { path = "swarm-matrix-client" }
|
|
swarm-queue-client = { path = "swarm-queue-client" }
|
|
swarm-secret-client = { path = "swarm-secret-client" }
|
|
swarm-wire = { path = "swarm-wire" }
|
|
thiserror = "2"
|
|
vaultrs = "0.8"
|
|
# vaultrs's transport crates. Direct dependencies because one endpoint is
|
|
# defined here rather than by it; both versions must stay the ones vaultrs
|
|
# resolves, since its `exec_with_empty` takes *its* `Endpoint` trait.
|
|
rustify = "0.7"
|
|
rustify_derive = "0.5"
|
|
tower-http = { version = "0.7", features = ["fs"] }
|
|
uuid = { version = "1", features = ["v4"] }
|
|
rmcp = { version = "2", default-features = false, features = [
|
|
"server",
|
|
"macros",
|
|
"transport-io",
|
|
"transport-streamable-http-server",
|
|
] }
|
|
rusqlite = { version = "0.37" }
|
|
schemars = "1.0"
|
|
serde = { version = "1", features = ["derive"] }
|
|
serde_json = "1"
|
|
similar = "2"
|
|
# `derive`-only: `Kind`'s segment strings via `#[strum(serialize = "...")]`
|
|
# instead of a hand-written match, so the derive macro is the single source
|
|
# of truth (see `swarm-secret-client::path::Kind`).
|
|
strum = { version = "0.28.0", features = ["derive"] }
|
|
tokio = { version = "1", features = [
|
|
"fs",
|
|
"io-util",
|
|
"macros",
|
|
"net",
|
|
"process",
|
|
"rt-multi-thread",
|
|
"signal",
|
|
"sync",
|
|
"time",
|
|
] }
|
|
tokio-stream = { version = "0.1", features = ["sync"] }
|
|
tracing = "0.1"
|
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
|
# The journald sink `hive-log` installs when the process runs under a
|
|
# systemd unit. Tokio-maintained; its whole dependency set is `libc` +
|
|
# `tracing-core` + `tracing-subscriber`, since it speaks the native journal
|
|
# protocol over a `UnixDatagram` and links no C journal library.
|
|
tracing-journald = "0.3.2"
|
|
# `fs::fstat` — the safe wrapper `hive-log` compares a descriptor's device
|
|
# and inode numbers against `$JOURNAL_STREAM` with. Already in the lock as
|
|
# a transitive dependency; direct here so that comparison needs no
|
|
# hand-written `unsafe libc::fstat` call. Only the `fs` API module is asked
|
|
# for: rustix gates each one behind its own feature.
|
|
rustix = { version = "1.1.4", default-features = false, features = [
|
|
"std",
|
|
"fs",
|
|
] }
|
|
reqwest = { version = "0.13", default-features = false, features = [
|
|
# RFC 7662 introspection posts an urlencoded body; without this,
|
|
# `.form()` does not exist and the alternative is percent-encoding a
|
|
# credential by hand.
|
|
"form",
|
|
"json",
|
|
"rustls",
|
|
] }
|
|
hyper = { version = "1", features = ["client", "http1"] }
|
|
hyper-util = { version = "0.1", features = ["tokio"] }
|
|
# OTEL SDK, shared by every crate that pushes metrics (hive-c0re, hive-metric,
|
|
# swarm-controller). The blocking OTLP client is deliberate everywhere it's
|
|
# used: the metrics SDK's `PeriodicReader` drives its export from a
|
|
# background thread with no Tokio reactor, where the async client panics.
|
|
# `default-features = false` on the OTLP crate drops its own diagnostics
|
|
# ("internal-logs") unless a member opts back in — hive-c0re does, via
|
|
# `{ workspace = true, features = ["internal-logs"] }`.
|
|
opentelemetry = "0.32"
|
|
opentelemetry_sdk = { version = "0.32", features = ["metrics"] }
|
|
opentelemetry-otlp = { version = "0.32", default-features = false, features = [
|
|
"metrics",
|
|
"http-json",
|
|
"reqwest-blocking-client",
|
|
"reqwest-rustls",
|
|
] }
|
|
# The trait an OTLP exporter's HTTP transport is built on
|
|
# (`opentelemetry_otlp::WithHttpConfig::with_http_client`) — pulled in
|
|
# directly (not just transitively via `opentelemetry-otlp` above) by
|
|
# `swarm-controller`'s own `otel_http_client` module, which implements the
|
|
# trait itself rather than using the crate's own blanket `reqwest`/
|
|
# `reqwest-blocking` impls (this workspace's implementation needs to mint
|
|
# a fresh bearer token per request, which no blanket impl can do). No
|
|
# extra cargo features requested here for exactly that reason — the
|
|
# blanket impls live behind `reqwest`/`reqwest-blocking` features this
|
|
# workspace never turns on for this crate.
|
|
opentelemetry-http = "0.32"
|
|
# `async-trait`: `opentelemetry_http::HttpClient` predates stable
|
|
# `async fn` in traits and is still expressed with this macro upstream —
|
|
# implementing the trait (rather than only consuming the crate's own
|
|
# blanket impls) means writing `#[async_trait::async_trait]` on the impl
|
|
# block to match its expansion.
|
|
async-trait = "0.1"
|
|
# `bytes`/`http`: the payload and request/response types
|
|
# `opentelemetry_http::HttpClient::send_bytes` is expressed in terms of —
|
|
# needed to implement the trait, not just call something that already
|
|
# does.
|
|
bytes = "1"
|
|
http = "1"
|
|
http-body-util = "0.1"
|
|
# ⚠️ Keep at 0.11.1 or newer, and keep it on the SAME reqwest as everything
|
|
# else. 0.11.0 links reqwest 0.12 while the workspace is on 0.13, and cargo
|
|
# resolves features per (crate, VERSION): that older client got neither the
|
|
# workspace's TLS features nor the system trust store, so every https call to
|
|
# a gateway name died with `invalid peer certificate: UnknownIssuer` while
|
|
# clients built here worked (#3391). One version of reqwest in the tree is
|
|
# what keeps that class impossible rather than merely fixed.
|
|
forgejo-api = { version = "0.11.1", default-features = false, features = [
|
|
"rustls-tls",
|
|
] }
|
|
url = "2"
|
|
time = { version = "0.3", default-features = false, features = [
|
|
"formatting",
|
|
"parsing",
|
|
] }
|
|
petgraph = { version = "0.8", default-features = false, features = ["std"] }
|
|
matrix-sdk = { version = "0.18", default-features = false, features = [
|
|
"sqlite",
|
|
"markdown",
|
|
"e2e-encryption",
|
|
] }
|
|
futures-util = "0.3"
|
|
hmac = "0.13"
|
|
sha2 = "0.11"
|
|
# Constant-time comparison of a presented secret against the stored one
|
|
# (`swarm-agent-verify`). Already in the tree through the TLS stack.
|
|
subtle = "2.6"
|
|
# The NATS protocol client, for the swarm queue's auth-callout responder.
|
|
# `default-features = false` because the default set is broad - jetstream, kv,
|
|
# object-store, websockets, service - and a callout responder speaks none of
|
|
# them. What is named is the whole requirement: the server generation we
|
|
# deploy, nkey auth, and a TLS backend.
|
|
async-nats = { version = "0.50", default-features = false, features = [
|
|
"server_2_14",
|
|
"nkeys",
|
|
"ring",
|
|
] }
|
|
# Named only to install the process-wide provider (`swarm-secret-client`'s
|
|
# `install_crypto_provider`). `aws-lc-rs` is the provider reqwest already
|
|
# falls back to, so installing it leaves every HTTPS client unchanged.
|
|
rustls = { version = "0.23", default-features = false, features = [
|
|
"aws-lc-rs",
|
|
"std",
|
|
] }
|
|
data-encoding = "2"
|
|
# The nkey *format* - ed25519 + base32 + CRC16. The primitives are already in
|
|
# the tree; the format is not, and hand-rolling a key format on an auth path
|
|
# is how you get a CRC bug nobody reviews.
|
|
nkeys = "0.4"
|
|
# A TEST ORACLE, not a runtime dependency - see swarm-nats-auth's respond.rs.
|
|
nats-jwt = "0.3"
|
|
utoipa = { version = "5", features = ["axum_extras", "chrono"] }
|
|
utoipa-axum = "0.2"
|
|
# Reads an agent certificate's validity window, so `swarm-controller` can
|
|
# re-issue it at half its life. `der` and `spki` are already in the tree
|
|
# through `pkcs8`.
|
|
x509-cert = { version = "0.2", default-features = false, features = ["pem"] }
|