Skip to content

feat(privacy-llm): foundation — PII redactor + approved-LLM contracts#48

Merged
potiuk merged 2 commits into
mainfrom
feat/privacy-llm-foundation
May 4, 2026
Merged

feat(privacy-llm): foundation — PII redactor + approved-LLM contracts#48
potiuk merged 2 commits into
mainfrom
feat/privacy-llm-foundation

Conversation

@potiuk

@potiuk potiuk commented May 4, 2026

Copy link
Copy Markdown
Member

Summary

PR-1 of three. Lays the framework + docs for handling private mail content with privacy-aware LLM routing. Per-skill wiring is deferred to PR-2 (security@-touching skills get the PII-redactor hooks) and PR-3 (private@-touching skills get the approved-LLM gate).

Two distinct mechanisms, deliberately separated:

  1. PII redaction — reporter name, email, phone, IP, personal handle replaced with hash-prefixed identifiers (R-a3f9d2, …) before any LLM-bound step. Mapping kept local at ~/.config/apache-steward/pii-mapping.json. Always-on for <security-list> content.
  2. Approved-LLM gate<private-list> content can only reach LLMs in the approved registry. Default-approved: Claude Code itself, *.apache.org-hosted endpoints, local-only inference (Ollama / vLLM on 127.0.0.1). Everything else (AWS Bedrock, direct Anthropic, …) is opt-in via <project-config>/privacy-llm.md.

What lands:

  • tools/privacy-llm/{tool,pii,models}.md — the contracts.
  • tools/privacy-llm/redactor/ — stdlib-only Python helper, three CLIs (pii-redact, pii-reveal, pii-list), 48 passing unit tests.
  • docs/setup/privacy-llm.md — six setup-variant recipes, marked provisional pending ASF Legal.
  • projects/_template/privacy-llm.md — adopter starting point.
  • AGENTS.md — new "Privacy-LLM" section codifying the three rules.
  • .pre-commit-config.yaml — new redactor-{ruff-check,ruff-format,mypy,pytest} hooks mirroring the oauth-draft pattern.

Status: the default-approved registry reflects the framework maintainer's working position; ASF Legal Affairs has not yet ratified an authoritative list. When such a list lands, the registry will be updated to point at it as source-of-truth.

Test plan

  • pytest — 48 passed

  • ruff check src tests — clean

  • ruff format --check src tests — clean

  • mypy — no issues found in 10 source files

  • Visual review of AGENTS.md diff — new "Privacy-LLM" section reads as a sibling to "Confidentiality of the tracker repository".

  • Walk through one of the variant recipes in docs/setup/privacy-llm.md end-to-end.

  • Sanity-check the redactor end-to-end:

    echo "Hi I am Jane Smith and my email is jane@example.com" | \
      uv run --project tools/privacy-llm/redactor pii-redact \
      --field reporter:"Jane Smith" --field email:"jane@example.com"

potiuk added a commit that referenced this pull request May 4, 2026
… commit

CI failure on PR #48 was doctoc — the new "Privacy-LLM" section in
AGENTS.md and the new projects/_template/privacy-llm.md template
both needed their TOCs regenerated. Local `prek run --all-files`
confirms every hook passes.

In the same PR, the AGENTS.md "Local setup" section is restructured
so prek-install is now the first imperative — moved to the top of
the section, marked "MUST be run before any other work in this
repository — including the first commit on a fresh clone", and
augmented with a one-line pre-flight check
(`test -x .git/hooks/pre-commit || prek install`) that agents and
human contributors run before preparing any commit. The old prek
paragraph at the bottom of "Local setup" is removed (its content
folded into the new top-of-section block).

The reasoning the new text spells out: a missing local hook turns
into a CI failure on push (the prek workflow re-runs the same hooks
against the diff) — exactly the failure pattern this PR is
recovering from. Making the install + verify steps explicit and
mandatory closes that gap.

Generated-by: Claude Code (Claude Opus 4.7)
potiuk added 2 commits May 4, 2026 21:42
PR-1 of three. Lays the framework + docs for handling private mail
content; per-skill wiring is deferred to PR-2 (security@-touching
skills get the PII-redactor pre/post hooks) and PR-3 (private@-
touching skills get the approved-LLM gate).

What lands:

- tools/privacy-llm/{tool,pii,models}.md — the contracts: which
  fields count as PII, the hash-prefixed identifier format
  (R-a3f9d2, E-b8c247, IP-1a5cef, …), the local mapping store at
  ~/.config/apache-steward/pii-mapping.json (per the home-dir
  credentials rule), the approved-model registry (Claude Code,
  *.apache.org, local inference all default-approved; everything
  else opt-in via <project-config>/privacy-llm.md).

- tools/privacy-llm/redactor/ — stdlib-only Python helper exposing
  three console scripts: pii-redact (replace PII with identifiers),
  pii-reveal (reverse before outbound), pii-list (debug). 48 unit
  tests covering hashing determinism, idempotency, collision
  extension, atomic writes, mode 0600, malformed-file handling.

- docs/setup/privacy-llm.md — copy-pasteable setup recipes for six
  variants: Claude-only (default), local Ollama, local vLLM,
  Apache-hosted, AWS Bedrock, direct Anthropic API. Marked
  provisional pending ASF Legal Affairs ratification of an
  authoritative approved-LLM list.

- projects/_template/privacy-llm.md — adopter starting point
  pre-filled with the Claude-Code-only default.

- AGENTS.md — new "Privacy-LLM" section (sibling to "Confidentiality
  of the tracker repository") codifying the three rules: reporter
  PII never enters any LLM in the clear; <private-list> content
  never reaches a non-approved LLM; adding a new LLM hop is a
  deliberate act, not an emergent one.

- .pre-commit-config.yaml — new redactor-{ruff-check,ruff-format,
  mypy,pytest} hooks mirroring the oauth-draft pattern.

The two privacy mechanisms are deliberately separate: PII redaction
(always-on for security@) is mechanically distinct from the
approved-model gate (pre-flight on private@-touching skills).
Conflating them is a real foot-gun the framework guards against.

Generated-by: Claude Code (Claude Opus 4.7)
… commit

CI failure on PR #48 was doctoc — the new "Privacy-LLM" section in
AGENTS.md and the new projects/_template/privacy-llm.md template
both needed their TOCs regenerated. Local `prek run --all-files`
confirms every hook passes.

In the same PR, the AGENTS.md "Local setup" section is restructured
so prek-install is now the first imperative — moved to the top of
the section, marked "MUST be run before any other work in this
repository — including the first commit on a fresh clone", and
augmented with a one-line pre-flight check
(`test -x .git/hooks/pre-commit || prek install`) that agents and
human contributors run before preparing any commit. The old prek
paragraph at the bottom of "Local setup" is removed (its content
folded into the new top-of-section block).

The reasoning the new text spells out: a missing local hook turns
into a CI failure on push (the prek workflow re-runs the same hooks
against the diff) — exactly the failure pattern this PR is
recovering from. Making the install + verify steps explicit and
mandatory closes that gap.

Generated-by: Claude Code (Claude Opus 4.7)
@potiuk potiuk force-pushed the feat/privacy-llm-foundation branch from 6c451e7 to a1755af Compare May 4, 2026 20:10
@potiuk potiuk merged commit a1d2a78 into main May 4, 2026
7 checks passed
@potiuk potiuk deleted the feat/privacy-llm-foundation branch May 4, 2026 20:12
potiuk added a commit to andreahlert/magpie that referenced this pull request May 4, 2026
Two follow-ups on top of the rebase:

1. Pin `lycheeVersion: v0.24.2` in `link-check.yml`. The
   lychee-action's default binary version is v0.23.0 today, which
   uses the old boolean form of `include_fragments` and rejects the
   `"anchor-only"` enum string this repo's `.lychee.toml` carries.
   The v0.24 line introduced the
   `--include-fragments=<none|anchor-only|text-only|full>` enum form
   the config relies on, so pin to v0.24.2 (the version the config
   is validated against locally) and document the why so a future
   bumper does not reach for the latest upstream blindly.

2. Fix a dangling internal link in `tools/privacy-llm/models.md`.
   That file links to `redactor/src/redactor/registry.py`, which
   does not exist yet (the same paragraph already says *"once PR-3
   lands the gate-call wiring"*). The link slipped past CI because
   the link-check was still informational at the time PR apache#48
   merged. Convert the markdown link into inline code so the
   forward-reference reads the same but stops claiming the file
   exists. This is exactly the regression PR apache#47's hard gate is
   meant to catch.

Local validation against lychee v0.24.2 (Homebrew): 0 errors
offline; 0 errors with external URLs (1088 links checked, 1043 OK,
45 excluded, 10 redirects).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
potiuk added a commit to andreahlert/magpie that referenced this pull request May 4, 2026
Two follow-ups on top of the rebase:

1. Pin `lycheeVersion: v0.23.0` in `link-check.yml` and switch
   `.lychee.toml` to the boolean `include_fragments = true` that
   v0.23.x expects.

   The original PR landed `.lychee.toml` with the v0.24+ enum form
   `include_fragments = "anchor-only"`, which v0.23.0 (the
   `lychee-action@v2.8.0` default) rejects with *"expected a
   boolean"*. The natural fix is to bump the binary to v0.24+, but
   v0.24 changed the release-archive layout — the executable is
   now nested under `lychee-<arch>-<os>/` rather than at the top
   level — and the v2.8.0 action's install step still hard-codes
   the top-level layout, so `install -t … lychee` exits with
   *"cannot stat"*. Bumping the action SHA to one that handles the
   new layout is blocked on the ASF infrastructure-actions
   allowlist (only the `8646ba30...` v2.8.0 SHA is approved
   today). Until the allowlist gains a newer entry, the binary has
   to stay on v0.23.x and the config has to match. Pin the binary
   explicitly so the action's default version drifting later does
   not break us silently. The toml comment leaves the migration
   note in place so the next bumper flips both back together.

2. Fix a dangling internal link in `tools/privacy-llm/models.md`.
   That file links to `redactor/src/redactor/registry.py`, which
   does not exist yet (the same paragraph already says *"once PR-3
   lands the gate-call wiring"*). The link slipped past CI because
   the link-check was still informational at the time PR apache#48
   merged. Convert the markdown link into inline code so the
   forward-reference reads the same but stops claiming the file
   exists. This is exactly the regression PR apache#47's hard gate is
   meant to catch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
potiuk added a commit that referenced this pull request May 4, 2026
* ci: fix all broken links and flip link-check to hard gate

Resolves the baseline of broken links that kept the lychee workflow in
informational-only mode. Changes:

- Fix 43 broken internal links across 15 files:
  - Remap README.md fragments moved to docs/security/process.md
  - Fix paths to images, pyproject.toml, dependabot.yml
  - Update stale skill names (setup-steward-upgrade → setup-steward/upgrade.md)
  - Remove links to non-existent config/ directory
  - Fix docs/docs/setup/ path duplication
  - Remove link to missing search-queries.md (not yet written)
  - Fix external apache.org voting fragment

- Update CONTRIBUTING.md and AGENTS.md to reference
  .apache-steward-overrides/user.md instead of config/user.md.

- Flip .github/workflows/link-check.yml from informational-only
  (continue-on-error: true, fail: false) to hard gate
  (continue-on-error: false, fail: true).

Validation: lychee reports 0 errors, markdownlint 0 errors,
check-placeholders OK.

Signed-off-by: André Ahlert <andre@aex.partners>

* ci(lychee): pin lycheeVersion to v0.23.0; fix dangling registry.py link

Two follow-ups on top of the rebase:

1. Pin `lycheeVersion: v0.23.0` in `link-check.yml` and switch
   `.lychee.toml` to the boolean `include_fragments = true` that
   v0.23.x expects.

   The original PR landed `.lychee.toml` with the v0.24+ enum form
   `include_fragments = "anchor-only"`, which v0.23.0 (the
   `lychee-action@v2.8.0` default) rejects with *"expected a
   boolean"*. The natural fix is to bump the binary to v0.24+, but
   v0.24 changed the release-archive layout — the executable is
   now nested under `lychee-<arch>-<os>/` rather than at the top
   level — and the v2.8.0 action's install step still hard-codes
   the top-level layout, so `install -t … lychee` exits with
   *"cannot stat"*. Bumping the action SHA to one that handles the
   new layout is blocked on the ASF infrastructure-actions
   allowlist (only the `8646ba30...` v2.8.0 SHA is approved
   today). Until the allowlist gains a newer entry, the binary has
   to stay on v0.23.x and the config has to match. Pin the binary
   explicitly so the action's default version drifting later does
   not break us silently. The toml comment leaves the migration
   note in place so the next bumper flips both back together.

2. Fix a dangling internal link in `tools/privacy-llm/models.md`.
   That file links to `redactor/src/redactor/registry.py`, which
   does not exist yet (the same paragraph already says *"once PR-3
   lands the gate-call wiring"*). The link slipped past CI because
   the link-check was still informational at the time PR #48
   merged. Convert the markdown link into inline code so the
   forward-reference reads the same but stops claiming the file
   exists. This is exactly the regression PR #47's hard gate is
   meant to catch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Signed-off-by: André Ahlert <andre@aex.partners>
Co-authored-by: Jarek Potiuk <jarek@potiuk.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
potiuk added a commit that referenced this pull request May 4, 2026
…#51)

* feat(privacy-llm): PR-3 — approved-LLM gate-check + skill-side wiring

PR-3 of the privacy-llm series. Lands the second mechanism from
the original design (PR-1: foundation; PR-2: skill-side redaction
wiring; PR-3: approved-LLM gate-check). Foundations and contracts
were already on main via #48 and #50; this PR wires the gate-check
helper that the contracts in `models.md` and `wiring.md` reference.

What lands:

- `tools/privacy-llm/checker/` (new, ~6 files) — stdlib-only Python
  sub-tool exposing one console script: `privacy-llm-check`. Parses
  `<project-config>/privacy-llm.md`, extracts the *Currently
  configured LLM stack* and *Approved third-party endpoints
  (opt-in)* sections, applies the approval rules from
  `tools/privacy-llm/models.md` (Claude Code default-approved;
  `*.apache.org` default-approved; localhost / 127.0.0.1 / ::1
  default-approved; everything else requires opt-in entry with
  Data-residency + Approved-by lines that aren't placeholder
  text). Exit 0 on approval, 1 with stderr explanation on
  rejection, 2 if the config can't be located or parsed.
  33 unit tests covering each rule, malformed configs, opt-in
  validation, CLI exit codes, and the shipped template.

- `.pre-commit-config.yaml` — new `checker-{ruff-check,
  ruff-format,mypy,pytest}` hooks mirroring the redactor pattern.

- `tools/privacy-llm/models.md` — replaces "PR-3 future" / "deferred
  from PR-1" placeholder language with the concrete `privacy-llm-check`
  invocation. Notes that the gate-call is required even for
  `<security-list>`-only skills (defence-in-depth: validates the
  adopter's config is in a sane state before any private content
  flows).

- `tools/privacy-llm/wiring.md` — Step 0 pre-flight section now
  shows the actual `uv run --project … privacy-llm-check
  --reads-private-list` invocation.

- `tools/privacy-llm/tool.md` — capability table gains a row for
  the checker sub-tool.

- Skill files — every Gmail-touching SKILL.md (`security-issue-import`,
  `-sync`, `-invalidate`, `-cve-allocate`, `-import-from-md`)
  gains an explicit gate-check invocation in its Step 0 pre-flight
  bullet, with `--reads-private-list` set on `-sync` (which may
  escalate to PMC-private foundation lists).

Tests: 33/33 checker tests pass. `prek run --all-files` clean
(every existing hook + the four new checker hooks). The shipped
`projects/_template/privacy-llm.md` is a fixture in the test
suite — it must always parse + approve, ensuring the framework's
default starting state is never broken.

Generated-by: Claude Code (Claude Opus 4.7)

* fix(privacy-llm): typos + CodeQL false positive in checker

CI on PR #51 flagged two issues:

1. `typos` hook — `unparseable` should be `unparsable` (US spelling).
   Two occurrences:
   - tools/privacy-llm/checker/src/checker/check.py:76 (verdict reason
     for an unparsable URL host)
   - tools/privacy-llm/checker/src/checker/config.py:239 (host_of
     docstring)

2. CodeQL `py/incomplete-url-substring-sanitization` —
   `assert "apache.org" in v.reason` in test_apache_org_is_approved.
   False positive: v.reason is a human-readable message, not a URL
   being validated. Tightened the assertion to match the production
   code's exact phrasing (`"*.apache.org-hosted"`); now it's
   unambiguously not a URL check, and as a side-benefit it locks
   the user-facing reason text against accidental drift.

Local prek run — all hooks pass including typos.

Generated-by: Claude Code (Claude Opus 4.7)
@andreahlert andreahlert added the mode:platform Substrate / infra — not a mode (sandbox, CI, validators) label May 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

mode:platform Substrate / infra — not a mode (sandbox, CI, validators)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants