Built-in rules reference¶
Rules return True to pass and False to violate. Names, enforcement points and parameter schemas below come from the shipped registry. See the rule stability contract.
Complexities are worst case per evaluation. n counts task history steps; other dimensions are named per rule. Unnamed configured lists, filters, paths and step values are fixed size. History rules include the context's first copy of task history.
all_of¶
Compound rule: passes if and only if all sub-conditions pass. Supports arbitrary nesting.
Enforcement points: step_execution, agent_registration
Worst-case time complexity: O(c + sum of child costs); c is condition count
Parameters¶
{
"conditions": {
"description": "List of sub-condition objects, each with ``rule_type`` and ``params``",
"items": {
"properties": {
"params": {
"description": "That rule's params, checked against its own schema (default: none)",
"required": false,
"type": "object"
},
"rule_type": {
"description": "A registered rule name",
"required": true,
"type": "string"
}
},
"type": "object"
},
"required": true,
"type": "array"
}
}
Example parameters¶
{
"conditions": [
{
"params": {
"field": "step_name"
},
"rule_type": "field_not_empty"
},
{
"params": {
"end_hour": 17,
"start_hour": 9
},
"rule_type": "working_hours_only"
}
]
}
any_of¶
Compound rule: passes if at least one sub-condition passes. Supports arbitrary nesting.
Enforcement points: step_execution, agent_registration
Worst-case time complexity: O(c + sum of child costs); c is condition count
Parameters¶
{
"conditions": {
"description": "List of sub-condition objects, each with ``rule_type`` and ``params``",
"items": {
"properties": {
"params": {
"description": "That rule's params, checked against its own schema (default: none)",
"required": false,
"type": "object"
},
"rule_type": {
"description": "A registered rule name",
"required": true,
"type": "string"
}
},
"type": "object"
},
"required": true,
"type": "array"
}
}
Example parameters¶
{
"conditions": [
{
"params": {
"field": "step_name"
},
"rule_type": "field_not_empty"
},
{
"params": {
"field": "kind",
"values": [
"step.model"
]
},
"rule_type": "field_in_list"
}
]
}
conditional_successor_required¶
If the previous step matches a trigger condition, the current step must satisfy successor constraints.
Enforcement points: step_execution
Worst-case time complexity: O(n) worst case, over the attempts it looks past
Parameters¶
{
"forbidden_kinds": {
"description": "Current step must not be these types",
"items": {
"enum": [
"step.credential",
"step.exec",
"step.gate",
"step.message",
"step.model",
"step.resource",
"step.self",
"step.unknown",
"task.end",
"task.error",
"task.idle",
"task.start"
],
"type": "string"
},
"required": false,
"type": "array"
},
"required_kind": {
"description": "Current step must be this type",
"enum": [
"step.credential",
"step.exec",
"step.gate",
"step.message",
"step.model",
"step.resource",
"step.self",
"step.unknown",
"task.end",
"task.error",
"task.idle",
"task.start"
],
"required": false,
"type": "string"
},
"trigger_condition": {
"description": "Condition on a previous step field",
"path": "full",
"properties": {
"field": {
"description": "Full step field path on the previous step, e.g. 'properties.target.trust'",
"required": true,
"type": "string"
},
"op": {
"description": "Comparison; 'below' and 'above' need a numeric value",
"enum": [
"below",
"above",
"equals",
"contains"
],
"required": true,
"type": "string"
},
"value": {
"description": "Value compared against the field; any type, a number for 'below' and 'above'",
"required": true
}
},
"required": true,
"type": "object"
},
"trigger_kinds": {
"description": "Trigger previous kinds",
"items": {
"enum": [
"step.credential",
"step.exec",
"step.gate",
"step.message",
"step.model",
"step.resource",
"step.self",
"step.unknown",
"task.end",
"task.error",
"task.idle",
"task.start"
],
"type": "string"
},
"required": true,
"type": "array"
}
}
Example parameters¶
{
"required_kind": "step.gate",
"trigger_condition": {
"field": "properties.target.trust",
"op": "equals",
"value": "external"
},
"trigger_kinds": [
"step.resource"
]
}
current_is¶
Pre-flight check: the intended behavior matches the given kind, optional verb, and optional property_filter. Useful as a condition inside all_of / any_of compound rules.
Enforcement points: step_execution
Worst-case time complexity: O(f); f is property filter size
Parameters¶
{
"kind": {
"description": "Required kind",
"enum": [
"step.credential",
"step.exec",
"step.gate",
"step.message",
"step.model",
"step.resource",
"step.self",
"step.unknown",
"task.end",
"task.error",
"task.idle",
"task.start"
],
"required": true,
"type": "string"
},
"property_filter": {
"description": "Property filter on current step",
"path": "property",
"required": false,
"type": "object"
},
"verb": {
"description": "Required verb (if any)",
"enum": [
"DELETE",
"GET",
"PATCH",
"POST"
],
"required": false,
"type": "string"
}
}
Example parameters¶
{
"kind": "step.resource",
"verb": "DELETE"
}
domain_allowlist¶
Steps of the listed kinds must have a target host (properties.target.host) in the declared allowlist. A listed step with an absent, empty or blank host is a violation; steps of other types are not evaluated. A host is allowed when it equals a listed domain or is a subdomain of one. A URL and a bare host are normalised the same way: lower-cased, port, path and one trailing dot removed, the host taken after the last '@'. A value that is not a plain DNS name (backslash, whitespace, control characters, several '@', IPv6 literal, unicode) is a violation. A raw IP is allowed only if it is listed verbatim.
Enforcement points: step_execution
Worst-case time complexity: O(kh + d); k is domain count, h is host length, d is total domain length
Parameters¶
{
"allowed_domains": {
"description": "Permitted domain names",
"required": true,
"type": "array"
},
"target_kinds": {
"description": "Kinds the allowlist applies to (e.g. ['step.resource'])",
"items": {
"enum": [
"step.credential",
"step.exec",
"step.gate",
"step.message",
"step.model",
"step.resource",
"step.self",
"step.unknown",
"task.end",
"task.error",
"task.idle",
"task.start"
],
"type": "string"
},
"required": true,
"type": "array"
}
}
Example parameters¶
{
"allowed_domains": [
"api.openai.com",
"api.example.com"
],
"target_kinds": [
"step.resource"
]
}
execution_max_steps¶
Task cannot exceed a maximum number of steps matching the given kind (and optional verb and/or step_name).
Enforcement points: step_execution
Worst-case time complexity: O(n)
Parameters¶
{
"kind": {
"description": "Kind to count",
"enum": [
"step.credential",
"step.exec",
"step.gate",
"step.message",
"step.model",
"step.resource",
"step.self",
"step.unknown",
"task.end",
"task.error",
"task.idle",
"task.start"
],
"required": true,
"type": "string"
},
"max_steps": {
"description": "Maximum allowed steps, the intended one included; 0 forbids every matching step",
"minimum": 0,
"required": true,
"type": "integer"
},
"step_name": {
"description": "If set, count only steps with this step name (e.g. a specific tool)",
"required": false,
"type": "string"
},
"verb": {
"description": "If set, count only steps with this verb",
"enum": [
"DELETE",
"GET",
"PATCH",
"POST"
],
"required": false,
"type": "string"
}
}
Example parameters¶
{
"kind": "step.resource",
"max_steps": 1,
"step_name": "send_email"
}
field_in_list¶
Checks that a field value is in a list of allowed values. Step fields use kind, verb, step_name, input.
Enforcement points: agent_registration, step_execution
Worst-case time complexity: O(p + k); p is field path length, k is allowlist length
Parameters¶
{
"field": {
"description": "Step field path or registration field name",
"path": "full",
"required": true,
"type": "string"
},
"values": {
"description": "Allowed values",
"required": true,
"type": "array"
}
}
Example parameters¶
{
"field": "step_name",
"values": [
"fetch_invoice"
]
}
field_matches_regex¶
Checks that a field value matches an RE2 regular expression pattern (no lookaround or backreferences). The pattern is searched anywhere in the value; anchor with ^ and $ for a whole-value match. Step fields use kind, verb, step_name, input.
Enforcement points: agent_registration, step_execution
Worst-case time complexity: O(p + r + v); p is field path length, r is pattern length, v is value length (RE2)
Parameters¶
{
"field": {
"description": "Step field path or registration field name",
"path": "full",
"required": true,
"type": "string"
},
"pattern": {
"description": "RE2 regex; no lookaround or backreferences; anchor with ^ and $",
"required": true,
"type": "string"
}
}
Example parameters¶
{
"field": "step_name",
"pattern": "^example$"
}
field_not_empty¶
Checks that a field has a non-empty value. Absent, null, an empty or whitespace-only string, and an empty list, tuple, set or mapping are empty; false and 0 are values. Step fields use kind, verb, step_name, input.
Enforcement points: agent_registration, step_execution
Worst-case time complexity: O(p); p is field path length
Parameters¶
{
"field": {
"description": "Step field path or registration field name",
"path": "full",
"required": true,
"type": "string"
}
}
Example parameters¶
{
"field": "step_name"
}
history_contains¶
Pre-flight check: task history must contain at least one step matching the given kind, optional verb, optional property_filter and optional output_filter.
Enforcement points: step_execution
Worst-case time complexity: O(n)
Parameters¶
{
"count_attempts": {
"description": "Whether blocked attempts count as matches",
"required": true,
"type": "boolean"
},
"kind": {
"description": "Required kind in history",
"enum": [
"step.credential",
"step.exec",
"step.gate",
"step.message",
"step.model",
"step.resource",
"step.self",
"step.unknown",
"task.end",
"task.error",
"task.idle",
"task.start"
],
"required": true,
"type": "string"
},
"output_filter": {
"description": "Filter on the matched step's output (relative to output)",
"path": "output",
"required": false,
"type": "object"
},
"property_filter": {
"description": "Property filter on the matched step",
"path": "property",
"required": false,
"type": "object"
},
"verb": {
"description": "Also require this verb on the matched step",
"enum": [
"DELETE",
"GET",
"PATCH",
"POST"
],
"required": false,
"type": "string"
}
}
Example parameters¶
{
"count_attempts": false,
"kind": "step.gate"
}
max_consecutive_same_type¶
No more than N consecutive steps of the same kind are allowed.
Enforcement points: step_execution
Worst-case time complexity: O(n)
Parameters¶
{
"kind": {
"description": "Kind to monitor",
"enum": [
"step.credential",
"step.exec",
"step.gate",
"step.message",
"step.model",
"step.resource",
"step.self",
"step.unknown",
"task.end",
"task.error",
"task.idle",
"task.start"
],
"required": true,
"type": "string"
},
"max_consecutive": {
"description": "Max consecutive occurrences, the intended step included; 0 forbids the kind",
"minimum": 0,
"required": true,
"type": "integer"
}
}
Example parameters¶
{
"kind": "step.model",
"max_consecutive": 3
}
not¶
Compound rule: negates a single sub-condition. Supports arbitrary nesting.
Enforcement points: step_execution, agent_registration
Worst-case time complexity: O(child cost)
Parameters¶
{
"condition": {
"description": "Single sub-condition object with ``rule_type`` and ``params``",
"properties": {
"params": {
"description": "That rule's params, checked against its own schema (default: none)",
"required": false,
"type": "object"
},
"rule_type": {
"description": "A registered rule name",
"required": true,
"type": "string"
}
},
"required": true,
"type": "object"
}
}
Example parameters¶
{
"condition": {
"params": {
"field": "kind",
"values": [
"step.unknown"
]
},
"rule_type": "field_in_list"
}
}
path_within_root¶
Allowlist containment: the target path must resolve at or under the session project_root. Returns True (allowed) when the path is contained, False (violation) when it escapes the root. Use directly (not wrapped in 'not') — it is already an allowlist. Relative paths are resolved from project_root before containment is checked. When project_root is unknown (None) the rule fails open (allows), since containment is impossible without a root. Paths are normalized lexically (no filesystem I/O), so '..' is collapsed but symlinks are not resolved — an evadable edge documented, not pretended.
Enforcement points: step_execution
Worst-case time complexity: O(p); p is path length
Parameters¶
{
"field": {
"default": "properties.target.host",
"description": "Step field path of the target (default: 'properties.target.host')",
"path": "full",
"required": false,
"type": "string"
},
"target_kinds": {
"description": "Only enforce when current kind is in this list (default: all)",
"items": {
"enum": [
"step.credential",
"step.exec",
"step.gate",
"step.message",
"step.model",
"step.resource",
"step.self",
"step.unknown",
"task.end",
"task.error",
"task.idle",
"task.start"
],
"type": "string"
},
"required": false,
"type": "array"
},
"target_verb": {
"description": "Only enforce when current verb matches this single verb",
"enum": [
"DELETE",
"GET",
"PATCH",
"POST"
],
"required": false,
"type": "string"
},
"target_verbs": {
"description": "Only enforce when current verb is in this list. Use to scope to write verbs (e.g. ['POST', 'PATCH', 'DELETE']) so reads outside the root are not blocked. kinds with no verb (e.g. step.exec) are always in scope.",
"items": {
"enum": [
"DELETE",
"GET",
"PATCH",
"POST"
],
"type": "string"
},
"required": false,
"type": "array"
}
}
Example parameters¶
{
"field": "properties.target.host",
"target_kinds": [
"step.resource",
"step.exec"
],
"target_verbs": [
"POST",
"PATCH"
]
}
sequence_forbidden¶
The task path must not contain the specified ordered sequence of kinds. Sequence is checked against history + current step.
Enforcement points: step_execution
Worst-case time complexity: O(n)
Parameters¶
{
"forbidden_sequence": {
"description": "Ordered list of kind values to forbid",
"items": {
"enum": [
"step.credential",
"step.exec",
"step.gate",
"step.message",
"step.model",
"step.resource",
"step.self",
"step.unknown",
"task.end",
"task.error",
"task.idle",
"task.start"
],
"type": "string"
},
"required": true,
"type": "array"
}
}
Example parameters¶
{
"forbidden_sequence": [
"step.credential",
"step.exec"
]
}
step_directly_preceded_by¶
Target kinds must be immediately preceded by the required kind. Optionally scoped by target verb and property filters.
Enforcement points: step_execution
Worst-case time complexity: O(n)
Parameters¶
{
"required_kind": {
"description": "Required immediate predecessor kind",
"enum": [
"step.credential",
"step.exec",
"step.gate",
"step.message",
"step.model",
"step.resource",
"step.self",
"step.unknown",
"task.end",
"task.error",
"task.idle",
"task.start"
],
"required": true,
"type": "string"
},
"target_kinds": {
"description": "Kinds to enforce on (default: all)",
"items": {
"enum": [
"step.credential",
"step.exec",
"step.gate",
"step.message",
"step.model",
"step.resource",
"step.self",
"step.unknown",
"task.end",
"task.error",
"task.idle",
"task.start"
],
"type": "string"
},
"required": false,
"type": "array"
},
"target_property_filter": {
"description": "Dot-path property filter on current step",
"path": "property",
"required": false,
"type": "object"
},
"target_verb": {
"description": "Only enforce when current verb matches",
"enum": [
"DELETE",
"GET",
"PATCH",
"POST"
],
"required": false,
"type": "string"
}
}
Example parameters¶
{
"required_kind": "step.gate",
"target_kinds": [
"step.resource"
],
"target_verb": "DELETE"
}
step_forbidden_for_classification¶
Specific kinds are forbidden for agents with certain risk classifications.
Enforcement points: step_execution
Worst-case time complexity: O(k); k is classification list length
Parameters¶
{
"agent_risk_classifications": {
"description": "Risk tiers this applies to, compared exactly with the agent's (lower case)",
"items": {
"enum": [
"high",
"limited",
"minimal",
"unacceptable",
"unclassified"
],
"type": "string"
},
"required": true,
"type": "array"
},
"forbidden_kind": {
"description": "Forbidden kind value",
"enum": [
"step.credential",
"step.exec",
"step.gate",
"step.message",
"step.model",
"step.resource",
"step.self",
"step.unknown",
"task.end",
"task.error",
"task.idle",
"task.start"
],
"required": true,
"type": "string"
},
"forbidden_verb": {
"description": "Only forbid when verb matches",
"enum": [
"DELETE",
"GET",
"PATCH",
"POST"
],
"required": false,
"type": "string"
},
"target_property_filter": {
"description": "Additional property conditions",
"path": "property",
"required": false,
"type": "object"
}
}
Example parameters¶
{
"agent_risk_classifications": [
"high"
],
"forbidden_kind": "step.exec"
}
step_name_in_allowlist¶
Step name must appear in the agent's declared allowlist field.
Enforcement points: step_execution
Worst-case time complexity: O(k); k is tool allowlist length
Parameters¶
{
"agent_field": {
"description": "Agent attribute holding the allowlist",
"path": "agent",
"required": true,
"type": "string"
},
"target_kinds": {
"description": "Only enforce for these kinds",
"items": {
"enum": [
"step.credential",
"step.exec",
"step.gate",
"step.message",
"step.model",
"step.resource",
"step.self",
"step.unknown",
"task.end",
"task.error",
"task.idle",
"task.start"
],
"type": "string"
},
"required": false,
"type": "array"
}
}
Example parameters¶
{
"agent_field": "declared_tools",
"target_kinds": [
"step.resource",
"step.exec"
]
}
step_not_after¶
The target kind must not appear after any of the specified kinds in the task history. The block is permanent for the remainder of the task (tainted-path semantics): once a forbidden predecessor is recorded, the target kind is blocked regardless of what follows.
Enforcement points: step_execution
Worst-case time complexity: O(n)
Parameters¶
{
"forbidden_predecessor_kinds": {
"description": "Kinds that forbid the target from following",
"items": {
"enum": [
"step.credential",
"step.exec",
"step.gate",
"step.message",
"step.model",
"step.resource",
"step.self",
"step.unknown",
"task.end",
"task.error",
"task.idle",
"task.start"
],
"type": "string"
},
"required": true,
"type": "array"
},
"target_kinds": {
"description": "Kinds that must not follow the forbidden predecessors",
"items": {
"enum": [
"step.credential",
"step.exec",
"step.gate",
"step.message",
"step.model",
"step.resource",
"step.self",
"step.unknown",
"task.end",
"task.error",
"task.idle",
"task.start"
],
"type": "string"
},
"required": true,
"type": "array"
},
"target_verb": {
"description": "Only enforce when verb matches",
"enum": [
"DELETE",
"GET",
"PATCH",
"POST"
],
"required": false,
"type": "string"
}
}
Example parameters¶
{
"forbidden_predecessor_kinds": [
"step.unknown"
],
"target_kinds": [
"step.resource"
],
"target_verb": "POST"
}
step_preceded_by_without_intervening¶
Target step must be preceded by the required kind with no forbidden kinds appearing between them.
Enforcement points: step_execution
Worst-case time complexity: O(n)
Parameters¶
{
"forbidden_intervening": {
"description": "Kinds not allowed between predecessor and target",
"items": {
"enum": [
"step.credential",
"step.exec",
"step.gate",
"step.message",
"step.model",
"step.resource",
"step.self",
"step.unknown",
"task.end",
"task.error",
"task.idle",
"task.start"
],
"type": "string"
},
"required": true,
"type": "array"
},
"required_kind": {
"description": "Required predecessor",
"enum": [
"step.credential",
"step.exec",
"step.gate",
"step.message",
"step.model",
"step.resource",
"step.self",
"step.unknown",
"task.end",
"task.error",
"task.idle",
"task.start"
],
"required": true,
"type": "string"
},
"target_kinds": {
"description": "Target kinds",
"items": {
"enum": [
"step.credential",
"step.exec",
"step.gate",
"step.message",
"step.model",
"step.resource",
"step.self",
"step.unknown",
"task.end",
"task.error",
"task.idle",
"task.start"
],
"type": "string"
},
"required": true,
"type": "array"
},
"target_property_filter": {
"description": "Property filter on current step",
"path": "property",
"required": false,
"type": "object"
},
"target_verb": {
"description": "Only enforce when current verb matches",
"enum": [
"DELETE",
"GET",
"PATCH",
"POST"
],
"required": false,
"type": "string"
}
}
Example parameters¶
{
"forbidden_intervening": [
"step.model"
],
"required_kind": "step.gate",
"target_kinds": [
"step.resource"
],
"target_verb": "DELETE"
}
step_requires_dedicated_predecessor¶
Each target attempt must have its own unconsumed predecessor — one predecessor cannot authorise two target attempts.
Enforcement points: step_execution
Worst-case time complexity: O(n)
Parameters¶
{
"required_kind": {
"description": "Predecessor kind",
"enum": [
"step.credential",
"step.exec",
"step.gate",
"step.message",
"step.model",
"step.resource",
"step.self",
"step.unknown",
"task.end",
"task.error",
"task.idle",
"task.start"
],
"required": true,
"type": "string"
},
"target_kinds": {
"description": "Target kinds",
"items": {
"enum": [
"step.credential",
"step.exec",
"step.gate",
"step.message",
"step.model",
"step.resource",
"step.self",
"step.unknown",
"task.end",
"task.error",
"task.idle",
"task.start"
],
"type": "string"
},
"required": true,
"type": "array"
},
"target_property_filter": {
"description": "Property filter on target",
"path": "property",
"required": false,
"type": "object"
},
"target_verb": {
"description": "Only enforce when verb matches",
"enum": [
"DELETE",
"GET",
"PATCH",
"POST"
],
"required": false,
"type": "string"
}
}
Example parameters¶
{
"required_kind": "step.gate",
"target_kinds": [
"step.resource"
],
"target_verb": "DELETE"
}
step_requires_gate¶
Target kinds must be preceded by a step.gate with the specified kind and decision in the task history. Optionally require the gate within the last N history entries.
Enforcement points: step_execution
Worst-case time complexity: O(n)
Parameters¶
{
"gate_decision": {
"default": "pass",
"description": "Required output.decision value (default: 'pass')",
"enum": [
"pass",
"deny"
],
"required": false,
"type": "string"
},
"gate_kind": {
"description": "Required properties.kind value",
"enum": [
"human",
"guardrail"
],
"required": false,
"type": "string"
},
"max_steps_since_gate": {
"description": "Require a matching gate among the last N history entries",
"minimum": 1,
"required": false,
"type": "integer"
},
"target_kinds": {
"description": "Kinds requiring a gate",
"items": {
"enum": [
"step.credential",
"step.exec",
"step.gate",
"step.message",
"step.model",
"step.resource",
"step.self",
"step.unknown",
"task.end",
"task.error",
"task.idle",
"task.start"
],
"type": "string"
},
"required": true,
"type": "array"
},
"target_verb": {
"description": "Only enforce when verb matches",
"enum": [
"DELETE",
"GET",
"PATCH",
"POST"
],
"required": false,
"type": "string"
}
}
Example parameters¶
{
"gate_decision": "pass",
"gate_kind": "guardrail",
"max_steps_since_gate": 3,
"target_kinds": [
"step.message"
],
"target_verb": "POST"
}
step_requires_predecessor¶
Target kinds must have the required kind somewhere in task history (not necessarily immediately before).
Enforcement points: step_execution
Worst-case time complexity: O(n)
Parameters¶
{
"required_kind": {
"description": "Required predecessor kind",
"enum": [
"step.credential",
"step.exec",
"step.gate",
"step.message",
"step.model",
"step.resource",
"step.self",
"step.unknown",
"task.end",
"task.error",
"task.idle",
"task.start"
],
"required": true,
"type": "string"
},
"target_kinds": {
"description": "Kinds requiring the predecessor",
"items": {
"enum": [
"step.credential",
"step.exec",
"step.gate",
"step.message",
"step.model",
"step.resource",
"step.self",
"step.unknown",
"task.end",
"task.error",
"task.idle",
"task.start"
],
"type": "string"
},
"required": true,
"type": "array"
},
"target_property_filter": {
"description": "Property filter on current step",
"path": "property",
"required": false,
"type": "object"
},
"target_verb": {
"description": "Only enforce when current verb matches",
"enum": [
"DELETE",
"GET",
"PATCH",
"POST"
],
"required": false,
"type": "string"
}
}
Example parameters¶
{
"required_kind": "step.gate",
"target_kinds": [
"step.model"
]
}
tainted_path_block¶
Block the target step if any prior step in the task matches the taint pattern. Models 'credential read → exec block' and similar tainted paths.
Enforcement points: step_execution
Worst-case time complexity: O(n)
Parameters¶
{
"taint_kind": {
"description": "Kind that taints the path",
"enum": [
"step.credential",
"step.exec",
"step.gate",
"step.message",
"step.model",
"step.resource",
"step.self",
"step.unknown",
"task.end",
"task.error",
"task.idle",
"task.start"
],
"required": true,
"type": "string"
},
"taint_output_filter": {
"description": "Filter on the taint step's output (relative to output)",
"path": "output",
"required": false,
"type": "object"
},
"taint_property_filter": {
"description": "Property filter on the taint step",
"path": "property",
"required": false,
"type": "object"
},
"taint_verb": {
"description": "Also require this verb on the taint step",
"enum": [
"DELETE",
"GET",
"PATCH",
"POST"
],
"required": false,
"type": "string"
},
"target_kinds": {
"description": "Kinds blocked by taint",
"items": {
"enum": [
"step.credential",
"step.exec",
"step.gate",
"step.message",
"step.model",
"step.resource",
"step.self",
"step.unknown",
"task.end",
"task.error",
"task.idle",
"task.start"
],
"type": "string"
},
"required": true,
"type": "array"
},
"target_verb": {
"description": "Only block when current verb matches",
"enum": [
"DELETE",
"GET",
"PATCH",
"POST"
],
"required": false,
"type": "string"
}
}
Example parameters¶
{
"taint_kind": "step.credential",
"target_kinds": [
"step.exec"
]
}
usage_budget¶
Stops further steps of one kind once their cumulative numeric property value exceeds a budget. Applies to steps of the budgeted kind only: a step of any other type always passes. Counts what is recorded in the task plus the intended step's own value at property_path, if it declares one in its properties before the call, so the step that would cross the budget is the one refused. A cost that is only reported after a call cannot be counted in advance, so the block then arrives one step late (an overshoot of one step). Negative and non-finite values are ignored. Use for token counts, cost tracking, etc.
Enforcement points: step_execution
Worst-case time complexity: O(n)
Parameters¶
{
"budget": {
"description": "Maximum cumulative value allowed; 0 admits only steps that declare no positive value",
"minimum": 0,
"required": true,
"type": "number"
},
"kind": {
"description": "The budgeted kind: the only type this rule applies to and accumulates",
"enum": [
"step.credential",
"step.exec",
"step.gate",
"step.message",
"step.model",
"step.resource",
"step.self",
"step.unknown",
"task.end",
"task.error",
"task.idle",
"task.start"
],
"required": true,
"type": "string"
},
"property_path": {
"description": "Dot-separated path to the numeric value, read from each recorded step's properties and from the intended step's properties",
"path": "property",
"required": true,
"type": "string"
}
}
Example parameters¶
{
"budget": 50000,
"kind": "step.model",
"property_path": "usage.total_tokens"
}
working_hours_only¶
Agent can only operate within specified business hours. Supports IANA timezone names via the optional timezone param (defaults to UTC). Supports overnight windows when start_hour >= end_hour (e.g. 22–06).
Enforcement points: step_execution
Worst-case time complexity: O(1)
Parameters¶
{
"end_hour": {
"description": "Exclusive end hour (0-24); 24 and 0 both mean until midnight",
"maximum": 24,
"minimum": 0,
"required": true,
"type": "integer"
},
"start_hour": {
"description": "Inclusive start hour (0-23)",
"maximum": 23,
"minimum": 0,
"required": true,
"type": "integer"
},
"timezone": {
"default": "UTC",
"description": "IANA timezone name (default: UTC)",
"required": false,
"type": "string"
}
}
Example parameters¶
{
"end_hour": 17,
"start_hour": 9,
"timezone": "Europe/Amsterdam"
}
Usage notes¶
Rules that filter by GET or POST use the recorded verb. LangChain, LangGraph and CrewAI tool callbacks record POST by default; Kyvvu(read_only_tools=[...]) makes named tools record GET. A tool with data.* or target.trust properties must be declared in read_only_tools or write_tools before its callback is recorded.
Field rules¶
Applicable to agent_registration and step_execution.
Rule parameter regexes (field_matches_regex.pattern) use RE2 syntax: lookaround and backreferences are rejected when the policy loads. Patterns are searched anywhere in the value; anchor with ^ and $ for a whole-value match. A template's name_pattern also searches anywhere in the value and is case-insensitive.
Field and property paths¶
Any param named field is a full path from its enforcement view, including conditional_successor_required.trigger_condition.field. Params beginning with property_ (and property filters such as taint_property_filter and target_property_filter) are relative to step properties. output_filter (history_contains) and taint_output_filter (tainted_path_block) are relative to the matched step's output, for example output_filter: {decision: pass} to require a passed gate. Property filters read properties only.
For a step policy's field, the root is mandatory except for the three action fields. A bare input name or property path is rejected when the policy loads. Registration fields must start with a known agent profile attribute; metadata.team reads the team key inside metadata. The standard property groups are target, auth, data, model_params, message and usage; custom groups and ungrouped properties are allowed. There is no model or exec group: a step.model carries the bare strings properties.model and properties.provider (token counts, when reported, go in the usage group), and a step.exec the bare string properties.command. A gate uses the bare properties.kind (human or guardrail) and a completed gate records output.decision (pass or deny). A send_email step built by the decorator template can fill properties.message.recipient; open the trace to see which properties it supplies.
Three behaviours to know before you rely on a path:
- A path that does not exist reads as "absent" during enforcement. The engine cannot validate a path at load time because
propertiesis open-ended. What absent means depends on the rule:field_not_emptyandfield_in_listviolate,field_matches_regexsees an empty string (so a pattern that does not match the empty string violates), and aproperty_filterentry simply never matches, so atainted_path_blockorhistory_containswith a misspelt filter allows everything.kyvvu trywarns after a run when a step policy'sfieldor property filter path resolves on none of its trial steps. It still exits 0 if the run enforced policies; check the warning and the decisions together. - Property filters always read properties. A bare key such as
commandreadsproperties.commandeven if the input also has acommandkey. A dotted key such asdata.classificationreads that nested property. - Input keys never replace action fields or properties. For example,
field: verbreads the action verb, whilefield: input.verbreads the input value.
The filter values are compared with equality (data.classification: pii matches exactly pii); all entries of one filter must match. Regexes (field_matches_regex) are searched anywhere in the value.
Worked example¶
This manifest has one compound policy that reads properties.message.recipient and one path policy that filters on data.classification. The first is a compound because a step_execution policy is evaluated on every step: a bare field_matches_regex on properties.message.recipient would violate on every step that has no recipient.
name: Path examples
version: "1.0.0"
policies:
# field reads properties.message.recipient. The policy is evaluated on every
# step, so it passes any step that is not an outbound message.
- name: Emails go to approved domains only
rule_type: any_of
params:
conditions:
- rule_type: not
params:
condition:
rule_type: current_is
params: {kind: step.message, verb: POST}
- rule_type: field_matches_regex
params:
field: properties.message.recipient
pattern: '^[^@]+@(example\.com|partner\.example)$'
severity: critical
enforcement_point: step_execution
# property_filter on a history rule reads properties.data.classification.
- name: No message after a PII read
rule_type: tainted_path_block
params:
taint_kind: step.resource
taint_verb: GET
taint_property_filter:
data.classification: pii
target_kinds: ["step.message"]
target_verb: POST
severity: critical
enforcement_point: step_execution
And a path that exercises both, written as steps (input is left out; the paths read properties):
agent:
agent_id: path-demo
risk_classification: minimal
allowed_tools: [read_faq, read_crm, send_email]
steps:
- {step_name: read_faq, kind: step.resource, verb: GET}
- step_name: send_email
kind: step.message
verb: POST
properties: {message: {recipient: "customer@example.com"}}
- step_name: send_email
kind: step.message
verb: POST
properties: {message: {recipient: "bob@evil.io"}}
- step_name: read_crm
kind: step.resource
verb: GET
properties: {data: {classification: pii}}
- step_name: send_email
kind: step.message
verb: POST
properties: {message: {recipient: "customer@example.com"}}
$ KV_ENVIRONMENT=development kyvvu try -m paths.yaml --steps steps.yaml
## DECISION RISK BEHAVIOUR VIOLATED POLICY
1 ALLOW 0.00 step.resource GET read_faq -
2 ALLOW 0.00 step.message POST send_email -
3 BLOCK 1.00 step.message POST send_email Emails go to approved domains
only (critical)
4 ALLOW 0.00 step.resource GET read_crm -
5 BLOCK 1.00 step.message POST send_email No message after a PII read
(critical)
Step 3 is blocked by the recipient domain (properties.message.recipient), step 5 by the PII read at step 4 (data.classification). Change properties.message.recipient to properties.message.recpient in the policy and every email is blocked, including step 2: the path is absent, so the regex sees an empty string. Change data.classification to data.clasification and step 5 is allowed: the filter matches no step. Both manifests still load, and kyvvu try still exits 0, but it warns with the policy name and missing path. Nothing in the loader can tell a typo from a property nobody sets yet.
Load-time validation¶
A policy is checked against its rule when it is loaded. A policy whose params do not match the rule's params_schema (a required key missing, a key the rule does not know, a value of the wrong type, a number outside its bounds, params that is not a mapping), an unknown rule_type, a duplicate policy name within one manifest, a regex that does not compile, an action kind or verb that is not in the vocabulary (step.mesage, post), or the retired scope: key raises PolicyLoadError naming the policy, the rule and the key. Action kinds, verbs, and full field path roots are checked everywhere a rule names one, including inside the conditions of all_of, any_of and not. Every param that names an action kind takes any of the twelve kinds (task.* included), every param that names a verb takes GET, POST, PATCH or DELETE, and a list of either must hold only strings; the rule's params_schema declares these as an enum, so they are part of the frozen rule contract. A sub-condition of all_of, any_of or not holds exactly rule_type and, optionally, params; any other key (a description, a misspelt paramz) is refused, as it is in trigger_condition. A default the rule applies when a param is omitted is declared as default in its params_schema. Property names within a valid root remain open-ended and cannot be checked at load (see Field and property paths). Nothing is skipped and nothing loads partially: a configuration that loaded fewer policies than it names would enforce less than it claims. The same holds for the API validator, kyvvu try, kyvvu doctor and kyvvu serve.
Path rules¶
Require task history. step_execution only.
For example, max_steps_since_gate: 3 accepts a passing gate followed by at most two other history entries before the target. A third entry makes that approval stale. A blocked gate cannot approve the target.
Count rules¶
Classification rules¶
A policy's own risk_classification (the field on the policy or manifest entry, not a rule parameter) means this class or above, ordered minimal < limited < high: a limited policy covers limited and high agents, a minimal policy covers every classified agent, and no risk_classification covers every agent. A class the order does not know (a typo, unclassified) also covers every agent. The API's assignment filter and the engine's policy index use the same order, checked by the risk-agreement test.
Content rules¶
How the host is parsed. A URL (anything containing ://) and a bare host go through one normaliser, so API.COMPANY.COM, api.company.com:8443, api.company.com/path, api.company.com. and https://api.company.com/x all mean the same host. The steps are:
- For a URL, take the authority (between
://and the first/,?or#). A bare value is treated as//hostand parsed the same way. - Reject (violation) an authority that contains a backslash, whitespace, a control character or more than one
@. A browser reads\as a path separator in an http(s) URL where Python's parser does not, sohttps://evil.com\.company.com/names two different hosts to two readers. - Take what follows the last
@(user:pw@hostis allowed; the host is what counts), then remove any port, lower-case it and remove one trailing dot. - The result must be a plain DNS name: ASCII letters, digits, hyphens and dots, with
xn--labels allowed. Unicode dots, homographs and empty labels fail. Anything that fails is a violation (the rule fails closed). - Allowed means equal to a listed domain or a subdomain of one. The listed domains are lower-cased and stripped of one trailing dot the same way.
What it blocks, so you can plan for it:
- A listed step with no host. A
step.messagestep carries notarget.host(its destination isproperties.message.recipient) unless you set one, so includingstep.messageintarget_kindsblocks every message; check recipients withfield_in_listorfield_matches_regexinstead, or set the host in a template. - Raw IPs (
10.0.0.5,[::1]): never a subdomain match, and allowed only when the allowlist lists that exact string. - Bypass shapes:
https://api.company.com.evil.io/,https://evil.io/api.company.com,https://api.company.com@evil.io/, a space or;in the host, and unicode look-alikes.
target_kinds is required; a domain_allowlist without it is refused at load. That is deliberate and security-relevant: a step with no host used to be skipped or blocked by accident, so a destination passed in a parameter the template does not read (an endpoint= argument, say) could slip past the allowlist. Now every step of a listed type must name an allowed host. Name the types the allowlist is for, typically ["step.resource"].
The consequence to plan for: a local step.resource that has no network destination, such as a read_faq tool, is blocked by a domain_allowlist whose target_kinds include step.resource, because its host is absent. Either declare where it goes, with properties={"target": {"host": "faq.internal.example.com"}} (or a template rule that sets target.host; use {{ kwargs.url | omit_empty }} so a missing argument leaves the key out), or keep the step out of the rule's target_kinds.
Flow rules¶
Each sub-condition in compound rules is a dict with rule_type and params keys.
Programmatic discovery¶
Rules are self-documenting. To list all rules programmatically:
from kyvvu_engine import PolicyRule
## All rules for a given enforcement point
rules = PolicyRule.get_all_rules(enforcement_point="step_execution")
## → {"field_not_empty": {"description": "...", "enforcement_points": [...], "params_schema": {...}}, ...}
## Does a rule exist, and do these params match its schema?
assert PolicyRule.rule_exists("field_matches_regex")
problems = PolicyRule.check_params("field_matches_regex", {"field": "input.code", "pattern": r"\d{3}"})
assert problems == [] # a list of messages, one per problem; empty when the params are valid
check_params is the same check the engine runs when it loads a policy and the API runs when you create one. The metadata is also what GET /api/v1/policies/rules serves.
Next steps¶
- Creating Policies — use these rules to author policies
- Compound Policies — combine rules with
all_of,any_of,not - OWASP Default Template — see 8 of these rules in action