@deksden-com/dd-flow-cli 0.9.0-beta.137 → 0.9.0-beta.138
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 +4 -0
- package/README.md +9 -0
- package/dist/build-info.json +6 -6
- package/dist/cli/argument-values.js +5 -5
- package/dist/cli/command-inputs.js +89 -20
- package/dist/cli/command-metadata.js +197 -0
- package/dist/cli/command-option-descriptions.js +191 -0
- package/dist/cli/completion-cli.js +70 -0
- package/dist/cli/completion.js +57 -0
- package/dist/cli/error-output.js +52 -0
- package/dist/cli/executor-onboarding.js +76 -0
- package/dist/cli/flows.js +29 -0
- package/dist/cli/harnesses.js +84 -0
- package/dist/cli/help.js +240 -6
- package/dist/cli/input-preparation.js +54 -5
- package/dist/cli/memory-flow-admission.js +44 -0
- package/dist/cli/memory-init-invocation.js +78 -0
- package/dist/cli/memory-init.js +240 -0
- package/dist/cli/memory-storage-admission.js +53 -0
- package/dist/cli/models.js +39 -0
- package/dist/cli/native-hook-ingress.js +1 -1
- package/dist/cli/operator-profiles.js +61 -0
- package/dist/cli/output-options.js +35 -0
- package/dist/cli/response-file.js +71 -0
- package/dist/cli/run-cli.js +283 -160
- package/dist/cli/setup.js +149 -0
- package/dist/cli.js +54 -2
- package/dist/domain/stage-handler-identities.js +20 -0
- package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +4 -3
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +4 -2
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +11 -5
- package/dist/harness-runtime/lib/dd-codex.mjs +22 -10
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +2 -1
- package/dist/harness-runtime/lib/dd-droid.mjs +2 -1
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +4 -2
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +6 -4
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +9 -9
- package/dist/harness-runtime/lib/delegation-instructions.mjs +5 -3
- package/dist/harness-runtime/lib/lifecycle-command.mjs +1 -0
- package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +50 -15
- package/dist/harness-runtime/lib/managed-flow-env.mjs +15 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +1 -1
- package/dist/harness-runtime/lib/native-request-proof.d.mts +2 -0
- package/dist/harness-runtime/lib/native-request-proof.mjs +65 -0
- package/dist/harness-runtime/lib/opencode-plugin.mjs +2 -0
- package/dist/harness-runtime/lib/operation-progress.mjs +3 -1
- package/dist/runtime/context.js +1 -1
- package/dist/schemas/discovery-result.schema.json +184 -0
- package/dist/schemas/execution-bundle.schema.json +205 -0
- package/dist/schemas/execution-settings.schema.json +162 -0
- package/dist/schemas/flow-descriptor.schema.json +54 -0
- package/dist/schemas/flow-run.schema.json +22 -0
- package/dist/schemas/harness-catalog.schema.json +80 -0
- package/dist/schemas/mb-init-compose-result.schema.json +457 -0
- package/dist/schemas/mb-init-discover-result.schema.json +500 -0
- package/dist/schemas/mb-init-finish-result.schema.json +422 -0
- package/dist/schemas/mb-init-preflight-result.schema.json +367 -0
- package/dist/schemas/mb-init-review-result.schema.json +396 -0
- package/dist/schemas/mb-init-verify-result.schema.json +391 -0
- package/dist/schemas/memory-bootstrap-receipt-v2.schema.json +67 -0
- package/dist/schemas/memory-bootstrap-receipt.schema.json +354 -0
- package/dist/schemas/memory-composition-work-result.schema.json +38 -0
- package/dist/schemas/memory-repair-work-result.schema.json +49 -0
- package/dist/schemas/memory-review-correction-plan.schema.json +30 -0
- package/dist/schemas/memory-review-work-result.schema.json +43 -0
- package/dist/schemas/model-catalog.schema.json +153 -0
- package/dist/schemas/profile-settings.schema.json +122 -0
- package/dist/schemas/release-manifest.schema.json +291 -0
- package/dist/schemas/run-engine-binding.schema.json +1 -1
- package/dist/services/check-runner-evidence.js +51 -4
- package/dist/services/cli-operation-classifier.js +8 -47
- package/dist/services/cli-upgrade-allowlist.js +27 -0
- package/dist/services/code-checks.js +387 -34
- package/dist/services/controller-execution.js +88 -0
- package/dist/services/controller-fanout.js +71 -12
- package/dist/services/dashboard.js +8 -2
- package/dist/services/descriptor-coordinator-session.js +35 -0
- package/dist/services/descriptor-root-work.js +53 -0
- package/dist/services/descriptor-run-settlement.js +105 -0
- package/dist/services/descriptor-run-start.js +449 -0
- package/dist/services/descriptor-stage-coordinator.js +197 -0
- package/dist/services/descriptor-stage-settlement.js +222 -0
- package/dist/services/descriptor-stage-start.js +139 -0
- package/dist/services/discovery/agy-discovery.js +2 -0
- package/dist/services/discovery/codex-discovery.js +2 -0
- package/dist/services/discovery/droid-discovery.js +2 -0
- package/dist/services/discovery/grok-discovery.js +2 -0
- package/dist/services/discovery/opencode-discovery.js +2 -0
- package/dist/services/discovery/static.js +31 -0
- package/dist/services/discovery/zcode-discovery.js +2 -0
- package/dist/services/engines.js +39 -22
- package/dist/services/eval-snapshots.js +83 -25
- package/dist/services/execution-bundles.js +263 -0
- package/dist/services/execution-policy.js +20 -16
- package/dist/services/executor-onboarding.js +56 -0
- package/dist/services/external-work-launch.js +91 -11
- package/dist/services/flow-catalog.js +21 -0
- package/dist/services/flow-descriptors.js +82 -0
- package/dist/services/frozen-engine.js +242 -0
- package/dist/services/generic-execution.js +164 -0
- package/dist/services/harness-adapter.js +28 -2
- package/dist/services/harness-discovery.js +29 -0
- package/dist/services/harness-doctor.js +75 -0
- package/dist/services/harness-probe-effects.js +51 -0
- package/dist/services/harness-version-probe.js +100 -0
- package/dist/services/hooks.js +24 -6
- package/dist/services/hybrid-contracts.js +483 -0
- package/dist/services/ids.js +29 -1
- package/dist/services/installation-lock.js +95 -0
- package/dist/services/installed-flow-source.js +183 -0
- package/dist/services/lifecycle-invocations.js +257 -78
- package/dist/services/memory-accepted-composition.js +77 -0
- package/dist/services/memory-baseline-preservation.js +65 -0
- package/dist/services/memory-completed-correction-wave.js +320 -0
- package/dist/services/memory-compose-handler.js +258 -0
- package/dist/services/memory-composition-acceptance.js +199 -0
- package/dist/services/memory-composition-graph.js +134 -0
- package/dist/services/memory-composition-registration.js +146 -0
- package/dist/services/memory-composition-work-custody.js +151 -0
- package/dist/services/memory-composition-work-results.js +96 -0
- package/dist/services/memory-corrected-review-acceptance.js +129 -0
- package/dist/services/memory-corrected-review-batch.js +30 -0
- package/dist/services/memory-corrected-review-findings.js +106 -0
- package/dist/services/memory-corrected-review-inputs.js +210 -0
- package/dist/services/memory-corrected-review-projection.js +78 -0
- package/dist/services/memory-corrected-review-receipt.js +170 -0
- package/dist/services/memory-corrected-review-registration.js +191 -0
- package/dist/services/memory-correction-check-receipt.js +219 -0
- package/dist/services/memory-correction-input-owner.js +6 -0
- package/dist/services/memory-discover-handler.js +104 -0
- package/dist/services/memory-finish-evidence.js +435 -0
- package/dist/services/memory-finish-handler.js +215 -0
- package/dist/services/memory-finish-integration.js +280 -0
- package/dist/services/memory-finish-readiness.js +122 -0
- package/dist/services/memory-git-integration.js +151 -0
- package/dist/services/memory-historical-corrected-review.js +206 -0
- package/dist/services/memory-historical-original-review.js +130 -0
- package/dist/services/memory-historical-repair-registration.js +99 -0
- package/dist/services/memory-historical-second-review.js +128 -0
- package/dist/services/memory-init-bootstrap-readiness.js +210 -0
- package/dist/services/memory-init-contexts.js +89 -0
- package/dist/services/memory-init-controller-start.js +84 -0
- package/dist/services/memory-init-engine-release.js +223 -0
- package/dist/services/memory-init-git-policy.js +136 -0
- package/dist/services/memory-init-lint-capability.js +185 -0
- package/dist/services/memory-init-native-capacity.js +241 -0
- package/dist/services/memory-init-preflight.js +312 -0
- package/dist/services/memory-init-project-bootstrap.js +599 -0
- package/dist/services/memory-init-request-candidate.js +157 -0
- package/dist/services/memory-init-request-input.js +66 -0
- package/dist/services/memory-init-resource-storage.js +125 -0
- package/dist/services/memory-init-run-allocation.js +81 -0
- package/dist/services/memory-init-run-bundle.js +98 -0
- package/dist/services/memory-init-start.js +175 -0
- package/dist/services/memory-init-startup-custody.js +80 -0
- package/dist/services/memory-init-startup-input.js +240 -0
- package/dist/services/memory-init-storage.js +81 -0
- package/dist/services/memory-init-stores.js +359 -0
- package/dist/services/memory-init-worktrunk-capability.js +172 -0
- package/dist/services/memory-layer-publication.js +611 -0
- package/dist/services/memory-layer.js +156 -0
- package/dist/services/memory-lint-config.js +85 -0
- package/dist/services/memory-native-review-copy.js +165 -0
- package/dist/services/memory-normal-flow-entry.js +75 -0
- package/dist/services/memory-ordinary-defaults.js +112 -0
- package/dist/services/memory-preflight-bootstrap-custody.js +115 -0
- package/dist/services/memory-preflight-handler.js +259 -0
- package/dist/services/memory-repair-current-view.js +299 -0
- package/dist/services/memory-repair-document-delta.js +451 -0
- package/dist/services/memory-repair-history.js +491 -0
- package/dist/services/memory-repair-physical-binding.js +312 -0
- package/dist/services/memory-repair-publication-files.js +152 -0
- package/dist/services/memory-repair-publication-intent.js +27 -0
- package/dist/services/memory-repair-publication-plan.js +160 -0
- package/dist/services/memory-repair-publication-source.js +225 -0
- package/dist/services/memory-repair-publication.js +794 -0
- package/dist/services/memory-repair-registration.js +239 -0
- package/dist/services/memory-repair-work-baseline.js +413 -0
- package/dist/services/memory-repair-work-completion.js +97 -0
- package/dist/services/memory-repair-work-custody.js +73 -0
- package/dist/services/memory-repair-work-results.js +89 -0
- package/dist/services/memory-repair-workspace.js +252 -0
- package/dist/services/memory-repair-writer-copy.js +735 -0
- package/dist/services/memory-result-binding.js +173 -0
- package/dist/services/memory-retained-review-authority.js +129 -0
- package/dist/services/memory-retained-review-results.js +70 -0
- package/dist/services/memory-retained-second-baseline.js +102 -0
- package/dist/services/memory-retained-second-repairs.js +156 -0
- package/dist/services/memory-review-acceptance.js +214 -0
- package/dist/services/memory-review-correction-plan.js +171 -0
- package/dist/services/memory-review-handler.js +585 -0
- package/dist/services/memory-review-inputs.js +200 -0
- package/dist/services/memory-review-receipt.js +180 -0
- package/dist/services/memory-review-registration.js +108 -0
- package/dist/services/memory-review-work-custody.js +124 -0
- package/dist/services/memory-review-work-input.js +72 -0
- package/dist/services/memory-review-work-results.js +73 -0
- package/dist/services/memory-second-corrected-review-inputs.js +165 -0
- package/dist/services/memory-second-correction-plan.js +76 -0
- package/dist/services/memory-second-repair-baseline.js +91 -0
- package/dist/services/memory-second-repair-launch.js +614 -0
- package/dist/services/memory-second-repair-registration.js +88 -0
- package/dist/services/memory-second-repair-source.js +187 -0
- package/dist/services/memory-second-review-acceptance.js +115 -0
- package/dist/services/memory-source-observations.js +117 -0
- package/dist/services/memory-stage-dispatch.js +28 -0
- package/dist/services/memory-stage-results.js +199 -0
- package/dist/services/memory-verification-acceptance.js +135 -0
- package/dist/services/memory-verification-checks.js +252 -0
- package/dist/services/memory-verification-receipt.js +159 -0
- package/dist/services/memory-verify-handler.js +135 -0
- package/dist/services/memory-work-result-preparation.js +63 -0
- package/dist/services/model-catalog.js +109 -0
- package/dist/services/model-discovery.js +46 -0
- package/dist/services/operator-profiles.js +236 -0
- package/dist/services/projects.js +17 -2
- package/dist/services/recovery-snapshot-database.js +416 -1
- package/dist/services/release-archive-producer.js +52 -0
- package/dist/services/release-archive.js +143 -0
- package/dist/services/release-cache.js +142 -0
- package/dist/services/release-compatibility.js +18 -0
- package/dist/services/release-download.js +67 -0
- package/dist/services/release-envelope.js +104 -0
- package/dist/services/release-manifest.js +45 -0
- package/dist/services/release-payload.js +114 -0
- package/dist/services/release-producer.js +63 -0
- package/dist/services/release-provenance.js +160 -0
- package/dist/services/release-registry.js +66 -0
- package/dist/services/release-source.js +194 -0
- package/dist/services/repair-intents.js +1 -1
- package/dist/services/review-copy.js +10 -2
- package/dist/services/run-control.js +80 -9
- package/dist/services/run-controller-capture.js +10 -3
- package/dist/services/run-controller-process.js +21 -0
- package/dist/services/run-controller-recovery.js +3 -1
- package/dist/services/run-controller-state.js +1 -1
- package/dist/services/run-controller.js +101 -40
- package/dist/services/run-recovery-guard.js +2 -0
- package/dist/services/run-recovery.js +145 -5
- package/dist/services/run-start-custody.js +177 -0
- package/dist/services/runs.js +319 -25
- package/dist/services/setup.js +377 -0
- package/dist/services/stage-context.js +9 -1
- package/dist/services/stage-handlers.js +74 -0
- package/dist/services/stage-report-renderer.js +63 -0
- package/dist/services/stage-work-graph.js +17 -0
- package/dist/services/vnext-code.js +3 -3
- package/dist/services/vnext-execution-profile.js +2 -2
- package/dist/services/vnext-fanout.js +70 -9
- package/dist/services/vnext-merge.js +8 -3
- package/dist/services/vnext-plan-review.js +6 -4
- package/dist/services/work-registry.js +616 -71
- package/dist/services/workspace-files.js +1 -1
- package/dist/storage/database.js +84 -6
- package/dist/storage/writer-contract.js +11 -7
- package/package.json +13 -3
- package/tools/create-canon-release.mjs +33 -0
- package/tools/produce-hybrid-release.mjs +41 -0
|
@@ -4,11 +4,16 @@ export { recoveryGuard, assertRunMutationAllowed, assertRunRecoveryAllowed } fro
|
|
|
4
4
|
import crypto from "node:crypto";
|
|
5
5
|
import fs from "node:fs";
|
|
6
6
|
import path from "node:path";
|
|
7
|
+
import { DatabaseSync } from "node:sqlite";
|
|
8
|
+
import { parseLifecycleCommand } from "../harness-runtime/lib/lifecycle-command.mjs";
|
|
7
9
|
import { AppError } from "../shared/errors.js";
|
|
8
10
|
import { entityReferenceVariants } from "../shared/entity-references.js";
|
|
9
11
|
import { resolveProjectRoot } from "../storage/paths.js";
|
|
10
12
|
import { requireProjectByRoot } from "./projects.js";
|
|
11
|
-
import { appendFlowRunTimelineEvent, setFlowRunRecoveryPaused } from "./runs.js";
|
|
13
|
+
import { appendFlowRunTimelineEvent, setFlowRunRecoveryPaused, readFlowRunExecutionBundle, assertPreparedRunBundleSqlAuthority } from "./runs.js";
|
|
14
|
+
import { readDescriptorCoordinatorSession } from "./descriptor-coordinator-session.js";
|
|
15
|
+
import { appendAudit } from "./audit.js";
|
|
16
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
12
17
|
import { verifyRecoverySnapshot } from "./eval-snapshots.js";
|
|
13
18
|
import { findRecentMatchingHookEvent, hookSessionIdentity, recoveryAcceptMatchKey } from "./hooks.js";
|
|
14
19
|
import { flowCommand } from "./stage-pause.js";
|
|
@@ -19,6 +24,93 @@ export function recoveryPacketReceipt(binding) {
|
|
|
19
24
|
throw new AppError("recovery_packet_missing", "Recovery requires its original verified Work packet", 1);
|
|
20
25
|
return { packet_json: binding.packet_json, packet_sha256: binding.packet_sha256, packet: JSON.parse(binding.packet_json) };
|
|
21
26
|
}
|
|
27
|
+
/** Read-only custody permission for one prepared recovery ACK, not lifecycle admission. */
|
|
28
|
+
export class PreparedRecoveryCoordinatorRead {
|
|
29
|
+
#scope;
|
|
30
|
+
#recoveryId;
|
|
31
|
+
#eventKey;
|
|
32
|
+
#generation;
|
|
33
|
+
#signature;
|
|
34
|
+
constructor(scope, recoveryId, eventKey, generation, signature) {
|
|
35
|
+
this.#scope = Object.freeze(scope);
|
|
36
|
+
this.#recoveryId = recoveryId;
|
|
37
|
+
this.#eventKey = eventKey;
|
|
38
|
+
this.#generation = generation;
|
|
39
|
+
this.#signature = signature;
|
|
40
|
+
Object.freeze(this);
|
|
41
|
+
}
|
|
42
|
+
get scope() { return this.#scope; }
|
|
43
|
+
static prepare(context, scope, recoveryId, eventKey) {
|
|
44
|
+
const proof = recoveryCoordinatorReadProof(context, scope, recoveryId, eventKey);
|
|
45
|
+
return new PreparedRecoveryCoordinatorRead({ ...scope }, recoveryId, eventKey, proof.generation, proof.signature);
|
|
46
|
+
}
|
|
47
|
+
assertRead(context, scope) {
|
|
48
|
+
const proof = recoveryCoordinatorReadProof(context, scope, this.#recoveryId, this.#eventKey);
|
|
49
|
+
if (hybridDigest(scope) !== hybridDigest(this.#scope) || proof.generation !== this.#generation || proof.signature !== this.#signature)
|
|
50
|
+
throw new AppError("recovery_owner_changed", "Recovery read custody changed", 1);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function recoveryCoordinatorReadProof(context, scope, recoveryId, eventKey) {
|
|
54
|
+
const guard = recoveryGuard(context, scope.projectId, scope.runId);
|
|
55
|
+
const binding = context.db.get("SELECT * FROM run_recovery_bindings WHERE recovery_id = ?", [recoveryId]);
|
|
56
|
+
const identity = hookSessionIdentity(context, scope.projectId, eventKey);
|
|
57
|
+
const hook = context.db.get("SELECT status,event_name,match_key FROM hook_events WHERE id = ?", [identity.hookEventId]);
|
|
58
|
+
const run = context.db.get("SELECT r.index_json,p.root AS project_root FROM runs r JOIN projects p ON p.id=r.project_id WHERE r.project_id=? AND r.id=?", [scope.projectId, scope.runId]);
|
|
59
|
+
const packet = binding ? recoveryPacketReceipt(binding).packet : null;
|
|
60
|
+
const delivery = packet?.lifecycle_delivery;
|
|
61
|
+
const prior = binding && context.db.get("SELECT ws.id,ws.status AS session_status,w.status AS work_status FROM work_sessions ws JOIN works w ON w.work_id=ws.work_id WHERE ws.id=? AND ws.work_id=? AND ws.session_id=? AND ws.status='running' AND w.project_id=? AND w.run_id=? AND w.status IN ('running','paused')", [binding.prior_work_session_id, binding.work_id, binding.session_id, scope.projectId, scope.runId]);
|
|
62
|
+
if (!guard || guard.recovery_id !== recoveryId || guard.status !== "resuming" || !guard.capture_path || !binding || binding.status !== "prepared" || !run || !prior
|
|
63
|
+
|| binding.session_id !== identity.sessionId || binding.harness !== identity.harness || binding.provider_session_id !== identity.providerSessionId || binding.daemon_id !== identity.daemonId || identity.parentSessionId
|
|
64
|
+
|| hook?.status !== "observed" || hook.event_name !== "PreToolUse" || !entityReferenceVariants(scope.runId).some(reference => hook.match_key === recoveryAcceptMatchKey(reference, recoveryId, run.project_root))
|
|
65
|
+
|| packet?.schema_id !== "dd-flow/root-recovery-packet@1" || packet.recovery_id !== recoveryId || packet.mode !== "same-session" || packet.continuation !== "unfinished_work" || packet.run_id !== scope.runId || packet.work_id !== binding.work_id
|
|
66
|
+
|| packet.retained_session_status !== prior.session_status || packet.retained_work_status !== prior.work_status
|
|
67
|
+
|| packet.capture_path !== guard.capture_path
|
|
68
|
+
|| packet.generation !== guard.generation || packet.stage !== scope.stage || typeof delivery?.text !== "string"
|
|
69
|
+
|| crypto.createHash("sha256").update(delivery.text).digest("hex") !== delivery.sha256
|
|
70
|
+
|| JSON.parse(run.index_json).current_stage !== scope.stage)
|
|
71
|
+
throw new AppError("recovery_binding_mismatch", "Read requires exact prepared recovery acknowledgement custody", 1);
|
|
72
|
+
return { generation: guard.generation, signature: hybridDigest({ scope, guard, binding, identity, hook, run, prior }) };
|
|
73
|
+
}
|
|
74
|
+
export function recoveredCoordinatorBinding(original, input) {
|
|
75
|
+
const scope = original.scope;
|
|
76
|
+
if (original.run_id !== input.runId || scope?.project_id !== input.projectId || scope.run_id !== input.runId || scope.stage !== input.stage
|
|
77
|
+
|| original.work_id !== input.workId || original.work_session_id !== input.priorWorkSessionId || original.session_id !== input.sessionId
|
|
78
|
+
|| original.harness !== input.harness || original.provider_session_id !== input.providerSessionId || input.model !== input.frozenModel
|
|
79
|
+
|| original.execution_sha256 !== input.executionSha256 || original.profile_id !== input.profileId || original.profile_sha256 !== input.profileSha256 || original.session_mode !== input.sessionMode
|
|
80
|
+
|| !Number.isInteger(input.generation) || input.generation < 1 || !/^[a-f0-9]{64}$/.test(input.packetSha256)
|
|
81
|
+
|| !input.daemonId || !input.hookEventKey)
|
|
82
|
+
throw new AppError("recovery_binding_mismatch", "Recovered coordinator differs from original frozen physical custody", 1);
|
|
83
|
+
return { ...original, work_session_id: input.workSessionId, recovery: {
|
|
84
|
+
recovery_id: input.recoveryId, generation: input.generation, original_work_session_id: input.priorWorkSessionId,
|
|
85
|
+
original_binding_sha256: hybridDigest(original), packet_sha256: input.packetSha256,
|
|
86
|
+
acknowledgement_hook: input.hookEventKey, daemon_id: input.daemonId
|
|
87
|
+
} };
|
|
88
|
+
}
|
|
89
|
+
/** Read issued delivery authority from the verified capture, never a mutable prompt. */
|
|
90
|
+
export function capturedStageDelivery(capture, scope, originalPrompt) {
|
|
91
|
+
const db = new DatabaseSync(path.join(capture, "runtime", "db.sqlite"), { readOnly: true });
|
|
92
|
+
try {
|
|
93
|
+
const rows = db.prepare("SELECT command, outcome_json FROM lifecycle_invocations WHERE status = 'settled' AND json_extract(scope_json, '$.projectRoot') = ? AND json_extract(scope_json, '$.runId') = ? AND json_extract(scope_json, '$.rootSessionId') = ? AND json_extract(scope_json, '$.generation') < ? ORDER BY rowid DESC").all(scope.projectRoot, scope.runId, scope.sessionId, scope.generation);
|
|
94
|
+
for (const row of rows) {
|
|
95
|
+
const result = row.outcome_json ? JSON.parse(row.outcome_json).result : null;
|
|
96
|
+
const parsed = parseLifecycleCommand(row.command);
|
|
97
|
+
if (parsed.kind === "none" || !parsed.supported || parsed.invocation.operation !== "stage_start" || parsed.invocation.args.options.get("stage")?.[0] !== scope.stage
|
|
98
|
+
|| result?.stage !== scope.stage || result.attempt !== scope.attempt || result.work_id !== scope.workId || typeof result.worker_prompt_markdown !== "string")
|
|
99
|
+
continue;
|
|
100
|
+
const text = result.worker_prompt_markdown;
|
|
101
|
+
if (!text.startsWith(originalPrompt))
|
|
102
|
+
throw new AppError("recovery_packet_mismatch", "Captured Stage delivery differs from its original semantic prompt", 1);
|
|
103
|
+
const sha256 = crypto.createHash("sha256").update(text).digest("hex");
|
|
104
|
+
if (!db.prepare("SELECT 1 FROM lifecycle_packet_assignments WHERE packet_sha256 = ? LIMIT 1").get(sha256))
|
|
105
|
+
throw new AppError("recovery_packet_missing", "Captured Stage delivery has no committed lifecycle assignment", 1);
|
|
106
|
+
return { text, sha256 };
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
db.close();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
22
114
|
function rootRecoveryPacket(context, run, guard, owner) {
|
|
23
115
|
const work = context.db.get("SELECT task, status, result, result_schema, payload_json, depends_on_json FROM works WHERE work_id = ?", [owner.work_id]);
|
|
24
116
|
const session = context.db.get("SELECT prompt_path, status FROM work_sessions WHERE id = ?", [owner.id]);
|
|
@@ -34,9 +126,18 @@ function rootRecoveryPacket(context, run, guard, owner) {
|
|
|
34
126
|
}
|
|
35
127
|
const prompt = fs.readFileSync(promptPath, "utf8");
|
|
36
128
|
const currentPrompt = currentStagePrompt(context, session.prompt_path, run.run_root, prompt);
|
|
129
|
+
const index = JSON.parse(run.index_json);
|
|
130
|
+
const stage = index.current_stage ?? null;
|
|
131
|
+
const attempt = index.stage_runs?.find((item) => item.stage === stage)?.attempt;
|
|
132
|
+
const provider = context.db.get("SELECT s.provider_session_id FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id JOIN sessions s ON s.session_id = ws.session_id AND s.project_id = w.project_id WHERE ws.id = ?", [owner.id]);
|
|
133
|
+
const delivery = !boundary && stage && attempt ? capturedStageDelivery(guard.capture_path, { projectRoot: run.project_root, runId: run.id, sessionId: provider.provider_session_id, stage, workId: owner.work_id, generation: guard.generation, attempt }, prompt) : null;
|
|
134
|
+
if (!boundary && ["mb-init", "mb-upgrade"].includes(run.flow_kind) && !delivery)
|
|
135
|
+
throw new AppError("recovery_packet_missing", "Unfinished Memory Stage requires its captured published lifecycle delivery", 1);
|
|
37
136
|
const dependencies = JSON.parse(work.depends_on_json);
|
|
38
137
|
return JSON.stringify({
|
|
39
138
|
schema_id: "dd-flow/root-recovery-packet@1", recovery_id: guard.recovery_id, generation: guard.generation, run_id: run.id, work_id: owner.work_id,
|
|
139
|
+
stage,
|
|
140
|
+
...(delivery ? { lifecycle_delivery: delivery } : {}),
|
|
40
141
|
mode: "same-session", continuation: boundary ? "stage_boundary" : "unfinished_work", retained_work_status: work.status, retained_session_status: session.status, retained_result: work.result,
|
|
41
142
|
original_task: work.task, original_task_sha256: crypto.createHash("sha256").update(work.task).digest("hex"),
|
|
42
143
|
original_prompt: { path: promptPath, text: prompt, sha256: crypto.createHash("sha256").update(prompt).digest("hex") },
|
|
@@ -53,7 +154,7 @@ export function acceptanceCommand(context, runId, recoveryId, projectRoot) {
|
|
|
53
154
|
return managedLifecycleCommand(context, `${flowCommand(context)} run recovery accept ${JSON.stringify(runId)} --recovery-id ${JSON.stringify(recoveryId)} --project-root ${JSON.stringify(projectRoot)} --json`);
|
|
54
155
|
}
|
|
55
156
|
export function requireRecoveryRun(context, projectId, runId) {
|
|
56
|
-
const run = context.db.get("SELECT r.id, r.run_root, r.workspace_root, p.root AS project_root FROM runs r JOIN projects p ON p.id = r.project_id WHERE r.project_id = ? AND r.id = ?", [projectId, runId]);
|
|
157
|
+
const run = context.db.get("SELECT r.id, r.run_root, r.workspace_root, r.index_json, r.flow_kind, p.root AS project_root FROM runs r JOIN projects p ON p.id = r.project_id WHERE r.project_id = ? AND r.id = ?", [projectId, runId]);
|
|
57
158
|
if (!run?.run_root)
|
|
58
159
|
throw new AppError("runtime_missing", "RUN artifact root is unavailable", 1, { run_id: runId });
|
|
59
160
|
return run;
|
|
@@ -251,6 +352,7 @@ export function resumeRunRecovery(context, input, prepared = prepareRunRecoveryR
|
|
|
251
352
|
return { signature, owner, packet, running, evidence };
|
|
252
353
|
});
|
|
253
354
|
const { owner, packet, running } = publication;
|
|
355
|
+
verifyRecoverySnapshot(guard.capture_path, { runId: run.id, recoveryId: guard.recovery_id, projectRoot: run.project_root, workspaceRoot: run.workspace_root });
|
|
254
356
|
context.db.writeTransaction(() => {
|
|
255
357
|
if (authority() !== publication.signature)
|
|
256
358
|
throw new AppError("recovery_state_changed", "Recovery inputs changed while its packet was prepared", 1, { recovery_id: guard.recovery_id });
|
|
@@ -278,6 +380,16 @@ export function acceptRunRecovery(context, input) {
|
|
|
278
380
|
const eventKey = input.hookEventId ?? findRecentMatchingHookEvent(context, { projectId: project.id, matchKey, errorCode: "trusted_recovery_acceptance_required", operation: "run recovery accept" }).eventKey;
|
|
279
381
|
const identity = hookSessionIdentity(context, project.id, eventKey);
|
|
280
382
|
const now = context.now();
|
|
383
|
+
const preparedBinding = context.db.get("SELECT * FROM run_recovery_bindings WHERE recovery_id = ?", [input.recoveryId]);
|
|
384
|
+
const preparedPacket = preparedBinding ? recoveryPacketReceipt(preparedBinding).packet : null;
|
|
385
|
+
const bundle = preparedBinding?.status === "prepared" && preparedPacket?.continuation === "unfinished_work" && preparedPacket.lifecycle_delivery
|
|
386
|
+
? readFlowRunExecutionBundle(context, { projectRoot: run.project_root, runId: run.id }) : null;
|
|
387
|
+
const coordinatorScope = bundle && preparedPacket?.stage ? { projectId: project.id, runId: run.id, stage: preparedPacket.stage } : null;
|
|
388
|
+
if (preparedBinding?.status === "prepared" && preparedPacket?.continuation === "unfinished_work"
|
|
389
|
+
&& ["mb-init", "mb-upgrade"].includes(run.flow_kind) && (!preparedPacket.lifecycle_delivery || !coordinatorScope))
|
|
390
|
+
throw new AppError("recovery_packet_missing", "Memory recovery lacks retained Stage delivery and frozen coordinator scope", 1);
|
|
391
|
+
const recoveryRead = coordinatorScope ? PreparedRecoveryCoordinatorRead.prepare(context, coordinatorScope, input.recoveryId, eventKey) : undefined;
|
|
392
|
+
const preparedCustody = coordinatorScope && bundle ? readDescriptorCoordinatorSession(context, { scope: coordinatorScope, bundle, workId: preparedBinding.work_id, recoveryRead: recoveryRead }) : null;
|
|
281
393
|
const result = context.db.writeTransaction(() => {
|
|
282
394
|
assertRecoveryRunCanResume(context, project.id, run.id);
|
|
283
395
|
const guard = recoveryGuard(context, project.id, run.id);
|
|
@@ -287,11 +399,14 @@ export function acceptRunRecovery(context, input) {
|
|
|
287
399
|
throw new AppError("recovery_binding_mismatch", "Recovery acknowledgement does not belong to the authorized physical Session and generation", 1);
|
|
288
400
|
const packetReceipt = recoveryPacketReceipt(binding);
|
|
289
401
|
const retained = packetReceipt.packet;
|
|
290
|
-
|
|
402
|
+
const authorityPrompt = retained.lifecycle_delivery?.text ?? retained.original_prompt.text;
|
|
403
|
+
if (retained.lifecycle_delivery && crypto.createHash("sha256").update(authorityPrompt).digest("hex") !== retained.lifecycle_delivery.sha256)
|
|
404
|
+
throw new AppError("recovery_packet_mismatch", "Retained lifecycle delivery checksum differs", 1);
|
|
405
|
+
if (binding.harness === "zcode-acp" || (binding.harness === "codex-desktop" && hasLifecyclePacketAssignments(context, authorityPrompt))) {
|
|
291
406
|
const scope = { projectRoot: run.project_root, daemonId: binding.daemon_id, rootSessionId: binding.provider_session_id, runId: run.id, generation: guard.generation };
|
|
292
407
|
const assignments = new Set();
|
|
293
408
|
packetReceipt.lifecycle_recovery = {
|
|
294
|
-
commands: retained.continuation === "stage_boundary" ? [] : recoverLifecycleCommands({ ...context, captureLifecycleAssignment: id => { assignments.add(id); } },
|
|
409
|
+
commands: retained.continuation === "stage_boundary" ? [] : recoverLifecycleCommands({ ...context, captureLifecycleAssignment: id => { assignments.add(id); } }, authorityPrompt, scope),
|
|
295
410
|
instruction: "original_prompt is immutable historical evidence. Do not execute its old command authority. Use only replacement commands below; reconcile unknown outcomes before continuing. At a completed stage boundary return control to the runner.",
|
|
296
411
|
work_list_command: `${flowCommand({ ...context, env: { ...context.env, DD_FLOW_INVOCATION_SCOPE: JSON.stringify(scope) } })} work ls --run ${run.id} --ready --project-root ${JSON.stringify(run.project_root)} --json`
|
|
297
412
|
};
|
|
@@ -308,10 +423,35 @@ export function acceptRunRecovery(context, input) {
|
|
|
308
423
|
const prior = context.db.get("SELECT ws.prompt_path, ws.result_path FROM work_sessions ws JOIN works w ON w.work_id = ws.work_id WHERE ws.id = ? AND ws.work_id = ? AND ws.session_id = ? AND ws.status = ? AND w.status = ?", [binding.prior_work_session_id, binding.work_id, binding.session_id, packet.retained_session_status ?? "running", packet.retained_work_status ?? (JSON.parse(binding.packet_json).paused ? "paused" : "running")]);
|
|
309
424
|
if (!prior)
|
|
310
425
|
throw new AppError("recovery_owner_changed", "Retained root WorkSession changed before recovery acceptance", 1);
|
|
426
|
+
let physicalBinding = null;
|
|
427
|
+
if (!boundary && coordinatorScope && bundle) {
|
|
428
|
+
const currentRun = assertPreparedRunBundleSqlAuthority(context, coordinatorScope, bundle);
|
|
429
|
+
if (preparedBinding?.packet_sha256 !== binding.packet_sha256 || preparedBinding.prior_work_session_id !== binding.prior_work_session_id
|
|
430
|
+
|| preparedPacket?.stage !== JSON.parse(currentRun.index_json).current_stage
|
|
431
|
+
|| readDescriptorCoordinatorSession(context, { scope: coordinatorScope, bundle, workId: binding.work_id, recoveryRead: recoveryRead }) !== preparedCustody)
|
|
432
|
+
throw new AppError("recovery_owner_changed", "Original coordinator custody changed before recovery acknowledgement", 1);
|
|
433
|
+
const route = bundle.execution.stage(coordinatorScope.stage);
|
|
434
|
+
if (identity.model !== route.profile.model)
|
|
435
|
+
throw new AppError("recovery_binding_mismatch", "Recovery acknowledgement model differs from the frozen coordinator", 1);
|
|
436
|
+
const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'stage.coordinator_binding' AND json_extract(payload_json, '$.run_id') = ? AND json_extract(payload_json, '$.scope.stage') = ? AND json_extract(payload_json, '$.work_session_id') = ?", [project.id, run.id, coordinatorScope.stage, binding.prior_work_session_id]);
|
|
437
|
+
physicalBinding = JSON.parse(rows[0].payload_json);
|
|
438
|
+
}
|
|
311
439
|
context.db.run("UPDATE hook_events SET status = 'claimed', claimed_at = ? WHERE id = ? AND status = 'observed'", [now, identity.hookEventId]);
|
|
312
440
|
if (!boundary) {
|
|
441
|
+
const recoveredWorkSessionId = `WS-${crypto.randomUUID()}`;
|
|
313
442
|
context.db.run("UPDATE work_sessions SET status = 'interrupted', completed_at = ?, updated_at = ? WHERE id = ?", [now, now, binding.prior_work_session_id]);
|
|
314
|
-
context.db.run("INSERT INTO work_sessions (id, work_id, session_id, hook_event_id, status, prompt_path, result_path, created_at, updated_at) VALUES (?, ?, ?, ?, 'running', ?, ?, ?, ?)", [
|
|
443
|
+
context.db.run("INSERT INTO work_sessions (id, work_id, session_id, hook_event_id, status, prompt_path, result_path, created_at, updated_at) VALUES (?, ?, ?, ?, 'running', ?, ?, ?, ?)", [recoveredWorkSessionId, binding.work_id, binding.session_id, eventKey, prior.prompt_path, prior.result_path, now, now]);
|
|
444
|
+
if (physicalBinding)
|
|
445
|
+
appendAudit(context, { projectId: project.id, eventType: "stage.coordinator_binding", payload: recoveredCoordinatorBinding(physicalBinding, {
|
|
446
|
+
projectId: project.id, runId: run.id, stage: coordinatorScope.stage, workId: binding.work_id,
|
|
447
|
+
priorWorkSessionId: binding.prior_work_session_id, workSessionId: recoveredWorkSessionId,
|
|
448
|
+
sessionId: identity.sessionId, harness: identity.harness, providerSessionId: identity.providerSessionId,
|
|
449
|
+
model: identity.model, frozenModel: bundle.execution.stage(coordinatorScope.stage).profile.model,
|
|
450
|
+
executionSha256: bundle.execution.sha256, profileId: bundle.execution.stage(coordinatorScope.stage).profile.id,
|
|
451
|
+
profileSha256: hybridDigest(bundle.execution.stage(coordinatorScope.stage).profile), sessionMode: bundle.execution.stage(coordinatorScope.stage).session_mode,
|
|
452
|
+
recoveryId: guard.recovery_id, generation: guard.generation, packetSha256: binding.packet_sha256,
|
|
453
|
+
hookEventKey: eventKey, daemonId: identity.daemonId
|
|
454
|
+
}) });
|
|
315
455
|
}
|
|
316
456
|
context.db.run("UPDATE run_recovery_bindings SET status = 'accepted', hook_event_key = ?, accepted_at = ? WHERE recovery_id = ?", [eventKey, now, binding.recovery_id]);
|
|
317
457
|
context.db.run("UPDATE run_recovery_guards SET status = 'recovered', resumed_at = ?, updated_at = ? WHERE recovery_id = ?", [now, now, guard.recovery_id]);
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { AppError } from "../shared/errors.js";
|
|
4
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
5
|
+
export const runStartPayloadLimit = 64 * 1024 * 1024;
|
|
6
|
+
export function runStartBytesSha256(bytes) { return createHash("sha256").update(bytes).digest("hex"); }
|
|
7
|
+
export function refuseRunStart(reason) {
|
|
8
|
+
throw new AppError("run_start_custody_invalid", "RUN creation requires its exact retained request", 1, { reason, effect: "no_effect", next_action: "reconcile_exact_request" });
|
|
9
|
+
}
|
|
10
|
+
export function hasRunStartTable(context) {
|
|
11
|
+
return Boolean(context.db.get("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'run_start_requests'"));
|
|
12
|
+
}
|
|
13
|
+
function object(value) {
|
|
14
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
15
|
+
}
|
|
16
|
+
function exactKeys(value, keys) {
|
|
17
|
+
return Object.keys(value).sort().join("\0") === keys.sort().join("\0");
|
|
18
|
+
}
|
|
19
|
+
function digest(value) { return typeof value === "string" && /^[a-f0-9]{64}$/.test(value); }
|
|
20
|
+
function canonicalBase64(value) {
|
|
21
|
+
return typeof value === "string" && Buffer.from(value, "base64").toString("base64") === value;
|
|
22
|
+
}
|
|
23
|
+
/** Retained data can drive filesystem writes only after a closed shape check. */
|
|
24
|
+
export function validateRunStartPayload(value) {
|
|
25
|
+
if (!object(value) || !exactKeys(value, ["schema_id", "request_id", "project_id", "project_root", "workspace_root", "home_root",
|
|
26
|
+
"run_id", "request_sha256", "bundle_sha256", "subject_type", "subject_id", "slug", "run_root", "initial_index",
|
|
27
|
+
"bundle_manifest_base64", "blobs", "artifacts", "owners", "git"]))
|
|
28
|
+
refuseRunStart("invalid_allocation_shape");
|
|
29
|
+
for (const key of ["request_id", "project_id", "run_id", "slug", "subject_type", "subject_id"])
|
|
30
|
+
if (typeof value[key] !== "string" || !value[key].trim() || value[key].length > 1024)
|
|
31
|
+
refuseRunStart("invalid_allocation_shape");
|
|
32
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(value.project_id)
|
|
33
|
+
|| !/^RUN-\d+-[a-z0-9][a-z0-9-]*$/.test(value.run_id))
|
|
34
|
+
refuseRunStart("invalid_allocation_shape");
|
|
35
|
+
for (const key of ["project_root", "workspace_root", "home_root", "run_root"])
|
|
36
|
+
if (typeof value[key] !== "string" || !path.isAbsolute(value[key]) || path.resolve(value[key]) !== value[key])
|
|
37
|
+
refuseRunStart("invalid_allocation_scope");
|
|
38
|
+
if (value.run_root !== path.join(value.home_root, "projects", value.project_id, "runs", value.run_id)
|
|
39
|
+
|| !digest(value.request_sha256) || !digest(value.bundle_sha256) || !canonicalBase64(value.bundle_manifest_base64)
|
|
40
|
+
|| runStartBytesSha256(Buffer.from(value.bundle_manifest_base64, "base64")) !== value.bundle_sha256
|
|
41
|
+
|| !object(value.initial_index) || !object(value.owners) || !exactKeys(value.owners, ["home", "project", "workspace"]))
|
|
42
|
+
refuseRunStart("invalid_allocation_shape");
|
|
43
|
+
for (const key of ["home", "project", "workspace"]) {
|
|
44
|
+
const owner = value.owners[key];
|
|
45
|
+
if (!object(owner) || !exactKeys(owner, ["dev", "ino"]) || ![owner.dev, owner.ino].every(number => Number.isSafeInteger(number) && Number(number) >= 0))
|
|
46
|
+
refuseRunStart("invalid_allocation_owner");
|
|
47
|
+
}
|
|
48
|
+
if (!object(value.git) || !exactKeys(value.git, ["branch", "head", "status_sha256"]) || typeof value.git.branch !== "string" || !value.git.branch
|
|
49
|
+
|| value.git.head !== null && (typeof value.git.head !== "string" || !/^[a-f0-9]{40,64}$/.test(value.git.head))
|
|
50
|
+
|| !digest(value.git.status_sha256))
|
|
51
|
+
refuseRunStart("invalid_allocation_git");
|
|
52
|
+
for (const key of ["blobs", "artifacts"]) {
|
|
53
|
+
const entries = value[key];
|
|
54
|
+
if (!Array.isArray(entries) || entries.length > 4096)
|
|
55
|
+
refuseRunStart("invalid_allocation_assets");
|
|
56
|
+
const seen = new Set();
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
if (!object(entry) || !exactKeys(entry, ["path", "base64"]) || typeof entry.path !== "string"
|
|
59
|
+
|| !entry.path || entry.path.includes("\\") || entry.path.includes("\0") || entry.path.split("/").some(part => !part || part === "." || part === "..")
|
|
60
|
+
|| seen.has(entry.path) || !canonicalBase64(entry.base64))
|
|
61
|
+
refuseRunStart("invalid_allocation_assets");
|
|
62
|
+
if (key === "artifacts" && (!["engine-binding.json", "operational-policy.source.md", "operational-contract.json"].includes(entry.path)
|
|
63
|
+
|| Buffer.from(entry.base64, "base64").length > 1024 * 1024))
|
|
64
|
+
refuseRunStart("invalid_allocation_artifact");
|
|
65
|
+
seen.add(entry.path);
|
|
66
|
+
}
|
|
67
|
+
if (key === "artifacts" && (!seen.has("engine-binding.json") || seen.has("operational-policy.source.md") && !seen.has("operational-contract.json")))
|
|
68
|
+
refuseRunStart("invalid_allocation_artifact");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** Metadata and copied SQL bytes only. No engine/filesystem/provider admission. */
|
|
72
|
+
export function readRunStartRequest(context, projectId, requestId) {
|
|
73
|
+
return context.db.readSnapshot(() => readRunStartRequestSnapshot(context, projectId, requestId));
|
|
74
|
+
}
|
|
75
|
+
function readRunStartRequestSnapshot(context, projectId, requestId) {
|
|
76
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,95}$/.test(requestId))
|
|
77
|
+
refuseRunStart("invalid_request_id");
|
|
78
|
+
const row = hasRunStartTable(context) ? context.db.get("SELECT run_id, payload_json, payload_sha256 FROM run_start_requests WHERE project_id = ? AND request_id = ?", [projectId, requestId]) : undefined;
|
|
79
|
+
const starts = context.db.all("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.start_request' AND json_extract(payload_json, '$.request_id') = ? LIMIT 2", [projectId, requestId]);
|
|
80
|
+
if (!row) {
|
|
81
|
+
const marker = context.db.get("SELECT 1 FROM runs WHERE project_id = ? AND json_extract(index_json, '$.start_request.request_id') = ? LIMIT 1", [projectId, requestId]);
|
|
82
|
+
const bound = context.db.get("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'run_bundle_authorities'")
|
|
83
|
+
&& context.db.get("SELECT 1 FROM run_bundle_authorities WHERE project_id = ? AND json_extract(admission_json, '$.executionScope.requestId') = ? LIMIT 1", [projectId, requestId]);
|
|
84
|
+
if (starts.length || marker || bound)
|
|
85
|
+
refuseRunStart("request_receipt_missing");
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
if (starts.length !== 1 || Buffer.byteLength(row.payload_json) > runStartPayloadLimit
|
|
89
|
+
|| runStartBytesSha256(row.payload_json) !== row.payload_sha256)
|
|
90
|
+
refuseRunStart("allocation_changed");
|
|
91
|
+
let payload, receipt;
|
|
92
|
+
try {
|
|
93
|
+
payload = JSON.parse(row.payload_json);
|
|
94
|
+
receipt = JSON.parse(starts[0].payload_json);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return refuseRunStart("invalid_allocation_json");
|
|
98
|
+
}
|
|
99
|
+
validateRunStartPayload(payload);
|
|
100
|
+
if (!object(receipt))
|
|
101
|
+
refuseRunStart("invalid_allocation_receipt");
|
|
102
|
+
const run = context.db.get("SELECT project_root, workspace_root, run_root, flow_kind, subject_type, subject_id, index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, row.run_id]);
|
|
103
|
+
let index;
|
|
104
|
+
try {
|
|
105
|
+
if (!run)
|
|
106
|
+
refuseRunStart("allocated_run_missing");
|
|
107
|
+
index = JSON.parse(run.index_json);
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (error instanceof AppError)
|
|
111
|
+
throw error;
|
|
112
|
+
return refuseRunStart("allocated_run_changed");
|
|
113
|
+
}
|
|
114
|
+
if (!object(index))
|
|
115
|
+
refuseRunStart("allocated_run_changed");
|
|
116
|
+
const marker = index.start_request, initial = payload.initial_index;
|
|
117
|
+
if (!payload || payload.schema_id !== "dd-flow/run-start-allocation@1" || payload.project_id !== projectId
|
|
118
|
+
|| payload.request_id !== requestId || payload.run_id !== row.run_id || !Array.isArray(payload.blobs) || !Array.isArray(payload.artifacts)
|
|
119
|
+
|| !initial || initial.run_id !== row.run_id || initial.project?.id !== projectId || initial.start_request !== undefined
|
|
120
|
+
|| initial.execution_bundle?.manifest_sha256 !== payload.bundle_sha256 || !/^[a-f0-9]{64}$/.test(payload.request_sha256)
|
|
121
|
+
|| !marker || marker.schema_id !== "dd-flow/run-start-request@1" || marker.request_id !== requestId
|
|
122
|
+
|| marker.request_sha256 !== payload.request_sha256 || marker.allocation_sha256 !== row.payload_sha256
|
|
123
|
+
|| receipt.run_id !== row.run_id || receipt.request_id !== requestId || receipt.request_sha256 !== payload.request_sha256 || receipt.allocation_sha256 !== row.payload_sha256
|
|
124
|
+
|| run.project_root !== payload.project_root || run.workspace_root !== payload.workspace_root || run.run_root !== payload.run_root
|
|
125
|
+
|| run.flow_kind !== initial.flow_kind || run.subject_id !== payload.subject_id || run.subject_type !== payload.subject_type
|
|
126
|
+
|| !object(index.execution_bundle) || hybridDigest(index.execution_bundle) !== hybridDigest(initial.execution_bundle))
|
|
127
|
+
refuseRunStart("allocated_run_changed");
|
|
128
|
+
const publications = context.db.all("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.start_published' AND json_extract(payload_json, '$.run_id') = ? LIMIT 2", [projectId, row.run_id]);
|
|
129
|
+
if (publications.length > 1 || Boolean(marker.publication_sha256) !== (publications.length === 1))
|
|
130
|
+
refuseRunStart("publication_receipt_missing");
|
|
131
|
+
if (publications.length) {
|
|
132
|
+
let publication;
|
|
133
|
+
try {
|
|
134
|
+
publication = JSON.parse(publications[0].payload_json);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return refuseRunStart("invalid_publication_receipt");
|
|
138
|
+
}
|
|
139
|
+
if (!object(publication) || publication.schema_id !== "dd-flow/run-start-publication@1" || hybridDigest(publication) !== marker.publication_sha256 || publication.request_id !== requestId
|
|
140
|
+
|| publication.run_id !== row.run_id || publication.allocation_sha256 !== row.payload_sha256 || publication.bundle_sha256 !== payload.bundle_sha256)
|
|
141
|
+
refuseRunStart("publication_receipt_changed");
|
|
142
|
+
const owners = context.db.all("SELECT payload_json FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.start_root_owned' AND json_extract(payload_json, '$.run_id') = ? LIMIT 2", [projectId, row.run_id]);
|
|
143
|
+
if (owners.length !== 1)
|
|
144
|
+
refuseRunStart("root_ownership_receipt_missing");
|
|
145
|
+
let owner;
|
|
146
|
+
try {
|
|
147
|
+
owner = JSON.parse(owners[0].payload_json);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return refuseRunStart("root_ownership_receipt_changed");
|
|
151
|
+
}
|
|
152
|
+
if (!object(owner) || owner.schema_id !== "dd-flow/run-start-owner@1" || owner.run_id !== row.run_id || owner.request_id !== requestId
|
|
153
|
+
|| owner.allocation_sha256 !== row.payload_sha256 || hybridDigest(owner) !== publication.root_owner_sha256)
|
|
154
|
+
refuseRunStart("root_ownership_receipt_changed");
|
|
155
|
+
}
|
|
156
|
+
return { payload, payloadJson: row.payload_json, sha256: row.payload_sha256, published: Boolean(marker.publication_sha256) };
|
|
157
|
+
}
|
|
158
|
+
/** Lost markers/receipts cannot silently become legacy mutable execution. */
|
|
159
|
+
export function assertRunStartupSettled(context, projectId, runId) {
|
|
160
|
+
const run = context.db.get("SELECT index_json FROM runs WHERE project_id = ? AND id = ?", [projectId, runId]);
|
|
161
|
+
let marker;
|
|
162
|
+
try {
|
|
163
|
+
marker = run ? JSON.parse(run.index_json).start_request : undefined;
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return refuseRunStart("allocated_run_changed");
|
|
167
|
+
}
|
|
168
|
+
const row = hasRunStartTable(context) ? context.db.get("SELECT request_id FROM run_start_requests WHERE project_id = ? AND run_id = ?", [projectId, runId]) : undefined;
|
|
169
|
+
const audit = context.db.get("SELECT json_extract(payload_json, '$.request_id') AS request_id FROM audit_events WHERE project_id = ? AND event_type = 'flow_run.start_request' AND json_extract(payload_json, '$.run_id') = ? LIMIT 1", [projectId, runId]);
|
|
170
|
+
if (!marker && !row && !audit)
|
|
171
|
+
return;
|
|
172
|
+
if (!marker || !row || !audit || row.request_id !== marker.request_id || audit.request_id !== marker.request_id)
|
|
173
|
+
refuseRunStart("request_receipt_missing");
|
|
174
|
+
const retained = readRunStartRequest(context, projectId, marker.request_id);
|
|
175
|
+
if (!retained?.published)
|
|
176
|
+
throw new AppError("run_start_publication_pending", "Finish the original RUN creation request before lifecycle writes", 1, { effect: "no_effect", run_id: runId, request_id: marker.request_id, next_action: "reconcile_exact_request" });
|
|
177
|
+
}
|