From cf581d14b813878ff9e54908ccf340bc7f3e0363 Mon Sep 17 00:00:00 2001 From: atlas Date: Fri, 26 Jun 2026 00:00:04 +0200 Subject: [PATCH 1/4] docs(security): document the agent trust model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- CLAUDE.md | 3 +- docs/security.md | 86 +++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 04fe6dc2..21c14c64 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/docs/security.md b/docs/security.md index 02853500..dd5c999b 100644 --- a/docs/security.md +++ b/docs/security.md @@ -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//.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=

` serves files from agent state dirs and the shared space to authenticated dashboard users (browser, operator). From 9daed8acc18f85aff1d2a7752b270b7e49c0fde5 Mon Sep 17 00:00:00 2001 From: atlas Date: Fri, 26 Jun 2026 00:23:02 +0200 Subject: [PATCH 2/4] docs(security): restore the State-file endpoint heading The trust-model insert accidentally consumed the '## State-file endpoint security model' heading, leaving that section floating under the new one. Re-add the heading and collapse the extra blank lines (argus review). --- docs/security.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/security.md b/docs/security.md index dd5c999b..350fa7e9 100644 --- a/docs/security.md +++ b/docs/security.md @@ -84,7 +84,7 @@ 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. - +## State-file endpoint security model `GET /api/state-file?path=

` serves files from agent state dirs and the shared space to authenticated dashboard users (browser, operator). From 524b1de98c7e27c1fe0aa03a7d9c373a6e1e435f Mon Sep 17 00:00:00 2001 From: atlas Date: Fri, 26 Jun 2026 01:29:23 +0200 Subject: [PATCH 3/4] docs(security): correct the operator-merge bullet (mara review) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per mara: branch protection isn't a blanket enforced check, and it's not set up for external VCS (GitHub). Reframe the bullet as the operator-merge *convention* — technically enforced only on the core-managed config repos, and explicitly NOT wired up for GitHub/external VCS (process + accepted risk there, not a control). --- docs/security.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/security.md b/docs/security.md index 350fa7e9..20b1d571 100644 --- a/docs/security.md +++ b/docs/security.md @@ -52,11 +52,13 @@ 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. +- **Operator-merge convention** — an agent may *push* branches, but the norm + is that **a human (the operator) merges the PR**, keeping a person in the + loop on the highest-value action. This is largely **convention, not a + blanket enforced gate**: the internal forge applies branch protection only on + the `core`-managed config repos (the config-PR merge flow), and it is **not** + set up for external VCS (GitHub etc.) — there, operator-merge is process and + accepted risk, not a technical control. - **Approvals** — config changes, schedule additions, and other blast-radius-y operations route through the operator approval queue (see [`approvals.md`](approvals.md)). From 8e366c8a101b3955d1d14310d8308647eb3d860c Mon Sep 17 00:00:00 2001 From: atlas Date: Fri, 26 Jun 2026 01:42:04 +0200 Subject: [PATCH 4/4] docs(security): operator-merge IS enforced on the internal forge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per mara (verified in forge.rs): agents have max_repo_creation=0, so every internal-forge repo is core-created with branch protection on by default (merge restricted to operators team + required operators-team approval via apply_operator_branch_protection / the config-repo equivalent), and agents (write collaborators, not admins) can't change it or self-merge. So it's technically enforced there, not just convention — only external VCS (GitHub) is unprotected. Corrects my prior over-correction. --- docs/security.md | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/docs/security.md b/docs/security.md index 20b1d571..a8b6144b 100644 --- a/docs/security.md +++ b/docs/security.md @@ -52,13 +52,17 @@ 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-merge convention** — an agent may *push* branches, but the norm - is that **a human (the operator) merges the PR**, keeping a person in the - loop on the highest-value action. This is largely **convention, not a - blanket enforced gate**: the internal forge applies branch protection only on - the `core`-managed config repos (the config-PR merge flow), and it is **not** - set up for external VCS (GitHub etc.) — there, operator-merge is process and - accepted risk, not a technical control. +- **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`), + so every repo is `core`-created with branch protection **on by default** — + merges restricted to the operators team + a required operators-team approval + (`apply_operator_branch_protection` / the config-repo equivalent) — and an + agent (a write collaborator, not a repo admin) can neither change those + settings nor merge its own PR. It is **not** set up for external VCS (GitHub + etc.), though — there, operator-merge is process + accepted risk, not a + technical control. - **Approvals** — config changes, schedule additions, and other blast-radius-y operations route through the operator approval queue (see [`approvals.md`](approvals.md)).