Are you an LLM? You can read better optimized documentation at /docs/agent-guard/policies/cel-context-reference.md for this page in Markdown format
CEL context reference
Every assert expression in a rule is CEL, evaluated against a context the engine builds for that request. Which context you get depends on the policy kind: artifact for ArtifactPolicy, tool for ToolPolicy, guardrail for GuardrailPolicy, and request for both runtime kinds.
This page lists every field the engine exposes, as it is actually populated.
Fields that are not always there
Evaluation is fail-closed, and reading a field that is absent is an evaluation error, so an unguarded rule does not skip the case it did not anticipate: it blocks it, with an engine error in place of your message.
Most fields are always present. Four are conditional and need a has() guard before you touch them:
| Field | Absent when |
|---|---|
artifact.kitfile | The artifact has no Kitfile |
artifact.layers | The artifact has no layers |
tool.session | No session is being tracked for the call |
request.user | The caller supplied no identity |
yaml
assert: |
!has(artifact.kitfile) ||
artifact.kitfile.package.license == "Apache-2.0"Guarding turns a missing Kitfile into your decision with your message. Without the guard, the same artifact fails with no such key: kitfile, which tells the operator nothing.
Three fields are always present but are maps that may be empty, so the risk is the key, not the field. Guard membership before indexing tool.arguments, guardrail.scores, or artifact.annotations:
yaml
assert: |
!("command" in tool.arguments) ||
!tool.arguments.command.contains("rm -rf")Two list fields are always present, empty when there is nothing in them, so exists() over artifact.attestations or guardrail.findings is safe unguarded.
artifact: ArtifactPolicy, admission phase
| Field | Type | Notes |
|---|---|---|
artifact.registry | string | OCI registry hostname. "localhost" for refs resolved from the kitops on-disk store |
artifact.repository | string | Repository path |
artifact.tag | string | Version tag |
artifact.digest | string | Content-addressable digest |
artifact.mediaType | string | Artifact type from the OCI config descriptor, not the manifest media type. This is what match.artifactTypes compares against |
artifact.size | int | Size in bytes |
artifact.annotations | map | OCI annotations. May be empty; guard membership before indexing |
artifact.createdAt | timestamp | Compare with timestamp("..."), not a string |
artifact.pushedBy | string | Pusher identity |
artifact.attestations | list | Always present, empty when there are none |
artifact.attestations[].predicateType | string | What hasAttestation() matches on, exactly |
artifact.attestations[].issuer | string | Signer identity, as recorded on the attestation. For the cosign attestations Agent Guard adds itself, this is the path of the public key that verified the artifact, not an email or OIDC identity, which is what artifact.signedBy() compares against |
artifact.attestations[].predicate | map | Attestation payload. Every field other than the two above lives in here |
artifact.kitfile | map | Conditional. Guard with has(artifact.kitfile). Carries .manifestVersion, .package, .model, and .datasets when present |
artifact.layers | list | Conditional. Entries carry .digest, .mediaType, .size, .annotations |
artifact.attestations carries the attestations attached to the artifact being admitted, and rules can key on any predicate type. A SLSA provenance requirement is written the usual way:
yaml
assert: |
artifact.hasAttestation("https://slsa.dev/provenance/v1")Two sources feed the list. Attestations stored alongside the artifact in the registry, such as SLSA provenance attached to a ModelKit when it was built, arrive with the artifact. Agent Guard adds the results of its own cosign verification: one attestation per public key passed with --pub-key, each with predicateType: "cosign" and a predicate carrying subject, timestamp and verified.
hasAttestation() matches on predicateType exactly, and it answers whether an attestation is present, not whether anything validated it. For cosign, verified in the predicate is what says a signature actually checked out. For other predicate types, read the field in the predicate that carries the claim you care about rather than treating presence as proof.
Because hasAttestation() returns false when a predicate is absent, a rule requiring provenance denies artifacts that lack it. That is the intended behavior, but it makes such a rule a good candidate for Audit first, so you can see which artifacts in your estate carry the provenance before the rule starts blocking on it.
tool: ToolPolicy, runtime phase
| Field | Type | Notes |
|---|---|---|
tool.name | string | Tool name, e.g. Bash, Read, WebFetch |
tool.server | string | MCP server that exposes the tool |
tool.serverVersion | string | MCP server version |
tool.title | string | Human-readable tool title |
tool.description | string | Tool description |
tool.arguments | map | Tool-specific. Bash sends command, Read sends file_path, Grep sends pattern. Guard membership before reading any of them |
tool.session | map | Conditional. Carries .toolCalls, .thisToolCalls (ints) and .lastCallTime (timestamp) |
Tool and argument names come from the agent, not from Agent Guard, so they differ across agents. The shell tool is Bash in Claude Code and run_command in Antigravity CLI, and the file-reading tool is Read in Claude Code and view_file in Antigravity CLI. Antigravity CLI names the arguments differently (CommandLine, AbsolutePath). Agent Guard passes both key sets, the agent's own and command and file_path alongside them, so a rule written against command or file_path covers both agents once both tool names are in its match list.
guardrail: GuardrailPolicy, runtime phase
| Field | Type | Notes |
|---|---|---|
guardrail.name | string | Scanner name |
guardrail.version | string | Scanner version |
guardrail.direction | string | input, output or both |
guardrail.scores | map(string, double) | Only the keys the attached scanner produces. Guard membership before indexing |
guardrail.findings | list | Always present, empty when there are none |
guardrail.findings[].category | string | Where the text came from: body, full_body, file, json |
guardrail.findings[].severity | string | Scanner-assigned severity |
guardrail.findings[].message | string | The matched content or the scanner's description of it |
guardrail.findings[].span | map | Conditional. Carries .start and .end offsets |
Finding categories matter when writing content rules. body is the current turn's text, full_body the entire raw request including conversation history, file text extracted from an attachment, and json the canonicalized body, used when the current turn carries no text of its own. A turn carrying only an attachment emits no body finding, so a rule that checks body alone lets it through. Match json as well.
Attachment text comes from Word, Excel and PowerPoint documents, plain-text formats (.txt, .md, .csv, .tsv, .json, .xml, .yaml), PDFs with a text layer, and images through OCR. A file the extractor cannot open produces no file finding at all and is forwarded uninspected with a warning in the log: a scanned PDF with no text layer, a password-protected PDF, an unrecognized archive, or an image carrying no recognizable text. This is the one place where guardrail evaluation fails open rather than closed, so treat attachment scanning as a net rather than a boundary.
An unguarded score lookup is the most damaging mistake available in a guardrail policy. guardrail.scores["toxicity"] < 0.7 with no scanner wired means the key is absent, CEL raises, and fail-closed evaluation blocks every model request in the deployment. Written with a guard, the rule stays dormant until a scanner supplies the score and then takes effect on its own:
yaml
assert: |
!("toxicity" in guardrail.scores) ||
guardrail.scores["toxicity"] < 0.7request: both runtime kinds
| Field | Type | Notes |
|---|---|---|
request.action | string | Requested action |
request.sessionId | string | Session identifier |
request.conversationId | string | Conversation identifier |
request.timestamp | timestamp | Request time |
request.clientIp | string | Originating IP |
request.userAgent | string | Client user agent |
request.elicitation | map | Always present. Defaults to responded: false, action: "", content: {} when no elicitation is in flight |
request.user | map | Conditional. Carries .id, .type, .roles, .groups, .attributes |
Helper functions
| Call | Returns | Notes |
|---|---|---|
artifact.hasSignature() | bool | True when attestations is non-empty. It does not check that anything verified; use predicate.verified for that |
artifact.signedBy(id) | bool | True when some attestation's issuer equals id exactly. Compare against a key path, per issuer above |
artifact.hasAttestation(type) | bool | True when some attestation's predicateType equals type exactly |
artifact.attestation(type) | map or null | First matching attestation, null when none matches. Reading a field off null fails, so pair it with hasAttestation() |
<string>.isInternalIP() | bool | Called on a URL or bare IP string, not on a context object. Parses the URL, then tests the host against the private ranges |
hasSignature() answers "is there an attestation attached," not "did a signature verify." A policy that requires real verification reads the predicate:
yaml
assert: |
artifact.hasAttestation("cosign") &&
artifact.attestation("cosign").predicate.verifiedCEL dialect notes
Strings expose .contains(substring), .startsWith(prefix), .endsWith(suffix), .matches(regex), .size(), and concatenation with +.
.contains() is substring matching, not regex: .contains("curl") matches concurly as well as curl https://example.com. Use .matches() when you need a boundary.
Regular expressions are RE2. There are no backreferences and no lookahead, so some patterns cannot be written as a single expression and have to be split across rules or restructured. Character classes survive YAML quoting better than backslash escapes: [.]env and \.env mean the same thing to RE2, but [.] cannot be mangled on its way through the parser.
A literal regex that does not compile fails the policy at load time rather than at evaluation time, which means a bad pattern is caught by gerty validate instead of blocking an agent later.
