security: isolate agent behavior from the caller repo #37

Closed
ffaerber wants to merge 2 commits from security/agent-isolation into main
Owner

Summary

Locks agent behavior to the agents repo so a caller repo can no longer change it — the isolation problem raised for the shared workflow.

Threat: the agent runs inside the caller repo's checkout, and opencode auto-discovers project-level config from the working tree (opencode.json/.opencode/ → provider/model/permission/mcp/plugins, and AGENTS.md/CLAUDE.md → instructions). opencode has no flag to disable this. So any caller repo could swap the model, unlock skills/permissions, register MCP servers or plugins (arbitrary code execution on the runner, which holds ANTHROPIC_API_KEY/AGENT_TOKEN/TOKEN_*/SSH_PRIV_KEY), or rewrite the rules. The trigger gate only restricts who triggers, not what repo content defines behavior.

Changes

  • run-agent.sh — quarantine: before opencode runs, move caller-controlled opencode.json, opencode.jsonc, .opencode/, AGENTS.md, CLAUDE.md aside; restore on EXIT (trap). Verified: the committed tree / PR diff is unchanged even when the agent commits mid-run (restore + the existing publish commit net out to a clean diff).
  • run-agent.sh — authoritative rules: dev-agent golden rules now come from scripts/agent-rules.md (platform-controlled), injected into the prompt, instead of "read the caller's AGENTS.md and follow it exactly."
  • scripts/agent-rules.md (new): repo-agnostic authoritative rules.
  • SECURITY.md (new): documents the isolation model + the requirement that callers pin agent.yml@main.

Validation

  • bash -n clean.
  • Simulated: malicious opencode.json/.opencode/plugins/AGENTS.md are absent during the run while real source stays editable; tree fully restored after.
  • Simulated the git path: agent commits mid-run (recording the config as deleted) → after restore + publish commit, git diff base..branch shows only the real change, config intact.

Reviewer notes / trade-off

By design, an agent cannot durably edit these quarantined files during a run (change them via a human PR). This intentionally removes a caller repo's ability to self-configure the agent (e.g. a repo-local opencode.json permission allowlist) — that control now belongs centrally to this repo.

🤖 Generated with Claude Code

## Summary Locks agent behavior to the `agents` repo so a **caller repo can no longer change it** — the isolation problem raised for the shared workflow. **Threat:** the agent runs inside the caller repo's checkout, and opencode auto-discovers project-level config from the working tree (`opencode.json`/`.opencode/` → provider/model/permission/mcp/**plugins**, and `AGENTS.md`/`CLAUDE.md` → instructions). opencode has **no flag to disable** this. So any caller repo could swap the model, unlock skills/permissions, register MCP servers or plugins (**arbitrary code execution on the runner**, which holds `ANTHROPIC_API_KEY`/`AGENT_TOKEN`/`TOKEN_*`/`SSH_PRIV_KEY`), or rewrite the rules. The trigger gate only restricts *who* triggers, not *what repo content defines behavior*. ## Changes - **`run-agent.sh` — quarantine:** before opencode runs, move caller-controlled `opencode.json`, `opencode.jsonc`, `.opencode/`, `AGENTS.md`, `CLAUDE.md` aside; restore on `EXIT` (trap). Verified: the committed tree / PR diff is unchanged even when the agent commits mid-run (restore + the existing publish commit net out to a clean diff). - **`run-agent.sh` — authoritative rules:** dev-agent golden rules now come from `scripts/agent-rules.md` (platform-controlled), injected into the prompt, instead of *"read the caller's AGENTS.md and follow it exactly."* - **`scripts/agent-rules.md`** (new): repo-agnostic authoritative rules. - **`SECURITY.md`** (new): documents the isolation model + the requirement that callers pin `agent.yml@main`. ## Validation - `bash -n` clean. - Simulated: malicious `opencode.json`/`.opencode/plugins`/`AGENTS.md` are absent during the run while real source stays editable; tree fully restored after. - Simulated the git path: agent commits mid-run (recording the config as deleted) → after restore + publish commit, `git diff base..branch` shows **only the real change**, config intact. ## Reviewer notes / trade-off By design, an agent cannot durably edit these quarantined files **during a run** (change them via a human PR). This intentionally removes a caller repo's ability to self-configure the agent (e.g. a repo-local `opencode.json` permission allowlist) — that control now belongs centrally to this repo. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
ffaerber added 1 commit 2026-07-04 10:06:07 +02:00
The agent runs inside the CALLER repo's checkout and opencode auto-discovers project-level config
from the working tree (opencode.json / .opencode/ for provider/model/permission/mcp/plugins, and
AGENTS.md/CLAUDE.md for instructions). opencode has no flag to disable that discovery, so any caller
repo could swap the model, unlock skills/permissions, register MCP servers or plugins (arbitrary
code execution on the runner, which holds ANTHROPIC_API_KEY/AGENT_TOKEN/TOKEN_*/SSH_PRIV_KEY), or
rewrite the rules the agent follows. The trigger gate only restricts who triggers, not what repo
content defines behavior.

- run-agent.sh: quarantine caller-controlled opencode.json/opencode.jsonc/.opencode/AGENTS.md/CLAUDE.md
  for the duration of each run (moved aside, restored on EXIT via trap). Restore keeps the committed
  tree/PR diff unchanged even when the agent commits mid-run.
- run-agent.sh: dev-agent golden rules now come from scripts/agent-rules.md (platform-authoritative),
  injected into the prompt, instead of "read the caller's AGENTS.md and follow it exactly".
- scripts/agent-rules.md: new repo-agnostic authoritative rules.
- SECURITY.md: documents the isolation model and the requirement that callers pin agent.yml@main.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ffaerber added 1 commit 2026-07-04 10:21:05 +02:00
Fully hiding the caller's AGENTS.md dropped repo-specific operational knowledge agents need
(e.g. homelab's migration mechanism: deleting a service requires a matching migrations/*.sh, else
the old service keeps running in prod). Separate the two concerns:

- Platform golden rules stay authoritative (scripts/agent-rules.md), un-overridable.
- The caller's AGENTS.md / CLAUDE.md text is captured before quarantine and injected into the prompt
  as explicitly SUBORDINATE context — usable for repo mechanics, but unable to change behavior,
  grant permissions, or override the rules.
- opencode.json / .opencode remain fully blocked (config + RCE), never re-injected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author
Owner

Update after checking homelab: it has no opencode.json/.opencode/ (so config quarantine costs it nothing), but its AGENTS.md carries repo-specific ops knowledge agents need — the GitOps deploy model and the migrations rule (delete a service → add a migrations/*.sh docker service rm, or it keeps running in prod).

Fully hiding it would have regressed agent quality, so I refined the fix: platform golden rules stay authoritative (agent-rules.md), but the caller AGENTS.md/CLAUDE.md text is now injected as explicitly subordinate repo notes (usable for mechanics, cannot change behavior/permissions/rules). opencode.json/.opencode/ remain fully blocked. Verified homelab's migration guidance still reaches the agent.

Update after checking `homelab`: it has **no** `opencode.json`/`.opencode/` (so config quarantine costs it nothing), but its `AGENTS.md` carries repo-specific ops knowledge agents need — the GitOps deploy model and the migrations rule (delete a service → add a `migrations/*.sh` `docker service rm`, or it keeps running in prod). Fully hiding it would have regressed agent quality, so I refined the fix: platform golden rules stay authoritative (`agent-rules.md`), but the caller `AGENTS.md`/`CLAUDE.md` text is now injected as explicitly **subordinate** repo notes (usable for mechanics, cannot change behavior/permissions/rules). `opencode.json`/`.opencode/` remain fully blocked. Verified homelab's migration guidance still reaches the agent.
Author
Owner

Closing — abandoning the opencode.json quarantine approach. Decision: a caller repo's opencode.json permission block is the intended per-repo control (e.g. homelab's read-only node1 ssh allowlist) and must be respected, not quarantined. Repos are single-owner and trusted. The real bug the homelab test surfaced was routing: the agent roster omits skills, so @pm can't route by capability. Fixing that instead.

Closing — abandoning the opencode.json quarantine approach. Decision: a caller repo's `opencode.json` **permission** block is the intended per-repo control (e.g. homelab's read-only node1 ssh allowlist) and must be respected, not quarantined. Repos are single-owner and trusted. The real bug the homelab test surfaced was routing: the agent roster omits skills, so @pm can't route by capability. Fixing that instead.
ffaerber closed this pull request 2026-07-04 10:54:36 +02:00

Pull request closed

This pull request cannot be reopened because the branch was deleted.
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: gitea/agents#37