feat(#791): add request_init_config + request_apply_commit to AgentServer (topology-scoped)
This commit is contained in:
parent
dacd83f278
commit
3fa31a414f
4 changed files with 161 additions and 30 deletions
|
|
@ -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__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__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__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.
|
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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1047,6 +1047,83 @@ impl AgentServer {
|
||||||
})
|
})
|
||||||
.await
|
.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(
|
#[tool_handler(
|
||||||
|
|
|
||||||
|
|
@ -425,6 +425,60 @@ async fn dispatch(req: &AgentRequest, agent: &str, coord: &Arc<Coordinator>) ->
|
||||||
coord.emit_rebuild_queue_snapshot();
|
coord.emit_rebuild_queue_snapshot();
|
||||||
AgentResponse::Ok
|
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.
|
// Manager-only variants are not valid on the agent socket.
|
||||||
_ => AgentResponse::Err {
|
_ => AgentResponse::Err {
|
||||||
message: "request not supported on agent socket".to_owned(),
|
message: "request not supported on agent socket".to_owned(),
|
||||||
|
|
|
||||||
|
|
@ -83,34 +83,8 @@ async fn dispatch(req: &ManagerRequest, coord: &Arc<Coordinator>) -> ManagerResp
|
||||||
match req {
|
match req {
|
||||||
ManagerRequest::RequestInitConfig { name, description } => {
|
ManagerRequest::RequestInitConfig { name, description } => {
|
||||||
tracing::info!(%name, "manager: request_init_config");
|
tracing::info!(%name, "manager: request_init_config");
|
||||||
let proposed_dir = crate::coordinator::Coordinator::agent_proposed_dir(name);
|
match submit_init_config(coord, name, description.clone()).await {
|
||||||
if proposed_dir.join(".git").exists() {
|
Ok(_id) => ManagerResponse::Ok,
|
||||||
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
|
|
||||||
}
|
|
||||||
Err(e) => ManagerResponse::Err {
|
Err(e) => ManagerResponse::Err {
|
||||||
message: format!("{e:#}"),
|
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
|
/// Accepts a 7..=40 char hex string (short or full sha); the exact
|
||||||
/// commit is resolved + existence-checked against the proposed repo
|
/// commit is resolved + existence-checked against the proposed repo
|
||||||
/// later in `lifecycle::git_fetch_to_tag`.
|
/// 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 n = commit_ref.len();
|
||||||
let hex = commit_ref.chars().all(|c| c.is_ascii_hexdigit());
|
let hex = commit_ref.chars().all(|c| c.is_ascii_hexdigit());
|
||||||
if !(7..=40).contains(&n) || !hex {
|
if !(7..=40).contains(&n) || !hex {
|
||||||
|
|
@ -364,6 +338,30 @@ fn validate_commit_ref(commit_ref: &str) -> Result<()> {
|
||||||
Ok(())
|
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
|
/// Submit-time half of the apply flow: queue the approval row, then
|
||||||
/// fetch the manager's commit from the proposed repo into applied and
|
/// 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
|
/// 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
|
/// 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
|
/// surface the error to the manager. We don't try to roll the row
|
||||||
/// back — the failure is part of the audit trail.
|
/// back — the failure is part of the audit trail.
|
||||||
async fn submit_apply_commit(
|
pub(crate) async fn submit_apply_commit(
|
||||||
coord: &Arc<Coordinator>,
|
coord: &Arc<Coordinator>,
|
||||||
agent: &str,
|
agent: &str,
|
||||||
commit_ref: &str,
|
commit_ref: &str,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue