You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@magpie.apache.org by po...@apache.org on 2026/09/26 11:59:59 UTC
(magpie) branch main updated: fix(setup): ship the read-only allow set to adopters; allow both vetted-op-read forms (#1393)
This is an automated email from the ASF dual-hosted git repository.
potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/magpie.git
The following commit(s) were added to refs/heads/main by this push:
new e0bf9a29 fix(setup): ship the read-only allow set to adopters; allow both vetted-op-read forms (#1393)
e0bf9a29 is described below
commit e0bf9a29fad1dedc6b343279b0f1953764f04237
Author: Jarek Potiuk <ja...@potiuk.com>
AuthorDate: Sat Sep 26 13:59:53 2026 +0200
fix(setup): ship the read-only allow set to adopters; allow both vetted-op-read forms (#1393)
The adopter install copied sandbox, deny and ask but never
permissions.allow, and update never diffed it, so every skill read
prompted in an installed repo. vetted-op-read was excluded only in its
uv run --project form and allowed in neither; gatherer agents use
uvx --from. Allow and exclude both forms, add the missing read-only
MCP tools and registry WebFetch hosts, and teach install/update to
carry and diff the allow set. vetted-op (writes) stays on ask.
Generated-by: Claude Opus 5
---
.claude/settings.json | 18 ++++++++-
docs/setup/secure-agent-setup.md | 44 ++++++++++++++++++++--
.../skills/isolated-setup-install/SKILL.md | 19 ++++++++--
.../skills/isolated-setup-update/SKILL.md | 6 ++-
tools/sandbox-lint/expected.json | 18 ++++++++-
tools/sandbox-lint/tests/test_validator.py | 21 +++++++++++
.../src/setup_preflight/isolated_fingerprint.py | 2 +-
7 files changed, 117 insertions(+), 11 deletions(-)
diff --git a/.claude/settings.json b/.claude/settings.json
index 15c1aa5c..466b8774 100644
--- a/.claude/settings.json
+++ b/.claude/settings.json
@@ -6,6 +6,7 @@
"gh *",
"uv run --project tools/vetted-ops vetted-op-read *",
"uv run --project ~/.claude/plugins/cache/apache-magpie/magpie-vetted-ops/*/tools/vetted-ops vetted-op-read *",
+ "uvx --from ~/.claude/plugins/cache/apache-magpie/magpie-vetted-ops/*/tools/vetted-ops vetted-op-read *",
"uvx --from ~/.claude/plugins/cache/apache-magpie/magpie-adversarial-review/*/tools/adversarial-review adversarial-review *",
"~/.claude/scripts/magpie-run-evals.sh *"
],
@@ -108,7 +109,22 @@
"mcp__apache-projects__get_group_members",
"mcp__apache-projects__get_person",
"mcp__apache-projects__search_people",
- "Bash(zizmor *)"
+ "Bash(zizmor *)",
+ "Bash(uv run --project ~/.claude/plugins/cache/apache-magpie/magpie-vetted-ops/*/tools/vetted-ops vetted-op-read *)",
+ "Bash(uvx --from ~/.claude/plugins/cache/apache-magpie/magpie-vetted-ops/*/tools/vetted-ops vetted-op-read *)",
+ "mcp__ponymail__get_source",
+ "mcp__ponymail__list_lists",
+ "mcp__claude_ai_Gmail__get_message",
+ "mcp__claude_ai_Gmail__list_drafts",
+ "mcp__claude_ai_Gmail__get_draft",
+ "mcp__gmail-plaintext__check_auth",
+ "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)"
],
"deny": [
"Read(~/.aws/**)",
diff --git a/docs/setup/secure-agent-setup.md b/docs/setup/secure-agent-setup.md
index 98e78db1..a8cd196e 100644
--- a/docs/setup/secure-agent-setup.md
+++ b/docs/setup/secure-agent-setup.md
@@ -496,8 +496,13 @@ below, annotated.
// ~/.claude). Only its single-line, installed-plugin form is excluded;
// it keeps its permission prompt (no `allow`), and the plugin cache is
// `Edit`-denied below. See the isolated-setup-install skill, Step R.
+ // The vetted-ops READ dispatcher calls `gh` and the network, so it runs
+ // outside the sandbox too — in both invocation forms the skills and the
+ // read-only gatherer agents use (`uv run --project` and `uvx --from`).
"excludedCommands": [
"gh *",
+ "uv run --project ~/.claude/plugins/cache/apache-magpie/magpie-vetted-ops/*/tools/vetted-ops vetted-op-read *",
+ "uvx --from ~/.claude/plugins/cache/apache-magpie/magpie-vetted-ops/*/tools/vetted-ops vetted-op-read *",
"uvx --from ~/.claude/plugins/cache/apache-magpie/magpie-adversarial-review/*/tools/adversarial-review adversarial-review *"
],
// The `lychee` link-check hook runs in OFFLINE mode (`offline =
@@ -632,7 +637,28 @@ below, annotated.
// Allowlisting it on the strength of a read-only *caller name* would
// grant the whole catalogue, because --caller is chosen by the caller.
// The version segment is globbed: the plugin cache is versioned per install.
- "Bash(uv run --project ~/.claude/plugins/cache/apache-magpie/magpie-vetted-ops/*/tools/vetted-ops vetted-op-read *)"
+ "Bash(uv run --project ~/.claude/plugins/cache/apache-magpie/magpie-vetted-ops/*/tools/vetted-ops vetted-op-read *)",
+ "Bash(uvx --from ~/.claude/plugins/cache/apache-magpie/magpie-vetted-ops/*/tools/vetted-ops vetted-op-read *)", // same dispatcher, the form bulk gatherer agents use
+ // Read-only MCP tools the security skills call on every sync / import /
+ // triage run. Without these, each archive or mailbox read prompts, and a
+ // bulk sync fans out into hundreds of prompts. Write tools stay off this
+ // list (drafts, logins, label/thread mutations keep their prompt), with
+ // one exception: gmail-plaintext `create_draft` only creates an unsent
+ // draft the operator reviews in Gmail.
+ "mcp__ponymail__search_list", "mcp__ponymail__get_thread", "mcp__ponymail__get_email",
+ "mcp__ponymail__get_source", "mcp__ponymail__list_lists", "mcp__ponymail__list_restrictions",
+ "mcp__ponymail__auth_status",
+ "mcp__claude_ai_Gmail__search_threads", "mcp__claude_ai_Gmail__get_thread",
+ "mcp__claude_ai_Gmail__get_message", "mcp__claude_ai_Gmail__list_drafts", "mcp__claude_ai_Gmail__get_draft",
+ "mcp__gmail-plaintext__create_draft", "mcp__gmail-plaintext__check_auth",
+ "mcp__apache-projects__get_committee", "mcp__apache-projects__get_group_members",
+ "mcp__apache-projects__get_person", "mcp__apache-projects__search_people",
+ "mcp__apache-projects__project_stats", "mcp__apache-projects__get_releases",
+ "mcp__apache-projects__get_repositories", "mcp__apache-projects__list_committees",
+ "mcp__apache-projects__search_projects",
+ // Read-only fetches of public registries the skills consult: cve.org
+ // publication state, PyPI release detection, the public ASF list archive.
+ "WebFetch(domain:cveawg.mitre.org)", "WebFetch(domain:pypi.org)", "WebFetch(domain:lists.apache.org)"
],
"deny": [
"Read(~/.aws/**)", "Read(~/.ssh/**)", "Read(~/.netrc)",
@@ -2967,8 +2993,13 @@ Then walk through:
2. **Project `.claude/settings.json`.** Read
`<magpie>/.claude/settings.json` and copy its
- `sandbox`, `permissions.deny`, and `permissions.ask` blocks
- into this repo's `.claude/settings.json`. If a project
+ `sandbox`, `permissions.allow`, `permissions.deny`, and
+ `permissions.ask` blocks into this repo's
+ `.claude/settings.json`. `permissions.allow` is the read-only
+ set (read-only `gh`, the vetted-ops read dispatcher, archive /
+ mailbox / roster MCP reads) — without it every read the skills
+ make prompts, and a bulk sync prompts hundreds of times. Copy
+ only read-only entries; never add a write rule to `allow`. If a project
settings.json already exists, surface a diff of the merged
result first and ask me to approve before writing.
@@ -3385,7 +3416,12 @@ anything — I will decide what to apply:
`mcp/<server>/` dir, plus the
`github.com/apache/comdev/compare/<sha>...main` link. These
servers track `main` by design — no manifest bump, no cooldown.
-5. Re-run `cat ~/.aws/credentials`, `echo $AWS_ACCESS_KEY_ID`,
+5. Diff my project `.claude/settings.json` `permissions.allow`
+ against the framework's. List every read-only entry I am
+ missing (new vetted-ops read forms, MCP read tools, WebFetch
+ hosts) — each one is a prompt I am paying on every run — and
+ any entry I have that is not read-only. Do not merge.
+6. Re-run `cat ~/.aws/credentials`, `echo $AWS_ACCESS_KEY_ID`,
`curl https://example.com` and confirm each is still denied.
Note any newly-allowed call as a regression to investigate.
```
diff --git a/plugins/magpie-setup/skills/isolated-setup-install/SKILL.md b/plugins/magpie-setup/skills/isolated-setup-install/SKILL.md
index 24403bee..6110fc6e 100644
--- a/plugins/magpie-setup/skills/isolated-setup-install/SKILL.md
+++ b/plugins/magpie-setup/skills/isolated-setup-install/SKILL.md
@@ -17,7 +17,7 @@ when_to_use: >-
capability: capability:platform
surface_hash: sha256:eb1b228a501f2772
license: Apache-2.0
-measured_tokens: 4553
+measured_tokens: 4820
---
<!-- Placeholder convention (see AGENTS.md#placeholder-convention-used-in-skill-files):
@@ -115,7 +115,12 @@ Drift severity:
user already has a project `.claude/settings.json` or a
user-scope `~/.claude/settings.json`, the skill *diffs* the
desired merge against the existing file and asks for explicit
- approval before writing. Re-installs / partial-state recoveries
+ approval before writing. The merge carries the framework's
+ `permissions.allow` set (the read-only reads: `gh` reads, the
+ vetted-ops read dispatcher, archive / mailbox / roster MCP reads,
+ registry `WebFetch` hosts) alongside `deny` and `ask` — leaving it
+ out makes every skill read prompt. Only read-only entries belong in
+ `allow`. Re-installs / partial-state recoveries
are common — the skill must not blow away an unrelated
pre-existing hook or `permissions.ask` rule. The desired merge
**includes the agent-guard `hooks.PreToolUse` entry** (matcher
@@ -234,7 +239,8 @@ writes structurally.
```jsonc
"permissions": {
"allow": [
- "Bash(uv run --project ~/.claude/plugins/cache/apache-magpie/magpie-vetted-ops/*/tools/vetted-ops vetted-op-read *)"
+ "Bash(uv run --project ~/.claude/plugins/cache/apache-magpie/magpie-vetted-ops/*/tools/vetted-ops vetted-op-read *)",
+ "Bash(uvx --from ~/.claude/plugins/cache/apache-magpie/magpie-vetted-ops/*/tools/vetted-ops vetted-op-read *)"
],
"ask": [
"Bash(uv run --project ~/.claude/plugins/cache/apache-magpie/magpie-vetted-ops/*/tools/vetted-ops vetted-op *)"
@@ -253,6 +259,13 @@ writes structurally.
}
```
+Allow **both** invocation forms, and add both to
+`sandbox.excludedCommands` (`"uv run --project ~/.claude/plugins/cache/apache-magpie/magpie-vetted-ops/*/tools/vetted-ops vetted-op-read *"` and
+`"uvx --from ~/.claude/plugins/cache/apache-magpie/magpie-vetted-ops/*/tools/vetted-ops vetted-op-read *"`). Skills document `uv run --project`; read-only
+gatherer agents use `uvx --from`, because `uv run` in the plugin cache
+needs to write a venv there. A rule for only one form leaves every call
+in the other form prompting.
+
Tell the operator plainly what this buys and what it does not:
- The catalogue is protected twice — by these rules, and by the
diff --git a/plugins/magpie-setup/skills/isolated-setup-update/SKILL.md b/plugins/magpie-setup/skills/isolated-setup-update/SKILL.md
index 04b58472..c25a507f 100644
--- a/plugins/magpie-setup/skills/isolated-setup-update/SKILL.md
+++ b/plugins/magpie-setup/skills/isolated-setup-update/SKILL.md
@@ -18,7 +18,7 @@ when_to_use: >-
capability: capability:platform
surface_hash: sha256:1f327e069312dad2
license: Apache-2.0
-measured_tokens: 4530
+measured_tokens: 4635
---
<!-- Placeholder convention (see AGENTS.md#placeholder-convention-used-in-skill-files):
@@ -134,6 +134,10 @@ Walk each:
With the plugin, that hook comes from the plugin manifest and its absence from `settings.json` is correct.
Report new entries the user does not have; do not auto-merge.
+ **Diff `permissions.allow` too, not only `deny` / `ask`.**
+ Every read-only entry the framework allows and the user lacks — a vetted-ops read form, an MCP read tool, a registry `WebFetch` host — is a permission prompt on every skill run; a bulk sync multiplies it by the number of trackers.
+ List the missing entries as "prompts you are paying", and separately flag any entry the user has in `allow` that is not read-only.
+
Two `sandbox.network.*` settings are worth a look while diffing, but neither is a "missing default" to re-add:
- **`allowedDomains` is deliberately narrow.**
diff --git a/tools/sandbox-lint/expected.json b/tools/sandbox-lint/expected.json
index 15c1aa5c..466b8774 100644
--- a/tools/sandbox-lint/expected.json
+++ b/tools/sandbox-lint/expected.json
@@ -6,6 +6,7 @@
"gh *",
"uv run --project tools/vetted-ops vetted-op-read *",
"uv run --project ~/.claude/plugins/cache/apache-magpie/magpie-vetted-ops/*/tools/vetted-ops vetted-op-read *",
+ "uvx --from ~/.claude/plugins/cache/apache-magpie/magpie-vetted-ops/*/tools/vetted-ops vetted-op-read *",
"uvx --from ~/.claude/plugins/cache/apache-magpie/magpie-adversarial-review/*/tools/adversarial-review adversarial-review *",
"~/.claude/scripts/magpie-run-evals.sh *"
],
@@ -108,7 +109,22 @@
"mcp__apache-projects__get_group_members",
"mcp__apache-projects__get_person",
"mcp__apache-projects__search_people",
- "Bash(zizmor *)"
+ "Bash(zizmor *)",
+ "Bash(uv run --project ~/.claude/plugins/cache/apache-magpie/magpie-vetted-ops/*/tools/vetted-ops vetted-op-read *)",
+ "Bash(uvx --from ~/.claude/plugins/cache/apache-magpie/magpie-vetted-ops/*/tools/vetted-ops vetted-op-read *)",
+ "mcp__ponymail__get_source",
+ "mcp__ponymail__list_lists",
+ "mcp__claude_ai_Gmail__get_message",
+ "mcp__claude_ai_Gmail__list_drafts",
+ "mcp__claude_ai_Gmail__get_draft",
+ "mcp__gmail-plaintext__check_auth",
+ "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)"
],
"deny": [
"Read(~/.aws/**)",
diff --git a/tools/sandbox-lint/tests/test_validator.py b/tools/sandbox-lint/tests/test_validator.py
index 172efcd9..a3492777 100644
--- a/tools/sandbox-lint/tests/test_validator.py
+++ b/tools/sandbox-lint/tests/test_validator.py
@@ -94,6 +94,27 @@ def test_baseline_asks_on_gh_writes_not_on_reads(baseline: dict[str, Any]) -> No
assert "Bash(gh pr view *)" in baseline["permissions"]["allow"]
+VETTED_OP_READ = "~/.claude/plugins/cache/apache-magpie/magpie-vetted-ops/*/tools/vetted-ops vetted-op-read *"
+
+
+@pytest.mark.parametrize("form", ["uv run --project", "uvx --from"])
+def test_baseline_allows_and_excludes_every_vetted_op_read_form(baseline: dict[str, Any], form: str) -> None:
+ # Skills invoke the read dispatcher with `uv run --project`, read-only
+ # gatherer agents with `uvx --from`. A rule for only one form leaves every
+ # call in the other form prompting (a bulk sync makes hundreds of them) or
+ # failing inside the sandbox, where it cannot reach gh or the network.
+ assert f"{form} {VETTED_OP_READ}" in baseline["sandbox"]["excludedCommands"]
+ assert f"Bash({form} {VETTED_OP_READ})" in baseline["permissions"]["allow"]
+
+
+def test_baseline_never_allows_the_vetted_op_write_dispatcher(baseline: dict[str, Any]) -> None:
+ # `--caller` is an argv string the caller picks, so an allow on the write
+ # dispatcher would grant the whole catalogue. It stays on ask.
+ write = VETTED_OP_READ.replace("vetted-op-read", "vetted-op")
+ for form in ("uv run --project", "uvx --from"):
+ assert f"Bash({form} {write})" not in baseline["permissions"]["allow"]
+
+
def test_catch_all_gh_ask_rule_is_an_invariant_error(baseline: dict[str, Any]) -> None:
weakened = copy.deepcopy(baseline)
weakened["permissions"]["ask"].append("Bash(gh *)")
diff --git a/tools/setup-preflight/src/setup_preflight/isolated_fingerprint.py b/tools/setup-preflight/src/setup_preflight/isolated_fingerprint.py
index a978446b..78ba1122 100644
--- a/tools/setup-preflight/src/setup_preflight/isolated_fingerprint.py
+++ b/tools/setup-preflight/src/setup_preflight/isolated_fingerprint.py
@@ -21,4 +21,4 @@ The isolated-setup fingerprint of the framework this package shipped with,
for installs that do not carry the framework source (see `isolated.py`).
"""
-FRAMEWORK_FINGERPRINT = "sha256:17ed2370ffa95b7e"
+FRAMEWORK_FINGERPRINT = "sha256:29a50f940c07140b"