Skip to main content
Devctrl policies use the Common Expression Language (CEL) — a lightweight, fast expression language designed for security policy evaluation. CEL expressions are evaluated on every tool call.

Available variables

Every CEL expression has access to three top-level objects:

identity

Information about the agent making the request.
VariableTypeDescription
identity.namestringThe identity name (e.g., "support-agent")
identity.labelsmap[string, any]Key-value labels assigned to the identity

task

Information about the active task session. Only available when the request includes an X-Task-Token header.
VariableTypeDescription
task.namestringThe task definition name (e.g., "resolve-ticket")
task.labelsmap[string, any]Key-value labels assigned to the task definition
task.contextmap[string, any]The runtime context provided when the task session was created

request

Information about the current tool call.
VariableTypeDescription
request.tool.namestringThe name of the tool being called
request.tool.argsmap[string, any]The arguments passed to the tool

Operators

CEL supports standard operators:
OperatorExampleDescription
==identity.name == "bot"Equality
!=request.tool.name != "delete"Inequality
&&a == 1 && b == 2Logical AND
||a == 1 || b == 2Logical OR
!!identity.labels.restrictedLogical NOT
inrequest.tool.name in ["a", "b"]List membership
>, <, >=, <=task.context.amount < 1000Comparison

String functions

FunctionExampleDescription
startsWith()request.tool.name.startsWith("get_")String prefix check
endsWith()request.tool.name.endsWith("_read")String suffix check
contains()request.tool.name.contains("customer")Substring check
matches()request.tool.name.matches("^get_.*")Regex match

Map access

Access nested map values using dot notation or index notation:
// Dot notation
identity.labels.team

// Index notation (useful for keys with special characters)
identity.labels["team-name"]

// Check if a key exists using 'has'
has(identity.labels.admin)
Accessing a map key that doesn’t exist causes an error, which results in a deny. Use has() to check for optional keys before accessing them.

Common patterns

Tool names are namespaced as serverName__toolName (e.g., github__list_issues). Use the full namespaced name in your expressions. The examples below use short names for readability.

Allow specific tools

request.tool.name in ["github__get_issue", "github__list_issues", "github__add_comment"]

Check identity labels

identity.labels.team == "engineering"

Scope access by task context

task.context.customer_id == request.tool.args.customer_id

Combine multiple conditions

identity.labels.team == "support"
  && request.tool.name in ["get_issue", "get_customer", "add_comment"]
  && task.context.customer_id == request.tool.args.customer_id

Block dangerous tools

// As a deny rule — blocks these tools for everyone
request.tool.name in ["drop_table", "delete_database", "rm_rf"]

Allow based on optional label

// Check if label exists before reading it
has(identity.labels.admin) && identity.labels.admin == true

Evaluation rules

  1. Deny rules are evaluated first. If any deny rule’s expression returns true, the request is blocked.
  2. Allow rules are evaluated next. If any allow rule’s expression returns false, the request is blocked.
  3. If all rules pass, the request is allowed.
  4. If no active release exists, the project’s default action applies.
  5. If a CEL expression errors (e.g., accessing a missing key), it’s treated as a deny.
Order matters. Place deny rules before allow rules for clear, predictable behavior.

Next steps

Policy examples

Real-world policy patterns you can copy and adapt.

Write policies

Author policies in the console’s policy editor.