@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,242 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { spawnSync } from "node:child_process";
5
+ import { Ajv } from "ajv/dist/ajv.js";
6
+ import { AppError } from "../shared/errors.js";
7
+ import { engineSnapshotChecksum } from "./engines.js";
8
+ import { readBoundedReleaseFile } from "./release-source.js";
9
+ import { shellArgument } from "./runtime-command.js";
10
+ import { hybridDigest } from "./hybrid-contracts.js";
11
+ import { isMemoryInitResourceStorageFacts } from "./memory-init-resource-storage.js";
12
+ import { memoryInitStartupInputSchemas } from "./memory-init-startup-custody.js";
13
+ function storageProbeRefuse(reason) {
14
+ throw new AppError("frozen_engine_storage_incompatible", "Selected frozen engine cannot admit memory-init storage", 1, { reason, effect: "no_effect" });
15
+ }
16
+ const hash = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex");
17
+ function refuse(reason) {
18
+ throw new AppError("frozen_engine_invalid", "Frozen engine cannot be admitted", 1, { reason, phase: "prepare", effect: "no_effect" });
19
+ }
20
+ function json(bytes) {
21
+ try {
22
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
23
+ }
24
+ catch {
25
+ return refuse("invalid_utf8_json");
26
+ }
27
+ }
28
+ function shape(name, value, property) {
29
+ const schema = JSON.parse(fs.readFileSync(new URL(`../schemas/${name}.schema.json`, import.meta.url), "utf8"));
30
+ return new Ajv({ strict: false, validateFormats: false }).compile(property ? schema.properties[property] : schema)(value) === true;
31
+ }
32
+ function containedFile(root, relative) {
33
+ if (path.isAbsolute(relative) || relative.split(/[\\/]/).some(part => part === ".." || part === "." || !part))
34
+ refuse("escaping_engine_path");
35
+ const parts = relative.split(/[\\/]/);
36
+ let current = root;
37
+ for (const [index, part] of parts.entries()) {
38
+ current = path.join(current, part);
39
+ const stat = fs.lstatSync(current);
40
+ if (stat.isSymbolicLink() || (index === parts.length - 1 ? !stat.isFile() : !stat.isDirectory()))
41
+ refuse("non_regular_engine_path");
42
+ }
43
+ return current;
44
+ }
45
+ /** New-bundle admission only. It does not replace historical binding or storage admission. */
46
+ export class AdmittedFrozenEngine {
47
+ manifestSha256;
48
+ entrypoint;
49
+ nodeExecutable;
50
+ #manifest;
51
+ #bytes;
52
+ constructor(manifest, bytes, entrypoint) {
53
+ this.#manifest = manifest;
54
+ this.#bytes = bytes;
55
+ this.manifestSha256 = hash(bytes);
56
+ this.entrypoint = entrypoint;
57
+ this.nodeExecutable = fs.realpathSync(process.execPath);
58
+ Object.freeze(this);
59
+ }
60
+ /** Both hashes are independently selected by the RUN/bundle authority. No subprocess probe. */
61
+ static admit(binding, expectedManifestSha256) {
62
+ if (!shape("run-engine-binding", binding) || !/^[a-f0-9]{64}$/.test(expectedManifestSha256))
63
+ refuse("invalid_binding_identity");
64
+ return AdmittedFrozenEngine.fromIdentity(binding.engine, expectedManifestSha256);
65
+ }
66
+ /** Pre-RUN selection: no invented RUN id, binding or allocation authority. */
67
+ static admitSelection(identity, expectedManifestSha256) {
68
+ if (!shape("run-engine-binding", identity, "engine") || !/^[a-f0-9]{64}$/.test(expectedManifestSha256))
69
+ refuse("invalid_selection_identity");
70
+ return AdmittedFrozenEngine.fromIdentity(identity, expectedManifestSha256);
71
+ }
72
+ static fromIdentity(identity, expectedManifestSha256) {
73
+ try {
74
+ const root = path.resolve(identity.snapshot_root);
75
+ if (!path.isAbsolute(identity.snapshot_root) || !fs.lstatSync(root).isDirectory())
76
+ refuse("missing_or_linked_snapshot");
77
+ const bytes = readBoundedReleaseFile(path.join(root, "engine.json"), 1024 * 1024);
78
+ if (hash(bytes) !== expectedManifestSha256)
79
+ refuse("manifest_bytes_changed");
80
+ const value = json(bytes);
81
+ if (!shape("engine-manifest", value))
82
+ refuse("invalid_manifest_shape");
83
+ const manifest = value;
84
+ if (manifest.package_name !== identity.package_name || manifest.package_version !== identity.package_version
85
+ || manifest.engine_version !== identity.engine_version || path.resolve(manifest.snapshot_root) !== root
86
+ || manifest.integrity.mode !== "full_content" || manifest.integrity.checksum !== identity.integrity_checksum)
87
+ refuse("foreign_manifest_identity");
88
+ if (engineSnapshotChecksum(root) !== identity.integrity_checksum)
89
+ refuse("executable_snapshot_changed");
90
+ const entrypoint = containedFile(root, manifest.entrypoint);
91
+ if (!manifest.schema_registry || manifest.schema_registry.entries.length === 0)
92
+ refuse("missing_schema_registry");
93
+ const names = new Set(), paths = new Set();
94
+ for (const entry of manifest.schema_registry.entries) {
95
+ if (names.has(entry.name) || paths.has(entry.path))
96
+ refuse("ambiguous_schema_registry");
97
+ names.add(entry.name);
98
+ paths.add(entry.path);
99
+ const schemaBytes = readBoundedReleaseFile(containedFile(root, entry.path), 1024 * 1024);
100
+ if (hash(schemaBytes) !== entry.checksum || json(schemaBytes)?.$id !== entry.id)
101
+ refuse("schema_registry_identity_changed");
102
+ }
103
+ return new AdmittedFrozenEngine(manifest, bytes, entrypoint);
104
+ }
105
+ catch (error) {
106
+ if (error instanceof AppError && error.code === "frozen_engine_invalid")
107
+ throw error;
108
+ return refuse("snapshot_unavailable_or_unreadable");
109
+ }
110
+ }
111
+ manifest() { return structuredClone(this.#manifest); }
112
+ manifestBytes() { return Buffer.from(this.#bytes); }
113
+ /** Qualify actual descriptor handlers in this immutable executable, not the
114
+ * invoking CLI catalog. No storage or productive operation is admitted. */
115
+ admitMemoryInitFlow(source, startupInputSchema) {
116
+ const fail = (reason) => {
117
+ throw new AppError("frozen_engine_flow_incompatible", "Selected frozen engine cannot admit memory-init handlers", 1, { reason, phase: "prepare", effect: "no_effect" });
118
+ };
119
+ const revalidate = () => {
120
+ try {
121
+ if (hash(readBoundedReleaseFile(path.join(this.#manifest.snapshot_root, "engine.json"), 1024 * 1024)) !== this.manifestSha256
122
+ || engineSnapshotChecksum(this.#manifest.snapshot_root) !== this.#manifest.integrity.checksum)
123
+ fail("engine_changed_during_admission");
124
+ // Historical checksums exclude every engine.json path, including a
125
+ // nested directory. New memory execution must not admit unhashed code;
126
+ // do not change the checksum algorithm used by existing SDLC RUNs.
127
+ if (fs.readdirSync(this.#manifest.snapshot_root, { recursive: true, withFileTypes: true })
128
+ .some(entry => entry.name === "engine.json" && path.resolve(entry.parentPath) !== this.#manifest.snapshot_root))
129
+ fail("unhashed_engine_path");
130
+ }
131
+ catch (error) {
132
+ if (error instanceof AppError && error.code === "frozen_engine_flow_incompatible")
133
+ throw error;
134
+ fail("engine_unavailable_during_admission");
135
+ }
136
+ };
137
+ if (startupInputSchema !== undefined && !memoryInitStartupInputSchemas.includes(startupInputSchema))
138
+ fail("unsupported_startup_input_schema");
139
+ const input = Buffer.from(JSON.stringify({ source: source.snapshot(), source_sha256: source.sha256,
140
+ ...(startupInputSchema === undefined ? {} : { startup_input_schema: startupInputSchema }) }));
141
+ if (input.length > 64 * 1024 * 1024)
142
+ fail("unbounded_source");
143
+ const expected = { schema_id: startupInputSchema === undefined ? "dd-flow/memory-init-engine-flow@1" : "dd-flow/memory-init-engine-flow@2",
144
+ ...(startupInputSchema === undefined ? {} : { startup_input_schema: startupInputSchema }),
145
+ flow_id: "mb-init", source_sha256: source.sha256,
146
+ descriptor_sha256: source.descriptorReference().sha256, stages: source.resolveRegisteredFlow().descriptor.stages.map(stage => stage.id) };
147
+ revalidate();
148
+ const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !["NODE_OPTIONS", "NODE_PATH"].includes(key) && !key.startsWith("DD_FLOW_")));
149
+ const result = spawnSync(this.nodeExecutable, [this.entrypoint, "runtime", "flow", "admit", "--operation", "memory-init", "--json"], { input, cwd: this.#manifest.snapshot_root, env: { ...env, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: this.#manifest.snapshot_root },
150
+ timeout: 15000, maxBuffer: 1024 * 1024, shell: false, windowsHide: true });
151
+ if (result.error || result.signal || result.status !== 0)
152
+ fail("selected_engine_probe_failed");
153
+ revalidate();
154
+ let response;
155
+ try {
156
+ response = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(result.stdout));
157
+ }
158
+ catch {
159
+ return fail("invalid_probe_json");
160
+ }
161
+ if (!response || typeof response !== "object" || Array.isArray(response) || Object.keys(response).sort().join() !== "action,facts,ok,sha256"
162
+ || response.ok !== true || response.action !== "runtime_flow_admit" || !response.facts || typeof response.facts !== "object" || Array.isArray(response.facts)
163
+ || hybridDigest(response.facts) !== hybridDigest(expected)
164
+ || response.sha256 !== hybridDigest(expected))
165
+ fail("invalid_probe_response");
166
+ return { facts: structuredClone(expected), sha256: hybridDigest(expected), engine_manifest_sha256: this.manifestSha256,
167
+ engine_sha256: this.#manifest.integrity.checksum };
168
+ }
169
+ /** Execute admission in the selected immutable engine, never in the invoking CLI.
170
+ * This qualifies storage connections only, not provider or flow readiness.
171
+ */
172
+ admitMemoryInitStorage(home, resourceHome) {
173
+ for (const root of [home, resourceHome]) {
174
+ if (!path.isAbsolute(root) || path.resolve(root) !== root || fs.realpathSync(root) !== root)
175
+ storageProbeRefuse("noncanonical_storage_home");
176
+ }
177
+ const revalidateEngine = () => {
178
+ if (hash(readBoundedReleaseFile(path.join(this.#manifest.snapshot_root, "engine.json"), 1024 * 1024)) !== this.manifestSha256
179
+ || engineSnapshotChecksum(this.#manifest.snapshot_root) !== this.#manifest.integrity.checksum)
180
+ storageProbeRefuse("engine_changed_during_admission");
181
+ };
182
+ revalidateEngine();
183
+ const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !["NODE_OPTIONS", "NODE_PATH"].includes(key)));
184
+ const result = spawnSync(this.nodeExecutable, [this.entrypoint, "runtime", "storage", "admit", "--operation", "memory-init", "--json"], { env: { ...env, DD_FLOW_HOME: home, DD_FLOW_RESOURCE_HOME: resourceHome, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: this.#manifest.snapshot_root },
185
+ timeout: 15000, maxBuffer: 1024 * 1024, shell: false, windowsHide: true });
186
+ if (result.error || result.signal || result.status !== 0)
187
+ storageProbeRefuse("selected_engine_probe_failed");
188
+ revalidateEngine();
189
+ let response;
190
+ try {
191
+ response = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(result.stdout));
192
+ }
193
+ catch {
194
+ return storageProbeRefuse("invalid_probe_json");
195
+ }
196
+ if (!response || typeof response !== "object" || Array.isArray(response) || Object.keys(response).sort().join() !== "action,facts,ok,sha256"
197
+ || response.ok !== true || response.action !== "runtime_storage_admit")
198
+ storageProbeRefuse("invalid_probe_response");
199
+ const facts = response.facts, run = facts?.run;
200
+ if (!facts || Object.keys(facts).sort().join() !== "operation,outcome,resource,route,run,schema_id"
201
+ || facts.schema_id !== "dd-flow/memory-init-engine-storage@1" || facts.operation !== "memory-init"
202
+ || facts.route !== "verified-no-op" || facts.outcome !== "no-op" || response.sha256 !== hybridDigest(facts)
203
+ || !isMemoryInitResourceStorageFacts(facts.resource) || facts.resource.store_path !== path.join(resourceHome, "runtime.sqlite")
204
+ || !run || Object.keys(run).sort().join() !== "executing_writer_version,operation,outcome,route,schema_id,schema_sha256,store_dev,store_ino,store_kind,store_path"
205
+ || run.schema_id !== "dd-flow/memory-init-runtime-storage@1" || run.operation !== "memory-init" || run.store_kind !== "run"
206
+ || run.store_path !== path.join(home, "db.sqlite") || run.route !== "verified-no-op" || run.outcome !== "no-op"
207
+ || run.executing_writer_version !== 2 || typeof run.schema_sha256 !== "string" || !/^[a-f0-9]{64}$/.test(run.schema_sha256))
208
+ storageProbeRefuse("invalid_probe_facts");
209
+ for (const store of [run, facts.resource]) {
210
+ const stat = fs.lstatSync(store.store_path);
211
+ if (!stat.isFile() || stat.nlink !== 1 || stat.dev !== store.store_dev || stat.ino !== store.store_ino)
212
+ storageProbeRefuse("storage_identity_changed");
213
+ }
214
+ return { facts: structuredClone(facts), sha256: response.sha256, engine_manifest_sha256: this.manifestSha256,
215
+ engine_sha256: this.#manifest.integrity.checksum };
216
+ }
217
+ /** Read an exact registered schema from this snapshot, never the mutable CLI. */
218
+ schema(name) {
219
+ try {
220
+ if (!fs.lstatSync(this.#manifest.snapshot_root).isDirectory())
221
+ refuse("missing_or_linked_snapshot");
222
+ const entry = this.#manifest.schema_registry?.entries.find(item => item.name === name);
223
+ if (!entry)
224
+ refuse("schema_not_registered");
225
+ const bytes = readBoundedReleaseFile(containedFile(this.#manifest.snapshot_root, entry.path), 1024 * 1024);
226
+ if (hash(bytes) !== entry.checksum || json(bytes).$id !== entry.id)
227
+ refuse("schema_registry_identity_changed");
228
+ return Buffer.from(bytes);
229
+ }
230
+ catch (error) {
231
+ if (error instanceof AppError && error.code === "frozen_engine_invalid")
232
+ throw error;
233
+ return refuse("schema_unavailable");
234
+ }
235
+ }
236
+ /** No PATH/dd-flow fallback or mutable launcher environment reference. */
237
+ command(home, args) {
238
+ if (!path.isAbsolute(home))
239
+ refuse("runtime_home_not_absolute");
240
+ return `DD_FLOW_HOME=${shellArgument(home)} DD_FLOW_ENGINE_MODE=1 DD_FLOW_ENGINE_HOME=${shellArgument(this.#manifest.snapshot_root)} ${[this.nodeExecutable, this.entrypoint, ...args].map(shellArgument).join(" ")}`;
241
+ }
242
+ }
@@ -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
  }
@@ -0,0 +1,29 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { validateHybridContract } from "./hybrid-contracts.js";
3
+ import { AppError } from "../shared/errors.js";
4
+ import { discoverCodex } from "./discovery/codex-discovery.js";
5
+ import { discoverZcode } from "./discovery/zcode-discovery.js";
6
+ import { discoverAgy } from "./discovery/agy-discovery.js";
7
+ import { discoverOpenCode } from "./discovery/opencode-discovery.js";
8
+ import { discoverGrok } from "./discovery/grok-discovery.js";
9
+ import { discoverDroid } from "./discovery/droid-discovery.js";
10
+ import { readOnlyProbeScope, runScopedProbe, staticDiscoveryEffects } from "./harness-probe-effects.js";
11
+ const modules = { codex: discoverCodex, zcode: discoverZcode, agy: discoverAgy, opencode: discoverOpenCode, grok: discoverGrok, droid: discoverDroid };
12
+ export const harnessCatalog = {
13
+ schema_id: "dd-flow/harness-catalog@1", version: "1.0.0",
14
+ entries: Object.keys(modules).map(id => ({ id, executable_candidates: [id], probe_module: `${id}-discovery`,
15
+ probe_timeout_ms: 3000, version_range: "unknown_requires_review", protocol_version: "unknown_requires_protocol_probe" }))
16
+ };
17
+ export function discoverHarnesses(input) {
18
+ if (input.harness && !Object.hasOwn(modules, input.harness))
19
+ throw new AppError("usage", "Unknown harness family", 2, { effect: "no_effect" });
20
+ if (input.executable && !input.harness)
21
+ throw new AppError("usage", "Explicit executable requires one harness family", 2, { effect: "no_effect" });
22
+ const families = input.harness ? [input.harness] : Object.keys(modules);
23
+ const result = { schema_id: "dd-flow/discovery-result@1", request_id: input.requestId ?? randomUUID(), observed_at: new Date().toISOString(),
24
+ probe_scope: "static", live_authorization_receipt: null, entries: families.map(family => runScopedProbe(staticDiscoveryEffects, readOnlyProbeScope, () => modules[family](input))) };
25
+ if (validateHybridContract("harness-catalog", harnessCatalog).length || validateHybridContract("discovery-result", result).length) {
26
+ throw new AppError("discovery_contract", "Discovery owner produced invalid facts", 1, { effect: "no_effect" });
27
+ }
28
+ return result;
29
+ }
@@ -0,0 +1,75 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { parse } from "semver";
4
+ import { AppError } from "../shared/errors.js";
5
+ import { legacyDoctorEffects, runScopedProbe } from "./harness-probe-effects.js";
6
+ import { readBoundedReleaseFile } from "./release-source.js";
7
+ import { discoverExecutable } from "./discovery/static.js";
8
+ const binOptions = {
9
+ codex: "codex-bin", zcode: "zcode-acp-bin", agy: "agy-bin", opencode: "opencode-bin", grok: "grok-bin", droid: "droid-bin"
10
+ };
11
+ function refuse(code, message, exitCode = 1) {
12
+ throw new AppError(code, message, exitCode, { phase: "harness_doctor", help: "harnesses doctor" });
13
+ }
14
+ /** Existing non-generative adapter doctor, bounded by its owner transport.
15
+ * Reported compatibility is not qualification or verified account access.
16
+ * OpenCode observes an explicitly bound existing daemon; it never starts one.
17
+ */
18
+ export async function probeHarnessDoctor(input) {
19
+ const timeoutMs = input.timeoutMs ?? 30_000;
20
+ if (!Object.hasOwn(binOptions, input.harness) || !path.isAbsolute(input.executable)
21
+ || !Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 60_000
22
+ || input.stateDir !== undefined && !path.isAbsolute(input.stateDir)
23
+ || input.nativeExecutable !== undefined && (!path.isAbsolute(input.nativeExecutable) || input.harness !== "zcode")
24
+ || input.stateDir !== undefined && input.harness !== "opencode")
25
+ refuse("usage", "Doctor requires explicit contained diagnostic inputs", 2);
26
+ return runScopedProbe(legacyDoctorEffects[input.harness], input.scope, async () => {
27
+ if (input.harness === "droid" && discoverExecutable("system-ps", ["ps"], { env: { ...process.env, ...input.env } }).candidates.length === 0) {
28
+ throw new AppError("doctor_prerequisite_missing", "Droid doctor requires the OS ps utility for physical process settlement", 1, { phase: "harness_doctor", effect: "no_effect", prerequisite: "ps", help: "harnesses doctor" });
29
+ }
30
+ const args = [fileURLToPath(new URL(`../harness-runtime/bin/dd-${input.harness}.mjs`, import.meta.url)), "doctor",
31
+ `--${binOptions[input.harness]}`, input.executable, "--timeout-ms", String(timeoutMs), "--json"];
32
+ if (input.harness === "zcode") {
33
+ if (!input.nativeExecutable)
34
+ refuse("harness_selection_required", "ZCode doctor also requires --native-executable for its native runtime");
35
+ args.push("--zcode-path", input.nativeExecutable);
36
+ }
37
+ if (input.harness === "opencode") {
38
+ if (!input.stateDir)
39
+ refuse("harness_selection_required", "OpenCode doctor requires --state-dir for an existing owned daemon; no service is started");
40
+ let state;
41
+ try {
42
+ state = JSON.parse(readBoundedReleaseFile(path.join(input.stateDir, "daemon.json"), 1024 * 1024).toString("utf8"));
43
+ }
44
+ catch {
45
+ return refuse("doctor_state_unavailable", "Selected daemon identity cannot be read");
46
+ }
47
+ const selected = state;
48
+ if (selected?.config?.bin !== input.executable)
49
+ refuse("doctor_state_mismatch", "Selected daemon does not bind the discovered executable");
50
+ args.push("--state-dir", input.stateDir);
51
+ }
52
+ const transport = await import(new URL("../harness-runtime/lib/process-json.mjs", import.meta.url).href);
53
+ let report;
54
+ try {
55
+ report = await transport.commandJson(process.execPath, args, { env: input.env, timeoutMs, signal: AbortSignal.timeout(timeoutMs), phase: "native-doctor" });
56
+ }
57
+ catch {
58
+ return refuse("harness_doctor_failed", "Adapter doctor failed or could not confirm its observation; no model Turn was requested");
59
+ }
60
+ if (!report || typeof report !== "object" || Array.isArray(report) || report.ok !== true)
61
+ refuse("harness_doctor_failed", "Adapter doctor did not return an accepted diagnostic");
62
+ const observed = report.observed_runtime && typeof report.observed_runtime === "object" ? report.observed_runtime : {};
63
+ const versionField = input.harness === "codex" ? "codex_cli" : input.harness === "zcode" ? "zcode_acp" : input.harness;
64
+ const versions = report.versions && typeof report.versions === "object" && !Array.isArray(report.versions) ? report.versions : {};
65
+ // Existing OpenCode doctor reports versions separately from observed_runtime.
66
+ const versionValue = input.harness === "opencode" ? versions.opencode : observed[versionField];
67
+ const version = typeof versionValue === "string" ? parse(versionValue)?.version ?? null : null;
68
+ const protocol = observed.dd_harness_contract;
69
+ const models = Array.isArray(report.models) ? report.models.filter((model) => typeof model === "string" && /^[A-Za-z0-9][A-Za-z0-9._:/+-]{0,255}$/.test(model)).slice(0, 10_000) : [];
70
+ return { harness: input.harness, effects: legacyDoctorEffects[input.harness], reported_version: version,
71
+ reported_protocol: typeof protocol === "string" && /^dd-[a-z0-9-]+@[0-9]+$/.test(protocol) ? protocol : null,
72
+ reported_models: [...new Set(models)].sort(), models_complete: false, reported_compatible: typeof report.compatible === "boolean" ? report.compatible : null,
73
+ account_access: "unknown", qualified: "unknown", model_turn: "not_requested" };
74
+ });
75
+ }
@@ -0,0 +1,51 @@
1
+ import { AppError } from "../shared/errors.js";
2
+ export const readOnlyProbeScope = Object.freeze({
3
+ processes: false, services: false, credentials: false, writes: false
4
+ });
5
+ /** Source-grounded effects of existing adapters; this is not permission to invoke them. */
6
+ export const legacyDoctorEffects = {
7
+ codex: { child_process: true, service_process: true, credential_access: "possible", filesystem: "temporary_or_runtime_writes", model_turn: "not_requested", source_owner: "harness-runtime/lib/dd-codex.mjs:doctor/withBridge" },
8
+ zcode: { child_process: true, service_process: false, credential_access: "possible", filesystem: "read_only", model_turn: "not_requested", source_owner: "harness-runtime/lib/dd-zcode.mjs:doctor" },
9
+ agy: { child_process: true, service_process: false, credential_access: "possible", filesystem: "temporary_or_runtime_writes", model_turn: "not_requested", source_owner: "harness-runtime/lib/dd-agy.mjs:doctor" },
10
+ opencode: { child_process: true, service_process: true, credential_access: "possible", filesystem: "temporary_or_runtime_writes", model_turn: "not_requested", source_owner: "harness-runtime/lib/dd-opencode-daemon.mjs:daemon.doctor/startDaemon" },
11
+ grok: { child_process: true, service_process: false, credential_access: "possible", filesystem: "read_only", model_turn: "not_requested", source_owner: "harness-runtime/lib/dd-grok.mjs:doctor" },
12
+ droid: { child_process: true, service_process: true, credential_access: "possible", filesystem: "temporary_or_runtime_writes", model_turn: "not_requested", source_owner: "harness-runtime/lib/dd-droid-daemon.mjs:runtime.start" }
13
+ };
14
+ for (const effects of Object.values(legacyDoctorEffects))
15
+ Object.freeze(effects);
16
+ Object.freeze(legacyDoctorEffects);
17
+ export const staticDiscoveryEffects = Object.freeze({
18
+ child_process: false, service_process: false, credential_access: "none", filesystem: "read_only", model_turn: "not_requested",
19
+ source_owner: "services/discovery/static.ts:discoverExecutable"
20
+ });
21
+ /** Enforce effect ceilings before invoking a probe. A non-generative label alone is insufficient. */
22
+ export function assertProbeWithinScope(effects, scope) {
23
+ const scopeKeys = ["processes", "services", "credentials", "writes"];
24
+ const effectKeys = ["child_process", "service_process", "credential_access", "filesystem", "model_turn", "source_owner"];
25
+ if (!scope || typeof scope !== "object" || Array.isArray(scope)
26
+ || Object.keys(scope).some(key => !scopeKeys.includes(key))
27
+ || scopeKeys.some(key => !Object.hasOwn(scope, key) || typeof scope[key] !== "boolean")
28
+ || !effects || typeof effects !== "object" || Array.isArray(effects)
29
+ || effectKeys.some(key => !Object.hasOwn(effects, key)) || Object.keys(effects).some(key => !effectKeys.includes(key))
30
+ || typeof effects.child_process !== "boolean" || typeof effects.service_process !== "boolean"
31
+ || !["none", "possible"].includes(effects.credential_access)
32
+ || !["read_only", "temporary_or_runtime_writes"].includes(effects.filesystem)
33
+ || !["not_requested", "possible"].includes(effects.model_turn)
34
+ || typeof effects.source_owner !== "string" || !effects.source_owner.trim()) {
35
+ throw new AppError("probe_scope_invalid", "Probe effects and permissions require an explicit typed scope", 2, { phase: "probe_admission", effect: "no_effect", help: "harnesses" });
36
+ }
37
+ const blocked = [
38
+ effects.child_process && !scope.processes ? "child_process" : null,
39
+ effects.service_process && !scope.services ? "service_process" : null,
40
+ effects.credential_access !== "none" && !scope.credentials ? "credential_access" : null,
41
+ effects.filesystem !== "read_only" && !scope.writes ? "filesystem_writes" : null,
42
+ effects.model_turn !== "not_requested" ? "live_authorization_required" : null
43
+ ].filter(Boolean);
44
+ if (blocked.length)
45
+ throw new AppError("probe_effects_not_authorized", "Probe exceeds the selected effect scope", 1, { phase: "probe_admission", effect: "no_effect", blocked_effects: blocked, help: "harnesses" });
46
+ }
47
+ /** The callback is not entered, including synchronous effects, until admission succeeds. */
48
+ export function runScopedProbe(effects, scope, probe) {
49
+ assertProbeWithinScope(effects, scope);
50
+ return probe();
51
+ }