Watch
0
0
Fork
You've already forked hyperhive
0

Compare commits

..
Author SHA1 Message Date
atlas
0e7aa74d00 agent permissions: drop the test-only direct_get_subject; the failed-creation test names the refusing step 2026-10-15 18:01:40 +02:00
atlas
89013b5408 agent permissions: hold hive-c0re's stored grant until the agent's own read
Until its first successful read of its key, the harness read its
permissions as the default set, `execution` included. So did the
subagent daemon. An agent the operator had narrowed ran wider than
granted after every restart while the queue was unreachable, and in the
gap before the first 10 s poll. During the meta-repo import, every
non-ruth key is still unset, and the agent read that as the default too,
for as long as the import stayed pending.

mara picked option (a) on #5293: "a, removal of the old path as a follow
up in 1w".

hive-c0re now writes each agent's entry from
swarm-agent-permissions.json into the agent's runtime dir as
swarm-permissions.json (/run/hive in the container), together with
whether the import is still pending. It writes the file at every start
(write_dropins) and after every 30 s pass of the permissions worker. The
harness and the subagent daemon hold that stored list until their own
first read succeeds. While the import is pending, an unset key keeps
the stored list rather than reverting it to the default. A queue that
is down never blocks a start: the agent runs on the stored grant.

The decision lives in swarm_queue_client::agent_permissions::HiveCopy,
shared by both readers.

Part of #5170.
2026-10-15 18:01:40 +02:00
atlas
4f9e9bd680 agent permissions: a destroyed agent's key goes with it
Declaring an agent Destroyed now deletes its `agent-permissions` key,
next to the queue-credential revoke and with the same failure handling:
logged at error, the destroy goes ahead, and declaring it destroyed
again re-runs it. Agent names are reusable after a destroy, and a create
left on the default stores no key, so the old key handed a new agent of
the same name the destroyed agent's grant (argus on #5293).

Also: migrate.rs's create-time-setup note named ruth's tool-group seed
in `ensure_root_agent`, which this PR deletes; it now says she holds the
code-constant default when the swarm sets nothing.

Part of #5170.
2026-10-15 18:01:40 +02:00
atlas
a937a8ba9a permissions: drop the capability and tool-group wording left after the rework 2026-10-15 18:01:40 +02:00
atlas
f0844f6da3 agent permissions: one set, tool groups included, set at agent creation
mara on #5170: "dont split caps and perms. its all perms. / dont include
the now empty tool groups / nothing set -> default / also add perms to
agent creation screen".

- hive-sh4re: one `Permission` enum (messaging, meta, inbox, execution,
  web_tools, manage_root_agent) replaces `ToolGroup` and `Capability`;
  `lifecycle`/`forge` are gone. `Permission::default_for` is the default
  for an agent the swarm sets nothing for.
- The record is `{ "permissions": [...] }`. Missing key, unset field or
  no successful read yet = default; `[]` = none; a failed read after a
  good one keeps the last value.
- hive-agent reads its own key every 10s and builds each turn's --tools,
  --allowedTools and MCP config (rewritten before every turn) from it;
  the subagent daemon reads it on each start/continue. Agents get a
  read-only grant on their own key.
- hive-c0re restarts an agent only when its manage_root_agent changes.
  The import now merges meta/capabilities.json and meta/tool-groups.json
  into one list per agent and deletes both; the "import pending, don't
  read" decision is `ImportOutcome::read_allowed`, table-tested.
- swarm-controller: `GET /api/permissions` (names + default) and an
  optional `permissions` on `POST /api/agents`, stored before the job is
  queued. swarm-ui's create-agent form gets a permissions field.
- Deleted: HIVE_TOOL_GROUPS and its forward to the subagent daemon,
  tool_groups.rs, ruth's tool-group seed, the permissions rebuild job
  (PermPayload/WritePermFile/perm_change), the dashboard permissions tab
  and its stale-entries panel.

Part of #5170.
2026-10-15 18:01:40 +02:00
atlas
f862354e43 agent capabilities live in a swarm KV record, set from swarmctl
An agent's capabilities move from the hive's meta repo
(`meta/capabilities.json`, injected as `HIVE_CAPABILITIES`) into the
`capabilities` field of its key in a new `agent-permissions` KV bucket,
which swarm-controller alone writes.

- swarm-queue-client: `agent_permissions` (bucket, key, `AgentPermissions`,
  direct-get read, the import subject and its wire types).
- swarm-controller: creates the bucket at start; `GET|PUT|DELETE
  /api/agents/{name}/permissions` (deny_unknown_fields and unknown
  capability ⇒ 422, duplicate ⇒ 400, agent must be declared by a hive);
  answers each hive's one-shot import, writing a key only where there is
  none and only for agents that hive's wanted state declares.
- swarmctl: `agent permissions get|set|clear`.
- swarm-nats-auth: a hive reads every key of the bucket and sends its
  import on its own subject; the controller subscribes to the imports and
  may answer them (`responses`).
- hive-c0re: reads each of its agents' keys every 30s into
  /var/lib/hyperhive/swarm-agent-permissions.json and restarts an agent,
  gracefully, when its capabilities change. Unset is the compiled-in
  default (ruth: manage_root_agent; everyone else: none). A failed read
  keeps the stored value. On first boot it seeds that file from
  meta/capabilities.json, sends the grants to the controller, and deletes
  the file once the controller answers.

Deleted: the capabilities.json reader/writer, `HIVE_CAPABILITIES` in the
rendered meta flake, ruth's capability seed, the capabilities half of the
permissions rebuild job (`PermPayload::Capabilities`/`Combined`), the
dashboard's capabilities table and `capabilities_changed` event, and the
now-unused `Capability::description`.

Part of #5170.
2026-10-15 18:01:40 +02:00
83 changed files with 3419 additions and 3438 deletions

3
Cargo.lock generated
View file

@ -2049,7 +2049,6 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"strum", "strum",
"tracing",
] ]
[[package]] [[package]]
@ -4884,6 +4883,7 @@ dependencies = [
"hive-jobq-metrics", "hive-jobq-metrics",
"hive-jobq-wire", "hive-jobq-wire",
"hive-log", "hive-log",
"hive-sh4re",
"hive-types", "hive-types",
"hmac 0.13.0", "hmac 0.13.0",
"http", "http",
@ -5014,6 +5014,7 @@ dependencies = [
"axum", "axum",
"data-encoding", "data-encoding",
"futures-util", "futures-util",
"hive-sh4re",
"reqwest", "reqwest",
"rustls", "rustls",
"serde", "serde",

View file

@ -39,7 +39,7 @@ declarations.
## Trust boundary & security ## Trust boundary & security
- **What's the operator/agent trust boundary? What's a capability?** → - **What's the operator/agent trust boundary? What's a permission?** →
[`trust-boundary/boundary.md`](trust-boundary/boundary.md). [`trust-boundary/boundary.md`](trust-boundary/boundary.md).
- **Agent trust model, prompt-injection threat model, credential - **Agent trust model, prompt-injection threat model, credential
isolation?** → [`trust-boundary/security.md`](trust-boundary/security.md). isolation?** → [`trust-boundary/security.md`](trust-boundary/security.md).

View file

@ -2,8 +2,8 @@
<!-- vale write-good.Passive = NO --> <!-- vale write-good.Passive = NO -->
Agents are a **flat set**: the capability store scopes which agents can Agents are a **flat set**: the `manage_root_agent` permission scopes
manage which others. which agents can manage which others.
<!-- vale write-good.Passive = YES --> <!-- vale write-good.Passive = YES -->
@ -36,13 +36,13 @@ One entry per agent _this hive_ has state/config for, in name order.
Unlike the swarm roster, it's a derived cache: the reconcile pass below Unlike the swarm roster, it's a derived cache: the reconcile pass below
rebuilds it from what the hive observes locally (config repos cloned, containers spawned), and it rebuilds it from what the hive observes locally (config repos cloned, containers spawned), and it
answers a narrower question than "does this agent exist": _which of answers a narrower question than "does this agent exist": _which of
this hive's local agents the `ManageRootAgent` capability's bind-mounts this hive's local agents the `ManageRootAgent` permission's bind-mounts
should cover._ `topology::all_agents` is the only reader that matters. should cover._ `topology::all_agents` is the only reader that matters.
That reader is a permission boundary: the set it returns is what an agent That reader is a permission boundary: the set it returns is what an agent
holding the `ManageRootAgent` capability gets bind-mounted (each other holding the `ManageRootAgent` permission gets bind-mounted (each other
agent's `state` read-write and `config` read-only; never `harness`). An agent's `state` read-write and `config` read-only; never `harness`). An
agent holding no capability sees its own dirs and nothing else: agent holding no permission sees its own dirs and nothing else:
`ManageRootAgent` is the only grant that reaches another agent's state `ManageRootAgent` is the only grant that reaches another agent's state
dir. See dir. See
[`persistence.md`](persistence.md)'s _Cross-agent access to state._ [`persistence.md`](persistence.md)'s _Cross-agent access to state._
@ -51,7 +51,7 @@ dir. See
`topology.json` may also be a map of `name → parent | null`; the reader `topology.json` may also be a map of `name → parent | null`; the reader
accepts that shape and takes its keys as the roster. An empty accepts that shape and takes its keys as the roster. An empty
roster costs more than a cosmetic gap: every capability holder loses its roster costs more than a cosmetic gap: every permission holder loses its
mounts until the next reconcile pass writes the array form. mounts until the next reconcile pass writes the array form.
### Why meta, not per-agent `agent.nix` ### Why meta, not per-agent `agent.nix`
@ -79,7 +79,7 @@ module docs for the exact call chain.
## Hard-coded manager behaviour ## Hard-coded manager behaviour
The **manager (`ruth`)** differs from other agents in naming/bootstrap, The **manager (`ruth`)** differs from other agents in naming/bootstrap,
its socket flavour and its default capability grant; its prompt, tool its socket flavour and its default permissions; its prompt, tool
allow-list and state dirs work as for every other agent. A hive with allow-list and state dirs work as for every other agent. A hive with
`services.hyperhive.ruthless = true` runs no manager: `hive-c0re` skips `services.hyperhive.ruthless = true` runs no manager: `hive-c0re` skips
its root-agent create/start sweep its root-agent create/start sweep
@ -107,7 +107,7 @@ apply.
manage any agent's state dir — config isn't authored there, since a manage any agent's state dir — config isn't authored there, since a
real config change is a PR from a clone), plus RO mounts for real config change is a PR from a clone), plus RO mounts for
`/applied` (diff against what's deployed) and `/meta` (system-wide `/applied` (diff against what's deployed) and `/meta` (system-wide
deploy log). That grant is the `ManageRootAgent` capability: ruth deploy log). That grant is the `manage_root_agent` permission: ruth
holds it by default, and any agent holding it gets the same mounts. holds it by default, and any agent holding it gets the same mounts.
The operator dashboard's `POST /api/meta-update` is the only path The operator dashboard's `POST /api/meta-update` is the only path
that writes `flake.lock`. that writes `flake.lock`.
@ -118,11 +118,10 @@ apply.
`hive-agent/src/prompt.rs`'s own module doc). The `role:manager` `hive-agent/src/prompt.rs`'s own module doc). The `role:manager`
blocks are dead in production, exercised only by a unit test blocks are dead in production, exercised only by a unit test
(`filter_role_blocks(SAMPLE, "manager")`). (`filter_role_blocks(SAMPLE, "manager")`).
- **Tool allow-list** — the MCP tools claude sees come from - **Tool allow-list** — the MCP tools claude sees come from the agent's
`HIVE_TOOL_GROUPS` alone, the same mechanism for every agent permissions alone, the same mechanism for every agent
([`docs/turn-loop/mcp.md`](../turn-loop/mcp.md)). Ruth's wider default ([`docs/turn-loop/mcp.md`](../turn-loop/mcp.md)). Ruth's default
surface is a wider default grant (`ToolGroup::MANAGER_DEFAULT`, seeded permissions add only `manage_root_agent` (`Permission::default_for`).
by `auto_update.rs` whenever its groups aren't already set).
- **State dirs** — _not_ special-cased: `HYPERHIVE_STATE_DIR` is - **State dirs** — _not_ special-cased: `HYPERHIVE_STATE_DIR` is
injected uniformly via `systemd.globalEnvironment` for every injected uniformly via `systemd.globalEnvironment` for every
container including the manager, so all token/state paths resolve container including the manager, so all token/state paths resolve

View file

@ -244,7 +244,7 @@ bind-mounted **read-only** (`hive-c0re/src/lifecycle/host_config.rs`
calls `bind_child_agent_dirs` for each entry in calls `bind_child_agent_dirs` for each entry in
`topology::all_agents()`). it's a copy to *read* another agent's `topology::all_agents()`). it's a copy to *read* another agent's
current config — not an editing surface. An agent without the current config — not an editing surface. An agent without the
capability sees no other agent's config at all. permission sees no other agent's config at all.
An agent submits a change to another agent's config the same way it An agent submits a change to another agent's config the same way it
makes any other change: **clone the target agent's config repo from the makes any other change: **clone the target agent's config repo from the
@ -252,31 +252,29 @@ forge into its own state dir, commit on a branch, open a PR**, and let
the operator review and merge it. By design, no second, mount-shaped the operator review and merge it. By design, no second, mount-shaped
path reaches the same file without the review. path reaches the same file without the review.
Agents holding the `manage_root_agent` capability (granted per agent in Agents holding the `manage_root_agent` permission (granted per agent in
`capabilities.json`; see `hive-sh4re/src/permissions.rs`) get additional the swarm's `agent-permissions` record; see
host-side bind mounts via `set_nspawn_flags`: `docs/process/conventions.md#permissions`) get additional host-side bind
mounts via `set_nspawn_flags`:
- `/var/lib/hyperhive/agents/` → `/agents/` (RW) — **every** agent's - `/var/lib/hyperhive/agents/` → `/agents/` (RW) — **every** agent's
proposed repo, not just direct children. The capability means "may proposed repo, not just direct children. The permission means "may
manage any agent", so the mounted set is every agent. manage any agent", so the mounted set is every agent.
- `/var/lib/hyperhive/applied/` → `/applied/` (RO) — every agent's - `/var/lib/hyperhive/applied/` → `/applied/` (RO) — every agent's
authoritative applied repo, including `.git`. authoritative applied repo, including `.git`.
- `/var/lib/hyperhive/meta/` → `/meta/` (RO) — the swarm-wide - `/var/lib/hyperhive/meta/` → `/meta/` (RO) — the swarm-wide
deploy flake. deploy flake.
An agent without the capability only has its direct children's config An agent without the permission only has its direct children's config
dirs. dirs.
⚠️ nspawn bakes bind flags at container start, so granting or ⚠️ nspawn bakes bind flags at container start, so hive-c0re restarts
revoking this capability doesn't change any mount until that agent's an agent, after its current turn, when its `manage_root_agent` changes.
container is rebuilt/restarted.
The root agent gets the capability by default, seeded on its autodeploy The root agent holds the permission by default: an agent whose record
path (`workers::auto_update::ensure_root_agent`) so the recovery mounts leaves `permissions` unset gets the default, which includes
are there from its first container. That seed only fires while `manage_root_agent` for `ruth` and for no other agent. Setting her
`capabilities.json` doesn't exist yet: any grant or revoke through permissions without it revokes it.
the dashboard creates the file, so a revoked root-agent grant stays
revoked and isn't re-applied on the next hive-c0re restart.
Each proposed repo (`/agents/<n>/config/`) is pre-configured Each proposed repo (`/agents/<n>/config/`) is pre-configured
with `applied` as a git remote pointing at with `applied` as a git remote pointing at
@ -351,8 +349,8 @@ updates the root agent itself.
The system prompt (`hive-agent/prompts/system.md`, rendered by The system prompt (`hive-agent/prompts/system.md`, rendered by
`hive-agent/src/prompt.rs`) is the **same for every agent**; what `hive-agent/src/prompt.rs`) is the **same for every agent**; what
varies is which MCP tools it surfaces (gated by tool groups and varies is which MCP tools it surfaces (gated by the agent's permissions,
capabilities in `agent.nix`). No `role:manager` block renders only which the swarm sets). No `role:manager` block renders only
for the root agent. The root agent's role-specific for the root agent. The root agent's role-specific
behaviour comes from its CLAUDE.md / agent-specific instructions, not behaviour comes from its CLAUDE.md / agent-specific instructions, not
the system prompt template. the system prompt template.

View file

@ -329,14 +329,14 @@ Under `/var/lib/hyperhive/agents/<name>/`:
### Cross-agent access to state ### Cross-agent access to state
An agent holding the `ManageRootAgent` capability gets every other An agent holding the `manage_root_agent` permission gets every other
agent's `state` dir bind-mounted **read-write** and its `config` dir agent's `state` dir bind-mounted **read-write** and its `config` dir
**read-only** (`bind_child_agent_dirs` in `lifecycle/host_config.rs`). **read-only** (`bind_child_agent_dirs` in `lifecycle/host_config.rs`).
The RW on `state` is deliberate, not an oversight: the holder recovers The RW on `state` is deliberate, not an oversight: the holder recovers
other agents, which includes writing into their state (for example other agents, which includes writing into their state (for example
seeding notes, clearing a stuck sentinel) as well as reading it. seeding notes, clearing a stuck sentinel) as well as reading it.
This is the **only** cross-agent mount. An agent holding no capability This is the **only** cross-agent mount. An agent without that permission
sees its own dirs and nothing else. sees its own dirs and nothing else.
**`harness` isn't mounted at all.** It holds that agent's own runtime **`harness` isn't mounted at all.** It holds that agent's own runtime
@ -364,7 +364,7 @@ mount should be writable when it shouldn't.
<!-- vale write-good.Passive = YES --> <!-- vale write-good.Passive = YES -->
Isolation still holds by default: a container has its *own* dirs and, Isolation still holds by default: a container has its *own* dirs and,
unless it holds the capability, nothing else. unless it holds the permission, nothing else.
Under `/var/lib/hyperhive/applied/<name>/` — the hive-c0re-only Under `/var/lib/hyperhive/applied/<name>/` — the hive-c0re-only
applied repo. Tracks `flake.nix` (module-only boilerplate; never applied repo. Tracks `flake.nix` (module-only boilerplate; never
@ -384,17 +384,9 @@ Contents:
audit trail (one commit per successful deploy or hyperhive bump). audit trail (one commit per successful deploy or hyperhive bump).
- `topology.json` — the agent roster (`["alice", "bob", "ruth"]`). - `topology.json` — the agent roster (`["alice", "bob", "ruth"]`).
Written by `topology::reconcile` on every meta sync; read by Written by `topology::reconcile` on every meta sync; read by
`topology::all_agents`, which is the set the `ManageRootAgent` `topology::all_agents`, which is the set the `manage_root_agent`
capability grants mounts over. The reader also accepts a map-shaped permission grants mounts over. The reader also accepts a map-shaped
file carrying a `parent` per agent, and keeps its keys. file carrying a `parent` per agent, and keeps its keys.
- `tool-groups.json` — per-agent MCP tool group grants
(`{ "alice": ["messaging", "inbox", "execution"] }`). Written by
`tool_groups::set_groups`; injected as `HIVE_TOOL_GROUPS` env
var into each agent's container.
- `capabilities.json` — per-agent capability grants
(`{ "ruth": ["manage_root_agent"] }`). Written by
`capabilities::set_caps`; injected as `HIVE_CAPABILITIES` env
var. Absent agents have no extra capabilities.
- `swarm-resource-limits.json` — the caps the swarm sets for this - `swarm-resource-limits.json` — the caps the swarm sets for this
hive's agents hive's agents
(`{ "sock": { "cpu_quota": "400%", "memory_max": "8G" } }`). (`{ "sock": { "cpu_quota": "400%", "memory_max": "8G" } }`).
@ -418,6 +410,15 @@ Contents:
The root agent has the meta dir RO-mounted at `/meta/`. The root agent has the meta dir RO-mounted at `/meta/`.
Outside the meta repo, `/var/lib/hyperhive/swarm-agent-permissions.json`
holds the permissions the swarm grants this hive's agents, as
`workers::swarm_permissions` last read them from the swarm's
`agent-permissions` bucket (every 30 seconds): a copy, not the
record, which `swarmctl agent permissions` sets. `set_nspawn_flags`
reads it at every container start for `manage_root_agent`. Each agent gets
its own entry from it as `swarm-permissions.json` in its runtime dir, which
it holds until its own first read of its key succeeds.
hive-c0re renders containers onto `meta#<n>` at creation. hive-c0re renders containers onto `meta#<n>` at creation.
## Destroy vs purge ## Destroy vs purge

View file

@ -176,7 +176,7 @@ it directly as a bulleted list, no follow-up fetch needed.
`GetLooseEnds` takes no target — it always returns the caller's own `GetLooseEnds` takes no target — it always returns the caller's own
rows, on both the agent and manager sockets alike (the manager, `ruth`, rows, on both the agent and manager sockets alike (the manager, `ruth`,
is a normal agent here, just deployed automatically with different is a normal agent here, just deployed automatically with different
default capabilities). `Reminder` rows always carry `owner == self`. default permissions). `Reminder` rows always carry `owner == self`.
Per-variant fields: Per-variant fields:
@ -272,97 +272,83 @@ JetStream permission refusal, a TLS chain failure) rather than a summary.
Not in scope: the `hivectl` host-admin and in-agent unix sockets. Those are a Not in scope: the `hivectl` host-admin and in-agent unix sockets. Those are a
JSON-line protocol with their own result types; RFC 9457 is an HTTP format. JSON-line protocol with their own result types; RFC 9457 is an HTTP format.
## Tool groups ## Permissions
A set of named `ToolGroup` values (`hive_sh4re::permissions::ToolGroup`) One set of named `Permission` values (`hive_sh4re::permissions::Permission`)
determines the MCP tool surface an agent receives, not a hardcoded decides what an agent may do: which MCP and built-in tools it gets, and which
binary flavor. host access it has.
| Group | Tools | | Permission | Grants |
|---|---| |---|---|
| `messaging` | `send`, `recv`, `ack_until` | | `messaging` | `send`, `recv`, `ack_until` |
| `meta` | `get_agent_meta` (`set_status` is always-on, see below) | | `meta` | `get_agent_meta` (`set_status` is always-on, see below) |
| `inbox` | `get_loose_ends`, `cancel_loose_end`, `remind` | | `inbox` | `get_loose_ends`, `cancel_loose_end`, `remind` |
| `execution` | vestigial — `mcp__bash__run` / `mcp__bash__status` are always available unconditionally via `extraMcpServers`; this group's entries expand to non-existent `mcp__hyperhive__run` / `mcp__hyperhive__status` and have no effect. See `docs/tools/bash.md`. | | `execution` | the `bash` MCP server (`mcp__bash__run`/`status`/`kill`), rendered into the agent's MCP config only with it; a subagent gets Claude's own `Bash` instead. See `docs/tools/bash.md`. |
| `lifecycle` | none | | `web_tools` | the Claude built-ins `WebFetch`/`WebSearch` |
| `forge` | none | | `manage_root_agent` | every agent's state (rw) + config (ro) bind-mounted into the holder's container, plus `/applied` and `/meta` (ro) |
| `web_tools` | none (gates the Claude built-ins `WebFetch`/`WebSearch`, not an MCP tool) |
**Always-on tools** — `ToolGroup::ALWAYS_ON_TOOLS` exposes `set_status`, **Always-on tools** — `Permission::ALWAYS_ON_TOOLS` exposes `set_status`,
`compact`, and `mark_todos_done` to every agent regardless of which `compact`, and `mark_todos_done` to every agent regardless of its
groups it holds. The operator dashboard depends on every agent permissions. The operator dashboard depends on every agent being able to
being able to report its status chip, and the server-side `SetStatus` handler report its status chip, and the server-side `SetStatus` handler has no
has no tool-group check (only length validation), so gating it would only permission check (only length validation), so gating it would only desync
desync the `--allowedTools` list from what the host actually accepts. the `--allowedTools` list from what the host actually accepts. Revoking
Revoking `meta` therefore drops `get_agent_meta` but never `set_status`. `meta` therefore drops `get_agent_meta` but never `set_status`.
`mark_todos_done` is here because producers push todos to an agent independent of `mark_todos_done` is here because producers push todos to an agent whether
whether it holds `inbox` — an agent without that group still needs a way to or not it holds `inbox`, and an agent without it still needs to clear them.
clear them.
**Config storage** — per-agent tool groups live in **Where they live** — in the swarm, not on the hive: the `permissions` field of
`/var/lib/hyperhive/meta/tool-groups.json` (hive-c0re-owned, committed to the the agent's key in the `agent-permissions` KV bucket
meta repo alongside `topology.json`). Format: `{ "alice": ["messaging", "meta", (`swarm-queue-client/src/agent_permissions.rs`), which swarm-controller alone
"inbox", "lifecycle"], "bob": ["messaging", "meta", "inbox"] }`. An absent entry writes. Set them with `swarmctl agent permissions get|set|clear <agent>`
means "use role default." Tool permissions are intentionally NOT configurable (`/api/agents/{name}/permissions`), or on the swarm UI's create-agent screen
from `agent.nix` — that file changes through config PRs the manager opens, so (`permissions` in `POST /api/agents`). A missing key or an unset field means
letting it declare its own groups would let the manager grant itself any tool by the default (`Permission::default_for`): `messaging`, `meta`, `inbox`,
submitting a config commit, bypassing the operator gate. `execution`, plus `manage_root_agent` for `ruth`. `"permissions": []` grants
none.
**Setting groups** — the operator sets groups via the dashboard or **How a change applies** —
`hive-c0re::tool_groups::set_groups(name, groups)`. After a change - The agent's harness reads its own key every 10 seconds and builds every
`meta::sync_agents` commits the updated file; the next agent rebuild picks up turn's `--tools`, `--allowedTools` and MCP config from the last value, so a
the new `HIVE_TOOL_GROUPS` env var. Agents with no entry get no var. change reaches the next turn. An ACP agent's permission check reads it on
each request.
- The subagent daemon reads the key on each `start`/`continue`; a running
subagent keeps its tools.
- hive-c0re reads each of its agents' keys every 30 seconds
(`workers/swarm_permissions.rs`) into
`/var/lib/hyperhive/swarm-agent-permissions.json`, outside the meta repo.
nspawn fixes the bind mounts at container start, so when an agent's
`manage_root_agent` changes hive-c0re restarts it after its current turn.
- hive-c0re copies each agent's entry from that file into the agent's
runtime dir (`/run/hive/swarm-permissions.json` in the container) at every
start and after every 30-second pass, with whether the import below is
still pending. The harness and the subagent daemon hold that copy until
their own first read succeeds, so an agent whose queue is down starts on
its last stored grant, not the default.
**Runtime resolution** — at session start the harness reads `HIVE_TOOL_GROUPS` Every reader keeps its last good value on a failed read.
(a comma-separated list of snake_case group names injected by the meta renderer
from `tool-groups.json`), logging and skipping unrecognised tokens. Falls back
to `ToolGroup::AGENT_DEFAULT` (`messaging`, `meta`, `inbox`, `execution`) when
the var is absent or empty.
**Updating the surface** — when you add a new `#[tool]` fn to `AgentServer` **Moving off the meta repo** — on its first boot with this code a hive copies
in `hive-agent-mcp/src/mcp/mod.rs`, add its name to the matching `ToolGroup::tools()` `meta/capabilities.json` and `meta/tool-groups.json` into the stored value,
slice in `hive-sh4re/src/permissions.rs`. That's the single source of truth; sends them to swarm-controller once as one list per agent (without
`mcp_config::allowed_mcp_tools` (in `hive-agent/src/mcp_config.rs`) reads it at `lifecycle` and `forge`, which gave no tools; the controller writes a key
session start. only where there is none, and only for agents that hive's wanted state
declares), and deletes
both files from the meta repo when the controller answers
(`workers/permissions_import.rs`). Until then hive-c0re reads no keys, and an
agent that reads its own key unset keeps the grant in its copy.
## Capabilities **Not configurable from `agent.nix`** — that file changes through config PRs
an agent can open, so letting it declare its own permissions would let an
agent grant itself any tool by submitting a config commit, bypassing the
operator gate.
Capabilities gate system-level access that goes beyond the MCP tool surface — **Adding to the set** — add a variant to `Permission` in
things an agent can *access*, not just *call*. Parallel to tool groups but `hive-sh4re/src/permissions.rs` and to `Permission::ALL`, and enforce it where
orthogonal: an agent can have a tool group that registers a tool AND a capability it applies. A new `#[tool]` fn on `AgentServer` (`hive-agent-mcp/src/mcp/mod.rs`)
that allows the underlying resource access. goes into the matching `Permission::tools()` slice: that's the single source
of truth `mcp_config::allowed_mcp_tools` (`hive-agent/src/mcp_config.rs`)
| Capability | Effect | reads each turn.
|---|---|
| `manage_root_agent` | may manage *any* agent: bind-mounts every agent's state (rw) + config (ro) into the holder's container, plus `/applied` and `/meta` (ro). Takes effect on the holder's next container rebuild/restart |
**Config storage** — per-agent capabilities live in
`/var/lib/hyperhive/meta/capabilities.json` alongside `tool-groups.json`.
Format: `{ "ruth": ["manage_root_agent"] }`.
An absent entry means "no extra capabilities." `render_flake` in `meta.rs`
reads this file and injects `HIVE_CAPABILITIES` (comma-separated
`snake_case` names) into each agent's systemd service env; absent entries emit
no env var so agents without capabilities don't trigger a spurious rebuild.
**Setting capabilities** — the operator sets capabilities via the
C4P4B1L1T13S section in the dashboard's P3RM1SS10NS tab.
`hive-c0re::capabilities::set_caps(name, caps)` is the write path.
After a change `meta::sync_agents` commits the updated file; the next agent
rebuild picks up the new `HIVE_CAPABILITIES` env var.
**Runtime resolution** — at session start the harness reads `HIVE_CAPABILITIES`
and resolves each token to a `Capability` variant, logging and skipping
unrecognised ones. An absent or empty var means no extra capabilities.
**Capability NOT configurable from `agent.nix`** — same reasoning as tool
groups: an agent that could grant its own capabilities via a config commit would
bypass the operator approval gate.
**Adding a new capability** — add a variant to `Capability` in
`hive-sh4re/src/permissions.rs` + an arm to `as_str`. Add it to `Capability::ALL`
(the source of truth for the permissions UI columns). Implement the access
check in the relevant handler (`hive-c0re/src/socket_server/mod.rs`,
`coordinator.rs`, or a handler under `hive-c0re/src/dashboard/`).
## Async forms ## Async forms

View file

@ -71,9 +71,8 @@ Cheap — no build slot:
| `PurgeState` | the `purge = true` half of a destroy: delete the agent's state subvolume (via hive-priv) plus its state/applied dirs. Own node because it's conditional and the irreversible step | | `PurgeState` | the `purge = true` half of a destroy: delete the agent's state subvolume (via hive-priv) plus its state/applied dirs. Own node because it's conditional and the irreversible step |
| `DestroyBookkeeping` | the post-destroy tail — meta sync, drop the power intent, notify the manager, rescan, re-emit the tombstone. Same split rationale as `RebuildBookkeeping`/`Swap`. The destructive work is `PurgeState`'s | | `DestroyBookkeeping` | the post-destroy tail — meta sync, drop the power intent, notify the manager, rescan, re-emit the tombstone. Same split rationale as `RebuildBookkeeping`/`Swap`. The destructive work is `PurgeState`'s |
| `SetWanted` | write the durable power intent (`wanted = Up`/`Offline`) as the head node of a power-op DAG. Takes the agent lease even though it's a store write, so the intent write and the tail `Reconcile` are atomic per-agent — two racing power ops can't clobber each other's intent before either reconciles | | `SetWanted` | write the durable power intent (`wanted = Up`/`Offline`) as the head node of a power-op DAG. Takes the agent lease even though it's a store write, so the intent write and the tail `Reconcile` are atomic per-agent — two racing power ops can't clobber each other's intent before either reconciles |
| `EmitRebuilt` | tail of a rebuild/perm-change — emit the agent's `Rebuilt` manager event (ok/fail per outcome, nothing on cancel). One node per agent _and_ per outcome | | `EmitRebuilt` | tail of a rebuild — emit the agent's `Rebuilt` manager event (ok/fail per outcome, nothing on cancel). One node per agent _and_ per outcome |
| `WriteDropin` | `set_nspawn_flags` + `set_resource_limits` + daemon-reload | | `WriteDropin` | `set_nspawn_flags` + `set_resource_limits` + daemon-reload |
| `WritePermFile` | commit `tool-groups.json` / `capabilities.json` (single git commit under `META_LOCK`) + emit the P3RM1SS10NS snapshots |
| `ForgeSweep` | one-shot boot-time forge user/token sweep for every container (`forge::ensure_all`) as a first-class node, so it shows as real work on the dashboard instead of running invisibly in a bare `tokio::spawn`. Agentless | | `ForgeSweep` | one-shot boot-time forge user/token sweep for every container (`forge::ensure_all`) as a first-class node, so it shows as real work on the dashboard instead of running invisibly in a bare `tokio::spawn`. Agentless |
| `MatrixSweep` | matrix user/space sweep (`matrix::ensure_all`): the boot-time instance, plus one every 30 min from a loop in `main.rs`. Holds `Resource::MatrixSweep` (capacity 1), so two passes never overlap; each tick queues its own pass, which waits for the resource if one is already live. Agentless | | `MatrixSweep` | matrix user/space sweep (`matrix::ensure_all`): the boot-time instance, plus one every 30 min from a loop in `main.rs`. Holds `Resource::MatrixSweep` (capacity 1), so two passes never overlap; each tick queues its own pass, which waits for the resource if one is already live. Agentless |
| `KnowledgePull` | `/knowledge` pull (`knowledge::pull`): at boot (commits that landed while `hive-c0re` was down), on the swarm knowledge-changed event, and hourly as a fallback. Holds `Resource::KnowledgeTree` (capacity 1), so two pulls never overlap on the working tree; each trigger queues its own pass, which waits for the resource if one is already live. Agentless | | `KnowledgePull` | `/knowledge` pull (`knowledge::pull`): at boot (commits that landed while `hive-c0re` was down), on the swarm knowledge-changed event, and hourly as a fallback. Holds `Resource::KnowledgeTree` (capacity 1), so two pulls never overlap on the working tree; each trigger queues its own pass, which waits for the resource if one is already live. Agentless |
@ -90,7 +89,7 @@ Two further layers protect the meta repo across _windows_ that span multiple
- **The deploy window** (`Resource::MetaWindow`): a global, capacity-1 queue - **The deploy window** (`Resource::MetaWindow`): a global, capacity-1 queue
resource declared by every node kind that mutates the meta repo — `MetaSync`, resource declared by every node kind that mutates the meta repo — `MetaSync`,
`MetaLock`, `WritePermFile` and `Provision`'s agent registration. Two meta `MetaLock` and `Provision`'s agent registration. Two meta
mutations can therefore never interleave, so no commit lands inside another mutations can therefore never interleave, so no commit lands inside another
node's window. It's a queue resource rather than a runtime mutex node's window. It's a queue resource rather than a runtime mutex
because a subtree root holds a resource across its whole subtree, which because a subtree root holds a resource across its whole subtree, which
@ -139,7 +138,6 @@ restart(a..): online a: [Signal→Drain→ if graceful] StopForUpdate(a) → Re
offline a: Reconcile(a) (nothing to stop; Reconcile converges to existing wanted) offline a: Reconcile(a) (nothing to stop; Reconcile converges to existing wanted)
start(a..): a: SetWanted(a,Up) → Reconcile(a) (down+stale ⇒ SetWanted(a,Up) → «rebuild subgraph») start(a..): a: SetWanted(a,Up) → Reconcile(a) (down+stale ⇒ SetWanted(a,Up) → «rebuild subgraph»)
spawn(a): [wanted=Up at deploy] Provision(a) → Create(a) → WriteDropin(a) → Reconcile(a) spawn(a): [wanted=Up at deploy] Provision(a) → Create(a) → WriteDropin(a) → Reconcile(a)
perm-change(a): WritePermFile(a) → «rebuild subgraph»
meta-update(inp): MetaLock(inp) →(in-DAG) «rebuild subgraph» per affected agent meta-update(inp): MetaLock(inp) →(in-DAG) «rebuild subgraph» per affected agent
boot: (if any rev marker stale) MetaLock(hyperhive) →(in-DAG) «rebuild subgraph» per stale agent; boot: (if any rev marker stale) MetaLock(hyperhive) →(in-DAG) «rebuild subgraph» per stale agent;
plus Reconcile(a) for every drifted agent (all ONE DAG) plus Reconcile(a) for every drifted agent (all ONE DAG)
@ -215,7 +213,7 @@ resources are free. Resources:
(`SetWanted` is a store write, not a container op, but takes the lease anyway (`SetWanted` is a store write, not a container op, but takes the lease anyway
so a power-op DAG's intent write + reconcile is atomic — two racing ops can't so a power-op DAG's intent write + reconcile is atomic — two racing ops can't
clobber intent before either reconciles.) **Lease-exempt**: `MetaSync`, clobber intent before either reconciles.) **Lease-exempt**: `MetaSync`,
`Prebuild`, `Provision`, `MetaLock`, `WritePermFile` — they `Prebuild`, `Provision`, `MetaLock` — they
touch the store / meta, not the running container, which is exactly why a touch the store / meta, not the running container, which is exactly why a
stop can land while another DAG's prebuild is still building. Also exempt, for a different stop can land while another DAG's prebuild is still building. Also exempt, for a different
reason, are the rebuild subtree's own members (`StopForUpdate`, `Swap`, reason, are the rebuild subtree's own members (`StopForUpdate`, `Swap`,

View file

@ -8,10 +8,10 @@ domain, covers host-level detail for one hive.
## What it shows ## What it shows
| route | what | | route | what |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `/` | the start page: the swarm's name over a digital-rain backdrop; the swarm name in the header links here | | `/` | the start page: the swarm's name over a digital-rain backdrop; the swarm name in the header links here |
| `/hives` | the hive directory, each hive with its last reported status | | `/hives` | the hive directory, each hive with its last reported status |
| `/agents` | every agent: status, config PR, wanted state, inference settings, resource caps; create agents, link forge, matrix and GitHub accounts | | `/agents` | every agent: status, config PR, wanted state, inference settings, resource caps; create agents (with their permissions), link forge, matrix and GitHub accounts |
| `/agents/<name>/terminal` | one agent's live terminal | | `/agents/<name>/terminal` | one agent's live terminal |
| `/agents/<name>/subagents/<subagent>/terminal` | one of that agent's subagents' live terminal | | `/agents/<name>/subagents/<subagent>/terminal` | one of that agent's subagents' live terminal |
| `/alerts` | the alerts the swarm's Grafana holds now (firing, pending, no data, error), read through the controller with a Viewer service-account token | | `/alerts` | the alerts the swarm's Grafana holds now (firing, pending, no data, error), read through the controller with a Viewer service-account token |

View file

@ -7,11 +7,12 @@ by default (`lib.mkDefault`, with `allowedTools = ["*"]`) — an `agent.nix`
can override or drop the entry, so this is a default, not an can override or drop the entry, so this is a default, not an
unconditional grant. Even where the entry is present, the server is only unconditional grant. Even where the entry is present, the server is only
actually rendered into an agent's claude config when the agent also actually rendered into an agent's claude config when the agent also
holds the **`execution`** tool group: `extra_server_required_group` in holds the **`execution`** permission: `extra_server_required_permission` in
`hive-agent/src/mcp_config.rs` suppresses it entirely without that `hive-agent/src/mcp_config.rs` suppresses it entirely without that
group, so an agent lacking `execution` gets no `mcp__bash__*` tools at permission, so an agent lacking `execution` gets no `mcp__bash__*` tools at
all, not just an unused grant. See "Relationship to the `execution` tool all, not just an unused grant. The harness rewrites the config each turn,
group" below. so a change applies at the next one. See "Relationship to the
`execution` permission" below.
## Tools ## Tools
@ -122,14 +123,14 @@ unread rooms. An inline `wait_seconds` / `status` observation that already
delivered the result instead clears the keyed todo, so no redundant delivered the result instead clears the keyed todo, so no redundant
loose-end follows. loose-end follows.
## Relationship to the `execution` tool group ## Relationship to the `execution` permission
`ToolGroup::Execution` (in `AGENT_DEFAULT`) is the real gate on bash: `Permission::Execution` (in `Permission::DEFAULT`) is the real gate on bash:
`extra_server_required_group` (in `hive-agent/src/mcp_config.rs`) `extra_server_required_permission` (in `hive-agent/src/mcp_config.rs`)
controls whether the `bash` MCP server reaches the agent's claude controls whether the `bash` MCP server reaches the agent's claude
config at all, suppressing every `mcp__bash__*` tool config at all, suppressing every `mcp__bash__*` tool
when absent — not just leaving them unused. Its own `tools()` returns when absent — not just leaving them unused. Its own `tools()` returns
`&[]`: `mcp__bash__run`/`status`/`kill` live on the separate `bash` MCP `&[]`: `mcp__bash__run`/`status`/`kill` live on the separate `bash` MCP
server that `extra_server_required_group` gates, not on the server that `extra_server_required_permission` gates, not on the
`mcp__hyperhive__*` allowlist `tools()` builds. Removing `execution` `mcp__hyperhive__*` allowlist `tools()` builds. Removing `execution`
from an agent's groups **does** remove bash availability. from an agent's permissions **does** remove bash availability.

View file

@ -399,23 +399,24 @@ particular `--tools` does **not** filter `mcp__*` tools.
**A subagent gets exactly the built-ins its parent agent has, plus one **A subagent gets exactly the built-ins its parent agent has, plus one
deliberate exception** — the base resolution deliberate exception** — the base resolution
(`hive_sh4re::permissions::subagent_builtin_tools_arg`, wrapping the (`hive_sh4re::permissions::subagent_builtin_tools_for`, wrapping the
harness's own `builtin_tools_for`) from the same `HIVE_TOOL_GROUPS`: harness's own `builtin_tools_for`) over the agent's permissions, which the
daemon reads from the swarm on each `start`/`continue`:
`Edit`, `Glob`, `Grep`, `Read`, `Skill`, `Write`, plus `WebFetch`/ `Edit`, `Glob`, `Grep`, `Read`, `Skill`, `Write`, plus `WebFetch`/
`WebSearch` for an agent granted the `web_tools` tool group and not `WebSearch` for an agent granted `web_tools` and not otherwise. A running
otherwise. See subagent keeps the tools it started with. See
[the harness's own allowlist](../turn-loop/mcp.md#tool-allowlist-hive_sh4repermissionsallowed_builtin_tools) [the harness's own allowlist](../turn-loop/mcp.md#tool-allowlist-hive_sh4repermissionsallowed_builtin_tools)
for what that list contains and why. for what that list contains and why.
The exception: **a subagent gets Claude's built-in `Bash` iff its parent The exception: **a subagent gets Claude's built-in `Bash` iff its parent
holds the `execution` tool group** — the group that already grants the holds the `execution` permission** — the permission that already grants the
parent shell execution, via the out-of-process `bash` MCP server parent shell execution, via the out-of-process `bash` MCP server
(`mcp__bash__run`/`status`/`kill`). A subagent has no MCP server of its (`mcp__bash__run`/`status`/`kill`). A subagent has no MCP server of its
own by default (see [MCP servers](#mcp-servers---strict-mcp-config) own by default (see [MCP servers](#mcp-servers---strict-mcp-config)
below), so there is no `mcp__bash__*` for it to inherit that capability below), so there is no `mcp__bash__*` for it to inherit that capability
through; it gets Claude's own tool instead. The rule is the capability through; it gets Claude's own tool instead. The rule is the capability
transfers, not the mechanism — the parent itself never gains built-in transfers, not the mechanism — the parent itself never gains built-in
`Bash` no matter which groups it holds, only a subagent spawned by an `Bash` no matter which permissions it holds, only a subagent spawned by an
`execution`-holding parent does. `execution`-holding parent does.
Inheriting rather than listing is the point: a hardcoded subagent list Inheriting rather than listing is the point: a hardcoded subagent list

View file

@ -12,6 +12,10 @@ This document contains the help content for the `swarmctl` command-line program.
* [`swarmctl agent inference get`↴](#swarmctl-agent-inference-get) * [`swarmctl agent inference get`↴](#swarmctl-agent-inference-get)
* [`swarmctl agent inference set`↴](#swarmctl-agent-inference-set) * [`swarmctl agent inference set`↴](#swarmctl-agent-inference-set)
* [`swarmctl agent inference clear`↴](#swarmctl-agent-inference-clear) * [`swarmctl agent inference clear`↴](#swarmctl-agent-inference-clear)
* [`swarmctl agent permissions`↴](#swarmctl-agent-permissions)
* [`swarmctl agent permissions get`↴](#swarmctl-agent-permissions-get)
* [`swarmctl agent permissions set`↴](#swarmctl-agent-permissions-set)
* [`swarmctl agent permissions clear`↴](#swarmctl-agent-permissions-clear)
* [`swarmctl schedule`↴](#swarmctl-schedule) * [`swarmctl schedule`↴](#swarmctl-schedule)
* [`swarmctl schedule add`↴](#swarmctl-schedule-add) * [`swarmctl schedule add`↴](#swarmctl-schedule-add)
* [`swarmctl schedule list`↴](#swarmctl-schedule-list) * [`swarmctl schedule list`↴](#swarmctl-schedule-list)
@ -66,6 +70,7 @@ Manage agents across the swarm
* `create` — Queue creation of a new agent on a hive in this swarm * `create` — Queue creation of a new agent on a hive in this swarm
* `mint-forge-token` — Check one agent's forge token, and mint it if it's missing or stale * `mint-forge-token` — Check one agent's forge token, and mint it if it's missing or stale
* `inference` — Show the model, effort, subagent models and subagent cap the swarm sets for an agent, or change the model, effort and subagent models * `inference` — Show the model, effort, subagent models and subagent cap the swarm sets for an agent, or change the model, effort and subagent models
* `permissions` — Show or change the permissions the swarm grants an agent
@ -198,6 +203,77 @@ Clear every field for an agent, so it goes back to its own values
## `swarmctl agent permissions`
Show or change the permissions the swarm grants an agent.
The agent applies its tool permissions at its next turn; the hive running it restarts it, after its current turn, when `manage_root_agent` changes. Unset leaves the agent on the default: `messaging`, `meta`, `inbox`, `execution`, plus `manage_root_agent` for `ruth`.
**Usage:** `swarmctl agent permissions <COMMAND>`
###### **Subcommands:**
* `get` — Show the permissions the swarm grants an agent, or that it sets none
* `set` — Replace the permissions the swarm grants an agent
* `clear` — Clear the agent's permissions, so the default applies again
## `swarmctl agent permissions get`
Show the permissions the swarm grants an agent, or that it sets none
**Usage:** `swarmctl agent permissions get [OPTIONS] <AGENT>`
###### **Arguments:**
* `<AGENT>` — Agent name
###### **Options:**
* `--controller-socket <PATH>` — swarm-controller's unix socket.
Supplied by the nix module that installs this binary, from the same `socketPath` option the daemon binds; falls back to `SWARM_CONTROLLER_SOCKET`.
## `swarmctl agent permissions set`
Replace the permissions the swarm grants an agent
**Usage:** `swarmctl agent permissions set [OPTIONS] --permissions <PERMISSIONS> <AGENT>`
###### **Arguments:**
* `<AGENT>` — Agent name. A hive must declare it
###### **Options:**
* `--permissions <PERMISSIONS>` — The permissions to grant, comma-separated: any of `messaging`, `meta`, `inbox`, `execution`, `web_tools`, `manage_root_agent`. `--permissions ''` grants none
* `--controller-socket <PATH>` — swarm-controller's unix socket.
Supplied by the nix module that installs this binary, from the same `socketPath` option the daemon binds; falls back to `SWARM_CONTROLLER_SOCKET`.
## `swarmctl agent permissions clear`
Clear the agent's permissions, so the default applies again
**Usage:** `swarmctl agent permissions clear [OPTIONS] <AGENT>`
###### **Arguments:**
* `<AGENT>` — Agent name
###### **Options:**
* `--controller-socket <PATH>` — swarm-controller's unix socket.
Supplied by the nix module that installs this binary, from the same `socketPath` option the daemon binds; falls back to `SWARM_CONTROLLER_SOCKET`.
## `swarmctl schedule` ## `swarmctl schedule`
Manage scheduled prompts. Manage scheduled prompts.

View file

@ -177,7 +177,7 @@ harness between turns.
One harness serve binary (`hive-agent`, with its `hive-agent-mcp` One harness serve binary (`hive-agent`, with its `hive-agent-mcp`
sibling), one shared `nix/agent-modules/` tree, one service unit sibling), one shared `nix/agent-modules/` tree, one service unit
(`systemd.services.hive-agent`) for all agents. Privilege (`systemd.services.hive-agent`) for all agents. Privilege
differences live server-side in the broker socket (which tool groups differences live in the agent's swarm permissions and the broker socket (which tools
and manager-surface calls each agent receives). and manager-surface calls each agent receives).
`agent.nix` and `ruth.nix` both import the shared `nix/agent-modules/`. `agent.nix` and `ruth.nix` both import the shared `nix/agent-modules/`.
@ -250,7 +250,7 @@ The rest lives alongside this page, in three topic files:
custom static files, connectivity overrides, claude plugins, cargo message custom static files, connectivity overrides, claude plugins, cargo message
filtering). filtering).
- **[mcp.md](mcp.md)** — the MCP tool surface claude sees: core tools, - **[mcp.md](mcp.md)** — the MCP tool surface claude sees: core tools,
privileged tool groups, self-wake, authoritative state, the tool envelope, permission-gated tools, self-wake, authoritative state, the tool envelope,
and the built-in tool allowlist. and the built-in tool allowlist.
Per-subsystem impl detail lives in each module's `//!` doc-comment; these pages Per-subsystem impl detail lives in each module's `//!` doc-comment; these pages

View file

@ -22,10 +22,11 @@ toggle (default `false`) — see
[`docs/tools/subagent.md`](../tools/subagent.md#mcp-servers-available-to-a-subagent) [`docs/tools/subagent.md`](../tools/subagent.md#mcp-servers-available-to-a-subagent)
for what reaches a subagent's own `--mcp-config` and what doesn't. for what reaches a subagent's own `--mcp-config` and what doesn't.
Tool groups (`HIVE_TOOL_GROUPS`) gate tool access. The default The agent's permissions gate tool access
preset (`AGENT_DEFAULT`) includes `messaging`, `meta`, `inbox`, and ([`docs/process/conventions.md#permissions`](../process/conventions.md)).
`execution`. The privileged `lifecycle` group The default (`Permission::DEFAULT`) is `messaging`, `meta`, `inbox` and
is opt-in via the P3RM1SS10NS tab. `execution`; `swarmctl agent permissions set` changes it, and the next turn
runs on the change.
## Core tools (always available) ## Core tools (always available)
@ -106,7 +107,7 @@ hive_name?, swarm_name?, matrix_accounts? }`. `matrix_accounts` is a
`homeserver`); omitted for agents with no matrix provisioning. Omit `homeserver`); omitted for agents with no matrix provisioning. Omit
`name` to query self. `name` to query self.
**Always-on, no tool group**: `set_status(text)`, `compact()`, **Always-on, no permission needed**: `set_status(text)`, `compact()`,
`mark_todos_done(ids)`. `mark_todos_done(ids)`.
- `set_status` — set a free-text status string visible on the - `set_status` — set a free-text status string visible on the
@ -130,26 +131,23 @@ hive_name?, swarm_name?, matrix_accounts? }`. `matrix_accounts` is a
unknown or already-done ids are silently skipped. Dials the same unknown or already-done ids are silently skipped. Dials the same
in-agent todo socket as `cancel_loose_end`'s `todo` kind. in-agent todo socket as `cancel_loose_end`'s `todo` kind.
## Privileged tools (by tool group) ## Privileged tools (by permission)
- **Bash execution** (`execution`) — background shell tasks. See - **Bash execution** (`execution`) — background shell tasks. See
[`docs/tools/bash.md`](../tools/bash.md). [`docs/tools/bash.md`](../tools/bash.md).
- **Subagent spawning** — nested sessions on the agent's own runtime - **Subagent spawning** — nested sessions on the agent's own runtime
(claude or ACP, via `hive-runtime`) as background tasks, shipped (claude or ACP, via `hive-runtime`) as background tasks, shipped
default-on like bash execution; no tool group gates it. See default-on like bash execution; no permission gates it. See
[`docs/tools/subagent.md`](../tools/subagent.md). [`docs/tools/subagent.md`](../tools/subagent.md).
- **Lifecycle + config** (`lifecycle`) — gates nothing. - **Config changes** — no permission: they go through a forge PR on
Config changes go through a forge PR on `agent-configs/<name>` — see `agent-configs/<name>` — see
[`docs/agent-lifecycle/config-changes.md`](../agent-lifecycle/config-changes.md). [`docs/agent-lifecycle/config-changes.md`](../agent-lifecycle/config-changes.md).
- **Forge repos** (`forge`) — gates nothing.
- **Web egress** (`web_tools`) — enables Claude's built-in `WebFetch` - **Web egress** (`web_tools`) — enables Claude's built-in `WebFetch`
and `WebSearch` tools (not MCP tools; added directly to the and `WebSearch` tools (not MCP tools; added directly to the
`--allowedTools` list). Off by default; add the group in the `--allowedTools` list). Off by default.
P3RM1SS10NS tab and rebuild to enable. - **Recovery mounts** (`manage_root_agent`) — no tool: every agent's state
- **Capabilities** — orthogonal to tool groups, set via the P3RM1SS10NS and config mounted into this agent's container. See
tab. No capability registers an MCP tool today; the full list and their [`docs/process/conventions.md#permissions`](../process/conventions.md).
effects are in
[`docs/process/conventions.md#capabilities`](../process/conventions.md).
- **Matrix MCP + extra servers** — `mcp__matrix__*` tools and - **Matrix MCP + extra servers** — `mcp__matrix__*` tools and
per-agent extra MCP config. See per-agent extra MCP config. See
[`docs/tools/matrix.md`](../tools/matrix.md). [`docs/tools/matrix.md`](../tools/matrix.md).
@ -199,7 +197,7 @@ status hint lives in the wake prompt + UI header, not here.
## Tool allowlist (`hive_sh4re::permissions::ALLOWED_BUILTIN_TOOLS`) ## Tool allowlist (`hive_sh4re::permissions::ALLOWED_BUILTIN_TOOLS`)
The built-in list and the `--tools` value it resolves to live in The built-in list and the `--tools` value it resolves to live in
`hive-sh4re` alongside `ToolGroup`, not in the harness, because the `hive-sh4re` alongside `Permission`, not in the harness, because the
subagent daemon spawns its own `claude` and must resolve the same set — subagent daemon spawns its own `claude` and must resolve the same set —
see [`docs/tools/subagent.md`](../tools/subagent.md). see [`docs/tools/subagent.md`](../tools/subagent.md).
@ -207,15 +205,15 @@ see [`docs/tools/subagent.md`](../tools/subagent.md).
`Skill` is what makes an installed plugin's `SKILL.md` invokable — `Skill` is what makes an installed plugin's `SKILL.md` invokable —
without it, a skill's `description` frontmatter never gets seen by without it, a skill's `description` frontmatter never gets seen by
the model no matter how well it matches the task. the model no matter how well it matches the task.
- Tool-group-gated built-ins: `WebFetch`, `WebSearch` (added once the - Permission-gated built-ins: `WebFetch`, `WebSearch` (added once the
operator enables the `web_tools` tool group — see P3RM1SS10NS tab). agent holds `web_tools`).
- Denied by omission (absent from the harness `--tools` / - Denied by omission (absent from the harness `--tools` /
`--allowedTools`, so they "literally don't exist" in a harness turn): `--allowedTools`, so they "literally don't exist" in a harness turn):
`Bash`, `Task`, `NotebookEdit`, `TodoWrite`. `Bash`, `Task`, `NotebookEdit`, `TodoWrite`.
- Additionally in the managed-settings deny list - Additionally in the managed-settings deny list
(`/etc/claude-code/managed-settings.json`, un-overridable): `Task`, (`/etc/claude-code/managed-settings.json`, un-overridable): `Task`,
`TodoWrite`. `Bash` **isn't** in the managed deny — see below. `TodoWrite`. `Bash` **isn't** in the managed deny — see below.
- Allowed MCP tools: as listed above (by tool group). - Allowed MCP tools: as listed above (by permission).
The autonomous harness disallows `Bash` — shell execution goes The autonomous harness disallows `Bash` — shell execution goes
through `mcp__bash__run` (background tasks with structured output + through `mcp__bash__run` (background tasks with structured output +
@ -230,7 +228,7 @@ completion wake landing in the wrong session (the harness inbox) for a
choom-started task; `choom` is an operator action (root or `hive-admin`), choom-started task; `choom` is an operator action (root or `hive-admin`),
so built-in shell there stays within the existing trust boundary. The bash MCP server so built-in shell there stays within the existing trust boundary. The bash MCP server
(`run` / `status` / `kill`) uses `allowedTools = ["*"]` so all (`run` / `status` / `kill`) uses `allowedTools = ["*"]` so all
`mcp__bash__*` tools are always available regardless of tool groups. `mcp__bash__*` tools are available whenever the agent holds `execution`.
`WebFetch` / `WebSearch` are off by default; enable the `web_tools` `WebFetch` / `WebSearch` are off by default; grant `web_tools`
tool group in the P3RM1SS10NS tab and rebuild the agent to enable them. (`swarmctl agent permissions set`) to enable them from the next turn.

View file

@ -16,14 +16,13 @@ Settings, Core). Every page links back to H0M3, so you're
never more than one step from the hub. never more than one step from the hub.
The **dashboard** itself (`/dashboard.html`) is where you'll spend most The **dashboard** itself (`/dashboard.html`) is where you'll spend most
of your time. It's a single page with five tabs: of your time. It's a single page with four tabs:
- **4G3NTS** — every agent, live. This is the default tab and the one - **4G3NTS** — every agent, live. This is the default tab and the one
you'll check most. you'll check most.
- **R3BU1LD QU3U3**, **M3T4 1NPUTS** and **BUILD L0GS** — the build - **R3BU1LD QU3U3**, **M3T4 1NPUTS** and **BUILD L0GS** — the build
lifecycle: the rebuild queue with a live log of whatever is building, lifecycle: the rebuild queue with a live log of whatever is building,
the meta flake inputs you can bump, and the build log history. the meta flake inputs you can bump, and the build log history.
- **P3RM1SS10NS** — what tools and system-level access each agent has.
Everything else lives on its own page instead, all reachable from the Everything else lives on its own page instead, all reachable from the
H0M3 hub: **Flow** (`/flow.html`, the raw live message stream across H0M3 hub: **Flow** (`/flow.html`, the raw live message stream across
@ -58,10 +57,10 @@ shows as "needs update."
streaming log of whatever's currently building. Useful right after streaming log of whatever's currently building. Useful right after
merging a config change or bumping a flake input. merging a config change or bumping a flake input.
**Grant or revoke a tool/capability.** P3RM1SS10NS is a checkbox matrix **Grant or revoke a permission.** Not on the hive dashboard: an agent's
— rows are agents, columns are tool groups or capabilities. Nothing permissions live in the swarm. Set them with `swarmctl agent permissions`,
takes effect until you hit **save all** at the bottom of the tab; a or on the swarm UI's create-agent screen when creating the agent; see
save queues a rebuild for whichever agents actually changed. [`process/conventions.md`](../process/conventions.md#permissions).
**Read an agent's logs.** Logs aren't in the hive dashboard. Every **Read an agent's logs.** Logs aren't in the hive dashboard. Every
hive ships its agents', daemons' and infra containers' journals to the hive ships its agents', daemons' and infra containers' journals to the

View file

@ -20,7 +20,7 @@ from the dashboard tab strip.
- **← home back-link**: top-left of the chrome, links to the H0M3 hub - **← home back-link**: top-left of the chrome, links to the H0M3 hub
at `/`. Every surface links back to the hub rather than to each other. at `/`. Every surface links back to the hub rather than to each other.
- **Tab strip**: `◆ 4G3NTS ◆`, `◆ R3BU1LD QU3U3 ◆`, `◆ M3T4 1NPUTS ◆`, - **Tab strip**: `◆ 4G3NTS ◆`, `◆ R3BU1LD QU3U3 ◆`, `◆ M3T4 1NPUTS ◆`,
`◆ BUILD L0GS ◆`, `◆ P3RM1SS10NS ◆`. In-page tabs only — `◆ BUILD L0GS ◆`. In-page tabs only —
the SYST3M panels live on the the SYST3M panels live on the
standalone **C0R3** page (`/core.html`), and FL0W / ST4TS standalone **C0R3** page (`/core.html`), and FL0W / ST4TS
live on their own pages too, all reachable from the live on their own pages too, all reachable from the
@ -77,19 +77,15 @@ H0M3 hub (served at
`/core.html`), with minimal chrome: a `← home` back-link and a single pane, `/core.html`), with minimal chrome: a `← home` back-link and a single pane,
**K3PT ST4T3**. The page is its own esbuild bundle (`core.js`) that **K3PT ST4T3**. The page is its own esbuild bundle (`core.js`) that
cold-loads `/api/state` and subscribes to `/api/dashboard/stream` for cold-loads `/api/state` and subscribes to `/api/dashboard/stream` for
`tombstones_changed`, `capabilities_changed`, and `tool_groups_changed` `tombstones_changed`. (Rebuild queue and meta inputs are dashboard
(the latter two re-render the stale-perms sub-section when permission tabs — see below.)
data changes). (Rebuild queue and meta inputs are dashboard tabs — see
below.)
Per-container CPU, memory and disk load isn't shown in the hive UI. The Per-container CPU, memory and disk load isn't shown in the hive UI. The
same figures go out as OTEL metrics and appear in Grafana (see same figures go out as OTEL metrics and appear in Grafana (see
[observability](../scheduler/observability.md)). An agent's CPU and memory [observability](../scheduler/observability.md)). An agent's CPU and memory
caps live per agent in the swarm UI. caps live per agent in the swarm UI.
**K3PT ST4T3** — two sub-sections on one pane: **K3PT ST4T3** — agents with kept state and **no container** (size + age +
*Tombstones*: agents with kept state and **no container** (size + age +
claude-creds badge). One action: `PURG3` (wipes state + applied claude-creds badge). One action: `PURG3` (wipes state + applied
dirs; `POST /api/purge-tombstone/{name}`). dirs; `POST /api/purge-tombstone/{name}`).
@ -104,14 +100,6 @@ container is all hive-c0re can actually prove. Fixing it properly
needs a recorded destroy — **deferred** to the swarm-controller / needs a recorded destroy — **deferred** to the swarm-controller /
snapshot-storage rework where the problem changes shape. snapshot-storage rework where the problem changes shape.
*Stale permission entries*: agents with explicit capability or
tool-group JSON entries but no live container — typically renamed or
deleted agents whose entries persisted in `capabilities.json` /
`tool-groups.json`. Loaded when the page opens;
autorefreshes on `capabilities_changed` + `tool_groups_changed` SSE
events. Each ghost agent gets a `✕ clear perms` button
(`DELETE /api/permissions/{name}`).
## Build tabs (`#rebuild`, `#meta`, `#buildlogs`) ## Build tabs (`#rebuild`, `#meta`, `#buildlogs`)
The build lifecycle — rebuild queue, live build log, meta inputs, and The build lifecycle — rebuild queue, live build log, meta inputs, and
@ -228,94 +216,6 @@ badge with a ticking elapsed-time chip; expanding streams output via
(suspends on manual scroll-up). Deep-link: `?id=N#buildlogs` opens the (suspends on manual scroll-up). Deep-link: `?id=N#buildlogs` opens the
entry with that id pre-expanded. entry with that id pre-expanded.
## P3RM1SS10NS tab
Per-agent permission configuration. Two sections, each rendered as a
column-driven checkbox matrix: rows are agents, columns are the
permission names fetched from hive-c0re. The column list is
authoritative — adding a new tool-group or capability to hive-c0re
requires no UI change; the new column appears automatically.
The snapshot carries `agents` (the full manageable roster — live
containers ∪ agents with an explicit entry) and `effective` (per-agent
explicit-or-role-default values) alongside the explicit `assignments`
map. Rows come from `agents` so **agents on defaults always appear**
(not just those with an explicit entry), and checkboxes reflect the
`effective` values so a default agent shows the groups it actually runs
with rather than blank — which also means saving it won't silently
strip those defaults. The `(default)` badge keys off absence from
`assignments` (no explicit entry).
Fetches fire on tab activation (not page-load) to avoid unnecessary
work when the operator never visits this tab. Live mutations from the
rebuild-queue worker are also pushed via the `capabilities_changed` /
`tool_groups_changed` SSE events (same payload shape as the GET
endpoints), so an open P3RM1SS10NS tab reflects worker-applied changes
without requiring navigation. Tab-activation re-fetches remain as a
safety net for reconnect windows.
**C4P4B1L1T13S** — per-agent capability grants. Capabilities unlock
gated MCP tools and system-level access beyond the default agent
surface. A saving POST queues a rebuild for the affected agent so the
new `HIVE_CAPABILITIES` env var takes effect in the next session.
The current capabilities are:
| Name | Effect |
|------|--------|
| `manage_root_agent` | mounts every agent's state (rw) + config (ro) into this agent's container so it can manage/recover any agent |
Each row is one agent. Columns are the capability names returned by
`GET /api/capabilities` as `caps: Vec<String>`. Checking or unchecking
boxes only stages the change in-browser; the browser writes nothing until
the operator selects the page-level **save all** button (described below). Row
values follow the `effective`/`assignments` rule described above.
<!-- vale write-good.Passive = NO -->
**T00L GR0UPS** — per-agent tool-group permissions. Tool groups are
named buckets of MCP tools; each agent starts with a role default
(sub-agents: `messaging`, `meta`, `inbox`, `execution` —
`ToolGroup::AGENT_DEFAULT`; hive-c0re seeds the root agent to
`ToolGroup::MANAGER_DEFAULT` — `messaging`, `meta`, `inbox`,
`lifecycle`, `execution`,
that is, every group except `forge` and `web_tools`). Checking /
unchecking stages which groups are active for the agent; the
page-level **save all** button (below) commits it. Columns come from
`GET /api/tool-groups`. hive-c0re queues a rebuild so `HIVE_TOOL_GROUPS`
takes effect.
<!-- vale write-good.Passive = YES -->
The current tool groups are: `messaging`, `meta`, `inbox`, `lifecycle`,
`forge`, `execution`, `web_tools`. All listed in `ToolGroup::ALL` in `hive-sh4re`. The
`web_tools` group is special: it carries no MCP tools; instead it adds
Claude's built-in `WebFetch` and `WebSearch` to `--tools` /
`--allowedTools` for that agent session.
Both tables share the same visual shape: `.cap-table-wrap` /
`.tg-table-wrap` outer scroll container, `thead` with a label column
(`.cap-agent-col` / `.tg-agent-col`) + one column per permission
(`.cap-col` / `.tg-group-col`). Each tbody row is one agent: a name cell
and its checkbox cells, where each checkbox carries `data-baseline` (its
render-time state) and the row carries `data-agent` for dirty-tracking.
**Saving — one button for the whole page.** No per-row save
buttons exist. A single page-level `.perm-save-bar` with a **save all (N
agents)** button sits at the bottom of the pane, enabled only when some
checkbox diverges from its baseline. Selecting it diffs every checkbox
across *both* matrices and POSTs one batch to `POST /api/permissions` as
`{ changes: [ { agent, tool_groups?, capabilities? } ] }` — the batch
includes only the perm-types that actually changed for each agent (an
omitted field leaves that file untouched; an included array fully
replaces it).
hive-c0re coalesces an agent's capabilities + tool-groups into a
single rebuild, so changing both for one agent is one rebuild, not two.
The batch is atomic: it validates every change first and on any error
rejects the whole POST (`{error}`, nothing applied); a clean 200 (`ok`)
flips the bar to a queued→rebuilding state and re-fetches both tables.
Live `capabilities_changed` / `tool_groups_changed` events re-render the
matrices unless the section has unsaved edits, so an in-progress edit set
isn't clobbered.
## ST4TS page (`/stats.html`) ## ST4TS page (`/stats.html`)
Hive-wide turn statistics, aggregated across every agent's Hive-wide turn statistics, aggregated across every agent's
@ -754,46 +654,6 @@ below — some endpoints aren't in it yet.
- `POST /api/cancel-reminder/{id}` — hard-delete a pending reminder. - `POST /api/cancel-reminder/{id}` — hard-delete a pending reminder.
- `POST /api/retry-reminder/{id}` — re-arm a reminder whose delivery - `POST /api/retry-reminder/{id}` — re-arm a reminder whose delivery
failed (clears the failure state so the scheduler retries). failed (clears the failure state so the scheduler retries).
- `GET /api/tool-groups` — returns `{ groups: Vec<String>,
assignments: BTreeMap<String, Vec<String>>,
descriptions: BTreeMap<String, String> }`. `groups` is the
ordered list of all known tool-group names (drives the column
headers in the P3RM1SS10NS tab); `assignments` is the per-agent
override map (absent agents use the role default); `descriptions`
maps each group name to a short human-readable string surfaced as
a column-header tooltip on hover.
- `POST /api/tool-groups/{agent}` — body `{ groups: ["name", …] }`.
Writes the tool-group set for `{agent}` to
`/var/lib/hyperhive/meta/tool-groups.json` and queues a rebuild so
`HIVE_TOOL_GROUPS` takes effect. Agent name validated;
`guard_agent_name` applied.
- `GET /api/capabilities` — returns `{ caps: Vec<String>,
assignments: BTreeMap<String, Vec<String>>,
descriptions: BTreeMap<String, String> }`. `caps` is the
ordered list of all known capability names; `assignments` is the
per-agent grant map (absent agents have no extra capabilities);
`descriptions` maps each capability name to a short human-readable
string surfaced as a column-header tooltip on hover.
- `POST /api/capabilities/{agent}` — body `{ caps: ["name", …] }`.
Writes the capability set for `{agent}` to
`/var/lib/hyperhive/meta/capabilities.json` and queues a rebuild so
`HIVE_CAPABILITIES` takes effect. Agent name validated;
unknown capability strings rejected (400). `guard_agent_name`
applied.
- `POST /api/permissions` — batch perm apply for the save-all
permissions button. Body
`{ changes: [{ agent, tool_groups?: ["name", …], capabilities?: ["name", …] }] }`.
Sparse per agent: an omitted field leaves that perm-type untouched,
an empty array clears it, a populated array fully replaces it (same
replace semantics as the per-agent endpoints above). Each affected
agent gets ONE combined `PermChange` queue entry, so changing both
an agent's tool-groups and capabilities triggers a single rebuild,
not two. **Atomic**: the endpoint validates every change first (agent names via
`guard_agent_name`, group + capability names) and on any validation
error writes or enqueues nothing (non-2xx `{ error }`); it skips
rows with both fields omitted rather than erroring on them.
Returns `200 "ok"` on
success.
- `POST /api/meta-update` — `nix flake update` the selected - `POST /api/meta-update` — `nix flake update` the selected
`meta/flake.lock` inputs, then rebuild the affected agents. `meta/flake.lock` inputs, then rebuild the affected agents.
- `GET /api/dashboard/stream` — unified live event channel: - `GET /api/dashboard/stream` — unified live event channel:
@ -841,18 +701,6 @@ payload):
operator cancel / retry (`/api/system/reminders/*`), `cancel_loose_end` operator cancel / retry (`/api/system/reminders/*`), `cancel_loose_end`
with Reminder kind, and the scheduler tick after each delivery with Reminder kind, and the scheduler tick after each delivery
batch (`reminder_scheduler`). batch (`reminder_scheduler`).
- `capabilities_changed` (seq, caps: `Vec<str>`, descriptions: map,
assignments: `BTreeMap<String, Vec<String>>`) — full snapshot of
capability grants. Emitted from the rebuild-queue worker after a
`PermChange` / Capabilities entry commits the JSON file. Payload
matches `GET /api/capabilities` shape so the client can call
`renderCapabilities` directly. P3RM1SS10NS tab subscribes; activation
re-fetch still runs as a safety net.
- `tool_groups_changed` (seq, groups: `Vec<str>`, descriptions: map,
assignments: `BTreeMap<String, Vec<String>>`) — full snapshot of
tool-group assignments. Emitted from the rebuild-queue worker after
a `PermChange` / ToolGroups entry commits the JSON file. Same
shape as `GET /api/tool-groups`; P3RM1SS10NS tab subscribes.
- `tombstones_changed` (seq, tombstones: `Vec<TombstoneView>`) — - `tombstones_changed` (seq, tombstones: `Vec<TombstoneView>`) —
full snapshot of destroyed-but-state-kept agents. Emitted on full snapshot of destroyed-but-state-kept agents. Emitted on
destroy, purge, and revive. C0R3 K3PT ST4T3 tab subscribes. destroy, purge, and revive. C0R3 K3PT ST4T3 tab subscribes.

View file

@ -13,10 +13,9 @@
<body class="core-shell"> <body class="core-shell">
<!-- C0R3: the host/coordinator surface. Standalone page (served at <!-- C0R3: the host/coordinator surface. Standalone page (served at
/core.html) carved out of the dashboard's old SYST3M tab so the /core.html) carved out of the dashboard's old SYST3M tab so the
dashboard tab strip stays lean: kept state (tombstones) and the dashboard tab strip stays lean: kept state (tombstones). Rebuild
stale permission entries. Rebuild queue + meta inputs are tabs of queue + meta inputs are tabs of /dashboard.html. The section <div>
/dashboard.html. The section <div> ids (tombstones-section, id (tombstones-section) matches what core.js's renderers target. Hive
tombstones-stale-perms) match what core.js's renderers target. Hive
infra containers (`hive-forge`, `hive-gateway`, `hive-matrix`) have infra containers (`hive-forge`, `hive-gateway`, `hive-matrix`) have
no dashboard panel — `hivectl stop`/`start`/`restart` is the only no dashboard panel — `hivectl stop`/`start`/`restart` is the only
control surface. --> control surface. -->
@ -26,11 +25,7 @@
</header> </header>
<main class="core-main"> <main class="core-main">
<!-- tombstones-section shows destroyed agents (purge button). <!-- tombstones-section shows destroyed agents (purge button). -->
tombstones-stale-perms shows agents with explicit capability/
tool-group entries but no live container (typically renamed/deleted
agents whose JSON entries persisted). Each stale entry gets a
"✕ clear perms" button. -->
<p class="meta"> <p class="meta">
kept state from previously tombstoned agents — recreating an agent with kept state from previously tombstoned agents — recreating an agent with
the same name reuses it. the same name reuses it.
@ -38,7 +33,6 @@
<div id="tombstones-section"> <div id="tombstones-section">
<p class="meta">loading…</p> <p class="meta">loading…</p>
</div> </div>
<div id="tombstones-stale-perms"></div>
</main> </main>
<script type="module" src="/static/core.js" defer></script> <script type="module" src="/static/core.js" defer></script>

View file

@ -1,6 +1,6 @@
// C0R3 page entry (/core.html). The host/coordinator surface, its own // C0R3 page entry (/core.html). The host/coordinator surface, its own
// standalone page so the dashboard tab strip stays lean: kept state // standalone page so the dashboard tab strip stays lean: kept state
// (tombstones) and the stale permission entries. // (tombstones).
// //
// Rebuild queue + meta inputs are tabs of /dashboard.html, not here. This // Rebuild queue + meta inputs are tabs of /dashboard.html, not here. This
// page is its own esbuild bundle with its own runtime: it cold-loads // page is its own esbuild bundle with its own runtime: it cold-loads
@ -10,7 +10,7 @@
import { $, form, openStream, initServerWarnings } from "./common.js"; import { $, form, openStream, initServerWarnings } from "./common.js";
import { el } from "@hive/shared/dom.js"; import { el } from "@hive/shared/dom.js";
import { asyncBtn, bindAsyncForms } from "@hive/shared/forms.js"; import { bindAsyncForms } from "@hive/shared/forms.js";
// ─── derived state (own copies; this bundle has its own runtime) ────────── // ─── derived state (own copies; this bundle has its own runtime) ──────────
let tombstonesState = []; let tombstonesState = [];
@ -95,92 +95,6 @@ function renderTombstones(s) {
root.append(ul); root.append(ul);
} }
// ─── stale permission entries (K3PT ST4T3 pane sub-section) ──────────────
// Agents with explicit capability / tool-group JSON entries but no live
// container AND no kept-state tombstone (e.g. renamed agents like the old
// "root" manager name). Ghost detection is server-side via
// GET /api/permissions/stale so the client doesn't need to maintain a
// container-roster cache or perform set arithmetic. Lazy-loaded on first
// "kept" tab activation; refreshed when perm data changes via SSE.
let stalePermsLoaded = false;
function renderStalePerms(root, ghosts) {
root.replaceChildren();
if (!ghosts.length) return;
root.append(
el("p", { class: "tombstones-stale-heading" }, "stale permission entries"),
);
root.append(
el(
"p",
{ class: "meta" },
"agents with explicit capability or tool-group entries but no live container " +
"or kept state (typically renamed or manually-deleted agents whose JSON entries persisted).",
),
);
const errP = el("p", { class: "tombstones-stale-err", hidden: true });
const ul = el("ul", { class: "tombstones-stale-list" });
for (const name of ghosts) {
const li = el("li", { class: "tombstones-stale-row" });
li.append(el("span", { class: "tombstones-stale-name" }, name));
li.append(el("span", { class: "hive-pill-sm badge-muted" }, "stale perms"));
const btn = el(
"button",
{
type: "button",
class: "btn btn-destroy",
title: "remove explicit capability and tool-group entries for " + name,
},
"✕ clear perms",
);
btn.addEventListener("click", () =>
asyncBtn(btn, async () => {
errP.hidden = true;
try {
const resp = await fetch(
"/api/permissions/" + encodeURIComponent(name),
{ method: "DELETE" },
);
if (!resp.ok) {
const msg = await resp.text().catch(() => String(resp.status));
errP.textContent = "failed to clear perms for " + name + ": " + msg;
errP.hidden = false;
return;
}
await fetchAndRenderStalePerms();
} catch (err) {
errP.textContent = "failed to clear perms for " + name + ": " + err;
errP.hidden = false;
}
}),
);
li.append(btn);
ul.append(li);
}
root.append(ul, errP);
}
// Ghost detection is entirely server-side: GET /api/permissions/stale
// returns the computed list of agent names that have explicit JSON entries
// but are absent from both the live roster and the kept-state tombstones.
// One call, no client-side roster cache, always authoritative.
async function fetchAndRenderStalePerms() {
const root = $("tombstones-stale-perms");
if (!root) return;
try {
const resp = await fetch("/api/permissions/stale");
if (!resp.ok) throw new Error("http " + resp.status);
const data = await resp.json();
renderStalePerms(root, data.stale || []);
} catch (err) {
root.replaceChildren();
root.append(
el("p", { class: "meta" }, "failed to load stale perm data: " + err),
);
}
stalePermsLoaded = true;
}
// ─── render-all (cold load + any full re-render) ────────────────────────── // ─── render-all (cold load + any full re-render) ──────────────────────────
function renderAll() { function renderAll() {
renderTombstones({ tombstones: tombstonesState }); renderTombstones({ tombstones: tombstonesState });
@ -192,14 +106,6 @@ const SSE_HANDLERS = {
tombstonesState = (ev.tombstones || []).slice(); tombstonesState = (ev.tombstones || []).slice();
renderTombstones({ tombstones: tombstonesState }); renderTombstones({ tombstones: tombstonesState });
}, },
// Refresh the stale-perms sub-section when perm data changes (a ghost
// was cleared, or perms were saved for an agent whose name collides).
capabilities_changed(_ev) {
if (stalePermsLoaded) fetchAndRenderStalePerms();
},
tool_groups_changed(_ev) {
if (stalePermsLoaded) fetchAndRenderStalePerms();
},
}; };
// ─── boot ───────────────────────────────────────────────────────────────── // ─── boot ─────────────────────────────────────────────────────────────────
@ -226,17 +132,13 @@ async function init() {
bindAsyncForms(() => refreshState()); bindAsyncForms(() => refreshState());
await refreshState(); await refreshState();
fetchAndRenderStalePerms();
// kinds= matches SSE_HANDLERS below verbatim — narrows this from all // kinds= matches SSE_HANDLERS below verbatim — narrows the stream down
// 13 wire kinds down to the 3 this page acts on. This page was one // to the kinds this page acts on. This page was one
// of 4 unfiltered `/api/dashboard/stream` subscribers before this // of 4 unfiltered `/api/dashboard/stream` subscribers before this
// (subscription discipline, part 1 of the dashboard-event-stream- // (subscription discipline, part 1 of the dashboard-event-stream-
// split issue). // split issue).
const es = openStream( const es = openStream("/api/dashboard/stream?kinds=tombstones_changed");
"/api/dashboard/stream?kinds=tombstones_changed," +
"capabilities_changed,tool_groups_changed",
);
if (es) { if (es) {
es.onmessage = (e) => { es.onmessage = (e) => {
let ev; let ev;

View file

@ -566,155 +566,6 @@ footer .banner-thin {
margin-bottom: 0.8em; margin-bottom: 0.8em;
} }
/* ─── capabilities (permissions) table ─────────────────────────────
Agents × capabilities matrix in the P3RM1SS10NS tab. Same layout
as the tool-groups table below. Horizontally scrollable on narrow
viewports. */
.cap-table-wrap {
overflow-x: auto;
margin-top: 0.5em;
}
.cap-table {
border-collapse: collapse;
font-size: 0.82em;
min-width: 100%;
}
.cap-table th,
.cap-table td {
padding: 0.35em 0.6em;
border: 1px solid var(--border);
text-align: center;
vertical-align: middle;
}
.cap-table thead th {
background: var(--bg-elev);
color: var(--muted);
letter-spacing: 0.05em;
white-space: nowrap;
}
.cap-agent-col {
text-align: left !important;
min-width: 8em;
}
.cap-col {
min-width: 5em;
}
.cap-agent-name {
color: var(--fg);
font-weight: 600;
}
.cap-cb {
cursor: pointer;
width: 1em;
height: 1em;
accent-color: var(--purple);
}
.cap-row:hover td {
background: var(--bg-elev);
}
/* ─── tool-groups (permissions) table ──────────────────────────────
Agents × tool-groups matrix in the P3RM1SS10NS tab. Same layout
as the capabilities table above. Horizontally scrollable on narrow
viewports. */
.tg-table-wrap {
overflow-x: auto;
margin-top: 0.5em;
}
.tg-table {
border-collapse: collapse;
font-size: 0.82em;
min-width: 100%;
}
.tg-table th,
.tg-table td {
padding: 0.35em 0.6em;
border: 1px solid var(--border);
text-align: center;
vertical-align: middle;
}
.tg-table thead th {
background: var(--bg-elev);
color: var(--muted);
letter-spacing: 0.05em;
white-space: nowrap;
}
.tg-agent-col {
text-align: left !important;
min-width: 8em;
}
.tg-group-col {
min-width: 5em;
}
.tg-agent-name {
color: var(--fg);
font-weight: 600;
}
.tg-cb {
cursor: pointer;
width: 1em;
height: 1em;
accent-color: var(--purple);
}
.tg-row:hover td {
background: var(--bg-elev);
}
/* ─── permissions save bar (save-all) ──────────────────────────────
One page-level save button for the whole P3RM1SS10NS tab. Sticks to
the bottom of the pane so it stays reachable while scrolling a long
agent×perm matrix. Disabled (dim) until an edit makes the page dirty;
the (N agents) count in the label reflects how many agents will
rebuild. `#perm-save-note` carries the transient rebuilding/error
line beside it. */
.perm-save-bar {
position: sticky;
bottom: 0;
display: flex;
align-items: center;
gap: 0.8em;
margin-top: 1em;
padding: 0.7em 0;
background: var(--bg);
border-top: 1px solid var(--border);
}
.perm-save-bar .btn:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.perm-save-err {
color: var(--red);
}
/* Stale permission entry (agent not in the live roster) */
.perm-row-stale td {
opacity: 0.7;
}
.perm-stale-label {
font-size: 0.75em;
color: var(--muted);
margin-left: 0.4em;
}
.perm-remove-btn {
margin-left: 0.5em;
padding: 0.05em 0.5em;
font-size: 0.75em;
color: var(--red);
background: transparent;
border: 1px solid currentColor;
border-radius: 3px;
cursor: pointer;
font-family: inherit;
opacity: 0.8;
}
.perm-remove-btn:hover {
opacity: 1;
}
.perm-remove-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
/* Selection bar — sticky-bottom strip for bulk actions. */ /* Selection bar — sticky-bottom strip for bulk actions. */
.selection-bar { .selection-bar {
position: fixed; position: fixed;

View file

@ -71,18 +71,6 @@
> >
<span class="tab-label">◆ BUILD L0GS ◆</span> <span class="tab-label">◆ BUILD L0GS ◆</span>
</a> </a>
<!-- P3RM1SS10NS: per-agent capability grants and tool-group
assignments. Both tables are fetched on tab activation. -->
<a
class="tab"
id="tab-permissions"
href="#permissions"
role="tab"
aria-controls="tab-pane-permissions"
data-tab="permissions"
>
<span class="tab-label">◆ P3RM1SS10NS ◆</span>
</a>
<!-- ST4TS lives on its own page now (`/stats.html`), reached from <!-- ST4TS lives on its own page now (`/stats.html`), reached from
the H0M3 hub — the hive-wide rollup is a standalone read-only the H0M3 hub — the hive-wide rollup is a standalone read-only
@ -205,75 +193,6 @@
<div id="build-list"><p class="meta">loading…</p></div> <div id="build-list"><p class="meta">loading…</p></div>
</section> </section>
<!-- P3RM1SS10NS: per-agent capability grants + tool-group
assignments. Both tables are column-driven from the backend
(GET /api/capabilities, GET /api/tool-groups) so new entries
require no UI change. Edits stage in-browser across both
matrices; the single page-level "save all" button POSTs every
changed agent to /api/permissions in one atomic batch (caps +
groups coalesce into one rebuild per agent). -->
<section
class="tab-pane"
id="tab-pane-permissions"
data-tab-pane="permissions"
hidden
role="tabpanel"
aria-labelledby="tab-permissions"
>
<h2>◆ C4P4B1L1T13S ◆</h2>
<div class="divider">
══════════════════════════════════════════════════════════════
</div>
<p class="meta">
per-agent capability grants. capabilities unlock gated MCP tools and
system access. toggle any cells across both tables, then hit "save
all" at the bottom — each changed agent rebuilds exactly once.
</p>
<div id="capabilities-section">
<p class="meta">loading…</p>
</div>
<!-- T00L GR0UPS: per-agent tool-group permission matrix. Rows = agents,
cols = tool groups fetched from GET /api/tool-groups.
Checking / unchecking is staged in the UI; the single page-level
"save all" button POSTs every changed agent to
/api/permissions in one atomic batch, coalescing caps + groups
per agent into one rebuild each.
Absent agents default to the role default (shown in parens). -->
<h2>◆ T00L GR0UPS ◆</h2>
<div class="divider">
══════════════════════════════════════════════════════════════
</div>
<p class="meta">
per-agent tool-group permissions. columns are filled from the
backend — adding a new group requires no UI change. agents without
an explicit entry use the role default (agents: messaging, meta,
inbox, execution).
</p>
<div id="tool-groups-section">
<p class="meta">loading…</p>
</div>
<!-- Page-level save bar (save-all issue): one button for the whole
permissions page. Staged checkbox edits across BOTH matrices are
diffed against their render-time baselines and POSTed together to
/api/permissions; caps + groups for one agent coalesce into a
single rebuild. Disabled until something is dirty. The atomic
batch either fully lands (queued ✓ → rebuilding) or fully fails
(error note, nothing applied). -->
<div class="perm-save-bar">
<button
type="button"
id="perm-save-all"
class="btn btn-approve"
disabled
>
save all
</button>
<span id="perm-save-note" class="meta"></span>
</div>
</section>
<!-- ST4TS: hive-wide turn-stats aggregate lives on its own page now <!-- ST4TS: hive-wide turn-stats aggregate lives on its own page now
(`/stats.html`), reached from the H0M3 hub. The markup + the (`/stats.html`), reached from the H0M3 hub. The markup + the
hive-stats render JS moved there; when tabs.js boots on the hive-stats render JS moved there; when tabs.js boots on the

View file

@ -41,9 +41,7 @@
<span class="home-tile-icon" aria-hidden="true">🖥</span> <span class="home-tile-icon" aria-hidden="true">🖥</span>
<span class="home-tile-label">Dashboard</span> <span class="home-tile-label">Dashboard</span>
</span> </span>
<span class="home-tile-desc" <span class="home-tile-desc">containers · builds</span>
>containers · builds · permissions</span
>
</a> </a>
<a class="home-tile" href="/flow.html"> <a class="home-tile" href="/flow.html">
@ -69,7 +67,7 @@
<span class="home-tile-icon" aria-hidden="true">⬡</span> <span class="home-tile-icon" aria-hidden="true">⬡</span>
<span class="home-tile-label">Core</span> <span class="home-tile-label">Core</span>
</span> </span>
<span class="home-tile-desc">kept state · stale permissions</span> <span class="home-tile-desc">kept state</span>
</a> </a>
<!-- API tile: the OpenAPI spec + Swagger UI are always served by <!-- API tile: the OpenAPI spec + Swagger UI are always served by

Some files were not shown because too many files have changed in this diff Show more