diff --git a/docs/agent-hierarchy.md b/docs/agent-hierarchy.md index 96f38e0a..d3c11ff4 100644 --- a/docs/agent-hierarchy.md +++ b/docs/agent-hierarchy.md @@ -143,7 +143,9 @@ can't: The manager container's nspawn bind set: - `HOST_AGENTS_ROOT (/var/lib/hyperhive/agents) → /agents` RW — so the - manager can edit any agent's proposed config repo + manager can manage any agent's state dir. Config is **not** authored + here: a config change is a PR from a clone, and `/config/` is + a copy for reading (its write access is a defect tracked separately) - `HOST_APPLIED_ROOT (/var/lib/hyperhive/applied) → /applied` RO — so the manager can diff against what's deployed - `HOST_META_ROOT (/var/lib/hyperhive/meta) → /meta` RO — so the diff --git a/docs/approvals.md b/docs/approvals.md index c1c3f244..eaa6f938 100644 --- a/docs/approvals.md +++ b/docs/approvals.md @@ -25,10 +25,10 @@ CLI) before it takes effect. What you'll see, and what to do with it: 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` seeds a fresh config repo from - a template so the submitting agent can edit it; once you're happy - with what they wrote, `Spawn` actually creates the container from - that config. Every later change goes through the config-change flow + 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 is not 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. - **Meta/flake update** (`UpdateMetaInputs`) — an agent asked to bump one or more Nix flake inputs (or all of them). Approving runs the @@ -60,13 +60,14 @@ There is no bespoke MCP tool for config changes: opening the PR IS the request. 1. The submitting agent (the child's parent, holding the `approvals` - tool group) edits files in the child's proposed config repo - (any tracked path, but `agent.nix` is the contract entry point), - commits with its own git identity, and pushes a branch + opens a PR - on `agent-configs/` with `hive-forge`. The parent's container - has the child's proposed config repo bind-mounted read-write at - `/agents//config/` (topology-driven via `set_nspawn_flags`; - the agent's *own* config at `/agents//config/` is read-only). + tool group) **clones** `agent-configs/`, edits it there (any + tracked path, but `agent.nix` is the contract entry point), commits + with its own git identity, and pushes a branch + opens a PR with + `hive-forge` — the same way it would change any other repo. + The bind-mounted `/agents//config/` is a **copy for reading** a + config, not the tree to edit: authoring in place there produces no PR + and no approval. (It is currently mounted read-write, which is a + defect tracked separately, not an authoring path.) Branch protection (push/merge whitelist = `core`, approvals whitelist = operator team; see "Forge mirror" and #1787) makes the agent a write collaborator that **cannot merge its own config PR**. @@ -130,16 +131,18 @@ 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 seed a brand-new agent's proposed config repo. -On approve, hive-c0re seeds it with a default `agent.nix` template and +`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 submitting agent reviews, edits, and -commits the template; the operator then **spawns** the agent (the +in-container store. The operator then **spawns** the agent (the `Spawn` approval / `◆ R3QU3ST SP4WN` button), which creates the -container from that config. From then on, config changes go through a -forge PR (`MergeConfigPr`) — never a fresh spawn. This gives the -submitting agent (and operator) an explicit review gate on the initial -configuration before any container is created. +container from that config. + +Changing what the template seeded is not a special case: like every +later change, it's a PR on that config repo (`MergeConfigPr`), made +from a clone, reviewed and approved by the operator. The PR flow is +the one path — an operator can equally drive both steps herself +through the web UI or the forge. ### Approval kinds (wire shapes) diff --git a/docs/persistence.md b/docs/persistence.md index e1aeabdb..26bee789 100644 --- a/docs/persistence.md +++ b/docs/persistence.md @@ -319,10 +319,12 @@ A parent agent gets each direct child's `state`, `harness`, and `lifecycle.rs`). The RW on `state` is deliberate, not an oversight: a parent manages its children, which includes writing into a child's state for recovery (e.g. seeding notes, clearing a stuck sentinel) as -well as reading it. `config` is RW because the parent authors proposed -config changes for the child (the approval flow commits into the -child's config repo), and `harness` is RW for the same management -reasons. Per-child isolation still holds: a container only ever has +well as reading it. `config` and `harness` are RW too, but **nothing +justifies that for `config`**: a config change is a PR on the child's +config repo, made from a clone, so the bind-mounted `config` dir is a +read-only *copy* for reading a child's config — not a tree anyone edits +in place. Narrowing it is tracked separately, and depends on relocating +where `InitConfig` seeds. Per-child isolation still holds: a container only ever has its *own* dirs plus its direct children's bind-mounted, never a sibling's. diff --git a/hive-agent-mcp/src/mcp/args.rs b/hive-agent-mcp/src/mcp/args.rs index dfb78a77..ae452a7d 100644 --- a/hive-agent-mcp/src/mcp/args.rs +++ b/hive-agent-mcp/src/mcp/args.rs @@ -87,11 +87,11 @@ pub struct RemindArgs { #[derive(Debug, serde::Deserialize, schemars::JsonSchema)] pub struct RequestInitConfigArgs { /// New sub-agent name (≤9 chars). Queues an `InitConfig` approval; on - /// approval hive-c0re seeds the proposed config repo at - /// `/agents//config/agent.nix` with the default template. After - /// the approval the manager edits + commits the config, then the operator - /// spawns the agent; later config changes go through a PR on the child's - /// `agent-configs/` repo. + /// 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)] diff --git a/hive-agent-mcp/src/mcp/mod.rs b/hive-agent-mcp/src/mcp/mod.rs index d8e4d64c..83a4bec3 100644 --- a/hive-agent-mcp/src/mcp/mod.rs +++ b/hive-agent-mcp/src/mcp/mod.rs @@ -824,13 +824,16 @@ impl AgentServer { // hive-c0re performs a topology check server-side: only direct children // of the calling agent are accepted; all other names are rejected. #[tool( - description = "Initialise a brand-new direct child agent's proposed config repo and \ - queue an `InitConfig` approval for the operator to review. Requires the `approvals` \ - tool group. `name` must be a direct child of this agent in the topology tree. \ - Fails if a config repo for that child already exists. On approval hive-c0re seeds \ - `/agents//config/agent.nix` with the default template; customise + commit it, \ - then the operator spawns the agent. Later config changes go through a PR on the \ - child's `agent-configs/` repo, reviewed + approved by the operator." + description = "Create a brand-new direct child agent's config repo and queue an \ + `InitConfig` approval for the operator to review. Requires the `approvals` tool \ + group. `name` must be a direct child of this agent in the topology tree. Fails if a \ + config repo for that child 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, diff --git a/hive-c0re/src/lifecycle/setup.rs b/hive-c0re/src/lifecycle/setup.rs index eef1f6ec..e61b8950 100644 --- a/hive-c0re/src/lifecycle/setup.rs +++ b/hive-c0re/src/lifecycle/setup.rs @@ -1,6 +1,10 @@ -//! First-spawn provisioning: seed the manager-editable proposed repo and +//! First-spawn provisioning: seed the agent's proposed config repo and //! the hive-c0re-owned applied repo, and ensure the per-agent state / //! claude-credentials dirs (btrfs subvolume when available) exist. +//! +//! Seeding is the only write either repo gets from here. Later config +//! changes arrive as forge PRs authored in a clone — nothing edits a +//! working tree in place. use std::path::Path; @@ -10,17 +14,21 @@ use super::git::{ git, git_command, git_commit, git_read_tree_reset, git_rev_parse, git_root_commit, git_tag, }; -/// Initialize the manager-editable proposed repo. Seeds two tracked -/// files: `agent.nix` (the module the manager edits) and `flake.nix` -/// (the boilerplate that lets the meta flake import this repo as an -/// input — meta locks at a specific sha and reads -/// `nixosModules.default`, so `flake.nix` must be in the commit). The -/// manager shouldn't edit `flake.nix` (the prompt says so) but it's -/// visible so they can introspect. +/// Initialize an agent's config repo. Seeds two tracked files: +/// `agent.nix` (the agent's own module) and `flake.nix` (the +/// boilerplate that lets the meta flake import this repo as an input — +/// meta locks at a specific sha and reads `nixosModules.default`, so +/// `flake.nix` must be in the commit). `flake.nix` isn't meant to be +/// edited, but it's tracked so it can be read. /// -/// Touched by hive-c0re only on first spawn — never again — so the -/// manager can't be surprised by hive-c0re commits or working-tree -/// resets. +/// **Seeding is the whole of hive-c0re's write.** Changes to the repo +/// arrive as PRs from a clone, via the forge, like any other code +/// change — nothing edits a working tree in place, here or in the +/// agent's bind-mounted `config/` (which is a read-only copy). +/// +/// Touched by hive-c0re only on first spawn — never again — so a +/// hive-c0re commit or working-tree reset can't surprise anyone +/// holding a clone. pub async fn setup_proposed(proposed_dir: &Path, name: &str) -> Result<()> { let fresh = !proposed_dir.join(".git").exists(); if fresh {