//! Wire types for the `hive-priv` privileged-helper socket //! (`/run/hive/priv.sock`). //! //! Both `hive-priv` (server) and `hive-c0re` (client via `priv_client`) //! import these so the shapes stay in sync. Split out of `hive-sh4re` so //! `hive-priv` — the privileged root helper — can depend on just this //! protocol crate instead of the much larger daemon-shared crate: fewer //! dependencies in the root-privileged binary's supply chain, and a //! narrower interface makes the boundary this crate encodes easier to //! audit. No server/client implementation lives here, only the wire //! contract (mirrors `hive-host-sock`'s split for the host admin socket). use serde::{Deserialize, Serialize}; /// Default socket path for the privileged helper. pub const PRIV_SOCK: &str = "/run/hive/priv.sock"; /// File name of the pause marker inside an agent's harness dir. Defined /// here — the narrowest crate all three sides already share — because the /// marker is a two-sided contract with no protocol behind it: hive-priv /// creates and unlinks it as root, hive-c0re stats it to render the paused /// indicator, and the in-container harness stats it to gate its turn loop /// (via the `hive-sh4re::paths` re-export). A private copy on any one side /// would break pause *silently*, since every reader just sees "no marker" — /// exactly the failure mode a shared constant exists to prevent. pub const PAUSED_MARKER_FILE: &str = "paused"; /// Manager logical agent name. The manager's system container name is /// `h-ruth` (same `h-` prefix convention as every other agent). pub const MANAGER_NAME: &str = "ruth"; /// Sub-agent container prefix. System container name = `h-`. pub const AGENT_PREFIX: &str = "h-"; /// Sibling service containers managed by hive-c0re. This doubles as the /// authoritative allowlist for infra lifecycle ops /// ([`PrivRequest::ControlInfraContainer`]): any of these four may be /// started / stopped / restarted (by the hive-wide `hivectl stop`/`start` /// flow or an `infra_admin` agent's `restart`). `hive-c0re` is deliberately /// absent — stopping it would sever the very socket the request arrived on. /// hive-priv re-validates against this list root-side, so it's authoritative /// regardless of what the caller sends. pub const SIBLING_CONTAINERS: &[&str] = &["hive-forge", "hive-matrix", "hive-gateway", "hive-ci"]; /// Lifecycle verb for [`PrivRequest::ControlInfraContainer`]. Maps directly /// to `systemctl container@.service`. #[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum InfraAction { Start, Stop, Restart, } impl InfraAction { /// The `systemctl` subcommand this action maps to. pub fn systemctl_verb(self) -> &'static str { match self { InfraAction::Start => "start", InfraAction::Stop => "stop", InfraAction::Restart => "restart", } } } /// A hive infrastructure container that can be controlled (start / stop / /// restart) via [`PrivRequest::ControlInfraContainer`]. The variants ARE /// the allowlist: serde rejects any other value at the wire boundary, so an /// unknown or unsafe target — notably `hive-c0re`, which has no variant and /// would sever the daemon socket — is *unrepresentable* rather than caught /// by a runtime check. The c0re↔hive-priv wire form uses serde's default /// variant naming (`"Ci"`, `"Forge"`, …); it's an internal protocol (both /// ends rebuild together) so it needn't match the container name. /// [`unit_name`](Self::unit_name) is the separate systemd / container name /// (`hive-ci`). #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum InfraContainer { Ci, Forge, Gateway, Matrix, } impl InfraContainer { /// Every controllable infra container. The source of truth that /// [`SIBLING_CONTAINERS`] is kept consistent with (see the test). pub const ALL: [InfraContainer; 4] = [ InfraContainer::Ci, InfraContainer::Forge, InfraContainer::Gateway, InfraContainer::Matrix, ]; /// The container / systemd-unit name, e.g. `hive-ci` → /// `container@hive-ci.service`. (Distinct from the serde wire form, /// which is the default variant name `"Ci"`.) #[must_use] pub fn unit_name(self) -> &'static str { match self { InfraContainer::Ci => "hive-ci", InfraContainer::Forge => "hive-forge", InfraContainer::Gateway => "hive-gateway", InfraContainer::Matrix => "hive-matrix", } } } impl std::str::FromStr for InfraContainer { type Err = (); /// Parse a container name (`hive-ci`, …) into a variant. Used to decide /// whether an MCP `restart()` target is a controllable infra /// container. `Err(())` for anything that isn't one. fn from_str(s: &str) -> Result { Self::ALL.into_iter().find(|c| c.unit_name() == s).ok_or(()) } } /// Host path of the meta flake. The flake ref for agent `` is /// `{META_DIR}#{name}`, derived by `hive-priv` — never passed over the wire. /// Must stay in sync with `hive-c0re::paths::meta_root()` (`STATE_ROOT + /// "/meta"`); the privsep boundary prevents importing across the crate. pub const META_DIR: &str = "/var/lib/hyperhive/meta"; /// Root of per-agent state directories on the host. /// Subdirectory layout: `//state/`. /// Used by `WriteAgentStateFile` to derive the write path so the /// exact path is never passed over the wire. /// Must stay in sync with `hive-c0re::paths::AGENTS_ROOT`; the privsep /// boundary prevents importing across the crate. pub const AGENT_STATE_ROOT: &str = "/var/lib/hyperhive/agents"; /// Root of per-agent runtime directories on the host (regenerated each boot /// by `hive-priv` tmpfiles.d; not persistent). Used by `hive-priv` when /// creating per-agent subdirs via `nsenter` / tmpfiles. /// Must stay in sync with `hive-c0re::paths::agent_runtime_root()` /// (`RUNTIME_ROOT + "/agents"`); the privsep boundary prevents importing /// across the crate. pub const AGENT_RUNTIME_ROOT: &str = "/run/hyperhive/agents"; /// Root of the local staging area for `btrfs send` archives /// (`SendAgentSnapshotToFile`). A sibling of `AGENT_STATE_ROOT`, not inside /// it — these are exported streams, not live/subvolume state, and don't /// belong in the tree btrfs quota accounting or the subvolume-per-agent /// layout cares about. Root-owned; hive-priv creates it on first use. pub const MIGRATE_STAGING_ROOT: &str = "/var/lib/hyperhive/migrate-staging"; /// Output format for `ReadContainerJournal`. Maps to journalctl /// `--output=<...>`. Restricted to the two formats hive callers use so /// the wire type can't smuggle an arbitrary `--output` value. #[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum JournalOutput { /// `short` — the journalctl default (syslog-style timestamps). #[default] Short, /// `short-iso` — ISO 8601 timestamps. ShortIso, } impl JournalOutput { /// The string journalctl expects after `--output=`. #[must_use] pub fn as_journalctl(self) -> &'static str { match self { JournalOutput::Short => "short", JournalOutput::ShortIso => "short-iso", } } } /// journalctl knobs for `ReadContainerJournal`. Grouped into one value so /// the read-journal call chain (`priv_client::read_container_journal` → /// hive-priv's executor) and its several hive-c0re callers pass a single /// struct instead of eight positional args that travelled together 1:1. /// `Default` is the common case (last N lines, short format, no filters); /// callers fill `lines` and override only the knobs they need via /// `..Default::default()`. #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct JournalQuery { /// `-n `. pub lines: u32, /// `-b` — restrict to the current boot. #[serde(default)] pub boot: bool, /// `--output=<...>`. #[serde(default)] pub output: JournalOutput, /// `-u `. #[serde(default)] pub unit: Option, /// `-p `. #[serde(default)] pub priority: Option, /// `--grep=`. #[serde(default)] pub grep: Option, /// `--since=`. #[serde(default)] pub since: Option, /// `--until=`. #[serde(default)] pub until: Option, } /// One bind-mount entry for `WriteNspawnFlags`. /// hive-priv constructs `--bind=:` (or `--bind-ro=`) /// and validates both paths before writing the conf file. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BindMount { pub host_path: String, pub container_path: String, pub read_only: bool, } /// One credential-forwarding entry for `WriteNspawnFlags`. hive-priv /// constructs `--load-credential=:` so systemd-nspawn /// loads the host secret file into the container's credential store; an /// inner unit then reads it via `LoadCredential=` (inherit form). /// The secret never lands in a bind mount, the nix store, or the /// generated config — only its host path (validated like a bind path) /// crosses the wire. Used for the hive-wide OTEL auth-header credential. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CredentialMount { /// systemd credential id (e.g. `otel-headers`); inner units inherit /// it by this name. Restricted to `[A-Za-z0-9_-]` (no `.`) by hive-priv. pub name: String, /// Host path to the secret file, forwarded via nspawn /// `--load-credential=:`. pub host_path: String, } /// Network isolation parameters for `WriteNspawnFlags`. When `Some`, /// hive-priv writes `PRIVATE_NETWORK=1` + veth bridge wiring instead /// of the default `PRIVATE_NETWORK=0`. Containers receive their IP /// dynamically via DHCP from the bridge dnsmasq pool (`networking.useDHCP` /// in `harness-base.nix`); no static address is pre-assigned here. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NetworkIsolation { /// Host bridge interface name (e.g. `hive0`). pub bridge: String, /// Bridge gateway IP (the host-side bridge address, e.g. `10.42.0.1`). /// Written as `HOST_ADDRESS=` in the nspawn conf so nixos-container's /// container-side setup installs a default route (`default via `) /// before DHCP completes: without it the container has no route off /// the bridge subnet until the DHCP lease arrives. The same IP runs the /// hive dnsmasq resolver, so it's also written into the container's /// `/etc/resolv.conf` (see the isolated-DNS oneshot in `harness-base.nix`, /// gated on the marker hive-priv drops). pub gateway_ip: String, } /// A request to the privileged helper. /// /// Wire format: one JSON object per line over `/run/hive/priv.sock`. /// Every variant is a specific known operation — no pass-through /// shell commands or arbitrary paths. New privileged ops get new /// variants. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "op", rename_all = "snake_case")] pub enum PrivRequest { // --- Container lifecycle --- /// `nixos-container start ` StartContainer { name: String }, /// `nixos-container stop ` StopContainer { name: String }, /// `machinectl kill --signal=SIGKILL` — force-kills all processes /// in the container. nixos-container has no kill verb. KillContainer { name: String }, /// `nixos-container update --flake ` /// The flake ref is derived from `name` by hive-priv. /// When `stream` is `true`, hive-priv sends `PrivEvent::Line` messages /// as the process runs, then a terminal `PrivEvent::Done`. UpdateContainer { name: String, #[serde(default)] stream: bool, }, /// `nixos-container create --flake ` /// The flake ref is derived from `name` by hive-priv. /// When `stream` is `true`, hive-priv sends `PrivEvent::Line` messages /// as the process runs, then a terminal `PrivEvent::Done`. CreateContainer { name: String, #[serde(default)] stream: bool, }, /// `nixos-container destroy ` DestroyContainer { name: String }, /// `nixos-container list` ListContainers, // --- Container journal reads --- /// Read a container's journal via `journalctl -M `. /// Requires root: the machine-bus transport enters the container's /// namespace, so this can't run from the unprivileged hive-c0re /// process. hive-priv validates `container` against the managed- /// container allowlist, then runs journalctl and returns its output. /// /// The filters (`unit` / `priority` / `grep` / `since` / `until`) /// are applied within the already-authorized machine and passed to /// journalctl as plain argument values; they can't widen access /// beyond the validated `container`. ReadContainerJournal { /// System container name (`h-` or a sibling service). container: String, /// journalctl knobs (see [`JournalQuery`]). query: JournalQuery, }, // --- Config file writes --- /// Update `/etc/nixos-containers/.conf`: strip old network-isolation /// vars, write `PRIVATE_NETWORK` + bridge settings, and set `EXTRA_NSPAWN_FLAGS` /// from the provided bind-mount list. Written by `lifecycle::set_nspawn_flags`. /// When `isolation` is `Some`, writes `PRIVATE_NETWORK=1` + veth wiring; /// when `None`, writes `PRIVATE_NETWORK=0`. WriteNspawnFlags { container: String, binds: Vec, /// `None` = host netns (`PRIVATE_NETWORK=0`). `Some` = private netns with /// veth on the specified bridge (`PRIVATE_NETWORK=1`). #[serde(default)] isolation: Option, /// Host secrets forwarded into the container's credential store via /// nspawn `--load-credential=:`. Empty for agents /// with no credentials configured (the common case). `#[serde(default)]` /// so a hive-priv built before this field deserialises new requests. #[serde(default)] load_credentials: Vec, }, /// Write `/run/systemd/system/container@.service.d/hyperhive-limits.conf` /// with `[Service]` carrying `MemoryMax=` / `CPUQuota=` (hard caps) and /// `CPUWeight=` / `IOWeight=` (cgroup v2 relative shares, contention-only). /// Written by `lifecycle::set_resource_limits`. WriteResourceLimits { container: String, memory_max: String, cpu_quota: String, /// cgroup v2 `cpu.weight`, 1..=10000. `None` means "not /// configured" — the writer omits the line entirely, leaving the /// kernel default. `#[serde(default)]` so a request from a /// hive-c0re built before this field existed deserialises to /// `None` and reproduces the pre-weights drop-in. #[serde(default)] cpu_weight: Option, /// cgroup v2 `io.weight`, 1..=10000. Same `None` = omit rule as /// `cpu_weight`. #[serde(default)] io_weight: Option, }, /// Remove `/run/systemd/system/container@.service.d/` if present. /// Called by `lifecycle::destroy` to clean up the resource-limits drop-in. RemoveServiceDropin { container: String }, // --- System --- /// Run `systemctl daemon-reload`. DaemonReload, /// Synchronise the nginx unit inside the `hive-gateway` container. /// /// 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` /// /// Requires root: `--machine=hive-gateway` enters the container /// namespace via the machine bus (forbidden for unprivileged users). ReloadGatewayNginx, // --- Socket dir ownership --- /// Set ownership of `/run/hive-agent//` to `uid:gid`. /// Called by `lifecycle::set_nspawn_flags` after `create_dir_all`. ChownSocketDir { agent_name: String, uid: u32, gid: u32, }, /// Set mode of `/run/hive-agent//`. /// Fallback when uid lookup returns `None` on first spawn. ChmodSocketDir { agent_name: String, mode: u32 }, // --- Forge admin CLI --- /// Run `forgejo admin ` inside the `hive-forge` container as the /// `forgejo` unix user. hive-priv executes: /// /// nixos-container run hive-forge -- runuser -u forgejo -- /// forgejo --work-path /var/lib/forgejo admin /// /// `args` must not contain null bytes, newlines, or shell metacharacters; /// hive-priv validates this before spawning the subprocess. /// /// This operation requires root (to nsenter into the forge container's /// namespaces); hive-c0re (which runs as `hive-core`) calls it through /// this route instead of spawning `nixos-container run` directly. RunForgeAdmin { /// Argument list appended after `forgejo --work-path /var/lib/forgejo admin`. /// Each element is a separate argv word — no shell expansion occurs. args: Vec, }, // --- Agent turn-loop pause --- // (marker filename: `PAUSED_MARKER_FILE`, defined at the crate root) /// Create (`paused: true`) or remove (`paused: false`) the pause marker /// at `AGENT_STATE_ROOT//harness/paused`. Its presence parks /// the agent's turn loop; the harness stats it in-container through the /// harness bind-mount. /// /// Required because hive-c0re runs unprivileged: the harness dir is /// chowned to the agent user on first container boot (mode 0755), so /// hive-core can stat the marker but cannot create or unlink it. Both /// directions are idempotent — pausing an already-paused agent rewrites /// an empty file, and resuming a running one treats `NotFound` as /// success. SetAgentPaused { /// Logical agent name (validated by `validate_agent_name`). agent_name: String, /// `true` creates the marker, `false` removes it. paused: bool, }, // --- Agent credential writes --- /// Write `forge-token` into `AGENT_STATE_ROOT//state/forge-token`. /// /// hive-priv validates `agent_name`, creates the state dir if absent, /// writes the file 0600, and chowns it to the state dir's owner so /// the agent process can read it. Required because hive-c0re runs /// unprivileged and cannot write to agent-owned state directories. WriteAgentForgeToken { /// Logical agent name (validated by `validate_agent_name`). agent_name: String, /// Token value. hive-priv appends a trailing newline before writing. token: String, }, /// Write a matrix access token into the agent's state dir. With /// `account: None` it targets the hive-internal `matrix-token`; with /// `account: Some(name)` it targets `matrix-token-` for an extra /// (external) account. hive-priv validates both `agent_name` and the /// `account` suffix as plain identifiers before building the path, so a /// crafted account name cannot traverse out of the state dir. /// /// Same write semantics as `WriteAgentForgeToken` — validates names, /// creates dir, writes 0600, chowns to agent owner. WriteAgentMatrixToken { /// Logical agent name (validated by `validate_agent_name`). agent_name: String, /// Token value. hive-priv appends a trailing newline before writing. token: String, /// Extra-account suffix. `None` → `matrix-token` (the hive account); /// `Some(name)` → `matrix-token-` (validated as a plain ident). account: Option, /// Homeserver URL for an extra account. When `Some` (only meaningful /// alongside `account: Some`), hive-priv also writes the sidecar /// `matrix-account-.json` (`{"homeserver": }`, 0600, /// chowned to the agent) so the daemon can auto-discover the account /// without a config declaration. `None` → no sidecar written. #[serde(default)] homeserver: Option, }, /// Write `github-token` into `AGENT_STATE_ROOT//state/github-token`. /// /// The operator-supplied GitHub personal access token (PAT) for the /// agent's GitHub integration (`hyperhive.github.enable`). Same write /// semantics as /// `WriteAgentForgeToken` — validates `agent_name`, creates the state dir /// if absent, writes the file 0600, and chowns it to the agent so the /// `gh` wrapper / git credential helper can read it. No account suffix /// (single GitHub account per agent). WriteAgentGithubToken { /// Logical agent name (validated by `validate_agent_name`). agent_name: String, /// PAT value. hive-priv appends a trailing newline before writing. token: String, }, /// Write a per-agent account for an external, dashboard-declared forge: /// the access token to /// `AGENT_STATE_ROOT//state/forge-