hyperhive/hive-sh4re/src/priv_proto.rs

309 lines
12 KiB
Rust

//! Wire types for the `hive-priv` privileged-helper socket.
//!
//! Both `hive-priv` (server) and `hive-c0re` (client via `priv_client`)
//! import these so the shapes stay in sync.
use serde::{Deserialize, Serialize};
/// Default socket path for the privileged helper.
pub const PRIV_SOCK: &str = "/run/hive/priv.sock";
/// 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-<agent_name>`.
pub const AGENT_PREFIX: &str = "h-";
/// Sibling service containers managed by hive-c0re.
pub const SIBLING_CONTAINERS: &[&str] = &["hive-forge", "hive-matrix", "hive-gateway", "hive-ci"];
/// Host path of the meta flake. The flake ref for agent `<name>` is
/// `{META_DIR}#{name}`, derived by `hive-priv` — never passed over the wire.
pub const META_DIR: &str = "/var/lib/hyperhive/meta";
/// Root of per-agent state directories on the host.
/// Subdirectory layout: `<AGENT_STATE_ROOT>/<name>/state/<file>`.
/// Used by `WriteAgentStateFile` to derive the write path so the
/// exact path is never passed over the wire.
pub const AGENT_STATE_ROOT: &str = "/var/lib/hyperhive/agents";
/// 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=`.
pub fn as_journalctl(self) -> &'static str {
match self {
JournalOutput::Short => "short",
JournalOutput::ShortIso => "short-iso",
}
}
}
/// One bind-mount entry for `WriteNspawnFlags`.
/// hive-priv constructs `--bind=<host_path>:<container_path>` (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,
}
/// Network isolation parameters for `WriteNspawnFlags`. When `Some`,
/// hive-priv writes `PRIVATE_NETWORK=1` + veth bridge wiring instead
/// of the default `PRIVATE_NETWORK=0`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetworkIsolation {
/// Static IP address to assign to this container on the bridge subnet.
pub agent_ip: String,
/// Host bridge interface name (e.g. `hive0`).
pub bridge: 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 <name>`
StartContainer { name: String },
/// `nixos-container stop <name>`
StopContainer { name: String },
/// `nixos-container kill <name>`
KillContainer { name: String },
/// `nixos-container update <name> --flake <flake_ref>`
/// 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 <name> --flake <flake_ref>`
/// 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 <name>`
DestroyContainer { name: String },
/// `nixos-container list`
ListContainers,
// --- Container journal reads ---
/// Read a container's journal via `journalctl -M <container>`.
/// 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-<agent>` or a sibling service).
container: String,
/// `-n <lines>`.
lines: u32,
/// `-b` — restrict to the current boot.
#[serde(default)]
boot: bool,
/// `--output=<...>`.
#[serde(default)]
output: JournalOutput,
/// `-u <unit>`.
#[serde(default)]
unit: Option<String>,
/// `-p <priority>`.
#[serde(default)]
priority: Option<String>,
/// `--grep=<regex>`.
#[serde(default)]
grep: Option<String>,
/// `--since=<ts>`.
#[serde(default)]
since: Option<String>,
/// `--until=<ts>`.
#[serde(default)]
until: Option<String>,
},
// --- Config file writes ---
/// Update `/etc/nixos-containers/<container>.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<BindMount>,
/// `None` = host netns (PRIVATE_NETWORK=0). `Some` = private netns with
/// veth on the specified bridge (PRIVATE_NETWORK=1).
#[serde(default)]
isolation: Option<NetworkIsolation>,
},
/// Write `/run/systemd/system/container@<container>.service.d/hyperhive-limits.conf`
/// with `[Service]\nMemoryMax=<memory_max>\nCPUQuota=<cpu_quota>\n`.
/// Written by `lifecycle::set_resource_limits`.
WriteResourceLimits {
container: String,
memory_max: String,
cpu_quota: String,
},
/// Remove `/run/systemd/system/container@<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/<agent_name>/` 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/<agent_name>/`.
/// Fallback when uid lookup returns `None` on first spawn.
ChmodSocketDir { agent_name: String, mode: u32 },
// --- Forge admin CLI ---
/// Run `forgejo admin <args>` 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>
///
/// `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<String>,
},
// --- Agent credential writes ---
/// Write `forge-token` into `AGENT_STATE_ROOT/<agent_name>/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 `matrix-token` into `AGENT_STATE_ROOT/<agent_name>/state/matrix-token`.
///
/// Same semantics as `WriteAgentForgeToken` — validates name, 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,
},
}
/// Response from the privileged helper.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PrivResponse {
pub ok: bool,
#[serde(default)]
pub stdout: String,
#[serde(default)]
pub stderr: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
// ---------------------------------------------------------------------------
// Streaming protocol
// ---------------------------------------------------------------------------
/// Which output stream a `PrivStreamLine` came from.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PrivStream {
Stdout,
Stderr,
}
/// A single line forwarded from a streaming privileged operation.
/// Wire shape: `{"stream":"stdout","data":"..."}` — distinct from
/// `PrivResponse` (which has `ok` but not `stream`/`data`) so that
/// `PrivEvent` can disambiguate with `#[serde(untagged)]`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PrivStreamLine {
pub stream: PrivStream,
pub data: String,
}
/// An event in the streaming protocol used by long-running priv ops.
///
/// Wire format (multiple JSON lines over one connection):
/// - Zero or more `Line` events as the subprocess runs.
/// - One terminal `Done` event carrying the final status.
///
/// Non-streaming ops (and old hive-priv) send exactly one `Done` line,
/// which is wire-identical to a bare `PrivResponse` — so old `call()`
/// callers that deserialise straight to `PrivResponse` continue to work
/// with new hive-priv's terminal event.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum PrivEvent {
/// A line of output from the running subprocess.
Line(PrivStreamLine),
/// Terminal event: the operation has finished.
Done(PrivResponse),
}