Repository navigation
fix(security-issue-deduplicate): keep the CVE both trackers share - #1857
Merged
Merged
Conversation
potiuk
enabled auto-merge (squash)
October 27, 2026 20:01
potiuk
disabled auto-merge
October 27, 2026 20:02
dpol1
force-pushed
the
fix/dedup-same-cve-no-self-retract
branch
from
October 27, 2026 20:56
e4f1df4 to
92ac1bd
Compare
When the kept and the dropped tracker carry the same CVE ID, classify returned that ID as `retract`, so the proposal offered item 7 as "retract CVE-X, merged into CVE-X": a retraction of the record the kept tracker still needs. classify now proposes a retraction only when the two IDs differ, and apply-plan refuses item 7 when the ID to retract is the kept CVE. The skill and the lifecycle spec say the same: a retraction needs two distinct IDs. Signed-off-by: Davide Polato <dpol1@apache.org> Generated-by: Claude Code (Opus 5.5)
potiuk
force-pushed
the
fix/dedup-same-cve-no-self-retract
branch
from
October 27, 2026 22:04
92ac1bd to
ed24990
Compare
Member
|
I rebased this onto the latest |
potiuk
added a commit
that referenced
this pull request
Oct 27, 2026
…crub tests (#1855) (#1856) * test(security): use claude-code/testing tool noun in public-surface-scrub tests (#1855) * fix(code-review): give inline comments the finding's text, not just its rule (#1853) render built each inline comment from `comment`, `explanation` or `rule_id`. Findings in the shape review-flow.md documents carry `quoted_rule` and `suggestion` instead, so every inline comment was posted as the bare rule label and told the contributor nothing. An inline comment now carries the finding as the review body does, minus the excerpt it sits on, and a folded finding falls back to its suggestion before its rule. Generated-by: Claude Opus 5 * Restructure the threat model project-wide and add SECURITY.md (#1859) Rebuild docs/security/threat-model.md on the Alpha-Omega threat-model rubric (sections 1.1-1.19) and widen it from the security skill family to the whole project: enforcement layer, ingest adapters, distribution and configuration. The previous model is carried over unchanged as Appendix B, with a back-map from its sections to the new claims. Add SECURITY.md (report privately to security@apache.org, link the threat model) and point AGENTS.md and the project manifest at it. Generated-by: Claude Opus 5 * security: one reply, one thread -- never combine replies across threads (#1858) Make it a golden rule that every drafted email answers exactly one inbound thread. Combining the responses to several threads into one message -- the consolidated receipt in security-issue-import, or one foundation-security note listing several dashboard cases -- confuses reporters, who cannot tell which report the reply answers, and the security team, who track each case by its thread and Message-ID. - AGENTS.md and tools/gmail/threading.md carry the rule; issue-import and issue-sync add it as a golden rule; bulk sync, bulk invalidate, deduplicate and cve-allocate point at it. - issue-import drops the cross-thread consolidated receipt; one thread split into several trackers still gets one reply on that thread. - issue-sync's dashboard drafts one asf-security note per case. - security-management wording follows; evals flip the same-reporter case to separate receipts and add a one-thread-split case. Generated-by: Claude Opus 5 * agent-guard: see through more wrappers; read MAGPIE_GUARD_OFF from the hook env (#1860) The guards now unwrap exec, nohup and time as well as env and command, and check a shell's -c string (bash/sh/zsh) as a command line of its own, one level deep. A command line whose quoting cannot be parsed is refused when it names git or gh, instead of being allowed unchecked. MAGPIE_GUARD_OFF is read from the hook's own environment only, so the switch is set by whoever launches the agent rather than written into a command. Per-guard MAGPIE_ALLOW_* overrides work as before, inline or from the hook environment. Generated-by: Claude Opus 5 * container-gateway: refuse writable binds over agent and git config (#1861) * container-gateway: refuse writable binds over agent and git config A container may no longer get a writable bind whose source is, contains or sits under the paths the agent harness and git read their configuration from: .claude/settings*.json, .claude/hooks, .claude/skills, .claude/commands, .claude/agents, .mcp.json, .git, the common git directory's hooks and config, a linked worktree's git directory, the run directory and the personal config layer. The same binds read-only are still allowed, as is a writable bind of a subdirectory that covers none of them. Only an exact `ro` makes a bind read-only, and podman's `U` beside it counts as writable. Generated-by: Claude Opus 5 * container-gateway: refuse overlay mount options Generated-by: Claude Opus 5 * container-gateway: protect agent config inside nested worktrees Generated-by: Claude Opus 5 * Tighten adapter defaults: TLS, collaborator rollups, HTTPS-only bridge (#1862) * Tighten adapter defaults: TLS, collaborator rollups, HTTPS-only bridge - imap: connect with a verified TLS context (ssl.create_default_context()). - github-rollup and vetted-ops: a status-rollup comment is read only when its author is the authenticated gh user or a repository collaborator. - jira bridge.groovy: refuse non-HTTPS base URLs and do not follow redirects. - sourcehut: comment bodies go through --body-file only; graphql mutations need --allow-mutation; the service must be a bare subdomain. - jira-patch docs: the reviewed attachment is pinned by id and sha256 before it is applied. Generated-by: Claude Opus 5 * Stop on an unverifiable rollup author; end GraphQL comments at CR Generated-by: Claude Opus 5 * Security workflow: stricter relay detection and advisory URL checks (#1863) * Security workflow: stricter relay detection and advisory URL checks - Forwarder relay detection needs both the adapter's sender pattern and its preamble to match; a single match is surfaced to the user as "not a relay". Relay-derived reporter credit stays a proposal until a human confirms it. - New `security-management issue-sync advisory-archive` check: a Public advisory URL is accepted only for a thread on the advisory list whose root subject is `<CVE-ID>: <description>`, under the configured archive URL, and always with confirmation. - PonyMail: an empty result for a private list reads as "unknown" unless the list is visible to the session. - Docs: egress-gateway, cve-org, forgejo and mailman3 curl notes, and macOS egress in secure-agent-internals. Generated-by: Claude Opus 5 * advisory-archive: require an authorized sender and a configured archive Generated-by: Claude Opus 5 * advisory-archive: parse the sender as a mail header Generated-by: Claude Opus 5 * issue-sync: say the advisory sender is unverified when asking to confirm Generated-by: Claude Opus 5 * agent-guard: a continued heredoc line is not stripped (#1864) Generated-by: Claude Opus 5 * setup: import the pre-flight checker only from outside the working tree (#1866) * setup: import the pre-flight checker only from outside the working tree Pre-flight now runs `python3 -P -m setup_preflight` with PYTHONPATH set to the git directory's apache-magpie/ (or .svn/apache-magpie/) only. `-P` keeps the working tree off the import path, and no in-tree directory is on it, so a repository cannot ship the module pre-flight imports. `config` installs the checker into that checker_dir (new `checker_home()` in layers.py); the regenerated block is in every skill. The checker refuses a Python older than 3.11 before importing anything (exit 3, which the block reads as "did not run"), and the test session refuses to start on one. For a git-tag install, `upgrade`, `verify` and `install` also compare the committed lock's commit anchor with the fetched commit and with the tag resolved upstream. A tag that moved stops the upgrade; re-pinning to it needs `bump-committed` and an explicit confirmation. Generated-by: Claude Opus 5 * setup: trust .svn only in a real working copy; check an existing snapshot's commit Generated-by: Claude Opus 5 * agent-guard: strip heredoc bodies only where bash treats them as data (#1867) * agent-guard: open a heredoc only on an unquoted, uncommented << When a command line cannot be tokenised, the guard strips heredoc bodies before parsing again. It found the `<<WORD` operators with a regex over the raw line, so a `<<EOF` inside quotes or a `#` comment counted as a heredoc, and the lines after it were dropped as its body -- while bash runs them as commands: cat /dev/null # <<EOF it's git push --force EOF The operators are now found by a scan that tracks quotes (including `$'...'` and quotes left open on an earlier line) and stops at a comment. A body naming git / gh under an unquoted delimiter is also no longer stripped when it holds a `$(...)` or backtick substitution, which bash expands even in a heredoc fed to cat or tee. Generated-by: Claude Opus 5 * agent-guard: read heredoc delimiters and shifts as bash does Two more places where the heredoc stripper and bash disagreed: - `<<` inside arithmetic or a parameter expansion (`$[1<<x]`, `${a[1<<x]}`) is a shift, not a heredoc, yet its "body" was stripped. Such a line now leaves the command unstripped. - The delimiter was matched as `[A-Za-z_][\w.-]*` with optional surrounding quotes, so `E-O:F`, `'E'OF` or `E"O"F` closed the body on a different line than bash does. The delimiter is now the whole word up to the next metacharacter, after quote removal, and quoted when any quote or backslash appears in it; a word with `$` or a backtick is not stripped. A body naming git / gh is also stripped only when the introducing line is a bare `cat` / `tee` with plain file arguments and `>` / `>>` redirections. Generated-by: Claude Opus 5 * agent-guard: strip a heredoc body naming git / gh only from a bare cat Rather than chasing each shape in which a heredoc fed to cat / tee is not really data, strip a body naming git or gh only in the narrowest safe shape, and keep the command whole otherwise: - the introducing line starts a command (the line before does not end in `\`, `|`, `&&` or `||`); - its first word is exactly `cat`, `tee`, `/bin/cat` or `/usr/bin/cat`; - the command has no `alias`, no `cat` / `tee` function, and no `$((`, `((`, `$[` or `${` anywhere. Generated-by: Claude Opus 5 * agent-guard: keep a heredoc whole when its output may be run, and see into eval A body naming git / gh fed to a bare cat was still stripped when the cat output itself was run: `(cat <<EOF ... ) | bash`, `{ ...; } | bash`. Instead of matching each wrapper, a body naming git or gh is now stripped only when the command, with its heredoc bodies removed, holds no `$`, backtick, parenthesis or brace, no `alias`, and no `eval`, `source` or `.` in command position. This subsumes the earlier function and arithmetic checks. `eval` hid what it ran from every guard, heredoc or not. Its arguments, joined as bash joins them, are now split and checked like a shell -c string, one level deep. Generated-by: Claude Opus 5 * agent-guard: every heredoc a sink before stripping; harden eval unwrapping A heredoc fed to something other than cat / tee had its body stripped too, so `bash <<A` with `cat(){ bash; }` in its body redefined cat out of sight before a later stripped `cat <<EOF` body naming git ran. When any stripped body names git or gh, every heredoc in the command must now be a qualifying sink, or the command is kept whole. The eval / source / . check also matches them behind `builtin`, `command` or inline assignments (`X=1 eval`). eval unwrapping drops one leading `--` (`eval -- 'git push'`) and treats `builtin eval` like `eval`; an eval whose first argument is any other option is marked unparsed and refused. Generated-by: Claude Opus 5 * agent-guard: see eval through prefix chains; keep heredocs next to eval-like words eval unwrapping now strips any chain of `builtin`, `command`, `--` and `-p` in front of eval (`builtin command -- eval ...`). Any other option in that chain, or after eval, marks the segment unparsed so it is refused. A stripped heredoc body naming git or gh is now kept whenever `eval`, `source`, `.`, `hash` or `enable` appears as a word anywhere in the rest of the command after quote and backslash removal (`xargs e'v'al < s`, `env . s`, `hash -p /bin/bash cat`), not only in command position. The README notes the limit of a command-string guard: a cat heredoc that writes a script a later command runs is out of its reach, since the agent could write the same file with its file tools. Generated-by: Claude Opus 5 * skill-evals(setup): bring seven drifted steps in line with the skill (#1868) Twelve setup cases failed on main because the fixtures or output specs no longer matched what the skill says, not because the skill was wrong: - step-delegate-propose: name skills by their `name`, not their path. - config-local-custom-parser: the parser is confirmed on the user's terminal via trust-parser, with the upstream offer in the same reply; cases 1 and 4 now follow that flow, and "this run" is defined. - step-m3-baseline-pick: security brings magpie-security-tools. - step-m4-install-gates: the hand-off is /magpie-setup:setup adopt; a bare marketplace add has no scope. - step-override-bypass: the reason names the overrides as skipped or applied; "framework defaults" counts as skipped. - uninstall-default-set: enabledPlugins is edited, never preserved. - upgrade-adoption-split: updates_plugins means a version changed. Generated-by: Claude Opus 5 * agent-guard: fail closed on untokenisable lines that can spell git or gh (#1865) * agent-guard: see git / gh through quoting on an untokenisable line A command line shlex cannot tokenise is refused when it names git or gh as a word, and allowed otherwise. The check ran a plain word regex over the raw text, so quoting inside the word hid it, although bash removes the quotes and runs the command: g'i't push --force # it's Quotes and backslashes are now removed before the check, at all three places it runs: the untokenisable line, a stripped heredoc body, and a nested shell -c string. Generated-by: Claude Opus 5 * agent-guard: fail closed on an untokenisable line with an expansion Removing quotes and backslashes still missed words bash builds from an expansion that may be empty or a line continuation: `g$()it`, `g${x:-}it`, `g``it`, `g$@it`, `$CMD`, `g\<newline>it`. An untokenisable line holding `$`, a backtick or a backslash-newline is now refused outright, and the quote removal also drops `$` before a quote (`$'g'h`) and backslash-newline. Generated-by: Claude Opus 5 * agent-guard: fail closed on globs and braces in an untokenisable line `/usr/bin/gi?`, `/usr/bin/g*t`, `/usr/bin/g[i]t` and `/usr/bin/g{i,}t` all resolve to git. An untokenisable line holding `*`, `?`, `[` or `{` is now refused alongside the expansions and line continuations. Generated-by: Claude Opus 5 * setup-preflight: restamp the isolated-setup fingerprint after rebase Generated-by: Claude Opus 5 * adversarial-review: keep agy from handing the review to a subagent (#1871) Asked to list or search a tree, headless agy starts a research subagent and its turn ends at once, SUCCESS, with "waiting for the subagent" as the answer or none at all. Every Antigravity review on a real diff came back empty this way. Told only "file reads", it instead reads directories file by file and runs out of budget. The prompt note now names the tools that work (list_dir, find_by_name, grep_search, view_file / read_file) and forbids subagents, and the output check reports a subagent handoff by name instead of "no response text". Generated-by: Claude Opus 5 * skill-evals(security-issue-sync): follow the dashboard steps as they now read (#1872) step-d0: a proceeding mode asks for a session only after the bridge reports one missing. step-d4: since one reply, one thread, each case with an asf-security finding gets its own note. Generated-by: Claude Opus 5 * setup: import the pre-flight checker from a per-user directory, never from the project (#1870) #1866 moved the checker out of the working tree into the project's VCS directory (<git-common-dir>/apache-magpie/ or .svn/apache-magpie/), on the reasoning that no commit can carry it. That holds for a git commit, but not for a Subversion checkout, which versions a .git/ like any other directory, nor for a tarball, which can carry a .git/ or a .svn/ with its wc.db. `git rev-parse` and layers.py accept such a directory as real, so opening that tree put its planted setup_preflight/ on the import path of every skill's pre-flight. The checker now lives in one per-user directory, ~/.cache/apache-magpie/preflight/, and the block runs PYTHONPATH="$HOME/.cache/apache-magpie/preflight" python3 -P -m setup_preflight. Nothing inside the opened project is ever on the import path, and a project without version control now gets the checker too. The personal configuration layer is unchanged. `upgrade` moves a copy an earlier version left in the project's .git/ or .svn/ as well. Generated-by: Claude Opus 5 * agent-guard: keep a command whole when any line is continued (#1869) Heredoc stripping refused a continued line only when that line opened a heredoc (#1864). Any other line ending in a backslash was read on its own, but bash joins it with the next line: a `#` at the start of that line then sits mid-word instead of opening a comment. That let a guarded command through. In echo x\ #' cat <<EOF ' git push --force EOF bash reads the word `x#'`, whose quote swallows `cat <<EOF` and closes on the `'` line, and then runs `git push --force`. The guard took `#'` for a comment and stripped the `git` line as the body of a `cat` heredoc. Heredoc stripping now keeps the command whole whenever any line outside a heredoc body ends in a backslash, so such a line is refused as unparsable when it names git or gh. A backslash inside a body is still data. Generated-by: Claude Opus 5 * agent-guard: split a command line into commands the way bash does (#1873) * agent-guard: split a command line into commands the way bash does split_segments tokenised with shlex.split, which reads a newline as plain whitespace and only splits on an operator surrounded by spaces. So `echo hi` + newline + `git push`, or `echo hi;git push`, came out as one `echo` segment and no guard saw the git command. Subshells, groups, reserved words and command substitutions hid commands the same way. A small lexer now reads the line with bash's rules: a newline or any control operator ends a command, spaced or not; redirections (2>&1, &>, >|) stay words; `#` comments and quoted text are not commands; a leading reserved word is skipped; subshells, command and process substitutions (also inside double quotes, arithmetic and ${...}) are read as commands of their own. A heredoc body stays data on its command (and in its raw text for the trailer scan), except that its substitutions are read when the delimiter is unquoted, and a body naming git / gh that is fed to anything but a bare cat / tee is read as commands. Generated-by: Claude Opus 5 * agent-guard: close four gaps in the command-line lexer - A `case` word starting a command inside `$( … )` or a subshell makes the line untokenisable: its pattern's `)` would close the construct early, and inside double quotes that hid the rest of the line. - A redirection keeps `&` / `|` only after an unquoted, unescaped `>` / `<`; `echo '>'&git …` splits as bash splits it. - A standalone `{` or `}` ends the command before it, so `function f { git …; }` reaches the guards. - `$'…'` strings decode bash's escapes (`\xHH`, octal, `\uHHHH`, …); `\c` makes the line untokenisable. Generated-by: Claude Opus 5 * agent-guard: keep braces, NULs and `case` from moving command boundaries - `{` opens a group only in command position (or after `function name`) and `}` closes one only right after a control operator or newline; anywhere else a brace is an ordinary word, so `git commit -m x } -m …` keeps its second message on the commit. - A `$'…'` escape that decodes to NUL makes the line untokenisable: bash truncates the word there. - Any unquoted `case` word inside a substitution or subshell makes the line untokenisable, whatever precedes it (`time`, `time -p`, …). - #1865's untokenisable-spelling tests end in an unbalanced quote instead of a `# it's` comment, which the lexer now reads as a comment. Generated-by: Claude Opus 5 * agent-guard: a group behind time or coproc is a group `time { git push; }`, `time -p { … }` and `coproc [NAME] { … }` open a brace group, since `time` (with `-p` / `--`) and `coproc` keep command position. The lexer read the `{` there as a plain word, so the commands inside the group were never checked. Generated-by: Claude Opus 5 * agent-guard: check the command a literal name and leading redirections leave (#1874) * agent-guard: refuse a command whose name is an expansion or a glob A command line that tokenises but spells its command name through an expansion or a glob (`$CMD push`, `"$GIT" push`, `$(echo git) push`, `/usr/bin/gi? push`, `/usr/bin/g{i,}t push`) ran unchecked: the guards cannot see which command it resolves to. #1865 refused such spellings only on a line that could not be tokenised. Now a command whose name, after wrappers are unwrapped, holds `$`, a backtick or a glob or brace character is refused (`agent-guard[expansion]`); `[` and `[[` are exempt, and the vetted-ops and excluded-script spelling guards keep their more specific message. Generated-by: Claude Opus 5 * agent-guard: accept only a literal command name; skip leading redirections The command-name check refused a denylist of expansion and glob characters, which missed zsh's `=git` (expands to git's path) and `g(i)t` glob group. A command name must now be literal: letters, digits, `_ . / + : @ % , -` and an optional leading `~/` (`[` and `[[` aside). A `(` glued to a word other than `name()` or `arr=(…)` makes the line untokenisable, since bash rejects it and zsh reads it as a glob. Redirections written before the command name (`>/dev/null git …`, `2>&1 git …`, `nohup >log env git …`) were read as the command name, so the git command after them was never checked. They are now skipped like leading `NAME=value` assignments, also between wrappers, so the command after them is the one checked; `exec 3>&1` stays allowed. Generated-by: Claude Opus 5 * fix(security-issue-deduplicate): keep the CVE both trackers share (#1857) When the kept and the dropped tracker carry the same CVE ID, classify returned that ID as `retract`, so the proposal offered item 7 as "retract CVE-X, merged into CVE-X": a retraction of the record the kept tracker still needs. classify now proposes a retraction only when the two IDs differ, and apply-plan refuses item 7 when the ID to retract is the kept CVE. The skill and the lifecycle spec say the same: a retraction needs two distinct IDs. Generated-by: Claude Code (Opus 5.5) Signed-off-by: Davide Polato <dpol1@apache.org> * test(security): pin hook test mocks to the exact paths and env field - setupTrackerEnv answers only the session-root paths /home/dev/airflow-s/.apache-magpie-overrides/project.md and /home/dev/airflow-s/.git/config, as runInTracker did, so the tests again prove the hook resolves the tracker from $.session.root(). - The upstream-clone and tracker-cwd tests drop the catch-all `|| p.endsWith('.git/config')`, restoring the old path checks. - The env.get mock matches only `e.name`, the field the claude-code/testing harness sends (a mock keyed on `e.key` fails the bypass test). Generated-by: Claude Opus 5 --------- Signed-off-by: Davide Polato <dpol1@apache.org> Co-authored-by: Jarek Potiuk <jarek@potiuk.com> Co-authored-by: Davide Polato <dpol1@apache.org> Co-authored-by: Jarek Potiuk <potiuk@apache.org>
potiuk
added a commit
that referenced
this pull request
Oct 27, 2026
#1838) * feat(hook-manager): register managers that need no install fix (#1825) Add install strategy 'none' to the hook-manager contract and register managers that write directly to the repository's local hooks directory without requiring global core.hooksPath rewrites: simple-git-hooks, cargo-husky, git-build-hook, and git-code-format-maven-plugin. Document known worktree and configuration caveats. * chore(hook-manager): fix markdownlint blank line and ruff formatting * docs(security): Vulnogram review comments cannot be resolved (#1840) Sync told the operator to "open the CVE record and resolve the review comment". Vulnogram has no resolve action for comments, in the API or the UI, so the step could never be done. A comment is now handled once the body change it asked for is applied, its slug is in the processed-comments ledger, and the reviewer has a courtesy reply. The REVIEW -> READY note in apply-and-push.md and the round trip in the Vulnogram adapter's record.md (which still described the manual #source paste) now say the same. Generated-by: Claude Opus 5 * fix(setup): make isolated-setup-verify checks catch false passes (#1808) * fix(setup): make isolated-setup-verify checks catch false passes A verify run on a Linux host launched from an IDE extension, with the project settings only in settings.local.json, reported several checks as passing that proved nothing: - Check 1 read only the committed project file and never asked for sandbox.filesystem.denyRead. Without "~/" there, bubblewrap reads are deny-only and credential files stay readable. Check 1 now evaluates the merged project, project-local and user settings, requires the home denyRead entry, and names the file each key came from. - Check 7a treated bubblewrap's "No such file or directory" as a pass, which a missing file also prints. It now probes common credential paths with test -r and stat, never cat, and reports inconclusive until the user confirms from a terminal that one exists. - Check 7b echoed AWS_ACCESS_KEY_ID, which the reference sandbox.credentials already unsets, wrapper or not. It now lists variable names only, flags credential-shaped ones, and reports an IDE CLAUDE_CODE_ENTRYPOINT as a warning, since such a session never went through claude-iso. The clean-env wrapper doc says so too. - Checks 5, 10a and 13c compare against a framework checkout the skill never located. A pre-flight now finds it, and reports those comparisons as unchecked when there is none. - Check 10 ran the touch-overlay sub-checks for any signed commit. They now apply only to a hardware key; 10c still applies to every ssh-format key. - Sandbox /dev/null placeholders (character special 1,3) looked like empty untracked files. The verify and doctor skills now say how to recognise them and to take host state from git or the user. The mirrored prompt in secure-agent-setup.md is kept in sync, and the step-1-classify eval suite gains four cases for these verdicts. Generated-by: Claude Code (Claude Opus 5.5) * fix(setup): address review of isolated-setup-verify checks Check 10 counted a key as hardware only for an sk- public key or an OpenPGP-format card. ssh-format signing through gpg-agent uses the card's aut slot and shows a plain ssh-ed25519 or ssh-rsa key, so a card gpg --card-status finds now counts whatever gpg.format is. Check 7a was inconclusive on every correctly secured host, because with denyRead covering ~/ every path under it looks missing. It now proves the denial with a sentinel: a file known to exist on the host, such as the shell rc, that the sandbox cannot see. Inconclusive is left for the case with no sentinel. The step-1 output spec gains the inconclusive and unchecked verdicts the skill now emits, with cases for both and a step-2 case for the unchecked routing. The rest is review follow-up: managed settings in check 1's merge, the opt-in question for flagged variable names, the running claude-code version beside an unchecked check 5, the credential-shaped wording of the no-print rule, the update prompt's denial probes, and probe output attributed to stat in the fixtures. Generated-by: Claude Code (Claude Opus 5.5) * fix(setup): address second review of isolated-setup-verify With no framework checkout, the claude-code min_version floor could not be compared and was reported unchecked, so a marketplace-only install below the minimum passed. Report that floor as warn instead, and route the re-run accordingly. Under zsh on macOS, test -e on a denied sentinel still returns 0, so the 7a sentinel uses test -r with the /dev/null placeholder exception. A sentinel that stays readable and is not a placeholder now means denyRead is not in effect (fail). A one-call test -r ~/.ssh/id_* misreads as "not readable" with two or more matches (a key plus its .pub is enough), and zsh errors out on no match, so keys are listed with find -H (follows a symlinked ~/.ssh), which behaves the same in bash and zsh. A plugged-in smart card that does not hold the signing key made the touch-overlay checks mandatory for software-key users. A card now counts only when it holds the ssh-format signing key, an OpenPGP signing-capable key (looked up by user.signingkey, else the committer email), or the key a gpg-agent ssh remote authenticates with. A new eval case covers a software signing key with an unrelated card. 7b gains more credential-name patterns, and the docs prompt matches the skill. Generated-by: Claude Code (Opus 5.5, Sonnet 5.5) * fix(container-gateway): refuse driver options on volume create (#1842) A volume mount entry with a driver configuration is already refused, but POST /volumes/create (compat and libpod) only checked key spelling before labelling the volume, and the relay then lets a container mount any labelled volume by name. Apply the same rule where the volume is made: only the stock local driver with no options is allowed. The empty options map and local driver both CLIs send on a plain create still pass. Generated-by: Claude Opus 5 * feat(security): add deterministic public-surface scrub Claude Code mod (part of #1497) (#1676) * feat(security): add deterministic public-surface scrub Claude Code mod (part of #1497) Adds a zero-token Claude Code mod in plugins/magpie-security/hooks/ that intercepts Bash commands (git commit, gh pr create, gh pr comment) and guards public surfaces against accidental security embargo leaks (CVE IDs, vulnerability wording, security fix phrasing, and vulnerability classes). - Intercepts tool.call on Bash commands, parsing commit messages (-m, -am, -sm, -F) and PR descriptions (-t, -b, -F, --body-file). - Ingests heredocs and shell pipelines to prevent stdin bypasses. - Exempts private security tracker repositories (-s suffix convention). - Provides MAGPIE_ALLOW_EMBARGO_TERMS=1 maintainer escape hatch for legitimate post-coordinated-disclosure maintenance. - Passes clean behavioral changes through with zero token consumption. - Includes 29 unit tests covering all matching rules, command syntax, edge cases, and hook middleware integration. * fix(security): address maintainer review on deterministic public-surface scrub mod - Limit escape hatch strictly to options.env set by human, removing in-command bypasses. - Scope checks to upstream_repo from project.md; evaluate private tracker exemption per subcommand. - Split multi-command pipelines on shell separators (including newlines) and accept attached short flags. - Fail closed on unreadable message files and indeterminate dynamic input. - Resolve file paths relative to git -C directory without node:path. - Expand test suite to 47 unit tests covering all review shapes. * fix(security): address maintainer review on canonical terms and fail-closed gates (#1676) - Derive forbidden terms directly from canonical magpie_vcs.publication.DEFAULT_TERMS using tools/dev/sync-forbidden-terms.py, with pre-commit hook to prevent drift. - Tokenize shell commands with POSIX shell-words parser to handle quotes and escaped quotes. - Track directory context across chained sub-commands (cd <dir> && git ...). - Parse git -C only from global options and disambiguate clustered/attached flags. - Inspect remotes and worktrees to detect contributor fork checkouts as upstream scope. - Fail closed on indeterminate dynamic input with -F <file> guidance. - Pass environment from hook event and add register()-level tests for MAGPIE_ALLOW_EMBARGO_TERMS. - Document mod architecture, tradeoffs, and escape hatch in secure-agent-setup.md. * docs: document sync-forbidden-terms.py and update issue-fix token count stamp * fix(security): address maintainer review #3 on Claude Code hook and CLI parser - Correct Claude Code hook registration: register on('tool.call', { tool: 'Bash' }), read e.command directly, and return { deny: evaluation.reason } on block. - Read environment bypass via $.env.get('MAGPIE_ALLOW_EMBARGO_TERMS') to pass strict plugin validation. - Drop isTrackerCheckout shortcut; inspect git remotes directly and probe fallback user.md for upstream identification. - Expand CLI subcommand parsing in parseSubcommand: * Extract repository targets from PR/issue URLs (https://github.com/<owner>/<repo>/pull/<id>). * Extract leading GH_REPO= environment variables. * Support --body-file in gh pr review. * Inspect gh api --input, -F body=@file, and GraphQL mutations. - Enhance shell splitting and dynamic tokenization: * Split on single & command separator (unless part of >&). * Ignore literal $VAR inside single quotes (e.g. -m 'costs $HOME'). - Calibrate documentation in secure-agent-setup.md regarding zero-token deterministic guard. Generated-by: Antigravity * fix(security): address maintainer review round 4 on public-surface scrub hook - Parse environment.upstream_clone from user.md to detect upstream clone sessions without project.md; expand ~ via HOME/USERPROFILE - Follow .git gitdir pointer in linked worktrees to find common dir - Never exempt gh api graphql mutations from tracker checkout; inspect all -f/-F field values in graphql calls, not just body/title - Extract --trailer values; treat -C, -c, --reuse-message, --reedit-message, --fixup, --squash as dynamic input (fail closed) - Recognise clustered short flags containing m/F in git tag hasMsg gate - Use exact owner/repo slug matching (extractSlugFromUrl) instead of substring .includes(); upstream match takes precedence over tracker - Preserve separator type in splitShellSubcommands; cd only persists across sequential operators (&&, ;, newline), not pipes - Resolve git -C relative to virtualCwd; compose repeated -C flags - Track same-command file redirections; flag referenced files written earlier in the composite command as dynamic input - Remove dead reporterNames/privateTexts/shingle code and orphaned forbidden-terms.json; sync-forbidden-terms.py generates .ts only - Remove (e?.env ?? {}) fallback; read bypass exclusively via $.env.get - Guard process.env access with typeof check for sandbox safety - Add 9 targeted tests for review round 4 hardening * fix(security): address maintainer review round 5 on public-surface scrub hook * fix(security-scrub): invert default to fail-closed on unresolvable shapes and address review 6 findings - Track subshell parenthesis and brace depth in splitShellSubcommands to preserve grouped directory transitions - Extract binary basename to detect absolute and relative git/gh paths (/usr/bin/git, ~/bin/gh, ./git) - Limit command wrapper checking to known wrappers and avoid false positives on plain arguments (grep git, which gh) - Fail closed on repository redirection flags (--git-dir, --work-tree, GIT_DIR, GIT_WORK_TREE) and prevent tracker remote exemption - Skip gh api option values (--input, -F, -H, etc.) when extracting target repository - Treat gh api graphql with --input or @file as mutation to prevent tracker checkout exemption - Strip quotes, trailing slashes, and comments during git remote URL parsing - Track previous mutating commands on composite shell lines and fail closed when reading files - Fall through on dynamic target repositories (-R "$REPO") and unconfigured upstream - Update generateBlockingReason and documentation with accidental-leak guard framing - Add 15 regression tests covering all Round 6 review findings (94/94 passing) * fix(security-scrub): address maintainer review round 7 on public-surface scrub hook - Seed virtualCwd from $.session.root() and event cwd in register() hook to reliably evaluate bare git commit in upstream clones without explicit cd. - Track virtualCwd persistence across subshells vs brace groups: restore cwd after ( ... ) subshell while preserving cwd updates across { ...; }. - Treat wrapped commands as wrapped if git/gh appears in any subsequent token, skipping wrapper options and arguments (timeout 30 git, nice -n 10 git, sudo -u x git). - Strip leading slash and API host prefix in gh api endpoints; do not exempt literal endpoints without owner/repo placeholders via tracker cwd. - Scan all REST field values in gh api (-f, -F, --field, --raw-field) rather than whitelisting body/title. - Consume heredoc bodies up to delimiter without splitting or quote toggling; fail closed on unclosed quotes when git/gh is present. - Treat transparent shell control prefixes (then, do, else, elif, !, if, while, until, {, }) transparently, and fail closed on eval or sh/bash/zsh -c invoking git/gh. - Avoid false positives in unconfigured repositories by falling through to term scan instead of failing closed unconditionally on unconfigured upstream or --git-dir/GIT_DIR. - Restrict --git-dir and GIT_DIR checking to public-surface commands (commit/tag) and fall through to term scan without cwd remote exemption. - Fix typo in gh issue comment commandType. - Add comprehensive regression tests for all review findings (121 passing tests). * fix(security-scrub): guard against misparsing GitHub API non-repo routes as target repos and handle escaped heredoc delimiters * test(security-scrub): add mockup tests for session root and private tracker bare commits * fix(security-scrub): harden public-surface scrub against cd || guards, v3 API routes, env -C, and git merge * fix(security-scrub): address maintainer review round 8 on public-surface scrub hook * build(codex): declare no-op codex-hooks for magpie-security * fix(security-scrub): name --repo <tracker> when the cwd fallback blocks a tracker command The Bash tool call carries no cwd, so register() always takes the session-root fallback and the tracker cwd exemption never applies. A bare tracker command that mentions a CVE id is therefore always blocked; the block message now says to pass --repo <tracker>, the docs say the same, and a register() test covers an event with no cwd. Generated-by: Claude Opus 5 --------- Co-authored-by: Jarek Potiuk <potiuk@apache.org> * chore: bump dev version to 0.9.0.dev202610262031 (#1843) Move the dev stamp so adopters' `claude plugin update` picks up the work merged since the previous stamp. The marketplace is served from `main` and the update check compares version strings, so a frozen suffix is a silent no-op: an adopter running that command is told they are already current while sitting behind. Run from the Actions tab by @potiuk. Generated by .github/workflows/bump-dev-version.yml with the documented steps: bump `project.version` in `pyproject.toml`, then `tools/dev/check-family-plugins.py --fix` and `uv lock` to propagate to every manifest. The stamp is UTC at minute resolution. Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Jarek Potiuk <595491+potiuk@users.noreply.github.com> * chore(deps): bump certifi from 2026.7.22 to 2026.10.18 (#1844) Bumps [certifi](https://github.com/certifi/python-certifi) from 2026.7.22 to 2026.10.18. - [Commits](https://github.com/certifi/python-certifi/compare/2026.07.22...2026.10.18) --- updated-dependencies: - dependency-name: certifi dependency-version: 2026.10.18 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix(agent-isolation): hint on container-gateway policy refusals (#1810) * fix(agent-isolation): hint on container-gateway policy refusals A tool that drives the docker CLI (e.g. Airflow's breeze) hides the gateway's 403 body, or gets only "unable to upgrade to h2c, received 403" from an HTTP-upgrade endpoint, so the agent never saw why it was refused and routed around the gateway. Add a hint-hook branch for both signatures and document the refusals (BuildKit, socket bind-mount, untyped docker inspect) and the expected behaviour: report, don't bypass. Generated-by: Claude Code (Opus 5.5, Sonnet 5.5) * test(agent-isolation): parenthesize concatenated parametrize case CodeQL flagged the implicit string concatenation in the gateway-refusal case list as a possible missing comma. Wrap the two-part literal in parentheses so the concatenation is explicit. Generated-by: Claude Code (Opus 5.5) --------- Co-authored-by: Jarek Potiuk <potiuk@apache.org> * feat(tools/discourse): list category topics by latest activity (#1841) * feat(tools/discourse): list recent category activity Projects using Discourse need to discover category discussions before fetching individual topics. Resolve category IDs and catalog paths, then read explicit activity-ordered lists through the existing GET client. Filter on bumped_at without substituting the last-message timestamp. Deduplicate matches and look ahead for an additional match so truncation reports evidence rather than assuming that another page contains results. Fail incomplete reads through the existing CLI error channel, and keep the adapter partial until the remaining archive operations are available. Generated-by: Codex (GPT-6) * fix(tools/discourse): bound activity scans and resolve category slugs Use the requested native descending activity order to stop when an explicitly non-pinned topic is older than since. Missing pin evidence must not be replaced by the output default when deciding the cutoff. Keep category metadata separate from slug matching so an empty or unusable slug does not block numeric ID reads or unrelated selections. Resolve bare slugs only at the catalog root, requiring full ancestry or a numeric ID for subcategories and incomplete catalogs. Retain evidence-based truncation and the integer-conversion error channel. Add regressions and clarify the supported CLI behavior. Generated-by: Codex (GPT-6) * refactor(tools/discourse): simplify category list handling Resolve category IDs from native references and restrict catalog lookup to top-level slugs. Treat continuations as signals while rebuilding every request with the fixed category and activity order. Keep whole-page validation and exact truncation, consolidate regressions, and clarify that pinned category topics count toward the result limit. Generated-by: Codex (GPT-6) * fix(tools/discourse): accept rooted category paths Resolve category paths against the configured forum without duplicating its base path. Keep full URL boundaries and reject protocol-relative references before any request. Clarify unsupported list-view suffixes and cover rooted paths on root and subfolder installations. Generated-by: Codex (GPT-6) * refactor(tools/discourse): parenthesise the tag-name lookup The expression in _summary already bound as (name or slug) if dict else tag; the parentheses make that explicit. Generated-by: Claude Opus 5 --------- Co-authored-by: Jarek Potiuk <potiuk@apache.org> * feat(harness): support Antigravity, remove Gemini CLI (#1845) * feat(harness): support Antigravity, remove Gemini CLI Google retired Gemini CLI for individual accounts on 2026-06-18 and replaced it with the Antigravity CLI (`agy`). Magpie now supports Antigravity and drops the Gemini CLI harness. - agent-guard: `--antigravity` adapter for the PreToolUse hook on `run_command` (JSON deny decision on stdout); `--gemini` removed. - Secure setup is per user: tools/agent-isolation/antigravity/ ships the settings.json profile (terminal sandbox, proceed-in-sandbox, workspace-only file access, allow/ask/deny rules mapped from the Claude Code baseline) and the hooks.json guard registration for ~/.gemini/config/hooks.json. - sandbox-lint: `--antigravity` checks the profile, deriving the required asks and denies from expected.json; the Gemini profile checks and runtime integration tests are removed. - adversarial-review: `antigravity` backend (stream-json over stdin, plan mode, refuses trees that carry agy workspace hooks or MCP config); the `gemini` backend is removed and a configured `gemini` reviewer gets a pointer to its replacement. - spec-loop: `antigravity` runner profile replaces `gemini`. - setup skills, setup-preflight, evals, docs and specs: Antigravity replaces Gemini CLI; new docs/adapters/antigravity.md and an Antigravity install section; .gemini/, GEMINI.md, gemini-extension.json and docs/adapters/gemini.md are removed. Generated-by: Claude Opus 5 * fix(antigravity): require absolute paths and a confined reviewer mode - sandbox-lint: an installed Antigravity profile with `~/` path rules now fails. Whether agy expands `~` is unverified, so such a rule may enforce nothing; only the shipped template may keep the `~/` form. - adversarial-review: agy takes its permission mode only from the user's settings, so the backend reads it from the `init` event and discards a review run under any mode other than request-review, strict or proceed-in-sandbox. - docs: record that whether the workspace-hook write deny also binds sandboxed shell writes is unverified. Generated-by: Claude Opus 5 * chore: bump dev version to 0.9.0.dev202610270025 (#1846) Move the dev stamp so adopters' `claude plugin update` picks up the work merged since the previous stamp. The marketplace is served from `main` and the update check compares version strings, so a frozen suffix is a silent no-op: an adopter running that command is told they are already current while sitting behind. Run from the Actions tab by @potiuk. Generated by .github/workflows/bump-dev-version.yml with the documented steps: bump `project.version` in `pyproject.toml`, then `tools/dev/check-family-plugins.py --fix` and `uv lock` to propagate to every manifest. The stamp is UTC at minute resolution. Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Jarek Potiuk <595491+potiuk@users.noreply.github.com> * feat(harness): support Pi; fix OpenCode install docs (#1847) * WIP: Pi harness (temporary, to be re-committed) Generated-by: Claude Opus 5 * WIP: Pi policy hardening (plain-word unsandboxed rule, symlinks, directory tools) Generated-by: Claude Opus 5 * WIP: Pi integrations (guard tests, spec-loop, adversarial-review, setup, docs, evals; OpenCode docs fix) Generated-by: Claude Opus 5 * build(pi): commit the Pi package lockfile Pi installs a git package's dependencies with `npm install --omit=dev --legacy-peer-deps` in its clone, which honours a committed package-lock.json and does not pass --ignore-scripts. Committing the lockfile pins the sandbox runtime's whole tree to exact versions and integrity hashes, so every user gets the reviewed packages instead of whatever resolves on the day. The lockfile was generated with the same flags plus `--package-lock-only --ignore-scripts` and the repository's 7-day cooldown (`--before`); no package in it has an install script. A Dependabot npm entry keeps it current with the same cooldown. Generated-by: Claude Opus 5 * feat(pi): confirm native writes outside the working folder A native write or edit whose real path, symlinks resolved, falls outside the working folder now needs the user's confirmation, and is refused in a non-interactive run, matching the Antigravity profile's allowNonWorkspaceAccess: false. Generated-by: Claude Opus 5 * fix(pi): follow dangling links and pin the approved write target A write through a dangling symlink is now checked at the link's target, the confirmation shows the resolved target with control characters stripped, and a target that changes while the prompt is open is refused. The node test wrapper's skips are explicit returns (CodeQL py/mixed-returns). Generated-by: Claude Opus 5 * chore(deps): bump the python-deps group across 1 directory with 3 updates (#1850) Bumps the python-deps group with 3 updates in the / directory: [prek](https://github.com/j178/prek), [zizmor](https://github.com/zizmorcore/zizmor) and [ruff](https://github.com/astral-sh/ruff). Updates `prek` from 0.5.5 to 0.5.6 - [Release notes](https://github.com/j178/prek/releases) - [Changelog](https://github.com/j178/prek/blob/main/CHANGELOG.md) - [Commits](https://github.com/j178/prek/compare/v0.5.5...v0.5.6) Updates `zizmor` from 1.30.1 to 1.31.1 - [Release notes](https://github.com/zizmorcore/zizmor/releases) - [Changelog](https://github.com/zizmorcore/zizmor/blob/main/docs/release-notes.md) - [Commits](https://github.com/zizmorcore/zizmor/compare/v1.30.1...v1.31.1) Updates `ruff` from 0.17.0 to 0.17.1 - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.17.0...0.17.1) --- updated-dependencies: - dependency-name: prek dependency-version: 0.5.6 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: python-deps - dependency-name: zizmor dependency-version: 1.31.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: python-deps - dependency-name: ruff dependency-version: 0.17.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: python-deps ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump the github-actions group with 3 updates (#1849) Bumps the github-actions group with 3 updates: [github/codeql-action/init](https://github.com/github/codeql-action), [github/codeql-action/analyze](https://github.com/github/codeql-action) and [actions/setup-java](https://github.com/actions/setup-java). Updates `github/codeql-action/init` from 4.38.3 to 4.39.0 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/24c54180a607b1449ed407dd24f251e4e9147c8d...6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee) Updates `github/codeql-action/analyze` from 4.38.3 to 4.39.0 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/24c54180a607b1449ed407dd24f251e4e9147c8d...6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee) Updates `actions/setup-java` from 6.0.1 to 6.1.0 - [Release notes](https://github.com/actions/setup-java/releases) - [Commits](https://github.com/actions/setup-java/compare/de7274f081f381c8f8158605e0321c36c376e2e6...b843ebf9b913146b38da2eaa7aeddf76074ae1ad) --- updated-dependencies: - dependency-name: github/codeql-action/init dependency-version: 4.39.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions - dependency-name: github/codeql-action/analyze dependency-version: 4.39.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions - dependency-name: actions/setup-java dependency-version: 6.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump @anthropic-ai/sandbox-runtime (#1848) Bumps the pi-package group with 1 update: [@anthropic-ai/sandbox-runtime](https://github.com/anthropics/sandbox-runtime). Updates `@anthropic-ai/sandbox-runtime` from 0.0.80 to 0.0.81 - [Release notes](https://github.com/anthropics/sandbox-runtime/releases) - [Commits](https://github.com/anthropics/sandbox-runtime/compare/v0.0.80...v0.0.81) --- updated-dependencies: - dependency-name: "@anthropic-ai/sandbox-runtime" dependency-version: 0.0.81 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: pi-package ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * feat(harness): Goose plugin hook and whole-process sandbox (#1851) * feat(goose): plugin hook and whole-process sandbox (WIP) Generated-by: Claude Opus 5 * fix(goose): Goose-only skill manifest, file URLs, read-only config Generated-by: Claude Opus 5 * fix(goose): no URL exemption on writes; on-disk letter case for path rules Generated-by: Claude Opus 5 * fix(sandbox): add resolved paths to the sandbox rules (macOS /tmp) Generated-by: Claude Opus 5 * fix(sandbox): resolve only macOS system links in allow lists Generated-by: Claude Opus 5 * feat(goose): integrations, docs, evals; ignore non-Goose hook payloads Generated-by: Claude Opus 5 * fix(goose): fail closed on unexpected hook payloads; reviewer runs outside the tree Generated-by: Claude Opus 5 * fix(hook-manager): address review on #1838 * fix(adversarial-review): keep the end of a reviewer's raw output and its stderr (#1854) The report kept the first 2000 characters of a reviewer's output. A streamed reply ends with the event that says how the run finished, so the part that explains a failure was the part cut off, and stderr from a reviewer that exited 0 without an answer was not kept at all. Keep the first 500 and the last 1500 characters, mark the cut, and add stderr to a malformed reply. Generated-by: Claude Opus 5 * fix(adversarial-review): say why an Antigravity review came back empty (#1852) Headless agy cannot ask for approval, and the first tool it denies ends the turn with an empty response, still marked SUCCESS. A reviewer that reached for a shell command lost its whole review, and the report said only "malformed output". Ask agy to stick to file reads and searches, name the denied tool when it ends without an answer, and correct the docs that called the denial a soft one. Generated-by: Claude Opus 5 * fix(hook-manager): address review 2 on #1838 * fix(code-review): give inline comments the finding's text, not just its rule (#1853) render built each inline comment from `comment`, `explanation` or `rule_id`. Findings in the shape review-flow.md documents carry `quoted_rule` and `suggestion` instead, so every inline comment was posted as the bare rule label and told the contributor nothing. An inline comment now carries the finding as the review body does, minus the excerpt it sits on, and a folded finding falls back to its suggestion before its rule. Generated-by: Claude Opus 5 * Restructure the threat model project-wide and add SECURITY.md (#1859) Rebuild docs/security/threat-model.md on the Alpha-Omega threat-model rubric (sections 1.1-1.19) and widen it from the security skill family to the whole project: enforcement layer, ingest adapters, distribution and configuration. The previous model is carried over unchanged as Appendix B, with a back-map from its sections to the new claims. Add SECURITY.md (report privately to security@apache.org, link the threat model) and point AGENTS.md and the project manifest at it. Generated-by: Claude Opus 5 * security: one reply, one thread -- never combine replies across threads (#1858) Make it a golden rule that every drafted email answers exactly one inbound thread. Combining the responses to several threads into one message -- the consolidated receipt in security-issue-import, or one foundation-security note listing several dashboard cases -- confuses reporters, who cannot tell which report the reply answers, and the security team, who track each case by its thread and Message-ID. - AGENTS.md and tools/gmail/threading.md carry the rule; issue-import and issue-sync add it as a golden rule; bulk sync, bulk invalidate, deduplicate and cve-allocate point at it. - issue-import drops the cross-thread consolidated receipt; one thread split into several trackers still gets one reply on that thread. - issue-sync's dashboard drafts one asf-security note per case. - security-management wording follows; evals flip the same-reporter case to separate receipts and add a one-thread-split case. Generated-by: Claude Opus 5 * agent-guard: see through more wrappers; read MAGPIE_GUARD_OFF from the hook env (#1860) The guards now unwrap exec, nohup and time as well as env and command, and check a shell's -c string (bash/sh/zsh) as a command line of its own, one level deep. A command line whose quoting cannot be parsed is refused when it names git or gh, instead of being allowed unchecked. MAGPIE_GUARD_OFF is read from the hook's own environment only, so the switch is set by whoever launches the agent rather than written into a command. Per-guard MAGPIE_ALLOW_* overrides work as before, inline or from the hook environment. Generated-by: Claude Opus 5 * container-gateway: refuse writable binds over agent and git config (#1861) * container-gateway: refuse writable binds over agent and git config A container may no longer get a writable bind whose source is, contains or sits under the paths the agent harness and git read their configuration from: .claude/settings*.json, .claude/hooks, .claude/skills, .claude/commands, .claude/agents, .mcp.json, .git, the common git directory's hooks and config, a linked worktree's git directory, the run directory and the personal config layer. The same binds read-only are still allowed, as is a writable bind of a subdirectory that covers none of them. Only an exact `ro` makes a bind read-only, and podman's `U` beside it counts as writable. Generated-by: Claude Opus 5 * container-gateway: refuse overlay mount options Generated-by: Claude Opus 5 * container-gateway: protect agent config inside nested worktrees Generated-by: Claude Opus 5 * Tighten adapter defaults: TLS, collaborator rollups, HTTPS-only bridge (#1862) * Tighten adapter defaults: TLS, collaborator rollups, HTTPS-only bridge - imap: connect with a verified TLS context (ssl.create_default_context()). - github-rollup and vetted-ops: a status-rollup comment is read only when its author is the authenticated gh user or a repository collaborator. - jira bridge.groovy: refuse non-HTTPS base URLs and do not follow redirects. - sourcehut: comment bodies go through --body-file only; graphql mutations need --allow-mutation; the service must be a bare subdomain. - jira-patch docs: the reviewed attachment is pinned by id and sha256 before it is applied. Generated-by: Claude Opus 5 * Stop on an unverifiable rollup author; end GraphQL comments at CR Generated-by: Claude Opus 5 * Security workflow: stricter relay detection and advisory URL checks (#1863) * Security workflow: stricter relay detection and advisory URL checks - Forwarder relay detection needs both the adapter's sender pattern and its preamble to match; a single match is surfaced to the user as "not a relay". Relay-derived reporter credit stays a proposal until a human confirms it. - New `security-management issue-sync advisory-archive` check: a Public advisory URL is accepted only for a thread on the advisory list whose root subject is `<CVE-ID>: <description>`, under the configured archive URL, and always with confirmation. - PonyMail: an empty result for a private list reads as "unknown" unless the list is visible to the session. - Docs: egress-gateway, cve-org, forgejo and mailman3 curl notes, and macOS egress in secure-agent-internals. Generated-by: Claude Opus 5 * advisory-archive: require an authorized sender and a configured archive Generated-by: Claude Opus 5 * advisory-archive: parse the sender as a mail header Generated-by: Claude Opus 5 * issue-sync: say the advisory sender is unverified when asking to confirm Generated-by: Claude Opus 5 * agent-guard: a continued heredoc line is not stripped (#1864) Generated-by: Claude Opus 5 * setup: import the pre-flight checker only from outside the working tree (#1866) * setup: import the pre-flight checker only from outside the working tree Pre-flight now runs `python3 -P -m setup_preflight` with PYTHONPATH set to the git directory's apache-magpie/ (or .svn/apache-magpie/) only. `-P` keeps the working tree off the import path, and no in-tree directory is on it, so a repository cannot ship the module pre-flight imports. `config` installs the checker into that checker_dir (new `checker_home()` in layers.py); the regenerated block is in every skill. The checker refuses a Python older than 3.11 before importing anything (exit 3, which the block reads as "did not run"), and the test session refuses to start on one. For a git-tag install, `upgrade`, `verify` and `install` also compare the committed lock's commit anchor with the fetched commit and with the tag resolved upstream. A tag that moved stops the upgrade; re-pinning to it needs `bump-committed` and an explicit confirmation. Generated-by: Claude Opus 5 * setup: trust .svn only in a real working copy; check an existing snapshot's commit Generated-by: Claude Opus 5 * agent-guard: strip heredoc bodies only where bash treats them as data (#1867) * agent-guard: open a heredoc only on an unquoted, uncommented << When a command line cannot be tokenised, the guard strips heredoc bodies before parsing again. It found the `<<WORD` operators with a regex over the raw line, so a `<<EOF` inside quotes or a `#` comment counted as a heredoc, and the lines after it were dropped as its body -- while bash runs them as commands: cat /dev/null # <<EOF it's git push --force EOF The operators are now found by a scan that tracks quotes (including `$'...'` and quotes left open on an earlier line) and stops at a comment. A body naming git / gh under an unquoted delimiter is also no longer stripped when it holds a `$(...)` or backtick substitution, which bash expands even in a heredoc fed to cat or tee. Generated-by: Claude Opus 5 * agent-guard: read heredoc delimiters and shifts as bash does Two more places where the heredoc stripper and bash disagreed: - `<<` inside arithmetic or a parameter expansion (`$[1<<x]`, `${a[1<<x]}`) is a shift, not a heredoc, yet its "body" was stripped. Such a line now leaves the command unstripped. - The delimiter was matched as `[A-Za-z_][\w.-]*` with optional surrounding quotes, so `E-O:F`, `'E'OF` or `E"O"F` closed the body on a different line than bash does. The delimiter is now the whole word up to the next metacharacter, after quote removal, and quoted when any quote or backslash appears in it; a word with `$` or a backtick is not stripped. A body naming git / gh is also stripped only when the introducing line is a bare `cat` / `tee` with plain file arguments and `>` / `>>` redirections. Generated-by: Claude Opus 5 * agent-guard: strip a heredoc body naming git / gh only from a bare cat Rather than chasing each shape in which a heredoc fed to cat / tee is not really data, strip a body naming git or gh only in the narrowest safe shape, and keep the command whole otherwise: - the introducing line starts a command (the line before does not end in `\`, `|`, `&&` or `||`); - its first word is exactly `cat`, `tee`, `/bin/cat` or `/usr/bin/cat`; - the command has no `alias`, no `cat` / `tee` function, and no `$((`, `((`, `$[` or `${` anywhere. Generated-by: Claude Opus 5 * agent-guard: keep a heredoc whole when its output may be run, and see into eval A body naming git / gh fed to a bare cat was still stripped when the cat output itself was run: `(cat <<EOF ... ) | bash`, `{ ...; } | bash`. Instead of matching each wrapper, a body naming git or gh is now stripped only when the command, with its heredoc bodies removed, holds no `$`, backtick, parenthesis or brace, no `alias`, and no `eval`, `source` or `.` in command position. This subsumes the earlier function and arithmetic checks. `eval` hid what it ran from every guard, heredoc or not. Its arguments, joined as bash joins them, are now split and checked like a shell -c string, one level deep. Generated-by: Claude Opus 5 * agent-guard: every heredoc a sink before stripping; harden eval unwrapping A heredoc fed to something other than cat / tee had its body stripped too, so `bash <<A` with `cat(){ bash; }` in its body redefined cat out of sight before a later stripped `cat <<EOF` body naming git ran. When any stripped body names git or gh, every heredoc in the command must now be a qualifying sink, or the command is kept whole. The eval / source / . check also matches them behind `builtin`, `command` or inline assignments (`X=1 eval`). eval unwrapping drops one leading `--` (`eval -- 'git push'`) and treats `builtin eval` like `eval`; an eval whose first argument is any other option is marked unparsed and refused. Generated-by: Claude Opus 5 * agent-guard: see eval through prefix chains; keep heredocs next to eval-like words eval unwrapping now strips any chain of `builtin`, `command`, `--` and `-p` in front of eval (`builtin command -- eval ...`). Any other option in that chain, or after eval, marks the segment unparsed so it is refused. A stripped heredoc body naming git or gh is now kept whenever `eval`, `source`, `.`, `hash` or `enable` appears as a word anywhere in the rest of the command after quote and backslash removal (`xargs e'v'al < s`, `env . s`, `hash -p /bin/bash cat`), not only in command position. The README notes the limit of a command-string guard: a cat heredoc that writes a script a later command runs is out of its reach, since the agent could write the same file with its file tools. Generated-by: Claude Opus 5 * skill-evals(setup): bring seven drifted steps in line with the skill (#1868) Twelve setup cases failed on main because the fixtures or output specs no longer matched what the skill says, not because the skill was wrong: - step-delegate-propose: name skills by their `name`, not their path. - config-local-custom-parser: the parser is confirmed on the user's terminal via trust-parser, with the upstream offer in the same reply; cases 1 and 4 now follow that flow, and "this run" is defined. - step-m3-baseline-pick: security brings magpie-security-tools. - step-m4-install-gates: the hand-off is /magpie-setup:setup adopt; a bare marketplace add has no scope. - step-override-bypass: the reason names the overrides as skipped or applied; "framework defaults" counts as skipped. - uninstall-default-set: enabledPlugins is edited, never preserved. - upgrade-adoption-split: updates_plugins means a version changed. Generated-by: Claude Opus 5 * agent-guard: fail closed on untokenisable lines that can spell git or gh (#1865) * agent-guard: see git / gh through quoting on an untokenisable line A command line shlex cannot tokenise is refused when it names git or gh as a word, and allowed otherwise. The check ran a plain word regex over the raw text, so quoting inside the word hid it, although bash removes the quotes and runs the command: g'i't push --force # it's Quotes and backslashes are now removed before the check, at all three places it runs: the untokenisable line, a stripped heredoc body, and a nested shell -c string. Generated-by: Claude Opus 5 * agent-guard: fail closed on an untokenisable line with an expansion Removing quotes and backslashes still missed words bash builds from an expansion that may be empty or a line continuation: `g$()it`, `g${x:-}it`, `g``it`, `g$@it`, `$CMD`, `g\<newline>it`. An untokenisable line holding `$`, a backtick or a backslash-newline is now refused outright, and the quote removal also drops `$` before a quote (`$'g'h`) and backslash-newline. Generated-by: Claude Opus 5 * agent-guard: fail closed on globs and braces in an untokenisable line `/usr/bin/gi?`, `/usr/bin/g*t`, `/usr/bin/g[i]t` and `/usr/bin/g{i,}t` all resolve to git. An untokenisable line holding `*`, `?`, `[` or `{` is now refused alongside the expansions and line continuations. Generated-by: Claude Opus 5 * setup-preflight: restamp the isolated-setup fingerprint after rebase Generated-by: Claude Opus 5 * adversarial-review: keep agy from handing the review to a subagent (#1871) Asked to list or search a tree, headless agy starts a research subagent and its turn ends at once, SUCCESS, with "waiting for the subagent" as the answer or none at all. Every Antigravity review on a real diff came back empty this way. Told only "file reads", it instead reads directories file by file and runs out of budget. The prompt note now names the tools that work (list_dir, find_by_name, grep_search, view_file / read_file) and forbids subagents, and the output check reports a subagent handoff by name instead of "no response text". Generated-by: Claude Opus 5 * skill-evals(security-issue-sync): follow the dashboard steps as they now read (#1872) step-d0: a proceeding mode asks for a session only after the bridge reports one missing. step-d4: since one reply, one thread, each case with an asf-security finding gets its own note. Generated-by: Claude Opus 5 * setup: import the pre-flight checker from a per-user directory, never from the project (#1870) #1866 moved the checker out of the working tree into the project's VCS directory (<git-common-dir>/apache-magpie/ or .svn/apache-magpie/), on the reasoning that no commit can carry it. That holds for a git commit, but not for a Subversion checkout, which versions a .git/ like any other directory, nor for a tarball, which can carry a .git/ or a .svn/ with its wc.db. `git rev-parse` and layers.py accept such a directory as real, so opening that tree put its planted setup_preflight/ on the import path of every skill's pre-flight. The checker now lives in one per-user directory, ~/.cache/apache-magpie/preflight/, and the block runs PYTHONPATH="$HOME/.cache/apache-magpie/preflight" python3 -P -m setup_preflight. Nothing inside the opened project is ever on the import path, and a project without version control now gets the checker too. The personal configuration layer is unchanged. `upgrade` moves a copy an earlier version left in the project's .git/ or .svn/ as well. Generated-by: Claude Opus 5 * agent-guard: keep a command whole when any line is continued (#1869) Heredoc stripping refused a continued line only when that line opened a heredoc (#1864). Any other line ending in a backslash was read on its own, but bash joins it with the next line: a `#` at the start of that line then sits mid-word instead of opening a comment. That let a guarded command through. In echo x\ #' cat <<EOF ' git push --force EOF bash reads the word `x#'`, whose quote swallows `cat <<EOF` and closes on the `'` line, and then runs `git push --force`. The guard took `#'` for a comment and stripped the `git` line as the body of a `cat` heredoc. Heredoc stripping now keeps the command whole whenever any line outside a heredoc body ends in a backslash, so such a line is refused as unparsable when it names git or gh. A backslash inside a body is still data. Generated-by: Claude Opus 5 * agent-guard: split a command line into commands the way bash does (#1873) * agent-guard: split a command line into commands the way bash does split_segments tokenised with shlex.split, which reads a newline as plain whitespace and only splits on an operator surrounded by spaces. So `echo hi` + newline + `git push`, or `echo hi;git push`, came out as one `echo` segment and no guard saw the git command. Subshells, groups, reserved words and command substitutions hid commands the same way. A small lexer now reads the line with bash's rules: a newline or any control operator ends a command, spaced or not; redirections (2>&1, &>, >|) stay words; `#` comments and quoted text are not commands; a leading reserved word is skipped; subshells, command and process substitutions (also inside double quotes, arithmetic and ${...}) are read as commands of their own. A heredoc body stays data on its command (and in its raw text for the trailer scan), except that its substitutions are read when the delimiter is unquoted, and a body naming git / gh that is fed to anything but a bare cat / tee is read as commands. Generated-by: Claude Opus 5 * agent-guard: close four gaps in the command-line lexer - A `case` word starting a command inside `$( … )` or a subshell makes the line untokenisable: its pattern's `)` would close the construct early, and inside double quotes that hid the rest of the line. - A redirection keeps `&` / `|` only after an unquoted, unescaped `>` / `<`; `echo '>'&git …` splits as bash splits it. - A standalone `{` or `}` ends the command before it, so `function f { git …; }` reaches the guards. - `$'…'` strings decode bash's escapes (`\xHH`, octal, `\uHHHH`, …); `\c` makes the line untokenisable. Generated-by: Claude Opus 5 * agent-guard: keep braces, NULs and `case` from moving command boundaries - `{` opens a group only in command position (or after `function name`) and `}` closes one only right after a control operator or newline; anywhere else a brace is an ordinary word, so `git commit -m x } -m …` keeps its second message on the commit. - A `$'…'` escape that decodes to NUL makes the line untokenisable: bash truncates the word there. - Any unquoted `case` word inside a substitution or subshell makes the line untokenisable, whatever precedes it (`time`, `time -p`, …). - #1865's untokenisable-spelling tests end in an unbalanced quote instead of a `# it's` comment, which the lexer now reads as a comment. Generated-by: Claude Opus 5 * agent-guard: a group behind time or coproc is a group `time { git push; }`, `time -p { … }` and `coproc [NAME] { … }` open a brace group, since `time` (with `-p` / `--`) and `coproc` keep command position. The lexer read the `{` there as a plain word, so the commands inside the group were never checked. Generated-by: Claude Opus 5 * agent-guard: check the command a literal name and leading redirections leave (#1874) * agent-guard: refuse a command whose name is an expansion or a glob A command line that tokenises but spells its command name through an expansion or a glob (`$CMD push`, `"$GIT" push`, `$(echo git) push`, `/usr/bin/gi? push`, `/usr/bin/g{i,}t push`) ran unchecked: the guards cannot see which command it resolves to. #1865 refused such spellings only on a line that could not be tokenised. Now a command whose name, after wrappers are unwrapped, holds `$`, a backtick or a glob or brace character is refused (`agent-guard[expansion]`); `[` and `[[` are exempt, and the vetted-ops and excluded-script spelling guards keep their more specific message. Generated-by: Claude Opus 5 * agent-guard: accept only a literal command name; skip leading redirections The command-name check refused a denylist of expansion and glob characters, which missed zsh's `=git` (expands to git's path) and `g(i)t` glob group. A command name must now be literal: letters, digits, `_ . / + : @ % , -` and an optional leading `~/` (`[` and `[[` aside). A `(` glued to a word other than `name()` or `arr=(…)` makes the line untokenisable, since bash rejects it and zsh reads it as a glob. Redirections written before the command name (`>/dev/null git …`, `2>&1 git …`, `nohup >log env git …`) were read as the command name, so the git command after them was never checked. They are now skipped like leading `NAME=value` assignments, also between wrappers, so the command after them is the one checked; `exec 3>&1` stays allowed. Generated-by: Claude Opus 5 * fix(security-issue-deduplicate): keep the CVE both trackers share (#1857) When the kept and the dropped tracker carry the same CVE ID, classify returned that ID as `retract`, so the proposal offered item 7 as "retract CVE-X, merged into CVE-X": a retraction of the record the kept tracker still needs. classify now proposes a retraction only when the two IDs differ, and apply-plan refuses item 7 when the ID to retract is the kept CVE. The skill and the lifecycle spec say the same: a retraction needs two distinct IDs. Generated-by: Claude Code (Opus 5.5) Signed-off-by: Davide Polato <dpol1@apache.org> * setup: trust a VCS directory as the personal layer only when it is unambiguous and ours (#1875) An unadopted project keeps personal configuration in its VCS directory, `<git-common-dir>/apache-magpie/` or `<.svn>/apache-magpie/`. That directory is chosen from what the opened tree carries, and Subversion versions a `.git/` like any other directory while git versions a `.svn/`, so a crafted tree could hide configuration where nobody looks. Every copy of `unadopted_home` now uses the VCS directory only when the current user owns it (git's own `safe.directory` rule) and it is the only one at the root. With both a `.git` and a Subversion working copy there, or a VCS directory another user owns, the home is the in-tree `.apache-magpie-local/`, in plain sight. This replaces the old rule that git wins when both are present. agent-guard runs its own git commands with `core.fsmonitor=false` and an empty `core.hooksPath`, so a crafted `.git/config` names no program the hook runs. The commands it runs today (`rev-parse`, `merge-base`, `rev-list`) do not refresh the index, so this is defence in depth; the pre-flight already reads `.git` in pure Python and never spawns git in the project. Generated-by: Claude Opus 5 * security-cve-allocate: one CVE-allocated draft per reporter, each on its own thread (#1876) A tracker that merged several independent reporters (security-issue- deduplicate keeps each one's thread line) only drafted the "CVE allocated" email to the primary thread, so the other reporters were never told. Under the one-reply-one-thread rule each of them is owed a draft of their own, never one email to all. - draft-plan lists the further reporters' threads in `reporter_threads` when it plans the primary; each is planned by its own run with `thread_id` set and that reporter's data. A relay line is no further reporter; a thread the field does not record is blocked. - proposal, the rollup entry and the recap take one plan per reporter (`drafts`, `notifications`): item 5 creates one draft per reporter thread, and the rollup carries one notification line per reporter, named by its thread. - tracker-updates.md, reporter-draft.md and SKILL.md say so; a new eval case covers the second reporter's draft, which names no other reporter. Generated-by: Claude Opus 5 * test(security): use claude-code/testing tool noun in public-surface-scrub tests (#1855) (#1856) * test(security): use claude-code/testing tool noun in public-surface-scrub tests (#1855) * fix(code-review): give inline comments the finding's text, not just its rule (#1853) render built each inline comment from `comment`, `explanation` or `rule_id`. Findings in the shape review-flow.md documents carry `quoted_rule` and `suggestion` instead, so every inline comment was posted as the bare rule label and told the contributor nothing. An inline comment now carries the finding as the review body does, minus the excerpt it sits on, and a folded finding falls back to its suggestion before its rule. Generated-by: Claude Opus 5 * Restructure the threat model project-wide and add SECURITY.md (#1859) Rebuild docs/security/threat-model.md on the Alpha-Omega threat-model rubric (sections 1.1-1.19) and widen it from the security skill family to the whole project: enforcement layer, ingest adapters, distribution and configuration. The previous model is carried over unchanged as Appendix B, with a back-map from its sections to the new claims. Add SECURITY.md (report privately to security@apache.org, link the threat model) and point AGENTS.md and the project manifest at it. Generated-by: Claude Opus 5 * security: one reply, one thread -- never combine replies across threads (#1858) Make it a golden rule that every drafted email answers exactly one inbound thread. Combining the responses to several threads into one message -- the consolidated receipt in security-issue-import, or one foundation-security note listing several dashboard cases -- confuses reporters, who cannot tell which report the reply answers, and the security team, who track each case by its thread and Message-ID. - AGENTS.md and tools/gmail/threading.md carry the rule; issue-import and issue-sync add it as a golden rule; bulk sync, bulk invalidate, deduplicate and cve-allocate point at it. - issue-import drops the cross-thread consolidated receipt; one thread split into several trackers still gets one reply on that thread. - issue-sync's dashboard drafts one asf-security note per case. - security-management wording follows; evals flip the same-reporter case to separate receipts and add a one-thread-split case. Generated-by: Claude Opus 5 * agent-guard: see through more wrappers; read MAGPIE_GUARD_OFF from the hook env (#1860) The guards now unwrap exec, nohup and time as well as env and command, and check a shell's -c string (bash/sh/zsh) as a command line of its own, one level deep. A command line whose quoting cannot be parsed is refused when it names git or gh, instead of being allowed unchecked. MAGPIE_GUARD_OFF is read from the hook's own environment only, so the switch is set by whoever launches the agent rather than written into a command. Per-guard MAGPIE_ALLOW_* overrides work as before, inline or from the hook environment. Generated-by: Claude Opus 5 * container-gateway: refuse writable binds over agent and git config (#1861) * container-gateway: refuse writable binds over agent and git config A container may no longer get a writable bind whose source is, contains or sits under the paths the agent harness and git read their configuration from: .claude/settings*.json, .claude/hooks, .claude/skills, .claude/commands, .claude/agents, .mcp.json, .git, the common git directory's hooks and config, a linked worktree's git directory, the run directory and the personal config layer. The same binds read-only are still allowed, as is a writable bind of a subdirectory that covers none of them. Only an exact `ro` makes a bind read-only, and podman's `U` beside it counts as writable. Generated-by: Claude Opus 5 * container-gateway: refuse overlay mount options Generated-by: Claude Opus 5 * container-gateway: protect agent config inside nested worktrees Generated-by: Claude Opus 5 * Tighten adapter defaults: TLS, collaborator rollups, HTTPS-only bridge (#1862) * Tighten adapter defaults: TLS, collaborator rollups, HTTPS-only bridge - imap: connect with a verified TLS context (ssl.create_default_context()). - github-rollup and vetted-ops: a status-rollup comment is read only when its author is the authenticated gh user or a repository collaborator. - jira bridge.groovy: refuse non-HTTPS base URLs and do not follow redirects. - sourcehut: comment bodies go through --body-file only; graphql mutations need --allow-mutation; the service must be a bare subdomain. - jira-patch docs: the reviewed attachment is pinned by id and sha256 before it is applied. Generated-by: Claude Opus 5 * Stop on an unverifiable rollup author; end GraphQL comments at CR Generated-by: Claude Opus 5 * Security workflow: stricter relay detection and advisory URL checks (#1863) * Security workflow: stricter relay detection and advisory URL checks - Forwarder relay detection needs both the adapter's sender pattern and its preamble to match; a single match is surfaced to the user as "not a relay". Relay-derived reporter credit stays a proposal until a human confirms it. - New `security-management issue-sync advisory-archive` check: a Public advisory URL is accepted only for a thread on the advisory list whose root subject is `<CVE-ID>: <description>`, under the configured archive URL, and always with confirmation. - PonyMail: an empty result for a private list reads as "unknown" unless the list is visible to the session. - Docs: egress-gateway, cve-org, forgejo and mailman3 curl notes, and macOS egress in secure-agent-internals. Generated-by: Claude Opus 5 * advisory-archive: require an authorized sender and a configured archive Generated-by: Claude Opus 5 * advisory-archive: parse the sender as a mail header Generated-by: Claude Opus 5 * issue-sync: say the advisory sender is unverified when asking to confirm Generated-by: Claude Opus 5 * agent-guard: a continued heredoc line is not stripped (#1864) Generated-by: Claude Opus 5 * setup: import the pre-flight checker only from outside the working tree (#1866) * setup: import the pre-flight checker only from outside the working tree Pre-flight now runs `python3 -P -m setup_preflight` with PYTHONPATH set to the git directory's apache-magpie/ (or .svn/apache-magpie/) only. `-P` keeps the working tree off the import path, and no in-tree directory is on it, so a repository cannot ship the module pre-flight imports. `config` installs the checker into that checker_dir (new `checker_home()` in layers.py); the regenerated block is in every skill. The checker refuses a Python older than 3.11 before importing anything (exit 3, which the block reads as "did not run"), and the test session refuses to start on one. For a git-tag install, `upgrade`, `verify` and `install` also compare the committed lock's commit anchor with the fetched commit and with the tag resolved upstream. A tag that moved stops the upgrade; re-pinning to it needs `bump-committed` and an explicit confirmation. Generated-by: Claude Opus 5 * setup: trust .svn only in a real working copy; check an existing snapshot's commit Generated-by: Claude Opus 5 * agent-guard: strip heredoc bodies only where bash treats them as data (#1867) * agent-guard: open a heredoc only on an unquoted, uncommented << When a command line cannot be tokenised, the guard strips heredoc bodies before parsing again. It found the `<<WORD` operators with a regex over the raw line, so a `<<EOF` inside quotes or a `#` comment counted as a heredoc, and the lines after it were dropped as its body -- while bash runs them as commands: cat /dev/null # <<EOF it's git push --force EOF The operators are now found by a scan that tracks quotes (including `$'...'` and quotes left open on an earlier line) and stops at a comment. A body naming git / gh under an unquoted delimiter is also no longer stripped when it holds a `$(...)` or backtick substitution, which bash expands even in a heredoc fed to cat or tee. Generated-by: Claude Opus 5 * agent-guard: read heredoc delimiters and shifts as bash does Two more places where the heredoc stripper and bash disagreed: - `<<` inside arithmetic or a parameter expansion (`$[1<<x]`, `${a[1<<x]}`) is a shift, not a heredoc, yet its "body" was stripped. Such a line now leaves the command unstripped. - The delimiter was matched as `[A-Za-z_][\w.-]*` with optional surrounding quotes, so `E-O:F`, `'E'OF` or `E"O"F` closed the body on a different line than bash does. The delimiter is now the whole word up to the next metacharacter, after quote removal, and quoted when any quote or backslash appears in it; a word with `$` or a backtick is not stripped. A body naming git / gh is also stripped only when the introducing line is a bare `cat` / `tee` with plain file arguments and `>` / `>>` redirections. Generated-by: Claude Opus 5 * agent-guard: strip a heredoc body naming git / gh only from a bare cat Rather than chasing each shape in which a heredoc fed to cat / tee is not really data, strip a body naming git or gh only in the narrowest safe shape, and keep the command whole otherwise: - the introducing line starts a command (the line before does not end in `\`, `|`, `&&` or `||`); - its first word is exactly `cat`, `tee`, `/bin/cat` or `/usr/bin/cat`; - the command has no `alias`, no `cat` / `tee` function, and no `$((`, `((`, `$[` or `${` anywhere. Generated-by: Claude Opus 5 * agent-guard: keep a heredoc whole when its output may be run, and see into eval A body naming git / gh fed to a bare cat was still stripped when the cat output itself was run: `(cat <<EOF ... ) | bash`, `{ ...; } | bash`. Instead of matching each wrapper, a body naming git or gh is now stripped only when the command, with its heredoc bodies removed, holds no `$`, backtick, parenthesis or brace, no `alias`, and no `eval`, `source` or `.` in command position. This subsumes the earlier function and arithmetic checks. `eval` hid what it ran from every guard, heredoc or not. Its arguments, joined as bash joins them, are now split and checked like a shell -c string, one level deep. Gen…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
classifyreturned that ID asretract.The proposal then offered item 7 as
retract(CVE-X, "merged into CVE-X …"): a retraction of the record the kept tracker still needs.The governance gate still applied, but the step was presented as a normal part of the merge.
classifynow setsretractonly when the two IDs differ (issue_deduplicate/gate.py).apply-planrefuses item 7 when the ID to retract equals the kept CVE (issue_deduplicate/apply.py), since it takesretract_cvefrom what the agent passes it.Type of change
.claude/skills/<name>/) — eval fixtures updated belowtools/<system>/*.md)tools/*/withpyproject.toml)docs/,README.md,CONTRIBUTING.md)projects/_template/)prek, workflows, validators)tools/spec-loop/specs/security-issue-lifecycle.mdTest plan
prek run --all-filespasses — run asAGENTS.mddescribes:prek run --all-files --skip workspace-pytest, thenworkspace-pyteston the changed files; both passuv run pytest/ruff check/mypypasses —tools/security-management: full suite,ruff check,ruff format --checkandmypy srccleansecurity-issue-deduplicatesuite extracts only Step 3 ofmerge-templates.md, which this PR does not touchgate.pyandapply.pyand is covered by pytest; theSKILL.mdedits align the wording (measured_tokensrestamped)tools/security-management/tests/test_issue_deduplicate.py, each failing before the fix.A new row in
test_classify_applies_the_cve_rules(same CVE on both sides →proceed,retractisNone) failed with'CVE-2025-1111' == None.test_apply_plan_refuses_to_retract_the_kept_cvefailed with exit 0 instead of 2.The existing two-CVE case still proposes retracting the dropped side's CVE.
RFC-AI-0004 compliance
Linked issues
Refs #179
Notes for reviewers (optional)
security-issue-deduplicate gives no guidance when both trackers already have a CVE allocated #179 covers two distinct CVEs on the two trackers; the skill now has a rule for that case (keep the record further along, retract the other), coded in
gate.pyby perf(security-cve-allocate): decide and render the allocation rules as code #1705.This PR covers the case where both trackers carry the same ID.
Before / after, synthetic trackers 42 and 67 both linking
CVE-2025-1111(allocated):verdictretractproceedCVE-2025-1111retract(CVE-2025-1111, "merged into CVE-2025-1111 …")proceednullapply-planrefuses it if passedA whole-tree run that included
workspace-pytestshowed one timeout on my machine, intools/setup-preflight/tests/test_session_hook.py::test_a_relative_project_dir_outside_git_does_not_hang.This PR does not touch
setup-preflight, and I did not investigate the cause.Generative AI disclosure: prepared with Claude Code (Opus 5.5); I reviewed and tested the change.