@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,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
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { AppError } from "../shared/errors.js";
|
|
2
|
+
import { isDeliveryFlow } from "../domain/delivery-flows.js";
|
|
3
|
+
import { PreparedInstalledFlowSource } from "./installed-flow-source.js";
|
|
4
|
+
import { FrozenGenericExecution } from "./generic-execution.js";
|
|
5
|
+
import { AdmittedFrozenEngine } from "./frozen-engine.js";
|
|
6
|
+
import { requireProjectByRoot } from "./projects.js";
|
|
7
|
+
import { readRunStartRequest, runStartPayloadLimit } from "./run-start-custody.js";
|
|
8
|
+
import { PreparedDescriptorRunStart } from "./descriptor-run-start.js";
|
|
9
|
+
import { prepareDeliveryRunBundle } from "./diagnostic-run-bundle.js";
|
|
10
|
+
import { appendAudit } from "./audit.js";
|
|
11
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
12
|
+
const event = "delivery.request";
|
|
13
|
+
function refuse(reason) {
|
|
14
|
+
throw new AppError("delivery_request_invalid", "Delivery requires its original request, workspace and frozen selection", 2, { reason, effect: "no_effect", next_action: "reconcile_exact_request" });
|
|
15
|
+
}
|
|
16
|
+
/** SQL/data only: retries never consult a newer operator selection. */
|
|
17
|
+
export function readDeliveryRequest(context, scope) {
|
|
18
|
+
if (!isDeliveryFlow(scope.flowId) || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,95}$/.test(scope.requestId))
|
|
19
|
+
refuse("invalid_scope");
|
|
20
|
+
const project = requireProjectByRoot(context, scope.projectRoot);
|
|
21
|
+
const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type=? AND json_extract(payload_json,'$.scope.requestId')=? LIMIT 2", [project.id, event, scope.requestId]);
|
|
22
|
+
if (!rows.length)
|
|
23
|
+
return null;
|
|
24
|
+
if (rows.length !== 1 || Buffer.byteLength(rows[0].payload_json) > runStartPayloadLimit)
|
|
25
|
+
refuse("ambiguous_request");
|
|
26
|
+
const value = JSON.parse(rows[0].payload_json);
|
|
27
|
+
if (value.schema_id !== "dd-flow/delivery-request@1" || value.project_id !== project.id
|
|
28
|
+
|| hybridDigest(value.scope) !== hybridDigest(scope) || !Number.isFinite(Date.parse(value.created_at))
|
|
29
|
+
|| typeof value.objective !== "string" || !value.objective.trim() || Buffer.byteLength(value.objective) > 64 * 1024
|
|
30
|
+
|| !value.execution || hybridDigest(value.execution) !== value.execution_sha256)
|
|
31
|
+
refuse("request_changed");
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
export function retainDeliveryRequest(context, input) {
|
|
35
|
+
if (context.db.isOwnedWriteTransaction?.())
|
|
36
|
+
refuse("prepare_outside_writer");
|
|
37
|
+
if (!(input.source instanceof PreparedInstalledFlowSource) || !(input.execution instanceof FrozenGenericExecution)
|
|
38
|
+
|| !(input.engine instanceof AdmittedFrozenEngine) || !isDeliveryFlow(input.scope.flowId))
|
|
39
|
+
refuse("prepared_selection_required");
|
|
40
|
+
const frozen = input.execution.snapshot(), flow = input.source.resolveRegisteredFlow(), project = requireProjectByRoot(context, input.scope.projectRoot);
|
|
41
|
+
if (frozen.scope.projectRoot !== input.scope.projectRoot || frozen.scope.requestId !== input.scope.requestId
|
|
42
|
+
|| frozen.settings.flow_id !== input.scope.flowId || flow.descriptor.id !== input.scope.flowId
|
|
43
|
+
|| frozen.settings.descriptor_sha256 !== flow.sha256 || frozen.settings.stage_session_mode === "same_session"
|
|
44
|
+
|| frozen.settings.attached_session_receipt || frozen.settings.delegation.native_capacity_receipt
|
|
45
|
+
|| !input.objective.trim() || Buffer.byteLength(input.objective) > 64 * 1024)
|
|
46
|
+
refuse("selection_changed");
|
|
47
|
+
input.source.revalidate();
|
|
48
|
+
input.engine.admitDeliveryFlow(input.source, input.scope.flowId);
|
|
49
|
+
const manifest = input.engine.manifest(), checksum = manifest.integrity.checksum;
|
|
50
|
+
if (typeof checksum !== "string")
|
|
51
|
+
refuse("engine_identity_required");
|
|
52
|
+
const value = { schema_id: "dd-flow/delivery-request@1", project_id: project.id, scope: structuredClone(input.scope),
|
|
53
|
+
created_at: context.now(), objective: input.objective, selection: structuredClone(input.selection),
|
|
54
|
+
source: input.source.snapshot(), source_sha256: input.source.sha256, execution: frozen, execution_sha256: input.execution.sha256,
|
|
55
|
+
engine: { identity: { package_name: manifest.package_name, package_version: manifest.package_version, engine_version: manifest.engine_version,
|
|
56
|
+
integrity_checksum: checksum, snapshot_root: manifest.snapshot_root }, manifest_sha256: input.engine.manifestSha256 } };
|
|
57
|
+
if (Buffer.byteLength(JSON.stringify(value)) > runStartPayloadLimit)
|
|
58
|
+
refuse("request_exceeds_bound");
|
|
59
|
+
return context.db.writeTransaction(() => {
|
|
60
|
+
const prior = readDeliveryRequest(context, input.scope);
|
|
61
|
+
if (prior) {
|
|
62
|
+
if (hybridDigest({ ...value, created_at: prior.created_at }) !== hybridDigest(prior))
|
|
63
|
+
refuse("request_conflict");
|
|
64
|
+
return prior;
|
|
65
|
+
}
|
|
66
|
+
if (readRunStartRequest(context, project.id, input.scope.requestId))
|
|
67
|
+
refuse("foreign_run_request");
|
|
68
|
+
appendAudit(context, { projectId: project.id, eventType: event, payload: value });
|
|
69
|
+
return value;
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/** Called inside the common RUN allocation writer: retained data only. */
|
|
73
|
+
export function assertDeliveryRequestBundle(context, projectId, bundle) {
|
|
74
|
+
const frozen = bundle.execution.snapshot(), flowId = bundle.manifest.flow_id;
|
|
75
|
+
if (!isDeliveryFlow(flowId))
|
|
76
|
+
refuse("delivery_bundle_required");
|
|
77
|
+
const request = readDeliveryRequest(context, { projectRoot: frozen.scope.projectRoot, requestId: frozen.scope.requestId, flowId });
|
|
78
|
+
if (!request || request.project_id !== projectId || request.execution_sha256 !== bundle.execution.sha256
|
|
79
|
+
|| request.source.descriptor.sha256 !== bundle.manifest.descriptor.sha256 || bundle.manifest.bootstrap_receipts.length
|
|
80
|
+
|| request.engine.manifest_sha256 !== bundle.manifest.engine_manifest.sha256
|
|
81
|
+
|| !bundle.blob("runtime/source-request.md").equals(Buffer.from(request.objective)))
|
|
82
|
+
refuse("original_request_bundle_required");
|
|
83
|
+
for (const item of request.source.blobs)
|
|
84
|
+
if (!bundle.blob(item.path).equals(Buffer.from(item.base64, "base64")))
|
|
85
|
+
refuse("source_bytes_changed");
|
|
86
|
+
}
|
|
87
|
+
export function startDeliveryRun(context, scope) {
|
|
88
|
+
const request = readDeliveryRequest(context, scope);
|
|
89
|
+
if (!request)
|
|
90
|
+
refuse("original_request_required");
|
|
91
|
+
const project = requireProjectByRoot(context, scope.projectRoot);
|
|
92
|
+
if (readRunStartRequest(context, project.id, scope.requestId))
|
|
93
|
+
return PreparedDescriptorRunStart.restore(context, scope).apply(context);
|
|
94
|
+
const source = PreparedInstalledFlowSource.restoreRetained(request.source, request.source_sha256);
|
|
95
|
+
const execution = FrozenGenericExecution.restore(request.execution, request.execution_sha256, source.resolveRegisteredFlow(), request.execution.scope);
|
|
96
|
+
const engine = AdmittedFrozenEngine.admitSelection(request.engine.identity, request.engine.manifest_sha256);
|
|
97
|
+
const bundle = prepareDeliveryRunBundle(context, { source, execution, engine, boundAt: request.created_at, requestText: request.objective });
|
|
98
|
+
return PreparedDescriptorRunStart.prepare(context, { bundle }).apply(context);
|
|
99
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { types } from "node:util";
|
|
3
|
+
import { AppError } from "../shared/errors.js";
|
|
4
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
5
|
+
import { resolveDeliveryBinding } from "./delivery-policy.js";
|
|
6
|
+
import { executeDeliveryStep } from "./delivery-execution.js";
|
|
7
|
+
import { readDeliveryIntent } from "./delivery-intents.js";
|
|
8
|
+
import { appendAudit } from "./audit.js";
|
|
9
|
+
import { managedProcessStatus, managedProcessBinding, processTreeIsAlive, resourceHome } from "./managed-processes.js";
|
|
10
|
+
const record = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
11
|
+
const text = (value) => typeof value === "string" && Boolean(value.trim()) && value.length <= 4096 && !value.includes("\0");
|
|
12
|
+
const reference = (value) => value === null || record(value) && Object.keys(value).sort().join() === "id,kind" && text(value.id) && text(value.kind);
|
|
13
|
+
function validRollback(value, operation) {
|
|
14
|
+
if (operation !== "deploy")
|
|
15
|
+
return !Object.hasOwn(value, "rollback");
|
|
16
|
+
const rollback = value.rollback;
|
|
17
|
+
return record(rollback) && Object.keys(rollback).sort().join() === "reason,reference,status,strategy"
|
|
18
|
+
&& typeof rollback.strategy === "string" && ["rollback", "roll_forward", "operator_handoff"].includes(rollback.strategy)
|
|
19
|
+
&& typeof rollback.status === "string" && ["not_needed", "required", "completed", "failed", "unknown"].includes(rollback.status)
|
|
20
|
+
&& text(rollback.reason) && reference(rollback.reference)
|
|
21
|
+
&& (rollback.status !== "completed" || rollback.reference !== null)
|
|
22
|
+
&& (value.status === "complete" ? rollback.status === "not_needed" : rollback.status !== "not_needed");
|
|
23
|
+
}
|
|
24
|
+
function refuse(reason) {
|
|
25
|
+
throw new AppError("delivery_result_unproven", "Read back the original delivery; do not repeat its mutation", 2, { reason, effect: "unknown", next_action: "reconcile_original_delivery" });
|
|
26
|
+
}
|
|
27
|
+
/** Settlement reads the latest owned observation, never a supplied filename
|
|
28
|
+
* or an older optimistic answer after a newer uncertain readback. A changed
|
|
29
|
+
* operation log needs a new observation; this is not a retry authorization. */
|
|
30
|
+
export function readDeliveryResult(context, scope) {
|
|
31
|
+
const intent = readDeliveryIntent(context, scope);
|
|
32
|
+
if (!intent)
|
|
33
|
+
refuse("original_intent_required");
|
|
34
|
+
const row = context.db.get("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type='delivery.operation_readback' AND json_extract(payload_json,'$.run_id')=? ORDER BY id DESC LIMIT 1", [scope.projectId, scope.runId]);
|
|
35
|
+
if (!row || Buffer.byteLength(row.payload_json) > 131072)
|
|
36
|
+
refuse("retained_readback_required");
|
|
37
|
+
let receipt;
|
|
38
|
+
try {
|
|
39
|
+
receipt = JSON.parse(row.payload_json);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return refuse("retained_readback_invalid");
|
|
43
|
+
}
|
|
44
|
+
const grant = intent.plan.approval.scope, observed = receipt?.observation;
|
|
45
|
+
if (receipt?.schema_id !== "dd-flow/delivery-result@1" || receipt.run_id !== scope.runId || receipt.intent_sha256 !== intent.identity
|
|
46
|
+
|| receipt.operation_head_sha256 !== intent.head || !observed || observed.schema_id !== "dd-flow/delivery-result-observation@1"
|
|
47
|
+
|| observed.operation_id !== grant.operation_id || observed.binding_id !== grant.binding_id
|
|
48
|
+
|| hybridDigest(observed.artifact) !== hybridDigest(grant.artifact) || hybridDigest(observed.selector) !== hybridDigest(grant.selector)
|
|
49
|
+
|| !["complete", "absent", "partial", "unknown"].includes(observed.status)
|
|
50
|
+
|| !reference(observed.reference) || observed.status === "complete" && observed.reference === null
|
|
51
|
+
|| !validRollback(observed, grant.selector.operation)
|
|
52
|
+
|| !Number.isFinite(Date.parse(receipt.observed_at)) || !Array.isArray(receipt.processes) || !receipt.processes.length
|
|
53
|
+
|| receipt.processes.length > 16 || receipt.processes.some(id => typeof id !== "string" || !id.startsWith("PROC-")))
|
|
54
|
+
refuse("retained_readback_changed");
|
|
55
|
+
return { sha256: hybridDigest(receipt), receipt: structuredClone(receipt) };
|
|
56
|
+
}
|
|
57
|
+
/** Private result/reconciliation observation. The caller proves the actual
|
|
58
|
+
* Stage/coordinator authority and project readback safety. No new approval,
|
|
59
|
+
* login, retry, rollback or terminal Stage/RUN decision is inferred here.
|
|
60
|
+
* Reconciliation intentionally does not require unexpired mutation approval
|
|
61
|
+
* or local artifact availability: an earlier effect must remain observable. */
|
|
62
|
+
export async function observeDeliveryResult(context, input) {
|
|
63
|
+
if (context.db.isOwnedWriteTransaction?.())
|
|
64
|
+
refuse("observation_outside_writer_required");
|
|
65
|
+
const binding = resolveDeliveryBinding({ schema_id: "dd-flow/delivery-policy@1", bindings: [input.binding] }, input.binding.selector);
|
|
66
|
+
const original = readDeliveryIntent(context, input);
|
|
67
|
+
if (!original)
|
|
68
|
+
refuse("original_intent_required");
|
|
69
|
+
const grant = original.plan.approval.scope;
|
|
70
|
+
if (hybridDigest(binding) !== grant.binding_sha256)
|
|
71
|
+
refuse("original_binding_required");
|
|
72
|
+
const assertOwner = () => {
|
|
73
|
+
const checked = input.assertCurrent();
|
|
74
|
+
if (types.isPromise(checked)) {
|
|
75
|
+
void checked.catch(() => { });
|
|
76
|
+
refuse("synchronous_owner_fence_required");
|
|
77
|
+
}
|
|
78
|
+
const current = readDeliveryIntent(context, input);
|
|
79
|
+
if (!current || current.identity !== original.identity || current.head !== original.head)
|
|
80
|
+
refuse("original_operation_changed");
|
|
81
|
+
return current;
|
|
82
|
+
};
|
|
83
|
+
const assertCurrent = () => {
|
|
84
|
+
const current = assertOwner();
|
|
85
|
+
// The old command may lack a result event after interruption. Readback is
|
|
86
|
+
// allowed only after every originally admitted process is physically quiet.
|
|
87
|
+
const processes = managedProcessStatus(context);
|
|
88
|
+
for (const event of current.events) {
|
|
89
|
+
if (event.kind !== "admitted")
|
|
90
|
+
continue;
|
|
91
|
+
const process = processes.find(item => item.id === event.process.process_id);
|
|
92
|
+
if (event.process.resource_home !== resourceHome(context) || !process || process.state !== "stopped"
|
|
93
|
+
|| managedProcessBinding(process, resourceHome(context)).registration_sha256 !== event.process.registration_sha256
|
|
94
|
+
|| processTreeIsAlive(process))
|
|
95
|
+
refuse("original_process_not_settled");
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
assertCurrent();
|
|
99
|
+
const procedure = binding.procedures.result_readback, deadline = Date.now() + procedure.timeout_ms, processes = [];
|
|
100
|
+
let observed;
|
|
101
|
+
for (const step of procedure.procedure_steps) {
|
|
102
|
+
let value;
|
|
103
|
+
try {
|
|
104
|
+
const result = await executeDeliveryStep(context, { projectId: input.projectId, runId: input.runId,
|
|
105
|
+
operationId: `result-${randomUUID()}`, stepId: step.id, workspace: original.plan.artifact.workspace,
|
|
106
|
+
procedure, deadlineAt: deadline, assertCurrent, admit() { }, consumeOutput(bytes) {
|
|
107
|
+
value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
108
|
+
} });
|
|
109
|
+
processes.push(result.process_id);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return refuse("readback_failed");
|
|
113
|
+
}
|
|
114
|
+
const keys = binding.selector.operation === "deploy" ? "artifact,binding_id,operation_id,reference,rollback,schema_id,selector,status"
|
|
115
|
+
: "artifact,binding_id,operation_id,reference,schema_id,selector,status";
|
|
116
|
+
if (!record(value) || Object.keys(value).sort().join() !== keys
|
|
117
|
+
|| value.schema_id !== "dd-flow/delivery-result-observation@1" || value.operation_id !== grant.operation_id || value.binding_id !== binding.id
|
|
118
|
+
|| hybridDigest(value.artifact) !== hybridDigest(grant.artifact) || hybridDigest(value.selector) !== hybridDigest(grant.selector)
|
|
119
|
+
|| !["complete", "absent", "partial", "unknown"].includes(value.status)
|
|
120
|
+
|| !reference(value.reference) || !validRollback(value, binding.selector.operation)
|
|
121
|
+
|| value.status === "complete" && value.reference === null)
|
|
122
|
+
refuse("result_context_mismatch_or_unsafe_output");
|
|
123
|
+
const next = value;
|
|
124
|
+
// Ordered steps may confirm the same result; a conflicting earlier answer
|
|
125
|
+
// is not silently overwritten by a more optimistic later command.
|
|
126
|
+
if (observed && hybridDigest(observed) !== hybridDigest(next))
|
|
127
|
+
refuse("conflicting_readback");
|
|
128
|
+
observed = structuredClone(next);
|
|
129
|
+
}
|
|
130
|
+
assertCurrent();
|
|
131
|
+
if (!observed || Date.now() >= deadline)
|
|
132
|
+
refuse("readback_incomplete");
|
|
133
|
+
const receipt = { schema_id: "dd-flow/delivery-result@1", run_id: input.runId, intent_sha256: original.identity,
|
|
134
|
+
operation_head_sha256: original.head, observed_at: context.now(), observation: observed, processes };
|
|
135
|
+
if (!Number.isFinite(Date.parse(receipt.observed_at)))
|
|
136
|
+
refuse("observation_clock_invalid");
|
|
137
|
+
return context.db.writeTransaction(() => {
|
|
138
|
+
// Physical observation is already settled. Only current SQL/owner fences
|
|
139
|
+
// run in this short writer; receipt persistence does not close the intent.
|
|
140
|
+
assertOwner();
|
|
141
|
+
appendAudit(context, { projectId: input.projectId, eventType: "delivery.operation_readback", payload: receipt });
|
|
142
|
+
return structuredClone(receipt);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { deliveryFlowStages } from "../domain/delivery-flows.js";
|
|
3
|
+
import { AppError } from "../shared/errors.js";
|
|
4
|
+
import { runtimeCommand } from "./runtime-command.js";
|
|
5
|
+
import { managedLifecycleCommand } from "./lifecycle-invocations.js";
|
|
6
|
+
/** Packet candidates are not invocation admission or mutation approval. */
|
|
7
|
+
export function deliveryStageCommands(context, input) {
|
|
8
|
+
if (!deliveryFlowStages[input.flowId].includes(input.stage))
|
|
9
|
+
throw new AppError("delivery_stage_invalid", "Unknown delivery Stage", 2, { effect: "no_effect" });
|
|
10
|
+
const command = (operation, file) => runtimeCommand(context, ["delivery", operation, input.runId,
|
|
11
|
+
"--project-root", input.projectRoot, "--stage", input.stage, ...(file ? ["--result-file", file] : []), "--json"]);
|
|
12
|
+
return {
|
|
13
|
+
inputs: command("inputs"),
|
|
14
|
+
...(["prepare", "verify"].includes(input.stage) ? { checks: command("checks", path.join(input.stageRoot, "delivery-artifact.json")) } : {}),
|
|
15
|
+
...(["publish", "deploy"].includes(input.stage) ? { execute: command("execute", path.join(input.stageRoot, "delivery-approval.json")) } : {}),
|
|
16
|
+
...(["publish", "deploy", "verify", "finish"].includes(input.stage) ? { reconcile: command("reconcile") } : {})
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** Issue only after the native Stage coordinator is bound. Keep assignments
|
|
20
|
+
* out of immutable semantic packets and include literal commands for custody. */
|
|
21
|
+
export function issueDeliveryStageCommands(context, input) {
|
|
22
|
+
const commands = Object.fromEntries(Object.entries(deliveryStageCommands(context, input))
|
|
23
|
+
.map(([operation, candidate]) => [operation, operation === "inputs" ? candidate : managedLifecycleCommand(context, candidate)]));
|
|
24
|
+
const instructions = `\n## Bound delivery command candidates\n\n\`\`\`json\n${JSON.stringify(commands, null, 2)}\n\`\`\`\n\nWrite the exact artifact/approval JSON to the issued result-file before checks/execute. Starting this Stage grants no mutation approval. Reconcile the original intent; never retry an unknown effect.\n\n## Current issued delivery commands\n\n${Object.values(commands).join("\n\n")}\n`;
|
|
25
|
+
return { commands, instructions };
|
|
26
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { AppError } from "../shared/errors.js";
|
|
2
|
+
import { deliveryFlowStages } from "../domain/delivery-flows.js";
|
|
3
|
+
import { readFlowRunExecutionBundle, assertPreparedRunBundleSqlAuthority } from "./runs.js";
|
|
4
|
+
import { requireProjectByRoot } from "./projects.js";
|
|
5
|
+
import { resolveProjectRoot } from "../storage/paths.js";
|
|
6
|
+
import { readPreparedMemoryFlowUserInputs } from "./memory-flow-user-inputs.js";
|
|
7
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
8
|
+
import { PreparedDescriptorStageStart } from "./descriptor-stage-start.js";
|
|
9
|
+
import { PreparedDescriptorStageCoordinator } from "./descriptor-stage-coordinator.js";
|
|
10
|
+
import { readActiveDescriptorStageCoordinator } from "./descriptor-stage-settlement.js";
|
|
11
|
+
import { currentLifecycleAdmission, lifecycleInvocationScope, assertLifecycleInvocationCurrent } from "./lifecycle-invocations.js";
|
|
12
|
+
function refuse(reason) {
|
|
13
|
+
throw new AppError("delivery_stage_invalid", "Delivery requires its registered flow, original request and current Stage owner", 2, { reason, effect: "no_effect" });
|
|
14
|
+
}
|
|
15
|
+
/** Internal family entry, not a public launcher or permission to publish.
|
|
16
|
+
* The ordinary descriptor runtime owns attempt, coordinator Work and Session.
|
|
17
|
+
* Delivery uses its actual workspace, not a read-only diagnostic clone. */
|
|
18
|
+
export function readDeliveryStageScope(context, input) {
|
|
19
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot));
|
|
20
|
+
const bundle = readFlowRunExecutionBundle(context, input), flow = bundle?.manifest.flow_id;
|
|
21
|
+
if (!bundle || !flow || !Object.hasOwn(deliveryFlowStages, flow)
|
|
22
|
+
|| !deliveryFlowStages[flow].includes(input.stage))
|
|
23
|
+
refuse("registered_delivery_stage_required");
|
|
24
|
+
const flowId = flow, scope = { projectId: project.id, runId: input.runId, stage: input.stage };
|
|
25
|
+
bundle.descriptor.stage(input.stage);
|
|
26
|
+
const run = assertPreparedRunBundleSqlAuthority(context, scope, bundle);
|
|
27
|
+
const users = readPreparedMemoryFlowUserInputs(context, scope, flowId, bundle), userInputsSha256 = hybridDigest(users);
|
|
28
|
+
return { scope, bundle, run, flowId, users, userInputsSha256,
|
|
29
|
+
assertCustody(writer) {
|
|
30
|
+
assertPreparedRunBundleSqlAuthority(writer, scope, bundle);
|
|
31
|
+
if (hybridDigest(readPreparedMemoryFlowUserInputs(writer, scope, flowId, bundle)) !== userInputsSha256)
|
|
32
|
+
refuse("user_inputs_changed_during_entry");
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function startDeliveryStage(context, input) {
|
|
37
|
+
const selected = readDeliveryStageScope(context, input);
|
|
38
|
+
const start = PreparedDescriptorStageStart.prepare(context, input).apply(context, selected.assertCustody);
|
|
39
|
+
const coordinator = PreparedDescriptorStageCoordinator.prepare(context, input).apply(context, selected.assertCustody);
|
|
40
|
+
return { ...start, ...coordinator, flow_id: selected.flowId, user_inputs_sha256: selected.userInputsSha256 };
|
|
41
|
+
}
|
|
42
|
+
/** A matching SQL coordinator or ambient scope is not a command capability.
|
|
43
|
+
* The ordinary lifecycle gate must already have admitted this exact CLI process. */
|
|
44
|
+
export function assertDeliveryCommandOwner(context, input) {
|
|
45
|
+
const id = context.env.DD_FLOW_CURRENT_INVOCATION;
|
|
46
|
+
if (!id || !currentLifecycleAdmission(context, id))
|
|
47
|
+
refuse("issued_coordinator_command_required");
|
|
48
|
+
const native = lifecycleInvocationScope(context, id);
|
|
49
|
+
assertLifecycleInvocationCurrent(context, native, input.command);
|
|
50
|
+
const selected = readDeliveryStageScope(context, input), actor = readActiveDescriptorStageCoordinator(context, selected.scope, selected.bundle);
|
|
51
|
+
const hook = context.db.get("SELECT daemon_id FROM hook_events WHERE project_id=? AND event_key=?", [selected.scope.projectId, actor.start_hook]);
|
|
52
|
+
if (native.projectRoot !== input.projectRoot || native.runId !== input.runId || native.rootSessionId !== actor.provider_session_id
|
|
53
|
+
|| !hook?.daemon_id || hook.daemon_id !== native.daemonId)
|
|
54
|
+
refuse("current_physical_coordinator_required");
|
|
55
|
+
return selected;
|
|
56
|
+
}
|