docs(security): document the agent trust model

Add an 'Agent trust model' framing section at the top of docs/security.md
covering the conceptual model the existing mechanism-level sections serve:

- trust boundary = the container, not credential storage (agents have
  passwordless sudo by default; isolating creds from the agent itself is
  not a goal — cross-tenant leakage is)
- scoped per-agent forge/matrix tokens bound the blast radius
- threat model: prompt injection -> confused deputy (untrusted input is
  the adversary; the agent + its tools are trusted)
- branch protection (agents push, operator merges) as the human-in-loop
  checkpoint, incl. external VCS
- capability = accepted risk ('don't grant what you can't afford to lose')
- no auto-sandboxing of external tokens (operator-accepted scope)

Emerged from the 2026-06-24 security discussion. Updates the CLAUDE.md
pointer for findability.
This commit is contained in:
atlas 2026-06-26 00:00:04 +02:00 committed by mara
commit cf581d14b8
2 changed files with 87 additions and 2 deletions

View file

@ -10,7 +10,8 @@ when you need depth on a subsystem. This file is the index.
- Operator/agent trust-boundary design:
**[docs/boundary.md](docs/boundary.md)** (`area:ops` issues
for the deployment/gateway/privsep work).
- Credential isolation + sandbox threat model:
- Agent trust model (trust boundary, prompt-injection threat model,
capability = accepted risk), credential isolation + sandbox threat model:
**[docs/security.md](docs/security.md)**.
## Repo map

View file

@ -1,6 +1,90 @@
# Security model
## State-file endpoint security model
## Agent trust model
The sections below document specific mechanisms (the state-file endpoint,
nixbld isolation, privilege separation). This section frames the model they
serve: **what hyperhive defends, what it deliberately does not, and where the
operator is accepting risk.** It emerged from a security discussion on
2026-06-24 (prompted by the `gh` CLI helper work) and is 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 cannot 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/<name>/.claude`, and
run arbitrary commands as root inside its container. (The narrow exception is
*cross-tenant* leakage — e.g. the unsandboxed-nix-build `0600` token policy
below stops a build's nixbld user reading the agent's own forge token, and the
state-file endpoint stops one agent proxying another's files. Those harden the
boundary; they do not 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 is 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; cannot act as another agent or as `core`).
- **matrix token** → that agent's matrix account only.
So a compromised/confused agent's reach on the forge or matrix is bounded by
*its own* account's scope, not the swarm's. This is the main thing standing
between "one agent does something dumb" and "the whole hive is affected".
### 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:
- **Branch protection** — agents may *push* branches, but PRs are **merged by
the operator only** (existing hive convention; enforced on `main` via
branch-protection rules). This puts a human in the loop on the highest-value
action. It applies to **any** external VCS access too (GitHub etc.), not
just the internal forge.
- **Approvals** — config changes, schedule additions, and other
blast-radius-y operations route through the operator approval queue
(see [`approvals.md`](approvals.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 auto-sandboxing 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.
`GET /api/state-file?path=<p>` serves files from agent state dirs and
the shared space to authenticated dashboard users (browser, operator).