@deksden-com/dd-flow-cli 0.9.0-beta.137 → 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 (260) hide show
  1. package/CHANGELOG.md +4 -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 +197 -0
  7. package/dist/cli/command-option-descriptions.js +191 -0
  8. package/dist/cli/completion-cli.js +70 -0
  9. package/dist/cli/completion.js +57 -0
  10. package/dist/cli/error-output.js +52 -0
  11. package/dist/cli/executor-onboarding.js +76 -0
  12. package/dist/cli/flows.js +29 -0
  13. package/dist/cli/harnesses.js +84 -0
  14. package/dist/cli/help.js +240 -6
  15. package/dist/cli/input-preparation.js +54 -5
  16. package/dist/cli/memory-flow-admission.js +44 -0
  17. package/dist/cli/memory-init-invocation.js +78 -0
  18. package/dist/cli/memory-init.js +240 -0
  19. package/dist/cli/memory-storage-admission.js +53 -0
  20. package/dist/cli/models.js +39 -0
  21. package/dist/cli/native-hook-ingress.js +1 -1
  22. package/dist/cli/operator-profiles.js +61 -0
  23. package/dist/cli/output-options.js +35 -0
  24. package/dist/cli/response-file.js +71 -0
  25. package/dist/cli/run-cli.js +283 -160
  26. package/dist/cli/setup.js +149 -0
  27. package/dist/cli.js +54 -2
  28. package/dist/domain/stage-handler-identities.js +20 -0
  29. package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
  30. package/dist/harness-runtime/lib/daemon-operations.mjs +4 -3
  31. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +4 -2
  32. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +11 -5
  33. package/dist/harness-runtime/lib/dd-codex.mjs +22 -10
  34. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +2 -1
  35. package/dist/harness-runtime/lib/dd-droid.mjs +2 -1
  36. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +4 -2
  37. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +6 -4
  38. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +9 -9
  39. package/dist/harness-runtime/lib/delegation-instructions.mjs +5 -3
  40. package/dist/harness-runtime/lib/lifecycle-command.mjs +1 -0
  41. package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -0
  42. package/dist/harness-runtime/lib/managed-daemon.mjs +50 -15
  43. package/dist/harness-runtime/lib/managed-flow-env.mjs +15 -0
  44. package/dist/harness-runtime/lib/native-hook-command.mjs +1 -1
  45. package/dist/harness-runtime/lib/native-request-proof.d.mts +2 -0
  46. package/dist/harness-runtime/lib/native-request-proof.mjs +65 -0
  47. package/dist/harness-runtime/lib/opencode-plugin.mjs +2 -0
  48. package/dist/harness-runtime/lib/operation-progress.mjs +3 -1
  49. package/dist/runtime/context.js +1 -1
  50. package/dist/schemas/discovery-result.schema.json +184 -0
  51. package/dist/schemas/execution-bundle.schema.json +205 -0
  52. package/dist/schemas/execution-settings.schema.json +162 -0
  53. package/dist/schemas/flow-descriptor.schema.json +54 -0
  54. package/dist/schemas/flow-run.schema.json +22 -0
  55. package/dist/schemas/harness-catalog.schema.json +80 -0
  56. package/dist/schemas/mb-init-compose-result.schema.json +457 -0
  57. package/dist/schemas/mb-init-discover-result.schema.json +500 -0
  58. package/dist/schemas/mb-init-finish-result.schema.json +422 -0
  59. package/dist/schemas/mb-init-preflight-result.schema.json +367 -0
  60. package/dist/schemas/mb-init-review-result.schema.json +396 -0
  61. package/dist/schemas/mb-init-verify-result.schema.json +391 -0
  62. package/dist/schemas/memory-bootstrap-receipt-v2.schema.json +67 -0
  63. package/dist/schemas/memory-bootstrap-receipt.schema.json +354 -0
  64. package/dist/schemas/memory-composition-work-result.schema.json +38 -0
  65. package/dist/schemas/memory-repair-work-result.schema.json +49 -0
  66. package/dist/schemas/memory-review-correction-plan.schema.json +30 -0
  67. package/dist/schemas/memory-review-work-result.schema.json +43 -0
  68. package/dist/schemas/model-catalog.schema.json +153 -0
  69. package/dist/schemas/profile-settings.schema.json +122 -0
  70. package/dist/schemas/release-manifest.schema.json +291 -0
  71. package/dist/schemas/run-engine-binding.schema.json +1 -1
  72. package/dist/services/check-runner-evidence.js +51 -4
  73. package/dist/services/cli-operation-classifier.js +8 -47
  74. package/dist/services/cli-upgrade-allowlist.js +27 -0
  75. package/dist/services/code-checks.js +387 -34
  76. package/dist/services/controller-execution.js +88 -0
  77. package/dist/services/controller-fanout.js +71 -12
  78. package/dist/services/dashboard.js +8 -2
  79. package/dist/services/descriptor-coordinator-session.js +35 -0
  80. package/dist/services/descriptor-root-work.js +53 -0
  81. package/dist/services/descriptor-run-settlement.js +105 -0
  82. package/dist/services/descriptor-run-start.js +449 -0
  83. package/dist/services/descriptor-stage-coordinator.js +197 -0
  84. package/dist/services/descriptor-stage-settlement.js +222 -0
  85. package/dist/services/descriptor-stage-start.js +139 -0
  86. package/dist/services/discovery/agy-discovery.js +2 -0
  87. package/dist/services/discovery/codex-discovery.js +2 -0
  88. package/dist/services/discovery/droid-discovery.js +2 -0
  89. package/dist/services/discovery/grok-discovery.js +2 -0
  90. package/dist/services/discovery/opencode-discovery.js +2 -0
  91. package/dist/services/discovery/static.js +31 -0
  92. package/dist/services/discovery/zcode-discovery.js +2 -0
  93. package/dist/services/engines.js +39 -22
  94. package/dist/services/eval-snapshots.js +83 -25
  95. package/dist/services/execution-bundles.js +263 -0
  96. package/dist/services/execution-policy.js +20 -16
  97. package/dist/services/executor-onboarding.js +56 -0
  98. package/dist/services/external-work-launch.js +91 -11
  99. package/dist/services/flow-catalog.js +21 -0
  100. package/dist/services/flow-descriptors.js +82 -0
  101. package/dist/services/frozen-engine.js +242 -0
  102. package/dist/services/generic-execution.js +164 -0
  103. package/dist/services/harness-adapter.js +28 -2
  104. package/dist/services/harness-discovery.js +29 -0
  105. package/dist/services/harness-doctor.js +75 -0
  106. package/dist/services/harness-probe-effects.js +51 -0
  107. package/dist/services/harness-version-probe.js +100 -0
  108. package/dist/services/hooks.js +24 -6
  109. package/dist/services/hybrid-contracts.js +483 -0
  110. package/dist/services/ids.js +29 -1
  111. package/dist/services/installation-lock.js +95 -0
  112. package/dist/services/installed-flow-source.js +183 -0
  113. package/dist/services/lifecycle-invocations.js +257 -78
  114. package/dist/services/memory-accepted-composition.js +77 -0
  115. package/dist/services/memory-baseline-preservation.js +65 -0
  116. package/dist/services/memory-completed-correction-wave.js +320 -0
  117. package/dist/services/memory-compose-handler.js +258 -0
  118. package/dist/services/memory-composition-acceptance.js +199 -0
  119. package/dist/services/memory-composition-graph.js +134 -0
  120. package/dist/services/memory-composition-registration.js +146 -0
  121. package/dist/services/memory-composition-work-custody.js +151 -0
  122. package/dist/services/memory-composition-work-results.js +96 -0
  123. package/dist/services/memory-corrected-review-acceptance.js +129 -0
  124. package/dist/services/memory-corrected-review-batch.js +30 -0
  125. package/dist/services/memory-corrected-review-findings.js +106 -0
  126. package/dist/services/memory-corrected-review-inputs.js +210 -0
  127. package/dist/services/memory-corrected-review-projection.js +78 -0
  128. package/dist/services/memory-corrected-review-receipt.js +170 -0
  129. package/dist/services/memory-corrected-review-registration.js +191 -0
  130. package/dist/services/memory-correction-check-receipt.js +219 -0
  131. package/dist/services/memory-correction-input-owner.js +6 -0
  132. package/dist/services/memory-discover-handler.js +104 -0
  133. package/dist/services/memory-finish-evidence.js +435 -0
  134. package/dist/services/memory-finish-handler.js +215 -0
  135. package/dist/services/memory-finish-integration.js +280 -0
  136. package/dist/services/memory-finish-readiness.js +122 -0
  137. package/dist/services/memory-git-integration.js +151 -0
  138. package/dist/services/memory-historical-corrected-review.js +206 -0
  139. package/dist/services/memory-historical-original-review.js +130 -0
  140. package/dist/services/memory-historical-repair-registration.js +99 -0
  141. package/dist/services/memory-historical-second-review.js +128 -0
  142. package/dist/services/memory-init-bootstrap-readiness.js +210 -0
  143. package/dist/services/memory-init-contexts.js +89 -0
  144. package/dist/services/memory-init-controller-start.js +84 -0
  145. package/dist/services/memory-init-engine-release.js +223 -0
  146. package/dist/services/memory-init-git-policy.js +136 -0
  147. package/dist/services/memory-init-lint-capability.js +185 -0
  148. package/dist/services/memory-init-native-capacity.js +241 -0
  149. package/dist/services/memory-init-preflight.js +312 -0
  150. package/dist/services/memory-init-project-bootstrap.js +599 -0
  151. package/dist/services/memory-init-request-candidate.js +157 -0
  152. package/dist/services/memory-init-request-input.js +66 -0
  153. package/dist/services/memory-init-resource-storage.js +125 -0
  154. package/dist/services/memory-init-run-allocation.js +81 -0
  155. package/dist/services/memory-init-run-bundle.js +98 -0
  156. package/dist/services/memory-init-start.js +175 -0
  157. package/dist/services/memory-init-startup-custody.js +80 -0
  158. package/dist/services/memory-init-startup-input.js +240 -0
  159. package/dist/services/memory-init-storage.js +81 -0
  160. package/dist/services/memory-init-stores.js +359 -0
  161. package/dist/services/memory-init-worktrunk-capability.js +172 -0
  162. package/dist/services/memory-layer-publication.js +611 -0
  163. package/dist/services/memory-layer.js +156 -0
  164. package/dist/services/memory-lint-config.js +85 -0
  165. package/dist/services/memory-native-review-copy.js +165 -0
  166. package/dist/services/memory-normal-flow-entry.js +75 -0
  167. package/dist/services/memory-ordinary-defaults.js +112 -0
  168. package/dist/services/memory-preflight-bootstrap-custody.js +115 -0
  169. package/dist/services/memory-preflight-handler.js +259 -0
  170. package/dist/services/memory-repair-current-view.js +299 -0
  171. package/dist/services/memory-repair-document-delta.js +451 -0
  172. package/dist/services/memory-repair-history.js +491 -0
  173. package/dist/services/memory-repair-physical-binding.js +312 -0
  174. package/dist/services/memory-repair-publication-files.js +152 -0
  175. package/dist/services/memory-repair-publication-intent.js +27 -0
  176. package/dist/services/memory-repair-publication-plan.js +160 -0
  177. package/dist/services/memory-repair-publication-source.js +225 -0
  178. package/dist/services/memory-repair-publication.js +794 -0
  179. package/dist/services/memory-repair-registration.js +239 -0
  180. package/dist/services/memory-repair-work-baseline.js +413 -0
  181. package/dist/services/memory-repair-work-completion.js +97 -0
  182. package/dist/services/memory-repair-work-custody.js +73 -0
  183. package/dist/services/memory-repair-work-results.js +89 -0
  184. package/dist/services/memory-repair-workspace.js +252 -0
  185. package/dist/services/memory-repair-writer-copy.js +735 -0
  186. package/dist/services/memory-result-binding.js +173 -0
  187. package/dist/services/memory-retained-review-authority.js +129 -0
  188. package/dist/services/memory-retained-review-results.js +70 -0
  189. package/dist/services/memory-retained-second-baseline.js +102 -0
  190. package/dist/services/memory-retained-second-repairs.js +156 -0
  191. package/dist/services/memory-review-acceptance.js +214 -0
  192. package/dist/services/memory-review-correction-plan.js +171 -0
  193. package/dist/services/memory-review-handler.js +585 -0
  194. package/dist/services/memory-review-inputs.js +200 -0
  195. package/dist/services/memory-review-receipt.js +180 -0
  196. package/dist/services/memory-review-registration.js +108 -0
  197. package/dist/services/memory-review-work-custody.js +124 -0
  198. package/dist/services/memory-review-work-input.js +72 -0
  199. package/dist/services/memory-review-work-results.js +73 -0
  200. package/dist/services/memory-second-corrected-review-inputs.js +165 -0
  201. package/dist/services/memory-second-correction-plan.js +76 -0
  202. package/dist/services/memory-second-repair-baseline.js +91 -0
  203. package/dist/services/memory-second-repair-launch.js +614 -0
  204. package/dist/services/memory-second-repair-registration.js +88 -0
  205. package/dist/services/memory-second-repair-source.js +187 -0
  206. package/dist/services/memory-second-review-acceptance.js +115 -0
  207. package/dist/services/memory-source-observations.js +117 -0
  208. package/dist/services/memory-stage-dispatch.js +28 -0
  209. package/dist/services/memory-stage-results.js +199 -0
  210. package/dist/services/memory-verification-acceptance.js +135 -0
  211. package/dist/services/memory-verification-checks.js +252 -0
  212. package/dist/services/memory-verification-receipt.js +159 -0
  213. package/dist/services/memory-verify-handler.js +135 -0
  214. package/dist/services/memory-work-result-preparation.js +63 -0
  215. package/dist/services/model-catalog.js +109 -0
  216. package/dist/services/model-discovery.js +46 -0
  217. package/dist/services/operator-profiles.js +236 -0
  218. package/dist/services/projects.js +17 -2
  219. package/dist/services/recovery-snapshot-database.js +416 -1
  220. package/dist/services/release-archive-producer.js +52 -0
  221. package/dist/services/release-archive.js +143 -0
  222. package/dist/services/release-cache.js +142 -0
  223. package/dist/services/release-compatibility.js +18 -0
  224. package/dist/services/release-download.js +67 -0
  225. package/dist/services/release-envelope.js +104 -0
  226. package/dist/services/release-manifest.js +45 -0
  227. package/dist/services/release-payload.js +114 -0
  228. package/dist/services/release-producer.js +63 -0
  229. package/dist/services/release-provenance.js +160 -0
  230. package/dist/services/release-registry.js +66 -0
  231. package/dist/services/release-source.js +194 -0
  232. package/dist/services/repair-intents.js +1 -1
  233. package/dist/services/review-copy.js +10 -2
  234. package/dist/services/run-control.js +80 -9
  235. package/dist/services/run-controller-capture.js +10 -3
  236. package/dist/services/run-controller-process.js +21 -0
  237. package/dist/services/run-controller-recovery.js +3 -1
  238. package/dist/services/run-controller-state.js +1 -1
  239. package/dist/services/run-controller.js +101 -40
  240. package/dist/services/run-recovery-guard.js +2 -0
  241. package/dist/services/run-recovery.js +145 -5
  242. package/dist/services/run-start-custody.js +177 -0
  243. package/dist/services/runs.js +319 -25
  244. package/dist/services/setup.js +377 -0
  245. package/dist/services/stage-context.js +9 -1
  246. package/dist/services/stage-handlers.js +74 -0
  247. package/dist/services/stage-report-renderer.js +63 -0
  248. package/dist/services/stage-work-graph.js +17 -0
  249. package/dist/services/vnext-code.js +3 -3
  250. package/dist/services/vnext-execution-profile.js +2 -2
  251. package/dist/services/vnext-fanout.js +70 -9
  252. package/dist/services/vnext-merge.js +8 -3
  253. package/dist/services/vnext-plan-review.js +6 -4
  254. package/dist/services/work-registry.js +616 -71
  255. package/dist/services/workspace-files.js +1 -1
  256. package/dist/storage/database.js +84 -6
  257. package/dist/storage/writer-contract.js +11 -7
  258. package/package.json +13 -3
  259. package/tools/create-canon-release.mjs +33 -0
  260. package/tools/produce-hybrid-release.mjs +41 -0
@@ -0,0 +1,483 @@
1
+ import { createHash } from "node:crypto";
2
+ import fs from "node:fs";
3
+ import { Ajv2020 } from "ajv/dist/2020.js";
4
+ import { Ajv } from "ajv/dist/ajv.js";
5
+ /** Static E1 contracts only. No ingress, dispatch or runtime defaults are changed. */
6
+ export const hybridContractNames = [
7
+ "release-manifest", "harness-catalog", "model-catalog", "discovery-result",
8
+ "profile-settings", "flow-descriptor", "execution-settings",
9
+ "memory-bootstrap-receipt", "execution-bundle"
10
+ ];
11
+ const hybridContractRegistry = new Set(hybridContractNames);
12
+ const record = (input) => input;
13
+ const list = (input) => input;
14
+ const string = (input) => input;
15
+ /** Deterministic JSON content identity, not a signature or secret-derived hash. */
16
+ export function hybridDigest(input) {
17
+ const canonical = (value) => {
18
+ if (Array.isArray(value))
19
+ return value.map(canonical);
20
+ if (value !== null && typeof value === "object") {
21
+ return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b, "en"))
22
+ .map(([key, child]) => [key, canonical(child)]));
23
+ }
24
+ return value;
25
+ };
26
+ return createHash("sha256").update(JSON.stringify(canonical(input))).digest("hex");
27
+ }
28
+ /** E1 standalone validation. Runtime ingress must explicitly integrate it in E2/E3. */
29
+ export function validateHybridContract(name, input, context = {}) {
30
+ if (name === "flow-descriptor") {
31
+ if (!context.descriptorContext)
32
+ return [{ gate: "context", code: "missing_descriptor_context", path: "/" }];
33
+ return validateFlowDescriptor(input, context.descriptorContext);
34
+ }
35
+ const shape = validateHybridShape(name, input);
36
+ if (shape.length)
37
+ return shape;
38
+ const data = record(input), issues = [];
39
+ const fail = (code, path = "/") => issues.push({ gate: "context", code, path });
40
+ const seen = new Set();
41
+ const unique = (key, path) => {
42
+ if (seen.has(key))
43
+ fail("duplicate_identity", path);
44
+ seen.add(key);
45
+ };
46
+ const receipt = (reference, kind, required) => {
47
+ if (reference === null) {
48
+ if (required)
49
+ fail("missing_receipt", `/receipts/${kind}`);
50
+ return;
51
+ }
52
+ const ref = record(reference), fact = context.receipts?.get(string(ref.sha256));
53
+ if (!fact || fact.kind !== kind) {
54
+ fail("unverified_receipt", `/receipts/${kind}`);
55
+ return;
56
+ }
57
+ const expected = context.bootstrapIdentity;
58
+ if (expected && (fact.requestId !== expected.requestId || fact.projectRoot !== expected.projectRoot || fact.workspace !== expected.workspace)) {
59
+ fail("foreign_receipt", `/receipts/${kind}`);
60
+ }
61
+ };
62
+ if (name === "release-manifest") {
63
+ const canon = record(data.canon), engine = record(data.engine);
64
+ if (data.version !== canon.version)
65
+ fail("release_canon_version_mismatch");
66
+ const artifacts = [record(canon.archive), record(engine.archive)];
67
+ if (artifacts[0].url === artifacts[1].url || artifacts[0].sha256 === artifacts[1].sha256)
68
+ fail("conflicting_artifact_identity");
69
+ for (const value of list(data.qualified_routes)) {
70
+ const route = record(value);
71
+ unique([route.platform, route.harness, route.flow, route.delegation].join("/"), "/qualified_routes");
72
+ receipt(route.evidence, "route-qualification", true);
73
+ }
74
+ }
75
+ else if (name === "harness-catalog") {
76
+ const families = new Set(["codex", "zcode", "agy", "opencode", "grok", "droid"]);
77
+ for (const value of list(data.entries)) {
78
+ const entry = record(value), id = string(entry.id);
79
+ unique(id, "/entries");
80
+ families.delete(id);
81
+ if (entry.probe_module !== `${id}-discovery`)
82
+ fail("foreign_probe_module", "/entries");
83
+ }
84
+ if (families.size)
85
+ fail("missing_harness_family", "/entries");
86
+ }
87
+ else if (name === "model-catalog") {
88
+ for (const value of list(data.entries)) {
89
+ const entry = record(value), qualification = record(entry.qualification);
90
+ unique([entry.harness, entry.provider, entry.model].join("/"), "/entries");
91
+ if (qualification.scope === "seed-only") {
92
+ if (qualification.evidence !== null)
93
+ fail("seed_has_qualification_receipt", "/entries");
94
+ }
95
+ else
96
+ receipt(qualification.evidence, `model-${qualification.scope}-qualification`, true);
97
+ }
98
+ }
99
+ else if (name === "discovery-result") {
100
+ if (data.probe_scope === "live")
101
+ receipt(data.live_authorization_receipt, "live-probe-authorization", true);
102
+ else if (data.live_authorization_receipt !== null)
103
+ fail("unexpected_live_authorization");
104
+ for (const value of list(data.entries)) {
105
+ const entry = record(value);
106
+ unique(string(entry.harness), "/entries");
107
+ const selected = list(entry.candidates).filter(candidate => record(candidate).selected === true);
108
+ if (selected.length > 1)
109
+ fail("ambiguous_selected_executable", "/entries");
110
+ if (entry.installed === "yes" && selected.length !== 1)
111
+ fail("missing_selected_executable", "/entries");
112
+ if (entry.installed === "no" && (selected.length || entry.compatible === "yes" || entry.protocol === "verified" || entry.auth === "verified" || entry.qualified === "yes"))
113
+ fail("contradictory_discovery_facts", "/entries");
114
+ if (entry.auth === "verified" || entry.protocol === "verified" || entry.qualified === "yes") {
115
+ if (data.probe_scope === "static" || list(entry.evidence).length === 0)
116
+ fail("unsupported_verified_fact", "/entries");
117
+ for (const evidence of list(entry.evidence))
118
+ receipt(evidence, "discovery-observation", true);
119
+ }
120
+ }
121
+ }
122
+ else if (name === "profile-settings") {
123
+ const profiles = new Map();
124
+ for (const value of list(data.profiles)) {
125
+ const entry = record(value), profile = record(entry.profile), id = string(entry.id);
126
+ unique(id, "/profiles");
127
+ profiles.set(id, profile);
128
+ if (profile.id !== id || hybridDigest(profile) !== entry.revision)
129
+ fail("profile_revision_mismatch", "/profiles");
130
+ }
131
+ const aliases = record(data.aliases);
132
+ for (const [alias, target] of Object.entries(aliases)) {
133
+ if (!profiles.has(string(target)))
134
+ fail("dangling_or_chained_alias", `/aliases/${alias}`);
135
+ if (profiles.has(alias))
136
+ fail("alias_id_collision", `/aliases/${alias}`);
137
+ }
138
+ if (data.default_profile !== null && !profiles.has(string(data.default_profile)) && !Object.hasOwn(aliases, string(data.default_profile)))
139
+ fail("dangling_default", "/default_profile");
140
+ const content = Object.fromEntries(Object.entries(data).filter(([key]) => key !== "revision"));
141
+ if (hybridDigest(content) !== data.revision)
142
+ fail("settings_revision_mismatch", "/revision");
143
+ }
144
+ else if (name === "execution-settings") {
145
+ if (!context.descriptor || !context.descriptorSha256 || !context.profileSettings) {
146
+ fail("missing_execution_context");
147
+ return issues;
148
+ }
149
+ const descriptor = record(context.descriptor), settings = record(context.profileSettings);
150
+ if (validateHybridShape("flow-descriptor", descriptor).length || validateHybridContract("profile-settings", settings).length) {
151
+ fail("invalid_execution_context");
152
+ return issues;
153
+ }
154
+ if (data.flow_id !== descriptor.id || data.descriptor_sha256 !== context.descriptorSha256)
155
+ fail("foreign_descriptor");
156
+ const available = new Map(list(settings.profiles).map(value => { const entry = record(value); return [string(entry.id), entry]; }));
157
+ const frozen = new Set();
158
+ for (const value of list(data.profiles)) {
159
+ const profile = record(value), id = string(profile.id);
160
+ unique(id, "/profiles");
161
+ frozen.add(id);
162
+ if (!available.has(id) || available.get(id).revision !== profile.revision)
163
+ fail("foreign_profile_revision", "/profiles");
164
+ }
165
+ const stageIds = new Set(list(descriptor.stages).map(value => string(record(value).id)));
166
+ for (const stage of Object.keys(record(data.stage_profiles)))
167
+ if (!stageIds.has(stage))
168
+ fail("foreign_stage", `/stage_profiles/${stage}`);
169
+ const routes = [data.root_profile, ...Object.values(record(data.stage_profiles)), ...list(data.work_profiles)].map(string);
170
+ for (const id of routes)
171
+ if (!frozen.has(id))
172
+ fail("unapproved_profile", "/routing");
173
+ if (data.execution === "attached")
174
+ receipt(data.attached_session_receipt, "attached-session-binding", true);
175
+ else if (data.attached_session_receipt !== null)
176
+ fail("unexpected_attached_binding");
177
+ const delegation = record(data.delegation);
178
+ const root = available.get(string(data.root_profile));
179
+ const sameProfile = (id) => {
180
+ const entry = available.get(id);
181
+ if (!entry || !root)
182
+ return false;
183
+ const meaningful = (value) => Object.fromEntries(Object.entries(record(value)).filter(([key]) => key !== "id"));
184
+ return hybridDigest(meaningful(entry.profile)) === hybridDigest(meaningful(root.profile));
185
+ };
186
+ if (data.stage_session_mode === "same_session") {
187
+ for (const route of Object.values(record(data.stage_profiles)).map(string)) {
188
+ if (!sameProfile(route))
189
+ fail("same_session_profile_override", "/stage_profiles");
190
+ }
191
+ }
192
+ if (delegation.mode === "native") {
193
+ receipt(delegation.native_capacity_receipt, "native-capacity", true);
194
+ for (const id of routes)
195
+ if (!sameProfile(id))
196
+ fail("native_profile_override", "/routing");
197
+ if (delegation.native_capacity_receipt !== null) {
198
+ const fact = context.receipts?.get(string(record(delegation.native_capacity_receipt).sha256));
199
+ if (!fact || !root || fact.harness !== record(root.profile).harness || fact.profileRevision !== root.revision
200
+ || !Number.isInteger(fact.capacity) || fact.capacity < 1 || fact.capacity > 15
201
+ || fact.capacity < Number(delegation.max_parallel))
202
+ fail("native_capacity_scope_mismatch", "/delegation");
203
+ }
204
+ }
205
+ else if (delegation.native_capacity_receipt !== null)
206
+ fail("unexpected_native_capacity");
207
+ }
208
+ else if (name === "memory-bootstrap-receipt") {
209
+ const expected = context.bootstrapIdentity;
210
+ if (!expected) {
211
+ fail("missing_bootstrap_context");
212
+ return issues;
213
+ }
214
+ const workspace = record(data.workspace), execution = record(data.effective_execution), storage = record(data.storage);
215
+ if (data.request_id !== expected.requestId || data.project_root !== expected.projectRoot || workspace.path !== expected.workspace || data.baseline_sha256 !== expected.baselineSha256)
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
+ }
225
+ if (execution.engine_sha256 !== expected.engineSha256 || execution.canon_sha256 !== expected.canonSha256)
226
+ fail("foreign_execution_identity");
227
+ receipt(storage.compatibility_receipt, "storage-compatibility", true);
228
+ const noOp = storage.route === "verified-no-op";
229
+ if (noOp) {
230
+ if (storage.outcome !== "no-op")
231
+ fail("invalid_no_op_outcome");
232
+ for (const field of ["drain_receipt", "backup_receipt", "plan_receipt", "apply_receipt", "verification_receipt"])
233
+ if (storage[field] !== null)
234
+ fail("no_op_has_migration_effect");
235
+ }
236
+ else if (storage.outcome === "applied") {
237
+ for (const [field, kind] of [["drain_receipt", "writer-drain"], ["backup_receipt", "storage-backup"], ["plan_receipt", "storage-plan"], ["apply_receipt", "storage-apply"], ["verification_receipt", "storage-verification"]])
238
+ receipt(storage[field], kind, true);
239
+ }
240
+ if (data.readiness === "ready" && !(noOp || storage.outcome === "applied"))
241
+ fail("storage_not_ready");
242
+ if (data.flow_id === "mb-upgrade" && data.project_current === null)
243
+ fail("missing_upgrade_baseline");
244
+ for (const value of list(data.effects)) {
245
+ const effect = record(value);
246
+ receipt(effect.receipt, string(effect.kind), true);
247
+ if (effect.kind === "storage-apply" && (noOp || storage.outcome !== "applied"))
248
+ fail("undeclared_storage_effect");
249
+ }
250
+ }
251
+ else if (name === "execution-bundle") {
252
+ if (!context.blobs || !context.engineChecksum || !context.engineBindingSha256 || !context.engineManifestSha256 || !context.bundleProvenance || !context.descriptorSha256
253
+ || typeof context.bundleProjectRoot !== "string" || context.bundleProjectRoot.length === 0) {
254
+ fail("missing_bundle_context");
255
+ return issues;
256
+ }
257
+ if (record(data.descriptor).sha256 !== context.descriptorSha256)
258
+ fail("foreign_bundle_descriptor", "/descriptor");
259
+ if (hybridDigest(data.canon) !== hybridDigest(context.bundleProvenance.canon))
260
+ fail("foreign_bundle_canon", "/canon");
261
+ if (hybridDigest(data.project_pack) !== hybridDigest(context.bundleProvenance.project_pack))
262
+ fail("foreign_bundle_project_pack", "/project_pack");
263
+ const memoryFlow = data.flow_id === "mb-init" || data.flow_id === "mb-upgrade";
264
+ if (memoryFlow && context.bootstrapIdentity?.projectRoot !== context.bundleProjectRoot)
265
+ fail("foreign_bootstrap_identity", "/bootstrap_receipts");
266
+ if (memoryFlow && list(data.bootstrap_receipts).length === 0)
267
+ fail("missing_memory_bootstrap", "/bootstrap_receipts");
268
+ if (!memoryFlow && list(data.bootstrap_receipts).length > 0)
269
+ fail("foreign_memory_bootstrap", "/bootstrap_receipts");
270
+ const inventory = new Map();
271
+ for (const value of list(data.inventory)) {
272
+ const ref = record(value), path = string(ref.path);
273
+ unique(path, "/inventory");
274
+ inventory.set(path, string(ref.sha256));
275
+ const bytes = context.blobs.get(path);
276
+ if (!bytes)
277
+ fail("missing_bundle_blob", `/inventory/${path}`);
278
+ else if (createHash("sha256").update(bytes).digest("hex") !== ref.sha256)
279
+ fail("bundle_checksum_mismatch", `/inventory/${path}`);
280
+ }
281
+ for (const reference of [data.descriptor, data.execution_settings, data.profile_settings, data.engine_binding, data.engine_manifest, ...list(data.bootstrap_receipts)]) {
282
+ const ref = record(reference);
283
+ if (inventory.get(string(ref.path)) !== ref.sha256)
284
+ fail("unlisted_bundle_reference");
285
+ }
286
+ if (record(data.engine_binding).sha256 !== context.engineBindingSha256)
287
+ fail("foreign_engine_binding");
288
+ if (record(data.engine_manifest).sha256 !== context.engineManifestSha256)
289
+ fail("foreign_engine_manifest");
290
+ const loaded = (reference) => {
291
+ const bytes = context.blobs.get(string(record(reference).path));
292
+ try {
293
+ return bytes ? JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)) : null;
294
+ }
295
+ catch {
296
+ fail("invalid_bundle_json");
297
+ return null;
298
+ }
299
+ };
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
+ }
311
+ const binding = loaded(data.engine_binding);
312
+ const bindingSchema = JSON.parse(fs.readFileSync(new URL("../schemas/run-engine-binding.schema.json", import.meta.url), "utf8"));
313
+ const bindingValidator = new Ajv({ strict: false, validateFormats: false }).compile(bindingSchema);
314
+ const bindingValid = bindingValidator(binding);
315
+ if (!bindingValid)
316
+ fail("invalid_engine_binding");
317
+ else if (record(binding).run_id !== data.run_id || record(binding).project_root !== context.bundleProjectRoot
318
+ || record(record(binding).engine).integrity_checksum !== context.engineChecksum)
319
+ fail("foreign_engine_binding");
320
+ const manifest = loaded(data.engine_manifest);
321
+ const manifestSchema = JSON.parse(fs.readFileSync(new URL("../schemas/engine-manifest.schema.json", import.meta.url), "utf8"));
322
+ const manifestValidator = new Ajv({ strict: false, validateFormats: false }).compile(manifestSchema);
323
+ if (!manifestValidator(manifest))
324
+ fail("invalid_engine_manifest");
325
+ else if (bindingValid) {
326
+ const selected = record(manifest), engine = record(record(binding).engine), integrity = record(selected.integrity);
327
+ if (["package_name", "package_version", "engine_version", "snapshot_root"].some(key => selected[key] !== engine[key])
328
+ || integrity.mode !== "full_content" || integrity.checksum !== engine.integrity_checksum)
329
+ fail("foreign_engine_manifest");
330
+ }
331
+ if (!descriptor || !settings || !profiles)
332
+ fail("missing_bundle_contract");
333
+ else {
334
+ if (record(descriptor).id !== data.flow_id || record(settings).flow_id !== data.flow_id)
335
+ fail("foreign_bundle_flow");
336
+ const retained = new Map([...context.blobs].filter(([path]) => inventory.has(path)));
337
+ const descriptorContext = context.descriptorContext
338
+ ? { ...context.descriptorContext, blobs: retained } : undefined;
339
+ const bundleContext = descriptorContext ? { ...context, descriptorContext } : context;
340
+ issues.push(...validateHybridContract("flow-descriptor", descriptor, bundleContext));
341
+ issues.push(...validateHybridContract("profile-settings", profiles, context));
342
+ issues.push(...validateHybridContract("execution-settings", settings, { ...context, descriptor,
343
+ descriptorSha256: string(record(data.descriptor).sha256), profileSettings: profiles }));
344
+ for (const reference of list(data.bootstrap_receipts)) {
345
+ const bootstrap = loaded(reference);
346
+ if (!bootstrap)
347
+ fail("missing_bundle_bootstrap");
348
+ else {
349
+ if (record(bootstrap).flow_id !== data.flow_id || record(bootstrap).readiness !== "ready")
350
+ fail("bundle_bootstrap_not_admitted");
351
+ issues.push(...validateHybridContract("memory-bootstrap-receipt", bootstrap, context));
352
+ }
353
+ }
354
+ }
355
+ }
356
+ else
357
+ fail("unknown_hybrid_contract");
358
+ return issues;
359
+ }
360
+ export function validateHybridShape(name, input) {
361
+ if (!hybridContractRegistry.has(name)) {
362
+ return [{ gate: "shape", code: "unknown_contract_name", path: "/" }];
363
+ }
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);
367
+ const schema = JSON.parse(fs.readFileSync(schemaUrl, "utf8"));
368
+ const ajv = new Ajv2020({ allErrors: true, strict: true, validateFormats: false });
369
+ const validate = ajv.compile(schema);
370
+ if (validate(input))
371
+ return [];
372
+ return (validate.errors ?? []).map(error => ({
373
+ gate: "shape", code: error.keyword, path: error.instancePath || "/"
374
+ }));
375
+ }
376
+ /** Context must come from the trusted materializer, never from the descriptor. */
377
+ export function validateFlowDescriptor(input, context) {
378
+ const shape = validateHybridShape("flow-descriptor", input);
379
+ if (shape.length)
380
+ return shape;
381
+ const descriptor = input;
382
+ const issues = [];
383
+ const fail = (code, path) => issues.push({ gate: "context", code, path });
384
+ const ids = new Set(), directories = new Set();
385
+ const outgoing = new Map(), incoming = new Map();
386
+ for (const [index, stage] of descriptor.stages.entries()) {
387
+ if (ids.has(stage.id))
388
+ fail("duplicate_stage", `/stages/${index}/id`);
389
+ if (directories.has(stage.directory))
390
+ fail("duplicate_directory", `/stages/${index}/directory`);
391
+ ids.add(stage.id);
392
+ directories.add(stage.directory);
393
+ outgoing.set(stage.id, []);
394
+ incoming.set(stage.id, []);
395
+ if (!context.entryHandlers.has(stage.entry_handler))
396
+ fail("unknown_entry_handler", `/stages/${index}/entry_handler`);
397
+ if (!context.finishHandlers.has(stage.finish_handler))
398
+ fail("unknown_finish_handler", `/stages/${index}/finish_handler`);
399
+ for (const field of ["instruction", "result_schema"]) {
400
+ const reference = stage[field], bytes = context.blobs.get(reference.path);
401
+ if (!bytes) {
402
+ fail("missing_blob", `/stages/${index}/${field}`);
403
+ continue;
404
+ }
405
+ if (createHash("sha256").update(bytes).digest("hex") !== reference.sha256) {
406
+ fail("blob_checksum_mismatch", `/stages/${index}/${field}`);
407
+ continue;
408
+ }
409
+ if (field === "result_schema") {
410
+ try {
411
+ const schema = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
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);
420
+ }
421
+ catch {
422
+ fail("invalid_result_schema", `/stages/${index}/${field}`);
423
+ }
424
+ }
425
+ }
426
+ }
427
+ if (!ids.has(descriptor.entry_stage))
428
+ fail("foreign_entry", "/entry_stage");
429
+ for (const terminal of descriptor.terminal_stages) {
430
+ if (!ids.has(terminal))
431
+ fail("foreign_terminal", "/terminal_stages");
432
+ }
433
+ const transitions = new Set();
434
+ for (const [index, edge] of descriptor.edges.entries()) {
435
+ const transition = `${edge.from}/${edge.on}`;
436
+ if (transitions.has(transition))
437
+ fail("ambiguous_transition", `/edges/${index}`);
438
+ transitions.add(transition);
439
+ if (!ids.has(edge.from) || !ids.has(edge.to)) {
440
+ fail("foreign_edge", `/edges/${index}`);
441
+ continue;
442
+ }
443
+ outgoing.get(edge.from).push(edge.to);
444
+ incoming.get(edge.to).push(edge.from);
445
+ if (descriptor.terminal_stages.includes(edge.from))
446
+ fail("terminal_outgoing", `/edges/${index}`);
447
+ }
448
+ const closure = (seeds, graph) => {
449
+ const seen = new Set(), pending = [...seeds];
450
+ while (pending.length) {
451
+ const id = pending.pop();
452
+ if (seen.has(id) || !ids.has(id))
453
+ continue;
454
+ seen.add(id);
455
+ pending.push(...(graph.get(id) ?? []));
456
+ }
457
+ return seen;
458
+ };
459
+ const reachable = closure([descriptor.entry_stage], outgoing);
460
+ const canFinish = closure(descriptor.terminal_stages, incoming);
461
+ for (const id of ids) {
462
+ if (!reachable.has(id))
463
+ fail("unreachable_stage", `/stages/${id}`);
464
+ if (!canFinish.has(id))
465
+ fail("no_terminal_path", `/stages/${id}`);
466
+ }
467
+ const degrees = new Map([...ids].map(id => [id, incoming.get(id).length]));
468
+ const ready = [...ids].filter(id => degrees.get(id) === 0);
469
+ let visited = 0;
470
+ while (ready.length) {
471
+ const id = ready.pop();
472
+ visited++;
473
+ for (const next of outgoing.get(id)) {
474
+ const count = degrees.get(next) - 1;
475
+ degrees.set(next, count);
476
+ if (count === 0)
477
+ ready.push(next);
478
+ }
479
+ }
480
+ if (visited !== ids.size)
481
+ fail("cyclic_graph", "/edges");
482
+ return issues;
483
+ }
@@ -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);
@@ -0,0 +1,95 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ import { randomUUID } from "node:crypto";
5
+ import { AppError } from "../shared/errors.js";
6
+ function refuse(reason) {
7
+ throw new AppError("installation_locked", "Installation owner requires reconciliation", 1, { reason, effect: "no_effect", help: "setup", next_action: "Replay the original exact request after its owner exits" });
8
+ }
9
+ function readOwner(file) {
10
+ if (!fs.lstatSync(file).isFile())
11
+ refuse("foreign_lock_object");
12
+ let owner;
13
+ try {
14
+ owner = JSON.parse(fs.readFileSync(file, "utf8"));
15
+ }
16
+ catch {
17
+ return refuse("unreadable_lock_owner");
18
+ }
19
+ if (!owner || owner.schema_id !== "dd-flow/installation-lock@1" || !/^[a-f0-9]{64}$/.test(owner.identity) ||
20
+ owner.host !== os.hostname() || !Number.isSafeInteger(owner.pid) || owner.pid < 1 || !/^[a-f0-9-]{36}$/.test(owner.token))
21
+ refuse("unverified_lock_owner");
22
+ return owner;
23
+ }
24
+ function isDead(pid) {
25
+ try {
26
+ process.kill(pid, 0);
27
+ return false;
28
+ }
29
+ catch (error) {
30
+ return error.code === "ESRCH";
31
+ }
32
+ }
33
+ /** Publish a complete owner record atomically; only the same request can reclaim a kernel-confirmed dead owner. */
34
+ export function acquireInstallationLock(file, identity) {
35
+ if (!/^[a-f0-9]{64}$/.test(identity))
36
+ refuse("invalid_lock_identity");
37
+ const owner = { schema_id: "dd-flow/installation-lock@1", identity, host: os.hostname(), pid: process.pid, token: randomUUID() };
38
+ const temporary = path.join(path.dirname(file), `.lock-owner-${owner.token}.tmp`);
39
+ const fd = fs.openSync(temporary, "wx", 0o600);
40
+ try {
41
+ try {
42
+ fs.writeFileSync(fd, JSON.stringify(owner));
43
+ fs.fsyncSync(fd);
44
+ }
45
+ finally {
46
+ fs.closeSync(fd);
47
+ }
48
+ for (let attempt = 0; attempt < 3; attempt++) {
49
+ try {
50
+ fs.linkSync(temporary, file);
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();
58
+ fs.unlinkSync(file);
59
+ } };
60
+ }
61
+ catch (error) {
62
+ if (error.code !== "EEXIST")
63
+ throw error;
64
+ const previous = readOwner(file);
65
+ if (previous.identity !== identity)
66
+ refuse("different_request_owns_lock");
67
+ if (!isDead(previous.pid))
68
+ refuse("owner_process_alive_or_unknown");
69
+ // Atomic quarantine retains crash evidence. A competing reclaimer must retry, not overwrite an owner.
70
+ const quarantine = `${file}.abandoned-${previous.token}`;
71
+ if (fs.existsSync(quarantine))
72
+ refuse("ambiguous_abandoned_lock");
73
+ const before = fs.lstatSync(file);
74
+ const checked = readOwner(file);
75
+ if (checked.token !== previous.token || !isDead(checked.pid))
76
+ refuse("lock_owner_changed");
77
+ try {
78
+ fs.linkSync(file, quarantine);
79
+ }
80
+ catch {
81
+ return refuse("concurrent_lock_reconciliation");
82
+ }
83
+ const after = fs.lstatSync(file);
84
+ if (before.ino !== after.ino || before.dev !== after.dev || readOwner(file).token !== previous.token)
85
+ refuse("lock_owner_changed");
86
+ fs.unlinkSync(file);
87
+ }
88
+ }
89
+ return refuse("lock_contention");
90
+ }
91
+ finally {
92
+ if (fs.existsSync(temporary))
93
+ fs.unlinkSync(temporary);
94
+ }
95
+ }