@deksden-com/dd-flow-cli 0.9.0-beta.152 → 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 +114 -16
- package/dist/harness-runtime/lib/dd-opencode.mjs +10 -7
- package/dist/harness-runtime/lib/dd-zcode-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +115 -22
- package/dist/harness-runtime/lib/dd-zcode.mjs +75 -36
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +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 +45 -8
- package/dist/services/cli-operation-classifier.js +6 -1
- package/dist/services/code-checks.js +46 -29
- package/dist/services/codex-hook-delivery.js +11 -2
- package/dist/services/controller-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 -7
- 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 +8 -32
- package/dist/services/vnext-fanout.js +53 -8
- package/dist/services/vnext-merge.js +10 -5
- package/dist/services/vnext-plan-review.js +1 -1
- package/dist/services/vnext-plan.js +1 -1
- package/dist/services/vnext-protocolize.js +1 -1
- package/dist/services/vnext-specify.js +10 -5
- package/dist/services/work-registry.js +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 +3 -2
- package/tools/verification-jobs.mjs +35 -10
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { AppError } from "../shared/errors.js";
|
|
2
|
+
import { appendAudit } from "./audit.js";
|
|
3
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
4
|
+
import { prepareDeliveryApproval } from "./delivery-approval.js";
|
|
5
|
+
import { resolveDeliveryBinding } from "./delivery-policy.js";
|
|
6
|
+
import { PreparedDeliveryAccess } from "./delivery-access.js";
|
|
7
|
+
import { PreparedDeliveryArtifact } from "./delivery-artifact.js";
|
|
8
|
+
import { assertPreparedRunBundleSqlAuthority } from "./runs.js";
|
|
9
|
+
import { assertRunMutationAllowed, assertStageSettlementAllowed } from "./run-recovery-guard.js";
|
|
10
|
+
import { readDescriptorStageInput } from "./descriptor-stage-coordinator.js";
|
|
11
|
+
import { readActiveDescriptorStageCoordinator } from "./descriptor-stage-settlement.js";
|
|
12
|
+
import { readAcceptedStageData } from "./stage-report-renderer.js";
|
|
13
|
+
import { assertManagedProcessLease, managedProcessBinding, managedProcessStatus, processTreeIsAlive, resourceHome } from "./managed-processes.js";
|
|
14
|
+
const intentEvent = "delivery.operation_intent", progressEvent = "delivery.operation_event", anchor = '$.variables."delivery.operation"';
|
|
15
|
+
function refuse(reason) {
|
|
16
|
+
throw new AppError("delivery_intent_invalid", "Delivery needs its original approved operation and active Stage owner", 2, { reason, effect: "no_effect" });
|
|
17
|
+
}
|
|
18
|
+
function pending(reason) {
|
|
19
|
+
throw new AppError("delivery_operation_pending", "Observe or reconcile the original delivery before another dispatch", 2, { reason, effect: "unknown", next_action: "reconcile_original_delivery" });
|
|
20
|
+
}
|
|
21
|
+
/** Original SQL operation custody; never infer 'not dispatched' from a lost
|
|
22
|
+
* log, editable file, new request ID, expired observer or terminal process. */
|
|
23
|
+
export function readDeliveryIntent(context, scope) {
|
|
24
|
+
const run = context.db.get("SELECT json_extract(index_json,?) value FROM runs WHERE project_id=? AND id=?", [anchor, scope.projectId, scope.runId]);
|
|
25
|
+
if (!run)
|
|
26
|
+
refuse("bound_run_required");
|
|
27
|
+
const plans = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type=? AND json_extract(payload_json,'$.plan.approval.scope.run_id')=? LIMIT 2", [scope.projectId, intentEvent, scope.runId]);
|
|
28
|
+
const events = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type=? AND json_extract(payload_json,'$.run_id')=? ORDER BY id LIMIT 34", [scope.projectId, progressEvent, scope.runId]);
|
|
29
|
+
if (plans.length > 1 || events.length > 33 || plans.some(row => Buffer.byteLength(row.payload_json) > 262144)
|
|
30
|
+
|| events.some(row => Buffer.byteLength(row.payload_json) > 131072) || Boolean(plans.length) !== Boolean(run.value) || !plans.length && events.length)
|
|
31
|
+
pending("operation_custody_lost");
|
|
32
|
+
if (!plans.length)
|
|
33
|
+
return null;
|
|
34
|
+
let value, log, saved;
|
|
35
|
+
try {
|
|
36
|
+
value = JSON.parse(plans[0].payload_json);
|
|
37
|
+
log = events.map(row => JSON.parse(row.payload_json));
|
|
38
|
+
saved = JSON.parse(run.value);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return pending("operation_custody_invalid");
|
|
42
|
+
}
|
|
43
|
+
const plan = value.plan;
|
|
44
|
+
if (plan?.schema_id !== "dd-flow/delivery-intent@1" || plan.approval?.scope?.project_id !== scope.projectId
|
|
45
|
+
|| plan.approval.scope.run_id !== scope.runId || value.identity !== hybridDigest(plan) || saved.identity !== value.identity
|
|
46
|
+
|| !Array.isArray(plan.steps) || !plan.steps.length || plan.steps.length > 16
|
|
47
|
+
|| plan.steps.some(step => typeof step !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/.test(step)) || new Set(plan.steps).size !== plan.steps.length
|
|
48
|
+
|| saved.count !== log.length)
|
|
49
|
+
pending("operation_custody_changed");
|
|
50
|
+
let head = value.identity, next = 0, failed = false, open = null;
|
|
51
|
+
for (const entry of log) {
|
|
52
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry) || failed || entry.run_id !== scope.runId
|
|
53
|
+
|| entry.identity !== value.identity || entry.previous !== head || entry.step_id !== plan.steps[next])
|
|
54
|
+
pending("operation_event_changed");
|
|
55
|
+
if (entry.kind === "admitted" && !open && entry.process && typeof entry.process.resource_home === "string"
|
|
56
|
+
&& typeof entry.process.process_id === "string" && /^[a-f0-9]{64}$/.test(entry.process.registration_sha256)
|
|
57
|
+
&& entry.access?.schema_id === "dd-flow/delivery-access@1" && entry.access.operation_id === plan.approval.scope.operation_id
|
|
58
|
+
&& entry.access.binding_id === plan.approval.scope.binding_id)
|
|
59
|
+
open = entry;
|
|
60
|
+
else if (entry.kind === "command_result" && open && entry.result && entry.result.process_id === open.process.process_id
|
|
61
|
+
&& entry.result.operation_id === plan.approval.scope.operation_id && entry.result.step_id === entry.step_id
|
|
62
|
+
&& ["command_succeeded", "failed"].includes(entry.result.outcome) && ["no_effect", "unknown"].includes(entry.result.effect)) {
|
|
63
|
+
// A failed or uncertain command is never permission to run a later step.
|
|
64
|
+
if (entry.result.outcome === "command_succeeded") {
|
|
65
|
+
next++;
|
|
66
|
+
open = null;
|
|
67
|
+
}
|
|
68
|
+
else
|
|
69
|
+
failed = true;
|
|
70
|
+
}
|
|
71
|
+
else
|
|
72
|
+
pending("operation_event_order_invalid");
|
|
73
|
+
head = hybridDigest(entry);
|
|
74
|
+
}
|
|
75
|
+
if (head !== saved.head)
|
|
76
|
+
pending("operation_anchor_changed");
|
|
77
|
+
return { ...value, events: log, head, next, open, failed };
|
|
78
|
+
}
|
|
79
|
+
/** Original intent/admission owner, not an executor. The Stage independently
|
|
80
|
+
* verifies manifest references and project procedure safety. No public route yet. */
|
|
81
|
+
export function prepareDeliveryIntent(context, scope, bundle, raw, proposal) {
|
|
82
|
+
if (context.db.isOwnedWriteTransaction?.())
|
|
83
|
+
refuse("preparation_outside_writer_required");
|
|
84
|
+
const binding = resolveDeliveryBinding({ schema_id: "dd-flow/delivery-policy@1", bindings: [raw.binding] }, raw.binding.selector);
|
|
85
|
+
if (binding.procedures.execute.side_effect !== "mutation")
|
|
86
|
+
refuse("protected_mutation_required");
|
|
87
|
+
const selection = { ...raw, binding }, approval = prepareDeliveryApproval(context, scope, bundle, selection, proposal), grant = approval.value();
|
|
88
|
+
const artifact = PreparedDeliveryArtifact.prepare(bundle.execution.snapshot().scope.workspace, raw.artifactPath, selection.artifact);
|
|
89
|
+
const stage = grant.scope.flow_id === "deploy" ? "deploy" : "publish", previous = stage === "deploy" ? "prepare" : "verify";
|
|
90
|
+
const predecessor = readAcceptedStageData(context, { ...scope, stage: previous });
|
|
91
|
+
let data;
|
|
92
|
+
try {
|
|
93
|
+
data = JSON.parse(predecessor.bytes.toString("utf8"));
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return refuse("accepted_selection_required");
|
|
97
|
+
}
|
|
98
|
+
if (data.details?.delivery_scope_sha256 !== grant.scope_sha256)
|
|
99
|
+
refuse("accepted_selection_required");
|
|
100
|
+
const generation = assertRunMutationAllowed(context, scope.projectId, scope.runId);
|
|
101
|
+
const authority = (writer) => {
|
|
102
|
+
assertRunMutationAllowed(writer, scope.projectId, scope.runId, generation);
|
|
103
|
+
const run = assertPreparedRunBundleSqlAuthority(writer, scope, bundle), index = JSON.parse(run.index_json);
|
|
104
|
+
if (run.status !== "running" || index.current_stage !== stage
|
|
105
|
+
|| !index.stage_runs?.some((item) => item.stage === stage && item.status === "running"))
|
|
106
|
+
refuse("active_delivery_stage_required");
|
|
107
|
+
const packet = readDescriptorStageInput(writer, { ...scope, stage }, bundle), actor = readActiveDescriptorStageCoordinator(writer, { ...scope, stage }, bundle);
|
|
108
|
+
if (packet.input.predecessor?.stage !== previous || hybridDigest(packet.input.predecessor.result) !== hybridDigest(predecessor.result))
|
|
109
|
+
refuse("accepted_predecessor_changed");
|
|
110
|
+
if (writer.db.get("SELECT 1 FROM works WHERE project_id=? AND run_id=? AND parent_work_id=? AND status IN ('created','running','paused') LIMIT 1", [scope.projectId, scope.runId, actor.work_id]))
|
|
111
|
+
refuse("active_child_work");
|
|
112
|
+
return { packet, actor };
|
|
113
|
+
};
|
|
114
|
+
const owner = authority(context), original = readDeliveryIntent(context, scope);
|
|
115
|
+
const proposed = { schema_id: "dd-flow/delivery-intent@1", approval: grant, artifact: artifact.value(), predecessor: { stage: previous, result: predecessor.result },
|
|
116
|
+
steps: binding.procedures.execute.procedure_steps.map(step => step.id) };
|
|
117
|
+
// Retain the first human-readable interpretation rather than requiring a
|
|
118
|
+
// future observer to reproduce its wording. Scope, basis, inputs and expiry
|
|
119
|
+
// remain exact; neither commentary nor a fresh timestamp renews permission.
|
|
120
|
+
const semantic = (plan) => ({ ...plan, approval: { ...plan.approval, approved_at: null, interpretation: null } });
|
|
121
|
+
if (original && hybridDigest(semantic(original.plan)) !== hybridDigest(semantic(proposed)))
|
|
122
|
+
pending("original_operation_scope_changed");
|
|
123
|
+
const plan = original?.plan ?? proposed, identity = hybridDigest(plan), accessScope = {
|
|
124
|
+
projectId: scope.projectId, runId: scope.runId, operationId: selection.operationId, workspace: bundle.execution.snapshot().scope.workspace, binding
|
|
125
|
+
};
|
|
126
|
+
const assertCustody = (writer) => {
|
|
127
|
+
approval.assertCustody(writer);
|
|
128
|
+
if (hybridDigest(authority(writer)) !== hybridDigest(owner))
|
|
129
|
+
refuse("delivery_owner_changed");
|
|
130
|
+
const current = readDeliveryIntent(writer, scope);
|
|
131
|
+
if (current && current.identity !== identity)
|
|
132
|
+
pending("original_operation_changed");
|
|
133
|
+
return current;
|
|
134
|
+
};
|
|
135
|
+
const settlementCustody = (writer) => {
|
|
136
|
+
// Expiry/revocation forbids new effects, not recording what an already
|
|
137
|
+
// admitted command actually did. Ordinary Stage/control fences still own
|
|
138
|
+
// the allowed generation and cooperative settlement window.
|
|
139
|
+
assertStageSettlementAllowed(writer, scope.projectId, scope.runId, stage, generation);
|
|
140
|
+
assertPreparedRunBundleSqlAuthority(writer, scope, bundle);
|
|
141
|
+
const current = readDeliveryIntent(writer, scope);
|
|
142
|
+
if (!current || current.identity !== identity)
|
|
143
|
+
pending("original_operation_changed");
|
|
144
|
+
return current;
|
|
145
|
+
};
|
|
146
|
+
const assertArtifact = (writer) => {
|
|
147
|
+
if (writer.db.isOwnedWriteTransaction?.())
|
|
148
|
+
refuse("artifact_observation_outside_writer_required");
|
|
149
|
+
artifact.assertCurrent();
|
|
150
|
+
};
|
|
151
|
+
const retain = (writer) => {
|
|
152
|
+
assertArtifact(writer);
|
|
153
|
+
return writer.db.writeTransaction(() => {
|
|
154
|
+
const current = assertCustody(writer);
|
|
155
|
+
if (current)
|
|
156
|
+
return current;
|
|
157
|
+
appendAudit(writer, { projectId: scope.projectId, eventType: intentEvent, payload: { identity, plan } });
|
|
158
|
+
writer.db.run("UPDATE runs SET index_json=json_set(index_json,?,json(?)) WHERE project_id=? AND id=?", [anchor, JSON.stringify({ identity, count: 0, head: identity }), scope.projectId, scope.runId]);
|
|
159
|
+
return readDeliveryIntent(writer, scope);
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
return { value: () => structuredClone(plan), assertCustody, retain,
|
|
163
|
+
admitStep(writer, stepId, process, access) {
|
|
164
|
+
if (!(access instanceof PreparedDeliveryAccess))
|
|
165
|
+
refuse("owned_access_observation_required");
|
|
166
|
+
assertArtifact(writer);
|
|
167
|
+
return writer.db.writeTransaction(() => {
|
|
168
|
+
const current = assertCustody(writer);
|
|
169
|
+
if (!current)
|
|
170
|
+
refuse("retained_intent_required");
|
|
171
|
+
if (current.open || current.plan.steps[current.next] !== stepId)
|
|
172
|
+
pending("original_step_already_admitted_or_out_of_order");
|
|
173
|
+
if (process.kind !== "delivery" || process.project_id !== scope.projectId || process.run_id !== scope.runId
|
|
174
|
+
|| process.owner_id !== selection.operationId || process.operation_id !== `${selection.operationId}:${stepId}` || !process.pid)
|
|
175
|
+
refuse("selected_process_required");
|
|
176
|
+
assertManagedProcessLease(writer, { id: process.id, leaseToken: process.lease_token, pid: process.pid });
|
|
177
|
+
access.consume(writer, accessScope);
|
|
178
|
+
const entry = { run_id: scope.runId, identity, previous: current.head, kind: "admitted", step_id: stepId,
|
|
179
|
+
process: managedProcessBinding(process, resourceHome(writer)), access: access.value() };
|
|
180
|
+
appendAudit(writer, { projectId: scope.projectId, eventType: progressEvent, payload: entry });
|
|
181
|
+
writer.db.run("UPDATE runs SET index_json=json_set(index_json,?,json(?)) WHERE project_id=? AND id=?", [anchor,
|
|
182
|
+
JSON.stringify({ identity, count: current.events.length + 1, head: hybridDigest(entry) }), scope.projectId, scope.runId]);
|
|
183
|
+
});
|
|
184
|
+
},
|
|
185
|
+
recordCommand(writer, result) {
|
|
186
|
+
if (writer.db.isOwnedWriteTransaction?.())
|
|
187
|
+
refuse("physical_observation_outside_writer_required");
|
|
188
|
+
const before = settlementCustody(writer), admitted = before.open;
|
|
189
|
+
if (before.failed || !admitted || result.step_id !== admitted.step_id || result.process_id !== admitted.process.process_id
|
|
190
|
+
|| result.operation_id !== selection.operationId || !["command_succeeded", "failed"].includes(result.outcome)
|
|
191
|
+
|| !["no_effect", "unknown"].includes(result.effect))
|
|
192
|
+
pending("original_command_result_required");
|
|
193
|
+
if (admitted.process.resource_home !== resourceHome(writer))
|
|
194
|
+
pending("original_resource_home_required");
|
|
195
|
+
const process = managedProcessStatus(writer).find(item => item.id === result.process_id);
|
|
196
|
+
if (!process || managedProcessBinding(process, resourceHome(writer)).registration_sha256 !== admitted.process.registration_sha256
|
|
197
|
+
|| process.state !== "stopped" || processTreeIsAlive(process))
|
|
198
|
+
pending("original_process_not_settled");
|
|
199
|
+
return writer.db.writeTransaction(() => {
|
|
200
|
+
const current = settlementCustody(writer);
|
|
201
|
+
if (!current || current.head !== before.head)
|
|
202
|
+
pending("operation_changed_before_result");
|
|
203
|
+
const entry = { run_id: scope.runId, identity, previous: current.head, kind: "command_result", step_id: result.step_id,
|
|
204
|
+
result: { process_id: result.process_id, operation_id: result.operation_id, step_id: result.step_id, outcome: result.outcome, effect: result.effect } };
|
|
205
|
+
appendAudit(writer, { projectId: scope.projectId, eventType: progressEvent, payload: entry });
|
|
206
|
+
writer.db.run("UPDATE runs SET index_json=json_set(index_json,?,json(?)) WHERE project_id=? AND id=?", [anchor,
|
|
207
|
+
JSON.stringify({ identity, count: current.events.length + 1, head: hybridDigest(entry) }), scope.projectId, scope.runId]);
|
|
208
|
+
return readDeliveryIntent(writer, scope);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { AppError } from "../shared/errors.js";
|
|
2
|
+
import { readDeliveryStageScope } from "./delivery-stage-entry.js";
|
|
3
|
+
import { readAcceptedStageData } from "./stage-report-renderer.js";
|
|
4
|
+
import { readActiveDescriptorStageCoordinator } from "./descriptor-stage-settlement.js";
|
|
5
|
+
import { assertPreparedRunBundleSqlAuthority } from "./runs.js";
|
|
6
|
+
import { prepareDeliveryProjectBinding } from "./delivery-policy.js";
|
|
7
|
+
import { prepareDeliveryIntent, readDeliveryIntent } from "./delivery-intents.js";
|
|
8
|
+
import { PreparedDeliveryAccess } from "./delivery-access.js";
|
|
9
|
+
import { executeDeliveryStep } from "./delivery-execution.js";
|
|
10
|
+
import { observeDeliveryResult } from "./delivery-result.js";
|
|
11
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
12
|
+
import { prepareDeliveryCurrentAcceptedChecks } from "./delivery-checks.js";
|
|
13
|
+
function pending(reason) {
|
|
14
|
+
throw new AppError("delivery_operation_pending", "Reconcile the original delivery before another dispatch", 2, { reason, effect: "unknown", next_action: "reconcile_original_delivery" });
|
|
15
|
+
}
|
|
16
|
+
/** Internal protected operation composition. No Stage/RUN completion, login,
|
|
17
|
+
* provider SDK, ambient shell, fallback account or automatic mutation retry.
|
|
18
|
+
* Startup is not approval: the proposal cites an exact delivered user input. */
|
|
19
|
+
export async function executeDeliveryOperation(context, input) {
|
|
20
|
+
const selected = readDeliveryStageScope(context, input), { scope, bundle, flowId } = selected;
|
|
21
|
+
const mutation = flowId === "deploy" ? "deploy" : "publish";
|
|
22
|
+
if (input.stage !== mutation || context.db.isOwnedWriteTransaction?.())
|
|
23
|
+
throw new AppError("delivery_stage_invalid", "Protected execution requires its active delivery Stage", 2, { effect: "no_effect" });
|
|
24
|
+
const prior = readAcceptedStageData(context, { ...scope, stage: flowId === "deploy" ? "prepare" : "verify" });
|
|
25
|
+
const details = JSON.parse(prior.bytes.toString("utf8")).details;
|
|
26
|
+
const { decision, artifact } = details;
|
|
27
|
+
if (!decision || !artifact)
|
|
28
|
+
throw new AppError("delivery_stage_invalid", "Accepted delivery selection is required", 2, { effect: "no_effect" });
|
|
29
|
+
const workspace = bundle.execution.snapshot().scope.workspace;
|
|
30
|
+
const policy = prepareDeliveryProjectBinding(workspace, decision.policy, decision.binding.selector);
|
|
31
|
+
if (hybridDigest(policy.binding) !== hybridDigest(decision.binding))
|
|
32
|
+
throw new AppError("delivery_stage_invalid", "Accepted delivery policy changed", 2, { effect: "no_effect" });
|
|
33
|
+
const operation = { operationId: decision.operation_id, binding: policy.binding, artifactPath: artifact.path,
|
|
34
|
+
artifact: { subject_ref: artifact.subject_ref, version: artifact.version, sha256: artifact.sha256 } };
|
|
35
|
+
const intent = prepareDeliveryIntent(context, scope, bundle, operation, input.approval);
|
|
36
|
+
const checks = prepareDeliveryCurrentAcceptedChecks(context, { ...input, sourceStage: flowId === "deploy" ? "prepare" : "verify",
|
|
37
|
+
operationScopeSha256: hybridDigest(intent.value().approval.scope) });
|
|
38
|
+
checks.revalidate();
|
|
39
|
+
intent.retain(context);
|
|
40
|
+
const owner = readActiveDescriptorStageCoordinator(context, scope, bundle);
|
|
41
|
+
const assertOwner = () => {
|
|
42
|
+
assertPreparedRunBundleSqlAuthority(context, scope, bundle);
|
|
43
|
+
if (hybridDigest(readActiveDescriptorStageCoordinator(context, scope, bundle)) !== hybridDigest(owner))
|
|
44
|
+
pending("delivery_owner_changed");
|
|
45
|
+
policy.assertCurrent();
|
|
46
|
+
};
|
|
47
|
+
const assertCurrent = () => { assertOwner(); selected.assertCustody(context); intent.assertCustody(context); checks.assertCustody(context); };
|
|
48
|
+
const retained = readDeliveryIntent(context, scope);
|
|
49
|
+
if (retained.open || retained.failed)
|
|
50
|
+
pending("original_step_requires_reconciliation");
|
|
51
|
+
const procedure = policy.binding.procedures.execute, deadline = Date.now() + procedure.timeout_ms;
|
|
52
|
+
for (const step of procedure.procedure_steps.slice(retained.next)) {
|
|
53
|
+
assertCurrent();
|
|
54
|
+
checks.revalidate();
|
|
55
|
+
const access = await PreparedDeliveryAccess.observe(context, { ...scope, operationId: operation.operationId, workspace,
|
|
56
|
+
binding: policy.binding, assertCurrent });
|
|
57
|
+
checks.revalidate();
|
|
58
|
+
let result;
|
|
59
|
+
try {
|
|
60
|
+
result = await executeDeliveryStep(context, { ...scope, operationId: operation.operationId, workspace, stepId: step.id,
|
|
61
|
+
procedure, deadlineAt: deadline, assertCurrent,
|
|
62
|
+
admit(process) { checks.revalidate(); selected.assertCustody(context); checks.assertCustody(context); intent.admitStep(context, step.id, process, access); }, consumeOutput() { } });
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
// Record only the executor's original admitted and physically settled
|
|
66
|
+
// process. An admission refusal must not invent a failed effect event.
|
|
67
|
+
const current = readDeliveryIntent(context, scope);
|
|
68
|
+
if (current?.open && error instanceof AppError && current.open.process.process_id === error.details.process_id) {
|
|
69
|
+
intent.recordCommand(context, { process_id: current.open.process.process_id, operation_id: operation.operationId,
|
|
70
|
+
step_id: step.id, outcome: "failed", effect: error.details.effect === "no_effect" ? "no_effect" : "unknown" });
|
|
71
|
+
}
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
intent.recordCommand(context, result);
|
|
75
|
+
}
|
|
76
|
+
// Expiry or later revocation forbids new mutation, not honest observation of
|
|
77
|
+
// an already admitted effect. Preserve Stage ownership and original policy.
|
|
78
|
+
const readback = await observeDeliveryResult(context, { ...scope, binding: policy.binding, assertCurrent: assertOwner });
|
|
79
|
+
return { operation_id: operation.operationId, intent_sha256: hybridDigest(intent.value()),
|
|
80
|
+
readback_sha256: hybridDigest(readback), status: readback.observation.status, rollback: readback.observation.rollback ?? null };
|
|
81
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { AppError } from "../shared/errors.js";
|
|
2
|
+
import { prepareDeliveryProcedure } from "./delivery-procedures.js";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { PreparedDeliveryArtifact } from "./delivery-artifact.js";
|
|
6
|
+
import { readBoundedReleaseFile } from "./release-source.js";
|
|
7
|
+
const selectorKeys = ["operation", "profile_id", "subject_ref", "provider", "host", "target", "stage"];
|
|
8
|
+
const record = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
9
|
+
const text = (value) => typeof value === "string" && value.trim().length > 0 && value.length <= 4096 && !value.includes("\0");
|
|
10
|
+
const keys = (value, allowed) => Object.keys(value).length === allowed.length && allowed.every(key => Object.hasOwn(value, key));
|
|
11
|
+
function refuse(reason) {
|
|
12
|
+
throw new AppError("delivery_policy_invalid", "Delivery requires one exact project-owned operation binding", 2, { phase: "prepare", effect: "no_effect", recoverable: true, reason });
|
|
13
|
+
}
|
|
14
|
+
function selector(value) {
|
|
15
|
+
if (!record(value) || !keys(value, selectorKeys) || !selectorKeys.every(key => text(value[key]))
|
|
16
|
+
|| !["release", "deploy", "publish"].includes(value.operation))
|
|
17
|
+
refuse("invalid_selector");
|
|
18
|
+
return Object.fromEntries(selectorKeys.map(key => [key, value[key]]));
|
|
19
|
+
}
|
|
20
|
+
function publicId(value) {
|
|
21
|
+
if (!record(value) || !keys(value, ["kind", "id"]) || !text(value.kind) || !text(value.id))
|
|
22
|
+
refuse("invalid_public_identifier");
|
|
23
|
+
return { kind: value.kind, id: value.id };
|
|
24
|
+
}
|
|
25
|
+
function access(value) {
|
|
26
|
+
if (!record(value))
|
|
27
|
+
refuse("invalid_access_policy");
|
|
28
|
+
if (value.applicability === "not_required") {
|
|
29
|
+
if (!keys(value, ["applicability", "reason"]) || !text(value.reason))
|
|
30
|
+
refuse("not_required_needs_policy_reason");
|
|
31
|
+
return { applicability: "not_required", reason: value.reason };
|
|
32
|
+
}
|
|
33
|
+
if (value.applicability !== "required" || !keys(value, ["applicability", "identity", "authority", "target"]))
|
|
34
|
+
refuse("invalid_access_policy");
|
|
35
|
+
return { applicability: "required", identity: publicId(value.identity),
|
|
36
|
+
authority: value.authority === "not_required" ? "not_required" : publicId(value.authority), target: publicId(value.target) };
|
|
37
|
+
}
|
|
38
|
+
function binding(value) {
|
|
39
|
+
if (!record(value) || !keys(value, ["id", "selector", "access", "procedures"])
|
|
40
|
+
|| typeof value.id !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/.test(value.id)
|
|
41
|
+
|| !record(value.procedures) || !keys(value.procedures, ["access_readback", "execute", "result_readback"]))
|
|
42
|
+
refuse("invalid_binding");
|
|
43
|
+
const selectedAccess = access(value.access), procedures = value.procedures;
|
|
44
|
+
if (selectedAccess.applicability === "not_required" && procedures.access_readback !== null
|
|
45
|
+
|| selectedAccess.applicability === "required" && procedures.access_readback === null)
|
|
46
|
+
refuse("access_readback_mismatch");
|
|
47
|
+
return { id: value.id, selector: selector(value.selector), access: selectedAccess, procedures: {
|
|
48
|
+
access_readback: procedures.access_readback === null ? null : prepareDeliveryProcedure(procedures.access_readback, "readback"),
|
|
49
|
+
execute: prepareDeliveryProcedure(procedures.execute, "execute"),
|
|
50
|
+
result_readback: prepareDeliveryProcedure(procedures.result_readback, "readback")
|
|
51
|
+
} };
|
|
52
|
+
}
|
|
53
|
+
/** Exact data resolution, not operation permission. The owning flow must bind
|
|
54
|
+
* these project-policy bytes to its frozen decision, independently run safe
|
|
55
|
+
* access readback and check separate current approval for the exact artifact,
|
|
56
|
+
* version, RUN/request and target. Never infer account access from this match. */
|
|
57
|
+
export function resolveDeliveryBinding(policy, selected) {
|
|
58
|
+
const requested = selector(selected);
|
|
59
|
+
if (!record(policy) || !keys(policy, ["schema_id", "bindings"]) || policy.schema_id !== "dd-flow/delivery-policy@1"
|
|
60
|
+
|| !Array.isArray(policy.bindings) || policy.bindings.length > 256)
|
|
61
|
+
refuse("invalid_policy");
|
|
62
|
+
const bindings = policy.bindings.map(binding);
|
|
63
|
+
if (new Set(bindings.map(item => item.id)).size !== bindings.length)
|
|
64
|
+
refuse("duplicate_binding_id");
|
|
65
|
+
const matches = bindings.filter(item => selectorKeys.every(key => item.selector[key] === requested[key]));
|
|
66
|
+
if (!matches.length)
|
|
67
|
+
refuse("no_exact_binding");
|
|
68
|
+
if (matches.length !== 1)
|
|
69
|
+
refuse("ambiguous_binding");
|
|
70
|
+
return matches[0];
|
|
71
|
+
}
|
|
72
|
+
/** Observe a selected project file, not a binding invented in an agent result.
|
|
73
|
+
* Revalidation stays outside SQL writers; no provider command runs here. */
|
|
74
|
+
export function prepareDeliveryProjectBinding(workspace, reference, selected) {
|
|
75
|
+
if (!reference || typeof reference.path !== "string" || path.isAbsolute(reference.path))
|
|
76
|
+
refuse("project_policy_relative_path_required");
|
|
77
|
+
const file = PreparedDeliveryArtifact.prepare(workspace, reference.path, { subject_ref: "delivery-policy", version: "1", sha256: reference.sha256 });
|
|
78
|
+
const bytes = readBoundedReleaseFile(path.join(workspace, reference.path), 1024 * 1024);
|
|
79
|
+
if (createHash("sha256").update(bytes).digest("hex") !== reference.sha256)
|
|
80
|
+
refuse("project_policy_changed");
|
|
81
|
+
let policy;
|
|
82
|
+
try {
|
|
83
|
+
policy = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return refuse("invalid_project_policy");
|
|
87
|
+
}
|
|
88
|
+
const binding = resolveDeliveryBinding(policy, selected);
|
|
89
|
+
file.assertCurrent();
|
|
90
|
+
return { binding, assertCurrent: () => file.assertCurrent() };
|
|
91
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { AppError } from "../shared/errors.js";
|
|
2
|
+
const record = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
3
|
+
const text = (value) => typeof value === "string" && value.length > 0 && value.length <= 16384 && !value.includes("\0");
|
|
4
|
+
const identifier = (value) => typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/.test(value);
|
|
5
|
+
function refuse(reason) {
|
|
6
|
+
throw new AppError("delivery_procedure_invalid", "Delivery requires an exact project-owned noninteractive procedure", 2, { phase: "prepare", effect: "no_effect", recoverable: true, reason });
|
|
7
|
+
}
|
|
8
|
+
function step(value, effect) {
|
|
9
|
+
if (!record(value) || Object.keys(value).some(key => !["id", "argv", "side_effect", "noninteractive"].includes(key))
|
|
10
|
+
|| !identifier(value.id) || value.side_effect !== effect || value.noninteractive !== true
|
|
11
|
+
|| !Array.isArray(value.argv) || !value.argv.length || value.argv.length > 128 || !value.argv.every(text))
|
|
12
|
+
refuse("invalid_step");
|
|
13
|
+
return { id: value.id, argv: [...value.argv], side_effect: effect, noninteractive: true };
|
|
14
|
+
}
|
|
15
|
+
/** Prepare policy data only: no process, environment lookup, permission or
|
|
16
|
+
* provider-safety proof. Callers must bind reviewed project policy and separate
|
|
17
|
+
* operation approval before execution. Pass each argv directly with shell:false;
|
|
18
|
+
* never concatenate steps into shell text or persist authenticated raw output. */
|
|
19
|
+
export function prepareDeliveryProcedure(value, purpose) {
|
|
20
|
+
if (!record(value) || Object.keys(value).some(key => !["id", "argv", "procedure_steps", "side_effect", "noninteractive", "timeout_ms", "permitted_environment_variables"].includes(key))
|
|
21
|
+
|| !identifier(value.id) || value.side_effect !== "read_only" && value.side_effect !== "mutation" || value.noninteractive !== true
|
|
22
|
+
|| !Number.isSafeInteger(value.timeout_ms) || value.timeout_ms < 1 || value.timeout_ms > 1800000
|
|
23
|
+
|| !Array.isArray(value.permitted_environment_variables) || value.permitted_environment_variables.length > 128
|
|
24
|
+
|| !value.permitted_environment_variables.every(name => typeof name === "string" && /^[A-Z_][A-Z0-9_]*$/.test(name))
|
|
25
|
+
|| new Set(value.permitted_environment_variables).size !== value.permitted_environment_variables.length
|
|
26
|
+
|| Object.hasOwn(value, "argv") === Object.hasOwn(value, "procedure_steps"))
|
|
27
|
+
refuse("invalid_procedure");
|
|
28
|
+
if (purpose !== "readback" && purpose !== "execute")
|
|
29
|
+
refuse("invalid_purpose");
|
|
30
|
+
const effect = value.side_effect;
|
|
31
|
+
if (purpose === "readback" && effect !== "read_only")
|
|
32
|
+
refuse("readback_must_be_read_only");
|
|
33
|
+
const steps = Object.hasOwn(value, "argv")
|
|
34
|
+
? [step({ id: value.id, argv: value.argv, side_effect: effect, noninteractive: true }, effect)]
|
|
35
|
+
: value.procedure_steps;
|
|
36
|
+
if (!Array.isArray(steps) || !steps.length || steps.length > 16)
|
|
37
|
+
refuse("invalid_steps");
|
|
38
|
+
const prepared = steps.map(item => step(item, effect));
|
|
39
|
+
if (new Set(prepared.map(item => item.id)).size !== prepared.length)
|
|
40
|
+
refuse("duplicate_step");
|
|
41
|
+
return { id: value.id, side_effect: effect, noninteractive: true, timeout_ms: value.timeout_ms,
|
|
42
|
+
permitted_environment_variables: [...value.permitted_environment_variables], procedure_steps: prepared };
|
|
43
|
+
}
|
|
@@ -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
|
+
}
|