2026-05-10-x2-pii-emission

X-2 follow-up: data.pii_detected emission point

Date: 2026-05-10 Status: shipped Slug: x2-emission-points-batch-2 Branch: feature/x2-emission-points-batch-2 Strategic plan item: X-2 (continuation of #269)

Summary

Wires data.pii_detected emission in the legacy PII scanner branch of the guardrails middleware. The OCSF projector and EVENT_MAP entries shipped in #269 already handle the event; this PR connects the call site.

The emission carries:

  • tenantId from the resolved API key
  • requestId from the request context
  • piiTypes (deduped string list — e.g. ["email", "phone"])
  • action derived from effectivePiiMode: block"blocked", redact"redacted", otherwise "warned"
  • direction: "inbound" (request-side scan; outbound would be the response-stream guardrail)

Per the X-2 PII hard rule, the event carries only types and the action — no raw prompt text.

Why only this emission, and not anomaly + guardrail in the same PR

anomaly-detection.ts operates on entityType: "sender" | "service" — the "agent" entityType the X-2 plan referenced doesn't exist in the current detector type signature. Wiring behavior.anomaly_spike requires either expanding the entity-type union (out of scope) or emitting from agent-reputation.ts:163 instead (where the agent identity is in scope). That emission point is deferred.

data.guardrail_triggered lives in the V2 pipeline executor, which has a different verdict shape and lifecycle. Wiring it cleanly requires a small refactor to centralize the verdict emission rather than scatter void emit({...}) calls per check. Also deferred.

Files

Modified

  • src/api/middleware/guardrails.ts (+25 LOC) — data.pii_detected emission in the legacy PII branch

Verification

  • pnpm tsgo — exit 0
  • pnpm test:fast — 7755/0 (no regression)
  • pnpm exec oxfmt --check — clean
  • pnpm exec oxlint --type-aware — 0/0

Lockstep

No public surface change. Ship log only.