Compare commits

..
30 changed files with 200 additions and 541 deletions

View file

@ -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`

View file

@ -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:<port>`, 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:<core-port>`, 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:<core-port>`, 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/<name>/`.
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

View file

@ -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

View file

@ -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

View file

@ -323,10 +323,11 @@ connects to the compositor at `127.0.0.1:<vnc_port>`.
## 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 `<h2>` 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 '<hash>': 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://<forge>/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.

View file

@ -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 <bridge-ip>` |
| `address` rules target | `<bridge-ip>` (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 `<hive-domain>` 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.<bridge>.allowedUDPPorts = [ 53 ]`
`networking.firewall.interfaces.<bridge>.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.<bridge>.allowedTCPPorts = [ 80 443 ]` — lets isolated agents (private netns, veth on bridge) reach nginx on the host |
| Gateway access | `networking.firewall.interfaces.<bridge>.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.<domain>` — 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=<deterministic-ip>`,
`HOST_ADDRESS=<bridge-ip>`, and `HOST_BRIDGE=<bridgeName>` 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 <gateway-ip>` 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:<port>` 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/<name>/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

View file

@ -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 <N>s` / `max <N>` when set; `Bash`
flags `[bg]`; `remind` shows `+Xm "preview"`; matrix
tools show `→ <room>/<user>: "body"` or `<room> [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 <path>` |
| `Write` | rich diff row `Write <path> · +N` |
| `Edit` | rich diff row `Edit <path> · -N +N` |
| `Glob` | `Glob <pattern>` |
| `Grep` | `Grep <pattern>` |
| `Bash` | `Bash [bg] $ <cmd>` (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

View file

@ -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/<id>.{out,err}`.
When the task completes (or times out, or the process errors), the
harness fires a wake with `from: "bash-task-<id>"`; 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-<id>"` 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).

View file

@ -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/<name>`)** — 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 <name>`** — 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

View file

@ -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

View file

@ -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

View file

@ -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?"** →

View file

@ -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 <level>` 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 `<details>` 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
`<details>` `▸ ← 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 `⚙ <subtype>` 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 ← <from>` 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 <level>``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=<level>`) — 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).

View file

@ -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.

View file

@ -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");
}
}

View file

@ -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, `<parent>` sentinel
/// always allowed, or `to` is in the list); returns `Err(refusal)`
/// with a claude-readable string when blocked <20><><EFBFBD> the harness surfaces
/// the refusal as the tool result so claude knows the message didn't
/// land and can react (e.g. route via `<parent>` 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 `<parent>` — the allow-list constrains peer
// Always allow `<parent>` — 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.

View file

@ -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<String>,

View file

@ -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<Coordinator>) -> Result<()> {
let containers = match lifecycle::list().await {
Ok(c) => c,
@ -343,7 +336,22 @@ pub async fn run(coord: Arc<Coordinator>) -> 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<Coordinator>) -> 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<String> = 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,

View file

@ -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");

View file

@ -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<Vec<LooseEnd>> {
}
// 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;
}

View file

@ -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}, \

View file

@ -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<crate::coordinator::Coordinator>,
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?;

View file

@ -85,7 +85,7 @@ async fn dispatch(req: &HostRequest, coord: Arc<Coordinator>) -> HostResponse {
hive_sh4re::ApprovalKind::Spawn,
"",
None,
"operator",
hive_sh4re::MANAGER_AGENT,
)?;
tracing::info!(%id, %name, "spawn approval queued");
HostResponse::success()

View file

@ -803,25 +803,6 @@ async fn handle_start(coord: &Arc<Coordinator>, 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");

View file

@ -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;

View file

@ -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";

View file

@ -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/<name>)` 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 —

View file

@ -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 {

View file

@ -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.
'';
};

View file

@ -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