Quickstart
In five minutes you will install Edictum, write a contract, and see it deny a dangerous tool call.
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.
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:
| Template | What it protects against |
|---|---|
file-agent | Secret file reads, destructive bash commands, writes outside working directory |
research-agent | Secret files, PII in output, session runaway (50 call cap) |
devops-agent | All of the above + role-gated production deploys + ticket requirements |
nanobot-agent | Approval 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.pyExpected 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/shadowThe .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 bytesSee 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 callsAgno
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 oneAll 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: enforceIn 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: DENYBoth 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