Agent Policy Engine (InitGuard)
InitRunner uses InitGuard as an embedded agent-as-principal policy engine. Agents get their own identity derived from role metadata, and the engine governs what tools an agent can use and which agents it can delegate to — across all execution paths (CLI, flow, daemon, API, pipeline).
The engine runs in-process with no sidecar container, no network round-trips, and sub-millisecond policy evaluation. Policy decisions use deny-wins semantics — if any deny rule matches, the request is denied regardless of allow rules. Every evaluation returns a structured Decision with a human-readable reason and optional advice.
Agent policy enforcement is strictly opt-in. When INITRUNNER_POLICY_DIR is not set (the default), all tool calls and delegation requests are allowed.
Quick Start
# Point to your policy directory
export INITRUNNER_POLICY_DIR=./policies
# Run an agent — policies now enforce tool and delegation rules
initrunner run my-agent.yaml "do something"Configuration
| Variable | Default | Description |
|---|---|---|
INITRUNNER_POLICY_DIR | (unset) | Path to policy YAML directory. If unset, policy enforcement is disabled. |
INITRUNNER_AGENT_CHECKS | true | Enable per-agent tool and delegation checks. |
When INITRUNNER_POLICY_DIR is set, policies must load successfully or the first run fails (fail-fast). There is no allow-all fallback when the operator has explicitly opted into policy enforcement.
Policy Format
All policy documents use apiVersion: initguard/v1 and one of three kind values:
Schema
Optional lint-time validation of attribute names and types across your policy set. Defines expected attributes for principals and resources, plus valid actions per resource kind.
DerivedRoles
Conditional role elevation using CEL expressions. Derived roles let you define computed roles like trusted_agent or same_team based on principal attributes.
when— CEL expression that must be true for the role to applyunless— optional CEL expression that vetoes the role when true
ResourcePolicy
Allow/deny rules for a specific resource kind (e.g., tool or agent).
effect: alloworeffect: denyrolesorderivedRoles— which principals the rule applies to (one required)when/unless— optional CEL conditionsadvice— optional human-readable message included in deny decisionsimportDerivedRoles— explicitly scopes which derived role sets are available to this policy (prevents privilege leakage across policy domains)
Deny-wins: if any deny rule matches, the result is denied regardless of allow rules.
How Agent Principals Are Constructed
Every agent run constructs a principal from role.yaml metadata:
# role.yaml
metadata:
name: code-reviewer
team: platform
author: alice
tags: [trusted, code]
version: "1.0"Produces a principal:
| Field | Value |
|---|---|
| ID | agent:code-reviewer |
| Roles | ["agent", "team:platform"] |
| Attributes | {team: "platform", author: "alice", tags: ["trusted", "code"], version: "1.0"} |
The team:<name> role is only added when metadata.team is set. The tags attribute is a native list (not CSV), which allows CEL expressions like request.principal.attr.tags.exists(t, t == "trusted").
CEL Activation Structure
Every CEL expression in a policy accesses the same activation structure:
request
├── principal
│ ├── id "agent:code-reviewer"
│ ├── roles ["agent", "team:platform"]
│ └── attr {team: "platform", tags: ["trusted", "code"], ...}
└── resource
├── kind "tool"
├── id "run_command"
└── attr {tool_type: "shell", agent: "code-reviewer", ...}Missing attributes in expressions evaluate to false (not an error), which is a safe default for cross-resource policies.
Agent Principal Scoping
The agent principal is set per-run via a ContextVar in the executor:
- CLI/daemon:
_enter_agent_context(role)is called at the top ofexecute_run()/execute_run_stream()/execute_run_async()/execute_run_stream_async(), and reset infinally. - Flow: Each agent's run goes through the executor, so the principal is automatically scoped.
- Pipeline: Inline steps go through the executor. MCP steps construct a lightweight
Metadatafrom the step name.
The PolicyEngine instance is loaded once per process (immutable, thread-safe). Only the agent principal ContextVar changes per run.
Delegation Policy
Delegation policy checks happen at two levels:
Inline Delegation (full metadata)
When an agent delegates to another agent via InlineInvoker, the target role is loaded first. The policy check uses full metadata from both source and target:
- Source principal: constructed from the delegating agent's
role.metadata - Resource:
kind=agent,id=<target_name>,attrs={team, author, tags} - Action:
delegate
MCP Remote Delegation (name-only)
When an agent delegates to a remote agent via McpInvoker, only the target agent's name is known (no role YAML to load). The policy check uses:
- Source principal: constructed from the delegating agent's
role.metadata - Resource:
kind=agent,id=<target_name>,attrs={}(empty) - Action:
delegate
This is an explicit limitation: remote delegation policy can only match on the target name, not on team/tags/author.
Flow Delegation
DelegateSink routes agent output between flow agents. The policy check uses role metadata (from loaded role YAML), not the flow agent key. This matters when flow agent keys differ from role names (e.g., flow agent code-reviewer vs role name reviewer).
Agent Tool Policy
The PolicyToolset wraps every toolset and checks whether the current agent principal is allowed to execute a given tool:
- Principal: from
get_current_agent_principal()ContextVar - Resource:
kind=tool,id=<tool_function_name>,attrs={tool_type, agent, callable, instance} - Action:
execute
When agent_checks is disabled or no agent principal is set, the check is a no-op (allow-all).
Policy denials return a Decision with reason and optional advice, which are surfaced in the tool's permission-denied message.
Note: fnmatch
PermissionToolset(local per-role YAML rules) still coexists with InitGuard — fnmatch evaluates first, short-circuiting before the policy engine check. See Security — Tool Permissions for the fnmatch reference.
Example Policies
The following policies are shipped in examples/policies/agent/.
Schema (schema.yaml)
Defines expected attributes for principals and resources. Used for lint validation at load time.
apiVersion: initguard/v1
kind: Schema
principals:
agent:
attrs:
team: string
author: string
tags: list
version: string
resources:
tool:
attrs:
tool_type: string
agent: string
callable: string
instance: string
actions: [execute]
agent:
attrs:
team: string
author: string
tags: list
actions: [delegate]Derived Roles (derived_roles.yaml)
apiVersion: initguard/v1
kind: DerivedRoles
name: agent_derived_roles
definitions:
# Agents tagged "trusted" get elevated privileges
- name: trusted_agent
parentRoles: ["agent"]
when: request.principal.attr.tags.exists(t, t == "trusted")
# Agents on the same team as the target resource
- name: same_team
parentRoles: ["agent"]
when: request.principal.attr.team != ""
unless: request.principal.attr.team != request.resource.attr.teamDelegation Policy (delegation_policy.yaml)
apiVersion: initguard/v1
kind: ResourcePolicy
resource: agent
importDerivedRoles: [agent_derived_roles]
rules:
# Trusted agents can delegate to anyone
- actions: ["delegate"]
effect: allow
derivedRoles: ["trusted_agent"]
# Same-team agents can delegate to each other
- actions: ["delegate"]
effect: allow
derivedRoles: ["same_team"]
# Non-trusted agents cannot delegate to privileged agents
- actions: ["delegate"]
effect: deny
roles: ["agent"]
when: request.resource.attr.tags.exists(t, t == "privileged")
advice: "Delegation to privileged agents requires the 'trusted' tag."Tool Policy (tool_policy.yaml)
apiVersion: initguard/v1
kind: ResourcePolicy
resource: tool
importDerivedRoles: [agent_derived_roles]
rules:
# All agents can execute safe tool types
- actions: ["execute"]
effect: allow
roles: ["agent"]
when: >-
request.resource.attr.tool_type in
["datetime", "search", "web_reader", "http", "retrieval",
"memory_store", "delegate", "api", "web_scraper"]
# Trusted agents get all tools (including shell/python)
- actions: ["execute"]
effect: allow
derivedRoles: ["trusted_agent"]
# Deny shell and python tools to non-trusted agents
- actions: ["execute"]
effect: deny
roles: ["agent"]
when: request.resource.attr.tool_type in ["shell", "python"]
unless: request.principal.attr.tags.exists(t, t == "trusted")
advice: "Shell and Python tools require the 'trusted' tag."Audit Integration
The principal_id field in audit records tracks trigger source identity (e.g., telegram:12345, webhook:github). This is independent of agent principals and is preserved across all execution paths.
Delegation policy denials are logged as policy_denied audit events via the DelegateSink audit buffer. See Audit Trail for the full audit logging reference.
Docker
Mount the policy directory into your container:
volumes:
- ./policies:/data/policies
environment:
- INITRUNNER_POLICY_DIR=/data/policiesTroubleshooting
Policy directory not found
Verify INITRUNNER_POLICY_DIR points to a valid directory containing .yaml files. InitGuard fails fast when the directory is set but missing or empty.
ls $INITRUNNER_POLICY_DIR
# Should list your policy YAML filesPolicy load / validation error
- Check YAML syntax — all documents require
apiVersion: initguard/v1 - Verify CEL expressions compile — all expressions are compiled at load time, not at evaluation time
- Ensure
importDerivedRolesreferences match actualDerivedRolesdocument names - Check for duplicate derived role names across files
Schema validation error
Schema validation is optional lint. If you have a Schema document, verify:
- Attribute names in policies match the schema definitions
- Actions in resource policies match the schema's
actionslist - Attribute types are one of:
string,int,bool,list
403 / Policy denied
- Check the
decision.reasonanddecision.advicefields in the denial message — they identify which rule matched - Review agent metadata
tagsandteamin your role YAML - Review derived role definitions and
importDerivedRolesin your resource policies - Remember deny-wins: if any deny rule matches, the request is denied regardless of allow rules