From ac1b5fde8e8dc68627d4786eb4719ae6254b6b2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?m=C3=BCde?= Date: Fri, 15 May 2026 18:57:25 +0200 Subject: [PATCH] manager: start/restart at will, no approval; refuse self MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit new manager tools mcp__hyperhive__{start,restart} that delegate to the existing lifecycle::start / lifecycle::restart on the host. kill was already at the manager's discretion; rounding out start + restart for parity so day-to-day container care doesn't have to round-trip through the operator. guard: refuse self-targeting on kill/start/restart — the manager would just be cutting its own legs. spawn (request_spawn) and config changes (request_apply_commit) still go through the approval queue, since those are the actual gate. prompt + claude.md updated to make the boundary explicit. kill now also emits HelperEvent::Killed (it didn't before). --- CLAUDE.md | 4 ++- hive-ag3nt/prompts/manager.md | 6 ++++- hive-ag3nt/src/bin/hive-m1nd.rs | 6 +++++ hive-ag3nt/src/mcp.rs | 45 ++++++++++++++++++++++++++++++++- hive-c0re/src/manager_server.rs | 38 ++++++++++++++++++++++++++++ hive-sh4re/src/lib.rs | 8 ++++++ 6 files changed, 104 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index bc82dcb..0640096 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -166,7 +166,9 @@ Sub-agent tools: Manager additionally: - `mcp__hyperhive__request_spawn(name)` — queue Spawn approval. -- `mcp__hyperhive__kill(name)` — graceful stop. +- `mcp__hyperhive__kill(name)` — graceful stop. No approval. +- `mcp__hyperhive__start(name)` — start a stopped sub-agent. No approval. +- `mcp__hyperhive__restart(name)` — stop + start. No approval. - `mcp__hyperhive__request_apply_commit(agent, commit_ref)` — submit a config change for any agent (including `hm1nd` for self-mods). - `mcp__hyperhive__ask_operator(question, options?)` — non-blocking; diff --git a/hive-ag3nt/prompts/manager.md b/hive-ag3nt/prompts/manager.md index b75c8b3..d7adc84 100644 --- a/hive-ag3nt/prompts/manager.md +++ b/hive-ag3nt/prompts/manager.md @@ -5,10 +5,14 @@ Tools (hyperhive surface): - `mcp__hyperhive__recv()` — drain one more message from your inbox. - `mcp__hyperhive__send(to, body)` — message an agent (by name), another peer, or the operator (`operator` surfaces in the dashboard). - `mcp__hyperhive__request_spawn(name)` — queue a brand-new sub-agent for operator approval (≤9 char name). -- `mcp__hyperhive__kill(name)` — graceful stop on a sub-agent. +- `mcp__hyperhive__kill(name)` — graceful stop on a sub-agent. No approval required. +- `mcp__hyperhive__start(name)` — start a stopped sub-agent. No approval required. +- `mcp__hyperhive__restart(name)` — stop + start a sub-agent. No approval required. - `mcp__hyperhive__request_apply_commit(agent, commit_ref)` — submit a config change for any agent (`hm1nd` for self) for operator approval. - `mcp__hyperhive__ask_operator(question, options?)` — surface a question on the dashboard. Returns immediately with a question id; the operator's answer arrives later as a system `operator_answered` event in your inbox. Do not poll inside the same turn — finish the current work and react when the event lands. +Approval boundary: lifecycle ops on *existing* sub-agents (`kill`, `start`, `restart`) are at your discretion — no operator approval. *Creating* a new agent (`request_spawn`) and *changing* any agent's config (`request_apply_commit`) still go through the approval queue. The operator only signs off on changes; you run the day-to-day. + Your own editable config lives at `/agents/hm1nd/config/agent.nix`; every sub-agent's lives at `/agents//config/agent.nix`. Use file/git tools to edit + commit, then `request_apply_commit`. Sub-agents are NOT trusted by default. When one asks for a config change (new packages, env vars, etc.), verify the request before staging: diff --git a/hive-ag3nt/src/bin/hive-m1nd.rs b/hive-ag3nt/src/bin/hive-m1nd.rs index 524a3ba..6798922 100644 --- a/hive-ag3nt/src/bin/hive-m1nd.rs +++ b/hive-ag3nt/src/bin/hive-m1nd.rs @@ -41,6 +41,10 @@ enum Cmd { RequestSpawn { name: String }, /// Kill a sub-agent. Kill { name: String }, + /// Start a stopped sub-agent. + Start { name: String }, + /// Restart a sub-agent (stop + start). + Restart { name: String }, /// Submit a config commit on the agent's config repo for user approval. RequestApplyCommit { agent: String, commit_ref: String }, /// Run the manager MCP server on stdio. Spawned by claude via @@ -103,6 +107,8 @@ async fn main() -> Result<()> { one_shot(&cli.socket, ManagerRequest::RequestSpawn { name }).await } Cmd::Kill { name } => one_shot(&cli.socket, ManagerRequest::Kill { name }).await, + Cmd::Start { name } => one_shot(&cli.socket, ManagerRequest::Start { name }).await, + Cmd::Restart { name } => one_shot(&cli.socket, ManagerRequest::Restart { name }).await, Cmd::RequestApplyCommit { agent, commit_ref } => { one_shot( &cli.socket, diff --git a/hive-ag3nt/src/mcp.rs b/hive-ag3nt/src/mcp.rs index eec9bfa..575cf99 100644 --- a/hive-ag3nt/src/mcp.rs +++ b/hive-ag3nt/src/mcp.rs @@ -211,6 +211,18 @@ pub struct KillArgs { pub name: String, } +#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] +pub struct StartArgs { + /// Sub-agent name (without the `h-` container prefix). + pub name: String, +} + +#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] +pub struct RestartArgs { + /// Sub-agent name (without the `h-` container prefix). + pub name: String, +} + #[derive(Debug, serde::Deserialize, schemars::JsonSchema)] pub struct AskOperatorArgs { /// The question to surface on the dashboard. @@ -308,7 +320,7 @@ impl ManagerServer { #[tool( description = "Stop a sub-agent container (graceful). The state dir is kept; \ - recreating reuses prior config + Claude credentials." + recreating reuses prior config + Claude credentials. No approval required." )] async fn kill(&self, Parameters(args): Parameters) -> String { let log = format!("{args:?}"); @@ -322,6 +334,35 @@ impl ManagerServer { .await } + #[tool( + description = "Start a stopped sub-agent container. No approval required — \ + lifecycle ops on existing containers are at the manager's discretion." + )] + async fn start(&self, Parameters(args): Parameters) -> String { + let log = format!("{args:?}"); + let name = args.name.clone(); + run_tool_envelope("start", log, async move { + let resp = self + .dispatch(hive_sh4re::ManagerRequest::Start { name: args.name }) + .await; + format_ack(resp, "start", format!("started {name}")) + }) + .await + } + + #[tool(description = "Restart a sub-agent container (stop + start). No approval required.")] + async fn restart(&self, Parameters(args): Parameters) -> String { + let log = format!("{args:?}"); + let name = args.name.clone(); + run_tool_envelope("restart", log, async move { + let resp = self + .dispatch(hive_sh4re::ManagerRequest::Restart { name: args.name }) + .await; + format_ack(resp, "restart", format!("restarted {name}")) + }) + .await + } + #[tool( description = "Surface a question to the operator on the dashboard. Returns immediately \ with a question id — do NOT wait inline. When the operator answers, a system message \ @@ -426,6 +467,8 @@ pub fn allowed_mcp_tools(flavor: Flavor) -> Vec { "recv", "request_spawn", "kill", + "start", + "restart", "request_apply_commit", "ask_operator", ], diff --git a/hive-c0re/src/manager_server.rs b/hive-c0re/src/manager_server.rs index 84e8168..8eb1c4a 100644 --- a/hive-c0re/src/manager_server.rs +++ b/hive-c0re/src/manager_server.rs @@ -131,6 +131,11 @@ async fn dispatch(req: &ManagerRequest, coord: &Coordinator) -> ManagerResponse } ManagerRequest::Kill { name } => { tracing::info!(%name, "manager: kill"); + if name == crate::lifecycle::MANAGER_NAME { + return ManagerResponse::Err { + message: "refusing to kill the manager".into(), + }; + } let result: Result<()> = async { lifecycle::kill(name).await?; coord.unregister_agent(name); @@ -138,6 +143,39 @@ async fn dispatch(req: &ManagerRequest, coord: &Coordinator) -> ManagerResponse } .await; match result { + Ok(()) => { + coord.notify_manager(&hive_sh4re::HelperEvent::Killed { + agent: name.clone(), + }); + ManagerResponse::Ok + } + Err(e) => ManagerResponse::Err { + message: format!("{e:#}"), + }, + } + } + ManagerRequest::Start { name } => { + tracing::info!(%name, "manager: start"); + if name == crate::lifecycle::MANAGER_NAME { + return ManagerResponse::Err { + message: "refusing to start the manager from itself".into(), + }; + } + match lifecycle::start(name).await { + Ok(()) => ManagerResponse::Ok, + Err(e) => ManagerResponse::Err { + message: format!("{e:#}"), + }, + } + } + ManagerRequest::Restart { name } => { + tracing::info!(%name, "manager: restart"); + if name == crate::lifecycle::MANAGER_NAME { + return ManagerResponse::Err { + message: "refusing to restart the manager from itself".into(), + }; + } + match lifecycle::restart(name).await { Ok(()) => ManagerResponse::Ok, Err(e) => ManagerResponse::Err { message: format!("{e:#}"), diff --git a/hive-sh4re/src/lib.rs b/hive-sh4re/src/lib.rs index aa5e749..3415d20 100644 --- a/hive-sh4re/src/lib.rs +++ b/hive-sh4re/src/lib.rs @@ -267,6 +267,14 @@ pub enum ManagerRequest { Kill { name: String, }, + /// Start a previously-stopped sub-agent container. + Start { + name: String, + }, + /// Restart a sub-agent container (stop + start). + Restart { + name: String, + }, /// Submit a config commit for the user to approve. `commit_ref` is opaque /// to the host (typically a git sha pointing into the agent's config repo). /// On approval the host applies the change via `nixos-container update`.