MAPL Policy Guide
A Tutorial for the MACAW Agentic Policy Language
Version: 3.0.1
Last Updated: January 27, 2026
Table of Contents
- Introduction
- Part 1: Understanding MAPL
- Part 2: Your First MAPL Policy
- Part 3: Policy Inheritance with extends
- Part 4: Parameter Constraints
- Part 5: Attestations
- Part 6: Complete Tutorial Example
- Part 7: Best Practices
- Part 8: Troubleshooting
- Appendix A: Quick Reference
- Appendix B: Theoretical Foundations
- Appendix C: Future Enhancements
Introduction
Welcome to the MAPL (MACAW Agentic Policy Language) tutorial! This guide will teach you how to write security policies for AI agent systems.
What is MAPL?
MAPL is a declarative policy language designed specifically for agentic AI systems. Unlike traditional access control systems designed for static files and databases, MAPL handles the unique challenges of AI agents:
- Agents that morph identities and delegate capabilities
- Systems that scale 10-100x per user through agent multiplication
- Dynamic workflows where agents spawn sub-agents and invoke tools
Why MAPL is Different
Traditional policy systems require O(M×N) rules for M principals and N resources. This explodes quickly:
- 1,000 users × 100 resources = 100,000 rules to manage ❌
MAPL uses hierarchical composition to achieve O(log M + N) policies:
- log(1,000) ≈ 3-4 organizational levels + 100 resource policies ✅
Three Key Innovations
Deferred Principal Binding: Principals are resolved at runtime from authenticated context (JWT claims), not hardcoded in policies. Alice's role changes? No policy updates needed.
Dual Perspective Enforcement: Caller and resource policies are enforced independently, then composed via intersection. Even if the caller is compromised, the resource enforces its own limits.
Provable Security: MAPL policies compose via mathematical intersection semantics with formal guarantees:
- Monotonic Restriction: Adding policies can only make things more restrictive, never less
- Transitive Denial: If any policy denies something, it stays denied
- No Privilege Escalation: Combining policies cannot grant denied permissions
What You'll Learn
By the end of this tutorial, you'll be able to:
- Write MAPL policies for your organization
- Use inheritance to create hierarchical policies
- Specify parameter constraints for security and compliance
- Use attestations for multi-step workflows (including conditional attestations)
- Understand how policies compose and intersect
- Debug policy issues when things don't work as expected
Let's get started!
Part 1: Understanding MAPL
The Agentic Challenge
Traditional access control was designed for:
User → File System
"Alice can READ /etc/passwd"
But AI agents are different:
User → Agent → Sub-Agent → Tool → LLM → Database
Alice invokes AnalystAgent
→ AnalystAgent spawns DataAgent (as Alice's delegate)
→ DataAgent calls DatabaseTool
→ DatabaseTool queries SQL
→ Returns to Alice
This creates challenges:
- Contextual identity: The same agent represents different users at different times
- Delegation chains: Sub-agents inherit but restrict parent permissions
- Scale: 1 user might spawn 100 agents, creating combinatorial explosion
MAPL's Solution
Instead of writing rules like:
❌ alice CAN invoke data-agent
❌ data-agent CAN query database
❌ database CAN return financial-records
MAPL uses hierarchical composition:
✅ Company policy: "employees can access company tools"
✅ Finance BU policy: "finance team can access finance:* resources"
✅ Analyst team policy: "analysts limited to read-only, 500 token limits"
✅ Alice's runtime context: {dept: "finance", team: "analysts"}
→ Policies automatically composed at invocation time
The Composition Algebra (Simplified)
When policies compose, they follow simple rules:
Resources (what you can access):
- Parent allows A, B, C
- Child allows B, C, D
- Result: B, C (intersection - both must allow)
Denied Resources (what's explicitly blocked):
- Parent denies X
- Child denies Y
- Result: X, Y (union - either can deny)
Constraints (limits on parameters):
- Parent: max_tokens ≤ 2000
- Child: max_tokens ≤ 500
- Result: max_tokens ≤ 500 (most restrictive wins)
This means:
- ✅ Policies always get MORE restrictive down the hierarchy
- ✅ You can't accidentally grant more permissions by inheriting
- ✅ Denials always propagate (no overrides!)
Part 2: Your First MAPL Policy
The Basic Structure
A MAPL policy is a JSON document with this structure:
{
"policy_id": "unique-identifier",
"extends": "parent-policy-id",
"resources": ["resource-patterns"],
"denied_resources": ["denial-patterns"],
"attestations": ["attestation-requirements"],
"constraints": {
"parameters": {},
"denied_parameters": {},
"attestations": {}
}
}
Your First Policy
Let's create a simple policy for a financial analyst:
{
"policy_id": "user:alice",
"version": "1.0",
"description": "Alice - Financial Analyst",
"resources": [
"llm:openai/chat.completions",
"tool:database/query"
],
"denied_resources": [
"admin:**",
"*.secret"
],
"constraints": {
"parameters": {
"llm:openai/chat.completions": {
"max_tokens": {"max": 500}
}
}
}
}
What this policy does:
- ✅ Alice can use OpenAI chat completions
- ✅ Alice can query the database
- ❌ Alice cannot access admin resources
- ❌ Alice cannot access any
.secretfiles - ⚠️ Alice's OpenAI requests are limited to 500 tokens
Understanding Each Section
policy_id
"policy_id": "user:alice"
- Unique identifier for this policy
- Convention:
{scope}:{name}(e.g.,company:acme,bu:finance,team:analysts,user:alice) - Used for inheritance and reference
version
"version": "1.0"
- Semantic versioning for policy tracking
- Helps with auditing and rollback
description
"description": "Alice - Financial Analyst"
- Human-readable explanation
- Helps other admins understand the policy's purpose
resources
"resources": [
"llm:openai/chat.completions",
"tool:database/query"
]
- Arrays of operation patterns (not resources + actions!)
- In MAPL, resources ARE operations:
llm:openai/chat.completions- the operation of creating a chat completiontool:database/query- the operation of querying the database
- Supports wildcards (more on this below)
denied_resources
"denied_resources": [
"admin:**",
"*.secret"
]
- Explicit blocks that override allows
- Denials always win in conflicts
- Used for sensitive resources that should never be accessed
constraints
"constraints": {
"parameters": {
"llm:openai/chat.completions": {
"max_tokens": {"max": 500}
}
}
}
- Limits on what parameters can be passed to operations
- We'll cover this in detail in Part 4
Resource Patterns and Wildcards
MAPL supports hierarchical resource naming with wildcards:
| Pattern | Meaning | Examples |
|---|---|---|
llm:openai/chat.completions |
Exact match | Only that specific operation |
llm:openai/* |
Single-level wildcard | llm:openai/chat.completions, llm:openai/embeddings |
llm:openai/** |
Multi-level wildcard | All OpenAI LLM operations at any depth |
llm:** |
All LLMs | Any LLM provider, any operation |
** |
Everything | All resources (use with caution!) |
Examples:
{
"resources": [
"llm:openai/*", // All OpenAI operations
"tool:database/query", // Specific operation
"file:data/*/read" // Read any file in data/
]
}
Why Resources ARE Operations
You might wonder: "Where are the actions like read, write, execute?"
In traditional systems:
Resource: /etc/passwd
Actions: [read, write, execute]
In MAPL (agentic/RPC world):
Operations:
- file:/etc/passwd/read ← operation
- file:/etc/passwd/write ← operation
- file:/etc/passwd/execute ← operation
The resource path INCLUDES the operation. This is because:
Tools are callable operations by nature:
llm:openai/chat.completions- the operation IS creating completionstool:database/query- the operation IS querying
Granularity is in the path, not separate actions:
- Instead of:
database:users+ action=read - MAPL uses:
database:users/read(clearer!)
- Instead of:
No combinatorial explosion:
- Traditional: M resources × N actions = M×N rules
- MAPL: Resources encode operations = M patterns
Wildcards group operations naturally:
database:users/*- all user database operationsdatabase:*/read- read from any database table
Part 3: Policy Inheritance with extends
Why Inheritance?
Imagine you have:
- 1 company
- 5 business units
- 20 teams
- 500 users
Without inheritance, you'd need to repeat common rules in all 500 user policies. Inheritance lets you:
✅ Define common rules once (company level)
✅ Specialize at each level (BU, team, user)
✅ Scale from O(M×N) rules to O(log M + N) policies
Your First Inheritance Chain
Let's build a 3-level hierarchy:
Level 1: Company Policy
{
"policy_id": "company:FinTech",
"description": "Company-wide base policy",
"resources": [
"llm:openai/*"
],
"denied_resources": [
"*.secret",
"*.password"
],
"constraints": {
"rate_limit": 100,
"parameters": {
"llm:openai/chat.completions": {
"max_tokens": {"max": 4000}
}
}
}
}
Level 2: Business Unit Policy (extends company)
{
"policy_id": "bu:Analytics",
"extends": "company:FinTech",
"description": "Analytics BU - enforces deterministic results",
"constraints": {
"rate_limit": 50,
"parameters": {
"llm:openai/chat.completions": {
"max_tokens": {"max": 2000},
"temperature": {"max": 0.3}
}
}
}
}
Level 3: User Policy (extends BU)
{
"policy_id": "user:alice",
"extends": "bu:Analytics",
"description": "Alice - Analyst",
"resources": [
"llm:openai/chat.completions"
],
"constraints": {
"rate_limit": 10,
"parameters": {
"llm:openai/chat.completions": {
"model": ["gpt-3.5-turbo"],
"max_tokens": {"max": 500}
}
}
},
"denied_resources": [
"data:executive/*"
]
}
How Inheritance Works: Step-by-Step Resolution
When Alice makes a request, MAPL resolves the policy chain:
company:FinTech → bu:Analytics → user:alice
Step 1: Start with Company Policy
Resources: [llm:openai/*]
Denied: [*.secret, *.password]
max_tokens: 4000
rate_limit: 100
Step 2: Intersect with BU Policy
Resources: [llm:openai/*] (unchanged - BU didn't specify)
Denied: [*.secret, *.password] (unchanged - BU didn't add denials)
max_tokens: min(4000, 2000) = 2000 ← More restrictive!
temperature: 0.3 ← New constraint added
rate_limit: min(100, 50) = 50 ← More restrictive!
Step 3: Intersect with Alice's Policy
Resources: [llm:openai/chat.completions] ← More specific!
(intersection of llm:openai/* and llm:openai/chat.completions)
Denied: [*.secret, *.password, data:executive/*] ← Alice adds executive denial
max_tokens: min(2000, 500) = 500 ← Most restrictive!
temperature: 0.3 (Alice didn't override)
model: [gpt-3.5-turbo] ← Alice adds model restriction
rate_limit: min(50, 10) = 10 ← Most restrictive!
Final Effective Policy for Alice:
{
"resources": ["llm:openai/chat.completions"],
"denied_resources": ["*.secret", "*.password", "data:executive/*"],
"constraints": {
"rate_limit": 10,
"parameters": {
"llm:openai/chat.completions": {
"model": ["gpt-3.5-turbo"],
"max_tokens": {"max": 500},
"temperature": {"max": 0.3}
}
}
}
}
The Intersection Rules
Here's how different fields merge during inheritance:
| Field | Merge Strategy | Example |
|---|---|---|
| resources | Domain-Aware Extend+Restrict | Parent [finance:*, tool:calc] + Child [finance:trading/*] = [finance:trading/*, tool:calc] |
| denied_resources | Union | Parent [X] + Child [Y] = [X,Y] |
| max | Minimum | Parent max:2000 + Child max:500 = max:500 |
| min | Maximum | Parent min:0 + Child min:10 = min:10 |
| range | Intersection | Parent [0,2000] + Child [0,500] = [0,500] |
| allowed_values | Intersection | Parent ["A","B","C"] + Child ["B","C"] = ["B","C"] |
The key principle: Most restrictive always wins!
Note on resources: Resource inheritance uses domain-aware logic (not simple set intersection). If a child doesn't mention a domain (e.g., tool:), it automatically inherits the parent's resources for that domain. If the child specifies patterns in a domain, it restricts the parent's patterns for that domain only. This is what enables O(log M + N) complexity - children don't need to re-specify everything they inherit!
Visual Example: Range Intersection
Company Policy:
max_tokens: [------------ 0 to 4000 ------------]
BU Analytics:
max_tokens: [------ 0 to 2000 ------]
User Alice:
max_tokens: [-- 0 to 500 --]
Final Result:
max_tokens: [-- 0 to 500 --] ← Intersection = most restrictive
Visual Example: Allowed Values Intersection
Company Policy:
model: [gpt-3.5-turbo, gpt-4, gpt-4-turbo]
BU Analytics:
model: [gpt-3.5-turbo, gpt-4] (removes gpt-4-turbo)
User Alice:
model: [gpt-3.5-turbo] (removes gpt-4)
Final Result:
model: [gpt-3.5-turbo] ← Intersection
Advanced: Domain-Aware Resource Inheritance
Why domain-aware inheritance matters: This is one of the core innovations that enables MAPL to achieve O(log M + N) complexity instead of O(M×N). Without it, every child policy would need to re-specify every resource it wants, defeating the purpose of hierarchical composition.
The Algorithm: Extend + Restrict Per Domain
Resource inheritance works differently than simple set intersection. Here's the actual algorithm:
- Extend: Create union of parent and child resources
- Group by domain: Group patterns by domain prefix (
finance:,tool:,llm:, etc.) - Domain-aware restriction: For each parent pattern, find ALL child restrictions in that domain
- Inherit unrestricted domains: If child has no patterns in a domain, inherit parent's patterns as-is
Result: Child inherits what it doesn't mention, restricts what it does mention.
Example 1: Automatic Inheritance Without Re-Specification
This is the key use case - child doesn't need to re-list everything:
// Parent: BU Finance
{
"policy_id": "bu:finance",
"resources": [
"finance:*", // All finance operations
"tool:calculator", // Calculator tool
"tool:analyzer", // Analyzer tool
"report:*" // All reports
]
}
// Child: Trading Team
{
"policy_id": "team:trading",
"extends": "bu:finance",
"resources": [
"finance:trading/*", // Restrict finance domain
"finance:positions/*" // Additional finance restriction
]
// NOTE: Child doesn't mention tool: or report: domains!
}
// Final Effective Policy for team:trading
{
"resources": [
"finance:trading/*", // ← Restricted from finance:*
"finance:positions/*", // ← Additional restriction
"tool:calculator", // ← Inherited (child didn't mention tool: domain)
"tool:analyzer", // ← Inherited (child didn't mention tool: domain)
"report:*" // ← Inherited (child didn't mention report: domain)
]
}
Why this works:
- Child specifies patterns in
finance:domain → restricts parent'sfinance:*to specific sub-patterns - Child doesn't specify anything in
tool:domain → inheritstool:calculatorandtool:analyzerautomatically - Child doesn't specify anything in
report:domain → inheritsreport:*automatically
Without domain-aware inheritance, the child would need to manually list tool:calculator, tool:analyzer, and report:* - defeating the purpose of hierarchical composition!
Service Agent Policies (app: prefix)
So far we've focused on organizational policies (company, BU, team, user). But MAPL also supports service agent policies that define what services themselves allow.
The Dual Perspective Model
MAPL enforces policies from two perspectives:
- Caller Perspective (user:alice): What can Alice request?
- Service Perspective (app:openai-service): What does the OpenAI service allow?
The final effective policy is the intersection of both:
Effective Policy = Caller Policies ∩ Service Policies
This means BOTH must allow an operation for it to succeed.
Why No Overrides?
You might ask: "What if I need to give Alice emergency access?"
MAPL explicitly prohibits overrides. Why?
- Provable security: The theorems (Monotonic Restriction, Transitive Denial) don't hold with overrides
- Audit nightmare: If policies can be overridden, you can't trust the policy hierarchy
- Privilege escalation risk: Overrides create paths to bypass restrictions
Instead, use time-bounded groups:
{
"policy_id": "group:emergency-access",
"validity": {
"not_before": "2025-01-17T09:00:00Z",
"not_after": "2025-01-17T17:00:00Z"
},
"resources": ["admin:**"],
"constraints": {
"audit_level": "maximum",
"require_approval": true
}
}
Add Alice to the emergency group temporarily. When the validity period expires, access is automatically revoked.
Part 4: Parameter Constraints
What Are Parameter Constraints?
Parameter constraints limit the values passed to operations, not just which operations are allowed.
Example scenario:
- ✅ Alice can call
llm:openai/chat.completions - ⚠️ But her
max_tokensmust be ≤ 500 - ⚠️ And she can only use
gpt-3.5-turbo(not gpt-4)
This prevents:
- Cost overruns: Limit token usage
- Security issues: Block dangerous parameter values (injection attacks)
- Compliance violations: Enforce required parameters
Basic Constraint Structure
Parameter constraints are nested under constraints.parameters:
{
"constraints": {
"parameters": {
"operation-pattern": {
"parameter_name": constraint_specification
}
}
}
}
Example:
{
"constraints": {
"parameters": {
"llm:openai/chat.completions": {
"max_tokens": {"max": 500},
"model": ["gpt-3.5-turbo"],
"temperature": {"type": "number", "range": [0.0, 0.8]}
}
}
}
}
Constraint Types
1. Type Validation
Ensure parameters match expected types:
{
"constraints": {
"parameters": {
"llm:openai/chat.completions": {
"max_tokens": {"type": "integer"},
"temperature": {"type": "number"},
"model": {"type": "string"},
"stream": {"type": "boolean"},
"messages": {"type": "array"}
}
}
}
}
Supported types:
integer- Whole numbers (1, 2, 100)number- Integers or floats (1, 1.5, 3.14)string- Text valuesboolean- true or falsearray- Listsobject- Dictionaries
2. Numeric Constraints
Control numeric ranges:
{
"constraints": {
"parameters": {
"llm:openai/chat.completions": {
"max_tokens": {
"type": "integer",
"min": 1,
"max": 4000
},
"temperature": {
"type": "number",
"range": [0.0, 2.0]
}
},
"finance:transfer": {
"amount": {
"type": "number",
"min": 0,
"max": 1000000
}
}
}
}
}
Available numeric constraints:
min- Minimum value (inclusive)max- Maximum value (inclusive)range- Array[min, max](shorthand for min+max)
Note: Use either min/max OR range, not both.
3. String Constraints
Control string values:
{
"constraints": {
"parameters": {
"llm:openai/chat.completions": {
"model": {
"type": "string",
"allowed_values": ["gpt-3.5-turbo", "gpt-4"]
}
},
"report:generate": {
"format": {
"type": "string",
"allowed_values": ["PDF", "XLSX", "CSV"]
},
"time_period": {
"type": "string",
"pattern": "^(Q[1-4]|H[1-2]|FY)\\d{4}$"
}
},
"user:create": {
"username": {
"type": "string",
"min_length": 3,
"max_length": 32,
"pattern": "^[a-zA-Z0-9_]+$"
}
}
}
}
}
Available string constraints:
allowed_values- Array of permitted strings (enumeration)pattern- Regular expression (must match entire string)min_length- Minimum string lengthmax_length- Maximum string length (prevents buffer overflow, prompt injection)
4. Array Constraints
Control array sizes:
{
"constraints": {
"parameters": {
"llm:openai/chat.completions": {
"messages": {
"type": "array",
"min_items": 1,
"max_items": 100
}
},
"database:batch_insert": {
"records": {
"type": "array",
"max_items": 1000
}
}
}
}
}
Available array constraints:
min_items- Minimum array lengthmax_items- Maximum array length (prevents resource exhaustion)
5. Combining Constraints
You can combine multiple constraint types:
{
"constraints": {
"parameters": {
"llm:openai/chat.completions": {
"model": {
"type": "string",
"allowed_values": ["gpt-3.5-turbo", "gpt-4"]
},
"max_tokens": {
"type": "integer",
"min": 1,
"max": 4000
},
"temperature": {
"type": "number",
"range": [0.0, 2.0]
},
"messages": {
"type": "array",
"min_items": 1,
"max_items": 100
}
}
}
}
}
Shorthand Formats
For backward compatibility and convenience, MAPL supports shorthand formats:
List Enumeration (Shorthand for allowed_values)
{
"constraints": {
"parameters": {
"llm:openai/chat.completions": {
"model": ["gpt-3.5-turbo", "gpt-4"]
}
}
}
}
Equivalent to:
{
"model": {
"allowed_values": ["gpt-3.5-turbo", "gpt-4"]
}
}
Denied Parameters (Injection Defense)
The constraints.denied_parameters field lets you block dangerous parameter patterns:
{
"constraints": {
"parameters": {
"llm:openai/chat.completions": {
"model": ["gpt-3.5-turbo", "gpt-4"],
"max_tokens": {"max": 4000}
}
},
"denied_parameters": {
"llm:**": {
"prompt": [
"*DROP TABLE*",
"*rm -rf*",
"*eval(*",
"*exec(*"
]
},
"tool:shell/*": {
"command": [
"*sudo*",
"*rm -*",
"*dd if=*"
]
}
}
}
}
Important: Uses wildcard patterns, NOT regex!
*matches any characters (including spaces,/,:, special chars)**matches everything (same as*for parameter values)- Patterns are case-sensitive:
*DROP TABLE*≠*drop table* - Use wildcards to match substrings:
*DROP TABLE*matches "foo DROP TABLE bar" - For better patterns:
*rm -*(requires dash) instead of*rm *(matches "perform task")
How Constraints Merge During Inheritance
When policies inherit, constraints merge using the "most restrictive wins" rule:
Example: Numeric Constraints
// Company policy
{
"constraints": {
"parameters": {
"llm:openai/chat.completions": {
"max_tokens": {"max": 4000}
}
}
}
}
// BU policy (extends company)
{
"constraints": {
"parameters": {
"llm:openai/chat.completions": {
"max_tokens": {"max": 2000} // More restrictive
}
}
}
}
// User policy (extends BU)
{
"constraints": {
"parameters": {
"llm:openai/chat.completions": {
"max_tokens": {"max": 500} // Even more restrictive
}
}
}
}
// Final result: max_tokens max = 500 (most restrictive)
Part 5: Attestations - Provable Workflow Dependencies
What Are Attestations?
Imagine you're building a financial trading system with an AI agent. The agent needs to:
- Verify the user's identity
- Get manager approval for large trades
- Execute the trade
Traditional approach:
# Agent memory (can be tampered with!)
agent.memory = {
"identity_verified": True, # ← Agent says it did this
"manager_approved": True # ← But can you trust it?
}
if agent.memory["identity_verified"] and agent.memory["manager_approved"]:
execute_trade() # Hope the agent isn't lying!
The problem: You're trusting the agent to honestly report its own state. If the agent is compromised (or just buggy), it can:
- Skip the identity verification step
- Lie about getting manager approval
- Execute unauthorized trades
MAPL attestations solve this through cryptographic proofs:
# Attestation (cryptographically signed proof)
attestation = {
"key": "identity_verified",
"value": {"user_id": "alice@acme.com"},
"set_by": "tool.verify_identity", # ← Who created it
"signature": "a4b8c2...", # ← Cryptographic proof
"timestamp": 1700000000,
"one_time": True # ← Self-enforcing metadata
}
The signature proves:
- ✅ The
verify_identitytool actually executed - ✅ The attestation wasn't forged or tampered with
- ✅ The metadata (one_time, time_to_live) is cryptographically bound
Attestation Types: Internal vs External
MAPL v3 introduces a unified attestation model with two types:
Internal Attestations (Workflow/Self-Set)
Created by tools during workflow execution. The tool itself sets the attestation after successful completion.
{
"attestations": [
"identity_verified",
"mfa_complete"
]
}
Characteristics:
- Set by the tool that performed the operation
- Immediate (no waiting)
- Used for workflow sequencing ("A must happen before B")
External Attestations (Third-Party Approval)
Require approval from an external party (manager, admin, compliance officer).
{
"attestations": [
"trade_approved::{params.amount > 5000}"
],
"constraints": {
"attestations": {
"trade_approved": {
"approval_criteria": "role:manager",
"timeout": 300,
"time_to_live": 3600
}
}
}
}
Characteristics:
- Require
approval_criteriain metadata - Can block execution waiting for approval (
timeout > 0) - Used for human-in-the-loop workflows
The discriminator: If approval_criteria is present, it's an external attestation. If absent, it's internal.
Approval Outcomes:
- Approved: Approver approved with
client.approve_attestation()- requestor proceeds - Denied: Approver rejected with
client.deny_attestation()- requestor fails with reason
approval_criteria Formats:
| Format | Example | Matches |
|---|---|---|
user:xxx |
user:alice |
caller_id == "alice" OR email == "alice" |
user:xxx@domain |
user:alice@fintech.com |
Exact match on caller_id or email |
role:xxx |
role:manager |
Approver has role "manager" from IDP/JWT |
xxx (bare) |
manager |
Fallback: treated as role:manager |
The Blocking Flow
When a request requires an external attestation with timeout > 0, the system blocks and waits for approval:
Request arrives → Policy requires attestation
↓
Check in-memory cache → Not found
↓
Check external storage → Not found
↓
Create pending attestation (for_agent, approval_criteria, timeout)
↓
Poll with backoff [1, 1, 2, 2, 3, 5, 7, 9, 10] seconds
↓
┌─────────────────────────────────────────────┐
│ Outcome: │
│ • approved → Request proceeds │
│ • denied → Request fails with reason │
│ • timeout → Request fails (expired) │
└─────────────────────────────────────────────┘
Key points:
- The
for_agentfield identifies WHO needs the attestation (the requester) - The
approval_criteriafield specifies WHO can approve - Approvers call
client.list_attestations(status='pending')to see requests they can approve - Approvers call
client.approve_attestation()orclient.deny_attestation()to respond
The for_agent Field
When an attestation is created, for_agent identifies who needs it approved:
{
"key": "trade_approved",
"for_agent": "alice@trading-app", // WHO needs this approved
"approval_criteria": "role:manager", // WHO can approve
"invocation_id": "inv-123", // Links to the blocked request
"status": "pending"
}
Why for_agent matters:
Filtering in list_attestations(): When an agent calls
list_attestations(status='pending'), they see:- Attestations where they ARE the
for_agent(attestations they're waiting on) - Attestations where they MATCH the
approval_criteria(attestations they can approve)
- Attestations where they ARE the
Activity Graph correlation: The
invocation_idlinks the attestation to the original request, enabling end-to-end tracing in the Activity Graph.Audit trail: The
for_agentfield is recorded in audit events, showing who requested the attestation and who approved it.
Conditional Attestations (NEW in v3)
The most powerful feature: Attestation requirements that are conditional on runtime parameters.
Basic Syntax
"attestation_key::{condition}"
Examples:
{
"attestations": [
"identity_verified",
"trade_approved::{params.amount > 5000}",
"manager_override::{params.priority == 'urgent' AND params.amount > 10000}"
]
}
What this means:
identity_verified- Always required (no condition)trade_approved::{params.amount > 5000}- Only required when amount exceeds 5000manager_override::{...}- Only required when both conditions are true
Condition Syntax
References:
| Pattern | Description | Example |
|---|---|---|
params.X |
Invocation parameter | params.amount > 5000 |
principal.X |
Authenticated user attribute | principal.user_id == 'admin' |
principal.has_role('X') |
Role check | principal.has_role('manager') |
principal.has_group('X') |
Group check | principal.has_group('trading') |
context.has_attestation('X') |
Existing attestation check | context.has_attestation('mfa') |
Operators:
| Operator | Description | Example |
|---|---|---|
== |
Equals | params.currency == 'USD' |
!= |
Not equals | params.status != 'draft' |
<, <=, >, >= |
Comparison | params.amount > 5000 |
IN |
In list | params.region IN ('us', 'eu') |
Boolean Logic:
| Operator | Description | Example |
|---|---|---|
AND |
Both true | params.a > 10 AND params.b < 5 |
OR |
Either true | params.x == 'a' OR params.x == 'b' |
NOT |
Negation | NOT params.override |
() |
Grouping | (params.a > 10 AND params.b < 5) OR params.urgent |
Complex Conditional Examples
Tiered approval based on amount:
{
"attestations": [
"identity_verified",
"team_lead_approval::{params.amount > 1000 AND params.amount <= 10000}",
"manager_approval::{params.amount > 10000 AND params.amount <= 50000}",
"director_approval::{params.amount > 50000}"
]
}
Multi-factor conditions:
{
"attestations": [
"large_trade::{(params.amount > 25000 AND params.currency == 'USD') OR params.priority == 'urgent'}"
]
}
Role-based conditional:
{
"attestations": [
"extra_approval::{NOT principal.has_role('senior_trader') AND params.amount > 5000}"
]
}
Attestation Metadata
Configure attestation behavior in constraints.attestations:
{
"constraints": {
"attestations": {
"trade_approved": {
"approval_criteria": "role:manager",
"timeout": 300,
"time_to_live": 3600,
"one_time": true,
"max_uses": 1
}
}
}
}
Metadata Fields
| Field | Type | Description |
|---|---|---|
approval_criteria |
string | Who can approve (external attestation). Format: role:X, user:email@domain.com, admin |
timeout |
integer | Seconds to wait for approval. 0 = don't block, > 0 = block and wait |
time_to_live |
integer | Seconds the attestation remains valid after approval |
one_time |
boolean | If true, attestation is consumed after single verification |
max_uses |
integer | Maximum number of times the attestation can be verified (monitor pattern) |
Metadata Semantics
Internal vs External (determined by approval_criteria):
// Internal attestation (no approval_criteria)
{
"identity_verified": {
"one_time": true,
"time_to_live": 300
}
}
// External attestation (has approval_criteria)
{
"trade_approved": {
"approval_criteria": "role:manager",
"timeout": 300,
"time_to_live": 3600,
"one_time": true
}
}
Blocking behavior (determined by timeout):
timeout: 0or absent → Don't block, deny immediately if attestation missingtimeout: 300withapproval_criteria→ Block up to 300 seconds waiting for approval
one_time (Boolean)
logger.attest(key="sudo_verified", one_time=True)
What it does:
- Attestation is consumed after first verification
- Second verification attempt fails (attestation deleted)
Use cases:
- Authentication tokens (one login = one session)
- Approval workflows (one approval = one action)
- One-time operations (password reset, account activation)
Grants: Reusable Approvals (one_time: false)
Set one_time: false to create a grant that can be reused multiple times:
{
"constraints": {
"attestations": {
"trade_approved": {
"approval_criteria": "role:manager",
"one_time": false, // Reusable grant
"time_to_live": 3600 // Valid for 1 hour
}
}
}
}
What it does:
- Manager approves once → Grant is created
- Agent can execute multiple trades for 1 hour without re-approval
- Each use emits
attestation_accessedaudit event - Grant expires when
time_to_liveelapses
Use cases:
- Batch operations (approve once, run many)
- Time-boxed elevated access (trading window)
- Delegated authority (manager grants permission for a period)
time_to_live (Integer, seconds)
logger.attest(key="session_authenticated", time_to_live=300)
What it does:
- Attestation expires after 300 seconds (5 minutes)
- Verifier checks:
current_time > attestation.timestamp + time_to_live
Use cases:
- Session management (5 minute authentication window)
- Time-sensitive operations (trading hours enforcement)
- Temporary escalations (sudo access for 10 minutes)
max_uses (Integer) - Monitor Pattern
logger.attest(key="api_quota", max_uses=10)
What it does:
- Attestation valid for 10 verifications
- Use counter incremented on each verification
- Fails when
use_count >= max_uses
Use cases:
- API rate limiting (10 calls per attestation)
- Quota enforcement (10 file downloads)
- Multi-use but not unlimited (batch operations)
Example:
# Create attestation with max_uses
logger.attest(key="batch_quota", max_uses=3)
# Use 1
verify("batch_quota") # ✅ Works (use_count: 1/3)
# Use 2
verify("batch_quota") # ✅ Works (use_count: 2/3)
# Use 3
verify("batch_quota") # ✅ Works (use_count: 3/3)
# Use 4
verify("batch_quota") # ❌ Fails (use_count: 3/3, exhausted)
The Security Model: ToolAgent Signing + Registry TCB
Why Independent Verification?
In MACAW, each tool is wrapped in a ToolAgent (Policy Enforcement Point). This creates isolation:
User Agent (orchestrator)
↓ invokes
ToolAgent PEP (independent verifier)
↓ enforces policies for
Tool Code (business logic)
Benefit: If a tool is compromised, the blast radius is limited:
- Compromised tool can't access other tools directly
- ToolAgent PEP enforces resource policies
- ToolAgent verifies signatures on invocations
How Verification Works
- ToolAgent Creates Attestations (Not the Tool)
verify_identity tool executes successfully
↓
ToolAgent PEP creates attestation
↓
ToolAgent signs with its own private key
↓
Stores in session context
Why this matters: The tool code never sees the private key. Even if the tool is compromised, it cannot forge attestations.
- Registry as Trusted Computing Base (TCB)
When verifying an attestation:
Attestation says: "Created by tool.verify_identity"
↓
Need public key to verify signature
↓
Look up in GlobalAgentRegistry (TCB)
↓
Registry returns: tool.verify_identity's registered public key
Why this matters:
- Registry is part of the Trusted Computing Base (TCB)
- All ToolAgents register their public keys on startup
- Verifier trusts ONLY keys from the registry
- Attackers can't embed fake public keys in attestation data
Creating Attestations
1. Auto-Attestation (Config-Driven)
For simple "I executed successfully" cases, configure attestations declaratively:
# In your MACAWClient setup:
tool_attestations = {
"verify_identity": {
"key": "identity_verified",
"one_time": True,
"time_to_live": 300,
"scope": "session"
}
}
client = MACAWClient(
app_name="trading-app",
tool_handlers={"verify_identity": verify_identity_handler},
tool_attestations=tool_attestations
)
Use when:
- Simple "this operation completed" semantics
- No custom data needed in attestation value
- Standard metadata sufficient
2. Explicit Attestation (Programmatic)
For cases where you need custom values:
def manager_approval_handler(params):
"""Manager approval tool - creates attestation with custom data"""
logger = params.get('_logger')
trade_id = params.get('trade_id')
amount = params.get('amount')
# Business logic
check_manager_approval(trade_id, amount)
# Create attestation with custom value
logger.attest(
key="trade_approved",
value={
"trade_id": trade_id,
"amount": amount,
"approved_by": get_current_manager()
},
one_time=True
)
return {"status": "approved", "trade_id": trade_id}
Use when:
- Need to embed custom data (trade details, approval info)
- Want to capture who performed the action
- Need application-specific metadata
Enforcing Attestation Requirements in Policies
In your MAPL policy, require attestations before allowing operations:
{
"policy_id": "intent:trading",
"resources": [
"tool:verify_identity",
"tool:approve_trade",
"tool:execute_trade"
],
"attestations": [
"identity_verified",
"trade_approved::{params.amount > 5000}"
],
"constraints": {
"attestations": {
"trade_approved": {
"approval_criteria": "role:manager",
"timeout": 300,
"time_to_live": 3600,
"one_time": true
}
}
}
}
How enforcement works:
- Agent tries to execute trade with amount=10000
- Policy enforcer evaluates attestation requirements:
identity_verified- Always required → Checktrade_approved::{params.amount > 5000}- Condition: 10000 > 5000 = true → Required
- For each required attestation:
- Get attestation from context
- Look up signer's public key from registry (TCB)
- Verify cryptographic signature
- Check metadata (one_time used? time_to_live expired? max_uses exhausted?)
- Decision:
- ✅ All attestations verify → Allow execution
- ❌ Any attestation fails → Deny (or block if timeout > 0 for external)
Real-World Example: Financial Trading Workflow
The Requirement
Scenario: Trading platform where:
- Users can execute trades
- Trades over $5,000 require manager approval
- Identity must be verified before any trade
- Each approval is one-time use (prevents replay)
- Verifications expire after 5 minutes
Step 1: Define the Policy
{
"policy_id": "intent:secure-trading",
"description": "Secure trading with conditional approval",
"resources": [
"tool:verify_identity",
"tool:approve_trade",
"tool:execute_trade"
],
"attestations": [
"identity_verified",
"trade_approved::{params.amount > 5000}"
],
"constraints": {
"attestations": {
"identity_verified": {
"one_time": true,
"time_to_live": 300
},
"trade_approved": {
"approval_criteria": "role:manager",
"timeout": 300,
"time_to_live": 3600,
"one_time": true
}
},
"parameters": {
"tool:execute_trade": {
"amount": {"type": "number", "min": 0, "max": 1000000}
}
}
}
}
Step 2: Configure Auto-Attestation
tool_attestations = {
"verify_identity": {
"key": "identity_verified",
"one_time": True,
"time_to_live": 300
}
}
Step 3: Execute Workflow
# Trade 1: Small trade ($1000) - no manager approval needed
client.invoke_tool("verify_identity", {"user_id": "alice@acme.com"})
result = client.invoke_tool("execute_trade", {"trade_id": "T-001", "amount": 1000})
# ✅ Succeeds - identity_verified present, trade_approved not required (1000 < 5000)
# Trade 2: Large trade ($10000) - manager approval needed
client.invoke_tool("verify_identity", {"user_id": "alice@acme.com"})
result = client.invoke_tool("execute_trade", {"trade_id": "T-002", "amount": 10000})
# ⏳ Blocks waiting for manager approval (up to 300 seconds)
# Manager approves via separate interface
# ✅ Succeeds after approval
# Trade 3: Try to reuse attestations
result = client.invoke_tool("execute_trade", {"trade_id": "T-003", "amount": 500})
# ❌ Fails - identity_verified was consumed (one_time=true)
Step 4: Bob's Approval Code (Separate Terminal)
While Alice's Trade 2 is blocking, Bob runs this in another terminal:
from macaw_agent.client import MACAWClient
# Bob authenticates via Keycloak (has "manager" role)
jwt_token = get_keycloak_token("bob", "Bob@123!")
client = MACAWClient(
user_name="bob",
iam_token=jwt_token,
agent_type="user",
app_name="attestation-approver"
)
client.register()
# List pending attestations Bob can approve (matches his roles)
attestations = client.list_attestations(status="pending")
print(f"Found {len(attestations)} pending attestations")
# Approve Alice's trade request
for att in attestations:
if att['key'] == 'trade_approved':
# Approve
client.approve_attestation(att, reason="Manager approved")
print("Approved!")
# OR Deny
# client.deny_attestation(att, reason="Budget exceeded")
client.unregister()
When Bob approves, Alice's blocked request (in the other terminal) automatically proceeds.
MACAWClient Approval APIs
The SDK provides three methods for working with attestations:
list_attestations(status)
List attestations visible to the current agent:
# List all pending attestations (you can approve OR are waiting on)
pending = client.list_attestations(status="pending")
# List approved attestations
approved = client.list_attestations(status="approved")
# List all attestations (no filter)
all_atts = client.list_attestations()
Returns attestations where:
- You ARE the
for_agent(attestations you're waiting on), OR - You MATCH the
approval_criteria(attestations you can approve)
approve_attestation(attestation, reason)
Approve a pending attestation:
client.approve_attestation(
attestation,
reason="Approved for Q4 trading window"
)
Requirements:
- Attestation must be in
pendingstatus - Your identity must match
approval_criteria(e.g., have "manager" role) - Emits
attestation_approvedaudit event with your signature
deny_attestation(attestation, reason)
Deny a pending attestation:
client.deny_attestation(
attestation,
reason="Budget exceeded for this quarter"
)
Requirements:
- Attestation must be in
pendingstatus - Your identity must match
approval_criteria - Emits
attestation_deniedaudit event - Blocked requester receives denial with your reason
Security Guarantees Achieved:
- ✅ Agent can't skip identity verification
- ✅ Agent can't forge manager approval
- ✅ Agent can't reuse old approvals (one_time=true)
- ✅ Agent can't use expired verifications (time_to_live)
- ✅ Small trades don't need manager approval (conditional)
Comparison to Alternatives
| Approach | Forgeable? | Verifiable? | Temporal Control? | Conditional? | Infrastructure |
|---|---|---|---|---|---|
| Agent Memory | ✅ Yes | ❌ No | ❌ No | ❌ No | None |
| Blockchain | ❌ No | ✅ Yes | ⚠️ Via contract | ⚠️ Complex | 🔴 High |
| OAuth2 Scopes | ❌ No | ✅ Yes | ✅ Yes | ❌ No | 🟡 Medium |
| MAPL Attestations | ❌ No | ✅ Yes | ✅ Yes | ✅ Yes | 🟢 Low |
Part 6: Complete Tutorial Example
Let's build a complete policy system for a financial services company.
The Scenario
FinTech Corp has:
- Company-wide security baseline
- Analytics Business Unit (BU)
- Reporting Team
- Alice (Junior Analyst)
- Bob (Manager)
- Reporting Team
- All users access OpenAI for financial analysis
Requirements:
- Company: Max 4000 tokens, all OpenAI models allowed
- Analytics BU: Max 2000 tokens, low temperature for consistency
- Reporting Team: Max 1000 tokens
- Alice: GPT-3.5 only, 500 tokens max
- Bob: GPT-3.5 and GPT-4, 2000 tokens max
- Large trades (>$5000) require manager approval
Step 1: Company-Wide Base Policy
{
"policy_id": "company:FinTech",
"version": "1.0",
"description": "FinTech Corp company-wide policy",
"resources": [
"llm:openai/*",
"tool:trade/*"
],
"denied_resources": [
"*.secret",
"*.password",
"*.key"
],
"attestations": [
"identity_verified"
],
"constraints": {
"rate_limit": 100,
"parameters": {
"llm:openai/chat.completions": {
"model": ["gpt-3.5-turbo", "gpt-4"],
"max_tokens": {"max": 4000},
"temperature": {"min": 0, "max": 1.0}
}
},
"attestations": {
"identity_verified": {
"one_time": true,
"time_to_live": 3600
}
}
}
}
Step 2: Analytics BU Policy
{
"policy_id": "bu:Analytics",
"version": "1.0",
"extends": "company:FinTech",
"description": "Analytics BU - deterministic results",
"attestations": [
"trade_approved::{params.amount > 5000}"
],
"constraints": {
"rate_limit": 50,
"parameters": {
"llm:openai/chat.completions": {
"max_tokens": {"max": 2000},
"temperature": {"max": 0.3},
"seed": "required"
}
},
"attestations": {
"trade_approved": {
"approval_criteria": "role:manager",
"timeout": 300,
"time_to_live": 3600,
"one_time": true
}
}
}
}
Step 3: Reporting Team Policy
{
"policy_id": "team:Reporting",
"version": "1.0",
"extends": "bu:Analytics",
"description": "Reporting team under Analytics BU",
"constraints": {
"rate_limit": 30,
"parameters": {
"llm:openai/chat.completions": {
"max_tokens": {"max": 1000}
}
}
}
}
Step 4: Alice's Policy (Junior Analyst)
{
"policy_id": "user:alice",
"version": "1.0",
"extends": "team:Reporting",
"description": "Alice - Junior Financial Analyst",
"resources": [
"llm:openai/chat.completions"
],
"constraints": {
"rate_limit": 10,
"parameters": {
"llm:openai/chat.completions": {
"model": ["gpt-3.5-turbo"],
"max_tokens": {"max": 500},
"temperature": {"max": 0.5}
}
}
},
"denied_resources": [
"data:executive/*",
"data:confidential/*"
]
}
Step 5: Bob's Policy (Manager)
{
"policy_id": "user:bob",
"version": "1.0",
"extends": "team:Reporting",
"description": "Bob - Finance Manager",
"constraints": {
"rate_limit": 30,
"parameters": {
"llm:openai/chat.completions": {
"model": ["gpt-3.5-turbo", "gpt-4"],
"max_tokens": {"max": 2000},
"temperature": {"max": 0.8}
}
}
}
}
Final Effective Policies
Alice's Final Policy:
{
"resources": ["llm:openai/chat.completions"],
"denied_resources": ["*.secret", "*.password", "*.key", "data:executive/*", "data:confidential/*"],
"attestations": ["identity_verified", "trade_approved::{params.amount > 5000}"],
"constraints": {
"rate_limit": 10,
"parameters": {
"llm:openai/chat.completions": {
"model": ["gpt-3.5-turbo"],
"max_tokens": {"max": 500},
"temperature": {"max": 0.3},
"seed": "required"
}
}
}
}
Bob's Final Policy:
{
"resources": ["llm:openai/*", "tool:trade/*"],
"denied_resources": ["*.secret", "*.password", "*.key"],
"attestations": ["identity_verified", "trade_approved::{params.amount > 5000}"],
"constraints": {
"rate_limit": 30,
"parameters": {
"llm:openai/chat.completions": {
"model": ["gpt-3.5-turbo", "gpt-4"],
"max_tokens": {"max": 1000},
"temperature": {"max": 0.3},
"seed": "required"
}
}
}
}
Testing the Policies
Alice tries to execute trades:
# Test 1: Alice, small trade ($1000)
client.invoke_tool("execute_trade", {"trade_id": "T-001", "amount": 1000})
# Result: ✅ SUCCESS (identity_verified required, trade_approved NOT required)
# Test 2: Alice, large trade ($10000)
client.invoke_tool("execute_trade", {"trade_id": "T-002", "amount": 10000})
# Result: ⏳ BLOCKS waiting for manager approval
# After Bob approves: ✅ SUCCESS
Alice tries to call OpenAI:
# Test 1: Alice, gpt-3.5-turbo, 400 tokens
request = {"model": "gpt-3.5-turbo", "max_tokens": 400}
# Result: ✅ SUCCESS
# Test 2: Alice, gpt-3.5-turbo, 600 tokens
request = {"model": "gpt-3.5-turbo", "max_tokens": 600}
# Result: ❌ BLOCKED - "max_tokens=600 exceeds maximum: 500"
# Test 3: Alice, gpt-4, 400 tokens
request = {"model": "gpt-4", "max_tokens": 400}
# Result: ❌ BLOCKED - "model=gpt-4 not in allowed values"
Part 7: Best Practices
1. Start with Least Privilege
Begin with restrictive base policies, then expand as needed:
{
"policy_id": "company:default-deny",
"resources": [],
"denied_resources": ["**"]
}
Then grant specific access through inheritance.
2. Use Clear Policy IDs
Follow a consistent naming convention:
company:{name}- Top-level company policybu:{name}- Business unit policiesteam:{name}- Team policiesuser:{username}- User-specific policies
3. Document Your Policies
Always include meaningful descriptions:
{
"policy_id": "team:analysts",
"description": "Financial analysts: GPT-3.5 only, 500 token limit, read-only finance data",
"version": "1.2.0"
}
4. Version Your Policies
Use semantic versioning:
1.0.0- Initial policy1.1.0- Added new constraint (backward compatible)2.0.0- Breaking change (stricter limits)
5. Use Wildcards Wisely
{
"resources": [
"llm:openai/chat.completions", // ✅ Specific first
"llm:openai/text-embedding-*", // ✅ Targeted wildcard
"tool:database/query" // ✅ Specific operation
],
"denied_resources": [
"llm:openai/gpt-4*", // ✅ Block expensive models
"admin:**" // ✅ Block all admin operations
]
}
Avoid overly broad wildcards like ** unless necessary.
6. Leverage Inheritance for DRY
Don't repeat constraints across policies:
// ✅ GOOD: Define once in company policy, inherit everywhere
// company.json
{
"constraints": {
"parameters": {
"llm:**": {"max_tokens": {"max": 4000}}
}
}
}
// team-analysts.json (only override what changes)
{
"extends": "company:acme",
"constraints": {
"parameters": {
"llm:**": {"max_tokens": {"max": 500}}
}
}
}
7. Use Conditional Attestations for Tiered Approval
{
"attestations": [
"team_lead_approval::{params.amount > 1000 AND params.amount <= 10000}",
"manager_approval::{params.amount > 10000 AND params.amount <= 50000}",
"director_approval::{params.amount > 50000}"
]
}
8. Set Appropriate Timeouts for External Attestations
{
"constraints": {
"attestations": {
"urgent_approval": {
"approval_criteria": "role:manager",
"timeout": 60, // Short timeout for urgent
"time_to_live": 300
},
"standard_approval": {
"approval_criteria": "role:manager",
"timeout": 3600, // 1 hour for standard
"time_to_live": 86400
}
}
}
}
9. Use one_time for Security-Critical Attestations
{
"constraints": {
"attestations": {
"trade_approved": {
"one_time": true // Prevents replay attacks
}
}
}
}
10. Monitor and Audit
Enable audit logging in your policies:
{
"constraints": {
"audit_enabled": true,
"audit_level": "detailed"
}
}
Review audit logs regularly for:
- Unexpected denials
- Policy violations
- Usage patterns
- Attestation failures
Part 8: Troubleshooting
Common Issue 1: Access Denied Unexpectedly
Symptom: Request is denied but should be allowed.
Debugging steps:
Check inheritance chain: Verify which policies are being merged
python -m macaw_agent.policy_cli listCheck denied_resources: Denials override allows
{ "resources": ["llm:openai/*"], "denied_resources": ["llm:openai/gpt-4"] // ← This blocks gpt-4! }Check wildcards: Ensure patterns match intended resources
llm:openai/*matchesllm:openai/chat.completions- But NOT
llm:openai/v1/chat.completions(too deep!) - Use
llm:openai/**for multi-level
Check parameter constraints: Even if resource is allowed, constraints can cause denial
Enable debug logging:
export MACAW_LOG_LEVEL=DEBUG python your_app.py
Common Issue 2: Attestation Verification Failing
Symptom: "Missing or invalid attestation" error but attestation was created.
Debugging steps:
Check if attestation exists in context:
print(context.attestations.keys())Check conditional attestation evaluation:
- Is the condition evaluating to true?
- Check parameter values:
params.amount > 5000- is amount actually > 5000?
Enable attestation debug logging:
import logging logging.getLogger("macaw.protocol.mcp_types").setLevel(logging.DEBUG)Check metadata expiration:
# time_to_live expired? if "expires_at" in attestation: import time if time.time() > attestation["expires_at"]: print("Attestation expired!") # one_time already used? if attestation.get("one_time") and "use_count" in attestation: print("Attestation already consumed!") # max_uses exhausted? if attestation.get("max_uses") and attestation.get("use_count", 0) >= attestation["max_uses"]: print("Attestation max_uses exhausted!")Verify ToolAgent registered:
- Check if ToolAgent is in registry
- Is the ToolAgent's public key in the registry?
Common Issue 3: External Attestation Not Blocking
Symptom: Request denied immediately instead of waiting for approval.
Debugging steps:
Check timeout value:
{ "attestations": { "trade_approved": { "timeout": 300 // Must be > 0 to block } } }Check approval_criteria is present:
- External attestations require
approval_criteria - Without it, it's treated as internal attestation (no blocking)
- External attestations require
Check condition evaluation:
- Is the conditional attestation actually required?
- Verify:
trade_approved::{params.amount > 5000}- is amount > 5000?
Common Issue 4: Conditional Attestation Not Triggering
Symptom: Attestation should be required but isn't.
Debugging steps:
Check condition syntax:
// ❌ Wrong operator "trade_approved::{params.amount => 5000}" // Should be >= // ✅ Correct "trade_approved::{params.amount >= 5000}"Check parameter names:
// ❌ Wrong parameter name "trade_approved::{params.total > 5000}" // Should be 'amount' // ✅ Correct "trade_approved::{params.amount > 5000}"Check boolean logic:
// Both must be true "approval::{params.a > 10 AND params.b < 5}" // Either can be true "approval::{params.a > 10 OR params.b < 5}"
Common Issue 5: External Attestation Approval Rejected
Symptom: "Not authorized: caller does not match criteria" when approving.
Debugging steps:
Check approver's roles match approval_criteria:
# approval_criteria: "role:manager" # Bob's JWT must have "manager" in realm_access.roles claims = decode_jwt(bob_token) print(claims['realm_access']['roles']) # Should include 'manager'Check approval_criteria format:
// For exact user match "approval_criteria": "user:bob@acme.com" // For role match (most common) "approval_criteria": "role:manager" // Bare name (treated as role) "approval_criteria": "manager"Ensure approver is registered:
- Bob must call
client.register()before approving - Bob's roles are extracted from JWT claims and stored in registry
- Check:
metadata.principal.rolesin registry entry
- Bob must call
Verify approver can see the attestation:
# Bob should see attestations matching his approval_criteria attestations = client.list_attestations(status="pending") if not attestations: print("No pending attestations visible - check roles")
Getting Help
If you encounter issues:
- Check audit logs for policy evaluation traces
- Enable DEBUG logging for detailed policy resolution
- Use
python -m macaw_agent.policy_cli validateto test policies - Review this guide's examples and best practices
Appendix A: Quick Reference
Complete Policy Template
{
"policy_id": "{scope}:{name}",
"version": "1.0",
"extends": "parent-policy-id",
"description": "Human-readable description",
"resources": [
"operation-pattern",
"llm:openai/*",
"tool:database/query"
],
"denied_resources": [
"admin:**",
"*.secret"
],
"attestations": [
"always_required",
"conditional::{params.amount > threshold}"
],
"constraints": {
"rate_limit": 100,
"max_requests": 1000,
"timeout": 30000,
"parameters": {
"operation-pattern": {
"param_name": {
"type": "integer|number|string|boolean|array|object",
"min": 0,
"max": 1000,
"range": [0, 1000],
"allowed_values": ["value1", "value2"],
"pattern": "^regex$",
"min_length": 1,
"max_length": 100,
"min_items": 1,
"max_items": 100
}
}
},
"denied_parameters": {
"operation-pattern": {
"param_name": ["*dangerous*", "*DROP TABLE*"]
}
},
"attestations": {
"attestation_key": {
"approval_criteria": "role:manager",
"timeout": 300,
"time_to_live": 3600,
"one_time": true,
"max_uses": 1
}
}
}
}
Attestation Formats
Simple (always required):
"attestations": ["identity_verified", "mfa_complete"]
Conditional (required when condition is true):
"attestations": [
"trade_approved::{params.amount > 5000}",
"manager_override::{params.priority == 'urgent' AND params.amount > 10000}"
]
Condition Syntax Reference
| Pattern | Example |
|---|---|
| Parameter comparison | params.amount > 5000 |
| String equality | params.currency == 'USD' |
| In list | params.region IN ('us', 'eu') |
| Role check | principal.has_role('manager') |
| Group check | principal.has_group('trading') |
| Attestation check | context.has_attestation('mfa') |
| Boolean AND | params.a > 10 AND params.b < 5 |
| Boolean OR | params.x == 'a' OR params.x == 'b' |
| Negation | NOT params.override |
| Grouping | (params.a AND params.b) OR params.c |
Attestation Metadata Fields
| Field | Type | Description |
|---|---|---|
approval_criteria |
string | Who can approve (makes it external attestation) |
timeout |
integer | Seconds to wait (0 = don't block, >0 = block) |
time_to_live |
integer | Validity duration after approval (seconds) |
one_time |
boolean | Consume after single use |
max_uses |
integer | Maximum verification count |
Attestation Status Lifecycle
| Status | Description |
|---|---|
pending |
Awaiting approval (external) or set (internal) |
approved |
Approved by authorized party |
denied |
Rejected by authorized party |
expired |
Time window (TTL) elapsed |
approved + alive=false |
Consumed: one-time attestation used (status remains approved, but inactive) |
approval_criteria Format
| Format | Example | Matches |
|---|---|---|
user:xxx |
user:alice |
caller_id or email == "alice" |
user:xxx@domain |
user:alice@fintech.com |
Exact match on caller_id or email |
role:xxx |
role:manager |
Approver has role from IDP/JWT |
xxx (bare) |
manager |
Treated as role:manager |
Attestation Audit Events
| Event | When | Key Fields |
|---|---|---|
attestation_created |
Pending request created | key, for_agent, approval_criteria |
attestation_approved |
Approver granted | approved_by, reason, signature |
attestation_denied |
Approver rejected | denied_by, reason |
attestation_accessed |
Grant reused (one_time=false) | invocation_id, accessed_by |
attestation_consumed |
One-time used up | consumed_at |
attestation_expired |
Timeout/TTL exceeded | expires_at, reason |
All Constraint Types
| Constraint | Applies To | Format | Example |
|---|---|---|---|
type |
All | String | {"type": "integer"} |
min |
Numbers | Number | {"min": 0} |
max |
Numbers | Number | {"max": 4000} |
range |
Numbers | [min, max] |
{"range": [0, 100]} |
allowed_values |
Strings | Array | {"allowed_values": ["A", "B"]} |
pattern |
Strings | Regex | {"pattern": "^[a-z]+$"} |
min_length |
Strings | Integer | {"min_length": 3} |
max_length |
Strings | Integer | {"max_length": 255} |
min_items |
Arrays | Integer | {"min_items": 1} |
max_items |
Arrays | Integer | {"max_items": 100} |
Inheritance Merge Rules
| Constraint | Merge Strategy | Example |
|---|---|---|
resources |
Domain-Aware | Child restricts within domains it mentions |
denied_resources |
Union | Both enforced |
attestations |
Union | All required |
range |
Intersection | Parent [0,100] + Child [10,50] = [10,50] |
min |
Maximum | Parent min:0 + Child min:10 = min:10 |
max |
Minimum | Parent max:100 + Child max:50 = max:50 |
allowed_values |
Intersection | Parent ["A","B","C"] + Child ["B","C","D"] = ["B","C"] |
denied_parameters |
Union | Both patterns enforced |
Appendix B: Theoretical Foundations
Composition Algebra
A policy P = (R, D, A, C) consists of:
- R: Allowed resource patterns
- D: Denied resource patterns
- A: Required attestations (simple and conditional)
- C: Operational constraints (parameters, attestation metadata)
Policy Intersection P1 ∩ P2 = (R', D', A', C') where:
- R' = R1 ∩ R2 (allow only if both policies permit)
- D' = D1 ∪ D2 (deny if either policy forbids)
- A' = A1 ∪ A2 (all attestations required)
- C' = MostRestrictive(C1, C2) (apply tightest constraint)
Formal Security Properties
Theorem 1 (Monotonic Restriction): For composition P0 ∩ ... ∩ Pn:
∀i, j : (i < j) ⇒ π(P0 ∩ ... ∩ Pj) ⊆ π(P0 ∩ ... ∩ Pi)
Proof Sketch: By construction, Pi+1 = Pi ∩ Pnext. Resource intersection: Ri+1 ⊆ Ri. Denial union: Di+1 ⊇ Di. Therefore π(Pi+1) ⊆ π(Pi). By induction, π(Pj) ⊆ π(Pi) for i < j. □
Theorem 2 (Transitive Denial): If resource r is denied by any policy, it remains denied:
∃i: r ∈ Match(Di) ⇒ r ∈ Match(Deff)
Proof Sketch: Deff = D0 ∪ D1 ∪ ... ∪ Dn. If r ∈ Di for any i, then r ∈ Deff by set union. □
Theorem 3 (No Privilege Escalation): If base policy P0 denies r, no composition grants access:
(r ∈ Match(D0)) ⇒ r ∉ Allowed(Peff)
Proof: By Theorem 2, if r ∈ Match(D0), then r ∈ Match(Deff). By definition of π, denied resources cannot be allowed. □
Attestation Security Properties
Theorem 4 (Unforgeable Attestations): An adversary without access to a ToolAgent's private key cannot create a valid attestation that verifies under that ToolAgent's public key.
Proof Sketch: Follows from the security of the signature scheme (RSA/Ed25519). □
Theorem 5 (Metadata Binding): An adversary cannot modify attestation metadata (time_to_live, one_time, max_uses) without invalidating the signature.
Proof Sketch: Metadata is included in the canonical representation signed by the ToolAgent. Any modification changes the signed data, causing signature verification to fail. □
Theorem 6 (Conditional Attestation Soundness): A conditional attestation key::{condition} is required if and only if the condition evaluates to true at runtime.
Proof Sketch: The condition evaluator has access to the complete invocation context (params, principal, existing attestations). Evaluation is deterministic and side-effect-free. □
Security Implications
These theorems provide formal guarantees:
- Theorem 1 prevents privilege expansion
- Theorem 2 ensures any layer's denial is absolute
- Theorem 3 prevents escalation
- Theorems 4-6 ensure attestations are cryptographically verifiable and unforgeable
- Theorem 6 ensures conditional attestations are correctly enforced
Appendix C: Future Enhancements
The following features are planned for future releases of MAPL:
Phase 2 Enhancements
Enhanced Error Messages with Policy Context:
// Current error:
"Parameter 'max_tokens'=600 exceeds maximum: 500"
// Future error:
"Parameter 'max_tokens'=600 exceeds maximum: 500
Policy Chain: company:acme → bu:finance → team:analysts → user:alice
Constraint Source: team:analysts.json (line 15)
Resolved Path: 4000 → 2000 → 500 (most restrictive)"
Performance Optimizations:
- Condition evaluation caching
- Policy resolution caching
- Optimized inheritance chain evaluation
Phase 3 Enhancements (Advanced)
Array Item Validation:
{
"constraints": {
"parameters": {
"database:batch_insert": {
"records": {
"type": "array",
"max_items": 1000,
"items": {
"type": "object",
"required": ["id", "name"]
}
}
}
}
}
}
Nested Object Validation:
{
"constraints": {
"parameters": {
"user:create": {
"profile": {
"type": "object",
"properties": {
"email": {"pattern": "^[^@]+@[^@]+$"},
"age": {"min": 18, "max": 120}
}
}
}
}
}
}
Conclusion
Congratulations! You now know how to write MAPL policies for AI agent systems.
Key Takeaways:
- MAPL scales from O(M×N) to O(log M + N) through hierarchical composition
- Resources ARE operations (no separate actions needed)
- Inheritance composes via intersection (most restrictive always wins)
- Denials propagate (no overrides, provably secure)
- Parameter constraints provide fine-grained control
- Attestations enable provable workflow dependencies
- Conditional attestations allow dynamic, context-aware requirements
- External attestations support human-in-the-loop workflows
- Deferred principal binding enables dynamic identity resolution
Next Steps:
- Write policies for your organization
- Test with the policy CLI
- Deploy and monitor
- Iterate based on usage patterns
Need Help?
- Review the examples in this guide
- Check the troubleshooting section
- Enable debug logging
- Consult the quick reference
Happy policy writing!
Document Version: 3.0.1
MACAW SDK Version: 3.x
Last Updated: January 27, 2026
Copyright MACAW Security. All rights reserved.