Advanced Feature: This demo extends the previous two demos with delegation-as-data — the main agent can explicitly grant a subset of its permissions to a subagent, and Cedar policies enforce those narrower boundaries.
Start with the basics first! Complete the reactive authorization demo and query constraints demo before exploring this.
The previous demos treat the main agent as a single principal:
- Reactive (Demo 1): Agent tries, gets denied, adapts
- Proactive (Demo 2): Agent queries constraints before acting
This demo adds a third dimension — delegation:
Main Agent → Create delegation (read-only, /tmp/*) → Spawn SubAgent → SubAgent acts within bounds
A subagent spawned with "only read files in /tmp" cannot escape that constraint even if it tries.
- Main agent calls
delegate_authorizationto create a delegation record specifying what the subagent can do (allowed actions, path/command patterns, optional TTL) - Main agent spawns subagent via
sessions_spawn - SubAgent attempts tool calls — the PEP intercepts each one:
- Looks up the delegation record by subagent session key
- Checks TTL expiry (PEP-enforced — Cedar has no
now()) - Checks if the action is in the delegation's allowed list
- Injects delegation context into the Cedar PDP request
- Uses
SubAgentprincipal type instead ofAgent
- Cedar PDP evaluates delegation-aware policies that check
context.isDelegated,context.delegatedActions, path/command constraints
The main agent is told via system prompt to call delegate_authorization before spawning a subagent. This is guidance, not a hard gate — sessions_spawn does not require a delegation record to exist.
This is safe because the enforcement is on the subagent side, not the spawn side. If the main agent forgets (or chooses not) to delegate:
- The subagent spawns normally
- The subagent attempts a tool call
- The PEP finds no delegation record for that session key
- The request is denied immediately (policy
delegation-4-deny-undelegated-subagents) - The subagent (and main agent) learn reactively from the denial
In other words, the worst case for a missing delegation is that the subagent can't do anything — not that it can do too much. The default is deny-all for subagents without delegations, which is the secure default.
A production system could hard-wire delegation into sessions_spawn — refuse to spawn without a delegation record, or auto-generate one. We deliberately chose prompt guidance for this demo for three reasons:
-
It demonstrates the authorization pattern, not a product feature. The point of this demo is to show how Cedar policies can enforce delegation boundaries. Coupling delegation to the spawn API would obscure that — it would look like an application-level feature rather than a policy-level pattern. Keeping them separate makes the authorization layer visible and understandable.
-
It mirrors how the other demos work. The reactive demo (demo 1) doesn't prevent the agent from attempting denied operations — it lets it try and learn. The proactive demo (demo 2) adds prompt guidance to query constraints first. This demo follows the same progression: prompt guidance to delegate first, with policy enforcement as the backstop. The consistency makes the three demos a coherent series.
-
The agent choosing to delegate is part of the story. In a real multi-agent system, the main agent decides what to delegate based on the task at hand. A read-only subagent for data analysis gets different permissions than a git-only subagent for code review. That decision is inherently an LLM reasoning step, not something you'd want to hard-code. The prompt guidance gives the agent the tool and the nudge; the policy layer ensures the boundaries hold regardless.
Cedar has no built-in clock or now() function. Time-based expiry (TTL) cannot be expressed as a Cedar policy condition. Instead:
- The delegation record stores an
expiresAttimestamp - The PEP checks this before calling Cedar
- Expired delegations are rejected immediately — the PDP never sees the request
- This keeps the trusted code boundary small (PEP only) while Cedar handles the structural authorization
- Completed previous demos — reactive and query constraints
- Cedar PDP server with delegation policies loaded (automatic when
policies-delegation.cedarexists)
python3 demo/cedar-pdp-server.pyThe server auto-detects policies-delegation.cedar and loads it alongside the base policies. You should see:
Delegation: policies/cedar/policies-delegation.cedar
python3 demo/test-delegation.pyExpected output:
======================================================================
Cedar Delegation-as-Data Authorization Tests
======================================================================
Test: SubAgent delegated read: allow Read /tmp/data.txt
Decision: Allow (expected Allow)
Policies: delegation-1-allow-delegated-actions, policy-1-allow-readonly
✓ PASS
Test: SubAgent delegated read: deny Write (not in delegatedActions)
Decision: Deny (expected Deny)
Policies: delegation-4-deny-undelegated-subagents
✓ PASS
Test: SubAgent without delegation: deny Read
Decision: Deny (expected Deny)
Policies: delegation-4-deny-undelegated-subagents
✓ PASS
Test: Main Agent: allow Read (unaffected by delegation policies)
Decision: Allow (expected Allow)
Policies: policy-1-allow-readonly
✓ PASS
...
======================================================================
Results: 10 passed, 0 failed
======================================================================
The Jupyter notebook walks through each delegation scenario interactively:
jupyter notebook demo/delegation-demo.ipynbThe notebook simulates the PEP's behavior by constructing the same Cedar authorization requests the PEP would build — with and without delegation context — and shows the results step by step. It covers:
- Main agent with full access (baseline)
- SubAgent without delegation (denied everything)
- Creating a read-only
/tmp/*delegation - SubAgent operating within and outside delegation scope
- Git-only delegation pattern
- PEP-enforced expiry (simulated — Cedar can't check time)
Testing the full delegation flow end-to-end requires the OpenClaw gateway running with sessions_spawn actually creating subagent sessions. That's a much heavier setup than this demo repo provides.
The notebook and test script demonstrate the same Cedar policy evaluation that happens in production — they construct the exact authorization requests the PEP sends to the PDP. The only thing they don't exercise is the in-process PEP logic (delegation store lookup, expiry check, context injection), which is straightforward TypeScript that the notebook explains and simulates.
For a full end-to-end test with a live agent:
pnpm openclaw agent --agent main --message \
"Spawn a subagent that can only read files in /tmp. \
Delegate read-only access, then have it list what's there."This requires the full OpenClaw runtime (gateway, agent runner, PDP server all running).
python3 demo/test-pdp.py
python3 demo/test-query-constraints.pyAll existing tests should pass unchanged.
Delegation policies live in policies/cedar/policies-delegation.cedar (separate from base policies):
| Policy ID | Type | Purpose |
|---|---|---|
delegation-1-allow-delegated-actions |
permit | Allow SubAgent actions that are in delegatedActions set |
delegation-2-enforce-path-constraint |
forbid | Deny file ops outside delegatedPathPattern |
delegation-3-enforce-command-constraint |
forbid | Deny bash/exec outside delegatedCommandPattern |
delegation-4-deny-undelegated-subagents |
forbid | Deny all SubAgent actions without a valid delegation |
delegation-5-deny-out-of-scope-actions |
forbid | Deny SubAgent actions not listed in delegatedActions (overrides base permits) |
These policies only match principal is OpenClaw::SubAgent — main Agent principals are unaffected.
delegate_authorization({
subagentSessionKey: "agent:main:subagent:reader-1",
allowedActions: ["read", "glob", "grep"],
pathPattern: "/tmp/*"
})
- Can: Read, Glob, Grep files under
/tmp/ - Cannot: Write, Edit, Bash, or read outside
/tmp/
delegate_authorization({
subagentSessionKey: "agent:main:subagent:git-worker",
allowedActions: ["bash"],
commandPattern: "git *"
})
- Can: Run
git status,git log,git diff, etc. - Cannot: Run
rm -rf,curl | sh, or any non-git command
delegate_authorization({
subagentSessionKey: "agent:main:subagent:temp-1",
allowedActions: ["read", "write"],
pathPattern: "/tmp/*",
ttlSeconds: 300 // 5 minutes
})
- Can: Read/write under
/tmp/for 5 minutes - After 5 minutes: PEP rejects immediately (no PDP call)
| Aspect | No Delegation | Delegation-as-Data |
|---|---|---|
| SubAgent permissions | Same as main agent | Explicitly scoped subset |
| Escape boundaries | SubAgent can do anything agent can | SubAgent is confined to delegation |
| Time limits | None | PEP-enforced TTL |
| Path restrictions | Global policies only | Per-delegation path patterns |
| Command restrictions | Global policies only | Per-delegation command patterns |
| Audit trail | Agent principal only | Distinct SubAgent principal + delegation ID |
New file — in-memory store keyed by subagent session key:
export type DelegationRecord = {
id: string;
delegatorSessionKey: string;
subagentSessionKey: string;
allowedActions: string[];
pathPattern?: string;
commandPattern?: string;
expiresAt?: number; // enforced by PEP — Cedar has no now()
createdAt: number;
};
export function createDelegation(record: Omit<DelegationRecord, "id" | "createdAt">): DelegationRecord
export function getDelegation(subagentSessionKey: string): DelegationRecord | undefined
export function isDelegationExpired(record: DelegationRecord): booleanKey responsibilities:
- Stores delegation records created by
delegate_authorization - Provides lookup by subagent session key (used by the PEP on every tool call)
- Expiry check is here — not in Cedar — because Cedar has no
now()function
New file — tool the main agent calls before spawning a subagent:
{
name: "delegate_authorization",
description: "Create a delegation record granting a subagent specific permissions. Use before sessions_spawn to scope what the subagent can do.",
parameters: {
subagentSessionKey: string, // session key of the subagent to be spawned
allowedActions: string[], // e.g. ["read", "glob", "grep"]
pathPattern?: string, // e.g. "/tmp/*"
commandPattern?: string, // e.g. "git *"
ttlSeconds?: number // optional expiry
}
}Tool output:
{
"delegationId": "a3f1c2d4-...",
"subagentSessionKey": "agent:main:subagent:reader-1",
"allowedActions": ["read", "glob", "grep"],
"pathPattern": "/tmp/*",
"commandPattern": null,
"expiresAt": "2024-01-15T12:05:00.000Z",
"message": "Delegation created. The subagent can now perform the specified actions."
}Modified to add delegation checks before each Cedar PDP call for subagents:
if (isSubAgent) {
const record = getDelegation(sessionKey);
// No delegation record → deny immediately (no PDP call)
if (!record) {
return { blocked: true, reason: "SubAgent has no delegation record — access denied" };
}
// Expired → deny immediately (PEP-enforced, Cedar has no now())
if (isDelegationExpired(record)) {
return { blocked: true, reason: "Delegation has expired — access denied" };
}
// Action not in delegation scope → deny immediately
if (!record.allowedActions.includes(toolName)) {
return { blocked: true, reason: `Action "${toolName}" is not in delegation scope` };
}
// Inject delegation context for Cedar policy evaluation
delegation = {
isDelegated: true,
delegatedActions: record.allowedActions,
delegatedPathPattern: record.pathPattern,
delegatedCommandPattern: record.commandPattern,
};
}Key points:
- Three fast-path denials run before Cedar is called (no delegation, expired, out-of-scope action)
- Surviving requests inject delegation context so Cedar policies can enforce path/command patterns
- Uses
SubAgentprincipal type instead ofAgentfor Cedar evaluation
New file — five policies that only match principal is OpenClaw::SubAgent:
// Allow delegated actions that are in scope
@id("delegation-1-allow-delegated-actions")
permit(principal is OpenClaw::SubAgent, action, resource)
when { context has isDelegated && context.isDelegated && ... };
// Deny file ops outside the delegated path pattern
@id("delegation-2-enforce-path-constraint")
forbid(principal is OpenClaw::SubAgent, action, resource)
when { context has delegatedPathPattern && !(context.filePath like context.delegatedPathPattern) };
// Deny bash/exec outside the delegated command pattern
@id("delegation-3-enforce-command-constraint")
forbid(principal is OpenClaw::SubAgent, action, resource)
when { context has delegatedCommandPattern && !(context.command like context.delegatedCommandPattern) };
// Deny all SubAgent actions with no valid delegation (catch-all)
@id("delegation-4-deny-undelegated-subagents")
forbid(principal is OpenClaw::SubAgent, action, resource)
when { !(context has isDelegated) || !context.isDelegated };
// Deny SubAgent actions not listed in delegatedActions (overrides base permits)
@id("delegation-5-deny-out-of-scope-actions")
forbid(principal is OpenClaw::SubAgent, action, resource)
when { ... };
Main Agent principals are completely unaffected by these policies.
Modified to add the SubAgent entity type and delegation context attributes:
entity SubAgent;
context {
isDelegated?: Bool,
delegatedActions?: Set<String>,
delegatedPathPattern?: String,
delegatedCommandPattern?: String,
// ... existing attributes
}
- Basic Authorization Demo — reactive authorization
- Query Constraints Demo — proactive TPE queries
- Cedar Policy Language — official docs
- Cedar Entity Types — entity hierarchy (
inkeyword)
