Contracts¶
A contract is a check that Edictum evaluates on every tool call. Contracts are written in YAML and compiled to deterministic checks -- the LLM cannot bypass them.
There are four contract types: preconditions check before execution, postconditions check after, session contracts track state across multiple calls, and sandbox contracts define allowlists for what agents can do.
When to use this¶
Read this page when you are writing or modifying contracts. It covers all four contract types -- preconditions that deny dangerous inputs before the tool runs, postconditions that scan output after execution, session contracts that cap cumulative usage, and sandbox contracts that define allowlists for file paths, commands, and domains. If you need the full YAML syntax, see YAML reference. For the evaluation order between contract types, see how it works.
Choosing the Right Contract Type¶
| Type | Question | Approach | Use when... |
|---|---|---|---|
pre (deny) |
"Is this specific thing bad?" | Denylist | Short, stable list of things to deny (rm -rf /, .env reads) |
sandbox |
"Is this within allowed boundaries?" | Allowlist | Open-ended attack surface -- define what's allowed instead |
post |
"Did the output contain something bad?" | Output scan | Dangerous content is in the output (SSNs, API keys) |
session |
"Has the agent done too much?" | Rate limits | Cap total calls, per-tool calls, or retry attempts |
They compose: deny runs first, sandbox second, postconditions after execution, session limits across turns. For detailed scenarios and the motivation behind sandbox contracts, see sandbox contracts.
Preconditions¶
Preconditions evaluate before the tool runs. If the condition matches, the call is denied and the tool never executes.
- id: block-dotenv
type: pre
tool: read_file
when:
args.path: { contains: ".env" }
then:
effect: deny
message: "Read of sensitive file denied: {args.path}"
This contract fires when read_file is called with a path argument containing ".env". The effect is always deny -- preconditions exist to stop dangerous calls.
Key properties:
type: premarks this as a precondition.tooltargets a specific tool name, or"*"for all tools.whenis the condition tree. See operators for the full list.effect: denyis the only valid effect for preconditions.
Postconditions¶
Postconditions evaluate after the tool runs. They inspect the tool's output and produce findings.
- id: pii-in-output
type: post
tool: "*"
when:
output.text:
matches_any:
- '\b\d{3}-\d{2}-\d{4}\b'
- '\b[A-Z]{2}\d{2}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{0,2}\b'
then:
effect: warn
message: "PII pattern detected in output. Redact before using."
This contract scans every tool's output for SSN and IBAN patterns. When a pattern matches, it produces a finding that your application can act on -- redact the output, log it, or alert a human.
Key properties:
type: postmarks this as a postcondition.output.textis available only in postconditions. It contains the stringified tool response.effectcan bewarn,redact, ordeny.warnproduces findings.redactreplaces matched patterns with[REDACTED]for READ/PURE tools.denysuppresses the entire output for READ/PURE tools. WRITE/IRREVERSIBLE tools always fall back towarn. See postcondition effects.- Findings are structured objects with type, contract ID, field, and message. See findings.
Session Contracts¶
Session contracts track cumulative state across all tool calls within a session. They enforce limits on total calls, total attempts, and per-tool counts.
- id: session-limits
type: session
limits:
max_tool_calls: 50
max_attempts: 120
max_calls_per_tool:
deploy_service: 3
send_notification: 10
then:
effect: deny
message: "Session limit reached. Summarize progress and stop."
This contract caps the session at 50 successful tool executions, 120 total attempts (including denied calls), and per-tool limits on deploy_service and send_notification.
Key properties:
type: sessionmarks this as a session contract.- Session contracts have no
toolorwhenfields. They apply to all tools. max_attemptscounts denied calls too, catching agents stuck in retry loops.effect: denyis the only valid effect for session contracts.
Sandbox Contracts¶
Deny-list contracts enumerate what's bad. Sandbox contracts flip this: they define what's allowed and deny everything else. When the attack surface is open-ended -- shell access, arbitrary file paths, unrestricted URLs -- defining what's bad is infinite. Defining what's good is finite.
- id: file-sandbox
type: sandbox
tools: [read_file, write_file, edit_file]
within:
- /workspace
- /tmp
not_within:
- /workspace/.git
outside: deny
message: "File access outside workspace: {args.path}"
This contract restricts all file tools to /workspace and /tmp, excluding /workspace/.git. Any file path that falls outside the allowed directories is denied -- regardless of what command is used to access it.
Sandbox contracts do not use the when/then structure. Instead, they use declarative boundary fields: within/not_within for file paths, allows.commands for command allowlists, and allows.domains/not_allows.domains for URL domain restrictions.
The pipeline evaluates sandbox contracts after preconditions but before session limits. The full order is: preconditions (deny) -> sandbox -> session -> limits -> allow.
Key properties:
type: sandboxmarks this as a sandbox contract.toolortoolstargets one or more tools. Unlike other contract types, sandbox contracts can target multiple tools in a single contract.withinandnot_withindefine file path boundaries.not_withinoverrideswithin.allows.commandsrestricts which commands an exec tool can run (first token only).allows.domainsandnot_allows.domainsrestrict URL domains (supportsfnmatchwildcards).outsideis required:denyto deny calls outside the sandbox, orapproveto request human approval.- No
whenorthenblock. The boundary fields andoutside/messagereplace them.
For the full sandbox schema, path matching details, and combined examples, see the YAML reference sandbox section. For the conceptual motivation and known limitations, see sandbox contracts.
The when / then Structure¶
Every precondition and postcondition has a when block (the condition) and a then block (the action).
when is an expression tree that evaluates against the tool call's arguments, principal, environment, and output. It supports boolean combinators (all, any, not) and 15 operators (equality, membership, string matching, regex, numeric comparisons).
when:
all:
- environment: { equals: production }
- principal.role: { not_in: [admin, sre] }
- principal.ticket_ref: { exists: false }
This condition matches when all three sub-conditions are true: the environment is production, the principal's role is not admin or sre, and no ticket reference is attached.
then defines the action when the condition matches:
then:
effect: deny
message: "Production changes require admin/sre role and a ticket."
tags: [change-control, production]
metadata:
severity: high
effect--deny(preconditions, session) orwarn/redact/deny(postconditions).message-- sent to the agent and recorded in the audit event. Supports{placeholder}expansion from the envelope context.tags-- optional classification labels for filtering in audit systems.metadata-- optional key-value pairs stamped into the audit event.
Enforce vs. Observe¶
Each contract can run in one of two modes:
mode: enforce-- the contract actively denies tool calls (preconditions, sandbox) or produces findings (postconditions). This is the default.mode: observe-- the contract evaluates but does not deny. Preconditions and sandbox contracts that would fire emitCALL_WOULD_DENYaudit events instead. The tool call proceeds.
Set the default for all contracts in the bundle:
Override per-contract when you want to shadow-test a new contract:
- id: experimental-api-check
type: pre
mode: observe
tool: call_api
when:
args.endpoint: { contains: "/v1/expensive" }
then:
effect: deny
message: "Expensive API call detected (observe mode)."
For a full walkthrough of the observe-to-enforce workflow, see observe mode.
Contract Bundle Structure¶
Contracts live in a YAML file called a contract bundle. Every bundle starts with four required fields:
apiVersion: edictum/v1
kind: ContractBundle
metadata:
name: my-agent-contracts
defaults:
mode: enforce
contracts:
- id: block-dotenv
type: pre
# ...
Load a bundle in Python:
The bundle is hashed (SHA-256) at load time. The hash is stamped as policy_version on every audit event, linking each governance decision to the exact contract file that produced it.
Next Steps¶
- Sandbox contracts -- allowlist-based enforcement for file paths, commands, and domains
- YAML reference -- full contract syntax and schema
- Operators -- all 15 operators with examples
- How it works -- the pipeline that evaluates contracts
- Principals -- identity context in contract conditions