Skip to content

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., or properties..

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., or properties..

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., or properties..

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 properties is open-ended. What absent means depends on the rule: field_not_empty and field_in_list violate, field_matches_regex sees an empty string (so a pattern that does not match the empty string violates), and a property_filter entry simply never matches, so a tainted_path_block or history_contains with a misspelt filter allows everything. kyvvu try warns after a run when a step policy's field or 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 command reads properties.command even if the input also has a command key. A dotted key such as data.classification reads that nested property.
  • Input keys never replace action fields or properties. For example, field: verb reads the action verb, while field: input.verb reads 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:

  1. For a URL, take the authority (between :// and the first /, ? or #). A bare value is treated as //host and parsed the same way.
  2. 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, so https://evil.com\.company.com/ names two different hosts to two readers.
  3. Take what follows the last @ (user:pw@host is allowed; the host is what counts), then remove any port, lower-case it and remove one trailing dot.
  4. 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).
  5. 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.message step carries no target.host (its destination is properties.message.recipient) unless you set one, so including step.message in target_kinds blocks every message; check recipients with field_in_list or field_matches_regex instead, 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