This is an automated email from the ASF dual-hosted git repository.
Aias00 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git
The following commit(s) were added to refs/heads/master by this push:
new 87c22dde7b docs(ai): propose bounded content moderation execution
contract (#7893)
87c22dde7b is described below
commit 87c22dde7be14c1c3755440a4d8b082a86d10a87
Author: cloudbase-ai <[email protected]>
AuthorDate: Wed Oct 21 14:17:06 2026 +0800
docs(ai): propose bounded content moderation execution contract (#7893)
Signed-off-by: cloudbase-ai <[email protected]>
Co-authored-by: cloudbase-ai
<[email protected]>
---
docs/rfcs/ai-content-moderator.md | 424 ++++++++++++++++++++++++++++++++++++++
1 file changed, 424 insertions(+)
diff --git a/docs/rfcs/ai-content-moderator.md
b/docs/rfcs/ai-content-moderator.md
new file mode 100644
index 0000000000..53df1893e9
--- /dev/null
+++ b/docs/rfcs/ai-content-moderator.md
@@ -0,0 +1,424 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+# RFC: judge-agnostic AI content moderation
+
+Status: **proposal for discussion**, not an implemented plugin or an approved
API.
+Issue: [#7623](https://github.com/apache/shenyu/issues/7623).
+Source baseline: `69ed0a256`.
+
+Issue #7623 asks for design agreement before implementation and Admin
registration.
+This RFC makes the policy, streaming, and resource contracts reviewable first.
+The module names, fields, defaults, and rollout stages below are proposals.
+
+## Decisions requested
+
+| Decision | Recommended starting point | Consequence / alternative |
+| --- | --- | --- |
+| Name | `aiContentModerator`, module `shenyu-plugin-ai-content-moderator` |
One plugin with judge adapters, rather than one plugin per moderation vendor. |
+| Order | Default `196`, subject to rechecking at implementation time |
Default order precedes `sensitiveWord` (197), `agentGateway` (198), and
`aiProxy` (199). Admin sort overrides must preserve response-decoration order.
Dictionary-rejected requests can incur a judge call. |
+| Availability policy | `failClosed=false`, explicit rule override |
Unavailable judgements may pass traffic, but must remain visibly uninspected.
Strict enforcement requires `failClosed=true`. |
+| Enforcement action | `block` by default; `log` for observation rollouts |
`log` records a risky result without changing the original payload. |
+| Streaming default | `buffered`, with finite byte and text limits | Wait for
the complete bounded response. Reject overflow instead of silently inspecting
only a prefix. |
+| Initial protocol | OpenAI Chat Completions text; plain HTTP request text |
Other wire profiles require their own extraction and refusal fixtures before
being enabled. |
+| Circuit breaking | Timeout and bounded admission first | A shared breaker
can follow measured demand; installing the Resilience4j plugin does not
automatically put its classes on this plugin's classpath. |
+
+Maintainers should resolve this table before the first runtime implementation
PR.
+Changing one choice requires updating the examples and acceptance cases
together.
+
+## Current integration points
+
+These are observations about the baseline, not new capabilities:
+
+-
[PluginEnum](../../shenyu-common/src/main/java/org/apache/shenyu/common/enums/PluginEnum.java)
+ assigns `sensitiveWord=197`, `agentGateway=198`, and `aiProxy=199`; 196 is
unused.
+-
[ShenyuWebHandler](../../shenyu-web/src/main/java/org/apache/shenyu/web/handler/ShenyuWebHandler.java)
+ sorts by `PluginData.sort` when present, falling back to `getOrder()`, and
+ invokes the reactive chain. An enum value alone does not enforce relative
+ order. The moderator must install its response decorator before invoking the
+ downstream response producer.
+-
[AiProxyPlugin.writeResponse](../../shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-proxy/src/main/java/org/apache/shenyu/plugin/ai/proxy/enhanced/AiProxyPlugin.java)
+ writes the engine's response through `writeAndFlushWith`. Supporting only
+ `writeWith` would leave this path unmoderated.
+-
[ServerWebExchangeUtils.rewriteRequestBody](../../shenyu-plugin/shenyu-plugin-base/src/main/java/org/apache/shenyu/plugin/base/utils/ServerWebExchangeUtils.java)
+ reads a String and replays it through a decorator. It has no per-call
byte-limit
+ argument; using it alone would not enforce this RFC's request limit.
+-
[ResponseBodyCaptureUtils](../../shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-common/src/main/java/org/apache/shenyu/plugin/ai/common/utils/ResponseBodyCaptureUtils.java)
+ currently returns an empty String. It cannot supply a response for
moderation.
+-
[OpenAiProtocolAdapter](../../shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-common/src/main/java/org/apache/shenyu/plugin/ai/common/protocol/OpenAiProtocolAdapter.java)
+ serves the proxy's request conversion. It is not a general moderation
extraction
+ contract. Keep moderation independent of proxy-engine implementation classes.
+
+Proposed runtime placement is
+`shenyu-plugin/shenyu-plugin-ai/shenyu-plugin-ai-content-moderator`, with its
own
+starter and a rule-handle DTO in `shenyu-common`. Use ShenYu's existing
+`@SPI` / `@Join` discovery conventions. The request-only path must work on a
+matched HTTP route without an AI proxy dependency.
+The proposed `196` is an order code, not an allocated Admin database plugin ID.
+The implementation must validate the effective ordering, including Admin sort
+overrides, before advertising response coverage. Reordering the moderator after
+the response producer must surface as a configuration/capability error, not a
+silently successful response check. Admin seed IDs belong to the follow-up.
+
+## Judge and policy boundaries
+
+`ContentModerationJudge` receives an immutable moderation input and judge
settings
+and returns a reactive result. The input contains direction, extracted text,
+protocol profile, and bounded context. It does not expose `ServerWebExchange`,
a
+response writer, arbitrary inbound headers, or permission to continue the
chain.
+
+The two proposed adapters are:
+
+- `openAiCompatibleJudge`: call a configured chat endpoint; parse a strictly
+ configured structured result from the judge's message content.
+- `httpApiJudge`: call a configured vendor HTTP endpoint; map configured
response
+ fields into the same result without importing a vendor SDK or executing
scripts.
+
+A judge result describes `safe` or `risky`, an optional bounded category, and
an
+optional normalized risk score in `[0, 1]`. The rule owns the action. If a rule
+sets a score threshold, its adapter must provide a risk score with the
documented
+meaning; a classification confidence is not automatically a risk probability.
+A missing/invalid required score is unavailable, not a safe result. With a
+configured threshold, `riskScore >= riskThreshold` determines the risky policy
+result; the raw label is diagnostic, not a second vote. Without a threshold,
+use the normalized `safe` / `risky` classification. For example, score 0.93 at
+threshold 0.8 is risky, and score 0.2 is safe. A label-only judge can be used
+with no threshold; it cannot satisfy a rule that requires a numeric score.
+
+An empty publisher, timeout, non-success response, malformed result, or unknown
+classification becomes `UNAVAILABLE`. Do not convert judge failures to `PASS`.
+The moderator maps the result and rule action as follows:
+
+| Verdict | Trigger | Outcome |
+| --- | --- | --- |
+| `PASS` | A successful safe judgement | Continue with the original content. |
+| `FLAGGED` | A risky judgement with `action=log` | Continue with the original
content and record the decision. |
+| `BLOCKED` | A risky judgement with `action=block` | Deny before release, or
terminate the committed stream as described below. |
+| `UNAVAILABLE` | No usable judgement, including admission/call-budget
exhaustion | Apply `failClosed`; record the reason even if traffic proceeds. |
+
+No matching content / an unsupported representation is a separate inspection
+coverage outcome, not a fifth judge verdict. `unmatched=skip|warn|error` makes
the
+bypass visible; proposed default is `warn`. No judge call means no `PASS`
metric.
+A `failClosed=false` bypass remains `UNAVAILABLE` with `coverage=uninspected`.
+
+Keep each exchange on one immutable policy/configuration generation. A rule
+update affects new exchanges; it must not change thresholds halfway through a
+stream. Removal stops admitting new work and releases retired adapter resources
+after their bounded in-flight work finishes or is cancelled.
+
+## Configuration sketch
+
+This example is a proposed rule handle, not configuration supported by a
released
+version. The endpoint reference resolves to operator-managed plugin/starter
+configuration, including credentials; it is not a URL supplied by a caller.
+
+```json
+{
+ "policyVersion": 1,
+ "judge": "openAiCompatibleJudge",
+ "judgeModel": "guard-model",
+ "judgeEndpointRef": "moderation-primary",
+ "protocol": "openai-chat",
+ "checkRequest": true,
+ "checkResponse": true,
+ "action": "log",
+ "failClosed": false,
+ "unmatched": "warn",
+ "denyStatusCode": 403,
+ "streamCheckMode": "buffered",
+ "judgeTimeoutMillis": 1000,
+ "riskThreshold": 0.8,
+ "maxRequestBytes": 262144,
+ "maxBufferedResponseBytes": 262144,
+ "maxTextCodePoints": 32768,
+ "maxEventBytes": 65536,
+ "maxHeldFrames": 256,
+ "maxJudgeCallsPerExchange": 128,
+ "maxInspectionDurationSeconds": 120,
+ "maxBatchTextCodePoints": 512,
+ "maxBatchDelayMillis": 100,
+ "maxContextCodePoints": 2048,
+ "maxJudgeRequestBytes": 262144,
+ "maxJudgeResponseBytes": 16384
+}
+```
+
+Validate the supported `policyVersion`, enums, positive finite limits,
threshold
+range, endpoint reference, and extraction paths at configuration admission.
+Compile extraction paths once per
+policy generation. Reject `checkRequest=false` plus `checkResponse=false` and
+`final_packet` plus `action=block`. Reject unsupported profile/mode
combinations.
+A bad new generation must not silently become a bypass; expose configuration
+failure through the established configuration-error path and retain the
previous
+valid generation where the runtime supports that contract.
+
+The deployment sets hard ceilings; rule limits may lower them but cannot remove
+them. The numeric limits above are starting proposals, not benchmark-derived
+production defaults. Endpoint admission also needs a finite shared maximum of
+concurrent judges (proposed: 16 per configured endpoint) and a finite pending
+count, initially zero. Protect concurrently buffered exchanges (proposed: 32
+total per gateway) as well: limiting judge calls alone does not bound waiting
+response buffers. Both quotas are deployment-wide per gateway,
+with configured endpoint identity rather than arbitrary request data as keys.
+A rule's batch plus context code points must fit its text budget. Deployment
+ceilings also bound endpoint/policy generations retained during configuration
+rotation; a stream must not keep creating new adapter state.
+
+## Request processing and extraction
+
+1. Match a rule and capture its immutable policy. Check a declared oversized
+ `Content-Length` before subscribing to the body.
+2. Count actual bytes for chunked or missing-length bodies before aggregation;
+ stop at `maxRequestBytes`, release retained buffers, and cancel upstream
reads.
+ A header-only size check is insufficient.
+3. Decode and extract within the byte and Unicode-code-point budgets. For an
+ OpenAI Chat text profile, inspect configured textual message content in role
+ order, including text parts in supported content arrays. Do not silently
treat
+ image/audio/tool-result content as inspected text.
+4. Await the request verdict, then invoke the downstream chain at most once and
+ replay the original accepted bytes. Moderation does not rewrite a prompt.
+
+Profiles define the roles, fields, arrays, and tool data they cover. A
deployment
+that chooses only the last user message must be shown that limited scope; it
must
+not be described as whole-conversation moderation. Custom JSONPath overrides
+cannot expand a profile's refusal/streaming support automatically.
+
+The proposed plain HTTP profile is named `text`: request-only, UTF-8
+`text/plain` (charset absent or UTF-8), inspecting the entire nonblank text
+within its limits. Other media types, charsets, and absent Content-Type are
+unsupported representations. It does not guess that arbitrary JSON, binary,
+form, or multipart data is text. `text` plus `checkResponse=true` is invalid.
+Known image/audio parts or unimplemented tool-call representations in an AI
+message apply `unmatched`; they must not be hidden by a successful text
judgement.
+
+The initial response profile covers OpenAI Chat message text and incremental
+`choices[*].delta.content`. Additional profiles for Responses, Anthropic, and
MCP
+are implementation follow-ups with explicit text/tool/multimodal coverage. They
+must not be enabled through a speculative path table. If content encoding is
not
+supported by the bounded decoder, apply `unmatched`; never parse compressed
bytes
+as if they were JSON. A future decoder needs its own decompressed-byte limit.
+
+## Response and streaming contract
+
+Install one module-local response decorator covering `writeWith`,
+`writeAndFlushWith`, empty completion, cancellation, and errors. In
request-only
+mode, leave the response untouched. Do not buffer an unbounded `Flux` or turn a
+stream into an unrestricted `collectList` / `join`.
+
+The parser must tolerate arbitrary `DataBuffer` boundaries, split UTF-8 code
+points, split SSE lines/events, several events in one buffer, comments,
terminal
+frames, and a short final tail. Transport chunks are not moderation batches.
+All retained raw bytes, decoded text, frame objects, and context count toward
+explicit limits. A single oversized event and many content-less events must not
+bypass the byte/frame caps.
+
+| Mode | Release boundary | Coverage and exhaustion |
+| --- | --- | --- |
+| `buffered` | Hold the complete finite response and judge at completion;
release the original bytes only after its decision/policy permits it. | Applies
to the entire extracted response within the configured caps. Overflow
rejects/terminates; inspecting a safe prefix does not authorize the remainder. |
+| `realtime` | Serialize batches; each batch includes all its newly extracted
text in its own judgement before that batch is released. Bounded prior context
may also be supplied. | Coverage is the configured context/batch, not unlimited
conversation history. A later denial cannot retract earlier approved batches. |
+| `final_packet` | Forward content as it arrives and inspect a bounded
retained final window at completion. | Audit-only, explicitly partial/windowed
when truncated. It cannot claim prevention or enforce a denial after bytes have
been sent. |
+
+For a `final_packet` response, risky/unavailable results, malformed framing,
and
+inspection-budget exhaustion are observations only: preserve the original
+response even with `failClosed=true`. Stop inspection and discard retained
audit
+state when its limit is reached, while forwarding subsequent original bytes.
+Its separately enabled request check still obeys the request enforcement
policy.
+The final judge call remains part of an owned, timeout-bounded reactive
lifecycle;
+connection completion may wait for it even after content was visible. Do not
+claim zero added completion latency or create an unbounded background audit
queue.
+
+For `realtime`, proposed batching closes on the first of a configured
text/event
+threshold or a bounded batch timer, then waits for its judgement; at most one
+judgement per exchange is in flight. Use serial demand/prefetch bounds so a
slow
+judge cannot accumulate an unbounded queue. Flush the last nonempty batch at
end
+of stream even when shorter than the usual threshold. Adjacent content-less
+frames need no judge call, but their ordering and retained-frame budget still
+apply. In enforcing modes, hold leading metadata frames until the first release
+decision; for an all-metadata response, apply `unmatched` at completion.
+
+In enforcing modes, raw byte/text/frame overflow rejects by default (413 for a
+request or 502 for a response before commit, protocol termination after
commit);
+it is not a judge-availability error.
+Availability, call-budget, or inspection-deadline exhaustion follows
`failClosed`.
+An explicit fail-open release is uninspected and recorded as such; a previous
+batch's `PASS` must never authorize the current batch. Bounded bypass flushing
+switches the affected direction into passthrough for the remainder of the
+exchange: flush all not-yet-released original bytes, including the pending
batch,
+its tail, and held metadata, exactly once in order. Never replay an already
+released prefix. Stream subsequent bytes without retaining moderation buffers
or
+starting more judge calls for that direction. Record the bypass transition and
+uninspected coverage; it does not change the original wire protocol.
Request-side
+availability bypass does not disable the separately configured response check.
+
+When a response completes with no inspectable text, `skip`/`warn` releases the
+held metadata/terminal frames with no judgement; `error` rejects before commit
or
+terminates after commit. If text exists, hold its terminal frame until the
final
+batch is decided. An empty body and a content-less framed response both use
this
+coverage rule. Comments/heartbeats remain subject to byte/frame/deadline
budgets.
+
+Client cancellation cancels body reads and the outstanding judge request,
+releases every retained buffer once, and prevents later callbacks from writing.
+Use discard handling for queued buffers and test pooled-buffer reference
counts.
+Do not subscribe/fire-and-forget to judge publishers from a response callback.
+
+## Denial and failure responses
+
+This matrix is normative for request checks and enforcing response modes
+(`buffered` / `realtime`). The audit-only `final_packet` response exception
above
+takes precedence. "Before commit" includes headers: no status change is
promised
+after they are sent.
+
+| Condition | Request / response before commit | Committed response |
+| --- | --- | --- |
+| Successful safe judgement, or risky with `action=log` | Preserve and release
the accepted original content; record PASS or FLAGGED. | Release only the batch
permitted by its own decision. |
+| Risky with `action=block` | `denyStatusCode` (proposed 403). | Cancel
upstream; profile-specific termination, never retract earlier output. |
+| Unavailable judge, admission/call budget, or inspection deadline |
`failClosed=true`: 503. Otherwise passthrough with UNAVAILABLE/uninspected. |
`failClosed=true`: terminate. Otherwise latch passthrough with
UNAVAILABLE/uninspected. |
+| Missing text or unsupported representation | `unmatched=skip` or
`unmatched=warn`: passthrough, uninspected (warn records a warning). `error`:
request 415 / response 502. | `error`: terminate. Otherwise passthrough,
uninspected. |
+| Malformed recognized JSON, UTF-8, or SSE framing | Request 400 / response
502; no successful judgement. | Terminate; do not call malformed data safe. |
+| Raw/text/event/frame capacity exceeded | Request 413 / response 502,
independent of `failClosed`. | Cancel upstream and terminate. |
+| Client cancellation | Cancel reads/judges and release resources; no new
response write. | Same; late judge results cannot write. |
+
+Check declared size first, then cumulative raw size before copying or
aggregation, then
+framing/decode/text limits, then serialized judge request/response limits
before
+parsing their contents. If several limits apply to the same data, the first
+exceeded limit terminates processing once; it must not allocate past another
+ceiling to classify the error. Duration/admission limits never relax byte caps.
+An invalid judge response belongs to UNAVAILABLE; invalid protected wire data
+belongs to the malformed-input row.
+
+A judge outage is not evidence of forbidden content; do not count an
unavailable
+fail-closed result as a risky-content denial.
+
+Use the existing response-writing utilities and serialize typed objects. Plain
+HTTP errors can use
+[ResponsiveException](../../shenyu-plugin/shenyu-plugin-api/src/main/java/org/apache/shenyu/plugin/api/exception/ResponsiveException.java)
+and
[WebFluxResultUtils](../../shenyu-plugin/shenyu-plugin-api/src/main/java/org/apache/shenyu/plugin/api/utils/WebFluxResultUtils.java).
+Recognized AI routes require the profile's error envelope rather than assuming
a
+generic gateway result matches the SDK protocol. For example, an OpenAI-style
+pre-commit rejection can use this proposed error body:
+
+```json
+{
+ "error": {
+ "message": "Content moderation rejected this request",
+ "type": "content_policy_violation",
+ "code": "moderation_blocked"
+ }
+}
+```
+
+After commit, HTTP status and headers cannot be changed. Stop acquiring model
+content, cancel the upstream, and use a tested profile-specific refusal/error
+termination if supported. If none can be emitted safely, terminate the response
+and record the failure; do not inject a gateway JSON document into SSE or label
+an unfinished completion as successful. Refusal framing for each profile is an
+implementation gate tested against protocol fixtures, not an assumption about
+all OpenAI-compatible vendors. `final_packet` only records the result.
+
+A moderation denial is a terminal policy outcome. It must not look like a
+retryable model transport failure, trigger provider fallback, or execute the
+business chain a second time. The real proxy-writer fixture must cover those
+error/fallback boundaries in addition to testing a standalone decorator.
+Generated denial output bypasses the moderator's own inspection path to avoid
+recursion. Never echo the matched text, judge prompt, credentials, or raw
vendor
+response to the client. Remove obsolete content length/encoding/digest headers
+when replacing a response, and recompute headers for the actual denial payload.
+
+## Judge transport, limits, and observations
+
+Use a reused reactive HTTP client, one attempt per judgement initially, a
finite
+response-body cap, timeout, and cancellation propagation. Direct judge calls do
+not execute the gateway plugin chain. Endpoint origin and credentials come from
+trusted deployment configuration; callers cannot choose them or forward their
+Authorization/Cookie headers to the judge. Redirect behavior must not bypass
the
+configured endpoint boundary. Validate templates and structured result paths
+without evaluating code. Budget serialized judge request bytes as well as
source
+text: escaping and a prompt template can expand the payload.
+
+Parsing/normalization has a bounded input. Any substantial blocking/CPU adapter
+work needs explicit bounded isolation at the computation boundary; a
+`subscribeOn` before asynchronous HTTP results does not relocate later response
+parsing by itself. HTTP I/O needs no unconditional extra scheduler hop. Do not
+add a thread pool or breaker per request. There are no implicit application
+retries in the initial adapter: each logical judgement has one HTTP attempt,
+and a retry/fallback may not be hidden by the HTTP-client configuration.
+A rotated endpoint gets a new immutable adapter generation. Drain or cancel its
+old in-flight work by the existing deadlines; close only resources owned by
that
+generation, never a shared gateway connection pool.
+Defer any additional resilience-library dependency until its ownership and
+shared state are approved.
+
+Record direction, verdict, coverage (`inspected`, `windowed`, or
`uninspected`),
+bounded reason, judge latency, and released/held byte counts. Reasons include
timeout, malformed
+result, no text, unsupported profile, resource limit, admission rejection, and
+cancellation. Proposed deny/failure counters are `ai_sec_request_deny`,
+`ai_sec_response_deny`, and `ai_sec_judge_failure`. Counter registration
belongs
+to the existing metrics integration; the moderator exposes bounded exchange
+attributes/observations rather than adding a mandatory metrics plugin
dependency.
+No prompt text, arbitrary model response, URL, request ID, or free-form
category
+belongs in a metric label. Category values must map to a configured finite set.
+
+A bounded metadata-only decision event may support logging, but an audit event
+failure must not rewrite a completed request verdict or block business traffic.
+No cross-request judgement cache is proposed: a safe decision depends on the
+policy generation, direction, full inspected text, and judge configuration.
+
+## Acceptance and implementation sequence
+
+Tests below are requirements for future implementation, not tests run by this
+RFC. Use local HTTP judge fixtures and fixed protocol samples, with no vendor
+credentials or public service dependency.
+
+| Area | Required observation |
+| --- | --- |
+| Rule/configuration | Invalid limits, templates, score semantics, modes, and
endpoint references are rejected; a policy update does not mutate an in-flight
stream. |
+| Request replay | Known and chunked oversize requests stop before unbounded
allocation; accepted bytes/headers survive; the chain runs once. |
+| Judge outcomes | Safe/risky/log/block, empty result, malformed score,
non-success status, timeout, admission exhaustion, and cancellation stay
distinguishable. |
+| Actual proxy path | Both writer methods are covered, plus an
AiProxyEngine-to-decorated-response fixture so bypassing the decorator fails a
test. Admin sort overrides must fail coverage validation when they reverse the
required order. |
+| Frame boundaries | Split UTF-8/SSE/JSON, multi-event chunks, no-text frames,
terminal frames, empty completion, and short tails preserve ordering and reach
their documented decision. |
+| Pre-release gating | Withhold each judge response and assert that the
corresponding unapproved batch is not visible; an earlier safe result cannot
release the next risky batch. |
+| Limits/demand | Oversized single events, many empty frames, long streams,
exhausted call budgets, and a slow judge keep retained memory/queues bounded. |
+| Denial | Before/after commit, valid profile framing, no sensitive-text echo,
and no false success on truncated/error streams. |
+| Lifecycle | Client disconnect, rule removal, endpoint rotation, and shutdown
cancel work and release pooled buffers/connections without double release. |
+| Observe/fail-open | Original payload is preserved and explicit
uninspected/FLAGGED observations are emitted; no clean verdict is fabricated. |
+
+These are development milestones, not promises of a release at each step.
+A mode/profile is advertised only after its complete writer/framing fixtures
+pass. Do not expose a half-implemented response mode through a working-looking
+Admin form; reject unsupported configurations. Existing gateways without the
+starter cannot enforce the policy, so deployment inventory must verify module
+support before routing protected traffic through a mixed-version fleet.
+
+Proposed stages, each with its own reviewable PR:
+
+1. Agree this contract, numeric ceilings/defaults, and the decisions table.
+2. Add module/starter/DTO/SPI, both judge adapters, bounded request and
non-streaming
+ response support, and fixture-based contract tests. Keep unsupported
streaming
+ modes rejected until their implementation lands.
+3. Add bounded `buffered` streaming and the real proxy-writer tests; then
introduce
+ `realtime` and audit-only `final_packet` with their separate demand/framing
tests.
+4. Add protocol profiles only as their extraction/refusal fixtures pass. Track
+ Admin rows/forms, multi-dialect install/upgrade data, and rollout
documentation
+ separately, following the sensitive-word registration precedent.
+5. Benchmark local controlled judge delay/throughput, concurrent protected
+ exchanges, retained heap, cancellation recovery, and both slow/long SSE
+ workloads. Publish p50/p99, judge calls per request, and inspected/bypassed
+ denominators before recommending production limits.
+
+The semantic classifier's accuracy and its resistance to adversarial content
+remain properties of the chosen judge and deployment. A successful protocol or
+resource-bound test does not establish universal content safety.