From a91550597f503bd0d69f6910200dbe7ef51e8fd1 Mon Sep 17 00:00:00 2001 From: Jarek Potiuk Date: Fri, 16 Oct 2026 10:35:57 +0100 Subject: [PATCH] feat(setup): ship the framework's agent settings as a template The repository committed its own .claude/settings.json, so every contributor's agent silently inherited the maintainers' sandbox, permission and self-adoption choices, any change to them was a repository change, and a checkout could not differ from the baseline without a dirty tree. The file now ships, unchanged, as tools/agent-isolation/claude-settings.template.json, and .claude/settings.json is gitignored: - sandbox-lint lints the template by default; the workflow, its job name and the .asf.yaml note follow, so the baseline stays linted in CI. - The pre-flight fingerprint covers the template through tools/agent-isolation/ (constant regenerated). - CONTRIBUTING gains "Agent settings" (copy or merge the template; personal additions in settings.local.json); setup-isolated-setup-install gains Step T for a framework checkout; setup-isolated-setup-verify check 1 reports a missing copy. - Links and prose that meant this repository's file point at the template (threat model M.14/M.29, secure-agent docs, the write-skill checklist, specs). An adopter's committed .claude/settings.json floor is unchanged. Closes #1639. Generated-by: Claude Opus 5 --- .asf.yaml | 4 +- .github/workflows/sandbox-lint.yml | 12 +++-- .gitignore | 4 ++ .pre-commit-config.yaml | 19 +++++--- CONTRIBUTING.md | 19 ++++++++ docs/security/threat-model.md | 15 +++--- docs/setup/secure-agent-internals.md | 4 +- docs/setup/secure-agent-setup.md | 15 +++--- docs/spec-driven-development.md | 2 +- .../skills/isolated-setup-install/SKILL.md | 30 ++++++++++-- .../skills/isolated-setup-verify/SKILL.md | 3 +- .../skills/setup/setup_preflight/isolated.py | 4 +- .../setup_preflight/isolated_fingerprint.py | 2 +- .../skills/write-skill/security-checklist.md | 2 +- tools/agent-isolation/README.md | 2 +- .../claude-settings.template.json | 1 + tools/sandbox-lint/README.md | 15 ++++-- tools/sandbox-lint/expected.json | 1 + .../sandbox-lint/src/sandbox_lint/__init__.py | 46 ++++++++++++++++--- .../tests/test_doc_domain_consistency.py | 2 +- tools/sandbox-lint/tests/test_validator.py | 40 +++++++++++++++- tools/spec-loop/specs/adoption-and-setup.md | 5 +- .../specs/agent-isolation-sandbox.md | 9 ++-- tools/spec-loop/specs/container-gateway.md | 4 +- 24 files changed, 198 insertions(+), 62 deletions(-) rename .claude/settings.json => tools/agent-isolation/claude-settings.template.json (99%) diff --git a/.asf.yaml b/.asf.yaml index 3f024d807..02d083c13 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -124,8 +124,8 @@ 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 .claude/settings.json against baseline` from - # sandbox-lint (paths: `.claude/settings.json` + + # `lint the agent settings template against baseline` from + # sandbox-lint (paths: `tools/agent-isolation/claude-settings.template.json` + # `tools/sandbox-lint/**`). # # CodeQL (`Analyze (python)`, `Analyze (actions)`) is NOT diff --git a/.github/workflows/sandbox-lint.yml b/.github/workflows/sandbox-lint.yml index 0c1173626..16907dc02 100644 --- a/.github/workflows/sandbox-lint.yml +++ b/.github/workflows/sandbox-lint.yml @@ -17,7 +17,9 @@ # # Implements mitigation M.29 from # `docs/security/threat-model.md`: every change to the agent-host -# sandbox configuration in `.claude/settings.json` must be paired with +# 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 # 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. @@ -27,14 +29,14 @@ name: sandbox-lint on: # yamllint disable-line rule:truthy pull_request: paths: - - ".claude/settings.json" + - "tools/agent-isolation/claude-settings.template.json" - ".gemini/**" - "tools/sandbox-lint/**" - ".github/workflows/sandbox-lint.yml" push: branches: [main] paths: - - ".claude/settings.json" + - "tools/agent-isolation/claude-settings.template.json" - ".gemini/**" - "tools/sandbox-lint/**" - ".github/workflows/sandbox-lint.yml" @@ -47,7 +49,7 @@ concurrency: jobs: sandbox-lint: - name: lint .claude/settings.json against baseline + name: lint the agent settings template against baseline runs-on: ubuntu-slim permissions: contents: read @@ -79,7 +81,7 @@ 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 - # `.claude/settings.json` and `tools/sandbox-lint/expected.json` + # `tools/agent-isolation/claude-settings.template.json` and `tools/sandbox-lint/expected.json` # resolve relative to cwd. - name: Run sandbox-lint run: uv run sandbox-lint diff --git a/.gitignore b/.gitignore index 722a63628..12358fd63 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,10 @@ 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 7fe301a23..23cae3293 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -110,19 +110,24 @@ 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 three tracked path sets — the agent's own config - # (`.claude/settings.json`), the self-adoption skill relays - # (`.claude/skills/*` symlinks), and the plugin hook scripts + # 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 # (`plugins/magpie-setup/hooks/`) — because agents must never - # modify them. A hook script + # 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 # 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: `.claude/settings.json` is gated by the - # sandbox-lint baseline, the skill symlinks are validated by + # Exclude them: the template is 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 @@ -136,7 +141,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/(settings\.json|skills/)|plugins/magpie-setup/hooks/|\.apache-magpie-overrides/tools/vetted-ops/) + exclude: &sandbox_write_denied ^(\.claude/skills/|tools/agent-isolation/claude-settings\.template\.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/CONTRIBUTING.md b/CONTRIBUTING.md index 9bb4e165c..0322b1ed6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,6 +20,7 @@ - [Groovy](#groovy) - [Shell scripts](#shell-scripts) - [Getting set up](#getting-set-up) + - [Agent settings](#agent-settings) - [Lightening the agent context](#lightening-the-agent-context) - [Making changes](#making-changes) - [Authoring with an agent](#authoring-with-an-agent) @@ -550,6 +551,24 @@ 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. diff --git a/docs/security/threat-model.md b/docs/security/threat-model.md index 825a1f2e1..1ece2b5ba 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 [`.claude/settings.json`](../../.claude/settings.json) +- the agent host's sandbox configuration in [`tools/agent-isolation/claude-settings.template.json`](../../tools/agent-isolation/claude-settings.template.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 (`.claude/settings.json` is in the repository and can be - edited locally); ability to override the redactor. + 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. - **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,7 +547,8 @@ 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 `.claude/settings.json` + the framework's CI will lint the shipped settings template + (`tools/agent-isolation/claude-settings.template.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. @@ -587,7 +588,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. | [`.claude/settings.json` `sandbox.network.allowedDomains`](../../.claude/settings.json). | +| 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.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. | @@ -602,7 +603,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 `.claude/settings.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 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.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). | @@ -703,7 +704,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 `.claude/settings.json`** — the proposing +- **On every change to the settings template (`tools/agent-isolation/claude-settings.template.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 e76845c30..3c88c529c 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 -[`.claude/settings.json`](../../.claude/settings.json) the framework +[`tools/agent-isolation/claude-settings.template.json`](../../tools/agent-isolation/claude-settings.template.json) the framework dogfoods. Adopters copy the same shape into their own tracker repo (see [Adopter setup](secure-agent-setup.md#adopter-setup) @@ -111,7 +111,7 @@ 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 -[`.claude/settings.json`](../../.claude/settings.json) deny-lists +[`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. diff --git a/docs/setup/secure-agent-setup.md b/docs/setup/secure-agent-setup.md index fa5bd1582..b87e6695c 100644 --- a/docs/setup/secure-agent-setup.md +++ b/docs/setup/secure-agent-setup.md @@ -464,9 +464,12 @@ actually lands the bump. ## The framework's own `.claude/settings.json` -The framework dogfoods the secure config in -[`.claude/settings.json`](../../.claude/settings.json). The full block is -below, annotated. +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. ```jsonc { @@ -1594,7 +1597,7 @@ about to approve. ### Why install it user-scope, not project-scope Unlike the framework's -[`.claude/settings.json`](../../.claude/settings.json) (which is +[`tools/agent-isolation/claude-settings.template.json`](../../tools/agent-isolation/claude-settings.template.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 @@ -2076,7 +2079,7 @@ echo '{"model":{"display_name":"Sonnet 4.6"},"workspace":{"current_dir":"'"$PWD" ``` Expected output, *inside* this repo (its -[`.claude/settings.json`](../../.claude/settings.json) sets +[`tools/agent-isolation/claude-settings.template.json`](../../tools/agent-isolation/claude-settings.template.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 @@ -3134,7 +3137,7 @@ sub-section that follows. 1. Install the pinned tools per [Install commands](#install-commands) above. 2. Copy - [`.claude/settings.json`](../../.claude/settings.json) from the framework + [`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: - The `sandbox.network.allowedDomains` list — drop the framework diff --git a/docs/spec-driven-development.md b/docs/spec-driven-development.md index 5fdb02f5f..950b9064e 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 -[`.claude/settings.json`](../.claude/settings.json) — they require a human +[`tools/agent-isolation/claude-settings.template.json`](../tools/agent-isolation/claude-settings.template.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 f236b6b3a..cd997e1de 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:09bcd5e69ccdacd8 +surface_hash: sha256:8a931772d33ac93f license: Apache-2.0 -measured_tokens: 6692 +measured_tokens: 6934 ---