feat(#1004,#1006): capability system + read_host_journal / get_host_journal MCP tool

This commit is contained in:
damocles 2026-06-01 20:27:08 +02:00 committed by mara
commit dc8a4e2baf
7 changed files with 345 additions and 3 deletions

View file

@ -148,6 +148,11 @@ pub async fn sync_agents(
if crate::tool_groups::tool_groups_path().exists() {
git(&dir, &["add", "tool-groups.json"]).await?;
}
// Stage capabilities.json when it exists. Created on first
// `set_caps` call; absent = no agents have extra capabilities.
if crate::capabilities::capabilities_path().exists() {
git(&dir, &["add", "capabilities.json"]).await?;
}
// Stage roles.json when it exists. Written by topology::write_roles /
// reconcile_roles on first role assignment or manager default seeding.
// Without this, roles.json appears as untracked in the meta repo
@ -450,7 +455,7 @@ where
let pronouns_escaped = operator_pronouns.replace('\\', "\\\\").replace('"', "\\\"");
let _ = writeln!(
out,
" dashboardPort = {dashboard_port};\n operatorPronouns = \"{pronouns_escaped}\";\n mkAgent = {{ name, isManager, port, parent ? null, toolGroups ? null }}:"
" dashboardPort = {dashboard_port};\n operatorPronouns = \"{pronouns_escaped}\";\n mkAgent = {{ name, isManager, port, parent ? null, toolGroups ? null, capabilities ? null }}:"
);
out.push_str(
r#" let
@ -461,6 +466,7 @@ where
service = "hive-ag3nt";
parentEnv = if parent == null then {} else { HIVE_PARENT = parent; };
toolGroupsEnv = if toolGroups == null then {} else { HIVE_TOOL_GROUPS = toolGroups; };
capabilitiesEnv = if capabilities == null then {} else { HIVE_CAPABILITIES = capabilities; };
in
base.extendModules {
modules = [
@ -494,7 +500,7 @@ where
HYPERHIVE_STATE_DIR = "/agents/${name}/state";
HYPERHIVE_HARNESS_DIR = "/agents/${name}/harness";
};
systemd.services.${service}.environment = parentEnv // toolGroupsEnv // {
systemd.services.${service}.environment = parentEnv // toolGroupsEnv // capabilitiesEnv // {
HIVE_PORT = toString port;
HIVE_LABEL = name;
HIVE_DASHBOARD_PORT = toString dashboardPort;
@ -546,6 +552,7 @@ where
// on first run with manager as root + everyone else under manager.
let topology = crate::topology::read();
let tool_groups_map = crate::tool_groups::read();
let capabilities_map = crate::capabilities::read();
for spec in agents {
let parent_attr = topology
.get(&spec.name)
@ -562,15 +569,26 @@ where
let joined = groups.join(",");
format!("\"{joined}\"")
};
// Emit `capabilities = "cap1,cap2"` when the operator has
// granted capabilities to this agent. Absent entry = null = no
// capability env var injected, capability-gated tools hidden.
let caps = capabilities_map.get(&spec.name).cloned().unwrap_or_default();
let capabilities_attr = if caps.is_empty() {
"null".to_owned()
} else {
let joined = caps.join(",");
format!("\"{joined}\"")
};
let _ = writeln!(
out,
" {} = mkAgent {{ name = \"{}\"; isManager = {}; port = {}; parent = {}; toolGroups = {}; }};",
" {} = mkAgent {{ name = \"{}\"; isManager = {}; port = {}; parent = {}; toolGroups = {}; capabilities = {}; }};",
spec.name,
spec.name,
if spec.is_manager { "true" } else { "false" },
spec.port,
parent_attr,
tool_groups_attr,
capabilities_attr,
);
}
out.push_str(" };\n };\n}\n");