@deksden-com/dd-flow-cli 0.9.0-beta.144 → 0.9.0-beta.149
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +130 -0
- package/README.md +13 -8
- package/dist/build-info.json +7 -7
- package/dist/cli/command-inputs.js +13 -3
- package/dist/cli/command-metadata.js +51 -16
- package/dist/cli/diagnostic-invocation.js +46 -0
- package/dist/cli/diagnostic.js +120 -0
- package/dist/cli/help.js +67 -1
- package/dist/cli/hook-ingress.js +27 -3
- package/dist/cli/memory-flow-admission.js +9 -3
- package/dist/cli/memory-init-invocation.js +10 -3
- package/dist/cli/memory-init.js +77 -68
- package/dist/cli/memory-storage-admission.js +3 -2
- package/dist/cli/memory-storage-qualification.js +105 -0
- package/dist/cli/memory-storage-transition.js +123 -0
- package/dist/cli/memory-upgrade.js +189 -0
- package/dist/cli/native-hook-ingress.js +14 -4
- package/dist/cli/native-hook-preparation.js +55 -0
- package/dist/cli/response-file.js +2 -1
- package/dist/cli/run-cli.js +79 -40
- package/dist/cli/setup.js +2 -1
- package/dist/cli.js +18 -0
- package/dist/domain/diagnostic-aspects.js +103 -0
- package/dist/domain/diagnostic-flows.js +8 -0
- package/dist/domain/stage-handler-identities.js +8 -0
- package/dist/harness-runtime/bin/dd-agy.mjs +3 -3
- package/dist/harness-runtime/bin/dd-grok.mjs +3 -3
- package/dist/harness-runtime/bin/dd-zcode.mjs +5 -5
- package/dist/harness-runtime/lib/agy-error-boundary.mjs +58 -0
- package/dist/harness-runtime/lib/agy-native-inspection.mjs +216 -0
- package/dist/harness-runtime/lib/agy-native_pb.js +117 -0
- package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +84 -10
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +366 -52
- package/dist/harness-runtime/lib/dd-agy.mjs +17 -3
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +27 -13
- package/dist/harness-runtime/lib/dd-codex.mjs +4 -0
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +4 -3
- package/dist/harness-runtime/lib/dd-droid.mjs +71 -29
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +73 -16
- package/dist/harness-runtime/lib/dd-grok.mjs +6 -3
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +66 -20
- package/dist/harness-runtime/lib/dd-opencode.mjs +30 -3
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +7 -6
- package/dist/harness-runtime/lib/dd-zcode.mjs +44 -14
- package/dist/harness-runtime/lib/delegation-instructions.mjs +1 -1
- package/dist/harness-runtime/lib/driver-recovery.mjs +9 -8
- package/dist/harness-runtime/lib/entity-references.d.mts +2 -0
- package/dist/harness-runtime/lib/entity-references.mjs +9 -0
- package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -1
- package/dist/harness-runtime/lib/managed-daemon.mjs +24 -9
- package/dist/harness-runtime/lib/native-children.d.mts +8 -1
- package/dist/harness-runtime/lib/native-children.mjs +114 -8
- package/dist/harness-runtime/lib/native-hook-command.mjs +4 -4
- package/dist/harness-runtime/lib/native-request-proof.mjs +1 -1
- package/dist/harness-runtime/lib/observation-clock.d.mts +2 -2
- package/dist/harness-runtime/lib/observation-clock.mjs +7 -4
- package/dist/harness-runtime/lib/opencode-plugin.mjs +3 -3
- package/dist/harness-runtime/lib/operation-errors.mjs +1 -1
- package/dist/harness-runtime/lib/provider-overload-policy.d.mts +5 -3
- package/dist/harness-runtime/lib/provider-overload-policy.mjs +20 -7
- package/dist/harness-runtime/lib/runner-events.mjs +3 -1
- package/dist/harness-runtime/lib/setup-progress.mjs +8 -6
- package/dist/harness-runtime/lib/tool-observations.mjs +1 -1
- package/dist/schemas/diagnostic-stage-result.schema.json +354 -0
- package/dist/schemas/diagnostic-work-result.schema.json +296 -0
- package/dist/schemas/execution-bundle.schema.json +7 -1
- package/dist/schemas/mb-upgrade-stage-result.schema.json +863 -0
- package/dist/schemas/memory-repair-work-result.schema.json +9 -2
- package/dist/schemas/project-flow-pack-manifest.schema.json +1 -1
- package/dist/schemas/release-impact.schema.json +3 -3
- package/dist/schemas/stage-start-response.schema.json +4 -0
- package/dist/services/accepted-intent.js +13 -1
- package/dist/services/canon.js +43 -7
- package/dist/services/check-environment.js +21 -0
- package/dist/services/check-runner-evidence.js +267 -15
- package/dist/services/cli-operation-classifier.js +1 -1
- package/dist/services/cli-upgrade-allowlist.js +1 -1
- package/dist/services/code-checks.js +34 -19
- package/dist/services/compatibility-preflight.js +5 -2
- package/dist/services/controller-execution.js +10 -4
- package/dist/services/controller-fanout.js +34 -10
- package/dist/services/controller-memory-task.js +45 -0
- package/dist/services/controller-memory-worker.js +49 -0
- package/dist/services/dashboard.js +2 -2
- package/dist/services/descriptor-run-settlement.js +5 -3
- package/dist/services/descriptor-run-start.js +46 -20
- package/dist/services/descriptor-stage-coordinator.js +66 -13
- package/dist/services/descriptor-stage-settlement.js +2 -1
- package/dist/services/descriptor-stage-start.js +40 -4
- package/dist/services/diagnostic-evidence.js +73 -0
- package/dist/services/diagnostic-report-publication.js +72 -0
- package/dist/services/diagnostic-report.js +67 -0
- package/dist/services/diagnostic-run-bundle.js +74 -0
- package/dist/services/diagnostic-snapshot.js +159 -0
- package/dist/services/diagnostic-stage-handlers.js +108 -0
- package/dist/services/diagnostic-stage-results.js +132 -0
- package/dist/services/diagnostic-start.js +84 -0
- package/dist/services/diagnostic-work-batch.js +34 -0
- package/dist/services/diagnostic-work-collection.js +97 -0
- package/dist/services/diagnostic-work-registration.js +101 -0
- package/dist/services/diagnostic-work-results.js +93 -0
- package/dist/services/engines.js +64 -14
- package/dist/services/eval-snapshots.js +186 -119
- package/dist/services/execution-bundles.js +19 -7
- package/dist/services/external-work-launch.js +5 -3
- package/dist/services/flow-catalog.js +13 -4
- package/dist/services/frozen-engine.js +148 -11
- package/dist/services/hook-memory-routing.js +188 -0
- package/dist/services/hook-recovery-guard.js +14 -0
- package/dist/services/hook-session-observation.js +24 -0
- package/dist/services/hooks.js +27 -8
- package/dist/services/hybrid-contracts.js +4 -13
- package/dist/services/hybrid-digest.js +14 -0
- package/dist/services/installed-flow-source.js +47 -4
- package/dist/services/lifecycle-invocations.js +179 -38
- package/dist/services/memory-accepted-composition.js +5 -4
- package/dist/services/memory-completed-correction-wave.js +23 -15
- package/dist/services/memory-compose-handler.js +163 -21
- package/dist/services/memory-composition-acceptance.js +186 -10
- package/dist/services/memory-composition-graph.js +42 -11
- package/dist/services/memory-composition-registration.js +23 -8
- package/dist/services/memory-composition-work-custody.js +14 -9
- package/dist/services/memory-composition-work-results.js +15 -3
- package/dist/services/memory-corrected-review-batch.js +10 -3
- package/dist/services/memory-corrected-review-findings.js +4 -2
- package/dist/services/memory-corrected-review-inputs.js +10 -5
- package/dist/services/memory-corrected-review-receipt.js +17 -9
- package/dist/services/memory-corrected-review-registration.js +3 -2
- package/dist/services/memory-correction-check-receipt.js +20 -4
- package/dist/services/memory-custody-snapshot.js +36 -0
- package/dist/services/memory-finish-evidence.js +44 -22
- package/dist/services/memory-finish-handler.js +37 -23
- package/dist/services/memory-finish-integration.js +218 -48
- package/dist/services/memory-finish-readiness.js +66 -19
- package/dist/services/memory-git-integration.js +87 -18
- package/dist/services/memory-historical-corrected-review.js +21 -15
- package/dist/services/memory-historical-original-review.js +22 -9
- package/dist/services/memory-historical-repair-registration.js +14 -8
- package/dist/services/memory-historical-second-review.js +30 -11
- package/dist/services/memory-init-contexts.js +44 -2
- package/dist/services/memory-init-controller-start.js +27 -8
- package/dist/services/memory-init-git-policy.js +50 -21
- package/dist/services/memory-init-lint-capability.js +24 -6
- package/dist/services/memory-init-native-capacity.js +17 -4
- package/dist/services/memory-init-preflight.js +21 -17
- package/dist/services/memory-init-request-candidate.js +73 -17
- package/dist/services/memory-init-request-input.js +23 -7
- package/dist/services/memory-init-run-bundle.js +49 -12
- package/dist/services/memory-init-storage.js +17 -7
- package/dist/services/memory-init-worktrunk-capability.js +3 -7
- package/dist/services/memory-layer.js +71 -6
- package/dist/services/memory-lint-config.js +111 -2
- package/dist/services/memory-native-review-copy.js +5 -67
- package/dist/services/memory-normal-flow-entry.js +12 -0
- package/dist/services/memory-ordinary-defaults.js +22 -5
- package/dist/services/memory-preflight-handler.js +15 -0
- package/dist/services/memory-repair-current-view.js +1 -1
- package/dist/services/memory-repair-document-delta.js +70 -33
- package/dist/services/memory-repair-history.js +12 -7
- package/dist/services/memory-repair-physical-binding.js +1 -119
- package/dist/services/memory-repair-publication-plan.js +5 -3
- package/dist/services/memory-repair-publication-source.js +1 -1
- package/dist/services/memory-repair-publication.js +3 -2
- package/dist/services/memory-repair-registration.js +2 -1
- package/dist/services/memory-repair-work-baseline.js +4 -2
- package/dist/services/memory-repair-work-completion.js +32 -12
- package/dist/services/memory-repair-work-results.js +21 -2
- package/dist/services/memory-repair-workspace.js +11 -2
- package/dist/services/memory-repair-writer-copy.js +2 -1
- package/dist/services/memory-result-binding.js +78 -12
- package/dist/services/memory-retained-review-authority.js +17 -8
- package/dist/services/memory-retained-second-baseline.js +1 -1
- package/dist/services/memory-retained-second-repairs.js +12 -6
- package/dist/services/memory-review-correction-plan.js +47 -12
- package/dist/services/memory-review-handler.js +17 -17
- package/dist/services/memory-review-inputs.js +55 -9
- package/dist/services/memory-review-limits.js +10 -0
- package/dist/services/memory-review-receipt.js +2 -2
- package/dist/services/memory-review-registration.js +13 -4
- package/dist/services/memory-review-work-custody.js +3 -1
- package/dist/services/memory-review-work-input.js +26 -7
- package/dist/services/memory-review-work-results.js +26 -4
- package/dist/services/memory-second-corrected-review-inputs.js +5 -3
- package/dist/services/memory-second-correction-plan.js +2 -1
- package/dist/services/memory-second-repair-launch.js +4 -4
- package/dist/services/memory-second-repair-source.js +1 -1
- package/dist/services/memory-stage-dispatch.js +38 -1
- package/dist/services/memory-stage-results.js +113 -22
- package/dist/services/memory-tool-probe.js +25 -0
- package/dist/services/memory-upgrade-analysis-handler.js +243 -0
- package/dist/services/memory-upgrade-bootstrap-readiness.js +251 -0
- package/dist/services/memory-upgrade-evidence.js +100 -0
- package/dist/services/memory-upgrade-inventory.js +442 -0
- package/dist/services/memory-upgrade-publication-custody.js +73 -0
- package/dist/services/memory-upgrade-publication.js +413 -0
- package/dist/services/memory-upgrade-selection.js +430 -0
- package/dist/services/memory-upgrade-start.js +60 -0
- package/dist/services/memory-upgrade-storage-readiness.js +340 -0
- package/dist/services/memory-verification-acceptance.js +38 -17
- package/dist/services/memory-verification-checks.js +92 -27
- package/dist/services/memory-verification-receipt.js +6 -5
- package/dist/services/memory-verify-handler.js +15 -13
- package/dist/services/memory-work-result-json.js +12 -0
- package/dist/services/memory-work-result-preparation.js +5 -4
- package/dist/services/merge-server.js +4 -2
- package/dist/services/migrations.js +127 -98
- package/dist/services/native-daemon-history.js +2 -1
- package/dist/services/physical-startup.js +45 -0
- package/dist/services/project-registry.js +97 -0
- package/dist/services/project-summary.js +1 -1
- package/dist/services/projects.js +3 -93
- package/dist/services/prompt-delivery.js +71 -0
- package/dist/services/provider-capacity.js +112 -46
- package/dist/services/recovery-snapshot-database.js +32 -4
- package/dist/services/release-cache.js +8 -0
- package/dist/services/repair-intents.js +1 -12
- package/dist/services/repair-receipt.js +13 -0
- package/dist/services/run-control-worker.js +35 -18
- package/dist/services/run-control.js +2 -1
- package/dist/services/run-controller-adapter.js +2 -1
- package/dist/services/run-controller-process.js +4 -7
- package/dist/services/run-controller-recovery.js +8 -2
- package/dist/services/run-controller.js +215 -94
- package/dist/services/run-fork.js +1 -1
- package/dist/services/run-recovery-guard.js +30 -0
- package/dist/services/run-recovery.js +11 -44
- package/dist/services/run-start-custody.js +17 -5
- package/dist/services/runs.js +6 -1
- package/dist/services/runtime-scope-resume.js +12 -6
- package/dist/services/runtime-scope-stop.js +3 -2
- package/dist/services/runtime-scope-worker.js +46 -19
- package/dist/services/runtime-service.js +2 -1
- package/dist/services/sessions.js +3 -24
- package/dist/services/setup.js +63 -13
- package/dist/services/stage-handlers.js +22 -7
- package/dist/services/stage-pause.js +111 -44
- package/dist/services/version-status.js +1 -1
- package/dist/services/vnext-code.js +2 -2
- package/dist/services/vnext-fanout.js +51 -9
- package/dist/services/vnext-plan-review.js +3 -5
- package/dist/services/work-registry.js +117 -39
- package/dist/services/workspace-bootstrap.js +2 -1
- package/dist/services/workspace-files.js +1 -1
- package/dist/shared/entity-references.js +1 -8
- package/dist/storage/data-preservation.js +65 -0
- package/dist/storage/database.js +53 -19
- package/dist/storage/import-guard.js +84 -27
- package/dist/storage/memory-upgrade-schema-bootstrap.js +38 -0
- package/dist/storage/paired-writer-migration.js +424 -0
- package/dist/storage/writer-contract.js +11 -4
- package/dist/storage/writer-migration.js +313 -52
- package/dist/storage/writer-restore.js +217 -0
- package/package.json +13 -6
|
@@ -16,7 +16,7 @@ import { reviewExecutionCopyHash, reviewSourceFilter, reviewSourceHash, reviewSo
|
|
|
16
16
|
import { workspaceGitFiles } from "./workspace-files.js";
|
|
17
17
|
import { qualifyAgyHooks } from "../harness-runtime/lib/dd-agy-daemon.mjs";
|
|
18
18
|
import { lifecycleRetryInstruction } from "../harness-runtime/lib/delegation-instructions.mjs";
|
|
19
|
-
import { continueProviderCapacity } from "./provider-capacity.js";
|
|
19
|
+
import { continueProviderCapacity, ownedProviderBoundary } from "./provider-capacity.js";
|
|
20
20
|
import { errorRecord } from "./run-controller-state.js";
|
|
21
21
|
import { readFlowRunExecutionBundle } from "./runs.js";
|
|
22
22
|
import { stageWorkIds } from "./stage-work-graph.js";
|
|
@@ -79,7 +79,7 @@ export async function launchExternalWork(context, input) {
|
|
|
79
79
|
catch { /* Legacy non-memory payload handling remains owned by the Work registry. */ }
|
|
80
80
|
const repairWave = payload?.kind === "memory-repair" ? payload.memory_repair?.wave : undefined;
|
|
81
81
|
if (payload?.kind === "memory-repair" && (payload.read_only !== false || ![1, 2].includes(repairWave ?? 0)
|
|
82
|
-
|| bundle?.descriptor.descriptor.id !== "mb-init" || stage !== "review")) {
|
|
82
|
+
|| (bundle?.descriptor.descriptor.id !== "mb-init" && bundle?.descriptor.descriptor.id !== "mb-upgrade") || stage !== "review")) {
|
|
83
83
|
throw new AppError("memory_repair_launch_invalid", "Repair dispatch requires its registered memory review wave", 2, { phase: "prepare", effect: "no_effect" });
|
|
84
84
|
}
|
|
85
85
|
const repairScope = { projectRoot: run.project_root, runId: work.run_id, workId: work.work_id };
|
|
@@ -214,7 +214,9 @@ export async function launchExternalWork(context, input) {
|
|
|
214
214
|
if (reviewCopy && reviewSourceHash(run.workspace_root, reviewInputs, reviewBase) !== reviewCopy.source_sha256)
|
|
215
215
|
throw new AppError("read_only_input_changed", "Authoritative review workspace changed during capacity wait", 2);
|
|
216
216
|
},
|
|
217
|
-
decision: () => {
|
|
217
|
+
decision: (boundary) => {
|
|
218
|
+
if (boundary && (!stage || !ownedProviderBoundary(context, { projectId: work.project_id, runId: work.run_id, stage, sessionId, generation, workId: work.work_id, scope: "work" }, boundary)))
|
|
219
|
+
return "reconciliation_blocked";
|
|
218
220
|
const status = context.db.get("SELECT status FROM works WHERE work_id = ?", [work.work_id])?.status;
|
|
219
221
|
return status === "completed" ? "completed" : status === "paused" ? "paused" : "continue";
|
|
220
222
|
},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { vnextStages } from "../domain/stage-catalog.js";
|
|
2
|
-
import { sdlcHandlerIdentities, memoryInitHandlerIdentities } from "../domain/stage-handler-identities.js";
|
|
2
|
+
import { sdlcHandlerIdentities, memoryInitHandlerIdentities, memoryUpgradeHandlerIdentities } from "../domain/stage-handler-identities.js";
|
|
3
3
|
import { AppError } from "../shared/errors.js";
|
|
4
|
+
import { diagnosticFlowStages } from "../domain/diagnostic-flows.js";
|
|
4
5
|
/** Engine-owned catalog, not descriptor discovery or permission to execute a RUN. */
|
|
5
6
|
export function listBundledFlows() {
|
|
6
7
|
return [
|
|
@@ -8,9 +9,17 @@ export function listBundledFlows() {
|
|
|
8
9
|
stages: vnextStages.map(stage => ({ id: stage.id, directory: stage.directory,
|
|
9
10
|
entry_handler: sdlcHandlerIdentities[stage.id].entryId, finish_handler: sdlcHandlerIdentities[stage.id].finishId })),
|
|
10
11
|
edges: vnextStages.flatMap(stage => stage.next.map((to, index) => ({ from: stage.id, to, on: index ? "not-applicable" : "accepted" }))) },
|
|
11
|
-
{ id: "mb-init",
|
|
12
|
-
stages
|
|
13
|
-
|
|
12
|
+
...[{ id: "mb-init", handlers: memoryInitHandlerIdentities }, { id: "mb-upgrade", handlers: memoryUpgradeHandlerIdentities }].map(({ id, handlers }) => {
|
|
13
|
+
const stages = Object.values(handlers);
|
|
14
|
+
return { id, handler_status: "registered", entry_stage: "preflight", terminal_stages: ["finish"],
|
|
15
|
+
stages: stages.map((handler, index) => ({ id: handler.stage, directory: `${String(index + 1).padStart(2, "0")}-${handler.stage}`, entry_handler: handler.entryId, finish_handler: handler.finishId })),
|
|
16
|
+
edges: stages.slice(0, -1).flatMap((handler, index) => [{ from: handler.stage, to: stages[index + 1].stage, on: "accepted" },
|
|
17
|
+
...(handler.stage === "verify" ? [{ from: handler.stage, to: "review", on: "needs_repair" }] : [])]) };
|
|
18
|
+
}),
|
|
19
|
+
...Object.entries(diagnosticFlowStages).map(([id, stages]) => ({ id, handler_status: "registered", entry_stage: "preflight", terminal_stages: ["report"],
|
|
20
|
+
stages: stages.map((stage, index) => ({ id: stage, directory: `${String(index + 1).padStart(2, "0")}-${stage}`,
|
|
21
|
+
entry_handler: "diagnostic-stage-entry", finish_handler: "diagnostic-stage-finish" })),
|
|
22
|
+
edges: stages.slice(0, -1).map((from, index) => ({ from, to: stages[index + 1], on: "accepted" })) }))
|
|
14
23
|
];
|
|
15
24
|
}
|
|
16
25
|
export function showBundledFlow(id) {
|
|
@@ -11,8 +11,10 @@ import { hybridDigest } from "./hybrid-contracts.js";
|
|
|
11
11
|
import { isMemoryInitResourceStorageFacts } from "./memory-init-resource-storage.js";
|
|
12
12
|
import { memoryInitStartupInputSchemas } from "./memory-init-startup-custody.js";
|
|
13
13
|
import { SETUP_STARTUP_MS } from "../harness-runtime/lib/setup-progress.mjs";
|
|
14
|
+
import { isDiagnosticFlow } from "../domain/diagnostic-flows.js";
|
|
14
15
|
function storageProbeRefuse(reason, probe) {
|
|
15
|
-
|
|
16
|
+
const summary = probe ? ` (${probe.error_code ?? probe.signal ?? `exit ${probe.status}`})` : "";
|
|
17
|
+
throw new AppError("frozen_engine_storage_incompatible", `Selected frozen engine cannot admit memory-flow storage${summary}`, 1, { reason, effect: "no_effect", ...(probe ? { probe } : {}) });
|
|
16
18
|
}
|
|
17
19
|
function probeFailure(result) {
|
|
18
20
|
const code = result.error?.code ?? null;
|
|
@@ -75,6 +77,37 @@ export class AdmittedFrozenEngine {
|
|
|
75
77
|
refuse("invalid_selection_identity");
|
|
76
78
|
return AdmittedFrozenEngine.fromIdentity(identity, expectedManifestSha256);
|
|
77
79
|
}
|
|
80
|
+
/** Physical relocation preserves the original manifest as provenance. It
|
|
81
|
+
* cannot authorize a different executable or different semantic metadata. */
|
|
82
|
+
static admitRelocation(binding, retained, expectedSha256, target) {
|
|
83
|
+
if (hash(retained) !== expectedSha256 || !shape("run-engine-binding", binding)
|
|
84
|
+
|| !shape("run-engine-binding", target) || binding.run_id !== target.run_id)
|
|
85
|
+
refuse("invalid_relocation_identity");
|
|
86
|
+
const value = json(retained);
|
|
87
|
+
if (!shape("engine-manifest", value))
|
|
88
|
+
refuse("invalid_manifest_shape");
|
|
89
|
+
const original = value;
|
|
90
|
+
if (original.snapshot_root !== binding.engine.snapshot_root
|
|
91
|
+
|| original.package_name !== binding.engine.package_name || original.package_version !== binding.engine.package_version
|
|
92
|
+
|| original.engine_version !== binding.engine.engine_version || original.integrity.checksum !== binding.engine.integrity_checksum
|
|
93
|
+
|| Object.entries(binding.engine).some(([key, value]) => key !== "snapshot_root" && value !== target.engine[key])) {
|
|
94
|
+
refuse("foreign_relocation_identity");
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const bytes = readBoundedReleaseFile(path.join(target.engine.snapshot_root, "engine.json"), 1024 * 1024);
|
|
98
|
+
const selected = AdmittedFrozenEngine.admit(target, hash(bytes));
|
|
99
|
+
const semantic = (manifest) => Object.fromEntries(Object.entries(manifest)
|
|
100
|
+
.filter(([key]) => !["snapshot_root", "package_root", "installed_at", "install_source"].includes(key)));
|
|
101
|
+
if (hybridDigest(semantic(original)) !== hybridDigest(semantic(selected.manifest())))
|
|
102
|
+
refuse("relocated_manifest_changed");
|
|
103
|
+
return selected;
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
if (error instanceof AppError && error.code === "frozen_engine_invalid")
|
|
107
|
+
throw error;
|
|
108
|
+
return refuse("snapshot_unavailable_or_unreadable");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
78
111
|
static fromIdentity(identity, expectedManifestSha256) {
|
|
79
112
|
try {
|
|
80
113
|
const root = path.resolve(identity.snapshot_root);
|
|
@@ -119,8 +152,17 @@ export class AdmittedFrozenEngine {
|
|
|
119
152
|
/** Qualify actual descriptor handlers in this immutable executable, not the
|
|
120
153
|
* invoking CLI catalog. No storage or productive operation is admitted. */
|
|
121
154
|
admitMemoryInitFlow(source, startupInputSchema) {
|
|
122
|
-
|
|
123
|
-
|
|
155
|
+
return this.#admitMemoryFlow(source, "mb-init", startupInputSchema);
|
|
156
|
+
}
|
|
157
|
+
admitMemoryUpgradeFlow(source) {
|
|
158
|
+
return this.#admitMemoryFlow(source, "mb-upgrade");
|
|
159
|
+
}
|
|
160
|
+
admitDiagnosticFlow(source, flowId) {
|
|
161
|
+
return this.#admitMemoryFlow(source, flowId);
|
|
162
|
+
}
|
|
163
|
+
#admitMemoryFlow(source, flowId, startupInputSchema) {
|
|
164
|
+
const fail = (reason, details = {}) => {
|
|
165
|
+
throw new AppError("frozen_engine_flow_incompatible", `Selected frozen engine cannot admit ${flowId} handlers`, 1, { ...details, reason, phase: "prepare", effect: "no_effect" });
|
|
124
166
|
};
|
|
125
167
|
const revalidate = () => {
|
|
126
168
|
try {
|
|
@@ -146,18 +188,25 @@ export class AdmittedFrozenEngine {
|
|
|
146
188
|
...(startupInputSchema === undefined ? {} : { startup_input_schema: startupInputSchema }) }));
|
|
147
189
|
if (input.length > 64 * 1024 * 1024)
|
|
148
190
|
fail("unbounded_source");
|
|
149
|
-
const
|
|
191
|
+
const flow = source.resolveRegisteredFlow();
|
|
192
|
+
if (flow.descriptor.id !== flowId)
|
|
193
|
+
fail("foreign_flow_source");
|
|
194
|
+
const expected = { schema_id: isDiagnosticFlow(flowId) ? "dd-flow/diagnostic-engine-flow@1" : flowId === "mb-upgrade" ? "dd-flow/memory-upgrade-engine-flow@1"
|
|
195
|
+
: startupInputSchema === undefined ? "dd-flow/memory-init-engine-flow@1" : "dd-flow/memory-init-engine-flow@2",
|
|
150
196
|
...(startupInputSchema === undefined ? {} : { startup_input_schema: startupInputSchema }),
|
|
151
|
-
flow_id:
|
|
152
|
-
descriptor_sha256: source.descriptorReference().sha256, stages:
|
|
197
|
+
flow_id: flowId, source_sha256: source.sha256,
|
|
198
|
+
descriptor_sha256: source.descriptorReference().sha256, stages: flow.descriptor.stages.map(stage => stage.id) };
|
|
153
199
|
revalidate();
|
|
154
200
|
const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !["NODE_OPTIONS", "NODE_PATH"].includes(key) && !key.startsWith("DD_FLOW_")));
|
|
155
|
-
const
|
|
201
|
+
const started = performance.now();
|
|
202
|
+
const result = spawnSync(this.nodeExecutable, [this.entrypoint, "runtime", "flow", "admit", "--operation", isDiagnosticFlow(flowId) ? flowId : flowId === "mb-init" ? "memory-init" : "memory-upgrade", "--json"], { input, cwd: this.#manifest.snapshot_root, env: { ...env, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: this.#manifest.snapshot_root },
|
|
156
203
|
// This is cold startup of a selected CLI, not an already-loaded handler.
|
|
157
204
|
// Admission is bounded local validation; no provider operation is started.
|
|
158
205
|
timeout: SETUP_STARTUP_MS, maxBuffer: 1024 * 1024, shell: false, windowsHide: true });
|
|
159
206
|
if (result.error || result.signal || result.status !== 0)
|
|
160
|
-
fail("selected_engine_probe_failed",
|
|
207
|
+
fail("selected_engine_probe_failed", {
|
|
208
|
+
...probeFailure(result), operation: flowId, elapsed_ms: Math.round(performance.now() - started), probe: probeFailure(result)
|
|
209
|
+
});
|
|
161
210
|
revalidate();
|
|
162
211
|
let response;
|
|
163
212
|
try {
|
|
@@ -178,6 +227,91 @@ export class AdmittedFrozenEngine {
|
|
|
178
227
|
* This qualifies storage connections only, not provider or flow readiness.
|
|
179
228
|
*/
|
|
180
229
|
admitMemoryInitStorage(home, resourceHome) {
|
|
230
|
+
return this.admitMemoryStorage(home, resourceHome, "memory-init");
|
|
231
|
+
}
|
|
232
|
+
/** A distinct operation: old engines cannot accidentally claim support for
|
|
233
|
+
* upgrade request custody merely because init storage admission succeeds. */
|
|
234
|
+
admitMemoryUpgradeStorage(home, resourceHome) {
|
|
235
|
+
return this.admitMemoryStorage(home, resourceHome, "memory-upgrade");
|
|
236
|
+
}
|
|
237
|
+
/** Execute storage transition in this exact snapshot before AppContext opens
|
|
238
|
+
* a writer. A timeout or malformed acknowledgement never proves no effect. */
|
|
239
|
+
qualifyMemoryUpgradeStorage(home, resourceHome) {
|
|
240
|
+
const intact = () => hash(readBoundedReleaseFile(path.join(this.#manifest.snapshot_root, "engine.json"), 1024 * 1024)) === this.manifestSha256
|
|
241
|
+
&& engineSnapshotChecksum(this.#manifest.snapshot_root) === this.#manifest.integrity.checksum;
|
|
242
|
+
if (!intact())
|
|
243
|
+
refuse("engine_changed_before_storage_qualification");
|
|
244
|
+
const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !["NODE_OPTIONS", "NODE_PATH"].includes(key)));
|
|
245
|
+
const reply = spawnSync(this.nodeExecutable, [this.entrypoint, "runtime", "storage", "qualify", "--operation", "memory-upgrade", "--json"], { env: { ...env, DD_FLOW_HOME: home, DD_FLOW_RESOURCE_HOME: resourceHome, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: this.#manifest.snapshot_root },
|
|
246
|
+
encoding: "utf8", timeout: 120000, maxBuffer: 1024 * 1024, shell: false, windowsHide: true });
|
|
247
|
+
if (!intact())
|
|
248
|
+
refuse("engine_changed_during_storage_qualification");
|
|
249
|
+
if (reply.error || reply.signal || reply.status !== 0)
|
|
250
|
+
storageProbeRefuse("selected_engine_qualification_failed", { status: reply.status, signal: reply.signal });
|
|
251
|
+
let response;
|
|
252
|
+
try {
|
|
253
|
+
response = JSON.parse(reply.stdout);
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
return storageProbeRefuse("invalid_qualification_json");
|
|
257
|
+
}
|
|
258
|
+
const facts = response?.facts;
|
|
259
|
+
if (response?.ok !== true || response.action !== "runtime_storage_qualify" || facts?.schema_id !== "dd-flow/memory-upgrade-storage-capability@1"
|
|
260
|
+
|| facts.operation !== "memory-upgrade" || !["verified-no-op", "migration"].includes(facts.route) || facts.productive_admission !== false
|
|
261
|
+
|| hybridDigest(facts.source_stores) !== hybridDigest([path.join(home, "db.sqlite"), path.join(resourceHome, "runtime.sqlite")])
|
|
262
|
+
|| response.sha256 !== hybridDigest(facts))
|
|
263
|
+
storageProbeRefuse("invalid_qualification_facts");
|
|
264
|
+
return { facts, sha256: response.sha256, engine_manifest_sha256: this.manifestSha256, engine_sha256: this.#manifest.integrity.checksum };
|
|
265
|
+
}
|
|
266
|
+
transitionMemoryUpgradeStorage(input) {
|
|
267
|
+
const intact = () => hash(readBoundedReleaseFile(path.join(this.#manifest.snapshot_root, "engine.json"), 1024 * 1024)) === this.manifestSha256
|
|
268
|
+
&& engineSnapshotChecksum(this.#manifest.snapshot_root) === this.#manifest.integrity.checksum;
|
|
269
|
+
if (!intact())
|
|
270
|
+
refuse("engine_changed_before_storage_transition");
|
|
271
|
+
const args = ["runtime", "storage", "transition", "--action", input.action, "--project-root", input.scope.projectRoot,
|
|
272
|
+
"--workspace-root", input.scope.workspaceRoot, "--request-id", input.scope.requestId, "--selection-sha256", input.selectionSha256,
|
|
273
|
+
...(input.expectedSha256 ? ["--expected-sha256", input.expectedSha256] : []), ...(input.recoveryId ? ["--recovery-id", input.recoveryId] : []),
|
|
274
|
+
...(input.offline ? ["--offline"] : []), "--json"];
|
|
275
|
+
const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !["NODE_OPTIONS", "NODE_PATH"].includes(key)));
|
|
276
|
+
const reply = spawnSync(this.nodeExecutable, [this.entrypoint, ...args], { env: { ...env, DD_FLOW_HOME: input.home, DD_FLOW_RESOURCE_HOME: input.resourceHome,
|
|
277
|
+
DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: this.#manifest.snapshot_root }, encoding: "utf8", timeout: 120000, maxBuffer: 1024 * 1024, shell: false, windowsHide: true });
|
|
278
|
+
const fail = (reason, details = {}) => {
|
|
279
|
+
throw new AppError("frozen_engine_storage_transition_failed", "Observe or reconcile the original selected-engine storage request", 1, { reason, effect: "unknown", ...details });
|
|
280
|
+
};
|
|
281
|
+
let unchanged = false;
|
|
282
|
+
try {
|
|
283
|
+
unchanged = intact();
|
|
284
|
+
}
|
|
285
|
+
catch { /* Committed effects remain unknown if code custody is lost. */ }
|
|
286
|
+
if (!unchanged)
|
|
287
|
+
fail("engine_changed_during_storage_transition");
|
|
288
|
+
if (reply.error || reply.signal || reply.status !== 0) {
|
|
289
|
+
let child = {};
|
|
290
|
+
try {
|
|
291
|
+
child = JSON.parse(reply.stderr);
|
|
292
|
+
}
|
|
293
|
+
catch { /* Unknown acknowledgements retain unknown effect. */ }
|
|
294
|
+
fail("selected_engine_transition_failed", { status: reply.status, signal: reply.signal, error_code: reply.error?.code ?? null,
|
|
295
|
+
child_code: child.error?.code ?? null, child_reason: child.error?.details?.reason ?? null, effect: child.error?.details?.effect === "no_effect" ? "no_effect" : "unknown" });
|
|
296
|
+
}
|
|
297
|
+
let response;
|
|
298
|
+
try {
|
|
299
|
+
response = JSON.parse(reply.stdout);
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
return fail("invalid_transition_json");
|
|
303
|
+
}
|
|
304
|
+
if (!response || typeof response !== "object" || Array.isArray(response))
|
|
305
|
+
fail("invalid_transition_response");
|
|
306
|
+
const selected = response.engine;
|
|
307
|
+
if (response.ok !== true || response.action !== "runtime_storage_transition" || response.operation !== input.action
|
|
308
|
+
|| !response.scope || typeof response.scope !== "object" || Array.isArray(response.scope)
|
|
309
|
+
|| hybridDigest(response.scope) !== hybridDigest(input.scope) || response.selection_sha256 !== input.selectionSha256
|
|
310
|
+
|| selected?.manifest_sha256 !== this.manifestSha256 || selected.sha256 !== this.#manifest.integrity.checksum)
|
|
311
|
+
fail("foreign_transition_acknowledgement");
|
|
312
|
+
return response;
|
|
313
|
+
}
|
|
314
|
+
admitMemoryStorage(home, resourceHome, operation) {
|
|
181
315
|
for (const root of [home, resourceHome]) {
|
|
182
316
|
if (!path.isAbsolute(root) || path.resolve(root) !== root || fs.realpathSync(root) !== root)
|
|
183
317
|
storageProbeRefuse("noncanonical_storage_home");
|
|
@@ -189,10 +323,13 @@ export class AdmittedFrozenEngine {
|
|
|
189
323
|
};
|
|
190
324
|
revalidateEngine();
|
|
191
325
|
const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !["NODE_OPTIONS", "NODE_PATH"].includes(key)));
|
|
192
|
-
const
|
|
326
|
+
const started = performance.now();
|
|
327
|
+
const result = spawnSync(this.nodeExecutable, [this.entrypoint, "runtime", "storage", "admit", "--operation", operation, "--json"], { env: { ...env, DD_FLOW_HOME: home, DD_FLOW_RESOURCE_HOME: resourceHome, DD_FLOW_ENGINE_MODE: "1", DD_FLOW_ENGINE_HOME: this.#manifest.snapshot_root },
|
|
193
328
|
timeout: SETUP_STARTUP_MS, maxBuffer: 1024 * 1024, shell: false, windowsHide: true });
|
|
194
329
|
if (result.error || result.signal || result.status !== 0)
|
|
195
|
-
storageProbeRefuse("selected_engine_probe_failed",
|
|
330
|
+
storageProbeRefuse("selected_engine_probe_failed", {
|
|
331
|
+
...probeFailure(result), operation, elapsed_ms: Math.round(performance.now() - started)
|
|
332
|
+
});
|
|
196
333
|
revalidateEngine();
|
|
197
334
|
let response;
|
|
198
335
|
try {
|
|
@@ -206,7 +343,7 @@ export class AdmittedFrozenEngine {
|
|
|
206
343
|
storageProbeRefuse("invalid_probe_response");
|
|
207
344
|
const facts = response.facts, run = facts?.run;
|
|
208
345
|
if (!facts || Object.keys(facts).sort().join() !== "operation,outcome,resource,route,run,schema_id"
|
|
209
|
-
|| facts.schema_id !==
|
|
346
|
+
|| facts.schema_id !== `dd-flow/${operation}-engine-storage@1` || facts.operation !== operation
|
|
210
347
|
|| facts.route !== "verified-no-op" || facts.outcome !== "no-op" || response.sha256 !== hybridDigest(facts)
|
|
211
348
|
|| !isMemoryInitResourceStorageFacts(facts.resource) || facts.resource.store_path !== path.join(resourceHome, "runtime.sqlite")
|
|
212
349
|
|| !run || Object.keys(run).sort().join() !== "executing_writer_version,operation,outcome,route,schema_id,schema_sha256,store_dev,store_ino,store_kind,store_path"
|
|
@@ -0,0 +1,188 @@
|
|
|
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 { hybridDigest } from "./hybrid-digest.js";
|
|
6
|
+
import { assertRunMutationAllowed } from "./run-recovery-guard.js";
|
|
7
|
+
function repairRefuse(reason) { 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" }); }
|
|
8
|
+
function reviewRefuse(reason) { throw new AppError("memory_native_review_copy_invalid", "Native Memory review requires an independently owned immutable execution copy", 1, { reason, effect: "no_effect" }); }
|
|
9
|
+
export function memoryReviewPhysicalDestination(runRoot, relative) {
|
|
10
|
+
if (fs.realpathSync(runRoot) !== path.resolve(runRoot) || !fs.lstatSync(runRoot).isDirectory())
|
|
11
|
+
reviewRefuse("noncanonical_run_root");
|
|
12
|
+
let current = runRoot;
|
|
13
|
+
for (const part of relative.split("/")) {
|
|
14
|
+
if (!part || part === "." || part === "..")
|
|
15
|
+
reviewRefuse("invalid_copy_path");
|
|
16
|
+
current = path.join(current, part);
|
|
17
|
+
const stat = fs.lstatSync(current, { throwIfNoEntry: false });
|
|
18
|
+
if (stat && !stat.isDirectory())
|
|
19
|
+
reviewRefuse("linked_or_non_directory_copy_path");
|
|
20
|
+
if (!stat)
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Hook ingress selects only an independently prepared repair copy. This
|
|
25
|
+
* establishes a command's assigned directory, not claim/repair authority. */
|
|
26
|
+
export function memoryRepairAssignedHookCwd(context, input) {
|
|
27
|
+
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]);
|
|
28
|
+
let payload;
|
|
29
|
+
try {
|
|
30
|
+
payload = JSON.parse(work?.payload_json ?? "null");
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
if (payload?.kind !== "memory-repair" && work?.result_schema !== "dd-flow/memory-repair-work-result@1")
|
|
36
|
+
return null;
|
|
37
|
+
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]);
|
|
38
|
+
if (!run || (run.flow_kind !== "mb-init" && run.flow_kind !== "mb-upgrade") || run.project_root !== input.projectRoot || payload.read_only !== false
|
|
39
|
+
|| ![1, 2].includes(payload.memory_repair?.wave ?? 0) || work?.result_schema !== "dd-flow/memory-repair-work-result@1")
|
|
40
|
+
repairRefuse("registered_memory_repair_required");
|
|
41
|
+
const generation = assertRunMutationAllowed(context, input.projectId, input.runId), runRoot = run.run_root ?? run.run_dir;
|
|
42
|
+
const external = context.db.get("SELECT 1 FROM sqlite_schema WHERE type='table' AND name='work_launches'")
|
|
43
|
+
? context.db.get("SELECT operation_id,status,profile_json,profile_hash,provider_session_id FROM work_launches WHERE work_id=?", [input.workId]) : undefined;
|
|
44
|
+
const native = !external;
|
|
45
|
+
const admitted = (root) => {
|
|
46
|
+
if (native && input.observedCwd !== root && input.literalExecutionCwd !== root)
|
|
47
|
+
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,
|
|
48
|
+
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." });
|
|
49
|
+
return { root, native };
|
|
50
|
+
};
|
|
51
|
+
if (work.status === "created") {
|
|
52
|
+
// Routing consumes registered copy custody only. Actual Work start owns
|
|
53
|
+
// physical source/copy/profile admission after the durable native Hook.
|
|
54
|
+
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]);
|
|
55
|
+
const intents = rows("memory.repair_writer_copy_intent"), copies = rows("memory.repair_writer_copy_ready");
|
|
56
|
+
if (intents.length !== 1 || copies.length !== 1 || Buffer.byteLength(intents[0].payload_json) > 1024 * 1024
|
|
57
|
+
|| Buffer.byteLength(copies[0].payload_json) > 12 * 1024 * 1024)
|
|
58
|
+
repairRefuse("owned_ready_copy_required");
|
|
59
|
+
try {
|
|
60
|
+
const intent = JSON.parse(intents[0].payload_json), ready = JSON.parse(copies[0].payload_json);
|
|
61
|
+
const workspace_manifest_sha256 = ready.workspace_manifest_sha256, readyIntent = { ...ready };
|
|
62
|
+
delete readyIntent.workspace_manifest_sha256;
|
|
63
|
+
delete readyIntent.workspace_manifest_base64;
|
|
64
|
+
const relative = `works/${input.workId}/writer-workspaces/generation-${generation}`;
|
|
65
|
+
if (intent.schema_id !== "dd-flow/memory-repair-writer-copy-intent@1" || intent.project_id !== input.projectId || intent.run_id !== input.runId
|
|
66
|
+
|| intent.work_id !== input.workId || intent.generation !== generation || intent.run_root !== runRoot || !path.isAbsolute(runRoot)
|
|
67
|
+
|| intent.copy_relative !== relative || !/^[a-f0-9]{64}$/.test(intent.baseline_registration_sha256)
|
|
68
|
+
|| !/^[a-f0-9]{64}$/.test(workspace_manifest_sha256) || hybridDigest(readyIntent) !== hybridDigest(intent))
|
|
69
|
+
repairRefuse("owned_ready_copy_required");
|
|
70
|
+
if (external) {
|
|
71
|
+
if (external.provider_session_id !== input.providerSessionId)
|
|
72
|
+
repairRefuse("foreign_physical_session");
|
|
73
|
+
const profile = JSON.parse(external.profile_json), metadata = profile.memory_repair_copy;
|
|
74
|
+
if (profile.delegation?.mode !== "external" || hybridDigest(metadata) !== hybridDigest({ root: relative, copy_intent_sha256: hybridDigest(intent),
|
|
75
|
+
workspace_manifest_sha256, baseline_registration_sha256: intent.baseline_registration_sha256 }))
|
|
76
|
+
repairRefuse("native_copy_cwd_mismatch");
|
|
77
|
+
}
|
|
78
|
+
if (fs.realpathSync(runRoot) !== path.resolve(runRoot))
|
|
79
|
+
repairRefuse("owned_ready_copy_required");
|
|
80
|
+
let destination = runRoot;
|
|
81
|
+
for (const part of relative.split('/')) {
|
|
82
|
+
destination = path.join(destination, part);
|
|
83
|
+
if (!fs.lstatSync(destination).isDirectory())
|
|
84
|
+
repairRefuse("owned_ready_copy_required");
|
|
85
|
+
}
|
|
86
|
+
return admitted(destination);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (error instanceof AppError)
|
|
90
|
+
throw error;
|
|
91
|
+
return repairRefuse("owned_ready_copy_required");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Running writers retain their committed directory, not today's source bytes.
|
|
95
|
+
let binding = external;
|
|
96
|
+
let nativeStart;
|
|
97
|
+
if (!binding) {
|
|
98
|
+
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]);
|
|
99
|
+
if (rows.length !== 1 || Buffer.byteLength(rows[0].payload_json) > 1024 * 1024)
|
|
100
|
+
repairRefuse("native_registered_start_changed");
|
|
101
|
+
try {
|
|
102
|
+
const retained = JSON.parse(rows[0].payload_json);
|
|
103
|
+
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
|
|
104
|
+
|| typeof retained.work_session_id !== "string" || typeof retained.hook_event_key !== "string")
|
|
105
|
+
repairRefuse("native_registered_start_changed");
|
|
106
|
+
binding = retained.binding;
|
|
107
|
+
nativeStart = retained;
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (error instanceof AppError)
|
|
111
|
+
throw error;
|
|
112
|
+
return repairRefuse("native_registered_start_changed");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
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]);
|
|
116
|
+
if (work.status !== "running" || !binding || binding.provider_session_id !== input.providerSessionId || starts.length !== 1)
|
|
117
|
+
repairRefuse("native_registered_start_changed");
|
|
118
|
+
if (nativeStart && (nativeStart.work_session_id !== starts[0].id || nativeStart.hook_event_key !== starts[0].hook_event_id
|
|
119
|
+
|| binding.operation_id !== `native-hook:${nativeStart.hook_event_key}`
|
|
120
|
+
|| !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])))
|
|
121
|
+
repairRefuse("native_registered_start_changed");
|
|
122
|
+
try {
|
|
123
|
+
const text = starts[0].start_receipt_json;
|
|
124
|
+
if (typeof text !== "string" || Buffer.byteLength(text) > 16 * 1024 * 1024)
|
|
125
|
+
repairRefuse("native_registered_start_changed");
|
|
126
|
+
const start = JSON.parse(text), packet = JSON.parse(start.context), profile = JSON.parse(binding.profile_json), metadata = profile.memory_repair_copy;
|
|
127
|
+
if (profile.delegation?.mode !== (native ? "native" : "external") || start.version !== 2 || createHash("sha256").update(start.context).digest("hex") !== start.context_sha256
|
|
128
|
+
|| typeof metadata?.root !== "string" || metadata.root !== `works/${input.workId}/writer-workspaces/generation-${start.generation}`
|
|
129
|
+
|| packet.work_id !== input.workId || packet.run_id !== input.runId || packet.workspace_root !== path.join(runRoot, metadata.root)
|
|
130
|
+
|| packet.memory_repair_input?.provider_session_id !== input.providerSessionId
|
|
131
|
+
|| packet.memory_repair_input?.launch_operation_id !== binding.operation_id)
|
|
132
|
+
repairRefuse("native_registered_start_changed");
|
|
133
|
+
return admitted(packet.workspace_root);
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
if (error instanceof AppError)
|
|
137
|
+
throw error;
|
|
138
|
+
return repairRefuse("native_registered_start_changed");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/** Routing is not fresh source admission: an already claimed reviewer retains
|
|
142
|
+
* its committed private directory even after a later correction changes MB. */
|
|
143
|
+
export function memoryNativeReviewAssignedHookCwd(context, input) {
|
|
144
|
+
const work = context.db.get("SELECT status,payload_json FROM works WHERE project_id=? AND run_id=? AND work_id=?", [input.projectId, input.runId, input.workId]);
|
|
145
|
+
const payload = JSON.parse(work?.payload_json ?? "null");
|
|
146
|
+
if (payload?.kind !== "memory-review" || payload.read_only !== true)
|
|
147
|
+
return null;
|
|
148
|
+
const ready = context.db.get("SELECT 1 FROM audit_events WHERE project_id=? AND event_type='memory.native_review_copy_ready' AND json_extract(payload_json,'$.intent.run_id')=? AND json_extract(payload_json,'$.intent.work_id')=?", [input.projectId, input.runId, input.workId]);
|
|
149
|
+
if (!ready)
|
|
150
|
+
return null;
|
|
151
|
+
if (work?.status === "created") {
|
|
152
|
+
// Hook ingress routes an already allocated copy. Source admission belongs
|
|
153
|
+
// to the actual work-start owner, not a second physical graph in the Hook.
|
|
154
|
+
const generation = assertRunMutationAllowed(context, input.projectId, input.runId);
|
|
155
|
+
const run = context.db.get("SELECT run_root,run_dir FROM runs WHERE project_id=? AND id=?", [input.projectId, input.runId]);
|
|
156
|
+
if (!run)
|
|
157
|
+
reviewRefuse("owned_run_required");
|
|
158
|
+
const rows = (event) => context.db.all("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type=? AND COALESCE(json_extract(payload_json,'$.run_id'),json_extract(payload_json,'$.intent.run_id'))=? AND COALESCE(json_extract(payload_json,'$.work_id'),json_extract(payload_json,'$.intent.work_id'))=? AND COALESCE(json_extract(payload_json,'$.generation'),json_extract(payload_json,'$.intent.generation'))=? LIMIT 2", [input.projectId, event, input.runId, input.workId, generation]);
|
|
159
|
+
const intents = rows("memory.native_review_copy_intent"), copies = rows("memory.native_review_copy_ready");
|
|
160
|
+
if (intents.length !== 1 || copies.length !== 1)
|
|
161
|
+
reviewRefuse("registered_copy_required");
|
|
162
|
+
const intent = JSON.parse(intents[0].payload_json), copy = JSON.parse(copies[0].payload_json);
|
|
163
|
+
const relative = `works/${input.workId}/native-review-workspaces/generation-${generation}`, runRoot = run.run_root ?? run.run_dir;
|
|
164
|
+
if (intent.schema_id !== "dd-flow/memory-native-review-copy@1" || intent.project_id !== input.projectId || intent.run_id !== input.runId
|
|
165
|
+
|| intent.work_id !== input.workId || intent.generation !== generation || intent.root !== relative
|
|
166
|
+
|| !/^[a-f0-9]{64}$/.test(intent.input_sha256) || !/^[a-f0-9]{64}$/.test(intent.source_sha256)
|
|
167
|
+
|| hybridDigest(copy.intent) !== hybridDigest(intent) || !/^[a-f0-9]{64}$/.test(copy.sha256))
|
|
168
|
+
reviewRefuse("registered_copy_changed");
|
|
169
|
+
memoryReviewPhysicalDestination(runRoot, relative);
|
|
170
|
+
const root = path.join(runRoot, relative);
|
|
171
|
+
if (!fs.lstatSync(root, { throwIfNoEntry: false })?.isDirectory())
|
|
172
|
+
reviewRefuse("registered_copy_required");
|
|
173
|
+
return root;
|
|
174
|
+
}
|
|
175
|
+
const rows = context.db.all("SELECT ws.start_receipt_json,r.run_root,r.run_dir,s.cwd FROM work_sessions ws JOIN works w ON w.work_id=ws.work_id JOIN runs r ON r.project_id=w.project_id AND r.id=w.run_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.provider_session_id=? AND s.worker_id=w.work_id AND EXISTS(SELECT 1 FROM audit_events a WHERE a.project_id=w.project_id AND a.event_type='memory.native_review_binding' AND json_extract(a.payload_json,'$.work_session_id')=ws.id AND json_extract(a.payload_json,'$.run_id')=w.run_id AND json_extract(a.payload_json,'$.work_id')=w.work_id) LIMIT 2", [input.projectId, input.runId, input.workId, input.providerSessionId]);
|
|
176
|
+
if (rows.length !== 1)
|
|
177
|
+
reviewRefuse("registered_session_required");
|
|
178
|
+
const row = rows[0];
|
|
179
|
+
if (typeof row.start_receipt_json !== "string" || Buffer.byteLength(row.start_receipt_json) > 16 * 1024 * 1024)
|
|
180
|
+
reviewRefuse("registered_copy_required");
|
|
181
|
+
const receipt = JSON.parse(row.start_receipt_json), packet = JSON.parse(receipt.context);
|
|
182
|
+
const relative = `works/${input.workId}/native-review-workspaces/generation-${receipt.generation}`, root = path.join(row.run_root ?? row.run_dir, relative);
|
|
183
|
+
if (receipt.version !== 2 || createHash("sha256").update(receipt.context).digest("hex") !== receipt.context_sha256
|
|
184
|
+
|| packet.work_id !== input.workId || packet.run_id !== input.runId || packet.workspace_root !== root
|
|
185
|
+
|| packet.review_copy?.root !== relative || row.cwd !== root)
|
|
186
|
+
reviewRefuse("registered_copy_changed");
|
|
187
|
+
return root;
|
|
188
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { AppError } from "../shared/errors.js";
|
|
2
|
+
import { assertRunMutationAllowed } from "./run-recovery-guard.js";
|
|
3
|
+
/** A recovered RUN never grants the retired process a new execution segment. */
|
|
4
|
+
export function assertRecoveryHookCurrent(context, projectId, runId, identity) {
|
|
5
|
+
const generation = assertRunMutationAllowed(context, projectId, runId);
|
|
6
|
+
if (!generation)
|
|
7
|
+
return;
|
|
8
|
+
const current = identity.daemonId && context.db.get("SELECT b.recovery_id FROM run_recovery_bindings b JOIN run_recovery_guards g ON g.recovery_id = b.recovery_id WHERE g.project_id = ? AND g.run_id = ? AND g.generation = ? AND g.status = 'recovered' AND b.status = 'accepted' AND b.daemon_id = ? LIMIT 1", [projectId, runId, generation, identity.daemonId]);
|
|
9
|
+
if (current)
|
|
10
|
+
return;
|
|
11
|
+
const retired = identity.daemonId && (context.db.get(`SELECT ws.id 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 he.daemon_id = ? LIMIT 1`, [projectId, runId, identity.daemonId]) || context.db.get("SELECT b.recovery_id FROM run_recovery_bindings b JOIN run_recovery_guards g ON g.recovery_id = b.recovery_id WHERE g.project_id = ? AND g.run_id = ? AND b.daemon_id = ? AND (g.generation <> ? OR b.status = 'abandoned') LIMIT 1", [projectId, runId, identity.daemonId, generation]));
|
|
12
|
+
if (!identity.daemonId || retired)
|
|
13
|
+
throw new AppError("recovery_daemon_stale", "Lifecycle receipt belongs to an unidentified or retired recovery daemon", 1, { run_id: runId, generation, daemon_id: identity.daemonId });
|
|
14
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function recordFlowSessionObservation(context, input) {
|
|
2
|
+
const duplicate = context.db.get("SELECT id FROM flow_session_segments WHERE project_id = ? AND event_key = ?", [input.projectId, input.eventKey]);
|
|
3
|
+
if (duplicate)
|
|
4
|
+
return { changed: false, segment_id: duplicate.id };
|
|
5
|
+
const open = context.db.get(`SELECT id, run_id, protocol_id FROM flow_session_segments
|
|
6
|
+
WHERE project_id = ? AND session_id = ? AND ended_at IS NULL ORDER BY started_at DESC, id DESC LIMIT 1`, [input.projectId, input.sessionId]);
|
|
7
|
+
const now = context.now();
|
|
8
|
+
const runId = input.runId === undefined ? open?.run_id ?? null : input.runId;
|
|
9
|
+
const protocolId = input.protocolId === undefined ? open?.protocol_id ?? null : input.protocolId;
|
|
10
|
+
if (open && open.run_id === runId && open.protocol_id === protocolId) {
|
|
11
|
+
return { changed: false, segment_id: open.id };
|
|
12
|
+
}
|
|
13
|
+
if (open)
|
|
14
|
+
context.db.run("UPDATE flow_session_segments SET ended_at = ? WHERE id = ? AND ended_at IS NULL", [now, open.id]);
|
|
15
|
+
const result = context.db.run(`INSERT INTO flow_session_segments
|
|
16
|
+
(project_id, session_id, run_id, protocol_id, started_at, ended_at, cwd, tool_name, event_key)
|
|
17
|
+
VALUES (?, ?, ?, ?, ?, NULL, ?, ?, ?)`, [input.projectId, input.sessionId, runId, protocolId, now, input.cwd ?? null, input.toolName ?? null, input.eventKey]);
|
|
18
|
+
return { changed: true, segment_id: Number(result.lastInsertRowid) };
|
|
19
|
+
}
|
|
20
|
+
export function activeFlowSessionsForProject(context, projectId) {
|
|
21
|
+
return context.db.all(`SELECT * FROM sessions
|
|
22
|
+
WHERE project_id = ? AND status IN ('pending', 'active', 'waiting_user', 'blocked', 'stopping')
|
|
23
|
+
ORDER BY updated_at DESC`, [projectId]);
|
|
24
|
+
}
|
package/dist/services/hooks.js
CHANGED
|
@@ -9,15 +9,15 @@ import { parseJsonObject } from "../shared/json.js";
|
|
|
9
9
|
import { canonicalPath, ensureDir, resolveProjectRoot } from "../storage/paths.js";
|
|
10
10
|
import { resolveWorkReference } from "../storage/work-references.js";
|
|
11
11
|
import { appendAudit } from "./audit.js";
|
|
12
|
-
import { memoryRepairAssignedHookCwd } from "./memory-
|
|
13
|
-
import { memoryNativeReviewAssignedHookCwd } from "./memory-native-review-copy.js";
|
|
12
|
+
import { memoryRepairAssignedHookCwd, memoryNativeReviewAssignedHookCwd } from "./hook-memory-routing.js";
|
|
14
13
|
import { prepareTextFile, prepareJsonText, prepareError } from "../cli/input-preparation.js";
|
|
14
|
+
import { nativeHookNonParticipation } from "../cli/native-hook-preparation.js";
|
|
15
15
|
import { nativeSessionIdentity, storageSessionId } from "./session-identity.js";
|
|
16
|
-
import { assertRecoveryHookCurrent } from "./
|
|
16
|
+
import { assertRecoveryHookCurrent } from "./hook-recovery-guard.js";
|
|
17
17
|
import { observeLifecycleInvocation, observedLifecycleInvocation, managedNativeInvocationScope } from "./lifecycle-invocations.js";
|
|
18
18
|
import { commandHasOption, commandOption, commandPosition, shellSuffixOffset, parseLifecycleCommand, lifecycleCommandFromPayload, continuationCommand } from "./lifecycle-command.js";
|
|
19
|
-
import { registerProject, requireProjectByRoot } from "./
|
|
20
|
-
import { activeFlowSessionsForProject, recordFlowSessionObservation } from "./
|
|
19
|
+
import { registerProject, requireProjectByRoot } from "./project-registry.js";
|
|
20
|
+
import { activeFlowSessionsForProject, recordFlowSessionObservation } from "./hook-session-observation.js";
|
|
21
21
|
const defaultProfileName = "default";
|
|
22
22
|
const maxSanitizedSummaryLength = 1600;
|
|
23
23
|
function lifecycleObservationResult(receipt) {
|
|
@@ -496,6 +496,9 @@ function codexNativeParentId(payload, childId) {
|
|
|
496
496
|
/** Qualified ACP admission binds public commands to an exact issued assignment.
|
|
497
497
|
* Its provenance is an observation, never a synchronous PreToolUse assertion. */
|
|
498
498
|
export function handleZcodeEvent(context, input) {
|
|
499
|
+
const unrelated = nativeHookNonParticipation(["zcode", "event"], input.stdin);
|
|
500
|
+
if (unrelated)
|
|
501
|
+
return unrelated;
|
|
499
502
|
const notification = parseJsonObject(input.stdin || "{}", "zcode ACP notification");
|
|
500
503
|
const params = objectRecord(notification.params), update = objectRecord(params.update);
|
|
501
504
|
if (notification.method !== "session/update" || update.sessionUpdate !== "tool_call")
|
|
@@ -523,6 +526,9 @@ export function handleZcodeEvent(context, input) {
|
|
|
523
526
|
* admission; the native hook must not create a competing receipt for them.
|
|
524
527
|
*/
|
|
525
528
|
export function handleZcodeHook(context, input) {
|
|
529
|
+
const unrelated = nativeHookNonParticipation(["zcode", "hook"], input.stdin);
|
|
530
|
+
if (unrelated)
|
|
531
|
+
return unrelated;
|
|
526
532
|
return recordZcodeLifecycle(context, input, undefined, true);
|
|
527
533
|
}
|
|
528
534
|
function recordZcodeLifecycle(context, input, invocationId, nativePreTool = false) {
|
|
@@ -631,6 +637,9 @@ function recordZcodeLifecycle(context, input, invocationId, nativePreTool = fals
|
|
|
631
637
|
}
|
|
632
638
|
/** Convert a Grok Build PreToolUse hook into a trusted lifecycle receipt. */
|
|
633
639
|
export function handleGrokEvent(context, input) {
|
|
640
|
+
const unrelated = nativeHookNonParticipation(["grok", "event"], input.stdin);
|
|
641
|
+
if (unrelated)
|
|
642
|
+
return unrelated;
|
|
634
643
|
const hook = parseJsonObject(input.stdin || "{}", "Grok Build hook event");
|
|
635
644
|
const eventName = stringValue(hook.hook_event_name) ?? stringValue(hook.hookEventName);
|
|
636
645
|
if (eventName && eventName !== "PreToolUse" && eventName !== "pre_tool_use") {
|
|
@@ -703,6 +712,9 @@ export function handleGrokEvent(context, input) {
|
|
|
703
712
|
}
|
|
704
713
|
/** Convert a controlled OpenCode tool hook into the shared lifecycle receipt. */
|
|
705
714
|
export function handleOpenCodeEvent(context, input) {
|
|
715
|
+
const unrelated = nativeHookNonParticipation(["opencode", "event"], input.stdin);
|
|
716
|
+
if (unrelated)
|
|
717
|
+
return unrelated;
|
|
706
718
|
const event = parseJsonObject(input.stdin || "{}", "OpenCode tool event");
|
|
707
719
|
if (event.schema_id !== "dd-flow/opencode-tool-event@1")
|
|
708
720
|
throw new AppError("unsupported_opencode_event_schema", "Unsupported OpenCode event schema", 1);
|
|
@@ -710,6 +722,9 @@ export function handleOpenCodeEvent(context, input) {
|
|
|
710
722
|
}
|
|
711
723
|
/** Accept adapter-observed Factory identity, never a model-supplied Session ID. */
|
|
712
724
|
export function handleDroidEvent(context, input) {
|
|
725
|
+
const unrelated = nativeHookNonParticipation(["droid", "event"], input.stdin);
|
|
726
|
+
if (unrelated)
|
|
727
|
+
return unrelated;
|
|
713
728
|
const event = parseJsonObject(input.stdin || "{}", "Droid tool event");
|
|
714
729
|
if (event.schema_id !== "dd-flow/droid-tool-event@1")
|
|
715
730
|
throw new AppError("unsupported_droid_event_schema", "Unsupported Droid event schema", 1);
|
|
@@ -935,6 +950,9 @@ function handleControlledToolEvent(context, input, event, harness) {
|
|
|
935
950
|
}
|
|
936
951
|
/** Convert an Antigravity CLI tool hook into the shared lifecycle receipt. */
|
|
937
952
|
export function handleAgyEvent(context, input) {
|
|
953
|
+
const unrelated = nativeHookNonParticipation(["agy", "event"], input.stdin);
|
|
954
|
+
if (unrelated)
|
|
955
|
+
return unrelated;
|
|
938
956
|
const event = parseJsonObject(input.stdin || "{}", "Antigravity tool event");
|
|
939
957
|
if (event.schema_id !== "dd-flow/agy-tool-event@1")
|
|
940
958
|
throw new AppError("unsupported_agy_event_schema", "Unsupported Antigravity event schema", 1);
|
|
@@ -1493,9 +1511,10 @@ function hookEntry(matcher, command, statusMessage) {
|
|
|
1493
1511
|
}
|
|
1494
1512
|
function commandHook(command, statusMessage) {
|
|
1495
1513
|
// Hook admission is synchronous (the CLI must see a committed receipt), but
|
|
1496
|
-
//
|
|
1497
|
-
//
|
|
1498
|
-
|
|
1514
|
+
// Admission keeps its own shared 20s deadline. Leave separate headroom for
|
|
1515
|
+
// the shell, cold CLI imports and bounded subprocess cleanup: the host must
|
|
1516
|
+
// not kill the hook before it can report its typed admission failure.
|
|
1517
|
+
return { type: "command", command, timeout: 60, statusMessage };
|
|
1499
1518
|
}
|
|
1500
1519
|
function hookCommand(event) {
|
|
1501
1520
|
const args = `codex hook handle --event ${event} --json`;
|