From 3421925442996ed37ff99b49041a3d86000b079d Mon Sep 17 00:00:00 2001 From: damocles Date: Sun, 2 Aug 2026 18:30:55 +0200 Subject: [PATCH] hivectl: add start/stop agent verbs, rename spawn to create --- CLAUDE.md | 7 ++--- docs/approvals.md | 6 ++--- docs/tools/hivectl-cli.md | 48 ++++++++++++++++++++++++---------- hive-host-sock/src/lib.rs | 22 ++++++++++++---- hivectl/README.md | 2 +- hivectl/src/agents.rs | 55 +++++++++++++++++++++++++++++++++++++-- hivectl/src/cli.rs | 22 +++++++++++----- 7 files changed, 127 insertions(+), 35 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a47d22b4..6da66e31 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -39,9 +39,10 @@ hand-maintained per-file tree drifts out of sync with the code. - **`hivectl/`** — standalone operator CLI (`hivectl` binary). Talks to the `hive-c0re` daemon over the host admin socket (`hive-host-sock` wire types) — does NOT link `hive-c0re`. Verbs: `list-agents`, - `agent `, `approvals `, `forge`/`matrix`/ - `github`/`gateway` provisioning, `stop`/`start`, `wg`/`peer-config`. + `agent `, `approvals `, + `forge`/`matrix`/`github`/`gateway` provisioning, `stop`/`start`, + `wg`/`peer-config`. - **`hive-agent/`**, **`hive-agent-mcp/`** — in-container harness, two sibling crates for every agent (not a single `hive-ag3nt/` dir — that's the runtime/binary-family nickname, diff --git a/docs/approvals.md b/docs/approvals.md index e5305769..0dacc7c4 100644 --- a/docs/approvals.md +++ b/docs/approvals.md @@ -119,10 +119,10 @@ kind-specific payload carrier. - `Spawn` — direct container creation from the agent's config repo. `commit_ref` is empty. Submitted via `HostRequest::RequestSpawn` (operator-gated, the `◆ R3QU3ST SP4WN` dashboard button + - `hivectl agent request-spawn` CLI). The host-level `HostRequest::Spawn` + `hivectl agent request-create` CLI). The host-level `HostRequest::Spawn` variant bypasses the approval queue entirely — privileged-context use - only (operator on the host shell, test scripts, one-off recoveries). - This is the **canonical first-spawn**: a new agent's `InitConfig` + only (operator on the host shell, test scripts, one-off recoveries; + `hivectl agent create`). This is the **canonical first-spawn**: a new agent's `InitConfig` seeds its config repo, the submitting agent customises it, then the operator spawns to create the container. Subsequent config changes go through a `MergeConfigPr` PR. diff --git a/docs/tools/hivectl-cli.md b/docs/tools/hivectl-cli.md index 26a407bf..4b48d834 100644 --- a/docs/tools/hivectl-cli.md +++ b/docs/tools/hivectl-cli.md @@ -24,8 +24,10 @@ This document contains the help content for the `hivectl` command-line program. * [`hivectl agent restart`↴](#hivectl-agent-restart) * [`hivectl agent pause`↴](#hivectl-agent-pause) * [`hivectl agent resume`↴](#hivectl-agent-resume) -* [`hivectl agent spawn`↴](#hivectl-agent-spawn) -* [`hivectl agent request-spawn`↴](#hivectl-agent-request-spawn) +* [`hivectl agent start`↴](#hivectl-agent-start) +* [`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 destroy`↴](#hivectl-agent-destroy) * [`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 * `pause` — Park this agent's turn loop, leaving the container running * `resume` — Resume this paused agent — it drains whatever queued up while parked -* `spawn` — Spawn this agent container directly, bypassing the approval queue -* `request-spawn` — Queue a spawn request for operator approval -* `kill` — Stop this managed container (graceful) +* `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 +* `create` — Create this agent container from scratch (full first-time provisioning), bypassing the approval queue +* `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 * `rebuild` — Apply pending config to this managed container * `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 spawn` +**Usage:** `hivectl agent start` -## `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` -Stop this managed container (graceful) +Hard-stop this managed container **Usage:** `hivectl agent kill` diff --git a/hive-host-sock/src/lib.rs b/hive-host-sock/src/lib.rs index 5a28902a..5eb9419b 100644 --- a/hive-host-sock/src/lib.rs +++ b/hive-host-sock/src/lib.rs @@ -99,14 +99,26 @@ pub enum ReconcileDirection { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "cmd", rename_all = "snake_case")] pub enum HostRequest { - /// Create and start a sub-agent container directly, bypassing the - /// approval queue. Privileged-context only. See - /// `docs/approvals.md::Approval kinds (wire shapes)`. + /// Create and start a brand-new sub-agent container directly (full + /// first-time provisioning: proposed/applied repos, state subvolume, + /// meta-flake sync, `nixos-container create`), bypassing the approval + /// queue. Privileged-context only. Exposed on the CLI as `hivectl + /// agent create`. See `docs/approvals.md::Approval kinds + /// (wire shapes)`. Wire name kept as `Spawn` (unrenamed underneath + /// the CLI-verb rename — `hivectl agent start` reuses the + /// existing scope-based [`HostRequest::Start`] below instead of a + /// new per-agent variant, see its doc comment). 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`). + /// Exposed on the CLI as `hivectl agent request-create`. RequestSpawn { name: Ident }, - /// Stop a managed container (graceful). + /// Hard stop a managed container. Exposed on the CLI as `hivectl + /// agent kill` — kept distinct from the new graceful-only + /// `hivectl agent 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 }, /// Tear down a sub-agent container, optionally purging state. /// See `docs/approvals.md::Destroy semantics`. diff --git a/hivectl/README.md b/hivectl/README.md index afbbde0e..2668e6ab 100644 --- a/hivectl/README.md +++ b/hivectl/README.md @@ -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 + 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. - **`dag_progress.rs`** — rebuild-queue progress rendering. - **`power.rs`** — restart/start/stop at the container level. diff --git a/hivectl/src/agents.rs b/hivectl/src/agents.rs index d4fd9943..e37f33b9 100644 --- a/hivectl/src/agents.rs +++ b/hivectl/src/agents.rs @@ -36,6 +36,55 @@ async fn agents_restart(socket: &Path, name: &str, no_wait: bool) -> Result<()> } } +/// `hivectl agent 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 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 /// daemon (`HostRequest::AgentStatus`) and render it as a padded table, /// 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::Pause => set_paused(socket, name, true).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)?; render(crate::client::request(socket, HostRequest::Spawn { name }).await?) } - AgentCmd::RequestSpawn => { + AgentCmd::RequestCreate => { let name = crate::util::parse_ident(name)?; render(crate::client::request(socket, HostRequest::RequestSpawn { name }).await?) } + AgentCmd::Stop => agents_stop(socket, name).await, AgentCmd::Kill => { let name = crate::util::parse_ident(name)?; render(crate::client::request(socket, HostRequest::Kill { name }).await?) diff --git a/hivectl/src/cli.rs b/hivectl/src/cli.rs index 6a619b7d..5bed6734 100644 --- a/hivectl/src/cli.rs +++ b/hivectl/src/cli.rs @@ -468,14 +468,22 @@ pub enum AgentCmd { Pause, /// Resume this paused agent — it drains whatever queued up while parked. 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 - /// spawn. - Spawn, - /// Queue a spawn request for operator approval. - RequestSpawn, - /// Stop this managed container (graceful). + /// Operator-on-the-host only; use `request-create` for an + /// approval-gated creation. + Create, + /// Queue a first-creation request for operator approval. + RequestCreate, + /// 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.