Commit graph

2,698 commits

Author SHA1 Message Date
iris
a73216e8be fix agent-logs unit dropdown mismatch, add other hive daemons
Dashboard AGENT log tab sent unit=hive-ag3nt.service (the crate/dir
name) but the actual systemd unit is hive-agent.service, so every
fetch 400'd against the backend allow-list. Fixed the dropdown value
and, per the same issue's request, added the other per-agent daemons
(hive-mcp-http, hive-bash-daemon, hive-matrix-daemon) as selectable
units, plus hive-priv.service on the SYSTEM tab's host-daemon list.
Extended both backend allow-lists (post_journal / post_journal_host)
to match, and fixed a stale systemd.services.hive-ag3nt doc reference
in agent-hierarchy.md that had the same crate/unit-name confusion.
2026-07-15 20:41:59 +02:00
atlas
b87eac0a61 feat(#2484): unify in-DAG growth on append_subgraph (drop append_node)
append_subgraph is the multi-node/multi-agent generalisation of the
single-node append_node, so the two in-DAG-growth channels collapse to
one: the Reconcile planner now emits its mechanical Start/Stop as a
single-node append_subgraph rooted on the reconcile node (stamping
claim.agent on the NodeSpec, which append_node inherited implicitly).

Removes NodeOutput.append_nodes + its scheduler drain loop and
JobQueue::append_node. No behaviour change — a channel unification.
2026-07-15 20:23:11 +02:00
atlas
6c4ef5f798 refactor(#2427): extract shared hive-CA trust bind-mount helper
hive-ci and hive-forge both bind the runtime-generated hive CA cert
read-only and order their container@ unit after hive-tls-ca.service so
the bind source exists before nspawn sets the mount up — the same
bind-mount + ordering + rationale duplicated verbatim in two modules.

Extract that language-agnostic half into a pure helper,
nix/host-modules/lib/hive-ca-trust.nix, taking a container name and
returning { useSelfSigned, caContainerPath, bindMount, containerOrdering }.
The per-runtime consumption stays at each call site (hive-ci's additive
NODE_EXTRA_CA_CERTS, hive-forge's Go SSL_CERT_FILE concat). hive-ci folds
containerOrdering into its existing mkMerge alongside the TimeoutStartSec
bump.

The helper is a pure function, not a module: host-modules/default.nix is
an explicit aggregator (not a glob) and the docs eval imports that same
aggregator, so the lib/ file is never picked up as a module. A third
outbound-TLS-trusting container no longer means a third copy-paste.
2026-07-15 20:12:52 +02:00
atlas
edf9fd036e feat(#2453): remove DAG parent_id now that every op is one DAG
With the meta-update cascade (#2476) and startup sweep (#2450) folded
into single DAGs that grow per-agent subgraphs via append_subgraph,
nothing links parent/child DAGs anymore — parent_id is dead.

hive-c0re: drop parent_id from Dag/DagSpec (+ the DagView copy); delete
append_children and cancel_children (no callers); simplify trim_history
(no more terminal-parent-with-live-children guard — a one-big-DAG is
terminal only when its whole graph settles); drop the rebuild() parent_id
param; QueueDag returns just the polled DAG (no fan-out children to
gather). hive-sh4re: drop the DagView.parent_id wire field.

frontend: a multi-step op is one DAG now, so renderRebuildQueue drops the
childrenOf/orphans cross-DAG grouping and renders each entry flat; its
per-agent subgraphs render as nodes within the one row (split by deps).
Removed the dead rqe-child style + isChild plumbing.

Docs + the child-DAG queue tests updated/removed to match.
2026-07-15 19:50:04 +02:00
atlas
2b3130f63c feat(#2476): grow the meta-update cascade in-DAG instead of child DAGs
MetaLock's non-sweep completion now grows one rebuild subgraph per
affected agent into the same DAG (append_subgraph), replacing the
fan-out-child-DAGs + cancel_children dance. Drops NodeOutput.fanout and
scheduler's fanout_specs. meta_update DAG carries Rebuilding transient so
each cascade agent gets crash-watch suppression at Swap (the property the
old child Rebuild DAGs held via their own transient); MetaLock head needs
no lease so the pseudo-agent gets no pill.

append_children/parent_id and child-DAG tests are intentionally left for
the #2453 capstone.
2026-07-15 19:09:05 +02:00
damocles
31752d4089 fix(#2488): verify assignee change landed instead of trusting forge 200 2026-07-15 19:06:24 +02:00
damocles
001b1dae37 feat(#2479): add from/until event-id cursors to read_room 2026-07-15 18:43:31 +02:00
damocles
d90504b427 fix(#2417): accept forgejo's 'synchronized' pr-update webhook action 2026-07-15 18:38:47 +02:00
atlas
4545dd312e feat(#2450): collapse the startup sweep into one inline DAG
The boot is now ONE DAG, assembled inline in submit_boot_tree — no boot_root
Noop anchor, no per-agent child DAGs, no display-only parent_id grouping: a
sweep MetaLock root (only when something is stale) that grows one rebuild
subgraph per stale agent into the same DAG (via append_subgraph, previous
commit), plus one Reconcile root per drifted agent (independent — a boot
reconcile needs no lock bump).

- submit_boot_tree builds the DagSpec inline; removed the single-use
  templates::{boot_root, startup_sweep} builders (inlined per the operator's
  "don't force single-use shapes into templates.rs" steer).
- fanout_specs simplified to the meta-update cascade path only — the startup
  sweep no longer fans out child DAGs, so its branch was dead.
- test: append_subgraph_roots_on_emitter_and_rebases_local_deps.

Vestigial after this (deliberately left as follow-ups, flagged in the PR):
NodeKind::Noop is now unconstructed (contained to hive-c0re, removable);
Template::StartupSweep is unconstructed but a hive-sh4re wire type
(frontend-coordinated removal).
2026-07-15 18:24:30 +02:00
atlas
b6defdeaaf feat(#2450): append_subgraph primitive + sweep MetaLock grows rebuilds in-DAG
First half of making the startup sweep one DAG. Adds the runtime
subgraph-append machinery and switches the sweep MetaLock from fanning out
child Rebuild DAGs to growing one rebuild subgraph per stale agent into the
same DAG:

- JobQueue::append_subgraph(dag_id, nodes, dep_on) — the multi-node,
  multi-agent generalisation of append_node: rebases a subgraph's local deps
  onto the DAG's id space and roots it on the emitting node.
- NodeOutput.append_subgraph: Vec<Vec<NodeSpec>> — the executor→scheduler
  channel for it; scheduler drains it before completing the emitting node
  (same ordering as append_nodes).
- run_meta_lock sweep branch returns the stale agents' rebuild_nodes
  subgraphs via append_subgraph instead of fanout.

Follow-up commit collapses submit_boot_tree (drop boot_root + per-agent
reconcile child DAGs) so the whole boot is one DAG built inline.
2026-07-15 18:24:30 +02:00
iris
44286996ed fix(#2465): split node chain on deps graph, not agent field
mara's review comment: the frontend shouldn't apply its own grouping
logic on top of the DAG — it should render the structure the backend
already provides. The actual structure is the nodes' deps graph, not
the incidental n.agent field.

Replace the group-by-agent heuristic with nodeComponents(): splits
entry.nodes into weakly-connected components via the deps edges
(undirected reachability), then topo-sorts each component (Kahn's
algorithm) so a chain renders in true dependency order. A DAG made of
independent per-agent subgraphs (no cross-agent deps) still comes back
as separate components — same visual result for today's templates —
but the split is now driven by what the backend actually encodes, and
naturally extends to any future non-agent-aligned branching. Agent
name is still shown as a per-component label, but purely as adjunct
info sourced from that component's own nodes, not the grouping key.
2026-07-15 18:23:47 +02:00
iris
45cdd62116 fix(#2465): render multi-agent DAG nodes as per-agent subgraph lines
renderQueueEntry flattened entry.nodes into one arrow-joined chain
regardless of which agent each node belongs to. A multi-agent DAG
(e.g. hivectl restart --graceful with several agents) runs independent
per-agent subgraphs concurrently, no cross-agent deps, so joining them
all into one sequential-looking chain misrepresented the actual DAG
shape (mara's report: 'expected one dag that forks after the start
node into the per agent sub dags').

Group nodes by n.agent (stable, first-seen order) and render one
.rqe-nodes line per agent, with a small agent-label chip when the DAG
spans more than one. Single-agent DAGs (the common case) collapse back
to exactly the prior one-line render — no visible change there.
2026-07-15 18:23:47 +02:00
damocles
0d40583991 feat(#2482): bootstrap cross-signing so agent matrix devices show verified 2026-07-15 18:17:40 +02:00
damocles
1961afd9cc feat(#2471): post failing config deploy build log to the config pr 2026-07-15 18:16:12 +02:00
damocles
152d54dcdc fix(#2469): supersede stale config-PR approval on PR update instead of skipping 2026-07-15 18:16:08 +02:00
iris
d8567fc546 add graceful checkbox to restart confirm dialog
Restart now offers the same graceful-vs-hard choice stop already has:
single-agent menu item and the bulk-select action bar both grow a
'restart gracefully' checkbox that routes through the existing
submit::graceful_restart DAG (signal -> drain -> stop -> reconcile)
instead of a hard restart. Backend gains a ?graceful=true query param
on POST /api/restart/<name>, mirroring post_kill's shape (renamed
KillParams -> GracefulParams since it's now shared).
2026-07-15 18:05:47 +02:00
damocles
207b66e35d feat(#2470): capture config-approval eval/deploy nix runs in build logs 2026-07-15 17:43:24 +02:00
damocles
741852a8b4 fix(#2468,#2473): matrix read_room exposes in_reply_to + gates UTD sentinel on encryption 2026-07-15 17:06:13 +02:00
damocles
3f1643c594 refactor(#2464): rename hive-ag3nt crate to hive-agent, collapse lib into main 2026-07-15 16:09:39 +02:00
damocles
7b54e7aa50 refactor(#2352): subvolume snapshot ops via daemon wire commands 2026-07-15 00:50:02 +02:00
atlas
860484a193 refactor(#2439): build hive-wide stop/start/restart DAGs dynamically
Hive-wide `stop` / `start` / `restart` emit ONE DAG with a per-agent
subgraph each (concurrent on their own leases) instead of N DAGs — and each
subgraph is now built dynamically from the agent's live running state rather
than a fixed template shape:

- online agent: the full stop→reconcile (restart: stop-for-update→reconcile)
  chain; `graceful` prepends signal→drain.
- offline agent: just `SetWanted → Reconcile` (nothing to quiesce/stop; a
  restart of a down agent is really a start).

The head `SetWanted` (intent) and tail `Reconcile` (convergence guarantee)
are always present; only the mechanical `Signal`/`Drain`/`StopForUpdate`
nodes are state-conditional. Keeping `Reconcile` in every shape closes the
TOCTOU window — a race-up between the `is_running` read and node exec is
still converged in-DAG (with `StopForUpdate`-noop as the backstop) — with no
reliance on an external reconcile sweep.

The state-aware assembly needs an async `is_running` read, so it moves out
of the pure/sync `templates.rs` into `submit.rs`, layered as pure
`*_chain(running)` → pure `*_spec(targets)` (the unit-test seam) → async
`*_many` (reads live state + submits). `templates.rs` keeps only the shared
pure primitives (`node`/`after_ok`/`rebuild_nodes`).

Callers await the now-async submit fns (server, dashboard, socket_server).
Tests exercise both the online and offline shapes via the pure `*_spec`
seam. docs/coordinator.md shapes updated.
2026-07-15 00:29:54 +02:00
damocles
3797177e7f refactor(#2352): quota show via daemon wire command 2026-07-15 00:10:15 +02:00
damocles
e7f8254788 refactor(#2455): split hive-agent-mcp into its own bin crate 2026-07-14 23:49:22 +02:00
damocles
59dd3a0aa7 refactor(#2456): split hive-agent-wake into its own bin crate 2026-07-14 23:26:06 +02:00
atlas
c8bd0f7180 test+docs(#2448): multi-agent restart test + doc shapes
- multi_agent_restart_is_one_dag_with_concurrent_per_agent_subgraphs:
  two agents restart in one DAG; both SetWanted heads are roots, each
  acquiring its own agent's lease concurrently. Existing single-agent
  shape/lease tests updated for the templates::restart(&[..], graceful)
  signature.
- coordinator.md + templates.rs module doc: restart takes an agent list;
  a hive-wide restart is one DAG with N per-agent subgraphs.
2026-07-14 23:11:02 +02:00
atlas
1739716fa2 feat(#2448): emit one multi-agent DAG for hivectl restart / restart-all
A hive-wide restart was N separate single-agent DAGs (one submit::restart
per agent). Now that agent is per-node (#2445), make it ONE DAG with a
per-agent restart subgraph each.

- templates::restart takes an agent list: each agent gets an independent
  subgraph (a head SetWanted(Up) root, then its restart chain), so the N
  subgraphs run concurrently on their own leases. One agent = the ordinary
  single-agent restart; unifies the old restart + graceful_restart fns.
- submit::restart / graceful_restart stay as single-agent wrappers over
  the new submit::restart_many(agents, graceful).
- server.rs handle_restart_all + handle_restart_scoped submit one
  restart_many call instead of looping per agent. Infra containers
  unchanged (no lease/DAG, synchronous).

Scope: restart + restart-all only. Broad stop+start is the same pattern
(stop/start templates take agent lists) — a follow-up increment.
2026-07-14 23:11:02 +02:00
damocles
53d4f5ca32 refactor(#2352): quota enable/limit via daemon wire commands 2026-07-14 23:00:34 +02:00
atlas
9a39a54820 refactor(#2449): dashboard + docs for the SetWanted node
- builds.js NODE_KIND_LABEL: 'set wanted' (and the previously-missing
  'noop') so the new head node renders with a label, not the raw kind.
- docs/coordinator.md + templates.rs module doc: the power-op DAG shapes
  now show the head SetWanted node; SetWanted added to the lease-needing
  list with the atomicity rationale.
2026-07-14 22:34:44 +02:00
atlas
5fe8008cce refactor(#2449): write power intent via a SetWanted DAG node, not a pre-submit side effect
The durable 'wanted' power intent was written by submit::{start,stop,
restart,graceful_restart,graceful_stop} as a synchronous pre-submit side
effect, then read by the DAG's tail Reconcile. That's not crash-safe
(a crash between the write and the enqueue loses it) and, with agent now
per-node, can't be per-agent in a DAG that spans agents.

Move it into the DAG as a head SetWanted node:
- NodeKind::SetWanted { up } + run_set_wanted executor (fails the node on
  a write error, unlike the old warn-and-continue, so a stale intent
  never reaches Reconcile).
- LEASE-NEEDING, not lease-exempt: it takes the agent lease so a power-op
  DAG's intent-write + reconcile is atomic per-agent. If it were exempt,
  two racing ops (restart vs stop) would run both intent-writes up front
  and clobber each other before either reconciled — defeating the point
  of moving the write into the DAG. (In stale_start the lease is thus held
  across the head Prebuild, but that's a no-op there: the agent is down so
  prebuild is skipped.)
- templates: explicit SetWanted node 0 on restart/graceful_restart/
  graceful_stop, plus dedicated start/stop templates (SetWanted -> Reconcile)
  and stale_start (SetWanted(Up) -> rebuild subgraph, reusing rebuild_nodes).
  No compose helper / rebuild variant. reconcile_only is now boot-only.
- submit.rs: drop the set_wanted side effect; the stale-rev shape decision
  (start vs stale_start) stays submit-side.

All 33 job_queue tests pass (shape/lease tests updated for the head node).
2026-07-14 22:34:44 +02:00
damocles
6c654921a0 refactor(#2352): route hivectl github set-token through a daemon wire command 2026-07-14 22:17:09 +02:00
damocles
756e850681 fix(#2352): allow too_many_lines on the HostRequest dispatch router 2026-07-14 22:03:12 +02:00
damocles
a3c916813b refactor(#2352): route hivectl forge create-user through a daemon wire command 2026-07-14 22:03:12 +02:00
atlas
de69a9f02c refactor(#2441): live-log header shows the streaming node's agent, not the DAG's
Review (iris/argus): the dashboard rebuild live-log header labels one
specific node's log stream (liveNode, keyed by its build_log_id), so it
should show that node's own agent — entryAgents(running) listed every
agent in the DAG, which would mislabel a single agent's log once DAGs
span multiple. The other entryAgents() sites (row label, cancel-confirm,
fingerprint) are correct whole-DAG summaries and unchanged.
2026-07-14 21:58:40 +02:00
atlas
e2b48d2014 refactor(#2441): update queue consumers + docs for agent-per-node
DagView no longer has a DAG-level agent, so consumers derive it from the
per-node agents:
- hivectl dag_progress.rs: dag_agents(d) helper (distinct node agents,
  comma-joined) in place of d.agent.
- dashboard builds.js: entryAgents(entry) helper likewise for the
  rebuild-queue card + live-log header + cancel confirms.
- docs/coordinator.md: lease prose (node-agent-keyed, global per agent),
  wire shape (NodeView.agent, no DagView.agent), and dropped the removed
  dedup section.
2026-07-14 21:58:40 +02:00
atlas
2a59f2f5fc refactor(#2441): move agent field from DAG onto Node; drop dedup
Agent was a single field on Dag/DagSpec, making a DAG structurally
one-agent — a multi-agent op could only ever be N separate DAGs. Move it
onto Node/NodeSpec (and the NodeView wire type), drop it from Dag/DagSpec
(and DagView): a DAG can now span agents.

- lifecycle lease keys on the node's agent, still globally exclusive per
  agent across all DAGs (Inner.leases unchanged in shape). A DAG holds one
  lease per distinct agent it touches; settle() frees each at DAG-terminal
  (per-agent-subgraph early release is a follow-up, only observable with
  multi-agent DAGs).
- transient guard keyed (dag_id, agent); cancel-revert + Rebuilt events
  walk TerminalDag.agents.
- submit-time dedup removed (a multi-agent DAG has no single agent to key
  on); every submit enqueues a fresh DAG. Whether dedup needs reintroducing
  is tracked in a follow-up sub-issue.
- templates gain a node(agent, kind, deps) helper stamping the agent onto
  every node; meta templates stamp "hyperhive".

Templates stay single-agent in this PR — behaviour is unchanged, only the
representation + wire shape. Multi-agent DAG emission (restart/restart-all/
broad stop+start as one DAG) and the SetWanted-as-a-node change are
follow-ups off #2439.
2026-07-14 21:58:40 +02:00
damocles
91607f3896 refactor(#2442): split harness-state persistence out of events.rs 2026-07-14 21:15:37 +02:00
damocles
bf657177d3 fix(#2438): link config approval commit by full sha, not abbreviated 2026-07-14 20:48:35 +02:00
atlas
407965b6e1 fix(#2398): correct stale doc on handle_restart_scoped
argus caught it: the function-level /// comment still described the
old submit-await-submit graceful approach after the code moved to
one atomic GracefulRestart DAG.
2026-07-14 20:38:10 +02:00
atlas
901ab6a779 fix(#2398): graceful restart as one atomic DAG, not compose-and-await
mara's review on #2436: no submit-await-submit composition, even
server-side. Adds Template::GracefulRestart (Signal -> Drain ->
StopForUpdate -> Reconcile, wanted=Up) mirroring how Restart already
does StopForUpdate -> Reconcile, plus submit::graceful_restart and
templates::graceful_restart. handle_restart_scoped now submits exactly
one DAG per agent up front for both the graceful and non-graceful
case -- no await_dags in the loop anymore.
2026-07-14 20:38:10 +02:00
atlas
4cfa040154 feat(#2398): dagify hivectl restart (RestartScoped)
hivectl restart --agent NAME previously composed stop() then start() as
two separate client-side daemon calls glued by CLI-process control flow
— not one DAG, and a dropped hivectl connection mid-restart (ssh drop,
Ctrl-C) left the agent stopped with no automatic follow-up. mara flagged
this as the first target for the 'dagify hivectl commands' issue.

New HostRequest::RestartScoped{scope, graceful} handles it server-side:
each targeted agent now rides exactly one atomic Restart-template DAG
(same one hivectl agents restart / restart-all already use) in the
common non-graceful case. --graceful has no single-DAG template yet, so
it submits the graceful-stop DAGs, awaits them server-side, then submits
the start DAGs — still one daemon call end to end, just not yet a single
DAG (noted as a follow-up). Infra containers restart synchronously as
before (no lease/DAG concept for them).

CLI-side restart() now just makes the one call + waits, same output
shape as before via render_lifecycle.
2026-07-14 20:38:10 +02:00
damocles
5853ce2c8d fix(#2417): hardcode external,gateway in forgejo webhook allowlist, drop option 2026-07-14 20:26:02 +02:00
damocles
60e4ea8bb8 fix(#2417): keep 'external' in webhook allow-list so public webhooks still deliver 2026-07-14 20:19:49 +02:00
damocles
00c9a15ae6 fix(#2417): make webhook allow-list additive via forge.webhookAllowedHosts option 2026-07-14 20:16:12 +02:00
damocles
cada704856 fix(#2417): allow gateway host in forgejo webhook ssrf list so config-pr webhook delivers 2026-07-14 20:07:27 +02:00
atlas
e0461e1af6 refactor(#2431): extract hive-priv-sock crate from hive-sh4re
Split the priv-socket wire types (PrivRequest/PrivResponse/PrivEvent and
friends) out of hive-sh4re into their own hive-priv-sock crate, mirroring
the existing hive-host-sock split. hive-priv — the root-privileged
helper — now depends on just this narrow protocol crate instead of the
much larger daemon-shared crate, shrinking its dependency surface and
making the privsep boundary easier to audit. No server/client
implementation lives here, only the wire contract; hive-c0re still
depends on hive-sh4re directly for everything else.
2026-07-14 20:02:20 +02:00
damocles
ffde085c35 fix(#2433): mkforce container@hive-ci timeoutstartsec to resolve nixpkgs conflict 2026-07-14 19:59:32 +02:00
iris
46bdb78466 hive-forge: validate --forge <label> charset up front
argus flagged (approving) that an unvalidated label could build a path
outside the state dir; mara called it out as a usability issue in its
own right, not just a low-risk security nit — a typo'd label should
give a precise 'not a valid label' error, not a confusing file-not-found
or an unexpected traversal.

Reject anything outside the plain lowercase+digits+hyphens charset
dashboard/extra_forges.rs already enforces on write, before touching
the filesystem at all.
2026-07-14 19:37:20 +02:00
iris
8e58793f79 hive-forge: add -f/--forge <label> selector for external forge accounts
Targets a dashboard-provisioned external forge account (FORGES tab)
instead of the internal forge: resolves `forge-<label>-token` for the
token and `forge-<label>.json`'s base_url for the URL, the same two
files dashboard/extra_forges.rs writes, instead of
HIVE_FORGE_URL/forge-token. Falls back to today's behavior when unset.
Orthogonal to -r/--repo.

An unknown label gives a clear error listing the labels actually found
in the state dir instead of a raw file-not-found. The base_url JSON key
is read via a typed sidecar struct pinned to what extra_forges.rs
writes, so the read side can't silently drift from the write side.
2026-07-14 19:31:08 +02:00
atlas
60a253a2f6 fix(#1763): address argus review on PR #2430
- regenerate docs/tools/hivectl-cli.md for the new `subvol snapshot send` verb
- close the TOCTOU on the no-overwrite guard: File::options().create_new(true)
  (O_CREAT|O_EXCL) instead of exists()-then-create, so the guarantee is
  atomic against a concurrent request racing the same dest filename
- warn (not silently swallow) if cleaning up a partial export after a
  failed btrfs send itself fails, so a stuck garbage file masquerading
  as a completed export is visible in the log
2026-07-14 19:23:59 +02:00
atlas
90af0edab0 feat(#1763): local-file half of btrfs send/receive migration transport
SendAgentSnapshotToFile priv op: btrfs send [-p <parent>] <snapshot> to a
file under MIGRATE_STAGING_ROOT. Standalone-useful as a point-in-time
snapshot export/backup today; the cross-hive ssh-piped leg (auth/trust
design posted on #1763, awaiting mara/damocles steer) is a later,
separate piece this doesn't block on.

- hive-sh4re: PrivRequest::SendAgentSnapshotToFile + MIGRATE_STAGING_ROOT
- hive-priv: validates names, refuses to overwrite an existing export,
  cleans up a partial file on btrfs send failure
- hive-c0re: priv_client::send_agent_snapshot_to_file
- hivectl: `hivectl subvol snapshot send <agent> <label> [--parent <label>] --dest <file>`
2026-07-14 19:23:59 +02:00