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.
Variable Type Description 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.
Variable Type Description 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.
Variable Type Description 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:
Operator Example Description ==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
Function Example Description 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.
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
// 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
Deny rules are evaluated first. If any deny rule’s expression returns true, the request is blocked.
Allow rules are evaluated next. If any allow rule’s expression returns false, the request is blocked.
If all rules pass, the request is allowed.
If no active release exists, the project’s default action applies.
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.