Policy Patterns¶
Practical recipes for writing Aegis policies. Each pattern is drawn from real-world policy files in the policies/ directory.
Prerequisites
Read Writing Policies first for the YAML structure, rule matching, and condition operators.
Fail-Closed vs Fail-Open¶
The most important design decision in any policy is the default behavior -- what happens when no rule matches.
Fail-closed (recommended)¶
Block or require approval for any action not explicitly allowed. Use this for production systems, regulated environments, and any agent that touches sensitive data.
version: "1"
defaults:
risk_level: high
approval: approve # Nothing runs without human sign-off
rules:
# Explicitly allow safe operations
- name: view_auto
match: { type: "view" }
risk_level: low
approval: auto
- name: read_auto
match: { type: "read" }
risk_level: low
approval: auto
Every policy in policies/ (devops, financial, healthcare) uses fail-closed defaults. This is deliberate -- unknown actions are dangerous by default.
Fail-open¶
Auto-approve by default, block only known-dangerous actions. Use this only for development, sandboxes, or low-risk internal tools.
version: "1"
defaults:
risk_level: low
approval: auto # Everything runs unless blocked
rules:
- name: block_deletes
match: { type: "delete*" }
risk_level: critical
approval: block
Warning
Fail-open policies are risky in production. A new action type the agent discovers will run without any gate. Prefer fail-closed and add auto rules as you gain confidence.
When to choose which¶
| Scenario | Default | Why |
|---|---|---|
| Production agent | approval: approve |
Unknown actions need human review |
| Healthcare / Finance | approval: approve |
Regulatory mandate |
| Internal dev tool | approval: auto |
Speed matters, blast radius is small |
| Prototype / sandbox | approval: auto |
Iterate fast, worry later |
Time-Based Rules¶
Control when actions can execute. Useful for deployment windows, business-hours-only operations, and after-hours lockdowns.
Business hours deployment window¶
Allow staging deploys during work hours, require approval for production, block everything after hours.
rules:
# Staging: auto during business hours
- name: staging_deploy_hours
match: { type: "deploy" }
conditions:
param_eq: { env: "staging" }
time_after: "09:00"
time_before: "18:00"
weekdays: [1, 2, 3, 4, 5] # Mon-Fri
risk_level: medium
approval: auto
# Production: approval required, daytime only
- name: prod_deploy_approve
match: { type: "deploy" }
conditions:
param_eq: { env: "production" }
time_after: "09:00"
time_before: "17:00"
weekdays: [1, 2, 3, 4, 5]
risk_level: high
approval: approve
# Production after hours: hard block
- name: prod_deploy_afterhours
match: { type: "deploy" }
conditions:
param_eq: { env: "production" }
time_after: "17:00"
risk_level: critical
approval: block
This pattern comes directly from policies/devops-agent.yaml. Notice how rules go from specific (staging + time window) to restrictive (after-hours block). First match wins, so order matters.
After-hours lockdown¶
Block all write operations outside business hours. Useful for financial agents.
rules:
# ... your normal rules above ...
# Catch-all: block everything after 8 PM
- name: after_hours_block
match: { type: "*" }
conditions:
time_after: "20:00"
risk_level: critical
approval: block
Note
Place after-hours catch-all rules last. Since first match wins, specific rules above will take priority during business hours.
Weekday-only operations¶
Combine with time_after / time_before for precise windows. All conditions are AND-combined -- every condition must pass.
Parameter Thresholds¶
Gate actions based on their parameters. Useful for amount limits, row counts, and role-based controls.
Tiered payment approval¶
Small payments get lighter gates, large payments require human approval.
rules:
# Small payments: approve with review
- name: payment_small
match: { type: "payment" }
conditions:
param_lte: { amount: 100 }
risk_level: medium
approval: approve
# Large payments: high risk, needs approval
- name: payment_large
match: { type: "payment" }
conditions:
param_gt: { amount: 100 }
risk_level: high
approval: approve
From policies/financial-agent.yaml. You can add more tiers:
rules:
# Micro-payments: auto
- name: payment_micro
match: { type: "payment" }
conditions:
param_lte: { amount: 10 }
risk_level: low
approval: auto
# Standard: approve
- name: payment_standard
match: { type: "payment" }
conditions:
param_lte: { amount: 1000 }
risk_level: medium
approval: approve
# Large: critical, needs approval
- name: payment_large
match: { type: "payment" }
conditions:
param_gt: { amount: 1000 }
risk_level: critical
approval: approve
Bulk operation guards¶
Prevent agents from modifying too many records at once.
rules:
# Single record update: auto
- name: update_single
match: { type: "update" }
conditions:
param_lte: { row_count: 1 }
risk_level: low
approval: auto
# Moderate batch: needs approval
- name: update_batch
match: { type: "update" }
conditions:
param_gt: { row_count: 1 }
param_lte: { row_count: 100 }
risk_level: medium
approval: approve
# Large batch: block
- name: update_bulk_block
match: { type: "update" }
conditions:
param_gt: { row_count: 100 }
risk_level: critical
approval: block
Available comparison operators¶
| Operator | Meaning | Example |
|---|---|---|
param_eq |
Equals | param_eq: { env: "staging" } |
param_gt |
Greater than | param_gt: { amount: 100 } |
param_lt |
Less than | param_lt: { count: 10 } |
param_gte |
Greater or equal | param_gte: { priority: 3 } |
param_lte |
Less or equal | param_lte: { amount: 100 } |
param_contains |
Value in collection | param_contains: { tags: "urgent" } |
param_matches |
Regex match | param_matches: { email: "@corp\\.com$" } |
Industry Compliance Patterns¶
HIPAA (Healthcare)¶
Key principles: PHI access requires approval, modifications are blocked, everything is audited.
version: "1"
defaults:
risk_level: high
approval: approve # Fail-closed
rules:
# Non-PHI: safe
- name: view_schedule_auto
match: { type: "view_schedule" }
risk_level: low
approval: auto
# PHI read: needs approval
- name: view_patient_approve
match: { type: "view_patient" }
risk_level: medium
approval: approve
# Sensitive PHI fields (SSN, DOB): high risk
- name: access_phi_high
match: { type: "access_phi" }
risk_level: high
approval: approve
# After-hours PHI: escalate to critical
- name: after_hours_phi
match: { type: "access_phi" }
conditions:
time_after: "22:00"
risk_level: critical
approval: approve
# Append-only clinical notes
- name: create_note
match: { type: "create_note" }
risk_level: medium
approval: approve
# Modifications: always blocked
- name: modify_patient_block
match: { type: "modify_patient" }
risk_level: critical
approval: block
# Deletions: always blocked
- name: delete_block
match: { type: "delete" }
risk_level: critical
approval: block
From policies/healthcare-agent.yaml. The HIPAA pattern relies on three pillars:
- Approval for all PHI access -- no auto-approve for patient data
- Block all modifications -- agents can read and append, never mutate or delete
- Audit trail -- Aegis logs every action automatically; export with
aegis audit --format jsonl
SOC 2¶
SOC 2 requires access controls, audit logging, and change management. Map these to Aegis:
version: "1"
defaults:
risk_level: high
approval: approve
rules:
# Read-only: auto (access logged by Aegis)
- name: read_auto
match: { type: "read*" }
risk_level: low
approval: auto
# Config changes: approval required (change management)
- name: config_change
match: { type: "update_config" }
risk_level: high
approval: approve
# User management: critical
- name: user_management
match: { type: "create_user" }
risk_level: critical
approval: approve
# Permission changes: blocked for agents
- name: permission_block
match: { type: "modify_permissions" }
risk_level: critical
approval: block
PCI DSS¶
For agents that interact with payment systems:
version: "1"
defaults:
risk_level: critical
approval: block # Maximum restriction by default
rules:
# Read transaction history: allowed with approval
- name: read_transactions
match: { type: "read" }
risk_level: medium
approval: approve
# Process payment: strict approval
- name: process_payment
match: { type: "payment" }
risk_level: critical
approval: approve
# Access cardholder data: always blocked for agents
- name: cardholder_data_block
match: { type: "access_cardholder*" }
risk_level: critical
approval: block
# Refunds: approval with amount guard
- name: refund_small
match: { type: "refund" }
conditions:
param_lte: { amount: 50 }
risk_level: high
approval: approve
- name: refund_large
match: { type: "refund" }
conditions:
param_gt: { amount: 50 }
risk_level: critical
approval: approve
Layered Policies¶
Aegis supports merging multiple policy files. This lets you compose policies from reusable building blocks.
Loading multiple files¶
from aegis.core.policy import Policy
# Merge: base rules + team-specific overrides
policy = Policy.from_yaml_files(
"policies/base.yaml", # Company-wide defaults
"policies/healthcare-agent.yaml" # Domain-specific rules
)
Rules from later files are appended after earlier files. Since first-match-wins, put higher-priority rules in earlier files.
Example: base + domain layers¶
policies/base.yaml -- company-wide safety net:
version: "1"
defaults:
risk_level: high
approval: approve
rules:
# Every team gets read access
- name: read_auto
match: { type: "read*" }
risk_level: low
approval: auto
# Block destructive ops everywhere
- name: delete_block
match: { type: "delete*" }
risk_level: critical
approval: block
policies/team-payments.yaml -- payment-team additions:
version: "1"
defaults:
risk_level: high
approval: approve
rules:
- name: small_payment
match: { type: "payment" }
conditions:
param_lte: { amount: 100 }
risk_level: medium
approval: approve
When merged, the base delete_block rule fires first (it comes from the earlier file), so payment-team agents still cannot delete records.
Merge with Python API¶
base = Policy.from_yaml("policies/base.yaml")
team = Policy.from_yaml("policies/team-payments.yaml")
# merge() returns a new Policy; originals are unchanged
combined = base.merge(team)
Layering strategy¶
| Layer | Contains | Example |
|---|---|---|
| Base | Company-wide safety defaults, destructive-op blocks | base.yaml |
| Domain | Industry compliance rules (HIPAA, PCI) | healthcare.yaml |
| Team | Team-specific action types and thresholds | team-payments.yaml |
| Environment | Time windows, after-hours rules | prod-hours.yaml |
Testing Policies¶
Always test policies before deploying. Aegis provides two approaches: the CLI simulator and Python unit tests.
CLI: aegis simulate¶
Test actions against a policy without executing anything.
# Syntax: aegis simulate <policy_file> <type:target> [type:target ...]
aegis simulate policies/devops-agent.yaml deploy:staging destroy:infra
# Output shows the decision for each action:
# deploy:staging -> MEDIUM / APPROVE (matched: staging_deploy_hours)
# destroy:infra -> CRITICAL / BLOCK (matched: destroy_block)
CLI: aegis validate¶
Check policy syntax before loading.
Python unit tests¶
Write pytest tests that verify your policy logic.
import pytest
from aegis.core.action import Action
from aegis.core.policy import Policy
@pytest.fixture
def devops_policy():
return Policy.from_yaml("policies/devops-agent.yaml")
def test_monitoring_is_auto(devops_policy):
decision = devops_policy.evaluate(Action("monitor", "cpu"))
assert decision.approval.value == "auto"
assert decision.risk_level.value == "low"
def test_destroy_is_blocked(devops_policy):
decision = devops_policy.evaluate(Action("destroy", "vpc"))
assert decision.approval.value == "block"
assert decision.risk_level.value == "critical"
def test_force_push_blocked(devops_policy):
decision = devops_policy.evaluate(Action("force_push", "main"))
assert decision.approval.value == "block"
Testing parameter conditions¶
def test_small_payment_approved():
policy = Policy.from_yaml("policies/financial-agent.yaml")
action = Action("payment", "vendor", params={"amount": 50})
decision = policy.evaluate(action)
assert decision.approval.value == "approve"
assert decision.risk_level.value == "medium"
def test_large_payment_high_risk():
policy = Policy.from_yaml("policies/financial-agent.yaml")
action = Action("payment", "vendor", params={"amount": 5000})
decision = policy.evaluate(action)
assert decision.risk_level.value == "high"
Testing with the runtime¶
For end-to-end tests that verify the full governance pipeline:
@pytest.mark.asyncio
async def test_blocked_action_never_executes(tmp_path):
from aegis.runtime.engine import Runtime
from aegis.runtime.approval import AutoApprovalHandler
from aegis.runtime.audit import AuditLogger
policy = Policy.from_yaml("policies/devops-agent.yaml")
executor = FakeExecutor()
runtime = Runtime(
executor=executor,
policy=policy,
approval_handler=AutoApprovalHandler(),
audit_logger=AuditLogger(db_path=tmp_path / "test.db"),
)
result = await runtime.run_one(Action("destroy", "production"))
assert not result.ok
assert len(executor.executed) == 0 # Never reached the executor
Quick Reference¶
| Pattern | Key Idea | Default |
|---|---|---|
| Fail-closed | Block unknown actions | approval: approve |
| Fail-open | Allow unknown actions | approval: auto |
| Time windows | Combine time_after + time_before + weekdays |
-- |
| Amount tiers | Chain param_lte / param_gt rules |
-- |
| HIPAA | Approve PHI reads, block all mutations | approval: approve |
| SOC 2 | Audit everything, approve config changes | approval: approve |
| PCI | Block cardholder data access, tier refunds | approval: block |
| Layered | Policy.from_yaml_files() or .merge() |
Base file wins |
See also: Writing Policies | Governance Checklist | Cheatsheet