@deksden-com/dd-flow-cli 0.9.0-beta.136 → 0.9.0-beta.138
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +9 -0
- package/dist/build-info.json +6 -6
- package/dist/cli/argument-values.js +5 -5
- package/dist/cli/command-inputs.js +89 -20
- package/dist/cli/command-metadata.js +56 -3
- package/dist/cli/command-option-descriptions.js +5 -0
- package/dist/cli/executor-onboarding.js +8 -3
- package/dist/cli/flows.js +29 -0
- package/dist/cli/help.js +240 -6
- package/dist/cli/input-preparation.js +54 -5
- package/dist/cli/memory-flow-admission.js +44 -0
- package/dist/cli/memory-init-invocation.js +78 -0
- package/dist/cli/memory-init.js +240 -0
- package/dist/cli/memory-storage-admission.js +53 -0
- package/dist/cli/native-hook-ingress.js +1 -1
- package/dist/cli/response-file.js +2 -0
- package/dist/cli/run-cli.js +283 -160
- package/dist/cli/setup.js +9 -7
- package/dist/cli.js +54 -2
- package/dist/domain/stage-handler-identities.js +20 -0
- package/dist/harness-runtime/DD-ZCODE.md +13 -0
- package/dist/harness-runtime/bin/dd-opencode.mjs +3 -3
- package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +4 -3
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +4 -2
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +11 -5
- package/dist/harness-runtime/lib/dd-codex.mjs +22 -10
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +2 -1
- package/dist/harness-runtime/lib/dd-droid.mjs +2 -1
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +4 -2
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +20 -5
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +45 -10
- package/dist/harness-runtime/lib/dd-zcode.mjs +7 -3
- package/dist/harness-runtime/lib/delegation-instructions.mjs +5 -3
- package/dist/harness-runtime/lib/lifecycle-command.mjs +1 -0
- package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +50 -15
- package/dist/harness-runtime/lib/managed-flow-env.mjs +15 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +1 -1
- package/dist/harness-runtime/lib/native-request-proof.d.mts +2 -0
- package/dist/harness-runtime/lib/native-request-proof.mjs +65 -0
- package/dist/harness-runtime/lib/opencode-plugin.mjs +5 -1
- package/dist/harness-runtime/lib/operation-progress.mjs +3 -1
- package/dist/runtime/context.js +1 -1
- package/dist/schemas/discovery-result.schema.json +184 -0
- package/dist/schemas/execution-bundle.schema.json +205 -0
- package/dist/schemas/execution-settings.schema.json +162 -0
- package/dist/schemas/flow-descriptor.schema.json +54 -0
- package/dist/schemas/flow-run.schema.json +22 -0
- package/dist/schemas/harness-catalog.schema.json +80 -0
- package/dist/schemas/mb-init-compose-result.schema.json +457 -0
- package/dist/schemas/mb-init-discover-result.schema.json +500 -0
- package/dist/schemas/mb-init-finish-result.schema.json +422 -0
- package/dist/schemas/mb-init-preflight-result.schema.json +367 -0
- package/dist/schemas/mb-init-review-result.schema.json +396 -0
- package/dist/schemas/mb-init-verify-result.schema.json +391 -0
- package/dist/schemas/memory-bootstrap-receipt-v2.schema.json +67 -0
- package/dist/schemas/memory-bootstrap-receipt.schema.json +354 -0
- package/dist/schemas/memory-composition-work-result.schema.json +38 -0
- package/dist/schemas/memory-repair-work-result.schema.json +49 -0
- package/dist/schemas/memory-review-correction-plan.schema.json +30 -0
- package/dist/schemas/memory-review-work-result.schema.json +43 -0
- package/dist/schemas/model-catalog.schema.json +153 -0
- package/dist/schemas/profile-settings.schema.json +122 -0
- package/dist/schemas/release-manifest.schema.json +291 -0
- package/dist/schemas/run-engine-binding.schema.json +1 -1
- package/dist/services/check-runner-evidence.js +51 -4
- package/dist/services/cli-operation-classifier.js +8 -47
- package/dist/services/code-checks.js +387 -34
- package/dist/services/controller-child-observer.js +99 -0
- package/dist/services/controller-execution.js +88 -0
- package/dist/services/controller-fanout.js +79 -13
- package/dist/services/dashboard.js +8 -2
- package/dist/services/descriptor-coordinator-session.js +35 -0
- package/dist/services/descriptor-root-work.js +53 -0
- package/dist/services/descriptor-run-settlement.js +105 -0
- package/dist/services/descriptor-run-start.js +449 -0
- package/dist/services/descriptor-stage-coordinator.js +197 -0
- package/dist/services/descriptor-stage-settlement.js +222 -0
- package/dist/services/descriptor-stage-start.js +139 -0
- package/dist/services/engines.js +39 -22
- package/dist/services/eval-snapshots.js +83 -25
- package/dist/services/execution-bundles.js +263 -0
- package/dist/services/execution-policy.js +20 -16
- package/dist/services/external-work-launch.js +91 -11
- package/dist/services/flow-catalog.js +21 -0
- package/dist/services/flow-descriptors.js +82 -0
- package/dist/services/frozen-engine.js +242 -0
- package/dist/services/generic-execution.js +164 -0
- package/dist/services/harness-adapter.js +28 -2
- package/dist/services/hooks.js +24 -6
- package/dist/services/hybrid-contracts.js +31 -3
- package/dist/services/ids.js +29 -1
- package/dist/services/installation-lock.js +7 -4
- package/dist/services/installed-flow-source.js +183 -0
- package/dist/services/lifecycle-invocations.js +257 -78
- package/dist/services/memory-accepted-composition.js +77 -0
- package/dist/services/memory-baseline-preservation.js +65 -0
- package/dist/services/memory-completed-correction-wave.js +320 -0
- package/dist/services/memory-compose-handler.js +258 -0
- package/dist/services/memory-composition-acceptance.js +199 -0
- package/dist/services/memory-composition-graph.js +134 -0
- package/dist/services/memory-composition-registration.js +146 -0
- package/dist/services/memory-composition-work-custody.js +151 -0
- package/dist/services/memory-composition-work-results.js +96 -0
- package/dist/services/memory-corrected-review-acceptance.js +129 -0
- package/dist/services/memory-corrected-review-batch.js +30 -0
- package/dist/services/memory-corrected-review-findings.js +106 -0
- package/dist/services/memory-corrected-review-inputs.js +210 -0
- package/dist/services/memory-corrected-review-projection.js +78 -0
- package/dist/services/memory-corrected-review-receipt.js +170 -0
- package/dist/services/memory-corrected-review-registration.js +191 -0
- package/dist/services/memory-correction-check-receipt.js +219 -0
- package/dist/services/memory-correction-input-owner.js +6 -0
- package/dist/services/memory-discover-handler.js +104 -0
- package/dist/services/memory-finish-evidence.js +435 -0
- package/dist/services/memory-finish-handler.js +215 -0
- package/dist/services/memory-finish-integration.js +280 -0
- package/dist/services/memory-finish-readiness.js +122 -0
- package/dist/services/memory-git-integration.js +151 -0
- package/dist/services/memory-historical-corrected-review.js +206 -0
- package/dist/services/memory-historical-original-review.js +130 -0
- package/dist/services/memory-historical-repair-registration.js +99 -0
- package/dist/services/memory-historical-second-review.js +128 -0
- package/dist/services/memory-init-bootstrap-readiness.js +210 -0
- package/dist/services/memory-init-contexts.js +89 -0
- package/dist/services/memory-init-controller-start.js +84 -0
- package/dist/services/memory-init-engine-release.js +223 -0
- package/dist/services/memory-init-git-policy.js +136 -0
- package/dist/services/memory-init-lint-capability.js +185 -0
- package/dist/services/memory-init-native-capacity.js +241 -0
- package/dist/services/memory-init-preflight.js +312 -0
- package/dist/services/memory-init-project-bootstrap.js +599 -0
- package/dist/services/memory-init-request-candidate.js +157 -0
- package/dist/services/memory-init-request-input.js +66 -0
- package/dist/services/memory-init-resource-storage.js +125 -0
- package/dist/services/memory-init-run-allocation.js +81 -0
- package/dist/services/memory-init-run-bundle.js +98 -0
- package/dist/services/memory-init-start.js +175 -0
- package/dist/services/memory-init-startup-custody.js +80 -0
- package/dist/services/memory-init-startup-input.js +240 -0
- package/dist/services/memory-init-storage.js +81 -0
- package/dist/services/memory-init-stores.js +359 -0
- package/dist/services/memory-init-worktrunk-capability.js +172 -0
- package/dist/services/memory-layer-publication.js +611 -0
- package/dist/services/memory-layer.js +156 -0
- package/dist/services/memory-lint-config.js +85 -0
- package/dist/services/memory-native-review-copy.js +165 -0
- package/dist/services/memory-normal-flow-entry.js +75 -0
- package/dist/services/memory-ordinary-defaults.js +112 -0
- package/dist/services/memory-preflight-bootstrap-custody.js +115 -0
- package/dist/services/memory-preflight-handler.js +259 -0
- package/dist/services/memory-repair-current-view.js +299 -0
- package/dist/services/memory-repair-document-delta.js +451 -0
- package/dist/services/memory-repair-history.js +491 -0
- package/dist/services/memory-repair-physical-binding.js +312 -0
- package/dist/services/memory-repair-publication-files.js +152 -0
- package/dist/services/memory-repair-publication-intent.js +27 -0
- package/dist/services/memory-repair-publication-plan.js +160 -0
- package/dist/services/memory-repair-publication-source.js +225 -0
- package/dist/services/memory-repair-publication.js +794 -0
- package/dist/services/memory-repair-registration.js +239 -0
- package/dist/services/memory-repair-work-baseline.js +413 -0
- package/dist/services/memory-repair-work-completion.js +97 -0
- package/dist/services/memory-repair-work-custody.js +73 -0
- package/dist/services/memory-repair-work-results.js +89 -0
- package/dist/services/memory-repair-workspace.js +252 -0
- package/dist/services/memory-repair-writer-copy.js +735 -0
- package/dist/services/memory-result-binding.js +173 -0
- package/dist/services/memory-retained-review-authority.js +129 -0
- package/dist/services/memory-retained-review-results.js +70 -0
- package/dist/services/memory-retained-second-baseline.js +102 -0
- package/dist/services/memory-retained-second-repairs.js +156 -0
- package/dist/services/memory-review-acceptance.js +214 -0
- package/dist/services/memory-review-correction-plan.js +171 -0
- package/dist/services/memory-review-handler.js +585 -0
- package/dist/services/memory-review-inputs.js +200 -0
- package/dist/services/memory-review-receipt.js +180 -0
- package/dist/services/memory-review-registration.js +108 -0
- package/dist/services/memory-review-work-custody.js +124 -0
- package/dist/services/memory-review-work-input.js +72 -0
- package/dist/services/memory-review-work-results.js +73 -0
- package/dist/services/memory-second-corrected-review-inputs.js +165 -0
- package/dist/services/memory-second-correction-plan.js +76 -0
- package/dist/services/memory-second-repair-baseline.js +91 -0
- package/dist/services/memory-second-repair-launch.js +614 -0
- package/dist/services/memory-second-repair-registration.js +88 -0
- package/dist/services/memory-second-repair-source.js +187 -0
- package/dist/services/memory-second-review-acceptance.js +115 -0
- package/dist/services/memory-source-observations.js +117 -0
- package/dist/services/memory-stage-dispatch.js +28 -0
- package/dist/services/memory-stage-results.js +199 -0
- package/dist/services/memory-verification-acceptance.js +135 -0
- package/dist/services/memory-verification-checks.js +252 -0
- package/dist/services/memory-verification-receipt.js +159 -0
- package/dist/services/memory-verify-handler.js +135 -0
- package/dist/services/memory-work-result-preparation.js +63 -0
- package/dist/services/projects.js +17 -2
- package/dist/services/recovery-snapshot-database.js +416 -1
- package/dist/services/repair-intents.js +1 -1
- package/dist/services/review-copy.js +10 -2
- package/dist/services/run-control.js +80 -9
- package/dist/services/run-controller-capture.js +10 -3
- package/dist/services/run-controller-process.js +21 -0
- package/dist/services/run-controller-recovery.js +3 -1
- package/dist/services/run-controller-state.js +1 -1
- package/dist/services/run-controller.js +184 -48
- package/dist/services/run-recovery-guard.js +2 -0
- package/dist/services/run-recovery.js +145 -5
- package/dist/services/run-start-custody.js +177 -0
- package/dist/services/runs.js +319 -25
- package/dist/services/stage-context.js +9 -1
- package/dist/services/stage-handlers.js +74 -0
- package/dist/services/stage-lifecycle.js +2 -1
- package/dist/services/stage-report-renderer.js +76 -0
- package/dist/services/stage-work-graph.js +17 -0
- package/dist/services/vnext-code.js +3 -3
- package/dist/services/vnext-execution-profile.js +2 -2
- package/dist/services/vnext-fanout.js +136 -10
- package/dist/services/vnext-merge.js +8 -3
- package/dist/services/vnext-plan-review.js +6 -4
- package/dist/services/vnext-protocolize.js +60 -2
- package/dist/services/work-registry.js +616 -71
- package/dist/services/workspace-files.js +1 -1
- package/dist/storage/database.js +84 -6
- package/dist/storage/writer-contract.js +11 -7
- package/package.json +13 -3
- package/tools/create-canon-release.mjs +33 -0
- package/tools/produce-hybrid-release.mjs +41 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { AppError } from "../shared/errors.js";
|
|
5
|
+
import { appendAudit } from "./audit.js";
|
|
6
|
+
import { hookSessionIdentity } from "./hooks.js";
|
|
7
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
8
|
+
import { runtimeHarness } from "./execution-policy.js";
|
|
9
|
+
import { parseLifecycleCommand, commandPosition } from "./lifecycle-command.js";
|
|
10
|
+
import { resolveWorkReference } from "../storage/work-references.js";
|
|
11
|
+
import { assertRunMutationAllowed } from "./run-recovery-guard.js";
|
|
12
|
+
import { currentLifecycleAdmission } from "./lifecycle-invocations.js";
|
|
13
|
+
import { recoveryGuard } from "./run-recovery-guard.js";
|
|
14
|
+
const event = "memory.repair_native_binding";
|
|
15
|
+
function refuse(reason) {
|
|
16
|
+
throw new AppError("memory_repair_native_binding_invalid", "Native repair requires its actual direct-child Hook and independently owned copy", 1, { reason, effect: "no_effect" });
|
|
17
|
+
}
|
|
18
|
+
/** Preserve raw Hook CWD. A scoped literal shell route becomes physical
|
|
19
|
+
* execution proof only after the CLI independently admitted process.cwd. */
|
|
20
|
+
export function memoryRepairExecutionIdentity(context, input) {
|
|
21
|
+
const reader = { ...context };
|
|
22
|
+
delete reader.observeLifecycleHook;
|
|
23
|
+
const identity = hookSessionIdentity(reader, input.projectId, input.eventKey);
|
|
24
|
+
if (identity.cwd === input.root)
|
|
25
|
+
return { identity, invocationId: null };
|
|
26
|
+
const rows = context.db.all("SELECT id,status,command,scope_json,identity_json FROM lifecycle_invocations WHERE event_key=? AND status IN ('executing','settled') LIMIT 2", [input.eventKey]);
|
|
27
|
+
if (rows.length !== 1)
|
|
28
|
+
refuse("repair_execution_cwd_unproven");
|
|
29
|
+
const row = rows[0];
|
|
30
|
+
try {
|
|
31
|
+
if ([row.command, row.scope_json, row.identity_json].some(value => typeof value !== "string" || Buffer.byteLength(value) > 1024 * 1024))
|
|
32
|
+
refuse("repair_execution_cwd_unproven");
|
|
33
|
+
const scope = JSON.parse(row.scope_json), native = JSON.parse(row.identity_json), parsed = parseLifecycleCommand(row.command);
|
|
34
|
+
const project = context.db.get("SELECT root FROM projects WHERE id=?", [input.projectId]);
|
|
35
|
+
const parent = context.db.get("SELECT provider_session_id FROM sessions WHERE project_id=? AND session_id=? AND run_id=?", [input.projectId, identity.parentSessionId, input.runId]);
|
|
36
|
+
if (parsed.kind === "none" || parsed.supported === false || parsed.invocation.operation !== "work_start"
|
|
37
|
+
|| scope.runId !== input.runId || scope.projectRoot !== project?.root || !Array.isArray(native) || native.length !== 6
|
|
38
|
+
|| native[0] !== identity.daemonId || scope.daemonId !== identity.daemonId || native[1] !== scope.rootSessionId
|
|
39
|
+
|| !parent || native[3] !== parent.provider_session_id || typeof native[4] !== "string" || !native[4]
|
|
40
|
+
|| native[2] !== identity.providerSessionId || native[5]?.executionCwd !== input.root)
|
|
41
|
+
refuse("repair_execution_cwd_unproven");
|
|
42
|
+
const target = resolveWorkReference(context, commandPosition(parsed.invocation, 0), { projectId: input.projectId });
|
|
43
|
+
if (target.work_id !== input.workId)
|
|
44
|
+
refuse("repair_execution_cwd_unproven");
|
|
45
|
+
const retained = context.db.get("SELECT 1 FROM work_sessions ws JOIN works w ON w.work_id=ws.work_id JOIN hook_events he ON he.project_id=w.project_id AND he.event_key=ws.hook_event_id WHERE w.project_id=? AND w.run_id=? AND w.work_id=? AND ws.hook_event_id=? AND ws.session_id=? AND he.status='claimed' AND EXISTS (SELECT 1 FROM audit_events a WHERE a.project_id=w.project_id AND a.event_type IN ('memory.repair_work_start','memory.native_review_binding') AND json_extract(a.payload_json,'$.run_id')=w.run_id AND json_extract(a.payload_json,'$.work_id')=w.work_id AND json_extract(a.payload_json,'$.work_session_id')=ws.id AND (a.event_type='memory.repair_work_start' OR json_extract(a.payload_json,'$.invocation_id')=?))", [input.projectId, input.runId, input.workId, input.eventKey, identity.sessionId, row.id]);
|
|
46
|
+
if (!currentLifecycleAdmission(context, row.id) && !retained)
|
|
47
|
+
refuse("repair_execution_cwd_not_admitted");
|
|
48
|
+
return { identity: { ...identity, cwd: input.root }, invocationId: row.id };
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (error instanceof AppError && error.code === "memory_repair_native_binding_invalid")
|
|
52
|
+
throw error;
|
|
53
|
+
return refuse("repair_execution_cwd_unproven");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Hook ingress selects only an independently prepared repair copy. This
|
|
57
|
+
* establishes a command's assigned directory, not claim/repair authority. */
|
|
58
|
+
export function memoryRepairAssignedHookCwd(context, input) {
|
|
59
|
+
const work = context.db.get("SELECT payload_json,result_schema,status FROM works WHERE project_id=? AND run_id=? AND work_id=?", [input.projectId, input.runId, input.workId]);
|
|
60
|
+
let payload;
|
|
61
|
+
try {
|
|
62
|
+
payload = JSON.parse(work?.payload_json ?? "null");
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
if (payload?.kind !== "memory-repair" && work?.result_schema !== "dd-flow/memory-repair-work-result@1")
|
|
68
|
+
return null;
|
|
69
|
+
const run = context.db.get("SELECT r.run_root,r.run_dir,r.flow_kind,p.root AS project_root FROM runs r JOIN projects p ON p.id=r.project_id WHERE r.project_id=? AND r.id=?", [input.projectId, input.runId]);
|
|
70
|
+
if (!run || run.flow_kind !== "mb-init" || run.project_root !== input.projectRoot || payload.read_only !== false
|
|
71
|
+
|| ![1, 2].includes(payload.memory_repair?.wave ?? 0) || work?.result_schema !== "dd-flow/memory-repair-work-result@1")
|
|
72
|
+
refuse("registered_memory_repair_required");
|
|
73
|
+
const generation = assertRunMutationAllowed(context, input.projectId, input.runId), runRoot = run.run_root ?? run.run_dir;
|
|
74
|
+
const external = context.db.get("SELECT 1 FROM sqlite_schema WHERE type='table' AND name='work_launches'")
|
|
75
|
+
? context.db.get("SELECT operation_id,status,profile_json,profile_hash,provider_session_id FROM work_launches WHERE work_id=?", [input.workId]) : undefined;
|
|
76
|
+
const native = !external;
|
|
77
|
+
const admitted = (root) => {
|
|
78
|
+
if (native && input.observedCwd !== root && input.literalExecutionCwd !== root)
|
|
79
|
+
throw new AppError("memory_repair_native_copy_route_unavailable", "Native repair requires a genuinely observed child execution directory inside its owned writer copy", 1, { phase: "prepare", effect: "no_effect", recoverable: false, expected: root, actual: input.observedCwd ?? null,
|
|
80
|
+
next_action: "Use only a documented native child/tool capability that establishes this exact directory; otherwise stop and select external delegation in a new RUN." });
|
|
81
|
+
return { root, native };
|
|
82
|
+
};
|
|
83
|
+
if (work.status === "created") {
|
|
84
|
+
// Routing consumes registered copy custody only. Actual Work start owns
|
|
85
|
+
// physical source/copy/profile admission after the durable native Hook.
|
|
86
|
+
const rows = (eventType) => context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type=? AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.work_id')=? AND json_extract(payload_json,'$.generation')=? LIMIT 2", [input.projectId, eventType, input.runId, input.workId, generation]);
|
|
87
|
+
const intents = rows("memory.repair_writer_copy_intent"), copies = rows("memory.repair_writer_copy_ready");
|
|
88
|
+
if (intents.length !== 1 || copies.length !== 1 || Buffer.byteLength(intents[0].payload_json) > 1024 * 1024
|
|
89
|
+
|| Buffer.byteLength(copies[0].payload_json) > 12 * 1024 * 1024)
|
|
90
|
+
refuse("owned_ready_copy_required");
|
|
91
|
+
try {
|
|
92
|
+
const intent = JSON.parse(intents[0].payload_json), ready = JSON.parse(copies[0].payload_json);
|
|
93
|
+
const workspace_manifest_sha256 = ready.workspace_manifest_sha256, readyIntent = { ...ready };
|
|
94
|
+
delete readyIntent.workspace_manifest_sha256;
|
|
95
|
+
delete readyIntent.workspace_manifest_base64;
|
|
96
|
+
const relative = `works/${input.workId}/writer-workspaces/generation-${generation}`;
|
|
97
|
+
if (intent.schema_id !== "dd-flow/memory-repair-writer-copy-intent@1" || intent.project_id !== input.projectId || intent.run_id !== input.runId
|
|
98
|
+
|| intent.work_id !== input.workId || intent.generation !== generation || intent.run_root !== runRoot || !path.isAbsolute(runRoot)
|
|
99
|
+
|| intent.copy_relative !== relative || !/^[a-f0-9]{64}$/.test(intent.baseline_registration_sha256)
|
|
100
|
+
|| !/^[a-f0-9]{64}$/.test(workspace_manifest_sha256) || hybridDigest(readyIntent) !== hybridDigest(intent))
|
|
101
|
+
refuse("owned_ready_copy_required");
|
|
102
|
+
if (external) {
|
|
103
|
+
if (external.provider_session_id !== input.providerSessionId)
|
|
104
|
+
refuse("foreign_physical_session");
|
|
105
|
+
const profile = JSON.parse(external.profile_json), metadata = profile.memory_repair_copy;
|
|
106
|
+
if (profile.delegation?.mode !== "external" || hybridDigest(metadata) !== hybridDigest({ root: relative, copy_intent_sha256: hybridDigest(intent),
|
|
107
|
+
workspace_manifest_sha256, baseline_registration_sha256: intent.baseline_registration_sha256 }))
|
|
108
|
+
refuse("native_copy_cwd_mismatch");
|
|
109
|
+
}
|
|
110
|
+
if (fs.realpathSync(runRoot) !== path.resolve(runRoot))
|
|
111
|
+
refuse("owned_ready_copy_required");
|
|
112
|
+
let destination = runRoot;
|
|
113
|
+
for (const part of relative.split('/')) {
|
|
114
|
+
destination = path.join(destination, part);
|
|
115
|
+
if (!fs.lstatSync(destination).isDirectory())
|
|
116
|
+
refuse("owned_ready_copy_required");
|
|
117
|
+
}
|
|
118
|
+
return admitted(destination);
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
if (error instanceof AppError)
|
|
122
|
+
throw error;
|
|
123
|
+
return refuse("owned_ready_copy_required");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Running writers retain their committed directory, not today's source bytes.
|
|
127
|
+
let binding = external;
|
|
128
|
+
let nativeStart;
|
|
129
|
+
if (!binding) {
|
|
130
|
+
const rows = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type='memory.repair_native_binding' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.work_id')=? LIMIT 2", [input.projectId, input.runId, input.workId]);
|
|
131
|
+
if (rows.length !== 1 || Buffer.byteLength(rows[0].payload_json) > 1024 * 1024)
|
|
132
|
+
refuse("native_registered_start_changed");
|
|
133
|
+
try {
|
|
134
|
+
const retained = JSON.parse(rows[0].payload_json);
|
|
135
|
+
if (retained.schema_id !== "dd-flow/memory-repair-native-binding@1" || retained.project_id !== input.projectId || retained.run_id !== input.runId || retained.work_id !== input.workId
|
|
136
|
+
|| typeof retained.work_session_id !== "string" || typeof retained.hook_event_key !== "string")
|
|
137
|
+
refuse("native_registered_start_changed");
|
|
138
|
+
binding = retained.binding;
|
|
139
|
+
nativeStart = retained;
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
if (error instanceof AppError)
|
|
143
|
+
throw error;
|
|
144
|
+
return refuse("native_registered_start_changed");
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const starts = context.db.all("SELECT ws.id,ws.hook_event_id,ws.start_receipt_json FROM work_sessions ws JOIN sessions s ON s.project_id=? AND s.session_id=ws.session_id WHERE ws.work_id=? AND ws.status='running' AND s.run_id=? AND s.worker_id=? AND s.provider_session_id=? LIMIT 2", [input.projectId, input.workId, input.runId, input.workId, input.providerSessionId]);
|
|
148
|
+
if (work.status !== "running" || !binding || binding.provider_session_id !== input.providerSessionId || starts.length !== 1)
|
|
149
|
+
refuse("native_registered_start_changed");
|
|
150
|
+
if (nativeStart && (nativeStart.work_session_id !== starts[0].id || nativeStart.hook_event_key !== starts[0].hook_event_id
|
|
151
|
+
|| binding.operation_id !== `native-hook:${nativeStart.hook_event_key}`
|
|
152
|
+
|| !context.db.get("SELECT 1 FROM hook_events WHERE project_id=? AND event_key=? AND status='claimed' AND provider_session_id=?", [input.projectId, nativeStart.hook_event_key, input.providerSessionId])))
|
|
153
|
+
refuse("native_registered_start_changed");
|
|
154
|
+
try {
|
|
155
|
+
const text = starts[0].start_receipt_json;
|
|
156
|
+
if (typeof text !== "string" || Buffer.byteLength(text) > 16 * 1024 * 1024)
|
|
157
|
+
refuse("native_registered_start_changed");
|
|
158
|
+
const start = JSON.parse(text), packet = JSON.parse(start.context), profile = JSON.parse(binding.profile_json), metadata = profile.memory_repair_copy;
|
|
159
|
+
if (profile.delegation?.mode !== (native ? "native" : "external") || start.version !== 2 || createHash("sha256").update(start.context).digest("hex") !== start.context_sha256
|
|
160
|
+
|| typeof metadata?.root !== "string" || metadata.root !== `works/${input.workId}/writer-workspaces/generation-${start.generation}`
|
|
161
|
+
|| packet.work_id !== input.workId || packet.run_id !== input.runId || packet.workspace_root !== path.join(runRoot, metadata.root)
|
|
162
|
+
|| packet.memory_repair_input?.provider_session_id !== input.providerSessionId
|
|
163
|
+
|| packet.memory_repair_input?.launch_operation_id !== binding.operation_id)
|
|
164
|
+
refuse("native_registered_start_changed");
|
|
165
|
+
return admitted(packet.workspace_root);
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
if (error instanceof AppError)
|
|
169
|
+
throw error;
|
|
170
|
+
return refuse("native_registered_start_changed");
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/** Native children have no external launch row. Read the immutable physical
|
|
174
|
+
* Hook and copy journal instead; preparation cannot manufacture a Session. */
|
|
175
|
+
export function readMemoryRepairPhysicalBinding(context, workId, route, operationId) {
|
|
176
|
+
const external = context.db.get("SELECT 1 FROM sqlite_schema WHERE type='table' AND name='work_launches'")
|
|
177
|
+
? context.db.get("SELECT operation_id,status,profile_json,profile_hash,provider_session_id FROM work_launches WHERE work_id=?", [workId]) : undefined;
|
|
178
|
+
if (route.delegation.mode === "external")
|
|
179
|
+
return external;
|
|
180
|
+
if (external)
|
|
181
|
+
refuse("native_work_has_external_launch");
|
|
182
|
+
const work = context.db.get("SELECT project_id,run_id,parent_work_id FROM works WHERE work_id=?", [workId]);
|
|
183
|
+
if (!work?.parent_work_id)
|
|
184
|
+
refuse("owned_native_work_required");
|
|
185
|
+
const run = context.db.get("SELECT run_root,run_dir FROM runs WHERE project_id=? AND id=?", [work.project_id, work.run_id]);
|
|
186
|
+
if (!run)
|
|
187
|
+
refuse("owned_native_run_required");
|
|
188
|
+
const retained = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type=? AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.work_id')=? LIMIT 2", [work.project_id, event, work.run_id, workId]);
|
|
189
|
+
let prior;
|
|
190
|
+
if (retained.length) {
|
|
191
|
+
try {
|
|
192
|
+
if (retained.length !== 1 || Buffer.byteLength(retained[0].payload_json) > 1024 * 1024)
|
|
193
|
+
refuse("ambiguous_native_binding");
|
|
194
|
+
prior = JSON.parse(retained[0].payload_json);
|
|
195
|
+
if (!prior || prior.schema_id !== "dd-flow/memory-repair-native-binding@1" || prior.project_id !== work.project_id
|
|
196
|
+
|| prior.run_id !== work.run_id || prior.work_id !== workId || typeof prior.work_session_id !== "string"
|
|
197
|
+
|| !prior.work_session_id || Object.keys(prior).sort().join(",") !== "binding,hook_event_key,parent_provider_session_id,parent_session_id,project_id,run_id,schema_id,work_id,work_session_id")
|
|
198
|
+
refuse("foreign_native_binding");
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
if (error instanceof AppError)
|
|
202
|
+
throw error;
|
|
203
|
+
refuse("invalid_native_binding");
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const key = prior?.hook_event_key ?? (operationId?.startsWith("native-hook:") ? operationId.slice(12) : context.env.DD_FLOW_MEMORY_REPAIR_HOOK_EVENT);
|
|
207
|
+
if (!key || key.length > 1024)
|
|
208
|
+
refuse("native_start_hook_required");
|
|
209
|
+
const reader = { ...context };
|
|
210
|
+
delete reader.observeLifecycleHook;
|
|
211
|
+
let identity;
|
|
212
|
+
try {
|
|
213
|
+
identity = hookSessionIdentity(reader, work.project_id, key);
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
if (error instanceof AppError && error.code === "hook_event_not_found")
|
|
217
|
+
refuse("native_start_hook_required");
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
const hook = context.db.get("SELECT status,created_at FROM hook_events WHERE project_id=? AND event_key=? AND id=? AND event_name='PreToolUse'", [work.project_id, key, identity.hookEventId]);
|
|
221
|
+
const parents = context.db.all("SELECT DISTINCT s.session_id,s.harness,s.status,s.current_stage,s.run_id,s.worker_id,s.provider_session_id FROM work_sessions ws JOIN sessions s ON s.project_id=? AND s.session_id=ws.session_id WHERE ws.work_id=? AND s.session_id=? LIMIT 2", [work.project_id, work.parent_work_id, identity.parentSessionId]);
|
|
222
|
+
if (!hook || !["observed", "claimed"].includes(hook.status) || parents.length !== 1)
|
|
223
|
+
refuse("native_parent_or_hook_required");
|
|
224
|
+
const parent = parents[0];
|
|
225
|
+
if (parent.run_id !== work.run_id || !prior && parent.worker_id !== work.parent_work_id || parent.harness !== runtimeHarness(route.coordinator)
|
|
226
|
+
|| identity.parentSessionId !== parent.session_id || identity.sessionId === parent.session_id
|
|
227
|
+
|| identity.harness !== runtimeHarness(route.worker) || identity.model !== route.worker.model
|
|
228
|
+
|| identity.provider && identity.provider !== route.worker.provider)
|
|
229
|
+
refuse("native_route_or_parent_mismatch");
|
|
230
|
+
if (context.db.get("SELECT 1 FROM work_sessions ws JOIN works w ON w.work_id=ws.work_id JOIN sessions s ON s.project_id=w.project_id AND s.session_id=ws.session_id WHERE w.project_id=? AND w.run_id=? AND w.work_id<>? AND s.harness=? AND s.provider_session_id=? LIMIT 1", [work.project_id, work.run_id, workId, identity.harness, identity.providerSessionId]))
|
|
231
|
+
refuse("native_repair_session_not_fresh");
|
|
232
|
+
let freshnessAt = context.now();
|
|
233
|
+
const currentId = context.env.DD_FLOW_CURRENT_INVOCATION;
|
|
234
|
+
const admission = currentId ? currentLifecycleAdmission(context, currentId) : null;
|
|
235
|
+
if (admission) {
|
|
236
|
+
const project = context.db.get("SELECT root FROM projects WHERE id=?", [work.project_id]);
|
|
237
|
+
const attempt = context.db.get("SELECT event_key,scope_json,command FROM lifecycle_invocations WHERE id=?", [currentId]);
|
|
238
|
+
if (attempt?.event_key === key) {
|
|
239
|
+
const scope = JSON.parse(attempt.scope_json), parsed = parseLifecycleCommand(attempt.command);
|
|
240
|
+
if (scope.runId !== work.run_id || scope.projectRoot !== project?.root
|
|
241
|
+
|| scope.generation !== (recoveryGuard(context, work.project_id, work.run_id)?.generation ?? 0))
|
|
242
|
+
refuse("native_invocation_scope_changed");
|
|
243
|
+
if (parsed.kind !== "none" && parsed.supported !== false
|
|
244
|
+
&& parsed.invocation.operation === "work_start"
|
|
245
|
+
&& resolveWorkReference(context, commandPosition(parsed.invocation, 0), { projectId: work.project_id }).work_id === workId)
|
|
246
|
+
freshnessAt = admission.admittedAt;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (!prior && (parent.status !== "active" || parent.current_stage !== "review" || Date.parse(hook.created_at) < Date.parse(freshnessAt) - 60_000
|
|
250
|
+
|| Date.parse(hook.created_at) > Date.parse(freshnessAt) || Date.parse(freshnessAt) > Date.parse(context.now())
|
|
251
|
+
|| !Number.isFinite(Date.parse(hook.created_at))))
|
|
252
|
+
refuse("native_hook_or_parent_not_current");
|
|
253
|
+
if (prior && (hook.status !== "claimed" || !context.db.get("SELECT 1 FROM work_sessions ws JOIN sessions s ON s.project_id=? AND s.session_id=ws.session_id WHERE ws.id=? AND ws.work_id=? AND ws.hook_event_id=? AND ws.session_id=? AND s.run_id=? AND s.worker_id=? AND s.provider_session_id=? AND s.provider_parent_session_id=?", [work.project_id, prior.work_session_id, workId, key, identity.sessionId, work.run_id, workId, identity.providerSessionId, parent.session_id])))
|
|
254
|
+
refuse("native_registered_start_changed");
|
|
255
|
+
const copies = context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type='memory.repair_writer_copy_ready' AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.work_id')=? LIMIT 2", [work.project_id, work.run_id, workId]);
|
|
256
|
+
if (copies.length !== 1 || Buffer.byteLength(copies[0].payload_json) > 12 * 1024 * 1024)
|
|
257
|
+
refuse("owned_ready_copy_required");
|
|
258
|
+
let copy;
|
|
259
|
+
let intent;
|
|
260
|
+
try {
|
|
261
|
+
const value = JSON.parse(copies[0].payload_json), workspace_manifest_sha256 = value.workspace_manifest_sha256;
|
|
262
|
+
delete value.workspace_manifest_sha256;
|
|
263
|
+
delete value.workspace_manifest_base64;
|
|
264
|
+
copy = { ...value, workspace_manifest_sha256 };
|
|
265
|
+
intent = value;
|
|
266
|
+
if (value.schema_id !== "dd-flow/memory-repair-writer-copy-intent@1" || value.project_id !== work.project_id
|
|
267
|
+
|| value.run_id !== work.run_id || value.work_id !== workId || !Number.isSafeInteger(value.generation) || value.generation < 0
|
|
268
|
+
|| !path.isAbsolute(value.run_root) || value.run_root !== (run.run_root ?? run.run_dir)
|
|
269
|
+
|| value.copy_relative !== `works/${workId}/writer-workspaces/generation-${value.generation}`
|
|
270
|
+
|| !/^[a-f0-9]{64}$/.test(workspace_manifest_sha256) || !/^[a-f0-9]{64}$/.test(value.baseline_registration_sha256))
|
|
271
|
+
refuse("foreign_ready_copy");
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
if (error instanceof AppError)
|
|
275
|
+
throw error;
|
|
276
|
+
return refuse("invalid_ready_copy");
|
|
277
|
+
}
|
|
278
|
+
const physical = memoryRepairExecutionIdentity(context, { projectId: work.project_id, runId: work.run_id,
|
|
279
|
+
workId, eventKey: key, root: path.join(copy.run_root, copy.copy_relative) });
|
|
280
|
+
const frozen = { coordinator: route.coordinator, worker: route.worker, delegation: { mode: "native", max_parallel: null } };
|
|
281
|
+
const binding = { operation_id: `native-hook:${key}`, status: "start_requested",
|
|
282
|
+
profile_json: JSON.stringify({ ...frozen, memory_repair_copy: { root: copy.copy_relative,
|
|
283
|
+
copy_intent_sha256: hybridDigest(intent), workspace_manifest_sha256: copy.workspace_manifest_sha256,
|
|
284
|
+
baseline_registration_sha256: copy.baseline_registration_sha256 },
|
|
285
|
+
...(physical.invocationId ? { native_execution_cwd: { invocation_id: physical.invocationId, root: physical.identity.cwd } } : {}) }),
|
|
286
|
+
profile_hash: createHash("sha256").update(JSON.stringify(frozen)).digest("hex"), provider_session_id: identity.providerSessionId };
|
|
287
|
+
if (prior && (prior.parent_session_id !== parent.session_id || prior.parent_provider_session_id !== parent.provider_session_id
|
|
288
|
+
|| hybridDigest(prior.binding) !== hybridDigest(binding)))
|
|
289
|
+
refuse("native_binding_changed");
|
|
290
|
+
return binding;
|
|
291
|
+
}
|
|
292
|
+
/** Commit only inside the same writer that claimed the Hook and inserted
|
|
293
|
+
* the physical WorkSession; rollback removes both authorities together. */
|
|
294
|
+
export function recordMemoryRepairNativeBinding(context, workId, route, binding, workSessionId) {
|
|
295
|
+
if (route.delegation.mode !== "native")
|
|
296
|
+
return;
|
|
297
|
+
if (context.db.isOwnedWriteTransaction?.() !== true)
|
|
298
|
+
refuse("owned_claim_writer_required");
|
|
299
|
+
const actual = readMemoryRepairPhysicalBinding(context, workId, route, binding.operation_id);
|
|
300
|
+
const key = binding.operation_id.slice(12);
|
|
301
|
+
const row = context.db.get("SELECT w.project_id,w.run_id,he.parent_session_id,p.provider_session_id AS parent_provider_session_id FROM works w JOIN work_sessions ws ON ws.work_id=w.work_id JOIN hook_events he ON he.project_id=w.project_id AND he.event_key=ws.hook_event_id AND he.session_id=ws.session_id JOIN sessions p ON p.project_id=w.project_id AND p.session_id=he.parent_session_id WHERE w.work_id=? AND ws.id=? AND ws.status='running' AND he.status='claimed' AND he.event_key=? AND he.provider_session_id=?", [workId, workSessionId, key, binding.provider_session_id]);
|
|
302
|
+
if (!row || hybridDigest(actual) !== hybridDigest(binding))
|
|
303
|
+
refuse("native_claim_identity_changed");
|
|
304
|
+
const payload = { schema_id: "dd-flow/memory-repair-native-binding@1", project_id: row.project_id, run_id: row.run_id,
|
|
305
|
+
work_id: workId, work_session_id: workSessionId, parent_session_id: row.parent_session_id,
|
|
306
|
+
parent_provider_session_id: row.parent_provider_session_id, hook_event_key: key, binding };
|
|
307
|
+
const prior = context.db.get("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type=? AND json_extract(payload_json,'$.run_id')=? AND json_extract(payload_json,'$.work_id')=?", [row.project_id, event, row.run_id, workId]);
|
|
308
|
+
if (prior && hybridDigest(JSON.parse(prior.payload_json)) !== hybridDigest(payload))
|
|
309
|
+
refuse("native_binding_changed");
|
|
310
|
+
if (!prior)
|
|
311
|
+
appendAudit(context, { projectId: row.project_id, eventType: event, payload });
|
|
312
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
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
|
+
function refuse(reason) {
|
|
6
|
+
throw new AppError("memory_repair_staging_invalid", "Publication staging file is not the owned exact output prefix", 1, { reason, effect: "no_effect" });
|
|
7
|
+
}
|
|
8
|
+
const noFollow = (fs.constants.O_NOFOLLOW ?? 0) | (fs.constants.O_NONBLOCK ?? 0);
|
|
9
|
+
function regular(stat, limit) {
|
|
10
|
+
if (!stat.isFile() || stat.nlink !== 1 || stat.size > limit)
|
|
11
|
+
refuse("linked_special_or_oversized_staging_file");
|
|
12
|
+
}
|
|
13
|
+
function same(left, right) {
|
|
14
|
+
return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mode === right.mode
|
|
15
|
+
&& left.nlink === right.nlink && left.mtimeMs === right.mtimeMs && left.ctimeMs === right.ctimeMs;
|
|
16
|
+
}
|
|
17
|
+
/** Read-only verification after independently retained readiness. Unlike staging
|
|
18
|
+
* recovery, a ready file is never repaired if it becomes missing or partial.
|
|
19
|
+
*/
|
|
20
|
+
export function verifyRetainedMemoryRepairFile(file, value, mode) {
|
|
21
|
+
if (!(value instanceof Uint8Array) || value.byteLength > 16 * 1024 * 1024)
|
|
22
|
+
refuse("invalid_ready_bytes");
|
|
23
|
+
const before = fs.lstatSync(file);
|
|
24
|
+
regular(before, value.byteLength);
|
|
25
|
+
if (before.size !== value.byteLength || (before.mode & 0o7777) !== mode)
|
|
26
|
+
refuse("ready_staging_file_changed");
|
|
27
|
+
const fd = fs.openSync(file, fs.constants.O_RDONLY | noFollow);
|
|
28
|
+
try {
|
|
29
|
+
if (!same(before, fs.fstatSync(fd)))
|
|
30
|
+
refuse("staging_identity_changed");
|
|
31
|
+
const bytes = Buffer.alloc(value.byteLength);
|
|
32
|
+
let offset = 0;
|
|
33
|
+
while (offset < bytes.length) {
|
|
34
|
+
const count = fs.readSync(fd, bytes, offset, bytes.length - offset, offset);
|
|
35
|
+
if (!count)
|
|
36
|
+
refuse("ready_staging_file_changed");
|
|
37
|
+
offset += count;
|
|
38
|
+
}
|
|
39
|
+
if (!bytes.equals(Buffer.from(value)) || !same(before, fs.fstatSync(fd)) || !same(before, fs.lstatSync(file)))
|
|
40
|
+
refuse("ready_staging_file_changed");
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
fs.closeSync(fd);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Low-level staging effects only. The publication owner must independently
|
|
47
|
+
* journal this exact directory/slot before calling. This function grants no
|
|
48
|
+
* SQL/Session authority and never changes an MB destination. Unknown existing
|
|
49
|
+
* bytes are preserved; an owned exact partial file is completed by append.
|
|
50
|
+
*/
|
|
51
|
+
export function stageRetainedMemoryRepairFile(directory, slot, value, mode) {
|
|
52
|
+
if (!path.isAbsolute(directory) || fs.realpathSync(directory) !== path.resolve(directory)
|
|
53
|
+
|| !fs.lstatSync(directory).isDirectory() || (fs.lstatSync(directory).mode & 0o7777) !== 0o700
|
|
54
|
+
|| !/^action-[0-9]{1,6}\.tmp$/.test(slot) || !(value instanceof Uint8Array) || value.byteLength > 16 * 1024 * 1024
|
|
55
|
+
|| !Number.isInteger(mode) || mode < 0 || mode > 0o7777)
|
|
56
|
+
refuse("invalid_staging_binding");
|
|
57
|
+
const directoryIdentity = fs.lstatSync(directory);
|
|
58
|
+
const bytes = Buffer.from(value), file = path.join(directory, slot);
|
|
59
|
+
let fd, changed = false, initialSize = 0, replayed = false;
|
|
60
|
+
try {
|
|
61
|
+
const prior = fs.lstatSync(file, { throwIfNoEntry: false });
|
|
62
|
+
if (prior) {
|
|
63
|
+
replayed = true;
|
|
64
|
+
regular(prior, bytes.length);
|
|
65
|
+
if ((prior.mode & 0o7777) !== 0o600 && (prior.size !== bytes.length || (prior.mode & 0o7777) !== mode))
|
|
66
|
+
refuse("staging_mode_changed");
|
|
67
|
+
fd = fs.openSync(file, fs.constants.O_RDONLY | noFollow);
|
|
68
|
+
if (!same(prior, fs.fstatSync(fd)))
|
|
69
|
+
refuse("staging_identity_changed");
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
fd = fs.openSync(file, fs.constants.O_RDWR | fs.constants.O_CREAT | fs.constants.O_EXCL | noFollow, 0o600);
|
|
73
|
+
changed = true;
|
|
74
|
+
}
|
|
75
|
+
const before = fs.fstatSync(fd);
|
|
76
|
+
regular(before, bytes.length);
|
|
77
|
+
initialSize = before.size;
|
|
78
|
+
const retained = Buffer.alloc(initialSize);
|
|
79
|
+
let read = 0;
|
|
80
|
+
while (read < retained.length) {
|
|
81
|
+
const count = fs.readSync(fd, retained, read, retained.length - read, read);
|
|
82
|
+
if (!count)
|
|
83
|
+
refuse("staging_changed_during_read");
|
|
84
|
+
read += count;
|
|
85
|
+
}
|
|
86
|
+
if (!same(before, fs.fstatSync(fd)) || !retained.equals(bytes.subarray(0, initialSize)))
|
|
87
|
+
refuse("unknown_staging_bytes");
|
|
88
|
+
if (initialSize < bytes.length && replayed) {
|
|
89
|
+
fs.closeSync(fd);
|
|
90
|
+
fd = undefined;
|
|
91
|
+
fd = fs.openSync(file, fs.constants.O_RDWR | noFollow);
|
|
92
|
+
if (!same(before, fs.fstatSync(fd)))
|
|
93
|
+
refuse("staging_identity_changed");
|
|
94
|
+
}
|
|
95
|
+
let written = initialSize;
|
|
96
|
+
while (written < bytes.length) {
|
|
97
|
+
changed = true;
|
|
98
|
+
const count = fs.writeSync(fd, bytes, written, bytes.length - written, written);
|
|
99
|
+
if (!count)
|
|
100
|
+
throw new Error("No progress writing retained staging bytes");
|
|
101
|
+
written += count;
|
|
102
|
+
}
|
|
103
|
+
if ((fs.fstatSync(fd).mode & 0o7777) !== mode) {
|
|
104
|
+
changed = true;
|
|
105
|
+
fs.fchmodSync(fd, mode);
|
|
106
|
+
}
|
|
107
|
+
fs.fsyncSync(fd);
|
|
108
|
+
const after = fs.fstatSync(fd);
|
|
109
|
+
regular(after, bytes.length);
|
|
110
|
+
if ((after.mode & 0o7777) !== mode)
|
|
111
|
+
refuse("staging_final_mode_changed");
|
|
112
|
+
if (after.size !== bytes.length)
|
|
113
|
+
refuse("staging_identity_changed");
|
|
114
|
+
const verified = Buffer.alloc(bytes.length);
|
|
115
|
+
let checked = 0;
|
|
116
|
+
while (checked < verified.length) {
|
|
117
|
+
const count = fs.readSync(fd, verified, checked, verified.length - checked, checked);
|
|
118
|
+
if (!count)
|
|
119
|
+
refuse("staging_changed_during_read");
|
|
120
|
+
checked += count;
|
|
121
|
+
}
|
|
122
|
+
if (!verified.equals(bytes) || !same(after, fs.fstatSync(fd)))
|
|
123
|
+
refuse("staging_output_changed");
|
|
124
|
+
const named = fs.lstatSync(file);
|
|
125
|
+
if (after.size !== bytes.length || !same(after, named))
|
|
126
|
+
refuse("staging_identity_changed");
|
|
127
|
+
fs.closeSync(fd);
|
|
128
|
+
fd = undefined;
|
|
129
|
+
const parent = fs.openSync(directory, fs.constants.O_RDONLY | noFollow);
|
|
130
|
+
try {
|
|
131
|
+
const current = fs.fstatSync(parent), namedDirectory = fs.lstatSync(directory);
|
|
132
|
+
if (!current.isDirectory() || current.dev !== directoryIdentity.dev || current.ino !== directoryIdentity.ino
|
|
133
|
+
|| current.dev !== namedDirectory.dev || current.ino !== namedDirectory.ino || (current.mode & 0o7777) !== 0o700)
|
|
134
|
+
refuse("staging_directory_changed");
|
|
135
|
+
fs.fsyncSync(parent);
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
fs.closeSync(parent);
|
|
139
|
+
}
|
|
140
|
+
return { path: file, sha256: createHash("sha256").update(bytes).digest("hex"), status: "ready", replayed,
|
|
141
|
+
bytes_appended: bytes.length - initialSize };
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
if (!changed && error instanceof AppError)
|
|
145
|
+
throw error;
|
|
146
|
+
throw new AppError("memory_repair_staging_pending", "Owned staging output remains pending; preserve it for exact-prefix reconciliation", 1, { effect: "publication_pending", path: file, cause: error instanceof Error ? error.message : String(error) });
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
if (fd !== undefined)
|
|
150
|
+
fs.closeSync(fd);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { AppError } from "../shared/errors.js";
|
|
2
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
3
|
+
import { RetainedMemoryRepairPublicationPlan } from "./memory-repair-publication-plan.js";
|
|
4
|
+
/** Metadata decoding only: no start, source, Session or publication authority.
|
|
5
|
+
* v1 is never promoted to v2 by optional caller fields. */
|
|
6
|
+
export function retainedMemoryRepairPublicationPlans(receipt) {
|
|
7
|
+
if (!receipt || typeof receipt !== "object" || Array.isArray(receipt)) {
|
|
8
|
+
throw new AppError("memory_repair_publication_invalid", "Retained publication intent must be an object", 1, { reason: "invalid_retained_intent_shape", effect: "no_effect" });
|
|
9
|
+
}
|
|
10
|
+
const fields = ["schema_id", "project_id", "run_id", "work_id", "generation", "work_session_id", "source_root", "private_root", "output_sha256",
|
|
11
|
+
"start_receipt_sha256", "result_sha256", "result_base64", "delta_sha256", "transition_sha256", "before_sha256", "before_base64", "after_sha256", "after_base64", "actions"];
|
|
12
|
+
const current = receipt.schema_id === "dd-flow/memory-repair-publication-intent@2";
|
|
13
|
+
if (current)
|
|
14
|
+
fields.push("source_view_sha256", "source_receipt_sha256", "private_plan_sha256", "private_plan");
|
|
15
|
+
if ((!current && receipt.schema_id !== "dd-flow/memory-repair-publication-intent@1")
|
|
16
|
+
|| Object.keys(receipt).sort().join(",") !== fields.sort().join(",")
|
|
17
|
+
|| current && (!receipt.private_plan || typeof receipt.private_plan !== "object" || Array.isArray(receipt.private_plan)
|
|
18
|
+
|| [receipt.source_view_sha256, receipt.source_receipt_sha256, receipt.private_plan_sha256].some(value => typeof value !== "string" || !/^[a-f0-9]{64}$/.test(value)))
|
|
19
|
+
|| Buffer.byteLength(JSON.stringify(receipt)) > 160 * 1024 * 1024) {
|
|
20
|
+
throw new AppError("memory_repair_publication_invalid", "Retained publication intent has a foreign version or shape", 1, { reason: "invalid_retained_intent_shape", effect: "no_effect" });
|
|
21
|
+
}
|
|
22
|
+
const retained = { before_sha256: receipt.before_sha256, before_base64: receipt.before_base64,
|
|
23
|
+
after_sha256: receipt.after_sha256, after_base64: receipt.after_base64, actions: receipt.actions };
|
|
24
|
+
const plan = RetainedMemoryRepairPublicationPlan.restore(Buffer.from(JSON.stringify(retained)), hybridDigest(retained));
|
|
25
|
+
const privatePlan = current ? RetainedMemoryRepairPublicationPlan.restore(Buffer.from(JSON.stringify(receipt.private_plan)), receipt.private_plan_sha256) : plan;
|
|
26
|
+
return { current, plan, privatePlan };
|
|
27
|
+
}
|