Policy enforcement
How the runtime dispatches policies, aggregates outcomes, and halts blocked invocations.
This page describes the runtime dispatch mechanism. For the contract a policy implements, see policy model.
Dispatch sequence
Code dispatch
For a code policy, the dispatcher resolves the evaluator from the registry by codeEvaluatorPolicyId (default: the policy's own ID). Three failure modes:
| Situation | Result | Evidence carried |
|---|---|---|
| Evaluator not registered | block with reason "No evaluator registered for policy …" | code.registered: false, requestedPolicyId |
| Evaluator throws | Surfaces as failed invocation status | Stack trace recorded; outcome not persisted |
Evaluator returns block | block | code.policyId, code.version, dispatchPath: ["code"] |
A missing evaluator is treated as block, not pass. The default is deny.
Data dispatch
For a data policy, the dispatcher:
- Validates the definition (depth, count, path segment counts, operator compatibility — see invariants).
- If invalid →
blockwithdefinitionStatus: "invalid"and the validation errors. - If missing →
blockwithdefinitionStatus: "missing". - Otherwise, evaluates each top-level condition. Each condition tree's result rolls up via
all/any/not/parameter/comparison.
Aggregation within a single data policy:
top-level results
→ first "block" wins
→ otherwise first "warn"
→ otherwise definition.defaultResult ?? "pass"Hybrid dispatch
interface PolicyFallbackDefinition {
codeEvaluatorPolicyId: PolicyId;
onResults?: ("pass" | "warn" | "block")[]; // default ["warn", "block"]
triggers?: PolicyFallbackTrigger[]; // default all three
}The dispatcher evaluates data first. If definitionStatus is missing or invalid (and the corresponding trigger is enabled), the fallback fires. Otherwise the data outcome's result is checked against onResults. If matched and data_result is in triggers, the fallback fires.
The fallback runs the code evaluator with the original context. The final outcome carries combined dispatchEvidence:
datablock — what the data check evaluated.fallbackblock — that the fallback fired, the trigger, and the data result that caused it.codeblock — which evaluator ran and its version.dispatchPath: ["data", "fallback", "code"].
What gets persisted
Each policy outcome becomes a PolicyEvaluation row tied to the ActionInvocation. The row carries:
policyId,policyVersion,policyKindresultand optionalreason- The full
dispatchEvidenceJSON metadata(e.g.failedConditionIdfor data policies)
Halt behavior
When aggregation produces block:
- The invocation status moves to
blocked_by_policy. - The action's handler does not run.
- State-machine validation does not run.
- Adapter steps do not run.
- No domain events are emitted.
- A
ComplianceBlockedplatform event is emitted (subject =ActionInvocation, payload references the failing policy).
The audit trail for a blocked attempt is therefore: ActionInvocation row + one or more PolicyEvaluation rows + a ComplianceBlocked event.
See also
- Audit trail — how this evidence is read out.
- Compliance — privacy, redaction, retention.
- Architecture → policy checkpoints — why this stage exists where it does.