v0.12.1 — Durable resume decrypts strictly
Operational resume reads decrypt strictly via openStrict(), independent of the display decrypt_failure_policy.
Fixed
-
Durable resume reads decrypt strictly and fail loud on a wrong/rotated
APP_KEY(#212, the durable twin of #202). The durable runtime read its operational resume state — the run's top-level resume input (swarm_contexts.input), hierarchical node outputs (hierarchicalNodeOutputsFor), branch input/output (findBranch/branchesFor), and child-run context payloads (childRunForChild/childRuns) — back through the display-policy-awareSwarmPersistenceCipher::open(). That policy (null_with_log/legacy/throw) is the correct gate for evidence/display reads (audit, history, inspector) but the wrong gate for operational resume state: on a rotated or wrongAPP_KEYit silently fednullor still-sealed ciphertext into a durable resume, so a durable run could continue with a wrong/empty prompt instead of failing visibly and being re-dispatched. These operational reads now decrypt viaopenStrict()/ the newopenContextTopLevelInputStrict()and, on failure, throw a clearSwarmException("Cannot decrypt durable … for run [X] …; verify APP_KEY …") so the durable job fails visibly and is re-dispatchable by the operator. Unlike the streaming twin (#202), which degrades safely by re-executing the cheap step, the durable runtime cannot cheaply re-run a completed node mid-resume, so the durable path fails loud by design. Theswarm.persistence.decrypt_failure_policynow governs only the evidence/display reads — the durable run inspector reads through new policy-aware methods so audit/history surfaces still honor it.Decrypt-or-throw is applied at the point a value is actually consumed for resume (the single-run reads: context input, node outputs,
findBranch,childRunForChild), so a single undecryptable row fails exactly the run it belongs to. The cross-run recovery sweeps (recoverableBranches/dueRetryBranches/undispatchedChildRuns, which batch up to 50 unrelated runs and read only status/queue fields) decrypt non-strictly so one run's key mismatch cannot starve recovery of every healthy run in the batch — the strict read still fires per-row when each swept item is actually resumed. The strict-vs-display choice is a method-stack parameter (no instance/static state — Octane-safe), andopenStrict()passes plaintext /null/ empty values through unchanged, soencrypt_at_rest=falsedeployments and legitimately-plaintext rows never throw. The durable run inspector reads evidence through internal, policy-aware helpers on the concrete store (DatabaseDurableRunStore); the publicDurableRunStoreread API (find(),runIdsForLabels(), …) is unchanged, so applications reading durable rows for dashboards are unaffected. No migrations; no application change. See Compliance & audit.
Full entry in the CHANGELOG · upgrade notes in UPGRADING.md.