refactor(#2416): remove the non-pr config-change flow (request_apply_commit / applycommit)

This commit is contained in:
damocles 2026-07-15 20:45:29 +02:00 committed by mara
commit c2bd7db998
34 changed files with 293 additions and 1635 deletions

View file

@ -10,67 +10,55 @@ informed about what happens after a decision lands.
## End-to-end approval flow
Config changes flow through a **forge pull request** on the agent's
`agent-configs/<name>` repo — the same surface agents use for code PRs.
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)
and commits with its own git identity. The parent's container has
the child's proposed config repo bind-mounted read-write at
(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/<name>` with `hive-forge`. The parent's container
has the child's proposed config repo bind-mounted read-write at
`/agents/<name>/config/` (topology-driven via `set_nspawn_flags`;
the agent's *own* config at `/agents/<self>/config/` is read-only).
2. The submitting agent submits the commit sha via `request_apply_commit(agent,
commit_ref)`. `commit_ref` must be a commit **sha** (7-40 hex
chars, short or full) — a branch or tag name is rejected so the
approval pins an immutable commit.
3. **hive-c0re immediately fetches that commit from the proposed
repo into the applied repo and tags it `proposal/<id>`.** It
resolves the sha locally against the proposed repo, fetches all
of proposed's heads into applied's object db, then tags the
resolved commit — `git fetch <remote> <sha>:<dst>` can't fetch
by a bare sha (the left side of a refspec is a remote *ref
name*), so the resolution happens on hive-c0re's side. The
approval row stores both the submitted sha and the
canonical hive-c0re-vouched sha. From here on the proposed
repo is irrelevant for this approval — the submitter can amend,
force-push, or `rm -rf` the proposed repo and the queued
approval still points at an immutable git object inside
applied.
3a. **Flake validation (ApplyCommit only):** after the proposal tag
is planted, hive-c0re reads `proposal/<id>:flake.lock` and
runs two checks. If either check fails, no
pending approval is created for the operator — the row is
marked failed and surfaces on the dashboard with the
validation message:
- **Stale lock** — materialises the commit in a temp worktree,
runs `nix flake lock` (no `--update-input` flags, so it only
fills missing entries), and rejects if the committed
`flake.lock` differs from the result. Triggered when the
submitter added or removed `inputs` in `flake.nix` without
re-running `nix flake lock`. Fix: run `nix flake lock` in
the config repo, commit, and re-submit.
- **Duplicate inputs** — groups lock nodes by their canonical
`original` field; rejects if two or more nodes share the same
source. This usually means an input is missing
`inputs.<x>.inputs.nixpkgs.follows = "nixpkgs"`. Fix: add
the `follows` directive, re-lock, and re-submit.
Both checks only flag *new* violations — agents whose lock
already carried duplicates before this check was added are
unaffected until a coordinated config-change pass.
4. Operator sees the proposal as a card on the dashboard — a
full multi-file diff, toggleable between three bases (vs the
running tree / vs the last approved proposal / vs the
previous queued proposal) — and clicks ◆ APPR0VE (or
`hive-c0re approve <id>` on the CLI).
5. hive-c0re moves the working tree to `proposal/<id>` and runs
the build under a sequence of tags (see below). On success,
`applied/main` fast-forwards to the proposal commit. On
failure, main stays put and the working tree resets back to
the previous deployed commit.
6. `HelperEvent::ApprovalResolved` (and `Rebuilt` for the
ApplyCommit kind) land in the **submitting agent's** inbox, carrying
both the canonical sha and the terminal tag. Helper events route to
the submitting agent via `notify_submitter` (the approval row carries
a `submitter` column recording which agent called `request_apply_commit`
or `request_init_config`).
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**.
2. hive-c0re's `/webhook/config-pr` endpoint receives the Forgejo
`pull_request` event (opened / synchronized / reopened) and queues a
`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
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
"review PR on forge" deep link. They click ◆ APPR0VE (or
`hive-c0re approve <id>` on the CLI) once satisfied.
4. On approve, `run_merge_config_pr`:
- re-reads the live PR head and **aborts if it drifted** from the
reviewed `fetched_sha` (the submitter must push again, which queues
a fresh approval);
- fetches that head into the applied repo and **eval-verifies** it —
a flake eval on a throwaway checkout. This is the trust gate: it
relies on c0re's own eval, not on any in-repo (agent-forgeable)
signal like a CI status;
- fast-forwards the forge config repo's `main` to the reviewed head
(this IS the merge — a `core`-authenticated ff-push, so a moved PR
head can't substitute bytes) and marks the PR merged (best-effort;
`main` is already there);
- runs the shared deploy tail (`deploy_applied_target`): builds under
the tag sequence below, and on success `applied/main`
fast-forwards to the reviewed commit + the meta lock bumps. On
failure, main stays put and the working tree resets to the previous
deployed commit.
5. `HelperEvent::ApprovalResolved` (and `Rebuilt`) land in the
**submitting agent's** inbox via `notify_submitter`, carrying both the
canonical sha and the terminal tag (the approval row carries a
`submitter` column recording the agent the change is for).
### Withdrawing a pending approval
@ -88,53 +76,47 @@ 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 are the first step in a two-step spawn
flow. On approve, hive-c0re seeds the proposed config repo with
a default `agent.nix` template and sends `HelperEvent::ConfigReady { agent }`
to the submitting agent's inbox via `notify_submitter`. The submitting
agent then reviews,
edits, and commits the template before calling `request_apply_commit`
to proceed to an `ApplyCommit` approval. The first `ApplyCommit`
creates the container; subsequent ones rebuild it with new config.
This gives the submitting agent (and operator) an explicit review gate on the
initial configuration before any container is created.
`InitConfig` approvals seed a brand-new agent's proposed config repo.
On approve, hive-c0re seeds it with a default `agent.nix` template and
sends `HelperEvent::ConfigReady { agent }` to the submitting agent's
inbox via `notify_submitter`. The submitting agent reviews, edits, and
commits the template; 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.
### Approval kinds (wire shapes)
`ApprovalKind` carries six variants; each maps to a different
`ApprovalKind` carries five variants; each maps to a different
`commit_ref` encoding because that field is overloaded as the
kind-specific payload carrier.
- `ApplyCommit``commit_ref` is the submitted git sha
(7-40 hex chars). The canonical, hive-c0re-vouched sha after the
proposal fetch lives in `fetched_sha` on the same `Approval`
row (only `ApplyCommit` populates it). See the End-to-end flow
above.
- `MergeConfigPr` — the PR-based config flow's counterpart to
`ApplyCommit`. Triggered automatically: when an agent opens (or
force-pushes) a PR on its `agent-configs/<agent>` forge repo,
hive-c0re's `/webhook/config-pr` endpoint receives the Forgejo
pull_request event and queues this approval row. No MCP tool call
needed — the forge PR IS the request. `commit_ref` stores the
**PR number** (decimal), and `fetched_sha` is the PR **head sha
at queue time** (the "reviewed" sha). On approve,
- `MergeConfigPr` — the config-change flow. Triggered automatically:
when an agent opens (or force-pushes) a PR on its
`agent-configs/<agent>` forge repo, hive-c0re's `/webhook/config-pr`
endpoint receives the Forgejo pull_request event and queues this
approval row. No MCP tool call needed — the forge PR IS the request.
`commit_ref` stores the **PR number** (decimal), and `fetched_sha` is
the PR **head sha at queue time** (the "reviewed" sha). On approve,
`run_merge_config_pr` re-reads the live PR head and aborts if it
drifted from `fetched_sha` (submitter must push again to
re-trigger), then fetches that head into the applied repo,
eval-verifies it, fast-forwards the forge config repo's `main` to
it (the merge), marks the PR merged (best-effort — `main` is
already there), and runs the same shared deploy tail as
`ApplyCommit` (`deploy_applied_target`). Never a first spawn.
- `Spawn` — direct container creation under the default
`agent.nix` template. `commit_ref` is empty. Submitted via
`HostRequest::RequestSpawn` (operator-gated, the
`◆ R3QU3ST SP4WN` dashboard button + `hive-c0re request-spawn`
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). The
agent-side `RequestSpawn` is gone; the submitting agent goes through the
`InitConfig``ApplyCommit` two-step instead so the spawn
captures the customised config.
already there), and runs the shared deploy tail
(`deploy_applied_target`). Never a first spawn.
- `Spawn` — direct container creation from the agent's config repo.
`commit_ref` is empty. Submitted via `HostRequest::RequestSpawn`
(operator-gated, the `◆ R3QU3ST SP4WN` dashboard button +
`hive-c0re request-spawn` 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).
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.
@ -239,14 +221,14 @@ module that `setup_applied` used to generate inline.
Containers run against `--flake /var/lib/hyperhive/meta#<n>`.
Per-deploy lock flow (two-phase, owned by
`actions::run_apply_commit` → `meta::{prepare,finalize,abort}
_deploy`):
`actions::run_merge_config_pr` → `deploy_applied_target`
`meta::{prepare,finalize,abort}_deploy`):
1. `meta::prepare_deploy(name)` runs
`nix flake lock --update-input agent-<n>` without
committing. Working tree of meta now points the input at
`applied/<n>/main` (which `run_apply_commit` already
fast-forwarded to `proposal/<id>`).
`applied/<n>/main` (which the deploy already fast-forwarded to
the reviewed PR head).
2. `lifecycle::rebuild_no_meta` runs
`nixos-container update <c> --flake meta#<name>`. Nix
evaluates against the staged lock.
@ -323,34 +305,29 @@ wraps it with identity + `HIVE_PORT` / `HIVE_LABEL` /
### Tag state machine
Every approval id walks through a fixed set of tags on the
underlying commit inside the applied repo:
Each deploy leaves a tag on the underlying commit inside the applied
repo:
| Tag | When | Annotated? |
|---|---|---|
| `proposal/<id>` | request_apply_commit, after fetch | no |
| `approved/<id>` | operator approve | no |
| `building/<id>` | rebuild started | no |
| `deployed/<id>` | rebuild succeeded — `main` ff's here | no |
| `failed/<id>` | rebuild failed | yes (body = error) |
| `denied/<id>` | operator deny | yes (body = operator note) |
`applied/main` is always the latest `deployed/*`. `denied/` and
`failed/` are terminal; the submitting agent submits a new commit + new
approval id to retry. Because tags are first-class git objects,
rejected and failed trees stay browsable forever — `git log
--tags` in the applied repo is the audit trail.
`deployed/0` is planted 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
submitter pushes again (or closes it) to retry.
### Dispatch via the job queue
Long-running approval work — `ApplyCommit`, `UpdateMetaInputs`,
Long-running approval work — `MergeConfigPr`, `UpdateMetaInputs`,
`Spawn` — no longer runs inline inside `actions::approve`. Instead
the approval handler submits a DAG to the global job queue
(`docs/coordinator.md::Job queue`):
| `ApprovalKind` | DAG submitted | source |
|---|---|---|
| `ApplyCommit` | `rebuild` (single opaque `ApprovalDeploy` node) | `approval` |
| `MergeConfigPr` | `rebuild` (single opaque `ApprovalDeploy` node) | `approval` |
| `UpdateMetaInputs` | `meta_update` (`MetaLock` + rebuild fan-out) | `approval` |
| `Spawn` | `spawn` (`Create → WriteDropin → Reconcile`) | `approval` |
@ -358,9 +335,8 @@ the approval handler submits a DAG to the global job queue
| `SchedulePrompt` | — runs inline (single sqlite insert) | — |
The DAG carries the originating `approval_id`. The `ApprovalDeploy`
node runs the kind-specific pipeline (`run_approval_apply_commit` /
`run_approval_merge_config_pr` — the two-phase meta deploy stays
inside `actions.rs`) and fires the matching `HelperEvent::*` via
node runs `run_approval_merge_config_pr` (the two-phase meta deploy
stays inside `actions.rs`) and fires the matching `HelperEvent::*` via
`finish_approval` itself; `Spawn` and `UpdateMetaInputs` DAGs resolve
through `actions::resolve_approval_dag` when the DAG settles terminal
(a spawn additionally runs the post-spawn forge bookkeeping there).
@ -391,22 +367,25 @@ The bundled `hive-forge` container is mandatory (it deploys with
hyperhive), and hive-c0re mirrors every agent's applied repo into a
private `agent-configs` Forgejo org. `forge::push_config(<name>)` pushes `applied/main` plus
every tag to `agent-configs/<name>` after each ref mutation:
the spawn that seeds `deployed/0`, every `request_apply_commit`
(which plants `proposal/<id>`), every approve / deny, and a
the spawn that seeds `deployed/0`, every successful deploy (which
plants `deployed/<id>`) or failed build (`failed/<id>`), and a
sweep at startup. Pushes are best-effort — a missing or stopped
forge never blocks a deploy.
The org is private and agents are not members, so only the
`core` user (a Forgejo site admin) can read it: an agent can't
reach another agent's config — or even its own — through the
forge. The tokenised push URL is passed inline to `git push`,
never written into `applied/<n>/.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.
Each agent is a **write collaborator on its own** `agent-configs/<name>`
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
`applied/<n>/.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.
The dashboard deep-links into this org — a `config repo` link
per container row and a `commit on forge` link per approval
card. See `docs/web-ui.md`.
per container row and a `review PR on forge` link per config-PR
approval card. See `docs/web-ui.md`.
### Submitting agent's view of config repos
@ -484,8 +463,8 @@ hive-c0re creates it. The root agent's flake lives at
`/var/lib/hyperhive/applied/ruth/`; its proposed config at
`/var/lib/hyperhive/agents/ruth/config/`. The root agent can edit its own
`agent.nix` (visible inside the container at `/agents/ruth/config/`)
and submit `request_apply_commit("ruth", <sha>)` for operator
approval.
and open a config PR on `agent-configs/ruth` for operator approval,
same as any other agent.
Differences from sub-agents:
@ -559,11 +538,11 @@ root agent. Variants (`hive_sh4re::HelperEvent`):
- `ApprovalResolved { id, agent, commit_ref, status, note }`
fired by `actions::approve` + `actions::deny` whenever an
approval transitions to its terminal state.
- `Spawned { agent, ok, note }``actions::approve` (first-time
ApplyCommit-kind) + admin `HostRequest::Spawn` (deprecated).
- `Spawned { agent, ok, note }`the `Spawn` approval DAG +
admin `HostRequest::Spawn`.
- `Rebuilt { agent, ok, note }``auto_update::rebuild_agent`
(covers startup scan + manual `/rebuild` from dashboard) +
`actions::approve` (ApplyCommit).
the `MergeConfigPr` deploy.
- `Killed { agent }` — admin `HostRequest::Kill` + dashboard
`/kill` + the `Kill` MCP tool.
- `Destroyed { agent }``actions::destroy`.
@ -582,9 +561,9 @@ root agent. Variants (`hive_sh4re::HelperEvent`):
often greets the agent on this event.
- `ConfigReady { agent }` — a new agent's proposed config repo was
just seeded (post-`InitConfig` approval). The root agent can now
edit `/agents/<agent>/config/agent.nix`, commit the changes,
and submit `request_apply_commit` with the commit sha to create
the container (first ApplyCommit also triggers spawn bookkeeping).
edit `/agents/<agent>/config/agent.nix`, commit the changes, and
hand off to the operator to **spawn** the agent (the `Spawn`
approval creates the container from that config).
- `NeedsUpdate { agent }` — sub-agent's recorded flake rev is
stale. The root agent calls `update(name)` to rebuild — idempotent,
no approval required.