MAPL Policy Guide
A Tutorial for the MACAW Agentic Policy Language
Version: 3.2.0
Last Updated: September 14, 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: Object Policies
- Part 7: Complete Tutorial Example
- Part 8: Best Practices
- Part 9: 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"] |
| max_length, max_items | Minimum | Parent max_length:100 + Child max_length:10 = max_length:10 |
| min_length, min_items | Maximum | Parent min_items:1 + Child min_items:3 = min_items:3 |
| type, pattern | Both apply | Parent pattern:"^[a-z]+$" + Child pattern:"^.{1,8}$" = a value must match both |
| List shorthand | Both apply | Parent ["gpt-4", "gpt-3.5-turbo"] + Child ["gpt-4"] = only gpt-4 passes |
| validity | Overlap | Parent January–December + Child June–September = June–September |
The key principle: Most restrictive always wins! The same rules apply when policies from different levels (company, team, user, service) combine at request time.
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!
The :base Fallback
What happens when a policy doesn't exist? MAPL loads {namespace}:base instead.
Ask for tool:custom-scanner when no such policy exists, and MAPL loads tool:base. The same applies to extends — a missing parent falls back to its namespace base.
// Requested: data:finance/transactions (no policy exists)
// Loaded: data:base (the namespace default)
Why this matters: you write one policy per namespace instead of one per object.
{
"policy_id": "data:base",
"version": "1.0",
"description": "Default rules for every dataset",
"resources": ["tool:reporting/*"],
"denied_resources": ["tool:*/export", "tool:*/send"]
}
That single policy covers every data: object you haven't written a policy for. Write per-object policies only for the exceptions.
The fallback is one level deep, to the namespace. data:finance/transactions falls back to data:base — not to data:finance. Path segments are not a hierarchy; they're part of the name.
If you want a middle tier, declare it:
{
"policy_id": "data:finance/transactions",
"extends": "data:finance"
}
Create a :base for every namespace you use. Without it, a missing policy has nothing to fall back to. This matters most for object policies, where a missing policy denies the call — see Part 6.
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": {
"rate_limit": 10
}
}
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
List Shorthand
A list is a shorthand allowlist:
{
"constraints": {
"parameters": {
"llm:openai/chat.completions": {
"model": ["gpt-3.5-turbo", "gpt-4*"]
}
}
}
}
Each entry is a wildcard pattern matched against the whole value, so gpt-4* allows gpt-4 and gpt-4o. Use allowed_values for exact matches.
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=*"
]
}
}
}
}
Patterns use wildcards:
*matches any characters, including spaces,/,:, and line breaks**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")
Combining Patterns
Wrap a condition in {} when one pattern is too broad on its own:
{
"constraints": {
"denied_parameters": {
"tool:shell/*": {
"command": [
"{params.command MATCHES '*curl*' AND params.command MATCHES '*| sh*'}"
]
}
}
}
}
This denies curl https://example.com/install.sh | sh and allows curl https://example.com. The expression uses the condition syntax and may reference any parameter of the call.
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)
The same rules apply when policies from different levels combine at request time. See The Intersection Rules for every constraint.
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")
Built-in: phishing_resistant — MACAW ships one internal attestation that isn't tool-set: it's minted by the control plane at login from the token's amr. Add "phishing_resistant" to attestations to require a phishing-resistant login (passkey / WebAuthn / hardware key). No approval_criteria, no approver; reusable for the session.
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 |
| Comma-separated | role:manager,role:compliance |
Approver meets every listed criterion |
When two policies define the same attestation with different criteria, the approver must meet all of them.
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 |
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') |
MATCHES |
Glob pattern matching | params.command MATCHES '*npm install*' |
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.currency == 'USD' |
() |
Grouping | (params.a > 10 AND params.b < 5) OR params.urgent == true |
Precedence: NOT binds tightest, then AND, then OR. Use parentheses to group: NOT (params.a > 10 OR params.b > 10).
Boolean parameters are compared explicitly: params.urgent == true.
Conditions fail closed: a malformed condition requires the attestation. See Common Issue 5.
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'}"
]
}
Pattern matching for command strings:
{
"attestations": [
"tier2_session::{params.command MATCHES '*npm install*'}",
"tier3_operation::{params.command MATCHES '*rm -rf*' OR params.command MATCHES '*curl*'}",
"tier4_critical::{params.command MATCHES '*sudo*'}"
]
}
Pattern syntax uses glob-style wildcards:
*matches any characters*npm*matches any string containing "npm"npm*matches strings starting with "npm"*lodashmatches strings ending with "lodash"
Different requirements for different callers:
Conditions read the call's parameters. The caller's identity decides which policies load (deferred principal binding), so put a caller-specific requirement in that caller's policy:
{
"policy_id": "team:junior-traders",
"attestations": [
"extra_approval::{params.amount > 5000}"
]
}
Senior traders' policies leave it out.
Attestation Keys That Are Policy IDs
An attestation key with a namespace prefix names another policy to load, not a claim to verify:
"attestations": [
"identity_verified", // a claim: must be presented and signed
"data:customer_master" // a reference: loads that policy
]
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,
"priority": 100
}
}
}
}
Metadata Fields
| Field | Type | Description |
|---|---|---|
approval_criteria |
string | Who can approve (external attestation). Format: role:X, user:email@domain.com, admin; comma-separate criteria that must all be met |
timeout |
integer | Seconds to wait for approval. 0 or omitted = don't block, > 0 = block and wait |
time_to_live |
integer | Seconds the approval remains valid. 0 or omitted = no expiry |
one_time |
boolean | If true (the default), the approval is consumed after a single use |
priority |
integer | Processing order when multiple attestations match. Higher values processed first (default: 0) |
These fields apply to approvals (external attestations). A tool sets the limits for internal attestations when it creates them (see Creating Attestations).
Metadata Semantics
Internal vs External (determined by approval_criteria):
- Internal attestation (no
approval_criteria): the tool that creates it sets its lifetime and use limits. - External attestation (has
approval_criteria): the policy's metadata governs the approval request.
// External attestation
{
"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", ttl=300)
What it does:
- Attestation expires after 300 seconds (5 minutes)
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
max_uses is set when a tool creates an internal attestation:
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)
When Several Policies Define the Same Attestation
Attestation requirements accumulate: if any applicable policy requires an attestation, it is required.
When two policies both define metadata for the same attestation, each field keeps the stricter value:
| Field | Combined value |
|---|---|
timeout |
The shorter wait |
time_to_live |
The shorter duration (0 means no expiry) |
one_time |
true if either policy sets it |
approval_criteria |
Every criterion from both policies |
priority |
The higher value |
Fields a policy leaves out take the other policy's value or the default. Set a field to enforce it.
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:
client = MACAWClient(
app_name="trading-app",
tools={
"verify_identity": {
"handler": verify_identity_handler,
"attestation": {
"key": "identity_verified",
"one_time": True,
"ttl": 300,
"scope": "session"
}
}
}
)
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": {
"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
tools = {
"verify_identity": {
"handler": verify_identity_handler,
"attestation": {
"key": "identity_verified",
"one_time": True,
"ttl": 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: Object Policies
Attaching a Policy to a Thing
Every policy so far attaches to a caller — a company, a team, a user, a service. That works because callers invoke things, and invoking is what triggers policy resolution.
Some things are never invoked. A table, a file, a document, a model — these get used. A tool reads them. Nothing invokes them, so nothing loads their policy.
Object policies attach the rules to the thing itself. MAPL enforces them whenever a tool consumes it:
{
"policy_id": "data:employee_comp",
"version": "1.0",
"description": "Compensation data",
"resources": ["tool:reporting/query"],
"denied_resources": ["tool:*/export", "tool:*/send"],
"attestations": ["pii_training_current"]
}
Read it as: only reporting/query may touch me, never an export or messaging tool, and only from someone with current PII training.
The object doesn't care which team or user is calling. Whoever consumes it is bound by it.
Loading an Object Policy
Name the object in attestations. A key with a namespace prefix is a policy reference, not a claim:
{
"policy_id": "tool:reporting/query",
"resources": ["tool:reporting/query"],
"attestations": ["data:employee_comp"]
}
MAPL loads data:employee_comp and merges it like any other layer.
Dynamic References
Hardcoding one object per policy doesn't scale. Build the policy ID from the invocation instead:
{
"policy_id": "app:reporting",
"resources": ["tool:reporting/*"],
"attestations": ["data:{params.dataset}"]
}
A call with dataset = "employee_comp" loads data:employee_comp. A call with dataset = "finance/transactions" loads data:finance/transactions — or data:base if that object has no policy of its own.
One line covers every object in your system.
List parameters expand to one reference each:
// params: { "datasets": ["transactions", "employee_comp"] }
// loads: data:transactions AND data:employee_comp
The Two Rules
1. Fail closed. A reference must resolve — to its own policy, or to {namespace}:base. If neither exists, the call is denied. Naming an object declares that it has rules, so a missing policy is a configuration error, not permission.
2. Narrowing only. Object policies merge by intersection like every other layer. They can restrict; they can never grant. This is the same Monotonic Restriction property that governs organizational policies, so you can generate thousands of object policies with no risk of privilege escalation.
What Can Be Substituted
Only invocation parameters, using {params.name}:
| Situation | Result |
|---|---|
| Parameter present | Substituted into the policy ID |
| Parameter absent | Reference doesn't apply — nothing loaded, no denial |
Value contains a character outside A–Z a–z 0–9 _ . / -, or contains .. |
Denied — a value may only name an object inside the namespace |
finance/Q1-2026 resolves; hr:comp and ../hr are denied.
"Parameter absent" is what lets you put one reference at company level and have it apply only to the tools that have that parameter.
Binding Objects Per Tool Family
Use a tool pattern in constraints to give tool families different object defaults:
{
"policy_id": "company:acme",
"resources": ["tool:**"],
"constraints": {
"tool:reporting/*": { "attestations": ["data:{params.dataset}"] },
"tool:docs/*": { "attestations": ["doc:{params.document}"] }
}
}
Chaining: Derived Objects
An object policy can reference other objects. This is how you handle a summary, view, or export whose underlying sources never appear in the call:
{
"policy_id": "data:quarterly_summary",
"version": "1.0",
"description": "Derived from transactions and employee_comp",
"resources": ["**"],
"attestations": ["data:transactions", "data:employee_comp"]
}
It sets no restrictions of its own — ["**"] restricts nothing. Its sources carry the rules.
A call against quarterly_summary loads the derived object's policy, which loads both sources. Every restriction on either source applies.
Chains follow to any depth. Cycles terminate safely, and each policy loads once.
Worked Example: Three Objects, Any Number of Callers
The floor — applies to every object without a policy of its own:
{
"policy_id": "data:base",
"version": "1.0",
"description": "Default rules for every dataset",
"resources": ["**"],
"denied_resources": ["tool:*/export", "tool:*/send"]
}
A classified object with one extra restriction:
{
"policy_id": "data:transactions",
"version": "1.0",
"extends": "data:base",
"description": "Transaction records",
"resources": ["tool:reporting/*"],
"denied_resources": ["tool:*/publish"]
}
A sensitive object that requires proof:
{
"policy_id": "data:employee_comp",
"version": "1.0",
"extends": "data:base",
"description": "Compensation data - PII",
"resources": ["tool:reporting/query"],
"attestations": ["pii_training_current"]
}
A derived object built from the other two:
{
"policy_id": "data:quarterly_summary",
"version": "1.0",
"description": "Derived from transactions and employee_comp",
"resources": ["**"],
"attestations": ["data:transactions", "data:employee_comp"]
}
Wire it to the tools once:
{
"policy_id": "app:reporting",
"version": "1.0",
"description": "Reporting tools",
"resources": ["tool:reporting/*"],
"attestations": ["data:{params.dataset}"]
}
Now:
| Alice calls | Loads | Result |
|---|---|---|
reporting/query(dataset="transactions") |
data:transactions → data:base |
✅ Allowed |
reporting/export(dataset="transactions") |
same | ❌ Denied by tool:*/export, inherited from data:base |
reporting/publish(dataset="transactions") |
same | ❌ Denied by tool:*/publish, the object's own rule |
reporting/query(dataset="employee_comp") |
data:employee_comp → data:base |
⏳ Blocks for pii_training_current |
reporting/query(dataset="quarterly_summary") |
derived → data:transactions + data:employee_comp |
⏳ Blocks, and carries both objects' rules |
reporting/query(dataset="anything_new") |
data:base (fallback) |
✅ Allowed under the floor |
Two rows carry the lesson.
Row 5 is composition. Alice never named employee_comp, but querying a summary built on it inherits its PII requirement and its resource restriction:
query(dataset="quarterly_summary")
resources: ["tool:reporting/query"] ← from employee_comp
denied: [tool:*/export, tool:*/publish, tool:*/send] ← publish from transactions
attestations: ["pii_training_current"] ← from employee_comp
Row 6 is the floor. An object nobody has classified still gets data:base. Without data:base, that call would be denied instead.
Limitations
- Conditions on references are rejected.
data:employee_comp::{params.mode == 'write'}is a validation error. Put the condition insidedata:employee_compinstead. - Only parameters substitute.
{principal.x}is not supported. - Namespaces are lowercase.
Data:xis not recognized as a reference. - Objects must be namable before the call. An object that appears only in a result cannot be referenced. Use a verifier for those.
Part 7: 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}
}
}
}
}
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}
}
},
"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": {"min": 0, "max": 0.3}
}
}
}
}
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": {"min": 0, "max": 0.3}
}
}
}
}
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 8: 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
Audit logging is configured on the MACAW service. Review audit logs regularly for:
- Unexpected denials
- Policy violations
- Usage patterns
- Attestation failures
11. Create a :base for Every Namespace You Reference
Object references fail closed. Without data:base, a reference to an unclassified object denies the call.
{
"policy_id": "data:base",
"version": "1.0",
"description": "Floor for every dataset",
"resources": ["**"],
"denied_resources": ["tool:*/export"]
}
Make the base restrictive. It is what protects the objects nobody has classified yet.
12. Put Object Rules on the Object
If a rule is about a dataset, write it in that dataset's policy — not as a parameter constraint on every tool that might touch it. One object policy applies to every tool, including tools added later.
// Prefer this - one policy, applies everywhere
{ "policy_id": "data:employee_comp", "denied_resources": ["tool:*/export"] }
// Over this - repeated per tool, and misses the next tool you add
{ "constraints": { "denied_parameters": { "tool:reporting/export": { "dataset": ["employee_comp"] } } } }
Part 9: 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 other policies that define the same attestation:
- When several policies define it, the shortest
timeoutapplies
- When several policies define it, the shortest
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.
Cause: The condition evaluated to false. A comparison on a parameter the call does not send is false, so a misspelled parameter name means the requirement never applies.
Debugging steps:
Check parameter names against what the tool sends:
// ❌ 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}" // NOT binds tightest; group with parentheses when negating more "approval::{NOT (params.a > 10 OR params.b < 5)}"
Common Issue 5: Attestation Required on Every Call
Symptom: A conditional attestation, or a {...} denial pattern, applies to every call regardless of parameter values.
Cause: The condition is malformed, and MAPL fails closed: it requires the attestation, or denies the call for a denial pattern.
Debugging steps:
Check the logs: the message names the condition that could not be evaluated.
Check for these common mistakes:
// ❌ Unknown operator "trade_approved::{params.amount => 5000}" // Use >= // ❌ Parameter used without a comparison "override_ok::{params.urgent}" // Use params.urgent == true // ❌ Unbalanced parentheses "approval::{(params.a > 10 AND params.b < 5}"
Common Issue 6: 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")
Common Issue 7: Object Reference Denies Everything
Symptom: Every call is denied with "not in allowed resources", and the policy chain shows an object layer.
Cause: A reference didn't resolve. The referenced policy doesn't exist, and neither does its {namespace}:base. Object references fail closed, so an unresolvable reference denies the call.
Diagnosis: look for the resolver warning naming the id it couldn't load.
[RESOLVER] Object reference data:employee_comp did not resolve: Policy not found
Fix: create the object's policy, or create the namespace base:
{
"policy_id": "data:base",
"version": "1.0",
"resources": ["**"],
"denied_resources": ["tool:*/export"]
}
Common Issue 8: Dynamic Reference Never Fires
Symptom: "attestations": ["data:{params.dataset}"] appears to do nothing. No object policy is loaded, and no denial occurs.
Cause: the parameter isn't present on the invocation. A reference whose parameter is absent doesn't apply — that's deliberate, so one reference can sit at company level across tools that don't all share the parameter.
Fix: check the actual parameter name the tool sends. If the tool calls it table, the reference must be data:{params.table}.
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",
"validity": {
"not_before": "2026-01-01T00:00:00Z",
"not_after": "2026-12-31T23:59:59Z"
},
"resources": [
"operation-pattern",
"llm:openai/*",
"tool:database/query"
],
"denied_resources": [
"admin:**",
"*.secret"
],
"attestations": [
"always_required",
"conditional::{params.amount > threshold}",
"data:some_object",
"data:{params.dataset}"
],
"constraints": {
"rate_limit": 100,
"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,
"priority": 100
}
}
}
}
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}"
]
Policy reference (loads another policy):
"attestations": [
"data:some_object", // static
"data:{params.dataset}" // built from the invocation
]
A key with a lowercase namespace prefix names a policy, not a claim. Conditions are not allowed on a reference. See Part 6.
Namespace Fallback
| Requested | Falls back to |
|---|---|
data:finance/transactions (missing) |
data:base |
extends: "team:missing" |
team:base |
data:base (missing) |
nothing — a reference to it denies the call |
The fallback is one level deep, to the namespace. Path segments are not a hierarchy.
Condition Syntax Reference
| Pattern | Example |
|---|---|
| Parameter comparison | params.amount > 5000 |
| String equality | params.currency == 'USD' |
| In list | params.region IN ('us', 'eu') |
| Glob pattern match | params.command MATCHES '*rm -rf*' |
| Boolean parameter | params.urgent == true |
| 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.currency == 'USD' |
| Grouping | (params.a > 10 OR params.b > 10) AND params.urgent == true |
NOT binds tightest, then AND, then OR.
Attestation Metadata Fields
| Field | Type | Description |
|---|---|---|
approval_criteria |
string | Who can approve (makes it external attestation); comma-separate criteria that must all be met |
timeout |
integer | Seconds to wait (0 or omitted = don't block, >0 = block) |
time_to_live |
integer | Validity duration after approval, in seconds (0 or omitted = no expiry) |
one_time |
boolean | Consume after single use (default: true) |
priority |
integer | Processing order (higher = first, default: 0) |
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 |
| Comma-separated | role:manager,role:compliance |
Approver meets every listed criterion |
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 |
All | Array | {"allowed_values": ["A", "B"]}, {"allowed_values": [false]} |
pattern |
Strings | Regex matching the whole value | {"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, min_length, min_items |
Maximum | Parent min:0 + Child min:10 = min:10 |
max, max_length, max_items |
Minimum | Parent max:100 + Child max:50 = max:50 |
allowed_values |
Intersection | Parent ["A","B","C"] + Child ["B","C","D"] = ["B","C"] |
type, pattern, list shorthand |
Both apply | A value must satisfy both |
denied_parameters |
Union | Both patterns enforced |
validity |
Overlap | Later not_before, earlier not_after |
Attestation timeout |
Minimum | Parent 300 + Child 60 = 60 |
Attestation time_to_live |
Shorter positive duration | Parent 0 (no expiry) + Child 300 = 300 |
Attestation one_time |
true if either sets it |
Parent false + Child true = true |
Attestation approval_criteria |
All criteria | role:manager + role:compliance = both required |
Attestation priority |
Maximum | Parent 0 + Child 100 = 100 (higher wins) |
These rules apply to extends and to policies combining at request time.
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 reads the invocation parameters and 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.2.0
MACAW SDK Version: 3.x
Last Updated: September 14, 2026
Copyright MACAW Security. All rights reserved.