What it is
PolicyEngine (packages/policy/src/PolicyEngine.ts) is the piece of Parmana that turns a
policy document and a set of runtime signals into one decision: approve or reject, with a
reason and a trace of which rule matched. It is deliberately narrow. Its own doc comment
states what it must never do: authorize execution, execute business actions, access external
systems, create trust records, replay, or generate timestamps. It is a pure function from
(Policy, PolicySignals) to PolicyDecision.
Why it was built
A policy engine that could reach outside itself (call a database, hit the network, read a clock) would make every decision harder to reason about, replay, or audit. Parmana’s design keeps decisioning pure and pushes every side effect (independent verification of signals, persistence, signing) into layers around it.PolicyEngine is the load-bearing center that
everything else in this chapter’s neighboring chapters (signal binding, capability binding,
signal-state verification) exists to protect the inputs of.
How it works
The Policy document
APolicy (packages/policy/src/types/Policy.ts:198-300) has an identity (policyId,
policyVersion, schemaVersion), an optional signalsSchema describing expected signal
types, optional boundSignals and unboundSignalReasons (covered fully in Chapter 5), and an
ordered array of rules.
Each PolicyRule (Policy.ts:178-193) has an id, a condition, and an outcome
({ action: "approve" | "reject", reason: string }). Conditions are recursively composable
(Policy.ts:169-173):
- Leaf (
PolicyLeafCondition):{ fact, operator, value? }, a single comparison against one named signal. all: logical AND over child conditions.any: logical OR over child conditions.always: unconditionally true, used as the final catch-all rule.
PolicyValidator.OPERATORS,
packages/policy/src/PolicyValidator.ts:32-71):
The actual comparison logic lives in
OperatorEvaluator (packages/policy/src/OperatorEvaluator.ts,
imported by PolicyEngine.ts:1), PolicyEngine itself only walks the condition tree and
delegates each leaf to it.
Evaluation: first-match-wins
PolicyEngine.evaluate(policy, signals) (PolicyEngine.ts:35-55) calls a private
findFirstMatch, which iterates policy.rules in array order, pushing each rule’s id onto a
trace array as it’s visited, and returns the first rule whose condition evaluates true
against the supplied signals. This is deterministic by construction: the same policy and the
same signals always produce the same matched rule, because iteration order is fixed array
order, not a scored or prioritized search.
A missing fact never satisfies a leaf condition (PolicyEngine.ts:93-95: if (signal === undefined) return false), an absent signal is not treated as a wildcard or a pass.
If no rule matches, PolicyDecision.outcome defaults to REJECT via toOutcome’s default
branch (PolicyEngine.ts:140-142) with reason: "no_rule_matched" and matchedRuleId: "none". Every real policy in this codebase ends with an explicit always: true catch-all
rule specifically so this default path is never actually reached in practice, it exists as a
safety net, not the intended way to reject.
The returned PolicyDecision (packages/policy/src/types/PolicyDecision.ts) carries
policyId, policyVersion, outcome, reason, matchedRuleId, evaluatedRules (how many
rules were checked before a match), and matchedPath (the full trace of rule IDs visited) ,
enough for an auditor to reconstruct exactly why a decision came out the way it did, without
re-running the engine.
Loading and validating: PolicyRouter
PolicyRouter (packages/policy/src/PolicyRouter.ts) is the layer above PolicyEngine that
actually loads a policy by (name, version) via a PolicyRepository, then validates it before
handing it back. Two validator calls happen on every load:
validator.validate(policy)(PolicyRouter.ts:24), fail closed. ThrowsPolicyValidationErroron structural problems (missing identity fields, emptyrules, malformedboundSignals/unboundSignalReasons, an oversized or dangerousmatchesregex, see below) and, critically, on any rule-referenced fact that is neither inboundSignalsnorunboundSignalReasons(findUncoveredFacts, called internally atPolicyValidator.ts:229). An uncovered fact is a hard failure, not a warning: every signal a rule can reference must be either bound to the Intent or explicitly acknowledged as independently verified.validator.findRuleConflicts(policy)(PolicyRouter.ts:30), advisory only. Detects rule pairs whose conditions can both be true, which (under first-match-wins) means the earlier rule silently shadows the later one. Logged viaconsole.warnwith eventpolicy_rule_conflict_detected, never thrown.PolicyValidator.ts:442-466’s own doc comment explains why: unlike an uncovered fact (unambiguous fix, bind it or acknowledge it), a flagged overlap might be a real bug or the deliberately intended shape (a specific rule followed by a broader fallback). The analysis is also incomplete by design, it only reasons about pairs of single, non-nested-fact conditions; anything involvingall/anyis reported asNEEDS_REVIEW(levelINFO) without further analysis.
The matches regex guard
Because a matches condition is evaluated against live, potentially attacker-influenced
signal values, PolicyValidator.validateRegex (PolicyValidator.ts:361-391) rejects two
things before a policy is ever accepted: a pattern longer than 200 characters
(MAX_PATTERN_LENGTH, PolicyValidator.ts:331), and a pattern containing a quantified group
whose own contents are themselves quantified (e.g. (a+)+), the textbook shape of
catastrophic regex backtracking (ReDoS). The doc comment is explicit that this is a heuristic,
not a proof: it catches the single-level nested-quantifier case, not every pattern capable of
exponential-time backtracking. A linear-time engine or an execution timeout would be needed to
close that gap completely; this is a deliberate, bounded improvement over no check at all.
Two PolicyRepository implementations
PolicyRepository (packages/policy/src/PolicyRepository.ts) is a three-method interface:
load, save, listAll. Two implementations exist:
FilePolicyRepository, reads/writespolicies/{name}/{version}/policy.jsonon the local filesystem. Used in local development and in tests.SupabasePolicyRepository(packages/policy/src/SupabasePolicyRepository.ts), reads/writes apoliciestable via a direct Postgres connection. Added 2026-09-16, the same night as this handbook: Vercel’s serverless Functions run on a read-only filesystem, so the very first real production policy approval (PolicyChangeApprovalService.approve()’s live-policy write, covered fully in the policy governance chapter) failed withEROFSagainstFilePolicyRepository.packages/api/src/application.tsnow selects between the two based on whether a real database is configured, constructing whichever one lazily rather than at module import time (see that file’s own doc comment for why eager construction was tried first and caused a different bug).
PolicyRegistry (packages/policy/src/PolicyRegistry.ts) is a separate, much smaller piece ,
an in-memory Map from "name:version" to registration metadata ({ name, version, path }).
Its own doc comment is explicit that it does not load, evaluate, or choose policies; it only
tracks what’s available.
How it enables things, with a concrete example
- Tutorial 02 (
examples/tutorials/02-policy-evaluation/run.ts) exercisesPolicyEnginedirectly against a real policy document, showing both the approve and reject paths. - Tutorial 04 (
examples/tutorials/04-policy-router/run.ts) exercisesPolicyRouterloading a real policy from disk, including its validation step. - Tutorial 17 (
examples/tutorials/17-multi-policy-routing/run.ts) demonstrates routing across multiple distinct policies by name/version. - Tutorial 116 (
examples/tutorials/116-supabase-policy-repository/run.ts, 2026-09-16) demonstratesSupabasePolicyRepositorydirectly, including theEROFSfailure it fixes.
How to validate this yourself
- Read the engine itself:
packages/policy/src/PolicyEngine.ts(145 lines, worth reading in full). - Read
packages/policy/src/types/Policy.tsfor the exact shape of every field a policy document can have. - Run
packages/policy/tests/unit/PolicyEngine.test.ts,packages/policy/tests/unit/PolicyValidator.test.ts, andpackages/policy/tests/unit/PolicyRouter-boundSignals-coverage.test.tsto see the full assertion suite, including edge cases (missing facts, conflicting rules, oversized regex patterns) not covered above. - Pick any real policy under
policies/*/*/policy.jsonand trace a few signal combinations through its rules by hand, the format is simple enough to do this without running code.
Integration requirements
None beyond what’s already required to run the API at all:PARMANA_POLICY_DIR (for
FilePolicyRepository, local/test default ./policies) or PARMANA_STORAGE=supabase plus
DATABASE_URL (for SupabasePolicyRepository in a real deployment). No policy-engine-specific
configuration exists, a policy’s own content is the only per-policy configuration surface.