hivectl: add start/stop agent verbs, rename spawn to create

This commit is contained in:
damocles 2026-08-02 18:30:55 +02:00 committed by mara
commit 3421925442
7 changed files with 127 additions and 35 deletions

View file

@ -39,9 +39,10 @@ hand-maintained per-file tree drifts out of sync with the code.
- **`hivectl/`** — standalone operator CLI (`hivectl` binary). Talks to - **`hivectl/`** — standalone operator CLI (`hivectl` binary). Talks to
the `hive-c0re` daemon over the host admin socket (`hive-host-sock` the `hive-c0re` daemon over the host admin socket (`hive-host-sock`
wire types) — does NOT link `hive-c0re`. Verbs: `list-agents`, wire types) — does NOT link `hive-c0re`. Verbs: `list-agents`,
`agent <name> <spawn|kill|destroy|rebuild|restart|set-parent|choom| `agent <name> <start|create|request-create|stop|kill|destroy|rebuild|
watch|…>`, `approvals <pending|approve|deny>`, `forge`/`matrix`/ restart|set-parent|choom|watch|…>`, `approvals <pending|approve|deny>`,
`github`/`gateway` provisioning, `stop`/`start`, `wg`/`peer-config`. `forge`/`matrix`/`github`/`gateway` provisioning, `stop`/`start`,
`wg`/`peer-config`.
- **`hive-agent/`**, **`hive-agent-mcp/`** — - **`hive-agent/`**, **`hive-agent-mcp/`** —
in-container harness, two sibling crates for every agent (not a in-container harness, two sibling crates for every agent (not a
single `hive-ag3nt/` dir — that's the runtime/binary-family nickname, single `hive-ag3nt/` dir — that's the runtime/binary-family nickname,

View file

@ -119,10 +119,10 @@ kind-specific payload carrier.
- `Spawn` — direct container creation from the agent's config repo. - `Spawn` — direct container creation from the agent's config repo.
`commit_ref` is empty. Submitted via `HostRequest::RequestSpawn` `commit_ref` is empty. Submitted via `HostRequest::RequestSpawn`
(operator-gated, the `◆ R3QU3ST SP4WN` dashboard button + (operator-gated, the `◆ R3QU3ST SP4WN` dashboard button +
`hivectl agent <name> request-spawn` CLI). The host-level `HostRequest::Spawn` `hivectl agent <name> request-create` CLI). The host-level `HostRequest::Spawn`
variant bypasses the approval queue entirely — privileged-context use variant bypasses the approval queue entirely — privileged-context use
only (operator on the host shell, test scripts, one-off recoveries). only (operator on the host shell, test scripts, one-off recoveries;
This is the **canonical first-spawn**: a new agent's `InitConfig` `hivectl agent <name> create`). This is the **canonical first-spawn**: a new agent's `InitConfig`
seeds its config repo, the submitting agent customises it, then the seeds its config repo, the submitting agent customises it, then the
operator spawns to create the container. Subsequent config changes go operator spawns to create the container. Subsequent config changes go
through a `MergeConfigPr` PR. through a `MergeConfigPr` PR.

View file

@ -24,8 +24,10 @@ This document contains the help content for the `hivectl` command-line program.
* [`hivectl agent restart`↴](#hivectl-agent-restart) * [`hivectl agent restart`↴](#hivectl-agent-restart)
* [`hivectl agent pause`↴](#hivectl-agent-pause) * [`hivectl agent pause`↴](#hivectl-agent-pause)
* [`hivectl agent resume`↴](#hivectl-agent-resume) * [`hivectl agent resume`↴](#hivectl-agent-resume)
* [`hivectl agent spawn`↴](#hivectl-agent-spawn) * [`hivectl agent start`↴](#hivectl-agent-start)
* [`hivectl agent request-spawn`↴](#hivectl-agent-request-spawn) * [`hivectl agent create`↴](#hivectl-agent-create)
* [`hivectl agent request-create`↴](#hivectl-agent-request-create)
* [`hivectl agent stop`↴](#hivectl-agent-stop)
* [`hivectl agent kill`↴](#hivectl-agent-kill) * [`hivectl agent kill`↴](#hivectl-agent-kill)
* [`hivectl agent destroy`↴](#hivectl-agent-destroy) * [`hivectl agent destroy`↴](#hivectl-agent-destroy)
* [`hivectl agent rebuild`↴](#hivectl-agent-rebuild) * [`hivectl agent rebuild`↴](#hivectl-agent-rebuild)
@ -342,9 +344,11 @@ Everything here targets a single named agent (`hivectl agent foo restart`, `hive
* `restart` — Stop and start this agent container without rebuilding config * `restart` — Stop and start this agent container without rebuilding config
* `pause` — Park this agent's turn loop, leaving the container running * `pause` — Park this agent's turn loop, leaving the container running
* `resume` — Resume this paused agent — it drains whatever queued up while parked * `resume` — Resume this paused agent — it drains whatever queued up while parked
* `spawn` — Spawn this agent container directly, bypassing the approval queue * `start` — Start this EXISTING agent container. Fails immediately if `name` has no config/topology entry at all — it never attempts first-time creation. Use `create` for that
* `request-spawn` — Queue a spawn request for operator approval * `create` — Create this agent container from scratch (full first-time provisioning), bypassing the approval queue
* `kill` — Stop this managed container (graceful) * `request-create` — Queue a first-creation request for operator approval
* `stop` — Gracefully stop this agent container: signal → drain → reconcile. Never escalates to a hard kill — use `kill` for that
* `kill` — Hard-stop this managed container
* `destroy` — Tear down this sub-agent container, keeping its state by default. No undo * `destroy` — Tear down this sub-agent container, keeping its state by default. No undo
* `rebuild` — Apply pending config to this managed container * `rebuild` — Apply pending config to this managed container
* `set-parent` — Move this agent in the topology tree — under a new parent, or to root * `set-parent` — Move this agent in the topology tree — under a new parent, or to root
@ -390,27 +394,43 @@ Resume this paused agent — it drains whatever queued up while parked
## `hivectl agent spawn` ## `hivectl agent start`
Spawn this agent container directly, bypassing the approval queue. Start this EXISTING agent container. Fails immediately if `name` has no config/topology entry at all — it never attempts first-time creation. Use `create` for that
Operator-on-the-host only; use `request-spawn` for an approval-gated spawn. **Usage:** `hivectl agent start`
**Usage:** `hivectl agent spawn`
## `hivectl agent request-spawn` ## `hivectl agent create`
Queue a spawn request for operator approval Create this agent container from scratch (full first-time provisioning), bypassing the approval queue.
**Usage:** `hivectl agent request-spawn` Operator-on-the-host only; use `request-create` for an approval-gated creation.
**Usage:** `hivectl agent create`
## `hivectl agent request-create`
Queue a first-creation request for operator approval
**Usage:** `hivectl agent request-create`
## `hivectl agent stop`
Gracefully stop this agent container: signal → drain → reconcile. Never escalates to a hard kill — use `kill` for that
**Usage:** `hivectl agent stop`
## `hivectl agent kill` ## `hivectl agent kill`
Stop this managed container (graceful) Hard-stop this managed container
**Usage:** `hivectl agent kill` **Usage:** `hivectl agent kill`

View file

@ -99,14 +99,26 @@ pub enum ReconcileDirection {
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "cmd", rename_all = "snake_case")] #[serde(tag = "cmd", rename_all = "snake_case")]
pub enum HostRequest { pub enum HostRequest {
/// Create and start a sub-agent container directly, bypassing the /// Create and start a brand-new sub-agent container directly (full
/// approval queue. Privileged-context only. See /// first-time provisioning: proposed/applied repos, state subvolume,
/// `docs/approvals.md::Approval kinds (wire shapes)`. /// meta-flake sync, `nixos-container create`), bypassing the approval
/// queue. Privileged-context only. Exposed on the CLI as `hivectl
/// agent <name> create`. See `docs/approvals.md::Approval kinds
/// (wire shapes)`. Wire name kept as `Spawn` (unrenamed underneath
/// the CLI-verb rename — `hivectl agent <name> start` reuses the
/// existing scope-based [`HostRequest::Start`] below instead of a
/// new per-agent variant, see its doc comment).
Spawn { name: Ident }, Spawn { name: Ident },
/// Submit a spawn request for the operator to approve. See /// Submit a first-creation request for the operator to approve. See
/// `docs/approvals.md::Approval kinds (wire shapes)` (`Spawn`). /// `docs/approvals.md::Approval kinds (wire shapes)` (`Spawn`).
/// Exposed on the CLI as `hivectl agent <name> request-create`.
RequestSpawn { name: Ident }, RequestSpawn { name: Ident },
/// Stop a managed container (graceful). /// Hard stop a managed container. Exposed on the CLI as `hivectl
/// agent <name> kill` — kept distinct from the new graceful-only
/// `hivectl agent <name> stop` (which reuses the scope-based
/// [`HostRequest::Stop`] below with `graceful: true`); `Kill`
/// escalates to a hard stop, `Stop` never does. Wire name kept as
/// `Kill`.
Kill { name: Ident }, Kill { name: Ident },
/// Tear down a sub-agent container, optionally purging state. /// Tear down a sub-agent container, optionally purging state.
/// See `docs/approvals.md::Destroy semantics`. /// See `docs/approvals.md::Destroy semantics`.

View file

@ -19,7 +19,7 @@ talk to the daemon over their own MCP tool surface
One module per subcommand family; `main.rs` is just the clap parse + One module per subcommand family; `main.rs` is just the clap parse +
dispatch: dispatch:
- **`agents.rs`** — container lifecycle (spawn/kill/rebuild/restart/…). - **`agents.rs`** — container lifecycle (start/stop/create/kill/rebuild/restart/…).
- **`approvals.rs`** — the config/init-config/meta-input approval queue. - **`approvals.rs`** — the config/init-config/meta-input approval queue.
- **`dag_progress.rs`** — rebuild-queue progress rendering. - **`dag_progress.rs`** — rebuild-queue progress rendering.
- **`power.rs`** — restart/start/stop at the container level. - **`power.rs`** — restart/start/stop at the container level.

View file

@ -36,6 +36,55 @@ async fn agents_restart(socket: &Path, name: &str, no_wait: bool) -> Result<()>
} }
} }
/// `hivectl agent <name> start` — start an EXISTING agent container.
/// Fails immediately (no request even sent) if `name` has no state dir
/// at all, rather than silently resolving to an empty scope. Reuses the
/// exact hive-wide `hivectl start` DAG (`HostRequest::Start`'s
/// scope-based path), just scoped to this one name — a separate
/// per-agent wire request would have duplicated logic this scope-based
/// one already covers.
async fn agents_start(socket: &Path, name: &str) -> Result<()> {
if !crate::util::agent_exists(socket, name).await? {
bail!(
"no such agent: '{name}' (no state dir under {}/) — use 'hivectl agent {name} create' to provision a brand-new agent",
hive_host_sock::AGENTS_ROOT
);
}
render(
crate::client::request(
socket,
HostRequest::Start {
scope: hive_host_sock::LifecycleScope {
agent_names: vec![name.to_owned()],
..Default::default()
},
},
)
.await?,
)
}
/// `hivectl agent <name> stop` — graceful-only stop (signal → drain →
/// reconcile), never escalating to a hard kill. Reuses the hive-wide
/// `hivectl stop --graceful` DAG (`HostRequest::Stop`'s scope-based
/// path), scoped to this one name. Distinct from `AgentCmd::Kill`, which
/// hard-stops via a separate DAG that does escalate.
async fn agents_stop(socket: &Path, name: &str) -> Result<()> {
render(
crate::client::request(
socket,
HostRequest::Stop {
scope: hive_host_sock::LifecycleScope {
agent_names: vec![name.to_owned()],
..Default::default()
},
graceful: true,
},
)
.await?,
)
}
/// `hivectl list-agents` — fetch the per-agent status roster from the /// `hivectl list-agents` — fetch the per-agent status roster from the
/// daemon (`HostRequest::AgentStatus`) and render it as a padded table, /// daemon (`HostRequest::AgentStatus`) and render it as a padded table,
/// or the raw JSON rows with `--json`. Reuses the dashboard's /// or the raw JSON rows with `--json`. Reuses the dashboard's
@ -149,14 +198,16 @@ pub(crate) async fn run_agent(socket: &Path, name: &str, cmd: AgentCmd) -> Resul
AgentCmd::Restart { no_wait } => agents_restart(socket, name, no_wait).await, AgentCmd::Restart { no_wait } => agents_restart(socket, name, no_wait).await,
AgentCmd::Pause => set_paused(socket, name, true).await, AgentCmd::Pause => set_paused(socket, name, true).await,
AgentCmd::Resume => set_paused(socket, name, false).await, AgentCmd::Resume => set_paused(socket, name, false).await,
AgentCmd::Spawn => { AgentCmd::Start => agents_start(socket, name).await,
AgentCmd::Create => {
let name = crate::util::parse_ident(name)?; let name = crate::util::parse_ident(name)?;
render(crate::client::request(socket, HostRequest::Spawn { name }).await?) render(crate::client::request(socket, HostRequest::Spawn { name }).await?)
} }
AgentCmd::RequestSpawn => { AgentCmd::RequestCreate => {
let name = crate::util::parse_ident(name)?; let name = crate::util::parse_ident(name)?;
render(crate::client::request(socket, HostRequest::RequestSpawn { name }).await?) render(crate::client::request(socket, HostRequest::RequestSpawn { name }).await?)
} }
AgentCmd::Stop => agents_stop(socket, name).await,
AgentCmd::Kill => { AgentCmd::Kill => {
let name = crate::util::parse_ident(name)?; let name = crate::util::parse_ident(name)?;
render(crate::client::request(socket, HostRequest::Kill { name }).await?) render(crate::client::request(socket, HostRequest::Kill { name }).await?)

View file

@ -468,14 +468,22 @@ pub enum AgentCmd {
Pause, Pause,
/// Resume this paused agent — it drains whatever queued up while parked. /// Resume this paused agent — it drains whatever queued up while parked.
Resume, Resume,
/// Spawn this agent container directly, bypassing the approval queue. /// Start this EXISTING agent container. Fails immediately if `name`
/// has no config/topology entry at all — it never attempts
/// first-time creation. Use `create` for that.
Start,
/// Create this agent container from scratch (full first-time
/// provisioning), bypassing the approval queue.
/// ///
/// Operator-on-the-host only; use `request-spawn` for an approval-gated /// Operator-on-the-host only; use `request-create` for an
/// spawn. /// approval-gated creation.
Spawn, Create,
/// Queue a spawn request for operator approval. /// Queue a first-creation request for operator approval.
RequestSpawn, RequestCreate,
/// Stop this managed container (graceful). /// 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, Kill,
/// Tear down this sub-agent container, keeping its state by default. /// Tear down this sub-agent container, keeping its state by default.
/// No undo. /// No undo.