@deksden-com/dd-flow-cli 0.9.0-beta.136 → 0.9.0-beta.138
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +9 -0
- package/dist/build-info.json +6 -6
- package/dist/cli/argument-values.js +5 -5
- package/dist/cli/command-inputs.js +89 -20
- package/dist/cli/command-metadata.js +56 -3
- package/dist/cli/command-option-descriptions.js +5 -0
- package/dist/cli/executor-onboarding.js +8 -3
- package/dist/cli/flows.js +29 -0
- package/dist/cli/help.js +240 -6
- package/dist/cli/input-preparation.js +54 -5
- package/dist/cli/memory-flow-admission.js +44 -0
- package/dist/cli/memory-init-invocation.js +78 -0
- package/dist/cli/memory-init.js +240 -0
- package/dist/cli/memory-storage-admission.js +53 -0
- package/dist/cli/native-hook-ingress.js +1 -1
- package/dist/cli/response-file.js +2 -0
- package/dist/cli/run-cli.js +283 -160
- package/dist/cli/setup.js +9 -7
- package/dist/cli.js +54 -2
- package/dist/domain/stage-handler-identities.js +20 -0
- package/dist/harness-runtime/DD-ZCODE.md +13 -0
- package/dist/harness-runtime/bin/dd-opencode.mjs +3 -3
- package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
- package/dist/harness-runtime/lib/daemon-operations.mjs +4 -3
- package/dist/harness-runtime/lib/dd-agy-daemon.mjs +4 -2
- package/dist/harness-runtime/lib/dd-codex-daemon.mjs +11 -5
- package/dist/harness-runtime/lib/dd-codex.mjs +22 -10
- package/dist/harness-runtime/lib/dd-droid-daemon.mjs +2 -1
- package/dist/harness-runtime/lib/dd-droid.mjs +2 -1
- package/dist/harness-runtime/lib/dd-grok-daemon.mjs +4 -2
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +20 -5
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +45 -10
- package/dist/harness-runtime/lib/dd-zcode.mjs +7 -3
- package/dist/harness-runtime/lib/delegation-instructions.mjs +5 -3
- package/dist/harness-runtime/lib/lifecycle-command.mjs +1 -0
- package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -0
- package/dist/harness-runtime/lib/managed-daemon.mjs +50 -15
- package/dist/harness-runtime/lib/managed-flow-env.mjs +15 -0
- package/dist/harness-runtime/lib/native-hook-command.mjs +1 -1
- package/dist/harness-runtime/lib/native-request-proof.d.mts +2 -0
- package/dist/harness-runtime/lib/native-request-proof.mjs +65 -0
- package/dist/harness-runtime/lib/opencode-plugin.mjs +5 -1
- package/dist/harness-runtime/lib/operation-progress.mjs +3 -1
- package/dist/runtime/context.js +1 -1
- package/dist/schemas/discovery-result.schema.json +184 -0
- package/dist/schemas/execution-bundle.schema.json +205 -0
- package/dist/schemas/execution-settings.schema.json +162 -0
- package/dist/schemas/flow-descriptor.schema.json +54 -0
- package/dist/schemas/flow-run.schema.json +22 -0
- package/dist/schemas/harness-catalog.schema.json +80 -0
- package/dist/schemas/mb-init-compose-result.schema.json +457 -0
- package/dist/schemas/mb-init-discover-result.schema.json +500 -0
- package/dist/schemas/mb-init-finish-result.schema.json +422 -0
- package/dist/schemas/mb-init-preflight-result.schema.json +367 -0
- package/dist/schemas/mb-init-review-result.schema.json +396 -0
- package/dist/schemas/mb-init-verify-result.schema.json +391 -0
- package/dist/schemas/memory-bootstrap-receipt-v2.schema.json +67 -0
- package/dist/schemas/memory-bootstrap-receipt.schema.json +354 -0
- package/dist/schemas/memory-composition-work-result.schema.json +38 -0
- package/dist/schemas/memory-repair-work-result.schema.json +49 -0
- package/dist/schemas/memory-review-correction-plan.schema.json +30 -0
- package/dist/schemas/memory-review-work-result.schema.json +43 -0
- package/dist/schemas/model-catalog.schema.json +153 -0
- package/dist/schemas/profile-settings.schema.json +122 -0
- package/dist/schemas/release-manifest.schema.json +291 -0
- package/dist/schemas/run-engine-binding.schema.json +1 -1
- package/dist/services/check-runner-evidence.js +51 -4
- package/dist/services/cli-operation-classifier.js +8 -47
- package/dist/services/code-checks.js +387 -34
- package/dist/services/controller-child-observer.js +99 -0
- package/dist/services/controller-execution.js +88 -0
- package/dist/services/controller-fanout.js +79 -13
- package/dist/services/dashboard.js +8 -2
- package/dist/services/descriptor-coordinator-session.js +35 -0
- package/dist/services/descriptor-root-work.js +53 -0
- package/dist/services/descriptor-run-settlement.js +105 -0
- package/dist/services/descriptor-run-start.js +449 -0
- package/dist/services/descriptor-stage-coordinator.js +197 -0
- package/dist/services/descriptor-stage-settlement.js +222 -0
- package/dist/services/descriptor-stage-start.js +139 -0
- package/dist/services/engines.js +39 -22
- package/dist/services/eval-snapshots.js +83 -25
- package/dist/services/execution-bundles.js +263 -0
- package/dist/services/execution-policy.js +20 -16
- package/dist/services/external-work-launch.js +91 -11
- package/dist/services/flow-catalog.js +21 -0
- package/dist/services/flow-descriptors.js +82 -0
- package/dist/services/frozen-engine.js +242 -0
- package/dist/services/generic-execution.js +164 -0
- package/dist/services/harness-adapter.js +28 -2
- package/dist/services/hooks.js +24 -6
- package/dist/services/hybrid-contracts.js +31 -3
- package/dist/services/ids.js +29 -1
- package/dist/services/installation-lock.js +7 -4
- package/dist/services/installed-flow-source.js +183 -0
- package/dist/services/lifecycle-invocations.js +257 -78
- package/dist/services/memory-accepted-composition.js +77 -0
- package/dist/services/memory-baseline-preservation.js +65 -0
- package/dist/services/memory-completed-correction-wave.js +320 -0
- package/dist/services/memory-compose-handler.js +258 -0
- package/dist/services/memory-composition-acceptance.js +199 -0
- package/dist/services/memory-composition-graph.js +134 -0
- package/dist/services/memory-composition-registration.js +146 -0
- package/dist/services/memory-composition-work-custody.js +151 -0
- package/dist/services/memory-composition-work-results.js +96 -0
- package/dist/services/memory-corrected-review-acceptance.js +129 -0
- package/dist/services/memory-corrected-review-batch.js +30 -0
- package/dist/services/memory-corrected-review-findings.js +106 -0
- package/dist/services/memory-corrected-review-inputs.js +210 -0
- package/dist/services/memory-corrected-review-projection.js +78 -0
- package/dist/services/memory-corrected-review-receipt.js +170 -0
- package/dist/services/memory-corrected-review-registration.js +191 -0
- package/dist/services/memory-correction-check-receipt.js +219 -0
- package/dist/services/memory-correction-input-owner.js +6 -0
- package/dist/services/memory-discover-handler.js +104 -0
- package/dist/services/memory-finish-evidence.js +435 -0
- package/dist/services/memory-finish-handler.js +215 -0
- package/dist/services/memory-finish-integration.js +280 -0
- package/dist/services/memory-finish-readiness.js +122 -0
- package/dist/services/memory-git-integration.js +151 -0
- package/dist/services/memory-historical-corrected-review.js +206 -0
- package/dist/services/memory-historical-original-review.js +130 -0
- package/dist/services/memory-historical-repair-registration.js +99 -0
- package/dist/services/memory-historical-second-review.js +128 -0
- package/dist/services/memory-init-bootstrap-readiness.js +210 -0
- package/dist/services/memory-init-contexts.js +89 -0
- package/dist/services/memory-init-controller-start.js +84 -0
- package/dist/services/memory-init-engine-release.js +223 -0
- package/dist/services/memory-init-git-policy.js +136 -0
- package/dist/services/memory-init-lint-capability.js +185 -0
- package/dist/services/memory-init-native-capacity.js +241 -0
- package/dist/services/memory-init-preflight.js +312 -0
- package/dist/services/memory-init-project-bootstrap.js +599 -0
- package/dist/services/memory-init-request-candidate.js +157 -0
- package/dist/services/memory-init-request-input.js +66 -0
- package/dist/services/memory-init-resource-storage.js +125 -0
- package/dist/services/memory-init-run-allocation.js +81 -0
- package/dist/services/memory-init-run-bundle.js +98 -0
- package/dist/services/memory-init-start.js +175 -0
- package/dist/services/memory-init-startup-custody.js +80 -0
- package/dist/services/memory-init-startup-input.js +240 -0
- package/dist/services/memory-init-storage.js +81 -0
- package/dist/services/memory-init-stores.js +359 -0
- package/dist/services/memory-init-worktrunk-capability.js +172 -0
- package/dist/services/memory-layer-publication.js +611 -0
- package/dist/services/memory-layer.js +156 -0
- package/dist/services/memory-lint-config.js +85 -0
- package/dist/services/memory-native-review-copy.js +165 -0
- package/dist/services/memory-normal-flow-entry.js +75 -0
- package/dist/services/memory-ordinary-defaults.js +112 -0
- package/dist/services/memory-preflight-bootstrap-custody.js +115 -0
- package/dist/services/memory-preflight-handler.js +259 -0
- package/dist/services/memory-repair-current-view.js +299 -0
- package/dist/services/memory-repair-document-delta.js +451 -0
- package/dist/services/memory-repair-history.js +491 -0
- package/dist/services/memory-repair-physical-binding.js +312 -0
- package/dist/services/memory-repair-publication-files.js +152 -0
- package/dist/services/memory-repair-publication-intent.js +27 -0
- package/dist/services/memory-repair-publication-plan.js +160 -0
- package/dist/services/memory-repair-publication-source.js +225 -0
- package/dist/services/memory-repair-publication.js +794 -0
- package/dist/services/memory-repair-registration.js +239 -0
- package/dist/services/memory-repair-work-baseline.js +413 -0
- package/dist/services/memory-repair-work-completion.js +97 -0
- package/dist/services/memory-repair-work-custody.js +73 -0
- package/dist/services/memory-repair-work-results.js +89 -0
- package/dist/services/memory-repair-workspace.js +252 -0
- package/dist/services/memory-repair-writer-copy.js +735 -0
- package/dist/services/memory-result-binding.js +173 -0
- package/dist/services/memory-retained-review-authority.js +129 -0
- package/dist/services/memory-retained-review-results.js +70 -0
- package/dist/services/memory-retained-second-baseline.js +102 -0
- package/dist/services/memory-retained-second-repairs.js +156 -0
- package/dist/services/memory-review-acceptance.js +214 -0
- package/dist/services/memory-review-correction-plan.js +171 -0
- package/dist/services/memory-review-handler.js +585 -0
- package/dist/services/memory-review-inputs.js +200 -0
- package/dist/services/memory-review-receipt.js +180 -0
- package/dist/services/memory-review-registration.js +108 -0
- package/dist/services/memory-review-work-custody.js +124 -0
- package/dist/services/memory-review-work-input.js +72 -0
- package/dist/services/memory-review-work-results.js +73 -0
- package/dist/services/memory-second-corrected-review-inputs.js +165 -0
- package/dist/services/memory-second-correction-plan.js +76 -0
- package/dist/services/memory-second-repair-baseline.js +91 -0
- package/dist/services/memory-second-repair-launch.js +614 -0
- package/dist/services/memory-second-repair-registration.js +88 -0
- package/dist/services/memory-second-repair-source.js +187 -0
- package/dist/services/memory-second-review-acceptance.js +115 -0
- package/dist/services/memory-source-observations.js +117 -0
- package/dist/services/memory-stage-dispatch.js +28 -0
- package/dist/services/memory-stage-results.js +199 -0
- package/dist/services/memory-verification-acceptance.js +135 -0
- package/dist/services/memory-verification-checks.js +252 -0
- package/dist/services/memory-verification-receipt.js +159 -0
- package/dist/services/memory-verify-handler.js +135 -0
- package/dist/services/memory-work-result-preparation.js +63 -0
- package/dist/services/projects.js +17 -2
- package/dist/services/recovery-snapshot-database.js +416 -1
- package/dist/services/repair-intents.js +1 -1
- package/dist/services/review-copy.js +10 -2
- package/dist/services/run-control.js +80 -9
- package/dist/services/run-controller-capture.js +10 -3
- package/dist/services/run-controller-process.js +21 -0
- package/dist/services/run-controller-recovery.js +3 -1
- package/dist/services/run-controller-state.js +1 -1
- package/dist/services/run-controller.js +184 -48
- package/dist/services/run-recovery-guard.js +2 -0
- package/dist/services/run-recovery.js +145 -5
- package/dist/services/run-start-custody.js +177 -0
- package/dist/services/runs.js +319 -25
- package/dist/services/stage-context.js +9 -1
- package/dist/services/stage-handlers.js +74 -0
- package/dist/services/stage-lifecycle.js +2 -1
- package/dist/services/stage-report-renderer.js +76 -0
- package/dist/services/stage-work-graph.js +17 -0
- package/dist/services/vnext-code.js +3 -3
- package/dist/services/vnext-execution-profile.js +2 -2
- package/dist/services/vnext-fanout.js +136 -10
- package/dist/services/vnext-merge.js +8 -3
- package/dist/services/vnext-plan-review.js +6 -4
- package/dist/services/vnext-protocolize.js +60 -2
- package/dist/services/work-registry.js +616 -71
- package/dist/services/workspace-files.js +1 -1
- package/dist/storage/database.js +84 -6
- package/dist/storage/writer-contract.js +11 -7
- package/package.json +13 -3
- package/tools/create-canon-release.mjs +33 -0
- package/tools/produce-hybrid-release.mjs +41 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { AppError } from "../shared/errors.js";
|
|
2
|
+
import { sameSessionProfile } from "./execution-policy.js";
|
|
3
|
+
import { hybridDigest, validateHybridContract } from "./hybrid-contracts.js";
|
|
4
|
+
import { prepareOperatorProfile, resolveOperatorProfile } from "./operator-profiles.js";
|
|
5
|
+
function fail(message, issues) {
|
|
6
|
+
throw new AppError("generic_execution_invalid", message, 2, { phase: "prepare", effect: "no_effect", ...(issues ? { issues } : {}) });
|
|
7
|
+
}
|
|
8
|
+
function freeze(value) {
|
|
9
|
+
if (value !== null && typeof value === "object") {
|
|
10
|
+
for (const child of Object.values(value))
|
|
11
|
+
freeze(child);
|
|
12
|
+
Object.freeze(value);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function scopedReceipts(settings, scope, receipts) {
|
|
16
|
+
for (const [reference, kind] of [
|
|
17
|
+
[settings.attached_session_receipt, "attached-session-binding"],
|
|
18
|
+
[settings.delegation.native_capacity_receipt, "native-capacity"]
|
|
19
|
+
]) {
|
|
20
|
+
if (reference === null)
|
|
21
|
+
continue;
|
|
22
|
+
const fact = receipts.get(reference.sha256);
|
|
23
|
+
if (!fact || fact.kind !== kind || fact.requestId !== scope.requestId
|
|
24
|
+
|| fact.projectRoot !== scope.projectRoot || fact.workspace !== scope.workspace) {
|
|
25
|
+
fail("Execution receipt does not belong to the selected request and workspace");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function validateSnapshot(snapshot, flow, scope, receipts) {
|
|
30
|
+
if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot)
|
|
31
|
+
|| Object.keys(snapshot).some(key => !["schema_id", "scope", "settings", "profiles"].includes(key))
|
|
32
|
+
|| ["schema_id", "scope", "settings", "profiles"].some(key => !Object.hasOwn(snapshot, key))
|
|
33
|
+
|| snapshot.schema_id !== "dd-flow/frozen-execution@1"
|
|
34
|
+
|| !snapshot.scope || typeof snapshot.scope !== "object" || Array.isArray(snapshot.scope)
|
|
35
|
+
|| !scope || [scope.requestId, scope.projectRoot, scope.workspace].some(value => typeof value !== "string" || !value.trim())
|
|
36
|
+
|| hybridDigest(snapshot.scope) !== hybridDigest(scope))
|
|
37
|
+
fail("Invalid frozen execution identity");
|
|
38
|
+
const content = { schema_id: "dd-flow/profile-settings@1", profiles: snapshot.profiles, aliases: {}, default_profile: null };
|
|
39
|
+
const profiles = { ...content, revision: hybridDigest(content) };
|
|
40
|
+
const issues = validateHybridContract("execution-settings", snapshot.settings, {
|
|
41
|
+
descriptor: flow.descriptor, descriptorSha256: flow.sha256, profileSettings: profiles, receipts
|
|
42
|
+
});
|
|
43
|
+
if (issues.length)
|
|
44
|
+
fail("Frozen execution violates the admitted descriptor or profile policy", issues);
|
|
45
|
+
const settings = snapshot.settings;
|
|
46
|
+
if (snapshot.profiles.length !== settings.profiles.length
|
|
47
|
+
|| snapshot.profiles.some(profile => !settings.profiles.some(reference => reference.id === profile.id && reference.revision === profile.revision))) {
|
|
48
|
+
fail("Frozen profile bytes and revision inventory differ");
|
|
49
|
+
}
|
|
50
|
+
for (const stage of flow.descriptor.stages) {
|
|
51
|
+
if (!Object.hasOwn(settings.stage_profiles, stage.id))
|
|
52
|
+
fail("Every declared stage must have an explicit frozen route");
|
|
53
|
+
}
|
|
54
|
+
scopedReceipts(settings, scope, receipts);
|
|
55
|
+
}
|
|
56
|
+
/** Separate from SDLC settings: no plan review, merge, bootstrap command or stop target. */
|
|
57
|
+
export class FrozenGenericExecution {
|
|
58
|
+
sha256;
|
|
59
|
+
settings;
|
|
60
|
+
#snapshot;
|
|
61
|
+
#flow;
|
|
62
|
+
constructor(snapshot, flow) {
|
|
63
|
+
freeze(snapshot);
|
|
64
|
+
this.#snapshot = snapshot;
|
|
65
|
+
this.#flow = flow;
|
|
66
|
+
this.settings = snapshot.settings;
|
|
67
|
+
this.sha256 = hybridDigest(snapshot);
|
|
68
|
+
Object.freeze(this);
|
|
69
|
+
}
|
|
70
|
+
/** Pure selection from one admitted operator snapshot; never reread it during a RUN. */
|
|
71
|
+
static prepare(flow, operatorSettings, selection, scope, receipts = new Map()) {
|
|
72
|
+
const issues = validateHybridContract("profile-settings", operatorSettings);
|
|
73
|
+
if (issues.length)
|
|
74
|
+
fail("Operator profiles cannot be frozen", issues);
|
|
75
|
+
if (!selection || typeof selection !== "object" || Array.isArray(selection)
|
|
76
|
+
|| Object.keys(selection).some(key => !["rootProfile", "stageProfiles", "workProfiles", "execution", "stageSessionMode", "attachedSessionReceipt", "delegation"].includes(key))) {
|
|
77
|
+
fail("Unsupported execution selection");
|
|
78
|
+
}
|
|
79
|
+
if (!["mb-init", "mb-upgrade"].includes(flow.descriptor.id)
|
|
80
|
+
&& (selection.execution === undefined || selection.stageSessionMode === undefined || selection.delegation === undefined)) {
|
|
81
|
+
fail("Non-memory flows must supply their own execution defaults explicitly");
|
|
82
|
+
}
|
|
83
|
+
let rootSelection = selection.rootProfile;
|
|
84
|
+
if (rootSelection !== undefined && typeof rootSelection !== "string") {
|
|
85
|
+
const profile = prepareOperatorProfile(rootSelection);
|
|
86
|
+
const existing = operatorSettings.profiles.find(entry => entry.id === profile.id);
|
|
87
|
+
if (Object.hasOwn(operatorSettings.aliases, profile.id) || existing && existing.revision !== hybridDigest(profile))
|
|
88
|
+
fail("Operation-only profile conflicts with an existing ID or alias");
|
|
89
|
+
const { revision: _revision, ...content } = structuredClone(operatorSettings);
|
|
90
|
+
void _revision;
|
|
91
|
+
if (!existing)
|
|
92
|
+
content.profiles.push({ id: profile.id, revision: hybridDigest(profile), profile });
|
|
93
|
+
operatorSettings = { ...content, revision: hybridDigest(content) };
|
|
94
|
+
rootSelection = profile.id;
|
|
95
|
+
}
|
|
96
|
+
const profiles = new Map();
|
|
97
|
+
const resolve = (id) => {
|
|
98
|
+
const profile = prepareOperatorProfile(resolveOperatorProfile(operatorSettings, id));
|
|
99
|
+
profiles.set(profile.id, { id: profile.id, revision: hybridDigest(profile), profile });
|
|
100
|
+
return profile.id;
|
|
101
|
+
};
|
|
102
|
+
const root = resolve(rootSelection);
|
|
103
|
+
const stages = Object.create(null);
|
|
104
|
+
if (selection.stageProfiles !== undefined && (!selection.stageProfiles || typeof selection.stageProfiles !== "object" || Array.isArray(selection.stageProfiles))) {
|
|
105
|
+
fail("Stage profile selection must be an object");
|
|
106
|
+
}
|
|
107
|
+
for (const id of Object.keys(selection.stageProfiles ?? {}))
|
|
108
|
+
flow.stage(id);
|
|
109
|
+
for (const stage of flow.descriptor.stages) {
|
|
110
|
+
const selected = selection.stageProfiles && Object.hasOwn(selection.stageProfiles, stage.id) ? selection.stageProfiles[stage.id] : undefined;
|
|
111
|
+
if (selected !== undefined && typeof selected !== "string")
|
|
112
|
+
fail("Stage profile selection must name a profile");
|
|
113
|
+
stages[stage.id] = selected === undefined ? root : resolve(selected);
|
|
114
|
+
}
|
|
115
|
+
if (selection.workProfiles !== undefined && (!Array.isArray(selection.workProfiles) || selection.workProfiles.some(id => typeof id !== "string"))) {
|
|
116
|
+
fail("Approved Work profiles must be a list of profile IDs");
|
|
117
|
+
}
|
|
118
|
+
const works = [...new Set((selection.workProfiles ?? []).map(id => resolve(id)))];
|
|
119
|
+
const delegation = selection.delegation;
|
|
120
|
+
if (delegation !== undefined && (!delegation || typeof delegation !== "object" || Array.isArray(delegation)
|
|
121
|
+
|| Object.keys(delegation).some(key => !["mode", "max_parallel", "nativeCapacityReceipt"].includes(key))))
|
|
122
|
+
fail("Unsupported delegation selection");
|
|
123
|
+
const settings = {
|
|
124
|
+
schema_id: "dd-flow/execution-settings@1", flow_id: flow.descriptor.id, descriptor_sha256: flow.sha256,
|
|
125
|
+
execution: selection.execution ?? "managed", attached_session_receipt: selection.attachedSessionReceipt ?? null,
|
|
126
|
+
stage_session_mode: selection.stageSessionMode ?? "new_session",
|
|
127
|
+
delegation: { mode: delegation?.mode ?? "external", max_parallel: delegation?.max_parallel ?? 1, native_capacity_receipt: delegation?.nativeCapacityReceipt ?? null },
|
|
128
|
+
profiles: [...profiles.values()].map(({ id, revision }) => ({ id, revision })), root_profile: root,
|
|
129
|
+
stage_profiles: stages, work_profiles: works
|
|
130
|
+
};
|
|
131
|
+
const snapshot = structuredClone({ schema_id: "dd-flow/frozen-execution@1", scope, settings, profiles: [...profiles.values()] });
|
|
132
|
+
validateSnapshot(snapshot, flow, scope, receipts);
|
|
133
|
+
return new FrozenGenericExecution(snapshot, flow);
|
|
134
|
+
}
|
|
135
|
+
/** Admission uses retained bytes and trusted scope/receipts, never the current operator settings. */
|
|
136
|
+
static restore(snapshot, expectedSha256, flow, scope, receipts = new Map()) {
|
|
137
|
+
if (!/^[a-f0-9]{64}$/.test(expectedSha256) || hybridDigest(snapshot) !== expectedSha256)
|
|
138
|
+
fail("Frozen execution identity changed");
|
|
139
|
+
const retained = structuredClone(snapshot);
|
|
140
|
+
validateSnapshot(retained, flow, scope, receipts);
|
|
141
|
+
return new FrozenGenericExecution(retained, flow);
|
|
142
|
+
}
|
|
143
|
+
snapshot() { return structuredClone(this.#snapshot); }
|
|
144
|
+
stage(id) {
|
|
145
|
+
this.#flow.stage(id);
|
|
146
|
+
return { profile: this.profile(this.settings.stage_profiles[id]), session_mode: this.settings.stage_session_mode };
|
|
147
|
+
}
|
|
148
|
+
work(stage, profileId) {
|
|
149
|
+
const coordinator = this.stage(stage).profile;
|
|
150
|
+
const id = profileId ?? coordinator.id;
|
|
151
|
+
if (id !== coordinator.id && !this.settings.work_profiles.includes(id))
|
|
152
|
+
fail("Work profile is not approved for this RUN");
|
|
153
|
+
const worker = this.profile(id);
|
|
154
|
+
if (this.settings.delegation.mode === "native" && !sameSessionProfile(coordinator, worker))
|
|
155
|
+
fail("Native Work cannot change execution profile");
|
|
156
|
+
return { coordinator, worker, delegation: structuredClone(this.settings.delegation) };
|
|
157
|
+
}
|
|
158
|
+
profile(id) {
|
|
159
|
+
const value = this.#snapshot.profiles.find(entry => entry.id === id);
|
|
160
|
+
if (!value)
|
|
161
|
+
fail("Profile is absent from the frozen execution inventory");
|
|
162
|
+
return structuredClone(value.profile);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -10,9 +10,35 @@ import { canonicalNativeOperation, nativeOperationWait } from "../harness-runtim
|
|
|
10
10
|
import { readOperationProgress, readOperationRecord, readOperationWaiting, suspendOperationClock } from "../harness-runtime/lib/operation-progress.mjs";
|
|
11
11
|
import { recoverDaemonReply } from "../harness-runtime/lib/daemon-observation.mjs";
|
|
12
12
|
import { isObservationLoss } from "../harness-runtime/lib/operation-errors.mjs";
|
|
13
|
+
import { readBoundedReleaseFile } from "./release-source.js";
|
|
13
14
|
const maxAdapterOutputBytes = 1_000_000;
|
|
14
15
|
/** Native hooks need a real executable, not a PATH-dependent shell expression. */
|
|
15
|
-
export function prepareHarnessFlowExecutable(context, stateDir) {
|
|
16
|
+
export function prepareHarnessFlowExecutable(context, stateDir, engine) {
|
|
17
|
+
// Native login shells rewrite search paths. The owned SDK must execute with
|
|
18
|
+
// the same original paths as its controller, not merely hash them as equal.
|
|
19
|
+
const searchPaths = ["PATH", "FPATH", "INFOPATH"].map(name => context.env[name] === undefined
|
|
20
|
+
? `delete process.env.${name};\n` : `process.env.${name} = ${JSON.stringify(context.env[name])};\n`).join("");
|
|
21
|
+
if (engine) {
|
|
22
|
+
const executable = path.join(stateDir, "dd-flow");
|
|
23
|
+
const contents = `#!${engine.nodeExecutable}\n${searchPaths}process.env.DD_FLOW_HOME = ${JSON.stringify(context.ddFlowHome)};\nprocess.env.DD_FLOW_ENGINE_MODE = "1";\nprocess.env.DD_FLOW_ENGINE_HOME = ${JSON.stringify(engine.manifest().snapshot_root)};\nconst { spawnSync } = await import("node:child_process");\nconst child = spawnSync(${JSON.stringify(engine.nodeExecutable)}, [${JSON.stringify(engine.entrypoint)}, ...process.argv.slice(2)], { stdio: "inherit", env: process.env });\nif (child.error) throw child.error;\nif (child.signal) process.kill(process.pid, child.signal);\nprocess.exit(child.status ?? 1);\n`;
|
|
24
|
+
try {
|
|
25
|
+
fs.writeFileSync(executable, contents, { flag: "wx", mode: 0o700 });
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
const conflict = () => new AppError("harness_runtime_binding_conflict", "Launch runtime executable already has different or linked content", 1, { path: executable });
|
|
29
|
+
if (error.code !== "EEXIST")
|
|
30
|
+
throw conflict();
|
|
31
|
+
try {
|
|
32
|
+
const stat = fs.lstatSync(executable);
|
|
33
|
+
if (!stat.isFile() || stat.nlink !== 1 || readBoundedReleaseFile(executable, 64 * 1024).toString("utf8") !== contents)
|
|
34
|
+
throw conflict();
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
throw conflict();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return executable;
|
|
41
|
+
}
|
|
16
42
|
const configured = context.env.DD_FLOW_BIN;
|
|
17
43
|
if (configured !== undefined) {
|
|
18
44
|
if (!path.isAbsolute(configured) || !fs.existsSync(configured))
|
|
@@ -32,7 +58,7 @@ export function prepareHarnessFlowExecutable(context, stateDir) {
|
|
|
32
58
|
const dispatch = configured
|
|
33
59
|
? `const { spawnSync } = await import("node:child_process");\nconst child = spawnSync(${JSON.stringify(configured)}, process.argv.slice(2), { stdio: "inherit", env: process.env });\nif (child.error) throw child.error;\nif (child.signal) process.kill(process.pid, child.signal);\nprocess.exit(child.status ?? 1);\n`
|
|
34
60
|
: `await import(${JSON.stringify(pathToFileURL(entry).href)});\n`;
|
|
35
|
-
const contents = `#!${process.execPath}\
|
|
61
|
+
const contents = `#!${process.execPath}\n${searchPaths}process.env.DD_FLOW_HOME = ${JSON.stringify(context.ddFlowHome)};\n${dispatch}`;
|
|
36
62
|
try {
|
|
37
63
|
fs.writeFileSync(executable, contents, { flag: "wx", mode: 0o700 });
|
|
38
64
|
}
|
package/dist/services/hooks.js
CHANGED
|
@@ -9,6 +9,8 @@ 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-repair-physical-binding.js";
|
|
13
|
+
import { memoryNativeReviewAssignedHookCwd } from "./memory-native-review-copy.js";
|
|
12
14
|
import { prepareTextFile, prepareJsonText, prepareError } from "../cli/input-preparation.js";
|
|
13
15
|
import { nativeSessionIdentity, storageSessionId } from "./session-identity.js";
|
|
14
16
|
import { assertRecoveryHookCurrent } from "./run-recovery.js";
|
|
@@ -714,6 +716,11 @@ export function handleDroidEvent(context, input) {
|
|
|
714
716
|
return handleControlledToolEvent(context, input, event, "droid-cli");
|
|
715
717
|
}
|
|
716
718
|
/** Stable project identity is not the native Session's execution workspace. */
|
|
719
|
+
function requiresAssignedLifecycleCwd(facts) {
|
|
720
|
+
// Settlement and user interaction address the retained RUN explicitly;
|
|
721
|
+
// their tools/checks receive owned paths, not the shell's working directory.
|
|
722
|
+
return !facts || !["stage_finish", "stage_pause", "stage_resume", "work_finish", "work_fail"].includes(facts.invocation.operation);
|
|
723
|
+
}
|
|
717
724
|
function assignedLifecycleCwd(context, projectRoot, facts, providerSessionId, command, observedCwd, identity, harness) {
|
|
718
725
|
const auxiliary = continuationCommand(command);
|
|
719
726
|
if ((!facts && !auxiliary) || facts?.analysis.supported === false)
|
|
@@ -723,7 +730,9 @@ function assignedLifecycleCwd(context, projectRoot, facts, providerSessionId, co
|
|
|
723
730
|
const issuedRunId = facts && !facts.runId && !facts.workId && !facts.bootstrapStageStart && constraint ? observedLifecycleInvocation(context, command, constraint, identity ? { identity } : undefined)?.scope.runId : null;
|
|
724
731
|
if (facts?.invocation.operation.startsWith("stage_") && !facts.bootstrapStageStart && !facts.runId && !issuedRunId)
|
|
725
732
|
throw new AppError("invocation_assignment_missing", "Short lifecycle call has no unique issued RUN assignment; its workspace cannot be inferred", 1, { phase: "prepare", effect: "no_effect", recoverable: false });
|
|
726
|
-
|
|
733
|
+
if (!requiresAssignedLifecycleCwd(facts))
|
|
734
|
+
return undefined;
|
|
735
|
+
const expected = controlledLifecycleCwd(context, projectRoot, facts, providerSessionId, auxiliary, issuedRunId, observedCwd);
|
|
727
736
|
// Unknown CLI targets have no assigned workspace. Recording their native
|
|
728
737
|
// observation must not turn the stable identity root into cwd authority.
|
|
729
738
|
if (!expected)
|
|
@@ -737,7 +746,7 @@ function assignedLifecycleCwd(context, projectRoot, facts, providerSessionId, co
|
|
|
737
746
|
// physical process directory, rather than inventing native observation.
|
|
738
747
|
return expected;
|
|
739
748
|
}
|
|
740
|
-
function controlledLifecycleCwd(context, projectRoot, facts, providerSessionId, auxiliary, issuedRunId) {
|
|
749
|
+
function controlledLifecycleCwd(context, projectRoot, facts, providerSessionId, auxiliary, issuedRunId, observedCwd) {
|
|
741
750
|
if (facts?.bootstrapStageStart)
|
|
742
751
|
return projectRoot;
|
|
743
752
|
const project = context.db.get("SELECT id FROM projects WHERE root = ?", [projectRoot]);
|
|
@@ -768,6 +777,14 @@ function controlledLifecycleCwd(context, projectRoot, facts, providerSessionId,
|
|
|
768
777
|
if (!run)
|
|
769
778
|
return undefined; // Hook ingress does not validate a mistyped CLI target.
|
|
770
779
|
if (work) {
|
|
780
|
+
const repair = memoryRepairAssignedHookCwd(context, { projectRoot, projectId: project.id, runId: run.id,
|
|
781
|
+
workId: work.work_id, providerSessionId, ...(observedCwd ? { observedCwd: resolveProjectRoot(observedCwd) } : {}),
|
|
782
|
+
...(facts?.invocation.expectedCwd ? { literalExecutionCwd: resolveProjectRoot(facts.invocation.expectedCwd) } : {}) });
|
|
783
|
+
if (repair)
|
|
784
|
+
return resolveProjectRoot(repair.root);
|
|
785
|
+
const nativeReview = memoryNativeReviewAssignedHookCwd(context, { projectId: project.id, runId: run.id, workId: work.work_id, providerSessionId });
|
|
786
|
+
if (nativeReview)
|
|
787
|
+
return resolveProjectRoot(nativeReview);
|
|
771
788
|
// PROTOCOLIZE provisions the feature checkout after its coordinator
|
|
772
789
|
// Session has launched in the stable project root. Its transition Work
|
|
773
790
|
// must keep that physical route until the Stage is complete. Child Works
|
|
@@ -862,12 +879,13 @@ function handleControlledToolEvent(context, input, event, harness) {
|
|
|
862
879
|
}
|
|
863
880
|
// These controlled platforms additionally qualify their native launch
|
|
864
881
|
// route against the stable project root when no CLI target is assigned.
|
|
865
|
-
const
|
|
866
|
-
const
|
|
882
|
+
const workspaceBound = requiresAssignedLifecycleCwd(lifecycle);
|
|
883
|
+
const expectedCwd = shellRefusal || !workspaceBound ? observedCwd : controlledLifecycleCwd(context, expectedRoot, lifecycle, providerSessionId, auxiliary, issuedRunId, observedCwd) ?? expectedRoot;
|
|
884
|
+
const executionRoute = shellRefusal || !workspaceBound ? undefined : lifecycle?.invocation.expectedCwd ?? auxiliary?.expectedCwd;
|
|
867
885
|
if (executionRoute && resolveProjectRoot(executionRoute) !== expectedCwd) {
|
|
868
886
|
throw new AppError(`${errorPrefix}_directory_mismatch`, "Literal shell cd route does not match the assigned lifecycle workspace", 1, { expected: expectedCwd, actual: executionRoute, actual_provenance: "shell_literal_route", effect: "no_effect", recoverable: false });
|
|
869
887
|
}
|
|
870
|
-
if (!executionRoute && observedCwd !== expectedCwd) {
|
|
888
|
+
if (workspaceBound && !executionRoute && observedCwd !== expectedCwd) {
|
|
871
889
|
throw new AppError(`${errorPrefix}_directory_mismatch`, `${label} ${agy ? "launch route" : "Session directory"} does not match the assigned execution workspace`, 1, { expected: expectedCwd, actual: observedCwd, effect: "no_effect", recoverable: false, ...(agy ? { actual_provenance: stringValue(event.cwd_provenance) ?? "unknown" } : {}) });
|
|
872
890
|
}
|
|
873
891
|
const bootstrapStageStart = !shellRefusal && lifecycle?.bootstrapStageStart;
|
|
@@ -906,7 +924,7 @@ function handleControlledToolEvent(context, input, event, harness) {
|
|
|
906
924
|
let inserted = false;
|
|
907
925
|
const receipt = observeLifecycleInvocation(context, {
|
|
908
926
|
currentEventKey: baseKey,
|
|
909
|
-
command, harness, projectId: project.id, ...(!shellRefusal ? { executionCwd: expectedCwd } : {}), identity: { daemonId, rootSessionId: nativeParentId ?? providerSessionId, sessionId: providerSessionId, parentSessionId: nativeParentId ?? null, toolCallId: toolCallId },
|
|
927
|
+
command, harness, projectId: project.id, ...(!shellRefusal && workspaceBound ? { executionCwd: expectedCwd } : {}), identity: { daemonId, rootSessionId: nativeParentId ?? providerSessionId, sessionId: providerSessionId, parentSessionId: nativeParentId ?? null, toolCallId: toolCallId },
|
|
910
928
|
recordReceipt: scope => { inserted = persist(scope); return baseKey; }
|
|
911
929
|
});
|
|
912
930
|
const admitted = { ...result(inserted), ...lifecycleObservationResult(receipt), ...(receipt.invocationId ? { invocation_id: receipt.invocationId } : {}) };
|
|
@@ -197,7 +197,8 @@ export function validateHybridContract(name, input, context = {}) {
|
|
|
197
197
|
if (delegation.native_capacity_receipt !== null) {
|
|
198
198
|
const fact = context.receipts?.get(string(record(delegation.native_capacity_receipt).sha256));
|
|
199
199
|
if (!fact || !root || fact.harness !== record(root.profile).harness || fact.profileRevision !== root.revision
|
|
200
|
-
|| !Number.isInteger(fact.capacity) || fact.capacity <
|
|
200
|
+
|| !Number.isInteger(fact.capacity) || fact.capacity < 1 || fact.capacity > 15
|
|
201
|
+
|| fact.capacity < Number(delegation.max_parallel))
|
|
201
202
|
fail("native_capacity_scope_mismatch", "/delegation");
|
|
202
203
|
}
|
|
203
204
|
}
|
|
@@ -213,6 +214,14 @@ export function validateHybridContract(name, input, context = {}) {
|
|
|
213
214
|
const workspace = record(data.workspace), execution = record(data.effective_execution), storage = record(data.storage);
|
|
214
215
|
if (data.request_id !== expected.requestId || data.project_root !== expected.projectRoot || workspace.path !== expected.workspace || data.baseline_sha256 !== expected.baselineSha256)
|
|
215
216
|
fail("foreign_bootstrap_identity");
|
|
217
|
+
if (data.schema_id === "dd-flow/memory-bootstrap-receipt@2") {
|
|
218
|
+
const git = expected.workspaceGit;
|
|
219
|
+
if (!git)
|
|
220
|
+
fail("missing_bootstrap_git_observation");
|
|
221
|
+
else if (workspace.repository !== git.repository || workspace.branch !== git.branch || workspace.commit !== git.commit
|
|
222
|
+
|| workspace.git_state !== (git.commit === null ? "unborn" : "committed"))
|
|
223
|
+
fail("foreign_bootstrap_git_state");
|
|
224
|
+
}
|
|
216
225
|
if (execution.engine_sha256 !== expected.engineSha256 || execution.canon_sha256 !== expected.canonSha256)
|
|
217
226
|
fail("foreign_execution_identity");
|
|
218
227
|
receipt(storage.compatibility_receipt, "storage-compatibility", true);
|
|
@@ -289,6 +298,16 @@ export function validateHybridContract(name, input, context = {}) {
|
|
|
289
298
|
}
|
|
290
299
|
};
|
|
291
300
|
const descriptor = loaded(data.descriptor), settings = loaded(data.execution_settings), profiles = loaded(data.profile_settings);
|
|
301
|
+
// Execution receipts are evidence references, not just trusted facts. They
|
|
302
|
+
// must survive reopening the RUN inside the same frozen blob inventory.
|
|
303
|
+
for (const reference of [record(settings).attached_session_receipt,
|
|
304
|
+
record(record(settings).delegation).native_capacity_receipt]) {
|
|
305
|
+
if (reference === null || reference === undefined)
|
|
306
|
+
continue;
|
|
307
|
+
const ref = record(reference);
|
|
308
|
+
if (inventory.get(string(ref.path)) !== ref.sha256)
|
|
309
|
+
fail("unlisted_bundle_reference");
|
|
310
|
+
}
|
|
292
311
|
const binding = loaded(data.engine_binding);
|
|
293
312
|
const bindingSchema = JSON.parse(fs.readFileSync(new URL("../schemas/run-engine-binding.schema.json", import.meta.url), "utf8"));
|
|
294
313
|
const bindingValidator = new Ajv({ strict: false, validateFormats: false }).compile(bindingSchema);
|
|
@@ -342,7 +361,9 @@ export function validateHybridShape(name, input) {
|
|
|
342
361
|
if (!hybridContractRegistry.has(name)) {
|
|
343
362
|
return [{ gate: "shape", code: "unknown_contract_name", path: "/" }];
|
|
344
363
|
}
|
|
345
|
-
const
|
|
364
|
+
const versionedName = name === "memory-bootstrap-receipt" && input !== null && typeof input === "object"
|
|
365
|
+
&& input.schema_id === "dd-flow/memory-bootstrap-receipt@2" ? "memory-bootstrap-receipt-v2" : name;
|
|
366
|
+
const schemaUrl = new URL(`../schemas/${versionedName}.schema.json`, import.meta.url);
|
|
346
367
|
const schema = JSON.parse(fs.readFileSync(schemaUrl, "utf8"));
|
|
347
368
|
const ajv = new Ajv2020({ allErrors: true, strict: true, validateFormats: false });
|
|
348
369
|
const validate = ajv.compile(schema);
|
|
@@ -388,7 +409,14 @@ export function validateFlowDescriptor(input, context) {
|
|
|
388
409
|
if (field === "result_schema") {
|
|
389
410
|
try {
|
|
390
411
|
const schema = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
391
|
-
|
|
412
|
+
const draft = schema.$schema;
|
|
413
|
+
// Existing SDLC schemas require parent properties inside conditional
|
|
414
|
+
// subschemas. Keep all other strict checks without rejecting those
|
|
415
|
+
// valid JSON Schema contracts through Ajv's strictRequired heuristic.
|
|
416
|
+
const ajv = draft === "http://json-schema.org/draft-07/schema#"
|
|
417
|
+
? new Ajv({ strict: true, strictRequired: false, validateFormats: false })
|
|
418
|
+
: new Ajv2020({ strict: true, strictRequired: false, validateFormats: false });
|
|
419
|
+
ajv.compile(schema);
|
|
392
420
|
}
|
|
393
421
|
catch {
|
|
394
422
|
fail("invalid_result_schema", `/stages/${index}/${field}`);
|
package/dist/services/ids.js
CHANGED
|
@@ -77,9 +77,37 @@ function parseEntityKind(value) {
|
|
|
77
77
|
function databaseIds(context, table, idField, type, projectId) {
|
|
78
78
|
if (!projectId)
|
|
79
79
|
return [];
|
|
80
|
-
|
|
80
|
+
const ids = context.db
|
|
81
81
|
.all(`SELECT ${idField} AS id FROM ${table} WHERE project_id = ? AND ${idField} LIKE ?`, [projectId, `${type}-%`])
|
|
82
82
|
.map((row) => row.id);
|
|
83
|
+
if (type === "RUN") {
|
|
84
|
+
for (const row of context.db.all("SELECT json_extract(payload_json,'$.run_id') AS id FROM audit_events WHERE project_id=? AND event_type IN ('memory.init_run_allocation_intent','memory.init_run_allocation')", [projectId])) {
|
|
85
|
+
try {
|
|
86
|
+
if (typeof row.id !== "string")
|
|
87
|
+
throw new Error("Invalid reserved RUN id");
|
|
88
|
+
const parsed = parseFullEntityId(row.id), sequence = Number(parsed.shortId.slice(4));
|
|
89
|
+
if (parsed.type !== "RUN" || !Number.isSafeInteger(sequence) || sequence < 1)
|
|
90
|
+
throw new Error("Invalid reserved RUN id");
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
throw new AppError("memory_init_run_allocation_invalid", "Retained RUN reservation identity is invalid", 1, { effect: "no_effect" });
|
|
94
|
+
}
|
|
95
|
+
ids.push(row.id);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return ids;
|
|
99
|
+
}
|
|
100
|
+
/** Includes retained request reservations, even before their RUN row exists. */
|
|
101
|
+
export function nextRunId(context, projectId, slug) {
|
|
102
|
+
const used = new Set();
|
|
103
|
+
for (const id of databaseIds(context, "runs", "id", "RUN", projectId))
|
|
104
|
+
addSequence(used, id, "RUN");
|
|
105
|
+
let max = 0;
|
|
106
|
+
for (const sequence of used)
|
|
107
|
+
max = Math.max(max, sequence);
|
|
108
|
+
if (!Number.isSafeInteger(max + 1))
|
|
109
|
+
throw new AppError("validation", "RUN sequence cannot be represented safely", 1, { effect: "no_effect" });
|
|
110
|
+
return formatFullId("RUN", max + 1, normalizeSlug(slug));
|
|
83
111
|
}
|
|
84
112
|
function filesystemIds(projectRoot, relativeRoot, type) {
|
|
85
113
|
const root = path.join(projectRoot, relativeRoot);
|
|
@@ -48,10 +48,13 @@ export function acquireInstallationLock(file, identity) {
|
|
|
48
48
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
49
49
|
try {
|
|
50
50
|
fs.linkSync(temporary, file);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
const assertOwnership = () => {
|
|
52
|
+
const retained = readOwner(file);
|
|
53
|
+
if (retained.token !== owner.token || retained.pid !== process.pid || retained.identity !== identity)
|
|
54
|
+
refuse("lock_owner_changed");
|
|
55
|
+
};
|
|
56
|
+
return { token: owner.token, assertOwnership, release: () => {
|
|
57
|
+
assertOwnership();
|
|
55
58
|
fs.unlinkSync(file);
|
|
56
59
|
} };
|
|
57
60
|
}
|
|
@@ -0,0 +1,183 @@
|
|
|
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 { readSetupStatus } from "./setup.js";
|
|
6
|
+
import { readBoundedReleaseFile } from "./release-source.js";
|
|
7
|
+
import { hybridDigest, validateHybridShape } from "./hybrid-contracts.js";
|
|
8
|
+
import { admitBundledFlow } from "./stage-handlers.js";
|
|
9
|
+
const limit = 1024 * 1024;
|
|
10
|
+
const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
11
|
+
function refuse(reason) {
|
|
12
|
+
throw new AppError("installed_flow_source_invalid", "Installed flow source is not the selected verified canon", 1, { reason, phase: "prepare", effect: "no_effect" });
|
|
13
|
+
}
|
|
14
|
+
function directory(target) {
|
|
15
|
+
if (!fs.lstatSync(target).isDirectory())
|
|
16
|
+
refuse("unsafe_source_directory");
|
|
17
|
+
}
|
|
18
|
+
function containedFile(root, logical) {
|
|
19
|
+
if (!/^[A-Za-z0-9_-][A-Za-z0-9_.-]*(\/[A-Za-z0-9_-][A-Za-z0-9_.-]*)*$/.test(logical)
|
|
20
|
+
|| logical.split("/").some(part => part === "." || part === ".."))
|
|
21
|
+
refuse("unsafe_source_path");
|
|
22
|
+
let current = root;
|
|
23
|
+
for (const part of logical.split("/").slice(0, -1)) {
|
|
24
|
+
current = path.join(current, part);
|
|
25
|
+
directory(current);
|
|
26
|
+
}
|
|
27
|
+
return path.join(root, logical);
|
|
28
|
+
}
|
|
29
|
+
/** Initial RUN materialization input, not handler/engine/storage admission.
|
|
30
|
+
* Built-in memory flows live in the global canon, not the curated project pack.
|
|
31
|
+
* Existing RUNs must restore their retained bundle rather than call this reader.
|
|
32
|
+
*/
|
|
33
|
+
export class PreparedInstalledFlowSource {
|
|
34
|
+
sha256;
|
|
35
|
+
#home;
|
|
36
|
+
#installationSha256;
|
|
37
|
+
#flowId;
|
|
38
|
+
#descriptor;
|
|
39
|
+
#canon;
|
|
40
|
+
#blobs;
|
|
41
|
+
constructor(home, installationSha256, flowId, descriptor, canon, blobs) {
|
|
42
|
+
this.#home = home;
|
|
43
|
+
this.#installationSha256 = installationSha256;
|
|
44
|
+
this.#flowId = flowId;
|
|
45
|
+
this.#descriptor = descriptor;
|
|
46
|
+
this.#canon = canon;
|
|
47
|
+
this.#blobs = blobs;
|
|
48
|
+
this.sha256 = hybridDigest({ installation_sha256: installationSha256, canon, inventory: this.inventory() });
|
|
49
|
+
Object.freeze(this);
|
|
50
|
+
}
|
|
51
|
+
static fromInstalledRelease(home, expectedInstallationSha256, flowId) {
|
|
52
|
+
try {
|
|
53
|
+
if (!path.isAbsolute(home) || !/^[a-f0-9]{64}$/.test(expectedInstallationSha256) || flowId !== "mb-init")
|
|
54
|
+
refuse("invalid_selection");
|
|
55
|
+
const selected = readSetupStatus(home);
|
|
56
|
+
if (!selected.installation || selected.installation_readiness !== "installed")
|
|
57
|
+
refuse("installed_release_unavailable");
|
|
58
|
+
const installation = selected.installation;
|
|
59
|
+
if (installation.manifest_sha256 !== expectedInstallationSha256)
|
|
60
|
+
refuse("installation_selection_changed");
|
|
61
|
+
directory(installation.canon_root);
|
|
62
|
+
const roots = fs.readdirSync(installation.canon_root);
|
|
63
|
+
if (roots.length !== 1)
|
|
64
|
+
refuse("ambiguous_archive_root");
|
|
65
|
+
const canonRoot = path.join(installation.canon_root, roots[0]);
|
|
66
|
+
directory(canonRoot);
|
|
67
|
+
const dotRoot = path.join(canonRoot, ".memory-bank"), memoryRoot = fs.existsSync(dotRoot) ? dotRoot : canonRoot;
|
|
68
|
+
directory(memoryRoot);
|
|
69
|
+
const flowRoot = path.join(memoryRoot, "dd-flow");
|
|
70
|
+
directory(flowRoot);
|
|
71
|
+
const descriptorPath = `${flowId}/flow-descriptor.json`;
|
|
72
|
+
const bytes = readBoundedReleaseFile(containedFile(flowRoot, descriptorPath), limit);
|
|
73
|
+
const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
74
|
+
if (validateHybridShape("flow-descriptor", value).length || value.id !== flowId || value.origin !== "canon"
|
|
75
|
+
|| value.stages.length > 64)
|
|
76
|
+
refuse("invalid_descriptor");
|
|
77
|
+
const blobs = new Map([[descriptorPath, Buffer.from(bytes)]]), folded = new Map([[descriptorPath.toLowerCase(), descriptorPath]]);
|
|
78
|
+
let total = bytes.length;
|
|
79
|
+
for (const stage of value.stages)
|
|
80
|
+
for (const kind of ["instruction", "result_schema"]) {
|
|
81
|
+
const reference = stage[kind], existing = blobs.get(reference.path);
|
|
82
|
+
if (existing) {
|
|
83
|
+
if (hash(existing) !== reference.sha256)
|
|
84
|
+
refuse("conflicting_source_reference");
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (folded.has(reference.path.toLowerCase()))
|
|
88
|
+
refuse("conflicting_source_path");
|
|
89
|
+
const content = readBoundedReleaseFile(containedFile(flowRoot, reference.path), limit);
|
|
90
|
+
if (!content.length || (total += content.length) > 32 * limit || hash(content) !== reference.sha256)
|
|
91
|
+
refuse("source_bytes_changed");
|
|
92
|
+
new TextDecoder("utf-8", { fatal: true }).decode(content);
|
|
93
|
+
if (kind === "result_schema")
|
|
94
|
+
JSON.parse(content.toString("utf8"));
|
|
95
|
+
folded.set(reference.path.toLowerCase(), reference.path);
|
|
96
|
+
blobs.set(reference.path, Buffer.from(content));
|
|
97
|
+
}
|
|
98
|
+
// Verify compressed/expanded archive identity again after collecting bytes.
|
|
99
|
+
// A coherent descriptor+prompt edit cannot become a new trusted selection.
|
|
100
|
+
const final = readSetupStatus(home);
|
|
101
|
+
if (final.installation_readiness !== "installed" || hybridDigest(final.installation) !== hybridDigest(installation))
|
|
102
|
+
refuse("source_changed_during_preparation");
|
|
103
|
+
return new PreparedInstalledFlowSource(home, expectedInstallationSha256, flowId, { path: descriptorPath, sha256: hash(bytes) }, { version: installation.manifest.canon.version, commit: installation.manifest.canon.commit, archive_sha256: installation.manifest.canon.archive.sha256 }, blobs);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
if (error instanceof AppError && error.code === "installed_flow_source_invalid")
|
|
107
|
+
throw error;
|
|
108
|
+
return refuse("source_unavailable_or_changed");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
descriptorReference() { return { ...this.#descriptor }; }
|
|
112
|
+
installationSha256() { return this.#installationSha256; }
|
|
113
|
+
snapshot() {
|
|
114
|
+
return { schema_id: "dd-flow/installed-flow-source@1", installation_sha256: this.#installationSha256,
|
|
115
|
+
descriptor: this.descriptorReference(), canon: this.canonProvenance(),
|
|
116
|
+
blobs: [...this.#blobs].map(([logical, bytes]) => ({ path: logical, base64: bytes.toString("base64") })) };
|
|
117
|
+
}
|
|
118
|
+
/** Copied source restoration only. expectedSha256 must come from independent
|
|
119
|
+
* request custody, not from the same input. This does not verify a new release
|
|
120
|
+
* or admit its engine/storage; those remain the bootstrap owner's duties.
|
|
121
|
+
*/
|
|
122
|
+
static restoreRetained(input, expectedSha256) {
|
|
123
|
+
try {
|
|
124
|
+
if (!input || typeof input !== "object" || Array.isArray(input) || typeof expectedSha256 !== "string"
|
|
125
|
+
|| !/^[a-f0-9]{64}$/.test(expectedSha256))
|
|
126
|
+
refuse("invalid_retained_source");
|
|
127
|
+
const value = input;
|
|
128
|
+
if (Object.keys(value).sort().join() !== "blobs,canon,descriptor,installation_sha256,schema_id"
|
|
129
|
+
|| value.schema_id !== "dd-flow/installed-flow-source@1" || typeof value.installation_sha256 !== "string" || !/^[a-f0-9]{64}$/.test(value.installation_sha256)
|
|
130
|
+
|| !value.canon || Object.keys(value.canon).sort().join() !== "archive_sha256,commit,version"
|
|
131
|
+
|| typeof value.canon.commit !== "string" || !/^[a-f0-9]{40}$/.test(value.canon.commit)
|
|
132
|
+
|| typeof value.canon.archive_sha256 !== "string" || !/^[a-f0-9]{64}$/.test(value.canon.archive_sha256)
|
|
133
|
+
|| typeof value.canon.version !== "string" || !/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/.test(value.canon.version)
|
|
134
|
+
|| !value.descriptor || Object.keys(value.descriptor).sort().join() !== "path,sha256"
|
|
135
|
+
|| value.descriptor.path !== "mb-init/flow-descriptor.json" || typeof value.descriptor.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(value.descriptor.sha256)
|
|
136
|
+
|| !Array.isArray(value.blobs) || value.blobs.length < 1 || value.blobs.length > 129)
|
|
137
|
+
refuse("invalid_retained_source");
|
|
138
|
+
const blobs = new Map(), folded = new Set();
|
|
139
|
+
let total = 0;
|
|
140
|
+
for (const entry of value.blobs) {
|
|
141
|
+
if (!entry || Object.keys(entry).sort().join() !== "base64,path" || typeof entry.path !== "string"
|
|
142
|
+
|| !/^[A-Za-z0-9_-][A-Za-z0-9_.-]*(\/[A-Za-z0-9_-][A-Za-z0-9_.-]*)*$/.test(entry.path)
|
|
143
|
+
|| entry.path.split("/").some(part => part === "." || part === "..")
|
|
144
|
+
|| folded.has(entry.path.toLowerCase()) || typeof entry.base64 !== "string" || entry.base64.length > 2 * limit)
|
|
145
|
+
refuse("invalid_retained_source");
|
|
146
|
+
const content = Buffer.from(entry.base64, "base64");
|
|
147
|
+
if (!content.length || content.length > limit || content.toString("base64") !== entry.base64
|
|
148
|
+
|| (total += content.length) > 32 * limit)
|
|
149
|
+
refuse("invalid_retained_source");
|
|
150
|
+
new TextDecoder("utf-8", { fatal: true }).decode(content);
|
|
151
|
+
folded.add(entry.path.toLowerCase());
|
|
152
|
+
blobs.set(entry.path, content);
|
|
153
|
+
}
|
|
154
|
+
const descriptor = blobs.get(value.descriptor.path);
|
|
155
|
+
if (!descriptor || hash(descriptor) !== value.descriptor.sha256)
|
|
156
|
+
refuse("retained_source_changed");
|
|
157
|
+
const result = new PreparedInstalledFlowSource(null, value.installation_sha256, "mb-init", { ...value.descriptor }, { ...value.canon }, blobs);
|
|
158
|
+
if (result.sha256 !== expectedSha256)
|
|
159
|
+
refuse("retained_source_changed");
|
|
160
|
+
result.resolveRegisteredFlow();
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
if (error instanceof AppError && error.code === "installed_flow_source_invalid")
|
|
165
|
+
throw error;
|
|
166
|
+
return refuse("invalid_retained_source");
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
canonProvenance() { return { ...this.#canon }; }
|
|
170
|
+
blobs() { return new Map([...this.#blobs].map(([logical, bytes]) => [logical, Buffer.from(bytes)])); }
|
|
171
|
+
inventory() { return [...this.#blobs].map(([logical, bytes]) => ({ path: logical, sha256: hash(bytes) })).sort((a, b) => a.path.localeCompare(b.path, "en")); }
|
|
172
|
+
/** Production handler registry only: source declarations cannot register code. */
|
|
173
|
+
resolveRegisteredFlow() { return admitBundledFlow(this.#blobs.get(this.#descriptor.path), this.#descriptor.sha256, this.#blobs); }
|
|
174
|
+
/** Initial materialization only, outside SQL writers. */
|
|
175
|
+
revalidate() {
|
|
176
|
+
if (this.#home === null) {
|
|
177
|
+
PreparedInstalledFlowSource.restoreRetained(this.snapshot(), this.sha256);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (PreparedInstalledFlowSource.fromInstalledRelease(this.#home, this.#installationSha256, this.#flowId).sha256 !== this.sha256)
|
|
181
|
+
refuse("source_changed_after_preparation");
|
|
182
|
+
}
|
|
183
|
+
}
|