Research/Guide
Guide

Complete tutorial for the MACAW Agentic Policy Language

MAPL Policy Guide

A Tutorial for the MACAW Agentic Policy Language

Version: 3.0.1
Last Updated: January 27, 2026


Table of Contents

  1. Introduction
  2. Part 1: Understanding MAPL
  3. Part 2: Your First MAPL Policy
  4. Part 3: Policy Inheritance with extends
  5. Part 4: Parameter Constraints
  6. Part 5: Attestations
  7. Part 6: Complete Tutorial Example
  8. Part 7: Best Practices
  9. Part 8: Troubleshooting
  10. Appendix A: Quick Reference
  11. Appendix B: Theoretical Foundations
  12. 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

  1. 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.

  2. 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.

  3. 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 .secret files
  • ⚠️ 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 completion
    • tool: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:

  1. Tools are callable operations by nature:

    • llm:openai/chat.completions - the operation IS creating completions
    • tool:database/query - the operation IS querying
  2. Granularity is in the path, not separate actions:

    • Instead of: database:users + action=read
    • MAPL uses: database:users/read (clearer!)
  3. No combinatorial explosion:

    • Traditional: M resources × N actions = M×N rules
    • MAPL: Resources encode operations = M patterns
  4. Wildcards group operations naturally:

    • database:users/* - all user database operations
    • database:*/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:

  1. Extend: Create union of parent and child resources
  2. Group by domain: Group patterns by domain prefix (finance:, tool:, llm:, etc.)
  3. Domain-aware restriction: For each parent pattern, find ALL child restrictions in that domain
  4. 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's finance:* to specific sub-patterns
  • Child doesn't specify anything in tool: domain → inherits tool:calculator and tool:analyzer automatically
  • Child doesn't specify anything in report: domain → inherits report:* 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:

  1. Caller Perspective (user:alice): What can Alice request?
  2. 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?

  1. Provable security: The theorems (Monotonic Restriction, Transitive Denial) don't hold with overrides
  2. Audit nightmare: If policies can be overridden, you can't trust the policy hierarchy
  3. 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_tokens must 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 values
  • boolean - true or false
  • array - Lists
  • object - 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 length
  • max_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 length
  • max_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:

  1. Verify the user's identity
  2. Get manager approval for large trades
  3. 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_identity tool 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_criteria in 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_agent field identifies WHO needs the attestation (the requester)
  • The approval_criteria field specifies WHO can approve
  • Approvers call client.list_attestations(status='pending') to see requests they can approve
  • Approvers call client.approve_attestation() or client.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:

  1. 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)
  2. Activity Graph correlation: The invocation_id links the attestation to the original request, enabling end-to-end tracing in the Activity Graph.

  3. Audit trail: The for_agent field 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 5000
  • manager_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: 0 or absent → Don't block, deny immediately if attestation missing
  • timeout: 300 with approval_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_accessed audit event
  • Grant expires when time_to_live elapses

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

  1. 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.

  1. 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:

  1. Agent tries to execute trade with amount=10000
  2. Policy enforcer evaluates attestation requirements:
    • identity_verified - Always required → Check
    • trade_approved::{params.amount > 5000} - Condition: 10000 > 5000 = true → Required
  3. 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?)
  4. 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 pending status
  • Your identity must match approval_criteria (e.g., have "manager" role)
  • Emits attestation_approved audit 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 pending status
  • Your identity must match approval_criteria
  • Emits attestation_denied audit 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)
  • 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 policy
  • bu:{name} - Business unit policies
  • team:{name} - Team policies
  • user:{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 policy
  • 1.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:

  1. Check inheritance chain: Verify which policies are being merged

    python -m macaw_agent.policy_cli list
    
  2. Check denied_resources: Denials override allows

    {
      "resources": ["llm:openai/*"],
      "denied_resources": ["llm:openai/gpt-4"]  // ← This blocks gpt-4!
    }
    
  3. Check wildcards: Ensure patterns match intended resources

    • llm:openai/* matches llm:openai/chat.completions
    • But NOT llm:openai/v1/chat.completions (too deep!)
    • Use llm:openai/** for multi-level
  4. Check parameter constraints: Even if resource is allowed, constraints can cause denial

  5. 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:

  1. Check if attestation exists in context:

    print(context.attestations.keys())
    
  2. Check conditional attestation evaluation:

    • Is the condition evaluating to true?
    • Check parameter values: params.amount > 5000 - is amount actually > 5000?
  3. Enable attestation debug logging:

    import logging
    logging.getLogger("macaw.protocol.mcp_types").setLevel(logging.DEBUG)
    
  4. 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!")
    
  5. 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:

  1. Check timeout value:

    {
      "attestations": {
        "trade_approved": {
          "timeout": 300  // Must be > 0 to block
        }
      }
    }
    
  2. Check approval_criteria is present:

    • External attestations require approval_criteria
    • Without it, it's treated as internal attestation (no blocking)
  3. 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:

  1. Check condition syntax:

    // ❌ Wrong operator
    "trade_approved::{params.amount => 5000}"  // Should be >=
    
    // ✅ Correct
    "trade_approved::{params.amount >= 5000}"
    
  2. Check parameter names:

    // ❌ Wrong parameter name
    "trade_approved::{params.total > 5000}"  // Should be 'amount'
    
    // ✅ Correct
    "trade_approved::{params.amount > 5000}"
    
  3. 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:

  1. 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'
    
  2. 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"
    
  3. 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.roles in registry entry
  4. 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:

  1. Check audit logs for policy evaluation traces
  2. Enable DEBUG logging for detailed policy resolution
  3. Use python -m macaw_agent.policy_cli validate to test policies
  4. 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:

  1. MAPL scales from O(M×N) to O(log M + N) through hierarchical composition
  2. Resources ARE operations (no separate actions needed)
  3. Inheritance composes via intersection (most restrictive always wins)
  4. Denials propagate (no overrides, provably secure)
  5. Parameter constraints provide fine-grained control
  6. Attestations enable provable workflow dependencies
  7. Conditional attestations allow dynamic, context-aware requirements
  8. External attestations support human-in-the-loop workflows
  9. Deferred principal binding enables dynamic identity resolution

Next Steps:

  1. Write policies for your organization
  2. Test with the policy CLI
  3. Deploy and monitor
  4. 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.

Ready to implement agentic access control?

Start with the Developer tier - free forever with full security features.