Check the call, not the answer.
An agent can refuse in text and still make the call. When it decides to call a tool, Edictum checks that call against your YAML rules before the tool runs, inside your own process, and blocks it or sends it to an approval handler you plug in.
- License
- MIT
- Python
- 0.19.0
- TypeScript
- 0.5.0
Between the decision and the tool.
- The model decides to call a tool.
- Edictum receives the tool call and its arguments.
- Edictum checks the call against your rules, inside the agent process, before the tool runs.
- Allow: No rule stopped the call. The tool runs.
- Block: The tool does not run. The caller gets the rule's message.
- Ask: Your approval handler decides. No handler, or no answer in time: blocked.
What you get.
Block or ask before the tool runs.
Malformed policy fails at load.
Observe before you enforce.
Checks emit audit events.
Runs in your process.
Rules are YAML you keep with the code.
A rule names a tool, a condition on its arguments and an action. The ruleset below blocks any read_file call whose path contains .env, and sends shell commands matching rm or sudo to the approval handler.
The file ran unchanged with both SDKs. Both programs printed Read of .env blocked.
apiVersion: edictum/v1
kind: Ruleset
metadata:
name: demo
defaults:
mode: enforce
rules:
- id: block-dotenv
type: pre
tool: read_file
when:
args.path: { contains: '.env' }
then:
action: block
message: 'Read of {args.path} blocked.'
- id: ask-shell
type: pre
tool: shell_exec
when:
args.command: { matches: '\b(rm|sudo)\b' }
then:
action: ask
message: 'Needs approval: {args.command}'
timeout: 120
timeout_action: block
import asyncio
from edictum import Edictum, EdictumDenied
guard = Edictum.from_yaml("rules.yaml")
def read_file(path):
return open(path).read()
async def main():
try:
await guard.run("read_file", {"path": ".env"}, read_file)
except EdictumDenied as e:
print(e.reason) # Read of .env blocked.
asyncio.run(main())
import { readFileSync } from 'node:fs'
import { Edictum, EdictumDenied } from '@edictum/core'
const guard = Edictum.fromYaml('rules.yaml')
const readFile = (args: Record<string, unknown>) => readFileSync(String(args.path), 'utf8')
try {
await guard.run('read_file', { path: '.env' }, readFile)
} catch (e) {
if (e instanceof EdictumDenied) console.log(e.reason) // Read of .env blocked.
}
A shell_exec call that matches the second rule goes to the approval handler. With none configured, both SDKs block it and report that approval is required.
Install it where the agent runs.
Python
Python 3.11+ · 0.19.0, published 2026-08-11
pip install "edictum[yaml]"Adapters in the package
- LangChain
- OpenAI Agents SDK
- CrewAI
- Google ADK
- Agno
- Semantic Kernel
- Nanobot
TypeScript
Node 22+ · 0.5.0, published 2026-04-15
pnpm add @edictum/coreAdapters as packages
- Vercel AI SDK@edictum/vercel-ai
- LangChain.js@edictum/langchain
- OpenAI Agents SDK@edictum/openai-agents
Each adapter package lists its framework as a peer dependency.
Why the call, not the text.
Cartagena and Teixeira, arXiv:2602.16943, February 2026. Across six frontier models, six regulated domains and 17,420 datapoints, the finding is in the title: text safety does not transfer to tool-call safety.
It is the research behind Edictum. A model that declines in prose can still make the call, so Edictum checks the call.
Read the paper