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, runs every configured pre-authorization protection in a fixed order,
evaluates the policy, builds a Decision, signs an ExecutionAuthorizationPayload if
approved, and hands the result to two further pipelines (RuntimePipeline for execution,
BusinessTrustPipeline for the final trust record). 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, each as an optional, trailing
constructor parameter, specifically 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,
authorizationTtlSeconds) and eight optional trailing ones (hooks, refusalRecordBuilder, refusalRecordRepository, signalStateVerifier, capabilityPolicyBinder, policyExecutionVerifier, policyGovernanceAnchorResolver, signingReadiness).
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 exact order (comments quoted verbatim from
RuntimeEngine.ts):
- Policy load.
policyRouter.load(transaction.policy.name, transaction.policy.version).beforePolicyLoad/afterPolicyLoadhooks fire around this. - Policy content hash (G-24).
policyContentHash = policyContentHasher.hash(policy), computed from the actually loaded policy document, never from what the caller declared, using the sameTrustRecordHasher(canonicalize, then SHA-256) every other content hash in this codebase uses. - Policy Governance evidence anchor (G-45). If
policyGovernanceAnchorResolveris configured, it resolves whether this policy has a valid, matching approval record. Purely evidentiary, a resolver error is caught and logged, never allowed to affect the real outcome. - Policy Governance execution-time verification. If
policyExecutionVerifieris configured, it checks the same approval-record question, but this one can reject: “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 rejected beforePolicyEngineever evaluates a single rule in it.” - Capability/Policy binding (TD-22). If
capabilityPolicyBinderis configured and step 4 found no violation, it checks whether the invoked capability has a canonical policy binding and, if so, whether the declared policy matches it. Runs before signal-intent binding “for the same reason capabilityPolicyBinder runs before signalIntentBinder: checking a narrower guarantee against an already-wrong policy is meaningless.” - Signal-intent binding. If steps 4 and 5 found no violation,
signalIntentBinder.findViolations(policy, signals, {target, parameters})checks that everyboundSignalsentry actually matches the real Intent’s target/parameters. - Policy evaluation. If none of steps 4 through 6 rejected,
policyEngine.evaluate(policy, signals)runs the real rules. Any rejection from steps 4 through 6 becomes a syntheticPolicyDecisionwithoutcome: REJECT,evaluatedRules: 0, no rule is ever evaluated for a request that fails an earlier check. - Signal-state verification (G-24 residual closure, RFC-0022). Only runs if the
provisional decision is
APPROVE, “a request already rejected… needs no independent 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. - Decision.
decisionBuilder.build(transaction, policyDecision). - Refusal Record (RFC-0021). If the decision is not
APPROVED, aRefusalRecordis built and persisted, but this write is explicitly never allowed to affect or block anything downstream; a failure here is only logged (refusal_record_write_failed), the method quote: “The refusal itself must never depend on its own evidence being writable.” - Enforce.
executionGate.enforce(decision), this is the actual gate; aREJECTthrows here and nothing past this point runs for a rejected transaction. - Authorization. Only reached on
APPROVE.signalsHashis computed, thenauthorizationSigner.sign(...)produces theSignedExecutionAuthorization(Chapter 9 covers this envelope’s exact fields). - Execution + Runtime Context.
executionBuilder.build(...)and theRuntimeContextare assembled, the context carries a copy oftransaction.policyaugmented withcontentHashand, if resolved,governanceAnchor; the original caller-submitted transaction (already persisted before this method ever ran) is never mutated. - Signing readiness (G-52), then the Execution Intent (ADR-0012), then the Runtime Pipeline, then the Business Trust Pipeline. If
signingReadinessis configured,assertReady()runs first and throwsSigningUnavailableError(503 SIGNING_UNAVAILABLE) when the evidence signing path cannot produce a signature that verifies, so nothing is released. Then, ifexecutionIntentsis configured,executionIntents.prepare(context)signs the Execution Intent and stores it, and throwsExecutionIntentUnavailableError(503 EXECUTION_INTENT_UNAVAILABLE) when it cannot, so nothing is released. Thenpipeline.execute(context)runs the actual execution stages (Chapter 10 coversExecutionGateway, one implementation of theExecutionSysteminterface this pipeline calls into). If that raises an error, the intent is markedERRORED. When it returns,markReleased(...)saves the execution context on the intent. ThentrustPipeline.execute(...)produces the final, signedExecutionTrustRecord, and afterRuntime.execute()stores it the intent is markedFINALIZED.
Signing readiness and failure after release (G-52)
signingReadiness is CachedSigningReadiness in production. Its probe is VerificationCrypto.probeSigning(),
which 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, and concurrent requests share one probe. It is enforced by
createSigningReadiness() 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
(500 EXECUTION_RECORD_INCOMPLETE), which names the businessTransactionId and authorizationId, and a
critical execution_released_record_failed or execution_released_record_persist_failed event is
logged. A failure before release, such as a policy rejection, keeps its own error. See ADR-0011.
Execution Intents: signed evidence before release, and a repair path (G-52, G-53)
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 with
ExecutionIntentCrypto (the same signing key as the Trust Record, so KMS in production), 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
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, who and when. It is idempotent, never calls a connector, refuses RELEASED and FINALIZED intents and any transaction that 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, and 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/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, no optional protections.examples/tutorials/16-runtime-pipeline, the pipeline stages themselves.examples/tutorials/18-runtime-hooks, theRuntimeHookinterface (beforePolicyLoad,afterDecision, etc.) 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 in this file.packages/runtime/src/RuntimeFactory.ts,RuntimeBuilder.ts, how aRuntimeEngineactually gets constructed for a real deployment.packages/runtime/tests/e2e/runtime.e2e.test.ts, end-to-end proof, including the G-24 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’s 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 takes no environment variables directly; every dependency it needs is constructed and
passed in by RuntimeFactory/application.ts. The optional protections
(signalStateVerifier, capabilityPolicyBinder, policyExecutionVerifier,
policyGovernanceAnchorResolver) each have their own configuration surface, covered in the
chapters specific to them.