You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airflow.apache.org by po...@apache.org on 2026/05/04 21:16:27 UTC

(airflow-steward) branch feat/privacy-llm-foundation updated: feat(privacy-llm): wire skill-side protocol — wiring.md + per-skill Step 0 + Gmail-tool contract

This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch feat/privacy-llm-foundation
in repository https://gitbox.apache.org/repos/asf/airflow-steward.git


The following commit(s) were added to refs/heads/feat/privacy-llm-foundation by this push:
     new 8a2a233  feat(privacy-llm): wire skill-side protocol — wiring.md + per-skill Step 0 + Gmail-tool contract
8a2a233 is described below

commit 8a2a23368e5644b5f47f0f5e879279e41ee50272
Author: Jarek Potiuk <ja...@potiuk.com>
AuthorDate: Mon May 4 23:16:19 2026 +0200

    feat(privacy-llm): wire skill-side protocol — wiring.md + per-skill Step 0 + Gmail-tool contract
    
    PR-2's wiring layer on top of PR-1's foundation. Every Gmail- and
    markdown-fetching skill that touches `<security-list>` content now
    references the canonical redaction protocol; the Gmail tool contract
    makes the protocol unmissable for any future skill author; and the
    adopter's `<project-config>/privacy-llm.md` template gains a
    *Redaction configuration* section with three tunable knobs.
    
    What lands:
    
    - `tools/privacy-llm/wiring.md` (new) — single source-of-truth for
      the skill-side protocol: Step 0 pre-flight, redact-after-fetch
      (resolve collaborators → identify third-party PII → filter →
      pii-redact), reveal-before-send. Skill SKILL.md files link here
      rather than copy the pattern. Includes the table of skills that
      follow this pattern and explicit edge-case handling
      (collaborator-lookup fail, reporter-is-collaborator, lost mapping
      file, etc.).
    
    - `tools/gmail/operations.md` — every `mcp__claude_ai_Gmail__get_thread`
      call against `<security-list>` mail MUST be followed by the
      redaction protocol; every draft-create call MUST run pii-reveal
      on the rendered body when it carries third-party identifiers.
      Both are now part of the Gmail tool contract, not just the
      privacy-llm doc.
    
    - `projects/_template/privacy-llm.md` — new *Redaction
      configuration* section with three adopter-tunable knobs:
      - `collaborator_source` (default `<tracker>`): override when
        collaborators live in a different repo;
      - `collaborator_exemption` (default `enabled`): set `disabled`
        for a stricter posture that redacts collaborator names too;
      - `redaction_field_types` (default all six): drop a type
        when project sensitivity disagrees with the framework default.
      The redactor itself stays generic — knobs apply at the skill's
      filter step, before `--field` args are constructed.
    
    - `tools/privacy-llm/pii.md` — replaced the `--collaborators-file`
      placeholder language (the redactor doesn't grow a config arg;
      filtering stays in the skill); added a pointer to the
      collaborator-exemption knob in `<project-config>/privacy-llm.md`
      for the stricter-posture toggle.
    
    - `tools/privacy-llm/tool.md` — capability table extended with
      rows for `wiring.md` and `projects/_template/privacy-llm.md`
      so adopters / skill authors can find the wiring + config
      surfaces from the tool overview.
    
    - Skill files wired with a Step 0 *Privacy-LLM contract* bullet
      that links to `wiring.md`:
      - `security-issue-import` — full wiring (extract-template-fields
        Step 4 redacts before extracting; receipt-of-confirmation
        draft Step 7 reveals before send when third-party identifiers
        are referenced);
      - `security-issue-sync` — Step 0 + redact-on-each-Gmail-read
        + reveal-before-draft;
      - `security-issue-invalidate` — Step 0 + redact-on-Step-3-mining
        + reveal-before-Step-6-draft;
      - `security-cve-allocate` — Step 0 + redact-on-the-occasional
        inbound-thread Gmail read (no outbound drafts);
      - `security-issue-import-from-md` — Step 0 + treat the
        findings file like a Gmail body (third-party PII flow same
        way through markdown).
    
    Tests: 48/48 redactor tests pass. `prek run --all-files` clean
    (doctoc auto-regen'd the privacy-llm template TOC for the new
    *Redaction configuration* sub-sections).
    
    Generated-by: Claude Code (Claude Opus 4.7)
---
 .claude/skills/security-cve-allocate/SKILL.md      |  16 +-
 .../skills/security-issue-import-from-md/SKILL.md  |  12 +
 .claude/skills/security-issue-import/SKILL.md      |  70 +++++-
 .claude/skills/security-issue-invalidate/SKILL.md  |  18 +-
 .claude/skills/security-issue-sync/SKILL.md        |  15 ++
 projects/_template/privacy-llm.md                  |  67 ++++++
 tools/gmail/operations.md                          |  29 +++
 tools/privacy-llm/pii.md                           |  33 +--
 tools/privacy-llm/tool.md                          |   2 +
 tools/privacy-llm/wiring.md                        | 256 +++++++++++++++++++++
 10 files changed, 499 insertions(+), 19 deletions(-)

diff --git a/.claude/skills/security-cve-allocate/SKILL.md b/.claude/skills/security-cve-allocate/SKILL.md
index 77b91d5..8a80c18 100644
--- a/.claude/skills/security-cve-allocate/SKILL.md
+++ b/.claude/skills/security-cve-allocate/SKILL.md
@@ -191,8 +191,22 @@ Before touching the tracker, verify:
    generating the relay recipe and then realising no PMC member is
    available to act on it).
 
+4. **Privacy-LLM contract.** This skill mostly works in
+   `<tracker>` and Vulnogram (which is OAuth-gated and not
+   readable from agent context); the tracker body is already
+   redacted by the upstream `security-issue-import` skill. The
+   only Gmail touch in this skill is the optional inbound-thread
+   `mcp__claude_ai_Gmail__get_thread` referenced from Step 4 to
+   confirm the reporter's preferred CVE credit shape — that
+   read follows the [redact-after-fetch protocol](../../../tools/privacy-llm/wiring.md#redact-after-fetch-protocol)
+   per [`tools/privacy-llm/wiring.md`](../../../tools/privacy-llm/wiring.md).
+   No outbound draft is composed here, so no reveal step. The
+   pre-flight check is the same one-liner as the other security
+   skills: confirm `<project-config>/privacy-llm.md` exists and
+   `~/.config/apache-steward/` is writable.
+
 If any check fails, stop with a clear message. Do not start
-filling in the tracker until all three are green — a partial
+filling in the tracker until all four are green — a partial
 allocation (label added, JSON regeneration skipped) is worse than
 no allocation at all.
 
diff --git a/.claude/skills/security-issue-import-from-md/SKILL.md b/.claude/skills/security-issue-import-from-md/SKILL.md
index a3ed0db..3414829 100644
--- a/.claude/skills/security-issue-import-from-md/SKILL.md
+++ b/.claude/skills/security-issue-import-from-md/SKILL.md
@@ -169,6 +169,18 @@ Before parsing the file, verify:
    sections, then a `**Severity:** … **Status:** … **Category:**
    … **Repository:** … **Date created:** …` metadata block; blocks
    separated by `---` on their own line."*
+4. **Privacy-LLM contract.** The input markdown can carry
+   third-party PII the same way a `<security-list>` mail body
+   can — researcher names cited in a finding, victim emails in
+   a reproduction step, and so on. Apply the same pre-flight
+   from
+   [`tools/privacy-llm/wiring.md`](../../../tools/privacy-llm/wiring.md#step-0--pre-flight)
+   that the Gmail-touching skills do (config loads,
+   `~/.config/apache-steward/` writable, collaborator source
+   reachable). Findings parsed in Step 1 below feed the
+   redact-after-fetch protocol the same way Gmail bodies do —
+   the file IS the source-of-truth here, treat it like an
+   inbound mail body.
 
 If any check fails, do **not** proceed.
 
diff --git a/.claude/skills/security-issue-import/SKILL.md b/.claude/skills/security-issue-import/SKILL.md
index 63bd4df..1cb4047 100644
--- a/.claude/skills/security-issue-import/SKILL.md
+++ b/.claude/skills/security-issue-import/SKILL.md
@@ -189,11 +189,36 @@ Before touching any candidate thread, verify:
    when enabled but unauthenticated). See
    [`tools/ponymail/tool.md`](../../../tools/ponymail/tool.md)
    for the one-time setup instructions.
+4. **Privacy-LLM contract.** This skill reads `<security-list>`
+   bodies that may contain third-party PII the reporter
+   discloses about other people. Before fetching any body,
+   load `<project-config>/privacy-llm.md` (template at
+   [`projects/_template/privacy-llm.md`](../../../projects/_template/privacy-llm.md))
+   and verify:
+   - `~/.config/apache-steward/` 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`](../../../tools/privacy-llm/wiring.md#redact-after-fetch-protocol);
+   the receipt-of-confirmation draft assembly follows the
+   [reveal-before-send protocol](../../../tools/privacy-llm/wiring.md#reveal-before-send-protocol)
+   when (and only when) the draft references a third-party
+   identifier.
 
 If the Gmail or `gh` check fails (PonyMail degrades quietly), 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.
+no receipt reply). Fail fast instead. 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.
 
 ---
 
@@ -693,6 +718,35 @@ expects. Most fields the reporter did not explicitly supply stay as
 `_No response_`; the subsequent `security-issue-sync` run will prompt
 the triager to fill them as the discussion progresses.
 
+**Apply the redact-after-fetch protocol BEFORE extracting fields.**
+Every body fetched in Steps 2 / 2b / 3 (via `mcp__claude_ai_Gmail__get_thread`
+with `messageFormat: FULL_CONTENT`) goes through the redactor per
+[`tools/privacy-llm/wiring.md`](../../../tools/privacy-llm/wiring.md#redact-after-fetch-protocol)
+before its content is used for field extraction. Concretely:
+
+1. Resolve the collaborator set once for this skill run via
+   `gh api repos/<tracker>/collaborators --jq '.[].login'`
+   (the configured collaborator source from
+   `<project-config>/privacy-llm.md` — default `<tracker>`).
+2. For each candidate body, identify third-party PII candidates
+   (names / emails / handles / etc. that appear in the body or
+   signature, OTHER than the reporter from the `From:` header).
+3. Filter out the reporter and any collaborator (apply the
+   *Collaborator exemption* knob from `<project-config>/privacy-llm.md`
+   — default `enabled`, so collaborators flow through; set
+   `disabled` redacts them too).
+4. Pass the remaining set as `--field <type>:<value>` arguments
+   to `pii-redact`, capture the redacted body for use in this
+   step's field extraction below. The reporter's own values
+   (name, email, etc.) are NEVER redacted — they flow through
+   in the clear.
+
+The "issue description" template field below is sourced from the
+**redacted body**, not the raw body. Skill docs and proposals
+reviewed by the user in Step 5 / 6 will show third-party
+identifiers (`N-…`, `E-…`) where the reporter named someone
+else; the user can run `pii-list` to see the mapping if needed.
+
 The generic body-field schema (role → field-name contract, empty-field
 convention, body-field surgery pattern) lives in
 [`tools/github/issue-template.md`](../../../tools/github/issue-template.md);
@@ -971,7 +1025,19 @@ For each confirmed `Report` / `ASF-security relay`:
    workflow being correctly configured. The mutation is a no-op when
    the item is already on the board with the same Status.
 
-4. Draft the receipt-of-confirmation reply. **The draft must be
+4. Draft the receipt-of-confirmation reply. **Apply the
+   reveal-before-send protocol if (and only if) the rendered
+   draft body carries any third-party identifiers** (per the
+   Step 4 redact-after-fetch above; the receipt template
+   typically references only the reporter's own values, so most
+   drafts need no reveal — but when the reporter's body quoted
+   another individual the redactor mapped, that identifier may
+   appear in the receipt's quoted-context section). The reveal
+   protocol is in
+   [`tools/privacy-llm/wiring.md`](../../../tools/privacy-llm/wiring.md#reveal-before-send-protocol);
+   the `tools/gmail/operations.md` *Hard rules that apply to
+   both backends* section also requires this step before the
+   create-draft tool call. **The draft must be
    created on the inbound Gmail thread** — always pass the candidate's
    `threadId` to the project's preferred drafting backend per the
    precedence rule in
diff --git a/.claude/skills/security-issue-invalidate/SKILL.md b/.claude/skills/security-issue-invalidate/SKILL.md
index a98f96e..49c789c 100644
--- a/.claude/skills/security-issue-invalidate/SKILL.md
+++ b/.claude/skills/security-issue-invalidate/SKILL.md
@@ -167,8 +167,22 @@ Before any work, verify:
    Both hard stops are deliberate — the skill must not paper over
    a CVE-allocation or a published-advisory state by silently
    labelling and closing.
-
-If `gh` fails or any hard stop fires, do **not** proceed.
+4. **Privacy-LLM contract.** This skill drafts a closing reply
+   on the inbound `<security-list>` Gmail thread, so it reads
+   the original report's body to mine the team's reasoning
+   (Step 3) and assembles an outbound draft (Step 6). Before
+   either, load `<project-config>/privacy-llm.md` and verify
+   the pre-flight items in
+   [`tools/privacy-llm/wiring.md`](../../../tools/privacy-llm/wiring.md#step-0--pre-flight).
+   The Step 3 read follows the
+   [redact-after-fetch protocol](../../../tools/privacy-llm/wiring.md#redact-after-fetch-protocol);
+   the Step 6 draft follows the
+   [reveal-before-send protocol](../../../tools/privacy-llm/wiring.md#reveal-before-send-protocol)
+   when (and only when) the closing reply references a
+   third-party identifier.
+
+If `gh` fails or any hard stop fires, do **not** proceed. A
+privacy-llm pre-flight failure is also a hard stop.
 
 ---
 
diff --git a/.claude/skills/security-issue-sync/SKILL.md b/.claude/skills/security-issue-sync/SKILL.md
index 8364ce4..9a6071d 100644
--- a/.claude/skills/security-issue-sync/SKILL.md
+++ b/.claude/skills/security-issue-sync/SKILL.md
@@ -380,6 +380,21 @@ Before reading any tracker state, verify:
    if the user said `sync NNN` but the number does not exist in
    `<tracker>`, stop before Step 1 and ask which issue
    they meant.
+5. **Privacy-LLM contract.** This skill reads `<security-list>`
+   bodies (and may read `<private-list>` content when escalating)
+   that may contain third-party PII. Before fetching any body,
+   load `<project-config>/privacy-llm.md` and verify the
+   pre-flight items in
+   [`tools/privacy-llm/wiring.md`](../../../tools/privacy-llm/wiring.md#step-0--pre-flight) —
+   `~/.config/apache-steward/` is writable, the configured
+   collaborator source is reachable, the redaction-tuning knobs
+   are loaded into the observed-state bag. Subsequent body reads
+   in Step 1 (gather current state) follow the
+   [redact-after-fetch protocol](../../../tools/privacy-llm/wiring.md#redact-after-fetch-protocol);
+   Step 4 outbound drafts follow the
+   [reveal-before-send protocol](../../../tools/privacy-llm/wiring.md#reveal-before-send-protocol)
+   when (and only when) the rendered draft references a
+   third-party identifier.
 
 If any check fails (other than PonyMail, which degrades quietly),
 stop and surface what is missing. Do **not** proceed to Step 1 on a
diff --git a/projects/_template/privacy-llm.md b/projects/_template/privacy-llm.md
index 696aecf..9972931 100644
--- a/projects/_template/privacy-llm.md
+++ b/projects/_template/privacy-llm.md
@@ -6,6 +6,11 @@
   - [Currently configured LLM stack](#currently-configured-llm-stack)
   - [Approved third-party endpoints (opt-in)](#approved-third-party-endpoints-opt-in)
   - [Private mailing lists for this project](#private-mailing-lists-for-this-project)
+  - [Redaction configuration](#redaction-configuration)
+    - [Collaborator source](#collaborator-source)
+    - [Collaborator exemption](#collaborator-exemption)
+    - [Redaction field types](#redaction-field-types)
+    - [How the knobs are applied](#how-the-knobs-are-applied)
 
 <!-- END doctoc generated TOC please keep comment here to allow auto update -->
 
@@ -77,3 +82,65 @@ the per-variant setup recipes are at
      third-party PII (non-reporter, non-collaborator individuals
      named in the body) is redacted (per
      ../../tools/privacy-llm/pii.md). -->
+
+## Redaction configuration
+
+These knobs tune how skills apply the PII redactor (per
+[`../../tools/privacy-llm/wiring.md`](../../tools/privacy-llm/wiring.md))
+when reading `<security-list>` content. Defaults are listed in
+parentheses; uncomment a row to override.
+
+### Collaborator source
+
+```text
+# collaborator_source: <tracker>
+```
+
+(default: read from `<project-config>/project.md → tracker_repo`).
+The repository whose collaborator list is treated as "already
+public/known" and therefore NOT redacted. Override here if your
+project tracks security-team membership in a different repo
+(e.g. a parent-org roster repo).
+
+### Collaborator exemption
+
+```text
+# collaborator_exemption: enabled
+```
+
+(default: `enabled` — collaborators are NOT redacted; their
+identity is already public via the tracker's collaborator list).
+
+Set to `disabled` for a stricter posture: every non-reporter
+individual gets redacted, including collaborators. Use when
+your PMC has decided that even public collaborator identity
+should not flow through LLMs as a defence-in-depth measure.
+
+### Redaction field types
+
+```text
+# redaction_field_types: name, email, phone, ip, handle, address
+```
+
+(default: all six types are redacted). Remove a type from this
+list to disable redaction for that field type. Rare — most
+projects keep all six on. Examples of when an adopter might
+narrow:
+
+- A project whose security reports never include phone numbers
+  (and where redacting phone-shaped strings might cause false
+  positives in code excerpts) might drop `phone`.
+- A project with a strict "treat public IPs as non-PII" policy
+  might drop `ip`. The framework already excludes IPs that
+  identify a vulnerable production server (see
+  [`../../tools/privacy-llm/pii.md`](../../tools/privacy-llm/pii.md))
+  but this knob is the broader override.
+
+### How the knobs are applied
+
+The redactor itself reads no config file — these knobs are
+applied by **the skill** at filter time (Step 3 of the
+[redact-after-fetch protocol](../../tools/privacy-llm/wiring.md#redact-after-fetch-protocol)),
+before `pii-redact --field` arguments are constructed. A skill
+that does not respect a knob is a framework bug; report it on
+[`apache/airflow-steward`](https://github.com/apache/airflow-steward).
diff --git a/tools/gmail/operations.md b/tools/gmail/operations.md
index 2252d1d..25375e7 100644
--- a/tools/gmail/operations.md
+++ b/tools/gmail/operations.md
@@ -94,6 +94,24 @@ expensive — most skills filter candidates down on metadata first and
 only fetch bodies for the narrow set that actually warrants it
 (`security-issue-import` does this explicitly at Step 3).
 
+**Privacy-LLM contract — apply to every body read.** Every
+`get_thread` call against a `<security-list>` thread (or any
+`<private-list>` thread, where the approved-LLM gate also
+applies) MUST be followed by the redact-after-fetch protocol
+documented in
+[`../privacy-llm/wiring.md`](../privacy-llm/wiring.md#redact-after-fetch-protocol)
+before the body is used for any further processing. The window
+between `get_thread` returning and `pii-redact` running should
+be a single tool invocation wide; the redacted body is what
+flows through the rest of the skill. Skills that consume bodies
+without running the protocol are framework bugs.
+
+Skip the protocol on `messageFormat='METADATA'` calls — the
+returned envelope carries the reporter's `From:` header (which
+is not redacted under the contract) and routing fields, no
+free-form body content. The protocol applies once an actual
+body is fetched.
+
 ## Write — drafts only, never send
 
 ### Drafting backends
@@ -171,6 +189,17 @@ backend too — drafts only, never send; subject is always
 
 - **Never send.**
 - **Subject is always `Re: <root subject>`**, never fabricated.
+- **Run `pii-reveal` before passing the body to the create-draft
+  call.** If the draft body carries any third-party identifiers
+  the skill redacted earlier (e.g. when assembling a CVE-credit
+  line referencing a non-reporter, non-collaborator individual),
+  the rendered draft text MUST be passed through `pii-reveal`
+  *once*, immediately before the create-draft tool call, so the
+  recipient sees the real value. Drafts whose bodies contain only
+  the reporter's own identity (already not-redacted under the
+  contract) need no reveal step. The full reveal-before-send
+  protocol is in
+  [`../privacy-llm/wiring.md`](../privacy-llm/wiring.md#reveal-before-send-protocol).
 - **Surface which backend was used** in the proposal / recap so the
   user can tell at a glance whether the draft threads on their own
   Gmail view (`oauth_curl`) or only on the recipient's
diff --git a/tools/privacy-llm/pii.md b/tools/privacy-llm/pii.md
index 3aa4be4..df1dbce 100644
--- a/tools/privacy-llm/pii.md
+++ b/tools/privacy-llm/pii.md
@@ -91,20 +91,25 @@ mental model simple: a collaborator is a collaborator everywhere.
 
 The lookup is per-skill-run, not cached across runs — a
 collaborator added to or removed from the tracker takes effect
-on the next skill invocation. Skills SHOULD pass the resolved
-collaborator set to the redactor as a `--collaborators-file`
-argument (path to a one-login-per-line file), so the redactor
-can decline to redact a name / handle / email that maps to a
-collaborator's GitHub identity.
-
-> **Note for PR-1 / PR-2 readers.** The
-> `--collaborators-file` argument is a *future* addition to the
-> redactor (it lands when PR-2 wires the gate into the security
-> skills). For PR-1 the redactor accepts a flat
-> `--field <type>:<value>` list and trusts the caller to filter
-> against the collaborator set before passing values in. The
-> filtering responsibility stays with the skill either way; the
-> argument is purely an ergonomic shorthand.
+on the next skill invocation.
+
+**Filtering happens in the skill, not the redactor.** The
+redactor is a generic value→identifier swap; it does not know
+who the reporter is or who is a collaborator. Skills resolve the
+collaborator set, identify third-party PII candidates in the
+body, drop the reporter and collaborators, and only then pass
+the *should-be-redacted* set as `--field` arguments. The
+canonical step-by-step pattern is in
+[`wiring.md`](wiring.md#redact-after-fetch-protocol).
+
+The collaborator filter is **adopter-tunable** — the
+*"Collaborator exemption"* knob in the adopter's
+[`<project-config>/privacy-llm.md`](../../projects/_template/privacy-llm.md#collaborator-exemption)
+controls whether the exemption is enabled at all. Default is
+enabled (collaborator names flow as-is); disabled is the
+stricter posture (every non-reporter individual gets redacted,
+including collaborators). See the template doc for when to use
+each.
 
 ## The identifier format
 
diff --git a/tools/privacy-llm/tool.md b/tools/privacy-llm/tool.md
index b9b3c43..393abc9 100644
--- a/tools/privacy-llm/tool.md
+++ b/tools/privacy-llm/tool.md
@@ -49,6 +49,8 @@ agent's context, and `privacy-llm` is what stops it from leaking.
 |---|---|---|
 | PII redaction contract | [`pii.md`](pii.md) | Which fields are PII, the hash-prefixed identifier format (`N-a3f9d2`, `E-b8c247`, …), the local mapping store at `~/.config/apache-steward/pii-mapping.json`, the redact-then-reveal lifecycle. |
 | Approved-LLM registry | [`models.md`](models.md) | Which LLMs the framework treats as privacy-approved (Claude Code by default; anything at `*.apache.org`; local Ollama / vLLM; everything else opt-in), how to declare additions in `<project-config>/privacy-llm.md`, and what the pre-flight gate checks. |
+| Skill-wiring pattern | [`wiring.md`](wiring.md) | The canonical step-by-step pattern every `<security-list>`- or `<private-list>`-touching skill follows when applying the contract — Step 0 pre-flight, redact-after-fetch, reveal-before-send, plus edge cases. Skill `SKILL.md` files link here from their pre-flight section rather than copying the protocol. |
+| Per-project configuration | [`projects/_template/privacy-llm.md`](../../projects/_template/privacy-llm.md) | Template the adopter copies into `<project-config>/privacy-llm.md` to declare their LLM stack, private mailing-list set, collaborator source, and redaction-tuning knobs (collaborator exemption, enabled field types). Defaults are documented inline. |
 | Setup recipes | [`../../docs/setup/privacy-llm.md`](../../docs/setup/privacy-llm.md) | Copy-pasteable configurations for the supported variants — local inference, Apache-hosted endpoint, AWS Bedrock, opt-in third-party. Marked **provisional pending ASF Legal Affairs ratification** of an authoritative approved-model list. |
 | Reference Python helper | [`redactor/`](redactor/) | A small `uv` project exposing three console scripts — `pii-redact`, `pii-reveal`, `pii-list` — that skills shell out to so the redaction lifecycle is consistent across every consumer. |
 
diff --git a/tools/privacy-llm/wiring.md b/tools/privacy-llm/wiring.md
new file mode 100644
index 0000000..6a29a96
--- /dev/null
+++ b/tools/privacy-llm/wiring.md
@@ -0,0 +1,256 @@
+<!-- START doctoc generated TOC please keep comment here to allow auto update -->
+<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
+**Table of Contents**  *generated with [DocToc](https://github.com/thlorenz/doctoc)*
+
+- [Skill-wiring pattern — applying the privacy-LLM contract](#skill-wiring-pattern--applying-the-privacy-llm-contract)
+  - [The protocol at a glance](#the-protocol-at-a-glance)
+  - [Step 0 — pre-flight](#step-0--pre-flight)
+  - [Redact-after-fetch protocol](#redact-after-fetch-protocol)
+  - [Reveal-before-send protocol](#reveal-before-send-protocol)
+  - [Configuration knobs the adopter can tune](#configuration-knobs-the-adopter-can-tune)
+  - [Edge cases and failure modes](#edge-cases-and-failure-modes)
+  - [Skills that follow this pattern](#skills-that-follow-this-pattern)
+
+<!-- END doctoc generated TOC please keep comment here to allow auto update -->
+
+<!-- SPDX-License-Identifier: Apache-2.0
+     https://www.apache.org/legal/release-policy.html -->
+
+# Skill-wiring pattern — applying the privacy-LLM contract
+
+This file is the **canonical pattern** every `<security-list>`-
+or `<private-list>`-touching skill follows when it reads private
+mail content. Skill `SKILL.md` files link here from their pre-
+flight section rather than copying the protocol — keeping the
+pattern in one place avoids drift across skills.
+
+If you are writing a new skill that may read private mail
+content, add a Step 0 bullet that points at this file, then add
+the two work steps documented below (redact-after-fetch and
+reveal-before-send). The redactor itself is documented in
+[`pii.md`](pii.md); the approved-LLM gate is in
+[`models.md`](models.md); the adopter's per-project config lives
+at `<project-config>/privacy-llm.md`
+([`projects/_template/privacy-llm.md`](../../projects/_template/privacy-llm.md)
+is the starting point).
+
+## The protocol at a glance
+
+```text
+Step 0 (pre-flight)        check the privacy-llm config exists
+                           load <project-config>/privacy-llm.md
+                           verify the active LLM stack is approved
+                                  (when reading <private-list>)
+
+Step N (after fetch)       resolve collaborators (gh api repos/<tracker>/collaborators)
+                           identify third-party PII in body
+                           filter out: the reporter + every collaborator
+                           pii-redact --field <type>:<value> ... (the filtered set)
+                           use the redacted body for ALL subsequent processing
+
+Step M (before send)       (only when emitting a body that mentions a
+                            redacted third-party identifier)
+                           pii-reveal on the rendered draft / comment text
+                           send the revealed body
+```
+
+Three rules govern the whole protocol:
+
+1. **The skill does the filtering.** The redactor is a generic
+   value→identifier swap; it does not know who the reporter is
+   or who is a collaborator. The skill identifies third-party
+   PII candidates in the body, filters out the reporter and
+   collaborators, and only passes the *should-be-redacted* set
+   to `pii-redact`.
+2. **The body is redacted exactly once, immediately after fetch.**
+   No "redact later when convenient" — the window between the
+   `mcp__claude_ai_Gmail__get_thread` (or PonyMail equivalent)
+   tool call and the redact call should be a single tool
+   invocation wide. After redaction, the redacted body is what
+   flows through the rest of the skill.
+3. **Reveal runs only at the outbound boundary.** When the skill
+   is composing a draft / status comment / outbound text that
+   carries one of *its own* redacted identifiers AND needs the
+   real value at the destination, `pii-reveal` runs once on the
+   rendered text right before the send tool is called.
+
+## Step 0 — pre-flight
+
+Add to the skill's existing Step 0 (pre-flight check) section:
+
+```markdown
+- **Privacy-LLM contract.** Read
+  `<project-config>/privacy-llm.md` (or the framework's
+  template at
+  [`projects/_template/privacy-llm.md`](../../projects/_template/privacy-llm.md)
+  if the adopter has not yet customised it). Confirm:
+
+  - The configured LLM stack matches what is currently
+    active. If the skill may read `<private-list>` content
+    and the active LLM stack contains an entry not in the
+    approved-model registry per
+    [`tools/privacy-llm/models.md`](../../tools/privacy-llm/models.md#the-pre-flight-check),
+    stop and ask the user to wire an approved model first.
+  - `~/.config/apache-steward/` is writable (the redactor's
+    mapping file lives there). If not, prompt the user to
+    create it.
+  - The collaborator-source repository (default: the
+    `<tracker>` declared in
+    `<project-config>/project.md`; override per
+    `<project-config>/privacy-llm.md → Collaborator
+    source`) is reachable via `gh api`.
+```
+
+The pre-flight is read-only — no PII has been fetched yet. A
+failed pre-flight does not write to the mapping file or the
+tracker.
+
+## Redact-after-fetch protocol
+
+After every `mcp__claude_ai_Gmail__get_thread` (or
+`mcp__ponymail__get_thread` / `mcp__ponymail__get_email`) on
+`<security-list>` mail, run this sequence before any further
+processing of the body:
+
+1. **Resolve collaborators.** Fetch the collaborator login set
+   for the configured collaborator source:
+
+   ```bash
+   gh api repos/<tracker>/collaborators --jq '.[].login'
+   ```
+
+   The result is a list of GitHub logins. Skills SHOULD also
+   resolve `name` and `email` per login when those are
+   available (`gh api repos/<tracker>/collaborators --jq '.[]
+   | {login, name: .name, email: .email}'`) — that lets the
+   skill match real-name and email mentions in the body
+   against collaborators, not just GitHub-handle mentions.
+
+2. **Identify third-party PII candidates in the body.** The
+   skill scans the thread body, signature lines, CVE credit
+   fields, and HackerOne / GHSA fields for natural-person
+   names, emails, phones, IPs, and personal handles. The
+   reporter's own values (parsed from the `From:` header) are
+   identified separately so they can be excluded.
+
+3. **Filter the candidate set.** Drop any candidate that is:
+
+   - The reporter's own name / email / phone / handle from
+     the `From:` header.
+   - A collaborator's login, name, or email from the set
+     resolved in step 1.
+
+   What remains is the **should-be-redacted set** —
+   third-party PII that is neither the reporter nor a
+   collaborator.
+
+4. **Call the redactor.** For each remaining candidate, pass a
+   `--field <type>:<value>` argument to `pii-redact`. The
+   `<type>` codes are `name`, `email`, `phone`, `ip`,
+   `handle`, `address` — see
+   [`pii.md`](pii.md#what-counts-as-pii) for which fields
+   trigger which type.
+
+   ```bash
+   echo "$BODY" | uv run --project <framework>/tools/privacy-llm/redactor \
+     pii-redact \
+     --field name:"Other Researcher" \
+     --field email:"other@example.com" \
+     --field handle:"otherresearcher-personal"
+   ```
+
+   The redactor returns the body with the matched values
+   replaced by `<TYPE>-<hex>` identifiers. The mapping file at
+   `~/.config/apache-steward/pii-mapping.json` is updated in
+   place.
+
+5. **Use the redacted body for all subsequent processing.**
+   Tracker-issue body, status comments, candidate analysis,
+   prior-art lookup — every downstream step operates on the
+   redacted text. The original (un-redacted) body is dropped
+   from the agent's working set; if it is needed again, the
+   skill re-fetches.
+
+## Reveal-before-send protocol
+
+When the skill is about to emit a body that:
+
+- carries a redacted third-party identifier, AND
+- is destined for a surface that needs the real value (a draft
+  reply to the reporter, a CVE credit line, a public advisory
+  with the third-party's accepted credit),
+
+run `pii-reveal` on the rendered text once, right before the
+send tool is called:
+
+```bash
+echo "$DRAFT" | uv run --project <framework>/tools/privacy-llm/redactor pii-reveal
+```
+
+`pii-reveal` reads the local mapping and substitutes any
+identifier it knows about. Identifiers it does not recognise are
+passed through unchanged — no risk of collision-corruption.
+
+Reveal does **not** run on:
+
+- Status comments / sync messages where the redacted form is
+  fine (the security team can read identifiers).
+- Public surfaces where the third party has *not* consented to
+  being credited — the framework's
+  [Confidentiality of the tracker repository](../../AGENTS.md#confidentiality-of-the-tracker-repository)
+  rules apply on top.
+
+## Configuration knobs the adopter can tune
+
+The adopter's per-project `<project-config>/privacy-llm.md`
+controls these knobs (see
+[`projects/_template/privacy-llm.md`](../../projects/_template/privacy-llm.md)
+for the template):
+
+| Knob | Default | Notes |
+|---|---|---|
+| **Private mailing lists** | `<private-list>` only | Adding a list here gates skills that read it on the approved-LLM check (see [`models.md`](models.md)). |
+| **Approved LLM stack** | Claude Code only | Add other approved entries with the data-residency-contract line per [`models.md`](models.md#the-opt-in-entries--adopter-declares-explicitly). |
+| **Collaborator source** | `<tracker>` (from `<project-config>/project.md`) | Override here if collaborators are tracked in a different repo (e.g. a parent org repo, or a separate roster repo). |
+| **Collaborator exemption** | enabled (collaborators NOT redacted) | Disable here to redact every non-reporter individual including collaborators — stricter posture; use when the project's PMC has decided collaborator names should never enter LLMs even when public. |
+| **Redaction field types** | all six (`name`, `email`, `phone`, `ip`, `handle`, `address`) | Disable individual types here if a project has decided the sensitivity tradeoff differs (rare; the framework default is to redact all six). |
+
+The redactor itself reads no config file — it just does what
+the caller passes via `--field`. The skill applies the per-
+project knobs at filter time (step 3 above):
+
+- **Disabled type**: the skill does not generate `--field`
+  args of that type, even when matching values are found.
+- **Disabled collaborator exemption**: the skill skips step 3's
+  collaborator filter, so collaborator names get redacted too.
+- **Different collaborator source**: the skill uses the
+  configured repo in step 1 instead of `<tracker>`.
+
+## Edge cases and failure modes
+
+| Condition | Handling |
+|---|---|
+| `gh api` collaborator lookup fails (network, auth, rate-limit) | Skill stops with an error. Do **not** fall back to "redact everyone including collaborators" silently — that produces a body where collaborator names are now identifiers, which downstream skills would not expect. The user retries the skill once the lookup works. |
+| The reporter is *also* a collaborator | The reporter's own values are excluded from redaction (step 3, first bullet). The collaborator filter does not apply to them — there is no special second-pass. |
+| The body contains a self-reference (`I am X`) where X is a collaborator | X is filtered out as a collaborator regardless. (Also: probably not a thing — collaborators rarely send security@ mail about themselves.) |
+| The mapping file at `~/.config/apache-steward/pii-mapping.json` is corrupt | `pii-redact` returns exit code 2 with the parse error on stderr. The skill stops; the user investigates the mapping file before re-running. |
+| `pii-reveal` encounters identifiers not in the local map | They pass through unchanged. The skill should still complete its outbound, with the unknown identifiers preserved in the draft text. (This is the cross-machine case: a colleague redacted, you are revealing.) |
+
+## Skills that follow this pattern
+
+The skill files below all wire this protocol in their Step 0
+pre-flight + a redact-after-fetch step + (where applicable) a
+reveal-before-send step. Adding a new Gmail- or PonyMail-touching
+skill that handles `<security-list>` content? Add it here and
+add the corresponding wiring to the new `SKILL.md`.
+
+| Skill | Reads | Drafts |
+|---|---|---|
+| [`security-issue-import`](../../.claude/skills/security-issue-import/SKILL.md) | `<security-list>` Gmail threads | reporter receipt-of-confirmation reply |
+| [`security-issue-sync`](../../.claude/skills/security-issue-sync/SKILL.md) | `<security-list>` Gmail threads | reporter status updates |
+| [`security-issue-invalidate`](../../.claude/skills/security-issue-invalidate/SKILL.md) | `<security-list>` Gmail threads | reporter invalidation reply |
+| [`security-cve-allocate`](../../.claude/skills/security-cve-allocate/SKILL.md) | tracker + Vulnogram | n/a (Vulnogram is OAuth-gated, body is in tracker which is already redacted) |
+| [`security-issue-import-from-md`](../../.claude/skills/security-issue-import-from-md/SKILL.md) | adopter-supplied markdown file | n/a |
+| [`security-issue-import-from-pr`](../../.claude/skills/security-issue-import-from-pr/SKILL.md) | public PR | n/a (no `<security-list>` content) |
+| [`security-issue-fix`](../../.claude/skills/security-issue-fix/SKILL.md) | tracker (already redacted) | n/a (PR is public, must not include any PII) |
+| [`security-issue-deduplicate`](../../.claude/skills/security-issue-deduplicate/SKILL.md) | two trackers (already redacted) | n/a |