Edictum

Quickstart

In five minutes you will install Edictum, write a contract, and see it deny a dangerous tool call.

AI Assistance

Right page if: you want to install Edictum, write your first contract, and see it deny a tool call in under five minutes. Wrong page if: you already have Edictum running and need the full YAML schema -- see https://docs.edictum.ai/docs/contracts/yaml-reference. Gotcha: you must install with `pip install edictum[yaml]` (the `[yaml]` extra) to use YAML contract bundles. The base `pip install edictum` only supports Python-defined contracts.

In five minutes you will install Edictum, write a contract, and see it deny a dangerous tool call. The denied call never executes -- the agent sees a denial message and the audit trail records what happened.

Install

pip install edictum[yaml]

Requires Python 3.11+.

Write a Contract

Fastest Path: Use a Template

If you want contracts running in under a minute, use a built-in template:

from edictum import Edictum

guard = Edictum.from_template("file-agent")
# Secret file reads and destructive commands are now denied.
# No YAML needed.

Four templates ship with Edictum:

TemplateWhat it protects against
file-agentSecret file reads, destructive bash commands, writes outside working directory
research-agentSecret files, PII in output, session runaway (50 call cap)
devops-agentAll of the above + role-gated production deploys + ticket requirements
nanobot-agentApproval gates for destructive actions, path restrictions, session limits

See Templates for full YAML and customization.

Ready to write your own? Continue below.

Save this as contracts.yaml:

apiVersion: edictum/v1
kind: ContractBundle
metadata:
  name: agent-safety
defaults:
  mode: enforce
contracts:
  - id: block-dotenv
    type: pre
    tool: read_file
    when:
      args.path: { contains: ".env" }
    then:
      effect: deny
      message: "Read of sensitive file denied: {args.path}"

  - id: block-destructive-commands
    type: pre
    tool: run_command
    when:
      any:
        - args.command: { starts_with: "rm " }
        - args.command: { starts_with: "DROP " }
        - args.command: { contains: "mkfs" }
    then:
      effect: deny
      message: "Destructive command denied: {args.command}"

  - id: session-limits
    type: session
    limits:
      max_tool_calls: 50
      max_attempts: 120
      max_calls_per_tool:
        run_command: 10
    then:
      effect: deny
      message: "Session limit reached."

  - id: file-sandbox
    type: sandbox
    tool: read_file
    within:
      - /workspace
      - /tmp
    outside: deny
    message: "File read outside workspace: {args.path}"

Four contracts: one denies reads of .env files, one denies destructive commands, one caps the session at 50 tool calls, and one restricts file reads to /workspace and /tmp.

Run It

Save this as demo.py:

import asyncio
from edictum import Edictum, EdictumDenied


guard = Edictum.from_yaml("contracts.yaml")


async def read_file(path):
    return f"contents of {path}"


async def run_command(command):
    return f"executed: {command}"


async def main():
    # Allowed: normal file read
    result = await guard.run("read_file", {"path": "readme.txt"}, read_file)
    print(f"OK: {result}")

    # DENIED: agent tries to read .env
    try:
        await guard.run("read_file", {"path": ".env"}, read_file)
    except EdictumDenied as e:
        print(f"DENIED: {e.reason}")

    # DENIED: agent tries to rm -rf
    try:
        await guard.run("run_command", {"command": "rm -rf /tmp"}, run_command)
    except EdictumDenied as e:
        print(f"DENIED: {e.reason}")

    # DENIED: agent tries to read outside sandbox
    try:
        await guard.run("read_file", {"path": "/etc/shadow"}, read_file)
    except EdictumDenied as e:
        print(f"DENIED: {e.reason}")


asyncio.run(main())

Run it:

python demo.py

Expected output:

OK: contents of readme.txt
DENIED: Read of sensitive file denied: .env
DENIED: Destructive command denied: rm -rf /tmp
DENIED: File read outside workspace: /etc/shadow

The .env file was never read. The rm -rf command never executed. The /etc/shadow read was denied by the sandbox allowlist. All three calls were denied by contracts evaluated in Python, outside the LLM. The agent cannot talk its way past these checks.

Tip: If your YAML is generated programmatically or fetched from an API, use from_yaml_string() instead:

guard = Edictum.from_yaml_string(yaml_content)  # str or bytes

See the YAML reference for details.

Add to Your Framework

Create the guard from the same YAML, then use the adapter for your framework.

from edictum import Edictum

guard = Edictum.from_yaml("contracts.yaml")

LangChain

from edictum.adapters.langchain import LangChainAdapter
from langgraph.prebuilt import ToolNode

adapter = LangChainAdapter(guard)
tool_node = ToolNode(tools=tools, wrap_tool_call=adapter.as_tool_wrapper())

OpenAI Agents SDK

from edictum.adapters.openai_agents import OpenAIAgentsAdapter
from agents import function_tool

adapter = OpenAIAgentsAdapter(guard)
input_gr, output_gr = adapter.as_guardrails()

@function_tool(
    tool_input_guardrails=[input_gr],
    tool_output_guardrails=[output_gr],
)
def read_file(path: str) -> str:
    """Read a file and return its contents."""
    return open(path).read()

CrewAI

from edictum.adapters.crewai import CrewAIAdapter

adapter = CrewAIAdapter(guard)
adapter.register()
# Hooks are now active for all CrewAI tool calls

Agno

from edictum.adapters.agno import AgnoAdapter
from agno.agent import Agent

adapter = AgnoAdapter(guard)
agent = Agent(tool_hooks=[adapter.as_tool_hook()])

Semantic Kernel

from edictum.adapters.semantic_kernel import SemanticKernelAdapter
from semantic_kernel.kernel import Kernel

kernel = Kernel()
adapter = SemanticKernelAdapter(guard)
adapter.register(kernel)

Claude Agent SDK

from edictum.adapters.claude_agent_sdk import ClaudeAgentSDKAdapter

adapter = ClaudeAgentSDKAdapter(guard)
hooks = adapter.to_hook_callables()
# hooks = {"pre_tool_use": ..., "post_tool_use": ...}

Google ADK

from edictum.adapters.google_adk import GoogleADKAdapter

adapter = GoogleADKAdapter(guard)
plugin = adapter.as_plugin()
# Pass to Runner(plugins=[plugin])

Nanobot

from edictum.adapters.nanobot import NanobotAdapter

adapter = NanobotAdapter(guard)
governed_registry = adapter.wrap_registry(agent.tool_registry)
# Replace agent's registry with the governed one

All eight adapters enforce the same contracts. The YAML does not change between frameworks.

Observe Mode

Not ready to deny calls in production? Change one line to test contracts in observe mode without denying anything:

defaults:
  mode: observe   # was: enforce

In observe mode, calls that would be denied are logged as CALL_WOULD_DENY audit events but allowed to proceed. Review the audit trail, tune your contracts, then switch back to enforce when ready.

See observe mode for the full workflow.

Dry-Run Evaluation

Test a tool call against your contracts without executing anything -- useful for CI pipelines and contract development:

result = guard.evaluate("read_file", {"path": ".env"})
print(result.verdict)                   # "deny"
print(result.deny_reasons[0])           # "Read of sensitive file denied: .env"
print(result.contracts[0].contract_id)  # "block-dotenv"

Batch evaluation for testing multiple scenarios at once:

results = guard.evaluate_batch([
    {"tool": "read_file", "args": {"path": ".env"}},
    {"tool": "read_file", "args": {"path": "readme.txt"}},
    {"tool": "run_command", "args": {"command": "rm -rf /"}},
])
for r in results:
    print(f"{r.tool_name}: {r.verdict.upper()}")
# read_file: DENY
# read_file: ALLOW
# run_command: DENY

Both methods are synchronous -- no await needed. See Dry-Run Evaluation for the full API reference and Testing Contracts for YAML test cases and CI integration.

Next Steps

Connecting to Edictum Console? When using Edictum.from_server() with a remote (non-loopback) host, HTTPS is required by default. See Connecting Agents for details.

Last updated on

On this page