Custom Operators
Edictum ships with 15 built-in operators (`contains`, `matches`, `gt`, etc.).
Right page if: you need domain-specific validation in YAML rulesets that the 15 built-in operators cannot express (IBAN, CIDR, semver, etc.). Wrong page if: you need complex logic with branching or external API calls -- see https://docs.edictum.ai/docs/guides/python-hooks. For the built-in operator list, see https://docs.edictum.ai/docs/rulesets/operators. Gotcha: custom operator names that clash with built-in operators (e.g., "contains") raise EdictumConfigError. Unknown operators in YAML are caught at load time, not at evaluation time -- typos fail fast.
Edictum ships with 15 built-in operators (contains, matches, gt, etc.). Custom operators let you extend the expression grammar with domain-specific checks — IBAN validation, CIDR matching, semver comparison — so rulesets stay declarative YAML instead of falling back to Python hooks.
import ipaddress
def ip_in_cidr(field_value: str, cidr: str) -> bool:
"""Check if an IP address is within a CIDR range."""
return ipaddress.ip_address(field_value) in ipaddress.ip_network(cidr)
guard = Edictum.from_yaml(
"rules.yaml",
custom_operators={"ip_in_cidr": ip_in_cidr},
)# rules.yaml
- id: internal-only
type: pre
tool: ssh_connect
when:
args.target_ip: { ip_in_cidr: "10.0.0.0/8" }
then:
action: block
message: "Only internal IPs allowed. Got {args.target_ip}."Operator rule
Every custom operator is a callable with this signature:
def my_operator(field_value: Any, operator_value: Any) -> bool:
...| Parameter | Description |
|---|---|
field_value | The resolved value from the selector (e.g., the value of args.target_ip). |
operator_value | The value from the YAML operator (e.g., "10.0.0.0/8"). |
| Return | True if the condition is met (rule fires), False otherwise. |
The return value is coerced to bool. Truthy non-bool values (like 1 or "yes") work but True/False is preferred.
Error handling
- If the operator raises
TypeError, Edictum treats it as apolicy_error(fail-closed — the rule fires). - If the operator raises any other exception, Edictum treats it the same way (fail-closed).
- Missing fields are never passed to custom operators. When a selector resolves to a missing or null field, the expression evaluates to
Falsewithout calling the operator.
Registering operators
Pass custom_operators to any of the YAML loading methods:
guard = Edictum.from_yaml(
"rules.yaml",
custom_operators={"ip_in_cidr": ip_in_cidr},
)guard = Edictum.from_yaml_string(
yaml_content,
custom_operators={"ip_in_cidr": ip_in_cidr},
)guard = Edictum.from_template(
"file-agent",
custom_operators={"ip_in_cidr": ip_in_cidr},
)Multiple operators can be registered at once:
guard = Edictum.from_yaml(
"rules.yaml",
custom_operators={
"ip_in_cidr": ip_in_cidr,
"is_invalid_iban": is_invalid_iban,
"semver_lt": semver_lt,
},
)Name clash protection
Custom operator names must not collide with the 15 built-in operators. Attempting to register a name like contains or equals raises EdictumConfigError:
# This raises EdictumConfigError
guard = Edictum.from_yaml_string(
yaml_content,
custom_operators={"contains": my_fn}, # clash!
)EdictumConfigError: Custom operator names clash with built-in operators: ['contains']Unknown operator detection
If a YAML rule uses an operator name that is neither built-in nor registered as a custom operator, Edictum raises EdictumConfigError at compile time (when loading the ruleset), not at evaluation time:
# rules.yaml uses `ip_in_cidr` but no custom_operators registered
guard = Edictum.from_yaml_string(yaml_content)
# EdictumConfigError: Rule 'internal-only': unknown operator 'ip_in_cidr'This means typos and missing registrations are caught immediately, not when a tool call happens to trigger the rule.
Composing with boolean expressions
Custom operators compose with all:, any:, and not: the same way built-in operators do:
- id: internal-approved-only
type: pre
tool: ssh_connect
when:
all:
- args.target_ip: { ip_in_cidr: "10.0.0.0/8" }
- principal.role: { not_in: [sre, admin] }
then:
action: block
message: "Internal access requires SRE or admin role."Custom and built-in operators can be mixed freely within the same expression tree.
Dry-run evaluation
Custom operators work with the dry-run evaluation API:
result = guard.evaluate(
"ssh_connect",
{"target_ip": "192.168.1.1"},
)
print(result.decision) # "block" or "allow"Examples
IBAN validation
import re
IBAN_RE = re.compile(r'^[A-Z]{2}\d{2}[A-Z0-9]{4}\d{7}([A-Z0-9]?){0,16}$')
def is_invalid_iban(field_value: str, expected: bool) -> bool:
"""Return True when the IBAN is invalid (block condition)."""
valid = bool(IBAN_RE.match(str(field_value)))
return (not valid) == expected- id: validate-iban
type: pre
tool: wire_transfer
when:
args.destination_account: { is_invalid_iban: true }
then:
action: block
message: "Invalid IBAN: {args.destination_account}"CIDR range check
import ipaddress
def ip_in_cidr(field_value: str, cidr: str) -> bool:
"""Return True when the IP is within the CIDR range."""
try:
return ipaddress.ip_address(field_value) in ipaddress.ip_network(cidr)
except ValueError:
return False# Block connections to internal IPs
- id: block-internal
type: pre
tool: ssh_connect
when:
args.target_ip: { ip_in_cidr: "10.0.0.0/8" }
then:
action: block
message: "Internal IP {args.target_ip} blocked."Semver comparison
from packaging.version import Version
def semver_lt(field_value: str, threshold: str) -> bool:
"""Return True when field version is below threshold."""
return Version(field_value) < Version(threshold)# Block deploying packages below minimum version
- id: min-version
type: pre
tool: deploy_package
when:
args.version: { semver_lt: "2.0.0" }
then:
action: block
message: "Version {args.version} is below the minimum 2.0.0."Next steps
- Operator Reference — all 15 built-in operators
- Python Hooks — for complex validation with branching logic
- Writing Rulesets — YAML rule patterns
- Testing Rulesets — verifying your rulesets work
Last updated on
Python Rules
Define enforcement in Python when the built-in YAML operators are not enough.
Designing Block Messages
The block message is the agent's only feedback on what went wrong. A good message steers the agent toward a productive alternative. A bad one causes retries, confusion, and wasted compute.