Skip to content

feat(pii): PII egress policies — PiiPolicyEngine egress_text decorator (Step 6.5) - #152

Merged
officialCodeWork merged 1 commit into
mainfrom
build/phase-6/step-6.5-pii-egress-policies
Jun 7, 2026
Merged

feat(pii): PII egress policies — PiiPolicyEngine egress_text decorator (Step 6.5)#152
officialCodeWork merged 1 commit into
mainfrom
build/phase-6/step-6.5-pii-egress-policies

Conversation

@officialCodeWork

Copy link
Copy Markdown
Owner

Summary

Step 6.5 enforces per-tenant PII policy at egress. Step 1.7 already detects + redacts PII at ingest and Step 6.1 resolves each tenant's pii_policy onto RequestContext.pii_policy — but nothing enforced it on the way out. The egress_text PDP decision existed and the gateway already calls it at every generation boundary, yet the default engine answered allow.

  • PiiPolicyEngine (rag-pii) is a PolicyEngine decorator — like QuotaPolicyEngine / AclPolicyEngine — that answers egress_text and applies ctx.pii_policy.action: allow → delegate, redact / masktransform, blockdeny.
  • Handles both subject shapes the gateway already passes — the retrieved context (list[Chunk] → the LLM on /v1/query?generate, /v1/chat/completions, gRPC Converse) and the agent's final answer (str). So enforcement lands at all four existing egress_text call sites with zero route changes (the existing deny→drop / transform→substitute handling consumes the result unchanged).
  • Reuses Step 1.7 machinery: the injected PIIDetector SPI (default the dependency-free RegexPIIDetector; production injects Presidio), the redact_spans / mask_spans rewriters, and the same min_score + entities filter as the ingest PiiProcessor — so the two stages never disagree, and a clean scan is a no-op.
  • Lives in rag-pii (which gains a rag-policy dep, exactly as rag-quota does for QuotaPolicyEngine; the graph stays acyclic). filter_pushdown / health / non-egress_text decisions delegate inward, so it composes with the ACL + quota PDP.
  • Opt-in via cfg.pii.enabled (default off — it can redact / withhold content). PII-free events: pii.egress_blocked (block — the reserved Step 0.7b event, now emitted) / pii.detected (redact|mask), carrying entity types + counts + surface, never the matched values.

Scope / deferred: enforces at the existing egress_text sites (context for query/OpenAI/gRPC, answer for the agent). A post-generation answer re-check for the query/OpenAI/gRPC generate paths (they sanitise the context pre-LLM) and a PII check on retrieval-only citations are deferred — stored chunks are already ingest-sanitised (1.7).

Tests & gates

  • ~24 new tests: engine unit (tests/pii/test_policy.py — str/chunks × allow/redact/mask/block, no-PII passthrough, min_score/entities filter, pii.egress_blocked + pii.detected events, non-egress + filter_pushdown + health delegation) and gateway wiring + composition-with-ACL + behavioral redact/block (apps/gateway/tests/test_pii.py).
  • Full suite: 2315 passed, 1 skipped. Green: ruff, mypy --strict (297 files), RAG001, policy-coverage linter, log-schema/PII gate, config schema-drift (dist/rag.schema.* regenerated for PiiConfig).

Documentation

Test plan

  • uv run pytest tests/ packages/ -m "not integration" — 2315 passed, 1 skipped
  • uv run ruff check + ruff format --check — clean
  • uv run mypy packages/ apps/gateway/ — no issues (297 files)
  • RAG001 + policy-coverage + log-schema/PII gate + config schema-drift
  • Behavioral: an answer/context with PII is blocked (deny) or redacted (transform) per the tenant's pii_policy through the wired engine

🤖 Generated with Claude Code

…r (Step 6.5)

Enforce per-tenant PII policy at egress. PiiPolicyEngine (rag-pii) is a
PolicyEngine decorator — like QuotaPolicyEngine / AclPolicyEngine — that answers
the egress_text decision over text leaving the system and applies the action
resolved onto ctx.pii_policy: allow delegates, redact/mask -> transform, block
-> deny.

It handles both subject shapes the gateway already passes — the retrieved
context (list[Chunk]) on /v1/query?generate, /v1/chat/completions, gRPC Converse,
and the agent's final answer (str) — so enforcement lands at all four existing
egress_text call sites with no route change. Detection uses the injected
PIIDetector SPI (default RegexPIIDetector; production injects Presidio) and reuses
the Step 1.7 rewriters + the same min_score/entities filter, so it's a no-op on
clean text and never disagrees with the ingest processor.

Lives in rag-pii (which gains a rag-policy dep, like rag-quota; graph stays
acyclic). Opt-in via cfg.pii.enabled (default off). Emits PII-free
pii.egress_blocked (block) / pii.detected (redact|mask). filter_pushdown / health
/ other decisions delegate inward, so it composes with the ACL + quota PDP.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@officialCodeWork
officialCodeWork merged commit 0ecb150 into main Jun 7, 2026
12 of 17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant