@deksden-com/dd-flow-cli 0.9.0-beta.152 → 0.9.0-beta.156

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 (227) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/build-info.json +8 -8
  3. package/dist/cli/command-inputs.js +33 -3
  4. package/dist/cli/command-metadata.js +73 -2
  5. package/dist/cli/delivery.js +108 -0
  6. package/dist/cli/diagnostic-invocation.js +23 -7
  7. package/dist/cli/diagnostic.js +14 -3
  8. package/dist/cli/help.js +117 -1
  9. package/dist/cli/hook-ingress.js +36 -49
  10. package/dist/cli/interactive-invocation.js +33 -0
  11. package/dist/cli/interactive.js +114 -0
  12. package/dist/cli/memory-fix-invocation.js +52 -0
  13. package/dist/cli/memory-fix.js +100 -0
  14. package/dist/cli/memory-flow-admission.js +7 -3
  15. package/dist/cli/memory-init.js +5 -4
  16. package/dist/cli/native-hook-ingress.js +5 -1
  17. package/dist/cli/native-hook-preparation.js +31 -4
  18. package/dist/cli/run-cli.js +274 -19
  19. package/dist/cli.js +17 -3
  20. package/dist/domain/delivery-flows.js +8 -0
  21. package/dist/domain/distill-flow.js +20 -0
  22. package/dist/domain/interactive-flow.js +1 -0
  23. package/dist/domain/memory-fix-flow.js +2 -0
  24. package/dist/domain/review-fix-flow.js +2 -0
  25. package/dist/domain/source-snapshot-flows.js +4 -0
  26. package/dist/harness-runtime/bin/dd-agy.mjs +18 -3
  27. package/dist/harness-runtime/bin/dd-grok.mjs +15 -5
  28. package/dist/harness-runtime/bin/dd-zcode.mjs +15 -7
  29. package/dist/harness-runtime/lib/agy-error-boundary.mjs +18 -10
  30. package/dist/harness-runtime/lib/agy-native-inspection.mjs +12 -3
  31. package/dist/harness-runtime/lib/agy-native-inventory.mjs +19 -0
  32. package/dist/harness-runtime/lib/agy-terminal-envelope.mjs +13 -6
  33. package/dist/harness-runtime/lib/agy-transcript-stream.mjs +54 -0
  34. package/dist/harness-runtime/lib/daemon-observation.d.mts +1 -0
  35. package/dist/harness-runtime/lib/daemon-observation.mjs +12 -4
  36. package/dist/harness-runtime/lib/daemon-operations.mjs +20 -5
  37. package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +1 -0
  38. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +371 -114
  39. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +43 -3
  40. package/dist/harness-runtime/lib/dd-codex.mjs +36 -4
  41. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +25 -11
  42. package/dist/harness-runtime/lib/dd-droid.mjs +124 -11
  43. package/dist/harness-runtime/lib/dd-grok-daemon.d.mts +1 -0
  44. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +137 -25
  45. package/dist/harness-runtime/lib/dd-grok.mjs +4 -2
  46. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +115 -17
  47. package/dist/harness-runtime/lib/dd-opencode.mjs +10 -7
  48. package/dist/harness-runtime/lib/dd-zcode-daemon.d.mts +1 -0
  49. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +115 -22
  50. package/dist/harness-runtime/lib/dd-zcode.mjs +75 -36
  51. package/dist/harness-runtime/lib/delegation-instructions.d.mts +2 -1
  52. package/dist/harness-runtime/lib/delegation-instructions.mjs +15 -5
  53. package/dist/harness-runtime/lib/delivery-command.mjs +84 -0
  54. package/dist/harness-runtime/lib/driver-recovery.mjs +0 -19
  55. package/dist/harness-runtime/lib/droid-observation.mjs +9 -5
  56. package/dist/harness-runtime/lib/lifecycle-contract.d.mts +1 -0
  57. package/dist/harness-runtime/lib/lifecycle-contract.mjs +2 -1
  58. package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -2
  59. package/dist/harness-runtime/lib/native-hook-command.mjs +63 -8
  60. package/dist/harness-runtime/lib/native-streams.mjs +3 -10
  61. package/dist/harness-runtime/lib/opencode-plugin.mjs +36 -9
  62. package/dist/harness-runtime/lib/operation-errors.mjs +13 -0
  63. package/dist/harness-runtime/lib/operation-progress.mjs +6 -2
  64. package/dist/harness-runtime/lib/process-json.mjs +4 -5
  65. package/dist/harness-runtime/lib/tool-observations.mjs +20 -10
  66. package/dist/harness-runtime/lib/zcode-native-hook-ownership.mjs +109 -0
  67. package/dist/schemas/code-work-result.schema.json +2 -1
  68. package/dist/schemas/delivery-stage-result.schema.json +286 -0
  69. package/dist/schemas/distill-stage-result.schema.json +667 -0
  70. package/dist/schemas/distill-work-result.schema.json +428 -0
  71. package/dist/schemas/execution-bundle.schema.json +6 -2
  72. package/dist/schemas/interactive-stage-result.schema.json +47 -0
  73. package/dist/schemas/interactive-work-result.schema.json +63 -0
  74. package/dist/schemas/mb-fix-stage-result.schema.json +93 -0
  75. package/dist/schemas/project-flow-pack-manifest.schema.json +2 -2
  76. package/dist/schemas/review-fix-stage-result.schema.json +210 -0
  77. package/dist/schemas/run-control-receipt.schema.json +1 -0
  78. package/dist/services/assigned-lifecycle.js +382 -0
  79. package/dist/services/check-runner-evidence.js +45 -8
  80. package/dist/services/cli-operation-classifier.js +6 -1
  81. package/dist/services/code-checks.js +46 -29
  82. package/dist/services/codex-hook-delivery.js +11 -2
  83. package/dist/services/controller-child-repair.js +95 -0
  84. package/dist/services/controller-execution.js +4 -1
  85. package/dist/services/controller-fanout.js +19 -9
  86. package/dist/services/controller-memory-worker.js +6 -4
  87. package/dist/services/controller-result-authority.js +122 -0
  88. package/dist/services/dashboard.js +4 -2
  89. package/dist/services/delivery-access.js +123 -0
  90. package/dist/services/delivery-approval.js +68 -0
  91. package/dist/services/delivery-artifact.js +86 -0
  92. package/dist/services/delivery-checks.js +178 -0
  93. package/dist/services/delivery-execution.js +181 -0
  94. package/dist/services/delivery-intents.js +212 -0
  95. package/dist/services/delivery-operation.js +81 -0
  96. package/dist/services/delivery-policy.js +91 -0
  97. package/dist/services/delivery-procedures.js +43 -0
  98. package/dist/services/delivery-request.js +99 -0
  99. package/dist/services/delivery-result.js +144 -0
  100. package/dist/services/delivery-stage-commands.js +26 -0
  101. package/dist/services/delivery-stage-entry.js +56 -0
  102. package/dist/services/delivery-stage-handlers.js +135 -0
  103. package/dist/services/delivery-stage-results.js +94 -0
  104. package/dist/services/descriptor-coordinator-session.js +3 -1
  105. package/dist/services/descriptor-run-start.js +24 -4
  106. package/dist/services/descriptor-stage-coordinator.js +78 -10
  107. package/dist/services/descriptor-stage-settlement.js +59 -14
  108. package/dist/services/diagnostic-repair-intake.js +123 -0
  109. package/dist/services/diagnostic-repair-source-intake.js +102 -0
  110. package/dist/services/diagnostic-report.js +30 -0
  111. package/dist/services/diagnostic-run-bundle.js +48 -9
  112. package/dist/services/diagnostic-snapshot.js +2 -2
  113. package/dist/services/diagnostic-stage-handlers.js +3 -2
  114. package/dist/services/diagnostic-stage-results.js +26 -0
  115. package/dist/services/diagnostic-start.js +56 -6
  116. package/dist/services/diagnostic-work-collection.js +3 -3
  117. package/dist/services/distill-document-publication.js +280 -0
  118. package/dist/services/distill-promotion.js +197 -0
  119. package/dist/services/distill-publication-custody.js +162 -0
  120. package/dist/services/distill-stage-handlers.js +249 -0
  121. package/dist/services/distill-stage-results.js +105 -0
  122. package/dist/services/distill-user-inputs.js +4 -0
  123. package/dist/services/distill-work-batch.js +42 -0
  124. package/dist/services/distill-work-collection.js +74 -0
  125. package/dist/services/distill-work-registration.js +93 -0
  126. package/dist/services/distill-work-results.js +98 -0
  127. package/dist/services/eval-snapshots.js +17 -7
  128. package/dist/services/external-work-launch.js +8 -2
  129. package/dist/services/flow-catalog.js +24 -0
  130. package/dist/services/frozen-engine.js +19 -2
  131. package/dist/services/generic-execution.js +4 -3
  132. package/dist/services/harness-adapter.js +2 -14
  133. package/dist/services/hook-memory-routing.js +1 -1
  134. package/dist/services/hooks.js +31 -6
  135. package/dist/services/hybrid-contracts.js +6 -1
  136. package/dist/services/installed-flow-source.js +37 -2
  137. package/dist/services/interactive-attached-session.js +215 -0
  138. package/dist/services/interactive-baseline-custody.js +54 -0
  139. package/dist/services/interactive-before-versions.js +148 -0
  140. package/dist/services/interactive-bundle-data.js +85 -0
  141. package/dist/services/interactive-checks.js +179 -0
  142. package/dist/services/interactive-execution.js +33 -0
  143. package/dist/services/interactive-finish-integration.js +236 -0
  144. package/dist/services/interactive-git-baseline.js +230 -0
  145. package/dist/services/interactive-git-dispatch.js +278 -0
  146. package/dist/services/interactive-input-delivery.js +82 -0
  147. package/dist/services/interactive-mutation-scope.js +231 -0
  148. package/dist/services/interactive-request.js +287 -0
  149. package/dist/services/interactive-run-bundle.js +109 -0
  150. package/dist/services/interactive-stage-entry.js +41 -0
  151. package/dist/services/interactive-stage-handlers.js +289 -0
  152. package/dist/services/interactive-stage-observation.js +136 -0
  153. package/dist/services/interactive-stage-results.js +111 -0
  154. package/dist/services/interactive-user-inputs.js +142 -0
  155. package/dist/services/interactive-work-batch.js +43 -0
  156. package/dist/services/interactive-work-collection.js +119 -0
  157. package/dist/services/interactive-work-completion.js +180 -0
  158. package/dist/services/interactive-work-context.js +249 -0
  159. package/dist/services/interactive-work-history.js +112 -0
  160. package/dist/services/interactive-work-registration.js +124 -0
  161. package/dist/services/interactive-work-results.js +115 -0
  162. package/dist/services/interactive-work-start.js +155 -0
  163. package/dist/services/lifecycle-invocations.js +250 -33
  164. package/dist/services/managed-daemon-binding.js +52 -1
  165. package/dist/services/memory-compose-handler.js +2 -22
  166. package/dist/services/memory-finish-integration.js +58 -17
  167. package/dist/services/memory-fix-finish-integration.js +129 -0
  168. package/dist/services/memory-fix-git-dispatch.js +139 -0
  169. package/dist/services/memory-fix-intake-custody.js +11 -0
  170. package/dist/services/memory-fix-intake-settlement.js +127 -0
  171. package/dist/services/memory-fix-publication.js +196 -0
  172. package/dist/services/memory-fix-repair-admission.js +116 -0
  173. package/dist/services/memory-fix-request.js +141 -0
  174. package/dist/services/memory-fix-run-bundle.js +83 -0
  175. package/dist/services/memory-fix-scope.js +78 -0
  176. package/dist/services/memory-fix-sources.js +85 -0
  177. package/dist/services/memory-fix-stage-entry.js +24 -0
  178. package/dist/services/memory-fix-stage-handlers.js +282 -0
  179. package/dist/services/memory-fix-stage-results.js +106 -0
  180. package/dist/services/memory-fix-workspace.js +127 -0
  181. package/dist/services/memory-flow-user-inputs.js +88 -0
  182. package/dist/services/memory-init-contexts.js +55 -5
  183. package/dist/services/memory-init-controller-start.js +20 -4
  184. package/dist/services/memory-layer.js +7 -3
  185. package/dist/services/memory-native-review-copy.js +27 -3
  186. package/dist/services/memory-repair-document-delta.js +1 -0
  187. package/dist/services/project-flow-source.js +195 -0
  188. package/dist/services/project-git-integration.js +92 -0
  189. package/dist/services/repair-intents.js +2 -7
  190. package/dist/services/retained-native-hook.js +274 -0
  191. package/dist/services/review-fix-handoff.js +82 -0
  192. package/dist/services/review-fix-intake-custody.js +17 -0
  193. package/dist/services/review-fix-stage-entry.js +43 -0
  194. package/dist/services/review-fix-stage-handlers.js +176 -0
  195. package/dist/services/review-fix-stage-results.js +86 -0
  196. package/dist/services/run-control-worker.js +42 -1
  197. package/dist/services/run-control.js +40 -5
  198. package/dist/services/run-controller-adapter.js +37 -3
  199. package/dist/services/run-controller-process.js +25 -4
  200. package/dist/services/run-controller-recovery.js +46 -6
  201. package/dist/services/run-controller-state.js +3 -2
  202. package/dist/services/run-controller.js +257 -44
  203. package/dist/services/run-recovery.js +31 -2
  204. package/dist/services/run-timeline.js +59 -0
  205. package/dist/services/runs.js +18 -31
  206. package/dist/services/sessions.js +2 -1
  207. package/dist/services/stage-handlers.js +55 -3
  208. package/dist/services/stage-lifecycle.js +4 -3
  209. package/dist/services/stage-pause.js +26 -9
  210. package/dist/services/stage-report-renderer.js +2 -6
  211. package/dist/services/technical-notes.js +31 -0
  212. package/dist/services/verification-matrix.js +1 -1
  213. package/dist/services/version-status.js +1 -1
  214. package/dist/services/vnext-code-review.js +74 -5
  215. package/dist/services/vnext-code.js +8 -32
  216. package/dist/services/vnext-fanout.js +53 -8
  217. package/dist/services/vnext-merge.js +10 -5
  218. package/dist/services/vnext-plan-review.js +1 -1
  219. package/dist/services/vnext-plan.js +1 -1
  220. package/dist/services/vnext-protocolize.js +1 -1
  221. package/dist/services/vnext-specify.js +10 -5
  222. package/dist/services/work-registry.js +255 -69
  223. package/dist/services/worktrees.js +7 -4
  224. package/dist/storage/database.js +2 -1
  225. package/dist/verification-components.json +82 -0
  226. package/package.json +3 -2
  227. package/tools/verification-jobs.mjs +35 -10
@@ -0,0 +1,181 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { spawn } from "node:child_process";
4
+ import { types } from "node:util";
5
+ import { AppError } from "../shared/errors.js";
6
+ import { prepareDeliveryProcedure } from "./delivery-procedures.js";
7
+ import { managedLeaseMonitor } from "./managed-lease-monitor.js";
8
+ import { physicalStartup } from "./physical-startup.js";
9
+ import { registerManagedProcess, confirmManagedProcess, renewManagedProcess, finishManagedProcess, assertManagedProcessLease, processTreeIsAlive } from "./managed-processes.js";
10
+ import { stopProcessGroup } from "../harness-runtime/lib/managed-daemon.mjs";
11
+ const id = (value) => typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(value);
12
+ /** One step of an already retained delivery intent, not a public launcher or
13
+ * approval/retry policy. Reuse the ordinary process registry/lease/stop owners.
14
+ * No raw command output, environment values or argv enter logs or the registry. */
15
+ export async function executeDeliveryStep(context, input) {
16
+ const procedure = prepareDeliveryProcedure(input.procedure, "execute"), step = procedure.procedure_steps.find(item => item.id === input.stepId);
17
+ if (context.db.isOwnedWriteTransaction?.() || !step || ![input.projectId, input.runId, input.operationId, input.stepId].every(id)
18
+ || typeof input.workspace !== "string" || !path.isAbsolute(input.workspace) || typeof input.assertCurrent !== "function" || typeof input.admit !== "function"
19
+ || typeof input.consumeOutput !== "function")
20
+ throw new AppError("delivery_execution_invalid", "Delivery execution requires its retained operation and owner fence", 2, { effect: "no_effect" });
21
+ if (input.deadlineAt !== undefined && (!Number.isSafeInteger(input.deadlineAt) || input.deadlineAt <= Date.now())) {
22
+ throw new AppError("delivery_execution_invalid", "The original procedure deadline has expired", 2, { effect: "no_effect" });
23
+ }
24
+ const assertCurrent = () => {
25
+ const value = input.assertCurrent();
26
+ if (types.isPromise(value)) {
27
+ void value.catch(() => { });
28
+ throw new AppError("delivery_execution_invalid", "Delivery owner fences must complete synchronously", 2, { effect: "no_effect" });
29
+ }
30
+ };
31
+ try {
32
+ assertCurrent();
33
+ }
34
+ catch {
35
+ throw new AppError("delivery_execution_invalid", "Delivery owner fence refused before process allocation", 2, { effect: "no_effect" });
36
+ }
37
+ let record = registerManagedProcess(context, { kind: "delivery", ownerId: input.operationId, projectId: input.projectId,
38
+ runId: input.runId, operationId: `${input.operationId}:${input.stepId}`, ownerPid: process.pid, uniqueActiveOperation: true });
39
+ const lease = managedLeaseMonitor(attemptMs => renewManagedProcess({ ...context, databaseDeadlineAt: Date.now() + attemptMs }, { id: record.id, leaseToken: record.lease_token }), record.lease_expires_at);
40
+ let admitted = false, completed = false, settled = false, frame, outputSize = 0, publicBytes = 0, publicText = "";
41
+ const chunks = [];
42
+ const internalFailures = new WeakSet();
43
+ let interrupt;
44
+ const interrupted = new Promise((_resolve, reject) => { interrupt = reject; });
45
+ void interrupted.catch(() => { });
46
+ const failure = (reason) => {
47
+ const error = new AppError("delivery_execution_failed", "Delivery command requires original-operation reconciliation", 1, { reason, process_id: record.id, operation_id: input.operationId, step_id: input.stepId,
48
+ effect: admitted && procedure.side_effect === "mutation" && !(settled && frame?.started === false && frame.error === "spawn_failed") ? "unknown" : "no_effect" });
49
+ internalFailures.add(error);
50
+ return error;
51
+ };
52
+ let ready;
53
+ const readiness = new Promise(resolve => { ready = resolve; });
54
+ const child = spawn(process.execPath, [fileURLToPath(new URL("../harness-runtime/lib/delivery-command.mjs", import.meta.url))], { cwd: input.workspace, env: {}, detached: process.platform !== "win32", windowsHide: true, stdio: ["pipe", "pipe", "pipe", "pipe", "pipe"] });
55
+ child.once("error", () => interrupt(failure("wrapper_spawn_failed")));
56
+ const closed = new Promise(resolve => child.once("close", () => { if (!completed)
57
+ interrupt(failure("wrapper_closed_before_result")); resolve(); }));
58
+ for (const stream of child.stdio)
59
+ stream?.on("error", () => interrupt(failure("transport_unavailable")));
60
+ child.stderr.resume();
61
+ child.stdout.on("data", bytes => {
62
+ publicBytes += bytes.length;
63
+ if (publicBytes > 8192) {
64
+ interrupt(failure("unbounded_transport_frame"));
65
+ return;
66
+ }
67
+ publicText += bytes.toString("utf8");
68
+ while (publicText.includes("\n")) {
69
+ const end = publicText.indexOf("\n"), line = publicText.slice(0, end);
70
+ publicText = publicText.slice(end + 1);
71
+ try {
72
+ const value = JSON.parse(line);
73
+ if (value.operation_id !== input.operationId || value.step_id !== input.stepId)
74
+ throw new Error();
75
+ if (value.schema_id === "dd-flow/delivery-command-ready@1" && !admitted && !completed)
76
+ ready();
77
+ else if (value.schema_id === "dd-flow/delivery-command-result@1" && !completed) {
78
+ frame = value;
79
+ completed = true;
80
+ }
81
+ else
82
+ throw new Error();
83
+ }
84
+ catch {
85
+ interrupt(failure("invalid_transport_frame"));
86
+ }
87
+ }
88
+ });
89
+ child.stdio[4].on("data", (bytes) => {
90
+ outputSize += bytes.length;
91
+ if (outputSize > 1024 * 1024) {
92
+ interrupt(failure("unbounded_private_output"));
93
+ return;
94
+ }
95
+ chunks.push(Buffer.from(bytes));
96
+ });
97
+ const deadline = Math.min(Date.now() + procedure.timeout_ms, input.deadlineAt ?? Infinity);
98
+ const timeout = setTimeout(() => interrupt(failure("timeout")), Math.max(1, deadline - Date.now()));
99
+ const heartbeat = setInterval(() => lease.background(assertCurrent, () => interrupt(failure("owner_unconfirmed"))), 5000);
100
+ let terminalError;
101
+ try {
102
+ child.stdio[3].end(JSON.stringify({ schema_id: "dd-flow/delivery-command@1", operation_id: input.operationId,
103
+ step_id: input.stepId, argv: step.argv, permitted_environment_variables: procedure.permitted_environment_variables,
104
+ environment: Object.fromEntries(procedure.permitted_environment_variables.filter(name => context.env[name] !== undefined).map(name => [name, context.env[name]])) }));
105
+ await Promise.race([readiness, interrupted]);
106
+ record = await physicalStartup(Math.min(deadline, context.databaseDeadlineAt ?? deadline)).observe(() => confirmManagedProcess(context, { id: record.id, leaseToken: record.lease_token, pid: child.pid,
107
+ ...(process.platform === "win32" ? {} : { processGroupId: child.pid }), ownerPid: process.pid }), () => { assertCurrent(); if (child.exitCode !== null || child.signalCode !== null)
108
+ throw failure("wrapper_closed_before_result"); }, record.lease_expires_at);
109
+ await Promise.race([lease.ensureConfirmed(assertCurrent), interrupted]);
110
+ assertCurrent();
111
+ assertManagedProcessLease(context, { id: record.id, leaseToken: record.lease_token, pid: child.pid });
112
+ const admission = input.admit(record);
113
+ if (types.isPromise(admission)) {
114
+ void admission.catch(() => { });
115
+ throw failure("asynchronous_admission_refused");
116
+ }
117
+ lease.assertNoRenewal();
118
+ assertCurrent();
119
+ if (Date.now() >= deadline)
120
+ throw failure("timeout");
121
+ admitted = true;
122
+ child.stdin.end("admitted\n");
123
+ await Promise.race([closed, interrupted]);
124
+ if (frame?.admitted === true && frame.started === false && frame.error === "spawn_failed")
125
+ throw failure("command_spawn_failed");
126
+ if (!frame || frame.admitted !== true || frame.started !== true || frame.error !== null || frame.exit_code !== 0
127
+ || frame.signal !== null || frame.output_available !== true || frame.stdout_truncated !== false)
128
+ throw failure("command_unsuccessful");
129
+ }
130
+ catch (error) {
131
+ terminalError = error;
132
+ }
133
+ finally {
134
+ clearTimeout(timeout);
135
+ clearInterval(heartbeat);
136
+ lease.close();
137
+ try {
138
+ await stopProcessGroup(child, 1000, { detached: process.platform !== "win32" });
139
+ if (processTreeIsAlive(record))
140
+ terminalError = failure("process_tree_unsettled");
141
+ else if (!finishManagedProcess(context, { id: record.id, leaseToken: record.lease_token, state: "stopped",
142
+ reason: terminalError ? "delivery_command_failed" : "delivery_command_exited" }))
143
+ terminalError = failure("process_owner_changed");
144
+ else
145
+ settled = true;
146
+ }
147
+ catch {
148
+ terminalError = failure("process_tree_unsettled");
149
+ }
150
+ }
151
+ if (terminalError) {
152
+ chunks.length = 0;
153
+ throw failure(terminalError instanceof AppError && internalFailures.has(terminalError)
154
+ ? String(terminalError.details.reason) : "owner_or_execution_failed");
155
+ }
156
+ try {
157
+ assertCurrent();
158
+ }
159
+ catch {
160
+ chunks.length = 0;
161
+ throw failure("owner_or_execution_failed");
162
+ }
163
+ const bytes = Buffer.concat(chunks);
164
+ chunks.length = 0;
165
+ try {
166
+ const projection = input.consumeOutput(bytes);
167
+ if (types.isPromise(projection)) {
168
+ void projection.catch(() => { });
169
+ throw failure("output_projection_failed");
170
+ }
171
+ }
172
+ catch {
173
+ throw failure("output_projection_failed");
174
+ }
175
+ finally {
176
+ bytes.fill(0);
177
+ }
178
+ return { process_id: record.id, operation_id: input.operationId, step_id: input.stepId, outcome: "command_succeeded",
179
+ // Exit zero is not publication/deployment readback or permission to repeat.
180
+ effect: procedure.side_effect === "mutation" ? "unknown" : "no_effect" };
181
+ }
@@ -0,0 +1,212 @@
1
+ import { AppError } from "../shared/errors.js";
2
+ import { appendAudit } from "./audit.js";
3
+ import { hybridDigest } from "./hybrid-contracts.js";
4
+ import { prepareDeliveryApproval } from "./delivery-approval.js";
5
+ import { resolveDeliveryBinding } from "./delivery-policy.js";
6
+ import { PreparedDeliveryAccess } from "./delivery-access.js";
7
+ import { PreparedDeliveryArtifact } from "./delivery-artifact.js";
8
+ import { assertPreparedRunBundleSqlAuthority } from "./runs.js";
9
+ import { assertRunMutationAllowed, assertStageSettlementAllowed } from "./run-recovery-guard.js";
10
+ import { readDescriptorStageInput } from "./descriptor-stage-coordinator.js";
11
+ import { readActiveDescriptorStageCoordinator } from "./descriptor-stage-settlement.js";
12
+ import { readAcceptedStageData } from "./stage-report-renderer.js";
13
+ import { assertManagedProcessLease, managedProcessBinding, managedProcessStatus, processTreeIsAlive, resourceHome } from "./managed-processes.js";
14
+ const intentEvent = "delivery.operation_intent", progressEvent = "delivery.operation_event", anchor = '$.variables."delivery.operation"';
15
+ function refuse(reason) {
16
+ throw new AppError("delivery_intent_invalid", "Delivery needs its original approved operation and active Stage owner", 2, { reason, effect: "no_effect" });
17
+ }
18
+ function pending(reason) {
19
+ throw new AppError("delivery_operation_pending", "Observe or reconcile the original delivery before another dispatch", 2, { reason, effect: "unknown", next_action: "reconcile_original_delivery" });
20
+ }
21
+ /** Original SQL operation custody; never infer 'not dispatched' from a lost
22
+ * log, editable file, new request ID, expired observer or terminal process. */
23
+ export function readDeliveryIntent(context, scope) {
24
+ const run = context.db.get("SELECT json_extract(index_json,?) value FROM runs WHERE project_id=? AND id=?", [anchor, scope.projectId, scope.runId]);
25
+ if (!run)
26
+ refuse("bound_run_required");
27
+ const plans = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type=? AND json_extract(payload_json,'$.plan.approval.scope.run_id')=? LIMIT 2", [scope.projectId, intentEvent, scope.runId]);
28
+ const events = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type=? AND json_extract(payload_json,'$.run_id')=? ORDER BY id LIMIT 34", [scope.projectId, progressEvent, scope.runId]);
29
+ if (plans.length > 1 || events.length > 33 || plans.some(row => Buffer.byteLength(row.payload_json) > 262144)
30
+ || events.some(row => Buffer.byteLength(row.payload_json) > 131072) || Boolean(plans.length) !== Boolean(run.value) || !plans.length && events.length)
31
+ pending("operation_custody_lost");
32
+ if (!plans.length)
33
+ return null;
34
+ let value, log, saved;
35
+ try {
36
+ value = JSON.parse(plans[0].payload_json);
37
+ log = events.map(row => JSON.parse(row.payload_json));
38
+ saved = JSON.parse(run.value);
39
+ }
40
+ catch {
41
+ return pending("operation_custody_invalid");
42
+ }
43
+ const plan = value.plan;
44
+ if (plan?.schema_id !== "dd-flow/delivery-intent@1" || plan.approval?.scope?.project_id !== scope.projectId
45
+ || plan.approval.scope.run_id !== scope.runId || value.identity !== hybridDigest(plan) || saved.identity !== value.identity
46
+ || !Array.isArray(plan.steps) || !plan.steps.length || plan.steps.length > 16
47
+ || plan.steps.some(step => typeof step !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/.test(step)) || new Set(plan.steps).size !== plan.steps.length
48
+ || saved.count !== log.length)
49
+ pending("operation_custody_changed");
50
+ let head = value.identity, next = 0, failed = false, open = null;
51
+ for (const entry of log) {
52
+ if (!entry || typeof entry !== "object" || Array.isArray(entry) || failed || entry.run_id !== scope.runId
53
+ || entry.identity !== value.identity || entry.previous !== head || entry.step_id !== plan.steps[next])
54
+ pending("operation_event_changed");
55
+ if (entry.kind === "admitted" && !open && entry.process && typeof entry.process.resource_home === "string"
56
+ && typeof entry.process.process_id === "string" && /^[a-f0-9]{64}$/.test(entry.process.registration_sha256)
57
+ && entry.access?.schema_id === "dd-flow/delivery-access@1" && entry.access.operation_id === plan.approval.scope.operation_id
58
+ && entry.access.binding_id === plan.approval.scope.binding_id)
59
+ open = entry;
60
+ else if (entry.kind === "command_result" && open && entry.result && entry.result.process_id === open.process.process_id
61
+ && entry.result.operation_id === plan.approval.scope.operation_id && entry.result.step_id === entry.step_id
62
+ && ["command_succeeded", "failed"].includes(entry.result.outcome) && ["no_effect", "unknown"].includes(entry.result.effect)) {
63
+ // A failed or uncertain command is never permission to run a later step.
64
+ if (entry.result.outcome === "command_succeeded") {
65
+ next++;
66
+ open = null;
67
+ }
68
+ else
69
+ failed = true;
70
+ }
71
+ else
72
+ pending("operation_event_order_invalid");
73
+ head = hybridDigest(entry);
74
+ }
75
+ if (head !== saved.head)
76
+ pending("operation_anchor_changed");
77
+ return { ...value, events: log, head, next, open, failed };
78
+ }
79
+ /** Original intent/admission owner, not an executor. The Stage independently
80
+ * verifies manifest references and project procedure safety. No public route yet. */
81
+ export function prepareDeliveryIntent(context, scope, bundle, raw, proposal) {
82
+ if (context.db.isOwnedWriteTransaction?.())
83
+ refuse("preparation_outside_writer_required");
84
+ const binding = resolveDeliveryBinding({ schema_id: "dd-flow/delivery-policy@1", bindings: [raw.binding] }, raw.binding.selector);
85
+ if (binding.procedures.execute.side_effect !== "mutation")
86
+ refuse("protected_mutation_required");
87
+ const selection = { ...raw, binding }, approval = prepareDeliveryApproval(context, scope, bundle, selection, proposal), grant = approval.value();
88
+ const artifact = PreparedDeliveryArtifact.prepare(bundle.execution.snapshot().scope.workspace, raw.artifactPath, selection.artifact);
89
+ const stage = grant.scope.flow_id === "deploy" ? "deploy" : "publish", previous = stage === "deploy" ? "prepare" : "verify";
90
+ const predecessor = readAcceptedStageData(context, { ...scope, stage: previous });
91
+ let data;
92
+ try {
93
+ data = JSON.parse(predecessor.bytes.toString("utf8"));
94
+ }
95
+ catch {
96
+ return refuse("accepted_selection_required");
97
+ }
98
+ if (data.details?.delivery_scope_sha256 !== grant.scope_sha256)
99
+ refuse("accepted_selection_required");
100
+ const generation = assertRunMutationAllowed(context, scope.projectId, scope.runId);
101
+ const authority = (writer) => {
102
+ assertRunMutationAllowed(writer, scope.projectId, scope.runId, generation);
103
+ const run = assertPreparedRunBundleSqlAuthority(writer, scope, bundle), index = JSON.parse(run.index_json);
104
+ if (run.status !== "running" || index.current_stage !== stage
105
+ || !index.stage_runs?.some((item) => item.stage === stage && item.status === "running"))
106
+ refuse("active_delivery_stage_required");
107
+ const packet = readDescriptorStageInput(writer, { ...scope, stage }, bundle), actor = readActiveDescriptorStageCoordinator(writer, { ...scope, stage }, bundle);
108
+ if (packet.input.predecessor?.stage !== previous || hybridDigest(packet.input.predecessor.result) !== hybridDigest(predecessor.result))
109
+ refuse("accepted_predecessor_changed");
110
+ if (writer.db.get("SELECT 1 FROM works WHERE project_id=? AND run_id=? AND parent_work_id=? AND status IN ('created','running','paused') LIMIT 1", [scope.projectId, scope.runId, actor.work_id]))
111
+ refuse("active_child_work");
112
+ return { packet, actor };
113
+ };
114
+ const owner = authority(context), original = readDeliveryIntent(context, scope);
115
+ const proposed = { schema_id: "dd-flow/delivery-intent@1", approval: grant, artifact: artifact.value(), predecessor: { stage: previous, result: predecessor.result },
116
+ steps: binding.procedures.execute.procedure_steps.map(step => step.id) };
117
+ // Retain the first human-readable interpretation rather than requiring a
118
+ // future observer to reproduce its wording. Scope, basis, inputs and expiry
119
+ // remain exact; neither commentary nor a fresh timestamp renews permission.
120
+ const semantic = (plan) => ({ ...plan, approval: { ...plan.approval, approved_at: null, interpretation: null } });
121
+ if (original && hybridDigest(semantic(original.plan)) !== hybridDigest(semantic(proposed)))
122
+ pending("original_operation_scope_changed");
123
+ const plan = original?.plan ?? proposed, identity = hybridDigest(plan), accessScope = {
124
+ projectId: scope.projectId, runId: scope.runId, operationId: selection.operationId, workspace: bundle.execution.snapshot().scope.workspace, binding
125
+ };
126
+ const assertCustody = (writer) => {
127
+ approval.assertCustody(writer);
128
+ if (hybridDigest(authority(writer)) !== hybridDigest(owner))
129
+ refuse("delivery_owner_changed");
130
+ const current = readDeliveryIntent(writer, scope);
131
+ if (current && current.identity !== identity)
132
+ pending("original_operation_changed");
133
+ return current;
134
+ };
135
+ const settlementCustody = (writer) => {
136
+ // Expiry/revocation forbids new effects, not recording what an already
137
+ // admitted command actually did. Ordinary Stage/control fences still own
138
+ // the allowed generation and cooperative settlement window.
139
+ assertStageSettlementAllowed(writer, scope.projectId, scope.runId, stage, generation);
140
+ assertPreparedRunBundleSqlAuthority(writer, scope, bundle);
141
+ const current = readDeliveryIntent(writer, scope);
142
+ if (!current || current.identity !== identity)
143
+ pending("original_operation_changed");
144
+ return current;
145
+ };
146
+ const assertArtifact = (writer) => {
147
+ if (writer.db.isOwnedWriteTransaction?.())
148
+ refuse("artifact_observation_outside_writer_required");
149
+ artifact.assertCurrent();
150
+ };
151
+ const retain = (writer) => {
152
+ assertArtifact(writer);
153
+ return writer.db.writeTransaction(() => {
154
+ const current = assertCustody(writer);
155
+ if (current)
156
+ return current;
157
+ appendAudit(writer, { projectId: scope.projectId, eventType: intentEvent, payload: { identity, plan } });
158
+ writer.db.run("UPDATE runs SET index_json=json_set(index_json,?,json(?)) WHERE project_id=? AND id=?", [anchor, JSON.stringify({ identity, count: 0, head: identity }), scope.projectId, scope.runId]);
159
+ return readDeliveryIntent(writer, scope);
160
+ });
161
+ };
162
+ return { value: () => structuredClone(plan), assertCustody, retain,
163
+ admitStep(writer, stepId, process, access) {
164
+ if (!(access instanceof PreparedDeliveryAccess))
165
+ refuse("owned_access_observation_required");
166
+ assertArtifact(writer);
167
+ return writer.db.writeTransaction(() => {
168
+ const current = assertCustody(writer);
169
+ if (!current)
170
+ refuse("retained_intent_required");
171
+ if (current.open || current.plan.steps[current.next] !== stepId)
172
+ pending("original_step_already_admitted_or_out_of_order");
173
+ if (process.kind !== "delivery" || process.project_id !== scope.projectId || process.run_id !== scope.runId
174
+ || process.owner_id !== selection.operationId || process.operation_id !== `${selection.operationId}:${stepId}` || !process.pid)
175
+ refuse("selected_process_required");
176
+ assertManagedProcessLease(writer, { id: process.id, leaseToken: process.lease_token, pid: process.pid });
177
+ access.consume(writer, accessScope);
178
+ const entry = { run_id: scope.runId, identity, previous: current.head, kind: "admitted", step_id: stepId,
179
+ process: managedProcessBinding(process, resourceHome(writer)), access: access.value() };
180
+ appendAudit(writer, { projectId: scope.projectId, eventType: progressEvent, payload: entry });
181
+ writer.db.run("UPDATE runs SET index_json=json_set(index_json,?,json(?)) WHERE project_id=? AND id=?", [anchor,
182
+ JSON.stringify({ identity, count: current.events.length + 1, head: hybridDigest(entry) }), scope.projectId, scope.runId]);
183
+ });
184
+ },
185
+ recordCommand(writer, result) {
186
+ if (writer.db.isOwnedWriteTransaction?.())
187
+ refuse("physical_observation_outside_writer_required");
188
+ const before = settlementCustody(writer), admitted = before.open;
189
+ if (before.failed || !admitted || result.step_id !== admitted.step_id || result.process_id !== admitted.process.process_id
190
+ || result.operation_id !== selection.operationId || !["command_succeeded", "failed"].includes(result.outcome)
191
+ || !["no_effect", "unknown"].includes(result.effect))
192
+ pending("original_command_result_required");
193
+ if (admitted.process.resource_home !== resourceHome(writer))
194
+ pending("original_resource_home_required");
195
+ const process = managedProcessStatus(writer).find(item => item.id === result.process_id);
196
+ if (!process || managedProcessBinding(process, resourceHome(writer)).registration_sha256 !== admitted.process.registration_sha256
197
+ || process.state !== "stopped" || processTreeIsAlive(process))
198
+ pending("original_process_not_settled");
199
+ return writer.db.writeTransaction(() => {
200
+ const current = settlementCustody(writer);
201
+ if (!current || current.head !== before.head)
202
+ pending("operation_changed_before_result");
203
+ const entry = { run_id: scope.runId, identity, previous: current.head, kind: "command_result", step_id: result.step_id,
204
+ result: { process_id: result.process_id, operation_id: result.operation_id, step_id: result.step_id, outcome: result.outcome, effect: result.effect } };
205
+ appendAudit(writer, { projectId: scope.projectId, eventType: progressEvent, payload: entry });
206
+ writer.db.run("UPDATE runs SET index_json=json_set(index_json,?,json(?)) WHERE project_id=? AND id=?", [anchor,
207
+ JSON.stringify({ identity, count: current.events.length + 1, head: hybridDigest(entry) }), scope.projectId, scope.runId]);
208
+ return readDeliveryIntent(writer, scope);
209
+ });
210
+ }
211
+ };
212
+ }
@@ -0,0 +1,81 @@
1
+ import { AppError } from "../shared/errors.js";
2
+ import { readDeliveryStageScope } from "./delivery-stage-entry.js";
3
+ import { readAcceptedStageData } from "./stage-report-renderer.js";
4
+ import { readActiveDescriptorStageCoordinator } from "./descriptor-stage-settlement.js";
5
+ import { assertPreparedRunBundleSqlAuthority } from "./runs.js";
6
+ import { prepareDeliveryProjectBinding } from "./delivery-policy.js";
7
+ import { prepareDeliveryIntent, readDeliveryIntent } from "./delivery-intents.js";
8
+ import { PreparedDeliveryAccess } from "./delivery-access.js";
9
+ import { executeDeliveryStep } from "./delivery-execution.js";
10
+ import { observeDeliveryResult } from "./delivery-result.js";
11
+ import { hybridDigest } from "./hybrid-contracts.js";
12
+ import { prepareDeliveryCurrentAcceptedChecks } from "./delivery-checks.js";
13
+ function pending(reason) {
14
+ throw new AppError("delivery_operation_pending", "Reconcile the original delivery before another dispatch", 2, { reason, effect: "unknown", next_action: "reconcile_original_delivery" });
15
+ }
16
+ /** Internal protected operation composition. No Stage/RUN completion, login,
17
+ * provider SDK, ambient shell, fallback account or automatic mutation retry.
18
+ * Startup is not approval: the proposal cites an exact delivered user input. */
19
+ export async function executeDeliveryOperation(context, input) {
20
+ const selected = readDeliveryStageScope(context, input), { scope, bundle, flowId } = selected;
21
+ const mutation = flowId === "deploy" ? "deploy" : "publish";
22
+ if (input.stage !== mutation || context.db.isOwnedWriteTransaction?.())
23
+ throw new AppError("delivery_stage_invalid", "Protected execution requires its active delivery Stage", 2, { effect: "no_effect" });
24
+ const prior = readAcceptedStageData(context, { ...scope, stage: flowId === "deploy" ? "prepare" : "verify" });
25
+ const details = JSON.parse(prior.bytes.toString("utf8")).details;
26
+ const { decision, artifact } = details;
27
+ if (!decision || !artifact)
28
+ throw new AppError("delivery_stage_invalid", "Accepted delivery selection is required", 2, { effect: "no_effect" });
29
+ const workspace = bundle.execution.snapshot().scope.workspace;
30
+ const policy = prepareDeliveryProjectBinding(workspace, decision.policy, decision.binding.selector);
31
+ if (hybridDigest(policy.binding) !== hybridDigest(decision.binding))
32
+ throw new AppError("delivery_stage_invalid", "Accepted delivery policy changed", 2, { effect: "no_effect" });
33
+ const operation = { operationId: decision.operation_id, binding: policy.binding, artifactPath: artifact.path,
34
+ artifact: { subject_ref: artifact.subject_ref, version: artifact.version, sha256: artifact.sha256 } };
35
+ const intent = prepareDeliveryIntent(context, scope, bundle, operation, input.approval);
36
+ const checks = prepareDeliveryCurrentAcceptedChecks(context, { ...input, sourceStage: flowId === "deploy" ? "prepare" : "verify",
37
+ operationScopeSha256: hybridDigest(intent.value().approval.scope) });
38
+ checks.revalidate();
39
+ intent.retain(context);
40
+ const owner = readActiveDescriptorStageCoordinator(context, scope, bundle);
41
+ const assertOwner = () => {
42
+ assertPreparedRunBundleSqlAuthority(context, scope, bundle);
43
+ if (hybridDigest(readActiveDescriptorStageCoordinator(context, scope, bundle)) !== hybridDigest(owner))
44
+ pending("delivery_owner_changed");
45
+ policy.assertCurrent();
46
+ };
47
+ const assertCurrent = () => { assertOwner(); selected.assertCustody(context); intent.assertCustody(context); checks.assertCustody(context); };
48
+ const retained = readDeliveryIntent(context, scope);
49
+ if (retained.open || retained.failed)
50
+ pending("original_step_requires_reconciliation");
51
+ const procedure = policy.binding.procedures.execute, deadline = Date.now() + procedure.timeout_ms;
52
+ for (const step of procedure.procedure_steps.slice(retained.next)) {
53
+ assertCurrent();
54
+ checks.revalidate();
55
+ const access = await PreparedDeliveryAccess.observe(context, { ...scope, operationId: operation.operationId, workspace,
56
+ binding: policy.binding, assertCurrent });
57
+ checks.revalidate();
58
+ let result;
59
+ try {
60
+ result = await executeDeliveryStep(context, { ...scope, operationId: operation.operationId, workspace, stepId: step.id,
61
+ procedure, deadlineAt: deadline, assertCurrent,
62
+ admit(process) { checks.revalidate(); selected.assertCustody(context); checks.assertCustody(context); intent.admitStep(context, step.id, process, access); }, consumeOutput() { } });
63
+ }
64
+ catch (error) {
65
+ // Record only the executor's original admitted and physically settled
66
+ // process. An admission refusal must not invent a failed effect event.
67
+ const current = readDeliveryIntent(context, scope);
68
+ if (current?.open && error instanceof AppError && current.open.process.process_id === error.details.process_id) {
69
+ intent.recordCommand(context, { process_id: current.open.process.process_id, operation_id: operation.operationId,
70
+ step_id: step.id, outcome: "failed", effect: error.details.effect === "no_effect" ? "no_effect" : "unknown" });
71
+ }
72
+ throw error;
73
+ }
74
+ intent.recordCommand(context, result);
75
+ }
76
+ // Expiry or later revocation forbids new mutation, not honest observation of
77
+ // an already admitted effect. Preserve Stage ownership and original policy.
78
+ const readback = await observeDeliveryResult(context, { ...scope, binding: policy.binding, assertCurrent: assertOwner });
79
+ return { operation_id: operation.operationId, intent_sha256: hybridDigest(intent.value()),
80
+ readback_sha256: hybridDigest(readback), status: readback.observation.status, rollback: readback.observation.rollback ?? null };
81
+ }
@@ -0,0 +1,91 @@
1
+ import { AppError } from "../shared/errors.js";
2
+ import { prepareDeliveryProcedure } from "./delivery-procedures.js";
3
+ import path from "node:path";
4
+ import { createHash } from "node:crypto";
5
+ import { PreparedDeliveryArtifact } from "./delivery-artifact.js";
6
+ import { readBoundedReleaseFile } from "./release-source.js";
7
+ const selectorKeys = ["operation", "profile_id", "subject_ref", "provider", "host", "target", "stage"];
8
+ const record = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
9
+ const text = (value) => typeof value === "string" && value.trim().length > 0 && value.length <= 4096 && !value.includes("\0");
10
+ const keys = (value, allowed) => Object.keys(value).length === allowed.length && allowed.every(key => Object.hasOwn(value, key));
11
+ function refuse(reason) {
12
+ throw new AppError("delivery_policy_invalid", "Delivery requires one exact project-owned operation binding", 2, { phase: "prepare", effect: "no_effect", recoverable: true, reason });
13
+ }
14
+ function selector(value) {
15
+ if (!record(value) || !keys(value, selectorKeys) || !selectorKeys.every(key => text(value[key]))
16
+ || !["release", "deploy", "publish"].includes(value.operation))
17
+ refuse("invalid_selector");
18
+ return Object.fromEntries(selectorKeys.map(key => [key, value[key]]));
19
+ }
20
+ function publicId(value) {
21
+ if (!record(value) || !keys(value, ["kind", "id"]) || !text(value.kind) || !text(value.id))
22
+ refuse("invalid_public_identifier");
23
+ return { kind: value.kind, id: value.id };
24
+ }
25
+ function access(value) {
26
+ if (!record(value))
27
+ refuse("invalid_access_policy");
28
+ if (value.applicability === "not_required") {
29
+ if (!keys(value, ["applicability", "reason"]) || !text(value.reason))
30
+ refuse("not_required_needs_policy_reason");
31
+ return { applicability: "not_required", reason: value.reason };
32
+ }
33
+ if (value.applicability !== "required" || !keys(value, ["applicability", "identity", "authority", "target"]))
34
+ refuse("invalid_access_policy");
35
+ return { applicability: "required", identity: publicId(value.identity),
36
+ authority: value.authority === "not_required" ? "not_required" : publicId(value.authority), target: publicId(value.target) };
37
+ }
38
+ function binding(value) {
39
+ if (!record(value) || !keys(value, ["id", "selector", "access", "procedures"])
40
+ || typeof value.id !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/.test(value.id)
41
+ || !record(value.procedures) || !keys(value.procedures, ["access_readback", "execute", "result_readback"]))
42
+ refuse("invalid_binding");
43
+ const selectedAccess = access(value.access), procedures = value.procedures;
44
+ if (selectedAccess.applicability === "not_required" && procedures.access_readback !== null
45
+ || selectedAccess.applicability === "required" && procedures.access_readback === null)
46
+ refuse("access_readback_mismatch");
47
+ return { id: value.id, selector: selector(value.selector), access: selectedAccess, procedures: {
48
+ access_readback: procedures.access_readback === null ? null : prepareDeliveryProcedure(procedures.access_readback, "readback"),
49
+ execute: prepareDeliveryProcedure(procedures.execute, "execute"),
50
+ result_readback: prepareDeliveryProcedure(procedures.result_readback, "readback")
51
+ } };
52
+ }
53
+ /** Exact data resolution, not operation permission. The owning flow must bind
54
+ * these project-policy bytes to its frozen decision, independently run safe
55
+ * access readback and check separate current approval for the exact artifact,
56
+ * version, RUN/request and target. Never infer account access from this match. */
57
+ export function resolveDeliveryBinding(policy, selected) {
58
+ const requested = selector(selected);
59
+ if (!record(policy) || !keys(policy, ["schema_id", "bindings"]) || policy.schema_id !== "dd-flow/delivery-policy@1"
60
+ || !Array.isArray(policy.bindings) || policy.bindings.length > 256)
61
+ refuse("invalid_policy");
62
+ const bindings = policy.bindings.map(binding);
63
+ if (new Set(bindings.map(item => item.id)).size !== bindings.length)
64
+ refuse("duplicate_binding_id");
65
+ const matches = bindings.filter(item => selectorKeys.every(key => item.selector[key] === requested[key]));
66
+ if (!matches.length)
67
+ refuse("no_exact_binding");
68
+ if (matches.length !== 1)
69
+ refuse("ambiguous_binding");
70
+ return matches[0];
71
+ }
72
+ /** Observe a selected project file, not a binding invented in an agent result.
73
+ * Revalidation stays outside SQL writers; no provider command runs here. */
74
+ export function prepareDeliveryProjectBinding(workspace, reference, selected) {
75
+ if (!reference || typeof reference.path !== "string" || path.isAbsolute(reference.path))
76
+ refuse("project_policy_relative_path_required");
77
+ const file = PreparedDeliveryArtifact.prepare(workspace, reference.path, { subject_ref: "delivery-policy", version: "1", sha256: reference.sha256 });
78
+ const bytes = readBoundedReleaseFile(path.join(workspace, reference.path), 1024 * 1024);
79
+ if (createHash("sha256").update(bytes).digest("hex") !== reference.sha256)
80
+ refuse("project_policy_changed");
81
+ let policy;
82
+ try {
83
+ policy = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
84
+ }
85
+ catch {
86
+ return refuse("invalid_project_policy");
87
+ }
88
+ const binding = resolveDeliveryBinding(policy, selected);
89
+ file.assertCurrent();
90
+ return { binding, assertCurrent: () => file.assertCurrent() };
91
+ }
@@ -0,0 +1,43 @@
1
+ import { AppError } from "../shared/errors.js";
2
+ const record = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
3
+ const text = (value) => typeof value === "string" && value.length > 0 && value.length <= 16384 && !value.includes("\0");
4
+ const identifier = (value) => typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/.test(value);
5
+ function refuse(reason) {
6
+ throw new AppError("delivery_procedure_invalid", "Delivery requires an exact project-owned noninteractive procedure", 2, { phase: "prepare", effect: "no_effect", recoverable: true, reason });
7
+ }
8
+ function step(value, effect) {
9
+ if (!record(value) || Object.keys(value).some(key => !["id", "argv", "side_effect", "noninteractive"].includes(key))
10
+ || !identifier(value.id) || value.side_effect !== effect || value.noninteractive !== true
11
+ || !Array.isArray(value.argv) || !value.argv.length || value.argv.length > 128 || !value.argv.every(text))
12
+ refuse("invalid_step");
13
+ return { id: value.id, argv: [...value.argv], side_effect: effect, noninteractive: true };
14
+ }
15
+ /** Prepare policy data only: no process, environment lookup, permission or
16
+ * provider-safety proof. Callers must bind reviewed project policy and separate
17
+ * operation approval before execution. Pass each argv directly with shell:false;
18
+ * never concatenate steps into shell text or persist authenticated raw output. */
19
+ export function prepareDeliveryProcedure(value, purpose) {
20
+ if (!record(value) || Object.keys(value).some(key => !["id", "argv", "procedure_steps", "side_effect", "noninteractive", "timeout_ms", "permitted_environment_variables"].includes(key))
21
+ || !identifier(value.id) || value.side_effect !== "read_only" && value.side_effect !== "mutation" || value.noninteractive !== true
22
+ || !Number.isSafeInteger(value.timeout_ms) || value.timeout_ms < 1 || value.timeout_ms > 1800000
23
+ || !Array.isArray(value.permitted_environment_variables) || value.permitted_environment_variables.length > 128
24
+ || !value.permitted_environment_variables.every(name => typeof name === "string" && /^[A-Z_][A-Z0-9_]*$/.test(name))
25
+ || new Set(value.permitted_environment_variables).size !== value.permitted_environment_variables.length
26
+ || Object.hasOwn(value, "argv") === Object.hasOwn(value, "procedure_steps"))
27
+ refuse("invalid_procedure");
28
+ if (purpose !== "readback" && purpose !== "execute")
29
+ refuse("invalid_purpose");
30
+ const effect = value.side_effect;
31
+ if (purpose === "readback" && effect !== "read_only")
32
+ refuse("readback_must_be_read_only");
33
+ const steps = Object.hasOwn(value, "argv")
34
+ ? [step({ id: value.id, argv: value.argv, side_effect: effect, noninteractive: true }, effect)]
35
+ : value.procedure_steps;
36
+ if (!Array.isArray(steps) || !steps.length || steps.length > 16)
37
+ refuse("invalid_steps");
38
+ const prepared = steps.map(item => step(item, effect));
39
+ if (new Set(prepared.map(item => item.id)).size !== prepared.length)
40
+ refuse("duplicate_step");
41
+ return { id: value.id, side_effect: effect, noninteractive: true, timeout_ms: value.timeout_ms,
42
+ permitted_environment_variables: [...value.permitted_environment_variables], procedure_steps: prepared };
43
+ }