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 |