@moinulmoin/eden 0.6.0 → 0.7.1

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.
@@ -1,9 +1,12 @@
1
1
  import { createHash, } from "node:crypto";
2
2
  import { execFile, } from "node:child_process";
3
- import { chmod, lstat, mkdir, readFile, readlink, readdir, realpath, writeFile, } from "node:fs/promises";
3
+ import { chmod, lstat, mkdir, readFile, readlink, readdir, realpath, rm, writeFile, } from "node:fs/promises";
4
4
  import { promisify, } from "node:util";
5
+ import { EVE_HOST_ENVIRONMENT as HOST_ENVIRONMENT, EVE_START_COMMAND as START_COMMAND, } from "./eve-runtime-config.js";
5
6
  import { basename, dirname, isAbsolute, join, relative, resolve, } from "node:path";
6
7
  const execFileAsync = promisify(execFile);
8
+ /** Snapshot-relative root of Eve's prepared sandbox templates. */
9
+ export const EVE_SANDBOX_CACHE_ROOT = ".eve/sandbox-cache";
7
10
  export class EvePackagingError extends Error {
8
11
  code;
9
12
  subject;
@@ -27,6 +30,137 @@ export class EvePackagingError extends Error {
27
30
  // exempt from the ldd check, and the runtime stage sets
28
31
  // CBOR_NATIVE_ACCELERATION_DISABLED.
29
32
  const IGNORED_BUILDS_GUARD = `node -e 'const m=JSON.parse(require("fs").readFileSync("node_modules/.modules.yaml","utf8"));const b=(m.ignoredBuilds||[]).filter((p)=>!p.startsWith("cbor-extract@"));if(b.length){console.error("ERR_PNPM_IGNORED_BUILDS Ignored build scripts: "+b.join(", "));process.exit(1)}'`;
33
+ /**
34
+ * Eden's temporary fix for upstream https://github.com/vercel/eve/issues/4440
35
+ * (repro: https://github.com/moinulmoin/eve-justbash-resume-repro).
36
+ * Eve's just-bash provider permanently refuses to resume a conversation once
37
+ * the sandbox template changes (skills, `agent/sandbox/workspace/`, or the
38
+ * sandbox definition) and never falls back, so one `eden deploy` update would
39
+ * break every existing conversation's sandbox forever. The isolated builder
40
+ * rewrites one file in the installed Eve copy so an existing session reopens
41
+ * against the current template instead of throwing; user files under
42
+ * `/workspace` always win over the template. The patch applies only when the
43
+ * target file's sha256 matches a known binding and selects that binding's
44
+ * replacement; it is removed entirely once upstream ships the fix.
45
+ */
46
+ export const EVE_SANDBOX_RESUME_PATCH_KNOWN_SHA256 = "0ba0ac3a237c99af2f67a6028f1675bf7c04b22454b4ebfb4cd9d19e5bf5aaf8";
47
+ /** Eve 0.79.1 stores a template key instead of an absolute template path. */
48
+ export const EVE_SANDBOX_RESUME_PATCH_LATEST_SHA256 = "ab8b1ad7398af8f6fd2a1e91eb0d1ff8d2b9b9afe1836234f36ce0e896791c45";
49
+ /** Marker the patched binding always contains; proves the patch shipped. */
50
+ export const EVE_SANDBOX_RESUME_PATCH_SIGNATURE = "edenRefreshJustBashSessionSkills";
51
+ /** Path of the just-bash binding inside the resolved Eve package root. */
52
+ const EVE_JUST_BASH_BINDING_PATH = "dist/src/execution/sandbox/bindings/just-bash.js";
53
+ /** The exact `resume()` implementation in Eve 0.68.0, 0.71.2, 0.72.1, and 0.79.1. */
54
+ export const EVE_JUST_BASH_RESUME_ORIGINAL = `async resume(e,n,r){let i=requirePreparedJustBashArtifact(n),a=requireJustBashSessionState(r),o=createSandboxProviderIdentity({artifact:i,version:1}),s=sessionRootPath(e,i);if(a.generation!==o||a.rootPath!==s)throw Error(\`just-bash session state is incompatible with this environment.\`);if(!await pathExists(a.rootPath))throw Error(\`just-bash session root "\${a.rootPath}" no longer exists.\`);return await openHandle(e,a.rootPath,t)}`;
55
+ /**
56
+ * The replacement `resume()`: a session whose template generation moved is
57
+ * contained to the same sessions directory and reopened with its authored
58
+ * skills refreshed from the current template. A session folder that is gone
59
+ * (for example a deploy that could not carry it) is recreated from the
60
+ * current template instead of failing every later sandbox call.
61
+ */
62
+ export const EVE_SANDBOX_RESUME_PATCHED = `async resume(e,n,r){let i=requirePreparedJustBashArtifact(n),a=requireJustBashSessionState(r),o=createSandboxProviderIdentity({artifact:i,version:1}),s=sessionRootPath(e,i);if((a.generation!==o||a.rootPath!==s)&&dirname(a.rootPath)!==dirname(s))throw Error(\`just-bash session state is incompatible with this environment.\`);await ensureSessionRoot(i,a.rootPath);if(a.generation!==o)await edenRefreshJustBashSessionSkills(i.templateRootPath,a.rootPath);return await openHandle(e,a.rootPath,t)}`;
63
+ /**
64
+ * Eve 0.79.1 resolves the current template key under the runtime cache.
65
+ * Its ensureSessionRoot accepts that path, not the prepared artifact.
66
+ */
67
+ export const EVE_SANDBOX_RESUME_PATCHED_LATEST = `async resume(e,n,r){let i=requirePreparedJustBashArtifact(n),a=requireJustBashSessionState(r),o=createSandboxProviderIdentity({artifact:i,version:1}),s=sessionRootPath(e,i);if((a.generation!==o||a.rootPath!==s)&&dirname(a.rootPath)!==dirname(s))throw Error(\`just-bash session state is incompatible with this environment.\`);let c=resolveTemplateRootPath(e.storagePath,i.templateKey);await ensureSessionRoot(c,a.rootPath);if(a.generation!==o)await edenRefreshJustBashSessionSkills(c,a.rootPath);return await openHandle(e,a.rootPath,t)}`;
68
+ /**
69
+ * Helpers appended to the patched binding. They use only identifiers the
70
+ * module already imports (`readdir`, `rm`, `join`, `pathExists`,
71
+ * `copyDirectoryAtomically`). Skills live at `$HOME/.agents/skills` inside a
72
+ * just-bash root, so the session keeps the same path relative to its `fs`
73
+ * directory as the template; the template's `fs` is scanned because `$HOME`
74
+ * is only known inside the sandbox.
75
+ */
76
+ export const EVE_SANDBOX_RESUME_PATCH_HELPERS = `async function edenRefreshJustBashSessionSkills(e,t){let n=await edenFindTemplateSkillsRelativePath(join(e,"fs"));if(n===void 0)return;let r=join(t,"fs",n);await rm(r,{force:!0,recursive:!0}),await copyDirectoryAtomically(join(e,"fs",n),r)}
77
+ async function edenFindTemplateSkillsRelativePath(e){let t=[["",0]];for(;t.length>0;){let n=t.shift(),r=n[0],i=n[1];if(i>3)continue;let a=[];try{a=await readdir(join(e,r),{withFileTypes:!0})}catch{a=[]}for(let o of a){if(!o.isDirectory())continue;let s=r===""?o.name:r+"/"+o.name;if(o.name===".agents")return await pathExists(join(e,s,"skills"))?s+"/skills":void 0;t.push([s,i+1])}}return void 0}
78
+ `;
79
+ /**
80
+ * The CommonJS patcher the isolated builder runs between the frozen install
81
+ * and `eve build` (and again for the production reinstall). It never touches
82
+ * project files, applies only against known sha256 hashes, verifies the
83
+ * replacement matched exactly once, validates the result with `node --check`,
84
+ * and breaks hardlinks by writing a temp file and renaming it into place.
85
+ */
86
+ export const EVE_SANDBOX_RESUME_PATCH_SCRIPT = `// Eden temporary fix for https://github.com/vercel/eve/issues/4440: Eve's
87
+ // just-bash provider permanently refuses to resume a conversation after the
88
+ // sandbox template changes (skills, agent/sandbox/workspace/, sandbox
89
+ // definition) and never falls back. This rewrites one file in the installed
90
+ // Eve copy so an existing session reopens against the current template. It
91
+ // runs only against sha256 hashes Eden knows, with a replacement specific to
92
+ // each binding, and is removed once upstream ships the fix.
93
+ const fs = require("node:fs");
94
+ const path = require("node:path");
95
+ const crypto = require("node:crypto");
96
+ const { spawnSync } = require("node:child_process");
97
+ const KNOWN_PATCHES = ${JSON.stringify({
98
+ [EVE_SANDBOX_RESUME_PATCH_KNOWN_SHA256]: {
99
+ original: EVE_JUST_BASH_RESUME_ORIGINAL,
100
+ replacement: EVE_SANDBOX_RESUME_PATCHED,
101
+ },
102
+ [EVE_SANDBOX_RESUME_PATCH_LATEST_SHA256]: {
103
+ original: EVE_JUST_BASH_RESUME_ORIGINAL,
104
+ replacement: EVE_SANDBOX_RESUME_PATCHED_LATEST,
105
+ },
106
+ })};
107
+ const SIGNATURE = ${JSON.stringify(EVE_SANDBOX_RESUME_PATCH_SIGNATURE)};
108
+ const BINDING_PATH = ${JSON.stringify(EVE_JUST_BASH_BINDING_PATH)};
109
+ const HELPERS = ${JSON.stringify(EVE_SANDBOX_RESUME_PATCH_HELPERS)};
110
+ function finish(outcome, detail) {
111
+ console.log("eden-eve-sandbox-resume-patch: " + outcome + " (" + detail + ")");
112
+ }
113
+ let packageJsonPath;
114
+ try {
115
+ packageJsonPath = fs.realpathSync("node_modules/eve/package.json");
116
+ } catch {
117
+ finish("absent", "no project-local eve package to patch");
118
+ process.exit(0);
119
+ }
120
+ const binding = path.join(path.dirname(packageJsonPath), BINDING_PATH);
121
+ let source;
122
+ try {
123
+ source = fs.readFileSync(binding, "utf8");
124
+ } catch {
125
+ finish("absent", "this eve build has no just-bash binding");
126
+ process.exit(0);
127
+ }
128
+ if (source.includes(SIGNATURE)) {
129
+ finish("applied", "already patched");
130
+ process.exit(0);
131
+ }
132
+ const digest = crypto.createHash("sha256").update(source).digest("hex");
133
+ const patch = KNOWN_PATCHES[digest];
134
+ if (patch === undefined) {
135
+ finish("skipped", "sha256 " + digest + " is not a version Eden knows (eve#4440)");
136
+ process.exit(0);
137
+ }
138
+ const { original: ORIGINAL, replacement: PATCHED_RESUME } = patch;
139
+ const first = source.indexOf(ORIGINAL);
140
+ if (first === -1 || source.indexOf(ORIGINAL, first + 1) !== -1) {
141
+ console.error("eden-eve-sandbox-resume-patch: the known resume implementation did not match exactly once; refusing to patch");
142
+ process.exit(1);
143
+ }
144
+ const patched = source.slice(0, first) + PATCHED_RESUME +
145
+ source.slice(first + ORIGINAL.length) + "\\n" + HELPERS + "\\n";
146
+ const temporary = binding + ".tmp.js";
147
+ fs.rmSync(temporary, { force: true });
148
+ fs.writeFileSync(temporary, patched);
149
+ const parsed = spawnSync(process.execPath, ["--check", temporary], { encoding: "utf8" });
150
+ if (parsed.status !== 0) {
151
+ fs.rmSync(temporary, { force: true });
152
+ console.error("eden-eve-sandbox-resume-patch: the patched file failed node --check; refusing to install it" + (typeof parsed.stderr === "string" ? "\\n" + parsed.stderr : ""));
153
+ process.exit(1);
154
+ }
155
+ // Rename (not write) so any hardlink to the original inode is broken.
156
+ fs.renameSync(temporary, binding);
157
+ finish("applied", "temporary eve#4440 fix written into the isolated builder image");
158
+ `;
159
+ /** The heredoc COPY + RUN inserted after each frozen install in the builder. */
160
+ const EVE_SANDBOX_RESUME_PATCH_DOCKERFILE_STEP = `COPY <<'EDEN_EVE_PATCH_EOF' /tmp/eden-eve-sandbox-resume-patch.cjs
161
+ ${EVE_SANDBOX_RESUME_PATCH_SCRIPT}EDEN_EVE_PATCH_EOF
162
+ RUN node /tmp/eden-eve-sandbox-resume-patch.cjs
163
+ `;
30
164
  const INSTALL_COMMAND = [
31
165
  "corepack",
32
166
  "pnpm",
@@ -34,14 +168,6 @@ const INSTALL_COMMAND = [
34
168
  "--frozen-lockfile",
35
169
  ];
36
170
  const BUILD_COMMAND = ["./node_modules/.bin/eve", "build"];
37
- const START_COMMAND = [
38
- "./node_modules/.bin/eve",
39
- "start",
40
- "--host",
41
- "0.0.0.0",
42
- "--port",
43
- "8080",
44
- ];
45
171
  const EVE_ENTRYPOINT = ".output/server/index.mjs";
46
172
  const EVE_EXCLUDED_DIRECTORY_NAMES = new Set([
47
173
  ".eden",
@@ -78,27 +204,45 @@ const EVE_COMPETING_LOCKFILES = new Set([
78
204
  "package-lock.json",
79
205
  "yarn.lock",
80
206
  ]);
81
- const HOST_ENVIRONMENT = {
82
- HOST: "0.0.0.0",
83
- NITRO_HOST: "0.0.0.0",
84
- PORT: "8080",
85
- NITRO_PORT: "8080",
86
- NODE_ENV: "production",
87
- };
88
- function sha256(value) {
207
+ export function sha256(value) {
89
208
  return createHash("sha256").update(value).digest("hex");
90
209
  }
91
- function jsonBytes(value) {
210
+ export function jsonBytes(value) {
92
211
  return Buffer.from(`${JSON.stringify(value)}\n`, "utf8");
93
212
  }
94
- function isWithin(root, candidate) {
213
+ export function isWithin(root, candidate) {
95
214
  const normalizedRoot = root.endsWith("/") ? root.slice(0, -1) : root;
96
215
  return candidate === normalizedRoot ||
97
216
  candidate.startsWith(`${normalizedRoot}/`);
98
217
  }
99
- function safeRelativePath(root, candidate) {
218
+ export function safeRelativePath(root, candidate) {
100
219
  return relative(root, candidate).split("\\").join("/");
101
220
  }
221
+ export const IMAGE_ID_PATTERN = /^sha256:[0-9a-f]{64}$/u;
222
+ const NODE_IMAGE_PATTERN = /^node:24\.17\.0(?:-[a-z0-9][a-z0-9._-]*)?$/u;
223
+ export function imageReference(nodeImage, reason, remediation) {
224
+ if (!NODE_IMAGE_PATTERN.test(nodeImage.reference) ||
225
+ !IMAGE_ID_PATTERN.test(nodeImage.digest)) {
226
+ throw new EvePackagingError({
227
+ code: "DOCKER_PLATFORM_BLOCKED",
228
+ subject: "node-24-image",
229
+ reason,
230
+ remediation,
231
+ });
232
+ }
233
+ return `${nodeImage.reference}@${nodeImage.digest}`;
234
+ }
235
+ function safeDockerEnvironment() {
236
+ return {
237
+ PATH: process.env.PATH,
238
+ HOME: process.env.HOME,
239
+ TMPDIR: process.env.TMPDIR,
240
+ LANG: process.env.LANG,
241
+ LC_ALL: process.env.LC_ALL,
242
+ DOCKER_HOST: process.env.DOCKER_HOST,
243
+ DOCKER_CONTEXT: process.env.DOCKER_CONTEXT,
244
+ };
245
+ }
102
246
  function isAllowedSystemAlias(path) {
103
247
  const normalized = resolve(path);
104
248
  return normalized === "/var" || normalized === "/tmp";
@@ -770,6 +914,10 @@ async function verifySnapshotInputs(snapshotRoot, inputs) {
770
914
  if (firstSegment !== undefined && generatedRoots.has(firstSegment)) {
771
915
  continue;
772
916
  }
917
+ if (relativePath === EVE_SANDBOX_CACHE_ROOT ||
918
+ relativePath.startsWith(`${EVE_SANDBOX_CACHE_ROOT}/`)) {
919
+ continue;
920
+ }
773
921
  const candidate = join(directory, entry.name);
774
922
  if (entry.isSymbolicLink() || !entry.isDirectory()) {
775
923
  if (!entry.isFile() || entry.isSymbolicLink()) {
@@ -928,16 +1076,7 @@ export async function eveSnapshotSourceEntries(snapshotRoot) {
928
1076
  return names;
929
1077
  }
930
1078
  async function writeDockerBuildFiles(options) {
931
- if (!/^sha256:[0-9a-f]{64}$/u.test(options.nodeImage.digest) ||
932
- !/^node:24\.17\.0(?:-[a-z0-9][a-z0-9._-]*)?$/u.test(options.nodeImage.reference)) {
933
- throw new EvePackagingError({
934
- code: "DOCKER_PLATFORM_BLOCKED",
935
- subject: "node-24-image",
936
- reason: "The Linux/amd64 builder requires an immutable Node 24 image digest.",
937
- remediation: "Supply a verified Node 24 image reference and sha256 digest.",
938
- });
939
- }
940
- const image = `${options.nodeImage.reference}@${options.nodeImage.digest}`;
1079
+ const image = imageReference(options.nodeImage, "The Linux/amd64 builder requires an immutable Node 24 image digest.", "Supply a verified Node 24 image reference and sha256 digest.");
941
1080
  const installSources = ["package.json", "pnpm-lock.yaml"];
942
1081
  if ((await lstat(join(options.snapshotRoot, "pnpm-workspace.yaml"))
943
1082
  .catch(() => undefined))?.isFile() === true) {
@@ -956,16 +1095,18 @@ RUN corepack enable \\
956
1095
  && test "$(sha256sum pnpm-lock.yaml | cut -d ' ' -f1)" = "${options.lockfileSha256}" \\
957
1096
  && corepack pnpm install --frozen-lockfile --config.node-linker=hoisted --config.strict-dep-builds=false \\
958
1097
  && ${IGNORED_BUILDS_GUARD}
959
- RUN test "$(sha256sum pnpm-lock.yaml | cut -d ' ' -f1)" = "${options.lockfileSha256}"
1098
+ ${EVE_SANDBOX_RESUME_PATCH_DOCKERFILE_STEP}RUN test "$(sha256sum pnpm-lock.yaml | cut -d ' ' -f1)" = "${options.lockfileSha256}"
960
1099
  COPY . ./
961
1100
  RUN test -x ./node_modules/.bin/eve \\
962
- && ./node_modules/.bin/eve build
1101
+ && ./node_modules/.bin/eve build \\
1102
+ && mkdir -p .eve/sandbox-cache
963
1103
 
964
1104
  FROM builder AS runtime-deps
965
1105
  RUN rm -rf node_modules \\
966
1106
  && corepack pnpm install --frozen-lockfile --prod --config.node-linker=hoisted --config.strict-dep-builds=false \\
967
1107
  && ${IGNORED_BUILDS_GUARD} \\
968
1108
  && for l in node_modules/.bin/*; do t=$(readlink "$l") || continue; case "$t" in /*) ln -sfn "$(realpath --relative-to=node_modules/.bin "$t")" "$l" ;; esac; done
1109
+ ${EVE_SANDBOX_RESUME_PATCH_DOCKERFILE_STEP}
969
1110
 
970
1111
  FROM --platform=linux/amd64 ${image} AS runtime
971
1112
  WORKDIR /app
@@ -975,6 +1116,12 @@ ENV HOST=0.0.0.0 \\
975
1116
  NITRO_PORT=8080 \\
976
1117
  NODE_ENV=production
977
1118
  COPY --from=builder /workspace/.output /app/.output
1119
+ # Eve's prepared just-bash sandbox template: the builder stage's eve build
1120
+ # prewarm wrote it under /workspace/.eve/sandbox-cache, and the runtime sandbox
1121
+ # resolves it at /app/.eve/sandbox-cache (bash/read_file/write_file fail
1122
+ # without it). The mkdir in the build step guarantees the source exists even
1123
+ # when this Eve build prepared no template.
1124
+ COPY --from=builder /workspace/.eve/sandbox-cache /app/.eve/sandbox-cache
978
1125
  ${runtimeSourceCopies}
979
1126
  COPY --from=runtime-deps /workspace/node_modules /app/node_modules
980
1127
  EXPOSE 8080
@@ -1221,10 +1368,123 @@ async function scanGeneratedOutput(snapshotRoot) {
1221
1368
  fileCount: files.length,
1222
1369
  };
1223
1370
  }
1371
+ /**
1372
+ * A generous sanity bound for the prepared sandbox-template tree (templates
1373
+ * carry the sandbox filesystem seeds and authored skills; the minimal Eve
1374
+ * fixture produces ~1 KiB). Anything larger means the builder extracted more
1375
+ * than Eve's build-time prewarm could have written.
1376
+ */
1377
+ const EVE_SANDBOX_CACHE_MAX_BYTES = 64 * 1024 * 1024;
1378
+ /**
1379
+ * Validates the prepared sandbox-template tree the builder extracted into
1380
+ * `<snapshot>/.eve/sandbox-cache` with the same rigor as `.output`: regular
1381
+ * files and directories only, no symbolic links, no special files, stable
1382
+ * reads inside the immutable snapshot, and a total-size sanity bound.
1383
+ * Returns `null` when this Eve build prepared no sandbox template.
1384
+ */
1385
+ async function scanGeneratedSandboxCache(snapshotRoot) {
1386
+ const cacheRoot = join(snapshotRoot, EVE_SANDBOX_CACHE_ROOT);
1387
+ const rootDetails = await lstat(cacheRoot).catch(() => undefined);
1388
+ if (rootDetails === undefined)
1389
+ return null;
1390
+ const rootCanonical = await realpath(cacheRoot).catch(() => undefined);
1391
+ if (!rootDetails.isDirectory() ||
1392
+ rootDetails.isSymbolicLink() ||
1393
+ rootCanonical === undefined ||
1394
+ !isWithin(snapshotRoot, resolve(rootCanonical))) {
1395
+ throw new EvePackagingError({
1396
+ code: "UNSUPPORTED_EVE_OUTPUT",
1397
+ subject: EVE_SANDBOX_CACHE_ROOT,
1398
+ reason: "The prepared sandbox template root is missing, a symbolic link, or escapes the immutable snapshot.",
1399
+ remediation: "Rebuild the candidate so the isolated builder extracts a regular .eve/sandbox-cache directory.",
1400
+ });
1401
+ }
1402
+ const files = [];
1403
+ let totalBytes = 0;
1404
+ const visit = async (directory, relativeDirectory) => {
1405
+ const directoryDetails = await lstat(directory).catch(() => undefined);
1406
+ const directoryCanonical = await realpath(directory).catch(() => undefined);
1407
+ if (directoryDetails === undefined ||
1408
+ !directoryDetails.isDirectory() ||
1409
+ directoryDetails.isSymbolicLink() ||
1410
+ directoryCanonical === undefined ||
1411
+ !isWithin(snapshotRoot, resolve(directoryCanonical))) {
1412
+ throw new EvePackagingError({
1413
+ code: "UNSUPPORTED_EVE_OUTPUT",
1414
+ subject: relativeDirectory,
1415
+ reason: "The prepared sandbox template tree escaped or changed during validation.",
1416
+ remediation: "Rebuild the candidate so the isolated builder extracts regular template files.",
1417
+ });
1418
+ }
1419
+ const entries = await readdir(directory, { withFileTypes: true });
1420
+ entries.sort((left, right) => left.name.localeCompare(right.name));
1421
+ for (const entry of entries) {
1422
+ const relativePath = relativeDirectory.length === 0
1423
+ ? entry.name
1424
+ : `${relativeDirectory}/${entry.name}`;
1425
+ const candidate = join(directory, entry.name);
1426
+ if (entry.isSymbolicLink()) {
1427
+ throw new EvePackagingError({
1428
+ code: "UNSUPPORTED_EVE_OUTPUT",
1429
+ subject: `${EVE_SANDBOX_CACHE_ROOT}/${relativePath}`,
1430
+ reason: "The prepared sandbox template tree contains a symbolic link.",
1431
+ remediation: "Rebuild the candidate so the isolated builder extracts regular template files only.",
1432
+ });
1433
+ }
1434
+ if (entry.isDirectory()) {
1435
+ await visit(candidate, relativePath);
1436
+ continue;
1437
+ }
1438
+ if (!entry.isFile()) {
1439
+ throw new EvePackagingError({
1440
+ code: "UNSUPPORTED_EVE_OUTPUT",
1441
+ subject: `${EVE_SANDBOX_CACHE_ROOT}/${relativePath}`,
1442
+ reason: "The prepared sandbox template tree contains a device, socket, FIFO, or other unsupported file.",
1443
+ remediation: "Rebuild the candidate so the isolated builder extracts regular template files only.",
1444
+ });
1445
+ }
1446
+ let stable;
1447
+ try {
1448
+ stable = await readStableFile(candidate, snapshotRoot);
1449
+ }
1450
+ catch {
1451
+ throw new EvePackagingError({
1452
+ code: "UNSUPPORTED_EVE_OUTPUT",
1453
+ subject: `${EVE_SANDBOX_CACHE_ROOT}/${relativePath}`,
1454
+ reason: "The prepared sandbox template file is unreadable or changed during validation.",
1455
+ remediation: "Rebuild the candidate so the isolated builder extracts a stable template tree.",
1456
+ });
1457
+ }
1458
+ totalBytes += stable.bytes.byteLength;
1459
+ if (totalBytes > EVE_SANDBOX_CACHE_MAX_BYTES) {
1460
+ throw new EvePackagingError({
1461
+ code: "UNSUPPORTED_EVE_OUTPUT",
1462
+ subject: EVE_SANDBOX_CACHE_ROOT,
1463
+ reason: `The prepared sandbox template tree exceeds the ${EVE_SANDBOX_CACHE_MAX_BYTES}-byte sanity bound.`,
1464
+ remediation: "Rebuild the candidate; the extracted template tree must contain only Eve's prepared templates.",
1465
+ });
1466
+ }
1467
+ files.push({
1468
+ relativePath: `${EVE_SANDBOX_CACHE_ROOT}/${relativePath}`,
1469
+ sha256: stable.sha256,
1470
+ byteLength: stable.bytes.byteLength,
1471
+ });
1472
+ }
1473
+ };
1474
+ await visit(cacheRoot, "");
1475
+ return {
1476
+ present: true,
1477
+ root: EVE_SANDBOX_CACHE_ROOT,
1478
+ outputDigest: sha256(jsonBytes(files)),
1479
+ fileCount: files.length,
1480
+ totalBytes,
1481
+ };
1482
+ }
1224
1483
  async function assertGeneratedTreesExcludeSecrets(snapshotRoot) {
1225
1484
  const roots = [
1226
1485
  join(snapshotRoot, "node_modules"),
1227
1486
  join(snapshotRoot, ".output"),
1487
+ join(snapshotRoot, EVE_SANDBOX_CACHE_ROOT),
1228
1488
  ];
1229
1489
  const visit = async (directory, relativeDirectory) => {
1230
1490
  const directoryDetails = await lstat(directory).catch(() => undefined);
@@ -1301,7 +1561,7 @@ async function assertGeneratedTreesExcludeSecrets(snapshotRoot) {
1301
1561
  }
1302
1562
  }
1303
1563
  }
1304
- function safeError(error) {
1564
+ export function safeError(error) {
1305
1565
  return {
1306
1566
  code: error.code,
1307
1567
  subject: error.subject,
@@ -1447,12 +1707,11 @@ function blockedResult(error, extra = {}) {
1447
1707
  status: "blocked",
1448
1708
  returnCode: error.code,
1449
1709
  deployable: false,
1450
- project: extra.project ?? null,
1710
+ project: null,
1451
1711
  candidate: null,
1452
1712
  snapshot: null,
1453
1713
  toolchain: null,
1454
1714
  image: null,
1455
- runtime: null,
1456
1715
  secrets: {
1457
1716
  runtimeVariableNames: [],
1458
1717
  valuesRecorded: false,
@@ -1464,19 +1723,39 @@ function blockedResult(error, extra = {}) {
1464
1723
  excludedFromManifestsAndLogs: true,
1465
1724
  redactionRegisteredBeforeChildren: false,
1466
1725
  },
1467
- checks: extra.checks ?? [{
1726
+ checks: [{
1468
1727
  id: error.code,
1469
1728
  status: "blocked",
1470
1729
  subject: error.subject,
1471
1730
  reason: error.message,
1472
1731
  remediation: error.remediation,
1473
1732
  }],
1733
+ sandboxResumePatch: "absent",
1474
1734
  candidateImageId: null,
1475
1735
  candidateImageRetainedLocally: false,
1476
1736
  writtenPaths: extra.writtenPaths ?? [],
1477
1737
  error: safeError(error),
1478
1738
  };
1479
1739
  }
1740
+ /**
1741
+ * Decides the sandbox-resume patch outcome from the `node_modules` the
1742
+ * builder copied out of the finished image: the shipped just-bash binding is
1743
+ * the single source of truth and stays correct under Docker layer caching
1744
+ * (a cached patch step still ships the patched file). Reading never throws —
1745
+ * an unreadable tree counts as `absent`.
1746
+ */
1747
+ async function detectEveSandboxResumePatch(snapshotRoot) {
1748
+ try {
1749
+ const packageJsonPath = await realpath(join(snapshotRoot, "node_modules/eve/package.json"));
1750
+ const binding = await readFile(join(dirname(packageJsonPath), EVE_JUST_BASH_BINDING_PATH), "utf8");
1751
+ return binding.includes(EVE_SANDBOX_RESUME_PATCH_SIGNATURE)
1752
+ ? "applied"
1753
+ : "skipped";
1754
+ }
1755
+ catch {
1756
+ return "absent";
1757
+ }
1758
+ }
1480
1759
  async function currentInputIdentity(runtimeConfig) {
1481
1760
  const token = (value) => {
1482
1761
  if (typeof value === "string")
@@ -1743,7 +2022,9 @@ export async function buildEveProjectSnapshot(options) {
1743
2022
  return blockedResult(cleanupError ?? dependencyError, { writtenPaths });
1744
2023
  }
1745
2024
  const eve = await resolveProjectLocalEve(snapshotRoot);
2025
+ const sandboxResumePatch = await detectEveSandboxResumePatch(snapshotRoot);
1746
2026
  const output = await scanGeneratedOutput(snapshotRoot);
2027
+ const sandboxCache = await scanGeneratedSandboxCache(snapshotRoot);
1747
2028
  await assertGeneratedTreesExcludeSecrets(snapshotRoot);
1748
2029
  const snapshotSourceDigest = await snapshotDigest(snapshotRoot);
1749
2030
  const runtimeManifestPath = join(generationRoot, "runtime-manifest.json");
@@ -1758,6 +2039,7 @@ export async function buildEveProjectSnapshot(options) {
1758
2039
  sourceDigest: initialInputs.sourceDigest,
1759
2040
  snapshotDigest: snapshotSourceDigest,
1760
2041
  generatedOutput: output,
2042
+ sandboxCache,
1761
2043
  artifactPath: EVE_ENTRYPOINT,
1762
2044
  platform: "linux/amd64",
1763
2045
  runtimeVariableNames,
@@ -1791,10 +2073,8 @@ export async function buildEveProjectSnapshot(options) {
1791
2073
  excludedCategories: [...new Set(initialInputs.excludedRelativePaths.map((path) => isPathExcluded(path, undefined).category ?? "generated-state"))].sort(),
1792
2074
  sourceRaceChecked: true,
1793
2075
  };
1794
- if ((buildResult.imageId !== undefined &&
1795
- !/^sha256:[0-9a-f]{64}$/u.test(buildResult.imageId)) ||
1796
- (buildResult.imageId !== undefined &&
1797
- buildResult.imagePlatform !== "linux/amd64")) {
2076
+ if (buildResult.imageId !== undefined &&
2077
+ !IMAGE_ID_PATTERN.test(buildResult.imageId)) {
1798
2078
  const imageError = new EvePackagingError({
1799
2079
  code: "DOCKER_PLATFORM_BLOCKED",
1800
2080
  subject: "linux/amd64 image",
@@ -1835,6 +2115,7 @@ export async function buildEveProjectSnapshot(options) {
1835
2115
  sourceDigest: initialInputs.sourceDigest,
1836
2116
  snapshotDigest: snapshotSourceDigest,
1837
2117
  generatedOutput: output,
2118
+ sandboxCache,
1838
2119
  ...(initialRuntimeInputIdentity === undefined
1839
2120
  ? {}
1840
2121
  : { runtimeConfigInputIdentity: initialRuntimeInputIdentity }),
@@ -1868,7 +2149,6 @@ export async function buildEveProjectSnapshot(options) {
1868
2149
  },
1869
2150
  image,
1870
2151
  candidate,
1871
- runtime: null,
1872
2152
  secrets: {
1873
2153
  runtimeVariableNames,
1874
2154
  valuesRecorded: false,
@@ -1916,7 +2196,17 @@ export async function buildEveProjectSnapshot(options) {
1916
2196
  reason: "The resolved Eve executable is inside the isolated project dependency tree.",
1917
2197
  remediation: null,
1918
2198
  },
2199
+ ...(sandboxResumePatch === "applied"
2200
+ ? [{
2201
+ id: "EVE_SANDBOX_RESUME_PATCH",
2202
+ status: "pass",
2203
+ subject: "eve-sandbox-resume",
2204
+ reason: "applied temporary fix for eve#4440",
2205
+ remediation: null,
2206
+ }]
2207
+ : []),
1919
2208
  ],
2209
+ sandboxResumePatch,
1920
2210
  candidateImageId: buildResult.imageId ?? null,
1921
2211
  candidateImageRetainedLocally: buildResult.imageId !== undefined,
1922
2212
  writtenPaths,
@@ -2049,15 +2339,7 @@ export function createDockerEveProjectBuilder(options) {
2049
2339
  });
2050
2340
  }
2051
2341
  const imageIdFile = join(request.generationRoot, "image-id");
2052
- const safeEnv = {
2053
- PATH: process.env.PATH,
2054
- HOME: process.env.HOME,
2055
- TMPDIR: process.env.TMPDIR,
2056
- LANG: process.env.LANG,
2057
- LC_ALL: process.env.LC_ALL,
2058
- DOCKER_HOST: process.env.DOCKER_HOST,
2059
- DOCKER_CONTEXT: process.env.DOCKER_CONTEXT,
2060
- };
2342
+ const safeEnv = safeDockerEnvironment();
2061
2343
  let containerId;
2062
2344
  let imageBuilt = false;
2063
2345
  let retainImage = false;
@@ -2100,7 +2382,7 @@ export function createDockerEveProjectBuilder(options) {
2100
2382
  ], { env: safeEnv, cwd: request.generationRoot, maxBuffer: 1024 * 1024 });
2101
2383
  imageBuilt = true;
2102
2384
  builtImageId = (await readFile(imageIdFile, "utf8")).trim();
2103
- if (!/^sha256:[0-9a-f]{64}$/u.test(builtImageId)) {
2385
+ if (!IMAGE_ID_PATTERN.test(builtImageId)) {
2104
2386
  throw new EvePackagingError({
2105
2387
  code: "DOCKER_PLATFORM_BLOCKED",
2106
2388
  subject: "image identity",
@@ -2139,6 +2421,30 @@ export function createDockerEveProjectBuilder(options) {
2139
2421
  }
2140
2422
  await execFileAsync(dockerCommand, ["cp", `${containerId}:/app/.output`, join(request.snapshotRoot, ".output")], { env: safeEnv, cwd: request.generationRoot });
2141
2423
  await execFileAsync(dockerCommand, ["cp", `${containerId}:/app/node_modules`, join(request.snapshotRoot, "node_modules")], { env: safeEnv, cwd: request.generationRoot });
2424
+ // The builder stage's `eve build` prewarm writes the just-bash sandbox
2425
+ // template under its workdir; the runtime stage copies it to
2426
+ // /app/.eve/sandbox-cache (the mkdir in the Dockerfile guarantees the
2427
+ // path exists). Extract exactly that subtree so runtime sandboxes can
2428
+ // resolve their template; never the rest of `.eve` (sessions, locks,
2429
+ // builds). Treated as absent when the container has no such path.
2430
+ const sandboxCacheTarget = join(request.snapshotRoot, EVE_SANDBOX_CACHE_ROOT);
2431
+ await ensureSnapshotDirectory(request.snapshotRoot, dirname(sandboxCacheTarget));
2432
+ try {
2433
+ await execFileAsync(dockerCommand, ["cp", `${containerId}:/app/${EVE_SANDBOX_CACHE_ROOT}`, sandboxCacheTarget], { env: safeEnv, cwd: request.generationRoot });
2434
+ }
2435
+ catch (error) {
2436
+ const stderr = typeof error === "object" &&
2437
+ error !== null &&
2438
+ "stderr" in error &&
2439
+ typeof error.stderr === "string"
2440
+ ? error.stderr
2441
+ : "";
2442
+ if (!/(?:could not find|no such|not found|does not exist)/iu.test(stderr)) {
2443
+ throw error;
2444
+ }
2445
+ // Absent: remove the `.eve` parent this extraction created.
2446
+ await rm(dirname(sandboxCacheTarget), { recursive: true, force: true }).catch(() => undefined);
2447
+ }
2142
2448
  const result = {
2143
2449
  imageId: builtImageId,
2144
2450
  imagePlatform: "linux/amd64",
@@ -2174,15 +2480,7 @@ export function createDockerEveProjectBuilder(options) {
2174
2480
  const imageReference = result.imageReference;
2175
2481
  if (imageReference === undefined)
2176
2482
  return;
2177
- const safeEnv = {
2178
- PATH: process.env.PATH,
2179
- HOME: process.env.HOME,
2180
- TMPDIR: process.env.TMPDIR,
2181
- LANG: process.env.LANG,
2182
- LC_ALL: process.env.LC_ALL,
2183
- DOCKER_HOST: process.env.DOCKER_HOST,
2184
- DOCKER_CONTEXT: process.env.DOCKER_CONTEXT,
2185
- };
2483
+ const safeEnv = safeDockerEnvironment();
2186
2484
  await execFileAsync(dockerCommand, ["image", "rm", "--force", imageReference], { env: safeEnv });
2187
2485
  const remainingImage = await execFileAsync(dockerCommand, ["image", "inspect", imageReference, "--format", "{{.Id}}"], { env: safeEnv }).catch(() => undefined);
2188
2486
  if (remainingImage !== undefined && remainingImage.stdout.trim().length > 0) {
@@ -1,6 +1,13 @@
1
1
  export declare const EVE_RESERVED_HOST_VARIABLES: readonly ["HOST", "NITRO_HOST", "PORT", "NITRO_PORT", "NODE_ENV", "NODE_EXTRA_CA_CERTS", "WORKFLOW_LOCAL_BASE_URL", "EDEN_WORLD_URL", "EDEN_EVE_DEPLOYMENT_ID", "EDEN_EVE_GENERATION_ID"];
2
2
  export type EveReservedHostVariable = (typeof EVE_RESERVED_HOST_VARIABLES)[number];
3
3
  export declare const EVE_START_COMMAND: readonly ["./node_modules/.bin/eve", "start", "--host", "0.0.0.0", "--port", "8080"];
4
+ export declare const EVE_HOST_ENVIRONMENT: Readonly<{
5
+ readonly HOST: "0.0.0.0";
6
+ readonly NITRO_HOST: "0.0.0.0";
7
+ readonly PORT: "8080";
8
+ readonly NITRO_PORT: "8080";
9
+ readonly NODE_ENV: "production";
10
+ }>;
4
11
  type EveRuntimeValueMap = Readonly<Record<string, string>>;
5
12
  export type EveRuntimeConfigErrorCode = "EVE_ENV_FILE_NOT_FOUND" | "EVE_ENV_FILE_INVALID" | "EVE_ENV_FILE_UNREADABLE" | "EVE_ENV_FILE_ENCODING" | "EVE_ENV_FILE_MALFORMED" | "EVE_ENV_FILE_DUPLICATE" | "EVE_ENV_RESERVED_VARIABLE" | "EVE_ENV_FILE_RACE" | "EVE_RUNTIME_INJECTION_INVALID" | "EVE_RUNTIME_PROTECTED_STORE_UNAVAILABLE" | "EVE_RUNTIME_PROTECTED_UPLOAD_FAILED" | "EVE_RUNTIME_LOCAL_INJECTION_UNSUPPORTED";
6
13
  export declare class EveRuntimeConfigError extends Error {
@@ -119,8 +126,6 @@ export declare class EveRuntimeConfig {
119
126
  export declare function readEveRuntimeConfig(inputPath: string, options?: {
120
127
  readonly cwd?: string;
121
128
  }): Promise<EveRuntimeConfig>;
122
- export declare const parseEveRuntimeConfig: typeof readEveRuntimeConfig;
123
- export declare const loadEveRuntimeConfig: typeof readEveRuntimeConfig;
124
129
  export declare function prepareEveRuntimeInjection(config: EveRuntimeConfig, options: EveRuntimeInjectionOptions): Promise<EveRuntimeInjection>;
125
130
  export {};
126
131
  //# sourceMappingURL=eve-runtime-config.d.ts.map
@@ -21,6 +21,13 @@ export const EVE_START_COMMAND = Object.freeze([
21
21
  "--port",
22
22
  "8080",
23
23
  ]);
24
+ export const EVE_HOST_ENVIRONMENT = Object.freeze({
25
+ HOST: "0.0.0.0",
26
+ NITRO_HOST: "0.0.0.0",
27
+ PORT: "8080",
28
+ NITRO_PORT: "8080",
29
+ NODE_ENV: "production",
30
+ });
24
31
  const EVE_ENVIRONMENT_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/u;
25
32
  const EVE_RUNTIME_METADATA_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/u;
26
33
  const EVE_ENV_FILE_SOURCE = "--env-file";
@@ -323,8 +330,6 @@ export async function readEveRuntimeConfig(inputPath, options = {}) {
323
330
  activeRedactionRegistries.add(redaction);
324
331
  return EveRuntimeConfig.from(observation, values, redaction);
325
332
  }
326
- export const parseEveRuntimeConfig = readEveRuntimeConfig;
327
- export const loadEveRuntimeConfig = readEveRuntimeConfig;
328
333
  function validateProtectedPutResult(result) {
329
334
  if (typeof result !== "object" ||
330
335
  result === null ||