@deksden-com/dd-flow-cli 0.9.0-beta.137 → 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 +4 -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 +197 -0
- package/dist/cli/command-option-descriptions.js +191 -0
- package/dist/cli/completion-cli.js +70 -0
- package/dist/cli/completion.js +57 -0
- package/dist/cli/error-output.js +52 -0
- package/dist/cli/executor-onboarding.js +76 -0
- package/dist/cli/flows.js +29 -0
- package/dist/cli/harnesses.js +84 -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/models.js +39 -0
- package/dist/cli/native-hook-ingress.js +1 -1
- package/dist/cli/operator-profiles.js +61 -0
- package/dist/cli/output-options.js +35 -0
- package/dist/cli/response-file.js +71 -0
- package/dist/cli/run-cli.js +283 -160
- package/dist/cli/setup.js +149 -0
- package/dist/cli.js +54 -2
- package/dist/domain/stage-handler-identities.js +20 -0
- 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 +6 -4
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +9 -9
- 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 +2 -0
- 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/cli-upgrade-allowlist.js +27 -0
- package/dist/services/code-checks.js +387 -34
- package/dist/services/controller-execution.js +88 -0
- package/dist/services/controller-fanout.js +71 -12
- 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/discovery/agy-discovery.js +2 -0
- package/dist/services/discovery/codex-discovery.js +2 -0
- package/dist/services/discovery/droid-discovery.js +2 -0
- package/dist/services/discovery/grok-discovery.js +2 -0
- package/dist/services/discovery/opencode-discovery.js +2 -0
- package/dist/services/discovery/static.js +31 -0
- package/dist/services/discovery/zcode-discovery.js +2 -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/executor-onboarding.js +56 -0
- 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/harness-discovery.js +29 -0
- package/dist/services/harness-doctor.js +75 -0
- package/dist/services/harness-probe-effects.js +51 -0
- package/dist/services/harness-version-probe.js +100 -0
- package/dist/services/hooks.js +24 -6
- package/dist/services/hybrid-contracts.js +483 -0
- package/dist/services/ids.js +29 -1
- package/dist/services/installation-lock.js +95 -0
- 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/model-catalog.js +109 -0
- package/dist/services/model-discovery.js +46 -0
- package/dist/services/operator-profiles.js +236 -0
- package/dist/services/projects.js +17 -2
- package/dist/services/recovery-snapshot-database.js +416 -1
- package/dist/services/release-archive-producer.js +52 -0
- package/dist/services/release-archive.js +143 -0
- package/dist/services/release-cache.js +142 -0
- package/dist/services/release-compatibility.js +18 -0
- package/dist/services/release-download.js +67 -0
- package/dist/services/release-envelope.js +104 -0
- package/dist/services/release-manifest.js +45 -0
- package/dist/services/release-payload.js +114 -0
- package/dist/services/release-producer.js +63 -0
- package/dist/services/release-provenance.js +160 -0
- package/dist/services/release-registry.js +66 -0
- package/dist/services/release-source.js +194 -0
- 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 +101 -40
- 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/setup.js +377 -0
- package/dist/services/stage-context.js +9 -1
- package/dist/services/stage-handlers.js +74 -0
- package/dist/services/stage-report-renderer.js +63 -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 +70 -9
- package/dist/services/vnext-merge.js +8 -3
- package/dist/services/vnext-plan-review.js +6 -4
- 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,210 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
3
|
+
import { AppError } from "../shared/errors.js";
|
|
4
|
+
import { appendAudit } from "./audit.js";
|
|
5
|
+
import { AdmittedFrozenEngine } from "./frozen-engine.js";
|
|
6
|
+
import { hybridDigest, validateHybridContract } from "./hybrid-contracts.js";
|
|
7
|
+
import { PreparedMemoryInitProjectBootstrap, readMemoryInitProjectBootstrap, readMemoryInitProjectBootstrapEngineAdmission, readMemoryInitReleaseSelection } from "./memory-init-project-bootstrap.js";
|
|
8
|
+
const receiptEvent = "memory.bootstrap_ready", intentEvent = "memory.bootstrap_readiness_intent";
|
|
9
|
+
const bytes = (value) => Buffer.from(JSON.stringify(value));
|
|
10
|
+
const hash = (value) => createHash("sha256").update(value).digest("hex");
|
|
11
|
+
function refuse(reason) {
|
|
12
|
+
throw new AppError("memory_init_bootstrap_readiness_invalid", "Bootstrap readiness lacks exact independently accepted custody", 1, { reason, effect: "no_effect", next_action: "reconcile_exact_request" });
|
|
13
|
+
}
|
|
14
|
+
function decoded(base64, sha256) {
|
|
15
|
+
if (typeof base64 !== "string" || base64.length > 2 * 1024 * 1024 || !/^[a-f0-9]{64}$/.test(sha256))
|
|
16
|
+
refuse("invalid_manifest_binding");
|
|
17
|
+
const content = Buffer.from(base64, "base64");
|
|
18
|
+
if (!content.length || content.length > 1024 * 1024 || content.toString("base64") !== base64 || hash(content) !== sha256)
|
|
19
|
+
refuse("changed_manifest_bytes");
|
|
20
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(content));
|
|
21
|
+
}
|
|
22
|
+
/** Exact selected contract support, before invoking bootstrap publication. The
|
|
23
|
+
* public dispatcher must call this before its first project effect, not discover
|
|
24
|
+
* an unsupported engine after installing drafts. No provider operation occurs.
|
|
25
|
+
*/
|
|
26
|
+
export function prepareMemoryInitBootstrapSchema(engine) {
|
|
27
|
+
if (!(engine instanceof AdmittedFrozenEngine))
|
|
28
|
+
refuse("selected_engine_not_admitted");
|
|
29
|
+
const manifest = engine.manifest(), id = "dd-flow/memory-bootstrap-receipt@2";
|
|
30
|
+
const entries = manifest.schema_registry?.entries.filter(entry => entry.id === id) ?? [];
|
|
31
|
+
if (!Object.values(manifest.supports_contracts).flat().includes(id) || entries.length !== 1)
|
|
32
|
+
refuse("bootstrap_contract_not_supported");
|
|
33
|
+
return new Ajv2020({ strict: true, validateFormats: false }).compile(JSON.parse(engine.schema(entries[0].name).toString("utf8")));
|
|
34
|
+
}
|
|
35
|
+
/** Pure receipt construction from the prepared selection and accepted SQL
|
|
36
|
+
* dependencies. It is also used by writers/readers; no filesystem/schema IO.
|
|
37
|
+
*/
|
|
38
|
+
function material(context, binding) {
|
|
39
|
+
const scope = binding.scope, registered = readMemoryInitProjectBootstrap(context, scope);
|
|
40
|
+
const accepted = readMemoryInitProjectBootstrapEngineAdmission(context, scope);
|
|
41
|
+
const selection = readMemoryInitReleaseSelection(context, scope);
|
|
42
|
+
if (!registered || !accepted || registered.receipt.schema_id !== "dd-flow/memory-init-project-bootstrap@4"
|
|
43
|
+
|| accepted.bootstrap_sha256 !== registered.sha256
|
|
44
|
+
|| registered.receipt.target_sha256 !== hybridDigest(binding.target)
|
|
45
|
+
|| registered.receipt.execution_sha256 !== binding.execution_sha256
|
|
46
|
+
|| binding.target.request_id !== scope.requestId || binding.target.project_root !== scope.projectRoot
|
|
47
|
+
|| binding.target.workspace_root !== scope.workspaceRoot
|
|
48
|
+
|| binding.git_policy_sha256 !== accepted.git_policy_sha256
|
|
49
|
+
|| !/^[a-f0-9]{64}$/.test(binding.pack_sha256))
|
|
50
|
+
refuse("changed_bootstrap_dependencies");
|
|
51
|
+
if (!selection || selection.bootstrap_identity !== registered.receipt.identity
|
|
52
|
+
|| selection.execution_sha256 !== binding.execution_sha256 || selection.pack_sha256 !== binding.pack_sha256
|
|
53
|
+
|| selection.installation_manifest_sha256 !== binding.installation_manifest_sha256
|
|
54
|
+
|| selection.installation_manifest_base64 !== binding.installation_manifest_base64)
|
|
55
|
+
refuse("changed_release_selection");
|
|
56
|
+
if (!context.db.get("SELECT 1 FROM projects WHERE id=? AND root=? AND status='active'", [registered.receipt.project_id, scope.projectRoot]))
|
|
57
|
+
refuse("project_not_active");
|
|
58
|
+
const release = decoded(binding.installation_manifest_base64, binding.installation_manifest_sha256);
|
|
59
|
+
const engine = decoded(binding.engine_manifest_base64, accepted.proof.engine_manifest_sha256);
|
|
60
|
+
const engineSha256 = accepted.proof.engine_sha256;
|
|
61
|
+
if (typeof engineSha256 !== "string" || !/^[a-f0-9]{64}$/.test(engineSha256))
|
|
62
|
+
refuse("invalid_engine_identity");
|
|
63
|
+
if (release.engine.package !== engine.package_name || release.engine.version !== engine.package_version
|
|
64
|
+
|| engine.integrity.checksum !== accepted.proof.engine_sha256)
|
|
65
|
+
refuse("foreign_selected_engine");
|
|
66
|
+
const fact = { requestId: scope.requestId, projectRoot: scope.projectRoot, workspace: scope.workspaceRoot };
|
|
67
|
+
const evidence = [
|
|
68
|
+
{ kind: "storage-compatibility", path: "receipts/storage-compatibility.json", value: { schema_id: "dd-flow/memory-init-storage-evidence@1", scope, admission_identity: accepted.identity, proof: accepted.proof } },
|
|
69
|
+
{ kind: "scaffold", path: "receipts/scaffold.json", value: { schema_id: "dd-flow/memory-init-scaffold-evidence@1", scope, bootstrap_sha256: registered.sha256, receipt: registered.receipt } },
|
|
70
|
+
{ kind: "pack-install", path: "receipts/pack-install.json", value: { schema_id: "dd-flow/memory-init-pack-evidence@1", scope, bootstrap_sha256: registered.sha256, layer_sha256: registered.receipt.layer_sha256, pack_sha256: binding.pack_sha256, installation_sha256: binding.installation_manifest_sha256 } }
|
|
71
|
+
].map(entry => ({ kind: entry.kind, path: entry.path, sha256: hash(bytes(entry.value)), base64: bytes(entry.value).toString("base64") }));
|
|
72
|
+
const reference = (index) => ({ path: evidence[index].path, sha256: evidence[index].sha256 });
|
|
73
|
+
const receipt = { schema_id: "dd-flow/memory-bootstrap-receipt@2", request_id: scope.requestId, flow_id: "mb-init",
|
|
74
|
+
project_root: scope.projectRoot, workspace: { path: scope.workspaceRoot, repository: binding.target.repository,
|
|
75
|
+
branch: binding.target.branch, commit: binding.target.head, git_state: binding.target.head === null ? "unborn" : "committed" },
|
|
76
|
+
baseline_sha256: registered.receipt.target_sha256, project_current: null,
|
|
77
|
+
upgrade_target: { canon_version: release.canon.version, pack_sha256: binding.pack_sha256 },
|
|
78
|
+
effective_execution: { engine_sha256: engineSha256, canon_sha256: release.canon.archive.sha256 },
|
|
79
|
+
readiness: "ready", storage: { route: "verified-no-op", outcome: "no-op", compatibility_receipt: reference(0),
|
|
80
|
+
drain_receipt: null, backup_receipt: null, plan_receipt: null, apply_receipt: null, verification_receipt: null },
|
|
81
|
+
effects: [{ kind: "scaffold", receipt: reference(1) }, { kind: "pack-install", receipt: reference(2) }] };
|
|
82
|
+
const admission = { receipts: new Map(evidence.map(entry => [entry.sha256, { kind: entry.kind, ...fact }])),
|
|
83
|
+
bootstrapIdentity: { ...fact, baselineSha256: registered.receipt.target_sha256,
|
|
84
|
+
engineSha256, canonSha256: release.canon.archive.sha256,
|
|
85
|
+
workspaceGit: { repository: binding.target.repository, branch: binding.target.branch, commit: binding.target.head } } };
|
|
86
|
+
const authority = { schema_id: "dd-flow/memory-bootstrap-authority@1", request_id: scope.requestId, project_root: scope.projectRoot,
|
|
87
|
+
workspace_root: scope.workspaceRoot, flow_id: "mb-init", readiness: "ready", model_access: "unknown",
|
|
88
|
+
bootstrap_sha256: hash(bytes(receipt)), project_bootstrap_sha256: registered.sha256,
|
|
89
|
+
engine_admission_identity: accepted.identity, binding, receipt, evidence };
|
|
90
|
+
return { authority, admission, projectId: registered.receipt.project_id };
|
|
91
|
+
}
|
|
92
|
+
function row(context, scope, event) {
|
|
93
|
+
const rows = context.db.all("SELECT project_id,payload_json FROM audit_events WHERE event_type=? AND json_extract(payload_json,'$.request_id')=? AND json_extract(payload_json,'$.project_root')=? AND json_extract(payload_json,'$.workspace_root')=? LIMIT 2", [event, scope.requestId, scope.projectRoot, scope.workspaceRoot]);
|
|
94
|
+
if (rows.length > 1)
|
|
95
|
+
refuse("ambiguous_readiness_custody");
|
|
96
|
+
if (!rows.length)
|
|
97
|
+
return null;
|
|
98
|
+
if (Buffer.byteLength(rows[0].payload_json) > 4 * 1024 * 1024)
|
|
99
|
+
refuse("unbounded_readiness_custody");
|
|
100
|
+
return { ...rows[0], value: JSON.parse(rows[0].payload_json) };
|
|
101
|
+
}
|
|
102
|
+
/** Historical readiness custody only. Current physical admission belongs to
|
|
103
|
+
* prepare/apply. Loss of either independent record cannot republish authority.
|
|
104
|
+
*/
|
|
105
|
+
export function readMemoryInitBootstrapReadiness(context, scope) {
|
|
106
|
+
try {
|
|
107
|
+
return context.db.readSnapshot(() => {
|
|
108
|
+
const retained = row(context, scope, receiptEvent), intent = row(context, scope, intentEvent);
|
|
109
|
+
if (!retained && !intent)
|
|
110
|
+
return null;
|
|
111
|
+
if (!retained || !intent)
|
|
112
|
+
refuse("readiness_custody_missing");
|
|
113
|
+
const binding = retained.value.binding;
|
|
114
|
+
if (!binding || hybridDigest(binding.scope) !== hybridDigest(scope)
|
|
115
|
+
|| (Object.hasOwn(binding, "git_policy_sha256") && (typeof binding.git_policy_sha256 !== "string" || !/^[a-f0-9]{64}$/.test(binding.git_policy_sha256)))
|
|
116
|
+
|| Object.keys(binding).sort().join() !== (Object.hasOwn(binding, "git_policy_sha256")
|
|
117
|
+
? "engine_manifest_base64,execution_sha256,git_policy_sha256,installation_manifest_base64,installation_manifest_sha256,pack_sha256,scope,target"
|
|
118
|
+
: "engine_manifest_base64,execution_sha256,installation_manifest_base64,installation_manifest_sha256,pack_sha256,scope,target"))
|
|
119
|
+
refuse("invalid_readiness_binding");
|
|
120
|
+
const selected = material(context, binding), expected = { schema_id: "dd-flow/memory-bootstrap-readiness-intent@1", request_id: scope.requestId,
|
|
121
|
+
project_root: scope.projectRoot, workspace_root: scope.workspaceRoot, authority_sha256: hybridDigest(selected.authority) };
|
|
122
|
+
if (retained.project_id !== selected.projectId || intent.project_id !== selected.projectId
|
|
123
|
+
|| hybridDigest(retained.value) !== hybridDigest(selected.authority) || hybridDigest(intent.value) !== hybridDigest(expected))
|
|
124
|
+
refuse("changed_readiness_custody");
|
|
125
|
+
return { ...selected, authority: structuredClone(selected.authority) };
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
if (error instanceof AppError)
|
|
130
|
+
throw error;
|
|
131
|
+
return refuse("invalid_readiness_custody");
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/** Request-owned runtime readiness, not semantic memory acceptance or account
|
|
135
|
+
* qualification. Provider failures after this receipt keep a partial bootstrap;
|
|
136
|
+
* no hidden model turn is dispatched to obtain the receipt.
|
|
137
|
+
*/
|
|
138
|
+
export class PreparedMemoryInitBootstrapReadiness {
|
|
139
|
+
#bootstrap;
|
|
140
|
+
#engine;
|
|
141
|
+
#binding;
|
|
142
|
+
#material;
|
|
143
|
+
constructor(bootstrap, engine, binding, selected) {
|
|
144
|
+
this.#bootstrap = bootstrap;
|
|
145
|
+
this.#engine = engine;
|
|
146
|
+
this.#binding = structuredClone(binding);
|
|
147
|
+
this.#material = selected;
|
|
148
|
+
Object.freeze(this);
|
|
149
|
+
}
|
|
150
|
+
static prepare(context, input) {
|
|
151
|
+
if (!(input.bootstrap instanceof PreparedMemoryInitProjectBootstrap))
|
|
152
|
+
refuse("unprepared_project_bootstrap");
|
|
153
|
+
const validate = prepareMemoryInitBootstrapSchema(input.engine);
|
|
154
|
+
const { registered: _registered, accepted: _accepted, ...binding } = input.bootstrap.prepareReadiness(context, input.engine);
|
|
155
|
+
void _registered;
|
|
156
|
+
void _accepted;
|
|
157
|
+
const selected = context.db.readSnapshot(() => material(context, binding));
|
|
158
|
+
if (!validate(selected.authority.receipt) || validateHybridContract("memory-bootstrap-receipt", selected.authority.receipt, selected.admission).length)
|
|
159
|
+
refuse("invalid_bootstrap_contract");
|
|
160
|
+
const prior = readMemoryInitBootstrapReadiness(context, binding.scope);
|
|
161
|
+
if (prior && hybridDigest(prior.authority) !== hybridDigest(selected.authority))
|
|
162
|
+
refuse("changed_readiness_request");
|
|
163
|
+
return new PreparedMemoryInitBootstrapReadiness(input.bootstrap, input.engine, binding, selected);
|
|
164
|
+
}
|
|
165
|
+
apply(context) {
|
|
166
|
+
const { registered: _registered, accepted: _accepted, ...current } = this.#bootstrap.prepareReadiness(context, this.#engine);
|
|
167
|
+
void _registered;
|
|
168
|
+
void _accepted;
|
|
169
|
+
if (hybridDigest(current) !== hybridDigest(this.#binding))
|
|
170
|
+
refuse("readiness_changed_after_preparation");
|
|
171
|
+
return context.db.writeTransaction(() => {
|
|
172
|
+
const selected = material(context, this.#binding);
|
|
173
|
+
if (hybridDigest(selected.authority) !== hybridDigest(this.#material.authority))
|
|
174
|
+
refuse("changed_bootstrap_dependencies");
|
|
175
|
+
const prior = readMemoryInitBootstrapReadiness(context, this.#binding.scope);
|
|
176
|
+
if (prior) {
|
|
177
|
+
if (hybridDigest(prior.authority) !== hybridDigest(selected.authority))
|
|
178
|
+
refuse("changed_readiness_request");
|
|
179
|
+
return { ...prior, changed: false };
|
|
180
|
+
}
|
|
181
|
+
const scope = this.#binding.scope;
|
|
182
|
+
appendAudit(context, { projectId: selected.projectId, eventType: intentEvent,
|
|
183
|
+
payload: { schema_id: "dd-flow/memory-bootstrap-readiness-intent@1", request_id: scope.requestId, project_root: scope.projectRoot,
|
|
184
|
+
workspace_root: scope.workspaceRoot, authority_sha256: hybridDigest(selected.authority) } });
|
|
185
|
+
appendAudit(context, { projectId: selected.projectId, eventType: receiptEvent, payload: selected.authority });
|
|
186
|
+
const accepted = readMemoryInitBootstrapReadiness(context, scope);
|
|
187
|
+
if (!accepted)
|
|
188
|
+
refuse("readiness_not_committed");
|
|
189
|
+
return { ...accepted, changed: true };
|
|
190
|
+
}, "memoryInit.bootstrapReadiness");
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/** Productive bootstrap entry: contract admission precedes all project effects.
|
|
194
|
+
* A failure after draft publication is explicitly partial and resumes the same
|
|
195
|
+
* request; it never substitutes fresh initialization or a paid route probe.
|
|
196
|
+
*/
|
|
197
|
+
export function bootstrapMemoryInit(context, input, checkpoint) {
|
|
198
|
+
if (!(input.bootstrap instanceof PreparedMemoryInitProjectBootstrap))
|
|
199
|
+
refuse("unprepared_project_bootstrap");
|
|
200
|
+
prepareMemoryInitBootstrapSchema(input.engine);
|
|
201
|
+
const registered = input.bootstrap.applyWithEngine(context, input.engine, checkpoint);
|
|
202
|
+
try {
|
|
203
|
+
return PreparedMemoryInitBootstrapReadiness.prepare(context, input).apply(context);
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
throw new AppError("memory_init_bootstrap_readiness_failed", "Project drafts are retained but bootstrap readiness is incomplete", 1, { effect: "partial", project_bootstrap_sha256: registered.sha256,
|
|
207
|
+
reason: error instanceof AppError ? error.details.reason ?? error.code : "readiness_operation_failed",
|
|
208
|
+
next_action: "reconcile_exact_request" });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
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 { RetainedExecutionBundle } from "./execution-bundles.js";
|
|
6
|
+
import { publishImmutableInput } from "../storage/immutable-input.js";
|
|
7
|
+
import { readBoundedReleaseFile } from "./release-source.js";
|
|
8
|
+
import { readMemoryInitStartupInput } from "./memory-init-startup-custody.js";
|
|
9
|
+
/** Public init consumes the independently retained intake, never a newly
|
|
10
|
+
* inferred objective. Historical v1 SDK requests must not invent one. */
|
|
11
|
+
export function prepareMemoryInitContextsFromStartup(context, bundle, directory) {
|
|
12
|
+
if (!(bundle instanceof RetainedExecutionBundle) || bundle.manifest.flow_id !== "mb-init") {
|
|
13
|
+
throw new AppError("memory_init_context_invalid", "Init contexts require a retained memory bundle", 2, { effect: "no_effect" });
|
|
14
|
+
}
|
|
15
|
+
const scope = bundle.execution.snapshot().scope;
|
|
16
|
+
const retained = readMemoryInitStartupInput(context, { requestId: scope.requestId, projectRoot: scope.projectRoot, workspaceRoot: scope.workspace });
|
|
17
|
+
if (!retained || !["dd-flow/memory-init-startup-input@2", "dd-flow/memory-init-startup-input@3"].includes(retained.payload.schema_id)
|
|
18
|
+
|| retained.payload.objective === undefined || retained.payload.execution.sha256 !== bundle.execution.sha256
|
|
19
|
+
|| retained.payload.engine.manifest_sha256 !== bundle.manifest.engine_manifest.sha256) {
|
|
20
|
+
throw new AppError("memory_init_context_invalid", "Init contexts require the exact retained objective and execution selection", 1, { effect: "no_effect" });
|
|
21
|
+
}
|
|
22
|
+
return { ...prepareMemoryInitContexts(bundle, retained.payload.objective, directory), objective: retained.payload.objective };
|
|
23
|
+
}
|
|
24
|
+
/** Prepare bytes only. Immutable publication and the controller retain their
|
|
25
|
+
* existing ownership; this does not create directories, Sessions or RUNs. */
|
|
26
|
+
export function prepareMemoryInitContexts(bundle, objective, directory) {
|
|
27
|
+
if (!(bundle instanceof RetainedExecutionBundle) || bundle.manifest.flow_id !== "mb-init"
|
|
28
|
+
|| typeof objective !== "string" || !objective.trim() || Buffer.byteLength(objective) > 64 * 1024
|
|
29
|
+
|| !path.isAbsolute(directory) || path.resolve(directory) !== directory) {
|
|
30
|
+
throw new AppError("memory_init_context_invalid", "Init contexts require a retained memory bundle, bounded objective and absolute directory", 2, { effect: "no_effect" });
|
|
31
|
+
}
|
|
32
|
+
const files = new Map(), stages = Object.create(null);
|
|
33
|
+
const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
34
|
+
for (const stage of bundle.descriptor.descriptor.stages) {
|
|
35
|
+
const file = path.join(directory, `${stage.id}.json`);
|
|
36
|
+
const bytes = Buffer.from(JSON.stringify({ schema_id: "dd-flow/stage-context@1", stage: stage.id, objective }));
|
|
37
|
+
files.set(file, bytes);
|
|
38
|
+
stages[stage.id] = { file, sha256: hash(bytes) };
|
|
39
|
+
}
|
|
40
|
+
const file = path.join(directory, "managed-context.json"), bytes = Buffer.from(JSON.stringify({ schema_id: "dd-flow/managed-context@1", stages }));
|
|
41
|
+
files.set(file, bytes);
|
|
42
|
+
return { file, sha256: hash(bytes), files };
|
|
43
|
+
}
|
|
44
|
+
/** Publish candidates only; an existing controller SQL receipt still owns
|
|
45
|
+
* their execution authority. The caller supplies an already owned directory
|
|
46
|
+
* and independently retained package digest. Never replaces conflicting files. */
|
|
47
|
+
export function publishMemoryInitContexts(bundle, objective, directory, expectedSha256, expectedOwner) {
|
|
48
|
+
const prepared = prepareMemoryInitContexts(bundle, objective, directory);
|
|
49
|
+
if (prepared.sha256 !== expectedSha256)
|
|
50
|
+
throw new AppError("memory_init_context_invalid", "Context package selection changed", 1, { effect: "no_effect" });
|
|
51
|
+
const owner = fs.lstatSync(directory);
|
|
52
|
+
if (expectedOwner && (owner.dev !== expectedOwner.dev || owner.ino !== expectedOwner.ino)) {
|
|
53
|
+
throw new AppError("memory_init_context_invalid", "Context directory differs from its retained owner", 1, { effect: "no_effect" });
|
|
54
|
+
}
|
|
55
|
+
const assertOwner = () => {
|
|
56
|
+
const current = fs.lstatSync(directory);
|
|
57
|
+
if (!current.isDirectory() || current.dev !== owner.dev || current.ino !== owner.ino || fs.realpathSync(directory) !== directory) {
|
|
58
|
+
throw new AppError("memory_init_context_invalid", "Context directory owner changed", 1, { effect: "no_effect" });
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
assertOwner();
|
|
62
|
+
// Reject any known collision before creating the first candidate.
|
|
63
|
+
for (const [file, bytes] of prepared.files) {
|
|
64
|
+
try {
|
|
65
|
+
fs.lstatSync(file);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (error.code === "ENOENT")
|
|
69
|
+
continue;
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
if (!readBoundedReleaseFile(file, bytes.length).equals(bytes))
|
|
73
|
+
throw new AppError("memory_init_context_invalid", "Existing context bytes differ", 1, { effect: "no_effect", path: file });
|
|
74
|
+
}
|
|
75
|
+
let attempted = false;
|
|
76
|
+
try {
|
|
77
|
+
for (const [file, bytes] of prepared.files) {
|
|
78
|
+
assertOwner();
|
|
79
|
+
attempted = true;
|
|
80
|
+
publishImmutableInput(file, bytes, "memory_init_context_invalid");
|
|
81
|
+
}
|
|
82
|
+
assertOwner();
|
|
83
|
+
return { file: prepared.file, sha256: prepared.sha256 };
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
throw new AppError("memory_init_context_publication_incomplete", "Context candidates require exact-request reconciliation", 1, { effect: attempted ? "partial" : "no_effect", directory, expected_sha256: expectedSha256,
|
|
87
|
+
reason: error instanceof AppError ? error.code : "context_publication_failed" });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { AppError, errorRecord } from "../shared/errors.js";
|
|
4
|
+
import { validateMemoryInitStartupScope, readMemoryInitStartupInput } from "./memory-init-startup-custody.js";
|
|
5
|
+
import { startMemoryInitRun } from "./memory-init-start.js";
|
|
6
|
+
import { requireProjectByRoot } from "./projects.js";
|
|
7
|
+
import { readRunStartRequest } from "./run-start-custody.js";
|
|
8
|
+
import { readFlowRunExecutionBundle } from "./runs.js";
|
|
9
|
+
import { requireRun } from "./run-controller-state.js";
|
|
10
|
+
import { prepareRunControllerLaunch } from "./run-controller.js";
|
|
11
|
+
import { prepareMemoryInitContextsFromStartup, publishMemoryInitContexts } from "./memory-init-contexts.js";
|
|
12
|
+
import { parseRuntimeBudget } from "./runtime-budget.js";
|
|
13
|
+
function refuse(reason) {
|
|
14
|
+
throw new AppError("memory_init_controller_start_invalid", "Init controller requires its exact published startup request", 1, { reason, effect: "no_effect", next_action: "reconcile_exact_request" });
|
|
15
|
+
}
|
|
16
|
+
/** Bridge the already published logical RUN to the existing controller. The
|
|
17
|
+
* RUN-start owner admits the root; startup custody supplies the objective.
|
|
18
|
+
* An existing controller replays before candidate publication, so missing
|
|
19
|
+
* productive inputs are never reconstructed here. No process is launched. */
|
|
20
|
+
export function prepareMemoryInitControllerStart(context, scope) {
|
|
21
|
+
validateMemoryInitStartupScope(scope);
|
|
22
|
+
if (context.db.isOwnedWriteTransaction?.())
|
|
23
|
+
refuse("preparation_outside_writer_required");
|
|
24
|
+
parseRuntimeBudget(context.env.DD_FLOW_RUNTIME_BUDGET);
|
|
25
|
+
const startup = readMemoryInitStartupInput(context, scope);
|
|
26
|
+
if (!startup || !["dd-flow/memory-init-startup-input@2", "dd-flow/memory-init-startup-input@3"].includes(startup.payload.schema_id))
|
|
27
|
+
refuse("retained_objective_required");
|
|
28
|
+
const project = requireProjectByRoot(context, scope.projectRoot);
|
|
29
|
+
const request = readRunStartRequest(context, project.id, scope.requestId);
|
|
30
|
+
if (!request?.published || request.payload.initial_index.flow_kind !== "mb-init"
|
|
31
|
+
|| request.payload.workspace_root !== scope.workspaceRoot)
|
|
32
|
+
refuse("published_run_required");
|
|
33
|
+
// Restores the exact request and verifies independent SQL/physical root
|
|
34
|
+
// ownership without consulting current canon/profiles or creating a RUN.
|
|
35
|
+
const started = startMemoryInitRun(context, scope);
|
|
36
|
+
const run = requireRun(context, project.id, started.runId);
|
|
37
|
+
const bundle = readFlowRunExecutionBundle(context, { projectRoot: scope.projectRoot, runId: run.id });
|
|
38
|
+
if (!bundle || bundle.sha256 !== started.bundleSha256)
|
|
39
|
+
refuse("retained_bundle_required");
|
|
40
|
+
if (bundle.execution.settings.execution !== "managed")
|
|
41
|
+
refuse("managed_execution_required");
|
|
42
|
+
const contexts = prepareMemoryInitContextsFromStartup(context, bundle, run.run_root);
|
|
43
|
+
const input = { projectRoot: scope.projectRoot, runId: run.id, requestId: scope.requestId,
|
|
44
|
+
contextFile: contexts.file, contextSha256: contexts.sha256 };
|
|
45
|
+
const controllers = context.db.get("SELECT 1 FROM sqlite_schema WHERE type='table' AND name='run_controllers'")
|
|
46
|
+
? context.db.all("SELECT request_id FROM run_controllers WHERE project_id=? AND run_id=?", [project.id, run.id]) : [];
|
|
47
|
+
if (controllers.some(row => row.request_id === scope.requestId))
|
|
48
|
+
return { input, prepared: prepareRunControllerLaunch(context, input) };
|
|
49
|
+
if (controllers.length)
|
|
50
|
+
refuse("foreign_controller_request");
|
|
51
|
+
// A directory without a receipt is an interrupted/unknown outcome, not
|
|
52
|
+
// evidence that no owner ran. Do not create another physical controller.
|
|
53
|
+
let controllerRootFound = false;
|
|
54
|
+
try {
|
|
55
|
+
fs.lstatSync(path.join(run.run_root, "controllers"));
|
|
56
|
+
controllerRootFound = true;
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (error.code !== "ENOENT")
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
if (controllerRootFound)
|
|
63
|
+
refuse("controller_custody_missing");
|
|
64
|
+
if (context.db.get("SELECT 1 FROM sessions WHERE project_id=? AND run_id=? LIMIT 1", [project.id, run.id]))
|
|
65
|
+
refuse("session_owner_requires_reconciliation");
|
|
66
|
+
const rootOwner = context.db.readSnapshot(() => {
|
|
67
|
+
if (readRunStartRequest(context, project.id, scope.requestId)?.sha256 !== request.sha256)
|
|
68
|
+
refuse("run_request_changed");
|
|
69
|
+
const row = context.db.get("SELECT payload_json FROM audit_events WHERE project_id=? AND event_type='flow_run.start_root_owned' AND json_extract(payload_json,'$.run_id')=?", [project.id, run.id]);
|
|
70
|
+
if (!row)
|
|
71
|
+
refuse("root_owner_missing");
|
|
72
|
+
return JSON.parse(row.payload_json).owner;
|
|
73
|
+
});
|
|
74
|
+
if (!rootOwner || !Number.isSafeInteger(rootOwner.dev) || !Number.isSafeInteger(rootOwner.ino))
|
|
75
|
+
refuse("root_owner_changed");
|
|
76
|
+
publishMemoryInitContexts(bundle, contexts.objective, run.run_root, contexts.sha256, rootOwner);
|
|
77
|
+
try {
|
|
78
|
+
return { input, prepared: prepareRunControllerLaunch(context, input) };
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
throw new AppError("memory_init_controller_start_incomplete", "Retained context candidates require exact-request reconciliation", 1, { effect: "partial", run_id: run.id, request_id: scope.requestId, context_sha256: contexts.sha256,
|
|
82
|
+
cause: errorRecord(error), next_action: "reconcile_exact_request" });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
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 { AdmittedFrozenEngine } from "./frozen-engine.js";
|
|
6
|
+
import { readSetupStatus } from "./setup.js";
|
|
7
|
+
import { inspectReleaseArchive } from "./release-archive.js";
|
|
8
|
+
import { readBoundedReleaseFile } from "./release-source.js";
|
|
9
|
+
import { hybridDigest } from "./hybrid-contracts.js";
|
|
10
|
+
import { engineVersionRoot } from "../storage/paths.js";
|
|
11
|
+
import { engineSnapshotChecksum, installPackageEngine } from "./engines.js";
|
|
12
|
+
const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
13
|
+
function refuse(reason) {
|
|
14
|
+
throw new AppError("memory_init_engine_release_invalid", "Selected engine is not the verified installed release payload", 1, { reason, phase: "prepare", effect: "no_effect" });
|
|
15
|
+
}
|
|
16
|
+
/** Select already delivered release bytes, never the invoking CLI or highest
|
|
17
|
+
* package version. Ambiguous dependency snapshots require explicit selection. */
|
|
18
|
+
export function selectMemoryInitInstalledEngine(home, installationSha256, checksum) {
|
|
19
|
+
if (!path.isAbsolute(home) || path.resolve(home) !== home || fs.realpathSync(home) !== home)
|
|
20
|
+
refuse("noncanonical_installation_home");
|
|
21
|
+
const selected = readSetupStatus(home), installation = selected.installation;
|
|
22
|
+
if (!installation || selected.installation_readiness !== "installed" || installation.manifest_sha256 !== installationSha256)
|
|
23
|
+
refuse("installation_selection_changed");
|
|
24
|
+
if (checksum !== undefined && !/^[a-f0-9]{64}$/.test(checksum))
|
|
25
|
+
refuse("invalid_engine_checksum");
|
|
26
|
+
const base = engineVersionRoot(home, installation.manifest.engine.package, installation.manifest.engine.version);
|
|
27
|
+
const parent = path.dirname(base), version = path.basename(base), candidates = [];
|
|
28
|
+
if (!fs.existsSync(parent))
|
|
29
|
+
refuse("selected_engine_not_delivered");
|
|
30
|
+
if (!fs.lstatSync(parent).isDirectory() || fs.realpathSync(parent) !== parent)
|
|
31
|
+
refuse("unsafe_engine_store");
|
|
32
|
+
for (const entry of fs.readdirSync(parent, { withFileTypes: true })) {
|
|
33
|
+
if (!entry.isDirectory() || !(entry.name === version || entry.name.startsWith(`${version}--`)
|
|
34
|
+
&& /^[a-f0-9]{64}$/.test(entry.name.slice(version.length + 2))))
|
|
35
|
+
continue;
|
|
36
|
+
const root = path.join(parent, entry.name);
|
|
37
|
+
try {
|
|
38
|
+
const bytes = readBoundedReleaseFile(path.join(root, "engine.json"), 1024 * 1024);
|
|
39
|
+
const manifest = JSON.parse(bytes.toString("utf8"));
|
|
40
|
+
if (manifest.snapshot_root !== root || entry.name !== version && entry.name !== `${version}--${manifest.integrity?.checksum}`
|
|
41
|
+
|| checksum !== undefined && manifest.integrity?.checksum !== checksum)
|
|
42
|
+
continue;
|
|
43
|
+
const engine = AdmittedFrozenEngine.admitSelection({ package_name: manifest.package_name, package_version: manifest.package_version,
|
|
44
|
+
engine_version: manifest.engine_version, snapshot_root: root, integrity_checksum: manifest.integrity?.checksum }, hash(bytes));
|
|
45
|
+
prepareMemoryInitEngineRelease(home, installationSha256, engine);
|
|
46
|
+
candidates.push(engine);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
// An unavailable or mismatching old snapshot cannot authorize this release.
|
|
50
|
+
if (error instanceof AppError || error instanceof SyntaxError || error.code === "ENOENT")
|
|
51
|
+
continue;
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (candidates.length !== 1)
|
|
56
|
+
refuse(candidates.length ? "ambiguous_selected_engine" : "selected_engine_not_delivered");
|
|
57
|
+
return candidates[0];
|
|
58
|
+
}
|
|
59
|
+
function checkFile(root, entry) {
|
|
60
|
+
let file = root;
|
|
61
|
+
const parts = entry.path.split("/");
|
|
62
|
+
for (const [index, part] of parts.entries()) {
|
|
63
|
+
file = path.join(file, part);
|
|
64
|
+
const stat = fs.lstatSync(file);
|
|
65
|
+
if (stat.isSymbolicLink() || (index === parts.length - 1 ? !stat.isFile() : !stat.isDirectory()))
|
|
66
|
+
refuse("unsafe_snapshot_path");
|
|
67
|
+
}
|
|
68
|
+
if ((fs.lstatSync(file).mode & 0o111) !== entry.executable_mode)
|
|
69
|
+
refuse("snapshot_payload_mode_changed");
|
|
70
|
+
if (entry.bytes === 0) {
|
|
71
|
+
const fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
|
72
|
+
try {
|
|
73
|
+
const stat = fs.fstatSync(fd);
|
|
74
|
+
if (!stat.isFile() || stat.size !== 0 || fs.readSync(fd, Buffer.alloc(1), 0, 1, 0) !== 0)
|
|
75
|
+
refuse("snapshot_payload_changed");
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
fs.closeSync(fd);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else if (hash(readBoundedReleaseFile(file, entry.bytes)) !== entry.sha256)
|
|
82
|
+
refuse("snapshot_payload_changed");
|
|
83
|
+
}
|
|
84
|
+
function checkInventory(root, inventory) {
|
|
85
|
+
const expected = new Set(inventory.map(entry => entry.path));
|
|
86
|
+
for (const entry of fs.readdirSync(root, { recursive: true, withFileTypes: true })) {
|
|
87
|
+
const logical = path.relative(root, path.join(entry.parentPath, entry.name)).split(path.sep).join("/");
|
|
88
|
+
if (!entry.isFile() && !entry.isDirectory())
|
|
89
|
+
refuse("unsafe_snapshot_path");
|
|
90
|
+
if (logical !== "engine.json" && !expected.has(logical)
|
|
91
|
+
&& (entry.name === "engine.json" || entry.isFile() && !logical.startsWith("node_modules/")))
|
|
92
|
+
refuse("unselected_snapshot_file");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function archiveInventory(archive, engineRoot) {
|
|
96
|
+
const entries = inspectReleaseArchive(archive, readBoundedReleaseFile(path.join(path.dirname(engineRoot), "archive.tgz"), Math.min(archive.bytes, 256 * 1024 * 1024)));
|
|
97
|
+
const inventory = [];
|
|
98
|
+
for (const entry of entries) {
|
|
99
|
+
if (entry.path !== "package" && !entry.path.startsWith("package/"))
|
|
100
|
+
refuse("foreign_payload_root");
|
|
101
|
+
if (entry.kind !== "file")
|
|
102
|
+
continue;
|
|
103
|
+
const logical = entry.path.slice("package/".length);
|
|
104
|
+
if (!logical || logical === "engine.json")
|
|
105
|
+
refuse("reserved_payload_path");
|
|
106
|
+
inventory.push({ path: logical, sha256: hash(entry.bytes), executable_mode: entry.mode & 0o111, bytes: entry.bytes.length });
|
|
107
|
+
}
|
|
108
|
+
return inventory;
|
|
109
|
+
}
|
|
110
|
+
/** Existing package/dependency acquisition supplies the independently selected
|
|
111
|
+
* full checksum. This gate binds transferred package files to verified setup
|
|
112
|
+
* bytes before delivery and again in the owned staging directory. */
|
|
113
|
+
export function deliverMemoryInitReleaseEngine(context, input) {
|
|
114
|
+
const home = context.ddFlowHome;
|
|
115
|
+
if (!path.isAbsolute(home) || fs.realpathSync(home) !== home || !path.isAbsolute(input.packageRoot)
|
|
116
|
+
|| fs.realpathSync(input.packageRoot) !== input.packageRoot || !/^[a-f0-9]{64}$/.test(input.expectedChecksum))
|
|
117
|
+
refuse("invalid_delivery_selection");
|
|
118
|
+
const selected = readSetupStatus(home), installation = selected.installation;
|
|
119
|
+
if (!installation || selected.installation_readiness !== "installed" || installation.manifest_sha256 !== input.installationSha256)
|
|
120
|
+
refuse("installation_selection_changed");
|
|
121
|
+
const inventory = archiveInventory(installation.manifest.engine.archive, installation.engine_root);
|
|
122
|
+
const check = (root) => {
|
|
123
|
+
try {
|
|
124
|
+
for (const file of inventory)
|
|
125
|
+
checkFile(root, file);
|
|
126
|
+
checkInventory(root, inventory);
|
|
127
|
+
if (engineSnapshotChecksum(root) !== input.expectedChecksum)
|
|
128
|
+
refuse("delivery_checksum_changed");
|
|
129
|
+
const current = readSetupStatus(home);
|
|
130
|
+
if (current.installation_readiness !== "installed" || hybridDigest(current.installation) !== hybridDigest(installation))
|
|
131
|
+
refuse("installation_changed_during_delivery");
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
if (error instanceof AppError && error.code === "memory_init_engine_release_invalid")
|
|
135
|
+
throw error;
|
|
136
|
+
return refuse("payload_unavailable_or_changed");
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
check(input.packageRoot);
|
|
140
|
+
const delivered = installPackageEngine(context, { packageRoot: input.packageRoot, packageName: installation.manifest.engine.package,
|
|
141
|
+
version: installation.manifest.engine.version, expectedChecksum: input.expectedChecksum, validateStaged: check });
|
|
142
|
+
try {
|
|
143
|
+
return selectMemoryInitInstalledEngine(home, input.installationSha256, input.expectedChecksum);
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
throw new AppError("memory_init_engine_delivery_incomplete", "Delivered engine requires exact selection reconciliation", 1, { effect: delivered.changed ? "partial" : "no_effect", path: delivered.path, reason: error instanceof AppError ? error.details.reason ?? error.code : "delivered_selection_changed" });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/** expectedSha256 must come from independent request custody. No current
|
|
150
|
+
* installation/canon/profile reads and no restoration of missing files. */
|
|
151
|
+
export function revalidateMemoryInitEngineRelease(input, expectedSha256, engine) {
|
|
152
|
+
try {
|
|
153
|
+
if (!(engine instanceof AdmittedFrozenEngine) || !input || typeof input !== "object" || Array.isArray(input))
|
|
154
|
+
refuse("invalid_retained_pairing");
|
|
155
|
+
const facts = input, digest = (value) => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
|
|
156
|
+
if (Object.keys(facts).sort().join() !== "archive_sha256,engine_manifest_sha256,engine_sha256,installation_sha256,inventory,schema_id"
|
|
157
|
+
|| facts.schema_id !== "dd-flow/memory-init-engine-release@1" || !digest(expectedSha256)
|
|
158
|
+
|| [facts.installation_sha256, facts.archive_sha256, facts.engine_manifest_sha256, facts.engine_sha256].some(value => !digest(value))
|
|
159
|
+
|| !Array.isArray(facts.inventory) || !facts.inventory.length || facts.inventory.length > 100_000
|
|
160
|
+
|| hybridDigest(facts) !== expectedSha256)
|
|
161
|
+
refuse("invalid_retained_pairing");
|
|
162
|
+
const manifest = engine.manifest();
|
|
163
|
+
if (engine.manifestSha256 !== facts.engine_manifest_sha256 || manifest.integrity.checksum !== facts.engine_sha256)
|
|
164
|
+
refuse("foreign_engine_identity");
|
|
165
|
+
const seen = new Set();
|
|
166
|
+
let total = 0;
|
|
167
|
+
for (const entry of facts.inventory) {
|
|
168
|
+
if (!entry || Object.keys(entry).sort().join() !== "bytes,executable_mode,path,sha256" || typeof entry.path !== "string"
|
|
169
|
+
|| entry.path.length > 1024 || entry.path !== entry.path.normalize("NFC") || /[\\:]/.test(entry.path)
|
|
170
|
+
|| [...entry.path].some(character => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127)
|
|
171
|
+
|| entry.path.split("/").some(part => !part || part === "." || part === "..") || entry.path === "engine.json"
|
|
172
|
+
|| seen.has(entry.path.toLowerCase()) || !digest(entry.sha256) || !Number.isSafeInteger(entry.bytes) || entry.bytes < 0
|
|
173
|
+
|| entry.bytes === 0 && entry.sha256 !== hash(Buffer.alloc(0))
|
|
174
|
+
|| !Number.isSafeInteger(entry.executable_mode) || entry.executable_mode < 0 || entry.executable_mode > 0o111 || (entry.executable_mode & ~0o111) !== 0
|
|
175
|
+
|| (total += entry.bytes) > 512 * 1024 * 1024)
|
|
176
|
+
refuse("invalid_retained_inventory");
|
|
177
|
+
seen.add(entry.path.toLowerCase());
|
|
178
|
+
checkFile(manifest.snapshot_root, entry);
|
|
179
|
+
}
|
|
180
|
+
checkInventory(manifest.snapshot_root, facts.inventory);
|
|
181
|
+
AdmittedFrozenEngine.admitSelection({ package_name: manifest.package_name, package_version: manifest.package_version,
|
|
182
|
+
engine_version: manifest.engine_version, snapshot_root: manifest.snapshot_root, integrity_checksum: facts.engine_sha256 }, facts.engine_manifest_sha256);
|
|
183
|
+
return { facts: structuredClone(facts), sha256: expectedSha256 };
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
if (error instanceof AppError && error.code === "memory_init_engine_release_invalid")
|
|
187
|
+
throw error;
|
|
188
|
+
return refuse("payload_unavailable_or_changed");
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/** Fresh selection only. Restarts must retain this inventory independently,
|
|
192
|
+
* not consult the current installation. This does not install dependencies,
|
|
193
|
+
* admit handlers/storage/provider access or grant project effects. */
|
|
194
|
+
export function prepareMemoryInitEngineRelease(home, installationSha256, engine) {
|
|
195
|
+
try {
|
|
196
|
+
if (!(engine instanceof AdmittedFrozenEngine))
|
|
197
|
+
refuse("unprepared_engine");
|
|
198
|
+
if (!path.isAbsolute(home) || path.resolve(home) !== home || fs.realpathSync(home) !== home)
|
|
199
|
+
refuse("noncanonical_installation_home");
|
|
200
|
+
const selected = readSetupStatus(home), installation = selected.installation;
|
|
201
|
+
if (!installation || selected.installation_readiness !== "installed" || installation.manifest_sha256 !== installationSha256)
|
|
202
|
+
refuse("installation_selection_changed");
|
|
203
|
+
const manifest = engine.manifest();
|
|
204
|
+
if (manifest.package_name !== installation.manifest.engine.package || manifest.package_version !== installation.manifest.engine.version)
|
|
205
|
+
refuse("foreign_engine_identity");
|
|
206
|
+
AdmittedFrozenEngine.admitSelection({ package_name: manifest.package_name, package_version: manifest.package_version,
|
|
207
|
+
engine_version: manifest.engine_version, snapshot_root: manifest.snapshot_root, integrity_checksum: manifest.integrity.checksum }, engine.manifestSha256);
|
|
208
|
+
const archive = installation.manifest.engine.archive;
|
|
209
|
+
const inventory = archiveInventory(archive, installation.engine_root);
|
|
210
|
+
const final = readSetupStatus(home);
|
|
211
|
+
if (final.installation_readiness !== "installed" || hybridDigest(final.installation) !== hybridDigest(installation))
|
|
212
|
+
refuse("installation_changed_during_admission");
|
|
213
|
+
const facts = { schema_id: "dd-flow/memory-init-engine-release@1", installation_sha256: installationSha256,
|
|
214
|
+
archive_sha256: archive.sha256, engine_manifest_sha256: engine.manifestSha256,
|
|
215
|
+
engine_sha256: manifest.integrity.checksum, inventory: inventory.sort((a, b) => a.path.localeCompare(b.path, "en")) };
|
|
216
|
+
return revalidateMemoryInitEngineRelease(facts, hybridDigest(facts), engine);
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
if (error instanceof AppError && error.code === "memory_init_engine_release_invalid")
|
|
220
|
+
throw error;
|
|
221
|
+
return refuse("payload_unavailable_or_changed");
|
|
222
|
+
}
|
|
223
|
+
}
|