What it is
The Runtime Pipeline is the ordered sequence of checks and steps aBusinessTransaction
passes through, from arrival to a signed ExecutionTrustRecord. Its center is
RuntimeEngine.execute() (packages/runtime/src/RuntimeEngine.ts), a single method that
loads a policy, checks that the policy in force is the approved one, checks the caller’s
authority and the facts the policy needs, evaluates the policy, builds a Decision, signs an
ExecutionAuthorizationPayload if approved, and then hands the result to the release stage
(RuntimePipeline) and the final trust record stage (BusinessTrustPipeline). Everything
downstream of a request being accepted runs through this one method.
Why it was built
A system that authorizes real-world actions needs one place where every protection is guaranteed to run, in a guaranteed order, with no way for a caller or a misconfigured deployment to skip a step silently.RuntimeEngine is that place. Its own class doc comment
states its responsibilities directly: load the requested policy, evaluate it deterministically,
create the Decision artifact, create the initial Execution artifact, execute the Runtime
Pipeline, and produce the ExecutionTrustRecord. Most of the protections wired into it
(signal-intent binding, capability/policy binding, signal-state verification, policy
governance verification) were added incrementally, as optional constructor parameters
placed after the required ones, so every pre-existing call site keeps compiling and behaving
identically when a new protection is introduced. This is a deliberate, repeated pattern in
this codebase, not an accident of growth.
How it works
RuntimeEngine’s constructor takes ten required dependencies: RuntimePipeline,
PolicyRouter, PolicyEngine, SignalIntentBinder, DecisionBuilder, ExecutionGate,
ExecutionBuilder, BusinessTrustPipeline, RuntimeAuthorizationSigner, and
authorizationTtlSeconds. It then takes optional trailing parameters: hooks (defaults to
none), refusalRecordBuilder, refusalRecordRepository, signalStateVerifier,
capabilityPolicyBinder, policyExecutionVerifier, policyGovernanceAnchorResolver,
signingReadiness, executionIntents, and approvalNeededNotifier. Two more, the
trustedSignalResolver and the authorityGrantEvaluator, default to implementations that
refuse anything that depends on them (see steps 6 and 8 below). At construction, it logs which
optional protections are actually configured (runtime_engine_constructed), which is how an
operator can confirm, from log output alone, exactly which protections are active for a given
deployment.
execute(transaction) runs these steps, in this order:
-
Policy load.
policyRouter.load(transaction.policy.name, transaction.policy.version).beforePolicyLoadandafterPolicyLoadhooks fire around this. -
Policy content hash.
policyContentHashis computed from the actually loaded policy document, never from what the caller declared, using the same canonicalize-then-SHA-256 hasher every other content hash in this codebase uses. -
Governance anchor. If
policyGovernanceAnchorResolveris configured, it resolves whether this policy traces to a valid, matching approval record. This is evidence only. A resolver error is caught and logged, and it never changes the outcome. -
Policy execution verification. If
policyExecutionVerifieris configured, it checks the same approval-record question, but this check can refuse. A policy with no approval record, an approval record whose signature does not verify, or live content that no longer matches its approval record is refused before any rule is evaluated. The refusal is recorded as authority statusAUTHORITY_UNCLEAR, withmatchedRuleIdpolicy-execution-verification-violation. -
Capability/policy binding. Runs only if step 4 raised no violation. If
capabilityPolicyBinderis configured and the invoked capability has a canonical policy binding, a declared policy that does not match it is refused asNOT_AUTHORIZED, withmatchedRuleIdcapability-policy-binding-violation. This check runs before signal-intent binding for the same reason: checking a narrower guarantee against an already-wrong policy is meaningless. Capabilities with no canonical binding are unaffected. -
Per-agent authority grant. Runs only for a policy that declares
requireAuthorityGrant, and only if steps 4 and 5 passed. The authenticated caller (metadata.submittedBy, set by the server) must hold a current grant for the action, approved through maker-checker, and the request must be within that grant’s limits. Anything else refuses the request before any business system is asked. The defaultauthorityGrantEvaluatorreads no grants, so such a policy is refused asAUTHORITY_UNCLEAR(fail closed). -
Signal-intent binding. Runs only if steps 4 through 6 raised no refusal. It checks that
every
boundSignalsentry matches the real Intent’s target and parameters. Any mismatch is refused withmatchedRuleIdsignal-intent-binding-violation. -
Authority and business validation. Two separate answers, recorded separately on the
Decision. Authority asks whether this kind of action may be decided here at all. Business validation asks whether the facts the policy declares assignalSourcesare established by their sources for this exact business object. It runs only once authority is established and the proposal matches the Intent, so a request that is not authorized never reaches a business system. A sourced fact that the caller sent differently is refused asINVALID. Any status other thanVALIDrefuses the request; none is turned intoVALID. The defaulttrustedSignalResolverhas no sources registered, so a policy that declares any is refused asSOURCE_UNAVAILABLE. -
Policy evaluation.
policyEngine.evaluate(policy, evaluatedSignals)runs the real rules, whereevaluatedSignalsis the caller’s proposal with every sourced fact replaced by the value its source established. It runs only if none of the earlier steps refused.beforePolicyEvaluationandafterPolicyEvaluationhooks fire around it. A refusal from steps 4 through 8 becomes a syntheticREJECTwithevaluatedRules: 0, and no rule is evaluated for a request that already failed an earlier check. -
Signal-state verification. Runs only if the provisional decision is
APPROVE. A request already refused needs no re-fetch of real state. IfsignalStateVerifieris configured, it independently re-derives the declared facts from a real external source. A mismatch overrides the decision toREJECT, withmatchedRuleIdsignal-state-verification-violation. No authorization is generated for an approval that verified state contradicts. -
Approval verifier guard. If the decision is still
APPROVEand nosignalStateVerifieris configured, the decision is refused withmatchedRuleIdapproval-verifier-not-configured.PolicyValidatorrequires every approve rule to depend on a signed approval signal, but only the signal-state verifier checks that the approval behind that signal is real. Without a verifier, a caller could set the signal totrueon its own. -
Decision.
decisionBuilder.build(transaction, policyDecision, assessment). Theassessmentrecords the authority result and the business validation result. A non-approved decision also recordsNOT_EXECUTEDwith the reason. -
Refusal record. For any decision that is not
APPROVED, aRefusalRecordis built and persisted. This write is evidence only: a failure is logged and never blocks the outcome. If the refusal could be cured by an approval (a policy rule refusal, or an approval that did not verify),approvalNeededNotifieris notified. A refusal from binding, governance, authority or business validation cannot be cured that way, so no notification is sent for it. -
Enforce.
executionGate.enforce(decision)is the actual gate. AREJECTthrows here, and nothing past this point runs for a rejected transaction. -
Authorization.
signalsHashis computed, andauthorizationSigner.sign(...)produces theSignedExecutionAuthorization. The payload covers the decision, the policy name and version,policyContentHash,signalsHash, and the caller and tenant from the request metadata. It also carries the business conditions the authorization rests on, when business validation isVALID, and the authority grant reference, when one applies. The authorization expires afterauthorizationTtlSeconds. Chapter 9 covers the envelope’s exact fields. -
Execution and Runtime Context.
executionBuilder.build(...)builds theExecution, and aRuntimeContextis assembled. The context carries a copy oftransaction.policy, extended withcontentHashand, if resolved,governanceAnchor. The caller-submitted transaction, already persisted before this method ran, is never mutated. -
Release. Before anything reaches a connector:
signingReadiness.assertReady()runs, if configured. It throwsSigningUnavailableError(503 SIGNING_UNAVAILABLE) when the evidence signing path cannot produce a signature that verifies, so nothing is released.executionIntents.prepare(context)runs, if configured. It signs and stores what is about to be released, and throwsExecutionIntentUnavailableError(503 EXECUTION_INTENT_UNAVAILABLE) if it cannot.
pipeline.execute(context)releases the action to the connector. Chapter 10 coversExecutionGateway, one implementation of theExecutionSysteminterface this stage calls. If the release raises an error, the intent is markedERRORED, because the action may still have been performed. A typed error that already states its own outcome passes through unchanged. Any other failure, typically a connector that could not be reached, timed out, or returned an error, is reported asEXECUTION_OUTCOME_UNKNOWN, with thebusinessTransactionIdandauthorizationIdand a critical log entry. A retry on that response could repeat an action that already happened, so the error is never a bare 500. On success,executionIntents.markReleased(...)saves the execution context, so the trust record can be rebuilt later without calling the connector again. -
Trust record.
afterExecutionandbeforeTrustRecordhooks run, thentrustPipeline.execute(processedContext)produces the signedExecutionTrustRecord, thenafterTrustRecordruns. Any failure here, after the action was released, is thrown asExecutionRecordIncompleteError(500 EXECUTION_RECORD_INCOMPLETE), which names thebusinessTransactionIdandauthorizationId. An action released with no signed record is the outcome this error exists to make visible. Any error raised anywhere inexecute()also fires theonRuntimeErrorhook.
Signing readiness and failure after release
signingReadiness is CachedSigningReadiness in production. Its probe, VerificationCrypto.probeSigning(),
signs a synthetic artifact padded past 4096 bytes through the same signer and key id used for
trust records, then verifies it against the public key that signer publishes. A success is trusted
for 60 seconds, a failure is never cached, so the next request probes again, and concurrent requests
share one probe. createSigningReadiness() enforces this everywhere except when NODE_ENV is
exactly test or development.
Everything after pipeline.execute() returns happens after the action was released to the connector.
A failure there, in the trust record pipeline, the afterExecution and beforeTrustRecord hooks, or
persisting the record in Runtime.execute(), is thrown as ExecutionRecordIncompleteError, and a
critical execution_released_record_failed or execution_released_record_persist_failed event is
logged. A failure before release, such as a policy refusal, keeps its own error. See ADR-0011.
Execution Intents: signed evidence before release, and a repair path
The Execution Trust Record contains the execution result, so it can only be built after release. ADR-0012 adds a separate record that does not: the Execution Intent.ExecutionIntentService.prepare()
(packages/runtime/src/ExecutionIntentService.ts) builds it with ExecutionIntentBuilder, signs it,
and stores it, immediately before pipeline.execute(). It is fail closed: if the intent cannot be signed
and stored, nothing is released and the caller gets 503 EXECUTION_INTENT_UNAVAILABLE.
The signed fields are the ids, the policy reference, policyContentHash, signalsHash and
businessTransactionHash copied from the signed authorization, action, target and createdAt. The
execution result and the raw intent parameters are never in it. An intent proves what was about to be
released. It does not prove the action was released, or what its result was.
The intent has an unsigned operational state: PREPARED (signed and stored), RELEASED (the release stage
returned and the execution context was saved), FINALIZED (a signed Trust Record exists) and ERRORED (the
release stage raised an error, so the outcome is unknown). markReleased, markErrored and markFinalized
are best effort and never throw, because they run after release. They log at critical severity when they fail.
Marking an intent FINALIZED deletes the saved execution context, since the Trust Record then holds it. A fifth
state, RESOLVED, is set only by ExecutionIntentService.resolve(): a verified human closes a PREPARED or
ERRORED intent after reconciling it at the connector, recording resolution (NOT_EXECUTED or EXECUTED), a
required note, and who and when. It is idempotent, never calls a connector, refuses RELEASED and FINALIZED
intents and any transaction that already has a Trust Record, and is an unsigned operator statement, not a Trust Record.
ExecutionIntentFinalizer (packages/runtime/src/ExecutionIntentFinalizer.ts) rebuilds a missing Trust
Record from the saved context. It never calls a connector, it is idempotent, and it refuses with
409 EXECUTION_INTENT_RESULT_NOT_RECORDED when no context was saved. It is exposed as
POST /execution-intents/{businessTransactionId}/finalize for a verified human credential. It also
verifies the record and generates the receipt. Enforcement is the rule createExecutionIntents() applies:
on everywhere except NODE_ENV test or development, where EXECUTION_INTENTS_CHECK=true turns it on.
The full operator procedure is in the docs site page Execution Intents.
RuntimeFactory.create() (packages/runtime/src/RuntimeFactory.ts) is the composition root
that assembles a fully wired RuntimeEngine (via RuntimeBuilder) plus the surrounding
ExecutionTrustApplication (transaction, execution, verification and receipt services). It takes
the same optional protections as trailing parameters and only wires them into RuntimeBuilder
when supplied (if (signalStateVerifier) { builder.withSignalStateVerifier(...) }), the same
“absent means unconfigured, not broken” discipline as the constructor itself.
How it enables things, with examples
examples/tutorials/03-runtime-execution, the baseline: a transaction throughRuntimeEngine.execute()to a trust record, with no optional protections.examples/tutorials/16-runtime-pipeline, the pipeline stages themselves.examples/tutorials/18-runtime-hooks, theRuntimeHookinterface (beforePolicyLoad,afterDecision, and others) that lets an integrator observe or extend the pipeline without modifyingRuntimeEngineitself.examples/tutorials/19-runtime-composition, composing multiple pipeline stages.examples/tutorials/15-custom-runtime-component, writing a custom pipeline stage.
How to validate this yourself
packages/runtime/src/RuntimeEngine.ts, the method itself. Every ordering claim above is a direct comment or statement in this file.packages/runtime/src/RuntimeFactory.tsandRuntimeBuilder.ts, how aRuntimeEngineactually gets constructed for a real deployment.packages/runtime/tests/e2e/runtime.e2e.test.ts, end-to-end proof, including the content-hash-at-decision-time assertion against real on-disk policy content.packages/runtime/tests/unit/optional-protections-logging.test.ts, proves the construction-time log line accurately reflects what is wired.packages/runtime/tests/integration/runtime.integration.test.ts, the fuller integration surface.
Integration requirements
None beyond what Chapter 2 (Configuration and Bootstrapping) already covers.RuntimeEngine
itself reads no environment variables. Every dependency it needs is constructed and passed in by
RuntimeFactory and application.ts. The optional protections (signalStateVerifier,
capabilityPolicyBinder, policyExecutionVerifier, policyGovernanceAnchorResolver,
signingReadiness, executionIntents) each have their own configuration surface, covered in the
chapters specific to them.