Quickstart¶
Installation¶
pip install edictum # core only
pip install edictum[all] # all 6 framework adapters + OTel
pip install edictum[langchain] # individual adapter extras
Requires Python 3.11+. Zero runtime dependencies for the core package.
Framework-Agnostic Usage (guard.run)¶
guard.run() is the simplest way to govern a tool call. It runs the full governance pipeline, executes the tool if allowed, and raises EdictumDenied if blocked.
import asyncio
from edictum import (
Edictum,
EdictumDenied,
EdictumToolError,
Verdict,
deny_sensitive_reads,
precondition,
)
# A custom precondition: block Bash commands starting with "rm"
@precondition("Bash")
def no_destructive_commands(envelope):
if envelope.bash_command and envelope.bash_command.strip().startswith("rm"):
return Verdict.fail(
"Destructive command blocked. Use a safer alternative or "
"request explicit approval before deleting files."
)
return Verdict.pass_()
guard = Edictum(
contracts=[
deny_sensitive_reads(),
no_destructive_commands,
],
)
async def run_bash(command):
"""Mock tool — in production this would execute a shell command."""
return f"executed: {command}"
async def main():
# Allowed: normal command
result = await guard.run("Bash", {"command": "ls -la"}, run_bash)
print(result) # "executed: ls -la"
# Denied: destructive command
try:
await guard.run("Bash", {"command": "rm -rf /tmp/data"}, run_bash)
except EdictumDenied as e:
print(f"Denied: {e.reason}")
print(f"Source: {e.decision_source}") # "precondition"
# Denied: sensitive path
try:
await guard.run(
"Read",
{"file_path": "/home/user/.ssh/id_rsa"},
lambda file_path: open(file_path).read(),
)
except EdictumDenied as e:
print(f"Denied: {e.reason}")
asyncio.run(main())
Observe Mode¶
Observe mode runs the full pipeline but never blocks. Denials are logged as CALL_WOULD_DENY instead of CALL_DENIED. Use this for shadow deployment -- see what would break before enforcing rules.
import asyncio
from edictum import Edictum, Verdict, deny_sensitive_reads, precondition
from edictum.audit import FileAuditSink
guard = Edictum(
mode="observe",
contracts=[deny_sensitive_reads()],
audit_sink=FileAuditSink("audit.jsonl"),
)
async def run_bash(command):
return f"executed: {command}"
async def main():
# This would be denied in enforce mode, but observe mode allows it through.
# The audit log records CALL_WOULD_DENY so you can review violations.
result = await guard.run("Bash", {"command": "cat ~/.ssh/id_rsa"}, run_bash)
print(result) # "executed: cat ~/.ssh/id_rsa"
asyncio.run(main())
Framework Adapters¶
Edictum ships thin adapters for 6 agent frameworks. Each translates between the framework's hook interface and the shared governance pipeline.
from edictum import Edictum, deny_sensitive_reads, OperationLimits
guard = Edictum(
contracts=[deny_sensitive_reads()],
limits=OperationLimits(max_tool_calls=100),
)
LangChain:
from edictum.adapters.langchain import LangChainAdapter
adapter = LangChainAdapter(guard, session_id="session-lc")
# pre_result = await adapter._pre_tool_call(request)
# await adapter._post_tool_call(request, result)
CrewAI:
from edictum.adapters.crewai import CrewAIAdapter
adapter = CrewAIAdapter(guard, session_id="session-crew")
# allowed = await adapter._before_hook(context) # False = denied
# await adapter._after_hook(context)
Agno:
from edictum.adapters.agno import AgnoAdapter
adapter = AgnoAdapter(guard, session_id="session-agno")
# result = await adapter._hook_async(name, callable, args)
Semantic Kernel:
from edictum.adapters.semantic_kernel import SemanticKernelAdapter
adapter = SemanticKernelAdapter(guard, session_id="session-sk")
# pre = await adapter._pre(name, args, call_id) # {} or "DENIED: ..."
# await adapter._post(call_id, result)
OpenAI Agents SDK:
from edictum.adapters.openai_agents import OpenAIAgentsAdapter
adapter = OpenAIAgentsAdapter(guard, session_id="session-oai")
# pre = await adapter._pre(name, args, call_id) # None or "DENIED: ..."
# await adapter._post(call_id, result)
Claude Agent SDK:
from edictum.adapters.claude_agent_sdk import ClaudeAgentSDKAdapter
adapter = ClaudeAgentSDKAdapter(guard, session_id="session-claude")
# pre = await adapter._pre_tool_use(name, input, id) # {} or deny dict
# await adapter._post_tool_use(id, response)
Each adapter manages pending state between pre and post hooks, tracks call indices, and emits audit events. See examples/ for live demos of all 6 adapters.
Writing Contracts¶
Preconditions¶
Preconditions run before execution. They receive a ToolEnvelope and return a Verdict. If the verdict fails, the tool call is denied and the agent receives the failure message.
from edictum import Verdict, precondition
# Target a specific tool
@precondition("Bash")
def require_safe_prefix(envelope):
allowed = ["ls", "cat", "git status", "git diff", "pwd"]
cmd = (envelope.bash_command or "").strip()
if not any(cmd == p or cmd.startswith(p + " ") for p in allowed):
return Verdict.fail(
f"Command '{cmd}' is not in the safe prefix list. "
"Use one of: ls, cat, git status, git diff, pwd."
)
return Verdict.pass_()
# Target all tools with a wildcard
@precondition("*")
def block_production(envelope):
if envelope.environment == "production":
return Verdict.fail("Tool calls are disabled in production.")
return Verdict.pass_()
Postconditions¶
Postconditions run after execution. They are observe-only -- they emit warnings but never block. The warning message adapts to the tool's side-effect classification:
- Pure/Read tools: warning suggests retrying.
- Write/Irreversible tools: warning says "assess before proceeding" (no retry coaching for something that already mutated state).
from edictum import Verdict
from edictum.contracts import postcondition
@postcondition("Bash")
def check_exit_status(envelope, result):
if isinstance(result, str) and "Error:" in result:
return Verdict.fail(
f"Bash command returned an error: {result[:200]}"
)
return Verdict.pass_()
Session Contracts¶
Session contracts check cross-turn state using persisted atomic counters. They must be async because session methods are async.
from edictum import Verdict
from edictum.contracts import session_contract
@session_contract
async def limit_bash_calls(session):
bash_count = await session.tool_execution_count("Bash")
if bash_count >= 50:
return Verdict.fail(
"Bash execution limit reached (50). Summarize progress "
"and use non-Bash tools to continue."
)
return Verdict.pass_()
Writing Hooks¶
Hooks are lower-level than contracts. A before-hook receives the ToolEnvelope and returns a HookDecision. Hooks run before preconditions in the pipeline.
from edictum import Edictum
from edictum.hooks import HookDecision
from edictum.types import HookRegistration
def log_and_allow(envelope):
print(f"[hook] tool={envelope.tool_name} args={envelope.args}")
return HookDecision.allow()
def deny_after_hours(envelope):
from datetime import datetime, timezone
hour = datetime.now(timezone.utc).hour
if hour < 6 or hour > 22:
return HookDecision.deny("Tool calls blocked outside business hours (06-22 UTC).")
return HookDecision.allow()
guard = Edictum(
hooks=[
HookRegistration(phase="before", tool="*", callback=log_and_allow),
HookRegistration(phase="before", tool="Bash", callback=deny_after_hours),
],
)
After-hooks observe the result. They receive (envelope, result) and don't return a decision.
def audit_after(envelope, result):
print(f"[after] {envelope.tool_name} completed")
guard = Edictum(
hooks=[
HookRegistration(phase="after", tool="*", callback=audit_after),
],
)
Audit & Redaction¶
Every tool call emits a structured AuditEvent to a configurable sink. Two built-in sinks are provided:
from edictum import Edictum
from edictum.audit import FileAuditSink, RedactionPolicy, StdoutAuditSink
# JSON to stdout (default)
guard = Edictum(audit_sink=StdoutAuditSink())
# JSON lines to a file
guard = Edictum(audit_sink=FileAuditSink("audit.jsonl"))
Redaction¶
RedactionPolicy strips sensitive data at write time. Redaction is destructive -- there is no recovery path.
What gets auto-redacted:
- Sensitive keys: any dict key containing
token,key,secret,password, orcredential(plus a full default set). - Secret value patterns: OpenAI keys (
sk-...), AWS access keys (AKIA...), JWTs (eyJ...), GitHub tokens (ghp_...), Slack tokens (xox...). - Bash credentials:
export SECRET_KEY=...,-p password, URL credentials (://user:pass@). - Payload cap: audit events exceeding 32KB are truncated.
Custom configuration:
from edictum import Edictum
from edictum.audit import FileAuditSink, RedactionPolicy
redaction = RedactionPolicy(
sensitive_keys={"my_internal_token", "database_url", "password"},
)
guard = Edictum(
audit_sink=FileAuditSink("audit.jsonl", redaction=redaction),
redaction=redaction,
)
Custom Sink¶
Implement the AuditSink protocol -- a single async emit(event) method:
class MyAuditSink:
async def emit(self, event):
# event is an AuditEvent dataclass
print(f"{event.action}: {event.tool_name}")
Operation Limits¶
Operation limits cap how many tool calls an agent can make in a session. Two counter types serve different purposes:
max_attemptscaps all governance evaluations, including denied ones. This catches denial loops where an agent keeps retrying the same blocked call.max_tool_callscaps only successful executions. This caps total work done.max_calls_per_toolcaps individual tools independently.
from edictum import Edictum, OperationLimits
guard = Edictum(
limits=OperationLimits(
max_attempts=100,
max_tool_calls=50,
max_calls_per_tool={"Bash": 20, "Write": 10},
),
)
When a limit fires, the denial message tells the agent to stop and reassess rather than retry.
Observe Mode¶
Set mode="observe" to run the full governance pipeline without blocking anything. The pipeline evaluates all rules, emits audit events, but converts denials to CALL_WOULD_DENY and allows the tool through.
Use this for:
- Shadow deployment. Deploy Edictum alongside your agent, collect audit logs, and tune rules before switching to enforce mode.
- Rule development. Write new preconditions and see what they'd block without disrupting the agent.
- Compliance auditing. Record every tool call with governance evaluation results, even if you don't want to block anything yet.
The audit trail distinguishes three states:
| Audit Action | Meaning |
|---|---|
CALL_ALLOWED |
Pipeline passed, tool executed |
CALL_DENIED |
Pipeline denied, tool did not execute (enforce mode) |
CALL_WOULD_DENY |
Pipeline denied, tool executed anyway (observe mode) |
What's Next¶
- Architecture overview for the full module structure and design decisions.