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
the `hive-c0re` daemon over the host admin socket (`hive-host-sock`
wire types) — does NOT link `hive-c0re`. Verbs: `list-agents`,
`agent <name> <spawn|kill|destroy|rebuild|restart|set-parent|choom|
watch|…>`, `approvals <pending|approve|deny>`, `forge`/`matrix`/
`github`/`gateway` provisioning, `stop`/`start`, `wg`/`peer-config`.
`agent <name> <start|create|request-create|stop|kill|destroy|rebuild|
restart|set-parent|choom|watch|…>`, `approvals <pending|approve|deny>`,
`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,

View file

@ -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 <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
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 <name> 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.

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 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`

View file

@ -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 <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 },
/// 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 <name> request-create`.
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 },
/// Tear down a sub-agent container, optionally purging state.
/// 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 +
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.

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
/// 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?)

View file

@ -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.