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/10/25 19:41:12 UTC
(magpie) branch sync-specs-20261025-195043 created (now a25cd7363)
This is an automated email from the ASF dual-hosted git repository.
potiuk pushed a change to branch sync-specs-20261025-195043
in repository https://gitbox.apache.org/repos/asf/magpie.git
at a25cd7363 docs(spec-loop): sync specs with contributed functionality
This branch includes the following new commits:
new a25cd7363 docs(spec-loop): sync specs with contributed functionality
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(magpie) 01/01: docs(spec-loop): sync specs with contributed functionality
Posted by po...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
potiuk pushed a commit to branch sync-specs-20261025-195043
in repository https://gitbox.apache.org/repos/asf/magpie.git
commit a25cd7363199e85865875eaeb0ba2be40b55314a
Author: Jarek Potiuk <po...@apache.org>
AuthorDate: Sun Oct 25 20:02:31 2026 +0100
docs(spec-loop): sync specs with contributed functionality
Brings 20 specs up to main at 853c67818, covering the 87 commits since
03c15a6d1: complete-upgrade and the family welcome guides, the
session-start hook's trusted-interpreter rules, the hook-manager
contract and container-gateway hook trust model, security skills
without adoption, submit-to-oss-scan, email-delivered scans, the ASF
Security dashboard reconcile and ASF-only gating, CVE record-publish
preconditions, the magpie-security-tools substrate plugin, the
vetted-ops personal-layer policy, workflow-optimization checks,
Bitbucket Data Center, and the CI pytest batching.
Generated-by: Claude Opus 5
---
tools/spec-loop/.last-sync | 2 +-
tools/spec-loop/specs/adapters.md | 10 +++
tools/spec-loop/specs/adoption-and-setup.md | 80 +++++++++++++++++-
tools/spec-loop/specs/agent-isolation-sandbox.md | 74 ++++++++++++++--
tools/spec-loop/specs/codex-runtime.md | 5 ++
tools/spec-loop/specs/container-gateway.md | 83 +++++++++++++++++-
tools/spec-loop/specs/cve-tooling.md | 37 +++++++-
tools/spec-loop/specs/issue-management-family.md | 5 ++
tools/spec-loop/specs/marketplace-distribution.md | 31 ++++++-
tools/spec-loop/specs/meta-and-quality-tooling.md | 64 +++++++++++---
tools/spec-loop/specs/organization-adapters.md | 45 ++++++++--
tools/spec-loop/specs/pr-management-family.md | 11 +++
tools/spec-loop/specs/privacy-llm-gate.md | 12 ++-
tools/spec-loop/specs/project-agnosticism.md | 21 +++++
tools/spec-loop/specs/repo-health-family.md | 9 ++
tools/spec-loop/specs/sandbox-diagnostics.md | 35 ++++++--
tools/spec-loop/specs/security-issue-lifecycle.md | 98 +++++++++++++++++++++-
.../spec-loop/specs/security-model-preparation.md | 33 +++++++-
tools/spec-loop/specs/security-reporting.md | 17 ++++
tools/spec-loop/specs/vetted-command-surface.md | 12 +++
20 files changed, 643 insertions(+), 41 deletions(-)
diff --git a/tools/spec-loop/.last-sync b/tools/spec-loop/.last-sync
index b5d2b7317..3102b4c1b 100644
--- a/tools/spec-loop/.last-sync
+++ b/tools/spec-loop/.last-sync
@@ -1 +1 @@
-03c15a6d1c9f51c58e492ecff87036af05388d06
+853c678189fc18522a777bf9e0f3c71dbc0ff58b
diff --git a/tools/spec-loop/specs/adapters.md b/tools/spec-loop/specs/adapters.md
index 8767b35d1..bffec9532 100644
--- a/tools/spec-loop/specs/adapters.md
+++ b/tools/spec-loop/specs/adapters.md
@@ -49,6 +49,14 @@ by swapping the adapter, not the skill.
existing-draft check); an offline alternative to Gmail, selected per
project with a per-person override.
- `tools/ponymail/` — public mailing-list archive search.
+ The login uses a throwaway browser profile, so when a skill asks the user
+ to log in and no `~/.ponymail-mcp/browser-profile/` exists, it adds a
+ one-line tip, once per session, offering the MCP's opt-in
+ `PONYMAIL_BROWSER_PROFILE=persistent` kept profile (#1782).
+ Before enabling it the skill warns that the profile saves only the
+ password, that 2FA is still asked on every login, and that a logged-in
+ session reads the allowed private lists until it expires or the user logs
+ out.
- `tools/mail-archive/` — static mailing-list archive reader for
projects whose list history is in a local mbox/Maildir export.
- `tools/jira/` — issue-tracker adapter for projects on Jira.
@@ -186,6 +194,8 @@ by swapping the adapter, not the skill.
Data Center `pr jira-issues <id>` provides read-only Jira-key discovery for
issues associated with pull-request commits and hands those keys to
`tools/jira`; Bitbucket does not proxy Jira tracker operations.
+ Keys come only from the documented `key` field of each linked issue,
+ upper-cased and de-duplicated (#1727).
It is not a complete `contract:change-request` or `contract:tracker`
backend yet; broader Jira handoff, Data Center
review/merge writes, broader repository permissions, and fuller Pipelines
diff --git a/tools/spec-loop/specs/adoption-and-setup.md b/tools/spec-loop/specs/adoption-and-setup.md
index 1fa20e66d..6c48f1d96 100644
--- a/tools/spec-loop/specs/adoption-and-setup.md
+++ b/tools/spec-loop/specs/adoption-and-setup.md
@@ -272,6 +272,20 @@ committed version with drift detection.
personal layer's `reconciled.json` when the user declines the isolated setup.
On a non-Claude harness every `[bypass]` item becomes a *Run these yourself*
command.
+ The sandbox write scope is gathered, not assumed: a sandboxed probe write in
+ each directory a plan item would write to (the nearest existing parent for
+ one not created yet), and a write outside that scope carries `[bypass]`
+ (#1816, #1817).
+ A user-scope script copy that differs from its framework source is told
+ apart by its `git hash-object` blob: found in the framework's full history
+ it is a stale shipped version (*Update*, or *Fix* for a security fix); not
+ found, it carries local edits and goes under *Still open* with its diff.
+ With no history, or a shallow clone's, the copy becomes an item ending
+ `(replaces N lines only this copy has)` whose bypassed call backs the old
+ copy up first.
+ A script entry that is not a plain-content copy is routed to the
+ `setup-isolated-setup-install` step that installs it (K, P.3, L, M, or the
+ agent-guard wiring of P).
`setup upgrade` ends by pointing at it.
Skill names here are the marketplace form.
- Docs: `docs/setup/` (install recipes, agentic-overrides contract,
@@ -337,6 +351,31 @@ committed version with drift detection.
shows nothing.
Any error is a silent no-op; the `check-upgrade.sh` command hook stays
wired beside it for sessions where mods do not load.
+- **The session-start command hook runs only code the sandbox cannot
+ rewrite** (#1791, #1794).
+ `check-upgrade.sh` prints `Apache Magpie updated (<old> -> <new>)` (or a
+ first-activation line) naming `/magpie-setup:complete-upgrade` on any change
+ of the installed version string, once per version.
+ It runs outside the sandbox, so nothing the session environment sets may
+ choose what runs: `hooks.json` wires it as `/bin/bash -p <path>` (the
+ shebang says the same), it sets a fixed system `PATH`, and it runs
+ `setup_preflight` only under a root-owned Python 3.11+ from
+ `/usr/bin`, `/usr/local/bin` or `/opt/homebrew/bin` whose every path
+ component, symlink hop, standard library and neighbouring shared libraries
+ are root-owned and not group- or world-writable, with no `pyvenv.cfg`.
+ That interpreter runs under `env -i` with `-I -S`, so neither inherited
+ variables, site-packages nor the opened project reach `sys.path`.
+ With no such Python the hook prints its own notice alone, says when that
+ leaves the uv cleanup below undone, and skips the isolated-setup warning,
+ which only the pre-flight can decide.
+ Tests: `tools/setup-preflight/tests/test_session_hook.py`.
+- **Session start cleans up two stale sandbox settings**, adopted or not, and
+ tells the user what it did.
+ It removes a `~/.local/share/uv` write grant from the checkout's
+ `.claude/settings.local.json` and reports one it could not remove or that
+ sits elsewhere (#1788), and drops a repository-level `core.hooksPath`
+ pointing at the repo's own `.git/hooks` when the whole-user git-hook
+ dispatcher is installed, only reporting any other value (#1792).
- **An `svn-zip` lock may carry `sha512:`** (#1596), the released zip's
digest `locks.md` documents; `setup_preflight` accepts it only on
`method: svn-zip`, rejects it on any other method and reports an empty
@@ -436,8 +475,8 @@ committed version with drift detection.
fails to load, still stops.
- **The isolated-setup warning is shown once per session, in adopted repos
only.** `magpie-setup`'s `SessionStart` hook warns while the machine has no
- isolated setup (its stamp, or a sandbox enabled in project or user
- settings): optional for a trial, recommended for longer use, the
+ isolated setup (its stamp, or a sandbox enabled in the project's or the
+ user's `settings.json` or `settings.local.json`, #1763): optional for a trial, recommended for longer use, the
consequences of running without it, and a warning against pointing skills
at untrusted content. A project opts out with `isolation_warning: off` in
the committed `.apache-magpie-overrides/project.md`, which `adopt` asks
@@ -495,6 +534,12 @@ committed version with drift detection.
checker's lock parser accepts that key and returns no project findings
for the method, and `upgrade` on it only refreshes the pre-flight
checker from the in-repo package and stops.
+ Each canonical `.agents/skills/magpie-<n>` link of a self-adopted checkout
+ targets the skill's real directory, `../../plugins/<family>/skills/<alias>`
+ (`<alias>` is the frontmatter `name:`), derived from the `skills/<n>`
+ symlink, never `../../skills/<n>`, a symlink chain the release archive
+ refuses; `symlink-lint` enforces it, and `verify` and `install` describe
+ the same target (#1781).
- **Every skill's pre-flight also compares its own generated
`surface_hash` against the reconciliation stamp**, silently when they
match, at no extra cost inside a sandboxed session: both values are
@@ -553,6 +598,13 @@ committed version with drift detection.
9. A pre-flight on a machine at or ahead of the floor prints nothing; one
below it installs or updates only floor plugins, reports what ran, and
stops for a restart without removing, downgrading or pinning anything.
+ The installed versions come from the running harness's own listing
+ (`claude plugin list --json`, `codex plugin list --json`,
+ `gemini extensions list -o json`), the harness detected from its
+ environment markers innermost-first, and the below-floor finding names
+ that harness (`facts.harness`) and its commands; a harness with no listing
+ command (Copilot CLI) or a listing that cannot be produced leaves the floor
+ unknown (#1673; eval cases `preflight-floor` 16–18).
10. A pre-flight whose lock names a `url` other than `apache/magpie` runs
nothing and asks first.
11. Version comparison is PEP 440: `0.10.0` satisfies a `0.9.0` floor, and
@@ -676,6 +728,12 @@ committed version with drift detection.
run unattended from a skill's pre-flight. `upgrade` skips the refresh
when the directory does not exist rather than creating it, because its
absence is what marks a project as never configured.
+ The refresh copies files from the `setup_preflight/` package beside the
+ skill, never the `tools/setup-preflight/src` link (a plain `cp -R` of it
+ leaves a dangling link), checks the new copy in a temporary directory with
+ `--no-welcome`, and only then swaps it in, restoring the old copy if the
+ swap fails; on a marketplace install the user runs it outside the sandbox,
+ since the plugin cache is read-denied (#1722, #1747).
27. `tools/setup-preflight` also has a **machine** scope for the isolated
(secure agent) setup, reported only where it is used here — an
`isolated_setup` block in the personal layer's `reconciled.json`
@@ -751,8 +809,15 @@ committed version with drift detection.
37. `tools/setup-preflight` has a **family** scope: a `welcome` finding
(rules section `welcome`, guide in `facts.guide`, its name in
`facts.family`). `setup_preflight/guides/general.md` is due on the first
- run of any Magpie skill, and `guides/<family>.md` — today `security`
- and `utilities` (#1669) — on the first run of a skill in that family. At most one
+ run of any Magpie skill, and `guides/<family>.md` on the first run of a
+ skill in that family; every one of the ten skill families ships a guide
+ (`security`, `utilities` #1669, `contributor-growth` #1702,
+ `pr-management` #1701, `pairing` #1706, `mentoring` #1708, `issue` #1709,
+ `repo-health` #1724, `setup` #1725, `release-management` #1726), each
+ linked from the family's README under `docs/`. The general guide's family list
+ and every guide's shared footer (`guides/_footer.md`: the three choices and
+ the `/magpie-setup:welcome-guide <family>` way back) are generated into the
+ guide at `show` time, so a guide never names one that does not ship. At most one
guide per run: a first showing beats a repeat, and between equals the
general guide goes first, so a family guide first appears on the run
after the general one. The skill's family is looked up in `families.py`, generated
@@ -790,6 +855,13 @@ uv run --project tools/skill-and-tool-validator --group dev skill-and-tool-valid
here is the fallback. The skills delivered are the same tree either way —
only the namespace of their invocation differs.
+- **The `setup` family's welcome guide (#1725) is never shown on its own.**
+ The setup family is exempt from the shared pre-flight block
+ (`EXEMPT_FAMILIES` in `tools/dev/check-shared-blocks.py`), and no setup
+ skill runs the checker's welcome path, so `guides/setup.md` is reachable
+ only through `/magpie-setup:welcome-guide setup`; the checker's logic for it
+ is tested (`test_welcome.py`, eval case `preflight-floor` 20) but no skill
+ ever asks for it.
- `stable`; gaps appear as new agent targets to add to the registry
([`agents.md`](../../../plugins/magpie-setup/skills/setup/agents.md)) or new override
surfaces — recorded by the plan pass.
diff --git a/tools/spec-loop/specs/agent-isolation-sandbox.md b/tools/spec-loop/specs/agent-isolation-sandbox.md
index da719476e..71fbadddf 100644
--- a/tools/spec-loop/specs/agent-isolation-sandbox.md
+++ b/tools/spec-loop/specs/agent-isolation-sandbox.md
@@ -125,7 +125,18 @@ existing sandbox grants can widen the baseline. See `docs/adapters/gemini.md`.
(including a full-screen terminal's), and activation is only asked for
with `activate`, falling back to the deprecated
`activateIgnoringOtherApps:`; a refusal costs the keyboard, not the
- window (#1697). The
+ window (#1697).
+ A key that never waits for a touch draws no window at all: before
+ anything signs, `arm` and `wrap` read the signature and authentication
+ slots' touch policies from the `uif:` record of
+ `gpg --card-status --with-colons` (never `ykman`, which needs PC/SC,
+ denied in the sandbox, and contends with scdaemon for the card), and
+ start no watcher when both are `off` (#1715).
+ `cached`, any other value, and an unknown policy (no card, no gpg, no
+ `uif:` record) keep the window.
+ A definite answer is cached in the runtime directory for ten minutes,
+ but an `off` only for one, so a policy turned on since is honoured on
+ the next burst of commands (#1723). The
git the agent runs reads the same global config, so the wrapper's
two files are a `sandbox.filesystem.allowRead` grant of their own
(nothing wider under `~/.claude/`), or every sandboxed signed commit
@@ -178,6 +189,14 @@ existing sandbox grants can widen the baseline. See `docs/adapters/gemini.md`.
option or another `command` flag, is marked unparsed and refused, and
`env -S` is refused outright, because GNU env splits its string with
escapes `shlex` does not reproduce (#1691, #1694).
+ Beyond the bundled `guards.d`, skill-owned guards are discovered in
+ place, with no collection step: every `$MAGPIE_GUARD_DIRS` entry, then
+ every `plugins/*/skills/*/guards` and `skills/*/guards` of the framework
+ tree the engine runs from (#1757), or, on a marketplace install, where
+ each plugin is cached on its own as `<marketplace>/<plugin>/<version>/`
+ and there is no framework tree, the `skills/*/guards` of the family
+ plugins cached beside `magpie-agent-guard`, each at the engine's own
+ version, else its most recently installed one (#1762).
Hooks invoke the engine as a bare `python3`; when that resolves to a
pre-3.11 interpreter, the engine re-runs itself under the newest
`python3.N` (3.11+) on `PATH`, and exits 1 with an actionable message
@@ -205,8 +224,13 @@ existing sandbox grants can widen the baseline. See `docs/adapters/gemini.md`.
`~/.local/share/uv`, `~/.local/bin`; only the cache also writable,
and it removes a `~/.local/share/uv` write an earlier version added,
because uv's interpreters and tool venvs there run outside the
- sandbox), into each worktree's gitignored
- `.claude/settings.local.json`. The harness drops the committed
+ sandbox; sandbox-lint rejects any write grant over that directory,
+ #1785), into each worktree's gitignored
+ `.claude/settings.local.json`, through a temporary copy and a final
+ rename. A rename the sandbox denies — the dispatcher runs the helper
+ sandboxed on every post-checkout — removes the copy, warns and leaves
+ the file untouched rather than aborting the remaining worktrees
+ (#1812). The harness drops the committed
project-scope `sandbox.filesystem` allow entries (issue #197), so the
local file is where they take effect. It never adds the credential
paths the committed list also names; `--no-tool-paths` limits it to
@@ -261,12 +285,43 @@ existing sandbox grants can widen the baseline. See `docs/adapters/gemini.md`.
`tools/agent-isolation/hook-manager-shim.sh` (installed as
`~/.claude/bin/<tool>`, keyed by that name), the dispatcher and
`sandbox-lint`, and checks the committed settings against it.
+ For prek the split is concrete (#1792): the settings `env` sets
+ `PREK_HOME=~/.cache/prek-sandbox` for sandboxed runs,
+ `Edit(~/.cache/prek/**)` keeps sandboxed commands out of the default
+ home a terminal commit runs hooks from, and the dispatcher, when
+ `SANDBOX_RUNTIME` is absent (a terminal commit or a bypassed one),
+ points `PREK_HOME` at `~/.local/share/prek-host` and `XDG_CACHE_HOME`
+ at `~/.local/share/prek-host/cache`, keeping an operator's own value
+ outside `~/.cache`.
+ Detection (a root config file, a tracked nested config, or an
+ installed hook that runs the manager) decides only what a repository
+ is configured with — the sandbox home its settings must carry, the
+ shim, verify check 19d; the default-home deny and the dispatcher's host
+ home stay on for every registered manager, because both homes are per
+ user (#1819).
+- `plugins/magpie-setup/hooks/check-upgrade.sh` — magpie-setup's
+ `SessionStart` hook.
+ It runs outside the sandbox over a sandbox-writable tree, so it trusts
+ nothing the session environment supplies: `/bin/bash -p`, a fixed `PATH`,
+ and Python only from a vetted root-owned interpreter under `env -i` with
+ `-I -S` (#1769, #1791, #1794).
+ It also removes the stale `~/.local/share/uv` write grant (#1788) and a
+ repository-level `core.hooksPath` that would skip the whole-user
+ dispatcher (#1792).
+ The full contract, including the isolation warning, is specified in
+ [`adoption-and-setup.md`](adoption-and-setup.md).
- `tools/egress-gateway/` — local HTTP(S) forward proxy for egress
control. Framework tools point `HTTPS_PROXY`/`HTTP_PROXY` at it; the
gateway rejects any connection to a host not on its allowlist before a
socket is opened. Defence-in-depth per RFC-AI-0003: even a
prompt-injection reaching for an arbitrary endpoint is blocked at the
- network layer. Capability: `substrate:sandbox`.
+ network layer. The `egress-gateway` console script takes `--host`
+ (default `127.0.0.1`), `--port` (default `8899`) and `--log-level`,
+ passes any other argument through to proxy.py unfiltered (a trusted
+ operator's choice, outside the allowlist guarantee), and starts the
+ proxy with those arguments explicitly, as `proxy.Proxy(input_args=…)`
+ plus `sleep_loop()`, rather than letting proxy.py parse the process
+ argv (#1774). Capability: `substrate:sandbox`.
- `.claude/settings.json` — the committed floor: the `sandbox` block
(filesystem allow/deny, the core network `allowedDomains`, the vetted-ops
`excludedCommands`) and `permissions` (every `deny` / `ask`, the
@@ -282,6 +337,12 @@ existing sandbox grants can widen the baseline. See `docs/adapters/gemini.md`.
`~/.config/apache-magpie` in `allowRead`: the Gmail OAuth token there is
read only by the drafting MCP server outside the sandbox, and the
template no longer grants it or the Google API hosts (#1681).
+ Because that server runs outside the sandbox, it accepts a
+ caller-supplied `credentials_path` only as a `.json` file directly in
+ `~/.config/apache-magpie/` or in the directory of
+ `$GMAIL_OAUTH_CREDENTIALS`, reached without a symlink, so a tool call
+ can neither write the refresh token where sandboxed code reads it nor
+ load a planted file (#1795).
- `.gemini/settings.json` and `.gemini/policies/magpie.toml` — Magpie's Gemini profile: tool-sandboxing and an explicitly loaded User-tier approval policy.
Scoped shell reads are allowed; other shell calls, native edits, and MCP calls ask; listed commands and credential paths deny.
Credential-path denies name the canonical `grep_search`; the native probe verifies its `search_file_content` alias, canonical policy names, and search/multi-file/web/resource argument schemas against the loaded runtime.
@@ -490,7 +551,7 @@ framework's 7-day default unless a tool's `[tools.<name>]` table in
`tools/agent-isolation/pinned-versions.toml` sets its own
`cooldown_days`; `bubblewrap` carries `cooldown_days = 1`, so a pin
carrying a sandbox-setup security fix can move a day after release
-(#1360, pinned at 0.13.0).
+(#1360; pinned at 0.13.1 since #1721).
## Out of scope
@@ -529,6 +590,9 @@ python3 -c "import json,sys; s=json.load(open('.claude/settings.json')); \
and 'Bash(gh pr merge *)' in ask and 'Bash(gh *)' not in ask \
and 'Bash(gh api -X*)' in ask and 'Bash(gh api *)' not in ask else 1)"
uv run --project tools/sandbox-lint --group dev pytest
+python3 tools/dev/sync-hook-managers.py --check
+uv run --project tools/setup-preflight --group dev pytest \
+ tools/setup-preflight/tests/test_session_hook.py
```
## Known gaps
diff --git a/tools/spec-loop/specs/codex-runtime.md b/tools/spec-loop/specs/codex-runtime.md
index 6355e2e7a..abad63036 100644
--- a/tools/spec-loop/specs/codex-runtime.md
+++ b/tools/spec-loop/specs/codex-runtime.md
@@ -95,6 +95,11 @@ tool adapters, and uses its own sandbox, rules, and approvals.
native prompt decisions; generated rules never allowlist a general uv
command or a mutable checkout. Code and policy tamper limitations and the
remaining differences from Claude are reported explicitly.
+14. The Codex catalogue (`.agents/plugins/marketplace.json`) marks
+ `magpie-vetted-ops` `INSTALLED_BY_DEFAULT`, beside `magpie-setup`
+ (#1761). Installing it grants nothing: its native rules, like the
+ Claude allow rules and sandbox exclusions, are wired only where the
+ isolated setup is configured.
## Out of scope
diff --git a/tools/spec-loop/specs/container-gateway.md b/tools/spec-loop/specs/container-gateway.md
index 0de7f2d5d..6136c5afa 100644
--- a/tools/spec-loop/specs/container-gateway.md
+++ b/tools/spec-loop/specs/container-gateway.md
@@ -174,6 +174,82 @@ runs before it executes anything (so on-demand start reaches nothing
asks the gateway's `status` whether it already serves, and otherwise
starts it and waits up to five seconds for it to serve, so the command
does not race the start. It never blocks the command.
+"Serve" means `status` reports a serving backend, not merely a live pid:
+the gateway takes its pid lock before it discovers backends and binds
+its sockets, so waiting on the pid let the command race the bind. The
+wait ends early once the gateway has come up and exited again (no
+backend) or never came up (#1826). The `PreToolUse` wiring carries no
+`if` filter, which missed `docker-compose`, `podman-compose` and
+path-qualified CLIs; the script's own check leaves in about 10 ms.
+
+### Hook trust model
+
+The hook runs outside the sandbox, so everything it executes — the
+interpreter, the standard library under it and the gateway source — must
+live where neither the repository being opened nor a sandboxed command,
+which runs without a prompt, can write; otherwise sandboxed code planted
+there would run unsandboxed at the next session start (#1784).
+
+- **Writable roots.** A path is refused when it resolves, through every
+ symlink hop, under the project root, `~/.cache`, `~/.local`,
+ `/tmp/claude*`, `/private/tmp/claude*`, `$CLAUDE_CODE_TMPDIR`, or any
+ `sandbox.filesystem.allowWrite`, `permissions.additionalDirectories` or
+ `Edit` / `Write` allow-rule path in the user, project, local or managed
+ settings (relative entries resolved against their own settings root;
+ an entry with `..` refuses everything). In a linked worktree the main
+ checkout and its settings count too. A write grant over a system or
+ package-manager location (`/usr`, `/opt`, `/Library`, …) refuses the
+ gateway outright. A settings path that is not a regular file refuses
+ the start without being opened, so a planted FIFO cannot stall the
+ hook (#1826).
+- **Interpreter.** Never looked up on `PATH`. Candidates, in order: an
+ operator pin in `~/.claude/scripts/container-gateway/python`, fixed
+ Homebrew and system locations, then uv-managed CPython under
+ `~/.local/share/uv/python` (newest first), the one exemption from the
+ `~/.local` refusal, holding only for a real, unlinked directory and only
+ while no settings scope grants writes over it; the first 3.11+
+ interpreter outside every writable root wins. A virtualenv or launcher
+ stub is never run, `<prefix>/lib/python3.X` must be a real directory,
+ and every symlink in the standard library and the libpython beside it
+ must stay inside the prefix or pass only through system locations at
+ each step. It runs by its canonical path with `-I -S` and a one-line
+ bootstrap that puts the source on `sys.path`, so no `PYTHON*`
+ variable, site-packages or the opened project reaches it (#1769, #1784).
+ `container_gateway stop` recognises that bootstrap command line
+ (`HOOK_BOOTSTRAP`, kept identical to the hook by a test) as well as the
+ `-m` form.
+- **Source.** `$MAGPIE_CONTAINER_GATEWAY_SRC` (absolute, resolved once)
+ or `~/.claude/scripts/container-gateway/src`, judged file by file and
+ refused if it holds a symlink; a copy inside the repository, an
+ `.apache-magpie/` snapshot included, is never used.
+ `$MAGPIE_CONTAINER_GATEWAY_ARGS` is allow-listed token by token;
+ anything else, notably `--extra-bind-root`, makes the hook ignore the
+ whole variable.
+- **Environment.** Helpers (`jq`, `git`, `readlink`, `dirname`, `find`)
+ come from a fixed root-owned `PATH`; every inherited `GIT_*` variable is
+ dropped; the physical cwd and the account database's home (its
+ `.cache`, `.local` and user settings, whatever `$HOME` says) count as
+ writable (#1789). The checkout root is also derived from the nearest
+ directory holding a `.git`, and the hook refuses when git's toplevel
+ differs from it or `core.worktree` is set (#1804). The shebang is
+ `#!/bin/bash -p`, which ignores `BASH_ENV`, exported functions and
+ `SHELLOPTS`, so the hook must be executed directly by its absolute
+ path, never as `bash <path>` or through a `~` the launching shell
+ expands from `$HOME`; it refuses to start when the account home cannot
+ be determined (#1793). The script avoids here-strings, which bash 3.2
+ backs with a temporary file and which failed open when that file could
+ not be created (#1790).
+- **Worktree records.** The first main checkout the hook confirms for a
+ linked worktree (its `worktrees/<name>/gitdir` must point back) is
+ recorded under `~/.claude/magpie-gateway/worktrees`, keyed by the
+ SHA-256 of the path, and a later run deriving a different one refuses
+ until the operator deletes the record, so a session cannot hide the
+ main checkout's grants by rewriting `.git` metadata. The record
+ directory is refused for every checkout when it is sandbox-writable or
+ a symlink (#1790).
+- **Known gap.** Write grants given only on the command line
+ (`--add-dir`, a `--settings` file) or mid-session without saving them
+ (`/add-dir`) are invisible to a hook (#1789).
### Backends
@@ -471,6 +547,9 @@ See the frontmatter `acceptance:` list. Additionally:
```bash
# Unit + relay tests (no backend needed; integration auto-skips)
(cd tools/container-gateway && uv run --group dev pytest)
+# Session hook: trust model, ensure, worktree records
+uv run --project tools/agent-isolation --group dev pytest \
+ tools/agent-isolation/tests/test_container_gateway_hook.py
# Integration against whichever backend is installed
(cd tools/container-gateway && uv run --group dev pytest -m integration)
# Reference settings still lint clean with the gateway entries
@@ -532,8 +611,8 @@ PYTHONPATH=tools/skill-evals/src python3 -m skill_evals.runner \
the field, which is the signal to add it.
- The egress alias for Linux depends on the backend's bridge
configuration and on the egress gateway listening on a non-loopback
- address; until the egress gateway grows a `--bind` option, Linux
- adopters run it with an explicit address.
+ address; it binds loopback by default, so Linux adopters start it
+ with `--host <bridge address>`.
- Docker Desktop's file-sharing settings, not the gateway, decide
which host paths the VM can see; a project root outside the shared
set fails at mount time with Docker's own error.
diff --git a/tools/spec-loop/specs/cve-tooling.md b/tools/spec-loop/specs/cve-tooling.md
index 50642e5ea..2301d0066 100644
--- a/tools/spec-loop/specs/cve-tooling.md
+++ b/tools/spec-loop/specs/cve-tooling.md
@@ -53,6 +53,18 @@ reviewable.
Reader-facing links to the record (the attachment heading and the
sync comment templates' `RECORD_URL`) go to the record page, not its
`#source` tab, which only the manual-paste fallback still links.
+ The config is looked up as `--config`, then `$CVE_JSON_CONFIG`, then the
+ committed `.apache-magpie-overrides/tools/cve-tool-vulnogram/cve-json-config.toml`
+ (and its pre-rename `tools/vulnogram/` path), then the same
+ `tools/cve-tool-vulnogram/cve-json-config.toml` in each personal layer,
+ so a project that has not adopted Magpie can still generate records
+ (#1787).
+ `[cna].org_id` (the record's `assignerOrgId`) is required and has no
+ default, so a project outside the ASF never gets the ASF CNA's UUID by
+ omission; a missing value exits with a pointer to
+ `cve_authority.vulnogram.asf_org_id` in
+ `organizations/ASF/organization.md`, where the one ASF-wide UUID now
+ lives instead of a per-project row (#1779).
- `tools/cve-tool-vulnogram/oauth-api/` — a `uv` project exposing six
console scripts that talk to the Vulnogram HTTP API with a Bearer token
the operator copies from `https://cveprocess.apache.org/users/token`
@@ -62,7 +74,8 @@ reviewable.
under `~/.config/apache-magpie/vulnogram-session.json`),
`vulnogram-api-record-update` (POST to the `/cve5/<CVE-ID>` upsert
endpoint), `vulnogram-api-record-publish` (move a record
- `REVIEW` → `PUBLIC`), `vulnogram-api-record-fetch` (read-only: full
+ `REVIEW` → `PUBLIC`; `--allow-state` accepts only `REVIEW` / `READY`),
+ `vulnogram-api-record-fetch` (read-only: full
record, `--state-only`, or `--comments-only` for reviewer comments),
`vulnogram-api-allocate` (reserve a CVE ID through `/allocatecve`
without the form; prints the bare ID), and
@@ -87,7 +100,12 @@ reviewable.
or it has expired. `vulnogram-api-setup --pmc` first checks the server
(`--server`) and falls back to the paste path on a server without the
browser flow; an `allocate` token is stored in its own file
- (`vulnogram-allocate-session.json`). `security-cve-allocate` allocates
+ (`vulnogram-allocate-session.json`).
+ Without a terminal (an agent shell escape, a closed stdin) the paste
+ prompt and the ASF-account prompt catch `EOFError` and exit `1` with
+ instructions instead of a traceback; expired-token hints name
+ `--pmc <project>` first, and after a `--pmc` fallback to the paste
+ prompt they point at `--token` rather than back at `--pmc` (#1742). `security-cve-allocate` allocates
through `vulnogram-api-allocate` after one confirmation when both
`--server` and `--allocate` pass, and prints the form recipe otherwise.
- `tools/cve-org/` — CVE.org / CVE-services helpers; the `check-published`
@@ -117,6 +135,8 @@ reviewable.
out of `references[]` before serialising — they are OAuth-gated / private.
- **Allocation is PMC-gated.** The skill submits nothing itself when the
user is not on the PMC; it reshapes the recipe into a relay message.
+ Allocation never reads the CNA org id: the allocate form takes only the
+ PMC and the title, which a test pins (#1779).
- **API allocation runs once, after one confirmation.** When the Step 0
pre-flight finds both `vulnogram-api-check --server` and `--allocate`
passing, Step 3 shows a single proposal (the title, the PMC, and the form
@@ -181,6 +201,14 @@ reviewable.
Each guard has an explicit override (`--allow-state-downgrade`,
`--allow-product-change`, `--replace-references`, or the umbrella
`--full-replace`).
+- **Only an announced record is published (#1795).**
+ `vulnogram-api-record-publish` publishes from `REVIEW` by default and
+ from `READY` only when `--allow-state READY` is passed; no earlier state
+ is accepted.
+ It refuses with exit `4` a record whose `references[]` has no entry
+ tagged `vendor-advisory` — the archived advisory URL `generate-cve-json`
+ tags once the tracker's *Public advisory URL* is set — because clearing
+ an unannounced record for `cve.org` would disclose early.
## Out of scope
@@ -203,6 +231,11 @@ reviewable.
7. The record stays `DRAFT` while a required field other than the credit
is empty, and `--attach` never writes a body that changes a field
outside the attachment (#1672).
+8. `vulnogram-api-record-publish` refuses a record in a state other than
+ `REVIEW` / `READY`, or one with no `vendor-advisory` reference (#1795).
+9. `generate-cve-json` with no `[cna].org_id` exits with a pointer to the
+ organization manifest, never a `KeyError` and never a default UUID
+ (#1779).
## Validation
diff --git a/tools/spec-loop/specs/issue-management-family.md b/tools/spec-loop/specs/issue-management-family.md
index 7776b0ab0..137ed5e4f 100644
--- a/tools/spec-loop/specs/issue-management-family.md
+++ b/tools/spec-loop/specs/issue-management-family.md
@@ -117,6 +117,11 @@ unchanged.
component pressure, and stale-fraction without modifying any tracker
state. Ships `mode: Triage` + `capability: capability:stats`
+ `experimental`.
+ Each open issue gets exactly one triage class (`UNTRIAGED`, `TRIAGED`,
+ `IN-PROGRESS`, `SKIP-SECURITY`); staleness is not a class but the
+ separate `is_stale_candidate` boolean (`days_since_updated >= warn_days`),
+ so an issue keeps its real class and the `stale_candidate` bucket, the
+ pressure score and the untriaged funnel all read the boolean (#1753).
- Skill: `issue-deduplicate` — identifies two open issues with the same root
cause, drafts a merge rationale and cross-link comment, and proposes
diff --git a/tools/spec-loop/specs/marketplace-distribution.md b/tools/spec-loop/specs/marketplace-distribution.md
index 7d7aaeff7..4c25fb2e9 100644
--- a/tools/spec-loop/specs/marketplace-distribution.md
+++ b/tools/spec-loop/specs/marketplace-distribution.md
@@ -100,6 +100,14 @@ its manifest (#1552): the `SessionStart` `check-upgrade.sh` command hook and a
`modules` list naming `drift-check.ts`, a Claude Code mod that checks the
committed lock against the installed plugin or snapshot at session start, with
no model tokens, and shows a drift banner above the prompt.
+The command hook is wired as `/bin/bash -p "${CLAUDE_PLUGIN_ROOT}/hooks/check-upgrade.sh"`,
+because it runs outside the sandbox and the session environment must not
+choose the shell (#1794); on a version change it points at
+`/magpie-setup:complete-upgrade` (#1813), and the interpreter rules are in
+[`adoption-and-setup.md`](adoption-and-setup.md).
+`tools/setup-preflight/tests/test_session_hook.py` asserts that wiring;
+`check-family-plugins.py` checks only that `hooks.json` references
+`check-upgrade.sh`.
Its manifest declares `"types": "./hooks/types.d.ts"`, and
`check-family-plugins.py` fails when `hooks.json` lacks `modules` or the
`check-upgrade.sh` hook, or when the manifest also declares inline `hooks`,
@@ -161,12 +169,20 @@ adopter-facing page.
plugin with zero skills and no error. Every catalogue lists all ten families.
- **Substrate plugins publish a tool, not a family.** Beside the ten
- families, the Claude Code catalogue carries three substrate plugins —
- `magpie-agent-guard`, `magpie-vetted-ops` and `magpie-adversarial-review`
- (#1368) — declared in `SUBSTRATE_PLUGINS` in `check-family-plugins.py`.
+ families, the Claude Code catalogue carries four substrate plugins —
+ `magpie-agent-guard`, `magpie-vetted-ops`, `magpie-adversarial-review`
+ (#1368) and `magpie-security-tools` (#1787) — declared in `SUBSTRATE_PLUGINS` in `check-family-plugins.py`.
Each inherits the shared manifest metadata, declares no `skills`, exposes
its `tools/<name>` through a symlink whose entry point must resolve, and
declares hook wiring only where it has a hook.
+ `magpie-security-tools` carries the tools the security skills run
+ (tracker field and rollup edits, CVE JSON, the privacy gate, the scan
+ importer), so a marketplace install needs no framework checkout.
+ Its `SessionStart` hook (`tools/security-management/hooks/link-framework-path.sh`)
+ copies the plugin's `tools/` under `~/.cache/apache-magpie/framework/`
+ once per plugin version and points the `current` symlink at it; that is
+ `<framework>` on a marketplace install, writable so `uv run --project`
+ works, and the hook only ever replaces a symlink at `current`.
`magpie-agent-guard` is the exception to the symlink (#1516): it carries
a byte-identical copy of `tools/agent-guard/src/agent_guard` as real
files, because a marketplace client may drop a link whose target leaves
@@ -275,3 +291,12 @@ For the Claude Code family, `claude plugin validate . --strict`.
a bump that is forgotten fails silently in the one way that matters: adopters
keep being told they are up to date. The staleness is only visible by reading
the timestamp.
+- **`check-family-plugins.py`'s `SETUP_HOOKS` fallback lags the wiring.**
+ When `plugins/magpie-setup/hooks/hooks.json` is missing, `--fix` writes
+ inline manifest hooks with the bare `${CLAUDE_PLUGIN_ROOT}/hooks/check-upgrade.sh`
+ command, not the `/bin/bash -p` form `hooks.json` uses since #1794.
+- **The root `.codex-plugin/plugin.json` wires a hook that does not ship.**
+ Its inline `SessionStart` hook runs `${PLUGIN_ROOT}/hooks/check-upgrade.sh`,
+ but the repository root has had no `hooks/` directory since #1208; the
+ script lives only in `plugins/magpie-setup/hooks/`, and the root wiring also
+ lacks the `/bin/bash -p` form.
diff --git a/tools/spec-loop/specs/meta-and-quality-tooling.md b/tools/spec-loop/specs/meta-and-quality-tooling.md
index 5c569d8b6..c0b505c36 100644
--- a/tools/spec-loop/specs/meta-and-quality-tooling.md
+++ b/tools/spec-loop/specs/meta-and-quality-tooling.md
@@ -12,7 +12,8 @@ source: >
tools/sandbox-lint/, tools/dashboard-generator/, tools/probe-templates/,
tools/pilot-report-validator/, tools/vendor-neutrality-score/,
tools/preflight-audit/, tools/spec-inventory/,
- tools/dev/check-claude-mods.sh, tools/dev/bump-hook-npm-pins.py, and
+ tools/dev/check-claude-mods.sh, tools/dev/bump-hook-npm-pins.py,
+ tools/dev/ci-pytest-batches.py, and
the write-skill / list-skills utility skills.
acceptance:
- Skill definitions are validated (frontmatter keys name/description/
@@ -72,6 +73,14 @@ trustworthy as it grows.
explains the decision without restating it, because the decision itself
is compared exactly in its own structured fields; it answers no only on
a different or contradictory conclusion (#1398).
+ A compatible recommended next action the reference omits is not a
+ mismatch, and the grader must make each boolean verdict agree with its
+ stated reason before returning (#1751).
+ CI never runs a live model, so live runs are recorded by hand in a
+ *Run history* table (`| Date | Ref | Model | Runs | Result | Notes |`) in
+ `evals/<skill>/README.md`: a `main` SHA as the ref, the model name as the
+ CLI uses it, one appended row per batch of identical runs, and failures
+ recorded even when a rerun passed (#1626).
A step with no `user-prompt-template.md` gets a neutral user prompt: the
case report followed by "Return JSON only."; a step that needs the
`{corpus}` / `{roster}` / `{report}` framing carries its own template, as
@@ -239,8 +248,38 @@ trustworthy as it grows.
npm pins in hooks' `additional_dependencies`, which Dependabot never
touches, in one draft PR, with a 7-day cooldown and 12 hours for
Claude Code. A green PR check is therefore not a
- whole-repo result, which is why `prek run --all-files` before pushing
- is a required pre-flight (#1317).
+ whole-repo result, which is why a whole-tree prek run before pushing
+ is a required pre-flight (#1317; the tests in it are scoped since #1764,
+ below).
+- **Workspace pytest runs only the affected members, batched.**
+ `tools/dev/ci-pytest-batches.py` picks the members that get pytest (a
+ `[tool.pytest.ini_options]` section and no `[tool.magpie.checks]` opt-out,
+ the rule `run-workspace-check.sh` uses): on a pull request only members with
+ a changed file other than Markdown or a `.claude-plugin/` manifest
+ (directly or through a symlinked source) plus every member depending on
+ one, unless a shared build input (root `pyproject.toml`, `uv.lock`, the two
+ selection scripts, `tests.yml`) changed; on any other event, every member
+ (#1717).
+ Up to six picked members get one job each, named after the member; more
+ are split into at most eight jobs balanced by test size, and each job
+ syncs only its members' packages.
+ `--changed-file` reads the changed paths from a file or `-` for stdin, with
+ path separators normalised (#1767); `--paths` feeds the local
+ `workspace-pytest` hook, which runs the same members a pull request would,
+ in parallel (`pytest -n auto`), except that its file filter leaves out the
+ root `pyproject.toml` and `uv.lock`, so a dev-version bump runs nothing
+ locally (#1764).
+ The pre-PR gate is `prek run --all-files --skip workspace-pytest` then
+ `prek run workspace-pytest --from-ref origin/main --to-ref HEAD`
+ (`AGENTS.md` § Local setup).
+ The required `tests-ok` job runs `if: always()` and fails unless member
+ discovery succeeded and the matrix either succeeded or was skipped on an
+ explicitly empty `[]` selection (#1807).
+ CodeQL, not a required check, picks its analyses per pull request: `python`
+ only when a `.py` file changed, `actions` only when something under
+ `.github/` did, both when the workflow itself changed or on a push to
+ `main` or the weekly schedule (#1718); the Python analysis runs on
+ `ubuntu-latest` with a 30-minute ceiling (#1719).
- **Every required check runs on stacked PRs.** The `zizmor` workflow's
`pull_request` trigger carries no `branches:` filter, because that filter
matches the PR's base branch and a stacked PR based on another PR's branch
@@ -349,6 +388,10 @@ trustworthy as it grows.
14. `vendor-neutrality-score` counts each `+`-joined `**Vendor:**` entry
as a separate backend vendor and rejects `agnostic` mixed with named
vendors or an empty entry (#1575).
+15. A pull request's `tests` run picks only the workspace members its
+ changes affect, and the required `tests-ok` gate fails when member
+ discovery fails or the matrix is skipped without an empty selection
+ (#1717, #1807).
## Validation
@@ -357,11 +400,12 @@ uv run --project tools/skill-and-tool-validator --group dev pytest
uv run --project tools/skill-and-tool-validator --group dev skill-and-tool-validate
uv run --project tools/skill-evals --group dev pytest tools/skill-evals/tests
uv run --project tools/spec-inventory --group dev pytest tools/spec-inventory/tests
+uv run --project tools/dev pytest tools/dev/tests/test_ci_pytest_batches.py tools/dev/tests/test_ci_tests_gate.py
```
## Known gaps
-- **Eval coverage is complete.** All 78 shipped skills have a matching
+- **Eval coverage is complete.** All 83 shipped skills have a matching
suite in `tools/skill-evals/evals/`; the soft eval-coverage check in
`skill-and-tool-validator` (check #8) warns when a newly added skill has
no suite, keeping coverage complete going forward.
@@ -394,9 +438,9 @@ uv run --project tools/spec-inventory --group dev pytest tools/spec-inventory/te
- **Vendor-neutrality measurement is available but unscheduled.** `tools/vendor-neutrality-score/`
ships and can be run ad hoc, but is not yet wired into CI or the
build loop. Score drift is not automatically surfaced.
-- **The 500-line cap is advisory and widely exceeded.** `skill-line-limit`
- is a SOFT check. After the family trims (#1390 pr-management, #1400
- issue, #1435 to #1438 security), 23 `SKILL.md` files are still over 500
- lines, led by `release-prepare` (1,208), `release-verify-rc` (968) and
- `release-rc-cut` (932); 8 of the 10 release-management skills and 10
- security skills are among them.
+- **The 500-line cap is advisory and still exceeded.** `skill-line-limit`
+ is a SOFT check. After the family trims and deep splits (#1390
+ pr-management, #1400 issue, #1435 to #1438 and #1672 security, #1620
+ release-management), two `SKILL.md` files remain over 500 lines:
+ `setup-isolated-setup-install` (615) and `security-issue-import-from-pr`
+ (511).
diff --git a/tools/spec-loop/specs/organization-adapters.md b/tools/spec-loop/specs/organization-adapters.md
index 9e42c4ef1..e0aa71c4c 100644
--- a/tools/spec-loop/specs/organization-adapters.md
+++ b/tools/spec-loop/specs/organization-adapters.md
@@ -20,7 +20,8 @@ acceptance:
- A project selects its organization with a single key
(organization:<org> in project.md) and inherits the rest.
- Config resolves project.md -> organizations/<org>/ -> framework
- default, first hit wins; skills never branch on the organization.
+ default, first hit wins; skills never branch on the organization,
+ except to gate a surface marked as one organization's own.
- A new organization can be authored from
organizations/_template/ without editing any skill body.
---
@@ -71,9 +72,41 @@ identical "ASF default" values in its own `project.md`.
- Every placeholder and dotted config key resolves
`project.md → organizations/<org>/organization.md → framework default`,
first hit wins. This is the only inheritance in the config model.
-- Skills do **not** read the `organization:` key to branch behaviour;
- they read capability keys (`cve_authority.tool`, `archive_system.kind`,
- `<governance-body>`, …) and take the first value the chain yields.
+- Skills do **not** read the `organization:` key to pick a backend or a
+ value; they read capability keys (`cve_authority.tool`,
+ `archive_system.kind`, `<governance-body>`, …) and take the first value
+ the chain yields.
+- **The one exception is a surface that exists only for one
+ organization's services (#1796, #1798).** Such a surface is marked
+ (`**Organization:** ASF` on the tool, or a *Scope — `organization: ASF`*
+ note on the doc) and gated on an explicit `organization: ASF` in the
+ resolved `project.md`, never inferred from a capability value:
+ `security-issue-sync`'s dashboard mode (Step D0) and the
+ `magpie-asf-security-dash` CLI (exit `3` otherwise, before the cookie or
+ network is touched; HTML comments and fenced examples are ignored when
+ reading the key), `permission-audit --organization` (PonyMail,
+ apache-projects and Vulnogram recommendations only for `ASF`, as passed
+ by setup verify), `dependency-license-audit`'s `asf` default policy
+ (`allowlist` for every other project), `workflow-optimization`'s
+ GitHub Actions Policy / `.asf.yaml` / `creation_cap` checks (or, with no
+ project config, an `apache` repository owner), and the `asf-security`
+ forwarder, `tools/gmail/asf-relay.md` and `ponymail-archive.md`.
+ An unset or `TODO` organization is not `ASF`.
+- **Setup settles the organization before sourcing its values (#1779).**
+ It proposes `ASF` only on evidence — a GitHub remote under `apache/`, a
+ root `.asf.yaml`, or a Subversion URL under `https://svn.apache.org/`
+ from `magpie-vcs probe` — and otherwise asks, never defaulting to `ASF`;
+ a non-ASF project inherits no `apache.org` address, no `cna_private_*`
+ field and never the ASF CNA's UUID.
+- **Org-wide security values live in the organization (#1745, #1779).**
+ The ASF manifest carries the ASF CNA's single UUID
+ (`cve_authority.vulnogram.asf_org_id`, the `assignerOrgId` of every
+ Apache record, used only by the generated CVE JSON and never for
+ allocation), which replaced the per-project `asf_org_id` row of the
+ project template, and `security_inbox.cve_tool_senders:
+ [security@apache.org]`, the organization-wide CVE bookkeeping sender.
+ A project list replaces, never merges with, an organization list, so a
+ project override repeats the inherited entries.
- An organization declares only org-wide values. Per-project
values (security-list address, scope labels, product name, roster,
tracker label/body-field vocabulary) stay in `project.md`.
@@ -103,7 +136,9 @@ identical "ASF default" values in its own `project.md`.
`organizations/<org>/organization.md`, not duplicated per project.
- `organization: <org>` in `project.md` selects the adapter.
- Resolution is `project.md → organization → framework default`,
- first hit wins; no skill branches on the organization.
+ first hit wins; no skill branches on the organization except to gate a
+ surface marked as that organization's own, and such a gate stops for an
+ unset or `TODO` organization.
- A new organization can be authored from `organizations/_template/` with
no skill edits.
- Smoke fixtures cover security intake, release backend, and contributor
diff --git a/tools/spec-loop/specs/pr-management-family.md b/tools/spec-loop/specs/pr-management-family.md
index f9bbce2e6..eec8fe907 100644
--- a/tools/spec-loop/specs/pr-management-family.md
+++ b/tools/spec-loop/specs/pr-management-family.md
@@ -327,6 +327,17 @@ same directory; the behaviour is unchanged.
--author`, CODEOWNERS team ownership checks team membership rather than
organisation membership, and `maintain` permission gets the maintainer
footer (#1585).
+- **Third-party licence scan reads only real copyright notices.**
+ `scans.third_party_licences` treats an added file as third-party code when
+ the first 25 lines carry a licence identifier or block and either a
+ copyright line naming someone other than the ASF or an "adapted / derived /
+ ported / copied / vendored from" note; it then reports Category X and B
+ licences as `blocking` and a Category A licence as `major` when
+ `LICENSE` / `LICENSE.txt` was not updated.
+ A copyright notice has to open its line (after comment markers, optionally
+ "Portions"), so the ASF header's mid-line "regarding copyright ownership"
+ never marks a file carrying `SPDX-License-Identifier: Apache-2.0` and the
+ full ASF header as third-party (#1777).
- **Evidence-gated dependency-version findings.**
`pr-management-code-review` inventories every mandatory direct and
transitive constraint path, applies environment markers, and intersects
diff --git a/tools/spec-loop/specs/privacy-llm-gate.md b/tools/spec-loop/specs/privacy-llm-gate.md
index 5bffe81e0..3ba86be1c 100644
--- a/tools/spec-loop/specs/privacy-llm-gate.md
+++ b/tools/spec-loop/specs/privacy-llm-gate.md
@@ -46,7 +46,9 @@ artefact for leakage before emission.
requires an opt-in entry in `privacy-llm.md` whose host matches exactly
(or, for a name-only entry, the provider's default endpoint plus a
whole-word name match) with a data-residency contract and a
- non-placeholder `Approved-by`.
+ non-placeholder `Approved-by`; a placeholder is refused with a message
+ that a member of the project's governing body must sign off, not
+ specifically a PMC member (#1796).
The free-text Claude Code rule of the stack check does not apply to it.
- `tools/privacy-llm/models.md` — the approved-model registry and its
carve-outs.
@@ -61,13 +63,19 @@ artefact for leakage before emission.
`projects/_template/` in #1410). Without `--config` or
`$PRIVACY_LLM_CONFIG`, the checker looks it up per config layer, first
match wins: the personal layer (`<cwd>/.apache-magpie-local/` in an
- adopted repository, `<git-common-dir>/apache-magpie/` otherwise), then a
+ adopted repository, then the main checkout's in a linked worktree;
+ otherwise `<git-common-dir>/apache-magpie/`, `<.svn>/apache-magpie/` in a
+ Subversion working copy, or the in-tree `.apache-magpie-local/` with no
+ version control, #1779), then a
legacy in-tree `.apache-magpie-local/` of an unadopted repository, then
the committed `.apache-magpie-overrides/`. The `.apache-magpie/`
framework snapshot is never looked in: it is replaced by every upgrade
and holds no adopter config, and the earlier snapshot-then-overrides
order never read the personal file `setup-privacy-llm` writes. A file
found nowhere stops the gate.
+ On a marketplace install the checker runs from the
+ `magpie-security-tools` plugin's materialised copy at
+ `<framework>/tools/privacy-llm/checker` (#1787).
- Skill: `setup-privacy-llm` (`plugins/magpie-setup/skills/privacy-llm/`)
detects the LLM stack in use, writes `<project-config>/privacy-llm.md`,
and runs the gate and the redactor end to end so the approval is
diff --git a/tools/spec-loop/specs/project-agnosticism.md b/tools/spec-loop/specs/project-agnosticism.md
index 50d5ad266..5f03b48d0 100644
--- a/tools/spec-loop/specs/project-agnosticism.md
+++ b/tools/spec-loop/specs/project-agnosticism.md
@@ -47,6 +47,10 @@ The three mechanisms, in order of preference:
only installed Magpie (#1533, #1535; see
[adoption-and-setup.md](adoption-and-setup.md)), so a non-adopting
project carries no Magpie files in its working tree.
+ A Subversion working copy keeps that layer in `<.svn>/apache-magpie/`,
+ inside its admin directory and never committed, and a project with no
+ version control in the in-tree `.apache-magpie-local/`, so a Subversion
+ project can configure Magpie without adopting it (#1779).
2. **Adapters** for swapping the backing *system* a step talks to
(`tools/gmail`, `tools/ponymail`, `tools/jira`, `tools/github`,
`tools/mail-source`). See [adapters.md](adapters.md).
@@ -147,6 +151,20 @@ The three mechanisms, in order of preference:
carries no project slug: new rollups get the framework-wide
`<!-- apache-magpie: status-rollup v1 -->` marker, while the legacy
tracker-named one is still recognised (#1444, #1665).
+- **An ASF-only surface is marked and gated, never assumed (#1796,
+ #1798).** Where a step or tool exists only for an ASF service (the
+ `dash.security.apache.org` dashboard, PonyMail, `lists.apache.org`
+ archive URLs, the ASF security relay, ASF Infra workflow policy, the ASF
+ licence-category model, the ASF-only permission recommendations), it
+ carries an `organization: ASF` scope marker and runs only when
+ `project.md` declares `organization: ASF`; every other project gets the
+ documented non-ASF default (for licences, the `allowlist` policy) and is
+ told which default applied
+ ([organization-adapters.md](organization-adapters.md)).
+ Setup never defaults a project to `ASF` without evidence, and a non-ASF
+ project never inherits the ASF CNA's UUID (#1779).
+ Generic user-facing text says "governing body" rather than "PMC", as the
+ privacy-LLM checker's sign-off messages now do.
- **Advisory, not paternalistic.** The audit surfaces candidate coupling
for a maintainer to judge; some ASF strings are legitimate (examples,
the ASF default profile, ASF-specific docs). It does not auto-rewrite.
@@ -180,6 +198,9 @@ The three mechanisms, in order of preference:
4. The template profile and non-ASF example expose the same required
config surfaces, except where the example documents an intentional
omission or an organization-inherited default.
+5. Every ASF-only surface stops, or falls back to its non-ASF default, for
+ a project whose `project.md` does not declare `organization: ASF`
+ (#1796, #1798).
## Validation
diff --git a/tools/spec-loop/specs/repo-health-family.md b/tools/spec-loop/specs/repo-health-family.md
index ec466ec17..86fe9e4c9 100644
--- a/tools/spec-loop/specs/repo-health-family.md
+++ b/tools/spec-loop/specs/repo-health-family.md
@@ -81,6 +81,15 @@ grouped, prioritised report → wait for confirmation before any write.
prioritised plan to cut runner use: duplicate triggers, job consolidation,
scheduled canaries, timeouts, `ubuntu-slim`, Dependabot noise). Read-only;
ships `mode: Triage` + `experimental` with an eval suite.
+ The checks live in `workflow-optimization/checks.md`, tiered `P0` (broken
+ or about to break) to `P3` (needs community agreement); they include a
+ required check that a failed or cancelled dependency leaves skipped
+ (`P0-skipped-gate`, remedied by one `if: always()` umbrella job as the only
+ required context), cache entries that never restore (`P2-cache-miss`), and
+ per-run `apt-get` installs (`P2-apt-install`) (#1783).
+ `scripts/summarize_runs.py` summarises the run and job TSVs fetched in
+ Step 1. The GitHub Actions Policy, `.asf.yaml` and `creation_cap` checks
+ run only when `project.md` declares `organization: ASF`.
- Design docs: `docs/repo-health/README.md` — family overview and the
adopter-config scaffold.
- Adopter config: `plugins/magpie-setup/templates/repo-health-config.md`
diff --git a/tools/spec-loop/specs/sandbox-diagnostics.md b/tools/spec-loop/specs/sandbox-diagnostics.md
index d08052fdc..c830a803f 100644
--- a/tools/spec-loop/specs/sandbox-diagnostics.md
+++ b/tools/spec-loop/specs/sandbox-diagnostics.md
@@ -47,7 +47,7 @@ error, and two skills that probe or verify the setup on demand.
**Root cause** (which sandbox layer blocks it and why), **Fix** (a
settings widening with per-entry rationale, or — for the `gh` entry —
an invocation-shape rule, because there is nothing to widen), and
- **Notes**. Eleven entries today: SSH agent / Yubikey, signed commit
+ **Notes**. Twelve entries today: SSH agent / Yubikey, signed commit
failing before any touch (`gpg.format=ssh` key unreadable), signed
commit failing with `cannot exec` of the touch-overlay wrapper
(`gpg.ssh.program` under the read-denied `~/.claude/scripts/`),
@@ -58,7 +58,11 @@ error, and two skills that probe or verify the setup on demand.
socket, `/tmp` read-only, `gh` inside the sandbox (TLS
`OSStatus -26276` / `HTTP 401`), `prek` or `uv` not found or unable
to write its cache (the home-directory dev-tool paths not granted in
- the worktree's `settings.local.json`, #1359), git hooks silently
+ the worktree's `settings.local.json`, #1359), `uv python install` or
+ `uv tool install` failing under `~/.local/share/uv/` (deliberately
+ read-only to the sandbox, since hooks and excluded commands run the
+ interpreters there outside it; the fix is to run the install from a
+ terminal, #1785), git hooks silently
skipped for sandboxed commits (whole-user `core.hooksPath` under the
read-denied home, #1364), and reads of `/tmp/claude-<uid>` asking for
approval every time (`permissions.blockReadsOutsideWorkingDirectories`
@@ -78,7 +82,18 @@ error, and two skills that probe or verify the setup on demand.
`PROBE: <slug> → ✓|✗|⊘|⚠ (<evidence>)` line the skill's
interpretation tables match on; the commands stay out of the
skill body, so they cost no context and can be run and tested on
- their own (#1336). Probe 7 checks that `prek` and `uv` run inside
+ their own (#1336). A probe holds a command's output in a variable,
+ never a file: `/tmp` is read-only inside the sandbox, and a failed
+ redirection skips the command and exits 1, which probe 1 once read as
+ "agent reachable, no identities" for every agent (#1770). Probes 1, 3
+ and 5 have tests under the skill's `tests/` that run each script
+ against fake binaries (#1770, #1776). Probe 3 trusts a gateway socket
+ only on the gateway's own read-only `status` report, taken from the
+ pinned snapshot, the framework tree, or else the operator install at
+ `~/.claude/scripts/container-gateway/src` (a marketplace install); with
+ no report it fails as "gateway status unavailable" rather than "not
+ running", and a `status` that exits 2 without a report as "refused the
+ run directory" (#1815). Probe 7 checks that `prek` and `uv` run inside
the sandbox and can write `~/.cache`, telling a missing grant (⚠)
from a tool that is not installed (⊘) by reading the worktree's
`settings.local.json`, since a read-denied `~/.local/bin` looks the
@@ -105,7 +120,11 @@ error, and two skills that probe or verify the setup on demand.
whole-user scope the shared hook directory probed from inside the
sandbox, whose unreadability is ✗), the vetted-ops split, the touch
overlay and signing key (check 10), the `gh` exclusion (check 11),
- the container gateway (check 12), the eval-harness exclusion
+ the container gateway (check 12; 12a accepts the hooks in
+ `~/.claude/settings.json` or `~/.claude/settings.local.json`, fails a
+ registered absolute path that does not exist on this machine, and marks
+ one in a `settings.json` linked into a synced config repo as ◐, #1797),
+ the eval-harness exclusion
(check 13, if installed), the adversarial-review exclusion (check 14,
if the `magpie-adversarial-review` plugin is installed: the
`excludedCommands` entry on the fixed `~/.claude/magpie/adversarial-review`
@@ -263,11 +282,17 @@ PYTHONPATH=tools/skill-evals/src python3 -m skill_evals.runner \
## Known gaps
-- The doctor runs 9 probes over the catalog's 11 entries: the two
+- The doctor runs 9 probes over the catalog's 12 entries: the two
touch-overlay entries (`cannot exec` of the wrapper, and the
runtime-state directory) have no live probe. Verify check 10 covers
the wrapper's `allowRead` grant statically; the runtime-state entry
is reached only through the hint hook.
+- The `uv python install` / `uv tool install` entry (#1785) has neither a
+ hint-hook branch nor a doctor probe, and has no **Notes** section at all
+ (acceptance 1's four sections), so it breaks the "catalog entry first,
+ hook branch beside it" rule; the session-start hook's grant cleanup and `isolated-setup-update`'s
+ must-fix report are its only other surfaces.
+- Probes 2, 4 and 6–9 still have no script tests.
- The doctor's and verify skill's section headings (`## The 8 probes`,
`## The 12 checks`) lag their bodies (nine probes, seventeen checks).
- A command that spells a working directory with a literal `~` still
diff --git a/tools/spec-loop/specs/security-issue-lifecycle.md b/tools/spec-loop/specs/security-issue-lifecycle.md
index c2f817971..bae63b1f7 100644
--- a/tools/spec-loop/specs/security-issue-lifecycle.md
+++ b/tools/spec-loop/specs/security-issue-lifecycle.md
@@ -17,6 +17,8 @@ acceptance:
- Tracker contents stay private; only stable identifiers (URLs, #NNN)
are public-safe. Public PRs are scrubbed pre-disclosure.
- Every applied state change is audit-logged.
+ - Every security skill runs for a maintainer who only installed Magpie,
+ with no adopted project and no framework checkout.
---
# Security-issue lifecycle
@@ -38,6 +40,17 @@ publication, with a human gate and an audit-log entry at every step.
multi-finding scan output through a pluggable scan-format adapter
(`tools/scan-format/`), buckets each finding for operator review, and
creates trackers only after per-finding confirmation.
+ A scanner that mails the whole scan (the `anthropic-oss-scanner`
+ format) is first materialised into a scan folder (#1786): the read-only
+ `oauth-draft-fetch-message` saves the multi-MB message to a file, and
+ `security-management issue-import-from-scan parse-email` splits it into
+ `index.json` plus `evidence/<id>.md`, printing counts only, and refuses
+ triage unless `checks.complete` holds (preamble totals, severity
+ banners, `INDEX`, parsed blocks and signature agree).
+ Attachments are saved byte-exact with their OpenPGP framing checked,
+ never decrypted or run.
+ A report built from such a private source stays a local file: no gist
+ and no per-source post before disclosure.
`security-issue-import-via-forwarder` is the relay sub-skill: handles
reports relayed by an upstream broker (ASF security team or a
third-party disclosure platform) by applying preamble-detect,
@@ -232,6 +245,29 @@ publication, with a human gate and an audit-log entry at every step.
- Tools: `tools/cve-tool-vulnogram/generate-cve-json` (CVE 5.x JSON),
`tools/cve-org`, `tools/gmail` + `tools/ponymail` (mail), and the
`tools/privacy-llm` gate/redactor ([the privacy gate](privacy-llm-gate.md)).
+ The `gmail-plaintext` MCP server (`oauth_draft.mcp_server`) runs outside
+ the sandbox, so a caller-supplied `credentials_path` must be a `.json`
+ file directly in `~/.config/apache-magpie/` or in the directory of
+ `$GMAIL_OAUTH_CREDENTIALS`, reached through no symlink (#1795).
+- `tools/asf-security-dash` (`magpie-asf-security-dash`, `**Organization:**
+ ASF`) — the read-only bridge to `dash.security.apache.org` behind
+ `security-issue-sync`'s dashboard mode (#1780; see
+ [adapters.md](adapters.md)).
+- The `magpie-security-tools` substrate plugin (#1787) ships copies of the
+ tools the security skills run (`security-management`, `github-body-field`,
+ `github-rollup`, `generate-cve-json`, the Vulnogram `oauth-api`,
+ `privacy-llm/checker`, `vcs`, `local-tracker`, `jira`, `gmail/oauth-draft`,
+ `asf-security-dash`, `security-tracker-stats-dashboard`, and the
+ forwarder-relay registry with `gmail/asf-relay.md`),
+ kept identical to `tools/` by `tools/dev/check-family-plugins.py`.
+ Its SessionStart hook (`security-management/hooks/link-framework-path.sh`)
+ copies them once per plugin version to
+ `~/.cache/apache-magpie/framework/<version>` and points `current` at it,
+ so `<framework>` on a marketplace install is
+ `~/.cache/apache-magpie/framework/current` and every
+ `uv run --project <framework>/tools/<tool>` works unchanged.
+ Setup installs it with `magpie-security`; pre-flight and upgrade name it
+ when missing.
## Behaviour & contract
@@ -391,7 +427,13 @@ publication, with a human gate and an audit-log entry at every step.
activity; `--retriage` and explicitly numbered trackers are never
skipped.
`security-issue-import` drops CVE-tool bookkeeping mail by subject and
- sender in Step 1, before any thread body is read; the Step 3 row stays
+ sender in Step 1, before any thread body is read.
+ The senders are `<security-list>` plus `security_inbox.cve_tool_senders`,
+ which the ASF organization defaults to `security@apache.org`; setup's
+ pinned-snapshot install and adopt paths ask for the CNA reviewers' own addresses (never
+ guessed) and write a project list that repeats `security@apache.org`,
+ since a project value replaces the organization default (#1745).
+ The Step 3 row stays
as the backstop for the body-line variant, pre-filtered threads are
counted at Step 5 and listed at Step 8, and `keep <threadId>` sends one
back through Steps 2 to 5.
@@ -470,6 +512,14 @@ publication, with a human gate and an audit-log entry at every step.
A re-rendered hand-off comment keeps the release manager's tick, and a
ticked box keeps later runs quiet. The reminder links the project's
pages through the optional `security_pages_url` manifest key (#1355).
+- **Sync state markers are read from comments only (#1716).**
+ `issue-sync closed-announced` decides "records done", "pages ticked" and
+ "reporter notification drafted" from the tracker's comments, never its
+ body, because the body carries report text; the `advisory-records v1`
+ marker counts only as a comment's first line.
+ `issue-deduplicate merge-body` likewise refuses a value copied from the
+ dropped tracker that holds an HTML comment, so no machine marker travels
+ in copied field text.
- **With `advisory_records` configured, sync proposes the security-pages edit.**
The optional `advisory_records` manifest block names the site repository
(Git or Subversion), its working copy and a layout: one CVE 5.x JSON file
@@ -483,6 +533,42 @@ publication, with a human gate and an audit-log entry at every step.
idempotent, escapes tracker text, and refuses a page it cannot parse. The
reminder never fires for such a project. `magpie-vcs` gains `update` and
`checkout` for it.
+- **The CVE record is published only after the advisory ships (#1795).**
+ Sync's `REVIEW → PUBLIC` move through `vulnogram-api-record-publish`
+ runs only in the *advisory archived* combined apply, and the CLI refuses
+ (exit `4`) a record with no `references[]` entry tagged
+ `vendor-advisory`, so the CVE JSON is re-pushed with the archived URL
+ first ([CVE tooling](cve-tooling.md)).
+- **Dashboard mode reconciles with the ASF Security dashboard, ASF only
+ (#1780, #1798).** On *"sync dashboard"*, `security-issue-sync` follows
+ `issue-sync/dashboard.md`.
+ Step D0 reads `organization` from the resolved `project.md` and ends the
+ mode for anything but `ASF` (offering `/magpie-setup:setup config` when
+ `<upstream>` is under `apache/` but the key is unset), never asking for a
+ dashboard session; the bridge enforces the same rule itself, exiting `3`
+ unless `--project-md` declares `organization: ASF`.
+ D1 lists the PMC's open cases (`cna_private_owner`, else `<project>`),
+ D2 writes one entry per open or recently closed issue (decision,
+ CVEs, `Message-ID`s), D3 runs `reconcile`, and D4 proposes per `fix`:
+ `import`, `review` (a case that may be a standing label is confirmed as a
+ report before any import), `tracker`, `dashboard-triage` (a project-page
+ link and no reporter reply, since the dashboard emails the reporter) and
+ `asf-security` (one note drafted to `<foundation-security-list>`).
+ Nothing is applied unconfirmed, and case titles are external content.
+- **Runs without project adoption (#1787).** A maintainer who only
+ installed Magpie passes pre-flight and finishes the run: vetted-ops and
+ `generate-cve-json` read their config from the committed
+ `.apache-magpie-overrides/tools/…` first, then the personal layer at the
+ same relative path (`tools/vetted-ops/config.toml`,
+ `tools/cve-tool-vulnogram/cve-json-config.toml`), and `setup config`
+ drafts both.
+ The personal vetted-ops policy is read only from a layer the sandbox's
+ deny globs protect (`.git/**/apache-magpie`, `.svn/apache-magpie`,
+ `.apache-magpie-local`) and never through a symlink; the user saves it
+ from the draft themselves
+ ([vetted command surface](vetted-command-surface.md)).
+ `security-issue-fix` never saves personal data into the committed
+ overrides.
## Out of scope
@@ -508,6 +594,14 @@ publication, with a human gate and an audit-log entry at every step.
6. An imported report renders as one inert block, and every body-field
reader in the tree finds exactly the template fields once each in the
result, whatever fences or fake `###` lines the report holds (#1672).
+7. Dashboard mode and `magpie-asf-security-dash` stop for any project whose
+ `project.md` does not declare `organization: ASF`, before a session
+ cookie or the network is touched (#1798).
+8. Sync's closed-tracker markers are read from comments only; marker text
+ in the report body never skips a step (#1716).
+9. A scanner disclosure email is triaged only from a scan folder whose
+ completeness checks pass, and its report never leaves the machine
+ before disclosure (#1786).
## Validation
@@ -516,6 +610,8 @@ uv run --project tools/skill-and-tool-validator --group dev skill-and-tool-valid
uv run --project tools/security-management --group dev pytest
uv run --project tools/cve-tool-vulnogram/generate-cve-json --group dev pytest
uv run --project tools/gmail/oauth-draft pytest
+uv run --project tools/asf-security-dash --group dev pytest
+python3 tools/dev/check-family-plugins.py
```
## Known gaps
diff --git a/tools/spec-loop/specs/security-model-preparation.md b/tools/spec-loop/specs/security-model-preparation.md
index 5b357ce2a..73d0c1eeb 100644
--- a/tools/spec-loop/specs/security-model-preparation.md
+++ b/tools/spec-loop/specs/security-model-preparation.md
@@ -8,7 +8,7 @@ kind: feature
mode: Drafting
source: >
docs/security/security-model-preparation.md. The three
- security-model-* skills. The external rubric at
+ security-model-* skills and security-submit-to-oss-scan. The external rubric at
https://github.com/alpha-omega-security/threat-model. Transplanted and
generalised from the ASF security team's own model-preparation
workflow.
@@ -29,6 +29,8 @@ Gets a project's security model **written**, **findable**, and **current**
skills: produce a first model in draft-first mode and land it; pre-flight
an existing one for discoverability and completeness; and read the accrued
triage decisions back into it.
+A fourth, `security-submit-to-oss-scan`, carries the model to an automated
+scanner as a derived summary and keeps that copy in step with it.
The rubric for *what a model contains* is maintained externally by
Alpha-Omega and referenced by URL. The framework keeps no local copy — a
@@ -59,6 +61,19 @@ triage history back into the document.
that lacks it (model-update gained its declaration in #1444).
- Threat model: skill family F in `docs/security/threat-model.md`
(F.1–F.7, mitigations M.30–M.36).
+- Scanner copy (#1786): `security-submit-to-oss-scan` (Drafting,
+ `plugins/magpie-security/skills/submit-to-oss-scan/SKILL.md`, file
+ contracts in `references/enrolment-files.md`) prepares the `enrol` or
+ `update` PR against `anthropics/oss-scanner`: `project.yaml`, an
+ offline-capable `Dockerfile`, and a `threat_model.md` derived from this
+ model.
+ `security-model-prepare` points at it, and `security-model-update`
+ Step 12 proposes a refresh, one PR per copy, for every row of the
+ template's *Scanner threat-model summaries* table
+ (`<project-config>/security-model.md`) whose *derived from* model commit
+ is not the model file's latest.
+ Eval suite: `tools/skill-evals/evals/security-submit-to-oss-scan/`
+ (Step 5 derivation, 2 cases).
## Behaviour & contract
@@ -105,6 +120,19 @@ triage history back into the document.
As security-family skills they run it whenever at least one reviewer
is configured, whatever the `mode`; findings are advisory and never
block the flow.
+- **The scanner's threat model is derived, never authored.**
+ `security-submit-to-oss-scan` states every claim with the model section
+ it summarises (the scanner audits offline, so a link never stands in for
+ the statement), routes a line the model does not support to
+ `security-model-update` as a model gap, and stamps the copy with the
+ model URL and the model file's commit, so a stale copy is detectable.
+ It never ticks a checklist box, accepts terms or signs a CLA for the
+ maintainer, never runs `tools/check` or the built image itself, and
+ names the scan programme only in the scanner repository unless the team
+ chose in-repo placement.
+ Repeat false positives from `security-issue-import-from-scan` close the
+ loop the same way: a by-design cluster becomes a model-update candidate,
+ then a scanner-copy refresh.
## Out of scope
@@ -130,6 +158,9 @@ triage history back into the document.
4. No proposed model diff contains reporter identity, tracker contents, an
unpublished CVE ID, or detail of an unfixed issue.
5. The PR scaffold is idempotent and never edits existing prose.
+6. Every claim in a scanner `threat_model.md` cites the model section it
+ summarises, a claim the model lacks is reported as a gap rather than
+ written, and the copy records the model commit it came from (#1786).
## Validation
diff --git a/tools/spec-loop/specs/security-reporting.md b/tools/spec-loop/specs/security-reporting.md
index a318a95cb..de661190f 100644
--- a/tools/spec-loop/specs/security-reporting.md
+++ b/tools/spec-loop/specs/security-reporting.md
@@ -61,6 +61,21 @@ health without navigating the tracker issue-by-issue.
else the fetch scripts' default; before #1444 the skill ignored the
configured value, so it could judge freshness from a directory the
fetch never wrote.
+ That default is `<tempfile.gettempdir()>/tracker-stats-cache` (honouring
+ `$TMPDIR` / `%TEMP%`), not a hardcoded `/tmp` a sandboxed run cannot
+ write, and the default output path moved with it (#1752).
+- **Transient `gh` failures are retried (#1752).** `fetch_issues.py` and
+ `fetch_roster.py` run `gh` through `gh_retry.run_gh`, which retries a
+ 5xx, gateway timeout, connection reset or a call that hit its timeout up
+ to 3 attempts with exponential backoff, and fails fast on anything that
+ cannot clear on its own (auth, bad slug, unknown flag).
+ `run.sh` no longer guesses the output path; `render.py` prints the path
+ it wrote.
+- **Runs from a marketplace install.** The tool ships in the
+ `magpie-security-tools` plugin, so the skill's
+ `bash <framework>/tools/security-tracker-stats-dashboard/run.sh` works
+ without a framework checkout (#1787; see
+ [security-issue-lifecycle.md](security-issue-lifecycle.md)).
- **One list call, not one read per issue (#1443).**
`fetch_issues.py` requests `body` and `closedByPullRequestsReferences`
in its `gh issue list --state all --limit 1000` call, and
@@ -99,6 +114,8 @@ health without navigating the tracker issue-by-issue.
1. `render.py` / `run.sh` produces a valid HTML file from `<tracker>` data.
2. No tracker state is mutated (read-only `gh` calls only).
3. The tool ships its own tests under `tools/security-tracker-stats-dashboard/`.
+4. A single transient `gh` failure (5xx or timeout) on the issue or roster
+ fetch is retried rather than aborting the run (#1752).
## Validation
diff --git a/tools/spec-loop/specs/vetted-command-surface.md b/tools/spec-loop/specs/vetted-command-surface.md
index fbf35b71f..79b254e17 100644
--- a/tools/spec-loop/specs/vetted-command-surface.md
+++ b/tools/spec-loop/specs/vetted-command-surface.md
@@ -40,6 +40,18 @@ change.
Shipping implementation: [`tools/vetted-ops`](../../vetted-ops/README.md).
+**Where the policy is read from** (#1787).
+Without `--config`, the dispatcher reads the committed
+`.apache-magpie-overrides/tools/vetted-ops/config.toml` first, then
+`tools/vetted-ops/config.toml` in a personal layer, so a maintainer whose
+project has not adopted Magpie can still run the security skills.
+A personal layer counts only in a shape the sandbox profile's deny globs
+cover (`//**/.git/**/apache-magpie/…`, `//**/.svn/apache-magpie/…`,
+`//**/.apache-magpie-local/…`), and a policy reached through a symlink
+anywhere on its path, or that is not a regular file, is not read.
+The personal policy supplies data only and grants nothing `--config` could
+not; `setup config` drafts it, the user saves it.
+
The HTTP read backend (`http-read`, which implies `writes=False`) carries the
security-data lookups the adapters used to spell as raw `curl`: four OSV.dev
operations (`osv-get-vuln`, `osv-query-package`, `osv-query-commit`,