magpie-security-issue-import

SkillSecurity

Scan <security-list> for reports that have not yet been copied into <tracker> as tracking issues, present the proposed imports to the user, and — defaulting to *import unless the user rejects upfront* — create the tracking issues with the `Needs triage` project-board status and draft a receipt-of- confirmation reply to each reporter. This is the first step of the handling process: the entry point that converts an inbound email thread into a tracker the rest of the skills (security-issue-sync, security-issue-fix, generate-cve-json) operate on.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the magpie-security-issue-import skill

What this skill tells your AI

The instructions your AI receives, as published by apache/magpie in skills/security-issue-import/SKILL.md and read by ahel’s review.

security-issue-import

This skill is the on-ramp of the security-issue handling process. It converts an inbound <security-list> email thread into an <tracker> tracking issue that follows the repo's issue template, then drafts the receipt-of-confirmation reply to the reporter.

It never sends email. It never creates a tracker for a candidate the user has explicitly rejected. It never assumes a report is valid — the validity / invalid / CVE-worthy decision still happens later in the discussion on the created tracker (Step 3 of README.md).

Golden rule — propose, then default to import. Every import this skill performs is a proposal that lists the candidate emails, the extracted fields, and the draft confirmation reply. The user's default disposition for any Report or forwarder-relayed candidate (the latter classified by the optional security-issue-import-via-forwarder sub-skill when forwarders.enabled is non-empty) is "import as a new tracker landing in Needs triage"; the user only has to type back when they want to deviate from that default — skip NN to reject a candidate upfront with no reply, or NN:reject-with-canned <name> to reject upfront and draft a specific canned negative-assessment / out-of-scope reply. A bare all (or no reply at all to the proposal — the user typing "go", "proceed", "yes, all") means "import every non-rejected candidate as proposed". The skill must still surface each candidate one-by-one in the proposal so the user can scan and override if needed; what the skill must not do is sit on a report waiting for an explicit per-candidate green light. The bias is toward landing trackers — a wrongly-imported report is cheap to close at Step 5 / 6 of the handling process; a wrongly-skipped one gets buried in the inbox and the reporter is left without a disposition.

Golden rule — rejection means no tracker, ever. When the user rejects a candidate upfront — any of skip NN, NN:reject-with-canned <name>, an explicit "reject 1", "mark 1 invalid", "don't import 1", or a cancel / none / "hold off" on the whole proposal — the skill must not create a tracker for that candidate. This holds even when the user also asks for a canned reply to be drafted: the draft is a courtesy to the reporter, the absence of a tracker is the disposition. There is no "create the tracker so the team can close it as invalid later" path; if the team has decided pre-triage that the report is invalid, the audit trail lives on the Gmail thread and on the canned-responses.md precedent, not in a tracker that exists only to be closed. A tracker is created only when the candidate is imported as a real Report (or a forwarder-relayed candidate classified by the security-issue-import-via-forwarder sub-skill) for triage.

Non-import candidate classes (automated-scanner, consolidated-multi-issue, media-request, spam, cross-thread-followup, cve-tool-bookkeeping) keep the original "propose first, apply only on explicit confirm" rule — those never default to a tracker.

Golden rule — confidentiality. The inbound thread on <security-list> is private. The skill may paste the email body verbatim into the created <tracker> tracking issue (that repo is also private). It must never paste the report content into a public surface — not into <upstream>, not into a public GHSA, not into any comment on a public repo. The same confidentiality rule documented in the "Confidentiality of <tracker>" section of AGENTS.md applies in full.

Golden rule — every <tracker> / <upstream> reference is clickable in the surface it lands on. Whenever this skill emits a reference to a tracker issue, PR, or comment — the proposal shown to the user before import, the created tracker issue body (observed-state dump, sibling-tracker cross-links, prior-rejection cross-links, fix-already-public PR pointers), the receipt-of- confirmation draft email reply, the recap output — the reference must be one click away in whatever surface it lands on:

  • On markdown surfaces (the created tracker issue body, the draft email reply destined for the <security-list> thread, any markdown-rendered cross-link list): use the markdown link form per AGENTS.md § Linking tracker issues and PRs:

    • Sibling <tracker> issue: [<tracker>#NNN](https://github.com/<tracker>/issues/NNN)
    • Public <upstream> PR (e.g. fix-already-public match): [<upstream>#NNN](https://github.com/<upstream>/pull/NNN)
    • Comment: link to the #issuecomment-<C> anchor.
  • On terminal surfaces (the proposal shown to the user before import, the recap output): wrap the visible short form (<tracker>#NNN, <upstream>#NNN) in OSC 8 hyperlink escape sequences (\e]8;;<URL>\e\\<short>\e]8;;\e\\) so modern terminals (iTerm2, Kitty, GNOME Terminal, WezTerm, Windows Terminal, …) render the short text as clickable. Where OSC 8 is unsupported (CI logs, dumb terminals), fall back to printing the bare URL on the same line after the number.

Bare #NNN with no link wrapper of any kind is never acceptable. The created tracker issue is read by the security team who drill into the cross-links to assess; the draft email reply lands on <security-list> where the reporter needs the references to be one click away. Both surfaces are private, but <tracker> URLs themselves are public-safe per the Confidentiality of <tracker> rule — what stays private is the contents the link points at.

Self-check before posting any draft email or creating any tracker issue: grep the body for bare #\d+ / <tracker>#\d+ tokens that aren't already inside a markdown link or an OSC 8 wrapper, and convert any match.


Adopter overrides

Before running the default behaviour documented below, this skill consults .apache-magpie-local/security-issue-import.md (personal, gitignored) and .apache-magpie-overrides/security-issue-import.md (committed, project-wide) in the adopter repo if it exists, and applies any agent-readable overrides it finds. See docs/setup/agentic-overrides.md for the contract — what overrides may contain, hard rules, the reconciliation flow on framework upgrade, upstreaming guidance.

Hard rule: agents NEVER modify the snapshot under <adopter-repo>/.apache-magpie/. Local modifications go in the override file. Framework changes go via PR to apache/magpie.


Snapshot drift

Also at the top of every run, this skill compares the gitignored .apache-magpie.local.lock (per-machine fetch) against the committed .apache-magpie.lock (the project pin). On mismatch the skill surfaces the gap and proposes /magpie-setup upgrade. The proposal is non-blocking — the user may defer if they want to run with the local snapshot for now. See docs/setup/install-recipes.md § Subsequent runs and drift detection for the full flow.

Drift severity:

  • method or URL differ → ✗ full re-install needed.
  • ref differs (project bumped tag, or git-branch local is behind upstream tip) → ⚠ sync needed.
  • svn-zip SHA-512 mismatches the committed anchor → ✗ security-flagged; investigate before upgrading.

Prerequisites

Before running, the skill needs:

  • At least one configured mail-source backend per <project-config>/project.md → Mail sources. The skill treats every backend the same way — through the abstract operations defined in tools/mail-source/contract.md (list_recent_threads, read_thread, list_drafts, list_sent_since, create_draft, thread_url). Reference adapters: gmail (full read+write), ponymail (read-only ASF archive), imap (stub), mbox (read-only offline archive — stub). To discover new reports the configured backends must collectively cover list_recent_threads + read_thread; to draft the receipt-of-confirmation reply in Step 7 they must additionally cover create_draft. If no available backend covers create_draft, Step 7 surfaces a one-line "no draft backend available" note and the user composes the reply by hand.
  • gh CLI authenticated (gh auth status returns OK) with collaborator access to <tracker>. The skill calls gh issue create and gh search issues directly.

See Prerequisites for running the agent skills in docs/prerequisites.md for the overall setup.


Step 0 — Pre-flight check

Before touching any candidate thread, verify:

  1. Mail-source backends from <project-config>/project.md → Mail sources are available. For each declared backend, run the backend's trivial health probe (per its adapter doc — Gmail: mcp__claude_ai_Gmail__search_threads with pageSize: 1; Ponymail: mcp__ponymail__auth_status(); IMAP: a CAPABILITY against the configured host; mbox: a stat on the archive path) and record the result in the skill's observed-state bag. Apply the contract's resolution rule to figure out which backend serves which op for this run.

    • mandatory: yes backend unavailablestop immediately. Surface "mandatory mail-source backend <name> unavailable: <reason>; run aborted". The user fixes the auth / connection and re-invokes.
    • mandatory: no backend unavailable → continue with the remaining backends. If the resolution then leaves an operation with no provider (e.g. no available backend supports create_draft), the skill records "no <op> backend available" in the observed-state bag and the relevant downstream step omits that proposal with a clear hand-back to the user.
    • Every declared backend healthy → proceed; the observed-state bag records one provider per op so every dispatch later is unambiguous.
  2. gh is authenticated and has access. Run gh api repos/<tracker> --jq .name; if it errors (401, 403, 404), stop and tell the user to log in with gh auth login or get added to <tracker>.

  3. (Reference-adopter guidance.) The reference adopter lists gmail as primary mandatory: yes and — per the ASF default — ponymail as mandatory: yes too (fallback role for drafts, since PonyMail is read-only). So for the reference flow both backends are pre-flight prerequisites: a Gmail-MCP failure stops the run (drafts have no home), and a PonyMail-MCP miss — not registered, or registered but unauthenticated for the private <security-list> archive — stops it too, per item 1's mandatory: yes rule. Gmail handles reads of just-arrived inbound mail and all draft creation; PonyMail handles archive lookups (and is the primary read path when authenticated). Adopters whose Mail sources table sets ponymail to mandatory: no get the old degrade-quietly behaviour; the step-by-step references to "Gmail" below should be read as "the backend the resolution rule picked for the relevant op".

  4. Privacy-LLM contract. This skill reads <security-list> bodies that may contain third-party PII the reporter discloses about other people. Run the gate-check first — non-zero exit is a hard stop:

    uv run --project <framework>/tools/privacy-llm/checker \
      privacy-llm-check
    

    The checker auto-locates <project-config>/privacy-llm.md (template at projects/_template/privacy-llm.md) and verifies every entry in Currently configured LLM stack is approved per tools/privacy-llm/models.md. In addition, verify:

    • ~/.config/apache-magpie/ is writable (the redactor's mapping file lives there);
    • the configured collaborator source is reachable via gh api (default: <tracker> from project.md);
    • the redaction-tuning knobs (collaborator exemption, enabled field types) are loaded into the skill's observed-state bag — they apply at filter-time below.

    Each subsequent body fetch in Steps 4 / 7 / 7g (template- field extraction, draft assembly, recap) follows the redact-after-fetch protocol in tools/privacy-llm/wiring.md; the receipt-of-confirmation draft assembly follows the reveal-before-send protocol when (and only when) the draft references a third-party identifier.

  5. Disclosure governance from <project-config>/security-intake-config.md. If the file exists, read the disclosure_governance block and load these two keys into the observed-state bag for use in Step 7:

    • reporter_acknowledgement_modelmanual | auto | none. Controls whether and how the receipt-of-confirmation reply is drafted (Step 7.4).
    • window_days — integer; the CVD window in calendar days, used as the disclosure deadline hint when composing the acknowledgement draft.

    If the file does not exist or the disclosure_governance block is absent, silently default to reporter_acknowledgement_model: manual and window_days: 90. A missing file is not a stop condition — adopters who have not yet created this config receive the same ASF defaults the skill has always applied.

If a mandatory: yes mail-source backend or the gh check fails, do not proceed — the skill would fail mid-flow otherwise, leaving half-built state (a draft on the wrong thread, or a tracker with no receipt reply). Fail fast instead. mandatory: no backends degrade quietly per the contract's resolution rule. A privacy-llm pre-flight failure is also a hard stop — the redactor's mapping store and the collaborator-source lookup are both load-bearing for every subsequent body read.


Inputs

Before running, resolve the user's selector into a concrete set of candidate Gmail threads:

SelectorResolves to
import new (default)every security@ thread received in the last 14 days that has not yet been imported as an issue and has not already been answered-and-closed on-thread
import since:YYYY-MM-DDevery security@ thread received since the given date that is not yet imported
import thread:<id>the single Gmail thread with that threadId — useful for re-importing after a manual discard, or for picking up a single message the automatic scan missed
import last 30d / import all / import last Nd (explicit request only)a wider sweep — use when the skill has not been run in a while or the user is doing a backlog catch-up. The all alias spans disclosure_governance.window_days days (default 90) from <project-config>/security-intake-config.md.

If the user supplies no selector, default to import new (14-day window).

Why the default is 14 days. Most reports that land on security@ fall into one of three steady-state buckets: (a) imported as a tracker within days of arrival, (b) answered on-thread with a canned negative response that the reporter accepts silently, or (c) obvious spam the triager ignores. None of those need a second look past 14 days. Widening the default window past two weeks would keep re-surfacing the same already-handled threads every sync run, which is noise. The user can always pass import last 30d or import all explicitly when a deeper sweep is genuinely warranted (e.g. after a long quiet period, or during a backlog audit).


Step 1 — List candidate threads from Gmail

Search <security-list> for inbound reports, excluding the tooling / GitHub-notification / mailing-list chatter that isn't a report:

Use the canonical candidate-listing query template from tools/gmail/search-queries.md; substitute the adopting project's <security-list-domain> and the project's GitHub-notification exclusions — both declared in <project-config>/project.md.

Backend selection. Candidate listing is one of the cases where Gmail remains primary even when PonyMail MCP is enabled: the inbox is where just-arrived inbound reports land with the lowest latency, and the import skill's sole purpose is converting those freshly-arrived threads into trackers. The PonyMail archive lags the inbox by minutes-to-hours for brand-new messages, which is exactly the window this skill most cares about.

When PonyMail MCP is enabled and authenticated (Step 0) and <security-list> is in .apache-magpie-overrides/user.mdtools.ponymail.private_lists, run the archive as a paired authoritative check against the Gmail result set:

mcp__ponymail__search_list(
  list: "security",
  domain: "<project>.apache.org",
  timespan: "lte=30d",
  emails_only: true
)

Cross-reference the returned summaries against the Gmail result set by Message-ID. Surface two classes of mismatch as extra candidates in Step 5:

  • In PonyMail, not in Gmail → note "seen in the archive, not in this user's Gmail — LDAP-only subscription, Gmail-filter miss, or wrong account". Often worth importing; always worth surfacing.
  • In Gmail, not in PonyMail → note "in Gmail inbox, not yet in the archive — archive-indexing lag; Gmail snapshot is the authoritative source for now". Proceed with Gmail-only data for this thread; a future sync run will reconcile once the archive catches up.

When PonyMail MCP is disabled, unauthenticated, or the private list is not in the user's allowlist, skip the paired-check query and proceed Gmail-only.

Do not exclude -from:<security-list>. That address is used for three very different message types — CVE-tool bookkeeping, ASF Security Team forwarding of inbound reports, and ad-hoc ASF Security discussion / advice. Blanket-excluding the sender would drop the forwarded reports along with the bookkeeping noise, so the bookkeeping emails are filtered out at Step 3 by subject pattern instead — see the cve-tool-bookkeeping row of the classification table.

Do not exclude -from:notifications@github.com wholesale. GitHub uses this address for two distinct categories of messages:

  1. Tracker-mirror notifications — when an action lands on a tracker issue (comment, label, close), GitHub emails every subscriber. These arrive with subject [<tracker-repo>] ... and are not import candidates — they describe an existing tracker.
  2. GHSA-relayed reports — when a reporter files a GitHub Security Advisory against <upstream>, GitHub emails notifications@github.com → <security-list> with subject [<upstream>] ... (GHSA-...). These are import candidates. A GHSA relay is not a distinct class — at Step 3 classify it as a plain Report (the GHSA ID is captured as a de-dup signal and as provenance, not as the classification) and proceed to field extraction.

Filter the mirror notifications at Step 1 only by the project's declared dedicated noreply mirror addresses (e.g. <tracker-repo>@noreply.github.com, declared in <project-config>/project.md). Do not blanket-exclude notifications@github.com — the remaining tracker-mirror chatter on notifications@github.com is caught at Step 2 (threadId dedup against existing tracker bodies) and Step 2-bis (already-answered detection).

The canonical query template in tools/gmail/search-queries.md omits the blanket exclusion; project-specific <project-config>/project.md declarations enumerate dedicated mirror noreply senders only.

Mandatory second pass — run a positive GHSA query. The rule above is a negative one, and negative rules are not self-enforcing: an exclusion added anywhere for noise reduction removes the whole GHSA intake channel, and the miss is invisible — nothing reports that a report was filtered out. So every import scan also runs the GHSA-advisory query, which checks the channel by construction and cannot be filtered away by an exclusion elsewhere. Union its hits with the candidate-listing query's before Step 2.

For each hit whose subject carries [<upstream>] ... (GHSA-...):

  • Treat it as a Report candidate, not tracker-mirror noise.
  • The advisory body is confidential — it lands in the private tracker only, per the confidentiality golden rule; never echo it to a public surface.
  • Prefer the advisory record API (gh api repos/<upstream>/security-advisories/<GHSA>). If it 404s, the operator is not yet a collaborator on that specific advisory — this is an access state, not a missing advisory. Extract the report from the notification email body and flag the admin hand-off: someone with advisory-admin rights must add the operator as a collaborator before the record API and the reporter-reply path become usable. See the GHSA contract in security-issue-sync's github-advisory.md.

Adjust the time window per the user's selector (since:newer_than: or after:; import allnewer_than:90d).

Run the query via mcp__claude_ai_Gmail__search_threads (see tools/gmail/operations.md). For each result, record threadId — the downstream de-duplication hinges on this.

Do not read the thread bodies yet. Body reads cost Gmail budget and most threads will be filtered out at Step 2.


Step 2 — Deduplicate against existing issues

For each candidate threadId, check whether that ID already appears in an <tracker> issue body. The sync skill records each thread ID in the "Security mailing list thread" field of the tracking issue (either as the <mail-archive-url>/thread/<id> URL or as a textual note containing the Gmail threadId). One gh search issues call is enough:

gh search issues "<threadId>" --repo <tracker> --match body --limit 5 \
  --json number,title,state,url

If the search returns any hit, the thread is already imported — skip it. Do not propose re-importing (that would create a duplicate tracker). If the user explicitly passed import thread:<id> and the thread is already imported, tell the user and link the existing issue rather than trying to create a duplicate.

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
91
Forks
91
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
magpie-security-issue-import
Source
github.com/apache/magpie