You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
rules: fail closed via viral CEL unknowns instead of dispatch gates (#633)
Replaces the dispatch-level truncation/unparseable pre-checks with
value-level unknowns from cel-go partial evaluation, and makes runtime
CEL evaluation errors fail closed.
* `match.Matcher.Match` is now three-valued (Matched / NoMatch /
Unevaluable). On a Truncated or Unparseable request the matcher
marks the affected facet paths (the declared `TruncatablePaths` /
`UnparseablePaths`) as CEL unknowns via a partial activation
(`cel.PartialVars` + `OptPartialEval`); the unknowns propagate
virally, NaN-style, and surface as Unevaluable, which the
dispatcher turns into a synthesized deny attributed to the rule
whose contract broke
* More precise than the old reads-the-path gate: `&&`/`||`
absorption lets a condition resolve honestly when its outcome
doesn't depend on the unavailable value. `sql.verb == 'select' &&
sql.database == 'x'` on an unparseable query against database 'y'
now cleanly no-matches (`unknown && false == false`) instead of
synth-denying; the symmetric `unknown || true` fires as written
* CEL eval errors no longer coerce to "no match" — a deny rule whose
condition errors at runtime used to be silently skipped, failing
open; it now synth-denies. The common case is selecting a
`body_json` field the payload doesn't carry — or a missing map key
like `k8s.params.stdin`. Guard optional fields with `has()` /
`'key' in map`. `has()` cannot rescue a *truncated* field: the
presence test itself is unknown
* Agent-visible deny reasons carry only the rule name and the coarse
cause (truncated / unparseable / evaluation error); the full
detail — unknown facet paths or the raw CEL error text — goes to
the gateway log. cel-go errors like `no such key: <field>` would
otherwise let an agent probe which fields a rule inspects by
varying payloads and reading deny reasons
* New `runtime.MatchRequestFailClosed` backstop for SQL frontends
(postgres, clickhouse_native): when a truncated or unparseable
request matches no rule on an endpoint that declares rules, it
denies instead of riding the implicit-allow default — "no rule
matched" may just mean every condition resolved independent of the
missing bytes. Rule-less endpoints keep pass-through; the HTTPS
path keeps plain MatchRequest (rule-less LLM endpoints routinely
forward >cap bodies)
* `InspectsTruncatableFacet` stays as the compile-time laziness
signal (`CompiledEndpoint.InspectsTruncatable` / ssh stdin
buffering); `InspectsUnparseableFacet` is removed along with the
dispatch gate. `OptPartialEval` and the unknown-pattern slices are
built only for conditions that reference a truncatable /
unparseable path, keeping cel-go's default attribute factory on
the hot path
* New tests pin the absorption semantics, the strict-error deny, the
has() guard behavior on captured vs truncated bodies, unknown
propagation through `==` / `in` / `exists()`, reason sanitization,
and the SQL backstop; the `clawpatrol test` fixture corpus passes
unchanged
* Docs: rules.md gains a unified "Unevaluable conditions fail
closed" section covering truncation, parser refusal, eval errors,
and the SQL backstop; adds the missing ssh stdin row to the
inspection-cap table; the body_json and k8s-no-interactive
examples use guards; plugins.md documents that the optional-field
zero-fill covers declared fields only
0 commit comments