DSL Basic Structure

This page is for advanced users who need to manually write AgentGuard access control policies using the DSL. It covers the DSL syntax structure, common fields, condition expressions, call-chain rules, and action semantics.

AgentGuard policy files typically use the .rules suffix. A single file can contain multiple rules, each describing what conditions should cause a tool call to be allowed, denied, or sent for review.

Syntax overview

Here is the full syntax structure of the policy DSL:

RULE: <rule_id>
[ON: <event_match>]
[TRACE: <placeholder> <trace_sep> <placeholder> ...]
[CONDITION: <expr>]
POLICY: <action>
[Severity: critical | high | medium | low | info]
[Category: <identifier-or-string>]
[Reason: <string>]
[Priority: <number>]
[ttl_ms: <number>]

# Event matching
<event_match> :=
    tool_call(<tool_pattern>)
  | tool_call.<subtype>
  | tool_call.<subtype>(<tool_pattern>)

<subtype> := requested | completed | failed
<tool_pattern> := * | tool_name | namespace.tool | namespace.*

# Condition expressions
<expr> :=
    <expr> OR <expr>
  | <expr> AND <expr>
  | NOT <expr>
  | (<expr>)
  | <path> <op> <value>
  | <function_call>

<op> := == | != | < | <= | > | >= | IN | NOT IN | MATCHES | CONTAINS

# Actions
<action> :=
    DENY
  | ALLOW
  | HUMAN_CHECK
  | LLM_CHECK

# TRACE separators
<trace_sep> :=
    ->
  | -> * ->
  | -> ... ->
  | -> ...? ->

A minimal rule

This rule denies shell.exec calls from low-trust agents:

RULE: deny_low_trust_shell
ON: tool_call(shell.exec)
CONDITION: principal.trust_level < 2
POLICY: DENY
Severity: high
Category: capability
Reason: "Low-trust agent cannot execute shell commands"

A rule has four core parts:

  • RULE defines the rule ID.
  • ON limits which tool calls the rule applies to.
  • CONDITION defines when the rule matches.
  • POLICY defines the action to take when matched.

Severity, Category, and Reason are metadata. They don't affect condition evaluation, but they appear in audit and alert records. We recommend filling them in for production rules.

Rule ID

The value after RULE is the rule ID, used for auditing, debugging, and policy management. Choose stable, readable, unique names.

RULE: deny_external_email_low_trust

Naming guidelines:

  • Use lowercase letters, digits, underscores, or hyphens.
  • Make the name reflect the action and scenario — e.g. deny_..., review_....
  • Don't reuse the same ID across rules in the same batch; duplicate IDs overwrite each other when loaded into the registry.

ON: scoping rules to tool calls

ON pre-filters candidate rules by event type and tool name. The more precise the ON clause, the easier the rule is to understand and the less likely it is to cause false positives.

ON can be omitted. Omitting it means the rule isn't pre-filtered by tool name — this is generally only recommended for TRACE rules, since the last placeholder in a TRACE further constrains the current call. For single-step rules, always write an explicit ON.

Matching by tool name

RULE: deny_destructive_shell
ON: tool_call(shell.exec)
CONDITION: tool.cmd MATCHES ".*\\b(rm\\s+-rf|mkfs|dd\\s+if=)\\b.*"
POLICY: DENY
Severity: critical
Category: shell

tool_call(shell.exec) only matches the tool named shell.exec.

Wildcards

RULE: review_all_shell_tools
ON: tool_call(shell.*)
CONDITION: principal.trust_level < 3
POLICY: HUMAN_CHECK
Severity: high
Category: shell

shell.* matches shell.exec, shell.run, and similar names. tool_call(*) matches all tool calls.

Matching by tool-call phase

RULE: deny_external_http_before_execution
ON: tool_call.requested(http.post)
CONDITION: tool.domain NOT IN allowlist.http
POLICY: DENY
Severity: high
Category: egress

Common phases:

Phase Meaning
requested Pre-execution request phase — most commonly used for access control
completed Post-execution result phase, where tool.result is available
failed Failed call phase

If you want to intercept before execution, prefer ON: tool_call.requested(...) or simply ON: tool_call(...).

CONDITION: when a rule matches

CONDITION determines whether a rule fires. Conditions are built from paths, literals, comparison operators, functions, and boolean logic.

CONDITION can be omitted. If TRACE is present, the rule matches on call-chain pattern matching. If neither TRACE nor CONDITION is present, the rule fires whenever ON matches. Unless you truly want an unconditional rule, always write an explicit CONDITION.

RULE: deny_external_email_without_scope
ON: tool_call(email.send)
CONDITION: tool.recipient_domain NOT IN allowlist.email
           AND caller.scope_missing("external_email")
POLICY: DENY
Severity: high
Category: egress
Reason: "External email requires external_email scope"

Boolean logic precedence

Operator precedence (high to low):

Precedence Syntax
1 (...)
2 NOT
3 AND
4 OR

When mixing AND and OR, use explicit parentheses:

CONDITION: tool.boundary == "external"
           AND (caller.scope_missing("sensitive_export") OR goal_drift_detected())

Literals

The DSL supports these literal types:

Type Examples
String "evil.com", 'internal'
Number 0, 3, 0.8
Boolean true, false, TRUE, FALSE
String set {"email.send", "http.post"}

String sets are commonly used with IN / NOT IN:

CONDITION: tool.name IN {"email.send", "http.post", "slack.post"}

Comparison and set operators

Operator Meaning Example
== / != Equal / not equal principal.role == "basic"
< / <= / > / >= Numeric or comparable comparison principal.trust_level < 2
IN Left value belongs to right set, list, dict keys, or identical string tool.recipient_domain IN allowlist.email
NOT IN Negation of IN tool.domain NOT IN allowlist.http
MATCHES Regex match tool.url MATCHES ".*127\\.0\\.0\\.1.*"
CONTAINS String contains, list membership, or dict key membership tool.body CONTAINS "password"

MATCHES uses Python re semantics on the right-hand regex string. Common backslash escapes need double escaping, e.g. \\b, \\d.

Common paths

Paths read values from the current event, caller, tool-call arguments, and runtime context.

Caller identity

principal represents the identity of the agent making the current tool call. caller is an alias for principal.

CONDITION: principal.role == "basic"
CONDITION: caller.trust_level < 3

Common fields:

Path Meaning
principal.agent_id Agent ID
principal.session_id Session ID
principal.role Role — common values: basic, default, privileged, system
principal.trust_level Trust level — higher values typically grant more permissions

Tool info and arguments

tool is a convenience alias for tool_call.

CONDITION: tool.name == "send_email_to"
CONDITION: tool.recipient MATCHES ".*@evil\\.com"

Common fields:

Path Meaning
tool.name Current tool name
tool.<param> Current tool parameter — e.g. tool.domain, tool.sql, tool.recipient_domain
tool.result Tool execution result, mainly for result-phase events

For tool function parameters, we recommend tool.<param> because it reads more naturally in policies:

RULE: deny_sql_ddl
ON: tool_call(db.query)
CONDITION: tool.sql MATCHES "(?i).*\\b(DROP|TRUNCATE|ALTER|GRANT|REVOKE)\\b.*"
POLICY: DENY
Severity: critical
Category: database

Tool static labels

When registering a tool, you can declare static labels. Policies can read them via tool.boundary, tool.sensitivity, tool.integrity, and tool.tags.

@guard.tool(
    "send_email_to",
    sink_type="email",
    boundary="external",
    sensitivity="moderate",
    integrity="trusted",
    tags=["egress", "email"],
)
def send_email_to(recipient: str, subject: str, body: str) -> str:
    ...

Matching policy:

RULE: deny_high_sensitivity_external_tool
ON: tool_call.requested
CONDITION: tool.boundary == "external" AND tool.sensitivity == "high"
POLICY: DENY
Severity: critical
Category: data_exfiltration

Label field values:

Field Values
tool.boundary internal, external, privileged
tool.sensitivity low, moderate, high
tool.integrity trusted, unfiltered
tool.tags String list

Event fields

CONDITION: event.session_id == "session-001"
CONDITION: event.type == "tool_call_requested"

Common event aliases:

Path Meaning
event.type Event type
event.id Event ID
event.timestamp Event timestamp
event.session_id Current session ID

Allowlists

Allowlists are injected at runtime. Policies can access them via allowlist.<name> or whitelist("<name>").

POLICY: actions on match

POLICY defines what happens when a rule fires.

Action Meaning
DENY Block the tool call; the original tool is not executed
ALLOW Allow the tool call
HUMAN_CHECK Send to human approval workflow
LLM_CHECK Send to a configured LLM reviewer; falls back to human approval if LLM is not configured or fails

If no rule matches, the runtime defaults to allowing the tool call. Therefore, to express "block unknown targets" or "block anything not on the allowlist," write DENY / HUMAN_CHECK conditional rules rather than only a positive ALLOW rule.

Built-in functions

Functions can appear in conditions. Boolean-returning functions can be used directly as conditions; value-returning functions are typically combined with comparison operators.

Allowlist and set functions

Function Returns Description
whitelist("name") Set Read a runtime-injected allowlist
subset(values, container) Boolean All elements in values are in container
any_in(values, container) Boolean Any element in values is in container

Example: deny if any recipient is not in the user's address book.

RULE: deny_unknown_recipients
ON: tool_call(send_email_to)
CONDITION: NOT subset(tool.recipients, whitelist("user_address_book"))
POLICY: DENY
Severity: high
Category: email

Example: deny if any recipient hits the blocked list.

RULE: deny_blocked_recipient
ON: tool_call(send_email_to)
CONDITION: any_in(tool.recipients, whitelist("blocked_emails"))
POLICY: DENY
Severity: high
Category: email

String, URL, and email functions

Function Description
starts_with(text, prefix) Check string prefix
ends_with(text, suffix) Check string suffix
contains(container, value) Function-form containment check
url.domain(url) Extract URL hostname
url.is_external(url) Check whether a URL is outside the internal domain allowlist
email.domain(address) Extract email domain

Example: block access to local or private network addresses.

RULE: deny_http_ssrf
ON: tool_call(http.get)
CONDITION: tool.url MATCHES ".*(?:localhost|127\\.0\\.0\\.1|10\\.\\d+|172\\.1[6-9]\\.|192\\.168\\.).*"
POLICY: DENY
Severity: critical
Category: ssrf

Example: use URL parsing to check domain.

RULE: review_external_url
ON: tool_call(browser.open)
CONDITION: url.is_external(tool.url)
POLICY: HUMAN_CHECK
Severity: medium
Category: network

Labels, scopes, and tag functions

Function Description
input.has_label("pattern") Whether current session input or upstream data carries a label
input.has_any_label({"a/*", "b/*"}) Whether current session input or upstream data matches any label pattern
caller.scope_missing("scope") Whether the caller lacks a specific scope
tool.has_tag("tag") Whether the current tool has a specific static tag

Example: deny external calls when sensitive labels appear and the caller lacks export authorization.

RULE: deny_sensitive_label_external_without_scope
ON: tool_call.requested
CONDITION: tool.boundary == "external"
           AND input.has_any_label({"pii/*", "finance/*", "secret/*"})
           AND caller.scope_missing("sensitive_export")
POLICY: DENY
Severity: critical
Category: data_exfiltration

Session history and behavioral functions

Function Returns Description
upstream_contains_tool("tool") Boolean Whether a specific tool was called upstream in the current session
upstream_contains_any_tool({"a", "b"}) Boolean Whether any tool in the set was called upstream
derived_from_tool("tool") Boolean Currently evaluates by checking upstream tools
tool_sequence_matches({"a", "b"}) Boolean Whether the session's tool sequence contains these tools in order
repeated_attempts(tool="name", window="5m") Number How many times a tool was attempted in the current session within the window
distinct_targets() Number Count of distinct recent targets
path_length(source="tool") Number Approximate path length from an upstream tool to the current call

Example: LLM review for email after a database query.

RULE: review_email_after_db_query
ON: tool_call(email.send)
CONDITION: upstream_contains_any_tool({"db.query", "database_query"})
POLICY: LLM_CHECK
Severity: high
Category: data_exfiltration
Reason: "Email send follows a database query"

Example: human review for HTTP burst behavior.

RULE: review_http_burst
ON: tool_call(http.post)
CONDITION: repeated_attempts(tool="http.post", window="5m") > 4
POLICY: HUMAN_CHECK
Severity: medium
Category: behavioural_anomaly

Historical parameters and results

Function Returns Description
history_arg("tool", "param") Any Read the most recent call's parameter for a tool in the current session
history_result("tool") Any Read the most recent call's result for a tool in the current session
history_args_match("tool", "param", value) Boolean Whether a historical parameter equals a specified value

Example: deny external email if document classification result is "restricted".

RULE: deny_restricted_doc_external_email
ON: tool_call(email.send)
CONDITION: history_result("classify_doc") == "restricted"
           AND tool.recipient_domain NOT IN allowlist.email
POLICY: DENY
Severity: critical
Category: data_exfiltration

Note: history_arg and history_result read from calls already written to session history. If you need to read parameters from "the current call being evaluated" in a TRACE rule, prefer TRACE placeholders — e.g. Mailer.recipient.

Semantic signal functions

These functions read runtime-injected security signals.

Function Description
goal_drift_detected() Goal drift detected
scope_expansion_detected() Permission or scope expansion detected
suspicious_exfil_pattern() Suspicious exfiltration pattern detected
high_entropy_payload_detected() High-entropy payload detected
goal_changed_from_initial() Current goal has deviated from the initial goal

Example: human review when goal drift is detected before an external call.

RULE: review_goal_drift_external_call
ON: tool_call.requested
CONDITION: tool.boundary == "external" AND goal_drift_detected()
POLICY: HUMAN_CHECK
Severity: high
Category: goal_drift

TRACE: declarative call-chain rules

TRACE describes patterns where "an upstream call eventually flows to a downstream call." It's more powerful than upstream_contains_tool(...) because it lets you name each position in the chain and read its tool name, parameters, labels, and results in the CONDITION.

RULE: deny_secret_to_external_sink
TRACE: SecRead ->...?-> Sink
CONDITION: SecRead.name == "secret.read"
           AND Sink.name IN {"http.post", "email.send", "slack.post"}
POLICY: DENY
Severity: critical
Category: secret_exfiltration
Reason: "Tool chain reads secret and then contacts an external sink"

TRACE separators

Syntax Meaning Example
A -> B B immediately follows A; no other calls between them Fetcher -> Executor
A -> * -> B Exactly one call between A and B Reader -> * -> Writer
A -> ... -> B At least one call between A and B A -> ... -> B
A -> ...? -> B A appears before B with zero or more calls between them DbOp -> ...? -> Mailer

The most commonly used separator is -> ...? ->, meaning "an upstream call eventually reaches a downstream call."

TRACE placeholder fields

In TRACE: Src -> ...? -> Dst, Src and Dst are placeholders that bind to specific tool calls in the session's call chain.

Field Meaning
Placeholder.name Bound call's tool name
Placeholder.integrity Bound call's label.integrity
Placeholder.sensitivity Bound call's label.sensitivity
Placeholder.boundary Bound call's label.boundary
Placeholder.result Bound call's return value
Placeholder.<param> Bound call's parameter — e.g. Mailer.recipient

Example: LLM review when external input reaches shell execution.

RULE: review_external_input_to_shell
TRACE: Src ->...?-> Shell
CONDITION: Src.boundary == "external"
           AND Shell.name IN {"shell.exec", "shell_exec"}
POLICY: LLM_CHECK
Severity: critical
Category: prompt_injection
Reason: "External input reached shell execution"

Example: deny when a database query is immediately followed by email sending.

RULE: deny_adjacent_db_to_email
TRACE: DbOp -> Mailer
CONDITION: DbOp.name IN {"db.query", "database_query"}
           AND Mailer.name IN {"email.send", "send_email_to"}
POLICY: DENY
Severity: critical
Category: data_exfiltration

Metadata

Severity: critical
Category: data_exfiltration
Reason: "Sensitive data sent to non-allowlisted endpoint"

We recommend at least filling in:

Field Recommendation
Severity Used for alert triage; use critical, high, medium, low, or info
Category Used for classification — e.g. egress, database, data_exfiltration
Reason One sentence explaining the security reason for the rule match

Policy writing tips

Start with high-risk tools

Write rules first for outbound, command execution, file writes, database writes, and sensitive data read tools.

RULE: deny_db_write_basic
ON: tool_call(db.exec)
CONDITION: tool.sql MATCHES "(?i).*\\b(INSERT|UPDATE|DELETE|DROP|ALTER)\\b.*"
           AND principal.role == "basic"
POLICY: DENY
Severity: critical
Category: database

Write narrow rules first

Use ON: tool_call(email.send) rather than ON: tool_call(*). Use tool.name IN {...} rather than vague regexes.

Narrow rules are easier to audit and easier to troubleshoot for false positives.

Separate deny and approve

Use DENY for clearly dangerous behavior, and HUMAN_CHECK or LLM_CHECK for uncertain-but-high-risk behavior.

Use TRACE for cross-step risks

Single-step rules can only see the current operation. For risks like "read sensitive data, then exfiltrate it," use TRACE, or session history functions.

RULE: review_db_result_to_external_email
TRACE: DbOp ->...?-> Mailer
CONDITION: DbOp.name IN {"db.query", "database_query"}
           AND Mailer.name IN {"email.send", "send_email_to"}
           AND Mailer.boundary == "external"
POLICY: LLM_CHECK
Severity: high
Category: data_exfiltration

Don't rely on rule order for business intent

A policy file can contain multiple rules. When several rules match simultaneously, the runtime merges the results and selects the appropriate action. For predictable behavior, write explicit mutually exclusive conditions rather than relying on rule ordering.

Validate rules before deployment

After writing policies, run:

python -m agentguard check rules/my_policy.rules

You can also validate all .rules files in a directory:

python -m agentguard check rules/

This command parses, compiles, and runs semantic checks — it catches common issues like duplicate rule IDs, missing metadata, invalid TRACE separators, and incorrect label enum values.

results matching ""

    No results matching ""