docs: tag-driven config-apply plan + migration story

scratchpad in claude.md marks this as in-flight; docs/approvals.md
gets the new tag state machine (proposal/approved/building/deployed/
failed/denied) and the manager applied.git read-only mount. todo
picks up the unprivileged-containers git-identity caveat and a web
ui for config repos as a downstream follow-up.
This commit is contained in:
müde 2026-05-15 22:43:47 +02:00
parent 75e7faff0c
commit 497cd15137
3 changed files with 118 additions and 28 deletions

View file

@ -114,14 +114,25 @@ read them à la carte.
In-flight or recent context that hasn't earned a section yet. In-flight or recent context that hasn't earned a section yet.
Prune freely. Prune freely.
- **Imminent:** overhaul the git management of agent configs. - **In flight:** tag-driven config-apply overhaul. Keep the
Current shape: per-agent `proposed/` repo the manager edits two-repo split (proposed = manager RW, applied = core-only)
+ `applied/` repo hive-c0re owns, with `request_apply_commit` for safety — agent can rm -rf its own repo but never reaches
shuttling commits between them. Pre-compact note: keep an eye applied. New flow: at `request_apply_commit` time hive-c0re
on whether the two-repo split is still the right shape, or if fetches the manager's commit into applied and tags it
a single repo with `proposed/` and `applied/` branches (or a `proposal/<id>`; the manager's repo is then dead to core for
shared bare repo per agent with refs/proposed and refs/applied) that approval. Approve/deny/build are encoded as more tags
would simplify the diff / approve / apply path. (`approved/`, `building/`, `deployed/`, `failed/`, `denied/`)
on the same commit; `applied/main` only fast-forwards on
`deployed/`. Failure tags are annotated with the build error;
deny tags with the operator note. Manager gets `applied/.git`
bind-mounted RO at `/agents/<n>/applied.git` so it can `git
show` deployed/failed/denied trees and diff against its own
working tree. agent.nix stays the entry point but arbitrary
files in the manager's commit are now preserved; `flake.nix`
becomes hive-c0re-generated, gitignored, regenerated only on
spawn/rebuild. Migration: no in-place. Each existing agent
needs `destroy --purge` + re-spawn; tombstones lose their
history. See `docs/approvals.md` for the tag state machine.
- **Recent (since last compaction):** inline +/- diffs on - **Recent (since last compaction):** inline +/- diffs on
Write/Edit, send full body via collapsed details, operator Write/Edit, send full body via collapsed details, operator
cancel + ttl on questions, deny-with-reason, dashboard cancel + ttl on questions, deny-with-reason, dashboard

14
TODO.md
View file

@ -21,7 +21,12 @@ Pick anything from here when relevant. Cross-cutting design notes live in
nixos-container equivalent) so uid 0 inside maps to an unprivileged uid nixos-container equivalent) so uid 0 inside maps to an unprivileged uid
on the host, and a container-root compromise lands the attacker on an on the host, and a container-root compromise lands the attacker on an
ordinary user account, not the host's root. Requires per-agent state ordinary user account, not the host's root. Requires per-agent state
dirs to be chown'd to that uid on the host side. dirs to be chown'd to that uid on the host side. The per-agent git
identity (currently injected via `programs.git.config.user` against
the root user in `setup_applied`'s generated flake) also needs to be
provisioned for whatever non-root user claude runs as, or commits
the manager makes against `/agents/<n>/config` will fall back to a
generic `nixos@…` identity.
- **Bash command allow-list.** Replace the blanket `Bash` allow with a - **Bash command allow-list.** Replace the blanket `Bash` allow with a
pattern allow-list (`Bash(git *)`, `Bash(nix build .*)`, etc.) per pattern allow-list (`Bash(git *)`, `Bash(nix build .*)`, etc.) per
claude-code's `--allowedTools` extended grammar. Likely lives in claude-code's `--allowedTools` extended grammar. Likely lives in
@ -64,6 +69,13 @@ Pick anything from here when relevant. Cross-cutting design notes live in
## UI / UX ## UI / UX
- **Web UI for config repos.** Browse history, diffs, tags
(proposed + approval/* + applied/*) per agent, all from the
dashboard. Something lighter than a full forge — read-only
log + diff + raw-file view is enough. Pairs naturally with
the upcoming config-repo overhaul (tags become the audit
trail; UI surfaces them).
- **xterm.js terminal** embedded per-agent, attached to a PTY exposed by - **xterm.js terminal** embedded per-agent, attached to a PTY exposed by
the harness. Pairs well with the unprivileged-container work — would let the harness. Pairs well with the unprivileged-container work — would let
the operator drop into the container without `nixos-container root-login`. the operator drop into the container without `nixos-container root-login`.

View file

@ -8,15 +8,29 @@ happens after a decision lands.
## End-to-end approval flow ## End-to-end approval flow
1. Manager edits `/agents/<name>/config/agent.nix` (bind-mounted 1. Manager edits files under `/agents/<name>/config/` (any tracked
from the host's per-agent `proposed` repo) and commits. path, but `agent.nix` is the contract entry point) and commits
with its own git identity.
2. Manager submits the commit sha via `request_apply_commit(agent, 2. Manager submits the commit sha via `request_apply_commit(agent,
commit_ref)`. commit_ref)`.
3. Operator sees the diff on the dashboard, clicks ◆ APPR0VE (or 3. **hive-c0re immediately fetches that commit from the proposed
repo into the applied repo and tags it `proposal/<id>`.** The
approval row stores both the manager-supplied sha and the
canonical hive-c0re-vouched sha. From here on the proposed
repo is irrelevant for this approval — the manager can amend,
force-push, or `rm -rf` the proposed repo and the queued
approval still points at an immutable git object inside
applied.
4. Operator sees the diff on the dashboard, clicks ◆ APPR0VE (or
`hive-c0re approve <id>` on the CLI). `hive-c0re approve <id>` on the CLI).
4. hive-c0re reads the file at that sha from `proposed`, applies 5. hive-c0re moves the working tree to `proposal/<id>` and runs
into `applied`, commits there, runs `nixos-container update`. the build under a sequence of tags (see below). On success,
5. `HelperEvent::ApprovalResolved` lands in the manager's inbox. `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 manager's inbox, carrying both
the canonical sha and the terminal tag.
`Spawn` approvals follow the same shape but skip the commit-diff `Spawn` approvals follow the same shape but skip the commit-diff
step — the operator just sees the name. On approve, hive-c0re step — the operator just sees the name. On approve, hive-c0re
@ -26,27 +40,80 @@ shows a spinner.
## Two repos per agent ## Two repos per agent
``` ```
/var/lib/hyperhive/agents/<name>/config/ proposed /var/lib/hyperhive/agents/<name>/config/ proposed — manager RW
└── agent.nix # the only file the └── <anything> # any files the manager
# manager can change # wants in the commit.
# (initial commit by # agent.nix is the
# hive-c0re on first # convention entry
# spawn, never touched # point; flake.nix is
# again). # generated and not
# tracked here.
/var/lib/hyperhive/applied/<name>/ applied — hive-c0re-only /var/lib/hyperhive/applied/<name>/ applied — core-only
├── flake.nix # auto-generated ├── .git/ # tag-rich history
└── agent.nix # overwritten by approve ├── .gitignore # ignores flake.nix
# from the proposed commit ├── flake.nix # hive-c0re-generated,
│ # untracked, rewritten
│ # on spawn/rebuild only
├── agent.nix # working tree of main
└── <other manager files> # also tracked
``` ```
The container's `--flake` ref is `<applied_dir>#default`. The flake Why two physical repos: the manager's `/agents/<n>/config/` is
extends `hyperhive.nixosConfigurations.{agent-base|manager}` with RW — a buggy or hostile agent can `git clean -fdx` its own
proposed tree. The applied repo is never bind-mounted (except
the read-only `.git` exposure described below) so a destructive
move inside the container cannot reach it.
The container's `--flake` ref is `<applied_dir>#default`. The
generated `flake.nix` extends
`hyperhive.nixosConfigurations.{agent-base|manager}` with
`./agent.nix` plus an inline module setting `./agent.nix` plus an inline module setting
`programs.git.config.user` (committer identity = the agent's name) `programs.git.config.user` (committer identity = the agent's name)
and `systemd.services.<harness>.environment` (`HIVE_PORT`, and `systemd.services.<harness>.environment` (`HIVE_PORT`,
`HIVE_LABEL`, `HIVE_DASHBOARD_PORT`). `HIVE_LABEL`, `HIVE_DASHBOARD_PORT`).
### Tag state machine
Every approval id walks through a fixed set of tags 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 manager 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.
### Manager view of applied
`/agents/<n>/applied.git` is a **read-only bind-mount** of
`/var/lib/hyperhive/applied/<n>/.git` inside the manager
container. The manager fetches tags into its proposed clone
(`git fetch /agents/<n>/applied.git refs/tags/*:refs/tags/applied/*`)
and `git show` any deployed / failed / denied tree to see what
actually shipped, what error blocked the last build, or what
note the operator left on a denial. The RO mount means git
plumbing inside the manager cannot corrupt the applied repo.
## Migration from the pre-tag scheme
There is no in-place migration. Each existing agent must be
purged and re-spawned: `hive-c0re destroy --purge <name>` (or
PURG3 on the dashboard), then `request_spawn` and the operator
approves the fresh agent. The new agent starts with `deployed/0`
seeded by hive-c0re; the manager's first config edit becomes
`proposal/1` and walks the tag scheme from there. Pre-overhaul
tombstones lose their config history.
## Manager (`hm1nd`) is hive-c0re-managed ## Manager (`hm1nd`) is hive-c0re-managed
The manager container runs through the **same lifecycle as The manager container runs through the **same lifecycle as