Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .asf.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 7 additions & 5 deletions .github/workflows/sandbox-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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-<skill> symlinks (see skills/setup/adopt.md → "Local
# self-adoption"). .agents/skills/ is the ONE canonical home: its
Expand Down
19 changes: 12 additions & 7 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
19 changes: 19 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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-<skill>` symlinks point at the live
`skills/` source, so a fresh clone already runs the skills you are editing.
Expand Down
15 changes: 8 additions & 7 deletions docs/security/threat-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 `<cve-tool>` 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. |
Expand All @@ -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 `<cve-tool>` 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). |
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions docs/setup/secure-agent-internals.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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.
Expand Down
15 changes: 9 additions & 6 deletions docs/setup/secure-agent-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 `<your-tracker>/.apache-magpie/.claude/settings.json`
into `<your-tracker>/.claude/settings.json`. Adjust:
- The `sandbox.network.allowedDomains` list — drop the framework
Expand Down
2 changes: 1 addition & 1 deletion docs/spec-driven-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
Loading