Use Cases
Real scenarios where Edictum prevents AI agents from causing harm.
Right page if: you have a specific deployment scenario (coding agent, healthcare, finance, DevOps, legal, MCP server) and want a complete, copy-paste ruleset. Wrong page if: you need to understand rule types conceptually -- see https://docs.edictum.ai/docs/concepts/rules. Gotcha: each scenario uses `Edictum.from_template()` for quick starts, but the full YAML ruleset below it shows what the template actually contains so you can customize it.
Real scenarios where Edictum prevents AI agents from causing harm. Each example includes a complete ruleset you can copy and customize.
Coding Agent
The problem. A coding agent has access to read_file, write_file, and bash. Without enforcement, it can read .env files, leak API keys in its responses, and run rm -rf / if a jailbreak succeeds. The BashClassifier and secret redaction handle most of this automatically.
Quick start with a template:
from edictum import Edictum
guard = Edictum.from_template("file-agent")Complete ruleset:
apiVersion: edictum/v1
kind: Ruleset
metadata:
name: coding-agent
defaults:
mode: enforce
rules:
- id: block-sensitive-reads
type: pre
tool: read_file
when:
args.path:
contains_any: [".env", ".secret", "kubeconfig", "credentials", ".pem", "id_rsa"]
then:
action: block
message: "Sensitive file '{args.path}' blocked."
tags: [secrets, dlp]
- id: block-destructive-bash
type: pre
tool: bash
when:
any:
- args.command: { matches: '\\brm\\s+(-rf?|--recursive)\\b' }
- args.command: { matches: '\\bmkfs\\b' }
- args.command: { contains: '> /dev/' }
then:
action: block
message: "Destructive command blocked: '{args.command}'."
tags: [destructive, safety]
- id: block-write-outside-target
type: pre
tool: write_file
when:
args.path:
starts_with: /
then:
action: block
message: "Write to absolute path '{args.path}' blocked. Use relative paths."
tags: [write-scope]Wiring code:
import asyncio
from edictum import Edictum, EdictumDenied
async def main():
guard = Edictum.from_template("file-agent")
# Every tool call goes through guard.run()
try:
result = await guard.run(
"read_file",
{"path": "/app/.env"},
read_file_fn,
)
except EdictumDenied as e:
print(f"Blocked: {e.reason}")
# => "Blocked: Sensitive file '/app/.env' blocked."
asyncio.run(main())What this showcases: automatic security. Secret values are auto-redacted in audit events and block messages. Bash commands are sanitized (passwords, tokens, connection strings). Rule errors fail closed -- a misconfigured rule blocks by default, never silently passes.
Stronger alternative: sandbox rulesets. The known-bad list rulesets above catch known-bad patterns. If red team bypasses keep appearing (base64 /etc/shadow, awk '{print}' /etc/shadow), switch to a sandbox that defines what's allowed:
rules:
# Allowlist: only /workspace and /tmp
- id: file-sandbox
type: sandbox
tools: [read_file, write_file, edit_file]
within:
- /workspace
- /tmp
not_within:
- /workspace/.git
- /workspace/.env
outside: block
message: "File access outside workspace: {args.path}"
# Allowlist: only approved commands
- id: exec-sandbox
type: sandbox
tool: bash
allows:
commands: [git, npm, pnpm, node, python, pytest, ruff, ls, cat, grep, find]
outside: block
message: "Command not in allowlist: {args.command}"Now base64 /etc/shadow is blocked -- not because base64 is in a known-bad list, but because /etc/shadow is not in /workspace or /tmp. See sandbox rulesets for the full concept.
Healthcare / Pharma
The problem. A clinical assistant has access to query_clinical_data and update_patient_record. A nurse should be able to read vitals for their assigned patients but never access psychiatric notes. An unauthenticated request should never reach patient data at all.
Complete ruleset:
apiVersion: edictum/v1
kind: Ruleset
metadata:
name: healthcare-agent
defaults:
mode: enforce
rules:
- id: require-authenticated-principal
type: pre
tool: query_clinical_data
when:
principal.role:
exists: false
then:
action: block
message: "Clinical data access requires an authenticated principal."
tags: [access-control, hipaa]
- id: restrict-psychiatric-notes
type: pre
tool: query_clinical_data
when:
all:
- args.data_type: { equals: psychiatric_notes }
- principal.role: { not_in: [psychiatrist, attending_physician] }
then:
action: block
message: "Access to psychiatric notes blocked for role '{principal.role}'. Requires psychiatrist or attending physician."
tags: [access-control, hipaa, psychiatric]
- id: restrict-patient-updates
type: pre
tool: update_patient_record
when:
principal.role: { not_in: [physician, attending_physician, nurse_practitioner] }
then:
action: block
message: "Patient record updates blocked for role '{principal.role}'."
tags: [access-control, hipaa]
- id: session-cap
type: session
limits:
max_tool_calls: 30
max_attempts: 60
then:
action: block
message: "Session limit reached. End consultation and start a new session."
tags: [rate-limit]Wiring code:
from edictum import Edictum, Principal
from edictum.adapters.langchain import LangChainAdapter
guard = Edictum.from_yaml("healthcare.yaml")
# Principal comes from your auth layer -- JWT claims, session context, etc.
adapter = LangChainAdapter(
guard,
principal=Principal(
role="nurse",
claims={"department": "cardiology", "assigned_patients": ["P-1234"]},
),
)
wrapper = adapter.as_tool_wrapper()What this showcases: access control. Principal claims and role gates enforce who can access what. Dynamic messages include the blocked role so audit logs are immediately actionable. Session caps prevent runaway queries.
Finance
The problem. A financial analysis agent queries databases and returns results to analysts. Query results often contain SSNs, account numbers, and other PII. The agent shouldn't be blocked access to the data -- it needs it to do analysis -- but PII should never appear in the response the analyst sees.
Complete ruleset:
apiVersion: edictum/v1
kind: Ruleset
metadata:
name: finance-agent
defaults:
mode: enforce
tools:
query_database:
side_effect: read
rules:
- id: redact-ssn-in-output
type: post
tool: query_database
when:
output.text:
matches: '\b\d{3}-\d{2}-\d{4}\b'
then:
action: redact
message: "SSN pattern detected and redacted from query result."
tags: [pii, compliance]
- id: redact-account-numbers
type: post
tool: query_database
when:
output.text:
matches: '\b\d{8,17}\b'
then:
action: redact
message: "Account number pattern detected and redacted."
tags: [pii, compliance]
- id: block-bulk-export
type: pre
tool: query_database
when:
args.query:
matches: '(?i)select\s+\*.*limit\s+\d{4,}'
then:
action: block
message: "Bulk data export blocked. Use targeted queries with reasonable limits."
tags: [dlp, compliance]
- id: transaction-session-cap
type: session
limits:
max_tool_calls: 15
then:
action: block
message: "Query limit reached for this analysis session."
tags: [rate-limit]Wiring code:
from edictum import Edictum, EdictumDenied
guard = Edictum.from_yaml("finance.yaml")
# The tool executes, postconditions scan the output, PII is redacted
result = await guard.run(
"query_database",
{"query": "SELECT name, ssn, balance FROM accounts WHERE id = 42"},
db_query_fn,
)
# result contains "***-**-****" instead of the actual SSNWhat this showcases: postcondition effects. action: redact strips PII from tool output before the agent sees it. The tool still executes -- the agent gets the data it needs for analysis, but sensitive patterns are replaced. action: block on postconditions suppresses the entire output.
DevOps
The problem. A DevOps agent manages deployments, runs diagnostics, and modifies infrastructure. You need production deploy gates (only seniors, only with tickets), bash safety, and a way to roll out new rulesets without breaking existing enforcement.
Quick start with a template:
from edictum import Edictum
guard = Edictum.from_template("devops-agent")Complete ruleset:
apiVersion: edictum/v1
kind: Ruleset
metadata:
name: devops-agent
defaults:
mode: enforce
rules:
- id: prod-deploy-requires-senior
type: pre
tool: deploy_service
when:
all:
- env.DEPLOY_ENV: { equals: production }
- principal.role: { not_in: [senior_engineer, sre, admin] }
then:
action: block
message: "Production deploys require senior role (sre/admin)."
tags: [change-control, production]
- id: prod-requires-ticket
type: pre
tool: deploy_service
when:
all:
- env.DEPLOY_ENV: { equals: production }
- principal.ticket_ref: { exists: false }
then:
action: block
message: "Production changes require a ticket reference."
tags: [change-control, compliance]
- id: block-destructive-bash
type: pre
tool: bash
when:
any:
- args.command: { matches: '\\brm\\s+(-rf?|--recursive)\\b' }
- args.command: { matches: '\\bmkfs\\b' }
- args.command: { contains: '> /dev/' }
then:
action: block
message: "Destructive command blocked."
tags: [destructive, safety]
- id: session-limits
type: session
limits:
max_tool_calls: 20
max_attempts: 50
then:
action: block
message: "Session limit reached. Summarize progress and stop."
tags: [rate-limit]Safe rollout with observe_alongside:
# new-deploy-rules.yaml -- observe-tested alongside production rulesets
apiVersion: edictum/v1
kind: Ruleset
metadata:
name: devops-v2-observed
observe_alongside: true
defaults:
mode: enforce
rules:
- id: require-rollback-plan
type: pre
tool: deploy_service
when:
principal.claims.rollback_plan:
exists: false
then:
action: block
message: "Deploy requires a rollback plan in principal claims."
tags: [change-control]from edictum import Edictum
# Compose production + observed rulesets
guard = Edictum.from_yaml("devops.yaml", "new-deploy-rules.yaml")
# Observed rulesets log what would be blocked but don't block anything
# Review audit logs, then promote: change observe_alongside to false and mode to enforceChecking drift before promoting:
# See what changed between ruleset versions
$ edictum diff devops-v1.yaml devops-v2.yaml
# Replay historical audit events against new rulesets
$ edictum replay devops-v2.yaml --audit-log audit.jsonlWhat this showcases: safe rollouts. observe_alongside lets you test new rulesets in observe mode without affecting production enforcement. edictum diff shows exactly what changed. edictum replay predicts how new rulesets would have affected past tool calls. Promote when you're confident.
Education
The problem. A tutoring agent helps students with assignments. It has access to search_web, run_code, and retrieve_document. You need to prevent access to student records, cap tool calls per assignment session, and validate that your rulesets behave correctly before deploying to students.
Complete ruleset:
apiVersion: edictum/v1
kind: Ruleset
metadata:
name: education-agent
defaults:
mode: enforce
rules:
- id: block-student-records
type: pre
tool: retrieve_document
when:
args.path:
contains_any: ["/grades/", "/transcripts/", "/disciplinary/"]
then:
action: block
message: "Access to student records blocked."
tags: [ferpa, student-privacy]
- id: block-answer-keys
type: pre
tool: retrieve_document
when:
args.path:
contains: "/answer-keys/"
then:
action: block
message: "Access to answer keys blocked during student sessions."
tags: [academic-integrity]
- id: assignment-session-cap
type: session
limits:
max_tool_calls: 25
max_attempts: 50
then:
action: block
message: "Session limit reached for this assignment. Submit your work and start a new session."
tags: [rate-limit, academic-integrity]Testing rulesets before deployment:
from edictum import Edictum
guard = Edictum.from_yaml("education.yaml")
# Dry-run: does this call get blocked?
result = guard.evaluate(
"retrieve_document",
{"path": "/grades/student-123.json"},
)
assert result.decision == "block"
assert "student records" in result.block_reasons[0].lower()
# Batch: test multiple scenarios at once
results = guard.evaluate_batch([
{"tool": "retrieve_document", "args": {"path": "/textbooks/chapter-1.pdf"}},
{"tool": "retrieve_document", "args": {"path": "/grades/student-456.json"}},
{"tool": "run_code", "args": {"code": "print('hello')"}},
])
assert results[0].decision == "allow"
assert results[1].decision == "block"
assert results[2].decision == "allow"YAML test cases with edictum test:
# education-tests.yaml
apiVersion: edictum/v1
kind: Ruleset
metadata:
name: education-tests
cases:
- description: "Student record access is blocked"
tool: retrieve_document
args:
path: "/grades/student-123.json"
expect: block
- description: "Textbook access is allowed"
tool: retrieve_document
args:
path: "/textbooks/chapter-1.pdf"
expect: allow
- description: "Answer key access is blocked"
tool: retrieve_document
args:
path: "/answer-keys/quiz-3.json"
expect: block$ edictum test education.yaml --cases education-tests.yaml
3 test cases: 3 passed, 0 failedWhat this showcases: rule testing. guard.evaluate() dry-runs rulesets without executing the tool. evaluate_batch() tests multiple scenarios at once. edictum test runs YAML test cases from the CLI with expected decisions. CI/CD exit codes gate your deployment pipeline.
Legal
The problem. A legal research agent has access to search_documents, retrieve_case, and summarize_document. It handles privileged attorney-client communications, confidential case files, and documents under regulatory hold. Every access must be auditable for compliance.
Complete ruleset:
apiVersion: edictum/v1
kind: Ruleset
metadata:
name: legal-agent
defaults:
mode: enforce
rules:
- id: restrict-privileged-docs
type: pre
tool: retrieve_case
when:
all:
- args.classification: { equals: privileged }
- principal.role: { not_in: [attorney, partner, legal_ops] }
then:
action: block
message: "Privileged document access blocked for role '{principal.role}'."
tags: [privilege, compliance]
- id: block-regulatory-hold
type: pre
tool: retrieve_case
when:
args.tags:
contains: regulatory_hold
then:
action: block
message: "Document under regulatory hold. Access requires manual approval."
tags: [regulatory, compliance]
- id: redact-client-pii
type: post
tool: search_documents
when:
output.text:
matches: '\b\d{3}-\d{2}-\d{4}\b'
then:
action: redact
message: "Client PII detected and redacted from search results."
tags: [pii, compliance]Wiring with decision log:
from edictum import Edictum, Principal
from edictum.audit import FileAuditSink, RedactionPolicy
# File sink writes structured JSONL -- one event per line, queryable
redaction = RedactionPolicy()
audit_sink = FileAuditSink("legal-audit.jsonl", redaction)
guard = Edictum.from_yaml(
"legal.yaml",
audit_sink=audit_sink,
redaction=redaction,
)
# Every evaluation produces a structured audit event:
# - tool name, args (with secrets redacted), principal, verdict
# - policy_version hash ties every event to the exact ruleset
# - timestamps, session counters, environment context
result = await guard.run(
"retrieve_case",
{"case_id": "2024-CF-1234", "classification": "privileged"},
retrieve_case_fn,
principal=Principal(role="attorney", claims={"bar_number": "12345"}),
)Audit event example (written to legal-audit.jsonl):
{
"action": "call_allowed",
"tool_name": "retrieve_case",
"tool_args": {"case_id": "2024-CF-1234", "classification": "privileged"},
"principal": {"role": "attorney", "claims": {"bar_number": "12345"}},
"policy_version": "a3f8c2...",
"environment": "production",
"session_attempt_count": 1,
"session_execution_count": 1
}What this showcases: observability. Every evaluation -- allowed, blocked, or observed -- produces a structured audit event. The policy_version hash ties each event to the exact ruleset that was loaded. FileAuditSink writes JSONL for compliance archives. OpenTelemetry spans emit the same data for Grafana, Datadog, or any OTel-compatible backend.
Common Workflows
Composing templates with custom rulesets
Start with a built-in template and layer your own rulesets on top:
from edictum import Edictum
# Template provides base rulesets, your YAML adds domain-specific ones
base = Edictum.from_template("file-agent") # built-in template (secret protection, bash safety)
custom = Edictum.from_yaml("my-overrides.yaml") # your custom rulesets
guard = Edictum.from_multiple([base, custom])Or merge multiple guards programmatically:
base = Edictum.from_template("file-agent")
custom = Edictum.from_yaml("custom.yaml")
guard = Edictum.from_multiple([base, custom])Dry-run testing
Test your rulesets before deploying:
guard = Edictum.from_yaml("rules.yaml")
# Single call
result = guard.evaluate("read_file", {"path": ".env"})
print(result.decision) # "block"
print(result.block_reasons) # ["Sensitive file '.env' blocked."]
# Batch -- test an entire scenario
results = guard.evaluate_batch([
{"tool": "read_file", "args": {"path": "README.md"}},
{"tool": "read_file", "args": {"path": ".env"}},
{"tool": "bash", "args": {"command": "ls -la"}},
{"tool": "bash", "args": {"command": "rm -rf /"}},
])
# => [allow, block, allow, block]From the CLI:
# Quick check
$ edictum check rules.yaml --tool read_file --args '{"path": ".env"}'
DENIED by block-sensitive-reads
# Run test suite
$ edictum test rules.yaml --cases tests.yaml
4 test cases: 4 passed, 0 failedObserve, then enforce
Start in observe mode to see what your rulesets would block without blocking anything. Review the audit log. Tune. Then enforce.
# Step 1: Deploy in observe mode
defaults:
mode: observe
# Step 2: Review audit logs -- look for CALL_WOULD_DENY events
# Step 3: Tune rulesets based on false positives
# Step 4: Switch to enforce
defaults:
mode: enforceFor incremental rollout of new rulesets alongside existing enforcement:
# new-rules.yaml
observe_alongside: true
defaults:
mode: enforce
rules:
- id: new-restriction
# ... this rule logs but doesn't blockguard = Edictum.from_yaml("production.yaml", "new-rules.yaml")
# Production rulesets enforce normally
# New rulesets observe-log onlyMCP Servers
MCP servers expose tools with known, stable names — mcp__postgres__query, mcp__slack__send_message, mcp__github__create_issue. This makes rule authoring straightforward: you know the exact tool names and argument shapes upfront.
The problem. Your agent connects to Postgres, Slack, and GitHub MCP servers. It can query any table, message any channel, and create issues with arbitrary content. One prompt injection in a document and it's exfiltrating data through Slack or creating spam issues.
Complete ruleset:
apiVersion: edictum/v1
kind: Ruleset
metadata:
name: mcp-servers
defaults:
mode: enforce
tools:
mcp__postgres__query: { side_effect: read }
mcp__slack__send_message: { side_effect: write }
mcp__github__create_issue: { side_effect: write }
mcp__filesystem__read_file: { side_effect: read }
rules:
# --- Postgres MCP ---
- id: block-unrestricted-queries
type: pre
tool: mcp__postgres__query
when:
args.sql:
matches: '(?i)SELECT\s+\*'
then:
action: block
message: "SELECT * blocked. Specify columns explicitly."
tags: [data-protection, postgres]
- id: block-destructive-sql
type: pre
tool: mcp__postgres__query
when:
args.sql:
matches: '(?i)\b(DROP|TRUNCATE|DELETE\s+FROM|ALTER)\b'
then:
action: block
message: "Destructive SQL blocked: {args.sql}"
tags: [data-protection, postgres]
- id: redact-pii-from-query-results
type: post
tool: mcp__postgres__query
when:
output.text:
matches_any:
- '\b\d{3}-\d{2}-\d{4}\b'
- '\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b'
then:
action: redact
message: "PII redacted from query results."
tags: [pii, postgres]
# --- Slack MCP ---
- id: slack-restrict-channels
type: pre
tool: mcp__slack__send_message
when:
args.channel:
not_in: ["#agent-updates", "#alerts"]
then:
action: block
message: "Agent can only post to #agent-updates and #alerts."
tags: [access-control, slack]
# --- GitHub MCP ---
- id: github-require-label
type: pre
tool: mcp__github__create_issue
when:
args.labels:
exists: false
then:
action: block
message: "Issues must include at least one label."
tags: [compliance, github]
# --- Filesystem MCP ---
- id: block-sensitive-files
type: pre
tool: mcp__filesystem__read_file
when:
args.path:
contains_any: [".env", ".pem", "credentials", "id_rsa"]
then:
action: block
message: "Reading sensitive file blocked: {args.path}"
tags: [secrets, filesystem]
# --- Session limits across all MCP tools ---
- id: mcp-session-limits
type: session
limits:
max_tool_calls: 100
max_calls_per_tool:
mcp__slack__send_message: 10
mcp__github__create_issue: 5
then:
action: block
message: "Session limit reached."Wiring it up:
from edictum import Edictum
guard = Edictum.from_yaml("mcp-rules.yaml")
# Use guard.run() to wrap any MCP tool call
result = await guard.run(
"mcp__postgres__query",
{"sql": "SELECT name, email FROM users WHERE id = 42"},
mcp_query_fn,
)What this showcases: real tool names from real MCP servers. You know the tool names (mcp__postgres__query, mcp__slack__send_message) because MCP servers declare them. Write rulesets against exact names and argument shapes. Combine preconditions (block destructive SQL, restrict Slack channels), postconditions (redact PII from query results), and session limits (cap Slack messages) in one ruleset.
Last updated on