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

| Layer | Who sets it | Purpose | | ------------- | ----------------------- | ----------------------------------------------------------------------------- | | Workspace | Workspace administrator | Organization-wide rules (e.g., “no network access”) | | Lane | Lane configuration | Domain-specific restrictions (e.g., “this lane is read-only”) | | Pack | Pack manifest | Domain rules from the pack author (e.g., “gates must pass before completion”) | | Task | Task specification | Per-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:

| Field | Type | Description | | ---------- | ------- | ------------------------------------------------------------------------------------- | | id | string | Unique identifier (e.g., software-delivery.gate.format) | | trigger | enum | When to evaluate: on_tool_request, on_claim, on_completion, on_evidence_added | | decision | enum | allow, deny, or approval_required | | reason | string? | 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 the attempted action, but it is still overridden by any explicit deny. Tool requests receive a structured pending request identifier. Claims and completions fail with a typed APPROVAL_REQUIRED error and append no protected lifecycle events.

The kernel applies approval_required consistently at every implemented trigger:

| Trigger | Fail-closed result | | ----------------- | --------------------------------------------------------------------------------- | | on_tool_request | Handler is not called; the response includes a request ID and matching decisions. | | on_claim | No task_claimed or run_started event is appended. | | on_completion | No run_succeeded or task_completed event is appended. |

Rejecting a pending tool request never emits task_resumed, and resolving a request never dispatches the tool implicitly. Unknown and already-resolved identifiers fail deterministically.

Each trigger fires at a specific lifecycle moment:

| Trigger | When it fires | Example use | | ------------------- | ----------------------------------------- | ----------------------------------------------- | | on_tool_request | Every tool call, during authorization | Block specific tools, restrict write operations | | on_claim | When a task is claimed (ready → active) | Require approval before work starts | | on_completion | When a task completes (active → done) | Enforce gates (format, lint, test) | | on_evidence_added | When a new evidence record is written | Reserved 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:

| Gate | Policy ID | Trigger | | ------------ | ---------------------------------- | --------------- | | Format check | software-delivery.gate.format | on_completion | | Lint | software-delivery.gate.lint | on_completion | | Type check | software-delivery.gate.typecheck | on_completion | | Test | software-delivery.gate.test | on_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:

| ID | Purpose | | ---------------------------- | --------------------------------------- | | kernel.policy.allow-all | Development-only allow-all hook | | kernel.scope.reserved-path | Blocks writes to .lumenflow/** | | kernel.scope.boundary | Denies when scope intersection is empty | | kernel.reconciliation | Marks 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:

| Field | Type | Description | | -------------------------- | -------------------------- | ------------------------------------------------------------------ | | trigger | string | The trigger type that fired this evaluation | | run_id | string | Current run identifier | | tool_name | string? | Name of the tool being called (for on_tool_request) | | task_id | string? | Active task identifier | | lane_id | string? | Lane the task belongs to | | pack_id | string? | Pack that provides the tool | | tool_arguments | Record<string, unknown>? | Parsed tool input arguments (see below) | | execution_policy_profile | string? | Persisted, kernel-derived selector for configured execution policy |

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/');
  },
};

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