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
9 changes: 6 additions & 3 deletions .asf.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand All @@ -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/",
Expand All @@ -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": [
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -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/**)",
Expand All @@ -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/**)",
Expand Down
28 changes: 7 additions & 21 deletions .github/workflows/sandbox-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 0 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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-<skill> symlinks (see skills/setup/adopt.md → "Local
# self-adoption"). .agents/skills/ is the ONE canonical home: its
Expand Down
23 changes: 11 additions & 12 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <url>`.** 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 <PR-or-issue-number> -R <repo>`, `gh browse <path> -R <repo>` for a file, and
`gh browse <commit-SHA> -R <repo>` 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,
Expand Down
41 changes: 23 additions & 18 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<skill>` symlinks point at the live
`skills/` source, so a fresh clone already runs the skills you are editing.
Expand All @@ -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
Expand Down
Loading