feat(#1636): infra_admin capability — restart hive-ci/gateway/forge via restart tool
This commit is contained in:
parent
f0be0172d5
commit
f05031ebe3
6 changed files with 144 additions and 4 deletions
|
|
@ -979,7 +979,10 @@ impl AgentServer {
|
|||
#[tool(
|
||||
description = "Restart a direct child sub-agent container (stop + start). \
|
||||
Only succeeds if `name` is a direct child of this agent in the topology \
|
||||
tree — the server enforces this. No approval required."
|
||||
tree — the server enforces this. No approval required. \
|
||||
Agents holding the `infra_admin` capability may also pass a hive \
|
||||
infrastructure container name (`hive-ci`, `hive-gateway`, `hive-forge`) \
|
||||
to restart it directly via the privileged helper."
|
||||
)]
|
||||
async fn restart(&self, Parameters(args): Parameters<RestartArgs>) -> String {
|
||||
let log = format!("{args:?}");
|
||||
|
|
@ -1792,6 +1795,13 @@ fn allowed_capability_tools() -> Vec<String> {
|
|||
let t = token.trim().to_ascii_lowercase();
|
||||
match t.as_str() {
|
||||
"read_host_journal" => tools.push("get_host_journal".to_owned()),
|
||||
// infra_admin lets an agent restart hive infrastructure
|
||||
// containers (hive-ci / hive-gateway / hive-forge) through the
|
||||
// existing `restart` tool. Unlock it here so agents that hold
|
||||
// the capability without the full `lifecycle` group can still
|
||||
// call it; c0re re-checks the capability server-side and only
|
||||
// honours infra-container names via this path.
|
||||
"infra_admin" => tools.push("restart".to_owned()),
|
||||
// manage_root_agent / query_agent_state don't expose new MCP
|
||||
// tools: manage_root_agent gates existing lifecycle tools via
|
||||
// topology enforcement; query_agent_state unlocks the `agent`
|
||||
|
|
|
|||
|
|
@ -391,7 +391,7 @@ async fn dispatch(req: &AgentRequest, agent: &str, coord: &Arc<Coordinator>) ->
|
|||
agent: target,
|
||||
} => handle_reminder_rollup(coord, agent, target.as_deref(), *since_secs),
|
||||
AgentRequest::Start { name } => handle_start_child(coord, agent, name).await,
|
||||
AgentRequest::Restart { name } => handle_restart_child(coord, agent, name),
|
||||
AgentRequest::Restart { name } => handle_restart_child(coord, agent, name).await,
|
||||
AgentRequest::Kill { name } => handle_kill_child(coord, agent, name).await,
|
||||
AgentRequest::Update { name } => handle_update_child(coord, agent, name),
|
||||
AgentRequest::ListDescendants => handle_list_descendants(agent).await,
|
||||
|
|
@ -515,7 +515,15 @@ async fn handle_start_child(coord: &Arc<Coordinator>, agent: &str, name: &str) -
|
|||
/// `Restart` — enqueue a restart for a direct-child container.
|
||||
/// Topology parenthood is the only authorisation criterion — no
|
||||
/// capability flag needed.
|
||||
fn handle_restart_child(coord: &Arc<Coordinator>, agent: &str, name: &str) -> AgentResponse {
|
||||
async fn handle_restart_child(coord: &Arc<Coordinator>, agent: &str, name: &str) -> AgentResponse {
|
||||
// Infra-container restart: an agent holding the `infra_admin`
|
||||
// capability can restart a hive infrastructure container (hive-ci /
|
||||
// hive-gateway / hive-forge) by passing its name to the same restart
|
||||
// tool. These names are never agent children, so this branch is
|
||||
// disjoint from the child-restart path below.
|
||||
if hive_sh4re::priv_proto::RESTARTABLE_INFRA_CONTAINERS.contains(&name) {
|
||||
return handle_restart_infra(agent, name).await;
|
||||
}
|
||||
if let Some(err) = require_child(agent, name, "restart") {
|
||||
return err;
|
||||
}
|
||||
|
|
@ -531,6 +539,29 @@ fn handle_restart_child(coord: &Arc<Coordinator>, agent: &str, name: &str) -> Ag
|
|||
AgentResponse::Ok
|
||||
}
|
||||
|
||||
/// Restart a hive infrastructure container on behalf of an agent that
|
||||
/// holds the `infra_admin` capability. The container name is already
|
||||
/// known to be in `RESTARTABLE_INFRA_CONTAINERS`; this gates on the
|
||||
/// capability and routes the systemctl restart through hive-priv (which
|
||||
/// re-validates the name root-side). Direct, not approval-gated.
|
||||
async fn handle_restart_infra(agent: &str, container: &str) -> AgentResponse {
|
||||
if !crate::capabilities::has_cap(agent, hive_sh4re::Capability::InfraAdmin) {
|
||||
tracing::warn!(%agent, %container, "agent: infra restart denied (no infra_admin capability)");
|
||||
return AgentResponse::Err {
|
||||
message: format!(
|
||||
"restarting infra container `{container}` requires the `infra_admin` capability"
|
||||
),
|
||||
};
|
||||
}
|
||||
tracing::info!(%agent, %container, "agent: restart infra container");
|
||||
match crate::priv_client::restart_infra_container(container).await {
|
||||
Ok(()) => AgentResponse::Ok,
|
||||
Err(e) => AgentResponse::Err {
|
||||
message: format!("{e:#}"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// `Kill` — kill a direct-child container, unregister it, notify the
|
||||
/// manager.
|
||||
async fn handle_kill_child(coord: &Arc<Coordinator>, agent: &str, name: &str) -> AgentResponse {
|
||||
|
|
|
|||
|
|
@ -276,6 +276,18 @@ pub async fn restart_matrix_daemon(agent_name: &str) -> Result<()> {
|
|||
.await?)
|
||||
}
|
||||
|
||||
/// Restart a hive infrastructure container (hive-ci / hive-gateway /
|
||||
/// hive-forge) on the host via `systemctl restart
|
||||
/// container@<container>.service`. hive-priv re-validates `container`
|
||||
/// against its root-side allowlist; callers must already have checked
|
||||
/// the requesting agent holds the `infra_admin` capability.
|
||||
pub async fn restart_infra_container(container: &str) -> Result<()> {
|
||||
ok(call(&PrivRequest::RestartInfraContainer {
|
||||
container: container.to_owned(),
|
||||
})
|
||||
.await?)
|
||||
}
|
||||
|
||||
fn check(resp: PrivResponse) -> Result<(String, String)> {
|
||||
if resp.ok {
|
||||
Ok((resp.stdout, resp.stderr))
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ use anyhow::{Context as _, Result, bail};
|
|||
use hive_sh4re::priv_proto::{
|
||||
AGENT_PREFIX, AGENT_STATE_ROOT, BindMount, JournalQuery, MANAGER_NAME, META_DIR,
|
||||
NetworkIsolation, PRIV_SOCK, PrivEvent, PrivRequest, PrivResponse, PrivStream, PrivStreamLine,
|
||||
SIBLING_CONTAINERS,
|
||||
RESTARTABLE_INFRA_CONTAINERS, SIBLING_CONTAINERS,
|
||||
};
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
use tokio::net::unix::OwnedWriteHalf;
|
||||
|
|
@ -250,6 +250,10 @@ async fn exec(req: PrivRequest, writer: &mut OwnedWriteHalf) -> Result<(String,
|
|||
PrivRequest::RestartMatrixDaemon { ref agent_name } => {
|
||||
restart_matrix_daemon(agent_name).await
|
||||
}
|
||||
|
||||
PrivRequest::RestartInfraContainer { ref container } => {
|
||||
restart_infra_container(container).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -376,6 +380,35 @@ async fn restart_matrix_daemon(agent_name: &str) -> Result<(String, String)> {
|
|||
))
|
||||
}
|
||||
|
||||
/// `RestartInfraContainer` — restart a hive infrastructure container on
|
||||
/// the host via `systemctl restart container@<container>.service`. The
|
||||
/// `container` is validated against `RESTARTABLE_INFRA_CONTAINERS` here,
|
||||
/// root-side, so this is the authoritative allowlist even though
|
||||
/// hive-c0re also gates on the caller's `infra_admin` capability.
|
||||
async fn restart_infra_container(container: &str) -> Result<(String, String)> {
|
||||
if !RESTARTABLE_INFRA_CONTAINERS.contains(&container) {
|
||||
bail!("container {container:?} is not a restartable hive infra container");
|
||||
}
|
||||
let unit = format!("container@{container}.service");
|
||||
let out = Command::new("systemctl")
|
||||
.args(["restart", &unit])
|
||||
.output()
|
||||
.await
|
||||
.with_context(|| format!("systemctl restart {unit}"))?;
|
||||
if !out.status.success() {
|
||||
bail!(
|
||||
"systemctl restart {unit} exited {}: {}",
|
||||
out.status,
|
||||
String::from_utf8_lossy(&out.stderr).trim()
|
||||
);
|
||||
}
|
||||
tracing::info!(target: "infra-restart", "restarted {unit}");
|
||||
Ok((
|
||||
String::from_utf8_lossy(&out.stdout).into_owned(),
|
||||
String::from_utf8_lossy(&out.stderr).into_owned(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Shared helper for `WriteAgentForgeToken` and `WriteAgentMatrixToken`.
|
||||
/// Writes `content` to `AGENT_STATE_ROOT/<agent_name>/state/<filename>`,
|
||||
/// chowns to the agent user (derived from the state dir's existing owner),
|
||||
|
|
|
|||
|
|
@ -1031,6 +1031,13 @@ pub enum Capability {
|
|||
/// available on the agent socket even with this capability — use the
|
||||
/// manager socket for swarm-wide scans.
|
||||
QueryAgentState,
|
||||
/// Agent can restart hive infrastructure containers (hive-ci,
|
||||
/// hive-gateway, hive-forge) via the `restart` MCP tool. hive-c0re
|
||||
/// checks this capability before routing the restart through
|
||||
/// hive-priv; the concrete service allowlist lives root-side in
|
||||
/// hive-priv. Deliberately generic ("infra admin") so future
|
||||
/// privileged infra ops can hang off the same grant.
|
||||
InfraAdmin,
|
||||
}
|
||||
|
||||
impl Capability {
|
||||
|
|
@ -1040,6 +1047,7 @@ impl Capability {
|
|||
Self::ManageRootAgent,
|
||||
Self::ReadHostJournal,
|
||||
Self::QueryAgentState,
|
||||
Self::InfraAdmin,
|
||||
];
|
||||
|
||||
/// Canonical `snake_case` name for this capability (matches serde).
|
||||
|
|
@ -1049,6 +1057,7 @@ impl Capability {
|
|||
Self::ManageRootAgent => "manage_root_agent",
|
||||
Self::ReadHostJournal => "read_host_journal",
|
||||
Self::QueryAgentState => "query_agent_state",
|
||||
Self::InfraAdmin => "infra_admin",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1063,6 +1072,9 @@ impl Capability {
|
|||
Self::QueryAgentState => {
|
||||
"query non-child agents' loose ends and reminder state via get_loose_ends"
|
||||
}
|
||||
Self::InfraAdmin => {
|
||||
"restart hive infrastructure containers (hive-ci, hive-gateway, hive-forge) via the restart tool"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,15 @@ pub const AGENT_PREFIX: &str = "h-";
|
|||
/// Sibling service containers managed by hive-c0re.
|
||||
pub const SIBLING_CONTAINERS: &[&str] = &["hive-forge", "hive-matrix", "hive-gateway", "hive-ci"];
|
||||
|
||||
/// Infra containers an agent holding the `infra_admin` capability may
|
||||
/// restart via the `restart` MCP tool. A deliberate subset of
|
||||
/// [`SIBLING_CONTAINERS`]: hive-matrix is excluded (kicking the matrix
|
||||
/// backend mid-sync is its own concern) and hive-c0re is excluded
|
||||
/// entirely (a self-restart would sever the very socket the request
|
||||
/// arrived on). hive-priv re-validates against this list root-side, so
|
||||
/// it is the authoritative allowlist regardless of what the caller sends.
|
||||
pub const RESTARTABLE_INFRA_CONTAINERS: &[&str] = &["hive-ci", "hive-gateway", "hive-forge"];
|
||||
|
||||
/// Host path of the meta flake. The flake ref for agent `<name>` is
|
||||
/// `{META_DIR}#{name}`, derived by `hive-priv` — never passed over the wire.
|
||||
pub const META_DIR: &str = "/var/lib/hyperhive/meta";
|
||||
|
|
@ -287,6 +296,18 @@ pub enum PrivRequest {
|
|||
/// Logical agent name (validated by `validate_agent_name`).
|
||||
agent_name: String,
|
||||
},
|
||||
|
||||
/// Restart a hive infrastructure container on the host via
|
||||
/// `systemctl restart container@<container>.service`. hive-priv
|
||||
/// validates `container` against [`RESTARTABLE_INFRA_CONTAINERS`]
|
||||
/// before acting — the root-side allowlist is authoritative. Used
|
||||
/// by hive-c0re to service a `restart` request from an agent that
|
||||
/// holds the `infra_admin` capability.
|
||||
RestartInfraContainer {
|
||||
/// Infra container name (e.g. `hive-ci`); must be in
|
||||
/// [`RESTARTABLE_INFRA_CONTAINERS`].
|
||||
container: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Response from the privileged helper.
|
||||
|
|
@ -341,3 +362,24 @@ pub enum PrivEvent {
|
|||
/// Terminal event: the operation has finished.
|
||||
Done(PrivResponse),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{RESTARTABLE_INFRA_CONTAINERS, SIBLING_CONTAINERS};
|
||||
|
||||
#[test]
|
||||
fn restartable_infra_is_a_safe_subset_of_siblings() {
|
||||
// Every restartable infra container must be a known sibling.
|
||||
for c in RESTARTABLE_INFRA_CONTAINERS {
|
||||
assert!(
|
||||
SIBLING_CONTAINERS.contains(c),
|
||||
"{c} is not a managed sibling container"
|
||||
);
|
||||
}
|
||||
// hive-matrix and hive-c0re are deliberately excluded: kicking the
|
||||
// matrix backend mid-sync is its own concern, and a self-restart of
|
||||
// c0re would sever the request socket.
|
||||
assert!(!RESTARTABLE_INFRA_CONTAINERS.contains(&"hive-matrix"));
|
||||
assert!(!RESTARTABLE_INFRA_CONTAINERS.contains(&"hive-c0re"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue