Tutorial 37: Multi-Stage Policy Pipeline¶
Time: 15 minutes ยท Level: Intermediate ยท Prerequisites: Tutorial 36 (govern basics)
What You'll Build¶
A 4-stage governance pipeline that checks agent actions at every point in the execution lifecycle โ from user input to agent response.
User Input โ [pre_input] โ Agent Reasoning โ [pre_tool] โ Tool Call โ [post_tool] โ [pre_output] โ Response
โ deny? โ deny? โ deny? โ deny?
"Injection!" "Not allowed" "PII in output" "Secrets leaked"
The 4 Stages¶
| Stage | When It Runs | What It Catches |
|---|---|---|
pre_input | Before agent processes user input | Prompt injection, jailbreak patterns |
pre_tool | Before a tool call executes | Unauthorized actions, rate limits (default) |
post_tool | After tool returns, before agent uses result | PII in tool output, data classification |
pre_output | Before agent response reaches the user | Credential leaks, hallucinated secrets |
Complete Example: Financial Agent¶
# financial-agent-policy.yaml
apiVersion: governance.toolkit/v1
name: financial-agent
agents: ["*"]
default_action: allow
rules:
# โโ Stage 1: Pre-Input โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- name: block-injection
stage: pre_input
condition: "input.contains_injection"
action: deny
description: "Prompt injection detected"
priority: 1000
- name: block-role-override
stage: pre_input
condition: "input.has_role_override"
action: deny
description: "Role override attempt detected"
priority: 1000
# โโ Stage 2: Pre-Tool โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- name: block-unauthorized-transfer
stage: pre_tool
condition: "action.type == 'transfer' and amount.value > 10000"
action: require_approval
approvers: ["treasury-manager"]
description: "Transfers > $10K need treasury approval"
priority: 500
- name: block-external-api
stage: pre_tool
condition: "action.type == 'http_request' and target.is_external"
action: deny
description: "No external API calls from financial agents"
priority: 800
- name: allow-read-accounts
stage: pre_tool
condition: "action.type == 'read' and resource.type == 'account'"
action: allow
priority: 100
# โโ Stage 3: Post-Tool โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- name: block-pii-in-response
stage: post_tool
condition: "tool.output.contains_pii"
action: deny
description: "Tool returned PII โ must be redacted before agent uses it"
priority: 900
- name: classify-sensitive-output
stage: post_tool
condition: "tool.output.classification == 'restricted'"
action: deny
description: "Restricted data cannot be forwarded to the agent"
priority: 950
# โโ Stage 4: Pre-Output โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
- name: block-credential-leak
stage: pre_output
condition: "response.contains_credentials"
action: deny
description: "Agent response contains credentials โ blocked"
priority: 1000
- name: block-internal-urls
stage: pre_output
condition: "response.contains_internal_urls"
action: deny
description: "Internal URLs must not reach the end user"
priority: 800
Using It with govern()¶
from agentmesh.governance import govern, PolicyEngine
# Load the multi-stage policy
engine = PolicyEngine(conflict_strategy="deny_overrides")
policy = engine.load_yaml_file("financial-agent-policy.yaml")
# Group rules by stage
from collections import Counter
stage_counts = Counter(r.stage for r in policy.rules)
print("Rules per stage:", dict(stage_counts))
# โ {'pre_input': 2, 'pre_tool': 3, 'post_tool': 2, 'pre_output': 2}
Manual Stage-by-Stage Evaluation¶
For agents that need explicit control over when each stage fires:
from agentmesh.governance import PolicyEngine
engine = PolicyEngine(conflict_strategy="deny_overrides")
engine.load_yaml_file("financial-agent-policy.yaml")
# Stage 1: Check user input
input_check = engine.evaluate("*", {
"input": {"contains_injection": False, "has_role_override": False},
}, stage="pre_input")
print(f"Input check: {input_check.action}") # โ allow
# Stage 2: Check tool call
tool_check = engine.evaluate("*", {
"action": {"type": "read"},
"resource": {"type": "account"},
}, stage="pre_tool")
print(f"Tool check: {tool_check.action}") # โ allow
# Stage 3: Check tool output
output_check = engine.evaluate("*", {
"tool": {"output": {"contains_pii": True}},
}, stage="post_tool")
print(f"Output check: {output_check.action}") # โ deny (PII detected!)
# Stage 4: Check agent response (only if stage 3 passed)
response_check = engine.evaluate("*", {
"response": {"contains_credentials": False, "contains_internal_urls": False},
}, stage="pre_output")
print(f"Response check: {response_check.action}") # โ allow
Combining Stages with Composition¶
Parent policies can define pre_input rules while children add post_tool rules:
# org-baseline.yaml โ CISO defines input security
rules:
- name: org-injection-block
stage: pre_input
condition: "input.contains_injection"
action: deny
# app-policy.yaml โ App team adds DLP checks
extends: org-baseline.yaml
rules:
- name: app-pii-check
stage: post_tool
condition: "tool.output.contains_pii"
action: deny
The merged policy has rules across multiple stages from different governance layers.
What to Try Next¶
- Tutorial 39: Attribute ratchets (post_tool sets sensitivity โ pre_tool blocks export)
- Tutorial 40: OTel observability (trace each stage independently)