Commit graph hyperhive/hive-c0re/src/dashboard/lifecycle_ops.rs
Author SHA1 Message Date
atlas
7ac6819652 hive-c0re: fail on a malformed agent name and on an unreadable container list
An agent name that is not a valid Ident made `Coordinator::agent_paths`
panic. Job payloads carry names as plain strings (the swarm's published
wanted state is one source), and a panic inside a job-queue node never
reaches `complete_growing`, so the node's resources (the deploy
window included) were held until hive-c0re restarted. `agent_paths` now
returns an error; the job-queue nodes, the admin-socket spawn and
set-limits paths, the root-agent spawn and the dashboard set-limits
handler propagate it.

`lifecycle::list().await.unwrap_or_default()` turned a failed container
list into "no agents":
- meta-update cascade: the lock bump committed and zero rebuilds fanned
  out, reported as success. The cascade is now resolved before the lock
  bump and a list failure fails the node.
- dashboard update-all: queued nothing and returned 200 "ok". Now 500
  with the error.
- container rescan: every row was emitted as removed and the cache
  emptied. Now the last snapshot stands; `hivectl status` gets an error.
- dashboard journal: answered 404 "no managed container". Now 500.
- spawn/rebuild port-collision check: silently skipped. Now fails.
- startup migration: the per-agent phases ran over nothing, and phase 3
  handed an empty agent list to `meta::sync_agents`, which renders the
  meta flake with exactly the agents it is given. Both now log the list
  failure and skip.

The hive-jobq scheduler still leaks a node's resources on any executor
panic; that root is not addressed here.

Refs #4723
2026-09-27 05:13:21 +02:00
iris
27bed74d42 hive-c0re: accept friendly MemoryMax size spellings, normalize for systemd
mara hit this directly: setting an agent's memory cap to "16GB"
fails with 'invalid MemoryMax "16GB": expected a size such as
"8G"...'. Confirmed directly against a running systemd 260
(systemd-run -p MemoryMax=<value>): systemd's own parser accepts a
bare byte count, <digits>B, or <digits> plus exactly one uppercase
K/M/G/T, and rejects both "16GB" (redundant B after a multiplier)
and "16g" (lowercase) with "Invalid argument". So the prior
rejection of "16GB" matched systemd, but is bad UX for input a
human reasonably expects to work.

validate_memory_max now returns the value to store (not just an
ok/err verdict): it accepts 8gb/8Gb/8GB/8g/8G interchangeably and
normalizes all of them to systemd's own 8G form before it is ever
persisted or passed downstream. Also fixes the adjacent bug the same
investigation turned up: the old validator incorrectly accepted
lowercase (8g) even though systemd itself rejects it.

Updated both call sites (server.rs, dashboard/lifecycle_ops.rs) to
use the normalized return value. New test
friendly_size_spellings_normalize_to_systemds_own_form; flipped the
old "8GB should be rejected" assertion, which encoded the
human-hostile behavior this fixes.
2026-09-19 15:07:52 +02:00
atlas
87970a8c93 mcp: remove the restart/kill/start/update/get_logs agent verbs
Container lifecycle from inside an agent goes away: an agent no longer
starts, stops, restarts or rebuilds a container in its subtree, and no
longer reads another container's journal. Those are operator actions —
the dashboard and hivectl keep their own paths to the same job-queue
and hive-priv plumbing, which is why none of that machinery is removed
here, only the five MCP verbs and what they alone reached.

What went with them: the `Request` variants and `Response::Logs` on the
agent socket, the five tool definitions and their arg structs, the four
lifecycle handlers plus `handle_get_logs`, and `require_descendant` —
the topology guard those five were the only remaining callers of.
`ToolGroup::Diagnostics` goes too: `get_logs` was its only tool, so it
would otherwise be a grantable group that grants nothing. `lifecycle`
stays, now carrying `list_containers` alone.

An agent that gets a `needs_update` or `container_crash` helper event
has no remedy of its own left, so the system prompt and the docs now
send it to the operator instead of to a tool that no longer exists.

Refs #4480
2026-09-19 10:47:39 +02:00
iris
513554fe9a swarm: add a declared "paused" agent wanted state
mara (#4170): swarm-ui's wanted-state dropdown could only ever declare
up/offline/destroy, with no way to swarm-declare the existing hive-local
turn-loop pause (`hivectl agent pause|resume`).

`AgentState::Paused` is not a fifth peer of Up/Offline/Destroyed on the
power axis this enum otherwise answers — it's Up plus an orthogonal
turn-loop pause. `hive-c0re`'s `workers::wanted` reconcile loop now
decides the two axes independently (`decide` for power, the new
`decide_pause` for the marker), so a stopped agent declared Paused
converges with both a Start and a Pause in the same pass.

Known, deliberate limitation: a Paused declaration on an agent this
hive has never deployed only reaches Deploy this pass — writing the
pause marker into a harness dir that may not exist yet was judged not
worth the risk, so it converges on the next pass once the agent is
present instead.

swarm-ui's WantedMenu gains a fourth "paused" option (warning-tone
badge). No separate "resume" entry — selecting "up" from a paused row
already clears the marker via the same decide_pause path.

Pause/resume marker writes go through one shared
Coordinator::set_paused_by_name helper, used by both the interactive
dashboard pause/resume handlers and this reconcile loop, instead of
each duplicating the parse-name/write-marker/track-rescan shape.
swarm-ui's "offline" and "paused" confirm dialogs share one
confirmTarget state and one ConfirmDialog instead of two near-identical
copies.

Closes #4170
2026-09-11 01:35:40 +02:00
atlas
6338939657 refactor(#2916): destroy submits a DAG instead of an imperative teardown
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.
2026-08-14 00:24:45 +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
02e916feee wip(#3001): power chains name their group roots
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.
2026-08-04 19:57:32 +02:00
atlas
fe52037b0d wip(#3001): rename insert -> insert_job per mara's 50056 2026-08-04 19:57:32 +02:00
atlas
f04a0cee92 wip(#3001): convert remaining unblocked call sites; sweep docs
21 of 28 non-test call sites now insert directly. power.rs compiles.
The only remaining errors are server.rs's 5, which are blocked: those
sites feed the returned id into HostResponse::queued -> `queued_dags`,
a wire field hivectl polls via QueueDag. Removing the container without
answering that breaks hivectl's wait/progress loop; asked on the issue.

Also swept the deleted symbol out of prose, not just code:
- docs/coordinator.md: "the submit layer (job_queue/submit.rs)" ->
  the power layer (job_queue/power.rs), and "submits" -> "inserts".
- templates.rs module doc: points at super::power for the power ops.
- lifecycle_ops.rs module doc: says which path each op takes now.
- mod.rs's insert_group comment restated the open issue verbatim
  ("a DAG is addressed by its container node, which submit inserts
  itself"). Replaced with what is actually true for that path.

Dashboard behaviour deltas worth review: insert failures are now
logged per agent instead of swallowed, and UPDATE-ALL emits one queue
snapshot after the loop rather than one per agent.
2026-08-04 19:57:32 +02:00
iris
ec30277a90 hive-c0re: drop redundant METHOD/path prefixes from OpenAPI summaries
Swagger UI's endpoint-list row already shows the HTTP method badge +
path for every row, so restating `METHOD /path` at the start of a
handler's own summary is pure duplication. Strips that self-referential
prefix from every summary that has it and re-capitalizes what follows
as a standalone sentence.

Left two false positives untouched: misc_api.rs's operator-inbox
summary cross-references a *different* sibling endpoint
(mark-all-read) for context, and topology.rs's SetParentForm struct
doc happens to mention its endpoint's path but isn't a handler summary
line. Both are legitimate, not redundant.
2026-08-02 21:35:17 +02:00
iris
071dbd774c hive-c0re: split OpenAPI summary/description, move param docs to params
utoipa splits a handler's doc comment on the first blank `///` line:
everything before it becomes the OpenAPI `summary` (shown in Swagger
UI's collapsed endpoint-list row), everything after becomes the
`description` (only shown once that row is expanded). With no blank
line, the whole doc comment becomes the summary and the description is
empty — which is what every handler in hive-c0re/src/dashboard/ was
doing, so the all-endpoints list showed full multi-sentence prose next
to every route instead of a short one-liner.

For every `#[utoipa::path(...)]`-annotated handler across the 19 files
in that module:

- Inserted a blank `///` line after the first short sentence/clause so
  utoipa's split produces a real summary + description, where the doc
  comment had more to say. Left already-short single-clause docs alone
  (nothing to split).
- Where a query struct derives `IntoParams`, moved param prose that
  duplicated a field's own doc comment out of the handler doc (the
  field already documents itself in the generated spec), or added a
  field doc where the handler explained a param that had none.

No behavior changes — doc comments and `params()` description text
only. Verified `cargo build -p hive-c0re` (clean) and `nix fmt` (zero
changes) after.

Closes #2969
2026-08-02 21:35:17 +02:00
damocles
af3976a76a hivectl/dashboard: add --paused / ?paused=1 to agent start 2026-08-02 19:52:11 +02:00
damocles
582ebe5eee hive-c0re: annotate remaining dashboard routes with utoipa 2026-07-31 23:37:05 +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
iris
3239526f98 fix: apply rustfmt and address argus nit (cpu_quota optional chaining)
- rustfmt expanded two inline if-else expressions in lifecycle_ops.rs
  (lines exceeded rustfmt's line width limit)
- core.js: use optional chaining (cv?.cpu_quota || '—') so a null/empty
  cpu_quota/memory_max on ContainerView renders '—' correctly
  (argus review 🟡, PR #2706)
2026-07-26 16:18:35 +02:00
iris
6c9bf2012f dashboard: show and edit per-agent resource limits in core LOAD tab
Adds CPU/memory cap columns and an inline edit form to the container-load
table in /core.html, backed by a new POST /api/resource-limits/{name}
dashboard endpoint.

## Backend (hive-c0re)

lifecycle_ops.rs — new post_resource_limits handler:
- Parses ResourceLimitsForm { cpu_quota, memory_max } (both optional; empty
  string = clear override, fall back to hive-wide default).
- Validates each non-empty value via resource_limits::validate_cpu_quota /
  validate_memory_max — returns 422 UNPROCESSABLE_ENTITY with a human-
  readable message on invalid input so the dashboard can surface it inline.
- Calls meta::commit_resource_limits (staged git write under META_LOCK, same
  as hivectl set-limits).
- Re-applies the drop-in immediately via lifecycle::write_dropins so the new
  ceilings take effect on the next container start without waiting for a
  rebuild.
- Triggers rescan_containers_and_emit so ContainerView.cpu_quota/memory_max
  update via SSE without waiting for the next periodic sweep.

dashboard/mod.rs — registers the route:
  POST /api/resource-limits/{name}

## Frontend (core.js + system-sections.css)

core.js:
- containersState derived from /api/state snapshot alongside tombstonesState
  — supplies configured cpu_quota/memory_max to the LOAD table.
- lastLoadRows stash lets SSE-triggered re-renders call renderContainerLoad
  without waiting for the next 5s poll.
- renderContainerLoad: adds cpu cap / mem cap columns (muted; tooltip
  'configured ceiling — takes effect on next start') sourced from
  ContainerView, plus a per-row S3T toggle button that expands an inline
  edit form with cpu_quota / memory_max text inputs and a S4V3 button.
  The edit form shows a restart hint, surfaces validation errors inline, and
  collapses on success.
- container_state_changed SSE handler: updates containersState in place and
  re-renders the LOAD table so the cap columns flip immediately after a save.

system-sections.css:
- CSS for the new cap columns (.cload-cap-th, .cload-cap) and inline edit
  form (.cload-edit-row, .cload-edit-form, .cload-edit-label, etc.).
- Remove dead .rqe-step rule (step sub-step label retired from the wire in
  'job_queue: retire the now-off-wire step sub-step label').
2026-07-26 16:00:45 +02:00
iris
2cab121b35 fix(dashboard): replace unreachable! with proper 400 in post_pause/post_resume
Returning a 400 Bad Request instead of panicking on an invalid ident
makes the handlers correct in all codepaths, not just the happy path.
2026-07-26 14:11:02 +02:00
iris
59041d4f03 feat(dashboard): add paused badge and pause/resume toggle to agent cards
When ContainerView.paused is true, show a clickable yellow `⏸ paused`
badge on the agent card that POSTs to the new /api/resume/{name} endpoint
to un-park the turn loop. The badge doubles as the resume button so the
state is self-documenting and one click to fix.

The agent action menu gains ⏸ P4US3 (when not paused) and ▶ R3SUM3
(when paused), orthogonal to the running/stopped start/stop actions.

On the backend, /api/pause/{name} and /api/resume/{name} POST routes
wire to Coordinator::set_paused and trigger an immediate rescan so the
badge flips via the existing SSE ContainerUpdate without polling.

Depends on the ContainerView.paused field and Coordinator::set_paused
added in the parent PR.
2026-07-26 14:11:02 +02:00
damocles
144912f8e0 address review: drop backwards-compat request/response aliases, use canonical names 2026-07-19 15:53:09 +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
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
müde
7946e03fde feat(hive-c0re): replace rebuild queue with generic job-DAG queue
jobs are now DAGs of primitive nodes (prebuild, stop-for-update, swap,
reconcile, signal, drain, ...) driven by one scheduler with N build
slots + per-agent lifecycle leases. per-agent power intent (wanted
up/offline) is durable in agent_power.sqlite; Reconcile nodes converge
observed state to it. kills the graceful-stop watcher thread, the
deferred-start follow-up, and the cascade pre-enqueue (fan-out on
MetaLock completion instead). tracker: #2166
2026-07-06 20:36:57 +02:00
atlas
f5f06a5f14 refactor(#1865): consolidate agent + manager socket servers into one
The per-agent and manager sockets ran two parallel dispatchers with
duplicated lifecycle handlers (agent-side topology-gated, manager-side
ungated) plus a manager-only handler set. Collapse to one parameterized
server in socket_server.rs:

- one serve() + dispatch(req, agent, privileged, coord); start() binds
  the per-agent sockets (privileged=false), start_manager() binds the
  manager socket (privileged=true).
- each lifecycle/config handler (start/restart/kill/update/init_config/
  apply_commit) merges its dual: the topology guard (require_child /
  require_new_child) runs only on the !privileged path; init_config
  records the requester as parent only when !privileged. restart keeps
  the orthogonal, capability-gated + audited infra-container branch.
- the agent-state queries (loose-ends / reminder count + rollup) branch
  on privileged: privileged keeps any-target + the "*" hive-wide sweep
  (query_agent_state-gated), non-privileged keeps the topology/cap gate.
- the privileged-only verbs (schedules / meta-inputs / get_logs) plus
  the submit/schedule/watchdog helpers move into socket_server; they are
  reached via dispatch_privileged_only(), which rejects the whole group
  on a non-privileged socket.
- delete manager_server.rs; repoint refs; merge the test modules.

No behavior change: the topology guard still applies on every
non-privileged lifecycle call, the privileged socket still acts on any
agent, and privileged-only verbs are still rejected on agent sockets.
2026-06-22 13:58:52 +02:00
damocles
2966f682ce hive-c0re: route dashboard start/stop through the rebuild queue 2026-06-21 15:04:19 +02:00
damocles
03ea5d601b hive-c0re: graceful agent stop — quiesce harness, flush state, then stop 2026-06-19 11:50:59 +02:00
damocles
55705f17d3 refactor(#1456): extract dashboard lifecycle endpoints into dashboard/lifecycle_ops.rs 2026-06-09 00:12:15 +02:00