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 declaring each signal’s type
(boolean, number or string; a request whose signal has another type is refused before any
rule runs, as signal-type-violation), 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):
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:36) 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:121-123: 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:163-169) with reason: "no_rule_matched" (PolicyEngine.ts:75) 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:298). 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. Since 2026-09-30 it also refuses any policy with an approve rule that does not require a fact fromapprovalSignalswithis_true, as its whole condition or directly inside its top levelall(validateEveryApprovalNeedsSignedApproval): no AI agent action is authorized without a signed human approval, reads included (Human approval). The same check runs on every proposed policy change, so such a policy cannot be approved through governance either.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:763-781’s own doc comment explains why: unlike an uncovered fact, which has one fix (bind the signal or acknowledge it), a conflict can be intended, such as 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:438) rejects two
things before a policy is ever accepted: a pattern longer than 200 characters
(MAX_PATTERN_LENGTH, PolicyValidator.ts:408), 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. Serverless functions run on a read-only filesystem, so a live policy write to local files cannot succeed. Deployments therefore store policies in Postgres.
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(173 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, required, with no default) 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.