# Security model ## Agent trust model The implementation docs document the specific mechanisms (nixbld isolation, privilege separation; see the list at the end). This section frames the model they serve: **what hyperhive defends, what it deliberately doesn't, and where the operator is accepting risk.** It's the reference for "is it safe to give an agent capability X?." ### The trust boundary is the container, not credential storage An agent is **trusted code running inside its own nspawn container**. The boundary that matters is the container: a sub-agent can't see the host netns, another agent's container, or another agent's state dir. Within its own container the agent is privileged — it has **passwordless `sudo` by default**. Isolating credentials _from the agent itself_ is therefore **not a goal**: an agent can read its own tokens, its own `/home//.claude`, and run arbitrary commands as root inside its container. (The narrow exception is _cross-tenant_ leakage — for example the unsandboxed-nix-build `0600` token policy (see [nix builds](../../4-implementation/trust-boundary/nix-builds.md)) stops a build's nixbld user reading the agent's own forge token. That hardens the boundary; it doesn't sandbox the agent from itself.) The corollary: **don't reason about security as "can the agent be stopped from touching its credentials." Reason about it as "what's the blast radius if this agent does the worst possible thing with everything it can reach."** ### Scoped tokens bound the blast radius Each agent gets its own scoped credentials, never shared: - **forge token** → that agent's Forgejo account only (its own repos + collaborator grants; can't act as another agent or as `core`). - **matrix token** → that agent's matrix account only. _Its own_ account's scope bounds a compromised/confused agent's reach on the forge or matrix, not the swarm's. This is the main thing standing between "one agent does something unwise" and "the whole hive is affected." **Identity vs. secret (matrix).** The scoping is on the _secret_, not the _identity_: an agent's matrix **token** is private to its own account, but its matrix **identities** — the public handles (`name`, `user_id` `@user:server`, `homeserver`) — are intentionally readable by any agent via `GetAgentMeta`, so peers can find and address one another on a shared matrix instance. Only the public handle crosses that boundary; the token never does. ### The swarm secret store isn't a boundary between hives A hive reads its agents' credentials out of the swarm's secret store with its own certificate. **Every hive's policy grants read on every agent's credentials**, not only on the agents it hosts — so a compromised hive can read the matrix token of an agent running on a different hive. That includes each account's key-backup recovery passphrase, which with the token decrypts the agent's whole encrypted history, not only messages from then on. That's deliberate and it's the interim state, not the intent. An agent's credential path doesn't name the hive hosting it (agents move between hives), so a per-hive grant has to be an enumeration the controller re-emits whenever the roster changes — and an enumeration that can drift or land out of order advertises a boundary it doesn't actually hold. A wide grant that says what it is beats a narrow one that only looks narrow. What still holds: the grant is **read-only** (a hive can't write an agent's credential, so it can't hand itself an agent's identity), and it reaches two prefixes and nothing else in the store — every agent's credentials, and the reader's _own_ entry under the hive namespace, which names the hive asking and so widens nothing between them. A hive reads no **swarm service's** OIDC client secret. Every reader of `swarm/services/` presents a certificate that isn't a hive's: Grafana, the swarm collector and the store forwarder each fetch theirs with a leaf of their own, and the store's granter fetches the bao UI's. Three services read **every agent's queue secret** (`swarm/agents//queue`), each under a leaf of its own whose policy reaches that one leaf per agent: the queue's auth callout (`swarm-nats-auth`), the log store's agent check (`swarm-logs-auth`, on the log store's host), and the metrics store's (`swarm-metrics-agent-auth`, under the `swarm-metrics-auth` leaf on the metrics store's host). Any one compromised can present any agent's queue secret, at the queue and at both stores' read routes alike. The two store checks also read the agent's `swarm/agents//observability` leaf, which only the controller writes, and let the agent through only with the `logs` or `metrics` permission it names. ### Per-hive credentials live under the hive kind `swarm_secret_client::policy` scopes the **hive** stanza narrowly (unlike the agent stanza above) because a hive's own path names it, so scoping to the reader's own name costs nothing and drifts nowhere. That only holds if a credential that must be one-per-hive is actually stored under the hive kind. **A credential that must be one-per-hive goes under `Kind::Hive`**; putting it under the agent prefix and expecting the grant to scope it — that's the mistake this note exists to stop. ### Threat model: prompt injection → confused deputy The realistic adversary **never needs to breach the container**. They supply **untrusted input the agent reads and acts on**: a poisoned issue or PR comment, a cloned repo's README/CI, a scraped webpage, a crafted matrix message. The agent is the trusted, capable party; the _input_ is the untrusted part. A successful injection turns the agent into a **confused deputy** — it uses its legitimate capabilities (push, comment, deploy, run shell) on the attacker's behalf. Mitigations are therefore about **bounding capability and inserting human checkpoints**, not about sandboxing the agent from its own tools: - **Operator merges, not the agent** — an agent may _push_ branches, but a **human (the operator) merges the PR**, keeping a person in the loop on the highest-value action. On the **internal forge this is technically enforced, not just convention**: agents can't create repos (`max_repo_creation = 0`), and `main` on an `agent-configs/` repo carries swarm-controller's branch protection: merge and approval allowlisted to the `operators` team, which swarm-controller converges on every config repo. An operator's merge there is also what deploys the config. Existing `agents/` repos carry the same merge gate. An agent (a write collaborator, not a repo admin) can neither change those settings nor merge its own PR. It's **not** set up for external VCS (GitHub etc.), though — there, operator-merge is process + accepted risk, not a technical control. - **Operator sign-off** — config changes are config PRs an operator merges on the forge (see [`config-changes.md`](config-changes.md)), and scheduled prompts are operator-only, set at swarm level (see [`schedules.md`](../swarm/schedules.md)). Before that merge, the swarm builder evaluates an unmerged config PR off-hive, on any host that sets `deploy.builder.enable` (the all-swarm-services switch defaults it on): it builds the PR's head with a read-only forge token, in its own netns, as an untrusted client of its host's nix daemon (see [`builder.md`](../../4-implementation/swarm/builder.md)). ### Capability = accepted risk Every capability granted to an agent is a risk the operator is **explicitly accepting**. The rule of thumb: > **Don't give an agent access to something you can't afford to lose.** If an agent can deploy to prod, you are accepting the risk of a dropped production database (via injection or plain error). If that's unacceptable, the answer is _don't grant the capability_ — not "grant it and hope the sandbox holds," because there is no sandbox between an agent and the tools you handed it. ### No autosandboxing of external tokens hyperhive provisions and scopes its **own** per-agent forge + matrix tokens. It does **not** automatically sandbox or scope **external** credentials (GitHub PATs, cloud keys, third-party API tokens). The scope of an external token is **operator-accepted risk**: if you drop a broadly scoped GitHub token into an agent's config, that agent has exactly that reach, with no hyperhive layer narrowing it. Scope external tokens tightly at the source (the external provider) before handing them over. ## Agent-supplied names in paths Wherever an agent-supplied name reaches a filesystem path, hyperhive validates it first. The agent socket's `GetAgentMeta` takes `name` as a serde-validated `hive_types::Ident` (or falls back to `Ident::parse` for the "self" case) before building `agent_notes_dir(name)`, so a `..` component can't traverse. ## Implementation detail - How in-container `nix build` stays apart from the credentials, and its threat model: [nix builds](../../4-implementation/trust-boundary/nix-builds.md). - How `hive-c0re` runs unprivileged and delegates root operations: [privilege separation](../../4-implementation/trust-boundary/hive-c0re-privilege.md).