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

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 (161) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +33 -0
  3. package/dist/build-info.json +7 -7
  4. package/dist/cli/command-inputs.js +1 -0
  5. package/dist/cli/command-metadata.js +144 -0
  6. package/dist/cli/command-option-descriptions.js +186 -0
  7. package/dist/cli/completion-cli.js +70 -0
  8. package/dist/cli/completion.js +57 -0
  9. package/dist/cli/error-output.js +52 -0
  10. package/dist/cli/executor-onboarding.js +71 -0
  11. package/dist/cli/harnesses.js +84 -0
  12. package/dist/cli/models.js +39 -0
  13. package/dist/cli/operator-profiles.js +61 -0
  14. package/dist/cli/output-options.js +35 -0
  15. package/dist/cli/response-file.js +69 -0
  16. package/dist/cli/run-cli.js +5 -0
  17. package/dist/cli/setup.js +147 -0
  18. package/dist/harness-runtime/OPENCODE-LICENSE.txt +24 -0
  19. package/dist/harness-runtime/bin/dd-opencode.mjs +1 -1
  20. package/dist/harness-runtime/lib/codex-capacity-policy.mjs +13 -0
  21. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +193 -39
  22. package/dist/harness-runtime/lib/dd-opencode.mjs +295 -100
  23. package/dist/harness-runtime/lib/delegation-instructions.mjs +3 -3
  24. package/dist/harness-runtime/lib/managed-daemon.mjs +1 -1
  25. package/dist/harness-runtime/lib/native-hook-command.d.mts +2 -2
  26. package/dist/harness-runtime/lib/native-hook-command.mjs +4 -4
  27. package/dist/harness-runtime/lib/opencode-plugin.mjs +60 -0
  28. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-1y4a697q.js +81 -0
  29. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-2ny4vxct.js +92 -0
  30. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-6mptkzbc.js +0 -0
  31. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-7x67cf1q.js +52 -0
  32. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-9rqn6x4v.js +16 -0
  33. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-9vdz0zwn.js +1661 -0
  34. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-ecs5ftka.js +1466 -0
  35. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-frbwqjmf.js +145 -0
  36. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-gne7h7mq.js +50 -0
  37. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-gx9wnkzr.js +105 -0
  38. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-m2eywc8w.js +0 -0
  39. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-nwcpakzx.js +12 -0
  40. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-q4g3afnw.js +623 -0
  41. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-qav6mfdz.js +0 -0
  42. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-sxt88q69.js +12 -0
  43. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-t1xa31vm.js +39 -0
  44. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-ve4aws36.js +103 -0
  45. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-vmytr4q8.js +4 -0
  46. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-vt9jbch8.js +36 -0
  47. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-vyas3vgt.js +0 -0
  48. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-wjyeq25y.js +30 -0
  49. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-xgnj2w4v.js +20 -0
  50. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-y2etg5bs.js +9 -0
  51. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-ydhz1zrh.js +238 -0
  52. package/dist/harness-runtime/vendor/opencode-client/chunks/service-probe-z6qdwcsf.js +62 -0
  53. package/dist/harness-runtime/vendor/opencode-client/contract.d.ts +1 -0
  54. package/dist/harness-runtime/vendor/opencode-client/contract.js +12 -0
  55. package/dist/harness-runtime/vendor/opencode-client/effect/api/api.d.ts +2806 -0
  56. package/dist/harness-runtime/vendor/opencode-client/effect/api/api.js +0 -0
  57. package/dist/harness-runtime/vendor/opencode-client/effect/api.d.ts +4 -0
  58. package/dist/harness-runtime/vendor/opencode-client/effect/api.js +0 -0
  59. package/dist/harness-runtime/vendor/opencode-client/effect/client.d.ts +2530 -0
  60. package/dist/harness-runtime/vendor/opencode-client/effect/client.js +15 -0
  61. package/dist/harness-runtime/vendor/opencode-client/effect/generated/client-error.d.ts +7 -0
  62. package/dist/harness-runtime/vendor/opencode-client/effect/generated/client-error.js +6 -0
  63. package/dist/harness-runtime/vendor/opencode-client/effect/generated/client.d.ts +2529 -0
  64. package/dist/harness-runtime/vendor/opencode-client/effect/generated/client.js +8 -0
  65. package/dist/harness-runtime/vendor/opencode-client/effect/generated/index.d.ts +2 -0
  66. package/dist/harness-runtime/vendor/opencode-client/effect/generated/index.js +12 -0
  67. package/dist/harness-runtime/vendor/opencode-client/effect/index.d.ts +35 -0
  68. package/dist/harness-runtime/vendor/opencode-client/effect/index.js +75 -0
  69. package/dist/harness-runtime/vendor/opencode-client/effect/rpc.d.ts +20 -0
  70. package/dist/harness-runtime/vendor/opencode-client/effect/rpc.js +11 -0
  71. package/dist/harness-runtime/vendor/opencode-client/effect/service.d.ts +90 -0
  72. package/dist/harness-runtime/vendor/opencode-client/effect/service.js +187 -0
  73. package/dist/harness-runtime/vendor/opencode-client/package.json +84 -0
  74. package/dist/harness-runtime/vendor/opencode-client/promise/api.d.ts +21 -0
  75. package/dist/harness-runtime/vendor/opencode-client/promise/api.js +0 -0
  76. package/dist/harness-runtime/vendor/opencode-client/promise/client.d.ts +252 -0
  77. package/dist/harness-runtime/vendor/opencode-client/promise/client.js +14 -0
  78. package/dist/harness-runtime/vendor/opencode-client/promise/generated/client-error.d.ts +8 -0
  79. package/dist/harness-runtime/vendor/opencode-client/promise/generated/client-error.js +6 -0
  80. package/dist/harness-runtime/vendor/opencode-client/promise/generated/client.d.ts +259 -0
  81. package/dist/harness-runtime/vendor/opencode-client/promise/generated/client.js +7 -0
  82. package/dist/harness-runtime/vendor/opencode-client/promise/generated/index.d.ts +3 -0
  83. package/dist/harness-runtime/vendor/opencode-client/promise/generated/index.js +79 -0
  84. package/dist/harness-runtime/vendor/opencode-client/promise/generated/types.d.ts +8932 -0
  85. package/dist/harness-runtime/vendor/opencode-client/promise/generated/types.js +70 -0
  86. package/dist/harness-runtime/vendor/opencode-client/promise/index.d.ts +6 -0
  87. package/dist/harness-runtime/vendor/opencode-client/promise/index.js +83 -0
  88. package/dist/harness-runtime/vendor/opencode-client/promise/rpc.d.ts +32 -0
  89. package/dist/harness-runtime/vendor/opencode-client/promise/rpc.js +7 -0
  90. package/dist/harness-runtime/vendor/opencode-client/promise/service.d.ts +24 -0
  91. package/dist/harness-runtime/vendor/opencode-client/promise/service.js +187 -0
  92. package/dist/harness-runtime/vendor/opencode-client/pty-handoff.d.ts +12 -0
  93. package/dist/harness-runtime/vendor/opencode-client/pty-handoff.js +14 -0
  94. package/dist/harness-runtime/vendor/opencode-client/rpc-runtime.d.ts +20 -0
  95. package/dist/harness-runtime/vendor/opencode-client/rpc-runtime.js +14 -0
  96. package/dist/harness-runtime/vendor/opencode-client/service-contender.d.ts +37 -0
  97. package/dist/harness-runtime/vendor/opencode-client/service-contender.js +10 -0
  98. package/dist/harness-runtime/vendor/opencode-client/service-probe.d.ts +70 -0
  99. package/dist/harness-runtime/vendor/opencode-client/service-probe.js +16 -0
  100. package/dist/harness-runtime/vendor/opencode-client/service-timing.d.ts +12 -0
  101. package/dist/harness-runtime/vendor/opencode-client/service-timing.js +10 -0
  102. package/dist/harness-runtime/vendor/opencode-client/service-version.d.ts +2 -0
  103. package/dist/harness-runtime/vendor/opencode-client/service-version.js +6 -0
  104. package/dist/harness-runtime/vendor/opencode-client/service.d.ts +52 -0
  105. package/dist/harness-runtime/vendor/opencode-client/service.js +1 -0
  106. package/dist/harness-runtime/vendor/opencode-client/shared-events.d.ts +11 -0
  107. package/dist/harness-runtime/vendor/opencode-client/shared-events.js +8 -0
  108. package/dist/harness-runtime/vendor/opencode-client/solid/connection.d.ts +37 -0
  109. package/dist/harness-runtime/vendor/opencode-client/solid/connection.js +10 -0
  110. package/dist/harness-runtime/vendor/opencode-client/solid/data.d.ts +211 -0
  111. package/dist/harness-runtime/vendor/opencode-client/solid/data.js +18 -0
  112. package/dist/harness-runtime/vendor/opencode-client/solid/index.d.ts +3 -0
  113. package/dist/harness-runtime/vendor/opencode-client/solid/index.js +32 -0
  114. package/dist/harness-runtime/vendor/opencode-client/solid/pty.d.ts +22 -0
  115. package/dist/harness-runtime/vendor/opencode-client/solid/pty.js +8 -0
  116. package/dist/harness-runtime/vendor/opencode-plugin.mjs +3 -0
  117. package/dist/services/cli-operation-classifier.js +4 -0
  118. package/dist/services/cli-upgrade-allowlist.js +27 -0
  119. package/dist/services/discovery/agy-discovery.js +2 -0
  120. package/dist/services/discovery/codex-discovery.js +2 -0
  121. package/dist/services/discovery/droid-discovery.js +2 -0
  122. package/dist/services/discovery/grok-discovery.js +2 -0
  123. package/dist/services/discovery/opencode-discovery.js +2 -0
  124. package/dist/services/discovery/static.js +31 -0
  125. package/dist/services/discovery/zcode-discovery.js +2 -0
  126. package/dist/services/engines.js +23 -25
  127. package/dist/services/eval-snapshots.js +1 -1
  128. package/dist/services/executor-onboarding.js +56 -0
  129. package/dist/services/external-work-launch.js +1 -1
  130. package/dist/services/harness-discovery.js +29 -0
  131. package/dist/services/harness-doctor.js +75 -0
  132. package/dist/services/harness-probe-effects.js +51 -0
  133. package/dist/services/harness-version-probe.js +100 -0
  134. package/dist/services/hooks.js +1 -1
  135. package/dist/services/hybrid-contracts.js +455 -0
  136. package/dist/services/installation-lock.js +92 -0
  137. package/dist/services/managed-processes.js +30 -0
  138. package/dist/services/merge-server.js +1 -1
  139. package/dist/services/model-catalog.js +109 -0
  140. package/dist/services/model-discovery.js +46 -0
  141. package/dist/services/operator-profiles.js +236 -0
  142. package/dist/services/release-archive-producer.js +52 -0
  143. package/dist/services/release-archive.js +143 -0
  144. package/dist/services/release-cache.js +142 -0
  145. package/dist/services/release-compatibility.js +18 -0
  146. package/dist/services/release-download.js +67 -0
  147. package/dist/services/release-envelope.js +104 -0
  148. package/dist/services/release-manifest.js +45 -0
  149. package/dist/services/release-payload.js +114 -0
  150. package/dist/services/release-producer.js +63 -0
  151. package/dist/services/release-provenance.js +160 -0
  152. package/dist/services/release-registry.js +66 -0
  153. package/dist/services/release-source.js +194 -0
  154. package/dist/services/run-controller-recovery.js +2 -2
  155. package/dist/services/run-controller.js +3 -2
  156. package/dist/services/run-health.js +149 -0
  157. package/dist/services/runs.js +61 -13
  158. package/dist/services/setup.js +377 -0
  159. package/dist/storage/database.js +18 -3
  160. package/dist/storage/writer-contract.js +8 -0
  161. package/package.json +4 -1
@@ -0,0 +1,455 @@
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 < Number(delegation.max_parallel))
201
+ fail("native_capacity_scope_mismatch", "/delegation");
202
+ }
203
+ }
204
+ else if (delegation.native_capacity_receipt !== null)
205
+ fail("unexpected_native_capacity");
206
+ }
207
+ else if (name === "memory-bootstrap-receipt") {
208
+ const expected = context.bootstrapIdentity;
209
+ if (!expected) {
210
+ fail("missing_bootstrap_context");
211
+ return issues;
212
+ }
213
+ const workspace = record(data.workspace), execution = record(data.effective_execution), storage = record(data.storage);
214
+ if (data.request_id !== expected.requestId || data.project_root !== expected.projectRoot || workspace.path !== expected.workspace || data.baseline_sha256 !== expected.baselineSha256)
215
+ fail("foreign_bootstrap_identity");
216
+ if (execution.engine_sha256 !== expected.engineSha256 || execution.canon_sha256 !== expected.canonSha256)
217
+ fail("foreign_execution_identity");
218
+ receipt(storage.compatibility_receipt, "storage-compatibility", true);
219
+ const noOp = storage.route === "verified-no-op";
220
+ if (noOp) {
221
+ if (storage.outcome !== "no-op")
222
+ fail("invalid_no_op_outcome");
223
+ for (const field of ["drain_receipt", "backup_receipt", "plan_receipt", "apply_receipt", "verification_receipt"])
224
+ if (storage[field] !== null)
225
+ fail("no_op_has_migration_effect");
226
+ }
227
+ else if (storage.outcome === "applied") {
228
+ 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"]])
229
+ receipt(storage[field], kind, true);
230
+ }
231
+ if (data.readiness === "ready" && !(noOp || storage.outcome === "applied"))
232
+ fail("storage_not_ready");
233
+ if (data.flow_id === "mb-upgrade" && data.project_current === null)
234
+ fail("missing_upgrade_baseline");
235
+ for (const value of list(data.effects)) {
236
+ const effect = record(value);
237
+ receipt(effect.receipt, string(effect.kind), true);
238
+ if (effect.kind === "storage-apply" && (noOp || storage.outcome !== "applied"))
239
+ fail("undeclared_storage_effect");
240
+ }
241
+ }
242
+ else if (name === "execution-bundle") {
243
+ if (!context.blobs || !context.engineChecksum || !context.engineBindingSha256 || !context.engineManifestSha256 || !context.bundleProvenance || !context.descriptorSha256
244
+ || typeof context.bundleProjectRoot !== "string" || context.bundleProjectRoot.length === 0) {
245
+ fail("missing_bundle_context");
246
+ return issues;
247
+ }
248
+ if (record(data.descriptor).sha256 !== context.descriptorSha256)
249
+ fail("foreign_bundle_descriptor", "/descriptor");
250
+ if (hybridDigest(data.canon) !== hybridDigest(context.bundleProvenance.canon))
251
+ fail("foreign_bundle_canon", "/canon");
252
+ if (hybridDigest(data.project_pack) !== hybridDigest(context.bundleProvenance.project_pack))
253
+ fail("foreign_bundle_project_pack", "/project_pack");
254
+ const memoryFlow = data.flow_id === "mb-init" || data.flow_id === "mb-upgrade";
255
+ if (memoryFlow && context.bootstrapIdentity?.projectRoot !== context.bundleProjectRoot)
256
+ fail("foreign_bootstrap_identity", "/bootstrap_receipts");
257
+ if (memoryFlow && list(data.bootstrap_receipts).length === 0)
258
+ fail("missing_memory_bootstrap", "/bootstrap_receipts");
259
+ if (!memoryFlow && list(data.bootstrap_receipts).length > 0)
260
+ fail("foreign_memory_bootstrap", "/bootstrap_receipts");
261
+ const inventory = new Map();
262
+ for (const value of list(data.inventory)) {
263
+ const ref = record(value), path = string(ref.path);
264
+ unique(path, "/inventory");
265
+ inventory.set(path, string(ref.sha256));
266
+ const bytes = context.blobs.get(path);
267
+ if (!bytes)
268
+ fail("missing_bundle_blob", `/inventory/${path}`);
269
+ else if (createHash("sha256").update(bytes).digest("hex") !== ref.sha256)
270
+ fail("bundle_checksum_mismatch", `/inventory/${path}`);
271
+ }
272
+ for (const reference of [data.descriptor, data.execution_settings, data.profile_settings, data.engine_binding, data.engine_manifest, ...list(data.bootstrap_receipts)]) {
273
+ const ref = record(reference);
274
+ if (inventory.get(string(ref.path)) !== ref.sha256)
275
+ fail("unlisted_bundle_reference");
276
+ }
277
+ if (record(data.engine_binding).sha256 !== context.engineBindingSha256)
278
+ fail("foreign_engine_binding");
279
+ if (record(data.engine_manifest).sha256 !== context.engineManifestSha256)
280
+ fail("foreign_engine_manifest");
281
+ const loaded = (reference) => {
282
+ const bytes = context.blobs.get(string(record(reference).path));
283
+ try {
284
+ return bytes ? JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)) : null;
285
+ }
286
+ catch {
287
+ fail("invalid_bundle_json");
288
+ return null;
289
+ }
290
+ };
291
+ const descriptor = loaded(data.descriptor), settings = loaded(data.execution_settings), profiles = loaded(data.profile_settings);
292
+ const binding = loaded(data.engine_binding);
293
+ const bindingSchema = JSON.parse(fs.readFileSync(new URL("../schemas/run-engine-binding.schema.json", import.meta.url), "utf8"));
294
+ const bindingValidator = new Ajv({ strict: false, validateFormats: false }).compile(bindingSchema);
295
+ const bindingValid = bindingValidator(binding);
296
+ if (!bindingValid)
297
+ fail("invalid_engine_binding");
298
+ else if (record(binding).run_id !== data.run_id || record(binding).project_root !== context.bundleProjectRoot
299
+ || record(record(binding).engine).integrity_checksum !== context.engineChecksum)
300
+ fail("foreign_engine_binding");
301
+ const manifest = loaded(data.engine_manifest);
302
+ const manifestSchema = JSON.parse(fs.readFileSync(new URL("../schemas/engine-manifest.schema.json", import.meta.url), "utf8"));
303
+ const manifestValidator = new Ajv({ strict: false, validateFormats: false }).compile(manifestSchema);
304
+ if (!manifestValidator(manifest))
305
+ fail("invalid_engine_manifest");
306
+ else if (bindingValid) {
307
+ const selected = record(manifest), engine = record(record(binding).engine), integrity = record(selected.integrity);
308
+ if (["package_name", "package_version", "engine_version", "snapshot_root"].some(key => selected[key] !== engine[key])
309
+ || integrity.mode !== "full_content" || integrity.checksum !== engine.integrity_checksum)
310
+ fail("foreign_engine_manifest");
311
+ }
312
+ if (!descriptor || !settings || !profiles)
313
+ fail("missing_bundle_contract");
314
+ else {
315
+ if (record(descriptor).id !== data.flow_id || record(settings).flow_id !== data.flow_id)
316
+ fail("foreign_bundle_flow");
317
+ const retained = new Map([...context.blobs].filter(([path]) => inventory.has(path)));
318
+ const descriptorContext = context.descriptorContext
319
+ ? { ...context.descriptorContext, blobs: retained } : undefined;
320
+ const bundleContext = descriptorContext ? { ...context, descriptorContext } : context;
321
+ issues.push(...validateHybridContract("flow-descriptor", descriptor, bundleContext));
322
+ issues.push(...validateHybridContract("profile-settings", profiles, context));
323
+ issues.push(...validateHybridContract("execution-settings", settings, { ...context, descriptor,
324
+ descriptorSha256: string(record(data.descriptor).sha256), profileSettings: profiles }));
325
+ for (const reference of list(data.bootstrap_receipts)) {
326
+ const bootstrap = loaded(reference);
327
+ if (!bootstrap)
328
+ fail("missing_bundle_bootstrap");
329
+ else {
330
+ if (record(bootstrap).flow_id !== data.flow_id || record(bootstrap).readiness !== "ready")
331
+ fail("bundle_bootstrap_not_admitted");
332
+ issues.push(...validateHybridContract("memory-bootstrap-receipt", bootstrap, context));
333
+ }
334
+ }
335
+ }
336
+ }
337
+ else
338
+ fail("unknown_hybrid_contract");
339
+ return issues;
340
+ }
341
+ export function validateHybridShape(name, input) {
342
+ if (!hybridContractRegistry.has(name)) {
343
+ return [{ gate: "shape", code: "unknown_contract_name", path: "/" }];
344
+ }
345
+ const schemaUrl = new URL(`../schemas/${name}.schema.json`, import.meta.url);
346
+ const schema = JSON.parse(fs.readFileSync(schemaUrl, "utf8"));
347
+ const ajv = new Ajv2020({ allErrors: true, strict: true, validateFormats: false });
348
+ const validate = ajv.compile(schema);
349
+ if (validate(input))
350
+ return [];
351
+ return (validate.errors ?? []).map(error => ({
352
+ gate: "shape", code: error.keyword, path: error.instancePath || "/"
353
+ }));
354
+ }
355
+ /** Context must come from the trusted materializer, never from the descriptor. */
356
+ export function validateFlowDescriptor(input, context) {
357
+ const shape = validateHybridShape("flow-descriptor", input);
358
+ if (shape.length)
359
+ return shape;
360
+ const descriptor = input;
361
+ const issues = [];
362
+ const fail = (code, path) => issues.push({ gate: "context", code, path });
363
+ const ids = new Set(), directories = new Set();
364
+ const outgoing = new Map(), incoming = new Map();
365
+ for (const [index, stage] of descriptor.stages.entries()) {
366
+ if (ids.has(stage.id))
367
+ fail("duplicate_stage", `/stages/${index}/id`);
368
+ if (directories.has(stage.directory))
369
+ fail("duplicate_directory", `/stages/${index}/directory`);
370
+ ids.add(stage.id);
371
+ directories.add(stage.directory);
372
+ outgoing.set(stage.id, []);
373
+ incoming.set(stage.id, []);
374
+ if (!context.entryHandlers.has(stage.entry_handler))
375
+ fail("unknown_entry_handler", `/stages/${index}/entry_handler`);
376
+ if (!context.finishHandlers.has(stage.finish_handler))
377
+ fail("unknown_finish_handler", `/stages/${index}/finish_handler`);
378
+ for (const field of ["instruction", "result_schema"]) {
379
+ const reference = stage[field], bytes = context.blobs.get(reference.path);
380
+ if (!bytes) {
381
+ fail("missing_blob", `/stages/${index}/${field}`);
382
+ continue;
383
+ }
384
+ if (createHash("sha256").update(bytes).digest("hex") !== reference.sha256) {
385
+ fail("blob_checksum_mismatch", `/stages/${index}/${field}`);
386
+ continue;
387
+ }
388
+ if (field === "result_schema") {
389
+ try {
390
+ const schema = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
391
+ new Ajv2020({ strict: true, validateFormats: false }).compile(schema);
392
+ }
393
+ catch {
394
+ fail("invalid_result_schema", `/stages/${index}/${field}`);
395
+ }
396
+ }
397
+ }
398
+ }
399
+ if (!ids.has(descriptor.entry_stage))
400
+ fail("foreign_entry", "/entry_stage");
401
+ for (const terminal of descriptor.terminal_stages) {
402
+ if (!ids.has(terminal))
403
+ fail("foreign_terminal", "/terminal_stages");
404
+ }
405
+ const transitions = new Set();
406
+ for (const [index, edge] of descriptor.edges.entries()) {
407
+ const transition = `${edge.from}/${edge.on}`;
408
+ if (transitions.has(transition))
409
+ fail("ambiguous_transition", `/edges/${index}`);
410
+ transitions.add(transition);
411
+ if (!ids.has(edge.from) || !ids.has(edge.to)) {
412
+ fail("foreign_edge", `/edges/${index}`);
413
+ continue;
414
+ }
415
+ outgoing.get(edge.from).push(edge.to);
416
+ incoming.get(edge.to).push(edge.from);
417
+ if (descriptor.terminal_stages.includes(edge.from))
418
+ fail("terminal_outgoing", `/edges/${index}`);
419
+ }
420
+ const closure = (seeds, graph) => {
421
+ const seen = new Set(), pending = [...seeds];
422
+ while (pending.length) {
423
+ const id = pending.pop();
424
+ if (seen.has(id) || !ids.has(id))
425
+ continue;
426
+ seen.add(id);
427
+ pending.push(...(graph.get(id) ?? []));
428
+ }
429
+ return seen;
430
+ };
431
+ const reachable = closure([descriptor.entry_stage], outgoing);
432
+ const canFinish = closure(descriptor.terminal_stages, incoming);
433
+ for (const id of ids) {
434
+ if (!reachable.has(id))
435
+ fail("unreachable_stage", `/stages/${id}`);
436
+ if (!canFinish.has(id))
437
+ fail("no_terminal_path", `/stages/${id}`);
438
+ }
439
+ const degrees = new Map([...ids].map(id => [id, incoming.get(id).length]));
440
+ const ready = [...ids].filter(id => degrees.get(id) === 0);
441
+ let visited = 0;
442
+ while (ready.length) {
443
+ const id = ready.pop();
444
+ visited++;
445
+ for (const next of outgoing.get(id)) {
446
+ const count = degrees.get(next) - 1;
447
+ degrees.set(next, count);
448
+ if (count === 0)
449
+ ready.push(next);
450
+ }
451
+ }
452
+ if (visited !== ids.size)
453
+ fail("cyclic_graph", "/edges");
454
+ return issues;
455
+ }
@@ -0,0 +1,92 @@
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
+ return { token: owner.token, release: () => {
52
+ const retained = readOwner(file);
53
+ if (retained.token !== owner.token || retained.pid !== process.pid || retained.identity !== identity)
54
+ refuse("lock_owner_changed");
55
+ fs.unlinkSync(file);
56
+ } };
57
+ }
58
+ catch (error) {
59
+ if (error.code !== "EEXIST")
60
+ throw error;
61
+ const previous = readOwner(file);
62
+ if (previous.identity !== identity)
63
+ refuse("different_request_owns_lock");
64
+ if (!isDead(previous.pid))
65
+ refuse("owner_process_alive_or_unknown");
66
+ // Atomic quarantine retains crash evidence. A competing reclaimer must retry, not overwrite an owner.
67
+ const quarantine = `${file}.abandoned-${previous.token}`;
68
+ if (fs.existsSync(quarantine))
69
+ refuse("ambiguous_abandoned_lock");
70
+ const before = fs.lstatSync(file);
71
+ const checked = readOwner(file);
72
+ if (checked.token !== previous.token || !isDead(checked.pid))
73
+ refuse("lock_owner_changed");
74
+ try {
75
+ fs.linkSync(file, quarantine);
76
+ }
77
+ catch {
78
+ return refuse("concurrent_lock_reconciliation");
79
+ }
80
+ const after = fs.lstatSync(file);
81
+ if (before.ino !== after.ino || before.dev !== after.dev || readOwner(file).token !== previous.token)
82
+ refuse("lock_owner_changed");
83
+ fs.unlinkSync(file);
84
+ }
85
+ }
86
+ return refuse("lock_contention");
87
+ }
88
+ finally {
89
+ if (fs.existsSync(temporary))
90
+ fs.unlinkSync(temporary);
91
+ }
92
+ }
@@ -289,6 +289,36 @@ export function processIsAlive(record) {
289
289
  const started = processStartedAt(record.pid);
290
290
  return !record.pid_started_at || !started || record.pid_started_at === started;
291
291
  }
292
+ /** Unlike an admission guard, observability must distinguish unknown from
293
+ * alive. Unknown identity still cannot authorize termination or finalization. */
294
+ export function managedProcessObservation(record) {
295
+ if (!record.pid || !record.pid_started_at)
296
+ return 'unknown';
297
+ try {
298
+ process.kill(record.pid, 0);
299
+ }
300
+ catch (error) {
301
+ return error.code === 'ESRCH' ? 'absent' : 'unknown';
302
+ }
303
+ const reply = spawnSync('ps', ['-o', 'lstart=,stat=', '-p', String(record.pid)], { encoding: 'utf8', timeout: 1000, env: { ...process.env, TZ: 'UTC', LC_ALL: 'C' } });
304
+ if (reply.status !== 0 || !reply.stdout.trim())
305
+ return 'unknown';
306
+ const parts = reply.stdout.trim().split(/\s+/), state = parts.pop();
307
+ if (state?.startsWith('Z'))
308
+ return 'absent';
309
+ const metadata = parseMetadata(record.metadata_json);
310
+ if (Object.hasOwn(metadata, 'process_birth')) {
311
+ const expected = parseBirth(metadata.process_birth);
312
+ if (!expected)
313
+ return 'unknown';
314
+ const observed = `ps-lstart-utc-c@1:${parts.join(' ')}`;
315
+ return expected.replace(/\s+/g, ' ') === observed ? 'alive' : 'absent';
316
+ }
317
+ // Legacy lstart lacks a recorded timezone. A mismatch might be timezone
318
+ // drift, so only equal identity or a missing PID establishes an outcome.
319
+ const legacy = processStartedAt(record.pid);
320
+ return legacy && legacy === record.pid_started_at ? 'alive' : 'unknown';
321
+ }
292
322
  export function processTreeIsAlive(record) {
293
323
  if (processIsAlive(record))
294
324
  return true;
@@ -164,7 +164,7 @@ async function dispatch(context, input) {
164
164
  prompted = await adapterCall(["session", "prompt", "--session-id", sessionId, "--prompt-file", promptFile, ...common], null, `MERGE ${input.request.merge_request_id} Session ${sessionId} is still running`);
165
165
  }
166
166
  catch (error) {
167
- if (runtimeHarness(input.profile) !== "codex-desktop")
167
+ if (!["codex-desktop", "opencode-server"].includes(runtimeHarness(input.profile)))
168
168
  throw error;
169
169
  const capacityIntents = (retained.capacity_intents ?? []);
170
170
  const capacityFailures = (retained.capacity_failures ?? []);