@deksden-com/dd-flow-cli 0.9.0-beta.136 → 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 +12 -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 +56 -3
- package/dist/cli/command-option-descriptions.js +5 -0
- package/dist/cli/executor-onboarding.js +8 -3
- package/dist/cli/flows.js +29 -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/native-hook-ingress.js +1 -1
- package/dist/cli/response-file.js +2 -0
- package/dist/cli/run-cli.js +283 -160
- package/dist/cli/setup.js +9 -7
- package/dist/cli.js +54 -2
- package/dist/domain/stage-handler-identities.js +20 -0
- package/dist/harness-runtime/DD-ZCODE.md +13 -0
- package/dist/harness-runtime/bin/dd-opencode.mjs +3 -3
- 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 +20 -5
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +45 -10
- package/dist/harness-runtime/lib/dd-zcode.mjs +7 -3
- 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 +5 -1
- 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/code-checks.js +387 -34
- package/dist/services/controller-child-observer.js +99 -0
- package/dist/services/controller-execution.js +88 -0
- package/dist/services/controller-fanout.js +79 -13
- 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/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/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/hooks.js +24 -6
- package/dist/services/hybrid-contracts.js +31 -3
- package/dist/services/ids.js +29 -1
- package/dist/services/installation-lock.js +7 -4
- 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/projects.js +17 -2
- package/dist/services/recovery-snapshot-database.js +416 -1
- 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 +184 -48
- 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/stage-context.js +9 -1
- package/dist/services/stage-handlers.js +74 -0
- package/dist/services/stage-lifecycle.js +2 -1
- package/dist/services/stage-report-renderer.js +76 -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 +136 -10
- package/dist/services/vnext-merge.js +8 -3
- package/dist/services/vnext-plan-review.js +6 -4
- package/dist/services/vnext-protocolize.js +60 -2
- 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
package/dist/services/engines.js
CHANGED
|
@@ -6,31 +6,25 @@ import { spawn } from "node:child_process";
|
|
|
6
6
|
import { commandText } from '../harness-runtime/lib/process-json.mjs';
|
|
7
7
|
import { SETUP_STARTUP_MS, publishSetup } from '../harness-runtime/lib/setup-progress.mjs';
|
|
8
8
|
import { createRequire } from "node:module";
|
|
9
|
+
import { valid } from "semver";
|
|
9
10
|
import { getCliBuildInfo } from "./build-info.js";
|
|
10
11
|
import { AppError } from "../shared/errors.js";
|
|
11
12
|
import { ensureDir, engineStoreRoot, engineVersionRoot, resolveProjectRoot } from "../storage/paths.js";
|
|
12
|
-
import { classifyCliOperation } from "./cli-operation-classifier.js";
|
|
13
|
+
import { classifyCliOperation, isRouterNative } from "./cli-operation-classifier.js";
|
|
13
14
|
import { requireProtocol } from "./protocols.js";
|
|
14
15
|
import { resolveCanonRootBeforeContext } from "./canon.js";
|
|
15
16
|
import { appendAudit } from "./audit.js";
|
|
17
|
+
import { readBoundedReleaseFile } from "./release-source.js";
|
|
16
18
|
import { findProjectByRoot } from "./projects.js";
|
|
17
19
|
import { parseCommandArgs } from "./lifecycle-command.js";
|
|
18
20
|
import { allRunEngineBindings, locateRunRoot, readRunEngineBinding, runEngineBindingSchemaId, writeRunEngineBinding } from "./run-engine-bindings.js";
|
|
19
21
|
export const engineManifestSchemaId = "dd-flow/engine-manifest@1";
|
|
20
|
-
const routerNativeFamilies = new Set(["engine", "version", "schema"]);
|
|
21
22
|
const sourceFile = fileURLToPath(import.meta.url);
|
|
22
23
|
export function isEngineMode(env) {
|
|
23
24
|
return env.DD_FLOW_ENGINE_MODE === "1";
|
|
24
25
|
}
|
|
25
26
|
export function isRouterNativeCommand(args) {
|
|
26
|
-
|
|
27
|
-
if (!first || first === "--version")
|
|
28
|
-
return true;
|
|
29
|
-
if (first === "--help" || first === "-h")
|
|
30
|
-
return true;
|
|
31
|
-
if (args.includes("--help") || args.includes("-h"))
|
|
32
|
-
return true;
|
|
33
|
-
return first === "run" && args[1] === "reconcile" || routerNativeFamilies.has(first) || first === "codex" && args[1] === "hook" && args[2] === "handle";
|
|
27
|
+
return isRouterNative(args);
|
|
34
28
|
}
|
|
35
29
|
export function installCurrentEngine(context, input = {}) {
|
|
36
30
|
const installation = prepareInstallation(context, input);
|
|
@@ -56,6 +50,27 @@ function* prepareInstallation(context, input) {
|
|
|
56
50
|
if (!packageRoot) {
|
|
57
51
|
throw new AppError("engine_install_failed", "Cannot locate package root for current dd-flow engine", 1);
|
|
58
52
|
}
|
|
53
|
+
return yield* installPackageSnapshot(context, packageRoot, build.package_name, build.version, input);
|
|
54
|
+
}
|
|
55
|
+
/** Delivery from an independently selected package, not the invoking CLI.
|
|
56
|
+
* The caller owns release/dependency qualification of expectedChecksum.
|
|
57
|
+
* Existing snapshots are preserved even when not yet bound to a RUN. */
|
|
58
|
+
export function installPackageEngine(context, input) {
|
|
59
|
+
if (!path.isAbsolute(input.packageRoot) || fs.realpathSync(input.packageRoot) !== input.packageRoot
|
|
60
|
+
|| !/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/.test(input.packageName)
|
|
61
|
+
|| valid(input.version) !== input.version
|
|
62
|
+
|| !/^[a-f0-9]{64}$/.test(input.expectedChecksum)) {
|
|
63
|
+
throw new AppError("engine_install_failed", "Selected package requires a canonical root and independent content checksum", 1, { effect: "no_effect" });
|
|
64
|
+
}
|
|
65
|
+
const metadata = JSON.parse(readBoundedReleaseFile(path.join(input.packageRoot, "package.json"), 1024 * 1024).toString("utf8"));
|
|
66
|
+
if (metadata.name !== input.packageName || metadata.version !== input.version
|
|
67
|
+
|| snapshotFilesChecksum(packageSnapshotFiles(input.packageRoot)) !== input.expectedChecksum) {
|
|
68
|
+
throw new AppError("engine_install_failed", "Selected package identity or contents changed before delivery", 1, { effect: "no_effect" });
|
|
69
|
+
}
|
|
70
|
+
return installPackageSnapshot(context, input.packageRoot, input.packageName, input.version, { force: true, forRouting: true }, input.expectedChecksum, input.validateStaged).next().value;
|
|
71
|
+
}
|
|
72
|
+
function* installPackageSnapshot(context, packageRoot, packageName, version, input, expectedChecksum, validateStaged) {
|
|
73
|
+
const build = { package_name: packageName, version };
|
|
59
74
|
const target = engineVersionRoot(context.ddFlowHome, build.package_name, build.version);
|
|
60
75
|
const manifestPath = path.join(target, "engine.json");
|
|
61
76
|
const existing = readManifest(manifestPath);
|
|
@@ -90,11 +105,20 @@ function* prepareInstallation(context, input) {
|
|
|
90
105
|
copyPackageSnapshot(packageRoot, tmp);
|
|
91
106
|
const stagedManifest = buildManifest(context, build.package_name, build.version, packageRoot, target, tmp);
|
|
92
107
|
const stagedChecksum = stagedManifest.integrity.checksum;
|
|
108
|
+
if (expectedChecksum !== undefined && stagedChecksum !== expectedChecksum) {
|
|
109
|
+
throw new AppError("engine_install_failed", "Selected package changed during delivery", 1, { effect: "partial", path: tmp });
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
validateStaged?.(tmp);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
throw new AppError("engine_install_failed", "Staged package no longer matches its selected release", 1, { effect: "partial", path: tmp, reason: error instanceof AppError ? error.details.reason ?? error.code : "staged_selection_changed" });
|
|
116
|
+
}
|
|
93
117
|
const activationTarget = input.force
|
|
94
118
|
&& existing
|
|
95
119
|
&& stagedChecksum
|
|
96
120
|
&& stagedChecksum !== existing.integrity.checksum
|
|
97
|
-
&& engineIsBound(context.ddFlowHome, existing)
|
|
121
|
+
&& (expectedChecksum !== undefined || engineIsBound(context.ddFlowHome, existing))
|
|
98
122
|
? `${target}--${stagedChecksum}`
|
|
99
123
|
: target;
|
|
100
124
|
const duplicate = stagedChecksum ? findInstalledManifest(context.ddFlowHome, {
|
|
@@ -194,7 +218,6 @@ export async function routeArgsThroughEngine(context, args, io, stdin, env, reso
|
|
|
194
218
|
const bound = boundRunEngineForArgs(context, args, resolvedProjectRoot ?? resolveExplicitProjectRoot(args) ?? undefined);
|
|
195
219
|
if (bound) {
|
|
196
220
|
const entrypoint = resolveManifestEntrypoint(bound.manifest);
|
|
197
|
-
await probeEntrypoint(bound.manifest, undefined, context.ddFlowHome, process.env);
|
|
198
221
|
return spawnEngine(bound.manifest, entrypoint, args, io, stdin, env, getCliBuildInfo().version);
|
|
199
222
|
}
|
|
200
223
|
const selection = selectEngine(context, { projectRoot: resolvedProjectRoot ?? resolveExplicitProjectRoot(args) ?? undefined, env }, { allowCurrentInProcess: true }, classification);
|
|
@@ -213,15 +236,8 @@ export async function routeArgsThroughEngine(context, args, io, stdin, env, reso
|
|
|
213
236
|
return Promise.resolve({ routed: true, exitCode: 0, inProcess: true });
|
|
214
237
|
}
|
|
215
238
|
const entrypoint = resolveManifestEntrypoint(selection.selected);
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
219
|
-
catch (cause) {
|
|
220
|
-
const error = cause;
|
|
221
|
-
if (error.code !== 'engine_probe_failed')
|
|
222
|
-
throw error;
|
|
223
|
-
throw new AppError('missing_engine', 'Selected engine cannot load its runtime dependencies', 1, { probe: error.details });
|
|
224
|
-
}
|
|
239
|
+
// The admitted immutable entrypoint executes the requested operation itself.
|
|
240
|
+
// A separate --version process adds no operation-specific admission.
|
|
225
241
|
return spawnEngine(selection.selected, entrypoint, args, io, stdin, env, current.version);
|
|
226
242
|
}
|
|
227
243
|
export async function prepareRunEngineBind(context, input) {
|
|
@@ -694,7 +710,8 @@ function buildManifest(context, packageName, version, packageRoot, snapshotRoot,
|
|
|
694
710
|
supports_contracts: {
|
|
695
711
|
compatibility: ["dd-flow/compatibility@1"],
|
|
696
712
|
version: ["dd-flow/version-report@1"],
|
|
697
|
-
status: ["dd-flow/status-report@1"]
|
|
713
|
+
status: ["dd-flow/status-report@1"],
|
|
714
|
+
hybrid: ["dd-flow/flow-descriptor@1", "dd-flow/execution-settings@1", "dd-flow/execution-bundle@1", "dd-flow/memory-bootstrap-receipt@2"]
|
|
698
715
|
},
|
|
699
716
|
schema_registry: buildSchemaRegistry(checksumRoot),
|
|
700
717
|
integrity: {
|
|
@@ -16,13 +16,26 @@ import { validateRunOperationalDecision } from "./vnext-execution-profile.js";
|
|
|
16
16
|
import { readVnextFlowDefinition } from "./vnext-specify.js";
|
|
17
17
|
import { captureRecoveryDatabase, readRecoveryResourceEvidence } from "./recovery-snapshot-database.js";
|
|
18
18
|
import { resourceHome } from "./managed-processes.js";
|
|
19
|
+
import { getResourceDatabase } from "../storage/database.js";
|
|
19
20
|
import { codexHomeSharedEntries } from "./hooks.js";
|
|
20
21
|
import { prepareOutputFile } from "../cli/input-preparation.js";
|
|
21
22
|
import { localEnvironmentAllowed, projectServicePathFilter } from "./workspace-files.js";
|
|
22
23
|
import { cleanupScopeWriters } from "./scope-writer-cleanup.js";
|
|
24
|
+
import { PreparedInstalledFlowSource } from "./installed-flow-source.js";
|
|
25
|
+
import { readSetupStatus } from "./setup.js";
|
|
23
26
|
import { isObservationOperation, nativeInventoryVersion, retainedDaemonOperations } from "./run-control.js";
|
|
24
27
|
/** Capture the only flow boundary which exists before `stage start --bootstrap` creates a RUN. */
|
|
25
28
|
export function prepareEvalBootstrapSnapshot(context, input) {
|
|
29
|
+
let source;
|
|
30
|
+
if (input.flow !== undefined && input.flow !== "sdlc") {
|
|
31
|
+
if (input.flow !== "mb-init")
|
|
32
|
+
throw new AppError("snapshot_flow_unsupported", "Bootstrap snapshot flow is not registered", 2, { phase: "prepare", effect: "no_effect" });
|
|
33
|
+
const home = context.env.DD_FLOW_CONFIG_HOME ?? context.ddFlowHome;
|
|
34
|
+
const selected = readSetupStatus(home);
|
|
35
|
+
if (!selected.installation)
|
|
36
|
+
throw new AppError("snapshot_flow_unavailable", "Bootstrap snapshot requires the selected installed flow", 2, { phase: "prepare", effect: "no_effect" });
|
|
37
|
+
source = PreparedInstalledFlowSource.fromInstalledRelease(home, selected.installation.manifest_sha256, input.flow);
|
|
38
|
+
}
|
|
26
39
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
27
40
|
const output = path.resolve(input.output);
|
|
28
41
|
if (fs.existsSync(output))
|
|
@@ -40,10 +53,10 @@ export function prepareEvalBootstrapSnapshot(context, input) {
|
|
|
40
53
|
throw error;
|
|
41
54
|
throw new AppError(error.code, error.message, 2, { ...error.details, phase: "prepare", effect: "no_effect", recoverable: true });
|
|
42
55
|
}
|
|
43
|
-
return { projectRoot, output };
|
|
56
|
+
return { projectRoot, output, source };
|
|
44
57
|
}
|
|
45
58
|
export function createEvalBootstrapSnapshot(context, input) {
|
|
46
|
-
const { projectRoot, output } = prepareEvalBootstrapSnapshot(context, input);
|
|
59
|
+
const { projectRoot, output, source } = prepareEvalBootstrapSnapshot(context, input);
|
|
47
60
|
const temporary = `${output}.partial-${crypto.randomUUID()}`;
|
|
48
61
|
const before = bootstrapSourceVersion(projectRoot);
|
|
49
62
|
try {
|
|
@@ -53,8 +66,9 @@ export function createEvalBootstrapSnapshot(context, input) {
|
|
|
53
66
|
if (before !== after)
|
|
54
67
|
throw new AppError("snapshot_source_changed", "Bootstrap source changed during snapshot capture", 1, { phase: "bootstrap", recoverable: true });
|
|
55
68
|
const manifest = {
|
|
56
|
-
schema_id: "dd-eval/bootstrap-snapshot@1",
|
|
57
|
-
stage: "specify",
|
|
69
|
+
schema_id: source ? "dd-eval/bootstrap-snapshot@2" : "dd-eval/bootstrap-snapshot@1",
|
|
70
|
+
stage: source?.resolveRegisteredFlow().descriptor.stages[0]?.id ?? "specify",
|
|
71
|
+
...(source ? { flow: { id: "mb-init", source_sha256: source.sha256, descriptor_sha256: source.descriptorReference().sha256 } } : {}),
|
|
58
72
|
project_root: projectRoot,
|
|
59
73
|
project_sha256: treeHash(path.join(temporary, "project")),
|
|
60
74
|
project_git: snapshotProjectGit(projectRoot, temporary)
|
|
@@ -62,6 +76,7 @@ export function createEvalBootstrapSnapshot(context, input) {
|
|
|
62
76
|
writeJson(path.join(temporary, "bootstrap.json"), manifest);
|
|
63
77
|
if (after !== bootstrapSourceVersion(projectRoot))
|
|
64
78
|
throw new AppError("snapshot_source_changed", "Bootstrap source changed while preserving Git evidence", 1, { phase: "bootstrap", recoverable: true });
|
|
79
|
+
source?.revalidate();
|
|
65
80
|
syncSnapshotTree(temporary);
|
|
66
81
|
fs.renameSync(temporary, output);
|
|
67
82
|
syncPath(path.dirname(output));
|
|
@@ -97,7 +112,7 @@ export function restoreEvalBootstrapSnapshot(_context, input, prepared = prepare
|
|
|
97
112
|
clearProjectTree(projectRoot, false);
|
|
98
113
|
restoreProjectGit(snapshot, manifest.project_git, projectRoot);
|
|
99
114
|
replaceProjectTree(projectRoot, sourceProject);
|
|
100
|
-
return { ok: true, schema_id: "dd-eval/bootstrap-restore@1", snapshot, project_root: projectRoot, target_stage:
|
|
115
|
+
return { ok: true, schema_id: "dd-eval/bootstrap-restore@1", snapshot, project_root: projectRoot, target_stage: manifest.stage, ...(manifest.flow ? { flow: manifest.flow } : {}) };
|
|
101
116
|
}
|
|
102
117
|
/**
|
|
103
118
|
* Eval fixtures require a dedicated home. Sealed recovery exports instead
|
|
@@ -204,7 +219,7 @@ export function prepareEvalRunSnapshotCreation(context, input) {
|
|
|
204
219
|
if (isInside(physicalPath(projectRoot), physicalPath(output)) || isInside(physicalPath(workspaceRoot), physicalPath(output)))
|
|
205
220
|
throw new AppError("snapshot_output_inside_project", "Snapshot output must be outside the project and RUN workspace trees", 1, { output });
|
|
206
221
|
prepareOutputFile(output);
|
|
207
|
-
prepareSnapshotProjectGit(projectRoot);
|
|
222
|
+
prepareSnapshotProjectGit(projectRoot, recovery && status.run.flow_kind === "mb-init");
|
|
208
223
|
prepareSnapshotWorkspace(workspaceRoot, projectRoot);
|
|
209
224
|
return { projectRoot, project, status, recovery, output, workspaceRoot, activeChildren };
|
|
210
225
|
}
|
|
@@ -262,7 +277,7 @@ function captureEvalRunSnapshot(context, input) {
|
|
|
262
277
|
verbatimSymlinks: true,
|
|
263
278
|
filter: projectPayloadFilter(projectRoot)
|
|
264
279
|
});
|
|
265
|
-
const projectGit = snapshotProjectGit(projectRoot, output);
|
|
280
|
+
const projectGit = snapshotProjectGit(projectRoot, output, recovery && status.run.flow_kind === "mb-init");
|
|
266
281
|
const workspace = snapshotWorkspace(workspaceRoot, projectRoot, output);
|
|
267
282
|
// Accepted Work results are captured in SQLite. Their file representations
|
|
268
283
|
// are retained evidence, not a second authority or a checkpoint gate.
|
|
@@ -276,7 +291,7 @@ function captureEvalRunSnapshot(context, input) {
|
|
|
276
291
|
}
|
|
277
292
|
}
|
|
278
293
|
const manifest = {
|
|
279
|
-
schema_id: "dd-flow/eval-run-snapshot@5",
|
|
294
|
+
schema_id: projectGit.state === "unborn" ? "dd-flow/eval-run-snapshot@6" : "dd-flow/eval-run-snapshot@5",
|
|
280
295
|
run_id: input.runId,
|
|
281
296
|
project_id: project.id,
|
|
282
297
|
project_root: projectRoot,
|
|
@@ -793,27 +808,31 @@ function assertRestoreTargetSeparation(snapshot, targets) {
|
|
|
793
808
|
function sourceWorkspaceRoot(manifest) {
|
|
794
809
|
return manifest.workspace.kind === "project" ? manifest.project_root : manifest.workspace.source_root;
|
|
795
810
|
}
|
|
796
|
-
function prepareSnapshotProjectGit(projectRoot) {
|
|
811
|
+
function prepareSnapshotProjectGit(projectRoot, allowUnborn = false) {
|
|
797
812
|
const branch = git(projectRoot, ["symbolic-ref", "--quiet", "--short", "HEAD"]);
|
|
798
|
-
const head = git(projectRoot, ["rev-parse", "HEAD"]);
|
|
799
|
-
if (!branch || !head)
|
|
800
|
-
throw new AppError("snapshot_project_git_invalid", "Snapshot requires a
|
|
813
|
+
const head = git(projectRoot, ["rev-parse", "--verify", "HEAD"]);
|
|
814
|
+
if (!branch || !head && !(allowUnborn && git(projectRoot, ["for-each-ref", "--format=%(refname)"]) === ""))
|
|
815
|
+
throw new AppError("snapshot_project_git_invalid", "Snapshot requires a named Git branch and readable HEAD, or an admitted empty-ref Memory recovery", 1, { project_root: projectRoot });
|
|
801
816
|
return { branch, head };
|
|
802
817
|
}
|
|
803
|
-
function snapshotProjectGit(projectRoot, output) {
|
|
804
|
-
const { branch, head } = prepareSnapshotProjectGit(projectRoot);
|
|
818
|
+
function snapshotProjectGit(projectRoot, output, allowUnborn = false) {
|
|
819
|
+
const { branch, head } = prepareSnapshotProjectGit(projectRoot, allowUnborn);
|
|
820
|
+
if (head === null)
|
|
821
|
+
return { branch, head: null, bundle_sha256: null, state: "unborn", ...snapshotIndex(projectRoot, output, "project") };
|
|
805
822
|
const bundle = path.join(output, "project.bundle");
|
|
806
823
|
runGit(projectRoot, ["bundle", "create", bundle, `refs/heads/${branch}`]);
|
|
807
824
|
return { branch, head, bundle_sha256: fileHash(bundle), ...snapshotIndex(projectRoot, output, "project") };
|
|
808
825
|
}
|
|
809
826
|
function restoreProjectGit(snapshot, source, projectRoot) {
|
|
810
827
|
const bundle = path.join(snapshot, "project.bundle");
|
|
811
|
-
if (!fs.existsSync(bundle) || fileHash(bundle) !== source.bundle_sha256)
|
|
828
|
+
if (source.state === "unborn" ? fs.existsSync(bundle) : !fs.existsSync(bundle) || fileHash(bundle) !== source.bundle_sha256)
|
|
812
829
|
throw new AppError("snapshot_checksum_mismatch", "Snapshot project Git bundle checksum does not match manifest", 1, { snapshot });
|
|
813
830
|
fs.rmSync(path.join(projectRoot, ".git"), { recursive: true, force: true });
|
|
814
831
|
runGit(projectRoot, ["init", "-b", source.branch]);
|
|
815
|
-
|
|
816
|
-
|
|
832
|
+
if (source.state !== "unborn") {
|
|
833
|
+
runGit(projectRoot, ["fetch", bundle, `+refs/heads/${source.branch}:refs/remotes/snapshot/${source.branch}`]);
|
|
834
|
+
runGit(projectRoot, ["checkout", "-B", source.branch, source.head]);
|
|
835
|
+
}
|
|
817
836
|
restoreIndex(snapshot, "project", source, projectRoot);
|
|
818
837
|
}
|
|
819
838
|
const mergeStateFiles = ["MERGE_HEAD", "MERGE_MSG", "MERGE_MODE", "ORIG_HEAD", "AUTO_MERGE"];
|
|
@@ -912,7 +931,9 @@ export function verifyRecoverySnapshot(snapshot, input, checkWorkingState = true
|
|
|
912
931
|
const expectedModes = manifest.payload_modes?.[entry.name];
|
|
913
932
|
if (checkWorkingState && expectedModes && treeModesHash(entry.root, allowed) !== expectedModes)
|
|
914
933
|
throw new AppError("recovery_workspace_drift", "Executable file modes changed after recovery capture", 1, { payload: entry.name });
|
|
915
|
-
if (checkWorkingState && (treeHash(entry.root, allowed) !== entry.hash || git(entry.root, ["rev-parse", "
|
|
934
|
+
if (checkWorkingState && (treeHash(entry.root, allowed) !== entry.hash || git(entry.root, ["rev-parse", "--verify", "HEAD"]) !== entry.head
|
|
935
|
+
|| entry.head === null && git(entry.root, ["for-each-ref", "--format=%(refname)"]) !== ""
|
|
936
|
+
|| git(entry.root, ["symbolic-ref", "--quiet", "--short", "HEAD"]) !== entry.git.branch || (entry.git.index_sha256 && crypto.createHash("sha256").update(entry.git.index_format === "entries@1" ? indexEntries(entry.root) : indexPatch(entry.root)).digest("hex") !== entry.git.index_sha256)))
|
|
916
937
|
throw new AppError("recovery_workspace_drift", "Working files or Git state changed after recovery capture", 1, { payload: entry.name });
|
|
917
938
|
}
|
|
918
939
|
}
|
|
@@ -935,9 +956,23 @@ function verifySnapshotPayload(snapshot, manifest) {
|
|
|
935
956
|
const payloads = [{ name: "project", hash: manifest.project_sha256, git: manifest.project_git }, ...(manifest.workspace.kind === "git_worktree" ? [{ name: "workspace", hash: manifest.workspace.sha256, git: manifest.workspace }] : [])];
|
|
936
957
|
for (const payload of payloads)
|
|
937
958
|
verifyGitSnapshotPayload(snapshot, payload);
|
|
959
|
+
if (manifest.schema_id === "dd-flow/eval-run-snapshot@6") {
|
|
960
|
+
const db = new DatabaseSync(path.join(snapshot, "runtime", "db.sqlite"), { readOnly: true });
|
|
961
|
+
try {
|
|
962
|
+
const row = db.prepare("SELECT flow_kind, project_root, workspace_root FROM runs WHERE id=?").get(manifest.run_id);
|
|
963
|
+
if (!row || row.flow_kind !== "mb-init" || row.project_root !== manifest.project_root || row.workspace_root !== manifest.project_root)
|
|
964
|
+
throw new AppError("snapshot_invalid", "Unborn recovery must belong to its captured Memory RUN", 1, { snapshot });
|
|
965
|
+
}
|
|
966
|
+
finally {
|
|
967
|
+
db.close();
|
|
968
|
+
}
|
|
969
|
+
}
|
|
938
970
|
}
|
|
939
971
|
function verifyGitSnapshotPayload(snapshot, payload) {
|
|
940
|
-
|
|
972
|
+
const bundle = path.join(snapshot, `${payload.name}.bundle`);
|
|
973
|
+
const invalidBundle = payload.git.state === "unborn" ? payload.git.bundle_sha256 !== null || fs.existsSync(bundle)
|
|
974
|
+
: !fs.existsSync(bundle) || fileHash(bundle) !== payload.git.bundle_sha256;
|
|
975
|
+
if (treeHash(path.join(snapshot, payload.name)) !== payload.hash || invalidBundle || payload.git.index_sha256 && fileHash(path.join(snapshot, `${payload.name}.index.patch`)) !== payload.git.index_sha256 || payload.git.index_format === "entries@1" && (!payload.git.index_objects_sha256 || fileHash(path.join(snapshot, `${payload.name}.index.pack`)) !== payload.git.index_objects_sha256))
|
|
941
976
|
throw new AppError("snapshot_checksum_mismatch", "Snapshot payload or Git index checksum does not match", 1, { payload: payload.name });
|
|
942
977
|
}
|
|
943
978
|
function git(root, args) {
|
|
@@ -1073,8 +1108,8 @@ function runtimeSnapshotKind(file, excluded, providerHomes = []) {
|
|
|
1073
1108
|
}
|
|
1074
1109
|
return "ordinary";
|
|
1075
1110
|
}
|
|
1076
|
-
/** Productive writers are fenced separately. Compare
|
|
1077
|
-
*
|
|
1111
|
+
/** Productive writers are fenced separately. Compare the copied source set
|
|
1112
|
+
* without idle diagnostic appends; SQLite includes committed WAL pages without
|
|
1078
1113
|
* mistaking a physical checkpoint for new logical content. */
|
|
1079
1114
|
function snapshotSourceVersion(context, input, retainedDatabases, retainedJournals) {
|
|
1080
1115
|
const databases = retainedDatabases ?? new Map();
|
|
@@ -1085,6 +1120,11 @@ function snapshotSourceVersion(context, input, retainedDatabases, retainedJourna
|
|
|
1085
1120
|
if (!run)
|
|
1086
1121
|
throw new AppError("not_found", "Snapshot RUN is not registered", 1);
|
|
1087
1122
|
const entries = new Map();
|
|
1123
|
+
const diagnosticLogs = new Set(getResourceDatabase(resourceHome(context), "read_existing").all("SELECT stdout_path,stderr_path,metadata_json FROM managed_processes WHERE project_id=? AND run_id=? AND kind IN ('codex-daemon','zcode-daemon','grok-daemon','agy-daemon','opencode-daemon','droid-daemon')", [project.id, input.runId]).flatMap(row => {
|
|
1124
|
+
const directory = JSON.parse(row.metadata_json).state_dir;
|
|
1125
|
+
const log = typeof directory === "string" && path.isAbsolute(directory) ? path.join(directory, "daemon.log") : null;
|
|
1126
|
+
return log && isInside(context.ddFlowHome, log) && row.stdout_path === log && row.stderr_path === log ? [log] : [];
|
|
1127
|
+
}));
|
|
1088
1128
|
const excluded = codexHomeSnapshotExclusions(context, project.id, input.runId);
|
|
1089
1129
|
const providerHomes = input.recoveryId === undefined ? grokProviderHomes(context.ddFlowHome) : [];
|
|
1090
1130
|
const observations = new Map();
|
|
@@ -1132,7 +1172,12 @@ function snapshotSourceVersion(context, input, retainedDatabases, retainedJourna
|
|
|
1132
1172
|
}
|
|
1133
1173
|
entries.set(key, `sqlite:${sqliteSemanticFingerprint(db)}:${stat.mode & 0o111}`);
|
|
1134
1174
|
}
|
|
1135
|
-
else if (stat.isFile() &&
|
|
1175
|
+
else if (stat.isFile() && diagnosticLogs.has(file)) {
|
|
1176
|
+
// Daemon stdout/stderr can append while an otherwise settled Session is idle.
|
|
1177
|
+
// Keep its identity/mode fenced; the copied bytes remain sealed in the snapshot.
|
|
1178
|
+
entries.set(key, `diagnostic-log:${stat.dev}:${stat.ino}:${stat.mode & 0o111}`);
|
|
1179
|
+
}
|
|
1180
|
+
else if (stat.isFile() && (["adapter.events.jsonl", "events.jsonl", "model-observations.jsonl", "timeline.jsonl", "controller.log"].includes(path.basename(file)) || native && file === path.join(directory, "daemon.log"))) {
|
|
1136
1181
|
const bytes = fs.readFileSync(file);
|
|
1137
1182
|
let cut = journals.get(file);
|
|
1138
1183
|
if (!cut && !retainedJournals) {
|
|
@@ -1409,11 +1454,20 @@ function readManifest(snapshot) {
|
|
|
1409
1454
|
if (!fs.existsSync(file))
|
|
1410
1455
|
throw new AppError("snapshot_missing", "Snapshot manifest is missing", 1, { snapshot });
|
|
1411
1456
|
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
1412
|
-
if (
|
|
1457
|
+
if (!["dd-flow/eval-run-snapshot@5", "dd-flow/eval-run-snapshot@6"].includes(String(value.schema_id)) || !value.run_id || !value.project_root || !value.dd_flow_home || typeof value.purpose !== "string" || !["stage_entry", "candidate", "incomplete", "recovery"].includes(value.purpose) || (value.purpose === "stage_entry" && !value.stage_entry) || (value.purpose !== "stage_entry" && value.stage_entry !== null) || !value.runtime_sha256 || !value.project_sha256 || !value.project_git || !value.workspace)
|
|
1413
1458
|
throw new AppError("snapshot_invalid", "Snapshot manifest is invalid", 1, { snapshot });
|
|
1414
1459
|
if (value.runtime_payload_policy !== undefined && (value.runtime_payload_policy !== "flow-evidence@1" || value.purpose === "recovery"))
|
|
1415
1460
|
throw new AppError("snapshot_invalid", "Snapshot runtime payload policy is invalid for its purpose", 1, { snapshot });
|
|
1416
|
-
|
|
1461
|
+
const unborn = value.schema_id === "dd-flow/eval-run-snapshot@6";
|
|
1462
|
+
if (!value.project_git.branch || (unborn
|
|
1463
|
+
? value.purpose !== "recovery" || value.workspace.kind !== "project" || value.project_git.state !== "unborn"
|
|
1464
|
+
|| value.project_git.head !== null || value.project_git.bundle_sha256 !== null
|
|
1465
|
+
|| value.project_git.index_format !== "entries@1" || !/^[a-f0-9]{64}$/.test(value.project_git.index_sha256 ?? "")
|
|
1466
|
+
|| !/^[a-f0-9]{64}$/.test(value.project_git.index_objects_sha256 ?? "")
|
|
1467
|
+
|| Object.keys(value.project_git).sort().join() !== "branch,bundle_sha256,head,index_format,index_objects_sha256,index_sha256,state"
|
|
1468
|
+
|| typeof value.project_git.branch !== "string"
|
|
1469
|
+
|| spawnSync("git", ["check-ref-format", "--branch", value.project_git.branch], { encoding: "utf8" }).status !== 0
|
|
1470
|
+
: value.project_git.state !== undefined || !value.project_git.head || !value.project_git.bundle_sha256))
|
|
1417
1471
|
throw new AppError("snapshot_invalid", "Snapshot project Git metadata is incomplete", 1, { snapshot });
|
|
1418
1472
|
if (value.workspace.kind === "git_worktree" && (!value.workspace.branch || !value.workspace.base_ref || !value.workspace.bundle_sha256))
|
|
1419
1473
|
throw new AppError("snapshot_invalid", "Snapshot worktree manifest is incomplete", 1, { snapshot });
|
|
@@ -1424,7 +1478,11 @@ function readBootstrapManifest(snapshot) {
|
|
|
1424
1478
|
if (!fs.existsSync(file))
|
|
1425
1479
|
throw new AppError("snapshot_missing", "Bootstrap snapshot manifest is missing", 1, { snapshot });
|
|
1426
1480
|
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
1427
|
-
|
|
1481
|
+
const validBoundary = value.schema_id === "dd-eval/bootstrap-snapshot@1" ? value.stage === "specify" && value.flow === undefined
|
|
1482
|
+
: value.schema_id === "dd-eval/bootstrap-snapshot@2" && value.flow?.id === "mb-init" && value.stage === "preflight"
|
|
1483
|
+
&& Object.keys(value.flow).sort().join() === "descriptor_sha256,id,source_sha256"
|
|
1484
|
+
&& /^[a-f0-9]{64}$/.test(value.flow.source_sha256) && /^[a-f0-9]{64}$/.test(value.flow.descriptor_sha256);
|
|
1485
|
+
if (!validBoundary || !value.project_root || !value.project_sha256 || !value.project_git?.branch || !value.project_git.head || !value.project_git.bundle_sha256)
|
|
1428
1486
|
throw new AppError("snapshot_invalid", "Bootstrap snapshot manifest is invalid", 1, { snapshot });
|
|
1429
1487
|
return value;
|
|
1430
1488
|
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
5
|
+
import { AppError } from "../shared/errors.js";
|
|
6
|
+
import { ResolvedFlowDescriptor } from "./flow-descriptors.js";
|
|
7
|
+
import { AdmittedFrozenEngine } from "./frozen-engine.js";
|
|
8
|
+
import { FrozenGenericExecution } from "./generic-execution.js";
|
|
9
|
+
import { hybridDigest, validateHybridContract, validateHybridShape } from "./hybrid-contracts.js";
|
|
10
|
+
import { readBoundedReleaseFile } from "./release-source.js";
|
|
11
|
+
import { acquireInstallationLock } from "./installation-lock.js";
|
|
12
|
+
const hash = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex");
|
|
13
|
+
const limit = 1024 * 1024;
|
|
14
|
+
function refuse(reason, issues) {
|
|
15
|
+
throw new AppError("execution_bundle_invalid", "Execution bundle cannot be admitted", 1, { reason, phase: "prepare", effect: "no_effect", ...(issues ? { issues } : {}) });
|
|
16
|
+
}
|
|
17
|
+
function json(bytes) {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return refuse("invalid_utf8_json");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function frozen(value) {
|
|
26
|
+
if (value !== null && typeof value === "object") {
|
|
27
|
+
for (const child of Object.values(value))
|
|
28
|
+
frozen(child);
|
|
29
|
+
Object.freeze(value);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function directory(file) {
|
|
33
|
+
if (!fs.lstatSync(file).isDirectory())
|
|
34
|
+
refuse("non_regular_bundle_directory");
|
|
35
|
+
}
|
|
36
|
+
function fileIn(root, logical, create = false) {
|
|
37
|
+
const parts = logical.split("/");
|
|
38
|
+
let current = root;
|
|
39
|
+
for (const part of parts.slice(0, -1)) {
|
|
40
|
+
current = path.join(current, part);
|
|
41
|
+
if (create && !fs.existsSync(current))
|
|
42
|
+
fs.mkdirSync(current, { mode: 0o700 });
|
|
43
|
+
directory(current);
|
|
44
|
+
}
|
|
45
|
+
return path.join(current, parts.at(-1));
|
|
46
|
+
}
|
|
47
|
+
/** Retained contract data only. RUN generation, physical ownership and writer admission remain separate. */
|
|
48
|
+
export class RetainedExecutionBundle {
|
|
49
|
+
sha256;
|
|
50
|
+
manifest;
|
|
51
|
+
descriptor;
|
|
52
|
+
execution;
|
|
53
|
+
#bytes;
|
|
54
|
+
#blobs;
|
|
55
|
+
#admission;
|
|
56
|
+
#engine;
|
|
57
|
+
constructor(bytes, manifest, blobs, admission, descriptor, execution) {
|
|
58
|
+
frozen(manifest);
|
|
59
|
+
this.manifest = manifest;
|
|
60
|
+
this.sha256 = hash(bytes);
|
|
61
|
+
this.#bytes = bytes;
|
|
62
|
+
this.#blobs = blobs;
|
|
63
|
+
this.#admission = admission;
|
|
64
|
+
this.descriptor = descriptor;
|
|
65
|
+
this.execution = execution;
|
|
66
|
+
Object.freeze(this);
|
|
67
|
+
}
|
|
68
|
+
static admit(input, expectedSha256, sourceBlobs, selected) {
|
|
69
|
+
if (input.byteLength > limit || !/^[a-f0-9]{64}$/.test(expectedSha256))
|
|
70
|
+
refuse("invalid_bundle_identity");
|
|
71
|
+
const bytes = Buffer.from(input);
|
|
72
|
+
if (hash(bytes) !== expectedSha256)
|
|
73
|
+
refuse("manifest_bytes_changed");
|
|
74
|
+
const value = json(bytes), shape = validateHybridShape("execution-bundle", value);
|
|
75
|
+
if (shape.length)
|
|
76
|
+
refuse("invalid_bundle_shape", shape);
|
|
77
|
+
const manifest = value;
|
|
78
|
+
if (manifest.run_id !== selected.runId || selected.executionScope.projectRoot !== selected.bundleProjectRoot)
|
|
79
|
+
refuse("foreign_run_identity");
|
|
80
|
+
const admission = structuredClone(selected);
|
|
81
|
+
const names = manifest.inventory.map(ref => ref.path), folded = new Set();
|
|
82
|
+
for (const name of names) {
|
|
83
|
+
const normalized = name.toLowerCase();
|
|
84
|
+
if (folded.has(normalized) || ["manifest.json", "publication.json"].includes(normalized)
|
|
85
|
+
|| names.some(other => other !== name && other.toLowerCase().startsWith(`${normalized}/`)))
|
|
86
|
+
refuse("conflicting_bundle_path");
|
|
87
|
+
folded.add(normalized);
|
|
88
|
+
}
|
|
89
|
+
const blobs = new Map();
|
|
90
|
+
let total = 0;
|
|
91
|
+
for (const reference of manifest.inventory) {
|
|
92
|
+
const source = sourceBlobs.get(reference.path);
|
|
93
|
+
if (!source || source.byteLength === 0 || source.byteLength > limit || (total += source.byteLength) > 32 * limit)
|
|
94
|
+
refuse("missing_or_unbounded_blob");
|
|
95
|
+
blobs.set(reference.path, Buffer.from(source));
|
|
96
|
+
}
|
|
97
|
+
const context = { ...admission, blobs };
|
|
98
|
+
const issues = validateHybridContract("execution-bundle", manifest, context);
|
|
99
|
+
if (issues.length)
|
|
100
|
+
refuse("invalid_bundle_context", issues);
|
|
101
|
+
if (!admission.descriptorContext)
|
|
102
|
+
refuse("missing_handler_context");
|
|
103
|
+
const descriptor = ResolvedFlowDescriptor.admit(blobs.get(manifest.descriptor.path), manifest.descriptor.sha256, { ...admission.descriptorContext, blobs });
|
|
104
|
+
for (const stage of descriptor.descriptor.stages) {
|
|
105
|
+
try {
|
|
106
|
+
new TextDecoder("utf-8", { fatal: true }).decode(descriptor.stageBlob(stage.id, "instruction"));
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
refuse("invalid_instruction_encoding");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const profiles = json(blobs.get(manifest.profile_settings.path));
|
|
113
|
+
const snapshot = { schema_id: "dd-flow/frozen-execution@1", scope: admission.executionScope,
|
|
114
|
+
settings: json(blobs.get(manifest.execution_settings.path)), profiles: profiles.profiles };
|
|
115
|
+
const execution = FrozenGenericExecution.restore(snapshot, hybridDigest(snapshot), descriptor, admission.executionScope, admission.receipts);
|
|
116
|
+
const admitted = new RetainedExecutionBundle(bytes, manifest, blobs, admission, descriptor, execution);
|
|
117
|
+
admitted.engine();
|
|
118
|
+
return admitted;
|
|
119
|
+
}
|
|
120
|
+
/** Read only the retained inventory. Mutable canon/profile sources are never consulted. */
|
|
121
|
+
static read(root, expectedSha256, admission) {
|
|
122
|
+
try {
|
|
123
|
+
directory(root);
|
|
124
|
+
const bytes = readBoundedReleaseFile(path.join(root, "manifest.json"), limit);
|
|
125
|
+
if (hash(bytes) !== expectedSha256)
|
|
126
|
+
refuse("manifest_bytes_changed");
|
|
127
|
+
const value = json(bytes), issues = validateHybridShape("execution-bundle", value);
|
|
128
|
+
if (issues.length)
|
|
129
|
+
refuse("invalid_bundle_shape", issues);
|
|
130
|
+
const manifest = value, blobs = new Map();
|
|
131
|
+
for (const ref of manifest.inventory)
|
|
132
|
+
blobs.set(ref.path, readBoundedReleaseFile(fileIn(root, ref.path), limit));
|
|
133
|
+
return RetainedExecutionBundle.admit(bytes, expectedSha256, blobs, admission);
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
if (error instanceof AppError && ["execution_bundle_invalid", "frozen_engine_invalid"].includes(error.code))
|
|
137
|
+
throw error;
|
|
138
|
+
return refuse("retained_bundle_unavailable");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
manifestBytes() { return Buffer.from(this.#bytes); }
|
|
142
|
+
blob(logical) {
|
|
143
|
+
const bytes = this.#blobs.get(logical);
|
|
144
|
+
if (!bytes)
|
|
145
|
+
refuse("unlisted_bundle_blob");
|
|
146
|
+
return Buffer.from(bytes);
|
|
147
|
+
}
|
|
148
|
+
admission() { return structuredClone(this.#admission); }
|
|
149
|
+
engine() {
|
|
150
|
+
// An issued bundle already admitted this exact immutable engine. Fresh
|
|
151
|
+
// read/admit creates a new owner; publication explicitly rechecks below.
|
|
152
|
+
return this.#engine ??= this.#admitEngine();
|
|
153
|
+
}
|
|
154
|
+
#admitEngine() {
|
|
155
|
+
const binding = json(this.blob(this.manifest.engine_binding.path));
|
|
156
|
+
const engine = AdmittedFrozenEngine.admit(binding, this.manifest.engine_manifest.sha256);
|
|
157
|
+
if (!engine.manifestBytes().equals(this.blob(this.manifest.engine_manifest.path)))
|
|
158
|
+
refuse("engine_manifest_bytes_changed");
|
|
159
|
+
const manifest = engine.manifest(), contracts = new Set(Object.values(manifest.supports_contracts).flat());
|
|
160
|
+
for (const id of ["dd-flow/flow-descriptor@1", "dd-flow/execution-settings@1", "dd-flow/execution-bundle@1"]) {
|
|
161
|
+
if (!contracts.has(id))
|
|
162
|
+
refuse("engine_contract_not_supported");
|
|
163
|
+
}
|
|
164
|
+
for (const reference of this.manifest.bootstrap_receipts) {
|
|
165
|
+
const receipt = json(this.blob(reference.path));
|
|
166
|
+
if (receipt.schema_id !== "dd-flow/memory-bootstrap-receipt@2")
|
|
167
|
+
continue;
|
|
168
|
+
if (!contracts.has(receipt.schema_id))
|
|
169
|
+
refuse("engine_contract_not_supported");
|
|
170
|
+
const entries = manifest.schema_registry.entries.filter(entry => entry.id === receipt.schema_id);
|
|
171
|
+
const entry = entries[0];
|
|
172
|
+
if (entries.length !== 1 || !entry)
|
|
173
|
+
refuse("unregistered_bootstrap_schema");
|
|
174
|
+
try {
|
|
175
|
+
const validate = new Ajv2020({ strict: true, validateFormats: false }).compile(json(engine.schema(entry.name)));
|
|
176
|
+
if (!validate(receipt))
|
|
177
|
+
refuse("invalid_frozen_bootstrap_shape", validate.errors);
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
if (error instanceof AppError)
|
|
181
|
+
throw error;
|
|
182
|
+
refuse("invalid_frozen_bootstrap_schema");
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
for (const stage of this.descriptor.descriptor.stages) {
|
|
186
|
+
const bytes = this.descriptor.stageBlob(stage.id, "result_schema"), schema = json(bytes);
|
|
187
|
+
if (!manifest.schema_registry.entries.some(entry => entry.id === schema.$id && entry.checksum === hash(bytes)))
|
|
188
|
+
refuse("unregistered_stage_schema");
|
|
189
|
+
}
|
|
190
|
+
return engine;
|
|
191
|
+
}
|
|
192
|
+
/** Deterministic publication under the RUN's existing root and immutable engine binding. */
|
|
193
|
+
materialize(runRoot) {
|
|
194
|
+
this.#admitEngine();
|
|
195
|
+
directory(runRoot);
|
|
196
|
+
if (hash(readBoundedReleaseFile(path.join(runRoot, "engine-binding.json"), limit)) !== this.manifest.engine_binding.sha256)
|
|
197
|
+
refuse("run_engine_binding_changed");
|
|
198
|
+
const target = path.join(runRoot, "execution-bundle"), pending = path.join(runRoot, `.execution-bundle.pending-${this.sha256}`);
|
|
199
|
+
const lock = acquireInstallationLock(path.join(runRoot, "execution-bundle.lock"), this.sha256);
|
|
200
|
+
let publicationStarted = false;
|
|
201
|
+
try {
|
|
202
|
+
if (fs.existsSync(target)) {
|
|
203
|
+
RetainedExecutionBundle.read(target, this.sha256, this.#admission);
|
|
204
|
+
return { path: target, sha256: this.sha256, changed: false };
|
|
205
|
+
}
|
|
206
|
+
const owner = Buffer.from(JSON.stringify({ schema_id: "dd-flow/bundle-publication@1", sha256: this.sha256, run_id: this.manifest.run_id }));
|
|
207
|
+
publicationStarted = true;
|
|
208
|
+
if (!fs.existsSync(pending)) {
|
|
209
|
+
fs.mkdirSync(pending, { mode: 0o700 });
|
|
210
|
+
fs.writeFileSync(path.join(pending, "publication.json"), owner, { flag: "wx", mode: 0o600 });
|
|
211
|
+
}
|
|
212
|
+
directory(pending);
|
|
213
|
+
if (!readBoundedReleaseFile(path.join(pending, "publication.json"), limit).equals(owner))
|
|
214
|
+
refuse("foreign_pending_publication");
|
|
215
|
+
const write = (logical, bytes) => {
|
|
216
|
+
const file = fileIn(pending, logical, true);
|
|
217
|
+
if (fs.existsSync(file)) {
|
|
218
|
+
if (!readBoundedReleaseFile(file, limit).equals(bytes))
|
|
219
|
+
refuse("retained_blob_changed");
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const temporary = path.join(pending, `.write-${hash(Buffer.from(logical))}.tmp`);
|
|
223
|
+
if (fs.existsSync(temporary)) {
|
|
224
|
+
const stat = fs.lstatSync(temporary);
|
|
225
|
+
if (!stat.isFile() || stat.nlink !== 1)
|
|
226
|
+
refuse("foreign_pending_write");
|
|
227
|
+
fs.unlinkSync(temporary);
|
|
228
|
+
}
|
|
229
|
+
// This temporary is owned by the exact publication receipt and lock.
|
|
230
|
+
// A crashed partial write is deterministic; no productive operation is retried.
|
|
231
|
+
const fd = fs.openSync(temporary, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | (fs.constants.O_NOFOLLOW ?? 0), 0o600);
|
|
232
|
+
try {
|
|
233
|
+
fs.writeFileSync(fd, bytes);
|
|
234
|
+
fs.fsyncSync(fd);
|
|
235
|
+
}
|
|
236
|
+
finally {
|
|
237
|
+
fs.closeSync(fd);
|
|
238
|
+
}
|
|
239
|
+
fs.renameSync(temporary, file);
|
|
240
|
+
};
|
|
241
|
+
for (const [logical, bytes] of this.#blobs)
|
|
242
|
+
write(logical, bytes);
|
|
243
|
+
write("manifest.json", this.#bytes);
|
|
244
|
+
RetainedExecutionBundle.read(pending, this.sha256, this.#admission);
|
|
245
|
+
if (fs.existsSync(target))
|
|
246
|
+
refuse("concurrent_bundle_publication");
|
|
247
|
+
fs.renameSync(pending, target);
|
|
248
|
+
return { path: target, sha256: this.sha256, changed: true };
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
if (!publicationStarted)
|
|
252
|
+
throw error;
|
|
253
|
+
throw new AppError("execution_bundle_publication_failed", "Execution bundle publication requires recovery", 1, {
|
|
254
|
+
phase: "publication", effect: "partial", run_id: this.manifest.run_id,
|
|
255
|
+
sha256: this.sha256, pending_path: pending,
|
|
256
|
+
reason: error instanceof AppError ? error.code : "publication_io_failure",
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
finally {
|
|
260
|
+
lock.release();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|