Governance is runtime enforcement, not prompt suggestion.
sys.exit(1) > "please don't."
AI agents can read any file, run any command, modify any code. When you write "don't touch the auth module" in a system prompt, you're making a suggestion. The model can — and will — ignore it.
No amount of prompt engineering creates a hard boundary. Prompt-level instructions are suggestions that fail silently.
verdikt provides a governance architecture for AI agents, built on one core thesis:
Every agent action must pass through a policy evaluation graph that returns machine-enforceable verdicts. A blocked verdict stops the action. No negotiation, no override, no prompt injection.
Everything in verdikt is built from a single primitive:
Guard: check(Action) → Verdict
A Guard inspects an Action (what the agent wants to do) and returns a Verdict (pass, block, or warn). That's it.
Guards are leaf nodes. Combinators compose them into a decision tree:
All( # AND — all must pass
All( # Layer 1: BOUNDARY (fail fast)
FileBoundaryGuard(),
BashSafetyGuard(),
short_circuit=True,
),
BudgetGuard(max_tasks=100), # Layer 2: RESOURCE
When( # Layer 3: INTENT (conditional)
condition=lambda a, ctx: a.task_type == "deploy",
then=Escalate( # strict check → fallback
DeployChecklist(),
AutoApprove(max_risk=0.3),
),
otherwise=DefaultIntent(),
),
)
Six combinators cover all governance composition patterns:
| Combinator | Semantics | Use case |
|---|---|---|
All |
AND — all children must pass | Layer grouping, multi-check |
Any |
OR — one child pass suffices | Alternative approval paths |
Not |
Invert verdict | Deny-list from allow-list |
When |
Conditional branch | Route by action type, risk, env |
Threshold |
N-of-M must pass | Quorum / multi-reviewer |
Escalate |
Primary → fallback on block | Strict check with override path |
Unlike a flat middleware stack, verdikt's policy graph carries state:
GovernanceContext:
action # the current action
verdicts[] # accumulated leaf verdicts
annotations{} # inter-guard communication blackboard
risk_score # dynamic [0,1] — guards can increment
session # cross-action state (action count, block count, history)
trace[] # auto-recorded evaluation pathEarly guards can write annotations that later guards read. Risk scores accumulate. Session state persists across evaluations — enabling progressive policies like "block after 3 consecutive sensitive-file accesses."
Governance intersects with agent execution at exactly two points:
Agent Runtime Governance
───────────── ──────────
Agent wants to act ──────────────→ engine.evaluate(action)
│
←── pass ──────────┤ → proceed
←── block ─────────┤ → stop + fix suggestion
Agent acted ─────────────────────→ engine.post_action(action, outcome)
│
└── record to session + trace
Any system that can call two functions can integrate governance.
from verdikt import Action, ActionKind, PolicyEngine, All
from verdikt.guards import BashSafetyGuard, FileBoundaryGuard
engine = PolicyEngine(
All(FileBoundaryGuard(), BashSafetyGuard(), short_circuit=True)
)
result = engine.evaluate(Action(kind=ActionKind.BASH, payload={"command": "rm -rf /"}))
print(result.passed) # False
print(result.reason) # "Command matches blocked pattern: rm\s+-rf\s+/"from verdikt import Action, ActionKind, PolicyEngine, All, When, Escalate
engine = PolicyEngine(
All(
BashSafetyGuard(),
When(
condition=lambda a, ctx: a.kind == ActionKind.BASH,
then=All(FileBoundaryGuard(), BashSafetyGuard(), name="strict"),
otherwise=FileBoundaryGuard(), # read/write only need boundary
),
)
)from verdikt import PolicyEngine, All, InMemorySessionStore
from verdikt.guards import BashSafetyGuard
engine = PolicyEngine(
All(BashSafetyGuard()),
session_store=InMemorySessionStore(), # or SQLiteSessionStore()
)
# Session state accumulates across evaluations
result = engine.evaluate(Action(kind=ActionKind.BASH, session_id="s1", payload={"command": "ls"}))
print(result.context.session.action_count) # 1from verdikt import PolicyEngine, All, StdoutTracer, CollectingTracer
# Print trace to stderr
engine = PolicyEngine(All(BashSafetyGuard()), tracer=StdoutTracer())
# Or collect traces programmatically
tracer = CollectingTracer()
engine = PolicyEngine(All(BashSafetyGuard()), tracer=tracer)
engine.evaluate(action)
print(tracer.traces) # [(action, [TraceEntry, ...])]| Concept | What it is |
|---|---|
| Action | What an agent wants to do (read, write, bash, execute_code, submit_task) |
| Guard | Leaf node — check an action, return a verdict |
| Verdict | Pass, block, or warn — with reason and fix suggestion |
| PolicyNode | Any node in the policy graph (Guard or combinator) |
| All / Any / Not / When / Threshold / Escalate | Combinators — compose guards into a decision tree |
| GovernanceContext | State that flows through the graph during evaluation |
| PolicyEngine | Evaluates the policy graph, manages sessions and tracing |
| Guard | Layer | What it checks |
|---|---|---|
FileBoundaryGuard |
BOUNDARY | Path allowlist/blocklist, red-line file protection |
BashSafetyGuard |
BOUNDARY | Regex pattern blocking, prefix blocking, allowlist exceptions |
LayerIsolationGuard |
BOUNDARY | Cross-layer file access in multi-layer projects |
SandboxGuard |
BOUNDARY | AST scan for dangerous code patterns |
BudgetGuard |
RESOURCE | Session-scoped resource limits (thread-safe) |
IntentGuard |
INTENT | Business rule validation for task submissions |
AuditGuard |
OBSERVE | Records every action (always passes, never blocks) |
Implement the protocol — name + check():
from verdikt import Action, Verdict, BaseGuard, GovernanceContext
from verdikt.layers import GovernanceLayer
class RateLimitGuard(BaseGuard):
governance_layer = GovernanceLayer.RESOURCE
def __init__(self, max_per_minute: int):
self._calls = []
self._max = max_per_minute
@property
def name(self) -> str:
return "rate_limit"
def check(self, action: Action, ctx: GovernanceContext) -> Verdict:
import time
now = time.time()
self._calls = [t for t in self._calls if now - t < 60]
self._calls.append(now)
if len(self._calls) > self._max:
ctx.add_risk(0.3) # signal risk to downstream guards
return Verdict.block(guard=self.name, reason="Rate limited",
fix=f"Max {self._max} calls/min. Wait and retry.")
return Verdict.pass_(guard=self.name)Or without inheritance — just satisfy the protocol:
class MyGuard:
@property
def name(self) -> str:
return "my_guard"
def check(self, action):
return Verdict.pass_(guard=self.name)Both old (check(action)) and new (check(action, ctx)) signatures work — the engine auto-detects.
from verdikt import PolicyEngine, All
from verdikt.guards import BashSafetyGuard
engine = PolicyEngine(All(BashSafetyGuard()))
# Before the agent acts:
result = engine.evaluate(action)
if not result.passed:
return result.reason # block
# After the agent acts:
from verdikt import Outcome
engine.post_action(action, Outcome(success=True)){
"hooks": {
"PreToolUse": [
{
"matcher": "Read|Glob|Grep",
"command": "python -m verdikt.adapters.claude_hooks read \"$TOOL_INPUT\""
},
{
"matcher": "Edit|Write",
"command": "python -m verdikt.adapters.claude_hooks write \"$TOOL_INPUT\""
},
{
"matcher": "Bash",
"command": "python -m verdikt.adapters.claude_hooks bash \"$TOOL_INPUT\""
}
]
}
}Guards accept config via constructor or governance.yaml:
boundaries:
allowed_dirs: [src, tests, docs]
red_line_files: [src/auth/handler.py]
bash_safety:
blocked_patterns:
- 'rm\s+-rf\s+/'
- 'DROP\s+TABLE'- Exit codes are enforcement.
sys.exit(1)blocks a tool call. No prompt engineering overrides it. - One primitive. Guard:
check(Action) → Verdict. Everything else is composition. - Policy graphs, not middleware stacks. Combinators (
All,Any,When,Escalate, ...) compose guards into decision trees with branching, fallback, and quorum logic. - State flows through the graph. GovernanceContext carries verdicts, annotations, risk scores, and session history — enabling inter-guard communication and progressive policies.
- Two cut points. Pre-action check + post-action record. That's the entire integration surface.
- Fail with fix instructions. Every block tells the agent exactly what to do instead.
- Protocol, not inheritance. Implement
name+check()and you're a Guard. - Runtime-agnostic. The core framework has zero coupling to any specific agent runtime.
verdikt/
├── primitives.py # Action, Verdict, Guard protocol
├── context.py # GovernanceContext, SessionState, TraceEntry
├── nodes.py # PolicyNode, All, Any, Not, When, Threshold, Escalate
├── engine.py # PolicyEngine, EvaluationResult, Tracers
├── session.py # SessionStore protocol + InMemory/SQLite
├── layers.py # Four-layer governance model (BOUNDARY→RESOURCE→INTENT→OBSERVE)
├── config.py # governance.yaml loading
├── guards/ # Built-in Guard implementations
│ ├── file_boundary.py # Layer 1: BOUNDARY
│ ├── bash_safety.py # Layer 1: BOUNDARY
│ ├── layer_isolation.py# Layer 1: BOUNDARY
│ ├── sandbox.py # Layer 1: BOUNDARY
│ ├── budget.py # Layer 2: RESOURCE
│ ├── intent.py # Layer 3: INTENT
│ └── audit.py # Layer 4: OBSERVE
├── storage.py # SQLite backends (audit + budget)
├── pipeline.py # Legacy linear pipeline (still works)
├── enforcement.py # Legacy Enforcer (still works)
└── adapters/ # Runtime adapters
└── claude_hooks.py # Claude Code PreToolUse bridge
- Python 3.10+
- pydantic >= 2.0
- pyyaml (optional, for governance.yaml)
Apache 2.0