InitRunner

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

VariableDefaultDescription
INITRUNNER_POLICY_DIR(unset)Path to policy YAML directory. If unset, policy enforcement is disabled.
INITRUNNER_AGENT_CHECKStrueEnable 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 apply
  • unless — optional CEL expression that vetoes the role when true

ResourcePolicy

Allow/deny rules for a specific resource kind (e.g., tool or agent).

  • effect: allow or effect: deny
  • roles or derivedRoles — which principals the rule applies to (one required)
  • when / unless — optional CEL conditions
  • advice — optional human-readable message included in deny decisions
  • importDerivedRoles — 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:

FieldValue
IDagent: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 of execute_run() / execute_run_stream() / execute_run_async() / execute_run_stream_async(), and reset in finally.
  • 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 Metadata from 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.team

Delegation 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/policies

Troubleshooting

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 files

Policy load / validation error

  1. Check YAML syntax — all documents require apiVersion: initguard/v1
  2. Verify CEL expressions compile — all expressions are compiled at load time, not at evaluation time
  3. Ensure importDerivedRoles references match actual DerivedRoles document names
  4. 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 actions list
  • Attribute types are one of: string, int, bool, list

403 / Policy denied

  • Check the decision.reason and decision.advice fields in the denial message — they identify which rule matched
  • Review agent metadata tags and team in your role YAML
  • Review derived role definitions and importDerivedRoles in your resource policies
  • Remember deny-wins: if any deny rule matches, the request is denied regardless of allow rules

On this page