feat(pii): PII egress policies — PiiPolicyEngine egress_text decorator (Step 6.5) - #152
Merged
Merged
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_policyontoRequestContext.pii_policy— but nothing enforced it on the way out. Theegress_textPDP decision existed and the gateway already calls it at every generation boundary, yet the default engine answeredallow.PiiPolicyEngine(rag-pii) is aPolicyEnginedecorator — likeQuotaPolicyEngine/AclPolicyEngine— that answersegress_textand appliesctx.pii_policy.action:allow→ delegate,redact/mask→transform,block→deny.list[Chunk]→ the LLM on/v1/query?generate,/v1/chat/completions, gRPCConverse) and the agent's final answer (str). So enforcement lands at all four existingegress_textcall sites with zero route changes (the existing deny→drop / transform→substitute handling consumes the result unchanged).PIIDetectorSPI (default the dependency-freeRegexPIIDetector; production injects Presidio), theredact_spans/mask_spansrewriters, and the samemin_score+entitiesfilter as the ingestPiiProcessor— so the two stages never disagree, and a clean scan is a no-op.rag-pii(which gains arag-policydep, exactly asrag-quotadoes forQuotaPolicyEngine; the graph stays acyclic).filter_pushdown/health/ non-egress_textdecisions delegate inward, so it composes with the ACL + quota PDP.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_textsites (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
tests/pii/test_policy.py— str/chunks × allow/redact/mask/block, no-PII passthrough, min_score/entities filter,pii.egress_blocked+pii.detectedevents, non-egress + filter_pushdown + health delegation) and gateway wiring + composition-with-ACL + behavioral redact/block (apps/gateway/tests/test_pii.py).mypy --strict(297 files), RAG001, policy-coverage linter, log-schema/PII gate, config schema-drift (dist/rag.schema.*regenerated forPiiConfig).Documentation
PiiPolicyEngineegress section.Test plan
uv run pytest tests/ packages/ -m "not integration"— 2315 passed, 1 skippeduv run ruff check+ruff format --check— cleanuv run mypy packages/ apps/gateway/— no issues (297 files)pii_policythrough the wired engine🤖 Generated with Claude Code