From ac0e4563d1132e1552a3e0cea1cfc44ce47139d0 Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Fri, 16 Oct 2026 12:52:05 +0100 Subject: [PATCH] feat(setup): commit only the shared agent-settings floor nothing every contributor must share - the sandbox, the deny and ask rules, the plugin wiring - reached a fresh checkout without a manual copy step. The committed file is back, now holding only that floor plus the vetted-ops read path and the MCP tools of the adopted families, which adopt and upgrade derive from a family registry the way they derive the plugin wiring. Everything else ships as a local template that setup reconciles into .claude/settings.local.json, per worktree, together with the MCP tools of families installed on one machine only; sandbox-lint lints the two layered, keeps every protective rule in the committed file, and checks the floor's MCP allows against the registry. Generated-by: Claude Opus 5 --- .asf.yaml | 9 +- .../settings.json | 64 +---- .github/workflows/sandbox-lint.yml | 28 +-- .gitignore | 4 - .pre-commit-config.yaml | 23 +- AGENTS.md | 4 +- CONTRIBUTING.md | 41 ++-- docs/security/threat-model.md | 15 +- docs/setup/secure-agent-internals.md | 14 +- docs/setup/secure-agent-setup.md | 33 ++- docs/spec-driven-development.md | 2 +- .../skills/isolated-setup-install/SKILL.md | 43 ++-- .../skills/isolated-setup-verify/SKILL.md | 17 +- plugins/magpie-setup/skills/setup/SKILL.md | 2 +- plugins/magpie-setup/skills/setup/adopt.md | 40 +++- .../skills/setup/family-mcp-allows.json | 68 ++++++ .../skills/setup/setup_preflight/isolated.py | 4 +- .../setup_preflight/isolated_fingerprint.py | 2 +- .../magpie-setup/skills/setup/uninstall.md | 7 +- plugins/magpie-setup/skills/setup/upgrade.md | 46 +++- .../skills/setup/worktree-init.md | 14 ++ .../skills/write-skill/security-checklist.md | 2 +- tools/agent-isolation/README.md | 6 +- .../claude-settings.local.template.json | 68 ++++++ tools/sandbox-lint/README.md | 24 +- tools/sandbox-lint/expected.json | 3 +- .../sandbox-lint/src/sandbox_lint/__init__.py | 222 +++++++++++++++--- .../tests/test_doc_domain_consistency.py | 8 +- tools/sandbox-lint/tests/test_layered.py | 186 +++++++++++++++ tools/sandbox-lint/tests/test_validator.py | 60 ++--- tools/spec-loop/specs/adoption-and-setup.md | 16 +- .../specs/agent-isolation-sandbox.md | 23 +- tools/spec-loop/specs/container-gateway.md | 4 +- 33 files changed, 800 insertions(+), 302 deletions(-) rename tools/agent-isolation/claude-settings.template.json => .claude/settings.json (82%) create mode 100644 plugins/magpie-setup/skills/setup/family-mcp-allows.json create mode 100644 tools/agent-isolation/claude-settings.local.template.json create mode 100644 tools/sandbox-lint/tests/test_layered.py diff --git a/.asf.yaml b/.asf.yaml index 8b4b7ca28..f16020e98 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -124,9 +124,12 @@ github: # semantics, so a path-filtered job that doesn't post a status # would block the PR). Excluded for that reason: # `asf-allowlist-check` (paths: `.github/**`) and - # `lint the agent settings template against baseline` from - # sandbox-lint (paths: `tools/agent-isolation/claude-settings.template.json`, - # `.claude/settings.json` + `tools/sandbox-lint/**`). + # `lint .claude/settings.json against baseline` from + # sandbox-lint (paths: `.claude/settings.json`, + # `tools/agent-isolation/claude-settings.local.template.json`, + # `plugins/magpie-setup/skills/setup/family-mcp-allows.json`, + # `.gemini/**` and `tools/sandbox-lint/**`; see `paths:` in + # `.github/workflows/sandbox-lint.yml`). # # CodeQL (`Analyze (python)`, `Analyze (actions)`) is NOT # required. The framework repo is docs- and skills-heavy with diff --git a/tools/agent-isolation/claude-settings.template.json b/.claude/settings.json similarity index 82% rename from tools/agent-isolation/claude-settings.template.json rename to .claude/settings.json index 5583374e6..b789e8788 100644 --- a/tools/agent-isolation/claude-settings.template.json +++ b/.claude/settings.json @@ -3,13 +3,10 @@ "sandbox": { "enabled": true, "excludedCommands": [ - "gh *", "uv run --project ~/.claude/magpie/vetted-ops vetted-op-read *", "uv run --project ~/.claude/magpie/vetted-ops vetted-op-tracker *", "uvx --from ~/.claude/magpie/vetted-ops vetted-op-read *", - "uvx --from ~/.claude/magpie/adversarial-review adversarial-review *", - "uvx --from ~/.claude/magpie/vetted-ops vetted-op-tracker *", - "~/.claude/scripts/magpie-run-evals.sh *" + "uvx --from ~/.claude/magpie/vetted-ops vetted-op-tracker *" ], "filesystem": { "denyRead": [ @@ -19,14 +16,9 @@ ".", "~/.gitconfig", "~/.config/git/", - "~/.config/gh/", "~/.cache/", "~/.local/share/uv/", - "~/.local/bin/", - "~/.docker/bin/", - "~/.docker/cli-plugins/", - "~/.gnupg/", - "/run/user/*/gnupg/" + "~/.local/bin/" ], "allowWrite": [ "~/.cache/", @@ -37,30 +29,14 @@ "allowedDomains": [ "github.com", "api.github.com", - "api.bitbucket.org", "raw.githubusercontent.com", "objects.githubusercontent.com", "codeload.github.com", "uploads.github.com", "pypi.org", "files.pythonhosted.org", - "lists.apache.org", - "dist.apache.org", - "repository.apache.org", - "downloads.apache.org", - "archive.apache.org", - "cveprocess.apache.org", - "cve.org", - "www.cve.org", - "cveawg.mitre.org", - "api.osv.dev", - "*.crates.io", - "static.rust-lang.org", - "registry.npmjs.org", - "nodejs.org", "releases.astral.sh" - ], - "enableWeakerNetworkIsolation": true + ] }, "credentials": { "envVars": [ @@ -79,27 +55,8 @@ }, "permissions": { "allow": [ - "Bash(gh api graphql *)", - "Bash(gh pr view *)", - "Bash(gh pr list *)", - "Bash(gh pr diff *)", - "Bash(gh pr checks *)", - "Bash(gh issue view *)", - "Bash(gh issue list *)", - "Bash(gh repo view *)", - "Bash(gh repo list *)", - "Bash(gh run view *)", - "Bash(gh run list *)", - "Bash(gh run watch *)", - "Bash(gh workflow view *)", - "Bash(gh workflow list *)", - "Bash(gh release view *)", - "Bash(gh release list *)", - "Bash(gh label list *)", - "Bash(gh cache list *)", - "Bash(gh search *)", - "Bash(gh browse *)", - "Bash(gh auth status*)", + "Bash(uv run --project ~/.claude/magpie/vetted-ops vetted-op-read *)", + "Bash(uvx --from ~/.claude/magpie/vetted-ops vetted-op-read *)", "mcp__claude_ai_Gmail__get_thread", "mcp__claude_ai_Gmail__search_threads", "mcp__gmail-plaintext__create_draft", @@ -113,9 +70,6 @@ "mcp__apache-projects__get_group_members", "mcp__apache-projects__get_person", "mcp__apache-projects__search_people", - "Bash(zizmor *)", - "Bash(uv run --project ~/.claude/magpie/vetted-ops vetted-op-read *)", - "Bash(uvx --from ~/.claude/magpie/vetted-ops vetted-op-read *)", "mcp__ponymail__get_source", "mcp__ponymail__list_lists", "mcp__claude_ai_Gmail__get_message", @@ -125,10 +79,7 @@ "mcp__apache-projects__get_releases", "mcp__apache-projects__get_repositories", "mcp__apache-projects__list_committees", - "mcp__apache-projects__search_projects", - "WebFetch(domain:cveawg.mitre.org)", - "WebFetch(domain:pypi.org)", - "WebFetch(domain:lists.apache.org)" + "mcp__apache-projects__search_projects" ], "deny": [ "Read(~/.aws/**)", @@ -144,8 +95,9 @@ "Read(//**/.env.local)", "Read(//**/.env.*.local)", "Edit(.claude/settings.json)", - "Edit(tools/agent-isolation/claude-settings.template.json)", "Edit(.claude/settings.local.json)", + "Edit(tools/agent-isolation/claude-settings.local.template.json)", + "Edit(plugins/magpie-setup/skills/setup/family-mcp-allows.json)", "Edit(.apache-magpie-overrides/tools/vetted-ops/**)", "Edit(~/.claude/plugins/cache/apache-magpie/magpie-vetted-ops/**)", "Edit(~/.claude/magpie/**)", diff --git a/.github/workflows/sandbox-lint.yml b/.github/workflows/sandbox-lint.yml index f5f5852c7..f8509da35 100644 --- a/.github/workflows/sandbox-lint.yml +++ b/.github/workflows/sandbox-lint.yml @@ -17,9 +17,7 @@ # # Implements mitigation M.29 from # `docs/security/threat-model.md`: every change to the agent-host -# sandbox configuration — in this repository the template at -# `tools/agent-isolation/claude-settings.template.json`, which contributors copy to -# `.claude/settings.json` (not committed, #1639) — must be paired with +# sandbox configuration in `.claude/settings.json` must be paired with # a matching update to the canonical baseline at # `tools/sandbox-lint/expected.json`, and the resulting configuration # must satisfy the security invariants encoded in the linter. @@ -29,20 +27,18 @@ name: sandbox-lint on: # yamllint disable-line rule:truthy pull_request: paths: - - "tools/agent-isolation/claude-settings.template.json" - # Not tracked any more (#1639), so a change here means someone - # committed it again; the job must run to refuse that. - ".claude/settings.json" + - "tools/agent-isolation/claude-settings.local.template.json" + - "plugins/magpie-setup/skills/setup/family-mcp-allows.json" - ".gemini/**" - "tools/sandbox-lint/**" - ".github/workflows/sandbox-lint.yml" push: branches: [main] paths: - - "tools/agent-isolation/claude-settings.template.json" - # Not tracked any more (#1639), so a change here means someone - # committed it again; the job must run to refuse that. - ".claude/settings.json" + - "tools/agent-isolation/claude-settings.local.template.json" + - "plugins/magpie-setup/skills/setup/family-mcp-allows.json" - ".gemini/**" - "tools/sandbox-lint/**" - ".github/workflows/sandbox-lint.yml" @@ -55,7 +51,7 @@ concurrency: jobs: sandbox-lint: - name: lint the agent settings template against baseline + name: lint .claude/settings.json against baseline runs-on: ubuntu-slim permissions: contents: read @@ -87,18 +83,8 @@ jobs: # `sandbox-lint` is the workspace member's CLI entry point; # after sync it is available directly via `uv run` from the # repo root, which is what the linter expects so - # `tools/agent-isolation/claude-settings.template.json` and `tools/sandbox-lint/expected.json` + # `.claude/settings.json` and `tools/sandbox-lint/expected.json` # resolve relative to cwd. - # A committed `.claude/settings.json` is loaded by Claude Code for - # everyone who checks the repository out, and it is no longer what - # CI lints: the baseline lives in the template. Refuse it outright, - # so it cannot weaken the sandbox around the linted template. - - name: Refuse a committed .claude/settings.json - run: | - if git ls-files --error-unmatch .claude/settings.json >/dev/null 2>&1; then - echo "::error file=.claude/settings.json::.claude/settings.json is each contributor's own copy and must not be committed (#1639). Change tools/agent-isolation/claude-settings.template.json and tools/sandbox-lint/expected.json instead, and remove the file with 'git rm --cached .claude/settings.json'." - exit 1 - fi - name: Run sandbox-lint run: uv run sandbox-lint - name: Check Gemini sandbox and policy profile diff --git a/.gitignore b/.gitignore index 12358fd63..722a63628 100644 --- a/.gitignore +++ b/.gitignore @@ -14,10 +14,6 @@ logs .claude/worktrees/ .worktrees/ .claude/settings.local.json -# This repository's own agent settings are each contributor's, not the -# project's: the baseline ships as tools/agent-isolation/claude-settings.template.json -# and CONTRIBUTING.md says how to put it in place (#1639). -/.claude/settings.json # Skill source lives in skills/. The framework self-adopts by committing # magpie- symlinks (see skills/setup/adopt.md → "Local # self-adoption"). .agents/skills/ is the ONE canonical home: its diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 23cae3293..9e472f3a3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -110,24 +110,23 @@ repos: # The next three hooks are *fixers*: they open every matched file # in read-write mode (to normalise it in place) before checking # whether a fix is even needed. The secure-agent sandbox - # write-denies tracked path sets — the self-adoption skill relays - # (`.claude/skills/*` symlinks), the agent-settings template that - # contributors copy to `.claude/settings.json` - # (`tools/agent-isolation/claude-settings.template.json`: it is the - # baseline everyone inherits, so an agent may not edit it), and the - # plugin hook scripts + # write-denies tracked path sets — the agent's own config + # (`.claude/settings.json` and the local settings template it is + # layered with, `tools/agent-isolation/claude-settings.local.template.json`, + # and the family MCP registry it derives allows from, + # `plugins/magpie-setup/skills/setup/family-mcp-allows.json`), + # the self-adoption skill relays + # (`.claude/skills/*` symlinks), and the plugin hook scripts # (`plugins/magpie-setup/hooks/`) — because agents must never - # modify them. (The agent's own `.claude/settings.json` is - # write-denied too, but it is not tracked: the baseline ships as - # `tools/agent-isolation/claude-settings.template.json`, #1639.) A hook script + # modify them. A hook script # runs automatically on a harness event, so an agent that could # rewrite one could arrange to run arbitrary code on the next # session start; the deny is the point, not an oversight. So on # `prek run --all-files` (which `agent-pre-commit.sh` and the CI # mirror both run) the read-write `open()` on those paths is denied # and the whole run aborts, even though the files need no fix. - # Exclude them: the template is gated by the sandbox-lint - # baseline, the skill symlinks are validated by + # Exclude them: `.claude/settings.json` and the template are gated + # by the sandbox-lint baseline, the skill symlinks are validated by # `symlink-lint`, the upgrade hook script is checked for # existence and correct wiring by `check-family-plugins.py`, and # the vetted-ops policy is parsed by `vetted_ops.config` on every @@ -141,7 +140,7 @@ repos: # every one of these three hooks aborts the run without it. - id: end-of-file-fixer name: Make sure that there is an empty line at the end - exclude: &sandbox_write_denied ^(\.claude/skills/|tools/agent-isolation/claude-settings\.template\.json|plugins/magpie-setup/hooks/|\.apache-magpie-overrides/tools/vetted-ops/) + exclude: &sandbox_write_denied ^(\.claude/(settings\.json|skills/)|tools/agent-isolation/claude-settings\.local\.template\.json|plugins/magpie-setup/skills/setup/family-mcp-allows\.json|plugins/magpie-setup/hooks/|\.apache-magpie-overrides/tools/vetted-ops/) # `--fix=lf`, not the default `auto`: `auto` only fails a file # that mixes the two styles, so a file converted wholesale to # CRLF (an editor on Windows, `core.autocrlf=true`) passes and diff --git a/AGENTS.md b/AGENTS.md index ebf82388e..922cc2fc5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -560,7 +560,9 @@ to a home-dir path and update the tool to read from there. and `--body-file` (including the Gen-AI disclosure block) so they only need to review, not edit. - **Open a GitHub page for the human with `gh browse`, never `open `.** The sandbox blocks macOS `open` (Launch Services and Apple Events: error `-10822`), while `gh` runs outside it - (`sandbox.excludedCommands`) and `gh browse` is in `permissions.allow`, so it opens the page with no + (`sandbox.excludedCommands`) and `gh browse` is in `permissions.allow` — both from your reconciled + `.claude/settings.local.json`, not the committed file (see + [CONTRIBUTING.md → Agent settings](CONTRIBUTING.md#agent-settings)) — so it opens the page with no prompt. Use `gh browse -R `, `gh browse -R ` for a file, and `gh browse -R ` for a commit. Run it as a bare command: a pipe, `$(…)` or a redirection puts `gh` back in the sandbox, where it fails. For a page `gh browse` cannot address, diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0322b1ed6..489bb249d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -551,24 +551,6 @@ prek install The hooks are described in detail under [Running the dev loop](#running-the-dev-loop). -### Agent settings - -The repository does not commit `.claude/settings.json`: the maintainers' sandbox and -permission baseline (plus the self-adoption plugin wiring) ships as a template, -[`tools/agent-isolation/claude-settings.template.json`](tools/agent-isolation/claude-settings.template.json), -and each contributor puts it in place as their own: - -```bash -cp tools/agent-isolation/claude-settings.template.json .claude/settings.json -``` - -If you already have a `.claude/settings.json` here, merge the template's blocks into it rather than overwrite it. -Keep personal additions in `.claude/settings.local.json`, so the next template update stays a plain copy. -`.claude/settings.json` is gitignored, so your copy never shows up as a change and can differ from the baseline. -[`setup-isolated-setup-install`](skills/setup-isolated-setup-install/SKILL.md) offers the same copy or merge, -and [`setup-isolated-setup-verify`](skills/setup-isolated-setup-verify/SKILL.md) reports it when it is missing. -The template itself is what CI lints against the baseline (`sandbox-lint`), so a change to the baseline is a change to the template. - **Do not install Magpie from a marketplace to work on it.** This repo **self-adopts**: the committed `magpie-` symlinks point at the live `skills/` source, so a fresh clone already runs the skills you are editing. @@ -593,6 +575,29 @@ skill to install the bubblewrap sandbox and pinned tools, then to confirm the install. The install tutorial lives at [`docs/setup/install-recipes.md`](docs/quick-start/other-install-methods.md). +### Agent settings + +The committed [`.claude/settings.json`](.claude/settings.json) is the floor every +contributor shares: the sandbox, every `deny` and `ask` rule, the plugin wiring, the +vetted-ops read path, and the MCP tools of the families this repository uses. +Everything else (the `gh` read allows, the remaining sandbox exclusions, extra network +domains, and paths for your own tools such as GnuPG or Docker) ships as +[`tools/agent-isolation/claude-settings.local.template.json`](tools/agent-isolation/claude-settings.local.template.json) +and belongs in your gitignored `.claude/settings.local.json`, which Claude Code layers on +top. [`setup-isolated-setup-install`](skills/setup-isolated-setup-install/SKILL.md) puts it +in place, and `setup upgrade` reconciles it whenever the template changes; both hand you +the copy, since an agent cannot write its own settings. The local file is per +worktree; `setup worktree-init` reconciles each new one. Keep your own additions in the +same file: reconciliation only adds what the template gained and asks before removing +what it dropped. CI lints the two layered together against the baseline (`sandbox-lint`). + +If you set this repository up while `.claude/settings.json` was untracked (#1671), your +copy may hold personal entries: before pulling, diff it against the committed floor and +move anything personal into `.claude/settings.local.json`. Git will not overwrite an +untracked file, so then move the old one aside in your own terminal +(`mv .claude/settings.json .claude/settings.json.bak`; the agent sandbox cannot touch it) +and pull. + ### Lightening the agent context Many skills in this repository are runtime workflows for adopters diff --git a/docs/security/threat-model.md b/docs/security/threat-model.md index 1ece2b5ba..e201513bc 100644 --- a/docs/security/threat-model.md +++ b/docs/security/threat-model.md @@ -86,7 +86,7 @@ In scope for this document: - the fifteen skills in the [security workflow skill family](README.md#skills); - the privacy-LLM tooling (redactor + checker) those skills invoke on inbound content; -- the agent host's sandbox configuration in [`tools/agent-isolation/claude-settings.template.json`](../../tools/agent-isolation/claude-settings.template.json) +- the agent host's sandbox configuration in [`.claude/settings.json`](../../.claude/settings.json) and the [secure-agent-internals guide](../setup/secure-agent-internals.md); - the credential surfaces a skill may touch — `gh` tokens, CNA-tool @@ -370,8 +370,8 @@ who misconfigures the agent in a way that broadens its authority beyond what the threat model assumes. - **Capabilities** — full tracker access; ability to relax the - sandbox (each maintainer's `.claude/settings.json` is a local copy of - `tools/agent-isolation/claude-settings.template.json` and can be edited); ability to override the redactor. + sandbox (`.claude/settings.json` is in the repository and can be + edited locally); ability to override the redactor. - **Goal** — none; the persona is non-malicious. The threat is accidental disclosure or accidental relaxation of a control. - **Surface** — every skill, but the framework's defence is the @@ -547,8 +547,7 @@ agent run reads the credentials. - **Mitigation** — M.29 *(planned, not yet shipped — see [residual risk #4](#residual-risk-and-accepted-gaps))*. When implemented, - the framework's CI will lint the shipped settings template - (`tools/agent-isolation/claude-settings.template.json`) + the framework's CI will lint the shipped `.claude/settings.json` against an allowlist of changes on every PR that touches the file. The local-override case is unavoidable if the maintainer edits the file outside a PR — accepted residual. @@ -588,7 +587,7 @@ describes it. | M.11 | Label transitions in `security-issue-sync` are computed from observed external state (PR merge, release tag), not from tracker comment content. | [`security-issue-sync/SKILL.md`](../../skills/security-issue-sync/SKILL.md). | | M.12 | Public PR ↔ tracker cross-reference is one-way until Step 14. Tracker → PR link is added at PR-open time; PR → tracker link is added only after the public advisory URL is captured. | [`process.md` Steps 10 and 14](process.md). | | M.13 | Public PRs reference CVE IDs, never tracker IDs. | [`security-issue-fix/SKILL.md`](../../skills/security-issue-fix/SKILL.md) and [`security-issue-deduplicate/SKILL.md`](../../skills/security-issue-deduplicate/SKILL.md). | -| M.14 | Network egress allowlist enforced by the runtime. | [`tools/agent-isolation/claude-settings.template.json` `sandbox.network.allowedDomains`](../../tools/agent-isolation/claude-settings.template.json). | +| M.14 | Network egress allowlist enforced by the runtime. | `sandbox.network.allowedDomains`: the core hosts in [`.claude/settings.json`](../../.claude/settings.json), family-specific ones in the local template [`tools/agent-isolation/claude-settings.local.template.json`](../../tools/agent-isolation/claude-settings.local.template.json). | | M.15 | Per-skill credential scope budget. The `gh` token granted to the agent is scoped to the minimum repos required by the skill family. | Per-adopter token configuration; documented in [`docs/setup/secure-agent-internals.md`](../setup/secure-agent-internals.md). | | M.16 | CVE allocation uses a sanitised title produced by the configured `` adapter's title-normalisation (named example: [`tools/cve-tool-vulnogram/`](../../tools/cve-tool-vulnogram/) for `airflow-s`). | [`projects/_template/title-normalization.md`](../../plugins/magpie-setup/templates/title-normalization.md). | | M.17 | TLS validation against the system trust store on every egress. | Default `requests`/`httpx` behaviour; pinning is *not* used — the assumption is that the system trust store is trustworthy. | @@ -603,7 +602,7 @@ describes it. | M.26 | The agent will not draft the CVE-record submission until the public advisory URL is present in the tracker. | [`security-issue-sync/SKILL.md`](../../skills/security-issue-sync/SKILL.md). | | M.27 | The CVE record is submitted to the configured `` by the release manager, who walks it through the generic `cve_authority.states` sequence (`allocated` → `review-ready` → `publish-ready` → `public`); only `public` pushes to `cve.org`. The release manager (a human) is the readback gate at every transition. The agent runs a separate post-close `cve.org` publication-check sweep on closed-and-`announced` trackers within the last 90 days and surfaces any mismatch (record missing, state regressed, content tampered) for human review. (Named example for `airflow-s`: Vulnogram's `DRAFT` → `REVIEW` → `READY` → `PUBLIC`.) | [`tools/cve-tool-vulnogram/record.md`](../../tools/cve-tool-vulnogram/record.md); [`security-issue-sync/SKILL.md`](../../skills/security-issue-sync/SKILL.md) (`sync closed announced` mode). | | M.28 | Step-16 credit corrections are appended as new tracker comments; they never edit the closed tracker body. | [`process.md` Step 16](process.md). | -| M.29 | CI lints the settings template (`tools/agent-isolation/claude-settings.template.json`) on every PR that touches it, comparing against the shipped baseline. | **Planned, not yet shipped** — see [residual risk #4](#residual-risk-and-accepted-gaps). | +| M.29 | CI lints `.claude/settings.json`, layered with the local template, on every PR that touches either, comparing the merge against the shipped baseline and checking that every `deny`/`ask` stays in the committed file. | **Planned, not yet shipped** — see [residual risk #4](#residual-risk-and-accepted-gaps). | | M.30 | `security-model-update` scrubs its corpus before the proposal is *displayed*, not before it is sent: reporter identity, tracker contents, unpublished CVE IDs, and any detail of an unfixed or embargoed issue are removed from every candidate entry. An entry that cannot be written without describing a live vulnerability is deferred. | [`security-model-update/SKILL.md`](../../skills/security-model-update/SKILL.md). | | M.31 | Tracker URLs and `#NNN` identifiers are public-safe; tracker contents are not. Model diffs are held to the same bar as public upstream PR descriptions. | [`AGENTS.md`](../../AGENTS.md); [`docs/confidentiality.md`](../confidentiality.md). | | M.32 | A known-non-finding entry must cite a discharging claim that already exists in the model and whose component set covers the entry. Process statements, third-party assertions, and report-quality conditions (*no reproducer*, *reachability not demonstrated*) are forbidden as justification or as match conditions. | [`security-model-update/SKILL.md`](../../skills/security-model-update/SKILL.md). | @@ -704,7 +703,7 @@ below are the framework's commitment. STRIDE row to the matching skill family in [STRIDE matrix per skill family](#stride-matrix-per-skill-family). An Agentic Drafting skill without a row does not pass review. -- **On every change to the settings template (`tools/agent-isolation/claude-settings.template.json`)** — the proposing +- **On every change to `.claude/settings.json`** — the proposing PR must update the [Trust boundaries](#trust-boundaries) and [Mitigation cross-reference](#mitigation-cross-reference) sections to reflect the new sandbox posture. diff --git a/docs/setup/secure-agent-internals.md b/docs/setup/secure-agent-internals.md index 3c88c529c..79ec3c080 100644 --- a/docs/setup/secure-agent-internals.md +++ b/docs/setup/secure-agent-internals.md @@ -84,7 +84,7 @@ It does **not** defend against: | **3. Forced confirmation** | Claude Code's `permissions.ask` | Visible-to-others writes (`git push`, `gh pr create`, …) without an explicit yes. | Layers 1, 2, and 3 are configured by the same -[`tools/agent-isolation/claude-settings.template.json`](../../tools/agent-isolation/claude-settings.template.json) the framework +[`.claude/settings.json`](../../.claude/settings.json) the framework dogfoods. Adopters copy the same shape into their own tracker repo (see [Adopter setup](secure-agent-setup.md#adopter-setup) @@ -110,11 +110,13 @@ The two layers are complementary, not redundant. The sandbox stops a Bash subprocess (an MCP server's child process, a `gh` CLI call, a `python` snippet the model decided to run) from reading a denied path. `permissions.deny` stops the agent's Read tool from reading -the same path. A secure setup needs both: the framework's -[`tools/agent-isolation/claude-settings.template.json`](../../tools/agent-isolation/claude-settings.template.json) deny-lists -`Read(~/.config/gh/**)` *and* allow-reads `~/.config/gh/` in the -sandbox, so `gh` can see its token but the agent can never read -the file. +the same path. A secure setup needs both: the framework's committed +[`.claude/settings.json`](../../.claude/settings.json) deny-lists +`Read(~/.config/gh/**)`, *and* its local template +[`claude-settings.local.template.json`](../../tools/agent-isolation/claude-settings.local.template.json) +(layered in through `.claude/settings.local.json`) allow-reads +`~/.config/gh/` in the sandbox, so `gh` can see its token but the agent +can never read the file. ## Linux: bubblewrap + user namespaces diff --git a/docs/setup/secure-agent-setup.md b/docs/setup/secure-agent-setup.md index c303e2173..f196bc0f4 100644 --- a/docs/setup/secure-agent-setup.md +++ b/docs/setup/secure-agent-setup.md @@ -464,12 +464,14 @@ actually lands the bump. ## The framework's own `.claude/settings.json` -The framework dogfoods the secure config. It ships as the template -[`tools/agent-isolation/claude-settings.template.json`](../../tools/agent-isolation/claude-settings.template.json); -the repository does not commit `.claude/settings.json`, and each contributor -puts the template in place as their own (see -[CONTRIBUTING.md → Agent settings](../../CONTRIBUTING.md#agent-settings)). -The full block is below, annotated. +The framework dogfoods the secure config in two layers: the committed +floor [`.claude/settings.json`](../../.claude/settings.json) (the sandbox, +every `deny` and `ask` rule, the plugin wiring, the vetted-ops read path and +the adopted families' MCP tools) and the local template +[`tools/agent-isolation/claude-settings.local.template.json`](../../tools/agent-isolation/claude-settings.local.template.json), +which each contributor reconciles into `.claude/settings.local.json` +([CONTRIBUTING.md → Agent settings](../../CONTRIBUTING.md#agent-settings)). +The full block below is the two merged, annotated. ```jsonc { @@ -1593,7 +1595,7 @@ about to approve. ### Why install it user-scope, not project-scope Unlike the framework's -[`tools/agent-isolation/claude-settings.template.json`](../../tools/agent-isolation/claude-settings.template.json) (which is +[`.claude/settings.json`](../../.claude/settings.json) (which is repo-scoped — only sessions started inside the tracker repo see it), this hook is most useful in **`~/.claude/settings.json`** — the user-scope config that applies @@ -2075,7 +2077,7 @@ echo '{"model":{"display_name":"Sonnet 4.6"},"workspace":{"current_dir":"'"$PWD" ``` Expected output, *inside* this repo (its -[`tools/agent-isolation/claude-settings.template.json`](../../tools/agent-isolation/claude-settings.template.json) sets +[`.claude/settings.json`](../../.claude/settings.json) sets `sandbox.enabled: true`, and assuming `.claude/settings.local.json` either does not exist or does not override the key): a line opening with `[sandbox]` in green, then `magpie`, the current branch, the PR @@ -3132,10 +3134,17 @@ sub-section that follows. 1. Install the pinned tools per [Install commands](#install-commands) above. -2. Copy - [`tools/agent-isolation/claude-settings.template.json`](../../tools/agent-isolation/claude-settings.template.json) from the framework - snapshot at `/.apache-magpie/.claude/settings.json` - into `/.claude/settings.json`. Adjust: +2. Build `/.claude/settings.json` from the framework's two + layers, the committed floor + [`.claude/settings.json`](../../.claude/settings.json) and the local + template + [`tools/agent-isolation/claude-settings.local.template.json`](../../tools/agent-isolation/claude-settings.local.template.json) + (the annotated block below is the two merged). The floor alone lacks the + `gh *` exclusion, the `gh` read allows and the family hosts, so a project + that copies only it gets a sandboxed `gh` and no egress for the security + and release skills. Drop the framework-only `Edit(...)` denies for + `tools/agent-isolation/` and `plugins/magpie-setup/` paths your project + does not have. Adjust: - The `sandbox.network.allowedDomains` list — drop the framework domains you don't actually use, add any project-specific hosts. - The `sandbox.filesystem.allowRead` list — same: drop the diff --git a/docs/spec-driven-development.md b/docs/spec-driven-development.md index 950b9064e..5fdb02f5f 100644 --- a/docs/spec-driven-development.md +++ b/docs/spec-driven-development.md @@ -107,7 +107,7 @@ single local branch. ## Why it never pushes `git push` and `gh pr create` are in the `ask` list of -[`tools/agent-isolation/claude-settings.template.json`](../tools/agent-isolation/claude-settings.template.json) — they require a human +[`.claude/settings.json`](../.claude/settings.json) — they require a human confirmation. The loop honours that: it ends every iteration at a **local commit** and prints the exact commands for the human to run: diff --git a/plugins/magpie-setup/skills/isolated-setup-install/SKILL.md b/plugins/magpie-setup/skills/isolated-setup-install/SKILL.md index a5c2471f6..a59da551a 100644 --- a/plugins/magpie-setup/skills/isolated-setup-install/SKILL.md +++ b/plugins/magpie-setup/skills/isolated-setup-install/SKILL.md @@ -15,9 +15,9 @@ when_to_use: >- If it is already installed, use `setup-isolated-setup-verify` to check it or `setup-isolated-setup-update` to refresh it. capability: capability:platform -surface_hash: sha256:8a931772d33ac93f +surface_hash: sha256:fe5f2a0f66586cb2 license: Apache-2.0 -measured_tokens: 6894 +measured_tokens: 6821 ---