@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.
- package/CHANGELOG.md +31 -0
- package/dist/build-info.json +8 -8
- package/dist/cli/command-inputs.js +33 -3
- package/dist/cli/command-metadata.js +73 -2
- package/dist/cli/delivery.js +108 -0
- package/dist/cli/diagnostic-invocation.js +23 -7
- package/dist/cli/diagnostic.js +14 -3
- package/dist/cli/help.js +117 -1
- package/dist/cli/hook-ingress.js +36 -49
- package/dist/cli/interactive-invocation.js +33 -0
- package/dist/cli/interactive.js +114 -0
- package/dist/cli/memory-fix-invocation.js +52 -0
- package/dist/cli/memory-fix.js +100 -0
- package/dist/cli/memory-flow-admission.js +7 -3
- package/dist/cli/memory-init.js +5 -4
- package/dist/cli/native-hook-ingress.js +5 -1
- package/dist/cli/native-hook-preparation.js +31 -4
- package/dist/cli/run-cli.js +274 -19
- package/dist/cli.js +17 -3
- package/dist/domain/delivery-flows.js +8 -0
- package/dist/domain/distill-flow.js +20 -0
- package/dist/domain/interactive-flow.js +1 -0
- package/dist/domain/memory-fix-flow.js +2 -0
- package/dist/domain/review-fix-flow.js +2 -0
- package/dist/domain/source-snapshot-flows.js +4 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +18 -3
- package/dist/harness-runtime/bin/dd-grok.mjs +15 -5
- package/dist/harness-runtime/bin/dd-zcode.mjs +15 -7
- package/dist/harness-runtime/lib/agy-error-boundary.mjs +18 -10
- package/dist/harness-runtime/lib/agy-native-inspection.mjs +12 -3
- package/dist/harness-runtime/lib/agy-native-inventory.mjs +19 -0
- package/dist/harness-runtime/lib/agy-terminal-envelope.mjs +13 -6
- package/dist/harness-runtime/lib/agy-transcript-stream.mjs +54 -0
- package/dist/harness-runtime/lib/daemon-observation.d.mts +1 -0
- package/dist/harness-runtime/lib/daemon-observation.mjs +12 -4
- package/dist/harness-runtime/lib/daemon-operations.mjs +20 -5
- package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +371 -114
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +43 -3
- package/dist/harness-runtime/lib/dd-codex.mjs +36 -4
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +25 -11
- package/dist/harness-runtime/lib/dd-droid.mjs +124 -11
- package/dist/harness-runtime/lib/dd-grok-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +137 -25
- package/dist/harness-runtime/lib/dd-grok.mjs +4 -2
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +115 -17
- package/dist/harness-runtime/lib/dd-opencode.mjs +10 -7
- package/dist/harness-runtime/lib/dd-zcode-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +115 -22
- package/dist/harness-runtime/lib/dd-zcode.mjs +75 -36
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +2 -1
- package/dist/harness-runtime/lib/delegation-instructions.mjs +15 -5
- package/dist/harness-runtime/lib/delivery-command.mjs +84 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +0 -19
- package/dist/harness-runtime/lib/droid-observation.mjs +9 -5
- package/dist/harness-runtime/lib/lifecycle-contract.d.mts +1 -0
- package/dist/harness-runtime/lib/lifecycle-contract.mjs +2 -1
- package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -2
- package/dist/harness-runtime/lib/native-hook-command.mjs +63 -8
- package/dist/harness-runtime/lib/native-streams.mjs +3 -10
- package/dist/harness-runtime/lib/opencode-plugin.mjs +36 -9
- package/dist/harness-runtime/lib/operation-errors.mjs +13 -0
- package/dist/harness-runtime/lib/operation-progress.mjs +6 -2
- package/dist/harness-runtime/lib/process-json.mjs +4 -5
- package/dist/harness-runtime/lib/tool-observations.mjs +20 -10
- package/dist/harness-runtime/lib/zcode-native-hook-ownership.mjs +109 -0
- package/dist/schemas/code-work-result.schema.json +2 -1
- package/dist/schemas/delivery-stage-result.schema.json +286 -0
- package/dist/schemas/distill-stage-result.schema.json +667 -0
- package/dist/schemas/distill-work-result.schema.json +428 -0
- package/dist/schemas/execution-bundle.schema.json +6 -2
- package/dist/schemas/interactive-stage-result.schema.json +47 -0
- package/dist/schemas/interactive-work-result.schema.json +63 -0
- package/dist/schemas/mb-fix-stage-result.schema.json +93 -0
- package/dist/schemas/project-flow-pack-manifest.schema.json +2 -2
- package/dist/schemas/review-fix-stage-result.schema.json +210 -0
- package/dist/schemas/run-control-receipt.schema.json +1 -0
- package/dist/services/assigned-lifecycle.js +382 -0
- package/dist/services/check-runner-evidence.js +45 -8
- package/dist/services/cli-operation-classifier.js +6 -1
- package/dist/services/code-checks.js +46 -29
- package/dist/services/codex-hook-delivery.js +11 -2
- package/dist/services/controller-child-repair.js +95 -0
- package/dist/services/controller-execution.js +4 -1
- package/dist/services/controller-fanout.js +19 -9
- package/dist/services/controller-memory-worker.js +6 -4
- package/dist/services/controller-result-authority.js +122 -0
- package/dist/services/dashboard.js +4 -2
- package/dist/services/delivery-access.js +123 -0
- package/dist/services/delivery-approval.js +68 -0
- package/dist/services/delivery-artifact.js +86 -0
- package/dist/services/delivery-checks.js +178 -0
- package/dist/services/delivery-execution.js +181 -0
- package/dist/services/delivery-intents.js +212 -0
- package/dist/services/delivery-operation.js +81 -0
- package/dist/services/delivery-policy.js +91 -0
- package/dist/services/delivery-procedures.js +43 -0
- package/dist/services/delivery-request.js +99 -0
- package/dist/services/delivery-result.js +144 -0
- package/dist/services/delivery-stage-commands.js +26 -0
- package/dist/services/delivery-stage-entry.js +56 -0
- package/dist/services/delivery-stage-handlers.js +135 -0
- package/dist/services/delivery-stage-results.js +94 -0
- package/dist/services/descriptor-coordinator-session.js +3 -1
- package/dist/services/descriptor-run-start.js +24 -4
- package/dist/services/descriptor-stage-coordinator.js +78 -10
- package/dist/services/descriptor-stage-settlement.js +59 -14
- package/dist/services/diagnostic-repair-intake.js +123 -0
- package/dist/services/diagnostic-repair-source-intake.js +102 -0
- package/dist/services/diagnostic-report.js +30 -0
- package/dist/services/diagnostic-run-bundle.js +48 -9
- package/dist/services/diagnostic-snapshot.js +2 -2
- package/dist/services/diagnostic-stage-handlers.js +3 -2
- package/dist/services/diagnostic-stage-results.js +26 -0
- package/dist/services/diagnostic-start.js +56 -6
- package/dist/services/diagnostic-work-collection.js +3 -3
- package/dist/services/distill-document-publication.js +280 -0
- package/dist/services/distill-promotion.js +197 -0
- package/dist/services/distill-publication-custody.js +162 -0
- package/dist/services/distill-stage-handlers.js +249 -0
- package/dist/services/distill-stage-results.js +105 -0
- package/dist/services/distill-user-inputs.js +4 -0
- package/dist/services/distill-work-batch.js +42 -0
- package/dist/services/distill-work-collection.js +74 -0
- package/dist/services/distill-work-registration.js +93 -0
- package/dist/services/distill-work-results.js +98 -0
- package/dist/services/eval-snapshots.js +17 -7
- package/dist/services/external-work-launch.js +8 -2
- package/dist/services/flow-catalog.js +24 -0
- package/dist/services/frozen-engine.js +19 -2
- package/dist/services/generic-execution.js +4 -3
- package/dist/services/harness-adapter.js +2 -14
- package/dist/services/hook-memory-routing.js +1 -1
- package/dist/services/hooks.js +31 -6
- package/dist/services/hybrid-contracts.js +6 -1
- package/dist/services/installed-flow-source.js +37 -2
- package/dist/services/interactive-attached-session.js +215 -0
- package/dist/services/interactive-baseline-custody.js +54 -0
- package/dist/services/interactive-before-versions.js +148 -0
- package/dist/services/interactive-bundle-data.js +85 -0
- package/dist/services/interactive-checks.js +179 -0
- package/dist/services/interactive-execution.js +33 -0
- package/dist/services/interactive-finish-integration.js +236 -0
- package/dist/services/interactive-git-baseline.js +230 -0
- package/dist/services/interactive-git-dispatch.js +278 -0
- package/dist/services/interactive-input-delivery.js +82 -0
- package/dist/services/interactive-mutation-scope.js +231 -0
- package/dist/services/interactive-request.js +287 -0
- package/dist/services/interactive-run-bundle.js +109 -0
- package/dist/services/interactive-stage-entry.js +41 -0
- package/dist/services/interactive-stage-handlers.js +289 -0
- package/dist/services/interactive-stage-observation.js +136 -0
- package/dist/services/interactive-stage-results.js +111 -0
- package/dist/services/interactive-user-inputs.js +142 -0
- package/dist/services/interactive-work-batch.js +43 -0
- package/dist/services/interactive-work-collection.js +119 -0
- package/dist/services/interactive-work-completion.js +180 -0
- package/dist/services/interactive-work-context.js +249 -0
- package/dist/services/interactive-work-history.js +112 -0
- package/dist/services/interactive-work-registration.js +124 -0
- package/dist/services/interactive-work-results.js +115 -0
- package/dist/services/interactive-work-start.js +155 -0
- package/dist/services/lifecycle-invocations.js +250 -33
- package/dist/services/managed-daemon-binding.js +52 -1
- package/dist/services/memory-compose-handler.js +2 -22
- package/dist/services/memory-finish-integration.js +58 -17
- package/dist/services/memory-fix-finish-integration.js +129 -0
- package/dist/services/memory-fix-git-dispatch.js +139 -0
- package/dist/services/memory-fix-intake-custody.js +11 -0
- package/dist/services/memory-fix-intake-settlement.js +127 -0
- package/dist/services/memory-fix-publication.js +196 -0
- package/dist/services/memory-fix-repair-admission.js +116 -0
- package/dist/services/memory-fix-request.js +141 -0
- package/dist/services/memory-fix-run-bundle.js +83 -0
- package/dist/services/memory-fix-scope.js +78 -0
- package/dist/services/memory-fix-sources.js +85 -0
- package/dist/services/memory-fix-stage-entry.js +24 -0
- package/dist/services/memory-fix-stage-handlers.js +282 -0
- package/dist/services/memory-fix-stage-results.js +106 -0
- package/dist/services/memory-fix-workspace.js +127 -0
- package/dist/services/memory-flow-user-inputs.js +88 -0
- package/dist/services/memory-init-contexts.js +55 -5
- package/dist/services/memory-init-controller-start.js +20 -4
- package/dist/services/memory-layer.js +7 -3
- package/dist/services/memory-native-review-copy.js +27 -3
- package/dist/services/memory-repair-document-delta.js +1 -0
- package/dist/services/project-flow-source.js +195 -0
- package/dist/services/project-git-integration.js +92 -0
- package/dist/services/repair-intents.js +2 -7
- package/dist/services/retained-native-hook.js +274 -0
- package/dist/services/review-fix-handoff.js +82 -0
- package/dist/services/review-fix-intake-custody.js +17 -0
- package/dist/services/review-fix-stage-entry.js +43 -0
- package/dist/services/review-fix-stage-handlers.js +176 -0
- package/dist/services/review-fix-stage-results.js +86 -0
- package/dist/services/run-control-worker.js +42 -1
- package/dist/services/run-control.js +40 -5
- package/dist/services/run-controller-adapter.js +37 -3
- package/dist/services/run-controller-process.js +25 -4
- package/dist/services/run-controller-recovery.js +46 -6
- package/dist/services/run-controller-state.js +3 -2
- package/dist/services/run-controller.js +257 -44
- package/dist/services/run-recovery.js +31 -2
- package/dist/services/run-timeline.js +59 -0
- package/dist/services/runs.js +18 -31
- package/dist/services/sessions.js +2 -1
- package/dist/services/stage-handlers.js +55 -3
- package/dist/services/stage-lifecycle.js +4 -3
- package/dist/services/stage-pause.js +26 -9
- package/dist/services/stage-report-renderer.js +2 -6
- package/dist/services/technical-notes.js +31 -0
- package/dist/services/verification-matrix.js +1 -1
- package/dist/services/version-status.js +1 -1
- package/dist/services/vnext-code-review.js +74 -5
- package/dist/services/vnext-code.js +8 -32
- package/dist/services/vnext-fanout.js +53 -8
- package/dist/services/vnext-merge.js +10 -5
- package/dist/services/vnext-plan-review.js +1 -1
- package/dist/services/vnext-plan.js +1 -1
- package/dist/services/vnext-protocolize.js +1 -1
- package/dist/services/vnext-specify.js +10 -5
- package/dist/services/work-registry.js +255 -69
- package/dist/services/worktrees.js +7 -4
- package/dist/storage/database.js +2 -1
- package/dist/verification-components.json +82 -0
- package/package.json +3 -2
- 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
|
+
}
|