diff --git a/docs/agent-lifecycle/agent-hierarchy.md b/docs/agent-lifecycle/agent-hierarchy.md index 8e4f8b08..cb8e78ef 100644 --- a/docs/agent-lifecycle/agent-hierarchy.md +++ b/docs/agent-lifecycle/agent-hierarchy.md @@ -1,8 +1,8 @@ # Agent hierarchy & privileges Every agent has a place in an operator-editable parent/child tree, used -to scope which agents can manage which others. This doc covers how the -tree is stored and edited today, the rules that are meant to run on top +to scope which agents can manage which others. This doc covers how +hive-c0re stores and edits the tree today, the rules that are meant to run on top of it once enforcement is finished, and where the manager still gets special-cased in the meantime. Tracking issue: hyperhive#361 (`$HIVE_FORGE_URL/hyperhive/hyperhive/issues/361`). @@ -24,7 +24,7 @@ Topology lives in the hive-c0re-owned **meta repo**, alongside structural manager that everything hangs under. Hierarchy is built explicitly: an agent that requests a sub-agent gets a requester-as-parent edge written at its `init_config` approval (so -`bob` above was spawned by `alice`), and the operator can reparent any +`alice` spawned `bob` above), and the operator can reparent any agent, including the bootstrap container (`ruth`) — it's just another root. The manager is reparentable like any other agent; there's no "structurally root" carve-out. Its privileges live on its MCP socket, @@ -64,11 +64,11 @@ where system-level facts live. install that hasn't synced yet). - **Reconcile** — runs alongside the periodic meta/flake regeneration. New agents default to root unless they already carry an explicit - parent edge from an `init_config` approval; existing entries - (including operator overrides) are preserved; removed agents drop. + parent edge from an `init_config` approval; Reconcile preserves existing entries + (including operator overrides); removed agents drop. Agents that are approved but not yet spawned keep their edge too, so it survives the gap until the container actually appears. -- **Inject** — each container's parent (if any) is exposed to its own +- **Inject** — hive-c0re exposes each container's parent (if any) to its own environment as `HIVE_PARENT`, so the harness / system-prompt renderer can see it. - **Surface** — every rescan re-reads `topology.json` and populates @@ -98,8 +98,8 @@ umount-old / mount-new / restart-cascade step. | `send` / `recv` routing | parent ↔ same-parent siblings ↔ self ↔ descendants; explicit allow-list for anyone else | | `request_update_meta_inputs` (bump meta lock) | root agents only (today: just `manager`) | -"Ancestor" walks `ContainerView.parent` chains; cycles are guarded by a -visited-set at dispatch time (a malformed `topology.json` can't lock +"Ancestor" walks `ContainerView.parent` chains; a visited-set guards against +cycles at dispatch time (a malformed `topology.json` can't lock the dispatcher into a loop). ## Manager special-casing today diff --git a/docs/agent-lifecycle/approvals.md b/docs/agent-lifecycle/approvals.md index 0bd9dfdb..3eb2f1fd 100644 --- a/docs/agent-lifecycle/approvals.md +++ b/docs/agent-lifecycle/approvals.md @@ -42,7 +42,7 @@ CLI) before it takes effect. What you'll see, and what to do with it: Don't want to approve something? **Deny it** (`DENY` on the dashboard card, or `hivectl approvals deny `) — nothing runs. Either way the -submitting agent is always notified their request was denied; what's +submitting agent is always notified that the operator denied their request; what's optional is only the reason text, which you can add on the dashboard's prompt (cancelling that prompt aborts the whole deny, not just the reason) but not from the CLI. Denying is final: a denied approval @@ -76,8 +76,8 @@ request. `MergeConfigPr` approval; a poll fallback catches any missed webhook. The approval row stores the PR **number** (`commit_ref`) and the PR **head sha at queue time** (`fetched_sha` — the "reviewed" sha). If - the PR head later moves, the stale approval is superseded by a fresh - one pinned to the new head, so the operator always reviews what will + the PR head later moves, a fresh approval pinned to the new head supersedes + the stale one, so the operator always reviews what will actually deploy. 3. The operator reviews the PR **on the forge** (native diff, threaded comments, CI status) and sees a matching card on the dashboard with a @@ -118,13 +118,13 @@ request. ### Withdrawing a pending approval The submitting agent can call `cancel_loose_end(kind: "approval", id)` to -withdraw an approval that hasn't been acted on yet. +withdraw an approval the operator hasn't acted on yet. The row transitions to `ApprovalStatus::Cancelled` (distinct from `Denied`/`Failed`), the dashboard pulls the card out of the pending pane, and `ApprovalResolved { status: "cancelled" }` fires -on the root agent + dashboard channels. Approvals that have already -been approved/denied/failed return an error — the resolution is -final once the operator (or a lifecycle failure) acted on the row. +on the root agent + dashboard channels. Approvals that the operator +(or a lifecycle failure) has already approved, denied, or failed return an error — the resolution is +final once acted on. The socket refuses the `approval` kind with a clear error for any agent that lacks the `approvals` tool group: only an agent with that @@ -178,7 +178,7 @@ kind-specific payload carrier. operator approval. Step 1 of the two-step spawn flow above. - `UpdateMetaInputs` — `commit_ref` stores the JSON-encoded inputs array (`"[]"` = all inputs, `"[\"nixpkgs\"]"` = just nixpkgs, - etc.). `agent` field is set to the requesting root agent. + etc.). hive-c0re sets the `agent` field to the requesting root agent. On approve hive-c0re runs `nix flake update [inputs...]` on the meta flake and commits the resulting lock changes. - `SchedulePrompt` — `commit_ref` stores the JSON-encoded @@ -194,15 +194,15 @@ kind-specific payload carrier. Two ways a row lands in `scheduled_prompts`: - **Operator-direct** (`source = "operator"`): the operator adds a schedule through the dashboard form. Lands in the table immediately, no approval gate — operator action is already the trust boundary. -- **Agent-requested** (`source = "approval:"`): an agent submits a `RequestSchedulePrompt` through its MCP socket (the `request_schedule_prompt` tool, `scheduling` group). An `ApprovalKind::SchedulePrompt` row is queued; on approve, hive-c0re inserts the schedule row with `source = approval:` so the audit trail points back at the operator decision (above). +- **Agent-requested** (`source = "approval:"`): an agent submits a `RequestSchedulePrompt` through its MCP socket (the `request_schedule_prompt` tool, `scheduling` group). hive-c0re queues an `ApprovalKind::SchedulePrompt` row; on approve, it inserts the schedule row with `source = approval:` so the audit trail points back at the operator decision (above). No self-target shortcut: even agent-self schedules need approval. The existing `remind` MCP tool stays the quick self-wake path (no approval, lands directly in the agent's own inbox); this module is the bigger, multi-recipient, operator-visible thing. ### Scheduled prompt worker (catch-up clamp) -When hive-c0re comes back from being down, the worker sees rows whose `next_fire_at_unix` is well in the past. For recurring rows that would mean firing N delayed pulses in a row — spammy and useless. Instead the worker fires **once** per row and bumps `next_fire_at_unix` to the next interval slot ≥ `now`, recording how many cycles were skipped in `last_result` (per-target). Operators see "fired late, caught up from 17 skipped" instead of 17 wake-up storms. +When hive-c0re comes back from being down, the worker sees rows whose `next_fire_at_unix` is well in the past. For recurring rows that would mean firing N delayed pulses in a row — spammy and useless. Instead the worker fires **once** per row and bumps `next_fire_at_unix` to the next interval slot ≥ `now`, recording how many cycles it skipped in `last_result` (per-target). Operators see "fired late, caught up from 17 skipped" instead of 17 wake-up storms. -One-shot rows fire once (if past due, on the next worker pass) and are deleted by the worker; recurring rows survive until cancelled. +The worker fires one-shot rows once (if past due, on the next worker pass) and deletes them; recurring rows survive until cancelled. `targets` is its own table (`scheduled_prompt_targets`) so partial cancellation flips a single row and the dashboard can show last-fired / last-result per recipient. Cancelling every target reaps the parent row on the next worker pass. @@ -239,7 +239,7 @@ Transient broker errors (sqlite lock contention, etc.) get the same - **Recurring rows** re-arm to the next interval slot — the retry self-heals on the next worker pass. -- **One-shot rows** are deleted unconditionally after their single +- **One-shot rows**: the worker deletes them unconditionally after their single fan-out pass; a broker error on a one-shot isn't retried (the operator advisory and `last_result` are the only audit trail). @@ -253,19 +253,18 @@ container, for example `/agents//state/foo.md`). On delivery hive-c0re: from outside the container. 2. **Validates** the path: rejects anything outside the agent's own state subtree, containing `..` (path traversal), or with an empty relative - tail. On rejection the write is skipped and the original message is - delivered inline with a warning — the reminder still fires. -3. **Defends against symlink escape**: after `create_dir_all`, the parent - dir is canonicalized and re-verified to live under the agent's host - state root. The final file is opened with + tail. On rejection hive-c0re skips the write and delivers the + original message inline with a warning — the reminder still fires. +3. **Defends against symlink escape**: after `create_dir_all`, hive-c0re + canonicalizes the parent dir and re-verifies it lives under the agent's host + state root. hive-c0re opens the final file with `O_NOFOLLOW | O_CREAT | O_TRUNC` so an existing symlink at the basename can't redirect the write to an arbitrary host path. 4. **Writes the body to disk** and delivers a short pointer message in its - place, keeping the agent's inbox / wake-prompt small while the bulky - payload is read out of band. + place, keeping the agent's inbox / wake-prompt small while the agent + reads the bulky payload out of band. -Atomicity of the inbox INSERT + `reminders.sent_at` UPDATE is handled -inside `Broker::deliver_reminders_batch`; the scheduler only computes the +`Broker::deliver_reminders_batch` handles atomicity of the inbox INSERT + `reminders.sent_at` UPDATE; the scheduler only computes the body strings before calling it. ### Destroy semantics @@ -354,7 +353,7 @@ from the agent list; if it differs from disk, runs The root agent has `/meta` RO-bound inside its container: `git -C /meta log --oneline` is the swarm-wide deploy log, `cat /meta/flake.lock | jq '.nodes["agent-"].locked'` -resolves which sha each agent is pinned at right now. +resolves which sha the flake pins each agent at right now. Dashboard surfaces the same info as a `deployed:` chip per container row. @@ -409,7 +408,7 @@ repo: | `deployed/` | rebuild succeeded — `main` ff's here | no | | `failed/` | rebuild failed | yes (body = error) | -`deployed/0` is planted at first spawn. `applied/main` is always the +hive-c0re plants `deployed/0` at first spawn. `applied/main` is always the latest `deployed/*`. A `failed/` tree stays browsable forever — `git log --tags` in the applied repo is the audit trail. A denied or failed config PR carries no extra state on the forge side: the PR stays open, and the @@ -477,8 +476,8 @@ repo — so it can push a branch and open a config PR — but not a member of any other agent's, so it can't reach another agent's config through the forge. Branch protection keeps `main` push/merge `core`-only with operator-team approval, so an agent can't fast-forward its own config or -self-merge its PR (see the End-to-end flow + #1787). The tokenised push -URL is passed inline to `git push`, never written into +self-merge its PR (see the End-to-end flow + #1787). hive-c0re passes the tokenised push +URL inline to `git push`, never writing it into `applied//.git/config`; that repo is RO-bind-mounted into the root agent, and a stored token would leak core's admin credential to an agent. @@ -497,7 +496,7 @@ for each entry in current config — not an editing surface. An agent with the `approvals` tool group submits a change the same way -any other change is made: **clone the child's config repo from the +it makes any other change: **clone the child's config repo from the forge into its own state dir, commit on a branch, open a PR**, and let the operator review and approve it. By design, no second, mount-shaped path reaches the same file without the review. @@ -543,7 +542,7 @@ host set up before the tag-driven-deploy + meta-flake scheme (both described above) converges to it automatically. Each phase is a no-op once already applied: -- **Tags**: agents from before the tag-driven scheme are tagged +- **Tags**: hive-c0re tags agents from before the tag-driven scheme `deployed/0` on `main` once. Non-destructive — it doesn't touch live containers, state dirs, or claude creds. - **Meta flake**: rewrites each `applied//flake.nix` to the @@ -579,7 +578,7 @@ Differences from sub-agents: authoritative applied repo (see "Root-agent view of applied" below). - First-deploy spawn bypasses the approval queue (the root agent is required infrastructure). -- The root agent's socket is bound by `socket_server::start_manager`, +- `socket_server::start_manager` binds the root agent's socket, pure transport with no dedicated helpers — it uses the same per-agent runtime dir as any other agent (`/run/hyperhive/agents/ruth/`), not a special manager-only path. @@ -592,7 +591,7 @@ updates the root agent itself. The system prompt (`hive-agent/prompts/system.md`, rendered by `hive-agent/src/prompt.rs`) is the **same for every agent**; what -varies is which MCP tools are surfaced (gated by tool groups and +varies is which MCP tools it surfaces (gated by tool groups and capabilities in `agent.nix`). No `role:manager` block renders only for the root agent. The root agent's approval-gating behaviour comes from its CLAUDE.md / agent-specific instructions, not @@ -648,7 +647,7 @@ commit (spawn / init_config / meta-update / deny, and the autoupdate sweep's `job_queue::templates::rebuild` reapplying the existing main, or the dashboard `↻ R3BU1LD` button when the lock didn't move). When set, `git show ` against `/applied//.git` inside the -bootstrap container yields the exact tree that was referenced. +bootstrap container yields the exact tree the sha referenced. To add a new lifecycle notice: if it needs to drive an immediate turn (something genuinely urgent, like `ContainerCrash`), add a diff --git a/docs/agent-lifecycle/persistence.md b/docs/agent-lifecycle/persistence.md index f36ce344..7c79dbed 100644 --- a/docs/agent-lifecycle/persistence.md +++ b/docs/agent-lifecycle/persistence.md @@ -65,7 +65,7 @@ power-intent registry: next_fire_at_unix / created_at_unix / source ("operator" or "approval:") / cancelled_at_unix / description`. `owner` drives cancel-permission checks (operator vs the submitting - agent). Cancelled rows are tombstoned and reaped by the worker + agent). The worker tombstones and reaps cancelled rows on its next pass. - `scheduled_prompt_targets` — per-target state for each schedule. `schedule_id / target / cancelled_at_unix / @@ -87,16 +87,16 @@ Retention: `hive-c0re::main`. Drops acked message rows older than 30 days (`acked_at IS NOT NULL`). Undelivered + delivered-but-not-acked rows are always kept — the harness `ack_turn`s only after a - successful turn, so an unacked row can still be requeued via - `requeue_inflight` on a crash. + successful turn, so `requeue_inflight` can still requeue + an unacked row on a crash. - Approvals are kept indefinitely — an audit trail. `actions::destroy` rows stay visible to anything that queries by id. -- Scheduled prompts: one-shot rows are deleted on fire by the - worker; recurring rows live until the operator cancels them +- Scheduled prompts: the worker deletes one-shot rows on fire; + recurring rows live until the operator cancels them (`cancel_schedule` MCP / dashboard ✗) which tombstones via `cancelled_at_unix`, then `reap_cancelled` drops the row on the next worker pass. -- `agent_power` rows live until the agent is destroyed (one row per +- `agent_power` rows live until the operator destroys the agent (one row per agent — nothing to vacuum). ### `/harness/hyperhive-events.sqlite` (per agent) @@ -148,9 +148,9 @@ Bin-loop helpers `build_row` + `record` land each row at `turn_end`; writes are best-effort, a sqlite hiccup logs + lets the turn loop continue. -A sibling `bash_commands(ts INTEGER, head TEXT)` table in the same -file is written by the `hive-bash-daemon` (not the harness): one -row per executed bash task recording the normalised command head - +The `hive-bash-daemon` (not the harness) writes a sibling +`bash_commands(ts INTEGER, head TEXT)` table in the same +file: one row per executed bash task recording the normalised command head - the basename of the first real command, looking past `cd repo &&` prefixes, env-assignments, and prefix-runners like `sudo`/`env`. It backs the "favorite tools" view on the /stats page (aggregated @@ -215,7 +215,7 @@ Three indices: as part of the existing hourly vacuum. - `(node_id)` — added by a later migration so a build log row can be looked up by the job-queue node it belongs to (a `hive_jobq` node is - immutable after insert, so the link is recorded on the log row + immutable after insert, so hive-c0re records the link on the log row instead); legacy rows predating the column keep `node_id IS NULL`. Writes are best-effort: `append_stdout` / `append_stderr` / `finish` @@ -313,15 +313,15 @@ Under `/var/lib/hyperhive/agents//`: store + wake `Notify` directly rather than dialling its own socket. `disk_watch` raises a keyed `disk` todo when the filesystem backing this agent's state gets tight, naming the agent's own biggest - directories; the summary is bucketed and carries no raw byte + directories; it buckets the summary and carries no raw byte counts, so an unchanged situation re-upserts as `changed == false` and never re-wakes. Retention, same hourly `hive-agent::vacuum::run` sweep as - `hyperhive-events.sqlite` below: delivered (soft-deleted) reminder - rows are reaped 14 days after delivery, kept that long only to serve - the trailing-window `ReminderRollup` stats; acked todo rows are - reaped 30 days after acking (long enough that only a genuinely quiet + `hyperhive-events.sqlite` below: it reaps delivered (soft-deleted) reminder + rows 14 days after delivery, kept that long only to serve + the trailing-window `ReminderRollup` stats, and reaps acked todo rows + 30 days after acking (long enough that only a genuinely quiet month triggers the "one spurious re-announcement" fallback a reconciled producer like `disk_watch` relies on — see `todos.rs`'s module doc). Un-acked todos and undelivered reminders are never @@ -371,8 +371,8 @@ files committed via the approval flow. `.git/` carries the proposal / approved / building / deployed / failed / denied tag history. Under `/var/lib/hyperhive/meta/` — the swarm-wide deploy flake plus -system-level config files. Single git repo for the whole host; every -hive-c0re mutation that should survive a restart is committed here. +system-level config files. Single git repo for the whole host; hive-c0re +commits every mutation that should survive a restart here. Contents: - `flake.nix` — declares one `nixpkgs` input per agent + one @@ -396,8 +396,8 @@ Contents: var. Absent agents have no extra capabilities. - `resource-limits.json` — per-agent container resource overrides (`{ "sock": { "cpu_quota": "400%", "memory_max": "8G" } }`). - Written by `resource_limits::set_limits`; read where the systemd - drop-in is generated (`lifecycle::write_dropins`), **not** injected + Written by `resource_limits::set_limits`; read where + `lifecycle::write_dropins` generates the systemd drop-in, **not** injected into the container — these are host-side caps on the container, so the capped party never sees or sets them. Fallback is per *field*: an absent file, absent agent, or absent field falls back to the @@ -411,7 +411,7 @@ The root agent has the meta dir RO-mounted at `/meta/`. The `.meta-migration-done` marker no longer exists: the one-shot container repoint it guarded has been removed, since -containers are rendered onto `meta#` at creation. A stale +hive-c0re renders containers onto `meta#` at creation. A stale marker file left over from an older hive is inert and can be deleted. @@ -426,8 +426,8 @@ an agent's state. The mechanics, for completeness: - `PURG3` wipes `/var/lib/hyperhive/{agents,applied}//` — the union of everything `DESTR0Y` left behind. -The root/bootstrap agent's specialness is implemented as a soft policy -guard in `actions::destroy` that refuses to destroy it, backstopped by +`actions::destroy` implements the root/bootstrap agent's specialness as a soft policy +guard that refuses to destroy it, backstopped by `auto_update::ensure_root_agent`, which recreates it on the next hive-c0re startup if it's ever absent (bypassing the approval queue, as required infrastructure) — so even without the guard, destroying it @@ -435,7 +435,7 @@ would only be transient. ### btrfs subvolumes for `/var/lib/hyperhive/agents/` -On a btrfs host, a brand-new agent's state root is created as a +On a btrfs host, `lifecycle::ensure_agent_state_subvolume` creates a brand-new agent's state root as a **btrfs subvolume** instead of a plain directory (progressive enhancement — see the #1762 lane). This is a no-op fallback on non-btrfs hosts and for any agent whose root already exists, so @@ -443,7 +443,7 @@ nothing is automigrated: existing agents keep their plain dirs until an explicit opt-in upgrade. - **Creation:** `lifecycle::ensure_agent_state_subvolume` runs before - the per-agent subdirs are created (spawn / rebuild / InitConfig). + hive-c0re creates the per-agent subdirs (spawn / rebuild / InitConfig). It skips the work when the root already exists; otherwise it asks hive-priv (`EnsureAgentSubvolume`) to `btrfs subvolume create` the root when the FS is btrfs (`statfs` magic gate) and chown it to the @@ -475,8 +475,8 @@ Only present on the one host running `services.hyperhive.deploy.swarm-controller.enable`. systemd `StateDirectory=`, so it survives restarts and redeploys. -- `webhook-secret` — the HMAC key the swarm's forge webhooks are signed - with. **Keep it.** It's handed to Forgejo when a hook is registered, +- `webhook-secret` — the HMAC key Forgejo signs the swarm's forge webhooks + with. **Keep it.** It's handed to Forgejo when swarm-controller registers a hook, so replacing the file means every subsequent delivery fails verification until the hook is re-registered with the new value. It's generated automatically on first start; there is nothing to configure. @@ -565,8 +565,8 @@ avatar (see below). ### matrix avatar (set by the daemon over the live Client) -The agent icon (`hyperhive.icon`, an SVG) is published as each matrix -account's profile avatar by `hive-matrix-daemon` itself +`hive-matrix-daemon` itself publishes the agent icon (`hyperhive.icon`, an SVG) as each matrix +account's profile avatar (`hive-matrix-mcp::client::sync_avatar`), not a separate oneshot. After the daemon builds + restores an account's `Client` (authenticated, pointed at that account's resolved homeserver), it calls matrix-sdk's @@ -574,7 +574,7 @@ pointed at that account's resolved homeserver), it calls matrix-sdk's `avatar_url`. Because it reuses the live Client, there is no hardcoded homeserver URL, no token re-read, and no token-file globbing: the daemon already iterates every configured + dashboard-discovered account in its -bring-up loop, so the avatar is set for **every** account. +bring-up loop, so it sets the avatar for **every** account. Nix rasterizes the SVG to a 512x512 PNG at build time (`iconPng`, via librsvg) and forwards its store path as `HIVE_ICON_PNG` on the daemon @@ -588,6 +588,6 @@ skips the upload when unchanged, because every upload mints a fresh re-uploading identical bytes is timeline spam. A dashboard-provisioned account gets its avatar when the `systemd.paths.hive-matrix-daemon` token watcher restarts the daemon (which re-runs the per-account bring-up), so -no separate avatar trigger is needed. Avatar failures are swallowed +no separate avatar trigger is needed. The daemon swallows avatar failures (logged, non-fatal) so they never break account bring-up or sync.