Skip to content

Policy Engine

The Policy Engine evaluates rules at every decision point in the kernel — tool calls, task claims, and task completions. Its core invariant: a deny decision at any layer is final and cannot be reversed by any other layer.

Policies are organized into four layers, evaluated in fixed order:

workspace → lane → pack → task
LayerWho sets itPurpose
WorkspaceWorkspace administratorOrganization-wide rules (e.g., “no network access”)
LaneLane configurationDomain-specific restrictions (e.g., “this lane is read-only”)
PackPack manifestDomain rules from the pack author (e.g., “gates must pass before completion”)
TaskTask specificationPer-task rules (e.g., “this task cannot modify src/auth/”)

Each layer can contain a default decision and a list of rules.

A rule has four parts:

FieldTypeDescription
idstringUnique identifier (e.g., software-delivery.gate.format)
triggerenumWhen to evaluate: on_tool_request, on_claim, on_completion, on_evidence_added
decisionenumallow, deny, or approval_required
reasonstring?Human-readable explanation

Rules in pack manifests are static — they apply unconditionally when their trigger fires. Packs can also register runtime-authored rules through a manifest policy_factory, and those rules can include a when predicate for conditional evaluation (see Conditional Rules with when() below).

This split is useful when a pack needs runtime-aware governance. The agent-runtime pack, for example, keeps a static baseline allow rule in the manifest, then uses policy_factory to add intent-aware deny and approval_required rules after pack config has been resolved.

The evaluation algorithm processes layers in order:

  1. Start with deny. The effective decision begins as deny (fail-closed).
  2. Apply defaults. The first layer to declare a default_decision sets the baseline. Subsequent layers can only tighten (deny), not loosen, unless explicitly granted allow_loosening.
  3. Match rules. For each layer, find rules whose trigger matches the current context.
  4. Sticky deny. Once any rule emits deny, no subsequent allow or approval_required rule in any layer can reverse it.
Workspace: default=allow          → effective: allow
Lane:      (no default)           → effective: allow
Pack:      rule deny(on_completion) → effective: deny  ← sticky
Task:      rule allow(on_completion) → REJECTED (cannot loosen a hard deny)
                                    → final: deny

approval_required sits between allow and deny: it blocks execution and records a pending human decision, but it is still overridden by any explicit deny.

Each trigger fires at a specific lifecycle moment:

TriggerWhen it firesExample use
on_tool_requestEvery tool call, during authorizationBlock specific tools, restrict write operations
on_claimWhen a task is claimed (ready → active)Require approval before work starts
on_completionWhen a task completes (active → done)Enforce gates (format, lint, test)
on_evidence_addedWhen a new evidence record is writtenReserved for future use

In the Software Delivery Pack, quality gates (format, lint, typecheck, test) are implemented as pack-layer policies with trigger: on_completion. When you run pnpm wu:prep or pnpm gates, the system evaluates these policies:

GatePolicy IDTrigger
Format checksoftware-delivery.gate.formaton_completion
Lintsoftware-delivery.gate.linton_completion
Type checksoftware-delivery.gate.typecheckon_completion
Testsoftware-delivery.gate.teston_completion

This means gates are not just CLI commands — they are kernel-enforced policies. An agent cannot skip gates by calling completeTask directly; the policy engine will deny the completion.

The kernel reserves several policy IDs for internal use:

IDPurpose
kernel.policy.allow-allDevelopment-only allow-all hook
kernel.scope.reserved-pathBlocks writes to .lumenflow/**
kernel.scope.boundaryDenies when scope intersection is empty
kernel.reconciliationMarks crash-reconciled evidence entries

Runtime policy rules can include a when predicate — a function that receives the full PolicyEvaluationContext and returns true if the rule should apply. Rules without a when predicate match unconditionally whenever their trigger fires.

const rule: PolicyRule = {
  id: 'my-pack.block-dangerous-paths',
  trigger: 'on_tool_request',
  decision: 'deny',
  reason: 'Writes to /etc are not permitted',
  when: (context) => {
    const args = context.tool_arguments;
    if (!args || typeof args.path !== 'string') return false;
    return args.path.startsWith('/etc');
  },
};

The when predicate receives a PolicyEvaluationContext with these fields:

FieldTypeDescription
triggerstringThe trigger type that fired this evaluation
run_idstringCurrent run identifier
tool_namestring?Name of the tool being called (for on_tool_request)
task_idstring?Active task identifier
lane_idstring?Lane the task belongs to
pack_idstring?Pack that provides the tool
tool_argumentsRecord<string, unknown>?Parsed tool input arguments (see below)
execution_metadataRecord<string, unknown>?Host-supplied runtime metadata used for conditional policy matching

The tool_arguments field provides the parsed input arguments for the tool being called. The kernel extracts this automatically from the raw tool input: if the input is a plain object, it is passed as tool_arguments; otherwise the field is undefined.

This enables argument-level policy decisions — rules that inspect what a tool is doing, not just which tool is being called:

// Block file writes to sensitive directories
const rule: PolicyRule = {
  id: 'security.block-sensitive-writes',
  trigger: 'on_tool_request',
  decision: 'deny',
  reason: 'Cannot write to credentials directory',
  when: (context) => {
    if (context.tool_name !== 'file:write') return false;
    const args = context.tool_arguments;
    if (!args || typeof args.path !== 'string') return false;
    return args.path.includes('.credentials/');
  },
};

Conditional rules can inspect execution_metadata as well as tool arguments. This lets a host classify an intent during one governed turn, then attach the result to the real tool call that follows:

const rule: PolicyRule = {
  id: 'agent-runtime.intent.deny',
  trigger: 'on_tool_request',
  decision: 'deny',
  reason: 'Scheduling intent does not permit outbound email.',
  when: (context) =>
    context.execution_metadata?.agent_intent === 'scheduling' && context.tool_name === 'email:send',
};

The same rule set is also used by policy-aware governed tool discovery, so a host can filter the tool catalog before the turn runs and still get the same allow/deny/approval behavior at execution time.

Every policy evaluation returns:

  • decision — the final allow, deny, or approval_required
  • decisions[] — every rule that matched, with its individual decision and reason
  • warnings[] — any loosening attempts that were rejected

All of this is recorded in the evidence store as part of the tool trace, providing a complete audit trail of why an action was allowed or denied.

  • Kernel Runtime — Where policy evaluation fits in the tool execution pipeline
  • Scope Intersection — The permission model that runs alongside policies
  • Evidence Store — How policy decisions are recorded
  • Gates — How the Software Delivery Pack uses policies for quality checks