hyperhive/Cargo.toml
atlas a39399f037 swarm-logs: an agent's CLI for the swarm log store
An agent can reach VictoriaLogs only through the gateway, and since the
machine query route landed the way to read it has been to hand-roll a
client_credentials token request and a curl, per query. This is the CLI
that closes that: `swarm-logs query '<LogsQL>'`, matched log lines on
stdout, so the answer pipes into grep like any other command's.

Built to the plan posted on the tracker thread: own crate, own
docs/tools reference generated off the clap tree, `query` as the one
verb, and the JSON error body surfaced on a non-200 rather than
swallowed. No `tail`: streaming is a different endpoint with a different
response shape, and folding it in here would be a fatter scope than the
ask.

Minting the token is NOT implemented here — swarm-queue-client already
owns the client_credentials request, its error type and its CA handling,
and a token-endpoint fix has to be findable in one place. What this crate
adds is the agent-shaped half: the client id arrives as a *file* beside
the secret, so nothing outside nix/agent-modules/queue.nix spells
`hive-<name>-agent` twice. That is the same problem hive-agent's
swarm_queue module solves, and swarm-logs/src/auth.rs is its `decide`
restated over this binary's inputs.

⚠️ The plan named one thing to verify empirically before calling the auth
settled: whether authelia's bearer policy for the logs vhost accepts the
agent client's audience. Measured from inside a container: it does not.
The client minted a token fine but with `aud: []` and `scp: []`, asking
for the logs URL as an audience answered `invalid_target`, and presenting
the audience-less token to the gateway answered a bare 401. So
swarm-authelia.nix's agentClients gains `authelia.bearer.authz` and the
query URL as a second audience — authelia authorises a bearer token by
the URL being requested, and that URL is now one binding read by three
places rather than three spellings of one address.

The URL reaches an agent the same way its queue coordinates do: computed
on the host (a container cannot derive a gateway address), forwarded by
hive_c0re::meta into the container's option set, and consumed by a new
agent module that installs the binary *wrapped* with its coordinates —
the shape swarm-controller.nix installs swarmctl in. Gated on the queue
credential as well as on the URL: a binary that can only answer 401 is
worse than no binary, because an agent reads a 401 as "no logs", which is
the exact confusion the store's machine route was added to end.
2026-09-17 01:02:14 +02:00

226 lines
8.2 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-matrix-mcp",
"hive-metric",
"hive-priv",
"hive-priv-sock",
"hive-sh4re",
"hive-sock-client",
"hive-subagent-mcp",
"hive-types",
"hivectl",
"swarm-authelia-bridge",
"swarm-authelia-bridge-sock",
"swarm-controller",
"swarm-nats-auth",
"swarm-queue-client",
"swarm-logs",
"swarm-secret-client",
"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"] }
base64 = "0.22"
bcrypt = "0.19"
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"] }
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-core-agent-sock = { path = "hive-core-agent-sock" }
hive-claude = "0.1.1"
hive-host-sock = { path = "hive-host-sock" }
hive-priv-sock = { path = "hive-priv-sock" }
hive-sock-client = { path = "hive-sock-client" }
hive-types = { path = "hive-types" }
swarm-authelia-bridge-sock = { path = "swarm-authelia-bridge-sock" }
swarm-queue-client = { path = "swarm-queue-client" }
swarm-secret-client = { path = "swarm-secret-client" }
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"] }
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"
# 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",
] }
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"