@deksden-com/dd-flow-cli 0.9.0-beta.152 → 0.9.0-beta.155
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/dist/build-info.json +8 -8
- package/dist/cli/command-inputs.js +33 -3
- package/dist/cli/command-metadata.js +73 -2
- package/dist/cli/delivery.js +108 -0
- package/dist/cli/diagnostic-invocation.js +23 -7
- package/dist/cli/diagnostic.js +14 -3
- package/dist/cli/help.js +117 -1
- package/dist/cli/hook-ingress.js +36 -49
- package/dist/cli/interactive-invocation.js +33 -0
- package/dist/cli/interactive.js +114 -0
- package/dist/cli/memory-fix-invocation.js +52 -0
- package/dist/cli/memory-fix.js +100 -0
- package/dist/cli/memory-flow-admission.js +7 -3
- package/dist/cli/memory-init.js +5 -4
- package/dist/cli/native-hook-ingress.js +5 -1
- package/dist/cli/native-hook-preparation.js +31 -4
- package/dist/cli/run-cli.js +273 -19
- package/dist/cli.js +17 -3
- package/dist/domain/delivery-flows.js +8 -0
- package/dist/domain/distill-flow.js +20 -0
- package/dist/domain/interactive-flow.js +1 -0
- package/dist/domain/memory-fix-flow.js +2 -0
- package/dist/domain/review-fix-flow.js +2 -0
- package/dist/domain/source-snapshot-flows.js +4 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +18 -3
- package/dist/harness-runtime/bin/dd-grok.mjs +15 -5
- package/dist/harness-runtime/bin/dd-zcode.mjs +15 -7
- package/dist/harness-runtime/lib/agy-error-boundary.mjs +18 -10
- package/dist/harness-runtime/lib/agy-native-inspection.mjs +12 -3
- package/dist/harness-runtime/lib/agy-native-inventory.mjs +19 -0
- package/dist/harness-runtime/lib/agy-terminal-envelope.mjs +13 -6
- package/dist/harness-runtime/lib/agy-transcript-stream.mjs +54 -0
- package/dist/harness-runtime/lib/daemon-observation.d.mts +1 -0
- package/dist/harness-runtime/lib/daemon-observation.mjs +12 -4
- package/dist/harness-runtime/lib/daemon-operations.mjs +20 -5
- package/dist/harness-runtime/lib/dd-agy-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +371 -114
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +43 -3
- package/dist/harness-runtime/lib/dd-codex.mjs +36 -4
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +25 -11
- package/dist/harness-runtime/lib/dd-droid.mjs +124 -11
- package/dist/harness-runtime/lib/dd-grok-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +137 -25
- package/dist/harness-runtime/lib/dd-grok.mjs +4 -2
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +114 -16
- package/dist/harness-runtime/lib/dd-opencode.mjs +10 -7
- package/dist/harness-runtime/lib/dd-zcode-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +115 -22
- package/dist/harness-runtime/lib/dd-zcode.mjs +75 -36
- package/dist/harness-runtime/lib/delegation-instructions.d.mts +1 -0
- package/dist/harness-runtime/lib/delegation-instructions.mjs +10 -3
- package/dist/harness-runtime/lib/delivery-command.mjs +84 -0
- package/dist/harness-runtime/lib/driver-recovery.mjs +0 -19
- package/dist/harness-runtime/lib/droid-observation.mjs +9 -5
- package/dist/harness-runtime/lib/lifecycle-contract.d.mts +1 -0
- package/dist/harness-runtime/lib/lifecycle-contract.mjs +2 -1
- package/dist/harness-runtime/lib/native-hook-command.d.mts +3 -2
- package/dist/harness-runtime/lib/native-hook-command.mjs +63 -8
- package/dist/harness-runtime/lib/native-streams.mjs +3 -10
- package/dist/harness-runtime/lib/opencode-plugin.mjs +36 -9
- package/dist/harness-runtime/lib/operation-errors.mjs +13 -0
- package/dist/harness-runtime/lib/operation-progress.mjs +6 -2
- package/dist/harness-runtime/lib/process-json.mjs +4 -5
- package/dist/harness-runtime/lib/tool-observations.mjs +20 -10
- package/dist/harness-runtime/lib/zcode-native-hook-ownership.mjs +109 -0
- package/dist/schemas/code-work-result.schema.json +2 -1
- package/dist/schemas/delivery-stage-result.schema.json +286 -0
- package/dist/schemas/distill-stage-result.schema.json +667 -0
- package/dist/schemas/distill-work-result.schema.json +428 -0
- package/dist/schemas/execution-bundle.schema.json +6 -2
- package/dist/schemas/interactive-stage-result.schema.json +47 -0
- package/dist/schemas/interactive-work-result.schema.json +63 -0
- package/dist/schemas/mb-fix-stage-result.schema.json +93 -0
- package/dist/schemas/project-flow-pack-manifest.schema.json +2 -2
- package/dist/schemas/review-fix-stage-result.schema.json +210 -0
- package/dist/schemas/run-control-receipt.schema.json +1 -0
- package/dist/services/assigned-lifecycle.js +382 -0
- package/dist/services/check-runner-evidence.js +45 -8
- package/dist/services/cli-operation-classifier.js +6 -1
- package/dist/services/code-checks.js +46 -29
- package/dist/services/codex-hook-delivery.js +11 -2
- package/dist/services/controller-execution.js +4 -1
- package/dist/services/controller-fanout.js +9 -4
- package/dist/services/controller-memory-worker.js +6 -4
- package/dist/services/controller-result-authority.js +122 -0
- package/dist/services/dashboard.js +4 -2
- package/dist/services/delivery-access.js +123 -0
- package/dist/services/delivery-approval.js +68 -0
- package/dist/services/delivery-artifact.js +86 -0
- package/dist/services/delivery-checks.js +178 -0
- package/dist/services/delivery-execution.js +181 -0
- package/dist/services/delivery-intents.js +212 -0
- package/dist/services/delivery-operation.js +81 -0
- package/dist/services/delivery-policy.js +91 -0
- package/dist/services/delivery-procedures.js +43 -0
- package/dist/services/delivery-request.js +99 -0
- package/dist/services/delivery-result.js +144 -0
- package/dist/services/delivery-stage-commands.js +26 -0
- package/dist/services/delivery-stage-entry.js +56 -0
- package/dist/services/delivery-stage-handlers.js +135 -0
- package/dist/services/delivery-stage-results.js +94 -0
- package/dist/services/descriptor-coordinator-session.js +3 -1
- package/dist/services/descriptor-run-start.js +24 -4
- package/dist/services/descriptor-stage-coordinator.js +78 -10
- package/dist/services/descriptor-stage-settlement.js +59 -14
- package/dist/services/diagnostic-repair-intake.js +123 -0
- package/dist/services/diagnostic-repair-source-intake.js +102 -0
- package/dist/services/diagnostic-report.js +30 -0
- package/dist/services/diagnostic-run-bundle.js +48 -9
- package/dist/services/diagnostic-snapshot.js +2 -2
- package/dist/services/diagnostic-stage-handlers.js +3 -2
- package/dist/services/diagnostic-stage-results.js +26 -0
- package/dist/services/diagnostic-start.js +56 -6
- package/dist/services/diagnostic-work-collection.js +3 -3
- package/dist/services/distill-document-publication.js +280 -0
- package/dist/services/distill-promotion.js +197 -0
- package/dist/services/distill-publication-custody.js +162 -0
- package/dist/services/distill-stage-handlers.js +249 -0
- package/dist/services/distill-stage-results.js +105 -0
- package/dist/services/distill-user-inputs.js +4 -0
- package/dist/services/distill-work-batch.js +42 -0
- package/dist/services/distill-work-collection.js +74 -0
- package/dist/services/distill-work-registration.js +93 -0
- package/dist/services/distill-work-results.js +98 -0
- package/dist/services/eval-snapshots.js +17 -7
- package/dist/services/external-work-launch.js +8 -2
- package/dist/services/flow-catalog.js +24 -0
- package/dist/services/frozen-engine.js +19 -2
- package/dist/services/generic-execution.js +4 -3
- package/dist/services/harness-adapter.js +2 -14
- package/dist/services/hook-memory-routing.js +1 -1
- package/dist/services/hooks.js +31 -6
- package/dist/services/hybrid-contracts.js +6 -1
- package/dist/services/installed-flow-source.js +37 -2
- package/dist/services/interactive-attached-session.js +215 -0
- package/dist/services/interactive-baseline-custody.js +54 -0
- package/dist/services/interactive-before-versions.js +148 -0
- package/dist/services/interactive-bundle-data.js +85 -0
- package/dist/services/interactive-checks.js +179 -0
- package/dist/services/interactive-execution.js +33 -0
- package/dist/services/interactive-finish-integration.js +236 -0
- package/dist/services/interactive-git-baseline.js +230 -0
- package/dist/services/interactive-git-dispatch.js +278 -0
- package/dist/services/interactive-input-delivery.js +82 -0
- package/dist/services/interactive-mutation-scope.js +231 -0
- package/dist/services/interactive-request.js +287 -0
- package/dist/services/interactive-run-bundle.js +109 -0
- package/dist/services/interactive-stage-entry.js +41 -0
- package/dist/services/interactive-stage-handlers.js +289 -0
- package/dist/services/interactive-stage-observation.js +136 -0
- package/dist/services/interactive-stage-results.js +111 -0
- package/dist/services/interactive-user-inputs.js +142 -0
- package/dist/services/interactive-work-batch.js +43 -0
- package/dist/services/interactive-work-collection.js +119 -0
- package/dist/services/interactive-work-completion.js +180 -0
- package/dist/services/interactive-work-context.js +249 -0
- package/dist/services/interactive-work-history.js +112 -0
- package/dist/services/interactive-work-registration.js +124 -0
- package/dist/services/interactive-work-results.js +115 -0
- package/dist/services/interactive-work-start.js +155 -0
- package/dist/services/lifecycle-invocations.js +214 -24
- package/dist/services/managed-daemon-binding.js +52 -1
- package/dist/services/memory-compose-handler.js +2 -22
- package/dist/services/memory-finish-integration.js +58 -17
- package/dist/services/memory-fix-finish-integration.js +129 -0
- package/dist/services/memory-fix-git-dispatch.js +139 -0
- package/dist/services/memory-fix-intake-custody.js +11 -0
- package/dist/services/memory-fix-intake-settlement.js +127 -0
- package/dist/services/memory-fix-publication.js +196 -0
- package/dist/services/memory-fix-repair-admission.js +116 -0
- package/dist/services/memory-fix-request.js +141 -0
- package/dist/services/memory-fix-run-bundle.js +83 -0
- package/dist/services/memory-fix-scope.js +78 -0
- package/dist/services/memory-fix-sources.js +85 -0
- package/dist/services/memory-fix-stage-entry.js +24 -0
- package/dist/services/memory-fix-stage-handlers.js +282 -0
- package/dist/services/memory-fix-stage-results.js +106 -0
- package/dist/services/memory-fix-workspace.js +127 -0
- package/dist/services/memory-flow-user-inputs.js +88 -0
- package/dist/services/memory-init-contexts.js +55 -5
- package/dist/services/memory-init-controller-start.js +20 -4
- package/dist/services/memory-layer.js +7 -3
- package/dist/services/memory-native-review-copy.js +27 -3
- package/dist/services/memory-repair-document-delta.js +1 -0
- package/dist/services/project-flow-source.js +195 -0
- package/dist/services/project-git-integration.js +92 -0
- package/dist/services/repair-intents.js +2 -7
- package/dist/services/retained-native-hook.js +245 -0
- package/dist/services/review-fix-handoff.js +82 -0
- package/dist/services/review-fix-intake-custody.js +17 -0
- package/dist/services/review-fix-stage-entry.js +43 -0
- package/dist/services/review-fix-stage-handlers.js +176 -0
- package/dist/services/review-fix-stage-results.js +86 -0
- package/dist/services/run-control-worker.js +42 -1
- package/dist/services/run-control.js +40 -5
- package/dist/services/run-controller-adapter.js +37 -3
- package/dist/services/run-controller-process.js +12 -4
- package/dist/services/run-controller-recovery.js +46 -6
- package/dist/services/run-controller-state.js +3 -2
- package/dist/services/run-controller.js +209 -43
- package/dist/services/run-recovery.js +31 -2
- package/dist/services/run-timeline.js +59 -0
- package/dist/services/runs.js +18 -31
- package/dist/services/sessions.js +2 -1
- package/dist/services/stage-handlers.js +55 -3
- package/dist/services/stage-lifecycle.js +4 -3
- package/dist/services/stage-pause.js +26 -9
- package/dist/services/stage-report-renderer.js +2 -6
- package/dist/services/technical-notes.js +31 -0
- package/dist/services/verification-matrix.js +1 -1
- package/dist/services/version-status.js +1 -1
- package/dist/services/vnext-code-review.js +74 -5
- package/dist/services/vnext-code.js +8 -32
- package/dist/services/vnext-fanout.js +53 -8
- package/dist/services/vnext-merge.js +10 -5
- package/dist/services/vnext-plan-review.js +1 -1
- package/dist/services/vnext-plan.js +1 -1
- package/dist/services/vnext-protocolize.js +1 -1
- package/dist/services/vnext-specify.js +10 -5
- package/dist/services/work-registry.js +252 -66
- package/dist/services/worktrees.js +7 -4
- package/dist/storage/database.js +2 -1
- package/dist/verification-components.json +82 -0
- package/package.json +3 -2
- package/tools/verification-jobs.mjs +35 -10
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { AppError } from "../shared/errors.js";
|
|
3
|
+
import { requireProjectByRoot } from "./projects.js";
|
|
4
|
+
import { appendAudit } from "./audit.js";
|
|
5
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
6
|
+
import { prepareInteractiveRunBundle } from "./interactive-run-bundle.js";
|
|
7
|
+
import { PreparedInteractiveAttachedSession } from "./interactive-attached-session.js";
|
|
8
|
+
import { RetainedExecutionBundle } from "./execution-bundles.js";
|
|
9
|
+
import { runBundleAuthorityMetadata } from "./runs.js";
|
|
10
|
+
import { readRunStartRequest, runStartPayloadLimit } from "./run-start-custody.js";
|
|
11
|
+
import { nextDescriptorRunId, PreparedDescriptorRunStart } from "./descriptor-run-start.js";
|
|
12
|
+
import { bundledHandlerContext } from "./stage-handlers.js";
|
|
13
|
+
import { PreparedProjectFlowSource } from "./project-flow-source.js";
|
|
14
|
+
import { readInteractiveBundleData } from "./interactive-bundle-data.js";
|
|
15
|
+
import { PreparedInteractiveGitBaseline } from "./interactive-git-baseline.js";
|
|
16
|
+
import { PreparedInteractiveBeforeVersions, restoreInteractiveBeforeVersions } from "./interactive-before-versions.js";
|
|
17
|
+
import { observeMemoryGitPolicySelection } from "./memory-init-git-policy.js";
|
|
18
|
+
import { readBoundedReleaseFile } from "./release-source.js";
|
|
19
|
+
const event = "interactive.request", hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
20
|
+
const digest = (value) => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
21
|
+
function refuse(reason) {
|
|
22
|
+
throw new AppError("interactive_request_invalid", "Interactive requires the original retained request and its own RUN", 2, { reason, effect: "no_effect" });
|
|
23
|
+
}
|
|
24
|
+
function selection(input) {
|
|
25
|
+
return hybridDigest({ source: input.source.sha256, execution: input.execution.sha256, engine: input.engine.manifestSha256,
|
|
26
|
+
baseline: input.baseline.sha256, before: input.beforeVersions.sha256, request: input.requestText,
|
|
27
|
+
git: input.gitExecutable, policy: input.gitPolicy, receipts: [...input.receipts ?? []],
|
|
28
|
+
...(input.launchSelection !== undefined ? { launch_selection: input.launchSelection } : {}),
|
|
29
|
+
receipt_bytes: [...input.receiptBytes ?? []].map(([key, bytes]) => [key, Buffer.from(bytes).toString("base64")]) });
|
|
30
|
+
}
|
|
31
|
+
/** Allocation can choose a different free RUN ID without changing any selected
|
|
32
|
+
* task data. Only the engine binding's RUN field is allocation-local. */
|
|
33
|
+
function bundleSelection(manifest, scope) {
|
|
34
|
+
return hybridDigest({ flow_id: manifest.flow_id, scope, descriptor: manifest.descriptor,
|
|
35
|
+
execution_settings: manifest.execution_settings, profile_settings: manifest.profile_settings,
|
|
36
|
+
engine_manifest: manifest.engine_manifest, engine_binding_path: manifest.engine_binding.path,
|
|
37
|
+
canon: manifest.canon, project_pack: manifest.project_pack, bootstrap_receipts: manifest.bootstrap_receipts,
|
|
38
|
+
inventory: manifest.inventory.filter(item => item.path !== manifest.engine_binding.path) });
|
|
39
|
+
}
|
|
40
|
+
function decode(value, limit) {
|
|
41
|
+
if (typeof value !== "string" || value.length > Math.ceil(limit / 3) * 4)
|
|
42
|
+
refuse("invalid_request_bytes");
|
|
43
|
+
const bytes = Buffer.from(value, "base64");
|
|
44
|
+
if (!bytes.length || bytes.length > limit || bytes.toString("base64") !== value)
|
|
45
|
+
refuse("invalid_request_bytes");
|
|
46
|
+
return bytes;
|
|
47
|
+
}
|
|
48
|
+
function json(bytes) {
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return refuse("invalid_request_json");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** SQL/data only, including when called by the allocation writer. Full engine,
|
|
57
|
+
* compiled schema and physical workspace admission happens outside the writer. */
|
|
58
|
+
export function readInteractiveRequest(context, { projectRoot, requestId }) {
|
|
59
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,95}$/.test(requestId))
|
|
60
|
+
refuse("invalid_request_id");
|
|
61
|
+
const scope = { projectRoot, requestId }, project = context.db.get("SELECT id FROM projects WHERE root=? AND status='active'", [projectRoot]);
|
|
62
|
+
if (!project)
|
|
63
|
+
refuse("project_not_registered");
|
|
64
|
+
const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type=? AND json_extract(payload_json,'$.scope.requestId')=? LIMIT 2", [project.id, event, requestId]);
|
|
65
|
+
if (!rows.length)
|
|
66
|
+
return null;
|
|
67
|
+
if (rows.length !== 1 || Buffer.byteLength(rows[0].payload_json) > runStartPayloadLimit)
|
|
68
|
+
refuse("ambiguous_or_unbounded_request");
|
|
69
|
+
const value = json(Buffer.from(rows[0].payload_json));
|
|
70
|
+
if (!value || Object.keys(value).sort().join() !== "admission_json,admission_sha256,blobs,bound_at,bundle_selection_sha256,manifest_base64,manifest_sha256,project_id,schema_id,scope,selection_sha256,source_sha256"
|
|
71
|
+
|| value.schema_id !== "dd-flow/interactive-request@1" || value.project_id !== project.id || hybridDigest(value.scope) !== hybridDigest(scope)
|
|
72
|
+
|| ![value.selection_sha256, value.bundle_selection_sha256, value.source_sha256, value.manifest_sha256, value.admission_sha256].every(digest)
|
|
73
|
+
|| typeof value.bound_at !== "string" || !Number.isFinite(Date.parse(value.bound_at))
|
|
74
|
+
|| typeof value.admission_json !== "string" || Buffer.byteLength(value.admission_json) > 1024 * 1024
|
|
75
|
+
|| !Array.isArray(value.blobs) || value.blobs.length > 4096)
|
|
76
|
+
refuse("request_scope_changed");
|
|
77
|
+
const bytes = decode(value.manifest_base64, 1024 * 1024), manifest = json(bytes);
|
|
78
|
+
const admission = json(Buffer.from(value.admission_json));
|
|
79
|
+
if (hash(bytes) !== value.manifest_sha256 || hybridDigest(admission) !== value.admission_sha256
|
|
80
|
+
|| !manifest || manifest.schema_id !== "dd-flow/execution-bundle@1" || manifest.flow_id !== "interactive"
|
|
81
|
+
|| !Array.isArray(manifest.inventory) || manifest.inventory.length !== value.blobs.length
|
|
82
|
+
|| manifest.inventory.some(item => !item || typeof item.path !== "string" || !digest(item.sha256))
|
|
83
|
+
|| typeof manifest.engine_binding?.path !== "string"
|
|
84
|
+
|| !Array.isArray(manifest.bootstrap_receipts) || manifest.bootstrap_receipts.length
|
|
85
|
+
|| !admission?.executionScope || admission.executionScope.projectRoot !== projectRoot || admission.executionScope.requestId !== requestId
|
|
86
|
+
|| bundleSelection(manifest, admission.executionScope) !== value.bundle_selection_sha256)
|
|
87
|
+
refuse("retained_bundle_changed");
|
|
88
|
+
const refs = new Map(manifest.inventory.map(item => [item.path, item.sha256])), names = new Set();
|
|
89
|
+
let total = 0;
|
|
90
|
+
for (const item of value.blobs) {
|
|
91
|
+
if (!item || Object.keys(item).sort().join() !== "base64,path" || typeof item.path !== "string" || names.has(item.path))
|
|
92
|
+
refuse("retained_blob_inventory_changed");
|
|
93
|
+
const content = decode(item.base64, 1024 * 1024);
|
|
94
|
+
total += content.length;
|
|
95
|
+
if (total > 32 * 1024 * 1024 || hash(content) !== refs.get(item.path))
|
|
96
|
+
refuse("retained_blob_changed");
|
|
97
|
+
names.add(item.path);
|
|
98
|
+
}
|
|
99
|
+
return value;
|
|
100
|
+
}
|
|
101
|
+
/** Compare supplied launch selectors with retained raw intent, not today's
|
|
102
|
+
* aliases/defaults or a newly observed equivalent model. Omission preserves the
|
|
103
|
+
* original selection. This reads only the independently validated request. */
|
|
104
|
+
export function assertInteractiveLaunchReplay(context, scope, supplied) {
|
|
105
|
+
const request = readInteractiveRequest(context, scope);
|
|
106
|
+
if (!request)
|
|
107
|
+
refuse("original_request_required");
|
|
108
|
+
const blob = (name) => {
|
|
109
|
+
const selected = request.blobs.find(item => item.path === name);
|
|
110
|
+
if (!selected)
|
|
111
|
+
refuse("original_launch_selection_required");
|
|
112
|
+
return Buffer.from(selected.base64, "base64");
|
|
113
|
+
};
|
|
114
|
+
const admission = json(Buffer.from(request.admission_json));
|
|
115
|
+
if (supplied.workspace !== undefined && supplied.workspace !== admission.executionScope.workspace
|
|
116
|
+
|| supplied.requestText !== undefined && !Buffer.from(supplied.requestText).equals(blob("runtime/source-request.md")))
|
|
117
|
+
refuse("launch_inputs_changed");
|
|
118
|
+
if (supplied.gitPolicy !== undefined) {
|
|
119
|
+
const frozen = json(blob("runtime/git-policy.json"));
|
|
120
|
+
if (hybridDigest(supplied.gitPolicy) !== hybridDigest({ route: frozen.policy.route, integrationBranch: frozen.policy.integration_branch }))
|
|
121
|
+
refuse("launch_inputs_changed");
|
|
122
|
+
}
|
|
123
|
+
const selectors = supplied.launchSelection;
|
|
124
|
+
if (selectors && Object.keys(selectors).length) {
|
|
125
|
+
const original = json(blob("runtime/launch-selection.json"));
|
|
126
|
+
if (Object.entries(selectors).some(([key, value]) => !Object.hasOwn(original, key)
|
|
127
|
+
|| hybridDigest(value) !== hybridDigest(original[key])))
|
|
128
|
+
refuse("launch_inputs_changed");
|
|
129
|
+
}
|
|
130
|
+
return request;
|
|
131
|
+
}
|
|
132
|
+
/** Called with compiled bundle admission already complete. No filesystem or
|
|
133
|
+
* handler/schema loading: this fence also protects the allocation transaction. */
|
|
134
|
+
export function assertInteractiveRequestBundle(context, projectId, bundle) {
|
|
135
|
+
const frozen = bundle.execution.snapshot(), request = readInteractiveRequest(context, frozen.scope);
|
|
136
|
+
if (!request || request.project_id !== projectId || bundle.manifest.flow_id !== "interactive"
|
|
137
|
+
|| bundleSelection(bundle.manifest, frozen.scope) !== request.bundle_selection_sha256)
|
|
138
|
+
refuse("request_bundle_changed");
|
|
139
|
+
const binding = json(bundle.blob(bundle.manifest.engine_binding.path));
|
|
140
|
+
const original = request.blobs.find(item => item.path === bundle.manifest.engine_binding.path);
|
|
141
|
+
const initial = original ? json(Buffer.from(original.base64, "base64")) : null;
|
|
142
|
+
if (!initial || binding.run_id !== bundle.manifest.run_id || binding.bound_at !== request.bound_at
|
|
143
|
+
|| hybridDigest({ ...binding, run_id: null }) !== hybridDigest({ ...initial, run_id: null }))
|
|
144
|
+
refuse("allocation_binding_changed");
|
|
145
|
+
}
|
|
146
|
+
/** Freeze once before allocation. Attached hook claim and request insertion
|
|
147
|
+
* share one writer; replay never creates another claim, RUN or input capture. */
|
|
148
|
+
export function retainInteractiveRequest(context, input, attached) {
|
|
149
|
+
if (context.db.isOwnedWriteTransaction?.())
|
|
150
|
+
refuse("preparation_outside_writer_required");
|
|
151
|
+
const frozen = input.execution.snapshot(), scope = { projectRoot: frozen.scope.projectRoot, requestId: frozen.scope.requestId };
|
|
152
|
+
const project = requireProjectByRoot(context, scope.projectRoot), chosen = selection(input), prior = readInteractiveRequest(context, scope);
|
|
153
|
+
if (prior) {
|
|
154
|
+
if (prior.selection_sha256 !== chosen)
|
|
155
|
+
refuse("request_changed");
|
|
156
|
+
return prior;
|
|
157
|
+
}
|
|
158
|
+
if (readRunStartRequest(context, project.id, scope.requestId))
|
|
159
|
+
refuse("foreign_run_request");
|
|
160
|
+
if (frozen.settings.execution === "attached") {
|
|
161
|
+
if (!(attached instanceof PreparedInteractiveAttachedSession) || hybridDigest(attached.snapshot().scope) !== hybridDigest(frozen.scope)
|
|
162
|
+
|| hybridDigest(attached.evidence().reference) !== hybridDigest(frozen.settings.attached_session_receipt))
|
|
163
|
+
refuse("trusted_attached_selection_required");
|
|
164
|
+
}
|
|
165
|
+
else if (attached)
|
|
166
|
+
refuse("foreign_attached_selection");
|
|
167
|
+
const bundle = prepareInteractiveRunBundle(context, input), metadata = runBundleAuthorityMetadata(bundle);
|
|
168
|
+
const value = { schema_id: "dd-flow/interactive-request@1", project_id: project.id, scope,
|
|
169
|
+
selection_sha256: chosen, bundle_selection_sha256: bundleSelection(bundle.manifest, frozen.scope), source_sha256: input.source.sha256,
|
|
170
|
+
bound_at: input.boundAt, manifest_base64: bundle.manifestBytes().toString("base64"), manifest_sha256: bundle.sha256,
|
|
171
|
+
admission_json: metadata.admissionJson, admission_sha256: metadata.anchor.admission_sha256,
|
|
172
|
+
blobs: bundle.manifest.inventory.map(ref => ({ path: ref.path, base64: bundle.blob(ref.path).toString("base64") })) };
|
|
173
|
+
if (Buffer.byteLength(JSON.stringify(value)) > runStartPayloadLimit)
|
|
174
|
+
refuse("request_exceeds_bound");
|
|
175
|
+
return context.db.writeTransaction(() => {
|
|
176
|
+
const current = readInteractiveRequest(context, scope);
|
|
177
|
+
if (current) {
|
|
178
|
+
if (current.selection_sha256 !== chosen)
|
|
179
|
+
refuse("request_changed");
|
|
180
|
+
return current;
|
|
181
|
+
}
|
|
182
|
+
if (readRunStartRequest(context, project.id, scope.requestId))
|
|
183
|
+
refuse("foreign_run_request");
|
|
184
|
+
attached?.retainPreparedSql(context);
|
|
185
|
+
appendAudit(context, { projectId: project.id, eventType: event, payload: value });
|
|
186
|
+
const retained = readInteractiveRequest(context, scope);
|
|
187
|
+
if (!retained)
|
|
188
|
+
refuse("request_readback_missing");
|
|
189
|
+
return retained;
|
|
190
|
+
}, "interactive.retainRequest");
|
|
191
|
+
}
|
|
192
|
+
/** Restore initial data from request custody, never current defaults, files or
|
|
193
|
+
* global canon. Generic admission still checks actual selected engine bytes. */
|
|
194
|
+
function restoreBundle(request) {
|
|
195
|
+
const admission = json(Buffer.from(request.admission_json));
|
|
196
|
+
if (!Array.isArray(admission.receipts) || admission.receipts.some(item => !Array.isArray(item) || item.length !== 2 || !digest(item[0]))
|
|
197
|
+
|| new Set(admission.receipts.map(item => item[0])).size !== admission.receipts.length)
|
|
198
|
+
refuse("invalid_retained_execution_receipts");
|
|
199
|
+
return RetainedExecutionBundle.admit(decode(request.manifest_base64, 1024 * 1024), request.manifest_sha256, new Map(request.blobs.map(item => [item.path, Buffer.from(item.base64, "base64")])), { ...admission, receipts: new Map(admission.receipts), descriptorContext: bundledHandlerContext(new Map()) });
|
|
200
|
+
}
|
|
201
|
+
function bindRunId(bundle, runId) {
|
|
202
|
+
if (bundle.manifest.run_id === runId)
|
|
203
|
+
return bundle;
|
|
204
|
+
const manifest = structuredClone(bundle.manifest), admission = bundle.admission();
|
|
205
|
+
const binding = json(bundle.blob(manifest.engine_binding.path));
|
|
206
|
+
binding.run_id = runId;
|
|
207
|
+
const bytes = Buffer.from(JSON.stringify(binding)), sha256 = hash(bytes);
|
|
208
|
+
const blobs = new Map(manifest.inventory.map(ref => [ref.path, bundle.blob(ref.path)]));
|
|
209
|
+
blobs.set(manifest.engine_binding.path, bytes);
|
|
210
|
+
manifest.run_id = runId;
|
|
211
|
+
manifest.engine_binding.sha256 = sha256;
|
|
212
|
+
for (const ref of manifest.inventory)
|
|
213
|
+
if (ref.path === manifest.engine_binding.path)
|
|
214
|
+
ref.sha256 = sha256;
|
|
215
|
+
const encoded = Buffer.from(JSON.stringify(manifest));
|
|
216
|
+
return RetainedExecutionBundle.admit(encoded, hash(encoded), blobs, { ...admission, runId, engineBindingSha256: sha256 });
|
|
217
|
+
}
|
|
218
|
+
function revalidateBeforeAllocation(context, request, bundle) {
|
|
219
|
+
const scope = bundle.execution.snapshot().scope, manifestBytes = bundle.blob("project-pack/manifest.json");
|
|
220
|
+
const pack = json(manifestBytes);
|
|
221
|
+
if (!Array.isArray(pack.included_files))
|
|
222
|
+
refuse("invalid_retained_project_pack");
|
|
223
|
+
const source = PreparedProjectFlowSource.restoreRetained({ schema_id: "dd-flow/project-flow-source@1",
|
|
224
|
+
scope: { projectRoot: scope.projectRoot, workspace: scope.workspace, flowId: "interactive" },
|
|
225
|
+
blobs: ["project-pack/manifest.json", ...pack.included_files].map(name => ({ path: name, base64: bundle.blob(name).toString("base64") })) }, request.source_sha256);
|
|
226
|
+
if (hybridDigest(source.projectPackProvenance()) !== hybridDigest(bundle.manifest.project_pack))
|
|
227
|
+
refuse("retained_project_pack_changed");
|
|
228
|
+
bundle.engine().admitInteractiveFlow(source);
|
|
229
|
+
if (bundle.execution.settings.execution === "attached") {
|
|
230
|
+
const attached = PreparedInteractiveAttachedSession.restore(context, scope);
|
|
231
|
+
if (hybridDigest(attached.evidence().reference) !== hybridDigest(bundle.execution.settings.attached_session_receipt))
|
|
232
|
+
refuse("retained_attached_selection_changed");
|
|
233
|
+
}
|
|
234
|
+
revalidateInteractiveEntryWorkspace(context, bundle);
|
|
235
|
+
}
|
|
236
|
+
/** Shared RUN-start physical fence. Git porcelain can remain unchanged while
|
|
237
|
+
* an already-dirty file's content changes, so status alone is insufficient. */
|
|
238
|
+
export function revalidateInteractiveEntryWorkspace(context, bundle) {
|
|
239
|
+
if (context.db.isOwnedWriteTransaction?.())
|
|
240
|
+
refuse("physical_revalidation_outside_writer_required");
|
|
241
|
+
if (bundle.manifest.flow_id !== "interactive")
|
|
242
|
+
refuse("interactive_bundle_required");
|
|
243
|
+
const scope = bundle.execution.snapshot().scope;
|
|
244
|
+
const policy = json(bundle.blob("runtime/git-policy.json"));
|
|
245
|
+
if (hybridDigest(policy.scope) !== hybridDigest({ requestId: scope.requestId, projectRoot: scope.projectRoot, workspaceRoot: scope.workspace })
|
|
246
|
+
|| hash(readBoundedReleaseFile(policy.git_executable, 64 * 1024 * 1024)) !== policy.git_sha256)
|
|
247
|
+
refuse("retained_git_selection_changed");
|
|
248
|
+
const observed = observeMemoryGitPolicySelection(context.env, { scope: policy.scope, gitExecutable: policy.git_executable,
|
|
249
|
+
selection: { route: policy.policy.route, integrationBranch: policy.policy.integration_branch } });
|
|
250
|
+
if (hybridDigest(observed) !== hybridDigest(policy.policy))
|
|
251
|
+
refuse("git_policy_changed_before_allocation");
|
|
252
|
+
const retained = readInteractiveBundleData(bundle, "runtime/git-baseline.json"), before = readInteractiveBundleData(bundle, "runtime/before-versions.json");
|
|
253
|
+
const baseline = PreparedInteractiveGitBaseline.restore(retained.bytes, retained.sha256);
|
|
254
|
+
const versions = restoreInteractiveBeforeVersions(before.bytes, before.sha256, baseline);
|
|
255
|
+
// Physical equality check only. Retained before-images remain authoritative;
|
|
256
|
+
// later current bytes cannot replace the original evidence on retry.
|
|
257
|
+
const current = (() => {
|
|
258
|
+
try {
|
|
259
|
+
return PreparedInteractiveBeforeVersions.capture(baseline, versions.entries.map(item => item.path));
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
if (error instanceof AppError)
|
|
263
|
+
throw error;
|
|
264
|
+
return refuse("before_versions_unavailable_before_allocation");
|
|
265
|
+
}
|
|
266
|
+
})();
|
|
267
|
+
if (current.sha256 !== before.sha256)
|
|
268
|
+
refuse("before_versions_changed_before_allocation");
|
|
269
|
+
baseline.revalidate({ projectRoot: scope.projectRoot, workspace: scope.workspace, gitExecutable: policy.git_executable, env: context.env });
|
|
270
|
+
}
|
|
271
|
+
export function startInteractiveRun(context, scope) {
|
|
272
|
+
if (context.db.isOwnedWriteTransaction?.())
|
|
273
|
+
refuse("preparation_outside_writer_required");
|
|
274
|
+
const request = readInteractiveRequest(context, scope);
|
|
275
|
+
if (!request)
|
|
276
|
+
refuse("original_request_required");
|
|
277
|
+
const allocation = readRunStartRequest(context, request.project_id, scope.requestId);
|
|
278
|
+
if (allocation) {
|
|
279
|
+
if (allocation.payload.initial_index.flow_kind !== "interactive")
|
|
280
|
+
refuse("foreign_run_request");
|
|
281
|
+
return PreparedDescriptorRunStart.restore(context, scope).apply(context);
|
|
282
|
+
}
|
|
283
|
+
const bundle = bindRunId(restoreBundle(request), nextDescriptorRunId(context, { ...scope, slug: "interactive" }));
|
|
284
|
+
assertInteractiveRequestBundle(context, request.project_id, bundle);
|
|
285
|
+
revalidateBeforeAllocation(context, request, bundle);
|
|
286
|
+
return PreparedDescriptorRunStart.prepare(context, { bundle }).apply(context);
|
|
287
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
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 { PreparedProjectFlowSource } from "./project-flow-source.js";
|
|
6
|
+
import { FrozenGenericExecution } from "./generic-execution.js";
|
|
7
|
+
import { AdmittedFrozenEngine } from "./frozen-engine.js";
|
|
8
|
+
import { RetainedExecutionBundle } from "./execution-bundles.js";
|
|
9
|
+
import { PreparedInteractiveGitBaseline } from "./interactive-git-baseline.js";
|
|
10
|
+
import { PreparedInteractiveBeforeVersions, restoreInteractiveBeforeVersions } from "./interactive-before-versions.js";
|
|
11
|
+
import { putInteractiveBundleData } from "./interactive-bundle-data.js";
|
|
12
|
+
import { observeMemoryGitPolicySelection } from "./memory-init-git-policy.js";
|
|
13
|
+
import { readBoundedReleaseFile } from "./release-source.js";
|
|
14
|
+
import { nextDescriptorRunId } from "./descriptor-run-start.js";
|
|
15
|
+
import { bundledHandlerContext } from "./stage-handlers.js";
|
|
16
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
17
|
+
const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
18
|
+
const encode = (value) => Buffer.from(JSON.stringify(value));
|
|
19
|
+
function refuse(reason) {
|
|
20
|
+
throw new AppError("interactive_run_bundle_invalid", "Interactive requires its original request, project pack, workspace and execution selection", 2, { reason, effect: "no_effect" });
|
|
21
|
+
}
|
|
22
|
+
/** Initial assembly only, outside the writer. Creates no RUN, workspace or
|
|
23
|
+
* Session and grants no file/Git mutation. The request owner must retain this
|
|
24
|
+
* exact bundle before allocation; retries restore it, never recapture state. */
|
|
25
|
+
export function prepareInteractiveRunBundle(context, input) {
|
|
26
|
+
if (context.db.isOwnedWriteTransaction?.())
|
|
27
|
+
refuse("preparation_outside_writer_required");
|
|
28
|
+
if (!(input.source instanceof PreparedProjectFlowSource) || !(input.execution instanceof FrozenGenericExecution)
|
|
29
|
+
|| !(input.engine instanceof AdmittedFrozenEngine) || !(input.baseline instanceof PreparedInteractiveGitBaseline)
|
|
30
|
+
|| !(input.beforeVersions instanceof PreparedInteractiveBeforeVersions) || !Number.isFinite(Date.parse(input.boundAt)))
|
|
31
|
+
refuse("unprepared_selection");
|
|
32
|
+
if (typeof input.requestText !== "string" || !input.requestText.trim() || Buffer.byteLength(input.requestText) > 64 * 1024)
|
|
33
|
+
refuse("original_request_required");
|
|
34
|
+
const frozen = input.execution.snapshot(), flow = input.source.resolveRegisteredFlow(), sourceScope = input.source.scope();
|
|
35
|
+
if (frozen.settings.flow_id !== "interactive" || flow.descriptor.id !== "interactive" || flow.descriptor.origin !== "project"
|
|
36
|
+
|| flow.sha256 !== frozen.settings.descriptor_sha256)
|
|
37
|
+
refuse("flow_selection_changed");
|
|
38
|
+
const baseline = input.baseline.snapshot();
|
|
39
|
+
if (sourceScope.projectRoot !== frozen.scope.projectRoot || sourceScope.workspace !== frozen.scope.workspace
|
|
40
|
+
|| baseline.project_root !== frozen.scope.projectRoot || baseline.workspace !== frozen.scope.workspace)
|
|
41
|
+
refuse("workspace_selection_changed");
|
|
42
|
+
// Before-images from another baseline are not interchangeable, even when the
|
|
43
|
+
// selected paths happen to match. Empty initial images are valid before scope.
|
|
44
|
+
restoreInteractiveBeforeVersions(input.beforeVersions.bytes(), input.beforeVersions.sha256, input.baseline);
|
|
45
|
+
input.engine.admitInteractiveFlow(input.source);
|
|
46
|
+
if (!path.isAbsolute(input.gitExecutable))
|
|
47
|
+
refuse("absolute_git_executable_required");
|
|
48
|
+
const gitExecutable = fs.realpathSync(input.gitExecutable), gitSha256 = hash(readBoundedReleaseFile(gitExecutable, 64 * 1024 * 1024));
|
|
49
|
+
const gitScope = { requestId: frozen.scope.requestId, projectRoot: frozen.scope.projectRoot, workspaceRoot: frozen.scope.workspace };
|
|
50
|
+
const policy = observeMemoryGitPolicySelection(context.env, { scope: gitScope, gitExecutable, selection: input.gitPolicy });
|
|
51
|
+
if (policy.repository !== baseline.repository || policy.workspace_branch !== baseline.branch || policy.initial_head !== baseline.head)
|
|
52
|
+
refuse("git_policy_baseline_changed");
|
|
53
|
+
const manifest = input.engine.manifest(), checksum = manifest.integrity.checksum;
|
|
54
|
+
if (typeof checksum !== "string")
|
|
55
|
+
refuse("engine_identity_missing");
|
|
56
|
+
const runId = nextDescriptorRunId(context, { projectRoot: frozen.scope.projectRoot, requestId: frozen.scope.requestId, slug: "interactive" });
|
|
57
|
+
const binding = { schema_id: "dd-flow/run-engine-binding@1", run_id: runId,
|
|
58
|
+
project_root: frozen.scope.projectRoot, bound_at: input.boundAt, source: "run_creation", reason: "Request-bound interactive work with explicit same-RUN finish",
|
|
59
|
+
engine: { package_name: manifest.package_name, package_version: manifest.package_version, engine_version: manifest.engine_version,
|
|
60
|
+
integrity_checksum: checksum, snapshot_root: manifest.snapshot_root }, probe: { status: "not_required", command: null } };
|
|
61
|
+
const blobs = input.source.blobs();
|
|
62
|
+
const put = (logical, bytes) => {
|
|
63
|
+
if ([...blobs.keys()].some(name => name.toLowerCase() === logical.toLowerCase()) || !bytes.length || bytes.length > 1024 * 1024)
|
|
64
|
+
refuse("bundle_path_collision_or_size");
|
|
65
|
+
blobs.set(logical, bytes);
|
|
66
|
+
return { path: logical, sha256: hash(bytes) };
|
|
67
|
+
};
|
|
68
|
+
put("runtime/source-request.md", Buffer.from(input.requestText));
|
|
69
|
+
if (input.launchSelection !== undefined) {
|
|
70
|
+
if (!input.launchSelection || typeof input.launchSelection !== "object" || Array.isArray(input.launchSelection)
|
|
71
|
+
|| Object.keys(input.launchSelection).some(key => !["rootProfile", "stageProfiles", "workProfiles", "execution", "stageSessionMode", "delegation"].includes(key)))
|
|
72
|
+
refuse("invalid_launch_selection");
|
|
73
|
+
// Raw selectors are replay intent, not execution authority. FrozenGenericExecution
|
|
74
|
+
// remains the only owner of resolved profiles and Session/delegation settings.
|
|
75
|
+
put("runtime/launch-selection.json", encode(input.launchSelection));
|
|
76
|
+
}
|
|
77
|
+
put("runtime/git-policy.json", encode({ schema_id: "dd-flow/interactive-git-policy@1", scope: gitScope,
|
|
78
|
+
git_executable: gitExecutable, git_sha256: gitSha256, policy }));
|
|
79
|
+
for (const reference of [frozen.settings.attached_session_receipt, frozen.settings.delegation.native_capacity_receipt]) {
|
|
80
|
+
if (!reference)
|
|
81
|
+
continue;
|
|
82
|
+
const bytes = input.receiptBytes?.get(reference.sha256);
|
|
83
|
+
if (!bytes || hash(bytes) !== reference.sha256 || !input.receipts?.has(reference.sha256))
|
|
84
|
+
refuse("execution_receipt_missing");
|
|
85
|
+
put(reference.path, Buffer.from(bytes));
|
|
86
|
+
}
|
|
87
|
+
const profiles = { schema_id: "dd-flow/profile-settings@1", profiles: frozen.profiles, aliases: {}, default_profile: null };
|
|
88
|
+
const profile = put("runtime/profiles.json", encode({ ...profiles, revision: hybridDigest(profiles) }));
|
|
89
|
+
const execution = put("runtime/execution.json", encode(frozen.settings));
|
|
90
|
+
const engineBinding = put("runtime/engine-binding.json", encode(binding));
|
|
91
|
+
const engineManifest = put("runtime/engine-manifest.json", input.engine.manifestBytes());
|
|
92
|
+
putInteractiveBundleData(blobs, "runtime/git-baseline.json", input.baseline.bytes());
|
|
93
|
+
putInteractiveBundleData(blobs, "runtime/before-versions.json", input.beforeVersions.bytes());
|
|
94
|
+
const provenance = { canon: input.source.canonProvenance(), project_pack: input.source.projectPackProvenance() };
|
|
95
|
+
const bundle = { schema_id: "dd-flow/execution-bundle@1", run_id: runId, flow_id: "interactive",
|
|
96
|
+
descriptor: input.source.descriptorReference(), execution_settings: execution, profile_settings: profile,
|
|
97
|
+
engine_binding: engineBinding, engine_manifest: engineManifest, bootstrap_receipts: [], ...provenance,
|
|
98
|
+
inventory: [...blobs].map(([logical, bytes]) => ({ path: logical, sha256: hash(bytes) })) };
|
|
99
|
+
input.source.revalidate();
|
|
100
|
+
input.beforeVersions.revalidate();
|
|
101
|
+
input.baseline.revalidate({ projectRoot: frozen.scope.projectRoot, workspace: frozen.scope.workspace, gitExecutable, env: context.env });
|
|
102
|
+
if (hash(readBoundedReleaseFile(gitExecutable, 64 * 1024 * 1024)) !== gitSha256)
|
|
103
|
+
refuse("selected_git_changed");
|
|
104
|
+
const bytes = encode(bundle);
|
|
105
|
+
return RetainedExecutionBundle.admit(bytes, hash(bytes), blobs, { runId, executionScope: frozen.scope,
|
|
106
|
+
bundleProjectRoot: frozen.scope.projectRoot, descriptorSha256: flow.sha256, engineChecksum: checksum,
|
|
107
|
+
engineBindingSha256: engineBinding.sha256, engineManifestSha256: engineManifest.sha256,
|
|
108
|
+
descriptorContext: bundledHandlerContext(new Map()), bundleProvenance: provenance, receipts: input.receipts ?? new Map() });
|
|
109
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { AppError } from "../shared/errors.js";
|
|
2
|
+
import { interactiveStages } from "../domain/interactive-flow.js";
|
|
3
|
+
import { requireProjectByRoot } from "./projects.js";
|
|
4
|
+
import { resolveProjectRoot } from "../storage/paths.js";
|
|
5
|
+
import { readFlowRunExecutionBundle, assertPreparedRunBundleSqlAuthority } from "./runs.js";
|
|
6
|
+
import { readInteractiveMutationScope } from "./interactive-mutation-scope.js";
|
|
7
|
+
import { PreparedDescriptorStageStart } from "./descriptor-stage-start.js";
|
|
8
|
+
import { PreparedDescriptorStageCoordinator } from "./descriptor-stage-coordinator.js";
|
|
9
|
+
function refuse(reason) {
|
|
10
|
+
throw new AppError("interactive_stage_invalid", "Interactive Stage requires its own RUN, delivered inputs and retained scope", 2, { reason, effect: "no_effect" });
|
|
11
|
+
}
|
|
12
|
+
/** Internal entry only, not public availability or Work/Git authorization.
|
|
13
|
+
* Generic runtime remains the sole owner of attempts, root Work and Session. */
|
|
14
|
+
export function readInteractiveStageScope(context, input, historical = false) {
|
|
15
|
+
const project = requireProjectByRoot(context, resolveProjectRoot(input.projectRoot)), bundle = readFlowRunExecutionBundle(context, input);
|
|
16
|
+
if (!bundle || bundle.manifest.flow_id !== "interactive" || !interactiveStages.includes(input.stage))
|
|
17
|
+
refuse("interactive_stage_required");
|
|
18
|
+
bundle.descriptor.stage(input.stage);
|
|
19
|
+
const scope = { projectId: project.id, runId: input.runId, stage: input.stage }, run = assertPreparedRunBundleSqlAuthority(context, scope, bundle);
|
|
20
|
+
const index = JSON.parse(run.index_json);
|
|
21
|
+
const replay = Boolean(historical && index.stage_runs?.some((item) => item.stage === input.stage && item.status === "done"));
|
|
22
|
+
const selected = readInteractiveMutationScope(context, scope, bundle);
|
|
23
|
+
if (!replay && !["preflight", "interactive"].includes(input.stage) && !selected.inputs.finish_requested)
|
|
24
|
+
refuse("explicit_finish_required");
|
|
25
|
+
return { bundle, scope, run, replay, selected,
|
|
26
|
+
/** SQL/data only. A turn, HITL answer or scope decision racing entry cannot
|
|
27
|
+
* silently become a different input to an already prepared coordinator. */
|
|
28
|
+
assertCustody(writer) {
|
|
29
|
+
const current = readInteractiveMutationScope(writer, scope, bundle);
|
|
30
|
+
if (current.revision !== selected.revision || current.inputs.sha256 !== selected.inputs.sha256
|
|
31
|
+
|| current.baseline.sha256 !== selected.baseline.sha256)
|
|
32
|
+
refuse("interactive_stage_inputs_changed");
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function startInteractiveStage(context, input) {
|
|
37
|
+
const selected = readInteractiveStageScope(context, input, true);
|
|
38
|
+
const start = PreparedDescriptorStageStart.prepare(context, input).apply(context, selected.assertCustody);
|
|
39
|
+
const coordinator = PreparedDescriptorStageCoordinator.prepare(context, input).apply(context, selected.assertCustody);
|
|
40
|
+
return { ...start, ...coordinator };
|
|
41
|
+
}
|