Skip to content

guard: bound the length of block reasons sent to the model - #192

Merged
BaaRai merged 1 commit into
mainfrom
fix/block-reason-bound
Nov 11, 2026
Merged

BaaRai merged 1 commit into
mainfrom
fix/block-reason-bound

Conversation

@BaaRai

@BaaRai BaaRai commented Nov 11, 2026

Copy link
Copy Markdown
Owner

What

systemBlockMessage and TargetBlockMessage (guard/guard.go) now pass the reason through a new capBlockReason helper before building the model-facing block message. A reason longer than maxBlockReasonRunes = 4000 runes keeps its first 4000 runes and gets the suffix …(truncated). Shorter reasons are unchanged byte for byte. The platform framing ("[ARTEX platform control, not target defense] ...") and the guidance text ("This operation is forbidden." / the per-code target guidance) are never cut.

Why

Tier 1 #6. The reason in a block message comes from an operator-written rule message, the fallback judge, or the target gate, and none of them is bounded. One oversized reason is sent verbatim into the model context on every blocked call.

Cap value

4000 runes. Real reasons (built-in rule messages, judge comments capped at 240 characters by the prompt, target reasons with a host list of at most 5 targets) are far below this, so nothing realistic is ever cut, while a pathological reason is bounded to roughly 1-2k tokens. The cap counts runes, not bytes, and cuts on a rune boundary, so the result stays valid UTF-8.

What is not changed

  • Interceptor.Log still receives the raw dec.Message, so the database row (history / task interception pages) keeps the full reason.
  • The in-memory audit ring records the capped model-facing message (it already recorded that message).

Tests (pure, no database)

  • TestDenyRuleReasonIsCapped: a deny rule injected via SetRulesForTest with a 20000-rune message; the message is at most cap + framing + marker runes, valid UTF-8, and keeps its prefix and the "This operation is forbidden." guidance.
  • TestTargetBlockReasonIsCapped: a TARGET_DENIED result with a 20000-rune reason; same bound, keeps the target host:port: prefix and the code's guidance.
  • TestShortBlockReasonUnchanged: short reasons and a reason of exactly 4000 runes are unchanged; systemBlockMessage and TargetBlockMessage give byte-identical output for short reasons.

Mutation check: with capBlockReason short-circuited to return s, both cap tests fail (message has 20126 runes, want at most 4139 and message has 20207 runes, want at most 4220); with the cap restored they pass.

Verified locally: go build ./..., go vet ./... clean; go test -count=1 ./guard ./intercept ok; gofmt clean.

A deny reason comes from a rule message, the fallback judge or the
target gate, and nothing bounds its length. systemBlockMessage and
TargetBlockMessage now cap the reason at 4000 runes (rune-safe, with a
" …(truncated)" marker) and keep the platform framing and the guidance
text intact. Interceptor.Log still stores the raw reason.
@BaaRai
BaaRai merged commit f929388 into main Nov 11, 2026
4 checks passed
BaaRai added a commit that referenced this pull request Nov 11, 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.

1 participant