feat(3088): move the gateway's nginx + dnsmasq onto the host
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.
This commit is contained in:
parent
cae2cf8df6
commit
07852cabc1
34 changed files with 704 additions and 618 deletions
|
|
@ -121,8 +121,25 @@ The mode is load-bearing, not cosmetic. Write permission on a
|
|||
them, and the sticky bit is the only thing that would restrain that (it
|
||||
is not set here). A world-writable socket dir therefore lets anything
|
||||
able to reach the path delete an agent's socket and bind its own — and
|
||||
the gateway container has all of `/run/hive-agent` bind-mounted in.
|
||||
Dropping `o=w` removes that permission rather than qualifying it.
|
||||
nginx reaches all of `/run/hive-agent` (as a plain host path since the
|
||||
gateway moved out of its container; it used to be bind-mounted in, which
|
||||
was the same reach through a longer route). Dropping `o=w` removes that
|
||||
permission rather than qualifying it.
|
||||
|
||||
⚠️ **The gateway leaving its container is a deliberate trade, recorded
|
||||
here so it is not mistaken for an oversight.** nginx and dnsmasq run on
|
||||
the host next to `hive-c0re` (see `docs/gateway.md`). What was given up
|
||||
is a *mount/pid* namespace — **not** a network one: that container ran
|
||||
with `privateNetwork = false` and shared the host's netns, so nginx was
|
||||
already binding host ports and already reaching `localhost` upstreams.
|
||||
The boundary bought no network isolation while costing a resolv.conf
|
||||
sync, a reload that had to cross the machine bus, and three bind mounts.
|
||||
🔑 It did cost one real thing, and the replacement is explicit: the
|
||||
privileged reload verb used to be scoped by `--machine=hive-gateway`,
|
||||
which could only ever reach into that one container. With no namespace
|
||||
to bound it, the unit name is hard-coded in `hive-priv` instead — see
|
||||
`PrivRequest::ReloadGatewayNginx`. **A caller cannot name the unit, so
|
||||
the verb cannot be steered at another service.**
|
||||
|
||||
⚠️ Contrast `/shared`, which *is* sticky world-writable (`1777`): it has
|
||||
many legitimate writers, so sticky is the best available answer there.
|
||||
|
|
|
|||
|
|
@ -381,7 +381,7 @@ that allows the underlying resource access.
|
|||
| `manage_root_agent` | may lifecycle-manage the root/manager agent via `kill`/`start`/`restart` |
|
||||
| `read_host_journal` | `get_host_journal` MCP tool is registered + `GET /journal-host` requests are served |
|
||||
| `query_agent_state` | may call `get_loose_ends` / `CountPendingReminders` targeting non-child agents |
|
||||
| `infra_admin` | may call `restart(name)` on hive infrastructure containers (`hive-ci`, `hive-gateway`, `hive-forge`); each restart is logged to the dashboard AUDIT trail |
|
||||
| `infra_admin` | may call `restart(name)` on hive infrastructure containers (`hive-ci`, `hive-forge`, `hive-matrix` — **not** `hive-gateway`, which is the host's nginx and is operator-only); each restart is logged to the dashboard AUDIT trail |
|
||||
|
||||
**Config storage** — per-agent capabilities live in
|
||||
`/var/lib/hyperhive/meta/capabilities.json` alongside `tool-groups.json`.
|
||||
|
|
|
|||
|
|
@ -500,9 +500,9 @@ Two things to know about the weights:
|
|||
override in `meta/resource-limits.json`, so every agent carries the
|
||||
same value and the weight does *not* rank agents against each other.
|
||||
What `80` buys is that agents yield to everything **not** on this
|
||||
drop-in path: host services and the infra containers (`hive-ci`,
|
||||
`hive-forge`, `hive-gateway`, `hive-matrix`), which stay at the
|
||||
kernel default of `100`.
|
||||
drop-in path: host services (nginx and dnsmasq among them) and the
|
||||
infra containers (`hive-ci`, `hive-forge`, `hive-matrix`), which stay
|
||||
at the kernel default of `100`.
|
||||
- `IOWeight=` is only honoured when the backing device runs the BFQ
|
||||
scheduler or has blk-iocost QoS enabled. On a host using
|
||||
`none`/`mq-deadline`/`kyber` without iocost, systemd writes the value
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# hive-gateway
|
||||
|
||||
Single nginx in front of every hyperhive web surface. Container `hive-gateway`, shared host netns, system-config (not meta-flake managed). Configured via `services.hyperhive.gateway.*` + per-subsystem opt-in flags in `services.hyperhive.{forge,matrix,...}`.
|
||||
Single nginx in front of every hyperhive web surface. Runs on the **host**, next to hive-c0re; system-config (not meta-flake managed). Configured via `services.hyperhive.gateway.*` + per-subsystem opt-in flags in `services.hyperhive.{forge,matrix,...}`. (It lived in a `hive-gateway` container until #3088 — one that shared the host netns anyway, so the boundary gave no network isolation while costing a resolv.conf sync, a machine-bus reload and three bind mounts.)
|
||||
|
||||
## Vhost map
|
||||
|
||||
|
|
@ -112,18 +112,22 @@ now set unconditionally for every agent. The mechanism:
|
|||
a UDS upstream (`http://unix:/run/hive-agent/<name>/web.sock:/`);
|
||||
if the socket is not yet bound, nginx returns 502 caught by the
|
||||
`error_page 502 503 504 = /__hive_agent_unreachable` directive.
|
||||
The gateway container bind-mounts `/var/lib/hyperhive/gateway/` at
|
||||
`/run/hive-state/`; nginx includes `/run/hive-state/agents.conf`.
|
||||
After each write, c0re triggers the appropriate nginx action inside
|
||||
the gateway container via `hive-priv` (which runs as root and has
|
||||
`--machine=hive-gateway` transport rights that hive-c0re lacks).
|
||||
nginx includes `/var/lib/hyperhive/gateway/agents.conf` — the same
|
||||
path c0re writes, since both run on the host.
|
||||
After each write, c0re triggers the appropriate nginx action via
|
||||
`hive-priv` (which is root; hive-c0re runs as the unprivileged
|
||||
`hive-core` user and cannot act on a system unit).
|
||||
`hive-priv` queries `ActiveState` and dispatches:
|
||||
- active → `systemctl reload nginx` (SIGHUP, zero-downtime)
|
||||
- failed → `systemctl reset-failed nginx` + `systemctl start nginx`
|
||||
- otherwise → `systemctl start nginx`
|
||||
This is intentionally host-side: `IN_MOVED_TO` from an atomic rename
|
||||
does not propagate across the nspawn mount-namespace boundary, so a
|
||||
path unit inside the container would never fire.
|
||||
This is an explicit trigger rather than a path unit watching the
|
||||
file. It used to be *impossible* to do it any other way — `IN_MOVED_TO`
|
||||
from the atomic rename did not cross the nspawn mount-namespace
|
||||
boundary, so an in-container path unit never fired. With nginx on the
|
||||
host a path unit would now work, and it is still not wanted: the write
|
||||
and the reload belong in one causal chain c0re can retry and report on
|
||||
(`RELOAD_PENDING`), not two units racing on an inotify event.
|
||||
|
||||
c0re regenerates `agents.conf` (and triggers a reload) on two
|
||||
triggers: every topology change (new/removed agents) and every 10s
|
||||
|
|
@ -160,8 +164,8 @@ selected by which (if any) external TLS source is set:
|
|||
| mode | config | cert source | `.well-known` scheme |
|
||||
|---|---|---|---|
|
||||
| self-signed (default) | neither `tls.certDir` nor `tls.acme` set | host hive-CA signs a gateway leaf (RSA-4096) | `https` |
|
||||
| ACME (Let's Encrypt) | `tls.acme.enable = true` | nginx inside container via HTTP-01 | `https` |
|
||||
| operator cert | `tls.certDir` set | bind-mounted from host | `https` |
|
||||
| ACME (Let's Encrypt) | `tls.acme.enable = true` | nginx via HTTP-01 | `https` |
|
||||
| operator cert | `tls.certDir` set | read from the operator's dir | `https` |
|
||||
|
||||
The `gateway.selfSignedTls` option is **deprecated and ignored** — self-signed
|
||||
is now derived from the absence of `tls.certDir` / `tls.acme`. Setting it to
|
||||
|
|
@ -182,7 +186,7 @@ services.hyperhive.gateway = {
|
|||
};
|
||||
```
|
||||
|
||||
nginx inside the gateway container obtains and auto-renews certs via the ACME HTTP-01 challenge on `port` (default 80). The gateway container shares the host network namespace (`privateNetwork = false`) so outbound ACME requests work without any extra routing. Certs are stored inside the container's persistent state dir (`/var/lib/acme/` inside `hive-gateway`; survives restarts because `ephemeral = false`).
|
||||
nginx obtains and auto-renews certs via the ACME HTTP-01 challenge on `port` (default 80). Certs land in `/var/lib/acme/` on the host, managed by nixpkgs's `security.acme` in the ordinary way.
|
||||
|
||||
**Requirements**: `services.hyperhive.domain` must be publicly DNS-resolvable to this host, and `openFirewall = true` so Let's Encrypt can reach `/.well-known/acme-challenge/`. Each active vhost (main domain, `forge.<swarm-domain>`, `chat.<swarm-domain>`) gets its own cert via separate ACME challenges — the swarm services default to names under `services.hyperhive.swarm.domain`, so **every one of those names must resolve to this host too**, not just the hive's own.
|
||||
|
||||
|
|
@ -194,9 +198,11 @@ Mutual exclusion: `tls.certDir` set together with `tls.acme.enable = true` fails
|
|||
|
||||
On by default, and listens on `httpsPort` (default 443) on every vhost beside the plain-http `port` (default 80).
|
||||
|
||||
The issuer is a **host-held hive CA**, not a bare self-signed leaf. A host service (`hive-tls-ca.service`, from the `hive-tls` module) generates a long-lived CA (`services.hyperhive.tls.caValidityDays`, default ~20y) under `services.hyperhive.tls.stateDir` (default `/var/lib/hive-tls`), then signs a gateway **leaf** (`leafValidityDays`, default 30d) with it. The leaf dir is bind-mounted read-only into the gateway container at `/run/hive-ca`; an in-container import unit copies the leaf into nginx's state dir (`/var/lib/hive-gateway/tls/{cert,key}.pem`) with the owner/mode nginx needs.
|
||||
The issuer is a **host-held hive CA**, not a bare self-signed leaf. A host service (`hive-tls-ca.service`, from the `hive-tls` module) generates a long-lived CA (`services.hyperhive.tls.caValidityDays`, default ~20y) under `services.hyperhive.tls.stateDir` (default `/var/lib/hive-tls`), then signs a gateway **leaf** (`leafValidityDays`, default 30d) with it. `hive-gateway-self-signed-cert` then imports the leaf into nginx's state dir (`/var/lib/hive-gateway/tls/{cert,key}.pem`).
|
||||
|
||||
**Why a CA, not a bare leaf**: a bare self-signed leaf is its own trust anchor, so every regeneration is a new anchor every consumer must re-trust — and a runtime-generated, in-container leaf can't be wired into an agent's build-time trust store at all. With a stable CA, agents and federation peers trust it *once*; leaf rotation never re-breaks them.
|
||||
⚠️ **That import unit is not a leftover of the old container — do not collapse it into pointing nginx at the CA dir.** It does two jobs. It re-modes the leaf (`hive-tls-ca` writes the key `0600 root:root`; nginx's pre-start `nginx -t` runs as the *nginx user*, so a `0600` key fails the config test and blocks the unit), and it guarantees **every cert path the nginx config names exists** — which is what the swarm-services fallback below is for. Removing it re-creates the #3097 outage.
|
||||
|
||||
**Why a CA, not a bare leaf**: a bare self-signed leaf is its own trust anchor, so every regeneration is a new anchor every consumer must re-trust — and a runtime-generated leaf can't be wired into an agent's build-time trust store at all. With a stable CA, agents and federation peers trust it *once*; leaf rotation never re-breaks them.
|
||||
|
||||
**What consumers trust**: `trust-bundle.pem` in the same state dir, not `ca.pem`. The hive CA is itself issued under the swarm root ([`swarm/ca.md`](swarm/ca.md) has the hierarchy), and an intermediate is not a chain a verifier can terminate at — so the bundle carries the hive CA plus whatever it is rooted at. nginx is handed the leaf with the hive CA appended for the same reason. Everything that trusts the hive's TLS reads the bundle: agents (via `security.pki.certificateFiles`), the CI and forge containers, and a federating peer.
|
||||
|
||||
|
|
@ -220,11 +226,11 @@ services.hyperhive.gateway = {
|
|||
};
|
||||
```
|
||||
|
||||
The directory is bind-mounted read-only into the gateway container at `/run/hive-tls/`. nginx uses `cert.pem` + `key.pem` (override `tls.certName`/`tls.keyName` for different filenames). Both modes listen on `httpsPort` (default 443) and emit `https://` in `.well-known` responses.
|
||||
nginx reads the directory directly and uses `cert.pem` + `key.pem` (override `tls.certName`/`tls.keyName` for different filenames). Both modes listen on `httpsPort` (default 443) and emit `https://` in `.well-known` responses.
|
||||
|
||||
`tls.certDir` and `tls.acme.enable` set together is an assertion error.
|
||||
|
||||
**Key file permissions**: nixpkgs's `security.acme` outputs private keys as `0640 root:acme` by default. nginx inside the gateway container runs as the `nginx` user and cannot read a key with that ownership. Fix with:
|
||||
**Key file permissions**: nixpkgs's `security.acme` outputs private keys as `0640 root:acme` by default. nginx runs as the `nginx` user and cannot read a key with that ownership. Fix with:
|
||||
|
||||
```nix
|
||||
security.acme.certs."example.com".group = "nginx";
|
||||
|
|
@ -424,11 +430,12 @@ JS/CSS assets skip `try_files` entirely and get the immutable cache
|
|||
headers. Nix store paths are content-addressed — the hash changes on
|
||||
any content change — so `max-age=31536000` is safe.
|
||||
|
||||
**Why nix store is reachable from the gateway container**: nspawn
|
||||
containers bind-mount `/nix/store` read-only by default. The
|
||||
`HIVE_AGENT_FRONTEND_DIR` path is a nix store path baked in at
|
||||
hive-c0re build time — the same path is visible to both c0re (writing
|
||||
`agents.conf`) and the gateway nginx (serving files from it).
|
||||
**Why the nix store path resolves**: `HIVE_AGENT_FRONTEND_DIR` is a nix
|
||||
store path baked in at hive-c0re build time, and c0re (writing
|
||||
`agents.conf`) and nginx (serving files from it) are on the same machine,
|
||||
so they see the same store. This used to need explaining — nspawn
|
||||
bind-mounts `/nix/store` read-only into a container, which is what made
|
||||
the baked-in path work from inside the gateway.
|
||||
|
||||
**Graceful degradation**: if `HIVE_AGENT_FRONTEND_DIR` is empty or
|
||||
unset (e.g. a build that predates `cfg.frontend`), each agent gets the
|
||||
|
|
@ -493,9 +500,7 @@ services.hyperhive.gateway.auth = {
|
|||
The credential store lives at the fixed path
|
||||
`/var/lib/hyperhive/gateway/gateway.htpasswd` on the host. A tmpfiles
|
||||
rule pre-creates the file on first boot; no manual path configuration
|
||||
is required. The file is exposed inside the gateway container at
|
||||
`/run/hive-state/gateway.htpasswd` via the existing gateway state
|
||||
bind-mount.
|
||||
is required. nginx reads it at that path directly.
|
||||
|
||||
Manage users with `hivectl gateway`. `hivectl` sends the request over the
|
||||
host admin socket and the `hive-c0re` daemon performs the write at its
|
||||
|
|
|
|||
|
|
@ -115,49 +115,46 @@ schemes pick their own.
|
|||
dnsmasq is **authoritative** for the hive's own zones — answers
|
||||
`<hive-domain>`, `forge.<hive-domain>`, `matrix.<hive-domain>`
|
||||
queries with the bridge IP (where nginx is reachable). Everything
|
||||
else is forwarded to the host's own resolvers: dnsmasq reads the
|
||||
gateway container's `/etc/resolv.conf`, the host copy nixos-container
|
||||
makes at each container start. Containers don't need to know the
|
||||
upstream — they query the bridge IP and dnsmasq does the right thing
|
||||
per-name.
|
||||
else is forwarded to the host's own resolvers: dnsmasq runs on the host
|
||||
and reads the host's `/etc/resolv.conf` directly. Containers don't need
|
||||
to know the upstream — they query the bridge IP and dnsmasq does the
|
||||
right thing per-name.
|
||||
|
||||
That copy is one-shot — systemd-nspawn(1) is explicit that nothing
|
||||
propagates into it after early init, because resolv.conf is normally
|
||||
updated by rename rather than in place. Left alone, a host network
|
||||
change (new router, new lease, laptop moving networks) would strand
|
||||
dnsmasq on a resolver that no longer answers, and every non-hive
|
||||
lookup from every agent would hang until someone restarted the
|
||||
gateway. The host-side **`hive-gateway-resolv`** path unit closes
|
||||
that: it watches `/etc/resolv.conf`, `machinectl copy-to`s it into
|
||||
the container, and reloads dnsmasq (`SIGHUP` — re-read upstreams +
|
||||
flush cache, nothing dropped). The watch is armed before
|
||||
`network-pre.target` so the boot's first DHCP write is caught as well,
|
||||
and the sync also runs once per gateway start to pick up a resolver
|
||||
change that happened while the container was down. A host file with no
|
||||
`nameserver` line is
|
||||
skipped rather than pushed, so a mid-rewrite snapshot can't blank the
|
||||
hive's DNS. There is deliberately no fallback `server=`: dnsmasq
|
||||
queries all known upstreams in parallel, so a hardcoded public
|
||||
resolver would take a share of normal traffic, not just cover the gap.
|
||||
There is deliberately no fallback `server=`: dnsmasq queries all known
|
||||
upstreams in parallel, so a hardcoded public resolver would take a share
|
||||
of normal traffic, not just cover the gap.
|
||||
|
||||
Two alternatives that look simpler and aren't:
|
||||
### History: the resolv.conf sync, and why it is gone
|
||||
|
||||
- **A path unit inside the container.** The host replaces
|
||||
`/etc/resolv.conf` by rename, and that `IN_MOVED_TO` doesn't cross
|
||||
the nspawn mount namespace — the same reason hive-c0re reloads nginx
|
||||
from the host side after each `agents.conf` write.
|
||||
- **Bind-mounting the host's `/etc/resolv.conf` into the container.**
|
||||
openresolv writes a temp file and renames over the target, so the
|
||||
bind mount would pin the *first* inode for the container's whole
|
||||
lifetime — strictly worse than the copy, which at least a restart
|
||||
clears. (Reachability is not the problem here: the gateway runs with
|
||||
`privateNetwork = false`, so it shares the host's netns and can reach
|
||||
anything the host can.)
|
||||
Until the gateway moved onto the host, dnsmasq ran in the `hive-gateway`
|
||||
container and read *that* container's `/etc/resolv.conf` — a one-shot
|
||||
copy nixos-container made at start. systemd-nspawn(1) is explicit that
|
||||
nothing propagates into it after early init, because resolv.conf is
|
||||
normally updated by rename rather than in place. So a host network change
|
||||
(new router, new lease, laptop moving networks) stranded dnsmasq on a
|
||||
resolver that no longer answered, and every non-hive lookup from every
|
||||
agent hung until someone restarted the gateway.
|
||||
|
||||
`machinectl copy-to` is used rather than writing to the container's
|
||||
rootfs from the host, so the push goes through the container's own
|
||||
mount namespace and stays correct if `/etc` is ever assembled
|
||||
differently (e.g. `system.etc.overlay`).
|
||||
A host-side `hive-gateway-resolv` path unit closed that gap: watch
|
||||
`/etc/resolv.conf`, `machinectl copy-to` it into the container, reload
|
||||
dnsmasq. Roughly eighty lines of watcher, marker file, is-active guard
|
||||
and mid-rewrite-snapshot check — **all of it bridging two copies of one
|
||||
file.** With one machine there is one file, and the whole unit is
|
||||
deleted.
|
||||
|
||||
🔑 Worth keeping as a shape, not just a story: **the sync was not
|
||||
complexity anyone chose. It was the cost of a boundary that bought
|
||||
nothing here** — the gateway already ran with `privateNetwork = false`,
|
||||
sharing the host's netns, so the container never provided network
|
||||
isolation in the first place. When a workaround is that elaborate, the
|
||||
question to ask is what the boundary is *for*.
|
||||
|
||||
(Two alternatives were considered at the time and both were worse than
|
||||
the copy: a path unit *inside* the container never fired, because the
|
||||
host replaces the file by rename and `IN_MOVED_TO` does not cross the
|
||||
nspawn mount namespace; and bind-mounting the host's `/etc/resolv.conf`
|
||||
would have pinned the *first* inode for the container's whole lifetime,
|
||||
since openresolv writes a temp file and renames over the target.)
|
||||
|
||||
`bind-interfaces` + `interface = [ bridgeName "lo" ]` means the
|
||||
listener only accepts queries from the bridge interface (plus lo for
|
||||
|
|
@ -289,4 +286,4 @@ loopback — the hive-c0re admin socket is a UDS, not TCP.
|
|||
|
||||
## Cross-references
|
||||
|
||||
- `docs/gateway.md` — vhost map + the gateway container's other duties
|
||||
- `docs/gateway.md` — vhost map + the gateway's other duties
|
||||
|
|
|
|||
|
|
@ -187,8 +187,20 @@ bind-mount compatibility with user namespace UID mapping and is tracked as a TOD
|
|||
|
||||
`hive-c0re` runs as the unprivileged system user `hive-core`
|
||||
(`/var/lib/hyperhive` owned by `hive-core:hive-core`). It cannot
|
||||
directly invoke `nixos-container`, `journalctl -M`, or `systemctl
|
||||
-M hive-gateway` — those require root. `hive-priv` fills this gap.
|
||||
directly invoke `nixos-container`, `journalctl -M`, or act on a system
|
||||
unit (`systemctl reload nginx`) — those require root. `hive-priv` fills
|
||||
this gap.
|
||||
|
||||
⚠️ **Note what that costs when a helper verb loses its namespace.**
|
||||
`ReloadGatewayNginx` used to run `systemctl -M hive-gateway …`, and the
|
||||
`--machine=` flag was doing two jobs: it was the *transport* into the
|
||||
container **and** the *scope* — the verb could not reach anything
|
||||
outside that one machine. With nginx on the host the transport is
|
||||
unnecessary and the scope went with it, so the containment is now the
|
||||
hard-coded unit name in `hive-priv`: a caller cannot name the unit, so
|
||||
the verb cannot be steered at another service. **When a privileged verb
|
||||
stops needing a namespace, check whether the namespace was also what
|
||||
bounded it.**
|
||||
|
||||
### hive-priv
|
||||
|
||||
|
|
@ -207,7 +219,7 @@ known operations; there is no arbitrary command pass-through:
|
|||
| `DestroyContainer` | `nixos-container destroy <name>` |
|
||||
| `ListContainers` | `nixos-container list` |
|
||||
| `ReadContainerJournal` | `journalctl -M <container> -n <n> [filters...]` |
|
||||
| `ReloadGatewayNginx` | `systemctl -M hive-gateway reload/start/reset-failed nginx` |
|
||||
| `ReloadGatewayNginx` | `systemctl reload/start/reset-failed nginx` (host unit; the unit name is hard-coded, not a parameter) |
|
||||
| `WriteNspawnFlags` | write `/etc/nixos-containers/<container>.conf` (bind-mount list + network isolation vars) |
|
||||
| `WriteResourceLimits` | write `CPUQuota=`/`MemoryMax=`/`CPUWeight=`/`IOWeight=` systemd drop-in for agent container |
|
||||
| `RemoveServiceDropin` | remove `container@<name>.service.d/` drop-in on destroy |
|
||||
|
|
|
|||
|
|
@ -808,7 +808,7 @@ Bare `hivectl stop` stops everything; scope flags narrow it to specific sub-agen
|
|||
* `--agent <NAME>` — A specific sub-agent by name. Repeatable: `--agent a --agent b`
|
||||
* `--ci` — The CI runner container (`hive-ci`)
|
||||
* `--forge` — The forge container (`hive-forge`)
|
||||
* `--gateway` — The gateway container (`hive-gateway`)
|
||||
* `--gateway` — The gateway (`hive-gateway`) — nginx on the host, not a container
|
||||
* `--matrix` — The matrix container (`hive-matrix`)
|
||||
* `--graceful` — Gracefully quiesce each agent before stopping, instead of a hard stop. Each agent gets a graceful-stop DAG on the job queue: the harness is signalled, runs one stop-checkpoint turn to flush durable `/state`, drains, then the container is stopped (bounded by a 3-min timeout that falls back to a hard stop). All drains overlap. Applies to agents only
|
||||
* `--no-wait` — Return immediately after the stop DAGs are queued instead of waiting for them with live per-node progress
|
||||
|
|
@ -829,7 +829,7 @@ Bare `hivectl start` restores the agents stopped by the last broad-scope `stop`
|
|||
* `--agent <NAME>` — A specific sub-agent by name. Repeatable: `--agent a --agent b`
|
||||
* `--ci` — The CI runner container (`hive-ci`)
|
||||
* `--forge` — The forge container (`hive-forge`)
|
||||
* `--gateway` — The gateway container (`hive-gateway`)
|
||||
* `--gateway` — The gateway (`hive-gateway`) — nginx on the host, not a container
|
||||
* `--matrix` — The matrix container (`hive-matrix`)
|
||||
* `--no-wait` — Return immediately after the start DAGs are queued instead of waiting for them with live per-node progress
|
||||
|
||||
|
|
@ -849,7 +849,7 @@ Bare `hivectl restart` restarts everything; scope flags narrow it.
|
|||
* `--agent <NAME>` — A specific sub-agent by name. Repeatable: `--agent a --agent b`
|
||||
* `--ci` — The CI runner container (`hive-ci`)
|
||||
* `--forge` — The forge container (`hive-forge`)
|
||||
* `--gateway` — The gateway container (`hive-gateway`)
|
||||
* `--gateway` — The gateway (`hive-gateway`) — nginx on the host, not a container
|
||||
* `--matrix` — The matrix container (`hive-matrix`)
|
||||
* `--graceful` — Gracefully quiesce each agent on the stop half (see `stop --graceful`). Applies to agents only
|
||||
|
||||
|
|
|
|||
|
|
@ -79,8 +79,10 @@ lifecycle events, or another container's boot log.
|
|||
- `unit` — filter to a systemd unit (e.g. `hive-c0re.service`).
|
||||
- `container` — nspawn machine name verbatim. Agent containers use
|
||||
the `h-<name>` prefix (e.g. `h-iris`); infrastructure containers
|
||||
use their full name (e.g. `hive-ci`, `hive-forge`, `hive-matrix`,
|
||||
`hive-gateway`). Omit for the host journal.
|
||||
use their full name (e.g. `hive-ci`, `hive-forge`, `hive-matrix`).
|
||||
Omit for the host journal. The gateway has no machine — its nginx
|
||||
runs on the host, so read it with `unit: nginx.service` and no
|
||||
`container`.
|
||||
- `lines` — how many lines to return (default 30, max 100).
|
||||
- `priority` — minimum syslog level (`emerg` … `debug`).
|
||||
- `grep` — regex matched against log message fields (`journalctl --grep`).
|
||||
|
|
|
|||
|
|
@ -180,10 +180,12 @@ omitted — agents share the host netns, so there is no per-container net
|
|||
counter (per-agent network needs the netns-isolation roadmap in
|
||||
`docs/network.md`).
|
||||
|
||||
**1NFR4** — start / stop / restart the four hive infrastructure
|
||||
containers (`hive-ci`, `hive-forge`, `hive-gateway`, `hive-matrix`)
|
||||
directly from the dashboard, without needing an `infra_admin` agent's
|
||||
`restart` MCP tool. One row per container: name, a `badge-ok`/`badge-fail`
|
||||
**1NFR4** — start / stop / restart the four hive infrastructure services
|
||||
(`hive-ci`, `hive-forge`, `hive-gateway`, `hive-matrix`) directly from the
|
||||
dashboard, without needing an `infra_admin` agent's `restart` MCP tool.
|
||||
Three are containers; `hive-gateway` is the host's `nginx.service`, and is
|
||||
the one an agent may **not** restart — this panel is the way it gets
|
||||
bounced. One row per service: name, a `badge-ok`/`badge-fail`
|
||||
running/stopped dot, and `↺ R3ST4RT` + `■ ST0P` (running) or `▶ ST4RT`
|
||||
(stopped) buttons, same themed-confirm pattern as the K3PT ST4T3
|
||||
tombstone actions. Backed by
|
||||
|
|
@ -460,7 +462,7 @@ The current capabilities are:
|
|||
| `manage_root_agent` | allows the `set_status` / lifecycle tools on the root agent |
|
||||
| `read_host_journal` | unlocks `get_host_journal` to read journald from inside a container |
|
||||
| `query_agent_state` | allows `get_loose_ends(agent: "<name>")` calls targeting other agents |
|
||||
| `infra_admin` | allows `restart` on hive infrastructure containers (`hive-ci`, `hive-gateway`, `hive-forge`); each restart is logged to the AUDIT trail |
|
||||
| `infra_admin` | allows `restart` on hive infrastructure containers (`hive-ci`, `hive-forge`, `hive-matrix`; the gateway is operator-only); each restart is logged to the AUDIT trail |
|
||||
|
||||
Each row is one agent. Columns are the capability names returned by
|
||||
`GET /api/capabilities` as `caps: Vec<String>`. Checking or unchecking
|
||||
|
|
@ -1340,9 +1342,11 @@ below — some endpoints aren't in it yet.
|
|||
rootfs every ~5 min, `-x` excluding the shared read-only nix store.
|
||||
`null` until the first sample lands.
|
||||
- `POST /api/infra-container/{name}/{action}` — start / stop / restart a
|
||||
hive infra container (C0R3 › 1NFR4 panel). `name` parses into the
|
||||
hive infra service (C0R3 › 1NFR4 panel). `name` parses into the
|
||||
`InfraContainer` allowlist (`hive-ci`/`hive-forge`/`hive-gateway`/
|
||||
`hive-matrix`, 400 on unknown), `action` ∈ `start|stop|restart`. Calls
|
||||
`hive-matrix`, 400 on unknown), and the variant decides the unit —
|
||||
`container@<name>.service`, or `nginx.service` for the gateway.
|
||||
`action` ∈ `start|stop|restart`. Calls
|
||||
the same `priv_client::control_infra_container` helper the
|
||||
`infra_admin` agent path uses; records an `audit_log` entry
|
||||
(`start_infra`/`stop_infra`/`restart_infra`, actor `"operator"`) either
|
||||
|
|
|
|||
Loading…
Reference in a new issue