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"