| name | agent-governance |
| description | Patterns and techniques for adding governance, safety, and trust controls to AI agent systems. Use this skill when:
- Building AI agents that call external tools (APIs, databases, file systems)
- Implementing policy-based access controls for agent tool usage
- Adding semantic intent classification to detect dangerous prompts
- Creating trust scoring systems for multi-agent workflows
- Building audit trails for agent actions and decisions
- Enforcing rate limits, content filters, or tool restrictions on agents
- Working with any agent framework (PydanticAI, CrewAI, OpenAI Agents, LangChain, AutoGen)
|
Agent Governance Patterns
Patterns for adding safety, trust, and policy enforcement to AI agent systems.
Overview
Governance patterns ensure AI agents operate within defined boundaries — controlling which tools they can call, what content they can process, how much they can do, and maintaining accountability through audit trails.
User Request → Intent Classification → Policy Check → Tool Execution → Audit Log
↓ ↓ ↓
Threat Detection Allow/Deny Trust Update
When to Use
- Agents with tool access: Any agent that calls external tools (APIs, databases, shell commands)
- Multi-agent systems: Agents delegating to other agents need trust boundaries
- Production deployments: Compliance, audit, and safety requirements
- Sensitive operations: Financial transactions, data access, infrastructure management
Pattern 1: Governance Policy
Define what an agent is allowed to do as a composable, serializable policy object.
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional
import re
class PolicyAction(Enum):
ALLOW = "allow"
DENY = "deny"
REVIEW = "review"
@dataclass
class GovernancePolicy:
"""Declarative policy controlling agent behavior."""
name: str
allowed_tools: list[str] = field(default_factory=list)
blocked_tools: list[str] = field(default_factory=list)
blocked_patterns: list[str] = field(default_factory=list)
max_calls_per_request: int = 100
require_human_approval: list[str] = field(default_factory=list)
def check_tool(self, tool_name: str) -> PolicyAction:
"""Check if a tool is allowed by this policy."""
if tool_name in self.blocked_tools:
return PolicyAction.DENY
if tool_name in self.require_human_approval:
return PolicyAction.REVIEW
if self.allowed_tools and tool_name not in self.allowed_tools:
return PolicyAction.DENY
return PolicyAction.ALLOW
def check_content(self, content: str) -> Optional[str]:
"""Check content against blocked patterns. Returns matched pattern or None."""
for pattern in self.blocked_patterns:
if re.search(pattern, content, re.IGNORECASE):
return pattern
return None
Policy Composition
Combine multiple policies (e.g., org-wide + team + agent-specific):
def compose_policies(*policies: GovernancePolicy) -> GovernancePolicy:
"""Merge policies with most-restrictive-wins semantics."""
combined = GovernancePolicy(name="composed")
for policy in policies:
combined.blocked_tools.extend(policy.blocked_tools)
combined.blocked_patterns.extend(policy.blocked_patterns)
combined.require_human_approval.extend(policy.require_human_approval)
combined.max_calls_per_request = min(
combined.max_calls_per_request,
policy.max_calls_per_request
)
if policy.allowed_tools:
if combined.allowed_tools:
combined.allowed_tools = [
t for t in combined.allowed_tools if t in policy.allowed_tools
]
else:
combined.allowed_tools = list(policy.allowed_tools)
return combined
Policy as YAML
Store policies as configuration, not code:
name: production-agent
allowed_tools:
- search_documents
- query_database
- send_email
blocked_tools:
- shell_exec
- delete_record
blocked_patterns:
- "(?i)(api[_-]?key|secret|password)\\s*[:=]"
- "(?i)(drop|truncate|delete from)\\s+\\w+"
max_calls_per_request: 25
require_human_approval:
- send_email
Pattern 2: Semantic Intent Classification
Detect dangerous intent in prompts before they reach the agent, using pattern-based signals.
from dataclasses import dataclass
@dataclass
class IntentSignal:
category: str
confidence: float
evidence: str
THREAT_SIGNALS = [
(r"(?i)send\s+(all|every|entire)\s+\w+\s+to\s+", "data_exfiltration", 0.8),
(r"(?i)export\s+.*\s+to\s+(external|outside|third.?party)", "data_exfiltration", 0.9),
(r"(?i)(sudo|as\s+root|admin\s+access)", "privilege_escalation", 0.8),
(r"(?i)(rm\s+-rf|del\s+/[sq]|format\s+c:)", "system_destruction", 0.95),
(r"(?i)(drop\s+database|truncate\s+table)", "system_destruction", 0.9),
(r"(?i)ignore\s+(previous|above|all)\s+(instructions?|rules?)", "prompt_injection", 0.9),
]
def classify_intent(content: str) -> list[IntentSignal]:
"""Classify content for threat signals."""
signals = []
for pattern, category, weight in THREAT_SIGNALS:
match = re.search(pattern, content)
if match:
signals.append(IntentSignal(
category=category,
confidence=weight,
evidence=match.group()
))
return signals
def is_safe(content: str, threshold: float = 0.7) -> bool:
"""Quick check: is the content safe above the given threshold?"""
signals = classify_intent(content)
return not any(s.confidence >= threshold for s in signals)
Pattern 3: Tool-Level Governance Decorator
Wrap individual tool functions with governance checks:
import functools
import time
from collections import defaultdict
_call_counters: dict[str, int] = defaultdict(int)
def govern(policy: GovernancePolicy, audit_trail=None):
"""Decorator that enforces governance policy on a tool function."""
def decorator(func):
@functools.wraps(func)
async def wrapper(*args, **kwargs):
tool_name = func.__name__
action = policy.check_tool(tool_name)
if action == PolicyAction.DENY:
raise PermissionError(f"Policy '{policy.name}' blocks tool '{tool_name}'")
if action == PolicyAction.REVIEW:
raise PermissionError(f"Tool '{tool_name}' requires human approval")
_call_counters[policy.name] += 1
if _call_counters[policy.name] > policy.max_calls_per_request:
raise PermissionError(f"Rate limit exceeded: {policy.max_calls_per_request} calls")
for arg in list(args) + list(kwargs.values()):
if isinstance(arg, str):
matched = policy.check_content(arg)
if matched:
raise PermissionError(f"Blocked pattern detected: {matched}")
start = time.monotonic()
try:
result = await func(*args, **kwargs)
if audit_trail is not None:
audit_trail.append({
"tool": tool_name, "action": "allowed",
"duration_ms": (time.monotonic() - start) * 1000,
"timestamp": time.time()
})
return result
except Exception as e:
if audit_trail is not None:
audit_trail.append({
"tool": tool_name, "action": "error",
"error": str(e), "timestamp": time.time()
})
raise
return wrapper
return decorator
Pattern 4: Trust Scoring
Track agent reliability over time with decay-based trust scores:
from dataclasses import dataclass, field
import math
import time
@dataclass
class TrustScore:
"""Trust score with temporal decay."""
score: float = 0.5
successes: int = 0
failures: int = 0
last_updated: float = field(default_factory=time.time)
def record_success(self, reward: float = 0.05):
self.successes += 1
self.score = min(1.0, self.score + reward * (1 - self.score))
self.last_updated = time.time()
def record_failure(self, penalty: float = 0.15):
self.failures += 1
self.score = max(0.0, self.score - penalty * self.score)
self.last_updated = time.time()
def current(self, decay_rate: float = 0.001) -> float:
"""Get score with temporal decay."""
elapsed = time.time() - self.last_updated
decay = math.exp(-decay_rate * elapsed)
return self.score * decay
Pattern 5: Audit Trail
Append-only audit log for all agent actions:
from dataclasses import dataclass, field
import json
import time
@dataclass
class AuditEntry:
timestamp: float
agent_id: str
tool_name: str
action: str
policy_name: str
details: dict = field(default_factory=dict)
class AuditTrail:
def __init__(self):
self._entries: list[AuditEntry] = []
def log(self, agent_id: str, tool_name: str, action: str,
policy_name: str, **details):
self._entries.append(AuditEntry(
timestamp=time.time(), agent_id=agent_id,
tool_name=tool_name, action=action,
policy_name=policy_name, details=details
))
def denied(self) -> list[AuditEntry]:
return [e for e in self._entries if e.action == "denied"]
def export_jsonl(self, path: str):
with open(path, "w") as f:
for entry in self._entries:
f.write(json.dumps({
"timestamp": entry.timestamp, "agent_id": entry.agent_id,
"tool": entry.tool_name, "action": entry.action,
"policy": entry.policy_name, **entry.details
}) + "\n")
Governance Levels
| Level | Controls | Use Case |
|---|
| Open | Audit only | Internal dev/testing |
| Standard | Tool allowlist + content filters | General production |
| Strict | All controls + human approval | Financial, healthcare |
| Locked | Allowlist only, full audit | Compliance-critical |
Best Practices
| Practice | Rationale |
|---|
| Policy as configuration | Store in YAML/JSON, not hardcoded |
| Most-restrictive-wins | Deny always overrides allow |
| Pre-flight intent check | Classify intent before tool execution |
| Trust decay | Require ongoing good behavior |
| Append-only audit | Immutability enables compliance |
| Fail closed | If check errors, deny the action |