Commit graph hyperhive/hive-c0re/src/server.rs
Author SHA1 Message Date
atlas
433b294099 refactor(nix): swarm.peers becomes swarm.hives, a directory of every hive
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.
2026-08-05 20:44:16 +02:00
atlas
5235166bb3 docs: name the matrix host options by their new path
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.
2026-08-05 13:45:09 +02:00
atlas
ddc017f01b wip(#3001): convert tests off the container id; drop Source + insert_group
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.
2026-08-04 19:57:32 +02:00
atlas
0523b4f7de wip(#3001): convert the last submit call sites; the binary compiles again
`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.
2026-08-04 19:57:32 +02:00
atlas
f707c60f90 jobq: delete DagView/NodeView, the second projection of one graph
Two views of the same graph existed: the typed `DagView`/`NodeView`
(`/api/state.rebuild_queue`, the `QueueDag` socket request, and the
`RebuildQueueChanged` payload) and `hive-jobq-wire`'s generic
`GraphNode` (`/api/jobq/graph`, `QueueNodes`). Every consumer has moved
to the generic one, so the typed pair is deleted rather than kept in
agreement with it.

What that removes, beyond the types: the `QueueDag` request and
`HostResponse::dags`; `Queue::snapshot`; `dag_view`, `visible_dags`,
`shown_on_wire`, `dag_finished_at` and `containers`; and the
`rebuild_queue` field on `/api/state`. `RebuildQueueChanged` keeps its
seq and loses its payload — nothing read it, and shipping the graph
both on an event and on an endpoint is the duplication this issue is
about. It stays an event rather than becoming a poll because
push-on-change is what every other live surface here does.

Two behaviours came out simpler for a structural reason. `await_dags`
needed two rules — settled means "gone from the snapshot" *or* "present
with every node terminal" — because the typed view evicted finished
groups; the generic view doesn't, so pending is just "some node isn't
terminal". And `state_of` in the tests no longer derives a roll-up at
all: a group root's own state is the scheduler's answer.

That second one found a bug. `cancelled_dag_still_runs_its_approval
tail` asserted the group reads `Cancelled` while the tail it exists to
protect was still pending — `rollup_state` flattened the surviving
child away and called the group settled. The root reads `Finishing`,
which is what the scheduler documents: own logic done, children still
running. The test now asserts that, with the reasoning inline so it
doesn't get "fixed" back.

Kept: `Source`, `State`, `PermPayload` and the `NodeId` alias in
`hive-host-sock::jobs` — shared vocabulary, still used by hivectl.
2026-08-03 21:25:07 +02:00
damocles
0c4d56a585 hivectl: batch QueueNodes polling by id set, drop remaining dag wording 2026-08-03 20:35:24 +02:00
damocles
309c1c91c6 job_queue: drop the NodeKind::Dag/root filter from the QueueNodes lookup 2026-08-03 20:35:24 +02:00
damocles
10b0f640af hivectl: migrate dag_progress to hive-jobq-wire's generic GraphNode 2026-08-03 20:35:24 +02:00
atlas
d3f2d246e3 fix(#2860): hive-c0re stops assuming matrix is on localhost
`MATRIX_HTTP` was `http://localhost:8008`, compiled in, used at 18 call
sites. That address is right only while the homeserver happens to share
this daemon's netns, and its doc comment asserted exactly that as a
general fact. A hive whose homeserver lives anywhere else builds fine
and then talks to the wrong machine.

It now reads `HIVE_MATRIX_API_URL`, which `hive-c0re.nix` sets from
`hyperhive.matrix.apiUrl`. The matrix module fills that in with its own
loopback listener when it is the thing running tuwunel — there it is not
a guess but a fact about what it just started — and the operator sets it
by hand otherwise. There is no compiled-in fallback, for the same reason
`forge_http_base()` has none.

`is_present()` follows. It used to scan `nixos-container list` for
`hive-matrix`, which answers "is the homeserver a container on this
host" — a different question, and the reason a remote homeserver would
silently no-op no matter how it was addressed. It now asks whether a URL
is configured. A co-located hive is unaffected: the module supplies the
loopback URL whenever it runs tuwunel itself. It also stops being
`async`, since it no longer does IO, and `require_matrix_present`'s
message names both ways to have a homeserver rather than only the local
container.

Absent a URL, every matrix path no-ops exactly as it did with no
container, and the two accessors make that structural: `Option` for the
callers that fall back to `None`, a `Result` flavour naming the skipped
`is_present()` gate for the ones that propagate.
2026-08-03 20:34:36 +02:00
damocles
7c6f108716 coordinator: push_todo returns Result so callers can see delivery outcome 2026-08-03 01:08:24 +02:00
damocles
d77f81cd1e hive-c0re: push HelperEvent::Spawned as a todo, not a broker message
Follow-up from #2955 (mara: 'make core able to give agent a todo').
First migration slice: Spawned was pure FYI-check-when-convenient
material, not something needing an immediate turn.

Coordinator::push_todo/push_todo_submitter do a best-effort live dial
of the target agent's own hive-agent-sock (hive_host_sock::agent_todo_
socket), sending the exact UpsertTodo request in-container producers
(matrix/bash/forge-notify) already send. Push, not queue: agent
offline (socket absent) or dial failure is a silent no-op, no retry,
no fallback delivery -- matches mara's 'not available if offline'
call exactly.

HelperEvent::Spawned removed entirely (enum variant + all 3 call
sites migrated: handle_spawn's two arms, finish_approval's Spawn
approval-kind arm) rather than kept alongside a translation layer --
per mara's correction on the first design attempt, migrating the
producer means deleting the old path, not bridging it.

Verified: cargo build/clippy/test -p hive-c0re -p hive-host-sock
-p hive-sh4re clean (318 tests), nix fmt clean.
2026-08-02 23:00:14 +02:00
atlas
ba71e45486 refactor(#2862): one snapshot store per swarm, not one per peer
The push side modelled a store per peer hive: a --peer argument, a
swarm.peers.<domain>.snapshotStorePort option, and a swarm_peers module
whose entire job was answering "which peer". A swarm has exactly one
store, so none of that had anything to select between.

The receiver already proved it. It keys destination directories by
agent, not by sending hive, precisely so an agent that migrates keeps
one unbroken incremental chain -- which only makes sense if every hive
pushes to the same place. Per-hive stores would split the chain in two,
the case that keying exists to prevent.

So the destination moves to services.hyperhive.swarm.snapshotStore,
rendered into HYPERHIVE_SNAPSHOT_STORE, and swarm_peers is deleted
rather than adapted. address has no default because it is a
deployment fact this host cannot derive; port defaults because it is a
convention both ends read from the same option docs. An unset or empty
address fails naming the option instead of connecting somewhere
arbitrary, and a test asserts the message suggests no value.
2026-07-31 22:15:37 +02:00
atlas
282bbc3709 feat(#2862): push a snapshot to a peer hive's store over the mesh
Adds the caller the fd-passing machinery existed for: hivectl agent
<name> subvol snapshot push --peer <hive> resolves the peer, connects
to its snapshot store, writes the agent header, and hands the connected
socket to hive-priv, which runs btrfs send straight into it.

The split keeps the root helper ignorant. Everything that involves
knowing where a peer is, what the wire protocol looks like, and which
hive to trust happens in the unprivileged daemon; hive-priv only ever
receives an already-open descriptor. Once btrfs send starts, neither
process is in the data path, so a multi-gigabyte transfer costs no
per-byte work and survives a hive-c0re restart.

call_with_fd takes the descriptor by value and closes it as soon as the
kernel has it. A socket stays open until every copy closes, so holding
one back would leave the receiver waiting for an EOF that never comes:
btrfs receive blocks and this side reports success for a transfer the
peer never committed. Ownership makes that unrepresentable.

The peer's store port is a new swarm.peers.<domain>.snapshotStorePort
option rather than a constant matching the module default. A pushing
hive cannot read the receiver's configuration, so assuming 51821 would
push at a port nobody promised to listen on; absent, the push fails
naming the option. swarm_peers parses the mesh address the host module
has always rendered into HYPERHIVE_PEERS but nothing read.
2026-07-31 22:15:37 +02:00
damocles
03afbd1316 hivectl: rename hivectl agents to hivectl agent <name> <verb> 2026-07-27 19:07:18 +02:00
atlas
de09628c7c fix(#2733): write the agent pause marker via hive-priv
`Coordinator::set_paused` wrote the marker directly with `std::fs::write`
from hive-c0re, which runs as the unprivileged `hive-core` user. The
agent's harness dir is chowned to the agent user on every container boot
(`user.nix`'s activation chown), mode 0755 — so hive-core can stat the
marker but gets EACCES creating or unlinking it. Pause therefore only
ever worked on an agent that had never booted; the read side works
because a stat needs traverse, not write, which is why the paused pill
and `is_paused` looked healthy.

Route both directions through hive-priv, the root helper that already
owns the other writes into agent-owned directories:

- `PrivRequest::SetAgentPaused { agent_name, paused }`, with the marker
  filename constant moved to hive-priv-sock. That is the narrowest crate
  all three sides share (hive-priv deliberately does not depend on
  hive-sh4re, which re-exports it for the in-container resolver). A
  private copy on any one side would break pause silently, since every
  reader just sees "no marker".
- `write_agent_state_file` generalised to `write_agent_dir_file`, taking
  the target directory: `state/` and `harness/` are both agent-owned,
  which is the same reason both need root.
- resume unlinks via `remove_file`, which acts on the leaf and never
  follows a symlink — an agent could otherwise plant a link at the
  marker path and have root delete an arbitrary file.

`Coordinator::set_paused` becomes an async round-trip; its three call
sites were already async. Both directions stay idempotent because the
dashboard toggle and `hivectl pause|resume` fire without reading the
current state first.
2026-07-27 09:42:31 +02:00
atlas
170fd817ea fix(hivectl): ask the daemon whether an agent exists
The agents root is 0700 and owned by the daemon's user, so hivectl's
client-side existence guard hit EACCES on traversal for anyone not root.
It reported that as "this command needs root; re-run with sudo", which
turned three verbs' pre-flight check into a permission error about the
wrong thing: `choom`, `subvol upgrade` and `subvol snapshot create` all
failed at the guard rather than at whatever they actually needed.

The daemon runs as the owning user and already answers this question for
its own provisioning paths, so expose it on the host socket as
`AgentExists` and have hivectl ask. Operators reach that socket through
the `hive-admin` group, so the guard now works without sudo.

`choom` still needs root for `machinectl shell` — we ship no polkit rule
granting those actions — so it now checks the effective uid and says so
directly instead of failing later inside systemd's authorisation.
2026-07-27 09:34:43 +02:00
damocles
bd14cc5c46 feat: derive BUN_JSC_forceRAMSize from effective per-agent MemoryMax= 2026-07-26 21:56:25 +02:00
damocles
18745f1a98 job_queue: finish reparent call-site swap, delete dead sync path 2026-07-26 19:47:36 +02:00
atlas
766b1f71fb refactor(hivectl): move quota + subvol under agents
Both groups only ever act on a single managed agent's state dir, so
they belong in the `agents` namespace rather than as top-level verbs
next to `forge` / `matrix` / `wg`.

Renames `quota limit` -> `quota set`: the enclosing group already
carries the noun, so the bare verb matches the flat `set-parent` /
`set-limits` spelling without stuttering, and it removes the
`set-limits` (cpu/mem) vs `quota limit` (disk) ambiguity. Adds a
cross-pointer from `set-limits` to `agents quota`.

Handlers stay in their own modules; `run_agents` gains the reparenting
glue. Regenerates docs/tools/hivectl-cli.md.

Refs #2724
2026-07-26 18:26:27 +02:00
atlas
a6dc980700 feat: per-agent CPU and memory limits
The hive applies one `agentCpuQuota` / `agentMemoryMax` to every
container. That's the right default and the wrong ceiling: a build-heavy
agent needs headroom the other twelve don't, and raising the hive-wide
value to suit it hands that headroom to everyone.

Adds a per-agent override, persisted host-side and resolved per-field
against the hive defaults.

Follows the existing `meta/*.json` pattern (`capabilities.json`,
`tool-groups.json`): a host-side map read by `hive-c0re`, staged and
committed in the meta repo so every change lands in the audit trail.

```json
{ "sock": { "cpu_quota": "400%", "memory_max": "8G" } }
```

Fallback is **per field**, not per agent: an entry with only
`memory_max` leaves that agent on the hive-wide CPU quota. Absent file,
absent agent and absent field all resolve to the hive default, so the
feature is inert until someone opts an agent in.

Unlike the other meta files this one is **not** injected into the
container — a limit is something done *to* an agent, not something it
reads about itself.

```
hivectl agents set-limits sock --cpu-quota 400% --memory-max 8G
hivectl agents set-limits sock --reset
```

Values are validated before they're persisted: they go into a systemd
drop-in verbatim, and a typo there makes the unit fail to *start* —
turning a fat-fingered quota into a container that won't come back.

The command is declarative: each call replaces the agent's whole entry.
That makes a forgotten flag a silent revert, so a bare `set-limits
<name>` is rejected at the clap layer and clearing needs an explicit
`--reset`.

`ContainerView` gains `cpu_quota` / `memory_max`, both always populated:
there's no "unset" state to render, only "same as everyone else". They
reflect what the drop-in *says* — what the next start will enforce — not
a live cgroup reading.

The write goes through `meta::commit_resource_limits` rather than the
bare setter, so it's staged and committed under `META_LOCK`. Writing
without committing would leave the meta working tree dirty for the next
`prepare_deploy` to trip over.

Docs: `persistence.md` (the new meta file, and why it isn't injected),
`tools/hivectl.md` (the prose guide), `tools/hivectl-cli.md`
(regenerated clap dump).

Closes: internal/requests issue 25
2026-07-26 14:15:05 +02:00
atlas
31008c83df feat: pause an agent's turn loop without stopping its container
A paused agent keeps its container, its claude session and its
dashboard/todo servers up, but stops driving turns. Messages queue
unacked and are drained on resume.

The whole protocol is a single marker file, `<harness>/paused`. That
directory is already a bind-mount shared between host and container, so
both sides just stat the same path: the harness reads it to decide
whether to drive a turn, hive-c0re reads it to render the badge and
writes/removes it for `hivectl pause|resume`. No new wire protocol, no
container round-trip, and it is sticky across restarts by construction.

Not calling `recv_next` while paused *is* the queueing semantic, so
there is no fencing to get wrong: reminders buffer in their unbounded
channel, the todo `Notify` permit coalesces, and a `request_next_turn`
that raced the pause survives because the gate sits above
`self_continue.take()`.

Graceful stop is handled host-side rather than in the harness: a paused
agent provably has no turn in flight, so `run_signal` skips the fence
entirely instead of eating the full `GRACEFUL_STOP_TIMEOUT` waiting for
a checkpoint turn that will never run.

`paused` is reported on `ContainerView` / `AgentStatusRow` for the
dashboard, orthogonal to `running` and reported for stopped containers
too.

Closes: hyperhive/hyperhive issue 2271
2026-07-26 03:11:33 +02:00
atlas
51bcad1adb refactor(#2591): fix wire-shape consumers (server await_dags, tests)
- server::await_dags: a DAG is settled when gone from the snapshot (fully
  Done) or present with all nodes terminal; pending only with a non-terminal
  node (DagView no longer carries a rolled-up state).
- DagView::rollup_state() added to hive-sh4re — the shared node-set roll-up
  derivation every Rust consumer uses.
- JobQueue::build_log_id_of(node_id) — the node_id -> build_logs lookup the
  query endpoint will use; tests assert log-id via it now.
- tests: derive roll-up state; drop the off-wire step/build_log_id wire asserts.
2026-07-23 16:40:46 +02:00
damocles
a80d0b0fed delete c0re-side reminder plumbing (#2635 inc 1 commit 6) 2026-07-23 00:12:30 +02:00
damocles
84b750fba5 refactor(#2302): type socket wire fields as ident, validated by serde on deserialize 2026-07-20 21:46:18 +02:00
damocles
bf644cc126 feat(#2302): thread &Ident through agent path builders 2026-07-20 21:46:18 +02:00
damocles
d124dd205a feat(#2554): add hivectl forge reconcile-config to reconcile local applied config against forge main 2026-07-17 12:55:42 +02:00
damocles
f1812335d1 route gateway htpasswd management through a daemon wire command (#2504) 2026-07-15 23:23:47 +02:00
damocles
12ce346d02 refactor(#2416): drop the now-always-none Spawned.sha field + deny tag leftover 2026-07-15 21:03:52 +02:00
damocles
c2bd7db998 refactor(#2416): remove the non-pr config-change flow (request_apply_commit / applycommit) 2026-07-15 21:03: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
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
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
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
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
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
9674fd42ac refactor(#2352): move matrix provisioning behind host-socket wire commands 2026-07-13 19:23:35 +02:00
damocles
96c748475e refactor(#2352): re-home host-control wire types into hive-host-sock crate 2026-07-13 15:55:20 +02:00
atlas
ad0752822a fix(#2377): extract handle_agent_status — drop clippy::too_many_lines allow
dispatch was 101 lines (1 over limit) due to the AgentStatus arm.
Extract it to a dedicated handle_agent_status helper to bring dispatch
under the 100-line lint limit without the allow attribute.

Per mara review comment on PR #2379.
2026-07-11 12:19:52 +02:00
atlas
18e7c406b0 fix(#2377): doc_markdown + too_many_lines clippy lints
- Backtick-quote `pull_request` in doc comments (4x doc_markdown)
- Add #[allow(clippy::too_many_lines)] to server::dispatch (101/100;
  +1 line from submit_kind fetched_sha param in 5dd0a36f)
2026-07-11 12:19:52 +02:00
atlas
96eda4ed6b fix(#2375): pr_is_open state check at submission + atomic fetched_sha INSERT
Two hardening items from argus's review of #2374:

1. PR state check at submission:
   - Add `pr_is_open(repo, pr)` to forge/pr_merge.rs using
     `repo_get_pull_request` + `StateType` — early error if the PR is
     already closed or merged instead of queuing a card that fails later
   - Call it in `submit_merge_config_pr` before fetching the head sha

2. Atomic fetched_sha INSERT:
   - Add `fetched_sha: Option<&str>` to `Approvals::submit_kind` so
     the sha can be included in the INSERT rather than a follow-up UPDATE
   - MergeConfigPr already knows the sha before inserting the row
     (pr_head_sha runs first) → pass `Some(&sha)`, drop the separate
     `set_fetched_sha` call → truly atomic
   - ApplyCommit still needs two writes (sha resolved by git_fetch_to_tag
     after the row exists) → pass `None`, `set_fetched_sha` unchanged
   - All other callers (InitConfig, Spawn, UpdateMetaInputs,
     SchedulePrompt) pass `None` — no behavioural change
   - Add `fetched_sha_in_insert_is_readable_via_get` test covering the
     MergeConfigPr path
2026-07-11 12:19:52 +02:00
damocles
cfb84b420a refactor(#2285): drop coordinator 1:1 path accessors, callers use paths:: directly 2026-07-10 20:32:03 +02:00
atlas
66c6828359 fix(#2290): register_agent in handle_spawn (event-driven, replaces stale spawn_poll comment) 2026-07-09 01:08:38 +02:00
atlas
afdd8c6c9f feat(#2290): converge unification cleanup — pull preamble into lifecycle
Collapse the scattered ensure_agent_runtime_dir calls into the lifecycle
functions themselves so callers have a single responsibility:

- lifecycle::spawn: calls ensure_agent_runtime_dir before write_dropins.
  Callers (handle_spawn, ensure_root_agent) no longer need a separate
  preamble step.

- lifecycle::rebuild_no_meta spawn path: calls ensure_agent_runtime_dir
  before write_dropins. apply_commit / merge_config_pr flows no longer
  need a manual ensure_agent_runtime_dir.

- run_create (job-queue): drops ensure_agent_runtime_dir + register_agent.
  The tail Reconcile's converge_start_preamble handles the runtime dir
  and mcp_sockets::spawn_poll handles the listener. Create stays purely
  'provision + create', not 'create + start'.

- handle_spawn (server.rs): drops manual preamble; lifecycle::spawn owns it.
  Drops unneeded unregister_agent on failure (supervisor handles listener).

- ensure_root_agent (auto_update.rs): drops manual ensure_agent_runtime_dir.

- actions.rs apply_commit / merge_config_pr: drop manual
  ensure_agent_runtime_dir; rebuild_no_meta's spawn path handles it.

Result: ensure_agent_runtime_dir lives in exactly two places —
lifecycle::spawn (direct spawn) and converge_start_preamble (start/reconcile
path). All other callers are clean call sites.
2026-07-09 01:08:38 +02:00