@deksden-com/dd-flow-cli 0.9.0-beta.136 → 0.9.0-beta.137
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 +8 -0
- package/dist/build-info.json +3 -3
- package/dist/harness-runtime/DD-ZCODE.md +13 -0
- package/dist/harness-runtime/bin/dd-opencode.mjs +3 -3
- package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +14 -1
- package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +36 -1
- package/dist/harness-runtime/lib/dd-zcode.mjs +7 -3
- package/dist/harness-runtime/lib/opencode-plugin.mjs +3 -1
- package/dist/services/controller-child-observer.js +99 -0
- package/dist/services/controller-fanout.js +9 -2
- package/dist/services/run-controller.js +83 -8
- package/dist/services/stage-lifecycle.js +2 -1
- package/dist/services/stage-report-renderer.js +13 -0
- package/dist/services/vnext-fanout.js +66 -1
- package/dist/services/vnext-protocolize.js +60 -2
- package/package.json +1 -1
- package/dist/cli/command-metadata.js +0 -144
- package/dist/cli/command-option-descriptions.js +0 -186
- package/dist/cli/completion-cli.js +0 -70
- package/dist/cli/completion.js +0 -57
- package/dist/cli/error-output.js +0 -52
- package/dist/cli/executor-onboarding.js +0 -71
- package/dist/cli/harnesses.js +0 -84
- package/dist/cli/models.js +0 -39
- package/dist/cli/operator-profiles.js +0 -61
- package/dist/cli/output-options.js +0 -35
- package/dist/cli/response-file.js +0 -69
- package/dist/cli/setup.js +0 -147
- package/dist/services/cli-upgrade-allowlist.js +0 -27
- package/dist/services/discovery/agy-discovery.js +0 -2
- package/dist/services/discovery/codex-discovery.js +0 -2
- package/dist/services/discovery/droid-discovery.js +0 -2
- package/dist/services/discovery/grok-discovery.js +0 -2
- package/dist/services/discovery/opencode-discovery.js +0 -2
- package/dist/services/discovery/static.js +0 -31
- package/dist/services/discovery/zcode-discovery.js +0 -2
- package/dist/services/executor-onboarding.js +0 -56
- package/dist/services/harness-discovery.js +0 -29
- package/dist/services/harness-doctor.js +0 -75
- package/dist/services/harness-probe-effects.js +0 -51
- package/dist/services/harness-version-probe.js +0 -100
- package/dist/services/hybrid-contracts.js +0 -455
- package/dist/services/installation-lock.js +0 -92
- package/dist/services/model-catalog.js +0 -109
- package/dist/services/model-discovery.js +0 -46
- package/dist/services/operator-profiles.js +0 -236
- package/dist/services/release-archive-producer.js +0 -52
- package/dist/services/release-archive.js +0 -143
- package/dist/services/release-cache.js +0 -142
- package/dist/services/release-compatibility.js +0 -18
- package/dist/services/release-download.js +0 -67
- package/dist/services/release-envelope.js +0 -104
- package/dist/services/release-manifest.js +0 -45
- package/dist/services/release-payload.js +0 -114
- package/dist/services/release-producer.js +0 -63
- package/dist/services/release-provenance.js +0 -160
- package/dist/services/release-registry.js +0 -66
- package/dist/services/release-source.js +0 -194
- package/dist/services/setup.js +0 -377
|
@@ -1,194 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { createPublicKey } from "node:crypto";
|
|
4
|
-
import { AppError } from "../shared/errors.js";
|
|
5
|
-
import { authenticateReleaseEnvelope, releaseProducerKeyId } from "./release-envelope.js";
|
|
6
|
-
import { verifyReleaseArchive } from "./release-manifest.js";
|
|
7
|
-
import { downloadReleaseArchive } from "./release-download.js";
|
|
8
|
-
function refuse(reason) {
|
|
9
|
-
throw new AppError("release_source", "Release source cannot be read", 1, { reason, phase: "release_source", effect: "no_effect", help: "setup" });
|
|
10
|
-
}
|
|
11
|
-
/** Bounded regular-file reads; paths, parser excerpts and key material never enter diagnostics. */
|
|
12
|
-
export function readBoundedReleaseFile(file, maxBytes) {
|
|
13
|
-
if (!Number.isSafeInteger(maxBytes) || maxBytes < 1)
|
|
14
|
-
refuse("invalid_source_limit");
|
|
15
|
-
let fd;
|
|
16
|
-
try {
|
|
17
|
-
if (!fs.lstatSync(file).isFile())
|
|
18
|
-
refuse("source_not_regular_file");
|
|
19
|
-
fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
|
20
|
-
}
|
|
21
|
-
catch (error) {
|
|
22
|
-
if (error instanceof AppError)
|
|
23
|
-
throw error;
|
|
24
|
-
return refuse("source_unavailable");
|
|
25
|
-
}
|
|
26
|
-
try {
|
|
27
|
-
const stat = fs.fstatSync(fd);
|
|
28
|
-
if (!stat.isFile() || stat.size < 1 || stat.size > maxBytes)
|
|
29
|
-
refuse("source_size_outside_limit");
|
|
30
|
-
const bytes = Buffer.alloc(stat.size);
|
|
31
|
-
let offset = 0;
|
|
32
|
-
while (offset < bytes.length) {
|
|
33
|
-
const received = fs.readSync(fd, bytes, offset, bytes.length - offset, offset);
|
|
34
|
-
if (!received)
|
|
35
|
-
refuse("source_changed_during_read");
|
|
36
|
-
offset += received;
|
|
37
|
-
}
|
|
38
|
-
if (fs.readSync(fd, Buffer.alloc(1), 0, 1, offset) !== 0)
|
|
39
|
-
refuse("source_changed_during_read");
|
|
40
|
-
return bytes;
|
|
41
|
-
}
|
|
42
|
-
catch (error) {
|
|
43
|
-
if (error instanceof AppError)
|
|
44
|
-
throw error;
|
|
45
|
-
return refuse("source_unavailable");
|
|
46
|
-
}
|
|
47
|
-
finally {
|
|
48
|
-
fs.closeSync(fd);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
function json(bytes) {
|
|
52
|
-
try {
|
|
53
|
-
return JSON.parse(bytes.toString("utf8"));
|
|
54
|
-
}
|
|
55
|
-
catch {
|
|
56
|
-
return refuse("invalid_source_json");
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
/** Explicit operator trust input is separate from the release bundle. No implicit key enrollment. */
|
|
60
|
-
export function readTrustedReleaseProducers(file) {
|
|
61
|
-
const input = json(readBoundedReleaseFile(file, 1024 * 1024));
|
|
62
|
-
if (!input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).length !== 2 || Object.keys(input).some(key => !["producers", "schema_id"].includes(key)))
|
|
63
|
-
refuse("invalid_trust_shape");
|
|
64
|
-
const settings = input;
|
|
65
|
-
if (settings.schema_id !== "dd-flow/release-trust@1" || !Array.isArray(settings.producers) || settings.producers.length < 1 || settings.producers.length > 100)
|
|
66
|
-
refuse("invalid_trust_shape");
|
|
67
|
-
const result = new Map();
|
|
68
|
-
for (const input of settings.producers) {
|
|
69
|
-
if (!input || typeof input !== "object" || Array.isArray(input) || Object.keys(input).length !== 3 || Object.keys(input).some(key => !["channels", "public_key_pem", "repository"].includes(key)))
|
|
70
|
-
refuse("invalid_producer_shape");
|
|
71
|
-
const producer = input;
|
|
72
|
-
if (typeof producer.public_key_pem !== "string" || !/^-----BEGIN PUBLIC KEY-----\r?\n[A-Za-z0-9+/=\r\n]+-----END PUBLIC KEY-----\r?\n?$/.test(producer.public_key_pem) ||
|
|
73
|
-
typeof producer.repository !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.-]*\/[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(producer.repository) ||
|
|
74
|
-
!Array.isArray(producer.channels) || producer.channels.length < 1 || producer.channels.length > 3 ||
|
|
75
|
-
producer.channels.some(channel => !["stable", "preview", "development"].includes(channel)) || new Set(producer.channels).size !== producer.channels.length)
|
|
76
|
-
refuse("invalid_producer_scope");
|
|
77
|
-
try {
|
|
78
|
-
const publicKey = createPublicKey(producer.public_key_pem), id = releaseProducerKeyId(publicKey);
|
|
79
|
-
if (result.has(id))
|
|
80
|
-
refuse("duplicate_producer_key");
|
|
81
|
-
result.set(id, { publicKey, repository: producer.repository, channels: new Set(producer.channels) });
|
|
82
|
-
}
|
|
83
|
-
catch (error) {
|
|
84
|
-
if (error instanceof AppError)
|
|
85
|
-
throw error;
|
|
86
|
-
return refuse("invalid_producer_key");
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return result;
|
|
90
|
-
}
|
|
91
|
-
/** Authenticate metadata before reading archives; all integrity checks precede installation writes. */
|
|
92
|
-
export function readLocalReleaseBundle(root, producers, compatibility) {
|
|
93
|
-
try {
|
|
94
|
-
if (!fs.lstatSync(root).isDirectory())
|
|
95
|
-
refuse("source_not_directory");
|
|
96
|
-
}
|
|
97
|
-
catch (error) {
|
|
98
|
-
if (error instanceof AppError)
|
|
99
|
-
throw error;
|
|
100
|
-
return refuse("source_unavailable");
|
|
101
|
-
}
|
|
102
|
-
const envelope = json(readBoundedReleaseFile(path.join(root, "release-envelope.json"), 24 * 1024 * 1024));
|
|
103
|
-
const authenticated = authenticateReleaseEnvelope(envelope, producers, compatibility);
|
|
104
|
-
const canonArchive = readBoundedReleaseFile(path.join(root, "canon.tgz"), Math.min(authenticated.manifest.canon.archive.bytes, 256 * 1024 * 1024));
|
|
105
|
-
const engineArchive = readBoundedReleaseFile(path.join(root, "engine.tgz"), Math.min(authenticated.manifest.engine.archive.bytes, 256 * 1024 * 1024));
|
|
106
|
-
verifyReleaseArchive(authenticated.manifest.canon.archive, canonArchive);
|
|
107
|
-
verifyReleaseArchive(authenticated.manifest.engine.archive, engineArchive);
|
|
108
|
-
return { envelope, authenticated, canonArchive, engineArchive };
|
|
109
|
-
}
|
|
110
|
-
function remoteUrl(input) {
|
|
111
|
-
let url;
|
|
112
|
-
try {
|
|
113
|
-
url = new URL(input);
|
|
114
|
-
}
|
|
115
|
-
catch {
|
|
116
|
-
return refuse("invalid_remote_source");
|
|
117
|
-
}
|
|
118
|
-
// The explicit source selects a transport origin, never a signing key.
|
|
119
|
-
if (url.protocol !== "https:" || url.username || url.password || url.hash || (url.port && url.port !== "443"))
|
|
120
|
-
refuse("unsafe_remote_source");
|
|
121
|
-
return url;
|
|
122
|
-
}
|
|
123
|
-
/** Remote metadata is untrusted until signature verification; archives are fetched only afterward.
|
|
124
|
-
* Redirects, cross-origin artifacts and implicit credential forwarding are never admitted.
|
|
125
|
-
*/
|
|
126
|
-
export async function readRemoteReleaseBundle(source, producers, compatibility, options = {}) {
|
|
127
|
-
const url = remoteUrl(source), timeoutMs = options.timeoutMs ?? 60_000;
|
|
128
|
-
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 60_000)
|
|
129
|
-
refuse("invalid_source_timeout");
|
|
130
|
-
const fetcher = options.fetch ?? globalThis.fetch;
|
|
131
|
-
let response;
|
|
132
|
-
try {
|
|
133
|
-
response = await fetcher(url, { redirect: "error", signal: AbortSignal.timeout(timeoutMs), credentials: "omit" });
|
|
134
|
-
}
|
|
135
|
-
catch {
|
|
136
|
-
return refuse("remote_source_unavailable_or_redirect");
|
|
137
|
-
}
|
|
138
|
-
if (!response.ok || !response.body) {
|
|
139
|
-
await response.body?.cancel();
|
|
140
|
-
return refuse("remote_source_http_failure");
|
|
141
|
-
}
|
|
142
|
-
const maxBytes = 24 * 1024 * 1024, length = response.headers.get("content-length");
|
|
143
|
-
if (length !== null && (!/^[0-9]+$/.test(length) || Number(length) < 1 || Number(length) > maxBytes)) {
|
|
144
|
-
await response.body.cancel();
|
|
145
|
-
return refuse("remote_source_size_outside_limit");
|
|
146
|
-
}
|
|
147
|
-
const reader = response.body.getReader(), chunks = [];
|
|
148
|
-
let received = 0;
|
|
149
|
-
try {
|
|
150
|
-
while (true) {
|
|
151
|
-
const { done, value } = await reader.read();
|
|
152
|
-
if (done)
|
|
153
|
-
break;
|
|
154
|
-
received += value.byteLength;
|
|
155
|
-
if (received > maxBytes) {
|
|
156
|
-
await reader.cancel();
|
|
157
|
-
return refuse("remote_source_size_outside_limit");
|
|
158
|
-
}
|
|
159
|
-
chunks.push(Buffer.from(value));
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
catch (error) {
|
|
163
|
-
if (error instanceof AppError)
|
|
164
|
-
throw error;
|
|
165
|
-
return refuse("remote_source_interrupted");
|
|
166
|
-
}
|
|
167
|
-
finally {
|
|
168
|
-
reader.releaseLock();
|
|
169
|
-
}
|
|
170
|
-
if (!received || (length !== null && Number(length) !== received))
|
|
171
|
-
refuse("remote_source_length_mismatch");
|
|
172
|
-
const envelope = json(Buffer.concat(chunks, received));
|
|
173
|
-
const authenticated = authenticateReleaseEnvelope(envelope, producers, compatibility);
|
|
174
|
-
if (options.expectedVersion !== undefined && authenticated.manifest.version !== options.expectedVersion)
|
|
175
|
-
refuse("selected_version_mismatch");
|
|
176
|
-
for (const artifact of [authenticated.manifest.canon.archive, authenticated.manifest.engine.archive]) {
|
|
177
|
-
if (remoteUrl(artifact.url).origin !== url.origin)
|
|
178
|
-
refuse("cross_origin_artifact_refused");
|
|
179
|
-
}
|
|
180
|
-
const archives = [];
|
|
181
|
-
for (const artifact of ["canon", "engine"]) {
|
|
182
|
-
archives.push(await downloadReleaseArchive(authenticated.manifest[artifact].archive, {
|
|
183
|
-
allowedHosts: new Set([url.hostname]), timeoutMs, fetch: fetcher,
|
|
184
|
-
...(options.onProgress ? { onProgress: progress => options.onProgress({ artifact, ...progress }) } : {})
|
|
185
|
-
}));
|
|
186
|
-
}
|
|
187
|
-
return { envelope, authenticated, canonArchive: archives[0], engineArchive: archives[1] };
|
|
188
|
-
}
|
|
189
|
-
/** One source selector for setup/update; local bundles remain fully offline. */
|
|
190
|
-
export async function readReleaseBundle(source, producers, compatibility, options = {}) {
|
|
191
|
-
if (/^https?:\/\//i.test(source))
|
|
192
|
-
return readRemoteReleaseBundle(source, producers, compatibility, options);
|
|
193
|
-
return readLocalReleaseBundle(source, producers, compatibility);
|
|
194
|
-
}
|
package/dist/services/setup.js
DELETED
|
@@ -1,377 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
-
import { AppError } from "../shared/errors.js";
|
|
5
|
-
import { hybridDigest, validateHybridShape } from "./hybrid-contracts.js";
|
|
6
|
-
import { admitReleaseManifest } from "./release-manifest.js";
|
|
7
|
-
import { validateReleasePayloadIdentity } from "./release-payload.js";
|
|
8
|
-
import { publishReleaseCache, verifyPublishedReleaseCache } from "./release-cache.js";
|
|
9
|
-
import { acquireInstallationLock } from "./installation-lock.js";
|
|
10
|
-
import { authenticateReleaseEnvelope } from "./release-envelope.js";
|
|
11
|
-
import { prepareOperatorProfile, validateProfileCreationIntent, commitProfileCreation, readOperatorProfiles } from "./operator-profiles.js";
|
|
12
|
-
/** Production ingress authenticates the source before constructing any installation authority. */
|
|
13
|
-
export function installAuthenticatedRelease(home, input) {
|
|
14
|
-
const authenticated = authenticateReleaseEnvelope(input.envelope, input.trustedProducers, input.compatibility);
|
|
15
|
-
return installRelease(home, { requestId: input.requestId, action: input.action,
|
|
16
|
-
manifest: authenticated.manifest, manifestBytes: authenticated.manifestBytes, authority: authenticated.authority,
|
|
17
|
-
canonArchive: input.canonArchive, engineArchive: input.engineArchive,
|
|
18
|
-
...(input.executor !== undefined ? { executor: input.executor } : {}),
|
|
19
|
-
...(input.profileCreation !== undefined ? { profileCreation: input.profileCreation } : {}),
|
|
20
|
-
...(input.deferredOnboarding ? { deferredOnboarding: true } : {}),
|
|
21
|
-
...(input.onCheckpoint ? { onCheckpoint: input.onCheckpoint } : {}) });
|
|
22
|
-
}
|
|
23
|
-
function refuse(reason, effect = "no_effect") {
|
|
24
|
-
throw new AppError("setup_conflict", "Setup request cannot proceed", 1, { reason, phase: "setup", effect, help: "setup", next_action: "dd-flow setup status --json" });
|
|
25
|
-
}
|
|
26
|
-
const bytesDigest = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
27
|
-
function manifestFromBytes(bytes) {
|
|
28
|
-
try {
|
|
29
|
-
return JSON.parse(bytes.toString("utf8"));
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
return refuse("invalid_manifest_bytes");
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
function regularJson(file) {
|
|
36
|
-
if (!fs.lstatSync(file).isFile())
|
|
37
|
-
refuse("unsafe_setup_file");
|
|
38
|
-
try {
|
|
39
|
-
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
40
|
-
}
|
|
41
|
-
catch {
|
|
42
|
-
return refuse("invalid_setup_file");
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
function atomicJson(file, value) {
|
|
46
|
-
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
47
|
-
const fd = fs.openSync(temporary, "wx", 0o600);
|
|
48
|
-
try {
|
|
49
|
-
try {
|
|
50
|
-
fs.writeFileSync(fd, JSON.stringify(value));
|
|
51
|
-
fs.fsyncSync(fd);
|
|
52
|
-
}
|
|
53
|
-
finally {
|
|
54
|
-
fs.closeSync(fd);
|
|
55
|
-
}
|
|
56
|
-
fs.renameSync(temporary, file);
|
|
57
|
-
}
|
|
58
|
-
finally {
|
|
59
|
-
if (fs.existsSync(temporary))
|
|
60
|
-
fs.unlinkSync(temporary);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
function readSetupRequest(file) {
|
|
64
|
-
const value = regularJson(file);
|
|
65
|
-
const required = ["schema_id", "request_id", "identity", "previous_selection", "phase", "installation"];
|
|
66
|
-
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
67
|
-
|| required.some(key => !Object.hasOwn(value, key))
|
|
68
|
-
|| Object.keys(value).some(key => ![...required, "executor", "profileCreation", "deferredOnboarding"].includes(key)))
|
|
69
|
-
refuse("invalid_request_record");
|
|
70
|
-
const request = value;
|
|
71
|
-
if (Object.hasOwn(request, "deferredOnboarding") && (request.deferredOnboarding !== true || Object.hasOwn(request, "executor") || Object.hasOwn(request, "profileCreation")))
|
|
72
|
-
refuse("invalid_deferred_onboarding");
|
|
73
|
-
if (request.schema_id !== "dd-flow/setup-request@1"
|
|
74
|
-
|| typeof request.request_id !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,95}$/.test(request.request_id)
|
|
75
|
-
|| typeof request.identity !== "string" || !/^[a-f0-9]{64}$/.test(request.identity)
|
|
76
|
-
|| request.previous_selection !== null && (typeof request.previous_selection !== "string" || !/^[a-f0-9]{64}$/.test(request.previous_selection))
|
|
77
|
-
|| !["prepared", "canon_cached", "engine_cached", "profiles_saved", "committed"].includes(request.phase)
|
|
78
|
-
|| !request.installation || typeof request.installation !== "object" || Array.isArray(request.installation))
|
|
79
|
-
refuse("invalid_request_record");
|
|
80
|
-
if (Object.hasOwn(request, "executor") && request.executor !== null) {
|
|
81
|
-
try {
|
|
82
|
-
prepareOperatorProfile(request.executor);
|
|
83
|
-
}
|
|
84
|
-
catch {
|
|
85
|
-
refuse("invalid_request_executor");
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
if (Object.hasOwn(request, "profileCreation")) {
|
|
89
|
-
try {
|
|
90
|
-
validateProfileCreationIntent(request.profileCreation);
|
|
91
|
-
}
|
|
92
|
-
catch {
|
|
93
|
-
refuse("invalid_request_profile_creation");
|
|
94
|
-
}
|
|
95
|
-
if (!request.executor || hybridDigest(request.executor) !== hybridDigest(request.profileCreation.profile))
|
|
96
|
-
refuse("request_profile_executor_mismatch");
|
|
97
|
-
}
|
|
98
|
-
return request;
|
|
99
|
-
}
|
|
100
|
-
/** Read retained executor consent for an exact retry, without installation or profile effects.
|
|
101
|
-
* Full source/installation request identity is still checked by installRelease.
|
|
102
|
-
*/
|
|
103
|
-
export function readRetainedSetupExecutor(home, requestId) {
|
|
104
|
-
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,95}$/.test(requestId))
|
|
105
|
-
refuse("invalid_request");
|
|
106
|
-
const root = path.resolve(home), directory = path.join(root, "setup-requests");
|
|
107
|
-
for (const candidate of [root, directory])
|
|
108
|
-
if (fs.existsSync(candidate) && !fs.lstatSync(candidate).isDirectory())
|
|
109
|
-
refuse("unsafe_setup_directory");
|
|
110
|
-
const file = path.join(directory, `${requestId}.json`);
|
|
111
|
-
if (!fs.existsSync(file))
|
|
112
|
-
return null;
|
|
113
|
-
const request = readSetupRequest(file);
|
|
114
|
-
if (request.request_id !== requestId)
|
|
115
|
-
refuse("request_identity_conflict");
|
|
116
|
-
if (!Object.hasOwn(request, "executor"))
|
|
117
|
-
return null;
|
|
118
|
-
return { executor: structuredClone(request.executor ?? null),
|
|
119
|
-
...(request.profileCreation ? { profileCreation: structuredClone(request.profileCreation) } : {}) };
|
|
120
|
-
}
|
|
121
|
-
/** A deferred request is immutable installation consent, not an implicit skip. */
|
|
122
|
-
export function hasDeferredSetupOnboarding(home, requestId) {
|
|
123
|
-
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,95}$/.test(requestId))
|
|
124
|
-
refuse("invalid_request");
|
|
125
|
-
const root = path.resolve(home), directory = path.join(root, "setup-requests");
|
|
126
|
-
for (const candidate of [root, directory])
|
|
127
|
-
if (fs.existsSync(candidate) && !fs.lstatSync(candidate).isDirectory())
|
|
128
|
-
refuse("unsafe_setup_directory");
|
|
129
|
-
const file = path.join(directory, `${requestId}.json`);
|
|
130
|
-
if (!fs.existsSync(file))
|
|
131
|
-
return false;
|
|
132
|
-
const request = readSetupRequest(file);
|
|
133
|
-
if (request.request_id !== requestId)
|
|
134
|
-
refuse("request_identity_conflict");
|
|
135
|
-
return request.deferredOnboarding === true;
|
|
136
|
-
}
|
|
137
|
-
function deferredRequest(home, requestId) {
|
|
138
|
-
if (!hasDeferredSetupOnboarding(home, requestId))
|
|
139
|
-
refuse("onboarding_not_deferred");
|
|
140
|
-
const request = readSetupRequest(path.join(path.resolve(home), "setup-requests", `${requestId}.json`));
|
|
141
|
-
if (request.phase !== "committed")
|
|
142
|
-
refuse("installation_not_committed");
|
|
143
|
-
return request;
|
|
144
|
-
}
|
|
145
|
-
/** Retained consent allows recovery without asking again or importing current defaults. */
|
|
146
|
-
export function readSetupOnboarding(home, requestId) {
|
|
147
|
-
const request = deferredRequest(home, requestId);
|
|
148
|
-
const directory = path.join(path.resolve(home), "setup-onboarding");
|
|
149
|
-
if (fs.existsSync(directory) && !fs.lstatSync(directory).isDirectory())
|
|
150
|
-
refuse("unsafe_onboarding_directory");
|
|
151
|
-
const file = path.join(directory, `${requestId}.json`);
|
|
152
|
-
if (!fs.existsSync(file))
|
|
153
|
-
return null;
|
|
154
|
-
const receipt = regularJson(file);
|
|
155
|
-
if (!receipt || typeof receipt !== "object" || Array.isArray(receipt)
|
|
156
|
-
|| Object.keys(receipt).some(key => !["schema_id", "request_id", "installation_identity", "phase", "executor", "profileCreation"].includes(key))
|
|
157
|
-
|| receipt.schema_id !== "dd-flow/setup-onboarding@1" || receipt.request_id !== requestId
|
|
158
|
-
|| receipt.installation_identity !== request.identity || !["selected", "committed"].includes(receipt.phase)
|
|
159
|
-
|| !Object.hasOwn(receipt, "executor"))
|
|
160
|
-
refuse("invalid_onboarding_receipt");
|
|
161
|
-
try {
|
|
162
|
-
if (receipt.executor !== null)
|
|
163
|
-
prepareOperatorProfile(receipt.executor);
|
|
164
|
-
if (receipt.profileCreation !== undefined) {
|
|
165
|
-
validateProfileCreationIntent(receipt.profileCreation);
|
|
166
|
-
if (!receipt.executor || hybridDigest(receipt.executor) !== hybridDigest(receipt.profileCreation.profile))
|
|
167
|
-
refuse("onboarding_profile_mismatch");
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
catch {
|
|
171
|
-
refuse("invalid_onboarding_receipt");
|
|
172
|
-
}
|
|
173
|
-
return structuredClone(receipt);
|
|
174
|
-
}
|
|
175
|
-
/** Persist human consent before profile effects; replay never restores later profile edits. */
|
|
176
|
-
export function commitSetupOnboarding(home, requestId, selection) {
|
|
177
|
-
const request = deferredRequest(home, requestId), root = path.resolve(home);
|
|
178
|
-
const executor = selection.executor === null ? null : prepareOperatorProfile(selection.executor);
|
|
179
|
-
if (selection.profileCreation) {
|
|
180
|
-
validateProfileCreationIntent(selection.profileCreation);
|
|
181
|
-
if (!executor || hybridDigest(executor) !== hybridDigest(selection.profileCreation.profile))
|
|
182
|
-
refuse("onboarding_profile_mismatch");
|
|
183
|
-
}
|
|
184
|
-
const candidate = { schema_id: "dd-flow/setup-onboarding@1", request_id: requestId,
|
|
185
|
-
installation_identity: request.identity, phase: "selected", executor,
|
|
186
|
-
...(selection.profileCreation ? { profileCreation: selection.profileCreation } : {}) };
|
|
187
|
-
const lease = acquireInstallationLock(path.join(root, "setup.lock"), hybridDigest({ request_id: requestId, identity: request.identity }));
|
|
188
|
-
let failure, result;
|
|
189
|
-
try {
|
|
190
|
-
const retained = readSetupOnboarding(root, requestId);
|
|
191
|
-
if (retained && hybridDigest({ ...retained, phase: "selected" }) !== hybridDigest(candidate))
|
|
192
|
-
refuse("onboarding_selection_conflict");
|
|
193
|
-
if (retained?.phase === "committed")
|
|
194
|
-
result = retained;
|
|
195
|
-
else {
|
|
196
|
-
if (hybridDigest(readSetupStatus(root).installation) !== hybridDigest(request.installation))
|
|
197
|
-
refuse("onboarding_installation_changed");
|
|
198
|
-
const directory = path.join(root, "setup-onboarding");
|
|
199
|
-
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
200
|
-
const file = path.join(directory, `${requestId}.json`);
|
|
201
|
-
if (!retained)
|
|
202
|
-
atomicJson(file, candidate);
|
|
203
|
-
if (candidate.profileCreation)
|
|
204
|
-
commitProfileCreation(root, candidate.profileCreation);
|
|
205
|
-
result = { ...candidate, phase: "committed" };
|
|
206
|
-
atomicJson(file, result);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
catch (error) {
|
|
210
|
-
failure = error instanceof AppError ? error : new AppError("setup_onboarding_interrupted", "Installed release is retained; executor onboarding requires reconciliation", 1, { phase: "executor_onboarding", effect: "unknown", request_id: requestId, help: "setup", next_action: "Replay the exact setup request" });
|
|
211
|
-
}
|
|
212
|
-
finally {
|
|
213
|
-
try {
|
|
214
|
-
lease.release();
|
|
215
|
-
}
|
|
216
|
-
catch {
|
|
217
|
-
if (failure)
|
|
218
|
-
failure.details.cleanup = "not_confirmed";
|
|
219
|
-
else
|
|
220
|
-
failure = new AppError("setup_cleanup_failed", "Onboarding owner cleanup requires reconciliation", 1, { effect: "unknown", cleanup: "not_confirmed", request_id: requestId, help: "setup" });
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
if (failure)
|
|
224
|
-
throw failure;
|
|
225
|
-
return result;
|
|
226
|
-
}
|
|
227
|
-
/** Read-only global readiness. No SQLite connection, migration, project registration or probes. */
|
|
228
|
-
export function readSetupStatus(home) {
|
|
229
|
-
const root = path.resolve(home);
|
|
230
|
-
if (fs.existsSync(root) && !fs.lstatSync(root).isDirectory())
|
|
231
|
-
refuse("unsafe_setup_home");
|
|
232
|
-
const file = path.join(root, "installation.json");
|
|
233
|
-
const installation = fs.existsSync(file) ? regularJson(file) : null;
|
|
234
|
-
if (installation) {
|
|
235
|
-
if (!installation.manifest || typeof installation.manifest_bytes_base64 !== "string" || validateHybridShape("release-manifest", installation.manifest).length ||
|
|
236
|
-
installation.canon_root !== path.join(root, "release-cache", "canon", installation.manifest.canon.version, "payload") ||
|
|
237
|
-
installation.engine_root !== path.join(root, "release-cache", "engine", installation.manifest.engine.version, "payload"))
|
|
238
|
-
refuse("invalid_installation_selection");
|
|
239
|
-
const bytes = Buffer.from(installation.manifest_bytes_base64, "base64");
|
|
240
|
-
if (bytes.toString("base64") !== installation.manifest_bytes_base64 || bytesDigest(bytes) !== installation.manifest_sha256 ||
|
|
241
|
-
hybridDigest(manifestFromBytes(bytes)) !== hybridDigest(installation.manifest))
|
|
242
|
-
refuse("invalid_installation_selection");
|
|
243
|
-
}
|
|
244
|
-
let readiness = installation ? "installed" : "not_installed";
|
|
245
|
-
if (installation) {
|
|
246
|
-
try {
|
|
247
|
-
verifyPublishedReleaseCache(path.dirname(installation.canon_root), installation.manifest.canon.archive);
|
|
248
|
-
verifyPublishedReleaseCache(path.dirname(installation.engine_root), installation.manifest.engine.archive);
|
|
249
|
-
}
|
|
250
|
-
catch {
|
|
251
|
-
readiness = "degraded";
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
const existingStorage = fs.existsSync(path.join(root, "db.sqlite"));
|
|
255
|
-
return { installation, installation_readiness: readiness,
|
|
256
|
-
storage_readiness: existingStorage ? "unknown_requires_diagnostic" : "not_initialized",
|
|
257
|
-
runtime_readiness: "unknown_requires_qualification",
|
|
258
|
-
next_action: !installation ? "dd-flow setup --help" : existingStorage ? "dd-flow migration plan --help" : "dd-flow engine doctor --json",
|
|
259
|
-
...(existingStorage ? { storage_continuation: { kind: "storage_admission",
|
|
260
|
-
status: "requires_diagnostic", reason: "existing_storage_not_inspected",
|
|
261
|
-
help_command: "dd-flow migration plan --help", automatic_migration: false } } : {}) };
|
|
262
|
-
}
|
|
263
|
-
/** Exact-request installation transaction. Selected immutable caches do not affect existing RUNs. */
|
|
264
|
-
export function installRelease(home, input) {
|
|
265
|
-
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]{0,95}$/.test(input.requestId) || !["install", "update"].includes(input.action))
|
|
266
|
-
refuse("invalid_request");
|
|
267
|
-
const executor = input.executor === undefined ? undefined : input.executor === null ? null : prepareOperatorProfile(input.executor);
|
|
268
|
-
const executorBinding = executor === undefined ? {} : { executor: structuredClone(executor) };
|
|
269
|
-
if (input.deferredOnboarding !== undefined && (input.deferredOnboarding !== true || executor !== undefined || input.profileCreation !== undefined))
|
|
270
|
-
refuse("invalid_deferred_onboarding");
|
|
271
|
-
const onboardingBinding = input.deferredOnboarding ? { deferredOnboarding: true } : {};
|
|
272
|
-
if (input.profileCreation !== undefined) {
|
|
273
|
-
validateProfileCreationIntent(input.profileCreation);
|
|
274
|
-
if (!executor || hybridDigest(executor) !== hybridDigest(input.profileCreation.profile))
|
|
275
|
-
refuse("profile_executor_mismatch");
|
|
276
|
-
}
|
|
277
|
-
const profileBinding = input.profileCreation === undefined ? {} : { profileCreation: structuredClone(input.profileCreation) };
|
|
278
|
-
const manifest = admitReleaseManifest(input.manifest, input.authority);
|
|
279
|
-
if (!Buffer.isBuffer(input.manifestBytes) || hybridDigest(manifestFromBytes(input.manifestBytes)) !== hybridDigest(manifest))
|
|
280
|
-
refuse("manifest_bytes_mismatch");
|
|
281
|
-
// Both archives must be valid before even creating the home or publishing the first cache.
|
|
282
|
-
validateReleasePayloadIdentity(manifest, input.canonArchive, input.engineArchive);
|
|
283
|
-
const root = path.resolve(home), requests = path.join(root, "setup-requests"), lock = path.join(root, "setup.lock");
|
|
284
|
-
const installation = { manifest, manifest_sha256: bytesDigest(input.manifestBytes), manifest_bytes_base64: input.manifestBytes.toString("base64"),
|
|
285
|
-
canon_root: path.join(root, "release-cache", "canon", manifest.canon.version, "payload"),
|
|
286
|
-
engine_root: path.join(root, "release-cache", "engine", manifest.engine.version, "payload") };
|
|
287
|
-
const identity = hybridDigest({ action: input.action, installation, ...executorBinding, ...profileBinding, ...onboardingBinding });
|
|
288
|
-
readSetupStatus(root);
|
|
289
|
-
for (const directory of [root, requests]) {
|
|
290
|
-
if (fs.existsSync(directory) && !fs.lstatSync(directory).isDirectory())
|
|
291
|
-
refuse("unsafe_setup_directory");
|
|
292
|
-
}
|
|
293
|
-
if (input.profileCreation) {
|
|
294
|
-
const existingFile = path.join(requests, `${input.requestId}.json`);
|
|
295
|
-
const retained = fs.existsSync(existingFile) ? readSetupRequest(existingFile) : null;
|
|
296
|
-
if (retained?.phase !== "committed") {
|
|
297
|
-
const revision = readOperatorProfiles(root).revision;
|
|
298
|
-
if (revision !== input.profileCreation.before.revision && revision !== input.profileCreation.after.revision)
|
|
299
|
-
refuse("profile_selection_changed");
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
fs.mkdirSync(requests, { recursive: true, mode: 0o700 });
|
|
303
|
-
const lease = acquireInstallationLock(lock, hybridDigest({ request_id: input.requestId, identity }));
|
|
304
|
-
let failure;
|
|
305
|
-
const perform = () => {
|
|
306
|
-
const file = path.join(requests, `${input.requestId}.json`);
|
|
307
|
-
const current = readSetupStatus(root).installation;
|
|
308
|
-
let request = { schema_id: "dd-flow/setup-request@1", request_id: input.requestId, identity,
|
|
309
|
-
previous_selection: current ? hybridDigest(current) : null, phase: "prepared", installation, ...executorBinding, ...profileBinding, ...onboardingBinding };
|
|
310
|
-
if (fs.existsSync(file)) {
|
|
311
|
-
request = readSetupRequest(file);
|
|
312
|
-
if (request.schema_id !== "dd-flow/setup-request@1" || request.request_id !== input.requestId || request.identity !== identity || hybridDigest(request.installation) !== hybridDigest(installation))
|
|
313
|
-
refuse("request_identity_conflict");
|
|
314
|
-
if (request.deferredOnboarding !== input.deferredOnboarding)
|
|
315
|
-
refuse("request_onboarding_conflict");
|
|
316
|
-
if (hybridDigest({ executor: request.executor ?? null, present: Object.hasOwn(request, "executor") }) !==
|
|
317
|
-
hybridDigest({ executor: executor ?? null, present: executor !== undefined }))
|
|
318
|
-
refuse("request_executor_conflict");
|
|
319
|
-
if (hybridDigest({ profileCreation: request.profileCreation ?? null }) !== hybridDigest({ profileCreation: input.profileCreation ?? null }))
|
|
320
|
-
refuse("request_profile_creation_conflict");
|
|
321
|
-
if (!["prepared", "canon_cached", "engine_cached", "profiles_saved", "committed"].includes(request.phase))
|
|
322
|
-
refuse("invalid_request_phase");
|
|
323
|
-
}
|
|
324
|
-
// Replay still revalidates retained payload bytes; it never reactivates an older selection.
|
|
325
|
-
if (request.phase === "committed") {
|
|
326
|
-
publishReleaseCache(root, "canon", manifest.canon.version, manifest.canon.archive, input.canonArchive);
|
|
327
|
-
publishReleaseCache(root, "engine", manifest.engine.version, manifest.engine.archive, input.engineArchive);
|
|
328
|
-
return { ok: true, replayed: true, installation: request.installation, ...executorBinding };
|
|
329
|
-
}
|
|
330
|
-
const selectedDigest = current ? hybridDigest(current) : null;
|
|
331
|
-
if (selectedDigest !== request.previous_selection && selectedDigest !== hybridDigest(installation))
|
|
332
|
-
refuse("selection_changed_since_request");
|
|
333
|
-
if (input.action === "install" && current && selectedDigest !== hybridDigest(installation))
|
|
334
|
-
refuse("existing_installation_requires_update");
|
|
335
|
-
const checkpoint = (phase) => {
|
|
336
|
-
request.phase = phase;
|
|
337
|
-
atomicJson(file, request);
|
|
338
|
-
input.onCheckpoint?.(phase);
|
|
339
|
-
};
|
|
340
|
-
checkpoint("prepared");
|
|
341
|
-
publishReleaseCache(root, "canon", manifest.canon.version, manifest.canon.archive, input.canonArchive);
|
|
342
|
-
checkpoint("canon_cached");
|
|
343
|
-
publishReleaseCache(root, "engine", manifest.engine.version, manifest.engine.archive, input.engineArchive);
|
|
344
|
-
checkpoint("engine_cached");
|
|
345
|
-
if (request.profileCreation) {
|
|
346
|
-
commitProfileCreation(root, request.profileCreation);
|
|
347
|
-
checkpoint("profiles_saved");
|
|
348
|
-
}
|
|
349
|
-
atomicJson(path.join(root, "installation.json"), installation);
|
|
350
|
-
checkpoint("committed");
|
|
351
|
-
return { ok: true, replayed: false, installation, ...executorBinding };
|
|
352
|
-
};
|
|
353
|
-
let result;
|
|
354
|
-
try {
|
|
355
|
-
result = perform();
|
|
356
|
-
}
|
|
357
|
-
catch (error) {
|
|
358
|
-
failure = error instanceof AppError ? error : new AppError("setup_interrupted", "Setup request requires reconciliation", 1, { phase: "setup", effect: "unknown", request_id: input.requestId, next_action: "Replay the exact setup request", help: "setup" });
|
|
359
|
-
}
|
|
360
|
-
finally {
|
|
361
|
-
try {
|
|
362
|
-
lease.release();
|
|
363
|
-
}
|
|
364
|
-
catch {
|
|
365
|
-
// Cleanup is secondary to the retained operation result. Never leak a
|
|
366
|
-
// raw filesystem cause or erase the original failure with an unlink error.
|
|
367
|
-
if (failure)
|
|
368
|
-
failure.details.cleanup = "not_confirmed";
|
|
369
|
-
else
|
|
370
|
-
failure = new AppError("setup_cleanup_failed", "Setup owner cleanup requires reconciliation", 1, { phase: "setup", effect: "unknown", cleanup: "not_confirmed", request_id: input.requestId,
|
|
371
|
-
next_action: "Inspect dd-flow setup status --json; replay the exact request after its owner exits", help: "setup" });
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
if (failure)
|
|
375
|
-
throw failure;
|
|
376
|
-
return result;
|
|
377
|
-
}
|