Edictum
Concepts

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.

AI Assistance

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

TypeQuestionApproachUse when...
pre (deny)"Is this specific thing bad?"DenylistShort, stable list of things to deny (rm -rf /, .env reads)
sandbox"Is this within allowed boundaries?"AllowlistOpen-ended attack surface -- define what's allowed instead
post"Did the output contain something bad?"Output scanDangerous content is in the output (SSNs, API keys)
session"Has the agent done too much?"Rate limitsCap 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 emit CALL_WOULD_DENY audit 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

Last updated on

On this page