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 evaluated (in order workspace, lane, pack, task) initialises the decision from its default_decision, or leaves it at deny if it declares none. Later layers that declare a default can only tighten (deny), not loosen, unless explicitly granted allow_loosening. The workspace layer is therefore the baseline.
  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.

When a host passes policyLayers to initializeKernelRuntime, the kernel still builds the pack layer from the installed packs (manifest policies plus every policy_factory rule) and merges it in. This means a host that passes layers also runs every pack policy_factory at start.

  • Workspace, lane and task layers you supply are used as given and can only tighten the result.
  • The array you supply must include a workspace baseline: a workspace-level layer that sets default_decision. The kernel inserts no default layer for a host that supplies layers (the mandatory pack layer declares no default and is evaluated before a task layer), so without a baseline every claim starts denied and a task-level default_decision: 'allow' is rejected as loosening. Runtime start refuses this with PolicyLayerWorkspaceBaselineMissingError (POLICY_LAYER_WORKSPACE_BASELINE_MISSING), including for []. The minimal shape is { level: 'workspace', default_decision: 'allow', allow_loosening: true, rules: [] }; use default_decision: 'deny' for a deny-by-default host. Omit policyLayers entirely to keep the kernel default. Do not use a task-level allow_loosening instead.
  • A pack-level layer you supply contributes rules only. Setting default_decision or allow_loosening on it is refused at runtime start with PolicyLayerPackViolationError (POLICY_LAYER_PACK_VIOLATION).
  • At any level, a rule that reuses the id of a pack rule, or uses the reserved kernel. id prefix, is refused. A layer level outside workspace, lane, pack and task is refused.
  • Every when predicate receives an isolated, frozen copy of the context built from the declared fields only, with tool_arguments snapshotted through the canonical serializer (the same value domain as the input hash). Arguments outside that domain, such as NaN, Infinity, Map, Date or a toJSON method, deny with kernel.policy.context-isolation instead of being coerced. A predicate that tries to modify it, or that throws, produces a POLICY_DENIED for its rule and nothing dispatches. Your own context and input are never frozen. The guarantee covers the context object only: predicates still run in-process with host authority. The cost is a JSON snapshot per evaluation (about 10 microseconds for a small argument object) and grows with argument size.

The kernel applies approval_required consistently at every implemented trigger:

TriggerFail-closed result
on_tool_requestHandler is not called; the response includes a request ID and matching decisions.
on_claimNo task_claimed or run_started event is appended.
on_completionNo 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.

Trusted provenance and pre-dispatch monitors

Section titled “Trusted provenance and pre-dispatch monitors”

Trusted read adapters can attach a versioned provenance label to content returned from an external tool read, remote signal, knowledge retrieval, or authenticated channel ingress. A label identifies the source record; it is attribution data, not authority, and message text or caller-supplied metadata cannot create a trusted label.

Each active context is limited to 64 labels, and each nonempty source_id is at most 256 characters. Malformed or excessive provenance data fails closed; the kernel does not truncate it and then treat the result as clean.

The agent runtime stores labels with tool-result messages in durable session/run state. Before a consequential tool call, the kernel loads and validates labels from the active persisted context. Trusted capability effect_classification identifies consequential calls; if absent, the kernel derives consequence from permission. While labeled content remains in context, a consequential call returns APPROVAL_REQUIRED with a typed provenance reason. Approval consumption checks the current context again before dispatch. Missing or malformed context fails closed. Agent-runtime provider computation is classified non_consequential for this effect check because it proposes steps by returning model text. The provider call remains governed by authenticated caller, network, credential, budget, purpose, and base policy controls; requested external effects continue through this gate.

Hosts that replace active context with a summary must use the governed workflow checkpoint boundary so source labels are inherited from the persisted messages and committed with the fenced compare-and-swap. The engine does not schedule compaction automatically.

A host may opt in to a pre-dispatch monitor. It receives the proposed tool step and active labels, and may return unchanged, hold, or deny. Invalid or unavailable monitor results hold the call; the monitor has no allow or grant result and cannot reverse a policy denial. Policy and monitor denials record a coalesced provenance owner notice in the durable run aggregate.

The notice contains at most 64 sorted, deduplicated kind-prefixed source references, each at most 300 characters. Its key uses stable source references and decision classes rather than diagnostic text; invalid or excessive notice data holds the action.

The engine/reference proof uses a local initialized kernel and agent runtime. It covers same-batch external-read denial and persisted provenance across a killed process and resume; it does not prove hosted Cloud delivery, spend controls, or peer-consent behavior.

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 kernel state
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_policy_profilestring?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