feat(#791): add request_init_config + request_apply_commit to AgentServer (topology-scoped)

This commit is contained in:
damocles 2026-06-03 22:39:45 +02:00 committed by mara
commit 3fa31a414f
4 changed files with 161 additions and 30 deletions

View file

@ -45,6 +45,8 @@ Tools (hyperhive surface):
- `mcp__hyperhive__restart(name)`*(requires `lifecycle` tool group)* restart a direct child sub-agent (stop + start). The server enforces topology: the call is rejected unless `name` is a direct child of yours per `topology.json`. No approval required.
- `mcp__hyperhive__kill(name)`*(requires `lifecycle` tool group)* stop a direct child sub-agent (graceful). Direct children only — server enforces topology. State dir kept; recreating reuses prior config + credentials. No approval required.
- `mcp__hyperhive__update(name)`*(requires `lifecycle` tool group)* rebuild a direct child sub-agent: re-applies the current hyperhive flake + agent.nix and restarts it. Direct children only — server enforces topology. No approval required. Idempotent.
- `mcp__hyperhive__request_init_config(name, description?)`*(requires `approvals` tool group)* initialise a brand-new direct child agent's proposed config repo. Queues an `InitConfig` approval; on approval hive-c0re seeds `/agents/<name>/config/agent.nix`. `name` must be a direct child in the topology tree — server enforces. Fails if the config repo already exists (use `request_apply_commit` instead).
- `mcp__hyperhive__request_apply_commit(agent, commit_ref, description?)`*(requires `approvals` tool group)* submit a config commit for a direct child agent, queued for operator approval. `agent` must be a direct child in the topology tree — server enforces. `commit_ref` must be a 7-40 char hex sha (not a branch/tag name). On approval hive-c0re rebuilds the container with the pinned commit.
Need new packages, env vars, or other NixOS config for yourself? You can't edit your own config directly — message the manager (recipient `root`) describing what you need + why. The manager evaluates the request (it doesn't rubber-stamp), edits `/agents/{label}/config/agent.nix` on your behalf, commits, and submits an approval that the operator can accept on the dashboard; on approve hive-c0re rebuilds your container with the new config.

View file

@ -1047,6 +1047,83 @@ impl AgentServer {
})
.await
}
// IMPORTANT: this tool is only available when the `approvals` tool group
// is configured for the agent (`HIVE_TOOL_GROUPS` contains `approvals`).
// hive-c0re performs a topology check server-side: only direct children
// of the calling agent are accepted; all other names are rejected.
#[tool(
description = "Initialise a brand-new direct child agent's proposed config repo and \
queue an `InitConfig` approval for the operator to review. Requires the `approvals` \
tool group. `name` must be a direct child of this agent in the topology tree. \
Fails if a config repo for that child already exists use `request_apply_commit` \
to update an existing agent's config. On approval hive-c0re seeds \
`/agents/<name>/config/agent.nix` with the default template so you can \
customise it and then call `request_apply_commit` with the commit sha."
)]
async fn request_init_config(
&self,
Parameters(args): Parameters<RequestInitConfigArgs>,
) -> String {
let log = format!("{args:?}");
let name = args.name.clone();
run_tool_envelope("request_init_config", log, async move {
let (resp, retries) = self
.dispatch(hive_sh4re::AgentRequest::RequestInitConfig {
name: args.name,
description: args.description,
})
.await;
annotate_retries(
format_ack(
resp,
"request_init_config",
format!("init_config approval queued for {name}"),
),
retries,
)
})
.await
}
// IMPORTANT: this tool is only available when the `approvals` tool group
// is configured for the agent (`HIVE_TOOL_GROUPS` contains `approvals`).
// hive-c0re performs a topology check server-side: only direct children
// of the calling agent are accepted; all other names are rejected.
#[tool(
description = "Submit a config change for a direct child agent, queued for operator \
approval. Requires the `approvals` tool group. `agent` must be a direct child \
of this agent in the topology tree. Pass a commit sha (7-40 hex chars, full or \
short) from that agent's proposed config repo branch/tag names like `main` are \
rejected, the approval pins the exact commit. On approval hive-c0re rebuilds \
the container with the new config."
)]
async fn request_apply_commit(
&self,
Parameters(args): Parameters<RequestApplyCommitArgs>,
) -> String {
let log = format!("{args:?}");
let agent = args.agent.clone();
let commit_ref = args.commit_ref.clone();
run_tool_envelope("request_apply_commit", log, async move {
let (resp, retries) = self
.dispatch(hive_sh4re::AgentRequest::RequestApplyCommit {
agent: args.agent,
commit_ref: args.commit_ref,
description: args.description,
})
.await;
annotate_retries(
format_ack(
resp,
"request_apply_commit",
format!("apply approval queued for {agent} @ {commit_ref}"),
),
retries,
)
})
.await
}
}
#[tool_handler(

View file

@ -425,6 +425,60 @@ async fn dispatch(req: &AgentRequest, agent: &str, coord: &Arc<Coordinator>) ->
coord.emit_rebuild_queue_snapshot();
AgentResponse::Ok
}
AgentRequest::RequestInitConfig { name, description } => {
if !crate::topology::children_of(agent)
.iter()
.any(|c| c == name)
{
return AgentResponse::Err {
message: format!(
"agent `{agent}` cannot request_init_config for `{name}`: \
not a direct child in the topology tree"
),
};
}
tracing::info!(%agent, %name, "agent: request_init_config for child");
match crate::manager_server::submit_init_config(coord, name, description.clone()).await {
Ok(_id) => AgentResponse::Ok,
Err(e) => AgentResponse::Err {
message: format!("{e:#}"),
},
}
}
AgentRequest::RequestApplyCommit {
agent: target_agent,
commit_ref,
description,
} => {
if !crate::topology::children_of(agent)
.iter()
.any(|c| c == target_agent)
{
return AgentResponse::Err {
message: format!(
"agent `{agent}` cannot request_apply_commit for `{target_agent}`: \
not a direct child in the topology tree"
),
};
}
tracing::info!(%agent, %target_agent, %commit_ref, "agent: request_apply_commit for child");
match crate::manager_server::submit_apply_commit(
coord,
target_agent,
commit_ref,
description.as_deref(),
)
.await
{
Ok((id, sha)) => {
tracing::info!(%id, %target_agent, %sha, "agent: apply_commit approval queued");
AgentResponse::Ok
}
Err(e) => AgentResponse::Err {
message: format!("{e:#}"),
},
}
}
// Manager-only variants are not valid on the agent socket.
_ => AgentResponse::Err {
message: "request not supported on agent socket".to_owned(),

View file

@ -83,34 +83,8 @@ async fn dispatch(req: &ManagerRequest, coord: &Arc<Coordinator>) -> ManagerResp
match req {
ManagerRequest::RequestInitConfig { name, description } => {
tracing::info!(%name, "manager: request_init_config");
let proposed_dir = crate::coordinator::Coordinator::agent_proposed_dir(name);
if proposed_dir.join(".git").exists() {
return ManagerResponse::Err {
message: format!(
"proposed config repo for '{name}' already exists at {} - \
use request_apply_commit to update an existing agent's config",
proposed_dir.display()
),
};
}
match coord.approvals.submit_kind(
name,
hive_sh4re::ApprovalKind::InitConfig,
"",
description.as_deref(),
) {
Ok(id) => {
tracing::info!(%id, %name, "init_config approval queued");
coord.emit_approval_added(
id,
name,
"init_config",
None,
None,
description.clone(),
);
ManagerResponse::Ok
}
match submit_init_config(coord, name, description.clone()).await {
Ok(_id) => ManagerResponse::Ok,
Err(e) => ManagerResponse::Err {
message: format!("{e:#}"),
},
@ -352,7 +326,7 @@ async fn dispatch(req: &ManagerRequest, coord: &Arc<Coordinator>) -> ManagerResp
/// Accepts a 7..=40 char hex string (short or full sha); the exact
/// commit is resolved + existence-checked against the proposed repo
/// later in `lifecycle::git_fetch_to_tag`.
fn validate_commit_ref(commit_ref: &str) -> Result<()> {
pub(crate) fn validate_commit_ref(commit_ref: &str) -> Result<()> {
let n = commit_ref.len();
let hex = commit_ref.chars().all(|c| c.is_ascii_hexdigit());
if !(7..=40).contains(&n) || !hex {
@ -364,6 +338,30 @@ fn validate_commit_ref(commit_ref: &str) -> Result<()> {
Ok(())
}
/// Queue an `InitConfig` approval for a brand-new agent whose config repo
/// does not yet exist. Shared between the manager and agent sockets.
pub(crate) async fn submit_init_config(
coord: &Arc<Coordinator>,
name: &str,
description: Option<String>,
) -> anyhow::Result<i64> {
let proposed_dir = crate::coordinator::Coordinator::agent_proposed_dir(name);
if proposed_dir.join(".git").exists() {
anyhow::bail!(
"proposed config repo for '{name}' already exists at {} - \
use request_apply_commit to update an existing agent's config",
proposed_dir.display()
);
}
let id = coord
.approvals
.submit_kind(name, hive_sh4re::ApprovalKind::InitConfig, "", description.as_deref())
.map_err(|e| anyhow::anyhow!("queue approval row: {e:#}"))?;
tracing::info!(%id, %name, "init_config approval queued");
coord.emit_approval_added(id, name, "init_config", None, None, description);
Ok(id)
}
/// Submit-time half of the apply flow: queue the approval row, then
/// fetch the manager's commit from the proposed repo into applied and
/// pin it as `refs/tags/proposal/<id>`. From this point on the manager
@ -375,7 +373,7 @@ fn validate_commit_ref(commit_ref: &str) -> Result<()> {
/// proposed, fs error, git plumbing crash) we mark the row failed and
/// surface the error to the manager. We don't try to roll the row
/// back — the failure is part of the audit trail.
async fn submit_apply_commit(
pub(crate) async fn submit_apply_commit(
coord: &Arc<Coordinator>,
agent: &str,
commit_ref: &str,