From a3b672d1d5dc7377510457d0d6df2e2988fb2851 Mon Sep 17 00:00:00 2001 From: atlas Date: Mon, 14 Sep 2026 18:50:24 +0200 Subject: [PATCH] refactor(hive-c0re): drop the request_init_config tool and InitConfig approval swarm-controller's `InitAgentConfigRepo` node already covers config-repo creation, so this deletes a duplicate rather than a capability; old `init_config` rows are skipped by `collect_lenient` with no migration, by operator decision. Refs #4398 --- docs/README.md | 2 +- docs/agent-lifecycle/agent-hierarchy.md | 29 ++--- docs/agent-lifecycle/approvals.md | 44 +++---- docs/agent-lifecycle/persistence.md | 10 +- docs/getting-started/setup.md | 17 ++- docs/process/conventions.md | 2 +- docs/tools/lifecycle.md | 51 +++----- docs/turn-loop/mcp.md | 2 +- docs/web-ui/dashboard.md | 4 +- frontend/packages/dashboard/src/call.js | 9 +- frontend/packages/dashboard/src/tabs.js | 7 +- hive-agent-mcp/src/mcp/args.rs | 14 -- hive-agent-mcp/src/mcp/mod.rs | 46 +------ hive-agent-mcp/src/mcp/render.rs | 8 +- hive-agent/prompts/system.md | 10 +- hive-agent/src/stream_enrich.rs | 3 +- hive-c0re/src/actions.rs | 84 +----------- hive-c0re/src/agent_config/topology.rs | 112 ++-------------- hive-c0re/src/coordinator.rs | 4 +- hive-c0re/src/dashboard/approvals.rs | 10 +- hive-c0re/src/dashboard/state_snapshot.rs | 10 -- hive-c0re/src/lifecycle/host_config.rs | 9 +- hive-c0re/src/lifecycle/setup.rs | 8 +- .../src/socket_server/config_approvals.rs | 122 +----------------- hive-c0re/src/socket_server/mod.rs | 45 +------ hive-c0re/src/stores/approvals.rs | 36 +----- hive-core-agent-sock/src/lib.rs | 7 - hive-sh4re/src/approvals.rs | 6 - hive-sh4re/src/permissions.rs | 6 +- hivectl/README.md | 2 +- nix/reserved-names.nix | 12 +- 31 files changed, 132 insertions(+), 599 deletions(-) diff --git a/docs/README.md b/docs/README.md index fc3c4648..3483f42a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -26,7 +26,7 @@ declarations. ## Agent lifecycle - **How do config changes flow from manager to operator to container?** → - [`agent-lifecycle/approvals.md`](agent-lifecycle/approvals.md) (two-step spawn, approval + [`agent-lifecycle/approvals.md`](agent-lifecycle/approvals.md) (approval kinds, approval state machine, `flake.lock` validation). - **What state survives destroy / purge / restart?** → [`agent-lifecycle/persistence.md`](agent-lifecycle/persistence.md). diff --git a/docs/agent-lifecycle/agent-hierarchy.md b/docs/agent-lifecycle/agent-hierarchy.md index 3eb6a025..7e66d48d 100644 --- a/docs/agent-lifecycle/agent-hierarchy.md +++ b/docs/agent-lifecycle/agent-hierarchy.md @@ -21,11 +21,10 @@ Topology lives in the hive-c0re-owned **meta repo**, alongside `null` = root-level agent. New agents **default to root** — there is no 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 -`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 +explicitly: an agent gets a parent edge written before its first spawn, +or the operator reparents it afterwards (so `bob` above sits under +`alice`). Any agent is reparentable, the bootstrap container (`ruth`) +included — 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, not its tree position (see _Manager special-casing today_ below). @@ -63,10 +62,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; 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. + parent edge written before their first spawn; Reconcile preserves + existing entries (including operator overrides); removed agents drop. + Agents whose config repo exists but that haven't spawned yet keep + their edge too, so it survives the gap until the container actually + appears. - **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. @@ -90,7 +90,6 @@ umount-old / mount-new / restart-cascade step. | operation | who can do it | | ----------------------------------------------------------- | --------------------------------------------------------------------------------------- | | `kill` / `start` / `restart` / `update` (any descendant) | any ancestor | -| `request_init_config` (spawn a new child) | any agent, child added under self | | config change via forge PR (any descendant's config) | any ancestor | | `get_logs` (any descendant) | any ancestor | | moderate reminders (cancel any open thread of a descendant) | any ancestor | @@ -110,15 +109,15 @@ other agents don't: - **Naming/bootstrap** — the manager's broker recipient name, state-dir key, and nixos-container name are all `ruth` (container `h-ruth`). `hive-c0re` spawns it directly at boot if missing, with no operator - approval step — every other agent goes through `request_init_config` - → approval. Topology-wise, `ruth` is still just another root agent. + approval step — every other agent goes through a `Spawn` approval. + Topology-wise, `ruth` is still just another root agent. - **Wire-protocol** — the privileged `Request` variants - (`RequestInitConfig`; `Kill` / `Start` / `Restart` / `Update`; + (`Kill` / `Start` / `Restart` / `Update`; `GetLogs`; `RequestUpdateMetaInputs`) — marked `*(privileged)*` in `hive-core-agent-sock`'s unified `Request` enum — are reachable only from the manager's socket flavour today. Planned rule for each is in the - table above ("any agent, child added under self" for init-config, - "any ancestor" for lifecycle/logs); `RequestUpdateMetaInputs` stays + table above ("any ancestor" for lifecycle/logs); + `RequestUpdateMetaInputs` stays a root-only capability even post-milestone, not a topology rule. One exception: `Wake` (inject a `from: ` message into the caller's own inbox) isn't really privileged — every per-agent daemon diff --git a/docs/agent-lifecycle/approvals.md b/docs/agent-lifecycle/approvals.md index ffd3aa32..80222b13 100644 --- a/docs/agent-lifecycle/approvals.md +++ b/docs/agent-lifecycle/approvals.md @@ -24,12 +24,14 @@ CLI) before it takes effect. What you'll see, and what to do with it: anything in that chain fails, the change rolls back automatically — the agent stays on its last-good config, no recovery action needed from you. -- **New agent** (`InitConfig` then `Spawn`) — creating a brand-new - agent is two approvals. `InitConfig` creates the config repo and - seeds it from a template; `Spawn` creates the container from that - config. Tailoring the template first isn't a separate mechanism — - it's the config-change flow above, a PR you review like any other. Every later change goes through the config-change flow - above — there's no repeat "spawn" for an existing agent. +- **New agent** (`Spawn`) — the one approval in creating a brand-new + agent. Its config repo is scaffolded first, outside the approval + queue, by the swarm controller's `InitAgentConfigRepo` job + (`POST /api/agents`); `Spawn` then creates the container from that + config. Tailoring what the template seeded isn't a separate + mechanism — it's the config-change flow above, a PR you review like + any other. Every later change goes through that flow — there's no + repeat "spawn" for an existing agent. - **Meta/flake update** (`UpdateMetaInputs`) — an agent asked to bump one or more Nix flake inputs (or all of them). Approving runs the update and commits the lock change; it doesn't rebuild anything by @@ -131,11 +133,10 @@ agent that lacks the `approvals` tool group: only an agent with that group submits approvals (for its direct children), so an agent without it has nothing of its own to withdraw. -`InitConfig` approvals create a brand-new agent's config repo. On -approve, hive-c0re seeds it with a default `agent.nix` template and -pushes a todo (`push_todo_submitter`) into the submitting agent's -in-container store. The operator then **spawns** the agent (the -`Spawn` approval / `◆ R3QU3ST SP4WN` button), which creates the +A brand-new agent's config repo is created outside this queue, by the +swarm controller's `InitAgentConfigRepo` job, which seeds it with a +default `agent.nix` template. The operator then **spawns** the agent +(the `Spawn` approval / `◆ R3QU3ST SP4WN` button), which creates the container from that config. Changing what the template seeded isn't a special case: like every @@ -146,7 +147,7 @@ through the web UI or the forge. ### Approval kinds (wire shapes) -`ApprovalKind` carries five variants; each maps to a different +`ApprovalKind` carries four variants; each maps to a different `commit_ref` encoding because that field is overloaded as the kind-specific payload carrier. @@ -169,13 +170,11 @@ kind-specific payload carrier. `hivectl agent request-create` CLI). The host-level `HostRequest::Spawn` variant bypasses the approval queue entirely — privileged-context use only (operator on the host shell, test scripts, one-off recoveries; - `hivectl agent create`). This is the **canonical first-spawn**: a new agent's `InitConfig` - seeds its config repo, the submitting agent customises it, then the - operator spawns to create the container. Subsequent config changes go - through a `MergeConfigPr` PR. -- `InitConfig` — `commit_ref` is empty; the variant just gates - "seed the proposed repo with the default template" against - operator approval. Step 1 of the two-step spawn flow above. + `hivectl agent create`). This is the **canonical first-spawn**: the + swarm controller's `InitAgentConfigRepo` job seeds the agent's config + repo, it gets customised through a PR, then the operator spawns to + create the container. Subsequent config changes go through a + `MergeConfigPr` PR. - `UpdateMetaInputs` — `commit_ref` stores the JSON-encoded inputs array (`"[]"` = all inputs, `"[\"nixpkgs\"]"` = just nixpkgs, etc.). hive-c0re sets the `agent` field to the requesting root agent. @@ -421,7 +420,6 @@ rather than run inline: | `MergeConfigPr` | `rebuild` (`DeployWindow` root + `MergeVerify → DeployApply` + `DeployTail`) | `approval` | | `UpdateMetaInputs` | `meta_update` (`MetaLock` + rebuild fan-out) | `approval` | | `Spawn` | `spawn` (`Create → WriteDropin → Reconcile`) | `approval` | -| `InitConfig` | — runs inline (sub-second git seed) | — | | `SchedulePrompt` | — runs inline (single sqlite insert) | — | The DAG carries the originating `approval_id`, surfaced on the node that @@ -604,7 +602,7 @@ as a regular `system` inbox message so it drives a normal claude turn. through `Coordinator::push_todo`/`push_todo_submitter` instead, a direct live dial of the target agent's in-container todo socket (same `UpsertTodo` request in-container producers use); `finish_approval` fires -one of these too for `InitConfig`/`Spawn`/`MergeConfigPr`, *in addition to* +one of these too for `Spawn`/`MergeConfigPr`, *in addition to* the `ApprovalResolved` HelperEvent above, not instead of it. Legacy approval rows that predate the submitter column fall back to the root agent. Variants (`hive_sh4re::manager::HelperEvent`): @@ -625,7 +623,7 @@ root agent. Variants (`hive_sh4re::manager::HelperEvent`): no approval required. The remaining lower-urgency lifecycle notices — `Rebuilt`, `Killed`, -`Destroyed`, `NeedsLogin`, `LoggedIn`, `ConfigReady` — are "FYI, check +`Destroyed`, `NeedsLogin`, `LoggedIn` — are "FYI, check when convenient" events with no reason to drive an immediate turn, so they deliver via `push_todo`/`push_todo_submitter` (see above) instead of `HelperEvent`: an `agent_todo_socket` push instead of a broker @@ -638,7 +636,7 @@ hive-c0re-vouched commit sha. Optional `tag` carries the deploy bookkeeping tag — `deployed/` on a successful build or `failed/` on a failed one, planted by the `MergeConfigPr` deploy. Both fields are `Option`: `None` on the paths that don't deploy a new -commit (spawn / init_config / meta-update / deny, and the autoupdate +commit (spawn / 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 diff --git a/docs/agent-lifecycle/persistence.md b/docs/agent-lifecycle/persistence.md index 521556eb..218b7ce2 100644 --- a/docs/agent-lifecycle/persistence.md +++ b/docs/agent-lifecycle/persistence.md @@ -58,7 +58,7 @@ power-intent registry: below](#state-dirs-per-agent) for where reminders (and todos) actually live now. - `approvals` — the queue. `agent / kind (merge_config_pr | spawn | - init_config | update_meta_inputs | schedule_prompt) / + update_meta_inputs | schedule_prompt) / commit_ref / requested_at / status / resolved_at / note`. - `scheduled_prompts` — recurring + one-shot prompt queue. `owner / body / interval_seconds (NULL = one-shot) / @@ -353,9 +353,9 @@ tree anyone edits in place. Mounting it writable would leave a second path to the same file that skips the review entirely, which makes the boundary a convention rather than a permission. -⚠️ Not to be confused with the seeding done when an `InitConfig` -approval resolves: that writes the child's initial config repo as -**hive-c0re, against the host path**, and `read_only` on a bind +⚠️ Not to be confused with the config-repo seeding hive-c0re does at +spawn (`lifecycle::setup_proposed`): that writes the child's initial +config repo as **hive-c0re, against the host path**, and `read_only` on a bind constrains writers *inside* a container only. The two are unrelated — conflating them can lead you to reason your way into thinking this mount should be writable when it shouldn't. @@ -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 - hive-c0re creates the per-agent subdirs (spawn / rebuild / InitConfig). + hive-c0re creates the per-agent subdirs (spawn / rebuild). 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 diff --git a/docs/getting-started/setup.md b/docs/getting-started/setup.md index 0bb74dcd..66fcbc0c 100644 --- a/docs/getting-started/setup.md +++ b/docs/getting-started/setup.md @@ -247,18 +247,17 @@ a manual `hivectl` step — see _Swarm SSO_ above (`swarmctl user add`). ### 7 · Spawn sub-agents -Sub-agent creation goes through the approval queue — ruth proposes, the -operator approves, the container builds. From ruth's own turn (inside -the container, via MCP tools): +Sub-agent creation is an operator action — agents have no tool for it. +Two steps: ``` -# Step 1: initialise a new agent's config repo -request_init_config(name: "iris") -# → operator approves → config_ready event lands in the inbox +# Step 1: scaffold the new agent's config repo. The swarm controller's +# InitAgentConfigRepo job does this (POST /api/agents), seeding +# /agents/iris/config/agent.nix from the default template. -# Step 2: edit /agents/iris/config/agent.nix and commit it. Then the -# operator spawns iris (dashboard ◆ R3QU3ST SP4WN / Spawn approval), -# which builds + starts the container from that config. +# Step 2: edit /agents/iris/config/agent.nix and commit it. Then spawn +# iris from the dashboard (◆ R3QU3ST SP4WN / Spawn approval), which +# builds + starts the container from that config. # Later config changes: open a PR on agent-configs/iris (hive-forge); # the operator reviews + approves it — no MCP tool call. diff --git a/docs/process/conventions.md b/docs/process/conventions.md index f3728661..4872cf81 100644 --- a/docs/process/conventions.md +++ b/docs/process/conventions.md @@ -311,7 +311,7 @@ binary flavor. | `inbox` | `get_loose_ends`, `cancel_loose_end`, `remind` | | `execution` | vestigial — `mcp__bash__run` / `mcp__bash__status` are always available unconditionally via `extraMcpServers`; this group's entries expand to non-existent `mcp__hyperhive__run` / `mcp__hyperhive__status` and have no effect. See `docs/tools/bash.md`. | | `lifecycle` | `kill`, `start`, `restart`, `update`, `list_containers` *(privileged)* | -| `approvals` | `request_init_config`, `request_update_meta_inputs` *(privileged)* | +| `approvals` | `request_update_meta_inputs` *(privileged)* | | `scheduling` | `request_schedule_prompt`, `fire_schedule_now`, `cancel_schedule`, `edit_schedule`, `list_schedules` *(privileged)* | | `diagnostics` | `get_logs` *(privileged)* | | `forge` | `create_repo` — create git repos through hive-c0re (operator-gated merge) | diff --git a/docs/tools/lifecycle.md b/docs/tools/lifecycle.md index fd13b0b4..c59612fb 100644 --- a/docs/tools/lifecycle.md +++ b/docs/tools/lifecycle.md @@ -39,38 +39,20 @@ one-row answer naming itself. ## `approvals` tool group -Config changes and new-agent spawns route through the operator -approval queue. Topology-enforced the same way. +Meta-flake input bumps route through the operator approval queue. -### `request_init_config(name, description?)` +Creating a new agent is **not** in this group — agents have no tool for +it. A new agent's config repo is scaffolded by the swarm controller's +`InitAgentConfigRepo` job (`POST /api/agents`, see +`swarm-controller/`), and the operator spawns the container from the +dashboard (`◆ R3QU3ST SP4WN` / `Spawn` approval, routed via +`HostRequest::RequestSpawn`). -Step 1 of spawning a new sub-agent. Queues an `InitConfig` -approval; on operator approve, hive-c0re seeds the proposed config -repo at `/agents//config/agent.nix` with a default template and -delivers a `config_ready` system event. Then edit `agent.nix`, commit, -and the operator **spawns** the agent (the dashboard `◆ R3QU3ST SP4WN` -button / `Spawn` approval, routed via `HostRequest::RequestSpawn`), -which creates the container from that config. - -Subsequent config changes go through a **forge PR** on the agent's -`agent-configs/` repo (queues a `MergeConfigPr` approval on -open/update — no MCP tool involved), not a tool call. See +Config changes on an existing agent go through a **forge PR** on the +agent's `agent-configs/` repo (queues a `MergeConfigPr` approval +on open/update — no MCP tool involved), not a tool call. See `docs/agent-lifecycle/approvals.md`. -`name` must be either unused — in which case the caller becomes its -parent on approval — or an agent already in the caller's subtree whose -config is being re-seeded. The server refuses a name that exists outside -that subtree, so one agent can't hijack another's. - -Only **direct** children's config repos are bind-mounted into a parent's -container, though (`bind_child_agent_dirs`, driven by -`topology::children_of`). The server accepts re-seeding an agent further -down the subtree, and that still leaves the caller without a local copy -to edit afterwards. - -Fails if a proposed config repo for `name` already exists. -`name` is ≤ 9 characters. - ### `request_update_meta_inputs(inputs?, description?)` Queue an approval to run `nix flake update [inputs...]` on the meta @@ -83,15 +65,14 @@ agents after the approval resolves. ## Boundary summary -| Operation | Requires approval? | Scope | -| --------------------------------------- | ------------------ | ---------------------------------- | -| `kill` / `start` / `restart` / `update` | No | Own subtree | -| `list_containers` | No | Own subtree, caller included | -| `request_init_config` | Yes (InitConfig) | Unused name, or one in own subtree | -| `request_update_meta_inputs` | Yes (MetaUpdate) | Meta flake (global) | +| Operation | Requires approval? | Scope | +| --------------------------------------- | ------------------ | ---------------------------- | +| `kill` / `start` / `restart` / `update` | No | Own subtree | +| `list_containers` | No | Own subtree, caller included | +| `request_update_meta_inputs` | Yes (MetaUpdate) | Meta flake (global) | ## See also - [`docs/agent-lifecycle/approvals.md`](../agent-lifecycle/approvals.md) — full approval flow, kinds, - helper events (`config_ready`, `approval_resolved`), flake.lock + helper events (`approval_resolved`), flake.lock validation. diff --git a/docs/turn-loop/mcp.md b/docs/turn-loop/mcp.md index 90abcc89..eb54fc57 100644 --- a/docs/turn-loop/mcp.md +++ b/docs/turn-loop/mcp.md @@ -66,7 +66,7 @@ object with an `event` discriminant field). The **submitting agent** tool group for its own subtree) receives `container_crash`, `needs_update`, and `approval_resolved` this way. The remaining, lower-urgency lifecycle notices — `spawned`, `rebuilt`, `killed`, `destroyed`, `needs_login`, -`logged_in`, `config_ready` — skip the inbox entirely: they land as +`logged_in` — skip the inbox entirely: they land as todos on the submitting agent's in-container todo socket instead (`Coordinator::push_todo`/`push_todo_submitter`, `subsystem = "core"`), which still wakes a turn (the todo-wake path — see [Turn diff --git a/docs/web-ui/dashboard.md b/docs/web-ui/dashboard.md index 922cfcdf..1751f6e2 100644 --- a/docs/web-ui/dashboard.md +++ b/docs/web-ui/dashboard.md @@ -1060,7 +1060,6 @@ renderApprovals`) with three stacked sections: | `merge_config_pr` | `⇒` | `merge-pr` | PR-head sha (`sha_short`) | | `update_meta_inputs` | `↻` | `meta-update` | — | | `schedule_prompt` | `⏱` | `schedule` | — | - | `init_config` | `⊕` | `init` | — | | `spawn` | `⊕` | `spawn` | — | The chip ticks live every second via a `data-requested-at` @@ -1074,8 +1073,7 @@ renderApprovals`) with three stacked sections: config PR into `agent-configs//pulls/` (shown only when `forge_present` and `pr_number` is set). The config diff lives on the forge PR itself — no inline diff side-panel. - - `init_config` / `spawn`: a one-line "container will be created" - note instead. + - `spawn`: a one-line "container will be created" note instead. - **decision actions** — `◆ APPR0VE` and `DENY`. Deny pops a `prompt()` for an optional reason carried to the submitting agent as `HelperEvent::ApprovalResolved.note`. diff --git a/frontend/packages/dashboard/src/call.js b/frontend/packages/dashboard/src/call.js index 470e40ff..943962e9 100644 --- a/frontend/packages/dashboard/src/call.js +++ b/frontend/packages/dashboard/src/call.js @@ -232,7 +232,6 @@ export function renderApprovals() { const ul = el("ul", { class: "approvals" }); for (const a of pending) { - const isInit = a.kind === "init_config"; const isMergePr = a.kind === "merge_config_pr"; const isUpdateMeta = a.kind === "update_meta_inputs"; const isSchedule = a.kind === "schedule_prompt"; @@ -262,9 +261,7 @@ export function renderApprovals() { ? "meta-update" : isSchedule ? "schedule" - : isInit - ? "init" - : "spawn", + : "spawn", ), ); if (isMergePr && a.sha_short) head.append(el("code", {}, a.sha_short)); @@ -440,9 +437,7 @@ function renderApprovalHistory(root, history) { ? "meta-update" : a.kind === "schedule_prompt" ? "schedule" - : a.kind === "init_config" - ? "init" - : "spawn", + : "spawn", ), " ", ); diff --git a/frontend/packages/dashboard/src/tabs.js b/frontend/packages/dashboard/src/tabs.js index 54fe2979..6c53a445 100644 --- a/frontend/packages/dashboard/src/tabs.js +++ b/frontend/packages/dashboard/src/tabs.js @@ -81,12 +81,7 @@ window.marked = marked; for (const a of approvals) { if (seenApprovals.has(a.id)) continue; seenApprovals.add(a.id); - const verb = - a.kind === "spawn" - ? "spawn approval" - : a.kind === "init_config" - ? "config-init approval" - : "config commit"; + const verb = a.kind === "spawn" ? "spawn approval" : "config commit"; NOTIF.show( "◆ approval #" + a.id, `${verb} for ${a.agent}`, diff --git a/hive-agent-mcp/src/mcp/args.rs b/hive-agent-mcp/src/mcp/args.rs index b4576b08..1f88bbaa 100644 --- a/hive-agent-mcp/src/mcp/args.rs +++ b/hive-agent-mcp/src/mcp/args.rs @@ -97,20 +97,6 @@ pub struct CompactArgs { // Privileged tool arg types (lifecycle, approvals, scheduling, diagnostics) // ----------------------------------------------------------------------------- -#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] -pub struct RequestInitConfigArgs { - /// New sub-agent name (≤9 chars). Queues an `InitConfig` approval; on - /// approval hive-c0re creates the child's config repo and seeds it with a - /// default `agent.nix`. Approving the follow-up `Spawn` creates the - /// container. Config changes — including the child's first — are PRs on - /// that repo, made from a clone, reviewed + approved by the operator; - /// `/agents//config` is a read-only copy, not an editing surface. - pub name: String, - /// Optional description shown on the dashboard approval card. - #[serde(default)] - pub description: Option, -} - #[derive(Debug, serde::Deserialize, schemars::JsonSchema)] pub struct KillArgs { /// Sub-agent name (without the `h-` container prefix). diff --git a/hive-agent-mcp/src/mcp/mod.rs b/hive-agent-mcp/src/mcp/mod.rs index cbb35631..4d657e0b 100644 --- a/hive-agent-mcp/src/mcp/mod.rs +++ b/hive-agent-mcp/src/mcp/mod.rs @@ -26,9 +26,8 @@ mod render; pub use args::{ AckUntilArgs, AgentGetLooseEndsArgs, CancelLooseEndArgs, CancelScheduleArgs, CompactArgs, CreateRepoArgs, EditScheduleArgs, FireScheduleNowArgs, GetAgentMetaArgs, GetHostJournalArgs, - GetLogsArgs, KillArgs, MarkTodosDoneArgs, RecvArgs, RemindArgs, RequestInitConfigArgs, - RequestSchedulePromptArgs, RestartArgs, SendArgs, SetStatusArgs, StartArgs, UpdateArgs, - UpdateMetaInputsArgs, + GetLogsArgs, KillArgs, MarkTodosDoneArgs, RecvArgs, RemindArgs, RequestSchedulePromptArgs, + RestartArgs, SendArgs, SetStatusArgs, StartArgs, UpdateArgs, UpdateMetaInputsArgs, }; pub use render::{annotate_retries, format_ack, format_agent_meta, format_recv}; @@ -710,47 +709,6 @@ impl AgentServer { .await } - // IMPORTANT: this tool is only available when the `approvals` tool group - // is configured for the agent (`HIVE_TOOL_GROUPS` contains `approvals`). - // hive-c0re performs a topology check server-side: an unused `name` is - // accepted from any caller (the requester becomes its parent); an existing - // agent is accepted only from inside the caller's subtree. - #[tool(description = "Create a new agent's config repo and queue an \ - `InitConfig` approval for the operator to review. Requires the `approvals` tool \ - group. `name` must be either unused — in which case you become its parent — or an \ - agent already in your subtree, whose config you are re-seeding. Fails if a \ - config repo for that agent already exists. This tool **creates the repo** and \ - nothing else: on approval hive-c0re seeds it with a default `agent.nix`, and \ - approving the follow-up `Spawn` creates the container. \ - Every config change — the child's first one included — goes through a PR on its \ - config repo, made from a clone you take yourself, reviewed + approved by the \ - operator. `/agents//config` is a **read-only copy** for reading a config, \ - never an editing surface.")] - async fn request_init_config( - &self, - Parameters(args): Parameters, - ) -> String { - let log = format!("{args:?}"); - let name = args.name.clone(); - run_tool_envelope("request_init_config", log, async move { - let (resp, retries) = self - .dispatch(hive_core_agent_sock::Request::RequestInitConfig { - name: args.name, - description: args.description, - }) - .await; - annotate_retries( - format_ack( - resp, - "request_init_config", - format!("init_config approval queued for {name}"), - ), - retries, - ) - }) - .await - } - // IMPORTANT: this tool is only available when the `lifecycle` tool group // is granted to this agent. hive-c0re enforces the topology check // server-side: the call is rejected unless `name` is in the caller's diff --git a/hive-agent-mcp/src/mcp/render.rs b/hive-agent-mcp/src/mcp/render.rs index c16afb7e..48563a1b 100644 --- a/hive-agent-mcp/src/mcp/render.rs +++ b/hive-agent-mcp/src/mcp/render.rs @@ -643,12 +643,12 @@ mod tests { Ok(hive_core_agent_sock::Response::OkWarn { warnings: vec!["name is reserved".to_owned(), "second thing".to_owned()], }), - "request_init_config", - "init_config approval queued for forge".to_owned(), + "request_update_meta_inputs", + "update_meta_inputs approval queued".to_owned(), ); // The operation HAPPENED — dropping the success line would read as a // failure and invite a retry that queues a second approval. - assert!(out.starts_with("init_config approval queued for forge")); + assert!(out.starts_with("update_meta_inputs approval queued")); assert!(out.contains("⚠️ name is reserved")); assert!(out.contains("⚠️ second thing")); } @@ -659,7 +659,7 @@ mod tests { // warning marker would pass the test above. let out = format_ack( Ok(hive_core_agent_sock::Response::Ok), - "request_init_config", + "request_update_meta_inputs", "queued".to_owned(), ); assert_eq!(out, "queued"); diff --git a/hive-agent/prompts/system.md b/hive-agent/prompts/system.md index 5b16f7cf..e4a98c31 100644 --- a/hive-agent/prompts/system.md +++ b/hive-agent/prompts/system.md @@ -5,23 +5,23 @@ Tools (hyperhive surface). Full signature + behavior for each comes from the too - **Inbox / messaging** (always available): `mcp__hyperhive__recv`, `mcp__hyperhive__ack_until`, `mcp__hyperhive__send`, `mcp__hyperhive__get_loose_ends`, `mcp__hyperhive__cancel_loose_end`, `mcp__hyperhive__mark_todos_done`, `mcp__hyperhive__remind`, `mcp__hyperhive__set_status`, `mcp__hyperhive__get_agent_meta`. One habit worth internalizing beyond the tool descriptions themselves: prefer ending the turn over repeatedly polling `recv` when idle (only turn-boundaries observe in-container todo wakes — bash-task completions, matrix unread, forge activity — and ending the turn is also your checkpoint). For a large todo backlog (`get_loose_ends` caps at 40 rows), clear reviewed ids in bulk with `mark_todos_done` rather than cancelling one at a time — there's no blind range-clear, only ids you've actually looked at. - **Extra MCP tools** (some agents only): `mcp____` — agent-specific (matrix client, scraper, db connector, etc.) declared in your `agent.nix` under `hyperhive.extraMcpServers`. First-class tools, already operator-approved at deploy time. - **Lifecycle** (_requires `lifecycle` tool group_, your own subtree — children, their children, and so on down, no approval needed): `restart`, `kill`, `start`, `update`, `list_containers`. -- **Approvals** (_requires `approvals` tool group_, queues an operator approval): `request_init_config`, `request_update_meta_inputs`. +- **Approvals** (_requires `approvals` tool group_, queues an operator approval): `request_update_meta_inputs`. - **Scheduling** (_requires `scheduling` tool group_): `request_schedule_prompt` (queues an approval), `cancel_schedule`, `fire_schedule_now`, `edit_schedule`, `list_schedules` (these four don't need approval — you can manage schedules you own or that a sub-agent in your subtree owns). - **Diagnostics**: `get_logs` (_requires `diagnostics` tool group_), `get_host_journal` (_requires `read_host_journal` capability_). -Need new packages, env vars, or other NixOS config for yourself? You can't edit your own config directly — ask a peer agent with the `approvals` tool group, or contact the operator directly. Config repos live at `/agents/{label}/config/` (read-only inside your container). All changes flow through operator-approved commits. +Need new packages, env vars, or other NixOS config for yourself? You can't edit your own config directly — open a PR on your `agent-configs/` repo, or contact the operator directly. Config repos live at `/agents/{label}/config/` (read-only inside your container). All changes flow through operator-approved commits. Your config repo is mounted **read-only** at `/agents/{label}/config/` — `agent.nix` plus whatever extra files define you (declared packages, env vars, MCP servers). Read it to see exactly what defines you before asking for a change, so you can point at the precise file and line. -Approval boundary: lifecycle ops on _existing_ agents in your subtree (`kill`, `start`, `restart`) are at your discretion — no operator approval needed (requires `lifecycle` tool group). _Creating_ a new agent starts with `request_init_config` (requires `approvals` tool group), then the operator spawns it from the dashboard. _Changing_ any agent's config is not a tool call at all — it's a forge PR on the agent's `agent-configs/` repo, which queues a `MergeConfigPr` approval on open/update. The operator only signs off on changes; you run the day-to-day. +Approval boundary: lifecycle ops on _existing_ agents in your subtree (`kill`, `start`, `restart`) are at your discretion — no operator approval needed (requires `lifecycle` tool group). _Creating_ a new agent is not something you can do from here — ask the operator, who scaffolds the new agent's config repo and spawns it from the dashboard. _Changing_ any agent's config is not a tool call at all — it's a forge PR on the agent's `agent-configs/` repo, which queues a `MergeConfigPr` approval on open/update. The operator only signs off on changes; you run the day-to-day. Messages from sender `system` are hyperhive helper events (JSON body, `event` field discriminates): `approval_resolved`, `container_crash`, `needs_update`. Use these to react to lifecycle changes: - `needs_update` — agent's flake rev is stale. Call `update(name)` to rebuild — it's idempotent and doesn't need approval. - `container_crash` — restart with `start(name)`. If it crashes again, ask the operator. -- `approval_resolved` — one of your own submitted approvals (`request_init_config`, `request_update_meta_inputs`, a scheduled prompt, …) was approved, denied, or failed; the body carries the resolution. +- `approval_resolved` — one of your own submitted approvals (`request_update_meta_inputs`, a scheduled prompt, a config PR, …) was approved, denied, or failed; the body carries the resolution. -Lifecycle notices that don't need an immediate turn — a new agent spawned, its config repo seeded, a container rebuilt/killed/destroyed, or its login state changing — surface as todos instead of messages now. Call `get_loose_ends` to see them. +Lifecycle notices that don't need an immediate turn — a new agent spawned, a container rebuilt/killed/destroyed, or its login state changing — surface as todos instead of messages now. Call `get_loose_ends` to see them. Durable knowledge: diff --git a/hive-agent/src/stream_enrich.rs b/hive-agent/src/stream_enrich.rs index 67c921c3..4fbfb56e 100644 --- a/hive-agent/src/stream_enrich.rs +++ b/hive-agent/src/stream_enrich.rs @@ -646,8 +646,7 @@ fn fmt_hyperhive_tool(name: &str, short: &str, input: &Value) -> String { "mcp__hyperhive__kill" | "mcp__hyperhive__restart" | "mcp__hyperhive__start" - | "mcp__hyperhive__update" - | "mcp__hyperhive__request_init_config" => format!("{short} {}", sv(input, "name")), + | "mcp__hyperhive__update" => format!("{short} {}", sv(input, "name")), "mcp__hyperhive__ack_until" => { let up_to = input .get("up_to") diff --git a/hive-c0re/src/actions.rs b/hive-c0re/src/actions.rs index a56d162b..23fa8de5 100644 --- a/hive-c0re/src/actions.rs +++ b/hive-c0re/src/actions.rs @@ -12,11 +12,10 @@ use hive_sh4re::manager::HelperEvent; use crate::coordinator::Coordinator; use crate::lifecycle; -/// Approve a pending request. Marks the approval row durably, then -/// either runs the work inline (`InitConfig`, sub-second git ops) or -/// submits it to the job queue so the dashboard POST returns -/// immediately while the long-running pipeline runs off-thread -/// (operator no longer blocks on a 30-90s spinner for `MergeConfigPr`). +/// Approve a pending request. Marks the approval row durably, then submits +/// the work to the job queue so the dashboard POST returns immediately while +/// the long-running pipeline runs off-thread (operator no longer blocks on a +/// 30-90s spinner for `MergeConfigPr`). /// /// Dispatch: /// - `MergeConfigPr` → a `DeployWindow` DAG (`MergeVerify → DeployApply → @@ -24,7 +23,6 @@ use crate::lifecycle; /// resource-holding root; ~30-90s) /// - `UpdateMetaInputs` → a `MetaUpdate` DAG (fan-out on completion) /// - `Spawn` → a `Spawn` DAG (`Create → WriteDropin → Reconcile`) -/// - `InitConfig` → inline (<1s; queue card would be noise) /// /// Every queued kind — deploys included — resolves its approval row via /// [`resolve_approval_dag`] when the DAG settles terminal. @@ -38,15 +36,6 @@ pub async fn approve(coord: Arc, id: i64) -> Result<()> { "approval: dispatching", ); match approval.kind { - ApprovalKind::InitConfig => { - // Sub-second git seed + forge-remote wire. Routing through - // the queue would surface a queue card that's gone before - // the operator's eyes refocus. Run inline. - let proposed_dir = Coordinator::agent_proposed_dir(&approval.agent); - let claude_dir = Coordinator::agent_claude_dir(&approval.agent); - let notes_dir = Coordinator::agent_notes_dir(&approval.agent); - run_approval_init_config(&coord, approval, proposed_dir, claude_dir, notes_dir).await - } ApprovalKind::UpdateMetaInputs => { // Inputs JSON-encoded into commit_ref by the manager's // submit path — surface them on the DAG so the dashboard @@ -638,49 +627,6 @@ async fn forge_after_first_spawn(coord: &Arc, agent: &str) { crate::dashboard::emit_tombstones_snapshot(coord).await; } -/// Inline (non-queued) handler for `ApprovalKind::InitConfig`. Just -/// seeds the proposed git repo + the per-agent dirs — sub-second -/// work that doesn't justify a queue card. -async fn run_approval_init_config( - coord: &Coordinator, - approval: hive_sh4re::approvals::Approval, - proposed_dir: std::path::PathBuf, - claude_dir: std::path::PathBuf, - notes_dir: std::path::PathBuf, -) -> Result<()> { - let result: Result<()> = async { - // Place the new child under its requesting parent (carried in - // commit_ref by submit_init_config). An empty commit_ref means - // no explicit parent was named (privileged manager socket, or an - // approval queued before the new-child feature) — write no edge - // and let `topology::reconcile` assign the default position on - // first spawn, so this path never names a specific root agent. - if !approval.commit_ref.is_empty() { - crate::topology::add_child(approval.agent.as_str(), &approval.commit_ref) - .map_err(|e| anyhow::anyhow!("topology add_child: {e}"))?; - } - // Create the agent's state root as a btrfs subvolume FIRST, before - // any dir-seed touches it. `ensure_agent_state_subvolume` is - // progressive ("root exists → skip"), and `setup_proposed` does a - // `create_dir_all` on the proposed-config path which would - // materialise the state root as a plain directory — after which - // the subvolume create is silently skipped and the agent never - // lands on a subvolume (no quota, no snapshot). Order matters. - lifecycle::ensure_agent_state_subvolume(approval.agent.as_str()).await?; - lifecycle::setup_proposed(&proposed_dir, approval.agent.as_str()).await?; - lifecycle::ensure_claude_dir(&claude_dir)?; - lifecycle::ensure_state_dir(¬es_dir)?; - Ok(()) - } - .await; - if result.is_ok() - && let Err(e) = crate::forge::ensure_meta_remote(approval.agent.as_str()).await - { - tracing::warn!(agent = %approval.agent, error = ?e, "forge: ensure_meta_remote after init_config failed"); - } - finish_approval(coord, &approval, result, None).await -} - async fn finish_approval( coord: &Coordinator, approval: &hive_sh4re::approvals::Approval, @@ -727,28 +673,12 @@ async fn finish_approval( note: note.clone(), description: approval.description.clone(), }); - // For spawn/rebuild/init_config approvals, also surface the underlying - // action so the manager knows whether the lifecycle step succeeded. - // The ApprovalResolved event already carries the same `ok` signal but + // For spawn/rebuild approvals, also surface the underlying action so the + // manager knows whether the lifecycle step succeeded. The + // ApprovalResolved event already carries the same `ok` signal but // separating it lets the manager react to the lifecycle change // without having to special-case approvals. match approval.kind { - ApprovalKind::InitConfig => { - if ok { - let _ = coord - .push_todo_submitter( - approval.id, - "core", - Some(format!("config_ready:{}", approval.agent)), - format!( - "agent '{}' config repo ready — edit + apply", - approval.agent - ), - None, - ) - .await; - } - } ApprovalKind::Spawn => { let summary = if ok { format!("agent '{}' spawned", approval.agent) diff --git a/hive-c0re/src/agent_config/topology.rs b/hive-c0re/src/agent_config/topology.rs index 96b15dbb..cc74f4cc 100644 --- a/hive-c0re/src/agent_config/topology.rs +++ b/hive-c0re/src/agent_config/topology.rs @@ -229,9 +229,8 @@ pub fn write(topology: &BTreeMap>) -> std::io::Result<()> /// Compute the default topology for a fresh install: every agent is a /// root (parent = null). There is no structural "manager" — agents -/// arrange themselves via explicit parent edges (an agent-requested -/// sub-agent gets a requester-as-parent edge at `init_config`; the -/// operator reparents via the dashboard / `RequestSetParent` API). +/// arrange themselves via explicit parent edges, written by the operator +/// through the dashboard / `RequestSetParent` API. /// Used by `meta::sync_agents` on first call to seed `topology.json`. /// /// As soon as an explicit write lands (dashboard / `RequestSetParent` @@ -298,53 +297,6 @@ pub fn apply_set_parent( Ok(next) } -/// Declare a brand-new agent's parent edge before the agent exists in -/// the container set. Unlike [`crate::meta::bulk_commit_topology`] / -/// [`apply_set_parent`] (which reparent an entry that must already be -/// present), this inserts a fresh `child -> parent` -/// row. Used by the `InitConfig` approval to place a just-scaffolded -/// sub-agent under its requesting parent, so the edge is in place -/// before the first apply-commit spawns the container (and before -/// [`reconcile`] would otherwise default it to the manager). -/// -/// Idempotent when the edge already exists. Refuses to clobber an entry -/// whose parent differs (one agent can't steal another's child) and -/// validates that `parent` is itself a known agent. -pub fn add_child(child: &str, parent: &str) -> Result<(), String> { - let current = read(); - match apply_add_child(¤t, child, parent)? { - Some(next) => write(&next).map_err(|e| format!("write topology.json: {e}")), - None => Ok(()), - } -} - -/// Pure form of [`add_child`] for unit tests. Returns the post-insert -/// map (caller writes it back), `None` for an idempotent no-op (edge -/// already present), or an error string (unknown parent / name owned by -/// a different parent). -pub fn apply_add_child( - topo: &BTreeMap>, - child: &str, - parent: &str, -) -> Result>>, String> { - if !topo.contains_key(parent) { - return Err(format!("unknown parent: {parent}")); - } - match topo.get(child) { - Some(Some(p)) if p == parent => return Ok(None), - Some(existing) => { - return Err(format!( - "agent {child} already exists in topology (parent: {existing:?}); \ - refusing to reparent via add_child" - )); - } - None => {} - } - let mut next = topo.clone(); - next.insert(child.to_owned(), Some(parent.to_owned())); - Ok(Some(next)) -} - /// Reconcile `topology.json` against the current agent set. Adds an /// entry (default: parent = null — a new agent with no declared parent /// is its own root) for any agent missing from the file; removes @@ -353,10 +305,10 @@ pub fn apply_add_child( /// choices stick across regenerations. Returns true when the file /// changed and should be re-committed by the caller. /// -/// `pending` lists agents that have an operator-approved proposed config -/// repo but no container yet (init'd, not yet spawned). They are KEPT -/// (not dropped) so the parent edge written at `InitConfig` approval -/// survives until the first apply-commit, but they are NOT seeded with a +/// `pending` lists agents that have a provisioned proposed config repo +/// but no container yet (provisioned, not yet spawned). They are KEPT +/// (not dropped) so an explicit parent edge written before the first +/// spawn survives until the first apply-commit, but they are NOT seeded with a /// default parent here and NOT added to roles — that happens when the /// container actually spawns and the name moves into `agent_names`. pub fn reconcile(agent_names: &[String], pending: &[String]) -> std::io::Result { @@ -384,12 +336,11 @@ pub fn apply_reconcile( for name in agent_names { if !next.contains_key(name) { // A new agent with no declared parent defaults to root - // (parent = null). Agent-requested sub-agents always carry an - // explicit requester-as-parent edge (written at init_config - // approval), so they never hit this default — only - // user/operator-initiated spawns do, and those are roots. No - // agent is structurally privileged here: "root-ness" is just - // a null parent. + // (parent = null). An agent placed under a parent carries an + // explicit edge written before its first spawn, so it never + // hits this default — only spawns with no declared parent do, + // and those are roots. No agent is structurally privileged + // here: "root-ness" is just a null parent. next.insert(name.clone(), None); changed = true; } @@ -502,8 +453,7 @@ mod tests { #[test] fn default_seed_makes_every_agent_root() { // No structural manager: every agent defaults to root (null - // parent). Explicit edges (init_config / dashboard) are layered - // on later. + // parent). Explicit edges are layered on later. let agents = vec![ "alice".to_owned(), crate::lifecycle::MANAGER_NAME.to_owned(), @@ -630,42 +580,6 @@ mod tests { assert_eq!(next, topo_three_level()); } - #[test] - fn apply_add_child_inserts_new_edge_under_parent() { - // alice spawns a brand-new child `dora`: the edge lands - // with alice as parent without disturbing the rest of the tree. - let next = apply_add_child(&topo_three_level(), "dora", "alice") - .unwrap() - .expect("brand-new edge should produce a map"); - assert_eq!(next.get("dora"), Some(&Some("alice".to_owned()))); - // existing entries untouched. - assert_eq!(next.get("bob"), Some(&Some("alice".to_owned()))); - } - - #[test] - fn apply_add_child_is_idempotent_when_edge_exists() { - // bob already under alice — re-init returns a no-op (None). - assert!( - apply_add_child(&topo_three_level(), "bob", "alice") - .unwrap() - .is_none() - ); - } - - #[test] - fn apply_add_child_refuses_unknown_parent() { - let err = apply_add_child(&topo_three_level(), "dora", "nobody").unwrap_err(); - assert!(err.contains("unknown parent"), "err = {err}"); - } - - #[test] - fn apply_add_child_refuses_name_owned_by_other_parent() { - // bob lives under alice; the manager can't claim it via add_child. - let err = apply_add_child(&topo_three_level(), "bob", crate::lifecycle::MANAGER_NAME) - .unwrap_err(); - assert!(err.contains("already exists"), "err = {err}"); - } - #[test] fn apply_reconcile_adds_missing_live_agent_as_root() { // A live agent with no prior topology entry defaults to root @@ -696,7 +610,7 @@ mod tests { #[test] fn apply_reconcile_keeps_pending_init_agent_edge() { - // `dora` was init'd under alice (edge present) but has no + // `dora` was placed under alice (edge present) but has no // container yet, so it's absent from the live set. It must NOT // be dropped, and its alice-parent edge must be preserved (not // re-seeded under the manager). diff --git a/hive-c0re/src/coordinator.rs b/hive-c0re/src/coordinator.rs index 633533e3..00cb0184 100644 --- a/hive-c0re/src/coordinator.rs +++ b/hive-c0re/src/coordinator.rs @@ -1512,8 +1512,8 @@ impl Coordinator { /// Agents that have an operator-approved proposed config repo but /// were never deployed — proposed `.git` exists, applied `.git` does - /// not. Their `topology.json` parent edge (written at `InitConfig` - /// approval) must survive `topology::reconcile` until the first + /// not. Their `topology.json` parent edge (written before the first + /// spawn) must survive `topology::reconcile` until the first /// apply-commit spawns the container. Distinct from tombstones, /// which have an applied repo from a prior deploy. #[must_use] diff --git a/hive-c0re/src/dashboard/approvals.rs b/hive-c0re/src/dashboard/approvals.rs index 368d4f6d..df0e5803 100644 --- a/hive-c0re/src/dashboard/approvals.rs +++ b/hive-c0re/src/dashboard/approvals.rs @@ -83,13 +83,9 @@ pub(super) fn gc_orphans(coord: &Coordinator, approvals: Vec) -> Vec) -> Vec { commit_ref: None, requested_at: a.requested_at, }, - hive_sh4re::approvals::ApprovalKind::InitConfig => ApprovalView { - id: a.id, - agent: a.agent.to_string(), - kind: "init_config", - sha_short: None, - description: a.description, - pr_number: None, - commit_ref: None, - requested_at: a.requested_at, - }, hive_sh4re::approvals::ApprovalKind::UpdateMetaInputs => ApprovalView { id: a.id, agent: a.agent.to_string(), diff --git a/hive-c0re/src/lifecycle/host_config.rs b/hive-c0re/src/lifecycle/host_config.rs index 09b55c66..b1592dbe 100644 --- a/hive-c0re/src/lifecycle/host_config.rs +++ b/hive-c0re/src/lifecycle/host_config.rs @@ -106,11 +106,10 @@ fn config_bind_source(name: &str) -> PathBuf { /// hive-c0re still reads it directly on the host (`stats::hive_stats`), /// which needs no bind mount into the parent. /// -/// ⚠️ The seeding done at `InitConfig` approval is **not** affected by the -/// `config` flag and must not be read as a reason to widen it: that runs -/// as hive-c0re against the host path (see `actions.rs`, which seeds the -/// repo and wires its forge remote inline), and `read_only` on a bind -/// constrains writers *inside* the container only. +/// ⚠️ The config-repo seeding hive-c0re does at spawn is **not** affected by +/// the `config` flag and must not be read as a reason to widen it: that runs +/// as hive-c0re against the host path (see `lifecycle::setup_proposed`), and +/// `read_only` on a bind constrains writers *inside* the container only. fn bind_child_agent_dirs(child: &str, binds: &mut Vec) { let Ok(child) = hive_types::Ident::parse(child) else { tracing::warn!(%child, "skipping child bind: invalid agent name"); diff --git a/hive-c0re/src/lifecycle/setup.rs b/hive-c0re/src/lifecycle/setup.rs index b695c23d..d97c1562 100644 --- a/hive-c0re/src/lifecycle/setup.rs +++ b/hive-c0re/src/lifecycle/setup.rs @@ -171,8 +171,6 @@ pub async fn setup_applied( /// valid session; credential files inside (`.credentials.json` etc.) are 0600 so /// secrets stay private regardless of the directory mode. Idempotent: existing /// dirs are left untouched (an agent's OAuth tokens survive `destroy`/recreate). -/// Public for the `InitConfig` approval path in `actions.rs` which seeds -/// dirs without calling the full `spawn`. pub fn ensure_claude_dir(claude_dir: &Path) -> Result<()> { use std::io; if !claude_dir.exists() { @@ -208,9 +206,9 @@ pub fn ensure_claude_dir(claude_dir: &Path) -> Result<()> { Ok(()) } -/// Public for the `InitConfig` approval path in `actions.rs` which seeds -/// dirs without calling the full `spawn`. Also creates the sibling `harness/` -/// dir so the first harness startup can write its sqlite files immediately. +/// Create the per-agent state dir if missing. Also creates the sibling +/// `harness/` dir so the first harness startup can write its sqlite files +/// immediately. pub fn ensure_state_dir(notes_dir: &Path) -> Result<()> { if !notes_dir.exists() { std::fs::create_dir_all(notes_dir) diff --git a/hive-c0re/src/socket_server/config_approvals.rs b/hive-c0re/src/socket_server/config_approvals.rs index abb28b64..147c135e 100644 --- a/hive-c0re/src/socket_server/config_approvals.rs +++ b/hive-c0re/src/socket_server/config_approvals.rs @@ -1,6 +1,5 @@ -//! Config-approval request handlers: `RequestInitConfig` / -//! `RequestUpdateMetaInputs`, plus the shared submit helpers -//! (`submit_init_config` / `submit_merge_config_pr`). +//! Config-approval request handlers: `RequestUpdateMetaInputs`, plus the +//! shared submit helper `submit_merge_config_pr`. //! //! `submit_merge_config_pr` is called from the dashboard webhook handler //! (`dashboard::webhook`) — agents no longer need an MCP tool for config @@ -11,70 +10,8 @@ use std::sync::Arc; use hive_core_agent_sock::Response; -use super::require_new_child; use crate::coordinator::Coordinator; -/// `RequestInitConfig` — queue an `InitConfig` approval for an agent. The -/// `name` must be brand-new (absent from the topology) or already in the -/// caller's subtree; the requester is recorded as the new agent's parent (the -/// root requesting a new agent → a top-level agent, matching reconcile's -/// default). -pub(super) fn handle_request_init_config( - coord: &Arc, - agent: &str, - name: &str, - description: Option, -) -> Response { - if let Some(err) = require_new_child(agent, name, "request_init_config for") { - return err; - } - tracing::info!(%agent, %name, "request_init_config"); - // Warn, do not refuse: an agent already created under a colliding - // name must stay re-initialisable, so the refusal comes later, once - // the warning has had time to be seen. - // - // Checked HERE and not only in `swarm-controller::create_agent`: - // that daemon is opt-in and off on most hives, while this is the - // path the `request_init_config` tool takes on every hive. Guarding - // only the rarer one would have left the common flow exactly as - // unguarded as before. - // - // The blacklist itself comes from nix via `HIVE_RESERVED_NAMES`, so it - // stays a config change rather than a rebuild. An UNSET variable means - // this daemon was never told — which is not the same as "no name is - // reserved", and saying nothing there would be a check that reports - // clean because it could not run. - let raw = hive_types::reserved_names_raw(); - let warnings = match raw.as_deref().map(hive_types::parse_reserved_names) { - None => { - tracing::error!( - var = hive_types::RESERVED_NAMES_ENV, - "request_init_config: reserved-name check could not run — variable not set" - ); - vec![format!( - "the reserved-name check did not run: {} is unset, so {name:?} was accepted \ - without being checked against the protocol literals", - hive_types::RESERVED_NAMES_ENV - )] - } - Some(reserved) if hive_types::is_reserved_name(name, &reserved) => { - tracing::warn!(%agent, %name, "request_init_config: reserved name"); - vec![format!( - "agent name {name:?} is a reserved protocol name — messages from this agent will \ - be indistinguishable from hyperhive's own; this will become an error" - )] - } - Some(_) => Vec::new(), - }; - match submit_init_config(coord, name, Some(agent), description) { - Ok(_id) if warnings.is_empty() => Response::Ok, - Ok(_id) => Response::OkWarn { warnings }, - Err(e) => Response::Err { - message: format!("{e:#}"), - }, - } -} - /// `RequestUpdateMetaInputs` — queue an `UpdateMetaInputs` approval /// carrying the JSON-encoded input list in `commit_ref` (no git commit /// is involved; the field is the payload the approval handler decodes). @@ -214,58 +151,3 @@ pub(crate) async fn submit_merge_config_pr( }); Ok(id) } - -/// Queue an `InitConfig` approval for a brand-new agent whose config repo -/// does not yet exist. Shared between the manager and agent sockets. -/// -/// `parent`, when `Some`, is the agent that will own the new child once -/// the operator approves: it is stashed in the approval's `commit_ref` -/// field (unused for `InitConfig` otherwise — same pattern -/// `UpdateMetaInputs` uses to carry its inputs JSON) and consumed in -/// `run_approval_init_config` to write the `child -> parent` topology -/// edge. Callers pass the requesting agent, so the requester becomes the -/// new agent's parent (the root requesting a new agent → a top-level agent, -/// matching `topology::reconcile`'s default). `None` writes no explicit -/// edge (reconcile-default placement) — retained for that fallback. -pub(crate) fn submit_init_config( - coord: &Arc, - name: &str, - parent: Option<&str>, - description: Option, -) -> anyhow::Result { - let agent = hive_types::Ident::parse(name) - .map_err(|e| anyhow::anyhow!("invalid agent name {name:?}: {e}"))?; - let proposed_dir = crate::coordinator::Coordinator::agent_proposed_dir(&agent); - if proposed_dir.join(".git").exists() { - anyhow::bail!( - "proposed config repo for '{name}' already exists at {} - \ - nothing to init; config changes go through a forge PR on \ - agent-configs/{name}", - proposed_dir.display() - ); - } - let id = coord - .approvals - .submit_kind( - name, - hive_sh4re::approvals::ApprovalKind::InitConfig, - parent.unwrap_or(""), - description.as_deref(), - // `parent` is the requesting agent (becomes the new child's - // parent); it's also the submitter the approval events route - // back to. No declared parent = operator-initiated path. - parent.unwrap_or("operator"), - None, // no sha for InitConfig - ) - .map_err(|e| anyhow::anyhow!("queue approval row: {e:#}"))?; - tracing::info!(%id, %name, "init_config approval queued"); - coord.emit_approval_added(crate::coordinator::ApprovalAdded { - id, - agent: name, - approval_kind: "init_config", - sha_short: None, - description, - pr_number: None, - }); - Ok(id) -} diff --git a/hive-c0re/src/socket_server/mod.rs b/hive-c0re/src/socket_server/mod.rs index fea70579..3eb64313 100644 --- a/hive-c0re/src/socket_server/mod.rs +++ b/hive-c0re/src/socket_server/mod.rs @@ -30,7 +30,7 @@ pub(crate) use config_approvals::submit_merge_config_pr; pub(crate) use schedules::filter_ghost_schedule_targets; pub use schedules::schedule_to_wire_public; -use config_approvals::{handle_request_init_config, handle_request_update_meta_inputs}; +use config_approvals::handle_request_update_meta_inputs; use lifecycle_handlers::{ handle_kill, handle_list_descendants, handle_restart, handle_start, handle_update, }; @@ -560,9 +560,6 @@ async fn dispatch(req: &Request, agent: &str, coord: &Arc) -> Respo Request::Kill { name } => handle_kill(coord, agent, name).await, Request::Update { name } => handle_update(coord, agent, name), Request::ListDescendants => handle_list_descendants(coord, agent).await, - Request::RequestInitConfig { name, description } => { - handle_request_init_config(coord, agent, name, description.clone()) - } // Agent-state queries: own subtree is free; other agents + the // hive-wide `"*"` sweep require `QueryAgentState`. Request::GetLooseEnds { agent: target } => { @@ -695,46 +692,6 @@ fn require_group(agent: &str, group: &str, action: &str) -> Option { } } -/// Topology guard for `request_init_config`, which may legitimately target a -/// child that does not exist *yet* (seeding a brand-new sub-agent's config -/// repo). The caller may act on a -/// `target` that is EITHER already in its subtree (re-init / config -/// update of an agent it owns) OR brand-new (absent from the topology -/// tree — the requester becomes its parent). A name that already -/// exists outside the caller's subtree is refused so -/// one agent can't hijack another's sub-tree. -/// -/// Also re-runs the agent-name format check (a traversal / malformed name -/// could never be a descendant): a brand-new name now flows straight to -/// `submit_init_config`, which builds filesystem paths from it, so validate -/// before that. -fn require_new_child(agent: &str, target: &str, action: &str) -> Option { - if let Err(reason) = hive_types::Ident::parse(target) { - return Some(Response::Err { - message: format!("agent `{agent}` cannot {action} `{target}`: {reason}"), - }); - } - // brand-new name (absent from topology) — requester becomes the parent on - // approval; allowed for any caller. - if !crate::topology::read().contains_key(target) { - return None; - } - // existing agent — allowed only if it's in the caller's subtree - // (re-init / config update of an agent the caller owns; the root owns - // every existing agent). Refuses an agent outside the caller's subtree - // so one agent can't hijack another's config. - if crate::topology::is_descendant_of(target, agent) { - None - } else { - Some(Response::Err { - message: format!( - "agent `{agent}` cannot {action} `{target}`: it already exists \ - outside its subtree in the topology tree" - ), - }) - } -} - /// `GetLooseEnds` — read the target's loose ends. `None` / own / a subtree /// descendant resolve freely (a parent sees its subtree, the root sees all); /// any other named agent needs `QueryAgentState`; `"*"` is a hive-wide sweep diff --git a/hive-c0re/src/stores/approvals.rs b/hive-c0re/src/stores/approvals.rs index 417781db..12376da0 100644 --- a/hive-c0re/src/stores/approvals.rs +++ b/hive-c0re/src/stores/approvals.rs @@ -1,5 +1,5 @@ -//! Approval queue. Requests are submitted by the manager (`RequestInitConfig` -//! / `RequestUpdateMetaInputs`), the config-PR webhook (`MergeConfigPr`), or +//! Approval queue. Requests are submitted by the manager +//! (`RequestUpdateMetaInputs`), the config-PR webhook (`MergeConfigPr`), or //! the operator (`Spawn`); the user approves/denies via the host admin CLI; //! on approval the host runs the corresponding action. @@ -74,8 +74,7 @@ impl Approvals { /// Insert a new pending approval row. `fetched_sha` may be supplied /// when the sha is already known at submission time (e.g. `MergeConfigPr` /// fetches the PR head before inserting), making the insert + sha-set - /// atomic. Pass `None` when the kind carries no sha (e.g. `Spawn` / - /// `InitConfig`). + /// atomic. Pass `None` when the kind carries no sha (e.g. `Spawn`). pub fn submit_kind( &self, agent: &str, @@ -380,7 +379,6 @@ fn row_to_approval(row: &rusqlite::Row<'_>) -> rusqlite::Result { let kind: String = row.get(2)?; let kind = match kind.as_str() { "spawn" => ApprovalKind::Spawn, - "init_config" => ApprovalKind::InitConfig, "update_meta_inputs" => ApprovalKind::UpdateMetaInputs, "schedule_prompt" => ApprovalKind::SchedulePrompt, "merge_config_pr" => ApprovalKind::MergeConfigPr, @@ -434,7 +432,6 @@ fn row_to_approval(row: &rusqlite::Row<'_>) -> rusqlite::Result { fn kind_from_str(s: &str) -> Result { Ok(match s { "spawn" => ApprovalKind::Spawn, - "init_config" => ApprovalKind::InitConfig, "update_meta_inputs" => ApprovalKind::UpdateMetaInputs, "schedule_prompt" => ApprovalKind::SchedulePrompt, "merge_config_pr" => ApprovalKind::MergeConfigPr, @@ -454,31 +451,6 @@ mod tests { (dir, path, db) } - #[test] - fn init_config_approval_round_trips() { - // Regression test: an `init_config` row used to fail - // deserialization (row_to_approval matched only apply_commit + - // spawn), erroring out the whole `pending()` query — every - // approval then vanished from the dashboard. - let (_dir, _path, db) = open_temp(); - let id = db - .submit_kind( - "bitburner", - ApprovalKind::InitConfig, - "", - Some("scaffold"), - "bitburner", - None, - ) - .expect("submit init_config"); - let pending = db - .pending() - .expect("pending() must not error on an init_config row"); - assert_eq!(pending.len(), 1); - assert_eq!(pending[0].id, id); - assert!(matches!(pending[0].kind, ApprovalKind::InitConfig)); - } - #[test] fn mixed_kinds_all_listed() { let (_dir, _path, db) = open_temp(); @@ -493,7 +465,7 @@ mod tests { .unwrap(); db.submit_kind("b", ApprovalKind::Spawn, "", None, "b", None) .unwrap(); - db.submit_kind("c", ApprovalKind::InitConfig, "", None, "c", None) + db.submit_kind("c", ApprovalKind::UpdateMetaInputs, "[]", None, "c", None) .unwrap(); let pending = db.pending().expect("pending"); assert_eq!(pending.len(), 3, "all three kinds must be visible"); diff --git a/hive-core-agent-sock/src/lib.rs b/hive-core-agent-sock/src/lib.rs index 690af6cb..7dc85323 100644 --- a/hive-core-agent-sock/src/lib.rs +++ b/hive-core-agent-sock/src/lib.rs @@ -154,13 +154,6 @@ pub enum Request { }, // ---- privileged (manager socket only for now) --------------------------- - /// *(privileged)* Initialise a brand-new agent's proposed config repo - /// and queue an approval for the operator to review. - RequestInitConfig { - name: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - description: Option, - }, /// *(privileged)* Stop a sub-agent (graceful). Kill { name: String }, /// *(privileged)* Start a previously-stopped sub-agent container. diff --git a/hive-sh4re/src/approvals.rs b/hive-sh4re/src/approvals.rs index 94017ced..8e0867ea 100644 --- a/hive-sh4re/src/approvals.rs +++ b/hive-sh4re/src/approvals.rs @@ -49,12 +49,6 @@ pub enum ApprovalKind { /// Create + start a new sub-agent container with the given name /// (under the default `agent.nix` template). Spawn, - /// Create an agent's config repo and seed it from the default - /// template (step 1 of the two-step spawn flow). Creating it is the - /// whole of this step — tailoring what the template seeded is not a - /// separate mechanism, it's a `MergeConfigPr` like every later - /// change. - InitConfig, /// Run `nix flake update [inputs...]` on the meta flake and commit /// the resulting lock changes. UpdateMetaInputs, diff --git a/hive-sh4re/src/permissions.rs b/hive-sh4re/src/permissions.rs index e376196c..b7f8d8eb 100644 --- a/hive-sh4re/src/permissions.rs +++ b/hive-sh4re/src/permissions.rs @@ -24,7 +24,7 @@ pub enum ToolGroup { Inbox, /// `kill`, `start`, `restart`, `update` - *(privileged)* Lifecycle, - /// `request_init_config`, `request_update_meta_inputs` - *(privileged)* + /// `request_update_meta_inputs` - *(privileged)* Approvals, /// `request_schedule_prompt`, `fire_schedule_now`, `cancel_schedule`, /// `edit_schedule`, `list_schedules` - *(privileged)* @@ -60,7 +60,7 @@ impl ToolGroup { Self::Meta => &["get_agent_meta"], Self::Inbox => &["get_loose_ends", "cancel_loose_end", "remind"], Self::Lifecycle => &["kill", "start", "restart", "update", "list_containers"], - Self::Approvals => &["request_init_config", "request_update_meta_inputs"], + Self::Approvals => &["request_update_meta_inputs"], Self::Scheduling => &[ "request_schedule_prompt", "fire_schedule_now", @@ -167,7 +167,7 @@ impl ToolGroup { "kill, start, restart, update, list_containers — container lifecycle (privileged)" } Self::Approvals => { - "request_init_config, request_update_meta_inputs — config change flow (privileged)" + "request_update_meta_inputs — operator-approved meta-flake input bumps (privileged)" } Self::Scheduling => { "request_schedule_prompt and related — operator-visible scheduled prompts (privileged)" diff --git a/hivectl/README.md b/hivectl/README.md index 2668e6ab..bba33c99 100644 --- a/hivectl/README.md +++ b/hivectl/README.md @@ -20,7 +20,7 @@ One module per subcommand family; `main.rs` is just the clap parse + dispatch: - **`agents.rs`** — container lifecycle (start/stop/create/kill/rebuild/restart/…). -- **`approvals.rs`** — the config/init-config/meta-input approval queue. +- **`approvals.rs`** — the config/spawn/meta-input approval queue. - **`dag_progress.rs`** — rebuild-queue progress rendering. - **`power.rs`** — restart/start/stop at the container level. - **`choom.rs`** — drop into an interactive claude session in a diff --git a/nix/reserved-names.nix b/nix/reserved-names.nix index 60ecefdf..95599b28 100644 --- a/nix/reserved-names.nix +++ b/nix/reserved-names.nix @@ -4,15 +4,15 @@ # (`HIVE_RESERVED_NAMES`), so keeping it current is a config change rather than # a rebuild of a binary. Read by: # -# - `host-modules/hive-c0re/environment.nix` -> the env var, for the -# `request_init_config` path every hive uses -# - `host-modules/swarm-controller.nix` -> the same var, for the -# swarm-level `create_agent` path +# - `host-modules/swarm-controller.nix` -> the env var, for the +# swarm-level `create_agent` path — the only name check that runs today +# - `host-modules/hive-c0re/environment.nix` -> the same var. hive-c0re +# creates no agent, so nothing reads it there now; still handed over so a +# future host-side path is wired by construction, not by remembering to. # - `host-modules/swarm-otel.nix` -> its `` assertion, so # a hive name and an agent name are checked against ONE list # - `nix/checks.nix` -> exported into `cargo test`, -# which is what keeps the message layer's sentinels from drifting away -# from this file +# which keeps the message layer's sentinels from drifting from this file # # A plain nix file rather than a module option because two of those readers are # flake-level (`checks.nix`) and cannot see a NixOS option.