@deksden-com/dd-flow-cli 0.9.0-beta.136 → 0.9.0-beta.138

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +9 -0
  3. package/dist/build-info.json +6 -6
  4. package/dist/cli/argument-values.js +5 -5
  5. package/dist/cli/command-inputs.js +89 -20
  6. package/dist/cli/command-metadata.js +56 -3
  7. package/dist/cli/command-option-descriptions.js +5 -0
  8. package/dist/cli/executor-onboarding.js +8 -3
  9. package/dist/cli/flows.js +29 -0
  10. package/dist/cli/help.js +240 -6
  11. package/dist/cli/input-preparation.js +54 -5
  12. package/dist/cli/memory-flow-admission.js +44 -0
  13. package/dist/cli/memory-init-invocation.js +78 -0
  14. package/dist/cli/memory-init.js +240 -0
  15. package/dist/cli/memory-storage-admission.js +53 -0
  16. package/dist/cli/native-hook-ingress.js +1 -1
  17. package/dist/cli/response-file.js +2 -0
  18. package/dist/cli/run-cli.js +283 -160
  19. package/dist/cli/setup.js +9 -7
  20. package/dist/cli.js +54 -2
  21. package/dist/domain/stage-handler-identities.js +20 -0
  22. package/dist/harness-runtime/DD-ZCODE.md +13 -0
  23. package/dist/harness-runtime/bin/dd-opencode.mjs +3 -3
  24. package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
  25. package/dist/harness-runtime/lib/daemon-operations.mjs +4 -3
  26. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +4 -2
  27. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +11 -5
  28. package/dist/harness-runtime/lib/dd-codex.mjs +22 -10
  29. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +2 -1
  30. package/dist/harness-runtime/lib/dd-droid.mjs +2 -1
  31. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +4 -2
  32. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +20 -5
  33. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +45 -10
  34. package/dist/harness-runtime/lib/dd-zcode.mjs +7 -3
  35. package/dist/harness-runtime/lib/delegation-instructions.mjs +5 -3
  36. package/dist/harness-runtime/lib/lifecycle-command.mjs +1 -0
  37. package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -0
  38. package/dist/harness-runtime/lib/managed-daemon.mjs +50 -15
  39. package/dist/harness-runtime/lib/managed-flow-env.mjs +15 -0
  40. package/dist/harness-runtime/lib/native-hook-command.mjs +1 -1
  41. package/dist/harness-runtime/lib/native-request-proof.d.mts +2 -0
  42. package/dist/harness-runtime/lib/native-request-proof.mjs +65 -0
  43. package/dist/harness-runtime/lib/opencode-plugin.mjs +5 -1
  44. package/dist/harness-runtime/lib/operation-progress.mjs +3 -1
  45. package/dist/runtime/context.js +1 -1
  46. package/dist/schemas/discovery-result.schema.json +184 -0
  47. package/dist/schemas/execution-bundle.schema.json +205 -0
  48. package/dist/schemas/execution-settings.schema.json +162 -0
  49. package/dist/schemas/flow-descriptor.schema.json +54 -0
  50. package/dist/schemas/flow-run.schema.json +22 -0
  51. package/dist/schemas/harness-catalog.schema.json +80 -0
  52. package/dist/schemas/mb-init-compose-result.schema.json +457 -0
  53. package/dist/schemas/mb-init-discover-result.schema.json +500 -0
  54. package/dist/schemas/mb-init-finish-result.schema.json +422 -0
  55. package/dist/schemas/mb-init-preflight-result.schema.json +367 -0
  56. package/dist/schemas/mb-init-review-result.schema.json +396 -0
  57. package/dist/schemas/mb-init-verify-result.schema.json +391 -0
  58. package/dist/schemas/memory-bootstrap-receipt-v2.schema.json +67 -0
  59. package/dist/schemas/memory-bootstrap-receipt.schema.json +354 -0
  60. package/dist/schemas/memory-composition-work-result.schema.json +38 -0
  61. package/dist/schemas/memory-repair-work-result.schema.json +49 -0
  62. package/dist/schemas/memory-review-correction-plan.schema.json +30 -0
  63. package/dist/schemas/memory-review-work-result.schema.json +43 -0
  64. package/dist/schemas/model-catalog.schema.json +153 -0
  65. package/dist/schemas/profile-settings.schema.json +122 -0
  66. package/dist/schemas/release-manifest.schema.json +291 -0
  67. package/dist/schemas/run-engine-binding.schema.json +1 -1
  68. package/dist/services/check-runner-evidence.js +51 -4
  69. package/dist/services/cli-operation-classifier.js +8 -47
  70. package/dist/services/code-checks.js +387 -34
  71. package/dist/services/controller-child-observer.js +99 -0
  72. package/dist/services/controller-execution.js +88 -0
  73. package/dist/services/controller-fanout.js +79 -13
  74. package/dist/services/dashboard.js +8 -2
  75. package/dist/services/descriptor-coordinator-session.js +35 -0
  76. package/dist/services/descriptor-root-work.js +53 -0
  77. package/dist/services/descriptor-run-settlement.js +105 -0
  78. package/dist/services/descriptor-run-start.js +449 -0
  79. package/dist/services/descriptor-stage-coordinator.js +197 -0
  80. package/dist/services/descriptor-stage-settlement.js +222 -0
  81. package/dist/services/descriptor-stage-start.js +139 -0
  82. package/dist/services/engines.js +39 -22
  83. package/dist/services/eval-snapshots.js +83 -25
  84. package/dist/services/execution-bundles.js +263 -0
  85. package/dist/services/execution-policy.js +20 -16
  86. package/dist/services/external-work-launch.js +91 -11
  87. package/dist/services/flow-catalog.js +21 -0
  88. package/dist/services/flow-descriptors.js +82 -0
  89. package/dist/services/frozen-engine.js +242 -0
  90. package/dist/services/generic-execution.js +164 -0
  91. package/dist/services/harness-adapter.js +28 -2
  92. package/dist/services/hooks.js +24 -6
  93. package/dist/services/hybrid-contracts.js +31 -3
  94. package/dist/services/ids.js +29 -1
  95. package/dist/services/installation-lock.js +7 -4
  96. package/dist/services/installed-flow-source.js +183 -0
  97. package/dist/services/lifecycle-invocations.js +257 -78
  98. package/dist/services/memory-accepted-composition.js +77 -0
  99. package/dist/services/memory-baseline-preservation.js +65 -0
  100. package/dist/services/memory-completed-correction-wave.js +320 -0
  101. package/dist/services/memory-compose-handler.js +258 -0
  102. package/dist/services/memory-composition-acceptance.js +199 -0
  103. package/dist/services/memory-composition-graph.js +134 -0
  104. package/dist/services/memory-composition-registration.js +146 -0
  105. package/dist/services/memory-composition-work-custody.js +151 -0
  106. package/dist/services/memory-composition-work-results.js +96 -0
  107. package/dist/services/memory-corrected-review-acceptance.js +129 -0
  108. package/dist/services/memory-corrected-review-batch.js +30 -0
  109. package/dist/services/memory-corrected-review-findings.js +106 -0
  110. package/dist/services/memory-corrected-review-inputs.js +210 -0
  111. package/dist/services/memory-corrected-review-projection.js +78 -0
  112. package/dist/services/memory-corrected-review-receipt.js +170 -0
  113. package/dist/services/memory-corrected-review-registration.js +191 -0
  114. package/dist/services/memory-correction-check-receipt.js +219 -0
  115. package/dist/services/memory-correction-input-owner.js +6 -0
  116. package/dist/services/memory-discover-handler.js +104 -0
  117. package/dist/services/memory-finish-evidence.js +435 -0
  118. package/dist/services/memory-finish-handler.js +215 -0
  119. package/dist/services/memory-finish-integration.js +280 -0
  120. package/dist/services/memory-finish-readiness.js +122 -0
  121. package/dist/services/memory-git-integration.js +151 -0
  122. package/dist/services/memory-historical-corrected-review.js +206 -0
  123. package/dist/services/memory-historical-original-review.js +130 -0
  124. package/dist/services/memory-historical-repair-registration.js +99 -0
  125. package/dist/services/memory-historical-second-review.js +128 -0
  126. package/dist/services/memory-init-bootstrap-readiness.js +210 -0
  127. package/dist/services/memory-init-contexts.js +89 -0
  128. package/dist/services/memory-init-controller-start.js +84 -0
  129. package/dist/services/memory-init-engine-release.js +223 -0
  130. package/dist/services/memory-init-git-policy.js +136 -0
  131. package/dist/services/memory-init-lint-capability.js +185 -0
  132. package/dist/services/memory-init-native-capacity.js +241 -0
  133. package/dist/services/memory-init-preflight.js +312 -0
  134. package/dist/services/memory-init-project-bootstrap.js +599 -0
  135. package/dist/services/memory-init-request-candidate.js +157 -0
  136. package/dist/services/memory-init-request-input.js +66 -0
  137. package/dist/services/memory-init-resource-storage.js +125 -0
  138. package/dist/services/memory-init-run-allocation.js +81 -0
  139. package/dist/services/memory-init-run-bundle.js +98 -0
  140. package/dist/services/memory-init-start.js +175 -0
  141. package/dist/services/memory-init-startup-custody.js +80 -0
  142. package/dist/services/memory-init-startup-input.js +240 -0
  143. package/dist/services/memory-init-storage.js +81 -0
  144. package/dist/services/memory-init-stores.js +359 -0
  145. package/dist/services/memory-init-worktrunk-capability.js +172 -0
  146. package/dist/services/memory-layer-publication.js +611 -0
  147. package/dist/services/memory-layer.js +156 -0
  148. package/dist/services/memory-lint-config.js +85 -0
  149. package/dist/services/memory-native-review-copy.js +165 -0
  150. package/dist/services/memory-normal-flow-entry.js +75 -0
  151. package/dist/services/memory-ordinary-defaults.js +112 -0
  152. package/dist/services/memory-preflight-bootstrap-custody.js +115 -0
  153. package/dist/services/memory-preflight-handler.js +259 -0
  154. package/dist/services/memory-repair-current-view.js +299 -0
  155. package/dist/services/memory-repair-document-delta.js +451 -0
  156. package/dist/services/memory-repair-history.js +491 -0
  157. package/dist/services/memory-repair-physical-binding.js +312 -0
  158. package/dist/services/memory-repair-publication-files.js +152 -0
  159. package/dist/services/memory-repair-publication-intent.js +27 -0
  160. package/dist/services/memory-repair-publication-plan.js +160 -0
  161. package/dist/services/memory-repair-publication-source.js +225 -0
  162. package/dist/services/memory-repair-publication.js +794 -0
  163. package/dist/services/memory-repair-registration.js +239 -0
  164. package/dist/services/memory-repair-work-baseline.js +413 -0
  165. package/dist/services/memory-repair-work-completion.js +97 -0
  166. package/dist/services/memory-repair-work-custody.js +73 -0
  167. package/dist/services/memory-repair-work-results.js +89 -0
  168. package/dist/services/memory-repair-workspace.js +252 -0
  169. package/dist/services/memory-repair-writer-copy.js +735 -0
  170. package/dist/services/memory-result-binding.js +173 -0
  171. package/dist/services/memory-retained-review-authority.js +129 -0
  172. package/dist/services/memory-retained-review-results.js +70 -0
  173. package/dist/services/memory-retained-second-baseline.js +102 -0
  174. package/dist/services/memory-retained-second-repairs.js +156 -0
  175. package/dist/services/memory-review-acceptance.js +214 -0
  176. package/dist/services/memory-review-correction-plan.js +171 -0
  177. package/dist/services/memory-review-handler.js +585 -0
  178. package/dist/services/memory-review-inputs.js +200 -0
  179. package/dist/services/memory-review-receipt.js +180 -0
  180. package/dist/services/memory-review-registration.js +108 -0
  181. package/dist/services/memory-review-work-custody.js +124 -0
  182. package/dist/services/memory-review-work-input.js +72 -0
  183. package/dist/services/memory-review-work-results.js +73 -0
  184. package/dist/services/memory-second-corrected-review-inputs.js +165 -0
  185. package/dist/services/memory-second-correction-plan.js +76 -0
  186. package/dist/services/memory-second-repair-baseline.js +91 -0
  187. package/dist/services/memory-second-repair-launch.js +614 -0
  188. package/dist/services/memory-second-repair-registration.js +88 -0
  189. package/dist/services/memory-second-repair-source.js +187 -0
  190. package/dist/services/memory-second-review-acceptance.js +115 -0
  191. package/dist/services/memory-source-observations.js +117 -0
  192. package/dist/services/memory-stage-dispatch.js +28 -0
  193. package/dist/services/memory-stage-results.js +199 -0
  194. package/dist/services/memory-verification-acceptance.js +135 -0
  195. package/dist/services/memory-verification-checks.js +252 -0
  196. package/dist/services/memory-verification-receipt.js +159 -0
  197. package/dist/services/memory-verify-handler.js +135 -0
  198. package/dist/services/memory-work-result-preparation.js +63 -0
  199. package/dist/services/projects.js +17 -2
  200. package/dist/services/recovery-snapshot-database.js +416 -1
  201. package/dist/services/repair-intents.js +1 -1
  202. package/dist/services/review-copy.js +10 -2
  203. package/dist/services/run-control.js +80 -9
  204. package/dist/services/run-controller-capture.js +10 -3
  205. package/dist/services/run-controller-process.js +21 -0
  206. package/dist/services/run-controller-recovery.js +3 -1
  207. package/dist/services/run-controller-state.js +1 -1
  208. package/dist/services/run-controller.js +184 -48
  209. package/dist/services/run-recovery-guard.js +2 -0
  210. package/dist/services/run-recovery.js +145 -5
  211. package/dist/services/run-start-custody.js +177 -0
  212. package/dist/services/runs.js +319 -25
  213. package/dist/services/stage-context.js +9 -1
  214. package/dist/services/stage-handlers.js +74 -0
  215. package/dist/services/stage-lifecycle.js +2 -1
  216. package/dist/services/stage-report-renderer.js +76 -0
  217. package/dist/services/stage-work-graph.js +17 -0
  218. package/dist/services/vnext-code.js +3 -3
  219. package/dist/services/vnext-execution-profile.js +2 -2
  220. package/dist/services/vnext-fanout.js +136 -10
  221. package/dist/services/vnext-merge.js +8 -3
  222. package/dist/services/vnext-plan-review.js +6 -4
  223. package/dist/services/vnext-protocolize.js +60 -2
  224. package/dist/services/work-registry.js +616 -71
  225. package/dist/services/workspace-files.js +1 -1
  226. package/dist/storage/database.js +84 -6
  227. package/dist/storage/writer-contract.js +11 -7
  228. package/package.json +13 -3
  229. package/tools/create-canon-release.mjs +33 -0
  230. package/tools/produce-hybrid-release.mjs +41 -0
@@ -0,0 +1,164 @@
1
+ import { AppError } from "../shared/errors.js";
2
+ import { sameSessionProfile } from "./execution-policy.js";
3
+ import { hybridDigest, validateHybridContract } from "./hybrid-contracts.js";
4
+ import { prepareOperatorProfile, resolveOperatorProfile } from "./operator-profiles.js";
5
+ function fail(message, issues) {
6
+ throw new AppError("generic_execution_invalid", message, 2, { phase: "prepare", effect: "no_effect", ...(issues ? { issues } : {}) });
7
+ }
8
+ function freeze(value) {
9
+ if (value !== null && typeof value === "object") {
10
+ for (const child of Object.values(value))
11
+ freeze(child);
12
+ Object.freeze(value);
13
+ }
14
+ }
15
+ function scopedReceipts(settings, scope, receipts) {
16
+ for (const [reference, kind] of [
17
+ [settings.attached_session_receipt, "attached-session-binding"],
18
+ [settings.delegation.native_capacity_receipt, "native-capacity"]
19
+ ]) {
20
+ if (reference === null)
21
+ continue;
22
+ const fact = receipts.get(reference.sha256);
23
+ if (!fact || fact.kind !== kind || fact.requestId !== scope.requestId
24
+ || fact.projectRoot !== scope.projectRoot || fact.workspace !== scope.workspace) {
25
+ fail("Execution receipt does not belong to the selected request and workspace");
26
+ }
27
+ }
28
+ }
29
+ function validateSnapshot(snapshot, flow, scope, receipts) {
30
+ if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot)
31
+ || Object.keys(snapshot).some(key => !["schema_id", "scope", "settings", "profiles"].includes(key))
32
+ || ["schema_id", "scope", "settings", "profiles"].some(key => !Object.hasOwn(snapshot, key))
33
+ || snapshot.schema_id !== "dd-flow/frozen-execution@1"
34
+ || !snapshot.scope || typeof snapshot.scope !== "object" || Array.isArray(snapshot.scope)
35
+ || !scope || [scope.requestId, scope.projectRoot, scope.workspace].some(value => typeof value !== "string" || !value.trim())
36
+ || hybridDigest(snapshot.scope) !== hybridDigest(scope))
37
+ fail("Invalid frozen execution identity");
38
+ const content = { schema_id: "dd-flow/profile-settings@1", profiles: snapshot.profiles, aliases: {}, default_profile: null };
39
+ const profiles = { ...content, revision: hybridDigest(content) };
40
+ const issues = validateHybridContract("execution-settings", snapshot.settings, {
41
+ descriptor: flow.descriptor, descriptorSha256: flow.sha256, profileSettings: profiles, receipts
42
+ });
43
+ if (issues.length)
44
+ fail("Frozen execution violates the admitted descriptor or profile policy", issues);
45
+ const settings = snapshot.settings;
46
+ if (snapshot.profiles.length !== settings.profiles.length
47
+ || snapshot.profiles.some(profile => !settings.profiles.some(reference => reference.id === profile.id && reference.revision === profile.revision))) {
48
+ fail("Frozen profile bytes and revision inventory differ");
49
+ }
50
+ for (const stage of flow.descriptor.stages) {
51
+ if (!Object.hasOwn(settings.stage_profiles, stage.id))
52
+ fail("Every declared stage must have an explicit frozen route");
53
+ }
54
+ scopedReceipts(settings, scope, receipts);
55
+ }
56
+ /** Separate from SDLC settings: no plan review, merge, bootstrap command or stop target. */
57
+ export class FrozenGenericExecution {
58
+ sha256;
59
+ settings;
60
+ #snapshot;
61
+ #flow;
62
+ constructor(snapshot, flow) {
63
+ freeze(snapshot);
64
+ this.#snapshot = snapshot;
65
+ this.#flow = flow;
66
+ this.settings = snapshot.settings;
67
+ this.sha256 = hybridDigest(snapshot);
68
+ Object.freeze(this);
69
+ }
70
+ /** Pure selection from one admitted operator snapshot; never reread it during a RUN. */
71
+ static prepare(flow, operatorSettings, selection, scope, receipts = new Map()) {
72
+ const issues = validateHybridContract("profile-settings", operatorSettings);
73
+ if (issues.length)
74
+ fail("Operator profiles cannot be frozen", issues);
75
+ if (!selection || typeof selection !== "object" || Array.isArray(selection)
76
+ || Object.keys(selection).some(key => !["rootProfile", "stageProfiles", "workProfiles", "execution", "stageSessionMode", "attachedSessionReceipt", "delegation"].includes(key))) {
77
+ fail("Unsupported execution selection");
78
+ }
79
+ if (!["mb-init", "mb-upgrade"].includes(flow.descriptor.id)
80
+ && (selection.execution === undefined || selection.stageSessionMode === undefined || selection.delegation === undefined)) {
81
+ fail("Non-memory flows must supply their own execution defaults explicitly");
82
+ }
83
+ let rootSelection = selection.rootProfile;
84
+ if (rootSelection !== undefined && typeof rootSelection !== "string") {
85
+ const profile = prepareOperatorProfile(rootSelection);
86
+ const existing = operatorSettings.profiles.find(entry => entry.id === profile.id);
87
+ if (Object.hasOwn(operatorSettings.aliases, profile.id) || existing && existing.revision !== hybridDigest(profile))
88
+ fail("Operation-only profile conflicts with an existing ID or alias");
89
+ const { revision: _revision, ...content } = structuredClone(operatorSettings);
90
+ void _revision;
91
+ if (!existing)
92
+ content.profiles.push({ id: profile.id, revision: hybridDigest(profile), profile });
93
+ operatorSettings = { ...content, revision: hybridDigest(content) };
94
+ rootSelection = profile.id;
95
+ }
96
+ const profiles = new Map();
97
+ const resolve = (id) => {
98
+ const profile = prepareOperatorProfile(resolveOperatorProfile(operatorSettings, id));
99
+ profiles.set(profile.id, { id: profile.id, revision: hybridDigest(profile), profile });
100
+ return profile.id;
101
+ };
102
+ const root = resolve(rootSelection);
103
+ const stages = Object.create(null);
104
+ if (selection.stageProfiles !== undefined && (!selection.stageProfiles || typeof selection.stageProfiles !== "object" || Array.isArray(selection.stageProfiles))) {
105
+ fail("Stage profile selection must be an object");
106
+ }
107
+ for (const id of Object.keys(selection.stageProfiles ?? {}))
108
+ flow.stage(id);
109
+ for (const stage of flow.descriptor.stages) {
110
+ const selected = selection.stageProfiles && Object.hasOwn(selection.stageProfiles, stage.id) ? selection.stageProfiles[stage.id] : undefined;
111
+ if (selected !== undefined && typeof selected !== "string")
112
+ fail("Stage profile selection must name a profile");
113
+ stages[stage.id] = selected === undefined ? root : resolve(selected);
114
+ }
115
+ if (selection.workProfiles !== undefined && (!Array.isArray(selection.workProfiles) || selection.workProfiles.some(id => typeof id !== "string"))) {
116
+ fail("Approved Work profiles must be a list of profile IDs");
117
+ }
118
+ const works = [...new Set((selection.workProfiles ?? []).map(id => resolve(id)))];
119
+ const delegation = selection.delegation;
120
+ if (delegation !== undefined && (!delegation || typeof delegation !== "object" || Array.isArray(delegation)
121
+ || Object.keys(delegation).some(key => !["mode", "max_parallel", "nativeCapacityReceipt"].includes(key))))
122
+ fail("Unsupported delegation selection");
123
+ const settings = {
124
+ schema_id: "dd-flow/execution-settings@1", flow_id: flow.descriptor.id, descriptor_sha256: flow.sha256,
125
+ execution: selection.execution ?? "managed", attached_session_receipt: selection.attachedSessionReceipt ?? null,
126
+ stage_session_mode: selection.stageSessionMode ?? "new_session",
127
+ delegation: { mode: delegation?.mode ?? "external", max_parallel: delegation?.max_parallel ?? 1, native_capacity_receipt: delegation?.nativeCapacityReceipt ?? null },
128
+ profiles: [...profiles.values()].map(({ id, revision }) => ({ id, revision })), root_profile: root,
129
+ stage_profiles: stages, work_profiles: works
130
+ };
131
+ const snapshot = structuredClone({ schema_id: "dd-flow/frozen-execution@1", scope, settings, profiles: [...profiles.values()] });
132
+ validateSnapshot(snapshot, flow, scope, receipts);
133
+ return new FrozenGenericExecution(snapshot, flow);
134
+ }
135
+ /** Admission uses retained bytes and trusted scope/receipts, never the current operator settings. */
136
+ static restore(snapshot, expectedSha256, flow, scope, receipts = new Map()) {
137
+ if (!/^[a-f0-9]{64}$/.test(expectedSha256) || hybridDigest(snapshot) !== expectedSha256)
138
+ fail("Frozen execution identity changed");
139
+ const retained = structuredClone(snapshot);
140
+ validateSnapshot(retained, flow, scope, receipts);
141
+ return new FrozenGenericExecution(retained, flow);
142
+ }
143
+ snapshot() { return structuredClone(this.#snapshot); }
144
+ stage(id) {
145
+ this.#flow.stage(id);
146
+ return { profile: this.profile(this.settings.stage_profiles[id]), session_mode: this.settings.stage_session_mode };
147
+ }
148
+ work(stage, profileId) {
149
+ const coordinator = this.stage(stage).profile;
150
+ const id = profileId ?? coordinator.id;
151
+ if (id !== coordinator.id && !this.settings.work_profiles.includes(id))
152
+ fail("Work profile is not approved for this RUN");
153
+ const worker = this.profile(id);
154
+ if (this.settings.delegation.mode === "native" && !sameSessionProfile(coordinator, worker))
155
+ fail("Native Work cannot change execution profile");
156
+ return { coordinator, worker, delegation: structuredClone(this.settings.delegation) };
157
+ }
158
+ profile(id) {
159
+ const value = this.#snapshot.profiles.find(entry => entry.id === id);
160
+ if (!value)
161
+ fail("Profile is absent from the frozen execution inventory");
162
+ return structuredClone(value.profile);
163
+ }
164
+ }
@@ -10,9 +10,35 @@ import { canonicalNativeOperation, nativeOperationWait } from "../harness-runtim
10
10
  import { readOperationProgress, readOperationRecord, readOperationWaiting, suspendOperationClock } from "../harness-runtime/lib/operation-progress.mjs";
11
11
  import { recoverDaemonReply } from "../harness-runtime/lib/daemon-observation.mjs";
12
12
  import { isObservationLoss } from "../harness-runtime/lib/operation-errors.mjs";
13
+ import { readBoundedReleaseFile } from "./release-source.js";
13
14
  const maxAdapterOutputBytes = 1_000_000;
14
15
  /** Native hooks need a real executable, not a PATH-dependent shell expression. */
15
- export function prepareHarnessFlowExecutable(context, stateDir) {
16
+ export function prepareHarnessFlowExecutable(context, stateDir, engine) {
17
+ // Native login shells rewrite search paths. The owned SDK must execute with
18
+ // the same original paths as its controller, not merely hash them as equal.
19
+ const searchPaths = ["PATH", "FPATH", "INFOPATH"].map(name => context.env[name] === undefined
20
+ ? `delete process.env.${name};\n` : `process.env.${name} = ${JSON.stringify(context.env[name])};\n`).join("");
21
+ if (engine) {
22
+ const executable = path.join(stateDir, "dd-flow");
23
+ const contents = `#!${engine.nodeExecutable}\n${searchPaths}process.env.DD_FLOW_HOME = ${JSON.stringify(context.ddFlowHome)};\nprocess.env.DD_FLOW_ENGINE_MODE = "1";\nprocess.env.DD_FLOW_ENGINE_HOME = ${JSON.stringify(engine.manifest().snapshot_root)};\nconst { spawnSync } = await import("node:child_process");\nconst child = spawnSync(${JSON.stringify(engine.nodeExecutable)}, [${JSON.stringify(engine.entrypoint)}, ...process.argv.slice(2)], { stdio: "inherit", env: process.env });\nif (child.error) throw child.error;\nif (child.signal) process.kill(process.pid, child.signal);\nprocess.exit(child.status ?? 1);\n`;
24
+ try {
25
+ fs.writeFileSync(executable, contents, { flag: "wx", mode: 0o700 });
26
+ }
27
+ catch (error) {
28
+ const conflict = () => new AppError("harness_runtime_binding_conflict", "Launch runtime executable already has different or linked content", 1, { path: executable });
29
+ if (error.code !== "EEXIST")
30
+ throw conflict();
31
+ try {
32
+ const stat = fs.lstatSync(executable);
33
+ if (!stat.isFile() || stat.nlink !== 1 || readBoundedReleaseFile(executable, 64 * 1024).toString("utf8") !== contents)
34
+ throw conflict();
35
+ }
36
+ catch {
37
+ throw conflict();
38
+ }
39
+ }
40
+ return executable;
41
+ }
16
42
  const configured = context.env.DD_FLOW_BIN;
17
43
  if (configured !== undefined) {
18
44
  if (!path.isAbsolute(configured) || !fs.existsSync(configured))
@@ -32,7 +58,7 @@ export function prepareHarnessFlowExecutable(context, stateDir) {
32
58
  const dispatch = configured
33
59
  ? `const { spawnSync } = await import("node:child_process");\nconst child = spawnSync(${JSON.stringify(configured)}, process.argv.slice(2), { stdio: "inherit", env: process.env });\nif (child.error) throw child.error;\nif (child.signal) process.kill(process.pid, child.signal);\nprocess.exit(child.status ?? 1);\n`
34
60
  : `await import(${JSON.stringify(pathToFileURL(entry).href)});\n`;
35
- const contents = `#!${process.execPath}\nprocess.env.DD_FLOW_HOME = ${JSON.stringify(context.ddFlowHome)};\n${dispatch}`;
61
+ const contents = `#!${process.execPath}\n${searchPaths}process.env.DD_FLOW_HOME = ${JSON.stringify(context.ddFlowHome)};\n${dispatch}`;
36
62
  try {
37
63
  fs.writeFileSync(executable, contents, { flag: "wx", mode: 0o700 });
38
64
  }
@@ -9,6 +9,8 @@ import { parseJsonObject } from "../shared/json.js";
9
9
  import { canonicalPath, ensureDir, resolveProjectRoot } from "../storage/paths.js";
10
10
  import { resolveWorkReference } from "../storage/work-references.js";
11
11
  import { appendAudit } from "./audit.js";
12
+ import { memoryRepairAssignedHookCwd } from "./memory-repair-physical-binding.js";
13
+ import { memoryNativeReviewAssignedHookCwd } from "./memory-native-review-copy.js";
12
14
  import { prepareTextFile, prepareJsonText, prepareError } from "../cli/input-preparation.js";
13
15
  import { nativeSessionIdentity, storageSessionId } from "./session-identity.js";
14
16
  import { assertRecoveryHookCurrent } from "./run-recovery.js";
@@ -714,6 +716,11 @@ export function handleDroidEvent(context, input) {
714
716
  return handleControlledToolEvent(context, input, event, "droid-cli");
715
717
  }
716
718
  /** Stable project identity is not the native Session's execution workspace. */
719
+ function requiresAssignedLifecycleCwd(facts) {
720
+ // Settlement and user interaction address the retained RUN explicitly;
721
+ // their tools/checks receive owned paths, not the shell's working directory.
722
+ return !facts || !["stage_finish", "stage_pause", "stage_resume", "work_finish", "work_fail"].includes(facts.invocation.operation);
723
+ }
717
724
  function assignedLifecycleCwd(context, projectRoot, facts, providerSessionId, command, observedCwd, identity, harness) {
718
725
  const auxiliary = continuationCommand(command);
719
726
  if ((!facts && !auxiliary) || facts?.analysis.supported === false)
@@ -723,7 +730,9 @@ function assignedLifecycleCwd(context, projectRoot, facts, providerSessionId, co
723
730
  const issuedRunId = facts && !facts.runId && !facts.workId && !facts.bootstrapStageStart && constraint ? observedLifecycleInvocation(context, command, constraint, identity ? { identity } : undefined)?.scope.runId : null;
724
731
  if (facts?.invocation.operation.startsWith("stage_") && !facts.bootstrapStageStart && !facts.runId && !issuedRunId)
725
732
  throw new AppError("invocation_assignment_missing", "Short lifecycle call has no unique issued RUN assignment; its workspace cannot be inferred", 1, { phase: "prepare", effect: "no_effect", recoverable: false });
726
- const expected = controlledLifecycleCwd(context, projectRoot, facts, providerSessionId, auxiliary, issuedRunId);
733
+ if (!requiresAssignedLifecycleCwd(facts))
734
+ return undefined;
735
+ const expected = controlledLifecycleCwd(context, projectRoot, facts, providerSessionId, auxiliary, issuedRunId, observedCwd);
727
736
  // Unknown CLI targets have no assigned workspace. Recording their native
728
737
  // observation must not turn the stable identity root into cwd authority.
729
738
  if (!expected)
@@ -737,7 +746,7 @@ function assignedLifecycleCwd(context, projectRoot, facts, providerSessionId, co
737
746
  // physical process directory, rather than inventing native observation.
738
747
  return expected;
739
748
  }
740
- function controlledLifecycleCwd(context, projectRoot, facts, providerSessionId, auxiliary, issuedRunId) {
749
+ function controlledLifecycleCwd(context, projectRoot, facts, providerSessionId, auxiliary, issuedRunId, observedCwd) {
741
750
  if (facts?.bootstrapStageStart)
742
751
  return projectRoot;
743
752
  const project = context.db.get("SELECT id FROM projects WHERE root = ?", [projectRoot]);
@@ -768,6 +777,14 @@ function controlledLifecycleCwd(context, projectRoot, facts, providerSessionId,
768
777
  if (!run)
769
778
  return undefined; // Hook ingress does not validate a mistyped CLI target.
770
779
  if (work) {
780
+ const repair = memoryRepairAssignedHookCwd(context, { projectRoot, projectId: project.id, runId: run.id,
781
+ workId: work.work_id, providerSessionId, ...(observedCwd ? { observedCwd: resolveProjectRoot(observedCwd) } : {}),
782
+ ...(facts?.invocation.expectedCwd ? { literalExecutionCwd: resolveProjectRoot(facts.invocation.expectedCwd) } : {}) });
783
+ if (repair)
784
+ return resolveProjectRoot(repair.root);
785
+ const nativeReview = memoryNativeReviewAssignedHookCwd(context, { projectId: project.id, runId: run.id, workId: work.work_id, providerSessionId });
786
+ if (nativeReview)
787
+ return resolveProjectRoot(nativeReview);
771
788
  // PROTOCOLIZE provisions the feature checkout after its coordinator
772
789
  // Session has launched in the stable project root. Its transition Work
773
790
  // must keep that physical route until the Stage is complete. Child Works
@@ -862,12 +879,13 @@ function handleControlledToolEvent(context, input, event, harness) {
862
879
  }
863
880
  // These controlled platforms additionally qualify their native launch
864
881
  // route against the stable project root when no CLI target is assigned.
865
- const expectedCwd = shellRefusal ? observedCwd : controlledLifecycleCwd(context, expectedRoot, lifecycle, providerSessionId, auxiliary, issuedRunId) ?? expectedRoot;
866
- const executionRoute = shellRefusal ? undefined : lifecycle?.invocation.expectedCwd ?? auxiliary?.expectedCwd;
882
+ const workspaceBound = requiresAssignedLifecycleCwd(lifecycle);
883
+ const expectedCwd = shellRefusal || !workspaceBound ? observedCwd : controlledLifecycleCwd(context, expectedRoot, lifecycle, providerSessionId, auxiliary, issuedRunId, observedCwd) ?? expectedRoot;
884
+ const executionRoute = shellRefusal || !workspaceBound ? undefined : lifecycle?.invocation.expectedCwd ?? auxiliary?.expectedCwd;
867
885
  if (executionRoute && resolveProjectRoot(executionRoute) !== expectedCwd) {
868
886
  throw new AppError(`${errorPrefix}_directory_mismatch`, "Literal shell cd route does not match the assigned lifecycle workspace", 1, { expected: expectedCwd, actual: executionRoute, actual_provenance: "shell_literal_route", effect: "no_effect", recoverable: false });
869
887
  }
870
- if (!executionRoute && observedCwd !== expectedCwd) {
888
+ if (workspaceBound && !executionRoute && observedCwd !== expectedCwd) {
871
889
  throw new AppError(`${errorPrefix}_directory_mismatch`, `${label} ${agy ? "launch route" : "Session directory"} does not match the assigned execution workspace`, 1, { expected: expectedCwd, actual: observedCwd, effect: "no_effect", recoverable: false, ...(agy ? { actual_provenance: stringValue(event.cwd_provenance) ?? "unknown" } : {}) });
872
890
  }
873
891
  const bootstrapStageStart = !shellRefusal && lifecycle?.bootstrapStageStart;
@@ -906,7 +924,7 @@ function handleControlledToolEvent(context, input, event, harness) {
906
924
  let inserted = false;
907
925
  const receipt = observeLifecycleInvocation(context, {
908
926
  currentEventKey: baseKey,
909
- command, harness, projectId: project.id, ...(!shellRefusal ? { executionCwd: expectedCwd } : {}), identity: { daemonId, rootSessionId: nativeParentId ?? providerSessionId, sessionId: providerSessionId, parentSessionId: nativeParentId ?? null, toolCallId: toolCallId },
927
+ command, harness, projectId: project.id, ...(!shellRefusal && workspaceBound ? { executionCwd: expectedCwd } : {}), identity: { daemonId, rootSessionId: nativeParentId ?? providerSessionId, sessionId: providerSessionId, parentSessionId: nativeParentId ?? null, toolCallId: toolCallId },
910
928
  recordReceipt: scope => { inserted = persist(scope); return baseKey; }
911
929
  });
912
930
  const admitted = { ...result(inserted), ...lifecycleObservationResult(receipt), ...(receipt.invocationId ? { invocation_id: receipt.invocationId } : {}) };
@@ -197,7 +197,8 @@ export function validateHybridContract(name, input, context = {}) {
197
197
  if (delegation.native_capacity_receipt !== null) {
198
198
  const fact = context.receipts?.get(string(record(delegation.native_capacity_receipt).sha256));
199
199
  if (!fact || !root || fact.harness !== record(root.profile).harness || fact.profileRevision !== root.revision
200
- || !Number.isInteger(fact.capacity) || fact.capacity < Number(delegation.max_parallel))
200
+ || !Number.isInteger(fact.capacity) || fact.capacity < 1 || fact.capacity > 15
201
+ || fact.capacity < Number(delegation.max_parallel))
201
202
  fail("native_capacity_scope_mismatch", "/delegation");
202
203
  }
203
204
  }
@@ -213,6 +214,14 @@ export function validateHybridContract(name, input, context = {}) {
213
214
  const workspace = record(data.workspace), execution = record(data.effective_execution), storage = record(data.storage);
214
215
  if (data.request_id !== expected.requestId || data.project_root !== expected.projectRoot || workspace.path !== expected.workspace || data.baseline_sha256 !== expected.baselineSha256)
215
216
  fail("foreign_bootstrap_identity");
217
+ if (data.schema_id === "dd-flow/memory-bootstrap-receipt@2") {
218
+ const git = expected.workspaceGit;
219
+ if (!git)
220
+ fail("missing_bootstrap_git_observation");
221
+ else if (workspace.repository !== git.repository || workspace.branch !== git.branch || workspace.commit !== git.commit
222
+ || workspace.git_state !== (git.commit === null ? "unborn" : "committed"))
223
+ fail("foreign_bootstrap_git_state");
224
+ }
216
225
  if (execution.engine_sha256 !== expected.engineSha256 || execution.canon_sha256 !== expected.canonSha256)
217
226
  fail("foreign_execution_identity");
218
227
  receipt(storage.compatibility_receipt, "storage-compatibility", true);
@@ -289,6 +298,16 @@ export function validateHybridContract(name, input, context = {}) {
289
298
  }
290
299
  };
291
300
  const descriptor = loaded(data.descriptor), settings = loaded(data.execution_settings), profiles = loaded(data.profile_settings);
301
+ // Execution receipts are evidence references, not just trusted facts. They
302
+ // must survive reopening the RUN inside the same frozen blob inventory.
303
+ for (const reference of [record(settings).attached_session_receipt,
304
+ record(record(settings).delegation).native_capacity_receipt]) {
305
+ if (reference === null || reference === undefined)
306
+ continue;
307
+ const ref = record(reference);
308
+ if (inventory.get(string(ref.path)) !== ref.sha256)
309
+ fail("unlisted_bundle_reference");
310
+ }
292
311
  const binding = loaded(data.engine_binding);
293
312
  const bindingSchema = JSON.parse(fs.readFileSync(new URL("../schemas/run-engine-binding.schema.json", import.meta.url), "utf8"));
294
313
  const bindingValidator = new Ajv({ strict: false, validateFormats: false }).compile(bindingSchema);
@@ -342,7 +361,9 @@ export function validateHybridShape(name, input) {
342
361
  if (!hybridContractRegistry.has(name)) {
343
362
  return [{ gate: "shape", code: "unknown_contract_name", path: "/" }];
344
363
  }
345
- const schemaUrl = new URL(`../schemas/${name}.schema.json`, import.meta.url);
364
+ const versionedName = name === "memory-bootstrap-receipt" && input !== null && typeof input === "object"
365
+ && input.schema_id === "dd-flow/memory-bootstrap-receipt@2" ? "memory-bootstrap-receipt-v2" : name;
366
+ const schemaUrl = new URL(`../schemas/${versionedName}.schema.json`, import.meta.url);
346
367
  const schema = JSON.parse(fs.readFileSync(schemaUrl, "utf8"));
347
368
  const ajv = new Ajv2020({ allErrors: true, strict: true, validateFormats: false });
348
369
  const validate = ajv.compile(schema);
@@ -388,7 +409,14 @@ export function validateFlowDescriptor(input, context) {
388
409
  if (field === "result_schema") {
389
410
  try {
390
411
  const schema = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
391
- new Ajv2020({ strict: true, validateFormats: false }).compile(schema);
412
+ const draft = schema.$schema;
413
+ // Existing SDLC schemas require parent properties inside conditional
414
+ // subschemas. Keep all other strict checks without rejecting those
415
+ // valid JSON Schema contracts through Ajv's strictRequired heuristic.
416
+ const ajv = draft === "http://json-schema.org/draft-07/schema#"
417
+ ? new Ajv({ strict: true, strictRequired: false, validateFormats: false })
418
+ : new Ajv2020({ strict: true, strictRequired: false, validateFormats: false });
419
+ ajv.compile(schema);
392
420
  }
393
421
  catch {
394
422
  fail("invalid_result_schema", `/stages/${index}/${field}`);
@@ -77,9 +77,37 @@ function parseEntityKind(value) {
77
77
  function databaseIds(context, table, idField, type, projectId) {
78
78
  if (!projectId)
79
79
  return [];
80
- return context.db
80
+ const ids = context.db
81
81
  .all(`SELECT ${idField} AS id FROM ${table} WHERE project_id = ? AND ${idField} LIKE ?`, [projectId, `${type}-%`])
82
82
  .map((row) => row.id);
83
+ if (type === "RUN") {
84
+ for (const row of context.db.all("SELECT json_extract(payload_json,'$.run_id') AS id FROM audit_events WHERE project_id=? AND event_type IN ('memory.init_run_allocation_intent','memory.init_run_allocation')", [projectId])) {
85
+ try {
86
+ if (typeof row.id !== "string")
87
+ throw new Error("Invalid reserved RUN id");
88
+ const parsed = parseFullEntityId(row.id), sequence = Number(parsed.shortId.slice(4));
89
+ if (parsed.type !== "RUN" || !Number.isSafeInteger(sequence) || sequence < 1)
90
+ throw new Error("Invalid reserved RUN id");
91
+ }
92
+ catch {
93
+ throw new AppError("memory_init_run_allocation_invalid", "Retained RUN reservation identity is invalid", 1, { effect: "no_effect" });
94
+ }
95
+ ids.push(row.id);
96
+ }
97
+ }
98
+ return ids;
99
+ }
100
+ /** Includes retained request reservations, even before their RUN row exists. */
101
+ export function nextRunId(context, projectId, slug) {
102
+ const used = new Set();
103
+ for (const id of databaseIds(context, "runs", "id", "RUN", projectId))
104
+ addSequence(used, id, "RUN");
105
+ let max = 0;
106
+ for (const sequence of used)
107
+ max = Math.max(max, sequence);
108
+ if (!Number.isSafeInteger(max + 1))
109
+ throw new AppError("validation", "RUN sequence cannot be represented safely", 1, { effect: "no_effect" });
110
+ return formatFullId("RUN", max + 1, normalizeSlug(slug));
83
111
  }
84
112
  function filesystemIds(projectRoot, relativeRoot, type) {
85
113
  const root = path.join(projectRoot, relativeRoot);
@@ -48,10 +48,13 @@ export function acquireInstallationLock(file, identity) {
48
48
  for (let attempt = 0; attempt < 3; attempt++) {
49
49
  try {
50
50
  fs.linkSync(temporary, file);
51
- return { token: owner.token, release: () => {
52
- const retained = readOwner(file);
53
- if (retained.token !== owner.token || retained.pid !== process.pid || retained.identity !== identity)
54
- refuse("lock_owner_changed");
51
+ const assertOwnership = () => {
52
+ const retained = readOwner(file);
53
+ if (retained.token !== owner.token || retained.pid !== process.pid || retained.identity !== identity)
54
+ refuse("lock_owner_changed");
55
+ };
56
+ return { token: owner.token, assertOwnership, release: () => {
57
+ assertOwnership();
55
58
  fs.unlinkSync(file);
56
59
  } };
57
60
  }
@@ -0,0 +1,183 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { AppError } from "../shared/errors.js";
5
+ import { readSetupStatus } from "./setup.js";
6
+ import { readBoundedReleaseFile } from "./release-source.js";
7
+ import { hybridDigest, validateHybridShape } from "./hybrid-contracts.js";
8
+ import { admitBundledFlow } from "./stage-handlers.js";
9
+ const limit = 1024 * 1024;
10
+ const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
11
+ function refuse(reason) {
12
+ throw new AppError("installed_flow_source_invalid", "Installed flow source is not the selected verified canon", 1, { reason, phase: "prepare", effect: "no_effect" });
13
+ }
14
+ function directory(target) {
15
+ if (!fs.lstatSync(target).isDirectory())
16
+ refuse("unsafe_source_directory");
17
+ }
18
+ function containedFile(root, logical) {
19
+ if (!/^[A-Za-z0-9_-][A-Za-z0-9_.-]*(\/[A-Za-z0-9_-][A-Za-z0-9_.-]*)*$/.test(logical)
20
+ || logical.split("/").some(part => part === "." || part === ".."))
21
+ refuse("unsafe_source_path");
22
+ let current = root;
23
+ for (const part of logical.split("/").slice(0, -1)) {
24
+ current = path.join(current, part);
25
+ directory(current);
26
+ }
27
+ return path.join(root, logical);
28
+ }
29
+ /** Initial RUN materialization input, not handler/engine/storage admission.
30
+ * Built-in memory flows live in the global canon, not the curated project pack.
31
+ * Existing RUNs must restore their retained bundle rather than call this reader.
32
+ */
33
+ export class PreparedInstalledFlowSource {
34
+ sha256;
35
+ #home;
36
+ #installationSha256;
37
+ #flowId;
38
+ #descriptor;
39
+ #canon;
40
+ #blobs;
41
+ constructor(home, installationSha256, flowId, descriptor, canon, blobs) {
42
+ this.#home = home;
43
+ this.#installationSha256 = installationSha256;
44
+ this.#flowId = flowId;
45
+ this.#descriptor = descriptor;
46
+ this.#canon = canon;
47
+ this.#blobs = blobs;
48
+ this.sha256 = hybridDigest({ installation_sha256: installationSha256, canon, inventory: this.inventory() });
49
+ Object.freeze(this);
50
+ }
51
+ static fromInstalledRelease(home, expectedInstallationSha256, flowId) {
52
+ try {
53
+ if (!path.isAbsolute(home) || !/^[a-f0-9]{64}$/.test(expectedInstallationSha256) || flowId !== "mb-init")
54
+ refuse("invalid_selection");
55
+ const selected = readSetupStatus(home);
56
+ if (!selected.installation || selected.installation_readiness !== "installed")
57
+ refuse("installed_release_unavailable");
58
+ const installation = selected.installation;
59
+ if (installation.manifest_sha256 !== expectedInstallationSha256)
60
+ refuse("installation_selection_changed");
61
+ directory(installation.canon_root);
62
+ const roots = fs.readdirSync(installation.canon_root);
63
+ if (roots.length !== 1)
64
+ refuse("ambiguous_archive_root");
65
+ const canonRoot = path.join(installation.canon_root, roots[0]);
66
+ directory(canonRoot);
67
+ const dotRoot = path.join(canonRoot, ".memory-bank"), memoryRoot = fs.existsSync(dotRoot) ? dotRoot : canonRoot;
68
+ directory(memoryRoot);
69
+ const flowRoot = path.join(memoryRoot, "dd-flow");
70
+ directory(flowRoot);
71
+ const descriptorPath = `${flowId}/flow-descriptor.json`;
72
+ const bytes = readBoundedReleaseFile(containedFile(flowRoot, descriptorPath), limit);
73
+ const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
74
+ if (validateHybridShape("flow-descriptor", value).length || value.id !== flowId || value.origin !== "canon"
75
+ || value.stages.length > 64)
76
+ refuse("invalid_descriptor");
77
+ const blobs = new Map([[descriptorPath, Buffer.from(bytes)]]), folded = new Map([[descriptorPath.toLowerCase(), descriptorPath]]);
78
+ let total = bytes.length;
79
+ for (const stage of value.stages)
80
+ for (const kind of ["instruction", "result_schema"]) {
81
+ const reference = stage[kind], existing = blobs.get(reference.path);
82
+ if (existing) {
83
+ if (hash(existing) !== reference.sha256)
84
+ refuse("conflicting_source_reference");
85
+ continue;
86
+ }
87
+ if (folded.has(reference.path.toLowerCase()))
88
+ refuse("conflicting_source_path");
89
+ const content = readBoundedReleaseFile(containedFile(flowRoot, reference.path), limit);
90
+ if (!content.length || (total += content.length) > 32 * limit || hash(content) !== reference.sha256)
91
+ refuse("source_bytes_changed");
92
+ new TextDecoder("utf-8", { fatal: true }).decode(content);
93
+ if (kind === "result_schema")
94
+ JSON.parse(content.toString("utf8"));
95
+ folded.set(reference.path.toLowerCase(), reference.path);
96
+ blobs.set(reference.path, Buffer.from(content));
97
+ }
98
+ // Verify compressed/expanded archive identity again after collecting bytes.
99
+ // A coherent descriptor+prompt edit cannot become a new trusted selection.
100
+ const final = readSetupStatus(home);
101
+ if (final.installation_readiness !== "installed" || hybridDigest(final.installation) !== hybridDigest(installation))
102
+ refuse("source_changed_during_preparation");
103
+ return new PreparedInstalledFlowSource(home, expectedInstallationSha256, flowId, { path: descriptorPath, sha256: hash(bytes) }, { version: installation.manifest.canon.version, commit: installation.manifest.canon.commit, archive_sha256: installation.manifest.canon.archive.sha256 }, blobs);
104
+ }
105
+ catch (error) {
106
+ if (error instanceof AppError && error.code === "installed_flow_source_invalid")
107
+ throw error;
108
+ return refuse("source_unavailable_or_changed");
109
+ }
110
+ }
111
+ descriptorReference() { return { ...this.#descriptor }; }
112
+ installationSha256() { return this.#installationSha256; }
113
+ snapshot() {
114
+ return { schema_id: "dd-flow/installed-flow-source@1", installation_sha256: this.#installationSha256,
115
+ descriptor: this.descriptorReference(), canon: this.canonProvenance(),
116
+ blobs: [...this.#blobs].map(([logical, bytes]) => ({ path: logical, base64: bytes.toString("base64") })) };
117
+ }
118
+ /** Copied source restoration only. expectedSha256 must come from independent
119
+ * request custody, not from the same input. This does not verify a new release
120
+ * or admit its engine/storage; those remain the bootstrap owner's duties.
121
+ */
122
+ static restoreRetained(input, expectedSha256) {
123
+ try {
124
+ if (!input || typeof input !== "object" || Array.isArray(input) || typeof expectedSha256 !== "string"
125
+ || !/^[a-f0-9]{64}$/.test(expectedSha256))
126
+ refuse("invalid_retained_source");
127
+ const value = input;
128
+ if (Object.keys(value).sort().join() !== "blobs,canon,descriptor,installation_sha256,schema_id"
129
+ || value.schema_id !== "dd-flow/installed-flow-source@1" || typeof value.installation_sha256 !== "string" || !/^[a-f0-9]{64}$/.test(value.installation_sha256)
130
+ || !value.canon || Object.keys(value.canon).sort().join() !== "archive_sha256,commit,version"
131
+ || typeof value.canon.commit !== "string" || !/^[a-f0-9]{40}$/.test(value.canon.commit)
132
+ || typeof value.canon.archive_sha256 !== "string" || !/^[a-f0-9]{64}$/.test(value.canon.archive_sha256)
133
+ || typeof value.canon.version !== "string" || !/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/.test(value.canon.version)
134
+ || !value.descriptor || Object.keys(value.descriptor).sort().join() !== "path,sha256"
135
+ || value.descriptor.path !== "mb-init/flow-descriptor.json" || typeof value.descriptor.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(value.descriptor.sha256)
136
+ || !Array.isArray(value.blobs) || value.blobs.length < 1 || value.blobs.length > 129)
137
+ refuse("invalid_retained_source");
138
+ const blobs = new Map(), folded = new Set();
139
+ let total = 0;
140
+ for (const entry of value.blobs) {
141
+ if (!entry || Object.keys(entry).sort().join() !== "base64,path" || typeof entry.path !== "string"
142
+ || !/^[A-Za-z0-9_-][A-Za-z0-9_.-]*(\/[A-Za-z0-9_-][A-Za-z0-9_.-]*)*$/.test(entry.path)
143
+ || entry.path.split("/").some(part => part === "." || part === "..")
144
+ || folded.has(entry.path.toLowerCase()) || typeof entry.base64 !== "string" || entry.base64.length > 2 * limit)
145
+ refuse("invalid_retained_source");
146
+ const content = Buffer.from(entry.base64, "base64");
147
+ if (!content.length || content.length > limit || content.toString("base64") !== entry.base64
148
+ || (total += content.length) > 32 * limit)
149
+ refuse("invalid_retained_source");
150
+ new TextDecoder("utf-8", { fatal: true }).decode(content);
151
+ folded.add(entry.path.toLowerCase());
152
+ blobs.set(entry.path, content);
153
+ }
154
+ const descriptor = blobs.get(value.descriptor.path);
155
+ if (!descriptor || hash(descriptor) !== value.descriptor.sha256)
156
+ refuse("retained_source_changed");
157
+ const result = new PreparedInstalledFlowSource(null, value.installation_sha256, "mb-init", { ...value.descriptor }, { ...value.canon }, blobs);
158
+ if (result.sha256 !== expectedSha256)
159
+ refuse("retained_source_changed");
160
+ result.resolveRegisteredFlow();
161
+ return result;
162
+ }
163
+ catch (error) {
164
+ if (error instanceof AppError && error.code === "installed_flow_source_invalid")
165
+ throw error;
166
+ return refuse("invalid_retained_source");
167
+ }
168
+ }
169
+ canonProvenance() { return { ...this.#canon }; }
170
+ blobs() { return new Map([...this.#blobs].map(([logical, bytes]) => [logical, Buffer.from(bytes)])); }
171
+ inventory() { return [...this.#blobs].map(([logical, bytes]) => ({ path: logical, sha256: hash(bytes) })).sort((a, b) => a.path.localeCompare(b.path, "en")); }
172
+ /** Production handler registry only: source declarations cannot register code. */
173
+ resolveRegisteredFlow() { return admitBundledFlow(this.#blobs.get(this.#descriptor.path), this.#descriptor.sha256, this.#blobs); }
174
+ /** Initial materialization only, outside SQL writers. */
175
+ revalidate() {
176
+ if (this.#home === null) {
177
+ PreparedInstalledFlowSource.restoreRetained(this.snapshot(), this.sha256);
178
+ return;
179
+ }
180
+ if (PreparedInstalledFlowSource.fromInstalledRelease(this.#home, this.#installationSha256, this.#flowId).sha256 !== this.sha256)
181
+ refuse("source_changed_after_preparation");
182
+ }
183
+ }