argus on PR #832:
- 🟡 spawn_poll was inserted BETWEEN write's closing doc and the
pub fn write line; rust treated the consecutive /// as one block,
so spawn_poll inherited write's tail and write ended up with no
closing doc. moved spawn_poll AFTER write to fix attribution.
- 🟡 spawn_poll(coord) took Arc<Coordinator> just to drop it
immediately. dropped the param; main.rs call site now just
agent_sockets::spawn_poll().
no functional change. 10 tests still pass.
closes the gate atlas raised on PR #813: without per-agent opt-in
signal, agent-sockets.json listed every sub-agent, and any agent
that hadn't flipped hyperhive.web.useUnixSocket would 502 the
gateway (its harness still binds TCP, no socket at the published
path).
harness side (web_ui::bind_unix):
- after successful bind + chmod, drop a `.bound` marker in the
per-agent dir as a stable 'this agent has a unix socket here'
signal. best-effort: a failed marker write logs at WARN but
doesn't abort serve (the socket still binds fine; gateway just
keeps using TCP for one more poll).
c0re side (agent_sockets):
- new READY_MARKER const + ready_marker_for(name) helper
- build_map filters by ready_marker_for(name).exists() — only agents
whose harness has bound the socket appear in the JSON map
- new build_map_with<F> internal extracts the predicate so tests
pass a controlled is_ready closure (no real fs access)
- new spawn_poll() background task: re-fires agent_sockets::write
every 10s so the JSON catches up to fresh markers without
needing a container-start hook. write() idempotency means
steady-state cost is one stat per agent per tick.
10 tests: 6 prior + new build_map_filters_by_ready_predicate +
ready_marker_path_is_sibling_of_socket. existing tests adjusted to
call build_map_with(_, |_| true) since the default path now hits
the fs.
once this lands + #822 lands, atlas's gateway-side step 3 can drop
its eval-time `pathExists` fallback — c0re only publishes opted-in
agents, so the gateway can trust the JSON unconditionally.
mara: 'those comments seem very redundant'. true — the 16 pure-GET
verbs all got the same 'transport error + stdout I/O' boilerplate,
which just restates the Result<()> contract that's trivially
derivable from the type.
dropped # Errors from: assign, branches, close, comment_show,
comments, diff, issue, labels, lint, list, milestone, pr,
pr_reviews, subscription, timeline, tree_sha, view (17 files).
kept on the 7 verbs that have a non-Forgejo failure surface worth
documenting:
- comment, comment_edit, issue_create, issue_edit — body input I/O
via --body-file / stdin
- pr_create — body input + --push shellout to git
- attach::run_issue, attach::run_comment — explicit bail! on
missing file
net: 23 verbs touched in the original PR → 17 trimmed back to
no-doc, 6 kept (with the 7th call being attach::run_comment in the
same file). 38 tests still pass.
systemic gap argus flagged on PR #798 (timeline verb). every `pub fn
run` in hive-forge/src/verbs/*.rs lacked a `# Errors` block —
violates Rust API guidelines + obscures the failure surface for
operators reading the source.
uniform doc per verb category:
- pure GET + print verbs: "transport error from the Forgejo REST call
+ I/O error from stdout"
- body-from-file verbs (comment/comment_edit/issue_create/issue_edit/
pr_create): adds 'I/O error from --body-file/stdin input'
- file-upload verbs (attach-issue, attach-comment): adds 'file
read/exist check'
- pr_create: also mentions the --push shellout
23 `pub fn run` signatures touched. no behaviour change; pure
documentation sweep. cargo test green (38 tests).
Two new sections housing rationale that was inline in
`harness-base.nix`:
- `docs/conventions.md::Best-effort oneshot services` — shape
contract shared by `tea-login`, `forge-avatar-sync`, and
`matrix-avatar-sync`: always exit 0, no `set -e`, skip silently
on missing prerequisites, wired to multi-user.target,
re-runnable. Plus the artefact-under-agent-home + service-stays-
root posture and the operator-visible-via-journalctl trade-off.
- `docs/persistence.md::matrix-avatar-sync` — the two-step `media
upload` → `set avatar_url` matrix-spec dance + why
`RemainAfterExit = false` (so the `.path` watcher's re-fire on
token appearance actually re-executes the unit).
In-code comments trim to one-liner purpose + pointers; the script
bodies stay (they're the actual implementation) but their inline
`#` rationale comments collapse where the docs cover them.
`description = ''…''` blocks (operator-facing options docs)
preserved per iris #718.
`nix flake check` clean; `nix fmt` clean.
Defensive against the fresh-boot-before-any-agent-spawn window where
the bind-mount source wouldn't exist from c0re's per-agent
`set_nspawn_flags` mkdir chain yet. nspawn auto-creates missing
sources, so this is intent-explicit rather than functional fix.
Switch per-agent gateway upstreams from TCP loopback to unix-domain
socket when the agent has opted in via `hyperhive.web.useUnixSocket`
(#822). Coexists with the TCP path during rollout.
Changes:
- New `agentSocketsFile` option (default
`/var/lib/hyperhive/agent-sockets.json`) — c0re writes the map
there via `hive_c0re::agent_sockets::write` (#809).
- `agentSocketsTable = lib.importJSON ...` (graceful empty when
file missing).
- `agentUpstreamFor name port` picks `http://unix:<path>:/` when the
socket has a JSON entry AND the file exists at eval time; else
`http://127.0.0.1:<port>/`. Path-exists gate guards against
c0re's blanket-emit shape during the canary window (agents in
`agent-sockets.json` who haven't actually flipped have no
bound socket on disk → fall back to TCP). Damocles will ship a
`.bound` marker filter on the c0re side (#784 step 2d
follow-up); once that's in, the path-exists check is redundant
but harmless. Step 4 drops it entirely along with the TCP
fallback.
- `containers.hive-gateway.bindMounts."/run/hive-agent"` —
read-only, unconditional. Inert when no agents have opted in.
Required so nginx inside the gateway container can `connect(2)`
to the per-agent sockets damocles's #813 bind-mounts into agent
containers at the same paths.
Docs:
- `docs/gateway.md::Per-agent UDS upstream (#784)` — full rollout
flow, subdir-bind rationale (damocles #813), eval-time gate
explainer, step 4 drop plan.
`nix flake check` clean; `nix fmt` clean.
Canary plan: once #822 (`useUnixSocket` option) lands + this PR
merges, manager flips atlas's agent.nix to `useUnixSocket = true`
via the config-update flow. End-to-end validation against atlas
before broader rollout.
closes#815. last harness-side piece of the #784 phase 2 rollout.
new option `hyperhive.web.useUnixSocket` (default false). When true,
the harness service env gets HIVE_WEB_SOCKET set to the canonical
/run/hive-agent/${userName}/web.sock path — making web_ui::serve
bind a UnixListener (PR #800) instead of TCP. Path matches
hive_c0re::agent_sockets::socket_path_for(name) so the c0re
bind-mount (#813) and the gateway's eventual upstream config all
derive from the same canonical shape (no triangulation drift).
Default false so an agent's web UI keeps binding TCP until the
per-agent flip is explicit. Rollout shape:
1. flip one canary agent (atlas volunteered) to true via agent.nix
2. validate atlas's gateway-side step 3 against that canary
3. flip remaining agents per-agent as the gateway side soaks
4. eventually drop this option once everyone's on unix (step 4 of #784)
Sub-agent-only by design — manager UI serves at / via the c0re
dashboard upstream, never via /agent/<name>/, so the per-agent
unix path is irrelevant for it (the env var is set unconditionally
for code simplicity; the manager's bind socket would just sit
unused).
Move the harness systemd unit rationale (role-driven unit name,
manager-only forge defaults, PATH wrapper-dir trick, env vars,
standalone-eval fallbacks, RuntimeDirectory + User= reasoning)
from `nix/templates/harness-base.nix` to a new
`docs/agent-hierarchy.md::Harness systemd unit shape (per-role)`
section.
In-code comments trim to short purpose statements + pointers; the
PATH /bin auto-append behaviour is already documented in
docs/gotchas.md, so the harness file just cross-refs both.
`description = ''…''` blocks (operator-facing options docs)
preserved per iris #718.
`nix flake check` clean; `nix fmt` clean (after formatter pass).
builds on step 2a (#809). lifecycle::set_nspawn_flags now adds a
--bind={socket_dir}:{socket_dir} flag per sub-agent so the harness's
HIVE_WEB_SOCKET bind (PR #800) lives in a dir both the agent
container and the host can see.
design (matches #809's a1a601d explanation):
- bind the SUBDIR, not the socket file. file bind-mounts drop on
unlink; the harness's bind_unix unlinks any stale socket before
binding, so a file bind would land the new socket in the agent's
private namespace, invisible to the gateway. dir bind keeps both
sides on the same dir inode.
- per-agent dir (one /run/hive-agent/<name>/ per agent, not a shared
/run/hive-agent/ mount). The agent's container only sees its own
subdir — never siblings' (mara on #800).
- manager skipped — the manager's UI serves at / via the c0re
dashboard upstream, not via /agent/<name>/, so it never needs the
per-agent socket dir.
mkdir source defensively before bind: nspawn refuses to start when
the bind source is missing, and /run/hive-agent/ doesn't exist on
fresh hosts.
remaining work in this phase:
- step 3 (atlas): gateway proxy_pass http://unix:/run/hive-agent/<name>/web.sock:/
- per-agent: flip HIVE_WEB_SOCKET in agent.nix to opt in (separate PRs)
- step 4 (later): drop TCP fallback once everyone's flipped
was: /run/hive-agent/<name>.sock (flat single-file bind-mount).
issue: file bind-mounts don't survive the harness's 'unlink stale
socket then bind(2) a new one' cycle. The unlink drops the bind
inside the container; the rebind happens in private container
namespace; host never sees the new inode → gateway can't connect.
now: /run/hive-agent/<name>/web.sock (per-agent SUBDIR + fixed
filename). Lifecycle bind-mounts the parent dir per agent (step 2b)
so both sides see the same dir inode; the socket appears on the
host the moment the harness binds it.
new helpers:
- AGENT_SOCKET_DIR const (parent, gateway binds this whole tree)
- SOCKET_FILENAME const ("web.sock")
- agent_dir_for(name) (per-agent subdir, lifecycle bind-mounts this)
- socket_path_for(name) (= agent_dir_for(name).join(SOCKET_FILENAME))
per-agent dir isolation also satisfies mara on #800 directly:
agent's container only sees its own subdir + socket, never siblings'.
8 tests now (added agent_dir_for_is_socket_parent invariant).
sibling to agent_ports.rs (#748 / #15). Writes
/var/lib/hyperhive/agent-sockets.json mapping agent name → unix
socket path under /run/hive-agent/<name>.sock — the source of truth
for the gateway's eventual unix-socket upstreams once it flips off
TCP per #784's transition plan.
shape mirrors agent_ports.rs:
- BTreeMap for deterministic JSON output
- atomic <path>.tmp + rename for partial-write safety
- idempotent (skip rewrite when content unchanged)
- MANAGER_NAME filter (gateway routes manager via dashboard upstream,
not /agent/<n>/)
socket_path_for(name) is pure-deterministic so a name alone resolves
to the same path the c0re writes + the harness binds via
HIVE_WEB_SOCKET (post-PR #800).
hooked into meta::sync_agents alongside the existing
agent_ports::write call (same best-effort + non-fatal shape).
next: step 2b (lifecycle bind-mount /run/hive-agent/ into agent +
gateway containers, with per-agent isolation per mara's #800 nudge
'agents can only access their own sockets'). then atlas's step 3
(gateway proxy_pass http://unix:...). then step 4 drops TCP.
Mara reported the rebuild spinner rendered as a rotating L-corner
('_|' / '⌐') instead of a smooth orbiting arc. Two issues:
1. The previous version coloured TWO adjacent border sides amber
(border-top + border-right). With the rest of the border
transparent, this paints a sharp L-shape at the icon corner —
which when rotated reads as a spinning border-corner, not a
loading spinner.
2. @keyframes had only `to` defined. Safer to include explicit
`from { transform: rotate(0deg) }` so any browser that doesn't
default cleanly still picks up the rotation.
Fix is the classic CSS spinner shape: faint amber ring around the
full icon perimeter (`border: 2px solid rgba(250, 179, 135, 0.2)`)
with one brighter top arc (`border-top-color: var(--amber)`) that
rotates. Reads unambiguously as a loading indicator.
Closes#804.
Final pass on tabs.js. All 6 remaining cookies were either
attribution refs to closed issues / past reviews or pure-history
mile-markers:
- #259 disabled meta-update state → drop attribution
- dashboard.rs#2170 backend handler line ref → drop the line
number (handler still lives in dashboard.rs, line drifts)
- mara on #695 M0V3 picker any-selection → drop attribution
(substance already captured in docs/web-ui.md::Selection bar)
- #541 journald scroll surface → drop the migration history
framing, keep present-state behaviour
- argus on #471 textContent vs innerHTML review → drop the review
attribution, keep the safer-pattern rationale inline
- #369 tab routing section header → drop the cookie, keep the
section divider
tabs.js: 6 → 0 issue-ref cookies (100% reduction). Across
batches 1-14 (#712): 73 → 0 issue-ref cookies, with substantive
prose (Topology tree CSS geometry, Container row icon layout,
Pending-state derivation, Selection bar, R3BU1LD QU3U3 cancel +
step annotations, Approval card requested-at, SSE multiplexing
worker-death self-heal, FL0W page MESS4GE FL0W body layout)
migrated to docs/web-ui.md across the batches.
dashboard SPA files all at 0 cookies now (PR #795 covers the
other dashboard files; this PR covers the tabs.js residual).
phase 1 of #784 (unix-domain agent web UI for #14 prerequisite).
backwards-compatible: when HIVE_WEB_SOCKET is set + non-empty, bind a
UnixListener at that path instead of the legacy TCP bind on HIVE_PORT.
empty env var treated as unset so a stray HIVE_WEB_SOCKET= doesn't
trap an un-bindable empty path.
bind_unix helper:
- mkdir -p the socket parent (covers first-boot fresh /run/hive-agent/
bind-mount target)
- best-effort unlink of stale socket (clean exit removes it, crash
leaves it; bind(2) refuses to overwrite)
- mode 0o660 so gateway peers in the same unix group can connect (the
bind-mount source dir ACL is the real gate; perms are defence in
depth)
axum 0.8's Listener trait covers tokio::net::UnixListener directly
(no extra feature/dep required).
next phases (separate PRs):
- step 2: c0re bind-mounts /run/hive-agent/ + writes agent-sockets.json
alongside agent-ports.json
- step 3: gateway proxy_pass http://unix:… (atlas)
- step 4: drop TCP bind once gateway no longer needs it
was: `print_event` did the format + println inline; tests had a
parallel `captured` helper that re-implemented the dispatch and only
covered 4 of 14 event types. brittle — a new arm in `print_event`
silently went uncovered.
now: pure `format_event(ev: &Value) -> String` builds the line;
`print_event` is a thin wrapper that adds the trailing newline.
tests assert on `format_event` output directly so every new arm
gets test coverage by construction.
bonus: added 3 more test cases (assignees add/remove, pull_push
commit count + force-push, commit_ref sha truncation) since the
helper extraction made them cheap. 9 tests total, all green.
last unstarted piece of the original #694 epic. agents kept falling
back to curl for 'who closed this?' / 'when was this labelled?'
archaeology because view + comments only surface the body + comments,
not the structured timeline events (label adds, assignments, closes,
reopens, pushes, commit refs, review submissions, milestone changes).
separate verb rather than view --timeline because:
- composes naturally with view <n> / comments <n>
- keeps existing verb output shapes stable (no script breakage)
- argus on #770 already noted view's output is busy
human-readable by default ('**actor @ ts**: <summary>'), --json for
raw piping. unknown event types fall through to a '[<type>]'
placeholder so a forge schema bump doesn't panic the verb.
--tail N is a follow-up: timeline endpoint doesn't expose a total-count
field so the count-then-page strategy from #770 doesn't apply
directly.
6 tests cover comment / label add/remove / close / unknown-type
placeholder / missing-user fallback.
read the issue's `comments` count first, compute which page contains the
tail, fetch only `ceil(n/50) + 1` pages. drops the TAIL_MAX_PAGES cap
entirely — it was paging from the WRONG end (first 1000 comments instead
of the last n) on long threads, defeating the whole purpose of --tail.
work is now bounded by n, not by thread length.
mara on #755: "e.g. /agent/name should show an error page stating
that the agent could not be found if missing in json or that it is
not reachable if we get a connection error. we dont want a fully
generic fallback, only for routes already special cased in the
nginx config."
Adds two static HTML pages built at deploy time via
`pkgs.runCommand "hyperhive-agent-error-pages"`:
- **not-found.html** — served when `/agent/<unknown>/...` hits the
bare `/agent/` catch-all. The catch-all `return 404`s, and
`error_page 404 = /__hive_agent_not_found` rewrites to the static
page.
- **unreachable.html** — served when `/agent/<known>/...` proxy_pass
to the harness returns 502 / 503 / 504. `proxy_intercept_errors
on` + `error_page 502 503 504 = /__hive_agent_unreachable` on each
per-agent location block rewrites to the static page.
Mechanics:
- `agentErrorPagesDir` (in the `let` block) is a `runCommand` that
emits two HTML files using a `<<EOF` heredoc — no template engine
needed.
- Two `internal` nginx locations (`= /__hive_agent_not_found`,
`= /__hive_agent_unreachable`) `alias` the exact files. `internal`
keeps the URIs unreachable from direct operator request — only
nginx's own error-handling can hit them.
- Per-agent location blocks pick up the `error_page` directive
through the existing `lib.mapAttrs'` over `agentPortsTable`. No
per-agent generated content; same static page for all.
- `/agent/` catch-all generates from a tiny optionalAttrs alongside
the per-agent block — both are no-op when the agent table is
empty (matches the pre-#15 shape).
Pages: minimal inline CSS, catppuccin palette matching the
dashboard (`#1e1e2e` bg, `#cdd6f4` text, `#cba6f7` not-found heading,
`#f9e2af` unreachable heading). No frontend-dist dependency — render
even when hive-c0re is down. Both link back to `/`.
Per mara's "only for routes already special cased" — scope stays
narrow. Forge / matrix / fluffychat keep nginx defaults; extending
the custom-error pattern to other vhosts is a separate follow-up
if/when needed.
Verified:
- nginx location attrset has `["/", "/agent/", "= /__hive_agent_not_found", "= /__hive_agent_unreachable"]`
- container toplevel builds clean (`nixos-system-hive-gateway-26.05pre-git`)
- `docs/gateway.md::Per-agent error pages` section captures the
design + rationale + intentional narrowness
Closes#755.
closes#788. drops the per-role HIVE_LABEL fallback const ('hive-ag3nt'
on AgentSurface, 'hm1nd' on ManagerSurface) and replaces both with a
single literal 'hive' at the env-var unwrap site. real deploys set
HIVE_LABEL unconditionally via the meta-flake envelope; the fallback
is unreachable in production and there's no semantic reason for it to
differ per role.
nix-side standalone-eval fallback (HIVE_LABEL = 'hm1nd' in the
manager systemd unit) stays — that's wired so 'nixos-rebuild against
nixosConfigurations.manager' produces a sensibly-labelled container
even without the meta-flake wrapper.
Surface trait now: FLAVOR + FORGE_IS_MANAGER + 7 async wire methods.
next steps per #778 roadmap: #691 → #786 → #789.