Custom Operators
Edictum ships with 15 built-in operators (`contains`, `matches`, `gt`, etc.).
Right page if: you need domain-specific validation in YAML contracts 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/contracts/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 contracts 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(
"contracts.yaml",
custom_operators={"ip_in_cidr": ip_in_cidr},
)# contracts.yaml
- id: internal-only
type: pre
tool: ssh_connect
when:
args.target_ip: { ip_in_cidr: "10.0.0.0/8" }
then:
effect: deny
message: "Only internal IPs allowed. Got {args.target_ip}."Operator contract
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 (contract 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 contract 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(
"contracts.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(
"contracts.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 contract uses an operator name that is neither built-in nor registered as a custom operator, Edictum raises EdictumConfigError at compile time (when loading the bundle), not at evaluation time:
# contracts.yaml uses `ip_in_cidr` but no custom_operators registered
guard = Edictum.from_yaml_string(yaml_content)
# EdictumConfigError: Contract '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 contract.
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:
effect: deny
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.verdict) # "deny" 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 (deny 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:
effect: deny
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# Deny 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:
effect: deny
message: "Internal IP {args.target_ip} denied."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)# Deny deploying packages below minimum version
- id: min-version
type: pre
tool: deploy_package
when:
args.version: { semver_lt: "2.0.0" }
then:
effect: deny
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 Contracts — YAML contract patterns
- Testing Contracts — verifying your contracts work
Last updated on
Python Contracts
Define contracts in Python using decorators when you need logic that YAML operators cannot express.
Designing Denial Messages
The denial 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.