@moinulmoin/eden 0.5.0 → 0.7.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.
@@ -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,121 @@ 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 equals `EVE_SANDBOX_RESUME_PATCH_KNOWN_SHA256`, and it
44
+ * is removed entirely once upstream ships the fix.
45
+ */
46
+ export const EVE_SANDBOX_RESUME_PATCH_KNOWN_SHA256 = "0ba0ac3a237c99af2f67a6028f1675bf7c04b22454b4ebfb4cd9d19e5bf5aaf8";
47
+ /** Marker the patched binding always contains; proves the patch shipped. */
48
+ export const EVE_SANDBOX_RESUME_PATCH_SIGNATURE = "edenRefreshJustBashSessionSkills";
49
+ /** Path of the just-bash binding inside the resolved Eve package root. */
50
+ const EVE_JUST_BASH_BINDING_PATH = "dist/src/execution/sandbox/bindings/just-bash.js";
51
+ /** The exact `resume()` implementation in eve 0.68.0, 0.71.2, and 0.72.1. */
52
+ 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)}`;
53
+ /**
54
+ * The replacement `resume()`: a session whose template generation moved is
55
+ * contained to the same sessions directory and reopened with its authored
56
+ * skills refreshed from the current template. A session folder that is gone
57
+ * (for example a deploy that could not carry it) is recreated from the
58
+ * current template instead of failing every later sandbox call.
59
+ */
60
+ 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)}`;
61
+ /**
62
+ * Helpers appended to the patched binding. They use only identifiers the
63
+ * module already imports (`readdir`, `rm`, `join`, `pathExists`,
64
+ * `copyDirectoryAtomically`). Skills live at `$HOME/.agents/skills` inside a
65
+ * just-bash root, so the session keeps the same path relative to its `fs`
66
+ * directory as the template; the template's `fs` is scanned because `$HOME`
67
+ * is only known inside the sandbox.
68
+ */
69
+ 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)}
70
+ 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}
71
+ `;
72
+ /**
73
+ * The CommonJS patcher the isolated builder runs between the frozen install
74
+ * and `eve build` (and again for the production reinstall). It never touches
75
+ * project files, applies only against the known sha256, verifies the
76
+ * replacement matched exactly once, validates the result with `node --check`,
77
+ * and breaks hardlinks by writing a temp file and renaming it into place.
78
+ */
79
+ export const EVE_SANDBOX_RESUME_PATCH_SCRIPT = `// Eden temporary fix for https://github.com/vercel/eve/issues/4440: Eve's
80
+ // just-bash provider permanently refuses to resume a conversation after the
81
+ // sandbox template changes (skills, agent/sandbox/workspace/, sandbox
82
+ // definition) and never falls back. This rewrites one file in the installed
83
+ // Eve copy so an existing session reopens against the current template. It
84
+ // runs only against the sha256 Eden knows and is removed once upstream
85
+ // ships the fix.
86
+ const fs = require("node:fs");
87
+ const path = require("node:path");
88
+ const crypto = require("node:crypto");
89
+ const { spawnSync } = require("node:child_process");
90
+ const KNOWN_SHA256 = ${JSON.stringify(EVE_SANDBOX_RESUME_PATCH_KNOWN_SHA256)};
91
+ const SIGNATURE = ${JSON.stringify(EVE_SANDBOX_RESUME_PATCH_SIGNATURE)};
92
+ const BINDING_PATH = ${JSON.stringify(EVE_JUST_BASH_BINDING_PATH)};
93
+ const ORIGINAL = ${JSON.stringify(EVE_JUST_BASH_RESUME_ORIGINAL)};
94
+ const PATCHED_RESUME = ${JSON.stringify(EVE_SANDBOX_RESUME_PATCHED)};
95
+ const HELPERS = ${JSON.stringify(EVE_SANDBOX_RESUME_PATCH_HELPERS)};
96
+ function finish(outcome, detail) {
97
+ console.log("eden-eve-sandbox-resume-patch: " + outcome + " (" + detail + ")");
98
+ }
99
+ let packageJsonPath;
100
+ try {
101
+ packageJsonPath = fs.realpathSync("node_modules/eve/package.json");
102
+ } catch {
103
+ finish("absent", "no project-local eve package to patch");
104
+ process.exit(0);
105
+ }
106
+ const binding = path.join(path.dirname(packageJsonPath), BINDING_PATH);
107
+ let source;
108
+ try {
109
+ source = fs.readFileSync(binding, "utf8");
110
+ } catch {
111
+ finish("absent", "this eve build has no just-bash binding");
112
+ process.exit(0);
113
+ }
114
+ if (source.includes(SIGNATURE)) {
115
+ finish("applied", "already patched");
116
+ process.exit(0);
117
+ }
118
+ const digest = crypto.createHash("sha256").update(source).digest("hex");
119
+ if (digest !== KNOWN_SHA256) {
120
+ finish("skipped", "sha256 " + digest + " is not a version Eden knows (eve#4440)");
121
+ process.exit(0);
122
+ }
123
+ const first = source.indexOf(ORIGINAL);
124
+ if (first === -1 || source.indexOf(ORIGINAL, first + 1) !== -1) {
125
+ console.error("eden-eve-sandbox-resume-patch: the known resume implementation did not match exactly once; refusing to patch");
126
+ process.exit(1);
127
+ }
128
+ const patched = source.slice(0, first) + PATCHED_RESUME +
129
+ source.slice(first + ORIGINAL.length) + "\\n" + HELPERS + "\\n";
130
+ const temporary = binding + ".tmp.js";
131
+ fs.rmSync(temporary, { force: true });
132
+ fs.writeFileSync(temporary, patched);
133
+ const parsed = spawnSync(process.execPath, ["--check", temporary], { encoding: "utf8" });
134
+ if (parsed.status !== 0) {
135
+ fs.rmSync(temporary, { force: true });
136
+ console.error("eden-eve-sandbox-resume-patch: the patched file failed node --check; refusing to install it" + (typeof parsed.stderr === "string" ? "\\n" + parsed.stderr : ""));
137
+ process.exit(1);
138
+ }
139
+ // Rename (not write) so any hardlink to the original inode is broken.
140
+ fs.renameSync(temporary, binding);
141
+ finish("applied", "temporary eve#4440 fix written into the isolated builder image");
142
+ `;
143
+ /** The heredoc COPY + RUN inserted after each frozen install in the builder. */
144
+ const EVE_SANDBOX_RESUME_PATCH_DOCKERFILE_STEP = `COPY <<'EDEN_EVE_PATCH_EOF' /tmp/eden-eve-sandbox-resume-patch.cjs
145
+ ${EVE_SANDBOX_RESUME_PATCH_SCRIPT}EDEN_EVE_PATCH_EOF
146
+ RUN node /tmp/eden-eve-sandbox-resume-patch.cjs
147
+ `;
30
148
  const INSTALL_COMMAND = [
31
149
  "corepack",
32
150
  "pnpm",
@@ -34,14 +152,6 @@ const INSTALL_COMMAND = [
34
152
  "--frozen-lockfile",
35
153
  ];
36
154
  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
155
  const EVE_ENTRYPOINT = ".output/server/index.mjs";
46
156
  const EVE_EXCLUDED_DIRECTORY_NAMES = new Set([
47
157
  ".eden",
@@ -78,27 +188,45 @@ const EVE_COMPETING_LOCKFILES = new Set([
78
188
  "package-lock.json",
79
189
  "yarn.lock",
80
190
  ]);
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) {
191
+ export function sha256(value) {
89
192
  return createHash("sha256").update(value).digest("hex");
90
193
  }
91
- function jsonBytes(value) {
194
+ export function jsonBytes(value) {
92
195
  return Buffer.from(`${JSON.stringify(value)}\n`, "utf8");
93
196
  }
94
- function isWithin(root, candidate) {
197
+ export function isWithin(root, candidate) {
95
198
  const normalizedRoot = root.endsWith("/") ? root.slice(0, -1) : root;
96
199
  return candidate === normalizedRoot ||
97
200
  candidate.startsWith(`${normalizedRoot}/`);
98
201
  }
99
- function safeRelativePath(root, candidate) {
202
+ export function safeRelativePath(root, candidate) {
100
203
  return relative(root, candidate).split("\\").join("/");
101
204
  }
205
+ export const IMAGE_ID_PATTERN = /^sha256:[0-9a-f]{64}$/u;
206
+ const NODE_IMAGE_PATTERN = /^node:24\.17\.0(?:-[a-z0-9][a-z0-9._-]*)?$/u;
207
+ export function imageReference(nodeImage, reason, remediation) {
208
+ if (!NODE_IMAGE_PATTERN.test(nodeImage.reference) ||
209
+ !IMAGE_ID_PATTERN.test(nodeImage.digest)) {
210
+ throw new EvePackagingError({
211
+ code: "DOCKER_PLATFORM_BLOCKED",
212
+ subject: "node-24-image",
213
+ reason,
214
+ remediation,
215
+ });
216
+ }
217
+ return `${nodeImage.reference}@${nodeImage.digest}`;
218
+ }
219
+ function safeDockerEnvironment() {
220
+ return {
221
+ PATH: process.env.PATH,
222
+ HOME: process.env.HOME,
223
+ TMPDIR: process.env.TMPDIR,
224
+ LANG: process.env.LANG,
225
+ LC_ALL: process.env.LC_ALL,
226
+ DOCKER_HOST: process.env.DOCKER_HOST,
227
+ DOCKER_CONTEXT: process.env.DOCKER_CONTEXT,
228
+ };
229
+ }
102
230
  function isAllowedSystemAlias(path) {
103
231
  const normalized = resolve(path);
104
232
  return normalized === "/var" || normalized === "/tmp";
@@ -770,6 +898,10 @@ async function verifySnapshotInputs(snapshotRoot, inputs) {
770
898
  if (firstSegment !== undefined && generatedRoots.has(firstSegment)) {
771
899
  continue;
772
900
  }
901
+ if (relativePath === EVE_SANDBOX_CACHE_ROOT ||
902
+ relativePath.startsWith(`${EVE_SANDBOX_CACHE_ROOT}/`)) {
903
+ continue;
904
+ }
773
905
  const candidate = join(directory, entry.name);
774
906
  if (entry.isSymbolicLink() || !entry.isDirectory()) {
775
907
  if (!entry.isFile() || entry.isSymbolicLink()) {
@@ -928,16 +1060,7 @@ export async function eveSnapshotSourceEntries(snapshotRoot) {
928
1060
  return names;
929
1061
  }
930
1062
  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}`;
1063
+ 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
1064
  const installSources = ["package.json", "pnpm-lock.yaml"];
942
1065
  if ((await lstat(join(options.snapshotRoot, "pnpm-workspace.yaml"))
943
1066
  .catch(() => undefined))?.isFile() === true) {
@@ -956,16 +1079,18 @@ RUN corepack enable \\
956
1079
  && test "$(sha256sum pnpm-lock.yaml | cut -d ' ' -f1)" = "${options.lockfileSha256}" \\
957
1080
  && corepack pnpm install --frozen-lockfile --config.node-linker=hoisted --config.strict-dep-builds=false \\
958
1081
  && ${IGNORED_BUILDS_GUARD}
959
- RUN test "$(sha256sum pnpm-lock.yaml | cut -d ' ' -f1)" = "${options.lockfileSha256}"
1082
+ ${EVE_SANDBOX_RESUME_PATCH_DOCKERFILE_STEP}RUN test "$(sha256sum pnpm-lock.yaml | cut -d ' ' -f1)" = "${options.lockfileSha256}"
960
1083
  COPY . ./
961
1084
  RUN test -x ./node_modules/.bin/eve \\
962
- && ./node_modules/.bin/eve build
1085
+ && ./node_modules/.bin/eve build \\
1086
+ && mkdir -p .eve/sandbox-cache
963
1087
 
964
1088
  FROM builder AS runtime-deps
965
1089
  RUN rm -rf node_modules \\
966
1090
  && corepack pnpm install --frozen-lockfile --prod --config.node-linker=hoisted --config.strict-dep-builds=false \\
967
1091
  && ${IGNORED_BUILDS_GUARD} \\
968
1092
  && 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
1093
+ ${EVE_SANDBOX_RESUME_PATCH_DOCKERFILE_STEP}
969
1094
 
970
1095
  FROM --platform=linux/amd64 ${image} AS runtime
971
1096
  WORKDIR /app
@@ -975,6 +1100,12 @@ ENV HOST=0.0.0.0 \\
975
1100
  NITRO_PORT=8080 \\
976
1101
  NODE_ENV=production
977
1102
  COPY --from=builder /workspace/.output /app/.output
1103
+ # Eve's prepared just-bash sandbox template: the builder stage's eve build
1104
+ # prewarm wrote it under /workspace/.eve/sandbox-cache, and the runtime sandbox
1105
+ # resolves it at /app/.eve/sandbox-cache (bash/read_file/write_file fail
1106
+ # without it). The mkdir in the build step guarantees the source exists even
1107
+ # when this Eve build prepared no template.
1108
+ COPY --from=builder /workspace/.eve/sandbox-cache /app/.eve/sandbox-cache
978
1109
  ${runtimeSourceCopies}
979
1110
  COPY --from=runtime-deps /workspace/node_modules /app/node_modules
980
1111
  EXPOSE 8080
@@ -1221,10 +1352,123 @@ async function scanGeneratedOutput(snapshotRoot) {
1221
1352
  fileCount: files.length,
1222
1353
  };
1223
1354
  }
1355
+ /**
1356
+ * A generous sanity bound for the prepared sandbox-template tree (templates
1357
+ * carry the sandbox filesystem seeds and authored skills; the minimal Eve
1358
+ * fixture produces ~1 KiB). Anything larger means the builder extracted more
1359
+ * than Eve's build-time prewarm could have written.
1360
+ */
1361
+ const EVE_SANDBOX_CACHE_MAX_BYTES = 64 * 1024 * 1024;
1362
+ /**
1363
+ * Validates the prepared sandbox-template tree the builder extracted into
1364
+ * `<snapshot>/.eve/sandbox-cache` with the same rigor as `.output`: regular
1365
+ * files and directories only, no symbolic links, no special files, stable
1366
+ * reads inside the immutable snapshot, and a total-size sanity bound.
1367
+ * Returns `null` when this Eve build prepared no sandbox template.
1368
+ */
1369
+ async function scanGeneratedSandboxCache(snapshotRoot) {
1370
+ const cacheRoot = join(snapshotRoot, EVE_SANDBOX_CACHE_ROOT);
1371
+ const rootDetails = await lstat(cacheRoot).catch(() => undefined);
1372
+ if (rootDetails === undefined)
1373
+ return null;
1374
+ const rootCanonical = await realpath(cacheRoot).catch(() => undefined);
1375
+ if (!rootDetails.isDirectory() ||
1376
+ rootDetails.isSymbolicLink() ||
1377
+ rootCanonical === undefined ||
1378
+ !isWithin(snapshotRoot, resolve(rootCanonical))) {
1379
+ throw new EvePackagingError({
1380
+ code: "UNSUPPORTED_EVE_OUTPUT",
1381
+ subject: EVE_SANDBOX_CACHE_ROOT,
1382
+ reason: "The prepared sandbox template root is missing, a symbolic link, or escapes the immutable snapshot.",
1383
+ remediation: "Rebuild the candidate so the isolated builder extracts a regular .eve/sandbox-cache directory.",
1384
+ });
1385
+ }
1386
+ const files = [];
1387
+ let totalBytes = 0;
1388
+ const visit = async (directory, relativeDirectory) => {
1389
+ const directoryDetails = await lstat(directory).catch(() => undefined);
1390
+ const directoryCanonical = await realpath(directory).catch(() => undefined);
1391
+ if (directoryDetails === undefined ||
1392
+ !directoryDetails.isDirectory() ||
1393
+ directoryDetails.isSymbolicLink() ||
1394
+ directoryCanonical === undefined ||
1395
+ !isWithin(snapshotRoot, resolve(directoryCanonical))) {
1396
+ throw new EvePackagingError({
1397
+ code: "UNSUPPORTED_EVE_OUTPUT",
1398
+ subject: relativeDirectory,
1399
+ reason: "The prepared sandbox template tree escaped or changed during validation.",
1400
+ remediation: "Rebuild the candidate so the isolated builder extracts regular template files.",
1401
+ });
1402
+ }
1403
+ const entries = await readdir(directory, { withFileTypes: true });
1404
+ entries.sort((left, right) => left.name.localeCompare(right.name));
1405
+ for (const entry of entries) {
1406
+ const relativePath = relativeDirectory.length === 0
1407
+ ? entry.name
1408
+ : `${relativeDirectory}/${entry.name}`;
1409
+ const candidate = join(directory, entry.name);
1410
+ if (entry.isSymbolicLink()) {
1411
+ throw new EvePackagingError({
1412
+ code: "UNSUPPORTED_EVE_OUTPUT",
1413
+ subject: `${EVE_SANDBOX_CACHE_ROOT}/${relativePath}`,
1414
+ reason: "The prepared sandbox template tree contains a symbolic link.",
1415
+ remediation: "Rebuild the candidate so the isolated builder extracts regular template files only.",
1416
+ });
1417
+ }
1418
+ if (entry.isDirectory()) {
1419
+ await visit(candidate, relativePath);
1420
+ continue;
1421
+ }
1422
+ if (!entry.isFile()) {
1423
+ throw new EvePackagingError({
1424
+ code: "UNSUPPORTED_EVE_OUTPUT",
1425
+ subject: `${EVE_SANDBOX_CACHE_ROOT}/${relativePath}`,
1426
+ reason: "The prepared sandbox template tree contains a device, socket, FIFO, or other unsupported file.",
1427
+ remediation: "Rebuild the candidate so the isolated builder extracts regular template files only.",
1428
+ });
1429
+ }
1430
+ let stable;
1431
+ try {
1432
+ stable = await readStableFile(candidate, snapshotRoot);
1433
+ }
1434
+ catch {
1435
+ throw new EvePackagingError({
1436
+ code: "UNSUPPORTED_EVE_OUTPUT",
1437
+ subject: `${EVE_SANDBOX_CACHE_ROOT}/${relativePath}`,
1438
+ reason: "The prepared sandbox template file is unreadable or changed during validation.",
1439
+ remediation: "Rebuild the candidate so the isolated builder extracts a stable template tree.",
1440
+ });
1441
+ }
1442
+ totalBytes += stable.bytes.byteLength;
1443
+ if (totalBytes > EVE_SANDBOX_CACHE_MAX_BYTES) {
1444
+ throw new EvePackagingError({
1445
+ code: "UNSUPPORTED_EVE_OUTPUT",
1446
+ subject: EVE_SANDBOX_CACHE_ROOT,
1447
+ reason: `The prepared sandbox template tree exceeds the ${EVE_SANDBOX_CACHE_MAX_BYTES}-byte sanity bound.`,
1448
+ remediation: "Rebuild the candidate; the extracted template tree must contain only Eve's prepared templates.",
1449
+ });
1450
+ }
1451
+ files.push({
1452
+ relativePath: `${EVE_SANDBOX_CACHE_ROOT}/${relativePath}`,
1453
+ sha256: stable.sha256,
1454
+ byteLength: stable.bytes.byteLength,
1455
+ });
1456
+ }
1457
+ };
1458
+ await visit(cacheRoot, "");
1459
+ return {
1460
+ present: true,
1461
+ root: EVE_SANDBOX_CACHE_ROOT,
1462
+ outputDigest: sha256(jsonBytes(files)),
1463
+ fileCount: files.length,
1464
+ totalBytes,
1465
+ };
1466
+ }
1224
1467
  async function assertGeneratedTreesExcludeSecrets(snapshotRoot) {
1225
1468
  const roots = [
1226
1469
  join(snapshotRoot, "node_modules"),
1227
1470
  join(snapshotRoot, ".output"),
1471
+ join(snapshotRoot, EVE_SANDBOX_CACHE_ROOT),
1228
1472
  ];
1229
1473
  const visit = async (directory, relativeDirectory) => {
1230
1474
  const directoryDetails = await lstat(directory).catch(() => undefined);
@@ -1301,7 +1545,7 @@ async function assertGeneratedTreesExcludeSecrets(snapshotRoot) {
1301
1545
  }
1302
1546
  }
1303
1547
  }
1304
- function safeError(error) {
1548
+ export function safeError(error) {
1305
1549
  return {
1306
1550
  code: error.code,
1307
1551
  subject: error.subject,
@@ -1447,12 +1691,11 @@ function blockedResult(error, extra = {}) {
1447
1691
  status: "blocked",
1448
1692
  returnCode: error.code,
1449
1693
  deployable: false,
1450
- project: extra.project ?? null,
1694
+ project: null,
1451
1695
  candidate: null,
1452
1696
  snapshot: null,
1453
1697
  toolchain: null,
1454
1698
  image: null,
1455
- runtime: null,
1456
1699
  secrets: {
1457
1700
  runtimeVariableNames: [],
1458
1701
  valuesRecorded: false,
@@ -1464,19 +1707,39 @@ function blockedResult(error, extra = {}) {
1464
1707
  excludedFromManifestsAndLogs: true,
1465
1708
  redactionRegisteredBeforeChildren: false,
1466
1709
  },
1467
- checks: extra.checks ?? [{
1710
+ checks: [{
1468
1711
  id: error.code,
1469
1712
  status: "blocked",
1470
1713
  subject: error.subject,
1471
1714
  reason: error.message,
1472
1715
  remediation: error.remediation,
1473
1716
  }],
1717
+ sandboxResumePatch: "absent",
1474
1718
  candidateImageId: null,
1475
1719
  candidateImageRetainedLocally: false,
1476
1720
  writtenPaths: extra.writtenPaths ?? [],
1477
1721
  error: safeError(error),
1478
1722
  };
1479
1723
  }
1724
+ /**
1725
+ * Decides the sandbox-resume patch outcome from the `node_modules` the
1726
+ * builder copied out of the finished image: the shipped just-bash binding is
1727
+ * the single source of truth and stays correct under Docker layer caching
1728
+ * (a cached patch step still ships the patched file). Reading never throws —
1729
+ * an unreadable tree counts as `absent`.
1730
+ */
1731
+ async function detectEveSandboxResumePatch(snapshotRoot) {
1732
+ try {
1733
+ const packageJsonPath = await realpath(join(snapshotRoot, "node_modules/eve/package.json"));
1734
+ const binding = await readFile(join(dirname(packageJsonPath), EVE_JUST_BASH_BINDING_PATH), "utf8");
1735
+ return binding.includes(EVE_SANDBOX_RESUME_PATCH_SIGNATURE)
1736
+ ? "applied"
1737
+ : "skipped";
1738
+ }
1739
+ catch {
1740
+ return "absent";
1741
+ }
1742
+ }
1480
1743
  async function currentInputIdentity(runtimeConfig) {
1481
1744
  const token = (value) => {
1482
1745
  if (typeof value === "string")
@@ -1743,7 +2006,9 @@ export async function buildEveProjectSnapshot(options) {
1743
2006
  return blockedResult(cleanupError ?? dependencyError, { writtenPaths });
1744
2007
  }
1745
2008
  const eve = await resolveProjectLocalEve(snapshotRoot);
2009
+ const sandboxResumePatch = await detectEveSandboxResumePatch(snapshotRoot);
1746
2010
  const output = await scanGeneratedOutput(snapshotRoot);
2011
+ const sandboxCache = await scanGeneratedSandboxCache(snapshotRoot);
1747
2012
  await assertGeneratedTreesExcludeSecrets(snapshotRoot);
1748
2013
  const snapshotSourceDigest = await snapshotDigest(snapshotRoot);
1749
2014
  const runtimeManifestPath = join(generationRoot, "runtime-manifest.json");
@@ -1758,6 +2023,7 @@ export async function buildEveProjectSnapshot(options) {
1758
2023
  sourceDigest: initialInputs.sourceDigest,
1759
2024
  snapshotDigest: snapshotSourceDigest,
1760
2025
  generatedOutput: output,
2026
+ sandboxCache,
1761
2027
  artifactPath: EVE_ENTRYPOINT,
1762
2028
  platform: "linux/amd64",
1763
2029
  runtimeVariableNames,
@@ -1791,10 +2057,8 @@ export async function buildEveProjectSnapshot(options) {
1791
2057
  excludedCategories: [...new Set(initialInputs.excludedRelativePaths.map((path) => isPathExcluded(path, undefined).category ?? "generated-state"))].sort(),
1792
2058
  sourceRaceChecked: true,
1793
2059
  };
1794
- if ((buildResult.imageId !== undefined &&
1795
- !/^sha256:[0-9a-f]{64}$/u.test(buildResult.imageId)) ||
1796
- (buildResult.imageId !== undefined &&
1797
- buildResult.imagePlatform !== "linux/amd64")) {
2060
+ if (buildResult.imageId !== undefined &&
2061
+ !IMAGE_ID_PATTERN.test(buildResult.imageId)) {
1798
2062
  const imageError = new EvePackagingError({
1799
2063
  code: "DOCKER_PLATFORM_BLOCKED",
1800
2064
  subject: "linux/amd64 image",
@@ -1835,6 +2099,7 @@ export async function buildEveProjectSnapshot(options) {
1835
2099
  sourceDigest: initialInputs.sourceDigest,
1836
2100
  snapshotDigest: snapshotSourceDigest,
1837
2101
  generatedOutput: output,
2102
+ sandboxCache,
1838
2103
  ...(initialRuntimeInputIdentity === undefined
1839
2104
  ? {}
1840
2105
  : { runtimeConfigInputIdentity: initialRuntimeInputIdentity }),
@@ -1868,7 +2133,6 @@ export async function buildEveProjectSnapshot(options) {
1868
2133
  },
1869
2134
  image,
1870
2135
  candidate,
1871
- runtime: null,
1872
2136
  secrets: {
1873
2137
  runtimeVariableNames,
1874
2138
  valuesRecorded: false,
@@ -1916,7 +2180,17 @@ export async function buildEveProjectSnapshot(options) {
1916
2180
  reason: "The resolved Eve executable is inside the isolated project dependency tree.",
1917
2181
  remediation: null,
1918
2182
  },
2183
+ ...(sandboxResumePatch === "applied"
2184
+ ? [{
2185
+ id: "EVE_SANDBOX_RESUME_PATCH",
2186
+ status: "pass",
2187
+ subject: "eve-sandbox-resume",
2188
+ reason: "applied temporary fix for eve#4440",
2189
+ remediation: null,
2190
+ }]
2191
+ : []),
1919
2192
  ],
2193
+ sandboxResumePatch,
1920
2194
  candidateImageId: buildResult.imageId ?? null,
1921
2195
  candidateImageRetainedLocally: buildResult.imageId !== undefined,
1922
2196
  writtenPaths,
@@ -2049,15 +2323,7 @@ export function createDockerEveProjectBuilder(options) {
2049
2323
  });
2050
2324
  }
2051
2325
  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
- };
2326
+ const safeEnv = safeDockerEnvironment();
2061
2327
  let containerId;
2062
2328
  let imageBuilt = false;
2063
2329
  let retainImage = false;
@@ -2100,7 +2366,7 @@ export function createDockerEveProjectBuilder(options) {
2100
2366
  ], { env: safeEnv, cwd: request.generationRoot, maxBuffer: 1024 * 1024 });
2101
2367
  imageBuilt = true;
2102
2368
  builtImageId = (await readFile(imageIdFile, "utf8")).trim();
2103
- if (!/^sha256:[0-9a-f]{64}$/u.test(builtImageId)) {
2369
+ if (!IMAGE_ID_PATTERN.test(builtImageId)) {
2104
2370
  throw new EvePackagingError({
2105
2371
  code: "DOCKER_PLATFORM_BLOCKED",
2106
2372
  subject: "image identity",
@@ -2139,6 +2405,30 @@ export function createDockerEveProjectBuilder(options) {
2139
2405
  }
2140
2406
  await execFileAsync(dockerCommand, ["cp", `${containerId}:/app/.output`, join(request.snapshotRoot, ".output")], { env: safeEnv, cwd: request.generationRoot });
2141
2407
  await execFileAsync(dockerCommand, ["cp", `${containerId}:/app/node_modules`, join(request.snapshotRoot, "node_modules")], { env: safeEnv, cwd: request.generationRoot });
2408
+ // The builder stage's `eve build` prewarm writes the just-bash sandbox
2409
+ // template under its workdir; the runtime stage copies it to
2410
+ // /app/.eve/sandbox-cache (the mkdir in the Dockerfile guarantees the
2411
+ // path exists). Extract exactly that subtree so runtime sandboxes can
2412
+ // resolve their template; never the rest of `.eve` (sessions, locks,
2413
+ // builds). Treated as absent when the container has no such path.
2414
+ const sandboxCacheTarget = join(request.snapshotRoot, EVE_SANDBOX_CACHE_ROOT);
2415
+ await ensureSnapshotDirectory(request.snapshotRoot, dirname(sandboxCacheTarget));
2416
+ try {
2417
+ await execFileAsync(dockerCommand, ["cp", `${containerId}:/app/${EVE_SANDBOX_CACHE_ROOT}`, sandboxCacheTarget], { env: safeEnv, cwd: request.generationRoot });
2418
+ }
2419
+ catch (error) {
2420
+ const stderr = typeof error === "object" &&
2421
+ error !== null &&
2422
+ "stderr" in error &&
2423
+ typeof error.stderr === "string"
2424
+ ? error.stderr
2425
+ : "";
2426
+ if (!/(?:could not find|no such|not found|does not exist)/iu.test(stderr)) {
2427
+ throw error;
2428
+ }
2429
+ // Absent: remove the `.eve` parent this extraction created.
2430
+ await rm(dirname(sandboxCacheTarget), { recursive: true, force: true }).catch(() => undefined);
2431
+ }
2142
2432
  const result = {
2143
2433
  imageId: builtImageId,
2144
2434
  imagePlatform: "linux/amd64",
@@ -2174,15 +2464,7 @@ export function createDockerEveProjectBuilder(options) {
2174
2464
  const imageReference = result.imageReference;
2175
2465
  if (imageReference === undefined)
2176
2466
  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
- };
2467
+ const safeEnv = safeDockerEnvironment();
2186
2468
  await execFileAsync(dockerCommand, ["image", "rm", "--force", imageReference], { env: safeEnv });
2187
2469
  const remainingImage = await execFileAsync(dockerCommand, ["image", "inspect", imageReference, "--format", "{{.Id}}"], { env: safeEnv }).catch(() => undefined);
2188
2470
  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 ||