Skip to content

feat(plugin): add policy-driven PII detection for AI traffic - #8105

Merged
Aias00 merged 7 commits into
apache:masterfrom
IT-Althusser:feat/pii-detection
Oct 26, 2026
Merged

Aias00 merged 7 commits into
apache:masterfrom
IT-Althusser:feat/pii-detection

Conversation

@IT-Althusser

Copy link
Copy Markdown
Contributor

Fixes #8048

Problem

sensitiveWord only rejects dictionary hits. It does not provide typed PII
detection, field-aware redaction, or AI protocol awareness.

This change adds policy-driven PII detection for supported AI JSON payloads
while preserving the existing reject-only behavior of sensitiveWord.

What this changes

A standalone piiDetection plugin is added at order 196, before aiProxy
at order 199. The plugin is disabled by default.

The first release supports:

  • OpenAI chat completion payloads;
  • OpenAI responses payloads;
  • request-side scanning;
  • non-streaming JSON response scanning.

Supported actions are BLOCK, REDACT and AUDIT.

Redaction is limited to extracted text slots. Tool definitions, function
arguments and unrelated JSON members are not scanned or rewritten. The
rewritten result preserves JSON tree semantics; JSON escaping, formatting,
key ordering and byte-level representation may be normalized by Gson
serialization.

Detection

Built-in detectors:

  • EMAIL;
  • PHONE_CN;
  • NATIONAL_ID_CN;
  • CREDIT_CARD.

Custom regular expressions are supported through the rule handle. The
configuration is bounded by entry count, per-regex length and total length,
and built-in detector names cannot be overridden.

Detection has a per-direction deadline. Audit records contain only detector
type, match count, direction, action and rule id. Detected values are not
written to logs or audit records.

Overlapping matches use the strictest action: BLOCK > REDACT > AUDIT.

Protocol profiles

The plugin supports:

  • openai-chat: messages[].content and choices[].message.content;
  • openai-responses: input and output[].content[].text.

Extraction containers and text member types are validated before scanning.
Malformed shapes follow the failure policy. Valid textless messages, such as
tool calls or image-only content, remain supported.

Successful Responses payloads containing "error": null are scanned normally.
A non-null upstream error envelope is passed through unchanged.

Bounds and failure policy

maxBodySize applies to request and response bodies and is measured in UTF-8
bytes. Negative values fall back to the default; 0 explicitly means
unlimited.

When failClosed=true, unsupported content types, malformed payloads,
unsupported streaming modes and oversized bodies are rejected before protected
content is written.

When failClosed=false, unsupported modes are passed through without scanning
and a warning is logged. Fail-open means “not detected”; it does not mean that
the content was checked and found clean.

Response inspection is limited to JSON responses in this release. SSE and
compressed responses are not scanned. A strict response scope rejects
stream: true requests before the upstream is called. When request scanning is
also enabled, fail-open request inspection marks the response to be skipped.
With scope=response alone, fail-open does not read the request body to inspect
the stream flag; SSE is skipped by response content type and supported JSON
responses remain eligible for response inspection.

Request aggregation is also bounded by spring.codec.max-in-memory-size.

Configuration

Field Default Description
detectors empty Built-in detector types; empty enables all built-ins
customPatterns empty Custom TYPE=REGEX entries
actions empty Per-type TYPE=ACTION overrides
defaultAction REDACT Action for types without an override
scope request request, response or both
maxBodySize 1048576 Maximum scanned body size in bytes; 0 means unlimited
failClosed false Reject unsupported modes instead of passing through
detectorTimeoutMs 500 Detector budget per direction

Invalid detector types, actions, scopes, timeout values and negative body
limits fall back to safe defaults with a warning. A custom-pattern size or
count violation rejects rule compilation rather than installing a partial
policy. A configured rule without a compiled policy rejects traffic instead of
silently bypassing inspection. Clearing the handle drops the compiled policy.

Assembly

The change includes:

  • PII detection plugin module;
  • Spring Boot starter and auto-configuration;
  • parent and bootstrap wiring;
  • PiiDetectionHandle;
  • plugin README;
  • MySQL, PostgreSQL, Oracle and H2 schema rows;
  • plugin, handler, resource and permission configuration.

The generic console form can edit the handle because its fields remain
delimiter-separated.

Tests

The tests cover built-in detector boundaries, checksum validation, protocol
slot extraction, escaped text, JSON tree equivalence, false-positive
boundaries, action precedence, overlap merging, request and response bounds,
fail-open prefix replay, pooled buffer release, streaming prechecks,
configuration fallback, malformed protocol handling and starter registration.

Local validation passed:

PII module: 75 tests passed
Spring Boot starter: 4 tests passed
Checkstyle: 0 violations in both reactors

The local run used JDK 25, Maven 3.9.4, -Dnet.bytebuddy.experimental=true,
-Djacoco.skip=true and -DfailIfNoTests=false. The Maven Wrapper could not
download its distribution in this environment because the server returned
HTTP 403; the full JDK 17 ./mvnw clean install run remains for CI.

Known limitations

  • SSE cross-chunk scanning is deferred;
  • compressed response decoding and re-encoding are not supported;
  • non-OpenAI protocol profiles are not included;
  • reversible tokenization is not included;
  • an audit event bus is not included;
  • a dedicated concurrency test is still pending;
  • aiResponseTransformer must not be enabled on the same traffic because its
    response decorator can replace the redacted body.

Checklist

  • Contribution guidelines reviewed.
  • Unit tests added for the changed behavior.
  • Full ./mvnw clean install -Dmaven.javadoc.skip=true passed locally.

IT-Althusser and others added 7 commits October 26, 2026 00:30
…mantics (apache#8048)

- add AutoConfiguration.imports so the starter is assembled in bootstrap
- add permission rows and the plugin: perms prefix across the four schemas
- request scan hops off the netty event loop and catches detector deadlines
- gzip, sse and unscannable responses honour failClosed; error envelopes pass through
- responses enforce maxBodySize before buffering
- rule compilation falls back on unusable detectors, scope and timeout values
- one detector deadline per direction instead of per slot
- cleared handles drop the compiled rule; luhn rejects repeated digit cards
- document mutual exclusion with aiResponseTransformer
…egation (apache#8048)

- scan successful openai-responses completions: "error": null is not an
  error envelope, only a non null error object bypasses the scan
- merge overlapping redaction ranges on the original coordinates before
  replacing, so a contained detection never leaves a partially redacted
  island behind
- bound the response aggregation: a fail closed rule stops reading at the
  bound, a fail open rule reads to the end and writes the complete body
  back unscanned instead of truncating it
- pass unsupported payloads through as the original bytes, a gzip body
  never survives a utf-8 round trip
- precheck streaming requests of a strict response scope before the model
  is reached, even when the request text itself is not scanned
- measure the request bound in utf-8 bytes instead of characters
- default maxBodySize to 1 MiB (dto and the four schema dialects)
- restore the upstream auto configuration verification script
- add the plugin README and regression tests for every fix above
… size limits (apache#8048)

- a fail open rule whose response bound is exceeded now switches to
  unscanned streaming instead of aggregating the whole body: the buffered
  prefix is written, the overflowing chunk and every later chunk follow as
  they arrive, memory stays at the prefix size and the client receives the
  prefix while the upstream is still open; the upstream is subscribed once,
  nothing is lost or duplicated
- a fail closed rule still cancels the reading at the bound before any
  byte was written and answers with the rejection body
- the scanned completion path sets the content length of the rewritten body
- the declared size bound now applies to the response-only precheck path too
- a request that exceeds the codec in memory limit (independent of
  maxBodySize) is rejected with a clear reason instead of surfacing a codec
  exception
- regression tests: prefix readability before the upstream completes,
  declared over-limit precheck, codec-limit rejection; README updated
…rs at the switch (apache#8048)

- the response-only precheck measures the actual utf-8 length of an
  undeclared chunked body, so the maxBodySize bound no longer depends on
  how the request is transported
- the fail-open switch point copies the overflowing chunk out of its
  upstream buffer and releases the buffer immediately: what the switch
  emits is array backed, a cancellation between the replayed prefix and
  the chunk cannot leak pooled memory; a doOnDiscard hook covers a chunk
  that was taken from the upstream but never emitted
- regression tests: a real netty pooled buffer proves both chunk buffers
  reach refCnt 0 when the downstream cancels after the prefix, an
  undeclared 1 KiB body over a 128 byte rule bound is rejected, and the
  streaming test now replays a non empty prefix
Validate extraction shapes while preserving textless tool calls, normalize negative body limits, bound custom regex configuration before compilation, reject configured rules without compiled policies, and document fail-open response-only behavior. Add regression tests for protocol and configuration boundaries.

@Aias00 Aias00 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean, self-contained new plugin module shenyu-plugin-ai-pii. The handle (PiiDetectionHandle) is admin-form friendly (delimited detectors/actions/customPatterns, scope request/response/both, maxBodySize with 0=unlimited default, failClosed default false, and a detectorTimeoutMs deadline). Detection is structured into PiiDetector/RegexPiiDetector/BuiltinPiiDetectors; profiles (OpenAiChatProfile, OpenAiResponsesProfile, PiiProtocolProfiles) describe where PII lives in provider payloads; PiiResponseDecorator applies redaction/block; and PiiScanner is deadline-bounded (DeadlineCharSequence, PiiChecksum). Audit is pluggable (PiiAuditSink/LoggingPiiAuditSink). The PluginEnum.PII_DETECTION(196,...) registration, bootstrap/parent pom wiring, and a starter config are all present.

Minor follow-ups (non-blocking):

  • Consider an admin metadata/handle JSON example and a short doc so operators know the expected detectors/actions vocabulary and scope values.
  • failClosed=false is a safe default for a first release; worth a note in docs that response scanning + fail-closed is the stricter posture.

Approving.

@Aias00
Aias00 merged commit 32d7b56 into apache:master Oct 26, 2026
55 checks passed
@IT-Althusser
IT-Althusser deleted the feat/pii-detection branch October 26, 2026 12:26
@Aias00 Aias00 added this to the 2.7.2 milestone Nov 13, 2026
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.

[Feature][AI] Add policy-driven PII detection and redaction for model traffic

2 participants