Table of Contents generated with DocToc
- RFC-AI-0003: Privacy-aware LLM routing for foundation private information
- RFC: privacy-aware LLM routing for foundation private mail
RFC-AI-0003: Privacy-aware LLM routing for foundation private information
Apache Magpie — third-party PII redaction + approved-LLM gate
apache/magpie maintainers
2026-05-04
Status — provisional. The default-approved LLM registry described in this RFC reflects the framework maintainer’s working position pending ASF Privacy VP/Legal VP ratification of an authoritative approved-model list for foundation private data. When such a list lands, the registry will be updated to point at it as source-of-truth.
RFC: privacy-aware LLM routing for foundation private mail
| Field | Value |
|---|---|
| Status | Provisional — pending ASF Privacy VP/Legal VP ratification |
| Targets | apache/magpie (the Apache Magpie framework) + adopting projects |
| Implemented in | PR #48 (foundation), PR #50 (refinement + skill-side redactor wiring), PR #51 (gate-check + skill-side gate wiring) |
| Source-of-truth docs | tools/privacy-llm/{tool,pii,models,wiring}.md, docs/setup/privacy-llm.md, AGENTS.md → Privacy-LLM |
| Reference implementation | tools/privacy-llm/redactor/ (PII redactor, stdlib-only Python, 48 unit tests), tools/privacy-llm/checker/ (approved-LLM gate-check, stdlib-only Python, 33 unit tests) |
1. Abstract
The Apache Magpie framework lets agents drive ASF security workflows that read two distinct classes of private mail: external reporters’ mail to a project’s <security-list> and PMC-internal mail on <private-list>. Both classes must not leak through any LLM in the active stack — but they require different remediations, and a single conflated mechanism would either over-block (refuse to process <security-list> content needlessly) or under-protect (let <private-list> bodies flow through arbitrary LLMs).
This RFC describes — and the linked PRs implement — a two-mechanism design:
- A PII redactor that swaps third-party identifiers in
<security-list>mail for hash-prefixed tokens before any LLM step, with a deterministic local mapping that is reversed only at the outbound boundary. - An approved-LLM pre-flight gate that refuses to fetch
<private-list>content unless every LLM in the active stack matches the framework’s default-approved registry or an adopter-declared opt-in entry.
The reporter’s own identity flows through the agent’s context as-is, by design — they sent the mail and are operationally known to the security team. Collaborators on the project’s <tracker> repo are similarly exempt: their identity is already public via collaborator status.
Both mechanisms are now landed: the redactor (PR #48 + PR #50) and the gate-check (PR #51). The full design is shipped, end-to-end, behind explicit Step 0 pre-flight calls in every <security-list>-touching skill.
Complementary network-layer control. The two mechanisms above operate at the application layer — they decide what a skill deliberately sends to an LLM. They do not, by themselves, stop private data from leaving over an arbitrary HTTP call (a buggy tool, or a prompt-injection payload that coaxes the agent into exfiltration). §4.4 adds an optional egress-allowlist gateway (tools/egress-gateway/) as defence-in-depth: a default-deny host allowlist that funnels all tool egress through a single chokepoint, so private data physically cannot reach a non-sanctioned host even if a higher layer is bypassed. It is layered under the two LLM-routing mechanisms, not a replacement for them.
2. Background and motivation
ASF security work routinely handles two kinds of private content:
<security-list>mail. External reporters send vulnerability reports to the project’ssecurity@list. The reporter is a known correspondent (the team replies, attributes credit, and references them across the tracker discussion). The body, however, frequently mentions third parties — a co-researcher, a victim the reporter observed, a named individual called out in the body — whose identities are not operationally needed by the security team and which absolutely should not flow through arbitrary LLMs.<private-list>mail. PMC-private foundation lists (private@<project>.apache.organd any cross-project relay lists the security team subscribes to) are wholly private. Every byte — body and metadata alike — is sensitive.
A single mechanism cannot serve both. PII redaction is necessary but insufficient for <private-list> (the body itself is private, not just the identifiers). An approved-LLM gate alone is insufficient for <security-list> (an LLM may be approved to receive the body but third-party PII still belongs in the local map, not in any LLM’s context window or inference logs).
Two earlier candidate designs were rejected:
- “Just redact everything before any LLM call.” This loses the reporter’s identity, which the team needs operationally for replies, CVE credit, and cross-skill handoff. It also creates a much larger attack surface for over-redaction false positives in code excerpts (CVEs, IPs that identify production servers, etc.).
- “Just gate every LLM call against an allowlist.” This misses third-party PII inside
<security-list>content — that mail is allowed through approved LLMs, but the third-party identifiers are still in the body unredacted.
The two-mechanism design lets each remedy do what it is good at, and explicitly separates what is gated from what is redacted.
3. Goals and non-goals
Goals
- G1. Third-party PII in
<security-list>mail never enters an LLM’s context in the clear. - G2.
<private-list>content cannot reach any LLM the adopter has not explicitly approved. - G3. The reporter’s own identity continues to flow through normally — replies, CVE credits, sync comments work as before.
- G4. Adopting a stricter posture (e.g. “redact collaborators too”) is a single config flip, not a code change.
- G5. Adding a new LLM hop (summariser, classifier, delegated-analysis) is a deliberate act with PMC sign-off, not something a skill can grow into silently.
- G6. The mechanism is cross-cutting: hosting it under
tools/gmail/would couple it to one fetch backend, hosting it under any single skill would create N drifting copies. A dedicatedtools/privacy-llm/directory keeps the contract in one place.
Non-goals
- N1. A content classifier. The redactor does not guess which strings are PII; the calling skill identifies them explicitly via
--field <type>:<value>arguments. - N2. A replacement for the existing public-surface confidentiality rules in
AGENTS.md. Those govern human-visible surfaces (public PRs, public issue comments, public mail replies); privacy-llm governs machine-routed surfaces. Both apply, layered. - N3. An MCP-layer interception. Claude Code’s MCP runtime does not (yet) support per-tool transformation hooks, so the redactor and gate-check run as explicit steps inside the skill. If a future MCP gains hook support, the call points can move into the hook without changing the contract.
- N4. A ratified ASF-wide approved-model registry. The default-approved list reflects the framework maintainer’s working position pending ASF Privacy VP/Legal VP guidance; see §9.
4. Design
4.1 The two mechanisms at a glance
| Data class | Source | What privacy-llm does |
Gate runs at |
|---|---|---|---|
<security-list> body — reporter’s own PII |
Gmail / PonyMail public archive | Not redacted. Reporter is operationally known; identity flows through context as-is. | n/a |
<security-list> body — third-party PII |
Gmail / PonyMail | Redacted. Names, emails, phones, IPs, personal handles of non-reporter, non-collaborator individuals replaced with N-…, E-…, P-…, IP-…, H-…, A-… identifiers. Mapping kept local; never sent to any LLM. |
Immediately after fetch, before any further processing. |
<private-list> content |
Gmail / PonyMail (PMC-private archive) | Pre-flight gate. Refuse to fetch unless every entry in the active LLM stack is in the approved-model registry. No redaction (the body is private as a whole). | Step 0 pre-flight on every skill that may read a <private-list> thread. |
| Outbound drafts referencing redacted third parties | Skill draft assembly | Reverse identifiers → real values just before the draft is written, only for identifiers actually referenced in the draft. | Final assembly, after the LLM step that composed the draft body. |
4.2 Mechanism 1 — PII redactor
Field types and identifier shape
| Field | Code | Identifier example | Sources skills should redact |
|---|---|---|---|
| Third-party name | N |
N-a3f9d2 |
Body, signature lines, CVE credit fields, HackerOne/GHSA fields — names of non-reporter, non-collaborator individuals. |
| Email address | E |
E-b8c247 |
Same scope. Reporter’s own From: is not redacted. |
| Phone number | P |
P-7d4e91 |
Third-party signature blocks; “call me at” patterns. |
| IP address (v4/v6) | IP |
IP-1a5cef |
Reproducer logs; “I tested from” lines. Not IPs that identify a vulnerable production server. |
| Personal handle | H |
H-9e3b04 |
Personal GitHub/Twitter/IRC/Slack handles of third parties (not the reporter, not collaborators). |
| Postal/employer address | A |
A-… |
“I work at”/“my address is” lines referring to non-reporter individuals. |
The identifier format is <TYPE>-<6-char-lowercase-hex> where the hex is sha256(value.strip().lower())[:24-bits]. The 6-char default gives ~16M slots before collision pressure becomes meaningful — comfortably above any single ASF project’s lifetime PII volume. On collision (two distinct values hashing to the same prefix), the second-detected value’s hex is extended in 8-bit increments (N-a3f9d2ab, N-a3f9d2abcd, …) until the new identifier is unique. Extension is permanent for that mapping entry.
Determinism and idempotency
- Deterministic.
pii-redact name:"Jane Smith"produces the sameN-…on every machine and every run, because the identifier is derived from a normalised hash of the value. The mapping file is convenience storage forpii-reveal; the identifier itself is reproducible without it. - Idempotent. Running
pii-redacttwice on the same input with the same--fieldvalues writes the mapping file once and produces identical output the second time. - Cross-machine compatible. Two contributors redacting the same body produce the same identifier text without sharing the mapping file. Reveal is per-machine: a contributor can only reveal identifiers in their own local map; identifiers others created pass through unchanged (no risk of collision-corruption).
Mapping store
Path: ~/.config/apache-magpie/pii-mapping.json — outside the project tree, per the framework’s home-dir tool-credentials rule.
Format:
{
"version": 1,
"entries": {
"N-a3f9d2": {"type": "name", "value": "Jane Smith"},
"E-b8c247": {"type": "email", "value": "jane.smith@example.com"}
}
}
Properties:
- File mode
0o600, atomic writes (tempfile + os.replace). - Per-machine, never committed.
- Append-only in normal operation. Manual
rmis supported but loses the reverse mapping; the agent has to re-fetch source data to rebuild on demand.
Skill wiring — the redact-after-fetch protocol
Every <security-list>-touching skill follows this canonical sequence:
- Resolve collaborators —
gh api repos/<tracker>/collaborators --jq '.[].login'. Same source-of-truth as the prompt-injection rule’s “who is authorised to instruct the agent” lookup. - Identify third-party PII candidates in the body, signature lines, CVE credit fields, HackerOne/GHSA fields.
- Filter the candidate set: drop the reporter’s own values, drop every collaborator. What remains is the should-be-redacted set — third-party PII that is neither the reporter nor a collaborator.
- Call the redactor with
--field <type>:<value>for each remaining candidate. - Use the redacted body for all subsequent processing. 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 a skill is about to emit a body that (a) carries a redacted third-party identifier AND (b) is destined for a surface that needs the real value (a draft reply to the reporter, a CVE credit line), pii-reveal runs once on the rendered text right before the send tool is called. Reveal does not run on internal status comments / sync messages where the redacted form is fine for the security team.
4.3 Mechanism 2 — approved-LLM gate
The default-approved registry
Four classes are pre-approved by the framework:
| Class | Rationale | Examples |
|---|---|---|
| Claude Code itself | The Claude Code instance running framework skills is treated as approved for the data it directly processes. See §9 for the limits of this default. | The agent invoking the skill. The checker matches the case-insensitive substring claude code in the bullet’s raw text. |
*.apache.org endpoints |
Anything served from an ASF domain runs on infra under ASF governance — data residency, retention, access bounded by the ASF infra agreement. | Future ASF inference endpoint at e.g. inference.apache.org. |
| Local-only inference | Data never leaves the user’s machine. | Ollama / vLLM / llama.cpp on 127.0.0.1, localhost, ::1. |
| Air-gapped on-prem | Same rationale as local, scaled to the contributor’s organisation, on infra the adopter operationally controls and which has no path to a third-party LLM operator. | PMC-hosted inference appliance on a private VLAN. |
The opt-in tier
Every other LLM endpoint requires explicit declaration in <project-config>/privacy-llm.md, with three required fields per entry:
- the endpoint URL (or provider product name);
- the data-residency / retention contract that backs the choice (link to a contract clause, vendor doc, or BAA-equivalent);
- the security-team member who approved the addition (
Approved-by: <initials> <YYYY-MM-DD>).
The framework intentionally does not ship a curated allow-list of third-party endpoints. The opt-in mechanism puts the choice — and the responsibility — on the adopting project’s security team, where ASF policy expects it to live.
The gate-check rejects placeholder text in the Approved-by line — strings containing <pmc-member-initials>, <initials>, <yyyy-mm-dd>, or the literal yyyy-mm-dd are not accepted as a valid sign-off. An adopter that copies the template and forgets to fill in the approver gets a clear failure rather than a silent pass.
Setup recipes in docs/setup/privacy-llm.md cover six concrete variants: Claude Code only, Local Ollama, Local vLLM, Apache-hosted endpoint, AWS Bedrock (opt-in), Direct Anthropic API (opt-in).
The pre-flight check
Skills run this check at Step 0 by shelling out to the privacy-llm-check console script (PR #51):
uv run --project <framework>/tools/privacy-llm/checker privacy-llm-check \
--reads-private-list # set when the skill may read <private-list>
The checker:
- Locates the config. Precedence:
--config <path>→$PRIVACY_LLM_CONFIG→<cwd>/.apache-magpie/privacy-llm.md→<cwd>/.apache-magpie-overrides/privacy-llm.md. - Parses the Currently configured LLM stack and Approved third-party endpoints (opt-in) sections. The parser is permissive about comments and whitespace but strict about the section-heading anchors.
- Applies the rules for every active-stack entry:
- Claude Code → ✓ default-approved
- URL host ending in
.apache.org→ ✓ default-approved - URL host in
{localhost, 127.0.0.1, ::1}→ ✓ default-approved - Otherwise: match against the opt-in registry. A valid match requires a non-empty Data-residency contract sub-bullet AND a non-placeholder Approved-by sub-bullet.
- Returns an exit code:
0— all entries approved.1— one or more entries unapproved (or empty stack); stderr lists the offending entries plus aFix: edit <path>pointer.2— config file could not be located or parsed.
The check is deliberately conservative: any single unapproved entry stops the skill. Adding a new LLM hop is a deliberate act, not an emergent one.
Defence-in-depth: the gate-check is also required for <security-list>-only skills, even though their body classification permits Claude-Code-default LLMs by construction. Running the check at Step 0 ensures the adopter’s config is in a sane state — no half-configured opt-in entries, no LLMs in the active stack the adopter forgot to approve — before any private content flows. The --reads-private-list flag controls only the printed banner; the validation logic is the same either way.
4.4 Mechanism 3 (defence-in-depth) — egress-allowlist gateway
The PII redactor and approved-LLM gate both operate at the application layer: they constrain what a skill deliberately sends to an LLM. Neither stops an unintended outbound flow — a buggy skill, a mis-wired tool, or a prompt-injection payload hidden in an inbound report that coaxes the agent into curl-ing private data to an attacker-controlled host. docs/setup/secure-agent-setup.md flags exactly this: network egress via Bash(curl *) / Bash(wget *) bypasses the sandbox’s own proxy.
The egress-allowlist gateway closes that gap at the network layer. It is a local proxy.py forward proxy (shipped as tools/egress-gateway/) that enforces a default-deny host allowlist in its before_upstream_connection hook: any CONNECT / request to a host not on the allowlist is rejected with 403 before a socket is opened. Tools point HTTPS_PROXY / HTTP_PROXY at it; Python urllib-based tools (ponymail, whimsy, jira, …) honour that with no code change.
| Property | Value |
|---|---|
| Layer | Network egress (host-level), below the application-layer LLM controls |
| Policy | Default-deny; allowlist mirrors sandbox.network.allowedDomains (ASF infra, GitHub, Google APIs, PyPI), suffix-matched; loopback always allowed; adopter extends via EGRESS_ALLOW_EXTRA |
| Granularity | Host only — HTTPS is tunnelled via CONNECT, so no URL-path or payload inspection (no TLS interception) |
| Relationship | Defence-in-depth. Layered under mechanisms 1 + 2, never a replacement: the redactor still strips third-party PII, the gate still bounds which LLM may receive a body, and the gateway additionally bounds which host any tool may reach. |
The gateway runs outside the sandbox — it must bind a listener and make unrestricted outbound, which is precisely its job as the chokepoint. Sandboxed tools reach it over loopback, which requires localhost / 127.0.0.1 in sandbox.network.allowedDomains (loopback-only; this does not widen the internet egress surface — that becomes the gateway’s responsibility). The gateway’s allowlist and sandbox.network.allowedDomains encode the same egress policy at two layers and should be kept in sync.
This mechanism is optional and provisional: it ships as a tool with a documented contract and unit-tested allowlist policy, but it is not yet wired into a setup skill or the privacy-llm-check gate. See §10.6.
5. Data flow
┌─────────────────────┐
fetch (Gmail / ──┤ raw body + PII │
PonyMail) └──────────┬──────────┘
│ pii-redact (per-field, after collaborator filter)
▼
┌──────────────────────┐
│ body w/ identifiers │ ◄─── what Claude / any
└──────────┬───────────┘ downstream LLM ever sees
│
…agent processing,
draft composition,
cross-skill handoff…
│
▼
┌──────────────────────┐
│ draft w/ identifiers │
└──────────┬───────────┘
│ pii-reveal (only at outbound boundary)
▼
┌──────────────────────┐
│ draft w/ real names │ ──► sent to reporter
└──────────────────────┘
Three rules govern the lifecycle:
- Redact immediately after fetch. The window between the
mcp__claude_ai_Gmail__get_thread(or equivalent) tool call and the redact call is a single tool invocation wide. - Operate on identifiers throughout. All intermediate work (analysis, summarisation, draft composition, prior-art lookup, cross-skill handoff) runs against
<TYPE>-<hex>text. - Reveal only at the outbound boundary.
pii-revealruns exactly once per draft, at the moment the rendered draft is handed to the send/draft-create tool. It does not run while the agent is thinking about the draft — only when the bytes are leaving the framework.
The gate-check (privacy-llm-check) sits upstream of this pipeline at Step 0 — before any fetch, before any redact, before the agent has touched private data at all. Its failure mode is “skill never starts”; its success mode is “skill starts and the redactor takes over downstream”.
6. Implementation
tools/privacy-llm/ ships two stdlib-only uv Python sub-tools, each with its own pyproject.toml, lock file, and test suite.
6.1 The redactor sub-tool — tools/privacy-llm/redactor/
Three console scripts:
| Script | Purpose |
|---|---|
pii-redact |
Replace declared PII values in stdin with identifiers; persist new mappings to the local file. |
pii-reveal |
Replace identifiers in stdin with stored real values from the local mapping. |
pii-list |
Print the current mapping for debugging (text or JSON). |
Three call sites in skill files:
# Redact (immediately after fetch):
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"
# Reveal (only at outbound boundary):
echo "$DRAFT" | uv run --project <framework>/tools/privacy-llm/redactor pii-reveal
# List (debugging only — output goes to user's terminal, never to LLM):
uv run --project <framework>/tools/privacy-llm/redactor pii-list
<framework> is the standard placeholder convention — substitutes to the snapshot path inside an adopter, or to . standalone. The redactor reads no config file: it just does what the caller passes via --field. Per-project knobs are applied by the calling skill (see §7).
The implementation is stdlib-only by design — argparse, hashlib, json, pathlib, tempfile, os. No third-party runtime dependencies. The dev group adds pytest, ruff, mypy for lint and test. Test count: 48 unit tests, all passing.
6.2 The checker sub-tool — tools/privacy-llm/checker/ (PR #51)
One console script:
| Script | Purpose |
|---|---|
privacy-llm-check |
Parse <project-config>/privacy-llm.md, verify every entry in the Currently configured LLM stack section is approved per the rules in models.md. |
The internal structure is two modules:
checker/config.py— parses the markdown config file intoLLMEntryandOptInEntrydataclasses. Permissive about comments and whitespace; strict about the heading anchors## Currently configured LLM stackand## Approved third-party endpoints (opt-in)since those are the contract surfaces the gate-check relies on.checker/check.py— applies the default-approval rules (Claude Code substring match,*.apache.orghost suffix, local-host set), falls back to the opt-in registry, and produces aVerdictper stack entry with a human-readable reason.
Skill invocation pattern:
# Default lookup against <cwd>/.apache-magpie/privacy-llm.md or
# <cwd>/.apache-magpie-overrides/privacy-llm.md:
uv run --project <framework>/tools/privacy-llm/checker privacy-llm-check \
--reads-private-list
# Sample success output (stdout):
# privacy-llm-check: every active-stack entry is approved (skill reads <private-list>)
# ✓ Claude Code (the agent running framework skills) — Claude Code itself (default-approved)
# ✓ Local Ollama at http://127.0.0.1:11434/ — local-only inference at 127.0.0.1 (default-approved)
# Sample failure (stderr, exit 1):
# privacy-llm-check: 1 of 2 active-stack entries are not approved.
# ✓ Claude Code (the agent running framework skills) — Claude Code itself (default-approved)
# ✗ AWS Bedrock at https://bedrock-runtime.eu-central-1.amazonaws.com — no default-approval rule matches and no opt-in entry was declared for this LLM. Add an entry under 'Approved third-party endpoints (opt-in)' with a Data-residency contract line and an Approved-by sign-off, or remove this LLM from the active stack.
#
# Fix: edit /repo/.apache-magpie/privacy-llm.md per tools/privacy-llm/models.md.
Implementation: stdlib-only (argparse, dataclasses, re, urllib.parse, pathlib). Test count: 33 unit tests, all passing, including a fixture test that the shipped projects/_template/privacy-llm.md parses + approves out of the box. Pre-commit hooks (ruff, ruff-format, mypy, pytest) wired into the framework’s prek config in PR #51.
6.3 What never reaches any LLM
The framework treats these surfaces as off-limits to LLM context, even when an “approved” LLM is in the stack:
- The contents of
~/.config/apache-magpie/pii-mapping.json. The file is read bypii-redact/pii-revealonly. Skills MUST NOT include the mapping in any LLM-bound prompt, summary, or status comment. For debugging, runpii-listin the user’s terminal — that output goes to the user’s screen, not to Claude’s context. - The
--field <type>:<value>arguments themselves. Every value passed there is exactly what the redactor is replacing. - Any draft text before
pii-revealruns, when the destination is a non-internal surface (e.g. a public PR comment) — the body would still carry identifiers, which leak no PII, but skills should not emit identifier-laden drafts to non-internal destinations by accident. The destination check in the approved-LLM gate is a separate safety net for this.
6.4 The egress gateway — tools/egress-gateway/
A proxy.py-based forward proxy whose only first-party code is the allowlist plugin (egress_gateway.allowlist.EgressAllowlistPlugin). The host-matching policy (host_allowed) is a pure function, unit-tested in isolation; the proxy.py integration is intentionally not exercised in CI (it needs to bind a port). Unlike the stdlib-only privacy-llm sub-tools, this one carries a third-party runtime dependency (proxy.py) — which is why it is a separate tool rather than a privacy-llm sub-tool. Contract: tools/egress-gateway/tool.md; how-to: tools/egress-gateway/README.md.
7. Adopter configuration
Adopters declare their privacy-LLM posture in a single markdown file at <project-config>/privacy-llm.md (template at projects/_template/privacy-llm.md). The file has four sections:
- Currently configured LLM stack — every LLM the adopter has wired into any skill, one per line.
- Approved third-party endpoints (opt-in) — entries beyond the default-approved set, each with the data-residency contract link and PMC
Approved-byline. The checker rejects placeholder text (<initials>,<YYYY-MM-DD>, …). - Private mailing lists — every PMC-private list the security team reads. The framework’s
tools/ponymail/reuses this list for itsprivate_listsconfig knob, so the two stay in sync. - Redaction configuration — three per-project knobs:
| Knob | Default | Purpose |
|---|---|---|
collaborator_source |
<tracker> from <project-config>/project.md |
Override if collaborators are tracked in a different repo (parent-org roster, separate roster repo). |
collaborator_exemption |
enabled |
Flip to disabled for a stricter posture: every non-reporter individual gets redacted, including collaborators. |
redaction_field_types |
all six | Disable individual types if a project has decided a different sensitivity tradeoff (rare). |
The redactor itself reads no config — knobs are applied by the calling skill at filter time, before --field arguments are constructed. The checker reads only the Currently configured LLM stack, Approved third-party endpoints (opt-in) sections — the other knobs are skill-side concerns. A skill that does not respect a knob is a framework bug.
8. Skill wiring summary
Every skill that touches <security-list> (or may escalate to <private-list>) carries a Step 0 Privacy-LLM contract bullet that calls privacy-llm-check, plus the redact-after-fetch and (where applicable) reveal-before-send steps.
| Skill | Reads | Drafts | Step 0 gate-check | Redact-after-fetch | Reveal-before-send |
|---|---|---|---|---|---|
security-issue-import |
<security-list> |
reporter receipt-of-confirmation reply | ✓ | ✓ | ✓ |
security-issue-sync |
<security-list>, may escalate to <private-list> |
reporter status updates | ✓ (--reads-private-list) |
✓ | ✓ |
security-issue-invalidate |
<security-list> |
reporter invalidation reply | ✓ | ✓ | ✓ |
security-cve-allocate |
tracker + Vulnogram | (tracker already redacted) | ✓ | n/a (downstream of redaction) | n/a |
security-issue-import-from-md |
adopter-supplied markdown | n/a | ✓ | n/a | n/a |
security-issue-import-from-pr |
public PR | n/a | n/a (no <security-list> content) |
n/a | n/a |
security-issue-fix |
tracker (already redacted) | n/a (PR is public; no PII) | n/a | n/a | n/a |
security-issue-deduplicate |
two trackers (already redacted) | n/a | n/a | n/a | n/a |
Only security-issue-sync passes --reads-private-list today (it may escalate threads to PMC-private foundation lists). The other wired skills run the checker without the flag — the validation logic is the same; the flag only affects the printed banner.
9. Trust boundaries and status
The default-approved registry reflects the framework maintainer’s working position pending ASF Privacy VP/Legal VP ratification of an authoritative approved-LLM list for foundation private data. Specifically:
- The “Claude Code itself” default reflects the framework maintainer’s current trust posture. If ASF Privacy VP/Legal VP subsequently rules that Anthropic-hosted endpoints require a data-processing agreement for foundation private data, the framework will narrow this default and bump the registry version. Adopters running Variant 1 (Claude Code only) at that point will need to re-evaluate.
- The
*.apache.orgblanket approval assumes infra-level governance. If a future ASF endpoint runs at*.apache.orgbut proxies to a third-party LLM, that endpoint may need re-classification.
When ASF Privacy VP/Legal VP do ratify a list, tools/privacy-llm/models.md becomes the pointer to that list rather than the list itself, and the default-approved entries get re-checked against it. Until then, that file is the framework’s source-of-truth for adopters and the rationale-of-record for the choices it encodes.
PMC members and ASF Privacy VP/Legal VP reviewers who want to formalise the list should open an issue on apache/magpie referencing this RFC.
10. Open questions and future work
10.1 Resolved in PR-3
Earlier drafts of this RFC listed “gate-call wiring” as deferred to PR-3. PR #51 has now landed it: the tools/privacy-llm/checker/ sub-tool ships privacy-llm-check with the full default-approved logic, opt-in matching, placeholder rejection, and config auto-location. Every Gmail-touching skill calls it explicitly at Step 0. The full two-mechanism design is now live, end-to-end.
10.2 ASF Privacy VP/Legal VP ratification
The single largest remaining open question is the ASF-wide policy for AI-assisted handling of foundation private data. The framework’s working position is documented and adopter-overridable, but a ratified list would let the framework bump from “provisional” to “stable” and remove the burden of per-project sign-off for default-approved entries.
Concrete asks for the ASF Privacy VP / Legal VP:
- Confirm or narrow the “Claude Code itself” default. Today the framework treats the running Claude Code instance as approved for the data it processes. A formal data-processing agreement between ASF and Anthropic for foundation private data would make this stable; absence of one might narrow the default to “Claude Code is approved only for
<security-list>content, never for<private-list>”. - Confirm the
*.apache.orgblanket. The framework assumes any endpoint at an ASF domain runs under ASF infra governance. A formal articulation of what that means (including whether*.apache.orgproxies to third parties are permitted) would let the framework codify the boundary precisely. - Publish a curated allow-list of opt-in endpoints, if desired. The framework currently leaves the list open and shifts the responsibility to per-project security teams. A foundation-wide list would centralise the diligence.
10.3 MCP-layer hooks
If a future Claude Code MCP runtime gains per-tool transformation hooks, the redactor and gate-check call points can move from explicit-step-inside-the-skill into the hook without changing the contract. The current explicit-step design is forward-compatible with that migration: the --field <type>:<value> interface decouples what to redact (skill knowledge) from how to redact (helper logic), and the checker reads the same <project-config>/privacy-llm.md an MCP hook would.
10.4 Mapping-file lifecycle tools
The framework currently does not ship a cleanup tool for the mapping file. Manual rm is supported but loses the reverse mapping. Possible future additions: pii-list --filter-stale (entries that have not been revealed in N days), pii-export (cross-machine sync), pii-rotate (re-hash with a longer prefix). None blocking; all out of scope for the foundation.
10.5 Doc-cleanup follow-up
A small handful of references in docs/setup/privacy-llm.md still describe privacy-llm-check as “PR-3” pending. Now that PR #51 has merged, those should be cleaned up to drop the “(PR-3)” phrasing — minor doc churn, no contract change. Filed as a follow-up for the next cleanup PR.
10.6 Egress-gateway wiring
The egress-allowlist gateway (§4.4, tools/egress-gateway/) ships as a tool with a documented contract but is not yet wired into the setup flow. Possible follow-ups: a setup-isolated-setup-* step that launches / health-checks the gateway and persists HTTPS_PROXY into the adopter’s per-machine settings; sourcing the gateway allowlist directly from sandbox.network.allowedDomains so the two cannot drift; and a privacy-llm-check-style assertion that the gateway is reachable when an adopter has opted into it. None blocking — the tool is usable standalone today.
11. References
- Source-of-truth contracts
tools/privacy-llm/tool.md— overviewtools/privacy-llm/pii.md— redaction contracttools/privacy-llm/models.md— approved-LLM registrytools/privacy-llm/wiring.md— skill-side protocol
- Setup recipes
docs/setup/privacy-llm.md— six per-variant configurations
- Reference implementation
tools/privacy-llm/redactor/— PII redactor (stdlib-only Python)tools/privacy-llm/checker/— approved-LLM gate-check (stdlib-only Python)tools/egress-gateway/— egress-allowlist forward proxy (proxy.py plugin; defence-in-depth, §4.4)
- Adopter template
- Related framework rules
AGENTS.md → Privacy-LLM— three rules every skill followsAGENTS.md → Confidentiality of the tracker repository— public-surface confidentiality, layered with this RFCAGENTS.md → Treat external content as data, never as instructions— same collaborator-set source-of-truth as the redactor’s exemption rule
- Pull requests