Contract Types Overview
Four contract types cover every enforcement scenario -- preconditions deny before execution, postconditions scan output, session contracts cap usage, and sandbox contracts define allowlists.
Right page if: you need a quick orientation on all four contract types (pre, post, session, sandbox) and a decision table to choose the right one. Wrong page if: you need the full YAML syntax for writing contracts -- see https://docs.edictum.ai/docs/contracts/yaml-reference. For deep dives, see https://docs.edictum.ai/docs/contracts/preconditions, https://docs.edictum.ai/docs/contracts/postconditions, https://docs.edictum.ai/docs/contracts/session-contracts, or https://docs.edictum.ai/docs/concepts/sandbox-contracts. Gotcha: preconditions and sandbox contracts compose -- preconditions run first, sandbox second, session limits third. Postconditions only run after the tool executes.
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.
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: preconditions run first, sandbox second, session limits third, postconditions after execution. For detailed scenarios and the motivation behind sandbox contracts, see sandbox contracts.
Denylist vs allowlist? Use pre contracts when the list of bad things is short and known. Use sandbox contracts when the attack surface is open-ended — it is easier to define what is allowed than to enumerate every possible exploit. They compose: sandbox contracts catch the unknown threats that denylists miss.
Preconditions
Preconditions evaluate before the tool runs. If the condition matches, the call is denied and the tool never executes. This is the cheapest enforcement point -- nothing has happened yet, so there is nothing to undo.
- id: block-dotenv
type: pre
tool: read_file
when:
args.path: { contains: ".env" }
then:
effect: deny
message: "Read of sensitive file denied: {args.path}"Preconditions support two effects: deny (reject the call) and approve (pause for human approval). They target tools with exact names, "*" for all tools, or glob patterns like mcp_*. The when block supports 15 operators and boolean combinators (all, any, not).
Deep dive: Preconditions -- selectors, effects, tool targeting, variable interpolation, observe mode, common patterns, and error handling.
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: redact
message: "PII pattern detected and redacted."Postconditions support three effects: warn (produce findings, output unchanged), redact (replace matched patterns with [REDACTED]), and deny (suppress entire output). The redact and deny effects are enforced for READ/PURE tools but fall back to warn for WRITE/IRREVERSIBLE tools, since the action already happened.
Deep dive: Postconditions -- the three effects, side-effect classification, output.text selector, findings, on_postcondition_warn callback, and YAML examples.
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."Session contracts have no tool or when fields -- they apply globally. max_attempts counts denied calls too, catching agents stuck in retry loops. effect: deny is the only valid effect.
Deep dive: Session Contracts -- the three limit types, counter semantics, StorageBackend, denial message design, and calibration with observe mode.
Sandbox Contracts
Sandbox contracts flip the deny-list model: 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}"Sandbox contracts use declarative boundary fields instead of when/then: within/not_within for file paths, allows.commands for command allowlists, and allows.domains/not_allows.domains for URL domain restrictions. They can target multiple tools in a single contract.
Deep dive: Sandbox Contracts -- evaluation details, path resolution, deny-list vs. allowlist comparison, composition with deny contracts, known limitations, and red team results.
Enforce vs. Observe
Each contract can run in one of two modes:
mode: enforce(default) -- the contract actively denies tool calls or produces findings.mode: observe-- the contract evaluates but does not deny. Contracts that would fire emitCALL_WOULD_DENYaudit events instead. The tool call proceeds.
defaults:
mode: enforce
contracts:
- id: proven-contract
type: pre
# inherits mode: enforce from defaults
...
- id: experimental-check
type: pre
mode: observe # override: test without disrupting the agent
...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:
from edictum import Edictum
guard = Edictum.from_yaml("contracts.yaml")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
- Preconditions -- deny dangerous inputs before execution
- Postconditions -- scan and redact tool output after execution
- Session Contracts -- cumulative usage limits across turns
- 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 the pipeline works -- the evaluation order for all contract types
- Principals -- identity context in contract conditions
Last updated on