@deksden-com/dd-flow-cli 0.9.0-beta.151 → 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 +170 -25
- package/dist/harness-runtime/lib/dd-opencode.mjs +40 -19
- 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 +77 -10
- package/dist/services/check-runner-proof.js +69 -1
- package/dist/services/cli-operation-classifier.js +6 -1
- package/dist/services/code-checks.js +56 -23
- 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 -4
- 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 +9 -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 +4 -3
- package/tools/verification-jobs.mjs +342 -0
package/dist/services/hooks.js
CHANGED
|
@@ -319,7 +319,11 @@ export function handleCodexHook(context, input) {
|
|
|
319
319
|
const lifecycle = lifecycleFacts(command);
|
|
320
320
|
if (!lifecycle && !continuationCommand(command))
|
|
321
321
|
return { ok: true, observed: false, reason: "event_not_participating", event: eventName };
|
|
322
|
-
const parentProviderSessionId = rawChildProviderSessionId
|
|
322
|
+
const parentProviderSessionId = rawChildProviderSessionId
|
|
323
|
+
? context.codexHookProof?.sessionId === rawChildProviderSessionId
|
|
324
|
+
? context.codexHookProof.parentSessionId
|
|
325
|
+
: codexNativeParentId(payload, rawChildProviderSessionId)
|
|
326
|
+
: null;
|
|
323
327
|
// Resume legitimately receives an answer through stdin. It is matched by
|
|
324
328
|
// immutable lifecycle arguments below; never reject or rewrite that pipe.
|
|
325
329
|
const bootstrapStageStart = lifecycle?.bootstrapStageStart;
|
|
@@ -415,7 +419,18 @@ export function handleCodexHook(context, input) {
|
|
|
415
419
|
const toolCallId = stringValue(payload.tool_use_id) ?? stringValue(payload.event_id) ?? stringValue(payload.delivery_id) ?? stringValue(payload.id);
|
|
416
420
|
if (!daemonId || !rootProviderSessionId || !providerSessionId || !toolCallId)
|
|
417
421
|
throw new AppError("codex_identity_missing", "Managed Codex lifecycle hook has no complete native invocation identity", 1, { invocation_id: invocationId });
|
|
418
|
-
|
|
422
|
+
let executionCwd;
|
|
423
|
+
try {
|
|
424
|
+
executionCwd = assignedLifecycleCwd(context, project.root, lifecycle, providerSessionId, command, stringValue(payload.cwd), { daemonId, rootSessionId: rootProviderSessionId, sessionId: providerSessionId, parentSessionId: parentProviderSessionId, toolCallId }, "codex-desktop");
|
|
425
|
+
}
|
|
426
|
+
catch (error) {
|
|
427
|
+
// A proven external Work is not its Stage coordinator. Deny the tool
|
|
428
|
+
// normally; a policy refusal is not a broken native hook or a receipt.
|
|
429
|
+
if (!(error instanceof AppError) || error.code !== "work_stage_boundary")
|
|
430
|
+
throw error;
|
|
431
|
+
return { ...common, observed: false, reason: error.code, hookSpecificOutput: { hookEventName: "PreToolUse",
|
|
432
|
+
permissionDecision: "deny", permissionDecisionReason: error.message } };
|
|
433
|
+
}
|
|
419
434
|
const receipt = observeLifecycleInvocation(context, {
|
|
420
435
|
currentEventKey: eventKey,
|
|
421
436
|
...(invocationId ? { id: invocationId } : {}),
|
|
@@ -741,6 +756,12 @@ function assignedLifecycleCwd(context, projectRoot, facts, providerSessionId, co
|
|
|
741
756
|
if ((!facts && !auxiliary) || facts?.analysis.supported === false)
|
|
742
757
|
return undefined;
|
|
743
758
|
const proven = identity && harness ? managedNativeInvocationScope(context, projectRoot, identity, harness) : null;
|
|
759
|
+
if (proven && (facts?.invocation.operation.startsWith("stage_") || facts?.invocation.operation.startsWith("delivery_")) && !facts.bootstrapStageStart
|
|
760
|
+
&& context.db.get("SELECT 1 FROM sqlite_master WHERE type='table' AND name='work_launches'")) {
|
|
761
|
+
const worker = context.db.get("SELECT w.work_id FROM works w JOIN work_launches l ON l.work_id=w.work_id JOIN work_sessions ws ON ws.work_id=w.work_id JOIN sessions s ON s.session_id=ws.session_id AND s.project_id=w.project_id JOIN projects p ON p.id=w.project_id WHERE p.root=? AND w.run_id=? AND s.harness=? AND s.provider_session_id=? AND l.status IN ('launching','start_requested') LIMIT 1", [proven.projectRoot, proven.runId, harness, providerSessionId]);
|
|
762
|
+
if (worker)
|
|
763
|
+
throw new AppError("work_stage_boundary", "This external Work cannot issue coordinator Stage or delivery commands. Complete only its assigned Work; after successful work finish, end this Turn. The coordinator continues the Stage.", 1, { phase: "prepare", effect: "no_effect", recoverable: false });
|
|
764
|
+
}
|
|
744
765
|
const constraint = proven ?? (identity ? { projectRoot, daemonId: identity.daemonId, rootSessionId: identity.rootSessionId } : undefined);
|
|
745
766
|
const issuedRunId = facts && !facts.runId && !facts.workId && !facts.bootstrapStageStart && constraint ? observedLifecycleInvocation(context, command, constraint, identity ? { identity } : undefined)?.scope.runId : null;
|
|
746
767
|
if (facts?.invocation.operation.startsWith("stage_") && !facts.bootstrapStageStart && !facts.runId && !issuedRunId)
|
|
@@ -885,7 +906,10 @@ function handleControlledToolEvent(context, input, event, harness) {
|
|
|
885
906
|
// Syntax refusal precedes assignment/cwd lookup: an unsupported AST cannot
|
|
886
907
|
// supply a route or execution authority, including a short public Stage call.
|
|
887
908
|
const shellRefusal = lifecycle?.analysis.supported === false;
|
|
888
|
-
const
|
|
909
|
+
const proof = context.nativeHookProof;
|
|
910
|
+
if (proof && (proof.harness !== harness || proof.daemonId !== daemonId || proof.sessionId !== providerSessionId || proof.toolCallId !== toolCallId))
|
|
911
|
+
throw new AppError("native_hook_unproven", "Verified native Work callback differs from its physical identity", 1);
|
|
912
|
+
const nativeIdentity = { daemonId, rootSessionId: proof?.rootSessionId ?? nativeParentId ?? providerSessionId, sessionId: providerSessionId, parentSessionId: nativeParentId ?? null, toolCallId: toolCallId };
|
|
889
913
|
const provenScope = lifecycle && !shellRefusal ? managedNativeInvocationScope(context, expectedRoot, nativeIdentity, harness) : null;
|
|
890
914
|
const issuedRunId = lifecycle && !lifecycle.runId && !lifecycle.workId && !lifecycle.bootstrapStageStart
|
|
891
915
|
? observedLifecycleInvocation(context, command, provenScope ?? { projectRoot: expectedRoot, daemonId, rootSessionId: nativeIdentity.rootSessionId }, { identity: nativeIdentity })?.scope.runId : null;
|
|
@@ -939,7 +963,7 @@ function handleControlledToolEvent(context, input, event, harness) {
|
|
|
939
963
|
let inserted = false;
|
|
940
964
|
const receipt = observeLifecycleInvocation(context, {
|
|
941
965
|
currentEventKey: baseKey,
|
|
942
|
-
command, harness, projectId: project.id, ...(!shellRefusal && workspaceBound ? { executionCwd: expectedCwd } : {}), identity:
|
|
966
|
+
command, harness, projectId: project.id, ...(!shellRefusal && workspaceBound ? { executionCwd: expectedCwd } : {}), identity: nativeIdentity,
|
|
943
967
|
recordReceipt: scope => { inserted = persist(scope); return baseKey; }
|
|
944
968
|
});
|
|
945
969
|
const admitted = { ...result(inserted), ...lifecycleObservationResult(receipt), ...(receipt.invocationId ? { invocation_id: receipt.invocationId } : {}) };
|
|
@@ -1066,6 +1090,7 @@ function lifecycleFacts(command) {
|
|
|
1066
1090
|
const workStart = invocation.operation === "work_start";
|
|
1067
1091
|
const workFinish = invocation.operation === "work_finish";
|
|
1068
1092
|
const workFail = invocation.operation === "work_fail";
|
|
1093
|
+
const delivery = invocation.operation.startsWith("delivery_");
|
|
1069
1094
|
return {
|
|
1070
1095
|
analysis,
|
|
1071
1096
|
invocation,
|
|
@@ -1076,8 +1101,8 @@ function lifecycleFacts(command) {
|
|
|
1076
1101
|
workStart, workFinish, workFail,
|
|
1077
1102
|
bootstrapStageStart: stageStart && commandHasOption(invocation, "bootstrap"),
|
|
1078
1103
|
commandProjectRoot: commandOption(invocation, "project-root"),
|
|
1079
|
-
stageName: stageStart || stageFinish || stagePause || stageResume ? commandOption(invocation, "stage") : undefined,
|
|
1080
|
-
runId: stageStart || stageFinish || stagePause || stageResume ? commandPosition(invocation, 0) : undefined,
|
|
1104
|
+
stageName: stageStart || stageFinish || stagePause || stageResume || delivery ? commandOption(invocation, "stage") : undefined,
|
|
1105
|
+
runId: stageStart || stageFinish || stagePause || stageResume || delivery ? commandPosition(invocation, 0) : undefined,
|
|
1081
1106
|
workId: stagePause || stageResume ? commandOption(invocation, "work") : workStart || workFinish || workFail ? commandPosition(invocation, 0) : undefined,
|
|
1082
1107
|
contextSha256: commandOption(invocation, "context-sha256")
|
|
1083
1108
|
};
|
|
@@ -2,6 +2,8 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
4
4
|
import { Ajv } from "ajv/dist/ajv.js";
|
|
5
|
+
import { isSourceSnapshotFlow } from "../domain/source-snapshot-flows.js";
|
|
6
|
+
import { isDeliveryFlow } from "../domain/delivery-flows.js";
|
|
5
7
|
/** Static E1 contracts only. No ingress, dispatch or runtime defaults are changed. */
|
|
6
8
|
export const hybridContractNames = [
|
|
7
9
|
"release-manifest", "harness-catalog", "model-catalog", "discovery-result",
|
|
@@ -249,7 +251,7 @@ export function validateHybridContract(name, input, context = {}) {
|
|
|
249
251
|
fail("foreign_bundle_canon", "/canon");
|
|
250
252
|
if (hybridDigest(data.project_pack) !== hybridDigest(context.bundleProvenance.project_pack))
|
|
251
253
|
fail("foreign_bundle_project_pack", "/project_pack");
|
|
252
|
-
if ((data.flow_id === "mb-
|
|
254
|
+
if ((isSourceSnapshotFlow(data.flow_id) || data.flow_id === "mb-fix" || isDeliveryFlow(data.flow_id)) !== (data.project_pack === null))
|
|
253
255
|
fail("invalid_bundle_project_pack_role", "/project_pack");
|
|
254
256
|
const memoryFlow = data.flow_id === "mb-init" || data.flow_id === "mb-upgrade";
|
|
255
257
|
if (memoryFlow && context.bootstrapIdentity?.projectRoot !== context.bundleProjectRoot)
|
|
@@ -289,6 +291,9 @@ export function validateHybridContract(name, input, context = {}) {
|
|
|
289
291
|
}
|
|
290
292
|
};
|
|
291
293
|
const descriptor = loaded(data.descriptor), settings = loaded(data.execution_settings), profiles = loaded(data.profile_settings);
|
|
294
|
+
if (data.flow_id === "interactive" && (!descriptor || record(descriptor).origin !== "project"
|
|
295
|
+
|| record(data.project_pack).source_commit !== record(data.canon).commit))
|
|
296
|
+
fail("foreign_project_flow_provenance", "/project_pack");
|
|
292
297
|
// Execution receipts are evidence references, not just trusted facts. They
|
|
293
298
|
// must survive reopening the RUN inside the same frozen blob inventory.
|
|
294
299
|
for (const reference of [record(settings).attached_session_receipt,
|
|
@@ -8,7 +8,7 @@ import { hybridDigest, validateHybridShape } from "./hybrid-contracts.js";
|
|
|
8
8
|
import { admitBundledFlow } from "./stage-handlers.js";
|
|
9
9
|
import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
|
|
10
10
|
const limit = 1024 * 1024;
|
|
11
|
-
const flowIds = ["mb-init", "mb-upgrade", "mb-audit", "project-review"];
|
|
11
|
+
const flowIds = ["mb-init", "mb-upgrade", "mb-audit", "project-review", "mb-distill", "mb-fix", "review-fix", "release", "deploy", "publish"];
|
|
12
12
|
const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
13
13
|
function refuse(reason) {
|
|
14
14
|
throw new AppError("installed_flow_source_invalid", "Installed flow source is not the selected verified canon", 1, { reason, phase: "prepare", effect: "no_effect" });
|
|
@@ -117,6 +117,41 @@ export class PreparedInstalledFlowSource {
|
|
|
117
117
|
JSON.parse(content.toString("utf8"));
|
|
118
118
|
blobs.set(workSchema, Buffer.from(content));
|
|
119
119
|
}
|
|
120
|
+
if (flowId === "mb-fix" || flowId === "review-fix") {
|
|
121
|
+
for (const logical of [`dd-flow/${flowId}/hybrid/index.md`, "mbb/frontmatter-standards.md", "mbb/cross-references.md", "mbb/named-deferrals-guide.md"]) {
|
|
122
|
+
const content = readBoundedReleaseFile(containedFile(memoryRoot, logical), limit), name = `support/${logical}`;
|
|
123
|
+
if (!content.length || (total += content.length) > 32 * limit || folded.has(name.toLowerCase()))
|
|
124
|
+
refuse("invalid_support_inventory");
|
|
125
|
+
new TextDecoder("utf-8", { fatal: true }).decode(content);
|
|
126
|
+
folded.set(name.toLowerCase(), name);
|
|
127
|
+
blobs.set(name, Buffer.from(content));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (flowId === "mb-distill") {
|
|
131
|
+
// Both lenses need their actual comparison baseline, not only titles.
|
|
132
|
+
const directories = ["mbb", "mbb/aspects", "mbb/templates", "dd-flow/common", "dd-flow/vnext",
|
|
133
|
+
"dd-flow/plan", "dd-flow/code", "dd-flow/merge", "dd-flow/workers", "dd-flow/def",
|
|
134
|
+
"dd-flow/mb-distill/aspects", "dd-flow/mb-distill/process-aspects"];
|
|
135
|
+
const support = [...directories.flatMap(logical => {
|
|
136
|
+
const source = containedFile(memoryRoot, logical);
|
|
137
|
+
directory(source);
|
|
138
|
+
return fs.readdirSync(source).filter(name => name.endsWith(".md")).sort().map(name => `${logical}/${name}`);
|
|
139
|
+
}), "dd-flow/mb-audit.md", "dd-flow/mb-upgrade/index.md"];
|
|
140
|
+
for (const logical of support) {
|
|
141
|
+
const content = readBoundedReleaseFile(containedFile(memoryRoot, logical), limit), name = `support/${logical}`;
|
|
142
|
+
if (!content.length || (total += content.length) > 32 * limit || folded.has(name.toLowerCase()))
|
|
143
|
+
refuse("invalid_support_inventory");
|
|
144
|
+
new TextDecoder("utf-8", { fatal: true }).decode(content);
|
|
145
|
+
folded.set(name.toLowerCase(), name);
|
|
146
|
+
blobs.set(name, Buffer.from(content));
|
|
147
|
+
}
|
|
148
|
+
const workSchema = "schemas/distill-work-result.schema.json";
|
|
149
|
+
const content = readBoundedReleaseFile(containedFile(flowRoot, workSchema), limit);
|
|
150
|
+
if (!content.length || (total += content.length) > 32 * limit || folded.has(workSchema.toLowerCase()))
|
|
151
|
+
refuse("invalid_support_inventory");
|
|
152
|
+
JSON.parse(content.toString("utf8"));
|
|
153
|
+
blobs.set(workSchema, Buffer.from(content));
|
|
154
|
+
}
|
|
120
155
|
// Verify compressed/expanded archive identity again after collecting bytes.
|
|
121
156
|
// A coherent descriptor+prompt edit cannot become a new trusted selection.
|
|
122
157
|
const final = readSetupStatus(home);
|
|
@@ -167,7 +202,7 @@ export class PreparedInstalledFlowSource {
|
|
|
167
202
|
|| typeof value.canon.version !== "string" || !/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/.test(value.canon.version)
|
|
168
203
|
|| !value.descriptor || Object.keys(value.descriptor).sort().join() !== "path,sha256"
|
|
169
204
|
|| !flowIds.some(id => value.descriptor.path === `${id}/flow-descriptor.json`) || typeof value.descriptor.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(value.descriptor.sha256)
|
|
170
|
-
|| !Array.isArray(value.blobs) || value.blobs.length < 1 || value.blobs.length >
|
|
205
|
+
|| !Array.isArray(value.blobs) || value.blobs.length < 1 || value.blobs.length > 512)
|
|
171
206
|
refuse("invalid_retained_source");
|
|
172
207
|
const blobs = new Map(), folded = new Set();
|
|
173
208
|
let total = 0;
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { parse } from "shell-quote";
|
|
5
|
+
import { AppError } from "../shared/errors.js";
|
|
6
|
+
import { appendAudit } from "./audit.js";
|
|
7
|
+
import { runtimeHarness } from "./execution-policy.js";
|
|
8
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
9
|
+
import { hookSessionIdentity } from "./hooks.js";
|
|
10
|
+
import { prepareOperatorProfile } from "./operator-profiles.js";
|
|
11
|
+
import { findProjectByRoot } from "./projects.js";
|
|
12
|
+
import { parseCommandArgs } from "./lifecycle-command.js";
|
|
13
|
+
const eventType = "interactive.attached_session";
|
|
14
|
+
const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
15
|
+
function refuse(reason) {
|
|
16
|
+
throw new AppError("interactive_attached_session_required", "Interactive needs its current trusted attached Session; choose managed execution explicitly otherwise", 2, { phase: "prepare", effect: "no_effect", reason, next_action: "provide_matching_attached_session_or_choose_managed" });
|
|
17
|
+
}
|
|
18
|
+
function checkedScope(scope) {
|
|
19
|
+
if (!scope || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(scope.requestId)
|
|
20
|
+
|| [scope.projectRoot, scope.workspace].some(value => typeof value !== "string" || !path.isAbsolute(value)))
|
|
21
|
+
refuse("invalid_scope");
|
|
22
|
+
try {
|
|
23
|
+
if ([scope.projectRoot, scope.workspace].some(value => fs.realpathSync(value) !== value))
|
|
24
|
+
refuse("noncanonical_scope");
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
refuse("workspace_unavailable");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function project(context, scope) {
|
|
31
|
+
const value = findProjectByRoot(context, scope.projectRoot);
|
|
32
|
+
if (!value)
|
|
33
|
+
refuse("registered_project_required");
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
function launchArguments(context, command) {
|
|
37
|
+
let dynamic = false;
|
|
38
|
+
const entries = parse(command, () => { dynamic = true; return ""; });
|
|
39
|
+
if (dynamic || entries.some(entry => typeof entry !== "string") || /[`\n\r]/.test(command))
|
|
40
|
+
refuse("standalone_launch_required");
|
|
41
|
+
const words = entries;
|
|
42
|
+
let index = 0;
|
|
43
|
+
if (words[0]?.startsWith("DD_FLOW_HOME=")) {
|
|
44
|
+
if (words[0].slice("DD_FLOW_HOME=".length) !== context.ddFlowHome)
|
|
45
|
+
refuse("launch_home_mismatch");
|
|
46
|
+
index++;
|
|
47
|
+
}
|
|
48
|
+
if (path.basename(words[index] ?? "") === "node" && path.basename(words[index + 1] ?? "") === "cli.js")
|
|
49
|
+
index++;
|
|
50
|
+
else if (path.basename(words[index] ?? "") !== "dd-flow")
|
|
51
|
+
refuse("standalone_launch_required");
|
|
52
|
+
return words.slice(index + 1);
|
|
53
|
+
}
|
|
54
|
+
function checkLaunchCommand(context, scope, command) {
|
|
55
|
+
const args = parseCommandArgs(launchArguments(context, command)), option = (key) => args.options.get(key);
|
|
56
|
+
if (args.positional.join(" ") !== "flow interactive" || option("help") || option("execution")?.some(value => value !== "attached")
|
|
57
|
+
|| option("request-id")?.length !== 1 || option("request-id")[0] !== scope.requestId
|
|
58
|
+
|| option("project-root")?.length !== 1 || option("project-root")[0] !== scope.projectRoot
|
|
59
|
+
|| (option("workspace") ? option("workspace").length !== 1 || option("workspace")[0] !== scope.workspace : scope.workspace !== scope.projectRoot))
|
|
60
|
+
refuse("launch_scope_mismatch");
|
|
61
|
+
}
|
|
62
|
+
function binding(context, scope, selected, source, fresh) {
|
|
63
|
+
if (!source || typeof source.eventKey !== "string" || !source.eventKey || typeof source.command !== "string"
|
|
64
|
+
|| !source.command.trim() || Buffer.byteLength(source.command) > 32 * 1024)
|
|
65
|
+
refuse("exact_launch_receipt_required");
|
|
66
|
+
checkLaunchCommand(context, scope, source.command);
|
|
67
|
+
const profile = prepareOperatorProfile(selected), selectedProject = project(context, scope);
|
|
68
|
+
const event = context.db.get("SELECT id,event_name,status,sanitized_summary,created_at FROM hook_events WHERE project_id=? AND event_key=?", [selectedProject.id, source.eventKey]);
|
|
69
|
+
if (!event || event.event_name !== "PreToolUse" || event.status !== (fresh ? "observed" : "claimed"))
|
|
70
|
+
refuse("unavailable_launch_receipt");
|
|
71
|
+
const age = Date.parse(context.now()) - Date.parse(event.created_at);
|
|
72
|
+
// Reuse the existing hook admission horizon; historical restore does not
|
|
73
|
+
// pretend that the original launch event proves a currently live Session.
|
|
74
|
+
if (fresh && (!Number.isFinite(age) || age < 0 || age > 60_000))
|
|
75
|
+
refuse("stale_launch_receipt");
|
|
76
|
+
let summary;
|
|
77
|
+
try {
|
|
78
|
+
summary = JSON.parse(event.sanitized_summary);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return refuse("exact_launch_receipt_required");
|
|
82
|
+
}
|
|
83
|
+
if ((summary?.command ?? summary?.tool_input?.command) !== source.command)
|
|
84
|
+
refuse("launch_command_mismatch");
|
|
85
|
+
const identity = hookSessionIdentity(context, selectedProject.id, source.eventKey);
|
|
86
|
+
if (identity.parentSessionId || !identity.providerSessionId || identity.harness !== runtimeHarness(profile))
|
|
87
|
+
refuse("root_session_mismatch");
|
|
88
|
+
if (identity.cwd !== scope.workspace)
|
|
89
|
+
refuse("session_workspace_mismatch");
|
|
90
|
+
if (identity.provider !== profile.provider || identity.model !== profile.model || identity.reasoning !== profile.reasoning
|
|
91
|
+
|| identity.mode !== profile.mode)
|
|
92
|
+
refuse("session_profile_mismatch");
|
|
93
|
+
return { schema_id: "dd-flow/interactive-attached-session@1", scope: structuredClone(scope), profile,
|
|
94
|
+
hook_event_id: event.id, event_key: source.eventKey, command: source.command, harness: identity.harness,
|
|
95
|
+
provider_session_id: identity.providerSessionId, session_id: identity.sessionId, daemon_id: identity.daemonId, observed_at: event.created_at };
|
|
96
|
+
}
|
|
97
|
+
function retained(context, scope) {
|
|
98
|
+
const selectedProject = project(context, scope);
|
|
99
|
+
const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type=? AND json_extract(payload_json,'$.binding.scope.requestId')=? LIMIT 2", [selectedProject.id, eventType, scope.requestId]);
|
|
100
|
+
if (rows.length > 1)
|
|
101
|
+
refuse("ambiguous_attached_binding");
|
|
102
|
+
if (!rows.length)
|
|
103
|
+
return null;
|
|
104
|
+
if (Buffer.byteLength(rows[0].payload_json) > 256 * 1024)
|
|
105
|
+
refuse("invalid_attached_binding");
|
|
106
|
+
let value;
|
|
107
|
+
try {
|
|
108
|
+
value = JSON.parse(rows[0].payload_json);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return refuse("invalid_attached_binding");
|
|
112
|
+
}
|
|
113
|
+
if (!value || !value.binding || !value.binding.scope || Object.keys(value).sort().join() !== "binding,sha256" || hybridDigest(value.binding.scope) !== hybridDigest(scope)
|
|
114
|
+
|| hash(Buffer.from(JSON.stringify(value.binding))) !== value.sha256)
|
|
115
|
+
refuse("attached_binding_changed");
|
|
116
|
+
const actual = binding(context, scope, value.binding.profile, { eventKey: value.binding.event_key, command: value.binding.command }, false);
|
|
117
|
+
if (hybridDigest(actual) !== hybridDigest(value.binding))
|
|
118
|
+
refuse("attached_receipt_changed");
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
/** Produces generic execution receipt facts from the trusted hook ledger, not
|
|
122
|
+
* model-authored receipt JSON or ambient Session/model environment variables.
|
|
123
|
+
* This does not create a RUN or prove independent review/delegation capacity. */
|
|
124
|
+
export class PreparedInteractiveAttachedSession {
|
|
125
|
+
sha256;
|
|
126
|
+
#binding;
|
|
127
|
+
constructor(value) {
|
|
128
|
+
this.#binding = structuredClone(value);
|
|
129
|
+
this.sha256 = hash(Buffer.from(JSON.stringify(value)));
|
|
130
|
+
Object.freeze(this);
|
|
131
|
+
}
|
|
132
|
+
static prepare(context, scope, profile, source) {
|
|
133
|
+
checkedScope(scope);
|
|
134
|
+
if (retained(context, scope))
|
|
135
|
+
refuse("restore_retained_attached_binding");
|
|
136
|
+
return new PreparedInteractiveAttachedSession(binding(context, scope, profile, source, true));
|
|
137
|
+
}
|
|
138
|
+
/** Discover only an exact current argv observation. Same request/root/profile
|
|
139
|
+
* with a different intake or selector is not evidence for this invocation. */
|
|
140
|
+
static prepareCurrent(context, scope, profile, args) {
|
|
141
|
+
checkedScope(scope);
|
|
142
|
+
if (!Array.isArray(args) || args.length > 1024 || args.some(arg => typeof arg !== "string" || arg.includes("\0"))
|
|
143
|
+
|| args[0] !== "flow" || args[1] !== "interactive")
|
|
144
|
+
refuse("exact_launch_receipt_required");
|
|
145
|
+
if (retained(context, scope))
|
|
146
|
+
refuse("restore_retained_attached_binding");
|
|
147
|
+
const selectedProject = project(context, scope), matches = [];
|
|
148
|
+
const rows = context.db.all("SELECT event_key,sanitized_summary FROM hook_events WHERE project_id=? AND event_name='PreToolUse' AND status='observed' AND created_at>=? ORDER BY id DESC LIMIT 129", [selectedProject.id, new Date(Date.parse(context.now()) - 60_000).toISOString()]);
|
|
149
|
+
if (rows.length > 128)
|
|
150
|
+
refuse("launch_observation_window_exceeds_bound");
|
|
151
|
+
for (const row of rows) {
|
|
152
|
+
if (typeof row.sanitized_summary !== "string" || Buffer.byteLength(row.sanitized_summary) > 64 * 1024)
|
|
153
|
+
continue;
|
|
154
|
+
let summary;
|
|
155
|
+
try {
|
|
156
|
+
summary = JSON.parse(row.sanitized_summary);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const command = summary?.command ?? summary?.tool_input?.command;
|
|
162
|
+
if (typeof command !== "string" || Buffer.byteLength(command) > 32 * 1024)
|
|
163
|
+
continue;
|
|
164
|
+
try {
|
|
165
|
+
if (hybridDigest(launchArguments(context, command)) !== hybridDigest(args))
|
|
166
|
+
continue;
|
|
167
|
+
matches.push(binding(context, scope, profile, { eventKey: row.event_key, command }, true));
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
if (!(error instanceof AppError) || error.code !== "interactive_attached_session_required")
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (matches.length !== 1)
|
|
175
|
+
refuse(matches.length ? "ambiguous_launch_receipt" : "unavailable_launch_receipt");
|
|
176
|
+
return new PreparedInteractiveAttachedSession(matches[0]);
|
|
177
|
+
}
|
|
178
|
+
static restore(context, scope) {
|
|
179
|
+
checkedScope(scope);
|
|
180
|
+
const value = retained(context, scope);
|
|
181
|
+
if (!value)
|
|
182
|
+
refuse("attached_binding_missing");
|
|
183
|
+
return new PreparedInteractiveAttachedSession(value.binding);
|
|
184
|
+
}
|
|
185
|
+
snapshot() { return structuredClone(this.#binding); }
|
|
186
|
+
evidence() {
|
|
187
|
+
const value = this.#binding, bytes = Buffer.from(JSON.stringify(value));
|
|
188
|
+
const reference = { path: "receipts/interactive-attached-session.json", sha256: this.sha256 };
|
|
189
|
+
const receipts = new Map([[this.sha256, { kind: "attached-session-binding", ...value.scope,
|
|
190
|
+
harness: value.profile.harness, profileRevision: hybridDigest(value.profile) }]]);
|
|
191
|
+
return { reference, bytes, receipts };
|
|
192
|
+
}
|
|
193
|
+
/** Pair hook claim and custody with the caller's request/RUN writer. */
|
|
194
|
+
retainPreparedSql(context) {
|
|
195
|
+
if (!context.db.isOwnedWriteTransaction?.())
|
|
196
|
+
refuse("owned_writer_required");
|
|
197
|
+
const value = this.#binding, prior = retained(context, value.scope);
|
|
198
|
+
if (prior) {
|
|
199
|
+
if (prior.sha256 !== this.sha256)
|
|
200
|
+
refuse("attached_binding_changed");
|
|
201
|
+
return { changed: false };
|
|
202
|
+
}
|
|
203
|
+
const current = binding(context, value.scope, value.profile, { eventKey: value.event_key, command: value.command }, true);
|
|
204
|
+
if (hybridDigest(current) !== hybridDigest(value))
|
|
205
|
+
refuse("attached_receipt_changed");
|
|
206
|
+
const selectedProject = project(context, value.scope);
|
|
207
|
+
const claimed = context.db.run("UPDATE hook_events SET status='claimed',claimed_at=? WHERE id=? AND project_id=? AND status='observed'", [context.now(), value.hook_event_id, selectedProject.id]);
|
|
208
|
+
if (claimed.changes !== 1)
|
|
209
|
+
refuse("launch_receipt_consumed");
|
|
210
|
+
appendAudit(context, { projectId: selectedProject.id, eventType, payload: { binding: value, sha256: this.sha256 } });
|
|
211
|
+
if (retained(context, value.scope)?.sha256 !== this.sha256)
|
|
212
|
+
refuse("attached_readback_failed");
|
|
213
|
+
return { changed: true };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { AppError } from "../shared/errors.js";
|
|
2
|
+
import { PreparedInteractiveGitBaseline } from "./interactive-git-baseline.js";
|
|
3
|
+
import { assertPreparedRunBundleSqlAuthority } from "./runs.js";
|
|
4
|
+
import { restoreInteractiveBeforeVersions } from "./interactive-before-versions.js";
|
|
5
|
+
import { readInteractiveBundleData } from "./interactive-bundle-data.js";
|
|
6
|
+
const logical = "runtime/git-baseline.json";
|
|
7
|
+
function refuse(reason) {
|
|
8
|
+
throw new AppError("interactive_baseline_custody_invalid", "Interactive requires its own retained entry baseline", 2, { reason, effect: "no_effect" });
|
|
9
|
+
}
|
|
10
|
+
function selected(context, scope, bundle, file = logical) {
|
|
11
|
+
if (bundle.manifest.flow_id !== "interactive" || bundle.manifest.run_id !== scope.runId)
|
|
12
|
+
refuse("interactive_bundle_required");
|
|
13
|
+
assertPreparedRunBundleSqlAuthority(context, scope, bundle);
|
|
14
|
+
return readInteractiveBundleData(bundle, file);
|
|
15
|
+
}
|
|
16
|
+
/** Initial physical observation, not RUN allocation or mutation authority. The
|
|
17
|
+
* factory must revalidate before retaining these bytes and starting work. */
|
|
18
|
+
export function prepareInteractiveGitBaseline(input) {
|
|
19
|
+
const baseline = PreparedInteractiveGitBaseline.capture(input);
|
|
20
|
+
return { path: logical, sha256: baseline.sha256, bytes: baseline.bytes(), baseline };
|
|
21
|
+
}
|
|
22
|
+
/** SQL/in-memory fence for Stage acceptance: no project/Git/filesystem lookup,
|
|
23
|
+
* profile selection, fresh capture or adoption of a caller's replacement JSON. */
|
|
24
|
+
export function assertInteractiveGitBaselineCustody(context, scope, bundle, expectedSha256) {
|
|
25
|
+
if (selected(context, scope, bundle).sha256 !== expectedSha256)
|
|
26
|
+
refuse("baseline_binding_changed");
|
|
27
|
+
}
|
|
28
|
+
/** Historical custody uses the frozen execution scope. Current physical scope
|
|
29
|
+
* and supported relocation must be admitted separately before subsequent work. */
|
|
30
|
+
export function readPreparedInteractiveGitBaseline(context, scope, bundle) {
|
|
31
|
+
return context.db.readSnapshot(() => {
|
|
32
|
+
const { bytes, reference, sha256 } = selected(context, scope, bundle);
|
|
33
|
+
const baseline = PreparedInteractiveGitBaseline.restore(bytes, sha256), value = baseline.snapshot();
|
|
34
|
+
const frozen = bundle.execution.snapshot().scope;
|
|
35
|
+
if (value.project_root !== frozen.projectRoot || value.workspace !== frozen.workspace)
|
|
36
|
+
refuse("baseline_scope_changed");
|
|
37
|
+
return { baseline, sha256,
|
|
38
|
+
reference: { root: "run", path: `execution-bundle/${logical}`, sha256: reference.sha256 } };
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
export function assertInteractiveBeforeVersionsCustody(context, scope, bundle, expectedSha256) {
|
|
42
|
+
if (selected(context, scope, bundle, "runtime/before-versions.json").sha256 !== expectedSha256)
|
|
43
|
+
refuse("before_versions_binding_changed");
|
|
44
|
+
}
|
|
45
|
+
/** Initial before-images selected from this same RUN/baseline. Later per-turn
|
|
46
|
+
* before-images require their own accepted Work/packet custody, not bundle edits. */
|
|
47
|
+
export function readPreparedInteractiveBeforeVersions(context, scope, bundle) {
|
|
48
|
+
return context.db.readSnapshot(() => {
|
|
49
|
+
const baseline = readPreparedInteractiveGitBaseline(context, scope, bundle);
|
|
50
|
+
const { bytes, reference, sha256 } = selected(context, scope, bundle, "runtime/before-versions.json");
|
|
51
|
+
return { value: restoreInteractiveBeforeVersions(bytes, sha256, baseline.baseline), sha256,
|
|
52
|
+
reference: { root: "run", path: "execution-bundle/runtime/before-versions.json", sha256: reference.sha256 } };
|
|
53
|
+
});
|
|
54
|
+
}
|
|
@@ -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
|
+
}
|