Decision records
ADR 0111 — The execution's principal is durable
- Status: Accepted — implemented (2026-10-06; proposed 2026-10-05, design approved after 4 review rounds). See “Implementation (as built)”.
- Date: 2026-10-05
- Closes: R3 kernel half (agentic-readiness audit, governance appendix, principal-propagation rows)
- Builds on: R3 1P half (fail-closed execution authz,
executionAccessPolicy,SystemGrant, DISPATCHER_SPI 1.2.0ExecutionPackage.principal— all of which name this ADR as their follow-up), K12.1/K12.4 (PrincipalChain, delegation), ADR 0117. - Placement: kernel records and rehydrates; 1P decides. Kernel: a field on the start event,
a fold key, a resolver seam. 1P:
executionAccessPolicyreads the recorded owner, the delegation re-derivation for supervised children (@loom/orchestration), the DelegationAuthorizer fail-closed rule (@loom/effect-runtime). Shares the Wave 2 bump (EVENT_SCHEMA 1.23.0). No KERNEL_ABI change. DISPATCHER_SPI stays 1.2.0. - Revision: round 2 — identity is never written verbatim under strict/compliance without crypto or a codec
(D1); the resolver reads sequence 0 of the log, not a caller-supplied state, and
_principalsurvives a migration (D2). Round 1 (Opus review of PR #495). A fork now acts as the forker (D7);delegationUnresolvedis enforced by the kernel before any authorizer, and a start that cannot be restored is refused (D4); a shredded principal denies (D2); every 1.23 start writes an explicit record (D1);defineConfigregisters the identity paths as sensitive when crypto is composed (D1). First in lane A; a stop-gap CHANGELOG/usage note records today’s behaviour until it lands.
Context
The kernel threads PrincipalChain through RunOptions.principal into every effect’s
authorization, and does not record it. Grep of core, replay-vm, snapshot-runtime and
event-runtime for principal finds nothing. Three consequences, one of them worse than the audit
stated.
- The principal is lost on every kernel-internal path.
recoverAbandoned,TimerServicewakes, the child supervisor’s parent resume andLocalWorkerRuntimere-drive an execution with no principal (R3 1P gave them the system grant to skip lifecycle authz; it is not a principal and “never reaches an effect’s authorization”). After any of them the flow’s remaining effects run as nobody: tenant policies see tenant""(policy.ts), a tenant-scoped ALLOW silently denies and a tenant-scoped DENY silently does not fire. - A delegation-attenuated child regains base authority after recovery. The supervisor starts a
child with
principal = { actor: childId, delegation: token }.DelegationAuthorizerenforces the token when present and falls through tobasewhen absent. Recovery drops the token, so the child continues underbase, broader than its token. This is a privilege escalation across a crash. - The effect principal is the resumer, not the owner.
resume,restoreFromStateandresumeAsall end inthis.execute(…, options?.principal): the principal that answers a gate (an approver, or nobody for a timer) becomes the principal of the flow’s effects for the rest of the run. An approval makes the flow act as the approver, and a timer wake makes it act as no one. That conflates two different questions — who may perform this lifecycle op and whose authority does this execution act under — which the R3 1P half also had to keep apart (authzApplies(principal, system)is the former only).
Execution ownership has the same root: executionAccessPolicy remembers who started an execution in
a process-local table filled from ExecutionAuthorizer.committed, lost on restart unless the app
supplies resolveOwner.
Decision
D1 — Record a PrincipalRecord on EXECUTION_STARTED
EXECUTION_STARTED.payload.principal (additive, optional) holds:
interface PrincipalRecord {
v: 1;
ref?: string; // opaque, app-chosen, non-secret (D3)
identity?: { tenant?: string; actor?: string; onBehalfOf?: string }; // bounded strings, 256 chars
delegation?: { id: string; exp?: number }; // a DIGEST of the token + its expiry; never the token
}
The fold keeps it verbatim under the reserved key variables._principal; it rides snapshots, forks
and the worker ExecutionResult.state for free. Nothing else of PrincipalChain is persisted:
extra fields ([k: string]: unknown) are dropped, because a log is permanent and readable by
everyone who can read the log, and K10 forbids PII and bearer secrets in it. In particular a
delegation token is never written: it is a bearer capability (HMAC-signed, possibly long-lived)
and an append-only log cannot revoke it.
Every start on 1.23.0 writes a record, even an anonymous one: { v: 1 }. “Absent” then means exactly
one thing — a legacy log started before this ADR — and the fall-back to options.principal (D2) applies to
legacy logs only. An anonymous 1.23.0 start can no longer be confused with a legacy one, and nothing can
strip a record to regain the resumer’s authority.
Crypto. The identity subset is PII-adjacent. When a crypto plugin (K10) is composed, defineConfig
registers principal.identity.* (tenant, actor, onBehalfOf) with the same sensitive-path seam the plugin
uses for other payload fields (@loom/classify sensitive() markers feed @loom/plugin-crypto), so the
subset is encrypted at rest and shredded with the subject (PAYLOAD_SHREDDED, D2 handles the result).
Plaintext identity without crypto — a decided policy, not an accident. The { tenant, actor, onBehalfOf } subset is PII-adjacent, a log is permanent, and eraseSubject cannot redact a plaintext
field (it appends PAYLOAD_SHREDDED tombstones, which only mean something over encrypted fields). So:
(1) under security.strict, or when a compliance/erasure plugin is composed, a principal’s identity is
never written verbatim. If crypto (K10) is composed the subset is encrypted and shreddable (above);
otherwise defineConfig requires principalCodec (D3 ref mode: the log carries { v: 1, ref } only) and
refuses to compose with a typed error naming the three ways out (compose crypto, supply a codec, or
turn strict/compliance off) rather than silently writing identity into a log it was told can be erased.
(2) Outside strict/compliance the verbatim subset remains the zero-config default, and the trade-off is
a documented non-goal: such a deployment has said it does not erase, K10’s PII rule is satisfied by the
operator’s choice not to compose crypto, and nothing is promised about redaction of those three fields.
Rationale for the split: refusing everywhere would force a resolver on every app (rejected below), while
defaulting to plaintext under a stated erasure obligation is exactly the silent contradiction K10 exists to
prevent. Test: strict with no crypto and no codec refuses at composition; strict + codec writes no actor in
the log; non-strict default writes the subset and the docs state the limit.
The fold keeps parent as _parent and the chain identity as _chain (ADR 0109); a fork’s forkedBy
is read from the EXECUTION_STARTED envelope’s metadata and replaces the payload principal in
_principal. A malformed record or forkedBy (not an object) is ignored, never thrown.
parent (the supervisor’s parentExecutionId, today descriptive and only reported on
committed) is recorded beside it as EXECUTION_STARTED.payload.parent. It is lineage, not
identity; ADR 0109’s continuedFrom and R8k’s pin predicate use it.
D2 — Rehydration: the execution runs as its recorded principal
A kernel PrincipalResolver seam: resolve(record, { executionId }) → PrincipalChain | undefined | Promise<…>. The default (no configuration) rebuilds { tenant, actor, onBehalfOf } from
identity and, when delegation is recorded but unresolved, marks the chain
delegationUnresolved: <id>. The app may supply its own resolver (D3). The record is read from the log, never from a caller-supplied state. restoreFromState takes a
caller-supplied ExecutionState (WorkflowRuntime.ts:1441, whose comment already treats it as unverified),
so the resolver reads principal from sequence 0 of the execution’s own log (a point read; or the
fold the runtime itself built), and ignores preloadedState.variables._principal: a caller cannot forge or
strip the owner by restoring a doctored state. A resumeAs code migration appends EXECUTION_MIGRATED,
whose handler spreads ...state.variables (replay-vm/src/eventHandlers.ts:672-692) and so does not reset
_principal; a test pins that _principal is identical before and after a migration. The runtime calls it once per
execute() start-up and uses the result as the effect principal for the whole run, on every entry
path — start, resume, resumeAs, restoreFromState, recovery, timer, supervisor — replacing the
options.principal argument that execute() takes today. options.principal keeps exactly one
job: the lifecycle caller for ExecutionAuthorizer and ResumeAuthenticator
(verify(event, { principal })).
A shredded principal denies; it never falls back. If the recorded identity was crypto-shredded
(PAYLOAD_SHREDDED covering _principal), the resolver result is the explicit principalShredded marker
and every effect is denied (journaled EFFECT_DENIED, like D4), never evaluated under options.principal:
an erased subject’s executions must not silently run as whoever resumes them.
An execution with no principal field at all (a pre-1.23.0 log only; every 1.23.0 start writes at
least { v: 1 }, D1) falls back to today’s behaviour (options.principal), so legacy executions are
unchanged. A start that carries a
principal but ran on a node that predates this ADR leaves the log without a record; that is the same
legacy case, and the 1P executionAccessPolicy still honours resolveOwner.
D3 — Opaque ref mode
An app that must not put even an actor id in its log supplies
principalCodec: { toRef(chain): string; fromRef(ref): PrincipalChain | undefined | Promise<…> }
(WorkflowRuntimeDeps). The start records { v: 1, ref } only — no identity — and rehydration
asks fromRef. This is the mode for delegation: fromRef can re-issue the narrowed token from the
app’s own grant table, which the supervisor in @loom/orchestration does from the spawn decision it
already journals plus the delegation secret it holds (so a recovered child gets the same
attenuation back, not a fresh one). The kernel never interprets ref.
D4 — Fail closed when a delegation cannot be restored: in the kernel, and at start
The first draft put the rule only in DelegationAuthorizer, which is not the only authorizer (a
PolicyAuthorizer, a GrantTable, or a 3P authorizer is asked about a delegationUnresolved chain and
authorizes it on its own terms; and the default resolver cannot restore any delegation, so an attenuated
child would be denied only by luck of composition after its first ordinary park/resume). Three rules:
- The kernel enforces it. A principal marked
delegationUnresolved(or whose resolver result isundefinedfor a record that hasdelegation) makes the effect call end inEFFECT_DENIEDbefore any authorizer is consulted (in the step pipeline, ahead of the K2 ring).DelegationAuthorizerkeeps its own deny as defence in depth, but correctness no longer depends on which authorizer is composed. - Refuse at
start. A start whose principal carries a delegation is refused (PrincipalUnrestorableError, nothing appended) unless aprincipalCodec/ resolver able to restore it is wired. That moves the failure from “the child is mysteriously denied after its first park” to the one call that can still report it. defineConfigcomposes it. WhensuperviseChildrenis wired,defineConfigauto-composes@loom/orchestration’s principal codec (the one that re-issues the narrowed token from the journaled spawn decision, D3), so the supported path needs no configuration and rule 2 only fires for apps that hand-build a runtime.
The failure is the ordinary journaled EFFECT_DENIED, so an operator sees it and can resumeAs with a
restored principal.
D5 — Execution authz reads the recorded owner
ExecutionAuthzRequest gains an optional owner?: { tenant?, actor?, onBehalfOf? }, filled by the
kernel from the record for resume/cancel/query (read from sequence 0 of the log, one indexed
point read, performed after the existence-hiding order the runtime already uses so a denial is still
judged before “not found” is revealed). executionAccessPolicy uses it directly, so its in-process
owner table, committed, maxTracked and the “ownership is NOT durable” caveat go away; committed
stays on the interface (additive SPI) for policies that want it, and the conformance kit gains a case
that owner is supplied for an existing execution. A system-started execution (timer-started
schedule occurrence, supervisor child with no principal) has no owner, and the policy’s existing
unknownOwner rule applies.
D6 — Dispatcher and wire
ExecutionPackage.principal (DISPATCHER_SPI 1.2.0) stays and keeps its meaning: the lifecycle
caller for the dispatch. A remote worker needs nothing more to act as the owner: the owner is in
the log it reads (D1), so no second principal field crosses the seam and the SPI does not change.
wire-abi gains fold vectors for _principal.
D7 — Fork acts as the forker; continuation inherits
The first draft let a fork keep the owner’s authority, but a fork is authorized only as query on the
parent and start on the child (WorkflowRuntime.fork, around line 2329) and copyPrefix copies sequence
0, so a caller who may merely read a run and start a flow would obtain an execution that runs the
owner’s remaining effects under the owner’s grants: a privilege escalation. Now:
- A fork acts as the forker by default.
forkstampsforkedBy: PrincipalRecord(the forker’s record, built exactly as D1 builds a start record) intoforkMeta, which the copy merges into every copied envelope’s metadata, sequence 0 included (the same mechanism that carriesforkedFrom). The resolver prefersforkedByover the copiedprincipalwhen the log’s sequence 0 carries it. Ownership for authz (D5) of the fork is the forker too, which is whatexecutionAccessPolicydocuments today. - Acting as the owner is an explicit, separately authorized option:
fork(…, { actAs: "owner" }), authorized as a newExecutionOp"actAsOwner"on the parent (default policy:deny), recorded asforkedByomitted andforkActAs: "owner"in the metadata, so the log shows who chose it. - A continuation (ADR 0109) copies the record into the successor’s start; attenuation can narrow, never widen.
Replay and migration
A new optional payload field and a reserved fold key: logs without them replay byte-identically.
No re-write of existing executions. The behavioural change is D2’s: after upgrade, effects after an
approval run as the owner, not the approver, for executions that recorded a principal (i.e.
started on 1.23.0 — now every 1.23.0 start, since an anonymous one records { v: 1 }). Apps that relied on “the approver’s principal reaches the effects” must pass the
approver explicitly as data; the CHANGELOG states this as BREAKING for the new executions only.
Tests
Start with principal → crash → recoverAbandoned → effect sees the same principal (and a tenant
policy fires); timer wake and supervisor resume likewise; approval by a different actor → the effect
principal is the owner; attenuated child → crash → recovered child is still attenuated (resolver
present) or denied (resolver absent / delegationUnresolved), never base (the negative test:
the same scenario on main passes base); ref mode writes no actor into the log (assert the
payload); a delegation token string appears nowhere in any recorded event; owner supplied to the
authorizer for an execution started before a process restart; wire-abi vectors; the ExecutionAuthorizer
and PrincipalResolver conformance cases. Round-2 additions: strict without crypto or codec refuses at composition, strict with a codec writes no actor;
restoreFromState with a doctored _principal in the supplied state runs as the log’s recorded owner;
_principal survives resumeAs + transformState migration; a fork of a chain member gets a fresh
chain (ADR 0109 D7) while forkedBy still governs its authority. Round-1 additions: a delegated start with no restorer is refused
with the log empty; a delegationUnresolved principal is denied under a PolicyAuthorizer and under a 3P
authorizer that allows everything (the kernel rule fires first); a shredded principal denies and never runs
as the resumer; the crypto composition encrypts principal.identity.*; an anonymous 1.23.0 start records
{ v: 1 } and a log that strips it is not mistaken for a legacy one; a fork’s remaining effects run as the
forker, actAs: "owner" is refused without "actAsOwner", and the fork does not inherit the owner’s
tenant ALLOW.
Rejected
- Persisting the whole
PrincipalChain: stores bearer tokens and arbitrary fields in a permanent log. - Persisting only
refalways: forces every app to run a resolver to get tenant-scoped policy right; the verbatim identity subset is the zero-config path. - Refusing the verbatim identity subset everywhere: forces a resolver on every app; the split in D1 (refuse only under an erasure/strict obligation) keeps the zero-config path.
- Letting a fork inherit the owner’s authority (the first draft): an escalation; see D7.
- Enforcing
delegationUnresolvedonly inDelegationAuthorizer: other authorizers authorize it. - A principal event after sequence 0 (
PRINCIPAL_BOUND): not needed while the principal is immutable for an execution; it would also be a new forged-kernel-event surface.
Implementation (as built, 2026-10-06)
Decisions the text above left to the implementation; none changes a shape frozen at W2-0.
- Where the codec is composed (D4 rule 3). The child supervisor is app-owned and built AFTER
defineConfig, sodefineConfigcannot see it.superviseChildren({ delegation })itself composesorchestrationPrincipalCodec(secret)onto its runtime (WorkflowRuntime.composePrincipalCodec, a composition-time contributor; the newest codec is asked first,toRef/fromReffall through onundefined). The supported path still needs no configuration, and a runtime that never saw a supervisor refuses a delegated start (rule 2). - The orchestration
ref.loom:orch:v1:+ base64url{ a: childId, g: grants[], e?: exp }: the SUBSET the spawn conferred (plain effect ids, the description the parent’s spawn decision already journals), the holder and the expiry.fromRefre-signs it with the supervisor’s secret, so a recovered child gets the same attenuation back; the secret and the token never enter the log.toRefhandles only the supervisor’s own shape (a verified, holder-bound token on its own actor, no human identity fields); a token that does not verify is never re-signed and falls through to the default record, which is refused. - The acting principal is the REHYDRATED record on every path,
startincluded. A first run and a recovery must see the same chain, sostartalso runsrehydratePrincipal(record): only{ tenant, actor, onBehalfOf }reaches the effects (D1: extra fields are dropped); aprincipalCodecorprincipalResolvercarries anything more. PrincipalCodec.toRefmay returnundefined(“not mine”), so codecs compose and the default record applies to everything the codec does not claim. Underidentity: "ref"(strict/compliance without crypto) an unclaimed identified principal REFUSES the start withPrincipalUnrestorableError, log empty.- A ref no codec restores fails closed. The D4 text covers a recorded
delegation; a{ v: 1, ref }record with no codec (or whosefromRefreturnsundefined) is markedprincipalUnresolvedand the kernel denies the effects exactly likedelegationUnresolved: anonymous would silently drop tenant rules, the bug this ADR exists to fix. A record that is present but malformed or of anothervdenies the same way. defineConfigsurface.security.principalCodec,security.principalResolver, andsecurity.crypto: { events, snapshots? }(theEncryptingEventStore/EncryptingSnapshotStore, which gainedregisterSensitive). There is no other signal that crypto is composed, so it is passed explicitly. Under crypto,defineConfigregistersprincipal.identity.*onEXECUTION_STARTEDANDvariables._principal.identity.*on the snapshot store (the fold carries the principal; an encrypted log with a plaintext snapshot is the A1 erasure-theatre hole). Strict/compliance with neither crypto nor a codec throwsPrincipalRecordingRefusedError; “a composed compliance plugin” issecurity.compliance.- A fork under crypto-without-codec is refused.
forkedByrides ENVELOPE METADATA, which the crypto plugin does not encrypt, so a fork of an identified forker withidentity: "encrypted"and no codec refuses (PrincipalUnrestorableError, nothing written). With a codec the fork records aref. parentis recorded ONLY at a trusted start, in EVERY mode (superseded by review round 2: the “no authorizer records it” carve-out is gone). Trusted = the start presents the runtime’s lineage grant (runtime.lineageOptions(), which the supervisor always spreads) or its system grant. A caller-suppliedparentExecutionIdis OMITTED from the log, so a recordedparentis always the kernel’s/supervisor’s and needs no separate trust flag. R8k must read_parentas that: a retention pin may honour anyparentit finds (an untrusted one was never written), but must not be extended to a source that writesparentwithout the lineage grant.- Owners. A supervisor child started with no principal has NO owner of its own (D5): it no longer
inherits its parent’s owner from an in-process table (its LIFECYCLE follows the parent’s owner by D6
below, round 2).
executionAccessPolicylostownerOf,adopt,tracked,maxTrackedandcommittedhandling;resolveOwneranswers only for a log that recorded none; an owner naming neither tenant nor actor is no owner. - Reading the record. One point read of sequence 0 per
execute()(resume reuses the envelope it already read). APayloadShreddedErrorfrom that read, or$shreddedmarkers inside the record, is the shredded marker (denies). Nokernel.featuresflag: the optional payload fields are harmless to an older node (rolling-upgrade note in the Wave 2 design).
Review round 1 (as built)
- P1 — a resolver cannot widen a delegated child.
rehydratePrincipalnow answers arefrecord from the composedprincipalCodecFIRST (the codec is the ref’s author;superviseChildrencomposes one), then the resolver, thenprincipalUnresolved. A resolver returningundefinedfor ANY non-empty record (ref, identity, delegation) denies (principalUnresolved/delegationUnresolved); only the empty{ v: 1 }record means “no principal”. Test: orchestration with a realsuperviseChildren+ an identity-only resolver. - P2-a — the owner of a live execution is known before sequence 0 is durable. A minted-id start journals
sequence 0 behind the fold, so a prompt
cancel/querypoint-read nothing and read “legacy” (owner undefined, owner denied). The runtime keeps the start’sPrincipalRecordin a map keyed by live execution (set at start, cleared when the handle settles or the start throws; bounded by live executions) andrecordedPrincipalanswers from it. A read that fails is never legacy:ownerForAuthzreports an unknown owner and the policy’sunknownOwnerrule decides (deny by default). - P3-1 — the orchestration ref is MACed.
loom:orch:v2:<body>.<mac>;mac = HMAC(K, "loom:orch:ref:v1|" + body)withK = HMAC(secret, "loom:orch:ref-key:v1"), so a ref MAC is never a delegation signature.fromRefverifies (constant time) BEFORE re-signing; unsigned (v1) / malformed / foreign-secret refs returnundefinedand the child is denied. - P3-2 —
storedPrincipalOfasserts its input is sequence 0 /EXECUTION_STARTED; any other envelope ismalformed(deny), neverlegacy. - P3-3 —
fork(…, { actAs: "owner" })with noexecutionAuthorizeris unrestricted, like every other op without an authorizer; the default-denyactAsOwnerop exists only when an authorizer is configured.
Follow-up (not in this PR). A fork under crypto without a codec is refused (the forker’s identity rides
unencrypted envelope metadata), which makes forks unusable for an identified forker under crypto unless
the app wires a codec. Either the scaffold always generates a codec, or the crypto plugin encrypts the
metadata paths (forkedBy). Decide before crypto-by-default ships.
Review round 2 — decided (user, 2026-10-06)
Decided (user, 2026-10-06) — a principal-less supervised child’s lifecycle is authorized via its recorded
parent. On main an owner-tracking policy let the parent’s owner query/cancel its supervised child; round 1
removed that with the in-process table. Restored durably: ExecutionAuthzRequest gains the optional
parentOwner (additive; an authorizer that ignores it is unaffected). The kernel fills it when the
execution’s own record is ANONYMOUS (no identity at all — a shredded, unrestorable or delegation-only record
stays unknown) by walking the child’s recorded parent (sequence 0, written only at a trusted start) to the
nearest recorded owner: at most 8 hops, cycle-safe, an unreadable link or a chain that ends / runs out is
an unknown owner (deny by default). Cost: one sequence-0 read per hop, only for an anonymous child under an
authorizer. executionAccessPolicy uses parentOwner as the owner for every op EXCEPT actAsOwner, so
query/cancel/resume of the child follow the parent’s owner, a stranger is refused, and a caller-supplied
parentExecutionId confers nothing (it is never recorded). The child’s EFFECTS stay anonymous: nothing about
actingPrincipal changes. (Residual — a delegated child’s own owner, { actor: <childId> }, hid it from the
parent’s owner — is CLOSED by round 3 below.)
The trust token (closing the gap). After round 1 parent was recorded only with a valid system grant, but
the runtime mints that grant only under requirePrincipal, so with an authorizer and no requirePrincipal
the supervisor could not prove itself and no lineage was recorded. Options weighed: (a) mint the system grant
whenever an authorizer is configured — rejected: that grant also gates the timer wake (TIMER_FIRED is refused
without it), so it would silently tighten every non-strict deployment’s resume path, and it bypasses the
authorizer; (b) a separate LineageGrant, minted in every mode (even with no authorizer), handed out by
WorkflowRuntime.lineageOptions(), accepted for one purpose — recording parent (and reporting it on
committed). It bypasses no authorizer, gates nothing else, is identity-compared (a structurally equal object
is ignored), and is not a principal. (b) is chosen: the narrowest. Like the system grant it is a runtime-held
reference: anything in the process that can call lineageOptions() can record a parent; the public edge
never receives it (it is not part of any wire type). superviseChildren spreads it with systemOptions().
Tested in all three modes (no authorizer / authorizer / authorizer + requirePrincipal).
Decided (user, 2026-10-06) — threat model. Write access to the event store is INSIDE the principal trust
boundary: whoever can append to the log can already forge any event and outcome, including a principal
record, a parent or an APPROVAL_GRANTED. Integrity of the recorded principal therefore comes from the
store’s access control, plus the audit chain and crypto where they are composed — not from the record. The
orchestration ref stays MACed (defence in depth against a ref altered in transit or at rest), but nothing
here claims protection from a principal-record forger with store write access.
Review round 3 — decided (user, 2026-10-06)
Decided (user, 2026-10-06) — A. resume is included. The parent’s owner of a supervised child may query,
cancel AND resume it (executionAccessPolicy inherits for every op except actAsOwner). Consequence,
recorded deliberately: under the default "tenant" rule scope a same-tenant principal can approve a gate
inside a supervised child (documented in docs/usage.md and the CHANGELOG); an integrator who wants the
parent’s owner only sets rules.resume: "owner".
Decided (user, 2026-10-06) — B. a delegated child is reachable through its recorded parent. A child
started with an attenuated delegation records { actor: <childId> } as its owner, so round 2 left it
unreachable from the parent’s owner. The authz rule is now precise: the kernel fills parentOwner for any
execution whose recorded parent is TRUSTED (anonymous or identified/delegated; never for a shredded or
unrestorable record, which stays an unknown owner), walking at most 8 hops, cycle-safe, an unreadable or
shredded link / a chain that ends = no parentOwner. executionAccessPolicy allows a lifecycle op
(query/cancel/resume; NEVER actAsOwner) when the principal matches the child’s own recorded owner OR
parentOwner under the op’s scope; resolveOwner answers only when the kernel supplied neither. Nothing
widens the child’s AUTHORITY: its effects still run under its attenuated delegation (D2/D4). An untrusted
parent was never recorded, so it confers nothing.
Cost fold (controller). recordedPrincipal now returns the record’s parent alongside the principal, so
the lineage walk’s first hop re-reads nothing and each ancestor’s one read supplies both its owner and the
next link.
Source: docs/adr/0111-durable-principal.md in the loom repository.