Ports the shadow-DOM <hive-jobq-graph> custom element
(frontend/packages/shared/src/jobq-graph/) to a Preact component
(JobqGraph.js) shared by the dashboard and swarm-ui, per hyperhive#3310.
- JobqGraph.js: written with plain h() calls (no JSX) so the same file
compiles unmodified under both the dashboard's text-loader CSS config
and swarm-ui's JSX config. Exports `JobqGraph` for JSX use and
`mountJobqGraph(container, props)` for the dashboard's non-JSX
imperative mount, returning a `{refresh(), update()}` handle matching
the old custom element's public surface. Same rendering contract as
before: indented state tree, payload.label verbatim, payload.data as
a generic key/value list, "waits on: <label>" text for Node-kind deps,
per-state filter checkboxes, optional cancel button.
- jobq-graph.css: light-DOM adaptation of the old shadow-scoped
stylesheet (:host -> .jg-root, otherwise unchanged).
- dashboard/src/builds.js: local mountJobqGraph() renamed to
mountRebuildQueue() to avoid colliding with the newly-imported shared
mountJobqGraph; cancel handling is now a plain onCancel callback
instead of a DOM CustomEvent listener (no shadow boundary to cross
anymore).
- dashboard + shared package.json: added preact as a dependency (matches
swarm-ui's existing pin, 10.29.8) - the dashboard was a vanilla-JS MPA
with no Preact/JSX pipeline before this.
- Removed the old hive-jobq-graph.js/.css entirely (confirmed via grep
it had exactly one consumer, dashboard/src/builds.js, so this is a
clean swap, not parallel maintenance of two implementations).
- Updated stale doc-comment references to the old element name in
builds.html, tabs.js, swarm.js, docs/web-ui/dashboard.md, and
hive-c0re/src/job_queue/mod.rs.
Verified: npm run build (whole frontend workspace) and npm run
typecheck (swarm-ui) both clean; cargo build/clippy/test -p hive-c0re
all clean (331 tests, 0 failures); headless-chromium screenshot of
/builds.html against a mock GET /api/jobq/graph payload confirms full
visual/behavioral parity with the old custom element (tree, filter
checkboxes, cancel buttons, error text, waits-on line, data list, live
build log panel).
This covers the dashboard-replacement half of hyperhive#3310 only. The
swarm-ui half (rendering the CreateAgent DAG on the agent-creation page)
is downstream of hyperhive#3306/#3124 landing - no swarm-ui page exists
yet to mount it in.
Same review catch as the crate side: these two format the queue client's
own error with `{:#}`, and thiserror's Display ignores the alternate flag,
so the source was silently dropped. The banners read "swarm status
publishing is off: swarm queue is half-configured" with no list of missing
variables, and "...: connecting to the swarm queue at <url>" with no nats
error saying why.
These are the two worst places to lose it. `set_boot_warning` is for a
one-shot startup step with no retry: the banner leaks until the process
restarts, so it is the operator's whole account of what went wrong.
The controller reads per-hive status out of a JetStream KV bucket and
nothing was writing one, so every hive rendered `never_reported`. This is
the half that makes the read path mean anything.
A hive offers; the controller never reaches down to collect. The gateway
has gone down in a way where every recovery channel ran through the one
broken thing, so a status path that depended on the controller would go
dark exactly when it is needed to diagnose the controller's own network.
What it publishes is what the hive already says about itself —
`warnings::readiness()`, the same value `/health/ready` serves. Nothing
here stamps a time: freshness is derived by the reader from when the value
landed, so a hive cannot make itself look fresher than it is, and a hive
with a wrong clock skews only its own payload.
The key is this hive's `hiveName`, which `swarm.nix` already asserts is a
key of `swarm.hives` — so a hive that evaluates at all publishes under a
name the roster knows, rather than by convention.
Publish first, then wait: a hive that has just come up is the one whose
status someone is looking at, and sleeping first would make every restart
read stale for a full interval. The interval is one decision with the
controller's staleness threshold, not two — a ratio of 2 means one lost
publish still reads fresh and two consecutive misses read stale.
Failures go to the dashboard banner through SweepHealth, debounced, at
`warn` and deliberately not `crit`: `crit` is what makes this hive report
itself degraded, and a hive that cannot reach the queue is not unhealthy —
the swarm's view of it is. Publishing `degraded` because the publish
failed would be both false and self-erasing on the next tick.
`get_health_ready` computed "degraded iff any warning is crit" inline and
wrapped it in a private `ReadyBody`. The swarm status publisher needs the
same verdict, and the warnings module's own doc already states why it must
not compute its own: two systems independently deciding what counts as
unhealthy is how they end up disagreeing.
The disagreement would also be silent. Each side would look internally
consistent, and the day a second degraded condition is added to one of
them, the dashboard and the swarm view would report different things about
the same host with nothing to flag it.
`warnings::readiness()` is now the single producer and `Readiness` the
single type. `ReadyBody` is deleted rather than made public: the endpoint
keeps the part that genuinely is its own, the mapping onto an HTTP status
code, and serves the shared document as its body.
Mara wanted the underlying plumbing gone too, not just the dashboard
display. Traced every consumer before cutting:
- certFingerprint (services.hyperhive.swarm.hives.<name>.certFingerprint):
removed the nix option entirely. Its only consumer was the dashboard
code removed in the previous commits.
- HYPERHIVE_PEERS: removed entirely — the env var itself, the whole
block that built it in hive-c0re/environment.nix, and its entry in
meta.rs's FORWARDED_VARS (which forwarded it into every agent
container). Turned out to have zero real consumers, not just one:
the docs claimed hive-agent::identity::peers() read it for qualified
agent labels, but no such function exists — identity.rs only
qualifies THIS agent's own label with HYPERHIVE_HIVE_DOMAIN, nothing
peer-list-related. Grepped the whole hive-agent crate to confirm
before removing.
services.hyperhive.swarm.peerHives (the nix option HYPERHIVE_PEERS was
built from) is untouched — swarm-wireguard.nix reads it directly for
the wg-hive mesh, a real and unrelated consumer.
Verified: cargo build/clippy/test -p hive-c0re -p swarm-controller all
clean (needed nix develop -c per the usual -lsqlite3 gap), all touched
nix files pass nix-instantiate --parse, and a throwaway nixosSystem
eval confirms the wireguard mesh still configures a peer's
wireguardAddress into wg-hive correctly with certFingerprint gone.
Removes the per-hive dashboard's "peer hives" display support:
`peer_hives` field on StateSnapshot, the `PeerHiveView` struct,
`parse_peer_hives()`, and `validate_cert_fingerprint()`. That surface
moved to swarm-ui's own hive roster page — no longer needed at the
hive level.
HYPERHIVE_PEERS itself is untouched: hive-agent::identity::peers()
still reads it for qualified agent labels, and the nix module still
forwards it to agent containers. Only this crate's dashboard-only
consumption is gone.
Verified: cargo build/clippy/test -p hive-c0re clean, grepped the
whole tree for stray peer_hives/PeerHiveView/parse_peer_hives
references after the removal — none left.
mara: comments to code ratio too high. It was 16 comment lines for one
line of feature.
Kept only the fact that stops the feature being trimmed away again - it
is not a transport, it is in `default`, so `default-features = false`
drops it - and, in hive-metric, the one reason its declaration
deliberately differs. Why an HTTP 200 is the failure that matters, and
what a stderr-reporting CLI would cost, are the PR's and the follow-up
issue's job, not the manifest's.
`default-features = false` on opentelemetry-otlp was written to trim
transports and dropped `internal-logs` with them, so every otel_warn! and
otel_debug! inside that crate compiled to nothing. The failure that
matters is the one which returns HTTP 200: the collector accepts the
request and rejects the data points, and HttpMetricsClient.PartialSuccess
is the only place the rejection count and the collector's reason are ever
surfaced.
The feature is per-crate, not per-workspace: the macros are exported by
the opentelemetry API crate but their cfg and CARGO_PKG_NAME resolve in
the calling crate, so the API crate and the SDK had internal logs on
while the exporter did not.
hive-metric keeps the identical declaration on purpose - it installs no
tracing subscriber, so the feature would be inert there and would only
mislead. Both files now record which side of that they are on, and the
stale "same features as hive-metric" comment is corrected.
Not sufficient on its own: a hard export failure is logged by the SDK at
debug on the compiled timer path, so it stays below the info filter.
That needs a level rather than a feature and is left to review.
A bare `git pull --ff-only` merges whatever `branch.<current>.merge`
lists. A clone carrying more than one such entry aborts with "Cannot
fast-forward to multiple branches", which takes /knowledge out for every
agent on the hive - and the daemon neither writes that config nor can
see it, so the call worked only for as long as it happened to stay
clean.
Reproduced against two throwaway bare repos: duplicate merge entries
give that exact fatal, exit 128. A detached HEAD gives a different
error, and a missing tracking config does not reproduce at all - the
clone of an empty repo does write the tracking entry, so my first
explanation was wrong.
Naming origin and main makes the pull independent of local branch
config: a stray entry degrades to "the pull did not pick it up" instead
of breaking the shared mount. Does not explain how a second entry
appeared; nothing in this tree writes branch config.
Takes the crate from 26 rustdoc warnings to 1, on top of the ten in the
previous commit.
argus's review findings:
- agent_sockets.rs: [`write`] was still ambiguous (function vs macro).
The previous change narrowed the qualifier and left the ambiguity;
[`write()`] is what resolves it.
- forge/users.rs <hex> and stats/container_stats.rs <name>: unclosed
HTML tags in prose, now backticked.
The rest of the crate, so the count actually reaches zero:
- job_queue/mod.rs: Queue::graph_snapshot -> JobQueue::graph_snapshot
(there is no Queue type), and super::scheduler -> scheduler (mod.rs
*is* job_queue, so super:: pointed outside it)
- job_queue/resource.rs: NodeKind -> super::model::NodeKind
- matrix.rs: password_path(name) -> password_path; and
forge::provision_user_token -> crate::forge::provision_user_token.
Note the path has no `users` segment: forge/mod.rs declares `mod
users` private and re-exports it, so the canonical path comes from the
re-export rather than the directory tree.
- socket_server/lifecycle_handlers.rs: InfraContainer ->
hive_priv_sock::InfraContainer
- stats/otel_metrics.rs: crate::meta::otel_config is a private fn no
path can name from another module, so it becomes prose
- main.rs: redundant explicit link target dropped
coordinator.rs:405 (CrashWatchGuard) is deliberately untouched: #3244
deletes that doc block, so fixing it here would conflict with an open PR
and repair a symbol that is about to stop existing.
Remove or fix broken documentation links that accumulate silently:
- container_view.rs: HiveEnv reference
- forge/mod.rs: READY_TIMEOUT and webhook handler links
- workers/knowledge.rs: webhook handler link
- job_queue/model.rs: Claim::deps and WireNode::data references
- stats/hive_stats.rs: read_skill_breakdown reference
- stores/audit_log.rs: global() reference
- workers/agent_sockets.rs: ambiguous agent_sockets::write reference
- coordinator.rs: systemd.services.<harness> formatting
- resource_limits.rs: ambiguous write/read references
Some broken links were to deleted functions/types; these are replaced
with prose descriptions. Others referenced items outside this crate or
were private; these are replaced with plain text references or qualified
paths as appropriate.
Fixes: #3245
argus caught these on review. `recent_transient`'s doc still pointed at
NO_NODE_LABEL, and `running_transients`' still described destroy as
having no queue node behind it and linked suppress_crash_watch -- all
three deleted in the parent commit, so the first two were broken
intra-doc links and the third was simply false.
Migration is now the only operation in that sentence.
Destroy was a straight-line async fn with no queue node behind it, so
nothing in the graph could answer "is this container going down on
purpose?". That gap is why an imperative crash-watch suppression guard
existed: an RAII handle held for the operation's duration, a second way
to say what every other lifecycle op already says through its node.
Reuse the existing Stop node rather than teaching a new node to stop
things:
Stop -> DestroyContainer -> (PurgeState) -> DestroyBookkeeping
Stop already declares takes_container_down honestly, so the suppression
is now derived from the graph like every other op's. It also turns the
precondition into an edge: DestroyContainer runs only under a completed
Stop, so it operates on an already-stopped container and carries
takes_container_down = false permanently. A container still alive at
that point is a real bug and stays loud instead of being absorbed by a
flag -- which matters because a wrong true silently swallows a crash
while a wrong false only costs a spurious event.
Removes suppress_crash_watch, CrashWatchSuppression, crash_suppressed,
crash_watch_suppressed and NO_NODE_LABEL. The migration call sites went
with the obsolete startup migrations, so destroy was the last caller and
intent now has exactly one home.
destroy() becomes a submit-and-return, matching every sibling endpoint
(rebuild, kill, restart, start, pause, resume) -- it was the only
lifecycle op that awaited its work. The container rescan moves into the
bookkeeping tail, so ContainerRemoved now arrives after the 200 rather
than before it.
Also drops an orphaned doc-comment in coordinator.rs: two stacked blocks
where only the second described crash_suppressed, the first documenting
a field that no longer exists. Removing the field would have re-pointed
it at recent_transient.
/agents/<name>/config bound the working clone a config change is staged
in, so an agent could see a proposal that was never approved -- a config
that does not govern its container. Both objects already exist; this
repoints the bind at the deployed one.
Both mounts (own + child) now resolve through config_bind_source() so
they cannot drift, and agent_proposed_dir's doc-comment is corrected:
it claimed to be manager-editable and bind-mounted, and neither is true.
Both said a whole-hive graceful stop costs ONE `GRACEFUL_STOP_TIMEOUT`
in total because drains overlap. That is only true for a power op. In a
rebuild subtree the brace holds the build slot across the whole subtree,
drain included, so the boot sweep's per-agent drains serialise and the
sweep costs one timeout per wave of `buildSlots`.
Deleted rather than corrected. The right cost statement depends on an
operator knob and belongs in docs/coordinator.md if it belongs anywhere;
a comment that has to hedge about a config value is the kind that goes
stale silently. A comment saying nothing beats one that lies.
graph_snapshot previously filtered which whole roots got projected
based on the root node's own state, so a group root that was still
Running but had already-Done internal steps couldn't be filtered
down to just its live nodes, and a filtered-out root hid its entire
subtree even when a descendant still matched.
Apply the states filter after GraphWire::wire_snapshot instead, over
every node in the flattened tree, not just roots. The jobq-graph
client already handles an orphaned node (parent filtered out) by
promoting it to a rendered root, so this is safe on the client side
with no changes needed there.
Fixes hyperhive#3210
The parent's copy is for reading a child's config; a change to it is a PR
on the child's repo, made from a clone and merged after review. A
writable mount is a second path to the same file that skips that review,
which makes the boundary a convention rather than a permission.
Confirmed with ruth before flipping: it clones from the forge and opens a
PR, including for a brand-new child's first config.
The prose was the larger half. docs/approvals.md did not merely describe
the old mount, it *instructed* agents to use it ("can therefore edit,
commit, and submit changes for any of its direct children directly inside
its container"), and the doc comment in host_config.rs asserted a
dependency that never existed: the InitConfig seed runs as hive-c0re
against the host path, and read_only on a bind constrains writers inside
the container only. That comment is what produced issue #3206, now closed
as invalid.
`agents.conf` and `gateway.htpasswd` move from /var/lib/hyperhive/gateway
to /var/lib/hive-gateway/conf, alongside the `tls/` the gateway already
kept there.
nginx reads both as an unprivileged user. Under c0re's state dir it could
only reach them by traversing a directory systemd re-declares `0750
hive-core` on every c0re start — so nginx was given `SupplementaryGroups
= [ "hive-core" ]`, which also handed it read access to everything else
group-readable in that tree. The tokens are individually 0600, but the
broker sqlite carries no explicit mode: every message between every agent
was readable by the process whose job is parsing untrusted network input.
Moving the files removes the need and the exposure together. The group is
gone, and its absence is now commented as load-bearing so it doesn't come
back as a fix for a symptom it would recreate.
Also drops this module's `/var/lib/hyperhive` tmpfiles rule. It declared
`0755 root root` and could never win against `StateDirectoryMode`, and a
losing declaration still reads as a guarantee — that is what sent the
first diagnosis of the outage looking for who had changed the mode.
Ordering is unchanged and still the thing that makes a fresh boot work:
tmpfiles runs before services and seeds both files empty-but-valid, nginx
names them (an `include` of a missing file is fatal, not empty), and
content arrives when c0re writes and reloads — which it does on every
topology change, so a boot against the empty seed resolves itself.
Folds in the mode fix: `write` now sets 0644 on the tmp file before the
rename, because a rename carries the source's mode and discards the
destination's, and the tmpfiles rule that declares 0644 is
create-if-absent so it never re-applies.
Per review: docs represent current state. Every "used to" / "no longer"
clause this branch introduced is gone — including the History section in
network.md, which was a whole subsection about a sync mechanism that
doesn't exist.
Where the removed clause was carrying a real constraint, the constraint
stays and is stated in the present tense instead of as a delta: nothing
narrows what the gateway's nginx can reach except the directory
permissions in front of a socket, and nothing bounds `ReloadGatewayNginx`
except the hard-coded unit name. Those read as rules now rather than as
the story of how they came to be rules.
The gateway's nginx + dnsmasq no longer run in their own nspawn container.
`nix/host-modules/hive-gateway/default.nix` loses the
`containers.hive-gateway` wrapper and everything that existed only to punch
holes in it: `privateNetwork = false`, `CAP_NET_ADMIN`, five bind mounts,
its own `stateVersion`, `networking.firewall.enable = false`,
`networking.resolvconf.enable = false`, and the `hive-gateway-resolv`
path+service pair. 465 -> 303 lines.
The container never bought isolation here. It shared the host netns by
necessity — nginx binds the host's :80/:443, dnsmasq answers on the bridge —
so each of those settings was undoing a boundary the gateway could not
afford in the first place.
Four things made it more than a deletion, none of them visible in the nix
diff:
- The self-signed cert service also imports the hive CA leaf, so removing it
with the container would have left nginx naming a missing cert file, which
it refuses to load at all.
- The nginx reload is a hive-priv verb. It still needs root, but no longer
for the reason its doc gave, and `--machine=` was both transport and
scope — so the unit name is now hard-coded in the helper as the
containment.
- The lifecycle verb named a container that stops existing.
- `journalctl -M hive-gateway` had no machine to enter.
Per the operator's ruling, the operator verb keeps working and agents lose
it. `InfraContainer` answered three questions that used to share an answer;
it now splits into `name()` (identity), `target()` (Container vs HostUnit),
`service_unit()` (the systemd unit), and `agent_restartable()`, which the
MCP restart path checks before the capability so the refusal cannot read as
"ask for infra_admin". `SIBLING_CONTAINERS` drops the gateway — it gates the
requests that name a container as a string — while `FromStr` still accepts
it, because that answers what a name is, not who may act on it. The
dashboard's gateway journal reads host journald filtered to `nginx.service`.
Prose was corrected where it only named a location, and re-argued where the
container was doing security work: a `0666` per-agent socket was safe
because only the gateway container had the directory bind-mounted. There is
no mount now, so the directory permissions are the whole of the access
control — the constraint holds, its mechanism doesn't.
Gate: nix fmt / clippy --all-targets -D warnings / cargo test all clean (710
tests); hivectl-cli.md regenerated from the clap tree. The nix eval was run
in both TLS shapes at this commit: every delta in the rendered
virtualHosts is one of the three intended path moves, dnsmasq settings are
byte-identical, and the absence probe flips true -> false with bindMounts
emptied.
is_running collapsed every non-active state into false, so a container that
exhausted its bounded restarts read as plain "down" -- indistinguishable
from one an operator stopped deliberately. Bounding the restarts made that
gap sharper: a slow-failing agent used to grind on visibly, now it can stop
quietly.
Adds UnitState + unit_state() beside is_running rather than widening it.
is_running has ~8 call sites and nearly all are reconcile/power logic asking
"is it up? if not, start it" -- a question with two answers. Only the view
builder needs more, and it gets both facts from one systemctl call, since
is-active prints the state when not passed --quiet.
Surfaces as a flat failed flag on ContainerView and AgentStatusRow, matching
the shape those types already document: independent, orthogonally-observed
facts rather than a state machine. serde(default) keeps it order-independent
with the frontend half.
No behaviour change: nothing acts on the flag, per the ruling.
Closes the #3110 split — lib.rs is now just the crate doc comment and
the pub mod list.
journal.rs's new doc comment fixes a pre-existing bug: the old
JournalPriority doc text in lib.rs was actually half Capability's doc
(a leftover from an earlier reorder that moved the code but not the
comment above it).
GET /api/jobq/graph gains a states query param (comma-separated
hive_jobq::State names): narrows the served root groups to the named
states, keeping a group whole (filtering by a root's own state, which
is already its subtree's rolled-up answer). Absent, empty, or fully
unrecognised is the identity filter, matching prior behaviour.
hive-jobq-graph.js gains a row of per-state checkboxes above the tree,
re-fetching the endpoint with the selection on toggle. Default
selection hides Done and Skipped.
Server-side filtering (not client-side hiding) so hive-jobq-graph-update's
node list, and everything downstream of it in builds.js (count pill,
live-log panel), only ever sees what's actually shown.
hive_sh4re::assets::branding_svg() resolved a server-side default
icon at runtime from HIVE_ASSETS_DIR — the only consumer was
serve_icon(), which fell back to it whenever the agent had no
`hyperhive.icon` override. Removed both the fallback and the
function: serve_icon() now 404s when /etc/hyperhive/icon.svg is
absent, and the per-agent web UI (app.js) picks up the existing
dashboard swarm.js pattern — swap the <img> src to the
frontend-bundled /favicon.svg on load failure, guarded against
looping if the fallback itself 404s.
Updated the doc/comment claims that said the server always returns
an image (docs/web-ui/agent.md, nix/agent-modules/default.nix, the
hive-c0re/forge/users.rs comment referencing the old shared-asset
set). forge-avatar-sync and the matrix avatar sync are unaffected —
both are gated on hyperhive.icon != null and never depended on the
removed fallback.
bind_child_agent_dirs looped over state, harness and config alike and
mounted all three read-write, while the doc comment above it defended
only state. The rationale covered one dir, the loop covered three — the
uniformity is what erased the fact that the three have three different
answers.
harness holds the child's own runtime material (bash-tasks, the
turn-stats and event sqlite dbs) and nothing argues for a parent
touching it. The only other reader is stats::hive_stats, which reads the
host path directly and needs no mount into anyone.
config stays read-write here on purpose. The ruling is that it becomes
read-only, but request_init_config still has the manager seed a new
child's config in place, so flipping the mount before relocating that
step breaks agent creation hive-wide. That ordering now lives in the doc
comment, where someone about to finish the job in one line will see it.
docs/persistence.md justified all three dirs as RW; it now states the
boundary as three answers and names the right source file.
core_avatar_png() and config_org_avatar_png() were hive-sh4re
functions used only by hive-c0re::forge::users. Move them there as
private path-resolution helpers (core_avatar_png_path,
config_org_avatar_png_path), following the existing
std::env::var(...).expect(...) required-env-var style used elsewhere
in hive-c0re.
The org avatar is now independently configurable:
services.hyperhive.c0re.orgAvatarPng (nullable path, default null)
lets an operator override just the agent-configs org avatar PNG
without replacing the whole assets package. Wired via a new
HIVE_ORG_AVATAR_PNG env var that falls back to the bundled PNG when
the option is unset. The core avatar stays under HIVE_ASSETS_DIR,
unchanged.
argus's review note: current_room_state collapsed absent-state,
transport failure and an unparseable body into one None, so a
systematically failing GET was unobservable.
Only one of the three actually hides. A transport error takes the PUT
down with it one line later, and a 404 is the expected first-setup
case — both stay at debug. A non-404 HTTP failure is the silent one:
the read is broken while the write still succeeds, so the guard
switches off and the sweep resumes emitting with nothing to show for
it. That case, and only that case, warns.
Keeping the warn narrow is the point: one that also fired on every
expected 404 would train the reader to skip the line.
set_room_state PUT unconditionally and its doc called that idempotent.
It is, one level too high: a PUT of identical content is a no-op on the
room's STATE and the homeserver still appends an event to the TIMELINE.
Downstream an event is unread activity, then a todo, then a turn — for
every agent in the room. The provisioning sweep re-wiring the hive
Space's child link therefore woke the whole hive on the sweep's cadence,
forever, for a link that never changed (~1801s between events, measured
across eleven consecutive intervals).
Read the current content first and return early when it matches. The
lookup fails open — an unreadable state means write, because the
re-apply exists to repair a missing link and "don't know" must not be
treated as "fine". Only the steady state goes quiet.
One attrset describing every hive in the swarm including this one,
identical on every host, with hiveName selecting which entry is us.
"My peers" is derived (swarm.peerHives) rather than declared.
Every field in the old per-host peer list was intrinsic to the hive it
described, never to the pair -- so the list was a directory each host
kept its own copy of. Beyond the deduplication it removes a bug class:
two hosts could hold different endpoints for the same third hive with
nothing to detect the disagreement.
Drops the per-hive caCert. Trust inside a swarm derives from the swarm
root, which every hive chains to. What that genuinely removes is
trusting a hive whose root this swarm does not own -- a cross-swarm
problem that wants a mechanism of its own, not a field that happened to
work.
The matrix container's certificateFiles block goes with it and could
NOT be migrated: that list is read at build time and the swarm root is
a runtime file (its key must never enter the store), so there is no
build-time name to put there. caCert being a nix path was precisely
what made it the build-time distribution channel. Agents are unaffected
-- hive-tls folds the root into the hive trust bundle and the meta
renderer embeds that one file. Tracked separately.
Migration is an assertion plus warnings, not a rename: hives is peers
union {self}, and the set gains a member no existing config has written
down. A rename migrates a name and a default can re-root a meaning;
neither can conjure a new member. The warning explains, the self-entry
assertion stops the build.
Cross-hive trust was O(n²) hand-pinning: every hive had to name every
peer's CA. A swarm root makes it O(1) — trust the root once and every
present and future peer validates.
The root is generated by a new `swarm-ca` unit on a single-host swarm
and operator-provided otherwise; `swarm.ca.autoConfigure` picks between
them and derives its default from `swarm.peers` being empty, so "all on
one host" is read off the deployment rather than remembered. Both modes
produce the same artifacts in the same places, so splitting hosts later
is moving the service dirs, not switching code paths. The root key never
enters the nix store, and the root is never regenerated automatically —
replacing it invalidates every peer at once.
Each hive CA carries `nameConstraints` pinned to that hive's domain, so
a leaked hive CA can only mint names inside its own subdomain, enforced
by verifiers rather than by convention.
`ca.pem` was serving as both the issuer and the anchor consumers trust;
those are the same file only while it is self-signed. openssl will not
terminate a chain at a trusted cert that isn't self-signed (rustls and
Go will), so the promotion would have broken some consumers and not
others. `hive-tls-ca` now also writes `trust-bundle.pem` — the hive CA
plus whatever it is rooted at — and every anchor consumer reads that:
agents, the CI and forge containers, and the peer-config recipe. On a
hive with no swarm root the bundle is just that CA, so nothing consuming
it needs a mode to branch on.
Every prose and error-message reference to the moved options, including
two in hive-c0re/src/matrix.rs that omitted the services. prefix and so
read as the per-agent option. An error that names an option the operator
cannot find is worse than no hint.
Two sites spelled it as a brace group, services.hyperhive.{hiveName,
swarmName}, which no anchored rewrite can handle correctly now that only
one of the two moves; both are written out separately. One of them is an
MCP tool description, so it is rendered into every agent's system prompt.
Seven places described an authoring flow that does not exist: the
manager editing and committing a child's config in the bind-mounted
config dir. mara, on the snapshot issue: "the pr workflow is the main
one, anything else is legacy … the config dir in agent container is
read only copy to see own config … changes need to go through local
clone and via forge".
Two of the seven are the request_init_config MCP tool description and
its args struct, which are rendered into the system prompt of every
agent holding the approvals tool group. A wrong tool description is not
a stale comment; it is an instruction the whole hive reads as fact,
which is why the claim kept being repeated back.
The init tool creates the repo and seeds it. Nothing else. Tailoring
the seeded template is not a separate mechanism - it is the ordinary
config-change flow, a PR reviewed like any other.
Prose only; the code already implements the PR flow (an approval's
commit_ref is a PR number). Where a doc justified the parent's
read-write mount on the child's config dir with the authoring model,
the justification is removed rather than replaced: that mount is a
defect tracked on its own issue, and it should not read as intentional
while it waits.
The last of the DAG-container removal. `tests.rs` navigated by the id
`submit` returned, so removing the container removed the tests' way of
finding what they inserted; they name the roots they assert on now, which
is the same handle production uses.
Three findings the port surfaced, each a behaviour change rather than a
test fix:
- Cancelling a rebuild's head no longer drops the job. `Reconcile`'s edge
accepts a skipped brace, and a cancel-cascade skips rather than cancels,
so the tail stays claimable. Dropping a job means cancelling every id the
insert returned.
- A directly-cancelled group root reads terminal while a spared tail still
runs; the cancel used to land on a node above it, which rolled up
Finishing instead.
- "One DAG per hive-wide op" is not expressible without a container. The
three tests asserting it now assert that every named root is top-level,
which is what makes the per-agent subgraphs concurrent.
Deletes two tests: one asserted only that two containers get distinct ids,
the other re-ran an existing case under a second name.
`Source`, `insert_group` and the stop path's `reason` string went dead with
the container and are removed with it.
The variant, its label, its agent-accessor arm and its no-op executor arm are
gone, along with the module prose describing a job as "a single container node
whose subtree is the work". A job is now just its nodes: a template declares
them and names the roots it wants back.
`dag_of` becomes `root_of`. It always wrapped the graph's `root_of` and still
returns the same thing, but the old name asserted a concept that no longer
exists — with no container, the parent chain ends at whichever root the template
declared, so the honest question is "which root owns this node", not "which DAG
is this in".
One comment kept its old wording on purpose: `visible_roots` explains that the
projection it replaced keyed on the container kind rather than selecting
structurally. That is a statement about the past and stays true; it now says
"the since-removed container kind" rather than naming a type that is not there
to look up.
`server.rs`'s five sites move to `power::{stop,start,restart}_many` and direct
template inserts. `submit_single` routes through the `*_many` builders with a
one-element slice rather than keeping a parallel single-target shape.
`templates::rebuild` and `templates::reparent` now return the guids of the roots
they declare, so a caller that has to wait on them can name them; previously
only the void-returning form existed and every caller got an empty id list.
Two comments corrected while converting, both contradicted by the code they sit
above:
* `templates::rebuild` said its tail is edged onto "(MetaSync, Prebuild,
Reconcile)" and that "Prebuild's roll-up carries the subtree" — the brace has
been the middle root since the AgentWindow change.
* the restart handler described the per-agent shape as starting with SetWanted,
while `restart_chain`'s own doc says a restart never rewrites `wanted` — that
is the difference between restart and stop/start.
Error handling is no longer swallowed: a failed insert becomes a reported error
rather than a silently-absent id.
The `*_many` entry points returned `insert_job`'s result while their closures
ended in `Vec::new()` — naming nothing, so the returned id list was always
empty. `queued_dags` would have shipped `Some([])` and hivectl's wait loop would
have had nothing to poll. Silent: it compiles, the op still runs, and no test in
isolation looks.
Each `*_chain` now returns its group root's guid and the `*_nodes` collectors
gather them, so the ids a caller gets back are the roots it can actually wait on.
`start_chain` returns *four* in the stale branch, not one: `rebuild_nodes`
chains its roots behind `SetWanted` with `after_ok` rather than nesting them
under it, so `SetWanted` rolls up only itself. Naming it alone would have
reported the start complete while the rebuild was still running — the same
under-reporting bug one level down.