@deksden-com/dd-flow-cli 0.9.0-beta.144 → 0.9.0-beta.149
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 +130 -0
- package/README.md +13 -8
- package/dist/build-info.json +7 -7
- package/dist/cli/command-inputs.js +13 -3
- package/dist/cli/command-metadata.js +51 -16
- package/dist/cli/diagnostic-invocation.js +46 -0
- package/dist/cli/diagnostic.js +120 -0
- package/dist/cli/help.js +67 -1
- package/dist/cli/hook-ingress.js +27 -3
- package/dist/cli/memory-flow-admission.js +9 -3
- package/dist/cli/memory-init-invocation.js +10 -3
- package/dist/cli/memory-init.js +77 -68
- package/dist/cli/memory-storage-admission.js +3 -2
- package/dist/cli/memory-storage-qualification.js +105 -0
- package/dist/cli/memory-storage-transition.js +123 -0
- package/dist/cli/memory-upgrade.js +189 -0
- package/dist/cli/native-hook-ingress.js +14 -4
- package/dist/cli/native-hook-preparation.js +55 -0
- package/dist/cli/response-file.js +2 -1
- package/dist/cli/run-cli.js +79 -40
- package/dist/cli/setup.js +2 -1
- package/dist/cli.js +18 -0
- package/dist/domain/diagnostic-aspects.js +103 -0
- package/dist/domain/diagnostic-flows.js +8 -0
- package/dist/domain/stage-handler-identities.js +8 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +3 -3
- package/dist/harness-runtime/bin/dd-grok.mjs +3 -3
- package/dist/harness-runtime/bin/dd-zcode.mjs +5 -5
- package/dist/harness-runtime/lib/agy-error-boundary.mjs +58 -0
- package/dist/harness-runtime/lib/agy-native-inspection.mjs +216 -0
- package/dist/harness-runtime/lib/agy-native_pb.js +117 -0
- package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +84 -10
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +366 -52
- package/dist/harness-runtime/lib/dd-agy.mjs +17 -3
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +27 -13
- package/dist/harness-runtime/lib/dd-codex.mjs +4 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +4 -3
- package/dist/harness-runtime/lib/dd-droid.mjs +71 -29
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +73 -16
- package/dist/harness-runtime/lib/dd-grok.mjs +6 -3
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +66 -20
- package/dist/harness-runtime/lib/dd-opencode.mjs +30 -3
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +7 -6
- package/dist/harness-runtime/lib/dd-zcode.mjs +44 -14
- package/dist/harness-runtime/lib/delegation-instructions.mjs +1 -1
- package/dist/harness-runtime/lib/driver-recovery.mjs +9 -8
- package/dist/harness-runtime/lib/entity-references.d.mts +2 -0
- package/dist/harness-runtime/lib/entity-references.mjs +9 -0
- package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -1
- package/dist/harness-runtime/lib/managed-daemon.mjs +24 -9
- package/dist/harness-runtime/lib/native-children.d.mts +8 -1
- package/dist/harness-runtime/lib/native-children.mjs +114 -8
- package/dist/harness-runtime/lib/native-hook-command.mjs +4 -4
- package/dist/harness-runtime/lib/native-request-proof.mjs +1 -1
- package/dist/harness-runtime/lib/observation-clock.d.mts +2 -2
- package/dist/harness-runtime/lib/observation-clock.mjs +7 -4
- package/dist/harness-runtime/lib/opencode-plugin.mjs +3 -3
- package/dist/harness-runtime/lib/operation-errors.mjs +1 -1
- package/dist/harness-runtime/lib/provider-overload-policy.d.mts +5 -3
- package/dist/harness-runtime/lib/provider-overload-policy.mjs +20 -7
- package/dist/harness-runtime/lib/runner-events.mjs +3 -1
- package/dist/harness-runtime/lib/setup-progress.mjs +8 -6
- package/dist/harness-runtime/lib/tool-observations.mjs +1 -1
- package/dist/schemas/diagnostic-stage-result.schema.json +354 -0
- package/dist/schemas/diagnostic-work-result.schema.json +296 -0
- package/dist/schemas/execution-bundle.schema.json +7 -1
- package/dist/schemas/mb-upgrade-stage-result.schema.json +863 -0
- package/dist/schemas/memory-repair-work-result.schema.json +9 -2
- package/dist/schemas/project-flow-pack-manifest.schema.json +1 -1
- package/dist/schemas/release-impact.schema.json +3 -3
- package/dist/schemas/stage-start-response.schema.json +4 -0
- package/dist/services/accepted-intent.js +13 -1
- package/dist/services/canon.js +43 -7
- package/dist/services/check-environment.js +21 -0
- package/dist/services/check-runner-evidence.js +267 -15
- package/dist/services/cli-operation-classifier.js +1 -1
- package/dist/services/cli-upgrade-allowlist.js +1 -1
- package/dist/services/code-checks.js +34 -19
- package/dist/services/compatibility-preflight.js +5 -2
- package/dist/services/controller-execution.js +10 -4
- package/dist/services/controller-fanout.js +34 -10
- package/dist/services/controller-memory-task.js +45 -0
- package/dist/services/controller-memory-worker.js +49 -0
- package/dist/services/dashboard.js +2 -2
- package/dist/services/descriptor-run-settlement.js +5 -3
- package/dist/services/descriptor-run-start.js +46 -20
- package/dist/services/descriptor-stage-coordinator.js +66 -13
- package/dist/services/descriptor-stage-settlement.js +2 -1
- package/dist/services/descriptor-stage-start.js +40 -4
- package/dist/services/diagnostic-evidence.js +73 -0
- package/dist/services/diagnostic-report-publication.js +72 -0
- package/dist/services/diagnostic-report.js +67 -0
- package/dist/services/diagnostic-run-bundle.js +74 -0
- package/dist/services/diagnostic-snapshot.js +159 -0
- package/dist/services/diagnostic-stage-handlers.js +108 -0
- package/dist/services/diagnostic-stage-results.js +132 -0
- package/dist/services/diagnostic-start.js +84 -0
- package/dist/services/diagnostic-work-batch.js +34 -0
- package/dist/services/diagnostic-work-collection.js +97 -0
- package/dist/services/diagnostic-work-registration.js +101 -0
- package/dist/services/diagnostic-work-results.js +93 -0
- package/dist/services/engines.js +64 -14
- package/dist/services/eval-snapshots.js +186 -119
- package/dist/services/execution-bundles.js +19 -7
- package/dist/services/external-work-launch.js +5 -3
- package/dist/services/flow-catalog.js +13 -4
- package/dist/services/frozen-engine.js +148 -11
- package/dist/services/hook-memory-routing.js +188 -0
- package/dist/services/hook-recovery-guard.js +14 -0
- package/dist/services/hook-session-observation.js +24 -0
- package/dist/services/hooks.js +27 -8
- package/dist/services/hybrid-contracts.js +4 -13
- package/dist/services/hybrid-digest.js +14 -0
- package/dist/services/installed-flow-source.js +47 -4
- package/dist/services/lifecycle-invocations.js +179 -38
- package/dist/services/memory-accepted-composition.js +5 -4
- package/dist/services/memory-completed-correction-wave.js +23 -15
- package/dist/services/memory-compose-handler.js +163 -21
- package/dist/services/memory-composition-acceptance.js +186 -10
- package/dist/services/memory-composition-graph.js +42 -11
- package/dist/services/memory-composition-registration.js +23 -8
- package/dist/services/memory-composition-work-custody.js +14 -9
- package/dist/services/memory-composition-work-results.js +15 -3
- package/dist/services/memory-corrected-review-batch.js +10 -3
- package/dist/services/memory-corrected-review-findings.js +4 -2
- package/dist/services/memory-corrected-review-inputs.js +10 -5
- package/dist/services/memory-corrected-review-receipt.js +17 -9
- package/dist/services/memory-corrected-review-registration.js +3 -2
- package/dist/services/memory-correction-check-receipt.js +20 -4
- package/dist/services/memory-custody-snapshot.js +36 -0
- package/dist/services/memory-finish-evidence.js +44 -22
- package/dist/services/memory-finish-handler.js +37 -23
- package/dist/services/memory-finish-integration.js +218 -48
- package/dist/services/memory-finish-readiness.js +66 -19
- package/dist/services/memory-git-integration.js +87 -18
- package/dist/services/memory-historical-corrected-review.js +21 -15
- package/dist/services/memory-historical-original-review.js +22 -9
- package/dist/services/memory-historical-repair-registration.js +14 -8
- package/dist/services/memory-historical-second-review.js +30 -11
- package/dist/services/memory-init-contexts.js +44 -2
- package/dist/services/memory-init-controller-start.js +27 -8
- package/dist/services/memory-init-git-policy.js +50 -21
- package/dist/services/memory-init-lint-capability.js +24 -6
- package/dist/services/memory-init-native-capacity.js +17 -4
- package/dist/services/memory-init-preflight.js +21 -17
- package/dist/services/memory-init-request-candidate.js +73 -17
- package/dist/services/memory-init-request-input.js +23 -7
- package/dist/services/memory-init-run-bundle.js +49 -12
- package/dist/services/memory-init-storage.js +17 -7
- package/dist/services/memory-init-worktrunk-capability.js +3 -7
- package/dist/services/memory-layer.js +71 -6
- package/dist/services/memory-lint-config.js +111 -2
- package/dist/services/memory-native-review-copy.js +5 -67
- package/dist/services/memory-normal-flow-entry.js +12 -0
- package/dist/services/memory-ordinary-defaults.js +22 -5
- package/dist/services/memory-preflight-handler.js +15 -0
- package/dist/services/memory-repair-current-view.js +1 -1
- package/dist/services/memory-repair-document-delta.js +70 -33
- package/dist/services/memory-repair-history.js +12 -7
- package/dist/services/memory-repair-physical-binding.js +1 -119
- package/dist/services/memory-repair-publication-plan.js +5 -3
- package/dist/services/memory-repair-publication-source.js +1 -1
- package/dist/services/memory-repair-publication.js +3 -2
- package/dist/services/memory-repair-registration.js +2 -1
- package/dist/services/memory-repair-work-baseline.js +4 -2
- package/dist/services/memory-repair-work-completion.js +32 -12
- package/dist/services/memory-repair-work-results.js +21 -2
- package/dist/services/memory-repair-workspace.js +11 -2
- package/dist/services/memory-repair-writer-copy.js +2 -1
- package/dist/services/memory-result-binding.js +78 -12
- package/dist/services/memory-retained-review-authority.js +17 -8
- package/dist/services/memory-retained-second-baseline.js +1 -1
- package/dist/services/memory-retained-second-repairs.js +12 -6
- package/dist/services/memory-review-correction-plan.js +47 -12
- package/dist/services/memory-review-handler.js +17 -17
- package/dist/services/memory-review-inputs.js +55 -9
- package/dist/services/memory-review-limits.js +10 -0
- package/dist/services/memory-review-receipt.js +2 -2
- package/dist/services/memory-review-registration.js +13 -4
- package/dist/services/memory-review-work-custody.js +3 -1
- package/dist/services/memory-review-work-input.js +26 -7
- package/dist/services/memory-review-work-results.js +26 -4
- package/dist/services/memory-second-corrected-review-inputs.js +5 -3
- package/dist/services/memory-second-correction-plan.js +2 -1
- package/dist/services/memory-second-repair-launch.js +4 -4
- package/dist/services/memory-second-repair-source.js +1 -1
- package/dist/services/memory-stage-dispatch.js +38 -1
- package/dist/services/memory-stage-results.js +113 -22
- package/dist/services/memory-tool-probe.js +25 -0
- package/dist/services/memory-upgrade-analysis-handler.js +243 -0
- package/dist/services/memory-upgrade-bootstrap-readiness.js +251 -0
- package/dist/services/memory-upgrade-evidence.js +100 -0
- package/dist/services/memory-upgrade-inventory.js +442 -0
- package/dist/services/memory-upgrade-publication-custody.js +73 -0
- package/dist/services/memory-upgrade-publication.js +413 -0
- package/dist/services/memory-upgrade-selection.js +430 -0
- package/dist/services/memory-upgrade-start.js +60 -0
- package/dist/services/memory-upgrade-storage-readiness.js +340 -0
- package/dist/services/memory-verification-acceptance.js +38 -17
- package/dist/services/memory-verification-checks.js +92 -27
- package/dist/services/memory-verification-receipt.js +6 -5
- package/dist/services/memory-verify-handler.js +15 -13
- package/dist/services/memory-work-result-json.js +12 -0
- package/dist/services/memory-work-result-preparation.js +5 -4
- package/dist/services/merge-server.js +4 -2
- package/dist/services/migrations.js +127 -98
- package/dist/services/native-daemon-history.js +2 -1
- package/dist/services/physical-startup.js +45 -0
- package/dist/services/project-registry.js +97 -0
- package/dist/services/project-summary.js +1 -1
- package/dist/services/projects.js +3 -93
- package/dist/services/prompt-delivery.js +71 -0
- package/dist/services/provider-capacity.js +112 -46
- package/dist/services/recovery-snapshot-database.js +32 -4
- package/dist/services/release-cache.js +8 -0
- package/dist/services/repair-intents.js +1 -12
- package/dist/services/repair-receipt.js +13 -0
- package/dist/services/run-control-worker.js +35 -18
- package/dist/services/run-control.js +2 -1
- package/dist/services/run-controller-adapter.js +2 -1
- package/dist/services/run-controller-process.js +4 -7
- package/dist/services/run-controller-recovery.js +8 -2
- package/dist/services/run-controller.js +215 -94
- package/dist/services/run-fork.js +1 -1
- package/dist/services/run-recovery-guard.js +30 -0
- package/dist/services/run-recovery.js +11 -44
- package/dist/services/run-start-custody.js +17 -5
- package/dist/services/runs.js +6 -1
- package/dist/services/runtime-scope-resume.js +12 -6
- package/dist/services/runtime-scope-stop.js +3 -2
- package/dist/services/runtime-scope-worker.js +46 -19
- package/dist/services/runtime-service.js +2 -1
- package/dist/services/sessions.js +3 -24
- package/dist/services/setup.js +63 -13
- package/dist/services/stage-handlers.js +22 -7
- package/dist/services/stage-pause.js +111 -44
- package/dist/services/version-status.js +1 -1
- package/dist/services/vnext-code.js +2 -2
- package/dist/services/vnext-fanout.js +51 -9
- package/dist/services/vnext-plan-review.js +3 -5
- package/dist/services/work-registry.js +117 -39
- package/dist/services/workspace-bootstrap.js +2 -1
- package/dist/services/workspace-files.js +1 -1
- package/dist/shared/entity-references.js +1 -8
- package/dist/storage/data-preservation.js +65 -0
- package/dist/storage/database.js +53 -19
- package/dist/storage/import-guard.js +84 -27
- package/dist/storage/memory-upgrade-schema-bootstrap.js +38 -0
- package/dist/storage/paired-writer-migration.js +424 -0
- package/dist/storage/writer-contract.js +11 -4
- package/dist/storage/writer-migration.js +313 -52
- package/dist/storage/writer-restore.js +217 -0
- package/package.json +13 -6
|
@@ -23,18 +23,21 @@ import { localEnvironmentAllowed, projectServicePathFilter } from "./workspace-f
|
|
|
23
23
|
import { cleanupScopeWriters } from "./scope-writer-cleanup.js";
|
|
24
24
|
import { PreparedInstalledFlowSource } from "./installed-flow-source.js";
|
|
25
25
|
import { readSetupStatus } from "./setup.js";
|
|
26
|
+
import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
|
|
26
27
|
import { isObservationOperation, nativeInventoryVersion, retainedDaemonOperations } from "./run-control.js";
|
|
27
28
|
/** Capture the only flow boundary which exists before `stage start --bootstrap` creates a RUN. */
|
|
28
29
|
export function prepareEvalBootstrapSnapshot(context, input) {
|
|
29
30
|
let source;
|
|
30
31
|
if (input.flow !== undefined && input.flow !== "sdlc") {
|
|
31
|
-
if (input.flow !== "mb-init")
|
|
32
|
+
if (input.flow !== "mb-init" && input.flow !== "mb-upgrade" && !isDiagnosticFlow(input.flow))
|
|
32
33
|
throw new AppError("snapshot_flow_unsupported", "Bootstrap snapshot flow is not registered", 2, { phase: "prepare", effect: "no_effect" });
|
|
33
34
|
const home = context.env.DD_FLOW_CONFIG_HOME ?? context.ddFlowHome;
|
|
34
35
|
const selected = readSetupStatus(home);
|
|
35
36
|
if (!selected.installation)
|
|
36
37
|
throw new AppError("snapshot_flow_unavailable", "Bootstrap snapshot requires the selected installed flow", 2, { phase: "prepare", effect: "no_effect" });
|
|
37
38
|
source = PreparedInstalledFlowSource.fromInstalledRelease(home, selected.installation.manifest_sha256, input.flow);
|
|
39
|
+
if (source.resolveRegisteredFlow().descriptor.stages[0]?.id !== "preflight")
|
|
40
|
+
throw new AppError("snapshot_flow_unsupported", "Memory bootstrap requires its preflight entry", 2, { phase: "prepare", effect: "no_effect" });
|
|
38
41
|
}
|
|
39
42
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
40
43
|
const output = path.resolve(input.output);
|
|
@@ -68,7 +71,7 @@ export function createEvalBootstrapSnapshot(context, input) {
|
|
|
68
71
|
const manifest = {
|
|
69
72
|
schema_id: source ? "dd-eval/bootstrap-snapshot@2" : "dd-eval/bootstrap-snapshot@1",
|
|
70
73
|
stage: source?.resolveRegisteredFlow().descriptor.stages[0]?.id ?? "specify",
|
|
71
|
-
...(source ? { flow: { id:
|
|
74
|
+
...(source ? { flow: { id: source.resolveRegisteredFlow().descriptor.id, source_sha256: source.sha256, descriptor_sha256: source.descriptorReference().sha256 } } : {}),
|
|
72
75
|
project_root: projectRoot,
|
|
73
76
|
project_sha256: treeHash(path.join(temporary, "project")),
|
|
74
77
|
project_git: snapshotProjectGit(projectRoot, temporary)
|
|
@@ -220,7 +223,7 @@ export function prepareEvalRunSnapshotCreation(context, input) {
|
|
|
220
223
|
throw new AppError("snapshot_output_inside_project", "Snapshot output must be outside the project and RUN workspace trees", 1, { output });
|
|
221
224
|
prepareOutputFile(output);
|
|
222
225
|
prepareSnapshotProjectGit(projectRoot, recovery && status.run.flow_kind === "mb-init");
|
|
223
|
-
prepareSnapshotWorkspace(workspaceRoot, projectRoot);
|
|
226
|
+
prepareSnapshotWorkspace(workspaceRoot, projectRoot, isDiagnosticFlow(status.run.flow_kind));
|
|
224
227
|
return { projectRoot, project, status, recovery, output, workspaceRoot, activeChildren };
|
|
225
228
|
}
|
|
226
229
|
function captureEvalRunSnapshot(context, input) {
|
|
@@ -238,7 +241,7 @@ function captureEvalRunSnapshot(context, input) {
|
|
|
238
241
|
}
|
|
239
242
|
}
|
|
240
243
|
else
|
|
241
|
-
copyRuntimeTree(context.ddFlowHome, path.join(output, "runtime"), false, [...databaseFiles, ...codexExclusions], grokProviderHomes(context.ddFlowHome), input.observationCuts);
|
|
244
|
+
copyRuntimeTree(context.ddFlowHome, path.join(output, "runtime"), false, [...databaseFiles, ...codexExclusions, ...(isDiagnosticFlow(status.run.flow_kind) ? [workspaceRoot] : [])], grokProviderHomes(context.ddFlowHome), input.observationCuts);
|
|
242
245
|
for (const [file, cut] of input.journalCuts ?? []) {
|
|
243
246
|
const target = path.join(output, "runtime", path.relative(context.ddFlowHome, file));
|
|
244
247
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
@@ -278,7 +281,7 @@ function captureEvalRunSnapshot(context, input) {
|
|
|
278
281
|
filter: projectPayloadFilter(projectRoot)
|
|
279
282
|
});
|
|
280
283
|
const projectGit = snapshotProjectGit(projectRoot, output, recovery && status.run.flow_kind === "mb-init");
|
|
281
|
-
const workspace = snapshotWorkspace(workspaceRoot, projectRoot, output);
|
|
284
|
+
const workspace = snapshotWorkspace(workspaceRoot, projectRoot, output, isDiagnosticFlow(status.run.flow_kind));
|
|
282
285
|
// Accepted Work results are captured in SQLite. Their file representations
|
|
283
286
|
// are retained evidence, not a second authority or a checkpoint gate.
|
|
284
287
|
if (recovery) {
|
|
@@ -291,7 +294,7 @@ function captureEvalRunSnapshot(context, input) {
|
|
|
291
294
|
}
|
|
292
295
|
}
|
|
293
296
|
const manifest = {
|
|
294
|
-
schema_id: projectGit.state === "unborn" ? "dd-flow/eval-run-snapshot@6" : "dd-flow/eval-run-snapshot@5",
|
|
297
|
+
schema_id: workspace.kind === "git_repository" ? "dd-flow/eval-run-snapshot@7" : projectGit.state === "unborn" ? "dd-flow/eval-run-snapshot@6" : "dd-flow/eval-run-snapshot@5",
|
|
295
298
|
run_id: input.runId,
|
|
296
299
|
project_id: project.id,
|
|
297
300
|
project_root: projectRoot,
|
|
@@ -308,7 +311,7 @@ function captureEvalRunSnapshot(context, input) {
|
|
|
308
311
|
...(input.observationCuts ? { observation_cuts: [...input.observationCuts].map(([file, receipt]) => ({ path: path.relative(context.ddFlowHome, file), receipt })) } : {}),
|
|
309
312
|
project_git: projectGit,
|
|
310
313
|
workspace,
|
|
311
|
-
payload_modes: { project: treeModesHash(path.join(output, "project")), runtime: treeModesHash(path.join(output, "runtime")), ...(workspace.kind
|
|
314
|
+
payload_modes: { project: treeModesHash(path.join(output, "project")), runtime: treeModesHash(path.join(output, "runtime")), ...(workspace.kind !== "project" ? { workspace: treeModesHash(path.join(output, "workspace")) } : {}) },
|
|
312
315
|
source_status: status
|
|
313
316
|
};
|
|
314
317
|
writeJson(path.join(output, "snapshot.json"), manifest);
|
|
@@ -434,7 +437,7 @@ export function prepareSnapshotOperationalDecision(input) {
|
|
|
434
437
|
}
|
|
435
438
|
function assertRunRestoreTargets(context, input, snapshot, projectRoot, manifest) {
|
|
436
439
|
assertRestoreTargetSeparation(snapshot, [projectRoot, context.ddFlowHome]);
|
|
437
|
-
if (manifest.workspace.kind
|
|
440
|
+
if (manifest.workspace.kind !== "project") {
|
|
438
441
|
const workspaceRoot = restoredWorkspaceRoot(manifest, projectRoot, context.ddFlowHome);
|
|
439
442
|
assertRestoreTargetSeparation(snapshot, [workspaceRoot]);
|
|
440
443
|
if (isInside(physicalPath(projectRoot), physicalPath(workspaceRoot)) || isInside(physicalPath(workspaceRoot), physicalPath(projectRoot)) || physicalPath(workspaceRoot) === physicalPath(context.ddFlowHome))
|
|
@@ -460,105 +463,114 @@ export function restoreEvalRunSnapshot(context, input, prepared = prepareEvalRun
|
|
|
460
463
|
const sourceManifestSha256 = fileHash(path.join(snapshot, "snapshot.json"));
|
|
461
464
|
const importId = prearmedImportId ?? beginSnapshotImport({ home: context.ddFlowHome, projectRoot,
|
|
462
465
|
workspaceRoot: restoredWorkspaceRoot(manifest, projectRoot, context.ddFlowHome), sourceManifestSha256 });
|
|
463
|
-
clearProjectTree(projectRoot, false);
|
|
464
|
-
restoreProjectGit(snapshot, manifest.project_git, projectRoot);
|
|
465
|
-
replaceProjectTree(projectRoot, sourceProject);
|
|
466
|
-
context.db.close?.();
|
|
467
|
-
fs.rmSync(context.ddFlowHome, { recursive: true, force: true });
|
|
468
|
-
fs.cpSync(sourceRuntime, context.ddFlowHome, { recursive: true, verbatimSymlinks: true });
|
|
469
|
-
const workspaceRoot = restoreWorkspace(snapshot, manifest, projectRoot, context.ddFlowHome);
|
|
470
|
-
// The dispatch context was intentionally closed before replacing db.sqlite.
|
|
471
|
-
// A fresh process owns all subsequent normal commands.
|
|
472
|
-
const restored = createContext({ ...context.env, DD_FLOW_HOME: context.ddFlowHome }, "initialize", importId);
|
|
473
|
-
let result, primary, failed = false;
|
|
474
466
|
try {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
467
|
+
clearProjectTree(projectRoot, false);
|
|
468
|
+
restoreProjectGit(snapshot, manifest.project_git, projectRoot);
|
|
469
|
+
replaceProjectTree(projectRoot, sourceProject);
|
|
470
|
+
context.db.close?.();
|
|
471
|
+
fs.rmSync(context.ddFlowHome, { recursive: true, force: true });
|
|
472
|
+
fs.cpSync(sourceRuntime, context.ddFlowHome, { recursive: true, verbatimSymlinks: true });
|
|
473
|
+
const workspaceRoot = restoreWorkspace(snapshot, manifest, projectRoot, context.ddFlowHome);
|
|
474
|
+
// The dispatch context was intentionally closed before replacing db.sqlite.
|
|
475
|
+
// A fresh process owns all subsequent normal commands.
|
|
476
|
+
const restored = createContext({ ...context.env, DD_FLOW_HOME: context.ddFlowHome }, "initialize", importId);
|
|
477
|
+
let result, primary, failed = false;
|
|
478
|
+
try {
|
|
479
|
+
const runtimeLineage = recovery ? null : detachSnapshotRuntimeOwners(restored, snapshot, manifest);
|
|
480
|
+
const originalRunRoot = restored.db.get("SELECT run_root FROM runs WHERE id = ?", [manifest.run_id])?.run_root;
|
|
481
|
+
rebaseRuntime(restored, manifest.project_root, projectRoot, sourceWorkspaceRoot(manifest), workspaceRoot, manifest.dd_flow_home, context.ddFlowHome, recovery);
|
|
482
|
+
const publicationRun = restored.db.get("SELECT project_id, run_root FROM runs WHERE id = ?", [manifest.run_id]);
|
|
483
|
+
if (!originalRunRoot || !publicationRun?.run_root || !isInside(manifest.dd_flow_home, originalRunRoot) || !isInside(context.ddFlowHome, publicationRun.run_root))
|
|
484
|
+
throw new AppError("snapshot_invalid", "Restored publication roots are not bound to their runtime", 1);
|
|
485
|
+
const pathMapping = [{ from: manifest.dd_flow_home, to: context.ddFlowHome }, { from: sourceWorkspaceRoot(manifest), to: workspaceRoot }, { from: manifest.project_root, to: projectRoot }];
|
|
486
|
+
restored.db.run("INSERT INTO audit_events (project_id, event_type, forced, payload_json, created_at) VALUES (?, 'flow_run.publication_relocation', 0, ?, ?)", [publicationRun.project_id, JSON.stringify({ run_id: manifest.run_id, from: originalRunRoot, to: publicationRun.run_root, source_manifest_sha256: sourceManifestSha256, import_id: importId, path_mapping: pathMapping }), restored.now()]);
|
|
487
|
+
registerProject(restored, { root: projectRoot });
|
|
488
|
+
requireProjectByRoot(restored, projectRoot);
|
|
489
|
+
if (selectedProfile && runtimeLineage) {
|
|
490
|
+
const run = restored.db.get("SELECT run_root FROM runs WHERE id = ?", [manifest.run_id]);
|
|
491
|
+
if (routingInput) {
|
|
492
|
+
const artifact = path.join(run.run_root, selectedProfile.routing_input.artifact);
|
|
493
|
+
if (fs.existsSync(artifact)) {
|
|
494
|
+
if (!fs.readFileSync(artifact).equals(routingInput.bytes))
|
|
495
|
+
throw new AppError("snapshot_checksum_mismatch", "Existing routing input does not match its checksum", 1);
|
|
496
|
+
}
|
|
497
|
+
else
|
|
498
|
+
fs.writeFileSync(artifact, routingInput.bytes, { flag: "wx", mode: 0o600 });
|
|
492
499
|
}
|
|
493
|
-
|
|
494
|
-
fs.
|
|
500
|
+
if (input.operationalDecisionFile) {
|
|
501
|
+
if (!prepared.operationalDecisionBytes || !fs.readFileSync(input.operationalDecisionFile).equals(prepared.operationalDecisionBytes))
|
|
502
|
+
throw new AppError("operational_decision_changed", "Accepted operational decision changed before import publication", 2);
|
|
503
|
+
const binding = { project_id: publicationRun.project_id, run_id: manifest.run_id, ...selectedProfile.operational_decision.scope };
|
|
504
|
+
const policyBytes = fs.readFileSync(path.resolve(projectRoot, selectedProfile.operational_decision.policy.path));
|
|
505
|
+
const policySource = { path: `operational-policy.${selectedProfile.operational_decision.policy.sha256}.source.md`, sha256: crypto.createHash("sha256").update(policyBytes).digest("hex") };
|
|
506
|
+
if (policySource.sha256 !== selectedProfile.operational_decision.policy.sha256)
|
|
507
|
+
throw new AppError("operational_decision_policy_changed", "Imported policy changed before binding", 2);
|
|
508
|
+
if (!fs.existsSync(path.join(run.run_root, policySource.path)))
|
|
509
|
+
fs.writeFileSync(path.join(run.run_root, policySource.path), policyBytes, { flag: "wx" });
|
|
510
|
+
const bytes = `${JSON.stringify({ schema_id: "dd-flow/run-operational-contract@1", binding, settings: { merge_delivery: selectedProfile.settings.merge_delivery, merge_cleanup: selectedProfile.settings.merge_cleanup }, decision: selectedProfile.operational_decision, policy_source: policySource }, null, 2)}\n`;
|
|
511
|
+
const artifact = `operational-contract.${crypto.createHash("sha256").update(bytes).digest("hex")}.json`;
|
|
512
|
+
fs.writeFileSync(path.join(run.run_root, artifact), bytes, { flag: "wx" });
|
|
513
|
+
selectedProfile.operational_contract = { path: artifact, sha256: crypto.createHash("sha256").update(bytes).digest("hex"), policy_source: policySource, binding };
|
|
514
|
+
}
|
|
515
|
+
importFlowRunExecutionProfile(restored, { projectRoot, runId: manifest.run_id, stage: manifest.stage_entry, lineageFile: runtimeLineage, profile: selectedProfile });
|
|
495
516
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
if (!fs.existsSync(path.join(run.run_root, policySource.path)))
|
|
505
|
-
fs.writeFileSync(path.join(run.run_root, policySource.path), policyBytes, { flag: "wx" });
|
|
506
|
-
const bytes = `${JSON.stringify({ schema_id: "dd-flow/run-operational-contract@1", binding, settings: { merge_delivery: selectedProfile.settings.merge_delivery, merge_cleanup: selectedProfile.settings.merge_cleanup }, decision: selectedProfile.operational_decision, policy_source: policySource }, null, 2)}\n`;
|
|
507
|
-
const artifact = `operational-contract.${crypto.createHash("sha256").update(bytes).digest("hex")}.json`;
|
|
508
|
-
fs.writeFileSync(path.join(run.run_root, artifact), bytes, { flag: "wx" });
|
|
509
|
-
selectedProfile.operational_contract = { path: artifact, sha256: crypto.createHash("sha256").update(bytes).digest("hex"), policy_source: policySource, binding };
|
|
517
|
+
const relocation = recovery ? {
|
|
518
|
+
schema_id: "dd-flow/recovery-relocation@1", physical_runtime_id: crypto.randomUUID(), recovery_id: input.recoveryId, run_id: manifest.run_id,
|
|
519
|
+
path_mapping: [{ from: manifest.dd_flow_home, to: context.ddFlowHome }, { from: sourceWorkspaceRoot(manifest), to: workspaceRoot }, { from: manifest.project_root, to: projectRoot }]
|
|
520
|
+
} : null;
|
|
521
|
+
const relocationFile = relocation ? path.join(context.ddFlowHome, "recovery-imports", `${relocation.physical_runtime_id}.json`) : null;
|
|
522
|
+
if (relocationFile) {
|
|
523
|
+
fs.mkdirSync(path.dirname(relocationFile), { recursive: true });
|
|
524
|
+
writeJson(relocationFile, relocation);
|
|
510
525
|
}
|
|
511
|
-
|
|
526
|
+
const status = getFlowRunStatus(restored, { projectRoot, runId: manifest.run_id });
|
|
527
|
+
if (!recovery)
|
|
528
|
+
assertStageEntry(status.index.stage_runs ?? [], manifest.stage_entry);
|
|
529
|
+
else
|
|
530
|
+
restored.db.run("UPDATE run_recovery_guards SET capture_path = ? WHERE recovery_id = ? AND status = 'sealed'", [snapshot, input.recoveryId]);
|
|
531
|
+
if (!status.run.run_root)
|
|
532
|
+
throw new AppError("runtime_missing", "Restored RUN has no artifact root", 1, { run_id: manifest.run_id });
|
|
533
|
+
const runHome = status.run.run_root;
|
|
534
|
+
const expectedWorkspaceRoot = restoredWorkspaceRoot(manifest, projectRoot, context.ddFlowHome);
|
|
535
|
+
if (physicalPath(workspaceRoot) !== physicalPath(expectedWorkspaceRoot))
|
|
536
|
+
throw new AppError("snapshot_workspace_restore_failed", "Restored workspace differs from the import target", 1, { workspace_root: workspaceRoot, expected_workspace_root: expectedWorkspaceRoot });
|
|
537
|
+
completeSnapshotImport(context.ddFlowHome, importId, { projectRoot, workspaceRoot: expectedWorkspaceRoot, sourceManifestSha256 });
|
|
538
|
+
result = {
|
|
539
|
+
ok: true,
|
|
540
|
+
schema_id: "dd-flow/eval-run-restore@1",
|
|
541
|
+
snapshot,
|
|
542
|
+
run_id: status.run.id,
|
|
543
|
+
project_root: projectRoot,
|
|
544
|
+
workspace_root: workspaceRoot,
|
|
545
|
+
dd_flow_home: context.ddFlowHome,
|
|
546
|
+
run_home: runHome,
|
|
547
|
+
target_stage: manifest.stage_entry,
|
|
548
|
+
...(runtimeLineage ? { runtime_lineage_file: runtimeLineage } : {}),
|
|
549
|
+
...(recovery ? { recovery_id: input.recoveryId, native_session_portability: "requires_adapter_verification", relocation_file: relocationFile, relocation } : {}),
|
|
550
|
+
next_command: recovery ? `dd-flow run recovery inspect ${status.run.id} --project-root ${JSON.stringify(projectRoot)} --json` : `dd-flow stage start ${status.run.id} --stage ${manifest.stage_entry} --project-root ${JSON.stringify(projectRoot)} --json`
|
|
551
|
+
};
|
|
512
552
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
} : null;
|
|
517
|
-
const relocationFile = relocation ? path.join(context.ddFlowHome, "recovery-imports", `${relocation.physical_runtime_id}.json`) : null;
|
|
518
|
-
if (relocationFile) {
|
|
519
|
-
fs.mkdirSync(path.dirname(relocationFile), { recursive: true });
|
|
520
|
-
writeJson(relocationFile, relocation);
|
|
553
|
+
catch (error) {
|
|
554
|
+
primary = error;
|
|
555
|
+
failed = true;
|
|
521
556
|
}
|
|
522
|
-
const
|
|
523
|
-
if (
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
if (physicalPath(workspaceRoot) !== physicalPath(expectedWorkspaceRoot))
|
|
532
|
-
throw new AppError("snapshot_workspace_restore_failed", "Restored workspace differs from the import target", 1, { workspace_root: workspaceRoot, expected_workspace_root: expectedWorkspaceRoot });
|
|
533
|
-
completeSnapshotImport(context.ddFlowHome, importId, { projectRoot, workspaceRoot: expectedWorkspaceRoot, sourceManifestSha256 });
|
|
534
|
-
result = {
|
|
535
|
-
ok: true,
|
|
536
|
-
schema_id: "dd-flow/eval-run-restore@1",
|
|
537
|
-
snapshot,
|
|
538
|
-
run_id: status.run.id,
|
|
539
|
-
project_root: projectRoot,
|
|
540
|
-
workspace_root: workspaceRoot,
|
|
541
|
-
dd_flow_home: context.ddFlowHome,
|
|
542
|
-
run_home: runHome,
|
|
543
|
-
target_stage: manifest.stage_entry,
|
|
544
|
-
...(runtimeLineage ? { runtime_lineage_file: runtimeLineage } : {}),
|
|
545
|
-
...(recovery ? { recovery_id: input.recoveryId, native_session_portability: "requires_adapter_verification", relocation_file: relocationFile, relocation } : {}),
|
|
546
|
-
next_command: recovery ? `dd-flow run recovery inspect ${status.run.id} --project-root ${JSON.stringify(projectRoot)} --json` : `dd-flow stage start ${status.run.id} --stage ${manifest.stage_entry} --project-root ${JSON.stringify(projectRoot)} --json`
|
|
547
|
-
};
|
|
557
|
+
const cleanup = cleanupScopeWriters([{ name: restored.ddFlowHome, db: restored.db, rollback: false, close: "always" }]);
|
|
558
|
+
if (failed) {
|
|
559
|
+
if (primary instanceof AppError && cleanup.length)
|
|
560
|
+
primary.details.import_cleanup_errors = cleanup;
|
|
561
|
+
throw primary;
|
|
562
|
+
}
|
|
563
|
+
if (cleanup.length)
|
|
564
|
+
throw new AppError("snapshot_import_cleanup_failed", "Snapshot imported but its connection could not be closed", 1, { effect: "committed", import_complete: true, import_cleanup_errors: cleanup });
|
|
565
|
+
return result;
|
|
548
566
|
}
|
|
549
567
|
catch (error) {
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
if (primary instanceof AppError && cleanup.length)
|
|
556
|
-
primary.details.import_cleanup_errors = cleanup;
|
|
557
|
-
throw primary;
|
|
568
|
+
// The import fence and target writes have already started. A nested
|
|
569
|
+
// prepare/read validator must not report this operation as no-effect.
|
|
570
|
+
if (error instanceof AppError)
|
|
571
|
+
Object.assign(error.details, { phase: "restore", effect: error.details.effect === "committed" ? "committed" : "partial", import_id: importId, next_action: "reconcile_snapshot_import" });
|
|
572
|
+
throw error;
|
|
558
573
|
}
|
|
559
|
-
if (cleanup.length)
|
|
560
|
-
throw new AppError("snapshot_import_cleanup_failed", "Snapshot imported but its connection could not be closed", 1, { effect: "committed", import_complete: true, import_cleanup_errors: cleanup });
|
|
561
|
-
return result;
|
|
562
574
|
}
|
|
563
575
|
export function prepareVnextSpecifyRun(context, input) {
|
|
564
576
|
const projectRoot = resolveProjectRoot(input.projectRoot);
|
|
@@ -749,27 +761,29 @@ function rebaseStructuredValue(value, oldRoot, newRoot, oldWorkspace, newWorkspa
|
|
|
749
761
|
function rebasePathString(value, oldRoot, newRoot, oldWorkspace, newWorkspace, oldHome, newHome) { for (const [oldPrefix, newPrefix] of [[oldHome, newHome], [oldWorkspace, newWorkspace], [oldRoot, newRoot]])
|
|
750
762
|
if (value === oldPrefix || value.startsWith(`${oldPrefix}${path.sep}`))
|
|
751
763
|
return `${newPrefix}${value.slice(oldPrefix.length)}`; return value; }
|
|
752
|
-
function prepareSnapshotWorkspace(workspaceRoot, projectRoot) {
|
|
764
|
+
function prepareSnapshotWorkspace(workspaceRoot, projectRoot, allowUnborn = false) {
|
|
753
765
|
if (samePath(workspaceRoot, projectRoot))
|
|
754
766
|
return null;
|
|
755
767
|
const branch = git(workspaceRoot, ["symbolic-ref", "--quiet", "--short", "HEAD"]);
|
|
756
768
|
if (!branch)
|
|
757
769
|
throw new AppError("snapshot_workspace_invalid", "Feature workspace must be on a named Git branch", 1, { workspace_root: workspaceRoot });
|
|
758
770
|
const baseRef = git(workspaceRoot, ["rev-parse", "HEAD"]);
|
|
759
|
-
if (!baseRef)
|
|
771
|
+
if (!baseRef && !(allowUnborn && git(workspaceRoot, ['for-each-ref', '--format=%(refname)']) === ''))
|
|
760
772
|
throw new AppError("snapshot_workspace_invalid", "Feature workspace has no readable HEAD", 1, { workspace_root: workspaceRoot });
|
|
761
773
|
return { branch, baseRef };
|
|
762
774
|
}
|
|
763
|
-
function snapshotWorkspace(workspaceRoot, projectRoot, output) {
|
|
764
|
-
const prepared = prepareSnapshotWorkspace(workspaceRoot, projectRoot);
|
|
775
|
+
function snapshotWorkspace(workspaceRoot, projectRoot, output, independent = false) {
|
|
776
|
+
const prepared = prepareSnapshotWorkspace(workspaceRoot, projectRoot, independent);
|
|
765
777
|
if (!prepared)
|
|
766
778
|
return { kind: "project" };
|
|
767
779
|
const { branch, baseRef } = prepared;
|
|
768
780
|
const workspace = path.join(output, "workspace");
|
|
769
781
|
const bundle = path.join(output, "workspace.bundle");
|
|
770
782
|
fs.cpSync(workspaceRoot, workspace, { recursive: true, verbatimSymlinks: true, filter: projectPayloadFilter(workspaceRoot) });
|
|
771
|
-
|
|
772
|
-
|
|
783
|
+
if (baseRef === null)
|
|
784
|
+
return { kind: 'git_repository', source_root: workspaceRoot, branch, base_ref: null, bundle_sha256: null, state: 'unborn', sha256: treeHash(workspace), ...snapshotIndex(workspaceRoot, output, 'workspace') };
|
|
785
|
+
runGit(independent ? workspaceRoot : projectRoot, ["bundle", "create", bundle, branch]);
|
|
786
|
+
return { kind: independent ? "git_repository" : "git_worktree", source_root: workspaceRoot, branch, base_ref: baseRef, sha256: treeHash(workspace), bundle_sha256: fileHash(bundle), ...snapshotIndex(workspaceRoot, output, "workspace") };
|
|
773
787
|
}
|
|
774
788
|
function restoreWorkspace(snapshot, manifest, projectRoot, ddFlowHome) {
|
|
775
789
|
if (manifest.workspace.kind === "project")
|
|
@@ -778,15 +792,28 @@ function restoreWorkspace(snapshot, manifest, projectRoot, ddFlowHome) {
|
|
|
778
792
|
const bundle = path.join(snapshot, "workspace.bundle");
|
|
779
793
|
if (treeHash(source) !== manifest.workspace.sha256)
|
|
780
794
|
throw new AppError("snapshot_checksum_mismatch", "Snapshot workspace checksum does not match manifest", 1, { snapshot });
|
|
781
|
-
if (!fs.existsSync(bundle) || fileHash(bundle) !== manifest.workspace.bundle_sha256)
|
|
795
|
+
if (manifest.workspace.state === 'unborn' ? fs.existsSync(bundle) : !fs.existsSync(bundle) || fileHash(bundle) !== manifest.workspace.bundle_sha256)
|
|
782
796
|
throw new AppError("snapshot_checksum_mismatch", "Snapshot workspace Git bundle does not match manifest", 1, { snapshot });
|
|
783
797
|
const workspaceRoot = restoredWorkspaceRoot(manifest, projectRoot, ddFlowHome);
|
|
784
798
|
if (fs.existsSync(workspaceRoot))
|
|
785
799
|
throw new AppError("snapshot_workspace_target_exists", "Snapshot feature workspace target already exists", 1, { workspace_root: workspaceRoot });
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
800
|
+
if (manifest.workspace.kind === 'git_repository') {
|
|
801
|
+
fs.mkdirSync(path.dirname(workspaceRoot), { recursive: true });
|
|
802
|
+
if (manifest.workspace.state === 'unborn') {
|
|
803
|
+
fs.mkdirSync(workspaceRoot);
|
|
804
|
+
runGit(workspaceRoot, ['-c', 'init.templateDir=', 'init', '--initial-branch', manifest.workspace.branch]);
|
|
805
|
+
}
|
|
806
|
+
else
|
|
807
|
+
runGit(projectRoot, ['clone', '--no-checkout', '--branch', manifest.workspace.branch, bundle, workspaceRoot]);
|
|
808
|
+
}
|
|
809
|
+
else {
|
|
810
|
+
runGit(projectRoot, ["fetch", bundle, `+refs/heads/${manifest.workspace.branch}:refs/heads/${manifest.workspace.branch}`]);
|
|
811
|
+
if (git(projectRoot, ["rev-parse", `refs/heads/${manifest.workspace.branch}`]) !== manifest.workspace.base_ref)
|
|
812
|
+
throw new AppError("snapshot_workspace_restore_failed", "Snapshot Git bundle did not restore the frozen feature base", 1, { branch: manifest.workspace.branch, expected_base: manifest.workspace.base_ref });
|
|
813
|
+
runGit(projectRoot, ["worktree", "add", workspaceRoot, manifest.workspace.branch]);
|
|
814
|
+
}
|
|
815
|
+
if (git(workspaceRoot, ["rev-parse", "HEAD"]) !== manifest.workspace.base_ref)
|
|
816
|
+
throw new AppError("snapshot_workspace_restore_failed", "Snapshot Git bundle did not restore the frozen workspace base", 1, { branch: manifest.workspace.branch, expected_base: manifest.workspace.base_ref });
|
|
790
817
|
replaceProjectTree(workspaceRoot, source);
|
|
791
818
|
restoreIndex(snapshot, "workspace", manifest.workspace, workspaceRoot);
|
|
792
819
|
return fs.realpathSync(workspaceRoot);
|
|
@@ -836,7 +863,7 @@ function restoreProjectGit(snapshot, source, projectRoot) {
|
|
|
836
863
|
restoreIndex(snapshot, "project", source, projectRoot);
|
|
837
864
|
}
|
|
838
865
|
const mergeStateFiles = ["MERGE_HEAD", "MERGE_MSG", "MERGE_MODE", "ORIG_HEAD", "AUTO_MERGE"];
|
|
839
|
-
function indexEntries(root) {
|
|
866
|
+
function indexEntries(root, includeExclude = true) {
|
|
840
867
|
const entries = Buffer.from(gitBytes(root, ["ls-files", "--stage", "-z"])
|
|
841
868
|
.toString("utf8").split("\0").filter(entry => entry && localEnvironmentAllowed(entry.slice(entry.indexOf("\t") + 1)))
|
|
842
869
|
.map(entry => `${entry}\0`).join(""));
|
|
@@ -849,7 +876,22 @@ function indexEntries(root) {
|
|
|
849
876
|
if (fs.existsSync(resolved))
|
|
850
877
|
merge[name] = fs.readFileSync(resolved).toString("base64");
|
|
851
878
|
}
|
|
852
|
-
|
|
879
|
+
if (!includeExclude)
|
|
880
|
+
return Buffer.from(JSON.stringify({ entries: entries.toString("base64"), merge }));
|
|
881
|
+
const excludePath = git(root, ["rev-parse", "--git-path", "info/exclude"]);
|
|
882
|
+
if (!excludePath)
|
|
883
|
+
throw new AppError("snapshot_index_invalid", "Could not locate Git local excludes", 1);
|
|
884
|
+
const excludeFile = path.resolve(root, excludePath);
|
|
885
|
+
let exclude = null;
|
|
886
|
+
if (fs.existsSync(excludeFile)) {
|
|
887
|
+
const stat = fs.lstatSync(excludeFile);
|
|
888
|
+
if (!stat.isFile() || stat.size > 1024 * 1024)
|
|
889
|
+
throw new AppError("snapshot_index_invalid", "Git local excludes must be a bounded regular file", 1);
|
|
890
|
+
exclude = fs.readFileSync(excludeFile).toString("base64");
|
|
891
|
+
}
|
|
892
|
+
// Local excludes affect review-visible inputs, but are not part of a Git
|
|
893
|
+
// bundle/index. Keep them in the existing hashed, non-executable Git payload.
|
|
894
|
+
return Buffer.from(JSON.stringify({ entries: entries.toString("base64"), merge, exclude }));
|
|
853
895
|
}
|
|
854
896
|
function snapshotIndex(root, output, name) {
|
|
855
897
|
const bytes = indexEntries(root);
|
|
@@ -900,6 +942,8 @@ function restoreIndex(snapshot, name, metadata, root) {
|
|
|
900
942
|
if (!metadata.index_objects_sha256 || fileHash(pack) !== metadata.index_objects_sha256)
|
|
901
943
|
throw new AppError("snapshot_checksum_mismatch", "Snapshot Git index object checksum does not match", 1);
|
|
902
944
|
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
945
|
+
if (value.exclude !== undefined && value.exclude !== null && (typeof value.exclude !== "string" || Buffer.byteLength(value.exclude) > 1400000))
|
|
946
|
+
throw new AppError("snapshot_index_invalid", "Snapshot Git local excludes are invalid", 1);
|
|
903
947
|
gitBytes(root, ["unpack-objects"], fs.readFileSync(pack));
|
|
904
948
|
runGit(root, ["read-tree", "--empty"]);
|
|
905
949
|
gitBytes(root, ["update-index", "-z", "--index-info"], Buffer.from(value.entries, "base64"));
|
|
@@ -912,18 +956,36 @@ function restoreIndex(snapshot, name, metadata, root) {
|
|
|
912
956
|
else
|
|
913
957
|
fs.rmSync(path.resolve(root, target), { force: true });
|
|
914
958
|
}
|
|
959
|
+
if (value.exclude !== undefined) {
|
|
960
|
+
const target = git(root, ["rev-parse", "--git-path", "info/exclude"]);
|
|
961
|
+
if (!target)
|
|
962
|
+
throw new AppError("snapshot_index_invalid", "Could not locate restored Git local excludes", 1);
|
|
963
|
+
const destination = path.resolve(root, target);
|
|
964
|
+
if (fs.existsSync(destination) && !fs.lstatSync(destination).isFile())
|
|
965
|
+
throw new AppError("snapshot_index_invalid", "Restored Git local excludes are not a regular file", 1);
|
|
966
|
+
if (value.exclude === null)
|
|
967
|
+
fs.rmSync(destination, { force: true });
|
|
968
|
+
else {
|
|
969
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
970
|
+
fs.writeFileSync(destination, Buffer.from(value.exclude, "base64"));
|
|
971
|
+
}
|
|
972
|
+
}
|
|
915
973
|
return;
|
|
916
974
|
}
|
|
917
975
|
if (fs.statSync(file).size)
|
|
918
976
|
runGit(root, ["apply", "--cached", "--binary", file]);
|
|
919
977
|
}
|
|
978
|
+
function capturedIndexEntries(snapshot, name, root) {
|
|
979
|
+
const captured = JSON.parse(fs.readFileSync(path.join(snapshot, `${name}.index.patch`), "utf8"));
|
|
980
|
+
return indexEntries(root, Object.hasOwn(captured, "exclude"));
|
|
981
|
+
}
|
|
920
982
|
export function verifyRecoverySnapshot(snapshot, input, checkWorkingState = true) {
|
|
921
983
|
const manifest = readManifest(snapshot);
|
|
922
984
|
if (manifest.purpose !== "recovery" || manifest.recovery_id !== input.recoveryId || manifest.run_id !== input.runId)
|
|
923
985
|
throw new AppError("recovery_capture_invalid", "Snapshot does not identify this recovery", 1);
|
|
924
986
|
const entries = [
|
|
925
987
|
{ name: "project", root: input.projectRoot, hash: manifest.project_sha256, git: manifest.project_git, head: manifest.project_git.head },
|
|
926
|
-
...(manifest.workspace.kind
|
|
988
|
+
...(manifest.workspace.kind !== "project" ? [{ name: "workspace", root: input.workspaceRoot, hash: manifest.workspace.sha256, git: manifest.workspace, head: manifest.workspace.base_ref }] : [])
|
|
927
989
|
];
|
|
928
990
|
verifySnapshotPayload(snapshot, manifest);
|
|
929
991
|
for (const entry of entries) {
|
|
@@ -933,7 +995,7 @@ export function verifyRecoverySnapshot(snapshot, input, checkWorkingState = true
|
|
|
933
995
|
throw new AppError("recovery_workspace_drift", "Executable file modes changed after recovery capture", 1, { payload: entry.name });
|
|
934
996
|
if (checkWorkingState && (treeHash(entry.root, allowed) !== entry.hash || git(entry.root, ["rev-parse", "--verify", "HEAD"]) !== entry.head
|
|
935
997
|
|| 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" ?
|
|
998
|
+
|| 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" ? capturedIndexEntries(snapshot, entry.name, entry.root) : indexPatch(entry.root)).digest("hex") !== entry.git.index_sha256)))
|
|
937
999
|
throw new AppError("recovery_workspace_drift", "Working files or Git state changed after recovery capture", 1, { payload: entry.name });
|
|
938
1000
|
}
|
|
939
1001
|
}
|
|
@@ -953,7 +1015,7 @@ function verifySnapshotPayload(snapshot, manifest) {
|
|
|
953
1015
|
if (treeHash(path.join(snapshot, "runtime")) !== manifest.runtime_sha256)
|
|
954
1016
|
throw new AppError("snapshot_checksum_mismatch", "Snapshot runtime checksum does not match", 1);
|
|
955
1017
|
verifySnapshotModes(snapshot, manifest);
|
|
956
|
-
const payloads = [{ name: "project", hash: manifest.project_sha256, git: manifest.project_git }, ...(manifest.workspace.kind
|
|
1018
|
+
const payloads = [{ name: "project", hash: manifest.project_sha256, git: manifest.project_git }, ...(manifest.workspace.kind !== "project" ? [{ name: "workspace", hash: manifest.workspace.sha256, git: manifest.workspace }] : [])];
|
|
957
1019
|
for (const payload of payloads)
|
|
958
1020
|
verifyGitSnapshotPayload(snapshot, payload);
|
|
959
1021
|
if (manifest.schema_id === "dd-flow/eval-run-snapshot@6") {
|
|
@@ -1422,7 +1484,7 @@ function treeModesHash(root, filtered = false) {
|
|
|
1422
1484
|
function verifySnapshotModes(snapshot, manifest) {
|
|
1423
1485
|
if (!manifest.payload_modes)
|
|
1424
1486
|
return; // Legacy snapshots did not attest modes.
|
|
1425
|
-
for (const name of ["project", "runtime", ...(manifest.workspace.kind
|
|
1487
|
+
for (const name of ["project", "runtime", ...(manifest.workspace.kind !== "project" ? ["workspace"] : [])]) {
|
|
1426
1488
|
if (treeModesHash(path.join(snapshot, name)) !== manifest.payload_modes[name])
|
|
1427
1489
|
throw new AppError("snapshot_checksum_mismatch", "Snapshot executable modes do not match manifest", 1, { payload: name });
|
|
1428
1490
|
}
|
|
@@ -1454,7 +1516,7 @@ function readManifest(snapshot) {
|
|
|
1454
1516
|
if (!fs.existsSync(file))
|
|
1455
1517
|
throw new AppError("snapshot_missing", "Snapshot manifest is missing", 1, { snapshot });
|
|
1456
1518
|
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
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)
|
|
1519
|
+
if (!["dd-flow/eval-run-snapshot@5", "dd-flow/eval-run-snapshot@6", "dd-flow/eval-run-snapshot@7"].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)
|
|
1458
1520
|
throw new AppError("snapshot_invalid", "Snapshot manifest is invalid", 1, { snapshot });
|
|
1459
1521
|
if (value.runtime_payload_policy !== undefined && (value.runtime_payload_policy !== "flow-evidence@1" || value.purpose === "recovery"))
|
|
1460
1522
|
throw new AppError("snapshot_invalid", "Snapshot runtime payload policy is invalid for its purpose", 1, { snapshot });
|
|
@@ -1469,8 +1531,13 @@ function readManifest(snapshot) {
|
|
|
1469
1531
|
|| spawnSync("git", ["check-ref-format", "--branch", value.project_git.branch], { encoding: "utf8" }).status !== 0
|
|
1470
1532
|
: value.project_git.state !== undefined || !value.project_git.head || !value.project_git.bundle_sha256))
|
|
1471
1533
|
throw new AppError("snapshot_invalid", "Snapshot project Git metadata is incomplete", 1, { snapshot });
|
|
1472
|
-
if (
|
|
1473
|
-
|
|
1534
|
+
if (!['project', 'git_worktree', 'git_repository'].includes(value.workspace.kind)
|
|
1535
|
+
|| (value.schema_id === 'dd-flow/eval-run-snapshot@7') !== (value.workspace.kind === 'git_repository')
|
|
1536
|
+
|| value.workspace.kind !== 'project' && (!value.workspace.branch || (value.workspace.state === 'unborn'
|
|
1537
|
+
? value.workspace.kind !== 'git_repository' || value.workspace.base_ref !== null || value.workspace.bundle_sha256 !== null
|
|
1538
|
+
|| value.workspace.index_format !== 'entries@1' || !value.workspace.index_sha256 || !value.workspace.index_objects_sha256
|
|
1539
|
+
: value.workspace.state !== undefined || !value.workspace.base_ref || !value.workspace.bundle_sha256)))
|
|
1540
|
+
throw new AppError("snapshot_invalid", "Snapshot workspace manifest is incomplete or incompatible", 1, { snapshot });
|
|
1474
1541
|
return value;
|
|
1475
1542
|
}
|
|
1476
1543
|
function readBootstrapManifest(snapshot) {
|
|
@@ -1479,7 +1546,7 @@ function readBootstrapManifest(snapshot) {
|
|
|
1479
1546
|
throw new AppError("snapshot_missing", "Bootstrap snapshot manifest is missing", 1, { snapshot });
|
|
1480
1547
|
const value = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
1481
1548
|
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"
|
|
1549
|
+
: value.schema_id === "dd-eval/bootstrap-snapshot@2" && (value.flow?.id === "mb-init" || value.flow?.id === "mb-upgrade" || isDiagnosticFlow(value.flow?.id)) && value.stage === "preflight"
|
|
1483
1550
|
&& Object.keys(value.flow).sort().join() === "descriptor_sha256,id,source_sha256"
|
|
1484
1551
|
&& /^[a-f0-9]{64}$/.test(value.flow.source_sha256) && /^[a-f0-9]{64}$/.test(value.flow.descriptor_sha256);
|
|
1485
1552
|
if (!validBoundary || !value.project_root || !value.project_sha256 || !value.project_git?.branch || !value.project_git.head || !value.project_git.bundle_sha256)
|
|
@@ -9,6 +9,7 @@ import { FrozenGenericExecution } from "./generic-execution.js";
|
|
|
9
9
|
import { hybridDigest, validateHybridContract, validateHybridShape } from "./hybrid-contracts.js";
|
|
10
10
|
import { readBoundedReleaseFile } from "./release-source.js";
|
|
11
11
|
import { acquireInstallationLock } from "./installation-lock.js";
|
|
12
|
+
import { projectPacketLocators } from "./stage-context.js";
|
|
12
13
|
const hash = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex");
|
|
13
14
|
const limit = 1024 * 1024;
|
|
14
15
|
function refuse(reason, issues) {
|
|
@@ -54,7 +55,8 @@ export class RetainedExecutionBundle {
|
|
|
54
55
|
#blobs;
|
|
55
56
|
#admission;
|
|
56
57
|
#engine;
|
|
57
|
-
|
|
58
|
+
#engineRelocation;
|
|
59
|
+
constructor(bytes, manifest, blobs, admission, descriptor, execution, engineRelocation) {
|
|
58
60
|
frozen(manifest);
|
|
59
61
|
this.manifest = manifest;
|
|
60
62
|
this.sha256 = hash(bytes);
|
|
@@ -63,9 +65,10 @@ export class RetainedExecutionBundle {
|
|
|
63
65
|
this.#admission = admission;
|
|
64
66
|
this.descriptor = descriptor;
|
|
65
67
|
this.execution = execution;
|
|
68
|
+
this.#engineRelocation = engineRelocation && structuredClone(engineRelocation);
|
|
66
69
|
Object.freeze(this);
|
|
67
70
|
}
|
|
68
|
-
static admit(input, expectedSha256, sourceBlobs, selected) {
|
|
71
|
+
static admit(input, expectedSha256, sourceBlobs, selected, engineRelocation) {
|
|
69
72
|
if (input.byteLength > limit || !/^[a-f0-9]{64}$/.test(expectedSha256))
|
|
70
73
|
refuse("invalid_bundle_identity");
|
|
71
74
|
const bytes = Buffer.from(input);
|
|
@@ -113,12 +116,12 @@ export class RetainedExecutionBundle {
|
|
|
113
116
|
const snapshot = { schema_id: "dd-flow/frozen-execution@1", scope: admission.executionScope,
|
|
114
117
|
settings: json(blobs.get(manifest.execution_settings.path)), profiles: profiles.profiles };
|
|
115
118
|
const execution = FrozenGenericExecution.restore(snapshot, hybridDigest(snapshot), descriptor, admission.executionScope, admission.receipts);
|
|
116
|
-
const admitted = new RetainedExecutionBundle(bytes, manifest, blobs, admission, descriptor, execution);
|
|
119
|
+
const admitted = new RetainedExecutionBundle(bytes, manifest, blobs, admission, descriptor, execution, engineRelocation);
|
|
117
120
|
admitted.engine();
|
|
118
121
|
return admitted;
|
|
119
122
|
}
|
|
120
123
|
/** Read only the retained inventory. Mutable canon/profile sources are never consulted. */
|
|
121
|
-
static read(root, expectedSha256, admission) {
|
|
124
|
+
static read(root, expectedSha256, admission, engineRelocation) {
|
|
122
125
|
try {
|
|
123
126
|
directory(root);
|
|
124
127
|
const bytes = readBoundedReleaseFile(path.join(root, "manifest.json"), limit);
|
|
@@ -130,7 +133,7 @@ export class RetainedExecutionBundle {
|
|
|
130
133
|
const manifest = value, blobs = new Map();
|
|
131
134
|
for (const ref of manifest.inventory)
|
|
132
135
|
blobs.set(ref.path, readBoundedReleaseFile(fileIn(root, ref.path), limit));
|
|
133
|
-
return RetainedExecutionBundle.admit(bytes, expectedSha256, blobs, admission);
|
|
136
|
+
return RetainedExecutionBundle.admit(bytes, expectedSha256, blobs, admission, engineRelocation);
|
|
134
137
|
}
|
|
135
138
|
catch (error) {
|
|
136
139
|
if (error instanceof AppError && ["execution_bundle_invalid", "frozen_engine_invalid"].includes(error.code))
|
|
@@ -153,8 +156,17 @@ export class RetainedExecutionBundle {
|
|
|
153
156
|
}
|
|
154
157
|
#admitEngine() {
|
|
155
158
|
const binding = json(this.blob(this.manifest.engine_binding.path));
|
|
156
|
-
const
|
|
157
|
-
if (
|
|
159
|
+
const retainedManifest = this.blob(this.manifest.engine_manifest.path), relocation = this.#engineRelocation;
|
|
160
|
+
if (relocation) {
|
|
161
|
+
const root = JSON.parse(projectPacketLocators(JSON.stringify({ root: binding.engine.snapshot_root }), relocation.relocations)).root;
|
|
162
|
+
if (!relocation.relocations.length || root === binding.engine.snapshot_root || root !== relocation.binding.engine.snapshot_root
|
|
163
|
+
|| relocation.binding.run_id !== this.manifest.run_id)
|
|
164
|
+
refuse("foreign_engine_relocation");
|
|
165
|
+
}
|
|
166
|
+
const engine = relocation
|
|
167
|
+
? AdmittedFrozenEngine.admitRelocation(binding, retainedManifest, this.manifest.engine_manifest.sha256, relocation.binding)
|
|
168
|
+
: AdmittedFrozenEngine.admit(binding, this.manifest.engine_manifest.sha256);
|
|
169
|
+
if (!relocation && !engine.manifestBytes().equals(retainedManifest))
|
|
158
170
|
refuse("engine_manifest_bytes_changed");
|
|
159
171
|
const manifest = engine.manifest(), contracts = new Set(Object.values(manifest.supports_contracts).flat());
|
|
160
172
|
for (const id of ["dd-flow/flow-descriptor@1", "dd-flow/execution-settings@1", "dd-flow/execution-bundle@1"]) {
|