CEL engine

Understand DefenseClaw's typed ActionFacts projection, CEL admission rules, bounded evaluation, semantic ownership, and fallback behavior.

Shell examples

The shell examples require Bash. On native Windows, use Git Bash or WSL; PowerShell uses different continuation and environment-variable syntax.

DefenseClaw embeds CEL as a bounded semantic matcher, not as a general-purpose policy runtime. The server establishes a trusted tool-action boundary, normalizes the action into typed ActionFacts, and evaluates one immutable Boolean program per semantic rule owner.

Use Authoring CEL rules for the rule schema, field reference, enums, and examples. This page defines what the compiler and runtime accept and how a match becomes detection or enforcement evidence.

Evaluation pipeline

Each semantic rule is checked on its own. CEL runs only on arguments the parser fully understood, and a match is tried a second time against only the commands that will really run.

checks
yes
no
error, over budget
false
true
yes
no
Gateway parses the argumentsinto ActionFacts
Fully understood?no unsupported or ambiguous parts
CEL matches all facts?
Regex patternruns instead of CEL
No findingregex skipped
Matches what really runs?execute-only view
Detection only
Enforcement candidateto the final proof check

List view for small screens. Use the expand button to open the drawing.

  1. Gateway parses the argumentsinto ActionFacts
    • checksFully understood?
  2. Fully understood?no unsupported or ambiguous parts
    • yesCEL matches all facts?
    • noRegex pattern
  3. CEL matches all facts?
    • error, over budgetRegex pattern
    • falseNo finding
    • trueMatches what really runs?
  4. Regex patternruns instead of CEL
  5. No findingregex skipped
  6. Matches what really runs?execute-only view
    • yesEnforcement candidate
    • noDetection only
  7. Enforcement candidateto the final proof check
  8. Detection only
How one rule is checked. The execute-only view drops previews such as --help or --dry-run. A match also stays detection-only when the connector cannot block or the rule is detection-only.

Every finding, from CEL or from a fallback, then passes one final check before it can change the action.

checked
checked
yes
no
CEL enforcement candidate
Fallback and CodeGuard findings
Complete proof for this rule ID?and the connector can block
Severity picks the actionallow · alert · confirm · block
Detection onlyrecorded, action unchanged

List view for small screens. Use the expand button to open the drawing.

  1. Fallback and CodeGuard findings
    • checkedComplete proof for this rule ID?
  2. CEL enforcement candidate
    • checkedComplete proof for this rule ID?
  3. Complete proof for this rule ID?and the connector can block
    • yesSeverity picks the action
    • noDetection only
  4. Detection onlyrecorded, action unchanged
  5. Severity picks the actionallow · alert · confirm · block
The final proof check never removes a finding. It only decides whether the finding may change the action.

ActionFacts and raw tool arguments stay private to the in-process analysis path. They are not exposed as a public CEL endpoint, logged as CEL input, or written to the durable ordered-state ledger.

Admission and activation

The rule-pack loader compiles every supplied expression, including disabled rules. Each expression must be Boolean, contain no surrounding whitespace, and use only the admitted surface below. Its rule must also contain a valid bounded pattern fallback.

Catalog categories merge before compilation:

  • A unique custom category adds to the compiled baseline.
  • A category with the same name as a bundled category replaces that entire category.
  • Any schema, regex, type, admission, enum, or cost failure rejects the complete candidate generation.

A rejected candidate is not published. If the gateway already has an active in-process generation, that generation remains unchanged; offline validation reports the candidate failure.

Supported CEL subset

DefenseClaw admits a small, reviewable CEL surface.

SupportedExamples and constraints
Boolean logic&&, ||, !
Scalar comparison==, !=, and in for bounded Boolean, integer, and string values
Collection predicatesexists and all comprehensions; nesting is limited to two levels. A comprehension must give a Boolean, so validate-pack refuses map, filter and exists_one (semantic_surface)
String predicatesstartsWith, endsWith, contains, and member-form matches
LiteralsBoolean, integer, string, and scalar lists
Field selectionTyped fields reachable from f using protobuf snake-case names

Arithmetic, ordering comparisons, indexing, ternaries, maps, has, null, floating-point/unsigned/byte literals, arbitrary functions, message equality, and list equality are rejected. DefenseClaw adds no custom CEL helper functions. matches must use a literal Go/RE2 pattern of at most 512 bytes; dynamic regexes and global-form matches(value, pattern) are rejected.

Resource limits

LimitMaximum
Expression size16 KiB in bytes and runes
Checked AST4,096 nodes; depth 64
Comprehension nesting2
CEL regex literal512 bytes
Fallback regex2,048 bytes
Static cost per rule6,000,000
Runtime cost per rule1,250,000
Semantic rules per effective catalog256
Enabled catalog static cost32,000,000
Trusted dispatch budget50 ms and 24,000,000 total cost

These bounds apply before connector action mapping. Exceeding an admission limit rejects the candidate; exceeding a runtime limit takes that owner to its fallback without stopping unrelated rules.

validate-pack names only exists() and all() in a refusal, the two comprehensions it admits. A rule nested too deeply is refused with it nests exists() and all() too deeply; flatten the check or split it into separate rules. A rule over the static cost limit is refused with estimated worst-case evaluation cost is above the per-rule limit and the advice to test one list, prefer ==, in or startsWith over contains or matches inside a nested comprehension, or split the check into separate rules.

The 512-byte CEL regex limit caps its size. The static cost limit can reject a shorter pattern; check the whole rule with defenseclaw guardrail validate-pack.

Runtime results

ResultOwner behavior
Compile or admission failureReject the candidate generation; do not publish it.
Projection is unsupported, partial, ambiguous, invalid, or uncertainRun this owner's required regex fallback; a raw pattern match remains detection-only.
The only unknown part of a POSIX command is a redirect target the shell expands at run time and that can only name a file (> ~/out.txt, > "$HOME/out.txt", > out-*.txt)Analyze the command with a static stand-in target, wrapped commands included, and evaluate CEL on that analysis without the stand-in's redirect and path. A match counts as authoritative semantic evidence when the expression cannot depend on the dropped redirect and path: it reads redirects, paths, artifacts and archive_lineages only as the range of exists() reached through &&, || and quantifier predicates, and reads neither parse nor a lineage's authoritative. A command's argv_complete (c.argv_complete) may be read: each command on that analysis has the complete, static argv of a full analysis. A built-in owner's code prerequisite must hold both on that analysis and on the one with the stand-in target. A non-match, or any other rule, runs the owner's regex fallback on the whole command.
A POSIX command has && or || lists (cd <dir> && <cmd>, <cmd> || true)A block stops the whole call before any of it runs, so the command is judged as if every command of each list runs: analyze it with each list read as the sequence of its commands, and evaluate CEL, code prerequisites and the context checks for content and sensitive-path findings on that analysis. For an expression that reads neither parse nor a lineage's authoritative (a command's argv_complete may be read), a match or non-match there counts as it does for the same commands joined by ;, so a rule that blocks a; b also blocks a && b and a || b. Other rules run the owner's regex fallback, and a fallback finding blocks when that analysis proves it, as for a; b. A list with a runtime-expanded redirect target gets both treatments. A negated (!), background (&) or coprocess statement in a list, a function definition or a here-document keeps the regex fallback, and its matches stay detection-only.
A PowerShell statement list or pipeline is partial because of one of its statements (Write-Output <x>; exit $LASTEXITCODE, Get-Location; Write-Output <x>): the body of pwsh -Command or -EncodedCommand, or the command of a shell tool that runs in PowerShell, such as Codex on WindowsParse the statements one by one, in order, up to the first one that cannot be proved, and stop after a statement that may change what the next one runs (only output and read commands such as Write-Output, Get-Location and Get-Content let the next one in). A match of an expression that more commands and arguments cannot undo, such as f.commands.exists(c, "<x>" in c.argv), on those commands counts; a non-match, or any other rule, runs the owner's regex fallback on the whole command.
CEL evaluates falseProduce no owner finding and suppress only this owner's claimed fallback IDs on the structured route.
CEL matches full facts but not the execute-only projectionCreate detection-only evidence; preview behavior cannot synchronously block.
CEL matches both projections on an enforcement-eligible, synchronous surfaceCreate an enforcement candidate from the YAML metadata; the final same-rule proof gate must still authorize it.
CEL matches but the connector surface is not block-capable or enforcement-eligibleCreate detection-only evidence.
CEL errors, times out, or exceeds runtime costRun this owner's regex fallback; unrelated rules continue and raw matches remain detection-only.

Every trusted-action candidate then crosses a final, code-owned proof gate. It can contribute to enforcement only when its proof is complete and pinned to the same rule ID: authoritative ActionFacts semantic evidence, an exact code-owned built-in CodeGuard result from a complete scan, or a bounded exact fallback result. Raw regex matches, custom CodeGuard matches, parser-shadow evidence, partial or invalid projections, and unpinned results remain visible as detection-only findings. The gate never removes a finding; it only prevents incomplete evidence from authorizing a block. A finding that passes counts toward the action through the connector's severity thresholds; a built-in owner marked alert-only can raise at most alert.

Exact-boundary examples

FamilyEnforcement-eligible exampleAdvisory or quiet control
HTTP destination hostnameStatic ASCII DNS bytes from generated authority, HTTP CONNECT, remote-resolved SOCKS4a/5h destination fields, plaintext HTTP Host or canonical HTTPS SNI observed after a SOCKS handshake, or canonical HTTPS origin SNI; GNU Wget generated authority/origin SNI additionally requires --no-config, and every component is bound to the exact external origin or proxyA local-only route or numeric hostname, non-ASCII IDN spelling, dynamic or config-derived value, wrapper/pipeline/shell redirection, promptable authentication, unresolved file/TLS setup, a modeled eagerly checked compression/TLS/authentication capability option or modeled final enabled capability toggle, conflicting pre-wire option, direct plaintext HTTP Host override with no remaining proxy-visible authority, target routed through an HTTPS proxy without authenticated feature facts, unsupported multi-hop proxy chain, or bare PowerShell curl/wget alias
HAProxy client-IP preambleNone until #770 supplies an authenticated fact for the exact curl executable's version and build supportEvery --haproxy-clientip form remains LOW and detection-only, including direct HTTP(S), proxy/SOCKS/preproxy, --noproxy, multiple targets or --next, static/dynamic/oversized values, setup-preempted commands, aliases, and any executable-path spelling
FTP control metadataPOSIX or structured-argv static curl FTP account or alternative bytes bound to one external FTP(S) origin or proven plaintext proxy observerDynamic/config-driven values, raw Windows shell input, a local-only destination, or an unproved route
SOCKS authenticationStatic SOCKS4 username or SOCKS5 username/password bound to the exact external proxy peerDisabled/bypassed proxy, unsupported chain, ambiguous credentials, or pre-connect failure
Telnet metadataPOSIX or structured-argv static USER, TTYPE, XDISPLOC, or NEW_ENV bytes bound to one direct external Telnet peerRaw Windows shell input, capability-dependent, proxied, malformed, or local-only negotiation; invalid WS/BINARY settings
Static stdin uploadPortable literal echo or format-only printf bytes flowing into one exact external curl stdin uploadShell options/escapes, expansion, wrappers, intermediate transforms, or local upload
Minix formattingExact mkfs.minix grammar targeting a raw deviceImage target, help/version, invalid block count, near-miss executable, or untrusted path

For the modeled curl compression/TLS/authentication capability controls, an eagerly validated positive option, or the final enabled state of a final-toggle option, closes every exact transmission projector, not only the hostname lane. A disabled negative form can remain eligible only when no earlier eager validation failed, every transfer group ends with the modeled final-toggle capability disabled, and every other prerequisite is exact. Other optional features stay detection-only until DefenseClaw has an authenticated fact for the capability they depend on.

DefenseClaw has no current HAProxy client-IP enforcement lane. Curl 8.7.1 primary-source and loopback-wire evidence shows that a capable direct build writes the PROXY preamble before the HTTP request and, for HTTPS, before TLS. That audit also supplies future-projector constraints: the existing no-ambient-defaults boundary, a maximum 1976-byte operand, exact receiver binding, and rejection of --ipv4 with a literal IPv6 peer before any write. It does not prove the executable that will run. The option is absent before curl 8.2.0 and is compiled out with CURL_DISABLE_PROXY; native name, path, dialect, and operating system do not authenticate either property. #770 owns the resolved executable identity, version/build capabilities, provenance, and invalidation rules required before any of these forms can enforce.

Raw CMD/PowerShell curl gains exact ordinary HTTP(S) headers, origin credentials, inline/body and file-upload projection, plus supported direct proxy/SOCKS credentials. Exact hostname projection on PowerShell requires the explicit native curl.exe or wget.exe; the unqualified names belong to PowerShell aliases and remain detection-only. FTP account/alternative control fields, SMTP envelope fields, and Telnet USER/TTYPE/XDISPLOC/NEW_ENV remain detection-only on raw Windows shell input; their closed projectors still require POSIX or structured argv.

Custom rules own their own rule ID. DefenseClaw may define internal alias, prerequisite, and safe-negative contracts for bundled owners, but those maps are not configurable through CEL or YAML. A fallback match and authoritative CEL result never produce duplicate findings for the same owner.

The stateful connector lifecycle determines which native events are synchronous, block-capable, outcome-paired, or eligible to advance durable ordered state.

Single-call CEL and ordered state

Single-call CEL findings can run on trusted inspect calls and authenticated native pre-tool hooks. Durable chains are narrower: only authenticated native connector hook events with the required session and invocation identity can advance the SQLite state ledger. Inspect, proxy, router, and OTLP traffic never advance a chain.

The 26 ordered chains are a fixed compiled catalog. Adding a CEL/YAML rule does not define a new sequence; custom CEL rules remain single-action matchers. Twenty-two chains are alert-only and four can contribute enforcement-safe proof, still subject to connector capability, profile posture, and the same-rule proof gate. See the deterministic detection reference for the canonical inventory and Stateful connector lifecycle for the pending-to-success contract and connector matrix.

Test engine behavior

defenseclaw guardrail validate-pack is the public authority for schema, regex, CEL typing and admission, and aggregate-cost checks. It does not inject synthetic facts or prove that a connector projects the fields an expression expects.

Repository contributors should add focused Go fixtures and use Go 1.26.4, the CI toolchain. The gateway suite runs through the shard runner with an explicit timeout:

go test -timeout 30m ./internal/guardrail/semantic ./internal/guardrail
python3 scripts/go_test_shards.py --package-dir internal/gateway --shard-count 16 -- go test -timeout 30m ./internal/gateway

When the compiled default catalog changes, also run make generate-guardrail-catalog and make check-guardrail-catalog.