@deksden-com/dd-flow-cli 0.9.0-beta.152 → 0.9.0-beta.156
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 +31 -0
- package/dist/build-info.json +8 -8
- package/dist/cli/command-inputs.js +33 -3
- package/dist/cli/command-metadata.js +73 -2
- package/dist/cli/delivery.js +108 -0
- package/dist/cli/diagnostic-invocation.js +23 -7
- package/dist/cli/diagnostic.js +14 -3
- package/dist/cli/help.js +117 -1
- package/dist/cli/hook-ingress.js +36 -49
- package/dist/cli/interactive-invocation.js +33 -0
- package/dist/cli/interactive.js +114 -0
- package/dist/cli/memory-fix-invocation.js +52 -0
- package/dist/cli/memory-fix.js +100 -0
- package/dist/cli/memory-flow-admission.js +7 -3
- package/dist/cli/memory-init.js +5 -4
- package/dist/cli/native-hook-ingress.js +5 -1
- package/dist/cli/native-hook-preparation.js +31 -4
- package/dist/cli/run-cli.js +274 -19
- package/dist/cli.js +17 -3
- package/dist/domain/delivery-flows.js +8 -0
- package/dist/domain/distill-flow.js +20 -0
- package/dist/domain/interactive-flow.js +1 -0
- package/dist/domain/memory-fix-flow.js +2 -0
- package/dist/domain/review-fix-flow.js +2 -0
- package/dist/domain/source-snapshot-flows.js +4 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +18 -3
- package/dist/harness-runtime/bin/dd-grok.mjs +15 -5
- package/dist/harness-runtime/bin/dd-zcode.mjs +15 -7
- package/dist/harness-runtime/lib/agy-error-boundary.mjs +18 -10
- package/dist/harness-runtime/lib/agy-native-inspection.mjs +12 -3
- package/dist/harness-runtime/lib/agy-native-inventory.mjs +19 -0
- package/dist/harness-runtime/lib/agy-terminal-envelope.mjs +13 -6
- package/dist/harness-runtime/lib/agy-transcript-stream.mjs +54 -0
- package/dist/harness-runtime/lib/daemon-observation.d.mts +1 -0
- package/dist/harness-runtime/lib/daemon-observation.mjs +12 -4
- package/dist/harness-runtime/lib/daemon-operations.mjs +20 -5
- package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +371 -114
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +43 -3
- package/dist/harness-runtime/lib/dd-codex.mjs +36 -4
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +25 -11
- package/dist/harness-runtime/lib/dd-droid.mjs +124 -11
- package/dist/harness-runtime/lib/dd-grok-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +137 -25
- package/dist/harness-runtime/lib/dd-grok.mjs +4 -2
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +115 -17
- package/dist/harness-runtime/lib/dd-opencode.mjs +10 -7
- package/dist/harness-runtime/lib/dd-zcode-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +115 -22
- package/dist/harness-runtime/lib/dd-zcode.mjs +75 -36
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +2 -1
- package/dist/harness-runtime/lib/delegation-instructions.mjs +15 -5
- package/dist/harness-runtime/lib/delivery-command.mjs +84 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +0 -19
- package/dist/harness-runtime/lib/droid-observation.mjs +9 -5
- package/dist/harness-runtime/lib/lifecycle-contract.d.mts +1 -0
- package/dist/harness-runtime/lib/lifecycle-contract.mjs +2 -1
- package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -2
- package/dist/harness-runtime/lib/native-hook-command.mjs +63 -8
- package/dist/harness-runtime/lib/native-streams.mjs +3 -10
- package/dist/harness-runtime/lib/opencode-plugin.mjs +36 -9
- package/dist/harness-runtime/lib/operation-errors.mjs +13 -0
- package/dist/harness-runtime/lib/operation-progress.mjs +6 -2
- package/dist/harness-runtime/lib/process-json.mjs +4 -5
- package/dist/harness-runtime/lib/tool-observations.mjs +20 -10
- package/dist/harness-runtime/lib/zcode-native-hook-ownership.mjs +109 -0
- package/dist/schemas/code-work-result.schema.json +2 -1
- package/dist/schemas/delivery-stage-result.schema.json +286 -0
- package/dist/schemas/distill-stage-result.schema.json +667 -0
- package/dist/schemas/distill-work-result.schema.json +428 -0
- package/dist/schemas/execution-bundle.schema.json +6 -2
- package/dist/schemas/interactive-stage-result.schema.json +47 -0
- package/dist/schemas/interactive-work-result.schema.json +63 -0
- package/dist/schemas/mb-fix-stage-result.schema.json +93 -0
- package/dist/schemas/project-flow-pack-manifest.schema.json +2 -2
- package/dist/schemas/review-fix-stage-result.schema.json +210 -0
- package/dist/schemas/run-control-receipt.schema.json +1 -0
- package/dist/services/assigned-lifecycle.js +382 -0
- package/dist/services/check-runner-evidence.js +45 -8
- package/dist/services/cli-operation-classifier.js +6 -1
- package/dist/services/code-checks.js +46 -29
- package/dist/services/codex-hook-delivery.js +11 -2
- package/dist/services/controller-child-repair.js +95 -0
- package/dist/services/controller-execution.js +4 -1
- package/dist/services/controller-fanout.js +19 -9
- package/dist/services/controller-memory-worker.js +6 -4
- package/dist/services/controller-result-authority.js +122 -0
- package/dist/services/dashboard.js +4 -2
- package/dist/services/delivery-access.js +123 -0
- package/dist/services/delivery-approval.js +68 -0
- package/dist/services/delivery-artifact.js +86 -0
- package/dist/services/delivery-checks.js +178 -0
- package/dist/services/delivery-execution.js +181 -0
- package/dist/services/delivery-intents.js +212 -0
- package/dist/services/delivery-operation.js +81 -0
- package/dist/services/delivery-policy.js +91 -0
- package/dist/services/delivery-procedures.js +43 -0
- package/dist/services/delivery-request.js +99 -0
- package/dist/services/delivery-result.js +144 -0
- package/dist/services/delivery-stage-commands.js +26 -0
- package/dist/services/delivery-stage-entry.js +56 -0
- package/dist/services/delivery-stage-handlers.js +135 -0
- package/dist/services/delivery-stage-results.js +94 -0
- package/dist/services/descriptor-coordinator-session.js +3 -1
- package/dist/services/descriptor-run-start.js +24 -4
- package/dist/services/descriptor-stage-coordinator.js +78 -10
- package/dist/services/descriptor-stage-settlement.js +59 -14
- package/dist/services/diagnostic-repair-intake.js +123 -0
- package/dist/services/diagnostic-repair-source-intake.js +102 -0
- package/dist/services/diagnostic-report.js +30 -0
- package/dist/services/diagnostic-run-bundle.js +48 -9
- package/dist/services/diagnostic-snapshot.js +2 -2
- package/dist/services/diagnostic-stage-handlers.js +3 -2
- package/dist/services/diagnostic-stage-results.js +26 -0
- package/dist/services/diagnostic-start.js +56 -6
- package/dist/services/diagnostic-work-collection.js +3 -3
- package/dist/services/distill-document-publication.js +280 -0
- package/dist/services/distill-promotion.js +197 -0
- package/dist/services/distill-publication-custody.js +162 -0
- package/dist/services/distill-stage-handlers.js +249 -0
- package/dist/services/distill-stage-results.js +105 -0
- package/dist/services/distill-user-inputs.js +4 -0
- package/dist/services/distill-work-batch.js +42 -0
- package/dist/services/distill-work-collection.js +74 -0
- package/dist/services/distill-work-registration.js +93 -0
- package/dist/services/distill-work-results.js +98 -0
- package/dist/services/eval-snapshots.js +17 -7
- package/dist/services/external-work-launch.js +8 -2
- package/dist/services/flow-catalog.js +24 -0
- package/dist/services/frozen-engine.js +19 -2
- package/dist/services/generic-execution.js +4 -3
- package/dist/services/harness-adapter.js +2 -14
- package/dist/services/hook-memory-routing.js +1 -1
- package/dist/services/hooks.js +31 -6
- package/dist/services/hybrid-contracts.js +6 -1
- package/dist/services/installed-flow-source.js +37 -2
- package/dist/services/interactive-attached-session.js +215 -0
- package/dist/services/interactive-baseline-custody.js +54 -0
- package/dist/services/interactive-before-versions.js +148 -0
- package/dist/services/interactive-bundle-data.js +85 -0
- package/dist/services/interactive-checks.js +179 -0
- package/dist/services/interactive-execution.js +33 -0
- package/dist/services/interactive-finish-integration.js +236 -0
- package/dist/services/interactive-git-baseline.js +230 -0
- package/dist/services/interactive-git-dispatch.js +278 -0
- package/dist/services/interactive-input-delivery.js +82 -0
- package/dist/services/interactive-mutation-scope.js +231 -0
- package/dist/services/interactive-request.js +287 -0
- package/dist/services/interactive-run-bundle.js +109 -0
- package/dist/services/interactive-stage-entry.js +41 -0
- package/dist/services/interactive-stage-handlers.js +289 -0
- package/dist/services/interactive-stage-observation.js +136 -0
- package/dist/services/interactive-stage-results.js +111 -0
- package/dist/services/interactive-user-inputs.js +142 -0
- package/dist/services/interactive-work-batch.js +43 -0
- package/dist/services/interactive-work-collection.js +119 -0
- package/dist/services/interactive-work-completion.js +180 -0
- package/dist/services/interactive-work-context.js +249 -0
- package/dist/services/interactive-work-history.js +112 -0
- package/dist/services/interactive-work-registration.js +124 -0
- package/dist/services/interactive-work-results.js +115 -0
- package/dist/services/interactive-work-start.js +155 -0
- package/dist/services/lifecycle-invocations.js +250 -33
- package/dist/services/managed-daemon-binding.js +52 -1
- package/dist/services/memory-compose-handler.js +2 -22
- package/dist/services/memory-finish-integration.js +58 -17
- package/dist/services/memory-fix-finish-integration.js +129 -0
- package/dist/services/memory-fix-git-dispatch.js +139 -0
- package/dist/services/memory-fix-intake-custody.js +11 -0
- package/dist/services/memory-fix-intake-settlement.js +127 -0
- package/dist/services/memory-fix-publication.js +196 -0
- package/dist/services/memory-fix-repair-admission.js +116 -0
- package/dist/services/memory-fix-request.js +141 -0
- package/dist/services/memory-fix-run-bundle.js +83 -0
- package/dist/services/memory-fix-scope.js +78 -0
- package/dist/services/memory-fix-sources.js +85 -0
- package/dist/services/memory-fix-stage-entry.js +24 -0
- package/dist/services/memory-fix-stage-handlers.js +282 -0
- package/dist/services/memory-fix-stage-results.js +106 -0
- package/dist/services/memory-fix-workspace.js +127 -0
- package/dist/services/memory-flow-user-inputs.js +88 -0
- package/dist/services/memory-init-contexts.js +55 -5
- package/dist/services/memory-init-controller-start.js +20 -4
- package/dist/services/memory-layer.js +7 -3
- package/dist/services/memory-native-review-copy.js +27 -3
- package/dist/services/memory-repair-document-delta.js +1 -0
- package/dist/services/project-flow-source.js +195 -0
- package/dist/services/project-git-integration.js +92 -0
- package/dist/services/repair-intents.js +2 -7
- package/dist/services/retained-native-hook.js +274 -0
- package/dist/services/review-fix-handoff.js +82 -0
- package/dist/services/review-fix-intake-custody.js +17 -0
- package/dist/services/review-fix-stage-entry.js +43 -0
- package/dist/services/review-fix-stage-handlers.js +176 -0
- package/dist/services/review-fix-stage-results.js +86 -0
- package/dist/services/run-control-worker.js +42 -1
- package/dist/services/run-control.js +40 -5
- package/dist/services/run-controller-adapter.js +37 -3
- package/dist/services/run-controller-process.js +25 -4
- package/dist/services/run-controller-recovery.js +46 -6
- package/dist/services/run-controller-state.js +3 -2
- package/dist/services/run-controller.js +257 -44
- package/dist/services/run-recovery.js +31 -2
- package/dist/services/run-timeline.js +59 -0
- package/dist/services/runs.js +18 -31
- package/dist/services/sessions.js +2 -1
- package/dist/services/stage-handlers.js +55 -3
- package/dist/services/stage-lifecycle.js +4 -3
- package/dist/services/stage-pause.js +26 -9
- package/dist/services/stage-report-renderer.js +2 -6
- package/dist/services/technical-notes.js +31 -0
- package/dist/services/verification-matrix.js +1 -1
- package/dist/services/version-status.js +1 -1
- package/dist/services/vnext-code-review.js +74 -5
- package/dist/services/vnext-code.js +8 -32
- package/dist/services/vnext-fanout.js +53 -8
- package/dist/services/vnext-merge.js +10 -5
- package/dist/services/vnext-plan-review.js +1 -1
- package/dist/services/vnext-plan.js +1 -1
- package/dist/services/vnext-protocolize.js +1 -1
- package/dist/services/vnext-specify.js +10 -5
- package/dist/services/work-registry.js +255 -69
- package/dist/services/worktrees.js +7 -4
- package/dist/storage/database.js +2 -1
- package/dist/verification-components.json +82 -0
- package/package.json +3 -2
- package/tools/verification-jobs.mjs +35 -10
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { AppError } from "../shared/errors.js";
|
|
5
|
+
import { PreparedInteractiveGitBaseline } from "./interactive-git-baseline.js";
|
|
6
|
+
import { ObservedMemorySources } from "./memory-source-observations.js";
|
|
7
|
+
import { memorySourceKey } from "./memory-stage-results.js";
|
|
8
|
+
import { localEnvironmentAllowed } from "./workspace-files.js";
|
|
9
|
+
function refuse(reason) {
|
|
10
|
+
throw new AppError("interactive_before_versions_invalid", "Interactive must retain selected original versions before changing them", 2, { reason, effect: "no_effect" });
|
|
11
|
+
}
|
|
12
|
+
function safe(name) {
|
|
13
|
+
return typeof name === "string" && !!name && name.length <= 4096 && !path.posix.isAbsolute(name) && !path.win32.isAbsolute(name)
|
|
14
|
+
&& !name.includes("\\") && !name.includes("\0") && name.split("/").every(part => part && part !== "." && part !== ".."
|
|
15
|
+
&& ![".git", ".dd-flow", "node_modules"].includes(part.normalize("NFC").toLowerCase()) && localEnvironmentAllowed(part));
|
|
16
|
+
}
|
|
17
|
+
function absent(root, logical) {
|
|
18
|
+
let current = root;
|
|
19
|
+
const parts = logical.split("/");
|
|
20
|
+
for (let index = 0; index < parts.length; index++) {
|
|
21
|
+
current = path.join(current, parts[index]);
|
|
22
|
+
try {
|
|
23
|
+
const stat = fs.lstatSync(current);
|
|
24
|
+
if (index === parts.length - 1 || !stat.isDirectory())
|
|
25
|
+
refuse("unobserved_existing_source");
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (error.code === "ENOENT")
|
|
29
|
+
return;
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function rootIdentity(baseline) {
|
|
35
|
+
const value = baseline.snapshot(), actual = fs.lstatSync(value.workspace);
|
|
36
|
+
if (!actual.isDirectory() || fs.realpathSync(value.workspace) !== value.workspace
|
|
37
|
+
|| actual.dev !== value.workspace_identity.dev || actual.ino !== value.workspace_identity.ino)
|
|
38
|
+
refuse("workspace_identity_changed");
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
/** Before-image evidence, not write approval. Reuses the existing bounded,
|
|
42
|
+
* private-file-aware source reader. No backup file, Git object, RUN or project
|
|
43
|
+
* mutation is created; the request/Work owner must retain these bytes first. */
|
|
44
|
+
export class PreparedInteractiveBeforeVersions {
|
|
45
|
+
sha256;
|
|
46
|
+
#value;
|
|
47
|
+
#baseline;
|
|
48
|
+
#observed;
|
|
49
|
+
constructor(value, baseline, observed) {
|
|
50
|
+
if (Buffer.byteLength(JSON.stringify(value)) > 96 * 1024 * 1024)
|
|
51
|
+
refuse("before_versions_exceed_bound");
|
|
52
|
+
this.#value = structuredClone(value);
|
|
53
|
+
this.#baseline = baseline;
|
|
54
|
+
this.#observed = observed;
|
|
55
|
+
this.sha256 = createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
56
|
+
Object.freeze(this);
|
|
57
|
+
}
|
|
58
|
+
static capture(baseline, paths) {
|
|
59
|
+
if (!(baseline instanceof PreparedInteractiveGitBaseline) || !Array.isArray(paths) || paths.length > 20_000
|
|
60
|
+
|| paths.some(name => !safe(name)) || new Set(paths).size !== paths.length)
|
|
61
|
+
refuse("invalid_selected_paths");
|
|
62
|
+
const source = rootIdentity(baseline), byPath = new Map(source.entries.map(item => [item.path, item]));
|
|
63
|
+
const entries = [], files = [];
|
|
64
|
+
for (const name of [...paths].sort()) {
|
|
65
|
+
const before = byPath.get(name);
|
|
66
|
+
if (!before || before.kind === "missing") {
|
|
67
|
+
absent(source.workspace, name);
|
|
68
|
+
entries.push({ path: name, kind: "missing", mode: null, sha256: null, base64: null });
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
if (before.kind !== "file")
|
|
72
|
+
refuse("regular_file_scope_required");
|
|
73
|
+
const actual = fs.lstatSync(path.join(source.workspace, name));
|
|
74
|
+
if (!actual.isFile() || actual.nlink !== 1 || (actual.mode & 0o7777) !== before.mode)
|
|
75
|
+
refuse("before_version_changed");
|
|
76
|
+
files.push(name);
|
|
77
|
+
entries.push({ path: name, kind: "file", mode: before.mode, sha256: before.sha256, base64: null });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const observed = ObservedMemorySources.capture({ workspace: source.workspace }, files.map(name => ({ root: "workspace", path: name })));
|
|
81
|
+
const hashes = observed.hashes();
|
|
82
|
+
for (const item of entries.filter(item => item.kind === "file")) {
|
|
83
|
+
if (hashes.get(memorySourceKey("workspace", item.path)) !== item.sha256)
|
|
84
|
+
refuse("before_version_changed");
|
|
85
|
+
item.base64 = observed.bytes({ root: "workspace", path: item.path }).toString("base64");
|
|
86
|
+
}
|
|
87
|
+
const prepared = new PreparedInteractiveBeforeVersions({ schema_id: "dd-flow/interactive-before-versions@1",
|
|
88
|
+
baseline_sha256: baseline.sha256, workspace: source.workspace, entries }, baseline, observed);
|
|
89
|
+
prepared.revalidate();
|
|
90
|
+
return prepared;
|
|
91
|
+
}
|
|
92
|
+
snapshot() { return structuredClone(this.#value); }
|
|
93
|
+
bytes() { return Buffer.from(JSON.stringify(this.#value)); }
|
|
94
|
+
reference() { return { path: "runtime/before-versions.json", sha256: this.sha256 }; }
|
|
95
|
+
revalidate() {
|
|
96
|
+
rootIdentity(this.#baseline);
|
|
97
|
+
this.#observed.revalidate();
|
|
98
|
+
for (const item of this.#value.entries) {
|
|
99
|
+
if (item.kind === "missing")
|
|
100
|
+
absent(this.#value.workspace, item.path);
|
|
101
|
+
else {
|
|
102
|
+
const stat = fs.lstatSync(path.join(this.#value.workspace, item.path));
|
|
103
|
+
if (!stat.isFile() || stat.nlink !== 1 || (stat.mode & 0o7777) !== item.mode)
|
|
104
|
+
refuse("before_version_changed");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** Historical parsing against an independently selected retained baseline.
|
|
110
|
+
* Never re-read the now-edited project to reconstruct its original content. */
|
|
111
|
+
export function restoreInteractiveBeforeVersions(bytes, expectedSha256, baseline) {
|
|
112
|
+
if (!(baseline instanceof PreparedInteractiveGitBaseline) || !bytes.length || bytes.length > 96 * 1024 * 1024
|
|
113
|
+
|| createHash("sha256").update(bytes).digest("hex") !== expectedSha256)
|
|
114
|
+
refuse("retained_before_versions_changed");
|
|
115
|
+
let value;
|
|
116
|
+
try {
|
|
117
|
+
value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return refuse("invalid_retained_before_versions");
|
|
121
|
+
}
|
|
122
|
+
const original = baseline.snapshot(), byPath = new Map(original.entries.map(item => [item.path, item]));
|
|
123
|
+
if (!value || Object.keys(value).sort().join() !== "baseline_sha256,entries,schema_id,workspace"
|
|
124
|
+
|| value.schema_id !== "dd-flow/interactive-before-versions@1" || value.baseline_sha256 !== baseline.sha256
|
|
125
|
+
|| value.workspace !== original.workspace || !Array.isArray(value.entries) || value.entries.length > 20_000)
|
|
126
|
+
refuse("retained_before_scope_changed");
|
|
127
|
+
let previous = "", total = 0;
|
|
128
|
+
for (const item of value.entries) {
|
|
129
|
+
if (!item || Object.keys(item).sort().join() !== "base64,kind,mode,path,sha256" || !safe(item.path) || item.path <= previous)
|
|
130
|
+
refuse("invalid_retained_before_versions");
|
|
131
|
+
const before = byPath.get(item.path);
|
|
132
|
+
if (item.kind === "missing") {
|
|
133
|
+
if (before && before.kind !== "missing" || item.mode !== null || item.sha256 !== null || item.base64 !== null)
|
|
134
|
+
refuse("retained_absence_changed");
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
if (item.kind !== "file" || before?.kind !== "file" || item.mode !== before.mode || item.sha256 !== before.sha256 || typeof item.base64 !== "string")
|
|
138
|
+
refuse("retained_original_changed");
|
|
139
|
+
const content = Buffer.from(item.base64, "base64");
|
|
140
|
+
total += content.length;
|
|
141
|
+
if (content.length > 16 * 1024 * 1024 || total > 64 * 1024 * 1024 || content.toString("base64") !== item.base64
|
|
142
|
+
|| createHash("sha256").update(content).digest("hex") !== before.sha256)
|
|
143
|
+
refuse("retained_original_changed");
|
|
144
|
+
}
|
|
145
|
+
previous = item.path;
|
|
146
|
+
}
|
|
147
|
+
return structuredClone(value);
|
|
148
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { AppError } from "../shared/errors.js";
|
|
3
|
+
const partLimit = 1024 * 1024, totalLimit = 32 * partLimit;
|
|
4
|
+
const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
5
|
+
function refuse(reason) {
|
|
6
|
+
throw new AppError("interactive_bundle_data_invalid", "Interactive data must fit the existing retained bundle limits", 2, { reason, effect: "no_effect" });
|
|
7
|
+
}
|
|
8
|
+
function pathFor(logical, ordinal) { return `${logical}.parts/${String(ordinal).padStart(4, "0")}.bin`; }
|
|
9
|
+
function supported(logical) { if (!["runtime/git-baseline.json", "runtime/before-versions.json", "runtime/work-context.json"].includes(logical))
|
|
10
|
+
refuse("unsupported_data_role"); }
|
|
11
|
+
/** Packaging only, not admission. Keep ordinary small JSON unchanged; large
|
|
12
|
+
* before-images use explicit immutable parts instead of widening global limits. */
|
|
13
|
+
export function putInteractiveBundleData(blobs, logical, input) {
|
|
14
|
+
supported(logical);
|
|
15
|
+
if (!input.length || input.byteLength > totalLimit)
|
|
16
|
+
refuse("unbounded_data");
|
|
17
|
+
const bytes = Buffer.from(input), additions = new Map();
|
|
18
|
+
const sha256 = hash(bytes);
|
|
19
|
+
if (bytes.length <= partLimit)
|
|
20
|
+
additions.set(logical, bytes);
|
|
21
|
+
else {
|
|
22
|
+
const parts = [];
|
|
23
|
+
for (let offset = 0; offset < bytes.length; offset += partLimit) {
|
|
24
|
+
const part = Buffer.from(bytes.subarray(offset, offset + partLimit)), file = pathFor(logical, parts.length);
|
|
25
|
+
additions.set(file, part);
|
|
26
|
+
parts.push({ path: file, bytes: part.length, sha256: hash(part) });
|
|
27
|
+
}
|
|
28
|
+
const index = { schema_id: "dd-flow/interactive-bundle-parts@1", logical_path: logical, bytes: bytes.length, sha256, parts };
|
|
29
|
+
additions.set(logical, Buffer.from(JSON.stringify(index)));
|
|
30
|
+
}
|
|
31
|
+
const existing = new Set([...blobs.keys()].map(name => name.toLowerCase()));
|
|
32
|
+
if ([...additions.keys()].some(name => existing.has(name.toLowerCase())))
|
|
33
|
+
refuse("data_path_collision");
|
|
34
|
+
const total = [...blobs.values(), ...additions.values()].reduce((sum, value) => sum + value.length, 0);
|
|
35
|
+
if (total > totalLimit)
|
|
36
|
+
refuse("bundle_budget_exceeded");
|
|
37
|
+
for (const [file, content] of additions)
|
|
38
|
+
blobs.set(file, content);
|
|
39
|
+
return { sha256, reference: { path: logical, sha256: hash(additions.get(logical)) } };
|
|
40
|
+
}
|
|
41
|
+
/** Reads only the selected inventory, never nearby files or current project
|
|
42
|
+
* bytes. For multipart data reference hashes the physical index; sha256 hashes
|
|
43
|
+
* the reconstructed logical data. They intentionally are not interchangeable. */
|
|
44
|
+
export function readInteractiveBundleData(bundle, logical) {
|
|
45
|
+
supported(logical);
|
|
46
|
+
const physical = (file) => {
|
|
47
|
+
const refs = bundle.manifest.inventory.filter(item => item.path === file);
|
|
48
|
+
if (refs.length !== 1)
|
|
49
|
+
refuse("data_reference_required");
|
|
50
|
+
const bytes = bundle.blob(file);
|
|
51
|
+
if (!bytes.length || bytes.length > partLimit || hash(bytes) !== refs[0].sha256)
|
|
52
|
+
refuse("data_part_changed");
|
|
53
|
+
return { bytes, reference: refs[0] };
|
|
54
|
+
};
|
|
55
|
+
const root = physical(logical);
|
|
56
|
+
let value;
|
|
57
|
+
try {
|
|
58
|
+
value = JSON.parse(root.bytes.toString("utf8"));
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
value = null;
|
|
62
|
+
}
|
|
63
|
+
if (!value || typeof value !== "object" || value.schema_id !== "dd-flow/interactive-bundle-parts@1")
|
|
64
|
+
return { ...root, sha256: root.reference.sha256 };
|
|
65
|
+
const index = value;
|
|
66
|
+
if (Object.keys(index).sort().join() !== "bytes,logical_path,parts,schema_id,sha256" || index.logical_path !== logical
|
|
67
|
+
|| !Number.isSafeInteger(index.bytes) || index.bytes <= partLimit || index.bytes > totalLimit
|
|
68
|
+
|| typeof index.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(index.sha256) || !Array.isArray(index.parts)
|
|
69
|
+
|| index.parts.length !== Math.ceil(index.bytes / partLimit))
|
|
70
|
+
refuse("invalid_data_index");
|
|
71
|
+
const parts = [];
|
|
72
|
+
for (const [ordinal, part] of index.parts.entries()) {
|
|
73
|
+
if (!part || Object.keys(part).sort().join() !== "bytes,path,sha256" || part.path !== pathFor(logical, ordinal)
|
|
74
|
+
|| part.bytes !== Math.min(partLimit, index.bytes - ordinal * partLimit))
|
|
75
|
+
refuse("invalid_data_part");
|
|
76
|
+
const selected = physical(part.path);
|
|
77
|
+
if (selected.reference.sha256 !== part.sha256 || selected.bytes.length !== part.bytes)
|
|
78
|
+
refuse("data_part_changed");
|
|
79
|
+
parts.push(selected.bytes);
|
|
80
|
+
}
|
|
81
|
+
const bytes = Buffer.concat(parts);
|
|
82
|
+
if (bytes.length !== index.bytes || hash(bytes) !== index.sha256)
|
|
83
|
+
refuse("logical_data_changed");
|
|
84
|
+
return { bytes, sha256: index.sha256, reference: root.reference };
|
|
85
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { AppError } from "../shared/errors.js";
|
|
2
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
3
|
+
import { appendAudit } from "./audit.js";
|
|
4
|
+
import { readInteractiveStageScope } from "./interactive-stage-entry.js";
|
|
5
|
+
import { readDescriptorStageInput } from "./descriptor-stage-coordinator.js";
|
|
6
|
+
import { assertPreparedRunBundleSqlAuthority } from "./runs.js";
|
|
7
|
+
import { PreparedInteractiveWorkspaceChanges } from "./interactive-mutation-scope.js";
|
|
8
|
+
import { readInteractiveWorkGit } from "./interactive-work-start.js";
|
|
9
|
+
import { readInteractiveWorkHistory } from "./interactive-work-history.js";
|
|
10
|
+
import { codeCheckProfileHash, projectPolicyCheckDeclarations, runCodeChecks, PreparedCheckEvidence, validateCheckDeclaration } from "./code-checks.js";
|
|
11
|
+
import { memoryCheckEnvironment } from "./memory-verification-checks.js";
|
|
12
|
+
function refuse(reason) {
|
|
13
|
+
throw new AppError("interactive_checks_invalid", "Interactive checks require retained obligations and the exact current patch", 2, { reason, effect: "no_effect" });
|
|
14
|
+
}
|
|
15
|
+
const event = "interactive.check_plan";
|
|
16
|
+
function latest(context, scope) {
|
|
17
|
+
const row = context.db.get("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type=? AND json_extract(payload_json,'$.run_id')=? ORDER BY id DESC LIMIT 1", [scope.projectId, event, scope.runId]);
|
|
18
|
+
if (!row)
|
|
19
|
+
return null;
|
|
20
|
+
if (Buffer.byteLength(row.payload_json) > 1024 * 1024)
|
|
21
|
+
refuse("unbounded_check_plan");
|
|
22
|
+
const value = JSON.parse(row.payload_json);
|
|
23
|
+
if (value.run_id !== scope.runId || value.plan?.run_id !== scope.runId || value.plan.schema_id !== "dd-flow/interactive-check-plan@1"
|
|
24
|
+
|| value.sha256 !== hybridDigest(value.plan) || !Array.isArray(value.plan.checks))
|
|
25
|
+
refuse("changed_check_plan");
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
/** One existing aggregate check runner and receipt store. Selection is retained
|
|
29
|
+
* before execution, independently of the Stage proposal being assessed. */
|
|
30
|
+
export class PreparedInteractiveChecks {
|
|
31
|
+
sha256;
|
|
32
|
+
#selected;
|
|
33
|
+
#packet;
|
|
34
|
+
#changes;
|
|
35
|
+
#history;
|
|
36
|
+
#plan;
|
|
37
|
+
#environment;
|
|
38
|
+
constructor(selected, packet, changes, history, plan, environment) {
|
|
39
|
+
this.#selected = selected;
|
|
40
|
+
this.#packet = packet;
|
|
41
|
+
this.#changes = changes;
|
|
42
|
+
this.#history = history;
|
|
43
|
+
this.#plan = structuredClone(plan);
|
|
44
|
+
this.#environment = environment;
|
|
45
|
+
this.sha256 = hybridDigest(plan);
|
|
46
|
+
Object.freeze(this);
|
|
47
|
+
}
|
|
48
|
+
static prepare(context, input) {
|
|
49
|
+
if (context.db.isOwnedWriteTransaction?.())
|
|
50
|
+
refuse("preparation_outside_writer_required");
|
|
51
|
+
if (!["hardening", "review"].includes(input.stage) || !Array.isArray(input.additionalChecks) || input.additionalChecks.length > 4096)
|
|
52
|
+
refuse("invalid_check_selection");
|
|
53
|
+
const selected = readInteractiveStageScope(context, input), { scope, bundle, run } = selected;
|
|
54
|
+
const packet = readDescriptorStageInput(context, scope, bundle), frozen = bundle.execution.snapshot().scope, git = readInteractiveWorkGit(bundle);
|
|
55
|
+
if (!run.run_root)
|
|
56
|
+
refuse("run_root_required");
|
|
57
|
+
if (selected.selected.needs_scope_refresh)
|
|
58
|
+
refuse("current_scope_required");
|
|
59
|
+
const checks = [...projectPolicyCheckDeclarations(frozen.workspace, "code"), ...projectPolicyCheckDeclarations(frozen.workspace, "readiness")];
|
|
60
|
+
const ids = new Set(checks.map(check => check.id));
|
|
61
|
+
for (const extra of input.additionalChecks) {
|
|
62
|
+
if (!extra || Object.keys(extra).sort().join() !== "command,id,purpose" || typeof extra.id !== "string" || !/^CHK-[A-Za-z0-9-]{1,64}$/.test(extra.id)
|
|
63
|
+
|| ids.has(extra.id) || [extra.command, extra.purpose].some(value => typeof value !== "string" || !value.trim() || value.includes("\0") || Buffer.byteLength(value) > 16384))
|
|
64
|
+
refuse("invalid_check_selection");
|
|
65
|
+
ids.add(extra.id);
|
|
66
|
+
checks.push({ ...extra, source: "plan", availability: "available", run_at: "readiness" });
|
|
67
|
+
}
|
|
68
|
+
checks.sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
69
|
+
for (const check of checks)
|
|
70
|
+
validateCheckDeclaration(frozen.workspace, check);
|
|
71
|
+
if (!checks.length && (typeof input.noChecksReason !== "string" || !input.noChecksReason.trim() || input.noChecksReason.includes("\0") || Buffer.byteLength(input.noChecksReason) > 16384))
|
|
72
|
+
refuse("explicit_no_checks_reason_required");
|
|
73
|
+
if (checks.length && input.noChecksReason !== undefined)
|
|
74
|
+
refuse("no_checks_reason_with_checks");
|
|
75
|
+
const changes = PreparedInteractiveWorkspaceChanges.prepare(context, scope, bundle, { projectRoot: frozen.projectRoot,
|
|
76
|
+
workspace: frozen.workspace, gitExecutable: git.git_executable, env: context.env });
|
|
77
|
+
const plan = { schema_id: "dd-flow/interactive-check-plan@1", run_id: input.runId, stage: input.stage, attempt: packet.attempt,
|
|
78
|
+
bundle_sha256: bundle.sha256, input_sha256: packet.inputSha256, user_inputs_sha256: selected.selected.inputs.sha256,
|
|
79
|
+
scope_revision: selected.selected.revision, file_changes_sha256: hybridDigest(changes.snapshot().delta.changes),
|
|
80
|
+
profile_sha256: codeCheckProfileHash(frozen.workspace, checks), checks, no_checks_reason: checks.length ? null : input.noChecksReason };
|
|
81
|
+
if (Buffer.byteLength(JSON.stringify({ run_id: plan.run_id, sha256: "a".repeat(64), plan })) > 1024 * 1024)
|
|
82
|
+
refuse("unbounded_check_plan");
|
|
83
|
+
const prepared = new PreparedInteractiveChecks(selected, packet, changes, hybridDigest(readInteractiveWorkHistory(context, scope, bundle)), plan, memoryCheckEnvironment(context, scope.projectId, scope.runId));
|
|
84
|
+
prepared.revalidate(context);
|
|
85
|
+
return prepared;
|
|
86
|
+
}
|
|
87
|
+
static restore(context, input) {
|
|
88
|
+
const selected = readInteractiveStageScope(context, input), retained = latest(context, selected.scope);
|
|
89
|
+
if (!retained || retained.plan.stage !== input.stage)
|
|
90
|
+
refuse("retained_check_plan_required");
|
|
91
|
+
const prepared = this.prepare(context, { ...input, additionalChecks: retained.plan.checks.filter(check => check.source === "plan")
|
|
92
|
+
.map(({ id, command, purpose }) => ({ id, command, purpose })),
|
|
93
|
+
...(retained.plan.no_checks_reason === null ? {} : { noChecksReason: retained.plan.no_checks_reason }) });
|
|
94
|
+
if (prepared.sha256 !== retained.sha256)
|
|
95
|
+
refuse("retained_check_plan_changed");
|
|
96
|
+
return prepared;
|
|
97
|
+
}
|
|
98
|
+
/** Carry declared obligations into the initial critic's current Stage. The
|
|
99
|
+
* common evidence owner still rejects changed task/patch/policy or missing
|
|
100
|
+
* receipts; this does not execute or bless an author's test claims. */
|
|
101
|
+
static prepareReview(context, input) {
|
|
102
|
+
const selected = readInteractiveStageScope(context, { ...input, stage: "review" }), retained = latest(context, selected.scope);
|
|
103
|
+
if (!retained)
|
|
104
|
+
return null;
|
|
105
|
+
return this.prepare(context, { ...input, stage: "review", additionalChecks: retained.plan.checks.filter(check => check.source === "plan")
|
|
106
|
+
.map(({ id, command, purpose }) => ({ id, command, purpose })),
|
|
107
|
+
...(retained.plan.no_checks_reason === null ? {} : { noChecksReason: retained.plan.no_checks_reason }) });
|
|
108
|
+
}
|
|
109
|
+
value() { return structuredClone(this.#plan); }
|
|
110
|
+
checkInput() {
|
|
111
|
+
const { bundle_sha256, user_inputs_sha256, scope_revision, file_changes_sha256, profile_sha256, checks } = this.#plan;
|
|
112
|
+
return { projectId: this.#selected.scope.projectId, runId: this.#plan.run_id, runHome: this.#packet.runRoot,
|
|
113
|
+
workspaceRoot: this.#selected.bundle.execution.snapshot().scope.workspace, scope: "aggregate",
|
|
114
|
+
checks: structuredClone(checks), expectedProfileHash: profile_sha256,
|
|
115
|
+
// Stage custody changes do not by themselves invalidate checks of the
|
|
116
|
+
// same task/patch/policy. The common runner still owns reuse eligibility.
|
|
117
|
+
evidenceContext: `interactive:${hybridDigest({ bundle_sha256, user_inputs_sha256, scope_revision, file_changes_sha256, profile_sha256, checks })}`,
|
|
118
|
+
artifactDir: `${this.#selected.bundle.descriptor.stage(this.#plan.stage).directory}/checks/${this.sha256}`,
|
|
119
|
+
...(this.#environment ? { checkEnvironment: this.#environment } : {}) };
|
|
120
|
+
}
|
|
121
|
+
assertSelection(context) {
|
|
122
|
+
this.#selected.assertCustody(context);
|
|
123
|
+
const { scope, bundle } = this.#selected, run = assertPreparedRunBundleSqlAuthority(context, scope, bundle), index = JSON.parse(run.index_json);
|
|
124
|
+
if (run.status !== "running" || index.current_stage !== this.#plan.stage
|
|
125
|
+
|| hybridDigest(readDescriptorStageInput(context, scope, bundle)) !== hybridDigest(this.#packet)
|
|
126
|
+
|| hybridDigest(readInteractiveWorkHistory(context, scope, bundle)) !== this.#history)
|
|
127
|
+
refuse("check_selection_changed");
|
|
128
|
+
const previous = latest(context, scope);
|
|
129
|
+
if (previous && previous.plan.user_inputs_sha256 === this.#plan.user_inputs_sha256
|
|
130
|
+
&& previous.plan.checks.some(check => !this.#plan.checks.some(current => hybridDigest(current) === hybridDigest(check))))
|
|
131
|
+
refuse("check_obligation_dropped");
|
|
132
|
+
}
|
|
133
|
+
revalidate(context) {
|
|
134
|
+
this.#changes.revalidate();
|
|
135
|
+
readInteractiveWorkGit(this.#selected.bundle);
|
|
136
|
+
if (codeCheckProfileHash(this.checkInput().workspaceRoot, this.#plan.checks) !== this.#plan.profile_sha256
|
|
137
|
+
|| hybridDigest(memoryCheckEnvironment(context, this.#selected.scope.projectId, this.#plan.run_id) ?? null) !== hybridDigest(this.#environment ?? null))
|
|
138
|
+
refuse("check_policy_changed");
|
|
139
|
+
context.db.readSnapshot(() => this.assertSelection(context));
|
|
140
|
+
}
|
|
141
|
+
retain(context) {
|
|
142
|
+
this.revalidate(context);
|
|
143
|
+
return context.db.writeTransaction(() => {
|
|
144
|
+
this.#changes.assertCustody(context);
|
|
145
|
+
this.assertSelection(context);
|
|
146
|
+
if (latest(context, this.#selected.scope)?.sha256 === this.sha256)
|
|
147
|
+
return { changed: false };
|
|
148
|
+
appendAudit(context, { projectId: this.#selected.scope.projectId, eventType: event, payload: { run_id: this.#plan.run_id, sha256: this.sha256, plan: this.value() } });
|
|
149
|
+
return { changed: true };
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
assertCustody(context) {
|
|
153
|
+
this.#changes.assertCustody(context);
|
|
154
|
+
this.assertSelection(context);
|
|
155
|
+
if (latest(context, this.#selected.scope)?.sha256 !== this.sha256)
|
|
156
|
+
refuse("retained_check_plan_required");
|
|
157
|
+
}
|
|
158
|
+
async execute(context) {
|
|
159
|
+
this.retain(context);
|
|
160
|
+
if (!this.#plan.checks.length)
|
|
161
|
+
return [];
|
|
162
|
+
const receipts = await runCodeChecks(context, this.checkInput());
|
|
163
|
+
try {
|
|
164
|
+
this.revalidate(context);
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
if (error instanceof AppError)
|
|
168
|
+
error.details.effect = "committed";
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
return receipts;
|
|
172
|
+
}
|
|
173
|
+
evidence(context) {
|
|
174
|
+
this.revalidate(context);
|
|
175
|
+
if (latest(context, this.#selected.scope)?.sha256 !== this.sha256)
|
|
176
|
+
refuse("retained_check_plan_required");
|
|
177
|
+
return this.#plan.checks.length ? PreparedCheckEvidence.prepare(context, this.checkInput()) : null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { AppError } from "../shared/errors.js";
|
|
2
|
+
import { FrozenGenericExecution } from "./generic-execution.js";
|
|
3
|
+
import { PreparedInteractiveAttachedSession } from "./interactive-attached-session.js";
|
|
4
|
+
import { prepareOperatorProfile, resolveOperatorProfile } from "./operator-profiles.js";
|
|
5
|
+
/** Fresh interactive selection only; continuation restores the retained bundle
|
|
6
|
+
* rather than re-reading current defaults or claiming a different Session. */
|
|
7
|
+
export function prepareInteractiveExecution(context, input) {
|
|
8
|
+
const { scope, flow, profiles, selection } = input;
|
|
9
|
+
if (flow.descriptor.id !== "interactive" || Object.hasOwn(selection, "attachedSessionReceipt")) {
|
|
10
|
+
throw new AppError("interactive_execution_invalid", "Interactive execution must use its registered flow and CLI-owned Session binding", 2, { effect: "no_effect" });
|
|
11
|
+
}
|
|
12
|
+
const mode = selection.execution ?? "attached";
|
|
13
|
+
const stageSessionMode = selection.stageSessionMode ?? (mode === "attached" ? "same_session" : "new_session");
|
|
14
|
+
let attached = null;
|
|
15
|
+
let receipts = new Map();
|
|
16
|
+
const receiptBytes = new Map();
|
|
17
|
+
if (mode === "attached") {
|
|
18
|
+
if (!input.source && !input.launchArgs)
|
|
19
|
+
throw new AppError("interactive_attached_session_required", "Interactive defaults to the current trusted Session; select managed execution explicitly when unavailable", 2, { phase: "prepare", effect: "no_effect", next_action: "provide_matching_attached_session_or_choose_managed" });
|
|
20
|
+
if (stageSessionMode !== "same_session")
|
|
21
|
+
throw new AppError("interactive_execution_invalid", "An attached root cannot claim a new physical Session per Stage; choose managed execution for that route", 2, { effect: "no_effect" });
|
|
22
|
+
const profile = prepareOperatorProfile(typeof selection.rootProfile === "object" ? selection.rootProfile : resolveOperatorProfile(profiles, selection.rootProfile));
|
|
23
|
+
attached = input.source ? PreparedInteractiveAttachedSession.prepare(context, scope, profile, input.source)
|
|
24
|
+
: PreparedInteractiveAttachedSession.prepareCurrent(context, scope, profile, input.launchArgs);
|
|
25
|
+
const evidence = attached.evidence();
|
|
26
|
+
receipts = evidence.receipts;
|
|
27
|
+
receiptBytes.set(evidence.reference.sha256, evidence.bytes);
|
|
28
|
+
}
|
|
29
|
+
const execution = FrozenGenericExecution.prepare(flow, profiles, { ...selection, execution: mode,
|
|
30
|
+
stageSessionMode, delegation: selection.delegation ?? { mode: "external", max_parallel: 1 },
|
|
31
|
+
...(attached ? { attachedSessionReceipt: attached.evidence().reference } : {}) }, scope, receipts);
|
|
32
|
+
return { execution, attached, receipts, receiptBytes };
|
|
33
|
+
}
|