@moinulmoin/eden 0.1.0
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/LICENSE +201 -0
- package/NOTICE +42 -0
- package/README.md +20 -0
- package/dist/eve-control-plane.d.ts +228 -0
- package/dist/eve-control-plane.js +2007 -0
- package/dist/eve-packaging.d.ts +261 -0
- package/dist/eve-packaging.js +2056 -0
- package/dist/eve-runtime-config.d.ts +126 -0
- package/dist/eve-runtime-config.js +446 -0
- package/dist/eve-runtime-image.d.ts +121 -0
- package/dist/eve-runtime-image.js +1134 -0
- package/dist/eve.d.ts +49 -0
- package/dist/eve.js +234 -0
- package/dist/index.d.ts +196 -0
- package/dist/index.js +6840 -0
- package/package.json +56 -0
|
@@ -0,0 +1,1134 @@
|
|
|
1
|
+
import { createHash, } from "node:crypto";
|
|
2
|
+
import { execFile, spawn, } from "node:child_process";
|
|
3
|
+
import { cp, lstat, mkdir, readFile, readlink, readdir, realpath, rm, symlink, writeFile, } from "node:fs/promises";
|
|
4
|
+
import { dirname, join, relative, resolve, } from "node:path";
|
|
5
|
+
import { promisify, } from "node:util";
|
|
6
|
+
import { EvePackagingError, } from "./eve-packaging.js";
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
const START_COMMAND = [
|
|
9
|
+
"./node_modules/.bin/eve",
|
|
10
|
+
"start",
|
|
11
|
+
"--host",
|
|
12
|
+
"0.0.0.0",
|
|
13
|
+
"--port",
|
|
14
|
+
"8080",
|
|
15
|
+
];
|
|
16
|
+
const HEALTH_PATH = "/eve/v1/health";
|
|
17
|
+
const INTERNAL_PORT = 8080;
|
|
18
|
+
const IMAGE_ID_PATTERN = /^sha256:[0-9a-f]{64}$/u;
|
|
19
|
+
const NODE_IMAGE_PATTERN = /^node:24\.17\.0(?:-[a-z0-9][a-z0-9._-]*)?$/u;
|
|
20
|
+
const FORBIDDEN_RUNTIME_NAMES = new Set([
|
|
21
|
+
".env",
|
|
22
|
+
".env.local",
|
|
23
|
+
".env.production",
|
|
24
|
+
".npmrc",
|
|
25
|
+
".pnpmrc",
|
|
26
|
+
".pypirc",
|
|
27
|
+
".yarnrc",
|
|
28
|
+
".yarnrc.yml",
|
|
29
|
+
"credentials.json",
|
|
30
|
+
"service-account.json",
|
|
31
|
+
]);
|
|
32
|
+
const HOST_ENVIRONMENT = {
|
|
33
|
+
HOST: "0.0.0.0",
|
|
34
|
+
NITRO_HOST: "0.0.0.0",
|
|
35
|
+
PORT: "8080",
|
|
36
|
+
NITRO_PORT: "8080",
|
|
37
|
+
NODE_ENV: "production",
|
|
38
|
+
};
|
|
39
|
+
function sha256(value) {
|
|
40
|
+
return createHash("sha256").update(value).digest("hex");
|
|
41
|
+
}
|
|
42
|
+
function jsonBytes(value) {
|
|
43
|
+
return Buffer.from(`${JSON.stringify(value)}\n`, "utf8");
|
|
44
|
+
}
|
|
45
|
+
function isWithin(root, candidate) {
|
|
46
|
+
const normalizedRoot = root.endsWith("/") ? root.slice(0, -1) : root;
|
|
47
|
+
return candidate === normalizedRoot || candidate.startsWith(`${normalizedRoot}/`);
|
|
48
|
+
}
|
|
49
|
+
function safeRelativePath(root, candidate) {
|
|
50
|
+
return relative(root, candidate).split("\\").join("/");
|
|
51
|
+
}
|
|
52
|
+
async function assertFreshOwnedFilePath(path, root, subject) {
|
|
53
|
+
const canonicalRoot = await realpath(root).catch(() => undefined);
|
|
54
|
+
const canonicalParent = await realpath(dirname(path)).catch(() => undefined);
|
|
55
|
+
const existing = await lstat(path).catch(() => undefined);
|
|
56
|
+
if (canonicalRoot === undefined ||
|
|
57
|
+
canonicalParent === undefined ||
|
|
58
|
+
!isWithin(canonicalRoot, resolve(canonicalParent)) ||
|
|
59
|
+
existing !== undefined) {
|
|
60
|
+
throw packagingError("ROOT_INVALID", subject, "An Eden-owned generated path was missing a regular parent or was already occupied.", "Create a fresh Eden-owned generation and retry without following symbolic links.");
|
|
61
|
+
}
|
|
62
|
+
return canonicalParent;
|
|
63
|
+
}
|
|
64
|
+
async function verifyOwnedFilePath(path, root, expectedParent, subject) {
|
|
65
|
+
const canonicalRoot = await realpath(root).catch(() => undefined);
|
|
66
|
+
const canonicalParent = await realpath(dirname(path)).catch(() => undefined);
|
|
67
|
+
const details = await lstat(path).catch(() => undefined);
|
|
68
|
+
const canonicalPath = await realpath(path).catch(() => undefined);
|
|
69
|
+
if (canonicalRoot === undefined ||
|
|
70
|
+
canonicalParent !== expectedParent ||
|
|
71
|
+
details === undefined ||
|
|
72
|
+
!details.isFile() ||
|
|
73
|
+
details.isSymbolicLink() ||
|
|
74
|
+
canonicalPath === undefined ||
|
|
75
|
+
!isWithin(canonicalRoot, resolve(canonicalPath))) {
|
|
76
|
+
throw packagingError("SOURCE_RACE", subject, "An Eden-owned generated path changed or escaped during creation.", "Discard the mixed-generation candidate and retry from a clean Eden-owned generation.");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function imageReference(nodeImage) {
|
|
80
|
+
if (!NODE_IMAGE_PATTERN.test(nodeImage.reference) ||
|
|
81
|
+
!IMAGE_ID_PATTERN.test(nodeImage.digest)) {
|
|
82
|
+
throw new EvePackagingError({
|
|
83
|
+
code: "DOCKER_PLATFORM_BLOCKED",
|
|
84
|
+
subject: "node-24-image",
|
|
85
|
+
reason: "The runtime image requires an immutable Linux/amd64 Node 24 image.",
|
|
86
|
+
remediation: "Supply the verified Node 24 image reference and sha256 digest.",
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return `${nodeImage.reference}@${nodeImage.digest}`;
|
|
90
|
+
}
|
|
91
|
+
function packagingError(code, subject, reason, remediation) {
|
|
92
|
+
return new EvePackagingError({
|
|
93
|
+
code,
|
|
94
|
+
subject,
|
|
95
|
+
reason,
|
|
96
|
+
remediation,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
export function validateEveHostRequirements(requirements) {
|
|
100
|
+
if (requirements.architecture !== "linux/amd64") {
|
|
101
|
+
throw packagingError("DOCKER_PLATFORM_BLOCKED", "architecture", "The Eve Container requires Linux/amd64 execution.", "Run the project with a Docker/OrbStack builder that can prove linux/amd64; no alternate architecture is selected.");
|
|
102
|
+
}
|
|
103
|
+
if (requirements.world !== "supported") {
|
|
104
|
+
throw packagingError("UNSUPPORTED_HOST_REQUIREMENT", "Workflow World", "The selected Workflow World has an unknown or unsupported Container host requirement.", "Configure a project-owned Eve-compatible World whose runtime requirements are supported by a disposable Linux/amd64 Container.");
|
|
105
|
+
}
|
|
106
|
+
if (requirements.sandbox !== "supported") {
|
|
107
|
+
throw packagingError("UNSUPPORTED_HOST_REQUIREMENT", "sandbox", "The selected Eve sandbox has an unknown or unsupported Container host requirement.", "Use a sandbox that can prewarm without privileged, device, or host-kernel access, or run the project outside Eden Deploy.");
|
|
108
|
+
}
|
|
109
|
+
if (requirements.privileged) {
|
|
110
|
+
throw packagingError("UNSUPPORTED_HOST_REQUIREMENT", "privileged execution", "The Eve project requires privileged Container execution outside the hosting contract.", "Remove the privileged requirement or run the project on a host that explicitly supports it; Eden does not adapt or substitute the runtime.");
|
|
111
|
+
}
|
|
112
|
+
if (requirements.devices !== "none") {
|
|
113
|
+
throw packagingError("UNSUPPORTED_HOST_REQUIREMENT", "device access", "The Eve project requires a device or unknown device capability outside the hosting contract.", "Remove the device requirement or choose a supported host; Eden does not grant devices or fall back to Agent.");
|
|
114
|
+
}
|
|
115
|
+
if (requirements.kernel !== "supported") {
|
|
116
|
+
throw packagingError("UNSUPPORTED_HOST_REQUIREMENT", "kernel capability", "The Eve project requires an unknown or unsupported host-kernel capability.", "Use only the documented process, network, and disposable-filesystem capabilities of the Linux/amd64 Container.");
|
|
117
|
+
}
|
|
118
|
+
if (requirements.network !== "supported") {
|
|
119
|
+
throw packagingError("UNSUPPORTED_HOST_REQUIREMENT", "outbound network", "The Eve project requires an unknown or unsupported Container network capability.", "Use only bounded outbound networking supported by the Linux/amd64 Container; Eden does not provide a network adapter or fallback.");
|
|
120
|
+
}
|
|
121
|
+
if (requirements.durableLocalFilesystem) {
|
|
122
|
+
throw packagingError("UNSUPPORTED_HOST_REQUIREMENT", "durable local filesystem", "The Eve project requires durable local Container filesystem state.", "Configure a project-owned durable Eve-compatible World or external service; Container-local disk is disposable.");
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async function readRegularFile(path, containmentRoot) {
|
|
126
|
+
const canonicalRoot = containmentRoot === undefined
|
|
127
|
+
? undefined
|
|
128
|
+
: await realpath(containmentRoot).catch(() => undefined);
|
|
129
|
+
const parentBefore = containmentRoot === undefined
|
|
130
|
+
? undefined
|
|
131
|
+
: await realpath(dirname(path)).catch(() => undefined);
|
|
132
|
+
if (containmentRoot !== undefined &&
|
|
133
|
+
(canonicalRoot === undefined ||
|
|
134
|
+
parentBefore === undefined ||
|
|
135
|
+
!isWithin(canonicalRoot, resolve(parentBefore)))) {
|
|
136
|
+
throw packagingError("RUNTIME_CLOSURE_INCOMPLETE", safeRelativePath(containmentRoot ?? dirname(path), path), "The candidate runtime file parent escaped its immutable runtime root.", "Regenerate the immutable Eve candidate with regular runtime files inside the snapshot.");
|
|
137
|
+
}
|
|
138
|
+
const details = await lstat(path).catch(() => undefined);
|
|
139
|
+
if (details === undefined || !details.isFile() || details.isSymbolicLink()) {
|
|
140
|
+
throw packagingError("RUNTIME_CLOSURE_INCOMPLETE", path, "The candidate runtime closure contains a missing or non-regular file.", "Regenerate the immutable Eve candidate with a complete output and project-local start closure.");
|
|
141
|
+
}
|
|
142
|
+
const bytes = await readFile(path);
|
|
143
|
+
const secondRead = await readFile(path).catch(() => undefined);
|
|
144
|
+
const parentAfter = containmentRoot === undefined
|
|
145
|
+
? undefined
|
|
146
|
+
: await realpath(dirname(path)).catch(() => undefined);
|
|
147
|
+
if (secondRead === undefined ||
|
|
148
|
+
!bytes.equals(secondRead) ||
|
|
149
|
+
(containmentRoot !== undefined && parentAfter !== parentBefore)) {
|
|
150
|
+
throw packagingError("SOURCE_RACE", safeRelativePath(containmentRoot ?? dirname(path), path), "The candidate runtime file parent changed during validation.", "Discard the mixed-generation runtime candidate and rebuild from quiescent inputs.");
|
|
151
|
+
}
|
|
152
|
+
return { bytes, sha256: sha256(bytes) };
|
|
153
|
+
}
|
|
154
|
+
async function captureRuntimeTree(root, snapshotRoot, label) {
|
|
155
|
+
const canonicalRoot = await realpath(root).catch(() => undefined);
|
|
156
|
+
const canonicalSnapshotRoot = await realpath(snapshotRoot).catch(() => undefined);
|
|
157
|
+
if (canonicalRoot === undefined ||
|
|
158
|
+
canonicalSnapshotRoot === undefined ||
|
|
159
|
+
!isWithin(canonicalSnapshotRoot, resolve(canonicalRoot))) {
|
|
160
|
+
throw packagingError("RUNTIME_CLOSURE_INCOMPLETE", safeRelativePath(snapshotRoot, root), "The candidate runtime closure root is missing or escapes the immutable snapshot.", "Regenerate the immutable Eve candidate with regular runtime roots inside the snapshot.");
|
|
161
|
+
}
|
|
162
|
+
const files = [];
|
|
163
|
+
const nativeModules = [];
|
|
164
|
+
const visit = async (directory) => {
|
|
165
|
+
const directoryDetails = await lstat(directory).catch(() => undefined);
|
|
166
|
+
const directoryCanonical = await realpath(directory).catch(() => undefined);
|
|
167
|
+
if (directoryDetails === undefined ||
|
|
168
|
+
!directoryDetails.isDirectory() ||
|
|
169
|
+
directoryDetails.isSymbolicLink() ||
|
|
170
|
+
directoryCanonical === undefined ||
|
|
171
|
+
!isWithin(canonicalSnapshotRoot, resolve(directoryCanonical))) {
|
|
172
|
+
throw packagingError("RUNTIME_CLOSURE_INCOMPLETE", safeRelativePath(canonicalSnapshotRoot, directory), "The candidate runtime closure directory escaped or changed during validation.", "Regenerate the immutable Eve candidate with regular runtime directories inside the snapshot.");
|
|
173
|
+
}
|
|
174
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
175
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
176
|
+
for (const entry of entries) {
|
|
177
|
+
const candidate = join(directory, entry.name);
|
|
178
|
+
const relativePath = safeRelativePath(canonicalSnapshotRoot, candidate);
|
|
179
|
+
if (FORBIDDEN_RUNTIME_NAMES.has(entry.name) ||
|
|
180
|
+
entry.name.startsWith(".env") ||
|
|
181
|
+
entry.name.endsWith(".pem") ||
|
|
182
|
+
entry.name.endsWith(".key")) {
|
|
183
|
+
throw packagingError("SECRET_EXCLUSION_FAILED", relativePath, "The candidate runtime closure contains an environment or credential file.", "Remove credential material from the project dependency/output closure and rebuild the candidate.");
|
|
184
|
+
}
|
|
185
|
+
if (entry.isSymbolicLink()) {
|
|
186
|
+
const resolved = await realpath(candidate).catch(() => undefined);
|
|
187
|
+
if (resolved === undefined ||
|
|
188
|
+
!isWithin(canonicalRoot, resolve(resolved))) {
|
|
189
|
+
throw packagingError("RUNTIME_CLOSURE_INCOMPLETE", relativePath, "The candidate runtime closure contains a symbolic link that escapes its runtime root.", "Materialize the Eve output and dependencies inside the immutable runtime closure before image assembly.");
|
|
190
|
+
}
|
|
191
|
+
const targetParts = safeRelativePath(canonicalRoot, resolve(resolved))
|
|
192
|
+
.split("/");
|
|
193
|
+
if (targetParts.some((part) => FORBIDDEN_RUNTIME_NAMES.has(part) ||
|
|
194
|
+
part.startsWith(".env") ||
|
|
195
|
+
part.endsWith(".pem") ||
|
|
196
|
+
part.endsWith(".key"))) {
|
|
197
|
+
throw packagingError("SECRET_EXCLUSION_FAILED", relativePath, "The candidate runtime closure contains a symbolic link to credential or environment material.", "Remove links to credential material from the project dependency/output closure and rebuild the candidate.");
|
|
198
|
+
}
|
|
199
|
+
await validateRuntimeTargetTree(resolve(resolved), canonicalRoot, new Set());
|
|
200
|
+
files.push({
|
|
201
|
+
path: relativePath,
|
|
202
|
+
sha256: `link:${await readlink(candidate)}`,
|
|
203
|
+
byteLength: 0,
|
|
204
|
+
symbolicLink: true,
|
|
205
|
+
});
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (entry.isDirectory()) {
|
|
209
|
+
await visit(candidate);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (!entry.isFile()) {
|
|
213
|
+
throw packagingError("RUNTIME_CLOSURE_INCOMPLETE", relativePath, "The candidate runtime closure contains a device, socket, FIFO, or other unsupported file.", "Regenerate the Eve candidate with regular runtime files only.");
|
|
214
|
+
}
|
|
215
|
+
const { bytes, sha256: digest } = await readRegularFile(candidate, canonicalRoot);
|
|
216
|
+
files.push({
|
|
217
|
+
path: relativePath,
|
|
218
|
+
sha256: digest,
|
|
219
|
+
byteLength: bytes.byteLength,
|
|
220
|
+
symbolicLink: false,
|
|
221
|
+
});
|
|
222
|
+
if (entry.name.endsWith(".node")) {
|
|
223
|
+
const fileResult = await execFileAsync("file", [candidate], {
|
|
224
|
+
maxBuffer: 1024 * 1024,
|
|
225
|
+
}).catch(() => undefined);
|
|
226
|
+
if (fileResult === undefined ||
|
|
227
|
+
!/ELF 64-bit/u.test(fileResult.stdout)) {
|
|
228
|
+
throw packagingError("RUNTIME_CLOSURE_INCOMPLETE", relativePath, "A native Eve dependency could not be verified as a loadable module.", "Rebuild native dependencies for Linux/amd64 inside the immutable builder.");
|
|
229
|
+
}
|
|
230
|
+
if (!/(?:x86-64|x86_64|AMD64)/u.test(fileResult.stdout)) {
|
|
231
|
+
throw packagingError("RUNTIME_CLOSURE_INCOMPLETE", relativePath, "A native Eve dependency was built for an unsupported architecture.", "Rebuild native dependencies for Linux/amd64 inside the immutable builder.");
|
|
232
|
+
}
|
|
233
|
+
nativeModules.push({
|
|
234
|
+
path: relativePath,
|
|
235
|
+
sha256: digest,
|
|
236
|
+
platform: "linux/amd64",
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
await visit(canonicalRoot);
|
|
242
|
+
files.sort((left, right) => left.path.localeCompare(right.path));
|
|
243
|
+
nativeModules.sort((left, right) => left.path.localeCompare(right.path));
|
|
244
|
+
return {
|
|
245
|
+
files,
|
|
246
|
+
nativeModules,
|
|
247
|
+
digest: sha256(jsonBytes({
|
|
248
|
+
label,
|
|
249
|
+
files,
|
|
250
|
+
nativeModules,
|
|
251
|
+
})),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
async function validateRuntimeTargetTree(path, root, visited) {
|
|
255
|
+
const details = await lstat(path).catch(() => undefined);
|
|
256
|
+
const canonical = await realpath(path).catch(() => undefined);
|
|
257
|
+
if (details === undefined ||
|
|
258
|
+
canonical === undefined ||
|
|
259
|
+
!isWithin(root, resolve(canonical))) {
|
|
260
|
+
throw packagingError("RUNTIME_CLOSURE_INCOMPLETE", safeRelativePath(root, path), "A runtime closure link target is missing or escapes its runtime root.", "Materialize the Eve output and dependencies inside the immutable runtime closure before image assembly.");
|
|
261
|
+
}
|
|
262
|
+
const canonicalPath = resolve(canonical);
|
|
263
|
+
if (visited.has(canonicalPath))
|
|
264
|
+
return;
|
|
265
|
+
visited.add(canonicalPath);
|
|
266
|
+
const targetName = canonicalPath.split("/").at(-1) ?? "";
|
|
267
|
+
if (FORBIDDEN_RUNTIME_NAMES.has(targetName) ||
|
|
268
|
+
targetName.startsWith(".env") ||
|
|
269
|
+
targetName.endsWith(".pem") ||
|
|
270
|
+
targetName.endsWith(".key")) {
|
|
271
|
+
throw packagingError("SECRET_EXCLUSION_FAILED", safeRelativePath(root, path), "A runtime closure link target is credential or environment material.", "Remove credential material from the project dependency/output closure and rebuild the candidate.");
|
|
272
|
+
}
|
|
273
|
+
if (details.isDirectory()) {
|
|
274
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
275
|
+
for (const entry of entries) {
|
|
276
|
+
await validateRuntimeTargetTree(join(path, entry.name), root, visited);
|
|
277
|
+
}
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (!details.isFile() && !details.isSymbolicLink()) {
|
|
281
|
+
throw packagingError("RUNTIME_CLOSURE_INCOMPLETE", safeRelativePath(root, path), "A runtime closure link target is not a regular file or directory.", "Regenerate the immutable Eve candidate with regular runtime files only.");
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
async function validateCandidateClosure(candidate) {
|
|
285
|
+
const generationRoot = resolve(candidate.generationRoot);
|
|
286
|
+
const requestedSnapshotRoot = resolve(candidate.snapshotRoot);
|
|
287
|
+
const canonicalGenerationRoot = await realpath(generationRoot).catch(() => generationRoot);
|
|
288
|
+
const snapshotRoot = await realpath(requestedSnapshotRoot).catch(() => requestedSnapshotRoot);
|
|
289
|
+
if (!isWithin(generationRoot, requestedSnapshotRoot) ||
|
|
290
|
+
!isWithin(canonicalGenerationRoot, snapshotRoot) ||
|
|
291
|
+
!isWithin(generationRoot, resolve(candidate.inputManifestPath))) {
|
|
292
|
+
throw packagingError("RUNTIME_CLOSURE_INCOMPLETE", "candidate", "The validated Eve candidate points outside its Eden-owned generation.", "Pass the immutable candidate returned by the Eve snapshot/build boundary without changing its paths.");
|
|
293
|
+
}
|
|
294
|
+
if (candidate.generatedOutput.entrypointPath !== ".output/server/index.mjs") {
|
|
295
|
+
throw packagingError("UNSUPPORTED_EVE_OUTPUT", "generated Eve entrypoint", "The runtime candidate does not identify the required .output/server/index.mjs artifact.", "Regenerate the Eve candidate with the project-local Eve/Nitro entrypoint at .output/server/index.mjs.");
|
|
296
|
+
}
|
|
297
|
+
const outputRoot = join(snapshotRoot, ".output");
|
|
298
|
+
const dependenciesRoot = join(snapshotRoot, "node_modules");
|
|
299
|
+
const outputDetails = await lstat(outputRoot).catch(() => undefined);
|
|
300
|
+
const dependencyDetails = await lstat(dependenciesRoot).catch(() => undefined);
|
|
301
|
+
if (outputDetails === undefined ||
|
|
302
|
+
!outputDetails.isDirectory() ||
|
|
303
|
+
outputDetails.isSymbolicLink() ||
|
|
304
|
+
dependencyDetails === undefined ||
|
|
305
|
+
!dependencyDetails.isDirectory() ||
|
|
306
|
+
dependencyDetails.isSymbolicLink()) {
|
|
307
|
+
throw packagingError("RUNTIME_CLOSURE_INCOMPLETE", ".output/node_modules", "The validated Eve candidate is missing its generated output or project dependency closure.", "Regenerate the candidate and retain both .output and the installed runtime dependencies.");
|
|
308
|
+
}
|
|
309
|
+
const entrypoint = join(outputRoot, candidate.generatedOutput.entrypointPath.slice(".output/".length));
|
|
310
|
+
const entrypointDetails = await lstat(entrypoint).catch(() => undefined);
|
|
311
|
+
if (entrypointDetails === undefined ||
|
|
312
|
+
!entrypointDetails.isFile() ||
|
|
313
|
+
entrypointDetails.isSymbolicLink()) {
|
|
314
|
+
throw packagingError("UNSUPPORTED_EVE_OUTPUT", candidate.generatedOutput.entrypointPath, "The candidate does not retain the regular generated Eve/Nitro server entrypoint.", "Regenerate .output/server/index.mjs inside the immutable Eve candidate.");
|
|
315
|
+
}
|
|
316
|
+
const eveBin = join(dependenciesRoot, ".bin/eve");
|
|
317
|
+
const eveDetails = await lstat(eveBin).catch(() => undefined);
|
|
318
|
+
const eveResolved = await realpath(eveBin).catch(() => undefined);
|
|
319
|
+
if (eveDetails === undefined ||
|
|
320
|
+
(!eveDetails.isFile() && !eveDetails.isSymbolicLink()) ||
|
|
321
|
+
eveResolved === undefined ||
|
|
322
|
+
!isWithin(snapshotRoot, resolve(eveResolved))) {
|
|
323
|
+
throw packagingError("UNSUPPORTED_EVE_OUTPUT", "node_modules/.bin/eve", "The candidate does not retain a project-local Eve start executable inside its dependency tree.", "Retain the installed project-local Eve CLI and its start closure in the candidate.");
|
|
324
|
+
}
|
|
325
|
+
const eveResolvedDetails = await lstat(eveResolved).catch(() => undefined);
|
|
326
|
+
if (eveResolvedDetails === undefined ||
|
|
327
|
+
!eveResolvedDetails.isFile() ||
|
|
328
|
+
(eveResolvedDetails.mode & 0o111) === 0) {
|
|
329
|
+
throw packagingError("UNSUPPORTED_EVE_OUTPUT", "node_modules/.bin/eve", "The candidate Eve start executable is not executable.", "Rebuild the project-local Eve dependency closure in the pinned builder.");
|
|
330
|
+
}
|
|
331
|
+
const [output, dependencies] = await Promise.all([
|
|
332
|
+
captureRuntimeTree(outputRoot, snapshotRoot, "output"),
|
|
333
|
+
captureRuntimeTree(dependenciesRoot, snapshotRoot, "dependencies"),
|
|
334
|
+
]);
|
|
335
|
+
const entrypointBytes = await readRegularFile(entrypoint, snapshotRoot);
|
|
336
|
+
const candidateOutputDigest = sha256(jsonBytes(output.files.map((file) => ({
|
|
337
|
+
relativePath: file.path,
|
|
338
|
+
sha256: file.sha256,
|
|
339
|
+
byteLength: file.byteLength,
|
|
340
|
+
}))));
|
|
341
|
+
if (entrypointBytes.sha256 !== candidate.generatedOutput.sha256 ||
|
|
342
|
+
output.files.length !== candidate.generatedOutput.fileCount ||
|
|
343
|
+
candidateOutputDigest !== candidate.generatedOutput.outputDigest) {
|
|
344
|
+
throw packagingError("SOURCE_RACE", ".output", "The generated Eve output no longer matches the immutable candidate digest.", "Discard the mixed-generation runtime candidate and rebuild from a quiescent snapshot.");
|
|
345
|
+
}
|
|
346
|
+
return {
|
|
347
|
+
files: [...output.files, ...dependencies.files],
|
|
348
|
+
nativeModules: dependencies.nativeModules,
|
|
349
|
+
outputDigest: output.digest,
|
|
350
|
+
dependencyDigest: dependencies.digest,
|
|
351
|
+
digest: sha256(jsonBytes({
|
|
352
|
+
outputDigest: output.digest,
|
|
353
|
+
dependencyDigest: dependencies.digest,
|
|
354
|
+
})),
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
export async function revalidateEveRuntimeCandidate(candidate) {
|
|
358
|
+
const closure = await validateCandidateClosure(candidate);
|
|
359
|
+
return {
|
|
360
|
+
root: "/workspace/node_modules",
|
|
361
|
+
outputDigest: closure.outputDigest,
|
|
362
|
+
dependencyDigest: closure.dependencyDigest,
|
|
363
|
+
digest: closure.digest,
|
|
364
|
+
files: closure.files,
|
|
365
|
+
nativeModules: closure.nativeModules,
|
|
366
|
+
eveStartClosureRetained: true,
|
|
367
|
+
builderOnlyMaterialExcluded: true,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Node's recursive fs.cp rewrites relative symbolic links into absolute links
|
|
372
|
+
* that point back at the source tree. Map any copied absolute link onto the
|
|
373
|
+
* equivalent path inside the runtime context so the copied closure stays
|
|
374
|
+
* self-contained.
|
|
375
|
+
*/
|
|
376
|
+
async function rewriteContextLinks(sourceRoot, contextRoot) {
|
|
377
|
+
const walk = async (directory) => {
|
|
378
|
+
const entries = await readdir(directory, { withFileTypes: true })
|
|
379
|
+
.catch(() => []);
|
|
380
|
+
for (const entry of entries) {
|
|
381
|
+
const path = join(directory, entry.name);
|
|
382
|
+
if (entry.isDirectory()) {
|
|
383
|
+
await walk(path);
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
if (!entry.isSymbolicLink())
|
|
387
|
+
continue;
|
|
388
|
+
const text = await readlink(path).catch(() => undefined);
|
|
389
|
+
if (text === undefined || !text.startsWith("/"))
|
|
390
|
+
continue;
|
|
391
|
+
const mapped = join(contextRoot, relative(resolve(sourceRoot), resolve(text)));
|
|
392
|
+
if (resolve(mapped) === resolve(text))
|
|
393
|
+
continue;
|
|
394
|
+
const mappedDetails = await lstat(mapped).catch(() => undefined);
|
|
395
|
+
if (mappedDetails === undefined)
|
|
396
|
+
continue;
|
|
397
|
+
await rm(path, { force: true });
|
|
398
|
+
await symlink(relative(dirname(path), mapped), path);
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
await walk(contextRoot);
|
|
402
|
+
}
|
|
403
|
+
async function writeRuntimeContext(candidate, nodeImage, closure) {
|
|
404
|
+
const runtimeContextPath = join(candidate.generationRoot, "container/runtime-context");
|
|
405
|
+
const dockerfilePath = join(candidate.generationRoot, "container/runtime.Dockerfile");
|
|
406
|
+
const existing = await lstat(runtimeContextPath).catch(() => undefined);
|
|
407
|
+
if (existing !== undefined) {
|
|
408
|
+
throw packagingError("ROOT_INVALID", "runtime-context", "The Eden-owned runtime context already exists and cannot be reused.", "Create a fresh immutable Eve generation before assembling a runtime image.");
|
|
409
|
+
}
|
|
410
|
+
const runtimeContextParent = await assertFreshOwnedFilePath(runtimeContextPath, candidate.generationRoot, "runtime-context");
|
|
411
|
+
await mkdir(runtimeContextPath);
|
|
412
|
+
const runtimeContextDetails = await lstat(runtimeContextPath).catch(() => undefined);
|
|
413
|
+
const runtimeContextCanonical = await realpath(runtimeContextPath).catch(() => undefined);
|
|
414
|
+
if (runtimeContextDetails === undefined ||
|
|
415
|
+
!runtimeContextDetails.isDirectory() ||
|
|
416
|
+
runtimeContextDetails.isSymbolicLink() ||
|
|
417
|
+
runtimeContextCanonical === undefined ||
|
|
418
|
+
!isWithin(runtimeContextParent, resolve(runtimeContextCanonical))) {
|
|
419
|
+
throw packagingError("ROOT_INVALID", "runtime-context", "The Eden-owned runtime context could not be created as a regular contained directory.", "Create a fresh immutable Eve generation before assembling a runtime image.");
|
|
420
|
+
}
|
|
421
|
+
try {
|
|
422
|
+
await cp(join(candidate.snapshotRoot, ".output"), join(runtimeContextPath, ".output"), { recursive: true });
|
|
423
|
+
await cp(join(candidate.snapshotRoot, "node_modules"), join(runtimeContextPath, "node_modules"), { recursive: true });
|
|
424
|
+
await cp(join(candidate.snapshotRoot, "agent"), join(runtimeContextPath, "agent"), { recursive: true });
|
|
425
|
+
await cp(join(candidate.snapshotRoot, "package.json"), join(runtimeContextPath, "package.json"));
|
|
426
|
+
await rewriteContextLinks(join(candidate.snapshotRoot, "node_modules"), join(runtimeContextPath, "node_modules"));
|
|
427
|
+
await rewriteContextLinks(join(candidate.snapshotRoot, ".output"), join(runtimeContextPath, ".output"));
|
|
428
|
+
const copiedOutput = await captureRuntimeTree(join(runtimeContextPath, ".output"), runtimeContextPath, "output");
|
|
429
|
+
const copiedDependencies = await captureRuntimeTree(join(runtimeContextPath, "node_modules"), runtimeContextPath, "dependencies");
|
|
430
|
+
if (copiedOutput.digest !== closure.outputDigest ||
|
|
431
|
+
copiedDependencies.digest !== closure.dependencyDigest) {
|
|
432
|
+
throw packagingError("SOURCE_RACE", "runtime context", "The copied runtime context does not match the validated immutable closure.", "Discard the mixed-generation runtime context and rebuild from a quiescent candidate.");
|
|
433
|
+
}
|
|
434
|
+
const dockerfile = `# syntax=docker/dockerfile:1
|
|
435
|
+
FROM --platform=linux/amd64 ${imageReference(nodeImage)} AS candidate
|
|
436
|
+
WORKDIR /workspace
|
|
437
|
+
COPY .output /workspace/.output
|
|
438
|
+
COPY node_modules /workspace/node_modules
|
|
439
|
+
COPY agent /workspace/agent
|
|
440
|
+
COPY package.json /workspace/package.json
|
|
441
|
+
|
|
442
|
+
FROM --platform=linux/amd64 ${imageReference(nodeImage)} AS runtime
|
|
443
|
+
WORKDIR /workspace
|
|
444
|
+
ENV HOST=0.0.0.0 \\
|
|
445
|
+
NITRO_HOST=0.0.0.0 \\
|
|
446
|
+
PORT=8080 \\
|
|
447
|
+
NITRO_PORT=8080 \\
|
|
448
|
+
NODE_ENV=production
|
|
449
|
+
COPY --from=candidate /workspace/.output /workspace/.output
|
|
450
|
+
COPY --from=candidate /workspace/node_modules /workspace/node_modules
|
|
451
|
+
COPY --from=candidate /workspace/agent /workspace/agent
|
|
452
|
+
COPY --from=candidate /workspace/package.json /workspace/package.json
|
|
453
|
+
EXPOSE 8080
|
|
454
|
+
ENTRYPOINT ["./node_modules/.bin/eve", "start", "--host", "0.0.0.0", "--port", "8080"]
|
|
455
|
+
`;
|
|
456
|
+
const dockerignore = "";
|
|
457
|
+
const dockerfileParent = await assertFreshOwnedFilePath(dockerfilePath, candidate.generationRoot, "runtime Dockerfile");
|
|
458
|
+
await writeFile(dockerfilePath, dockerfile, {
|
|
459
|
+
flag: "wx",
|
|
460
|
+
encoding: "utf8",
|
|
461
|
+
mode: 0o600,
|
|
462
|
+
});
|
|
463
|
+
await verifyOwnedFilePath(dockerfilePath, candidate.generationRoot, dockerfileParent, "runtime Dockerfile");
|
|
464
|
+
const dockerignorePath = join(runtimeContextPath, ".dockerignore");
|
|
465
|
+
const dockerignoreParent = await assertFreshOwnedFilePath(dockerignorePath, candidate.generationRoot, "runtime .dockerignore");
|
|
466
|
+
await writeFile(dockerignorePath, dockerignore, {
|
|
467
|
+
flag: "wx",
|
|
468
|
+
encoding: "utf8",
|
|
469
|
+
mode: 0o600,
|
|
470
|
+
});
|
|
471
|
+
await verifyOwnedFilePath(dockerignorePath, candidate.generationRoot, dockerignoreParent, "runtime .dockerignore");
|
|
472
|
+
const forbiddenRuntimeConfigFile = (file) => file.path.split("/").some((segment) => segment.startsWith(".env") || segment === ".npmrc");
|
|
473
|
+
if (dockerfile.includes("RUNTIME_SECRET") ||
|
|
474
|
+
closure.files.some(forbiddenRuntimeConfigFile)) {
|
|
475
|
+
throw packagingError("SECRET_EXCLUSION_FAILED", "runtime image context", "The generated runtime context contains forbidden runtime configuration material.", "Remove runtime values and package-manager credentials before image assembly.");
|
|
476
|
+
}
|
|
477
|
+
return { runtimeContextPath, dockerfilePath };
|
|
478
|
+
}
|
|
479
|
+
catch (error) {
|
|
480
|
+
if (!(await removeOwnedRuntimeContext(runtimeContextPath, candidate.generationRoot))) {
|
|
481
|
+
throw packagingError("CLEANUP_UNVERIFIED", "runtime context", "The owned runtime context could not be removed with exact containment proof.", "Inspect only the recorded runtime context path; no broad filesystem cleanup was attempted.");
|
|
482
|
+
}
|
|
483
|
+
throw error;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
async function removeOwnedRuntimeContext(runtimeContextPath, generationRoot) {
|
|
487
|
+
let details;
|
|
488
|
+
try {
|
|
489
|
+
details = await lstat(runtimeContextPath);
|
|
490
|
+
}
|
|
491
|
+
catch (error) {
|
|
492
|
+
return typeof error === "object" &&
|
|
493
|
+
error !== null &&
|
|
494
|
+
"code" in error &&
|
|
495
|
+
error.code === "ENOENT";
|
|
496
|
+
}
|
|
497
|
+
if (details === undefined)
|
|
498
|
+
return true;
|
|
499
|
+
if (!details.isDirectory() || details.isSymbolicLink())
|
|
500
|
+
return false;
|
|
501
|
+
let canonical;
|
|
502
|
+
let canonicalGenerationRoot;
|
|
503
|
+
try {
|
|
504
|
+
canonical = await realpath(runtimeContextPath);
|
|
505
|
+
canonicalGenerationRoot = await realpath(generationRoot);
|
|
506
|
+
}
|
|
507
|
+
catch {
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
if (canonical === undefined ||
|
|
511
|
+
!isWithin(resolve(canonicalGenerationRoot), resolve(canonical))) {
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
try {
|
|
515
|
+
await rm(runtimeContextPath, { recursive: true, force: false });
|
|
516
|
+
}
|
|
517
|
+
catch {
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
try {
|
|
521
|
+
await lstat(runtimeContextPath);
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
524
|
+
catch (error) {
|
|
525
|
+
return typeof error === "object" &&
|
|
526
|
+
error !== null &&
|
|
527
|
+
"code" in error &&
|
|
528
|
+
error.code === "ENOENT";
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
function safeDockerEnvironment() {
|
|
532
|
+
return {
|
|
533
|
+
PATH: process.env.PATH,
|
|
534
|
+
HOME: process.env.HOME,
|
|
535
|
+
TMPDIR: process.env.TMPDIR,
|
|
536
|
+
LANG: process.env.LANG,
|
|
537
|
+
LC_ALL: process.env.LC_ALL,
|
|
538
|
+
DOCKER_HOST: process.env.DOCKER_HOST,
|
|
539
|
+
DOCKER_CONTEXT: process.env.DOCKER_CONTEXT,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
async function docker(state, args, options = {}) {
|
|
543
|
+
return execFileAsync(state.command, [...args], {
|
|
544
|
+
env: state.env,
|
|
545
|
+
cwd: state.generationRoot,
|
|
546
|
+
maxBuffer: options.maxBuffer ?? 4 * 1024 * 1024,
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
async function dockerWithInput(state, args, input) {
|
|
550
|
+
return new Promise((resolveResult, rejectResult) => {
|
|
551
|
+
const child = spawn(state.command, [...args], {
|
|
552
|
+
cwd: state.generationRoot,
|
|
553
|
+
env: state.env,
|
|
554
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
555
|
+
});
|
|
556
|
+
let stdout = "";
|
|
557
|
+
let stderr = "";
|
|
558
|
+
child.stdout.on("data", (chunk) => {
|
|
559
|
+
stdout += chunk.toString();
|
|
560
|
+
});
|
|
561
|
+
child.stderr.on("data", (chunk) => {
|
|
562
|
+
stderr += chunk.toString();
|
|
563
|
+
});
|
|
564
|
+
child.once("error", rejectResult);
|
|
565
|
+
child.once("close", (code) => {
|
|
566
|
+
if (code === 0) {
|
|
567
|
+
resolveResult({ stdout, stderr });
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
const error = new Error(stderr || "The Docker operation failed.");
|
|
571
|
+
Object.assign(error, { stdout, stderr, code });
|
|
572
|
+
rejectResult(error);
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
child.stdin.end(input);
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
function runtimeContainerEnvironment(request) {
|
|
579
|
+
return {
|
|
580
|
+
...HOST_ENVIRONMENT,
|
|
581
|
+
...(request.publicOrigin === undefined
|
|
582
|
+
? {}
|
|
583
|
+
: { WORKFLOW_LOCAL_BASE_URL: request.publicOrigin }),
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
function validateHealthPort(port) {
|
|
587
|
+
if (!Number.isInteger(port) || port < 4310 || port > 4399) {
|
|
588
|
+
throw packagingError("DOCKER_PLATFORM_BLOCKED", "health port", "The local Eve health probe must use a task-owned host port in 4310-4399.", "Choose an unused task-owned host port between 4310 and 4399.");
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
function validateHealthTiming(timeoutMs, pollIntervalMs) {
|
|
592
|
+
if (!Number.isFinite(timeoutMs) ||
|
|
593
|
+
timeoutMs <= 0 ||
|
|
594
|
+
!Number.isFinite(pollIntervalMs) ||
|
|
595
|
+
pollIntervalMs <= 0) {
|
|
596
|
+
throw packagingError("EVE_HEALTH_FAILED", "health deadline", "The Eve health probe requires finite positive timeout and polling intervals.", "Use a bounded positive health timeout and polling interval.");
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
async function recoverImageIdentity(state) {
|
|
600
|
+
const fromIid = await readFile(state.imageIdPath, "utf8")
|
|
601
|
+
.then((value) => value.trim())
|
|
602
|
+
.catch(() => "");
|
|
603
|
+
if (IMAGE_ID_PATTERN.test(fromIid))
|
|
604
|
+
return fromIid;
|
|
605
|
+
const listed = await docker(state, [
|
|
606
|
+
"image",
|
|
607
|
+
"ls",
|
|
608
|
+
"--no-trunc",
|
|
609
|
+
"--filter",
|
|
610
|
+
`label=${state.imageLabel}`,
|
|
611
|
+
"--format",
|
|
612
|
+
"{{.ID}}",
|
|
613
|
+
]).catch(() => undefined);
|
|
614
|
+
const matches = (listed?.stdout ?? "")
|
|
615
|
+
.split(/\r?\n/u)
|
|
616
|
+
.map((value) => value.trim())
|
|
617
|
+
.filter((value) => IMAGE_ID_PATTERN.test(value));
|
|
618
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
619
|
+
}
|
|
620
|
+
async function validateImageMetadata(state, imageId) {
|
|
621
|
+
const details = await docker(state, [
|
|
622
|
+
"image",
|
|
623
|
+
"inspect",
|
|
624
|
+
imageId,
|
|
625
|
+
"--format",
|
|
626
|
+
"{{.Id}} {{.Os}} {{.Architecture}}",
|
|
627
|
+
]);
|
|
628
|
+
const [actualId, os, architecture] = details.stdout.trim().split(/\s+/u);
|
|
629
|
+
if (actualId !== imageId || os !== "linux" || architecture !== "amd64") {
|
|
630
|
+
throw packagingError("DOCKER_PLATFORM_BLOCKED", "linux/amd64 image", "The runtime image did not report the required Linux/amd64 identity.", "Use a Docker/OrbStack builder that can inspect a Linux/amd64 image before retaining it.");
|
|
631
|
+
}
|
|
632
|
+
const entrypoint = await docker(state, [
|
|
633
|
+
"image",
|
|
634
|
+
"inspect",
|
|
635
|
+
imageId,
|
|
636
|
+
"--format",
|
|
637
|
+
"{{json .Config.Entrypoint}}",
|
|
638
|
+
]);
|
|
639
|
+
if (entrypoint.stdout.trim() !== JSON.stringify([...START_COMMAND])) {
|
|
640
|
+
throw packagingError("UNSUPPORTED_EVE_OUTPUT", "Container launch command", "The runtime image does not launch the project-local Eve start supervisor with the required host and port.", "Use ./node_modules/.bin/eve start --host 0.0.0.0 --port 8080 as the image entrypoint.");
|
|
641
|
+
}
|
|
642
|
+
const workingDirectory = await docker(state, [
|
|
643
|
+
"image",
|
|
644
|
+
"inspect",
|
|
645
|
+
imageId,
|
|
646
|
+
"--format",
|
|
647
|
+
"{{.Config.WorkingDir}}",
|
|
648
|
+
]);
|
|
649
|
+
if (workingDirectory.stdout.trim() !== "/workspace") {
|
|
650
|
+
throw packagingError("UNSUPPORTED_EVE_OUTPUT", "Container working directory", "The runtime image does not use /workspace as its self-contained working directory.", "Build the runtime image with the generated Eve output and start closure under /workspace.");
|
|
651
|
+
}
|
|
652
|
+
const environment = await docker(state, [
|
|
653
|
+
"image",
|
|
654
|
+
"inspect",
|
|
655
|
+
imageId,
|
|
656
|
+
"--format",
|
|
657
|
+
"{{json .Config.Env}}",
|
|
658
|
+
]);
|
|
659
|
+
let values;
|
|
660
|
+
try {
|
|
661
|
+
values = JSON.parse(environment.stdout.trim());
|
|
662
|
+
}
|
|
663
|
+
catch {
|
|
664
|
+
throw packagingError("DOCKER_PLATFORM_BLOCKED", "runtime environment", "The runtime image environment could not be inspected safely.", "Use a Docker builder that returns JSON image configuration metadata.");
|
|
665
|
+
}
|
|
666
|
+
if (!Array.isArray(values) ||
|
|
667
|
+
Object.entries(HOST_ENVIRONMENT).some(([name, value]) => !values.includes(`${name}=${value}`))) {
|
|
668
|
+
throw packagingError("UNSUPPORTED_EVE_OUTPUT", "host environment", "The runtime image is missing one or more required host-owned Eve variables.", "Retain HOST, NITRO_HOST, PORT, NITRO_PORT, and NODE_ENV in the final image.");
|
|
669
|
+
}
|
|
670
|
+
const history = await docker(state, ["history", "--no-trunc", imageId]);
|
|
671
|
+
if (/(?:\.env|\.npmrc|credentials|secret|token|password)/iu.test(history.stdout)) {
|
|
672
|
+
throw packagingError("SECRET_EXCLUSION_FAILED", "image history", "The runtime image history contains a credential or runtime environment marker.", "Rebuild without runtime values, credentials, or secret-bearing Docker arguments.");
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
async function removeOwnedContainer(state) {
|
|
676
|
+
if (state.containerId === undefined)
|
|
677
|
+
return true;
|
|
678
|
+
let verified = true;
|
|
679
|
+
await docker(state, ["stop", "--time", "5", state.containerId]).catch(() => {
|
|
680
|
+
verified = false;
|
|
681
|
+
});
|
|
682
|
+
await docker(state, ["rm", "--force", state.containerId]).catch(() => {
|
|
683
|
+
verified = false;
|
|
684
|
+
});
|
|
685
|
+
let remaining;
|
|
686
|
+
try {
|
|
687
|
+
remaining = await docker(state, [
|
|
688
|
+
"container",
|
|
689
|
+
"inspect",
|
|
690
|
+
state.containerId,
|
|
691
|
+
"--format",
|
|
692
|
+
"{{.Id}}",
|
|
693
|
+
]);
|
|
694
|
+
}
|
|
695
|
+
catch (error) {
|
|
696
|
+
const stderr = typeof error === "object" &&
|
|
697
|
+
error !== null &&
|
|
698
|
+
"stderr" in error &&
|
|
699
|
+
typeof error.stderr === "string"
|
|
700
|
+
? error.stderr
|
|
701
|
+
: "";
|
|
702
|
+
if (!/(?:no such|not found|does not exist)/iu.test(stderr)) {
|
|
703
|
+
verified = false;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
if (remaining !== undefined && remaining.stdout.trim().length > 0) {
|
|
707
|
+
verified = false;
|
|
708
|
+
}
|
|
709
|
+
return verified;
|
|
710
|
+
}
|
|
711
|
+
async function removeOwnedImage(state) {
|
|
712
|
+
if (!state.imageBuilt)
|
|
713
|
+
return true;
|
|
714
|
+
if (state.imageId === undefined)
|
|
715
|
+
return false;
|
|
716
|
+
let verified = true;
|
|
717
|
+
await docker(state, ["image", "rm", "--force", state.imageId]).catch(() => {
|
|
718
|
+
verified = false;
|
|
719
|
+
});
|
|
720
|
+
let remaining;
|
|
721
|
+
try {
|
|
722
|
+
remaining = await docker(state, [
|
|
723
|
+
"image",
|
|
724
|
+
"inspect",
|
|
725
|
+
state.imageId,
|
|
726
|
+
"--format",
|
|
727
|
+
"{{.Id}}",
|
|
728
|
+
]);
|
|
729
|
+
}
|
|
730
|
+
catch (error) {
|
|
731
|
+
const stderr = typeof error === "object" &&
|
|
732
|
+
error !== null &&
|
|
733
|
+
"stderr" in error &&
|
|
734
|
+
typeof error.stderr === "string"
|
|
735
|
+
? error.stderr
|
|
736
|
+
: "";
|
|
737
|
+
if (!/(?:no such|not found|does not exist)/iu.test(stderr)) {
|
|
738
|
+
verified = false;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
if (remaining !== undefined && remaining.stdout.trim().length > 0) {
|
|
742
|
+
verified = false;
|
|
743
|
+
}
|
|
744
|
+
return verified;
|
|
745
|
+
}
|
|
746
|
+
function healthReady(value) {
|
|
747
|
+
if (value === "ready")
|
|
748
|
+
return true;
|
|
749
|
+
if (typeof value !== "object" || value === null)
|
|
750
|
+
return false;
|
|
751
|
+
if (Array.isArray(value))
|
|
752
|
+
return value.some((item) => healthReady(item));
|
|
753
|
+
const record = value;
|
|
754
|
+
if (record.ready === true ||
|
|
755
|
+
record.healthy === true ||
|
|
756
|
+
record.status === "ready" ||
|
|
757
|
+
record.state === "ready" ||
|
|
758
|
+
record.health === "ready") {
|
|
759
|
+
return true;
|
|
760
|
+
}
|
|
761
|
+
return false;
|
|
762
|
+
}
|
|
763
|
+
async function pollEveHealth(port, timeoutMs, pollIntervalMs, fetchHealth) {
|
|
764
|
+
const url = `http://127.0.0.1:${port}${HEALTH_PATH}`;
|
|
765
|
+
const deadline = Date.now() + timeoutMs;
|
|
766
|
+
let lastStatus = "unreachable";
|
|
767
|
+
while (Date.now() <= deadline) {
|
|
768
|
+
try {
|
|
769
|
+
const response = await fetchHealth(url, {
|
|
770
|
+
method: "GET",
|
|
771
|
+
signal: AbortSignal.timeout(Math.max(1, Math.min(5000, timeoutMs))),
|
|
772
|
+
});
|
|
773
|
+
const body = await response.text();
|
|
774
|
+
lastStatus = `${response.status}`;
|
|
775
|
+
let parsed;
|
|
776
|
+
try {
|
|
777
|
+
parsed = JSON.parse(body);
|
|
778
|
+
}
|
|
779
|
+
catch {
|
|
780
|
+
parsed = body;
|
|
781
|
+
}
|
|
782
|
+
if (response.ok && healthReady(parsed))
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
catch {
|
|
786
|
+
lastStatus = "unreachable";
|
|
787
|
+
}
|
|
788
|
+
if (Date.now() + pollIntervalMs > deadline)
|
|
789
|
+
break;
|
|
790
|
+
await new Promise((resolvePromise) => {
|
|
791
|
+
setTimeout(resolvePromise, pollIntervalMs);
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
throw packagingError("EVE_HEALTH_FAILED", HEALTH_PATH, `The real Eve health route did not report ready before the bounded deadline (last status: ${lastStatus}).`, "Inspect the project-local eve start supervisor, Nitro child, sandbox prewarm, and Workflow World startup without replacing them with a synthetic listener.");
|
|
795
|
+
}
|
|
796
|
+
function errorRecord(error) {
|
|
797
|
+
return {
|
|
798
|
+
code: error.code,
|
|
799
|
+
subject: error.subject,
|
|
800
|
+
reason: error.message,
|
|
801
|
+
remediation: error.remediation,
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
function blockedResult(candidate, error, cleanup, writtenPaths) {
|
|
805
|
+
return {
|
|
806
|
+
schemaVersion: 1,
|
|
807
|
+
worker: "eve-packaging-worker",
|
|
808
|
+
operation: "runtime-image",
|
|
809
|
+
status: "blocked",
|
|
810
|
+
returnCode: error.code,
|
|
811
|
+
deployable: false,
|
|
812
|
+
candidate,
|
|
813
|
+
image: null,
|
|
814
|
+
runtime: null,
|
|
815
|
+
cleanup,
|
|
816
|
+
checks: [{
|
|
817
|
+
id: error.code,
|
|
818
|
+
status: "blocked",
|
|
819
|
+
subject: error.subject,
|
|
820
|
+
reason: error.message,
|
|
821
|
+
remediation: error.remediation,
|
|
822
|
+
}],
|
|
823
|
+
candidateImageId: null,
|
|
824
|
+
candidateImageRetainedLocally: false,
|
|
825
|
+
writtenPaths,
|
|
826
|
+
error: errorRecord(error),
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
export async function buildEveRuntimeImage(request) {
|
|
830
|
+
const dockerCommand = request.dockerCommand ?? "docker";
|
|
831
|
+
const healthPort = request.healthPort;
|
|
832
|
+
const timeoutMs = request.healthTimeoutMs ?? 30_000;
|
|
833
|
+
const pollIntervalMs = request.healthPollIntervalMs ?? 100;
|
|
834
|
+
const retainImage = request.retainImage ?? true;
|
|
835
|
+
const writtenPaths = [];
|
|
836
|
+
let imageIdentity = "indeterminate";
|
|
837
|
+
let imageRetained = false;
|
|
838
|
+
let cleanupVerified = true;
|
|
839
|
+
let containerRemoved = true;
|
|
840
|
+
let bootContainerId = null;
|
|
841
|
+
let runtimeContextPath;
|
|
842
|
+
const state = {
|
|
843
|
+
command: dockerCommand,
|
|
844
|
+
env: safeDockerEnvironment(),
|
|
845
|
+
generationRoot: request.candidate.generationRoot,
|
|
846
|
+
imageIdPath: join(request.candidate.generationRoot, "runtime-image-id"),
|
|
847
|
+
imageLabel: `eden.eve.generation=${request.candidate.generationId}`,
|
|
848
|
+
imageBuilt: false,
|
|
849
|
+
imageId: undefined,
|
|
850
|
+
containerId: undefined,
|
|
851
|
+
imageRetained: false,
|
|
852
|
+
};
|
|
853
|
+
try {
|
|
854
|
+
validateHealthPort(healthPort);
|
|
855
|
+
validateHealthTiming(timeoutMs, pollIntervalMs);
|
|
856
|
+
if (request.hostRequirements === undefined) {
|
|
857
|
+
throw packagingError("UNSUPPORTED_HOST_REQUIREMENT", "host requirements", "The Eve project host requirements were not proven for the runtime image.", "Provide an explicit host-capability result for Linux/amd64, World, sandbox, process, network, and disposable storage behavior.");
|
|
858
|
+
}
|
|
859
|
+
validateEveHostRequirements(request.hostRequirements);
|
|
860
|
+
const closure = await validateCandidateClosure(request.candidate);
|
|
861
|
+
const context = await writeRuntimeContext(request.candidate, request.nodeImage, closure);
|
|
862
|
+
runtimeContextPath = context.runtimeContextPath;
|
|
863
|
+
writtenPaths.push(context.runtimeContextPath, context.dockerfilePath);
|
|
864
|
+
await docker(state, ["version", "--format", "{{.Server.Version}}"]);
|
|
865
|
+
state.imageBuilt = true;
|
|
866
|
+
await docker(state, [
|
|
867
|
+
"build",
|
|
868
|
+
"--platform=linux/amd64",
|
|
869
|
+
"--file",
|
|
870
|
+
context.dockerfilePath,
|
|
871
|
+
"--iidfile",
|
|
872
|
+
state.imageIdPath,
|
|
873
|
+
"--label",
|
|
874
|
+
state.imageLabel,
|
|
875
|
+
context.runtimeContextPath,
|
|
876
|
+
]);
|
|
877
|
+
const postBuildOutput = await captureRuntimeTree(join(context.runtimeContextPath, ".output"), context.runtimeContextPath, "output");
|
|
878
|
+
const postBuildDependencies = await captureRuntimeTree(join(context.runtimeContextPath, "node_modules"), context.runtimeContextPath, "dependencies");
|
|
879
|
+
if (postBuildOutput.digest !== closure.outputDigest ||
|
|
880
|
+
postBuildDependencies.digest !== closure.dependencyDigest) {
|
|
881
|
+
throw packagingError("SOURCE_RACE", "runtime context", "The runtime context changed while the immutable Linux/amd64 image was being built.", "Discard the mixed-generation runtime image and rebuild from a quiescent candidate.");
|
|
882
|
+
}
|
|
883
|
+
state.imageId = await recoverImageIdentity(state);
|
|
884
|
+
if (state.imageId === undefined) {
|
|
885
|
+
throw packagingError("CLEANUP_UNVERIFIED", "runtime image identity", "The runtime image build completed but its immutable identity could not be recovered.", "Use a Docker/OrbStack builder that preserves an iidfile or one exact generation label; cleanup is indeterminate and no broad image deletion was attempted.");
|
|
886
|
+
}
|
|
887
|
+
imageIdentity = "exact";
|
|
888
|
+
await validateImageMetadata(state, state.imageId);
|
|
889
|
+
const containerName = `eden-eve-boot-${request.candidate.generationId}`;
|
|
890
|
+
const runtimeEnvironment = request.runtimeInjection === undefined
|
|
891
|
+
? runtimeContainerEnvironment(request)
|
|
892
|
+
: await request.runtimeInjection.runLocal({
|
|
893
|
+
cwd: request.candidate.generationRoot,
|
|
894
|
+
hostEnvironment: runtimeContainerEnvironment(request),
|
|
895
|
+
run: (startRequest) => startRequest.env,
|
|
896
|
+
});
|
|
897
|
+
const bootArgs = [
|
|
898
|
+
"run",
|
|
899
|
+
"--detach",
|
|
900
|
+
"--name",
|
|
901
|
+
containerName,
|
|
902
|
+
"--label",
|
|
903
|
+
state.imageLabel,
|
|
904
|
+
"--publish",
|
|
905
|
+
`${healthPort}:${INTERNAL_PORT}`,
|
|
906
|
+
...(request.runtimeInjection === undefined
|
|
907
|
+
? Object.entries(runtimeEnvironment).flatMap(([name, value]) => [
|
|
908
|
+
"--env",
|
|
909
|
+
`${name}=${value}`,
|
|
910
|
+
])
|
|
911
|
+
: ["--env-file", "/dev/stdin"]),
|
|
912
|
+
state.imageId,
|
|
913
|
+
];
|
|
914
|
+
const boot = request.runtimeInjection === undefined
|
|
915
|
+
? await docker(state, bootArgs)
|
|
916
|
+
: await dockerWithInput(state, bootArgs, `${Object.entries(runtimeEnvironment)
|
|
917
|
+
.map(([name, value]) => `${name}=${value}`)
|
|
918
|
+
.join("\n")}\n`);
|
|
919
|
+
state.containerId = boot.stdout.trim();
|
|
920
|
+
bootContainerId = state.containerId;
|
|
921
|
+
if (state.containerId.length === 0) {
|
|
922
|
+
throw packagingError("DOCKER_PLATFORM_BLOCKED", "boot container identity", "The Docker runtime did not return an owned boot-container identity.", "Use a Docker/OrbStack runtime that returns a stable container ID for exact lifecycle cleanup.");
|
|
923
|
+
}
|
|
924
|
+
const processTree = await docker(state, ["top", state.containerId]);
|
|
925
|
+
if (!processTree.stdout.includes("eve") ||
|
|
926
|
+
!processTree.stdout.includes("start") ||
|
|
927
|
+
!processTree.stdout.includes("--host") ||
|
|
928
|
+
!processTree.stdout.includes("0.0.0.0") ||
|
|
929
|
+
!processTree.stdout.includes("--port") ||
|
|
930
|
+
!processTree.stdout.includes("8080")) {
|
|
931
|
+
throw packagingError("UNSUPPORTED_EVE_OUTPUT", "eve start process", "The booted image process is not the project-local Eve start supervisor.", "Start the image with ./node_modules/.bin/eve start --host 0.0.0.0 --port 8080.");
|
|
932
|
+
}
|
|
933
|
+
await docker(state, [
|
|
934
|
+
"exec",
|
|
935
|
+
state.containerId,
|
|
936
|
+
"sh",
|
|
937
|
+
"-ceu",
|
|
938
|
+
"test -f /workspace/.output/server/index.mjs && test -x /workspace/node_modules/.bin/eve",
|
|
939
|
+
]);
|
|
940
|
+
await docker(state, [
|
|
941
|
+
"exec",
|
|
942
|
+
state.containerId,
|
|
943
|
+
"sh",
|
|
944
|
+
"-ceu",
|
|
945
|
+
"set -o noglob; status=0; for module in $(find /workspace/node_modules -type f -name '*.node'); do if ldd \"$module\" 2>&1 | grep -q 'not found'; then echo \"unloadable native module: $module\" >&2; status=1; fi; done; exit $status",
|
|
946
|
+
]);
|
|
947
|
+
await pollEveHealth(healthPort, timeoutMs, pollIntervalMs, request.fetchHealth ?? fetch);
|
|
948
|
+
await docker(state, [
|
|
949
|
+
"exec",
|
|
950
|
+
state.containerId,
|
|
951
|
+
"sh",
|
|
952
|
+
"-ceu",
|
|
953
|
+
"awk '$2 ~ /^00000000:1F90/ { found=1 } END { exit found ? 0 : 1 }' /proc/net/tcp /proc/net/tcp6",
|
|
954
|
+
]);
|
|
955
|
+
await docker(state, [
|
|
956
|
+
"exec",
|
|
957
|
+
state.containerId,
|
|
958
|
+
"sh",
|
|
959
|
+
"-ceu",
|
|
960
|
+
"test -z \"$(find /app -type f \\( -name '.env*' -o -name '.npmrc' -o -name '.pnpmrc' -o -name '*.pem' -o -name '*.key' -o -name 'credentials.json' -o -name 'service-account.json' \\) -print -quit)\"",
|
|
961
|
+
]);
|
|
962
|
+
imageRetained = retainImage;
|
|
963
|
+
state.imageRetained = retainImage;
|
|
964
|
+
containerRemoved = await removeOwnedContainer(state);
|
|
965
|
+
state.containerId = undefined;
|
|
966
|
+
cleanupVerified = containerRemoved;
|
|
967
|
+
if (!cleanupVerified) {
|
|
968
|
+
throw packagingError("CLEANUP_UNVERIFIED", "boot container", "The owned Eve boot container could not be stopped and verified absent.", "Inspect only the recorded boot-container identity; no broad Docker cleanup was attempted.");
|
|
969
|
+
}
|
|
970
|
+
if (!imageRetained) {
|
|
971
|
+
cleanupVerified = (await removeOwnedImage(state)) && cleanupVerified;
|
|
972
|
+
if (!cleanupVerified) {
|
|
973
|
+
throw packagingError("CLEANUP_UNVERIFIED", "runtime image", "The disposable Eve runtime image could not be removed and verified absent.", "Inspect only the recorded runtime image identity; no broad Docker cleanup was attempted.");
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
const contextRemoved = await removeOwnedRuntimeContext(context.runtimeContextPath, request.candidate.generationRoot);
|
|
977
|
+
if (!contextRemoved) {
|
|
978
|
+
throw packagingError("CLEANUP_UNVERIFIED", "runtime context", "The owned runtime context could not be removed with exact containment proof.", "Inspect only the recorded runtime context path; no broad filesystem cleanup was attempted.");
|
|
979
|
+
}
|
|
980
|
+
runtimeContextPath = undefined;
|
|
981
|
+
const runtimeManifestPath = join(request.candidate.generationRoot, "runtime-image-manifest.json");
|
|
982
|
+
await writeFile(runtimeManifestPath, `${JSON.stringify({
|
|
983
|
+
version: 1,
|
|
984
|
+
generationId: request.candidate.generationId,
|
|
985
|
+
platform: "linux/amd64",
|
|
986
|
+
imageId: state.imageId,
|
|
987
|
+
imageDigest: state.imageId,
|
|
988
|
+
launchCommand: START_COMMAND,
|
|
989
|
+
...(request.publicOrigin === undefined
|
|
990
|
+
? {}
|
|
991
|
+
: { publicOrigin: request.publicOrigin }),
|
|
992
|
+
applicationArtifact: request.candidate.generatedOutput.entrypointPath,
|
|
993
|
+
runtimeClosure: closure,
|
|
994
|
+
health: {
|
|
995
|
+
method: "GET",
|
|
996
|
+
path: HEALTH_PATH,
|
|
997
|
+
status: "ready",
|
|
998
|
+
},
|
|
999
|
+
hostEnvironment: HOST_ENVIRONMENT,
|
|
1000
|
+
durableLocalFilesystemClaim: false,
|
|
1001
|
+
})}\n`, { flag: "wx", encoding: "utf8", mode: 0o600 });
|
|
1002
|
+
writtenPaths.push(runtimeManifestPath, state.imageIdPath);
|
|
1003
|
+
const image = {
|
|
1004
|
+
dockerfilePath: context.dockerfilePath,
|
|
1005
|
+
runtimeContextPath: context.runtimeContextPath,
|
|
1006
|
+
platform: "linux/amd64",
|
|
1007
|
+
builderImage: imageReference(request.nodeImage),
|
|
1008
|
+
runtimeImage: imageReference(request.nodeImage),
|
|
1009
|
+
imageId: state.imageId,
|
|
1010
|
+
imageReference: state.imageId,
|
|
1011
|
+
imageDigest: state.imageId,
|
|
1012
|
+
launchCommand: START_COMMAND,
|
|
1013
|
+
workingDirectory: "/workspace",
|
|
1014
|
+
hostEnvironment: HOST_ENVIRONMENT,
|
|
1015
|
+
...(request.publicOrigin === undefined
|
|
1016
|
+
? {}
|
|
1017
|
+
: { publicOrigin: request.publicOrigin }),
|
|
1018
|
+
generatedOutput: request.candidate.generatedOutput,
|
|
1019
|
+
runtimeClosure: {
|
|
1020
|
+
root: "/workspace/node_modules",
|
|
1021
|
+
outputDigest: closure.outputDigest,
|
|
1022
|
+
dependencyDigest: closure.dependencyDigest,
|
|
1023
|
+
digest: closure.digest,
|
|
1024
|
+
files: closure.files,
|
|
1025
|
+
nativeModules: closure.nativeModules,
|
|
1026
|
+
eveStartClosureRetained: true,
|
|
1027
|
+
builderOnlyMaterialExcluded: true,
|
|
1028
|
+
},
|
|
1029
|
+
};
|
|
1030
|
+
return {
|
|
1031
|
+
schemaVersion: 1,
|
|
1032
|
+
worker: "eve-packaging-worker",
|
|
1033
|
+
operation: "runtime-image",
|
|
1034
|
+
status: "ready",
|
|
1035
|
+
returnCode: "EVE_PACKAGE_READY",
|
|
1036
|
+
deployable: true,
|
|
1037
|
+
candidate: request.candidate,
|
|
1038
|
+
image,
|
|
1039
|
+
runtime: {
|
|
1040
|
+
listenHost: "0.0.0.0",
|
|
1041
|
+
listenPort: INTERNAL_PORT,
|
|
1042
|
+
healthMethod: "GET",
|
|
1043
|
+
healthPath: HEALTH_PATH,
|
|
1044
|
+
healthStatus: "ready",
|
|
1045
|
+
healthVerified: true,
|
|
1046
|
+
healthVerifiedAt: new Date().toISOString(),
|
|
1047
|
+
durableLocalFilesystemClaim: false,
|
|
1048
|
+
},
|
|
1049
|
+
cleanup: {
|
|
1050
|
+
bootContainerId,
|
|
1051
|
+
bootContainerRemoved: true,
|
|
1052
|
+
imageIdentity,
|
|
1053
|
+
imageRetained: imageRetained,
|
|
1054
|
+
verified: cleanupVerified,
|
|
1055
|
+
},
|
|
1056
|
+
checks: [
|
|
1057
|
+
{
|
|
1058
|
+
id: "VAL-BUILD-005",
|
|
1059
|
+
status: "pass",
|
|
1060
|
+
subject: "runtime image",
|
|
1061
|
+
reason: "The runtime image reports Linux/amd64 and contains the generated Eve output and runtime closure.",
|
|
1062
|
+
remediation: null,
|
|
1063
|
+
},
|
|
1064
|
+
{
|
|
1065
|
+
id: "VAL-BUILD-006",
|
|
1066
|
+
status: "pass",
|
|
1067
|
+
subject: "eve start",
|
|
1068
|
+
reason: "The actual boot process is the project-local Eve start supervisor with host 0.0.0.0 and port 8080.",
|
|
1069
|
+
remediation: null,
|
|
1070
|
+
},
|
|
1071
|
+
{
|
|
1072
|
+
id: "VAL-BUILD-007",
|
|
1073
|
+
status: "pass",
|
|
1074
|
+
subject: "host requirements",
|
|
1075
|
+
reason: "World, sandbox, process, network, architecture, and disposable-storage requirements are supported.",
|
|
1076
|
+
remediation: null,
|
|
1077
|
+
},
|
|
1078
|
+
],
|
|
1079
|
+
candidateImageId: state.imageId,
|
|
1080
|
+
candidateImageRetainedLocally: imageRetained,
|
|
1081
|
+
writtenPaths,
|
|
1082
|
+
error: null,
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
catch (error) {
|
|
1086
|
+
if (runtimeContextPath !== undefined) {
|
|
1087
|
+
const contextRemoved = await removeOwnedRuntimeContext(runtimeContextPath, request.candidate.generationRoot);
|
|
1088
|
+
cleanupVerified = contextRemoved && cleanupVerified;
|
|
1089
|
+
runtimeContextPath = undefined;
|
|
1090
|
+
}
|
|
1091
|
+
if (state.containerId !== undefined) {
|
|
1092
|
+
containerRemoved = await removeOwnedContainer(state);
|
|
1093
|
+
state.containerId = undefined;
|
|
1094
|
+
}
|
|
1095
|
+
if (!imageRetained && state.imageBuilt) {
|
|
1096
|
+
if (state.imageId === undefined) {
|
|
1097
|
+
state.imageId = await recoverImageIdentity(state);
|
|
1098
|
+
if (state.imageId === undefined) {
|
|
1099
|
+
cleanupVerified = false;
|
|
1100
|
+
}
|
|
1101
|
+
else {
|
|
1102
|
+
imageIdentity = "exact";
|
|
1103
|
+
cleanupVerified = (await removeOwnedImage(state)) && cleanupVerified;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
else {
|
|
1107
|
+
cleanupVerified = (await removeOwnedImage(state)) && cleanupVerified;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
const cleanup = {
|
|
1111
|
+
bootContainerId,
|
|
1112
|
+
bootContainerRemoved: containerRemoved,
|
|
1113
|
+
imageIdentity,
|
|
1114
|
+
imageRetained,
|
|
1115
|
+
verified: cleanupVerified,
|
|
1116
|
+
};
|
|
1117
|
+
if (error instanceof EvePackagingError) {
|
|
1118
|
+
if (error.code === "CLEANUP_UNVERIFIED" || !cleanupVerified) {
|
|
1119
|
+
return blockedResult(request.candidate, error.code === "CLEANUP_UNVERIFIED"
|
|
1120
|
+
? error
|
|
1121
|
+
: packagingError("CLEANUP_UNVERIFIED", "owned Docker resources", "Owned runtime-image resources could not be verified absent.", "Inspect only the recorded image and boot-container identities; no broad Docker cleanup was attempted."), cleanup, writtenPaths);
|
|
1122
|
+
}
|
|
1123
|
+
return blockedResult(request.candidate, error, cleanup, writtenPaths);
|
|
1124
|
+
}
|
|
1125
|
+
const stderrText = typeof error.stderr === "string"
|
|
1126
|
+
? error.stderr
|
|
1127
|
+
: "";
|
|
1128
|
+
const fallback = packagingError("DOCKER_PLATFORM_BLOCKED", "runtime image", error instanceof Error && error.message.length > 0
|
|
1129
|
+
? `The Linux/amd64 Eve runtime image could not be built or inspected safely: ${[error.message, stderrText].join(" ").trim().slice(0, 600)}`
|
|
1130
|
+
: "The Linux/amd64 Eve runtime image could not be built or inspected safely.", "Retry with Docker/OrbStack BuildKit and preserve the exact candidate inputs.");
|
|
1131
|
+
return blockedResult(request.candidate, fallback, cleanup, writtenPaths);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
//# sourceMappingURL=eve-runtime-image.js.map
|