observability.md described a single collector holding the upstream
credential. It also said endpoint and protocol are what agents are
handed; agents get the derived first hop, which has been true since the
collector was introduced.
The swarm tier is documented beside its sibling swarm services rather
than here, and the one line an operator must not miss - swarm.otel.url
on a hive that does not run them - is called out in both places, since
leaving it unset loses telemetry silently.
Verified against nix/host-modules/hive-gateway/dnsmasq.nix + gateway.localNames
contributors (hive-forge, hive-matrix, swarm-authelia): forge and the matrix
gateway vhost are swarm-domain names (forge.<swarm-domain>, chat.<swarm-domain>),
not sub-domains of the hive's own domain as the doc previously claimed.
The vhost half of this change is only useful with an account behind it,
and the provider is generated with an empty user set on purpose. Document
the `swarmctl user add` step rather than automating it: bootstrapping an
IdP non-interactively means a secret arriving from a file, an env var or a
nix expression, all worse than one command typed once.
The gateway and network pages gain the rows they would otherwise be
missing — vhost map, local-dev hosts entry, and the resolver's
authoritative-name list.
Per review: docs represent current state. Every "used to" / "no longer"
clause this branch introduced is gone — including the History section in
network.md, which was a whole subsection about a sync mechanism that
doesn't exist.
Where the removed clause was carrying a real constraint, the constraint
stays and is stated in the present tense instead of as a delta: nothing
narrows what the gateway's nginx can reach except the directory
permissions in front of a socket, and nothing bounds `ReloadGatewayNginx`
except the hard-coded unit name. Those read as rules now rather than as
the story of how they came to be rules.
The gateway's nginx + dnsmasq no longer run in their own nspawn container.
`nix/host-modules/hive-gateway/default.nix` loses the
`containers.hive-gateway` wrapper and everything that existed only to punch
holes in it: `privateNetwork = false`, `CAP_NET_ADMIN`, five bind mounts,
its own `stateVersion`, `networking.firewall.enable = false`,
`networking.resolvconf.enable = false`, and the `hive-gateway-resolv`
path+service pair. 465 -> 303 lines.
The container never bought isolation here. It shared the host netns by
necessity — nginx binds the host's :80/:443, dnsmasq answers on the bridge —
so each of those settings was undoing a boundary the gateway could not
afford in the first place.
Four things made it more than a deletion, none of them visible in the nix
diff:
- The self-signed cert service also imports the hive CA leaf, so removing it
with the container would have left nginx naming a missing cert file, which
it refuses to load at all.
- The nginx reload is a hive-priv verb. It still needs root, but no longer
for the reason its doc gave, and `--machine=` was both transport and
scope — so the unit name is now hard-coded in the helper as the
containment.
- The lifecycle verb named a container that stops existing.
- `journalctl -M hive-gateway` had no machine to enter.
Per the operator's ruling, the operator verb keeps working and agents lose
it. `InfraContainer` answered three questions that used to share an answer;
it now splits into `name()` (identity), `target()` (Container vs HostUnit),
`service_unit()` (the systemd unit), and `agent_restartable()`, which the
MCP restart path checks before the capability so the refusal cannot read as
"ask for infra_admin". `SIBLING_CONTAINERS` drops the gateway — it gates the
requests that name a container as a string — while `FromStr` still accepts
it, because that answers what a name is, not who may act on it. The
dashboard's gateway journal reads host journald filtered to `nginx.service`.
Prose was corrected where it only named a location, and re-argued where the
container was doing security work: a `0666` per-agent socket was safe
because only the gateway container had the directory bind-mounted. There is
no mount now, so the directory permissions are the whole of the access
control — the constraint holds, its mechanism doesn't.
Gate: nix fmt / clippy --all-targets -D warnings / cargo test all clean (710
tests); hivectl-cli.md regenerated from the clap tree. The nix eval was run
in both TLS shapes at this commit: every delta in the rendered
virtualHosts is one of the three intended path moves, dnsmasq settings are
byte-identical, and the absence probe flips true -> false with bindMounts
emptied.
The three moves above falsify prose in five files, and none of it is
caught by a gate: clippy, cargo test and nix flake check read exactly
zero markdown.
Corrected where a doc stated a DEFAULT or enumerated the vhosts --
gateway (ACME requirements + the header table + the two Forgejo
reachability lines), matrix (gatewayHost, firewall rationale, agent
reachability), network (container table + the HTTP resolution bullet),
turn-loop config, dashboard.
Two mentions are deliberately left alone. `docs/tools/hivectl.md` says
the URL is read from the daemon "instead of assuming forge.<domain>" --
that sentence is about not assuming a shape and stays true. And
matrix.md's breaking-change note describes what `serverName` USED to
default to; it is history, correctly labelled, and rewriting it would
erase the record of an earlier migration.
The ACME line gained a requirement rather than a rename: with the
service names under the swarm domain, every one of those names has to
resolve to this host, not just the hive's own.
`services.hyperhive.domain` and `swarm.hives.<hiveName>.domain` were two
homes for one value with nothing asserting they agreed. The failure that
buys is the worst shape a config defect has: it evaluates cleanly, and
the symptom ("the other hives can't reach me") appears on a machine
other than the misconfigured one.
The directory is now the single source. `hives.<name>.domain` gains the
`<name>.<swarm.domain>` default -- a derivation from two values an
operator had to state explicitly, not a guess -- so a conventional swarm
is a list of names and a hive addressed by something else says so in the
one place every other hive reads. `services.hyperhive.domain` reads its
own entry; the direct formula is deleted rather than kept as a fallback,
which would have restored the second path (and, reading `swarm.domain`
itself, a second path that can disagree).
Setting it directly still wins, with a deprecation warning: nothing
breaks today, but a value written only there is invisible to the swarm.
The self-entry assertion now fires on an EMPTY directory too. Since
`swarm.domain` became required, every hive is in a swarm -- a swarm of
one is still a swarm -- and this host's address is read out of the
directory, so the entry is missing either way and the precise message
should be the one that fires.
Upgrading costs one line on hives that never listed themselves:
`services.hyperhive.swarm.hives.<hiveName> = { };`, no value.
`docs/swarm.md` becomes `docs/swarm/README.md` and the shared-services
material moves to `docs/swarm/services.md`, following the shape
`docs/turn-loop/` and `docs/web-ui/` already use. The README keeps a
pointer so the reading path is unbroken.
Every referrer moved with it — five docs pages, two option descriptions
in swarm.nix, and CLAUDE.md's reading path. A pointer to a file that
moved is worse than one to a file that was deleted: the content still
exists, so the reader concludes the note is wrong rather than the path.
The gateway container's /etc/resolv.conf is a one-shot copy: nixos-container
cps it in from the host in its start script, and nspawn's --resolv-conf=auto
copies (not binds) for a writable host-netns container. systemd-nspawn(1)
states the consequence outright — "no further propagation of configuration is
generally done after the one-time early initialization (this is because the
file is usually updated through copying and renaming)".
dnsmasq has no explicit upstream and follows that file, so a host network
change strands it on a resolver that no longer answers and every non-hive
lookup from every agent hangs. Agents' own resolvers point at the static
bridge IP and never go stale, which is why the symptom presents as "the
gateway needs a kick".
Add a host-side hive-gateway-resolv path unit watching /etc/resolv.conf.
On change it machinectl copy-to's the file into the container and reloads
dnsmasq — ExecReload is kill -HUP, so upstreams are re-read and the cache
flushed without dropping anything; nginx never notices.
- watched from the HOST: a rename on the host doesn't cross the nspawn mount
namespace, so an in-container path unit can't see it (same reason c0re
reloads nginx from the host side)
- copy, not a file bind-mount: openresolv renames over the file, so a bind
would pin the first inode forever — strictly worse than today
- machinectl copy-to writes through the container's own mount namespace, so
this holds regardless of how the container assembles /etc
- armed Before=network-pre.target so the boot's first DHCP write is caught,
and re-run on gateway start for changes made while it was down
- a host file with no nameserver line is skipped, not pushed, so a
mid-rewrite snapshot can't blank hive DNS
- deliberately no fallback server=: dnsmasq queries all known upstreams in
parallel, so a hardcoded public resolver would take a share of normal
traffic rather than only covering the gap
Trim the historical parenthetical about the dropped systemd-socket-proxyd
forwarder — argus flagged it (🟡 on the merged PR) and the constraint
(bind 0.0.0.0 or the bridge IP, not loopback) stands on its own without
the "why we dropped the proxy" narrative. No-history-in-docs preference.
- docs/network.md: LOCAL_ADDRESS is now empty (not deterministic-IP);
containers use dhcpcd + dnsmasq DHCP pool, not hash-derived static IPs
- nix/modules/hive-ci.nix: 'service containers' → 'all containers'
in the dnsmasq DHCP pool comment (agents also use the same pool)
The parenthetical was backwards — this rule opens bridge ports so
*isolated* agents (in private netns, connected via veth) can reach
nginx. Remove the misleading '(shared netns)' tag, replace with the
accurate description.
The two-phase network rollout (bridge-first, then isolateContainers) is
complete. Both options are now deprecated no-ops — isolation is always on.
Update the doc to reflect current state:
- Drop the phased-rollout intro ('off by default during rollout') and the
v1/v2 comparison table; keep a brief historical note
- Remove 'Why ship before netns isolation' section (rollout is done)
- Update configuration example: network.enable is no longer needed
- Merge firewall description into a single table (80/443 always open)
- Remove 'Prerequisites before flipping on' and 'Migration behaviour'
subsections (isolation was a one-time flip; no longer a toggle)
- Simplify resolver wiring: hive-priv always drops the marker; remove
'only when isolated' conditional framing
Add docs/observability.md covering all services.hyperhive.otel.*
options: enable, endpoint, protocol, headersCredential,
extraResourceAttributes, debug (new in cb0a66147a), and
metricIntervalMs.
Includes:
- Built-in OTEL_RESOURCE_ATTRIBUTES labels (service.name, agent, hive, swarm)
- Cumulative temporality note (avoids Prometheus DELTA drop)
- Network note for host-side collectors on non-standard ports,
cross-referencing docs/network.md exposeHostPorts
Also:
- CLAUDE.md: add reading-path entry for the new doc
- docs/network.md: link the OTEL mention to observability.md
Closes no issue — gap found during doc sweep.
- .prettierrc: proseWrap=preserve (no prose reflow)
- .prettierignore: exclude hivectl-cli.md (auto-generated) + 11 docs
with multi-line list-item continuations prettier would strip to col 0
(CommonMark limitation in prettier's list handling)
- format 16 markdown files: cosmetic only (*→_, table alignment,
heading normalisation) — verified no broken continuations, idempotent
- Drop the redundant 'including the root/bootstrap container' /
'sub-agents + root' enumerations in the port-range + state-dir
uniformity statements — 'every agent' / 'all agents' is enough (and
calling out '+ root' reintroduces the special-case framing).
- Stop describing the root agent's privileged (manager-flavour) socket
as a current thing (it's being removed): drop the conventions.md
capability clause, the network.md per-agent-privileged-socket list
entry, and the persistence.md /run/hyperhive/manager/mcp.sock
runtime-dir bullet.
Port-range, network-isolation, state-dir, web-UI-port, priv allowlist,
and rebuild-source mentions now refer to the root/bootstrap container
rather than 'the manager' — no structural manager exists (root-ness is
topological). Real artifact names (the manager.nix template toplevel,
/run/hyperhive/manager/mcp.sock) are left as-is.
The "What the Rust side does" section predated the isolated-container
egress fix and omitted two now-implemented mechanisms:
- HOST_ADDRESS=<bridge-ip> in the nspawn conf — load-bearing for the
container's default route (nixos-container only installs `default via`
when HOST_ADDRESS is non-empty; bridge mode skips host-side route setup).
- the resolver wiring: hive-priv's bridge-DNS marker + the
hyperhive-isolated-dns oneshot rewriting resolv.conf to the bridge
dnsmasq (nixos-container re-copies the host's resolv.conf each start, so
the fix is runtime, ordered before the harness).
Doc-only; brings network.md in line with the merged behavior.
when isolateContainers=true, isolated agents have dnsmasq as their
resolver — forge.<domain> resolves to bridgeIp. route HIVE_FORGE_URL
through nginx on port 80 instead of exposing the raw forge port.
- HIVE_FORGE_URL: http://<forge.domain> when isolated (nginx proxies)
- bridge firewall: open 80+443 for agents to reach nginx (gateway)
- remove forge-specific httpPort rule (no longer needed)
- update docs/gateway.md + docs/network.md
per mara's review comment on PR #1150.
When containers run in private netns (isolateContainers=true), host
loopback is unreachable so HIVE_FORGE_URL=http://127.0.0.1:3000 breaks.
- nix/modules/hive-network.nix: when isolateContainers is on + forge
is enabled, open forge.httpPort on the bridge interface so agents
can reach forgejo at bridgeIp:httpPort (forgejo binds 0.0.0.0)
- nix/modules/hive-c0re.nix: HIVE_FORGE_URL switches to bridge IP
when network.enable && isolateContainers; loopback path retained
when isolateContainers=false
- docs/network.md: add Forge access + Forge URL rows to effects table
- docs/gateway.md: rewrite HIVE_FORGE_URL section for both modes
Last pass of the docs-from-code → docs/ epic (#708). Drops every
attribution cookie from docs/ + README.md + CLAUDE.md so the
source-tree files no longer reference the issue tracker. Issue
threads + commit history retain the references — those are the
canonical record.
- README.md: drop #701 / #660×2 / #551 from matrix + display-name
sections, rephrase to convey the semantics directly
- CLAUDE.md: scrub 18 cookies from the file map (#655, #15, #784,
#832, #444, #425, #361, #548, #598, #539, #544, #589, #701,
#658, #280, #660, #551, #764, #772, #793, #14, #805)
- docs/agent-hierarchy.md: drop #658 ×3 (per-agent user is the
current shape, not a transition)
- docs/conventions.md: drop #571 (replaced with a docs xref to
persistence.md::matrix-avatar-sync)
- docs/gateway.md: scrub vhost-map table cookies + Sub-domain
rationale + Per-agent unix-socket upstream + Self-signed TLS +
Firewall posture + HIVE_FORGE_URL + Per-agent error pages
sections; drop the trailing 'Sequencing history' issue list +
the 'Next-up' issue-link footnote
- docs/matrix.md: scrub serverName/gatewayHost + Default-closed
firewall + Provisioning flow + Initial rollout + Assertion
rationale + fluffychat-web build fixes; drop the trailing
'Sequencing history' issue list
- docs/network.md: drop 'Why ship before #14' #805 quote +
Container shape #805 attribution + trailing 'Sequencing history'
+ Cross-references issue links; rename v2 column to 'after netns
isolation'
- docs/web-ui.md: drop #784 from Container row, replace with a
docs xref to docs/gateway.md::Per-agent unix-socket upstream
Only remaining #NNN in docs/ is the literal markdown-heading
example in docs/forge.md (`#tag`, `#123`, `#!/bin/bash`)
which demonstrates the renderer's behaviour — not an attribution
cookie.
Stand up the host-side bridge interface + per-agent DNS resolver
ahead of #14 (netns isolation). Mara on #805#11541: "we need it
before #14 so nothing breaks when we switch over". v1 ships the
endpoint live but containers stay on shared host netns — when #14
flips them to private netns the DNS contract is already there.
Shape:
- new `nix/modules/hive-network.nix` with `services.hyperhive.network.*`
options (enable + bridgeName + bridgeIp + bridgePrefixLength +
upstreamDns). Default off. Imported from `hive-c0re.nix`.
- bridge interface via `networking.bridges` (no slave NICs at v1;
per-agent veth pairs attach once #14 lands).
- bridge IP assigned via `networking.interfaces`.
- `networking.firewall.interfaces.<bridge>.allowed{UDP,TCP}Ports =
[ 53 ]` opens the resolver on the bridge interface only —
other interfaces stay closed.
- dnsmasq config added to the existing `hive-gateway` container
(mara on #805:10957: "put the resolver into the gateway container").
Listens only on `bridgeName` + `lo`; authoritative for
`<hive-domain>`, `forge.<hive>`, `matrix.<hive>` answering with
the bridge IP; forwards everything else to upstream.
`resolveLocalQueries = false` keeps the gateway container's own
resolver untouched.
Asserts `services.hyperhive.domain != null` + `gateway.enable =
true` — both required for the resolver to be meaningful.
Docs: new `docs/network.md` covering v1 vs v2 split, container shape
rationale, default addressing, resolver behaviour, firewall posture.
`nix flake check` clean.