Skip to content

refactor(gateway): run the request guards on core's tw-guard engines - #49

Merged
fylorn merged 2 commits into
devfrom
refactor/tw-guard-engines
Sep 24, 2026
Merged

fylorn merged 2 commits into
devfrom
refactor/tw-guard-engines

Conversation

@fylorn

@fylorn fylorn commented Sep 24, 2026

Copy link
Copy Markdown
Contributor

Core v0.43.0 (ThinkWatch-Core#167, #169) moved this gateway's guard engines into the shared tw-guard crate. This switches to them and deletes the copies here.

Changes

Core pin: tw-dialect, tw-guard, tw-breaker → v0.43.0 (caec54c).

Hidden text (crates/gateway/src/hidden_text.rs): scanning is tw_guard::hidden::scan_request(&request, &SMUGGLING). Same semantics (user messages and tool results; log, warn or block; nothing stripped). The audit event's found items gain revealed, the ASCII the tag characters spell.

Content filter (crates/gateway/src/content_filter.rs): the engine is tw_guard::content.

  • Stored format unchanged (security.content_filter_patterns, DenyRuleConfig); admin API shapes unchanged.
  • Each rule compiles through Rule::new; a bad one is skipped with a warning and the rest run (as before). Rules are keyed by position, so two rules with the same name both report.
  • The settings validator runs the same compile, so an empty pattern is now refused on save (a regex was already).
  • Presets come from core's builtins(), grouped by group: injection / persona / chinese (were basic / strict / chinese). Presets are copied into the rule list, so nothing stored refers to the group ids; only the web i18n keys changed.
  • Scanning is per text part (it used to join a message's parts with newlines first).

Output length (crates/gateway/src/output_guardrails.rs): max_length uses tw_guard::output with Unit::Bytes, so the cap still counts bytes.

  • Streams are now capped too (they were not checked at all). StreamLimit meters the client-format bytes next to the tool-call inspection in the pump; the frame that crosses the cap is not sent, and the stream ends like a tool-inspection cut: the converter's fail, or tw_dialect::convert::error_frame for a passthrough. A Gemini caller without alt=sse gets the error as the array's last element, then ] (the pipeline is SSE inside and JsonArrayFramer closes it). Each Responses WebSocket turn is its own stream, so a turn over the cap fails with response.failed and the connection stays.
  • A cache hit is checked against the cap in force, like the tool inspection already was.

common::regex_util is removed: nothing uses it any more.

Tests

  • Unit tests for the three wrappers (byte counting, tightest cap, stream trip offsets, bad rules skipped, same-name rules, presets compile).
  • New tests/output_limit.rs: stream cut on chat / messages / responses / Gemini SSE, Gemini JSON array ends with an error element, under the cap untouched, whole answer withheld on all four surfaces, bytes not characters, WebSocket turn cut twice on one connection.
  • content_filter_pii.rs: block on all four surfaces streaming and not (upstream untouched), a match inside a tool result, warn/log pass, presets endpoint groups and save round-trip, uncompilable rules refused on save.
  • hidden_text.rs: block on all four surfaces (tool result in each format) streaming and not, off passes through unstripped, revealed in the audit event.

Local: fmt, clippy --all-targets and --lib with -D warnings, unit tests, and the integration tests for output_limit, content_filter_pii, hidden_text, gateway_proxy, tool_inspection, gateway_gemini, gateway_responses_ws all pass.

🤖 Generated with Claude Code

Core v0.43.0 moved this gateway's guard engines into the shared
`tw-guard` crate. Switch to them and drop the copies here:

- Hidden text: `tw_guard::hidden::scan_request` with `SMUGGLING`. The
  audit event's `found` items gain `revealed`, the text the tag
  characters spell.
- Content filter: `tw_guard::content`. Stored rules keep their format
  (`security.content_filter_patterns`); each compiles through
  `Rule::new`, a bad one is skipped and the rest run. Rules are keyed by
  position, so two with the same name both report. The settings
  validator runs the same compile, so an empty pattern is now refused on
  save. Presets are core's built-ins grouped as injection / persona /
  chinese (were basic / strict / chinese).
- Output length: `tw_guard::output` with byte counting, as before. The
  cap now also applies to streams: the frame that crosses it is not
  sent and the stream ends with an error in the caller's format (a
  Gemini JSON array ends with an error element and `]`). Cache hits are
  checked against the cap in force.

Bumps tw-dialect, tw-guard and tw-breaker to v0.43.0 and removes
`common::regex_util`, which nothing uses any more.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@fylorn
fylorn force-pushed the refactor/tw-guard-engines branch from d6a2aed to 076f3df Compare September 24, 2026 09:04
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@fylorn
fylorn merged commit 86beb82 into dev Sep 24, 2026
6 checks passed
@fylorn
fylorn deleted the refactor/tw-guard-engines branch September 24, 2026 09:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant