diff --git a/CLAUDE.md b/CLAUDE.md index 1f446a2c..86f4913f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -129,6 +129,3 @@ The docs below own the details — this section just points at them. window:** → [`docs/turn-loop.md`](docs/turn-loop.md). - **Two-step spawn, approval flow, flake.lock validation:** → [`docs/approvals.md`](docs/approvals.md). -- **Pre-push lint hook** (catches tracker-tag and comment-block failures - before CI does — install once per clone): - `ln -sf ../../scripts/pre-push .git/hooks/pre-push` diff --git a/docs/boundary.md b/docs/boundary.md index 1fc5ed3b..c1608f7c 100644 --- a/docs/boundary.md +++ b/docs/boundary.md @@ -5,12 +5,11 @@ _implementation_ work — container network isolation, the unifying gateway, core-daemon privsep — is tracked as `area:ops` issues on the forge. -The operator/agent boundary is now technically enforced, not just a -convention. Containers run in private netns (network isolation is -always on), the gateway proxies all operator-facing traffic, and -`hive-c0re` runs as the unprivileged `hive-core` user. All three -`area:ops` pillars — network isolation, the gateway, and privsep — -are complete and active. +Today "the operator surface" and "the agent surface" are a +_convention_, not a boundary — nothing stops a container from +curling the core daemon on `localhost:`, or another agent's +web UI. Network isolation, the gateway, and privsep together turn +that convention into an enforced boundary. ## Two principals, two paths @@ -40,17 +39,14 @@ agent page). ## Why network isolation is the load-bearing step -Without network isolation, containers share the host network namespace -and can reach `localhost:`, the dashboard, and every other -agent's web port — the operator/agent split is on the honour system and -every boundary claim above is aspirational. Network isolation is what -makes the boundary _real_; the gateway and privsep are ergonomics and +Containers currently share the host network namespace, so a +container can reach `localhost:`, the dashboard, and +every other agent's web port. Until that changes, the +operator/agent split is on the honour system — every boundary +claim above is aspirational. Network isolation is what makes the +boundary _real_; the gateway and privsep are ergonomics and defence-in-depth layered on top. -Network isolation is now complete and always on: every agent container -runs in a private netns behind the hive bridge. The shared-netns mode -was removed. See `docs/network.md`. - The `area:ops` issues followed this sequencing: 1. **Gateway** — pure ergonomics win, unblocks same-origin (lets the @@ -58,8 +54,7 @@ The `area:ops` issues followed this sequencing: behavioural risk. An nginx nixos-container now sits in front of all surfaces; per-agent UIs are proxied under `/agent//`. 2. **Network isolation** — the load-bearing step that turns the - honour-system split into an enforced boundary. **Complete** — - always-on, unconditional; the shared-netns mode was removed. + honour-system split into an enforced boundary. In progress. 3. **Privsep** — defence in depth on the core process; `hive-c0re` runs as the unprivileged `hive-core` user and delegates root operations to `hive-priv`, a narrow socket-activated helper. See diff --git a/docs/ci.md b/docs/ci.md index 73da7583..b3da8e13 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -1,46 +1,6 @@ # hive-ci: Forgejo Actions Runner -The `hive-ci` module runs a Forgejo Actions runner in a `hive-ci` nixos-container, -executing CI jobs from `.forgejo/workflows/ci.yml` on every PR. - -## CI checks - -Three jobs run on every PR (and on `workflow_dispatch` for manual re-triggers): - -| Job | What it runs | Currently required | -| --- | --- | --- | -| **nix flake check** | treefmt + rustfmt formatting, `cargo clippy -D warnings`, `cargo test`, module evaluation | yes | -| **tracker-tag lint** | flags `#NNN` issue tags in source and comments (`scripts/check-issue-refs.sh`) | no (red, non-blocking) | -| **comment-block lint** | flags contiguous comment blocks over 30 lines (`scripts/check-comment-blocks.sh`) | no (red, non-blocking) | - -The tracker-tag and comment-block checks are non-blocking today (a hit fails the -check but does not prevent merge) while the legacy backlog is cleaned up. They are -expected to become required checks once the tree is clean. - -### Running checks locally - -Don't run `nix flake check` directly — it dispatches to the shared build farm and -wastes a remote-builder slot. Use the devshell equivalents instead: - -```sh -nix develop -c cargo clippy --all-targets -- -D warnings -nix develop -c cargo test -nix develop -c treefmt # same as nix fmt; treefmt covers rustfmt + nixfmt + taplo -sh scripts/check-issue-refs.sh # tracker-tag lint -sh scripts/check-comment-blocks.sh # comment-block lint -``` - -A git pre-push hook that automates the two lint checks is provided at -`scripts/pre-push`. Install it once per clone: - -```sh -ln -sf ../../scripts/pre-push .git/hooks/pre-push -``` - -After that, any `git push` automatically runs both lints and aborts with a -diagnostic if either fails — catching the issue locally before CI sees it. -Note that the hook does **not** run `cargo clippy` or `cargo test` (those are -slow); run those manually before pushing Rust changes. +The `hive-ci` module runs a Forgejo Actions runner in a `hive-ci` nixos-container, executing CI jobs from `.forgejo/workflows/ci.yml` (e.g., `nix flake check` on every PR). ## Operator bootstrap diff --git a/docs/coordinator.md b/docs/coordinator.md index 539aa159..b8ac35d6 100644 --- a/docs/coordinator.md +++ b/docs/coordinator.md @@ -117,28 +117,19 @@ render. ## Auto-update sweep -On startup, `auto_update.rs` rebuilds containers that actually need it. Two skip -rules keep boot-time work minimal: - -1. **Stopped containers** are deferred: the startup sweep enqueues nothing for them. - When the operator later starts a stopped container (via the dashboard or the - `start` MCP tool), both `run_start` (queue path) and `handle_start` (socket path) - check the rev marker first — if it's stale, the start is silently upgraded to a - full rebuild+start so the container runs current nix derivations. - -2. **Running containers with a matching rev marker** are skipped: if the per-agent - `.{name}.hyperhive-rev` file under `/var/lib/hyperhive/applied/` already holds - the current flake rev, no nix work is needed and the entry is omitted entirely. +On startup, `auto_update.rs` rebuilds every known container unconditionally. +`nixos-container update` is a no-op at the nix level when nothing changed (same +store path), so the cost is low and avoids rev-marker staleness — all agents always +need an update pass when any meta commit lands. `auto_update::run` enqueues a single `StartupSweep` parent entry (`kind = startup_sweep`, `agent = "hyperhive"`) followed by per-agent `Rebuild` children -for the agents that do need rebuilding (`source = startup_sweep`, `parent_id = -sweep_id`). The sweep description records the rebuild / deferred / skipped counts -so the operator can see at a glance how much work the boot triggered. The child +(`source = startup_sweep`, `parent_id = sweep_id`). The worker processes the parent +by bumping the meta `hyperhive` input lock, then transitions it to Done. The child rebuilds drain sequentially through the queue; the dashboard renders them nested -under the parent. +under the parent so the operator can see the whole boot-time sweep in one group. -Before the sweep-grouping change, each boot enqueued flat `Rebuild` entries with +Before this change, each boot enqueued flat `Rebuild` entries with `source = AutoUpdate` and no parent — visible but ungrouped. ## Meta flake diff --git a/docs/gotchas.md b/docs/gotchas.md index 153df0fa..23181fa3 100644 --- a/docs/gotchas.md +++ b/docs/gotchas.md @@ -323,10 +323,11 @@ connects to the compositor at `127.0.0.1:`. ## Nix options reference (`nix/docs/default.nix`) `pkgs.nixosOptionsDoc` over two evaluated module trees: -`hostEval` (a stub NixOS system loading `hive-c0re.nix` with every -hyperhive subsystem `mkForce false` so heavy build inputs stay out of -the eval) and `agentEval` (evaluates `agent-base.nix` fresh for the -per-agent options tree). +`hostEval` (a stub NixOS system loading `self.nixosModules.default` +with every hyperhive subsystem `mkForce false` so heavy build +inputs stay out of the eval) and `agentEval` (reuses the already-evaluated +`agent-base` container config so the per-agent options tree is +identical to what a real agent container sees). Three output trees consumed by `flake.nix`, all **markdown**: @@ -355,63 +356,3 @@ Host options live entirely under `services.hyperhive.*`. The options tree picks up everything under that root — picking against stray roots produces an empty tree and renders the host page as template chrome with no `

` headers. - -### Docs drv stability: `nixSrc` + stub overlay - -Naively, the docs evaluation depends on `self` (the flake's store path), -so every commit — even Rust-only or frontend-only changes — produces new -docs drv hashes. The remote builder must rebuild docs from scratch for -every PR branch, and if its store is full the build fails with a cached -failure that blocks CI for the whole branch. - -The fix (`nix/docs/default.nix`): - -1. **`nixSrc`** — `builtins.path` on the `nix/` directory, wrapped in - `builtins.unsafeDiscardStringContext` to strip `self`'s store-path - context. The resulting store path is content-addressed from the nix/ - file contents only. Docs drvs only change when a `.nix` file changes. - -2. **`docsStubOverlay`** — replaces `self.overlays.default` with stub - packages (`pkgs.emptyFile` / `pkgs.emptyDirectory`) for the docs eval. - `nixosOptionsDoc` renders `defaultText` for all package options anyway; - the stubs prevent attribute-missing eval errors without pulling in the - Rust or frontend build closure. - -3. Both `hostEval` and `agentEval` are evaluated from `nixSrc` paths - (not `self`), so the docs drv dependency chain ends at `nixSrc`. - -Why `builtins.unsafeDiscardStringContext`? The path string -`toString self + "/nix"` carries `self`'s string context, which would -make `builtins.path` include `self` as a build dependency even after -content-addressing the directory. Discarding the context makes the -resulting `nixSrc` truly independent of `self`'s store path. - -### `nix fmt` fails in a git worktree with "object not found" - -`nix fmt` (and any `nix` command that fetches a `git+file://` flake -URL) uses libgit2 internally to compute `revCount` — the number of -commits reachable from HEAD. This walk fails with: - -``` -error: getting Git object '': object not found (libgit2 error code = 9) -``` - -when a commit that was reachable at some earlier evaluation is now gone -(GC'd, rebased away, or pruned). The failure is persistent: clearing -`~/.cache/nix/{eval-cache-v6,gitv3,fetcher-cache-v4.sqlite}` does not -help because the missing object is a structural gap in the git object -graph itself, not in nix's caches. - -**Workaround: use a plain clone, not a git worktree.** - -```bash -git clone http:///hyperhive/hyperhive.git ~/hh-work -cd ~/hh-work && nix fmt -``` - -The root cause is specific to worktrees: a worktree shares the object -store with its parent repo. If the parent repo's history was rewritten -(rebase, force-push, `git gc --prune`) while the worktree was checked -out at a branch tip that references the pruned commits via its reflog or -history, libgit2's rev-walk encounters the gap. A plain clone has its -own self-consistent object store and is immune to the issue. diff --git a/docs/network.md b/docs/network.md index e87e5802..3fad6e3e 100644 --- a/docs/network.md +++ b/docs/network.md @@ -1,14 +1,31 @@ # hive-network -Host-side bridge + per-agent private-netns isolation — always on -whenever hyperhive is enabled. Configured via -`services.hyperhive.network.*`. +Host-side bridge + per-agent DNS resolver — the foundation that +makes container netns isolation safe to land. Configured via +`services.hyperhive.network.*`; off by default during rollout. -> **Historical note:** the bridge and private-netns isolation landed in -> two separate phases. `services.hyperhive.network.enable` and -> `services.hyperhive.network.isolateContainers` are retained as -> deprecated no-op options so existing configs eval without change; both -> are ignored — isolation is the only mode. +## Why ship before netns isolation + +If netns isolation lands first, agent containers lose +`/etc/resolv.conf` propagation from the host and DNS breaks until a +separate resolver is up. Inverting the sequence — bridge + dnsmasq +first, netns flip second — makes the flag day boring: the resolver +endpoint is already live, agents just discover it via veth instead +of shared netns. + +## v1 vs v2 + +| feature | v1 (this PR) | v2 (after netns isolation) | +| ------------------------ | ------------------------------------------------------- | ----------------------------------------------- | +| bridge interface | created on host, no slave NICs | per-agent veth pairs attach | +| dnsmasq binding | bridge IP (reachable via host loopback in shared netns) | bridge IP (reachable via veth in private netns) | +| agent container netns | shared host | private | +| agent `/etc/resolv.conf` | unchanged (host DNS) | `nameserver ` | +| `address` rules target | `` (works in both modes) | unchanged from v1 | + +The `address` rules ship pointing at the bridge IP from v1 so the +DNS contract is fixed before any container actually depends on it +— minimises the things that flip on netns day. ## Container shape (where dnsmasq lives) @@ -16,7 +33,9 @@ Co-located in the existing `hive-gateway` container — single front-door for both DNS and HTTP, saves a sibling container, single systemd-unit / state surface to monitor. The gateway shares host netns (`privateNetwork = false`) so dnsmasq's `bind-interfaces` -listener on `bridgeIp` is on the host's bridge interface. +listener on `bridgeIp` works without any veth gymnastics today; when +agent containers flip to private netns the binding doesn't change +(it's still on the host's bridge interface). ## Configuration @@ -25,14 +44,19 @@ listener on `bridgeIp` is on the host's bridge interface. services.hyperhive = { enable = true; domain = "darkest.space"; - # network.bridgeIp = "10.42.0.1"; # default - # network.upstreamDns = [ "1.1.1.1" "9.9.9.9" ]; # default + network.enable = true; # opt in to bridge + DNS + network.bridgeIp = "10.42.0.1"; # default + network.upstreamDns = [ # default Cloudflare + Quad9 + "1.1.1.1" + "9.9.9.9" + ]; }; } ``` -Requires `services.hyperhive.domain` to be set — the dnsmasq resolver -is authoritative for `` and its sub-domains. +Asserts `services.hyperhive.domain != null` (resolver needs a domain +to be authoritative for) + `services.hyperhive.gateway.enable = +true` (resolver lives in the gateway container). ## Bridge addressing @@ -67,14 +91,15 @@ agent containers. ## Firewall posture `networking.firewall.interfaces..allowedUDPPorts = [ 53 ]` -`networking.firewall.interfaces..allowedTCPPorts = [ 53 80 443 ]` -- Port 53 opens the resolver on the bridge interface only. Other - interfaces stay closed. The hive resolver isn't an external-facing - service. -- Ports 80 and 443 let isolated agents reach nginx (gateway - container, shared host netns) for the forge sub-domain, per-agent - UI proxies, and any other HTTP services. +- `allowedTCPPorts = [ 53 ]` opens the resolver on the bridge + interface only. Other interfaces stay closed. The hive resolver + isn't an external-facing service. + +When `isolateContainers = true`, `allowedTCPPorts` is extended with +`[ 80 443 ]` so isolated agents can reach nginx (gateway container, +shared host netns) for the forge sub-domain, per-agent UI proxies, +and any other HTTP services. ### Reaching host services (`exposeHostPorts`) @@ -101,16 +126,19 @@ DNS/gateway), so only expose services safe for any agent to reach. ## Container isolation -Each agent container runs in a private network namespace with a dedicated -veth pair attached to the bridge. The following table summarises what -the nix side sets up unconditionally: +`services.hyperhive.network.isolateContainers` (default `false`) flips +agent containers from shared host netns to private netns. Set only after +`enable = true` is stable in production — an assertion blocks the reverse. + +### What the nix side does when `isolateContainers = true` | effect | mechanism | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | IP forwarding | `boot.kernel.sysctl."net.ipv4.ip_forward" = 1` | | Internet NAT | `networking.nat { enable = true; internalInterfaces = [ bridgeName ]; }` — MASQUERADE on packets leaving via any external NIC | | Loopback DROP | `networking.firewall.extraInputRules` — drops bridge-subnet → `127.0.0.0/8` traffic; defence-in-depth against routing table leaks | -| Gateway access | `networking.firewall.interfaces..allowedTCPPorts = [ 80 443 ]` — lets isolated agents (private netns, veth on bridge) reach nginx on the host | +| Gateway access | `networking.firewall.interfaces..allowedTCPPorts = [ 80 443 ]` — lets isolated agents reach nginx on the host (shared netns) | +| Forge URL | `HIVE_FORGE_URL` flips from `http://127.0.0.1:3000` to `http://forge.` — agents resolve via dnsmasq, nginx proxies to forgejo | | c0re signal | `HIVE_NETWORK_ISOLATION=1`, `HIVE_NETWORK_BRIDGE`, `HIVE_NETWORK_SUBNET` in `systemd.services.hive-c0re.environment` | `HIVE_NETWORK_SUBNET` is the host-side bridge IP + prefix (e.g. @@ -120,7 +148,7 @@ address arithmetic. ### What the Rust side does -`hive-c0re` reads `HIVE_NETWORK_ISOLATION` and passes +`hive-c0re` reads `HIVE_NETWORK_ISOLATION` and, when set, passes `PRIVATE_NETWORK=1`, `LOCAL_ADDRESS=`, `HOST_ADDRESS=`, and `HOST_BRIDGE=` via `lifecycle::set_nspawn_flags` when creating or updating containers. Each @@ -143,17 +171,21 @@ but no path off the bridge subnet (no internet, no `api.anthropic.com`). nixos-container copies the **host's** `/etc/resolv.conf` into the container at every start. The host resolver (e.g. `127.0.0.53` from systemd-resolved, or a LAN router) is unreachable from a private netns and isn't -authoritative for the hive's own zones, so it is replaced with the -bridge dnsmasq at boot. Because the copy happens on every start, a +authoritative for the hive's own zones, so it must be replaced with the +bridge dnsmasq (the gateway IP). Because the copy happens on every start, a declarative `environment.etc."resolv.conf"` would be clobbered — so the wiring is runtime: - `hive-priv` drops a marker file (`/etc/hyperhive-bridge-dns`, carrying the - gateway IP) into each container's `/etc`. + gateway IP) into the container's `/etc` **only when isolated**, removing + it otherwise — so one shared container toplevel behaves correctly in both + netns modes. - the `hyperhive-isolated-dns` oneshot (harness-base.nix), gated on that marker, rewrites `/etc/resolv.conf` to `nameserver ` at boot. It is ordered `before` the harness (`hive-ag3nt`), the matrix daemon, and - `tea-login` so the resolver is correct before the first DNS lookup. + `tea-login` so the resolver is correct before the first DNS lookup; it's + an instant no-op in shared-netns mode (the marker is absent, so + `ConditionPathExists` skips it). **Why isolation is safe**: all hive-c0re communication goes through unix domain sockets (`/run/hive/mcp.sock` for agent requests, @@ -168,6 +200,19 @@ against a compromised agent reaching the c0re dashboard HTTP at `127.0.0.1`). Agents have no legitimate reason to reach the dashboard over loopback — the hive-c0re admin socket is a UDS, not TCP. +### Prerequisites before flipping on + +- All agents must have `hyperhive.web.useUnixSocket = true`. Agents that + still bind TCP on `0.0.0.0:` will be reachable at their bridge IP + from other agents on the same subnet — defeating the isolation goal. The + gateway routes via unix sockets so gateway reach is unaffected. + +### Migration behaviour + +Containers are destroyed and re-created when the flag flips. Agent state +under `/agents//state/` is bind-mounted and survives; the container +rootfs is recreated cleanly from the nix store. + ## Cross-references - `docs/gateway.md` — vhost map + the gateway container's other duties diff --git a/docs/terminal-rendering.md b/docs/terminal-rendering.md index d535cc3c..32da78cb 100644 --- a/docs/terminal-rendering.md +++ b/docs/terminal-rendering.md @@ -121,9 +121,12 @@ suffix, so the terminal degrades cleanly against older event shapes. `renderRichToolUse` (Write/Edit/send/ask/answer get custom renderings); on miss fall through to a flat `.tool-use` row with `fmtToolUse → fmtArgsGeneric`. - `fmtToolUse` surfaces the salient arg per built-in tool - (see [`fmtToolUse` patterns](#fmttooluse-patterns) below); - `fmtArgsGeneric` handles everything else. + `fmtToolUse` surfaces the salient arg per built-in tool: + `recv` shows `wait s` / `max ` when set; `Bash` + flags `[bg]`; `remind` shows `+Xm "preview"`; matrix + tools show `→ /: "body"` or ` [limit]`; + scheduling tools show `#id · fields`; `fmtArgsGeneric` + handles anything else. 4. `type == "user"` → walk `message.content[]` for `tool_result`; `renderToolResult` correlates via `tool_use_id → toolNameById` to default-open `recv` @@ -131,62 +134,6 @@ suffix, so the terminal degrades cleanly against older event shapes. long = collapsed details. 5. Unrecognised shape → `.sys` row (amber, `!` glyph). -### `fmtToolUse` patterns - -The `short` name strips the `mcp__hyperhive__` / `mcp__bash__` / -`mcp__matrix__` prefix and appends `*` (e.g. `recv*`, `run*`, -`send_message*`). Unprefixed tools (Read, Write, etc.) keep their -name as-is. - -| Tool | Rendered as | -|------|-------------| -| **Claude built-ins** | | -| `Read` | `Read ` | -| `Write` | rich diff row `Write · +N` | -| `Edit` | rich diff row `Edit · -N +N` | -| `Glob` | `Glob ` | -| `Grep` | `Grep ` | -| `Bash` | `Bash [bg] $ ` (also rich renderer for full body) | -| `TodoWrite` | `TodoWrite (N items)` | -| **Core hyperhive** | | -| `send*` | rich renderer: `send* → to · NL` (default-open body) | -| `recv*` | `recv*()` · `recv* wait Ns` · `recv* max N` | -| `ask*` | rich renderer: `ask* → to` (inline answer form for operator) | -| `answer*` | rich renderer: `answer* #id` | -| `remind*` | `remind* +Xm "preview"` or `remind* at HH:MMZ "preview"` | -| `set_status*` | `set_status* "text"` | -| `get_loose_ends*` | `get_loose_ends*()` or `get_loose_ends* [agent]` | -| `get_agent_meta*` | `get_agent_meta*()` or `get_agent_meta* name` | -| `cancel_loose_end*` | `cancel_loose_end* kind #id` | -| `ack_until*` | `ack_until* ≤N` | -| **Lifecycle** | | -| `kill*/restart*/start*/update*` | `kill* name` (etc.) | -| `get_logs*` | `get_logs* name` or `get_logs* name NL` | -| `get_host_journal*` | `get_host_journal*()` or with `[container] · [/grep/] · NL` | -| **Approvals / config** | | -| `request_apply_commit*` | `request_apply_commit* agent @ sha12` | -| `request_init_config*` | `request_init_config* name` | -| `request_update_meta_inputs*` | `request_update_meta_inputs* [inp1, …]` or `all` | -| **Scheduling** | | -| `list_schedules*` | `list_schedules*()` | -| `cancel_schedule*` | `cancel_schedule* #id all` or `#id [t1, t2]` | -| `fire_schedule_now*` | `fire_schedule_now* #id` | -| `edit_schedule*` | `edit_schedule* #id · body · interval · next · +N tgt · -N tgt` (only changed fields shown) | -| `request_schedule_prompt*` | `request_schedule_prompt* → t1, t2 at HH:MMZ` (+ `+Ns` if recurring) | -| **Bash MCP** | | -| `run*` | `run* [bg] $ cmd` (also rich renderer for full cmd body) | -| `status*` (bash) | `status* id:xyz` or `status* id:xyz · wait Ns` | -| `kill*` (bash) | `kill* id` or `kill* id [force]` | -| **Matrix MCP** | | -| `send_message*/send_dm*/send_reply*` | `send_message* → room: "body"` / `send_dm* → @user: "body"` | -| `send_reaction*` | `send_reaction* room emoji` | -| `read_room*` | `read_room* room` or `read_room* room [N]` | -| `mark_read*` | `mark_read* room` | -| `join_room*/open_dm*` | `join_room* room` / `open_dm* @user` | -| `invite_user*` | `invite_user* @user → room` | -| `download_file*` | `download_file* room` | -| **Everything else** | `fmtArgsGeneric` — see [Extra-MCP tools](#extra-mcp-tools) | - ## Markdown `mdNode(text)` wraps `marked.parse(text)` (the `marked` v4.x npm diff --git a/docs/tools/bash.md b/docs/tools/bash.md index 72d03ff0..6f2134f3 100644 --- a/docs/tools/bash.md +++ b/docs/tools/bash.md @@ -14,9 +14,9 @@ invocation regardless of tool groups. Submit a shell command for background execution (runs via `bash`). Stdout and stderr stream to `harness/bash-tasks/.{out,err}`. When the task completes (or times out, or the process errors), the -harness fires a wake with `from: "bash-task-"`; the body contains -the exit code and last stdout lines. Handle the completion on a future -turn. +harness fires a wake with `from: "bash-task-"` and the exit code + +- last stdout lines in the body; handle it on a future turn. * `timeout_secs` — kill the task after N seconds and mark it `timed_out`. Omit for no timeout (runs until natural exit). diff --git a/docs/tools/forge.md b/docs/tools/forge.md index 1245239a..605182da 100644 --- a/docs/tools/forge.md +++ b/docs/tools/forge.md @@ -178,20 +178,9 @@ plain comment show under `last comment`, not `reviews`. ### Repo management -Agents **cannot create repos directly via forge token** — Forgejo -disables push-to-create and the agent token doesn't have the Create -scope. Two paths exist depending on where the repo should live: - -**Agent repos (`agents/`)** — Use the `mcp__hyperhive__create_repo` -MCP tool (requires the `forge` tool group). hive-c0re creates the repo in -the c0re-owned `agents/` org, adds you as a write collaborator (not -owner), and enables branch protection (operator-team merge approval -required — you cannot self-merge). Clone URL is returned immediately. -This is the standard path for agents that need a working repo. - -**Other repos** — Use the CLI verbs below (`repo-create` / `repo-add-collaborator`). -These use the agent's own forge token so the repo lands under the agent's -user account or an org the agent belongs to. +Three verbs create repos and manage their membership — the supported +path for workflows that need a fresh repo (agents cannot push to +non-existent repos; the Forgejo instance disables push-to-create). **`repo-create `** — create a repo under the authenticated user and print its URL. Key flags: @@ -206,7 +195,8 @@ and print its URL. Key flags: active repo (`-r`/`HIVE_FORGE_REPO`). Companion to `repo-create`. The `--permission` flag accepts `read` / `write` (default) / `admin`. `hive-c0re` uses this internally when an agent's config repo is -initialised. +initialised (`create_repo` → `repo-add-collaborator` → agent gets +write access). **`repo-labels [PATTERN]`** — list every label defined on the repo, optionally filtered by a name substring (case-sensitive). Distinct from diff --git a/docs/tools/matrix.md b/docs/tools/matrix.md index 1c43bc02..bdf3f7fe 100644 --- a/docs/tools/matrix.md +++ b/docs/tools/matrix.md @@ -17,11 +17,6 @@ room you haven't read yet. accepts `!id:server` or `#alias:server`, daemon resolves either - `send_dm(user_id, body)` — open (or reuse) the DM room with `user_id` and post `body` to it -- `open_dm(user_id)` — resolve (find-or-create) the DM room with - `user_id` and return its room id **without sending anything**. - Use the returned id with room-based tools (`send_message`, - `send_file`, …) to deliver into a DM when `send_dm`'s body - parameter is inconvenient (e.g. for file attachments) - `send_reply(room, event_id, body)` — threaded reply to a specific event - `send_reaction(room, event_id, key)` — react to a message with an diff --git a/docs/turn-loop.md b/docs/turn-loop.md index d6e987b7..04f4a15e 100644 --- a/docs/turn-loop.md +++ b/docs/turn-loop.md @@ -616,17 +616,6 @@ at_unix_timestamp?)`, `request_next_turn()`. - **Scheduling + diagnostics** (`scheduling`, `diagnostics`) — scheduled prompts, `get_logs`. See [`docs/tools/scheduling.md`](tools/scheduling.md). -- **Forge repos** (`forge`) — `create_repo` — the only agent path to - create a repo under the `agents/` org (direct forge token creation is - disabled for agents). The repo is created in the c0re-owned `agents` - org; the calling agent gets write collaborator access; the default - branch is branch-protected (operator-team must approve merges, so the - agent cannot self-merge). Opt-in; not in any default preset. - See [`docs/tools/forge.md — Repo management`](tools/forge.md). -- **Web egress** (`web_tools`) — enables Claude's built-in `WebFetch` - and `WebSearch` tools (not MCP tools; added directly to the - `--allowedTools` list). Off by default; add the group in the - P3RM1SS10NS tab and rebuild to enable. - **Capability-gated** — `get_host_journal` (requires `read_host_journal` capability set via the P3RM1SS10NS tab; orthogonal to tool groups). Full list of capabilities and their diff --git a/docs/web-ui.md b/docs/web-ui.md index 07ac9e7b..e1f40c5a 100644 --- a/docs/web-ui.md +++ b/docs/web-ui.md @@ -37,9 +37,7 @@ This doc has been split for readability. Pick the section you need: [`web-ui/dashboard.md`](web-ui/dashboard.md) (Dashboard endpoints, Dashboard event channel). - **"How does the per-agent terminal render tool calls?"** → - [`terminal-rendering.md`](terminal-rendering.md) (full row - taxonomy + dispatch walkthrough); for a high-level summary see - [`web-ui/agent.md`](web-ui/agent.md) (Per-stream rendering). + [`web-ui/agent.md`](web-ui/agent.md) (Live view). - **"What slash commands does the agent accept?"** → [`web-ui/agent.md`](web-ui/agent.md) (Terminal-embedded prompt). - **"What are the per-agent HTTP endpoints?"** → diff --git a/docs/web-ui/agent.md b/docs/web-ui/agent.md index 84c5b6fa..24d7f420 100644 --- a/docs/web-ui/agent.md +++ b/docs/web-ui/agent.md @@ -96,8 +96,8 @@ through. Three flex columns: closes the menu without an extra POST. A second separator + **effort quick-picker** section labelled `effort` appears when the backend declares effort levels in - `state.available_efforts`. One button per level (e.g. `low`, - `medium`, `high`, `xhigh`, `max`); clicking POSTs `/api/effort` (same endpoint as + `state.available_efforts`. One button per level (e.g. `medium`, + `high`, `xhigh`); clicking POSTs `/api/effort` (same endpoint as the `/effort ` slash command). The active level's button is highlighted; `renderEffortChip` keeps the picker in sync with `StateSnapshot.effort` from the cold-load snapshot. @@ -219,8 +219,7 @@ SSE frame and the replayed history rows. The web UI: - terminal-themed: phosphor mauve glow, Crust bg, backdrop-filter blur, row fade-in slide-up. -Per-stream rendering (see [`docs/terminal-rendering.md`](../terminal-rendering.md) for -the full row taxonomy and dispatch logic): +Per-stream rendering: - `Stream` `tool_use` → - `Write` / `Edit`: collapsed `
` with a +/- diff body @@ -228,18 +227,13 @@ the full row taxonomy and dispatch logic): `input.new_string` or every line of `input.content`). Summary carries the path + line counts. - others (`Read /path`, `Bash $ cmd`, `mcp__hyperhive__send → - operator: "..."`, etc.): flat one-line per-tool format with - per-tool salient-arg extraction (`fmtToolUse`). + operator: "..."`, etc.): flat one-line per-tool format. - `Stream` `tool_result` short → flat `← ...`; long → collapsed `
` `▸ ← Nl · headline` (click to expand full body). -- `Stream` `thinking` → `.thinking` row with a `💭 thinking …` - indicator. -- `Stream` `system` → handled by subtype: `plugin_install` and - `compact_boundary` emit muted notes; `commands_changed` emits an - expandable details row listing slash commands; `thinking_tokens` - updates a single in-place `🧠` counter; `init`, `result`, and - `rate_limit_event` are dropped (noise / used elsewhere); other - subtypes → muted `⚙ ` note. +- `Stream` `thinking` → text content if claude provided one, + otherwise the bare `· thinking …` indicator. +- `Stream` `system init`, `result`, `rate_limit_event` are + dropped — too noisy. - `Note` → `· text`. - `TurnStart` → `◆ TURN ← ` with the wake-prompt body; a muted `· HH:MM:SS` time suffix from the event `ts`. @@ -269,7 +263,7 @@ Slash commands today: `/state/hyperhive-model` so the override survives harness restart / rebuild. - `/effort ` — `POST /api/effort` setting the claude effort - level (`low` / `medium` / `high` / `xhigh` / `max`). Takes effect on the next + level (`medium` / `high` / `xhigh`). Takes effect on the next turn. The overflow menu surfaces an effort picker that calls the same endpoint; both stay in sync via `StateSnapshot.effort`. - `/new-session` — `POST /api/new-session` (confirms first). @@ -310,7 +304,7 @@ shaped). future turns. `Bus::set_model` emits `ModelChanged`. - `POST /api/effort` (`effort=`) — switch the claude effort level for future sessions. Validated server-side against - `EFFORT_LEVELS` (`low`/`medium`/`high`/`xhigh`/`max`) — unknown values are + `EFFORT_LEVELS` (`medium`/`high`/`xhigh`) — unknown values are rejected rather than forwarded to `claude --effort`. Persists via `Bus::set_effort`, which emits `EffortChanged`. Applies on the next session start (no mid-session swap). diff --git a/frontend/packages/agent/src/app.js b/frontend/packages/agent/src/app.js index 190e3cc1..63916293 100644 --- a/frontend/packages/agent/src/app.js +++ b/frontend/packages/agent/src/app.js @@ -1626,40 +1626,6 @@ window.marked = marked; } case 'mcp__hyperhive__request_apply_commit': return short + ' ' + (input.agent || '') + ' @ ' + (input.commit_ref || '').slice(0, 12); - case 'mcp__hyperhive__request_init_config': - return short + ' ' + (input.name || '?'); - case 'mcp__hyperhive__request_update_meta_inputs': { - const ins = Array.isArray(input.inputs) && input.inputs.length - ? '[' + input.inputs.slice(0, 4).join(', ') - + (input.inputs.length > 4 ? ', …' : '') + ']' - : 'all'; - return short + ' ' + ins; - } - case 'mcp__hyperhive__list_schedules': - return short + '()'; - case 'mcp__hyperhive__cancel_schedule': - return short + ' #' + (input.id != null ? input.id : '?') - + (Array.isArray(input.targets) && input.targets.length - ? ' [' + input.targets.join(', ') + ']' : ' all'); - case 'mcp__hyperhive__fire_schedule_now': - return short + ' #' + (input.id != null ? input.id : '?'); - case 'mcp__hyperhive__edit_schedule': { - const parts = ['#' + (input.id != null ? input.id : '?')]; - if (input.body != null) parts.push('body'); - if (input.interval_seconds != null) parts.push('interval'); - if (input.next_fire_at_unix != null) parts.push('next'); - if (input.targets_add && input.targets_add.length) parts.push('+' + input.targets_add.length + ' tgt'); - if (input.targets_remove && input.targets_remove.length) parts.push('-' + input.targets_remove.length + ' tgt'); - return short + ' ' + parts.join(' · '); - } - case 'mcp__hyperhive__request_schedule_prompt': { - const tgts = Array.isArray(input.targets) ? input.targets : []; - const when = input.first_fire_at_unix != null - ? new Date(input.first_fire_at_unix * 1000).toISOString().slice(11, 16) + 'Z' - : '?'; - return short + ' → ' + (tgts.length ? tgts.join(', ') : '?') + ' at ' + when - + (input.interval_seconds != null ? ' +' + input.interval_seconds + 's' : ''); - } case 'mcp__bash__run': { // Rich renderer handles the full body; this summary covers any // fallback path and the details summary line. diff --git a/hive-ag3nt/src/events.rs b/hive-ag3nt/src/events.rs index c6fb2d46..e69a9cff 100644 --- a/hive-ag3nt/src/events.rs +++ b/hive-ag3nt/src/events.rs @@ -583,7 +583,7 @@ pub const DEFAULT_EFFORT: &str = "medium"; /// Valid claude `--effort` levels, ascending. The operator picker is /// constrained to these; [`is_valid_effort`] guards the persist path. -pub const EFFORT_LEVELS: [&str; 5] = ["low", "medium", "high", "xhigh", "max"]; +pub const EFFORT_LEVELS: [&str; 3] = ["medium", "high", "xhigh"]; /// True iff `level` is one of [`EFFORT_LEVELS`]. #[must_use] @@ -1271,7 +1271,7 @@ mod tests { } // Out-of-set, empty, and wrong-case inputs are rejected so a bad // picker POST never reaches `claude --effort`. - for bad in ["lowest", "", "MEDIUM", "ultra", "high "] { + for bad in ["low", "", "MEDIUM", "ultra", "high "] { assert!(!is_valid_effort(bad), "{bad:?} should be rejected"); } } diff --git a/hive-ag3nt/src/mcp.rs b/hive-ag3nt/src/mcp.rs index 95af42f2..fc78cf90 100644 --- a/hive-ag3nt/src/mcp.rs +++ b/hive-ag3nt/src/mcp.rs @@ -2182,14 +2182,21 @@ const EXTRA_MCP_PATH: &str = "/etc/hyperhive/extra-mcp.json"; const SEND_ALLOW_PATH: &str = "/etc/hyperhive/send-allow.json"; /// Enforce the per-agent send allow-list. Returns `Ok` when the -/// recipient is permitted (no list configured, `` sentinel -/// always allowed, or `to` is in the list); returns `Err(refusal)` -/// with a claude-readable string when blocked ��� the harness surfaces -/// the refusal as the tool result so claude knows the message didn't -/// land and can react (e.g. route via `` instead). +/// recipient is permitted (no list configured, manager always +/// allowed, or `to` is in the list); returns `Err(refusal)` with a +/// claude-readable string when blocked — the harness surfaces the +/// refusal as the tool result so claude knows the message didn't +/// land and can react (e.g. route via the manager instead). fn check_send_allowed(to: &str) -> Result<(), String> { + if to == hive_sh4re::MANAGER_AGENT { + // Always allow agents to talk to the manager — otherwise a + // misconfigured allow-list could leave a sub-agent unable + // to ask for help. + return Ok(()); + } if to == hive_sh4re::PARENT_RECIPIENT { - // Always allow `` — the allow-list constrains peer + // Always allow `` — same escape-hatch rationale as + // the manager exception. The allow-list constrains peer // chatter, not the structural reporting line; the operator // can rewire who the parent IS via `set_parent` without // having to remember to update the per-agent allow-list. diff --git a/hive-ag3nt/src/web_ui.rs b/hive-ag3nt/src/web_ui.rs index 1925ea90..694d6dba 100644 --- a/hive-ag3nt/src/web_ui.rs +++ b/hive-ag3nt/src/web_ui.rs @@ -461,7 +461,7 @@ struct StateSnapshot { /// runtime via `POST /api/effort`; applies on the next session. effort: String, /// Selectable effort levels for the picker, ascending. Fixed set - /// (`low`, `medium`, `high`, `xhigh`, `max`) — sourced from + /// (`medium`, `high`, `xhigh`) — sourced from /// [`crate::events::EFFORT_LEVELS`], not operator-configurable like /// `available_models`. The frontend renders one button per entry. available_efforts: Vec, diff --git a/hive-c0re/src/auto_update.rs b/hive-c0re/src/auto_update.rs index d17a1887..40d81b6e 100644 --- a/hive-c0re/src/auto_update.rs +++ b/hive-c0re/src/auto_update.rs @@ -1,13 +1,8 @@ -//! Startup auto-update: on `hive-c0re serve` boot, rebuild containers that -//! actually need it. Two skip rules keep boot-time work minimal: -//! -//! 1. **Stopped containers** are deferred — they will be rebuilt the first -//! time the operator starts them (see `rebuild_queue::run_start` and -//! `socket_server::handle_start`). -//! 2. **Running containers whose rev marker matches** the current hyperhive -//! flake path are skipped — nothing changed, no nix work to do. -//! -//! See `docs/coordinator.md::Auto-update sweep`. +//! Startup auto-update: on `hive-c0re serve` boot, rebuild every known +//! container unconditionally. `nixos-container update` is a no-op at the +//! nix level when nothing changed (same store path), so the cost is low +//! and avoids rev-marker staleness (all agents always need an update pass +//! when any meta commit lands). See `docs/coordinator.md::Auto-update sweep`. use std::path::{Path, PathBuf}; use std::sync::Arc; @@ -327,13 +322,11 @@ pub fn topology_sort( }); } -/// Rebuild containers that need it on startup. Skips: -/// - **Stopped containers**: deferred to on-start (`run_start` / `handle_start` -/// upgrades a plain start to rebuild+start when the rev marker is stale). -/// - **Running containers with a matching rev marker**: no nix work needed. -/// -/// Enqueues a `StartupSweep` parent entry followed by per-agent `Rebuild` -/// children linked via `parent_id`. Returns Ok even if some rebuilds failed. +/// Rebuild every container on startup. Enqueues a `StartupSweep` parent +/// entry (agent = `"hyperhive"`) followed by per-agent `Rebuild` children +/// linked via `parent_id`. The dashboard renders them nested so the operator +/// can see at a glance "boot N agents, here is each rebuild's status". +/// Returns Ok even if some rebuilds failed. pub async fn run(coord: Arc) -> Result<()> { let containers = match lifecycle::list().await { Ok(c) => c, @@ -343,7 +336,22 @@ pub async fn run(coord: Arc) -> Result<()> { } }; - let current_rev = current_flake_rev(&coord.hyperhive_flake); + // Enqueue the parent sweep entry. The worker processes it trivially + // (no-op dispatch) so it completes quickly; its purpose is to give the + // dashboard a "why" header for the per-agent child rebuilds below. + let sweep_id = coord.rebuild_queue.enqueue( + crate::rebuild_queue::QueueKind::StartupSweep, + "hyperhive".to_owned(), + crate::rebuild_queue::QueueSource::AutoUpdate, + format!("startup sweep ({} containers)", containers.len()), + None, + ); + + tracing::info!( + agents = containers.len(), + sweep_id, + "auto-update: queueing all on startup" + ); // Resolve container names to logical agent names, then sort by // topology depth so parents are always rebuilt before their @@ -355,56 +363,7 @@ pub async fn run(coord: Arc) -> Result<()> { .collect(); let topo = crate::topology::read(); topology_sort(&mut logical_names, &topo); - - // Pre-classify: decide which agents need a rebuild now vs can be skipped. - let mut to_rebuild: Vec = Vec::new(); - let mut n_deferred = 0usize; - let mut n_skipped = 0usize; - for name in &logical_names { - // Idea 2: stopped containers are deferred — rebuild happens the first - // time the operator starts them. - if !lifecycle::is_running(name).await { - n_deferred += 1; - tracing::debug!(%name, "startup sweep: stopped — deferring rebuild to on-start"); - continue; - } - // Idea 1: running containers with a matching rev marker need no rebuild. - if let Some(ref rev) = current_rev { - let stored = std::fs::read_to_string(rev_marker_path(name)).ok(); - if stored.as_deref() == Some(rev.as_str()) { - n_skipped += 1; - tracing::debug!(%name, "startup sweep: rev unchanged — skipping rebuild"); - continue; - } - } - to_rebuild.push(name.clone()); - } - - // Enqueue the parent sweep entry. The worker processes it trivially - // (no-op dispatch); its purpose is to give the dashboard a "why" header. - let sweep_id = coord.rebuild_queue.enqueue( - crate::rebuild_queue::QueueKind::StartupSweep, - "hyperhive".to_owned(), - crate::rebuild_queue::QueueSource::AutoUpdate, - format!( - "startup sweep: {} rebuild(s), {} deferred (stopped), {} skipped (up-to-date)", - to_rebuild.len(), - n_deferred, - n_skipped, - ), - None, - ); - - tracing::info!( - total = containers.len(), - rebuilds = to_rebuild.len(), - deferred = n_deferred, - skipped = n_skipped, - sweep_id, - "auto-update: startup sweep" - ); - - for name in to_rebuild { + for name in logical_names { coord.rebuild_queue.enqueue( crate::rebuild_queue::QueueKind::Rebuild, name, diff --git a/hive-c0re/src/dashboard.rs b/hive-c0re/src/dashboard.rs index d4789852..74353bc8 100644 --- a/hive-c0re/src/dashboard.rs +++ b/hive-c0re/src/dashboard.rs @@ -1681,7 +1681,7 @@ async fn post_request_spawn( hive_sh4re::ApprovalKind::Spawn, "", None, - "operator", + hive_sh4re::MANAGER_AGENT, ) { Ok(id) => { tracing::info!(%id, %name, "operator: spawn approval queued via dashboard"); diff --git a/hive-c0re/src/loose_ends.rs b/hive-c0re/src/loose_ends.rs index bdecc232..46f7e597 100644 --- a/hive-c0re/src/loose_ends.rs +++ b/hive-c0re/src/loose_ends.rs @@ -16,7 +16,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use anyhow::Result; -use hive_sh4re::LooseEnd; +use hive_sh4re::{LooseEnd, MANAGER_AGENT}; use crate::coordinator::Coordinator; @@ -56,12 +56,12 @@ pub fn for_agent(coord: &Coordinator, agent: &str) -> Result> { } // Show each pending approval to the agent that submitted it. The // submitter column is NULL for rows predating it; those count as - // operator-initiated (no agent tracking predates the column). + // the root agent's. for a in coord.approvals.pending()? { let submitter = coord .approvals .submitter_of(a.id)? - .unwrap_or_else(|| "operator".to_owned()); + .unwrap_or_else(|| MANAGER_AGENT.to_owned()); if submitter != agent { continue; } diff --git a/hive-c0re/src/questions.rs b/hive-c0re/src/questions.rs index ca388839..2d99dfd8 100644 --- a/hive-c0re/src/questions.rs +++ b/hive-c0re/src/questions.rs @@ -203,14 +203,13 @@ pub fn handle_cancel_loose_end( // may withdraw it. Without the ownership check, any // approvals-group agent could cancel any other's approval by // id. A NULL submitter (legacy row predating the column) is - // treated as operator-initiated (no agent tracking predates - // the column). + // owned by the root agent. check_can_cancel_approval(canceller)?; let submitter = coord .approvals .submitter_of(id) .map_err(|e| format!("{e:#}"))? - .unwrap_or_else(|| "operator".to_owned()); + .unwrap_or_else(|| hive_sh4re::MANAGER_AGENT.to_owned()); if submitter != canceller { return Err(format!( "cancel_loose_end: approval {id} was submitted by {submitter}, \ diff --git a/hive-c0re/src/rebuild_queue.rs b/hive-c0re/src/rebuild_queue.rs index b6853698..8482a838 100644 --- a/hive-c0re/src/rebuild_queue.rs +++ b/hive-c0re/src/rebuild_queue.rs @@ -1034,33 +1034,11 @@ async fn rebuild_for_entry( /// Uses the cold-start fallback (stop + kill + start retry) so the /// deferred start-after-rebuild keeps the same activation-error recovery /// it had when it ran inline on the build lane. -/// -/// If the hyperhive flake rev has changed since the container was last built -/// (i.e. the rev marker is stale or missing), the start is upgraded to a full -/// rebuild so the container runs current nix derivations. This is the -/// "deferred stopped container" path from `auto_update::run`. async fn run_start( coord: &std::sync::Arc, entry: &QueueEntry, ) -> anyhow::Result<()> { let name = &entry.agent; - // Upgrade to rebuild+start if the rev marker is stale. - let current_rev = crate::auto_update::current_flake_rev(&coord.hyperhive_flake); - if let Some(ref rev) = current_rev { - let stored = std::fs::read_to_string(crate::auto_update::rev_marker_path(name)).ok(); - if stored.as_deref() != Some(rev.as_str()) { - tracing::info!(%name, "start: rev stale — upgrading to rebuild+start"); - return crate::auto_update::rebuild_agent( - coord, - name, - rev, - Some(entry.id), - true, - Some(entry.source), - ) - .await; - } - } let _guard = coord.transient_guard(name, crate::coordinator::TransientKind::Starting); coord.set_queue_step(Some(entry.id), "nixos-container start"); crate::lifecycle::start_with_fallback(name).await?; diff --git a/hive-c0re/src/server.rs b/hive-c0re/src/server.rs index a711bf6b..a2c63a82 100644 --- a/hive-c0re/src/server.rs +++ b/hive-c0re/src/server.rs @@ -85,7 +85,7 @@ async fn dispatch(req: &HostRequest, coord: Arc) -> HostResponse { hive_sh4re::ApprovalKind::Spawn, "", None, - "operator", + hive_sh4re::MANAGER_AGENT, )?; tracing::info!(%id, %name, "spawn approval queued"); HostResponse::success() diff --git a/hive-c0re/src/socket_server.rs b/hive-c0re/src/socket_server.rs index a839f86c..d44b69d5 100644 --- a/hive-c0re/src/socket_server.rs +++ b/hive-c0re/src/socket_server.rs @@ -803,25 +803,6 @@ async fn handle_start(coord: &Arc, agent: &str, name: &str) -> Agen return err; } tracing::info!(%agent, %name, "start container"); - // If the hyperhive rev is stale, route through the rebuild queue so the - // container runs current nix derivations before it starts. Same logic as - // `run_start`; this covers the MCP `start` tool path. - let current_rev = crate::auto_update::current_flake_rev(&coord.hyperhive_flake); - if let Some(ref rev) = current_rev { - let stored = std::fs::read_to_string(crate::auto_update::rev_marker_path(name)).ok(); - if stored.as_deref() != Some(rev.as_str()) { - tracing::info!(%agent, %name, "start: rev stale — enqueuing rebuild"); - coord.rebuild_queue.enqueue( - crate::rebuild_queue::QueueKind::Rebuild, - name.to_owned(), - crate::rebuild_queue::QueueSource::Manual, - format!("start {name}: rev stale — rebuilding first"), - None, - ); - coord.emit_rebuild_queue_snapshot(); - return AgentResponse::Ok; - } - } match crate::lifecycle::start(name).await { Ok(()) => { coord.kick_agent(name, "container started"); @@ -1847,8 +1828,8 @@ pub(crate) fn submit_init_config( 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"), + // back to. No declared parent = operator/root path. + parent.unwrap_or(hive_sh4re::MANAGER_AGENT), ) .map_err(|e| anyhow::anyhow!("queue approval row: {e:#}"))?; tracing::info!(%id, %name, "init_config approval queued"); diff --git a/nix/docs/default.nix b/nix/docs/default.nix index e03df075..62e618d8 100644 --- a/nix/docs/default.nix +++ b/nix/docs/default.nix @@ -11,50 +11,17 @@ # prose `/docs/` tree. Full pipeline + subtree-pick / output-tree # rationale: docs/gotchas.md::Nix options reference. let - # Content-addressed narrow source covering only the nix/ directory. - # `builtins.unsafeDiscardStringContext` strips `self`'s store-path - # context so `builtins.path` hashes only the nix/ file content, not - # the full flake source (Rust, frontend, markdown, …). Result: the - # docs drvs only change when a .nix file changes, not on every - # commit — the muede-pc2 remote builder can reuse its cached result - # for any commit that doesn't touch nix/. - nixSrc = builtins.path { - path = builtins.unsafeDiscardStringContext (toString self + "/nix"); - name = "hyperhive-nix-src"; - }; - - # Stub overlay that satisfies pkgs.hyperhive-* references in module - # option defaults without depending on self's Rust / frontend builds. - # nixosOptionsDoc renders `defaultText` for these options anyway; the - # stubs just prevent attribute-missing eval errors. - docsStubOverlay = _final: _prev: { - hyperhive = pkgs.emptyFile; - hyperhive-frontend = pkgs.emptyDirectory; - hyperhive-assets = pkgs.emptyDirectory; - hyperhive-docs = pkgs.emptyDirectory; - }; - # Stub host system: every hyperhive subsystem `mkForce false` so # heavy build inputs (matrix container, forge, etc.) stay out of # the eval — only option *declarations* matter for the doc walk. - # Import hive-c0re.nix from the content-addressed nixSrc with stub - # package args so the eval doesn't depend on self's Rust builds. hostEval = nixosSystem { system = pkgs.stdenv.hostPlatform.system; modules = [ - (import "${nixSrc}/modules/hive-c0re.nix" { - hyperhivePackage = _system: pkgs.emptyFile; - hyperhiveFrontend = _system: pkgs.emptyFile; - hyperhiveAssets = _system: pkgs.emptyDirectory; - hyperhiveFlake = ""; - hyperhiveDocs = ""; - agentBaseToplevel = pkgs.emptyFile; - managerToplevel = pkgs.emptyFile; - }) + self.nixosModules.default ( { lib, ... }: { - nixpkgs.overlays = [ docsStubOverlay ]; + nixpkgs.overlays = [ self.overlays.default ]; fileSystems."/" = { device = "/dev/null"; fsType = "tmpfs"; @@ -68,23 +35,14 @@ let ]; }; - # Agent module eval from the content-addressed nixSrc. Relative - # imports inside agent-base.nix (e.g. ./harness-base.nix) resolve - # correctly against the nixSrc directory tree. - agentEval = nixosSystem { - system = pkgs.stdenv.hostPlatform.system; - modules = [ - "${nixSrc}/templates/agent-base.nix" - { nixpkgs.overlays = [ docsStubOverlay ]; } - ]; - }; + # Reuse the already-evaluated agent-base config — its options tree is + # identical to what a real agent container sees, no second eval needed. + agentEval = self.nixosConfigurations.agent-base; # Rewrite option declaration paths from nix-store absolute paths to # forge URLs so rendered docs link back to source. - # nixSrc is a content-addressed copy of nix/; strip its store prefix - # and prepend nix/ to recover the repo-relative path. forgeRoot = "https://forge.darkest.space/hyperhive/hyperhive/src/branch/main"; - nixSrcPrefix = builtins.unsafeDiscardStringContext (toString nixSrc + "/"); + storePrefix = toString self + "/"; transformOptions = opt: opt @@ -94,8 +52,8 @@ let let declStr = toString decl; relPath = - if lib.hasPrefix nixSrcPrefix declStr then - "nix/" + lib.removePrefix nixSrcPrefix declStr + if lib.hasPrefix storePrefix declStr then + lib.removePrefix storePrefix declStr else baseNameOf declStr; in @@ -178,7 +136,7 @@ let `hyperhive.forge.*`, `hyperhive.matrix.*`, `hyperhive.gui.*`). Regenerate with `nix build .#docs` (bundle), `.#docs-host`, or - `.#docs-agent`. Consumed by the website repo to render `/options/`. + `.#docs-agent`. ''; hostMD = mkMarkdownPage "docs-host" "hyperhive — host options" hostDoc; diff --git a/nix/modules/hive-c0re.nix b/nix/modules/hive-c0re.nix index 451a5a46..ea719720 100644 --- a/nix/modules/hive-c0re.nix +++ b/nix/modules/hive-c0re.nix @@ -795,9 +795,10 @@ in managerToplevel ]; - # Unprivileged coordinator user. hive-c0re runs as this user; - # privileged operations are delegated to hive-priv which runs as - # root, socket-activated at /run/hive/priv.sock. + # Unprivileged coordinator user. hive-c0re runs as this user + # (privsep phase 2); privileged operations are delegated to + # hive-priv which runs as root, socket-activated at + # /run/hive/priv.sock. users.users.hive-core = { isSystemUser = true; group = "hive-core"; diff --git a/nix/modules/hive-gateway.nix b/nix/modules/hive-gateway.nix index c494b1ae..dd0310af 100644 --- a/nix/modules/hive-gateway.nix +++ b/nix/modules/hive-gateway.nix @@ -423,7 +423,7 @@ in # # /run/hive-agent — per-agent UDS socket dir, written by c0re's # set_nspawn_flags when agents start. Owned by `hive-core` (the - # unprivileged coordinator user): c0re does the + # unprivileged coordinator user, privsep phase 2): c0re does the # `create_dir_all(/run/hive-agent/)` itself, so a root-owned # parent would EACCES on the very first agent create on a fresh host # (hive-priv only chowns the subdir afterwards, it doesn't make it). @@ -949,6 +949,7 @@ in # container. Listens on the bridge interface from # `services.hyperhive.network`; authoritative for the hive # domain + sub-domains, forwards everything else upstream. + # No-op when `network.enable = false`. services.dnsmasq = lib.mkIf networkCfg.enable { enable = true; # Don't substitute the container's /etc/resolv.conf — diff --git a/nix/modules/hive-network.nix b/nix/modules/hive-network.nix index 0dcca3d8..8c66f8e8 100644 --- a/nix/modules/hive-network.nix +++ b/nix/modules/hive-network.nix @@ -8,8 +8,9 @@ let in { # Hive-internal network — host-side bridge + per-agent DNS resolver. - # Always active when hyperhive is enabled: agent containers run in - # private netns behind the bridge. Full design: docs/network.md. + # Containers stay on shared host netns at v1; this module stands the + # bridge + resolver up so the endpoint is in place before network + # isolation flips containers to private netns. Full design: docs/network.md. options.services.hyperhive.network = { enable = lib.mkOption { diff --git a/nix/templates/harness-base.nix b/nix/templates/harness-base.nix index 1610fa6f..0629116b 100644 --- a/nix/templates/harness-base.nix +++ b/nix/templates/harness-base.nix @@ -219,11 +219,9 @@ in options.hyperhive.effortLevel = lib.mkOption { type = lib.types.enum [ - "low" "medium" "high" "xhigh" - "max" ]; default = "medium"; example = "high"; @@ -233,12 +231,12 @@ in effort as operator-override-file → this env → built-in `"medium"`, and passes the result to `claude --effort` at turn launch. - Ascending scale: `"low"` (minimal thinking budget), `"medium"` - (default — balanced), `"high"` (platform default), `"xhigh"` - (recommended for autonomous coding on capable models), `"max"` - (maximum thinking budget, highest cost). The operator can override - at runtime per-agent via the web UI (applied on the next session); - any rebuild that changes this option resets that override. + `"medium"` (the default) keeps token spend low and works well with + the harness across families. `"high"` matches the current platform + default; `"xhigh"` is recommended for coding / high-autonomy work on + capable models (Opus 4.8+) at higher token cost. The operator can + override at runtime per-agent via the web UI (applied on the next + session); any rebuild that changes this option resets that override. ''; }; diff --git a/scripts/pre-push b/scripts/pre-push deleted file mode 100755 index 3b6ea478..00000000 --- a/scripts/pre-push +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/sh -# Git pre-push hook: runs the tracker-tag lint and the comment-block -# lint against the working tree before any push lands on the remote. -# Catches issues that would fail CI and require a follow-up commit -# (common failure mode: a nix comment containing a hash-issue-number -# tag added mid-session). -# -# Install (once per clone): -# ln -sf ../../scripts/pre-push .git/hooks/pre-push -# -# The hook runs against the full working tree (not just staged or -# pushed files) to match what CI sees: `nix flake check` builds from -# the committed tree, so an untracked hit on a staged file would still -# trip CI. -set -eu - -repo_root="$(git rev-parse --show-toplevel)" - -echo "pre-push: running tracker-tag lint..." >&2 -if ! sh "$repo_root/scripts/check-issue-refs.sh"; then - echo "pre-push: tracker-tag lint FAILED — fix before pushing" >&2 - exit 1 -fi - -echo "pre-push: running comment-block lint..." >&2 -if ! sh "$repo_root/scripts/check-comment-blocks.sh"; then - echo "pre-push: comment-block lint FAILED — fix before pushing" >&2 - exit 1 -fi - -echo "pre-push: lints passed ✓" >&2 -exit 0