The forge and matrix are swarm services; per mara's ruling on #5475 (shape a) a hive no longer starts, stops or restarts them. - hive-priv-sock: InfraContainer keeps only Gateway; InfraTarget goes, service_unit maps the gateway to nginx.service directly. A ControlInfraContainer naming Forge/Matrix fails to decode. - hive-host-sock: LifecycleScope drops forge/matrix and refuses unknown fields, so an older hivectl's --forge is an error rather than an all-false scope meaning everything. - hive-c0re: scoped_infra selects only the gateway. - hivectl: --forge/--matrix removed from stop/start/restart. - docs: services.md says how the forge and matrix are managed; security.md, coordinator.md and hivectl-cli.md follow. - hive-host-sock, hivectl/power.rs: Stop and RestartScoped doc comments name the gateway (host nginx.service), not infra containers.
474 lines
18 KiB
Rust
474 lines
18 KiB
Rust
//! hivectl clap command tree: the `Cli` root, the top-level `Cmd`
|
|
//! verb enum, and every subcommand enum + shared args struct.
|
|
|
|
use clap::{Args, Parser, Subcommand};
|
|
use std::path::PathBuf;
|
|
|
|
#[derive(Parser)]
|
|
#[command(
|
|
name = "hivectl",
|
|
about = "hyperhive host CLI — operator-facing administration",
|
|
long_about = "\
|
|
Sibling to the `hive-c0re` daemon binary. Covers host-side admin \
|
|
operations that don't go through the broker — manual user \
|
|
provisioning on the bundled matrix container, plus future \
|
|
recovery / debugging verbs.\
|
|
"
|
|
)]
|
|
pub struct Cli {
|
|
/// Path to the hive-c0re host admin socket, used by the daemon-assisted
|
|
/// verbs (`agent`, `list-agents`, `stop`, `start`). Global: accepted
|
|
/// before or after the subcommand. Verbs that don't talk to the daemon
|
|
/// ignore it.
|
|
#[arg(long, global = true, default_value = DEFAULT_HOST_SOCKET)]
|
|
pub(crate) socket: PathBuf,
|
|
#[command(subcommand)]
|
|
pub(crate) cmd: Cmd,
|
|
}
|
|
|
|
#[derive(Subcommand)]
|
|
pub enum Cmd {
|
|
/// Reconcile an agent's config between this hive and the forge.
|
|
///
|
|
/// A human's first SSO login to the forge makes their forge
|
|
/// account.
|
|
Forge {
|
|
#[command(subcommand)]
|
|
cmd: ForgeCmd,
|
|
},
|
|
/// matrix-tuwunel invites.
|
|
///
|
|
/// Manual invites into the hive Space and its rooms; c0re's periodic
|
|
/// matrix sweep provisions the Space, the chat room and the agents'
|
|
/// invites on its own.
|
|
Matrix {
|
|
#[command(subcommand)]
|
|
cmd: MatrixCmd,
|
|
},
|
|
/// Lifecycle actions on ONE managed agent container. Needs the
|
|
/// hive-c0re daemon running.
|
|
///
|
|
/// Everything here targets a single named agent (`hivectl agent foo
|
|
/// restart`, `hivectl agent foo choom`, …) — anything that acts
|
|
/// hive-wide lives at the top level instead (`list-agents`,
|
|
/// `restart`/`stop`/`start` with a scope, `quota-enable`).
|
|
Agent {
|
|
/// Agent name (for example `damocles`, `iris`).
|
|
name: String,
|
|
#[command(subcommand)]
|
|
cmd: AgentCmd,
|
|
},
|
|
/// Show all managed agents with their status and technical state.
|
|
///
|
|
/// Global — not scoped to one agent, so it lives at the top level
|
|
/// rather than under `hivectl agent <name>`. Needs the hive-c0re
|
|
/// daemon running.
|
|
ListAgents {
|
|
/// Emit the raw JSON rows instead of the padded table (for
|
|
/// scripting). The table is the default human-readable shape.
|
|
#[arg(long)]
|
|
json: bool,
|
|
},
|
|
/// Enable btrfs qgroup accounting on the agent-state filesystem.
|
|
///
|
|
/// Global one-shot toggle (not per-agent), hence top-level rather than
|
|
/// under `hivectl agent <name>`. Idempotent — safe to re-run. Once
|
|
/// enabled, `hivectl agent <name> quota show`/`quota set` work.
|
|
QuotaEnable,
|
|
/// WireGuard inter-hive mesh setup helpers.
|
|
///
|
|
/// Generate this hive's mesh key and print the nix to enable the mesh,
|
|
/// add a peer, or inspect live interface state.
|
|
Wg {
|
|
#[command(subcommand)]
|
|
cmd: WgCmd,
|
|
},
|
|
/// Generate the federation peer-config block for THIS hive.
|
|
///
|
|
/// Prints the nix a peer operator pastes into their swarm config to
|
|
/// trust and reach this hive.
|
|
PeerConfig {
|
|
/// This hive's WireGuard mesh address (for example `10.42.0.1/32`),
|
|
/// emitted as `wireguardAddress`. Omit when not running the mesh.
|
|
#[arg(long)]
|
|
wg_address: Option<String>,
|
|
/// This hive's public WireGuard endpoint (`host:port`), emitted as
|
|
/// `wireguardEndpoint`. Omit when peers dial in / no mesh.
|
|
#[arg(long)]
|
|
wg_endpoint: Option<String>,
|
|
},
|
|
/// Stop containers hive-wide in one operator action.
|
|
///
|
|
/// Bare `hivectl stop` stops every agent and the gateway; scope flags
|
|
/// narrow it to specific sub-agents or the gateway. The swarm's forge
|
|
/// and matrix are never in scope: systemd on the services host runs
|
|
/// them.
|
|
Stop {
|
|
#[command(flatten)]
|
|
scope: ScopeArgs,
|
|
/// Gracefully quiesce each agent before stopping, instead of a
|
|
/// hard stop. Each agent gets a graceful-stop DAG on the job
|
|
/// queue: it signals the harness, which runs one stop-checkpoint
|
|
/// turn to flush durable `/state` and drains, then the DAG stops
|
|
/// the container (bounded by a 3-min timeout that falls back to a
|
|
/// hard stop). All drains overlap. Applies to agents only.
|
|
#[arg(long)]
|
|
graceful: bool,
|
|
/// Return immediately after hivectl queues the stop DAGs instead of
|
|
/// waiting for them with live per-node progress.
|
|
#[arg(long)]
|
|
no_wait: bool,
|
|
},
|
|
/// Start containers hive-wide — the inverse of `hivectl stop`.
|
|
///
|
|
/// Bare `hivectl start` restores the agents stopped by the last
|
|
/// broad-scope `stop` (or starts everything if none); scope flags
|
|
/// narrow it. The swarm's forge and matrix are never in scope.
|
|
Start {
|
|
#[command(flatten)]
|
|
scope: ScopeArgs,
|
|
/// Return immediately after hivectl queues the start DAGs instead
|
|
/// of waiting for them with live per-node progress.
|
|
#[arg(long)]
|
|
no_wait: bool,
|
|
},
|
|
/// Restart containers hive-wide — `stop` then `start` over one scope.
|
|
///
|
|
/// Bare `hivectl restart` restarts everything; scope flags narrow it.
|
|
/// The swarm's forge and matrix are never in scope.
|
|
Restart {
|
|
#[command(flatten)]
|
|
scope: ScopeArgs,
|
|
/// Gracefully quiesce each agent on the stop half (see
|
|
/// `stop --graceful`). Applies to agents only.
|
|
#[arg(long)]
|
|
graceful: bool,
|
|
},
|
|
/// Print (and best-effort open in a browser) a hive web surface URL.
|
|
///
|
|
/// Resolves the URL from the running daemon so custom forge / matrix
|
|
/// domains work. Bare `hivectl open` opens the operator dashboard.
|
|
Open {
|
|
/// Which surface to open. Defaults to the operator dashboard.
|
|
#[arg(value_enum, default_value_t = OpenTarget::Home)]
|
|
target: OpenTarget,
|
|
},
|
|
/// Emit the full CLI reference as `CommonMark` to stdout.
|
|
///
|
|
/// Hidden tooling command used by the docs build to keep the published
|
|
/// `hivectl` reference in lockstep with the code.
|
|
#[command(hide = true)]
|
|
MarkdownDocs,
|
|
/// Generate a shell completion script for `hivectl` and print it to
|
|
/// stdout.
|
|
///
|
|
/// Supports bash, zsh, fish, elvish, and powershell. The NixOS module
|
|
/// already installs the zsh script system-wide; this is for ad-hoc or
|
|
/// other-shell use.
|
|
Completions {
|
|
/// Shell to emit completions for.
|
|
shell: clap_complete::Shell,
|
|
},
|
|
}
|
|
|
|
/// Which hive web surface `hivectl open` targets.
|
|
#[derive(Copy, Clone, Debug, clap::ValueEnum)]
|
|
pub enum OpenTarget {
|
|
/// The operator dashboard (`https://<hive>.hives.<swarm domain>/`).
|
|
Home,
|
|
/// The forge (Forgejo) web UI.
|
|
Forge,
|
|
/// The matrix GUI (fluffychat).
|
|
Matrix,
|
|
}
|
|
|
|
/// Shared scope flags for `hivectl stop` / `start` / `restart`. With no
|
|
/// flag set the verb targets **everything** (all sub-agents + the
|
|
/// gateway). Setting any flag restricts to the selected classes,
|
|
/// additively.
|
|
#[derive(Args)]
|
|
pub struct ScopeArgs {
|
|
/// All sub-agent containers.
|
|
#[arg(long)]
|
|
agents: bool,
|
|
/// A specific sub-agent by name. Repeatable: `--agent a --agent b`.
|
|
#[arg(long = "agent", value_name = "NAME")]
|
|
agent: Vec<String>,
|
|
/// The gateway (`hive-gateway`) — nginx on the host, not a container.
|
|
#[arg(long)]
|
|
gateway: bool,
|
|
}
|
|
|
|
impl ScopeArgs {
|
|
pub(crate) fn to_scope(&self) -> hive_host_sock::LifecycleScope {
|
|
hive_host_sock::LifecycleScope {
|
|
agents: self.agents,
|
|
agent_names: self.agent.clone(),
|
|
gateway: self.gateway,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Subcommand)]
|
|
pub enum ForgeCmd {
|
|
/// Show + reconcile the divergence between an agent's local applied
|
|
/// config checkout and its forge `agent-configs/<agent>` main.
|
|
///
|
|
/// Always prints the diff first. `--from forge` resets the local
|
|
/// checkout to forge main (effective on the next deploy); `--from local`
|
|
/// isn't supported yet. With no `--from`, prompts for the direction.
|
|
ReconcileConfig {
|
|
/// Agent whose config branches to reconcile.
|
|
agent: String,
|
|
/// Which side to reconcile from. Omit it and hivectl prompts you after the diff.
|
|
#[arg(long, value_enum)]
|
|
from: Option<ReconcileFrom>,
|
|
/// Include the full diff (not just `--stat`) in the report.
|
|
#[arg(long)]
|
|
verbose: bool,
|
|
},
|
|
}
|
|
|
|
/// Which side to reconcile config branches from (`hivectl forge
|
|
/// reconcile-config --from`). Maps to [`hive_host_sock::ReconcileDirection`].
|
|
#[derive(Clone, Copy, clap::ValueEnum)]
|
|
pub enum ReconcileFrom {
|
|
/// Reset the local applied checkout to forge main.
|
|
Forge,
|
|
/// Advance forge main from local — not supported yet.
|
|
Local,
|
|
}
|
|
|
|
impl From<ReconcileFrom> for hive_host_sock::ReconcileDirection {
|
|
fn from(from: ReconcileFrom) -> Self {
|
|
match from {
|
|
ReconcileFrom::Forge => Self::Forge,
|
|
ReconcileFrom::Local => Self::Local,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Subcommand)]
|
|
pub enum MatrixCmd {
|
|
/// Invite a matrix user to the hive Space, or a specific room with
|
|
/// `--room`. Idempotent.
|
|
Invite {
|
|
/// User to invite: a full id (`@mara:server`) or a bare
|
|
/// localpart (qualified with the homeserver's `server_name`).
|
|
user: String,
|
|
/// Target room id (`!abc:server`) or alias (`#name:server`).
|
|
/// Omit to invite to the hive Space.
|
|
#[arg(long)]
|
|
room: Option<String>,
|
|
},
|
|
}
|
|
|
|
#[derive(Subcommand)]
|
|
pub enum WgCmd {
|
|
/// Generate this hive's WireGuard key (if absent) and print its public
|
|
/// key plus the nix to enable the mesh.
|
|
Init {
|
|
/// This hive's mesh address (for example `10.42.0.1/32`) to bake into the
|
|
/// printed snippet. Omit to get a placeholder you fill in.
|
|
#[arg(long)]
|
|
address: Option<String>,
|
|
},
|
|
/// Print the nix to add a peer hive to the mesh.
|
|
Peer {
|
|
/// Peer hive's DNS domain (its `swarm.hives` entry's `domain`)
|
|
domain: String,
|
|
/// Peer's WireGuard public key (from its `hivectl wg init`).
|
|
#[arg(long)]
|
|
pubkey: String,
|
|
/// Peer's mesh address (for example `10.42.0.2/32`).
|
|
#[arg(long)]
|
|
address: String,
|
|
/// Peer's `host:port` endpoint (omit for a peer that only dials out,
|
|
/// for example one behind NAT — it must set an endpoint pointing back here).
|
|
#[arg(long)]
|
|
endpoint: Option<String>,
|
|
},
|
|
/// Show the live mesh interface state.
|
|
Status,
|
|
}
|
|
|
|
// Default host admin socket path. Shared with `hive-c0re`'s `main.rs`
|
|
// default via `hive_host_sock::HOST_SOCKET` — the daemon binds there
|
|
// and `hivectl` connects to it.
|
|
pub(crate) use hive_host_sock::HOST_SOCKET as DEFAULT_HOST_SOCKET;
|
|
|
|
/// Verbs under `hivectl agent <name> <verb>` — every one of these targets
|
|
/// the single agent named on the parent command, so none of them carry
|
|
/// their own `name` field.
|
|
#[derive(Subcommand)]
|
|
pub enum AgentCmd {
|
|
/// Stop and start this agent container without rebuilding config.
|
|
Restart {
|
|
/// Return immediately after hivectl queues the restart DAG.
|
|
#[arg(long)]
|
|
no_wait: bool,
|
|
},
|
|
/// Park this agent's turn loop, leaving the container running.
|
|
///
|
|
/// The harness stops driving turns but keeps serving its web UI and
|
|
/// MCP daemons, so the container, its mounts and its warm caches stay
|
|
/// up while it burns no tokens. Inbox messages queue unacked and the
|
|
/// backlog drains on `resume`. Sticky: it survives a restart, and
|
|
/// pausing a stopped agent makes it come up paused.
|
|
Pause {
|
|
/// Return immediately after hivectl queues the pause DAG instead of
|
|
/// waiting for the harness to acknowledge it.
|
|
#[arg(long)]
|
|
no_wait: bool,
|
|
},
|
|
/// Resume this paused agent — it drains whatever queued up while parked.
|
|
Resume,
|
|
/// Start this EXISTING agent container. Fails immediately if `name`
|
|
/// has no config/topology entry at all — it never attempts
|
|
/// first-time creation, which is swarm-level (`swarmctl agent create`).
|
|
Start {
|
|
/// Start (or leave) the agent paused: if it's currently down, hivectl
|
|
/// writes the pause marker before the container boots, so it comes
|
|
/// up paused instead of driving turns immediately. If it's already
|
|
/// running, this pauses it in place and doesn't attempt a
|
|
/// (re)start.
|
|
#[arg(long)]
|
|
paused: bool,
|
|
},
|
|
/// Gracefully stop this agent container: signal → drain → reconcile.
|
|
/// Never escalates to a hard kill — use `kill` for that.
|
|
Stop,
|
|
/// Hard-stop this managed container.
|
|
Kill,
|
|
/// Tear down this sub-agent container, keeping its state by default.
|
|
/// No undo.
|
|
Destroy {
|
|
/// Also wipe the agent's state dirs (config + creds + notes).
|
|
#[arg(long)]
|
|
purge: bool,
|
|
},
|
|
/// Apply pending config to this managed container.
|
|
Rebuild,
|
|
/// Open an interactive Claude session inside this agent's container.
|
|
///
|
|
/// A fresh session by default, or resume a prior one. Requires root
|
|
/// or `hive-admin` group membership, plus a running container.
|
|
Choom {
|
|
/// Resume a prior claude session by its session id, passed
|
|
/// through as `claude --resume <value>` (claude's `--continue`
|
|
/// takes no value — it resumes the cwd's latest session, which
|
|
/// is the harness's, so choom never uses it; this flag matches
|
|
/// the claude flag it maps to). Omit for a fresh blank session.
|
|
/// Requires a value.
|
|
#[arg(long = "resume", value_name = "SESSION")]
|
|
resume_session: Option<String>,
|
|
},
|
|
/// Follow this agent's live turn/tool-call event stream from the CLI.
|
|
///
|
|
/// Prints one compact line per event as they happen. `Ctrl-C` to stop.
|
|
/// Requires the agent to be running.
|
|
Watch,
|
|
/// This agent's disk accounting + optional quota via btrfs qgroups.
|
|
///
|
|
/// Needs `hivectl quota-enable` run once hive-wide first. No-op on
|
|
/// non-btrfs hosts.
|
|
Quota {
|
|
#[command(subcommand)]
|
|
cmd: AgentQuotaCmd,
|
|
},
|
|
/// btrfs subvolume management for this agent's state dir.
|
|
///
|
|
/// Upgrade an existing plain-dir agent's state into a btrfs subvolume
|
|
/// so it gains snapshots and per-subvol usage/quota.
|
|
Subvol {
|
|
#[command(subcommand)]
|
|
cmd: SubvolCmd,
|
|
},
|
|
}
|
|
|
|
#[derive(Subcommand)]
|
|
pub enum AgentQuotaCmd {
|
|
/// Report this agent's disk usage from btrfs qgroups.
|
|
Show,
|
|
/// Set or clear this agent's disk-usage quota.
|
|
Set {
|
|
/// Size cap (`5G`, `500M`, `1073741824`) or `none` to clear.
|
|
size: String,
|
|
},
|
|
}
|
|
|
|
#[derive(Subcommand)]
|
|
pub enum SubvolCmd {
|
|
/// Convert a plain-dir agent state root into a btrfs subvolume in
|
|
/// place, so it gains snapshots and per-subvol usage/quota.
|
|
///
|
|
/// Bounces the agent to migrate its state, so it requires `--yes`.
|
|
Upgrade {
|
|
/// Confirm: this stops the agent, migrates its state dir, and
|
|
/// restarts it. Required — the command refuses without it.
|
|
#[arg(long)]
|
|
yes: bool,
|
|
},
|
|
/// Read-only snapshots of this agent's state subvolume.
|
|
Snapshot {
|
|
#[command(subcommand)]
|
|
cmd: SnapshotCmd,
|
|
},
|
|
}
|
|
|
|
#[derive(Subcommand)]
|
|
pub enum SnapshotCmd {
|
|
/// Create a read-only snapshot (agent must already be a subvolume).
|
|
Create {
|
|
/// Snapshot label. Mandatory, and must start with `hive-` — the
|
|
/// prefix doubles as an allow-list hive-priv checks so only
|
|
/// hivectl-issued snapshot names can reach the `btrfs subvolume
|
|
/// snapshot` shellout.
|
|
#[arg(long)]
|
|
label: String,
|
|
},
|
|
/// Delete a snapshot created by `subvol snapshot create`.
|
|
Delete {
|
|
/// Snapshot label passed to `subvol snapshot create --label`.
|
|
label: String,
|
|
},
|
|
/// Export a snapshot to a local file via `btrfs send` — the
|
|
/// local-file half of the inter-hive migration transport (`push`
|
|
/// is the network half). Also useful standalone as a point-in-time
|
|
/// backup: a full send with no `--parent` produces a
|
|
/// self-contained archive of the snapshot.
|
|
Send {
|
|
/// Snapshot label passed to `subvol snapshot create --label`.
|
|
label: String,
|
|
/// Optional parent snapshot label for an incremental send
|
|
/// (`btrfs send -p`) — must be an existing, older snapshot of the
|
|
/// same agent. Omit for a full send.
|
|
#[arg(long)]
|
|
parent: Option<String>,
|
|
/// Destination filename (not a path) under the migrate-staging
|
|
/// dir. Refused if it already exists.
|
|
#[arg(long)]
|
|
dest: String,
|
|
},
|
|
/// Stream a snapshot to the swarm's snapshot store over the
|
|
/// WireGuard mesh — the network half of the migration transport.
|
|
///
|
|
/// Stages nothing locally: `btrfs send` writes straight into the
|
|
/// connection, so a multi-gigabyte agent needs no scratch space on
|
|
/// this host. The mesh is the authentication (cryptokey routing
|
|
/// binds the sender's address to its key), so there is no credential
|
|
/// to pass here.
|
|
///
|
|
/// No destination argument exists: a swarm has one store, read
|
|
/// from `services.hyperhive.swarm.snapshotStore`.
|
|
Push {
|
|
/// Snapshot label passed to `subvol snapshot create --label`.
|
|
label: String,
|
|
/// Optional parent snapshot label for an incremental send
|
|
/// (`btrfs send -p`) — must be an existing, older snapshot of the
|
|
/// same agent, and must already be present on the receiver.
|
|
/// Omit for a full send.
|
|
#[arg(long)]
|
|
parent: Option<String>,
|
|
},
|
|
}
|