@deksden-com/dd-flow-cli 0.9.0-beta.151 → 0.9.0-beta.155
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 +23 -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 +273 -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 +170 -25
- package/dist/harness-runtime/lib/dd-opencode.mjs +40 -19
- 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 +1 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +10 -3
- 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 +77 -10
- package/dist/services/check-runner-proof.js +69 -1
- package/dist/services/cli-operation-classifier.js +6 -1
- package/dist/services/code-checks.js +56 -23
- package/dist/services/codex-hook-delivery.js +11 -2
- package/dist/services/controller-execution.js +4 -1
- package/dist/services/controller-fanout.js +9 -4
- 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 +214 -24
- 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 -4
- package/dist/services/retained-native-hook.js +245 -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 +12 -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 +209 -43
- 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 +9 -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 +252 -66
- package/dist/services/worktrees.js +7 -4
- package/dist/storage/database.js +2 -1
- package/dist/verification-components.json +82 -0
- package/package.json +4 -3
- package/tools/verification-jobs.mjs +342 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { AppError } from "../shared/errors.js";
|
|
2
|
+
import { readDeliveryStageScope } from "./delivery-stage-entry.js";
|
|
3
|
+
import { readDescriptorStageInput } from "./descriptor-stage-coordinator.js";
|
|
4
|
+
import { readActiveDescriptorStageCoordinator, readAcceptedDescriptorStageSettlement } from "./descriptor-stage-settlement.js";
|
|
5
|
+
import { readAcceptedStageData } from "./stage-report-renderer.js";
|
|
6
|
+
import { prepareDeliveryProjectBinding } from "./delivery-policy.js";
|
|
7
|
+
import { PreparedDeliveryArtifact } from "./delivery-artifact.js";
|
|
8
|
+
import { deliveryApprovalScope } from "./delivery-approval.js";
|
|
9
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
10
|
+
import { codeCheckProfileHash, projectPolicyCheckDeclarations, runCodeChecks, PreparedCheckEvidence, PreparedRetainedCheckEvidence } from "./code-checks.js";
|
|
11
|
+
import { entityReferenceVariants } from "../shared/entity-references.js";
|
|
12
|
+
import { stageLifecycleMatchKey } from "./hooks.js";
|
|
13
|
+
import { assertDiagnosticSessionIndependence } from "./diagnostic-work-collection.js";
|
|
14
|
+
import { memoryCheckEnvironment } from "./memory-verification-checks.js";
|
|
15
|
+
function refuse(reason) {
|
|
16
|
+
throw new AppError("delivery_checks_invalid", "Delivery verification needs current project checks for the exact selected artifact", 2, { reason, effect: "no_effect" });
|
|
17
|
+
}
|
|
18
|
+
/** Compare actual provider Sessions, not model names or distinct logical IDs.
|
|
19
|
+
* Accepted generic settlement proves the author; current coordinator custody
|
|
20
|
+
* proves the verifier. Lifecycle match keys resolve paths before the writer. */
|
|
21
|
+
export function prepareDeliveryVerifierIndependence(context, input) {
|
|
22
|
+
if (input.stage !== "verify" || context.db.isOwnedWriteTransaction?.())
|
|
23
|
+
refuse("verification_stage_required");
|
|
24
|
+
const selected = readDeliveryStageScope(context, input), { scope, bundle } = selected;
|
|
25
|
+
const stages = selected.flowId === "deploy" ? ["prepare", "deploy"] : ["prepare"];
|
|
26
|
+
const sources = stages.map(stage => ({ stage, keys: entityReferenceVariants(input.runId)
|
|
27
|
+
.map(reference => stageLifecycleMatchKey("stage_finish", reference, stage, input.projectRoot)) }));
|
|
28
|
+
const observe = (reader) => {
|
|
29
|
+
selected.assertCustody(reader);
|
|
30
|
+
const physical = (sessionId) => {
|
|
31
|
+
const session = reader.db.get("SELECT harness,provider_session_id FROM sessions WHERE project_id=? AND run_id=? AND session_id=?", [scope.projectId, scope.runId, sessionId]);
|
|
32
|
+
if (!session?.harness || !session.provider_session_id)
|
|
33
|
+
refuse("verification_physical_owner_required");
|
|
34
|
+
return session;
|
|
35
|
+
};
|
|
36
|
+
const verifier = readActiveDescriptorStageCoordinator(reader, scope, bundle);
|
|
37
|
+
const authors = sources.map(source => readAcceptedDescriptorStageSettlement(reader, { ...scope, stage: source.stage }, bundle, source.keys));
|
|
38
|
+
const verifierSession = physical(verifier.session_id), authorSessions = authors.map(author => physical(author.binding.sessionId));
|
|
39
|
+
assertDiagnosticSessionIndependence([verifierSession], authorSessions);
|
|
40
|
+
return { verifier, authors, verifierSession, authorSessions };
|
|
41
|
+
};
|
|
42
|
+
const original = observe(context);
|
|
43
|
+
return (writer) => {
|
|
44
|
+
if (hybridDigest(observe(writer)) !== hybridDigest(original))
|
|
45
|
+
refuse("verification_physical_owner_changed");
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** Downstream acceptance reuses the original accepted execution proof, not
|
|
49
|
+
* today's declarations, invented hashes or a new run of publication checks. */
|
|
50
|
+
export function prepareDeliveryHistoricalChecks(context, input) {
|
|
51
|
+
if (context.db.isOwnedWriteTransaction?.())
|
|
52
|
+
refuse("preparation_outside_writer_required");
|
|
53
|
+
const selected = readDeliveryStageScope(context, input), { scope, bundle } = selected;
|
|
54
|
+
if (input.sourceStage !== (selected.flowId === "deploy" && input.stage === "deploy" ? "prepare" : "verify"))
|
|
55
|
+
refuse("accepted_verification_stage_required");
|
|
56
|
+
const keys = entityReferenceVariants(input.runId).map(reference => stageLifecycleMatchKey("stage_finish", reference, input.sourceStage, input.projectRoot));
|
|
57
|
+
const read = (reader) => {
|
|
58
|
+
const accepted = readAcceptedStageData(reader, { ...scope, stage: input.sourceStage });
|
|
59
|
+
const data = JSON.parse(accepted.bytes.toString("utf8"));
|
|
60
|
+
const rows = reader.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type='delivery.stage_checks' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.stage')=? LIMIT 2", [scope.projectId, scope.runId, input.sourceStage]);
|
|
61
|
+
if (rows.length !== 1 || Buffer.byteLength(rows[0].payload_json) > 1024 * 1024)
|
|
62
|
+
refuse("accepted_verification_required");
|
|
63
|
+
const value = JSON.parse(rows[0].payload_json), receipt = value.receipt;
|
|
64
|
+
if (data.schema_id !== "dd-flow/delivery-stage-result@1" || data.flow_id !== selected.flowId || data.run_id !== input.runId || data.stage !== input.sourceStage
|
|
65
|
+
|| !receipt || value.sha256 !== hybridDigest(receipt) || value.sha256 !== data.details?.verification_sha256
|
|
66
|
+
|| receipt.schema_id !== "dd-flow/delivery-checks@1" || receipt.run_id !== input.runId || receipt.stage !== input.sourceStage
|
|
67
|
+
|| receipt.bundle_sha256 !== bundle.sha256 || receipt.input_sha256 !== data.input_sha256
|
|
68
|
+
|| receipt.operation_scope_sha256 !== input.operationScopeSha256 || data.details.delivery_scope_sha256 !== input.operationScopeSha256
|
|
69
|
+
|| !Array.isArray(receipt.checks) || !receipt.checks.length)
|
|
70
|
+
refuse("accepted_verification_changed");
|
|
71
|
+
return { accepted, value, owner: readAcceptedDescriptorStageSettlement(reader, { ...scope, stage: input.sourceStage }, bundle, keys) };
|
|
72
|
+
};
|
|
73
|
+
const original = read(context);
|
|
74
|
+
const proof = PreparedRetainedCheckEvidence.prepare(context, { ...scope, references: original.value.receipt.checks });
|
|
75
|
+
return { sha256: original.value.sha256, receipt: structuredClone(original.value.receipt),
|
|
76
|
+
revalidate() { proof.revalidate(context); },
|
|
77
|
+
assertCustody(writer) {
|
|
78
|
+
selected.assertCustody(writer);
|
|
79
|
+
proof.assertCustody(writer);
|
|
80
|
+
if (hybridDigest(read(writer)) !== hybridDigest(original))
|
|
81
|
+
refuse("accepted_verification_changed");
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/** Immediately before an external effect, historical success is insufficient:
|
|
86
|
+
* the existing check owner must still match today's workspace and policy.
|
|
87
|
+
* This also catches an edited publish/deploy script with unchanged argv. */
|
|
88
|
+
export function prepareDeliveryCurrentAcceptedChecks(context, input) {
|
|
89
|
+
const historical = prepareDeliveryHistoricalChecks(context, input), selected = readDeliveryStageScope(context, input);
|
|
90
|
+
if (!selected.run.run_root)
|
|
91
|
+
refuse("run_root_required");
|
|
92
|
+
const workspace = selected.bundle.execution.snapshot().scope.workspace;
|
|
93
|
+
const checks = [...projectPolicyCheckDeclarations(workspace, "code"), ...projectPolicyCheckDeclarations(workspace, "readiness")];
|
|
94
|
+
const environment = memoryCheckEnvironment(context, selected.scope.projectId, input.runId);
|
|
95
|
+
const proof = PreparedCheckEvidence.prepare(context, { projectId: selected.scope.projectId, runId: input.runId,
|
|
96
|
+
runHome: selected.run.run_root, workspaceRoot: workspace, scope: "aggregate", checks,
|
|
97
|
+
expectedProfileHash: historical.receipt.profile_sha256, evidenceContext: historical.receipt.evidence_context,
|
|
98
|
+
...(environment ? { checkEnvironment: environment } : {}) });
|
|
99
|
+
if (!proof.qualityPassed() || hybridDigest(proof.retainedReferences(context)) !== hybridDigest(historical.receipt.checks))
|
|
100
|
+
refuse("accepted_verification_changed");
|
|
101
|
+
return {
|
|
102
|
+
revalidate() {
|
|
103
|
+
if (hybridDigest(memoryCheckEnvironment(context, selected.scope.projectId, input.runId) ?? null) !== hybridDigest(environment ?? null))
|
|
104
|
+
refuse("managed_check_home_changed");
|
|
105
|
+
historical.revalidate();
|
|
106
|
+
proof.revalidate(context);
|
|
107
|
+
},
|
|
108
|
+
assertCustody(writer) { historical.assertCustody(writer); proof.assertCustody(writer); }
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/** The existing aggregate runner/check receipts, not model-reported tests.
|
|
112
|
+
* Declarations come from project policy, never the result being accepted.
|
|
113
|
+
* Semantic adequacy and independent verification remain Stage responsibilities. */
|
|
114
|
+
export function prepareDeliveryChecks(context, input) {
|
|
115
|
+
if (context.db.isOwnedWriteTransaction?.() || !["prepare", "verify"].includes(input.stage))
|
|
116
|
+
refuse("verification_stage_required");
|
|
117
|
+
if (!input.artifact || typeof input.artifact !== "object" || Array.isArray(input.artifact))
|
|
118
|
+
refuse("accepted_artifact_selection_required");
|
|
119
|
+
const selected = readDeliveryStageScope(context, input), { scope, bundle } = selected;
|
|
120
|
+
const independent = input.stage === "verify" ? prepareDeliveryVerifierIndependence(context, input) : undefined;
|
|
121
|
+
const packet = readDescriptorStageInput(context, scope, bundle);
|
|
122
|
+
if (!packet.input.predecessor || !selected.run.run_root)
|
|
123
|
+
refuse("accepted_predecessor_required");
|
|
124
|
+
const previousStage = packet.input.predecessor.stage;
|
|
125
|
+
const previous = readAcceptedStageData(context, { ...scope, stage: previousStage });
|
|
126
|
+
const prior = JSON.parse(previous.bytes.toString("utf8"));
|
|
127
|
+
const decision = prior.details.decision;
|
|
128
|
+
if (!decision || input.stage !== "prepare" && hybridDigest(prior.details.artifact) !== hybridDigest(input.artifact))
|
|
129
|
+
refuse("accepted_artifact_selection_required");
|
|
130
|
+
const workspace = bundle.execution.snapshot().scope.workspace;
|
|
131
|
+
const policy = prepareDeliveryProjectBinding(workspace, decision.policy, decision.binding.selector);
|
|
132
|
+
if (hybridDigest(policy.binding) !== hybridDigest(decision.binding))
|
|
133
|
+
refuse("accepted_policy_changed");
|
|
134
|
+
const artifact = PreparedDeliveryArtifact.prepare(workspace, input.artifact.path, { subject_ref: input.artifact.subject_ref, version: input.artifact.version, sha256: input.artifact.sha256 });
|
|
135
|
+
if (input.artifact.version !== decision.version)
|
|
136
|
+
refuse("accepted_version_required");
|
|
137
|
+
const operation = deliveryApprovalScope(scope, bundle, { operationId: decision.operation_id, binding: policy.binding,
|
|
138
|
+
artifact: { subject_ref: input.artifact.subject_ref, version: input.artifact.version, sha256: input.artifact.sha256 } });
|
|
139
|
+
const checks = [...projectPolicyCheckDeclarations(workspace, "code"), ...projectPolicyCheckDeclarations(workspace, "readiness")];
|
|
140
|
+
if (!checks.length)
|
|
141
|
+
refuse("project_verification_checks_required");
|
|
142
|
+
const owner = readActiveDescriptorStageCoordinator(context, scope, bundle), profile = codeCheckProfileHash(workspace, checks);
|
|
143
|
+
const environment = memoryCheckEnvironment(context, scope.projectId, input.runId);
|
|
144
|
+
const evidenceContext = `delivery:${hybridDigest({ operation, stage: input.stage, input: packet.inputSha256, users: selected.userInputsSha256 })}`;
|
|
145
|
+
const command = { projectId: scope.projectId, runId: scope.runId, runHome: selected.run.run_root, workspaceRoot: workspace,
|
|
146
|
+
scope: "aggregate", checks, expectedProfileHash: profile, evidenceContext,
|
|
147
|
+
...(environment ? { checkEnvironment: environment } : {}) };
|
|
148
|
+
const assertCustody = (writer) => {
|
|
149
|
+
selected.assertCustody(writer);
|
|
150
|
+
independent?.(writer);
|
|
151
|
+
if (hybridDigest(readDescriptorStageInput(writer, scope, bundle)) !== hybridDigest(packet)
|
|
152
|
+
|| hybridDigest(readAcceptedStageData(writer, { ...scope, stage: previousStage })) !== hybridDigest(previous)
|
|
153
|
+
|| hybridDigest(readActiveDescriptorStageCoordinator(writer, scope, bundle)) !== hybridDigest(owner))
|
|
154
|
+
refuse("verification_owner_or_inputs_changed");
|
|
155
|
+
};
|
|
156
|
+
const revalidate = () => {
|
|
157
|
+
assertCustody(context);
|
|
158
|
+
policy.assertCurrent();
|
|
159
|
+
artifact.assertCurrent();
|
|
160
|
+
if (hybridDigest(memoryCheckEnvironment(context, scope.projectId, input.runId) ?? null) !== hybridDigest(environment ?? null))
|
|
161
|
+
refuse("managed_check_home_changed");
|
|
162
|
+
if (codeCheckProfileHash(workspace, checks) !== profile)
|
|
163
|
+
refuse("verification_policy_changed");
|
|
164
|
+
};
|
|
165
|
+
revalidate();
|
|
166
|
+
return { operation: structuredClone(operation), artifact: structuredClone(input.artifact), assertCustody, revalidate,
|
|
167
|
+
async run() { revalidate(); const result = await runCodeChecks(context, command); revalidate(); return result; },
|
|
168
|
+
evidence() {
|
|
169
|
+
revalidate();
|
|
170
|
+
const proof = PreparedCheckEvidence.prepare(context, command);
|
|
171
|
+
if (!proof.qualityPassed())
|
|
172
|
+
refuse("verification_checks_failed");
|
|
173
|
+
return { proof, receipt: { schema_id: "dd-flow/delivery-checks@1", run_id: scope.runId, stage: input.stage,
|
|
174
|
+
bundle_sha256: bundle.sha256, input_sha256: packet.inputSha256, operation_scope_sha256: hybridDigest(operation),
|
|
175
|
+
evidence_context: evidenceContext, profile_sha256: profile, checks: proof.retainedReferences(context) } };
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
@@ -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
|
+
}
|