@moinulmoin/eden 0.8.0 → 0.10.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/README.md CHANGED
@@ -12,15 +12,15 @@ If you already have an Eve project, start with `eden deploy`.
12
12
  Use any one of these package managers:
13
13
 
14
14
  ```sh
15
- npm install --global @moinulmoin/eden@0.8.0
15
+ npm install --global @moinulmoin/eden@0.10.0
16
16
  ```
17
17
 
18
18
  ```sh
19
- pnpm add --global @moinulmoin/eden@0.8.0
19
+ pnpm add --global @moinulmoin/eden@0.10.0
20
20
  ```
21
21
 
22
22
  ```sh
23
- bun add --global @moinulmoin/eden@0.8.0
23
+ bun add --global @moinulmoin/eden@0.10.0
24
24
  ```
25
25
 
26
26
  Then confirm the CLI is available:
@@ -128,6 +128,11 @@ export interface EveDeploymentIdentity {
128
128
  readonly sourceDigest: string;
129
129
  readonly generationId: string;
130
130
  readonly deploymentId: string;
131
+ readonly releaseId?: string;
132
+ readonly eveVersion?: string;
133
+ readonly affinityContract?: number;
134
+ readonly retainedDeployments?: Readonly<Record<string, EveAffinityDeployment>>;
135
+ readonly pendingRetirements?: Readonly<Record<string, EveAffinityDeployment>>;
131
136
  readonly environment: EveCliEnvironment;
132
137
  readonly name: string;
133
138
  readonly accountId: string;
@@ -144,6 +149,28 @@ export type EveDeploymentIdentityProof = Partial<EveDeploymentIdentity> & {
144
149
  /** The deployment the serving Container verifiably started under; set only by a deployed Eden host that proves its running image before stamping. */
145
150
  readonly startedDeploymentId?: string;
146
151
  };
152
+ export interface EveAffinityDeployment {
153
+ readonly image: string;
154
+ readonly generationId: string;
155
+ readonly runtimeRevisionHandle?: string;
156
+ readonly eveVersion?: string;
157
+ readonly affinityContract?: number;
158
+ readonly releaseId?: string;
159
+ readonly worldRpcAuth?: boolean;
160
+ }
161
+ export interface EveDeploymentDrainState {
162
+ readonly liveRuns: number;
163
+ readonly liveSessions: number;
164
+ readonly pendingMessages: number;
165
+ readonly unfinishedTransfers: number;
166
+ readonly drained: boolean;
167
+ }
168
+ export type EveDeploymentRpcRunner = (request: {
169
+ readonly origin: string;
170
+ readonly operation: "deployments.drainState" | "deployments.retire";
171
+ readonly deploymentIds: readonly string[];
172
+ readonly controlToken: string;
173
+ }) => Promise<unknown>;
147
174
  export interface EveDeploymentPublicationRequest {
148
175
  readonly identity: EveDeploymentIdentity;
149
176
  readonly hostConfig: EveHostConfig;
@@ -233,6 +260,7 @@ export interface EvePreflightOptions {
233
260
  readonly protectedStore?: EveRuntimeProtectedStore;
234
261
  readonly publish?: EveDeploymentPublicationRunner;
235
262
  readonly publishImage?: EveImagePublicationRunner;
263
+ readonly deploymentRpc?: EveDeploymentRpcRunner;
236
264
  readonly compensate?: EveDeploymentCompensationRunner;
237
265
  readonly health?: EveDeploymentHealthRunner;
238
266
  readonly afterPromotion?: () => void | Promise<void>;
@@ -1,3 +1,4 @@
1
+ import { setTimeout as waitForActivation } from "node:timers/promises";
1
2
  import { createRequire, } from "node:module";
2
3
  import { createHash, randomUUID, } from "node:crypto";
3
4
  import { join, dirname, relative, } from "node:path";
@@ -8,10 +9,10 @@ import { execFile, spawn, } from "node:child_process";
8
9
  import { fileURLToPath, } from "node:url";
9
10
  import { promisify, } from "node:util";
10
11
  import { EveCliError, EVE_NAME_PATTERN, } from "./eve.js";
11
- import { buildEveProjectSnapshot, createDockerEveProjectBuilder, revalidateEveProjectCandidateInputs, EvePackagingError, IMAGE_ID_PATTERN, } from "./eve-packaging.js";
12
+ import { buildEveProjectSnapshot, checkEveSandboxProvider, createDockerEveProjectBuilder, revalidateEveProjectCandidateInputs, EvePackagingError, IMAGE_ID_PATTERN, } from "./eve-packaging.js";
12
13
  import { buildEveRuntimeImage, discardEveRuntimeImage, revalidateEveRuntimeCandidate, validateEveHostRequirements, } from "./eve-runtime-image.js";
13
14
  import { EveRuntimeConfigError, prepareEveRuntimeInjection, redactEveRuntimeOutput, } from "./eve-runtime-config.js";
14
- import { createEveHostConfig, generateEveHostWorkerSource, parseEveScheduleCron, resolveStableWorkersDevOrigin, } from "@moinulmoin/eden-runtime-cloudflare";
15
+ import { EDEN_AFFINITY_CONTRACT, callDeploymentControlRpc, createEveHostConfig, generateEveHostWorkerSource, parseEveScheduleCron, resolveStableWorkersDevOrigin, } from "@moinulmoin/eden-runtime-cloudflare";
15
16
  const execFileAsync = promisify(execFile);
16
17
  const require = createRequire(import.meta.url);
17
18
  const wranglerEntrypoint = require.resolve("wrangler");
@@ -32,6 +33,54 @@ export const DEFAULT_EVE_NODE_IMAGE = Object.freeze({
32
33
  });
33
34
  /** The single tag Eden pushes to the managed Cloudflare registry per generation. */
34
35
  const EVE_REGISTRY_IMAGE_TAG = "candidate";
36
+ function validateDeploymentDrainStates(value, ids) {
37
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
38
+ throw deploymentFailure("EVE_AFFINITY_DRAIN_UNPROVEN", "The authenticated World did not return deployment drain states.");
39
+ }
40
+ const states = value;
41
+ for (const id of ids) {
42
+ const state = states[id];
43
+ if (typeof state !== "object" || state === null ||
44
+ typeof state.drained !== "boolean" ||
45
+ [state.liveRuns, state.liveSessions, state.pendingMessages, state.unfinishedTransfers]
46
+ .some((count) => !Number.isSafeInteger(count) || count < 0) ||
47
+ state.drained !== (state.liveRuns + state.liveSessions + state.pendingMessages + state.unfinishedTransfers === 0)) {
48
+ throw deploymentFailure("EVE_AFFINITY_DRAIN_UNPROVEN", `The authenticated World returned an invalid drain state for ${id}.`);
49
+ }
50
+ }
51
+ return states;
52
+ }
53
+ const defaultDeploymentRpc = async (request) => {
54
+ // A successful Cloudflare secret write creates an active deployment, but
55
+ // existing stateless Worker isolates may briefly serve the preceding secret.
56
+ // Only the first read-only drain call following our own rotation may wait
57
+ // for that exact in-memory credential. All other failures remain terminal.
58
+ const activationRead = request.operation === "deployments.drainState";
59
+ const deadline = Date.now() + 60_000;
60
+ for (;;) {
61
+ const remaining = deadline - Date.now();
62
+ if (remaining <= 0) {
63
+ throw deploymentFailure("EVE_AFFINITY_CONTROL_ACTIVATION_TIMEOUT", "The freshly rotated deployment-control credential did not activate within 60 seconds; publication was refused.");
64
+ }
65
+ try {
66
+ return await callDeploymentControlRpc({
67
+ origin: request.origin,
68
+ token: request.controlToken,
69
+ operation: request.operation,
70
+ args: [{ deploymentIds: request.deploymentIds }],
71
+ signal: AbortSignal.timeout(remaining),
72
+ });
73
+ }
74
+ catch (error) {
75
+ if (!activationRead || typeof error !== "object" || error === null ||
76
+ !("status" in error) || error.status !== 401)
77
+ throw error;
78
+ const delay = Math.min(1_000, deadline - Date.now());
79
+ if (delay > 0)
80
+ await waitForActivation(delay);
81
+ }
82
+ }
83
+ };
35
84
  function safeText(value) {
36
85
  return redactEveRuntimeOutput(value)
37
86
  .replace(/(?:^|[\s"'=])(?:Bearer|Token)\s+\S+/giu, "$1[redacted]")
@@ -65,6 +114,7 @@ function mapPackagingCheck(value) {
65
114
  case "DOCKER_PLATFORM_BLOCKED":
66
115
  return "VAL-BUILD-005";
67
116
  case "EVE_SANDBOX_RESUME_PATCH":
117
+ case "EVE_SANDBOX_PROVIDER":
68
118
  return value.id;
69
119
  default:
70
120
  return `EVE-${value.id}`;
@@ -470,13 +520,13 @@ function safeDockerEnvironment() {
470
520
  DOCKER_CONTEXT: process.env.DOCKER_CONTEXT,
471
521
  };
472
522
  }
473
- async function readWranglerJson(args) {
523
+ async function readWranglerJson(args, timeoutMs = 30_000) {
474
524
  try {
475
525
  const result = await execFileAsync(process.execPath, [wranglerEntrypoint, ...args], {
476
526
  env: safeWranglerEnvironment(),
477
527
  cwd: wranglerCwd,
478
528
  maxBuffer: 2 * 1024 * 1024,
479
- timeout: 30_000,
529
+ timeout: timeoutMs,
480
530
  });
481
531
  const parsed = JSON.parse(result.stdout.trim());
482
532
  return { value: parsed, failed: false };
@@ -812,11 +862,33 @@ async function resolveEveTargetOwnership(request, result) {
812
862
  evidence.containerNamespaceId === evidence.workerContainerNamespaceId);
813
863
  const remoteProven = evidence !== undefined && workerProven && containerProven;
814
864
  if (!remoteProven) {
865
+ const failures = [];
866
+ if (evidence === undefined)
867
+ failures.push("provider evidence unavailable");
868
+ if (result.target.observed?.worker === true) {
869
+ if (evidence?.settingsReadable !== true)
870
+ failures.push("Worker settings unreadable");
871
+ else if (evidence.workerDeploymentId === undefined)
872
+ failures.push("Worker deployment stamp absent");
873
+ else if (!recordedDeploymentIds.has(evidence.workerDeploymentId))
874
+ failures.push("Worker deployment stamp differs from recorded identities");
875
+ }
876
+ if (result.target.observed?.container === true) {
877
+ if (evidence?.containerSchedulingPolicy !== "durable_object")
878
+ failures.push("Container scheduling policy unproven");
879
+ if (evidence?.containerNamespaceId === undefined)
880
+ failures.push("Container namespace unreadable");
881
+ if (evidence?.workerContainerNamespaceId === undefined)
882
+ failures.push("Worker Container binding namespace unreadable");
883
+ if (evidence?.containerNamespaceId !== undefined && evidence.workerContainerNamespaceId !== undefined &&
884
+ evidence.containerNamespaceId !== evidence.workerContainerNamespaceId)
885
+ failures.push("Container and Worker binding namespaces differ");
886
+ }
815
887
  return {
816
888
  ...result,
817
889
  target: {
818
890
  ...result.target,
819
- message: "The exact Worker or Container target exists, and although a local Eden record matches, provider state cannot prove it is the one Eden deployed — the target may have been recreated outside Eden.",
891
+ message: `The exact Worker or Container target exists, and although a local Eden record matches, provider state cannot prove it is the one Eden deployed — the target may have been recreated outside Eden. Unproven: ${failures.join("; ")}.`,
820
892
  remediation: "Inspect the Worker at its workers.dev origin and the recorded deployment history before redeploying; if it is not Eden's, deploy under a different --name or delete it yourself.",
821
893
  },
822
894
  };
@@ -994,11 +1066,17 @@ function transportIdentityMatches(expected, observed) {
994
1066
  "generationId",
995
1067
  "stableWorkersDevOrigin",
996
1068
  "workerName",
997
- "stableContainerInstanceName",
998
1069
  ];
999
1070
  if (!required.every((key) => observed[key] === expected[key])) {
1000
1071
  return false;
1001
1072
  }
1073
+ const expectedInstance = expected.retainedDeployments === undefined ||
1074
+ expected.deploymentId === "eden-cloudflare"
1075
+ ? expected.stableContainerInstanceName
1076
+ : `${expected.stableContainerInstanceName}:${expected.deploymentId}`;
1077
+ if (observed.stableContainerInstanceName !== expectedInstance ||
1078
+ observed.releaseId !== expected.releaseId)
1079
+ return false;
1002
1080
  return expected.runtimeRevisionHandle === undefined
1003
1081
  ? observed.runtimeRevisionHandle === undefined
1004
1082
  : observed.runtimeRevisionHandle === expected.runtimeRevisionHandle;
@@ -1011,6 +1089,7 @@ function identityMatches(expected, observed) {
1011
1089
  "sourceDigest",
1012
1090
  "generationId",
1013
1091
  "deploymentId",
1092
+ "releaseId",
1014
1093
  "environment",
1015
1094
  "name",
1016
1095
  "accountId",
@@ -1038,6 +1117,11 @@ function safeIdentity(identity) {
1038
1117
  sourceDigest: identity.sourceDigest,
1039
1118
  generationId: identity.generationId,
1040
1119
  deploymentId: identity.deploymentId,
1120
+ releaseId: identity.releaseId,
1121
+ eveVersion: identity.eveVersion,
1122
+ affinityContract: identity.affinityContract,
1123
+ retainedDeployments: identity.retainedDeployments,
1124
+ pendingRetirements: identity.pendingRetirements,
1041
1125
  environment: identity.environment,
1042
1126
  name: identity.name,
1043
1127
  accountId: identity.accountId,
@@ -1280,6 +1364,7 @@ function responseIdentity(response, body) {
1280
1364
  read("sourceDigest", "x-eden-eve-source-digest");
1281
1365
  read("generationId", "x-eden-eve-generation-id");
1282
1366
  read("deploymentId", "x-eden-eve-deployment-id");
1367
+ read("releaseId", "x-eden-eve-release-id");
1283
1368
  read("environment", "x-eden-eve-environment");
1284
1369
  read("name", "x-eden-eve-name");
1285
1370
  read("accountId", "x-eden-eve-account-id");
@@ -1340,7 +1425,7 @@ function defaultHealthRunner() {
1340
1425
  // must wait until the *served container* verifiably runs this
1341
1426
  // generation's image (the host stamps the header only after
1342
1427
  // inspecting the running instance).
1343
- if (observedIdentity.startedDeploymentId !== request.identity.deploymentId) {
1428
+ if (observedIdentity.startedDeploymentId !== (request.identity.releaseId ?? request.identity.deploymentId)) {
1344
1429
  lastReason =
1345
1430
  "The served Container has not verifiably started this deployment's image yet; waiting for the rollout to land.";
1346
1431
  await wait();
@@ -1459,8 +1544,8 @@ async function promoteEveTargetPointer(deploymentRoot, pointerKey, generationRoo
1459
1544
  }
1460
1545
  return currentPath;
1461
1546
  }
1462
- async function writeEveDeploymentRecord(candidate, identity, hostConfig, status, extra = {}) {
1463
- await writeFile(join(candidate.generationRoot, "deployment.json"), `${JSON.stringify({
1547
+ async function writeEveDeploymentRecord(candidate, identity, hostConfig, status, extra = {}, filename = "deployment.json") {
1548
+ await writeFile(join(candidate.generationRoot, filename), `${JSON.stringify({
1464
1549
  version: 1,
1465
1550
  status,
1466
1551
  identity: safeIdentity(identity),
@@ -1473,6 +1558,10 @@ async function writeEveDeploymentRecord(candidate, identity, hostConfig, status,
1473
1558
  flag: "wx",
1474
1559
  });
1475
1560
  }
1561
+ async function deploymentRecordPath(generationRoot) {
1562
+ const path = join(generationRoot, "deployment.json");
1563
+ return await lstat(path).then(() => path, () => join(generationRoot, "deployment-intent.json"));
1564
+ }
1476
1565
  async function writeEveIndeterminateEvidence(candidate, evidence, extra = {}) {
1477
1566
  await writeFile(join(candidate.generationRoot, "deployment-attempt.json"), `${JSON.stringify({
1478
1567
  version: 1,
@@ -1537,6 +1626,14 @@ async function runEveDeployment(request, options, collected) {
1537
1626
  }
1538
1627
  const priorIdentity = prior?.identity;
1539
1628
  const priorDurableObjects = prior?.durableObjects;
1629
+ const worldManifest = collected.workflowWorld === "@moinulmoin/eden-world-cloudflare"
1630
+ ? await readInstalledManifest(candidate.snapshotRoot, collected.workflowWorld)
1631
+ : undefined;
1632
+ const affinityEnabled = worldManifest?.eden?.capabilities?.deploymentAffinity === true;
1633
+ const worldRpcAuth = worldManifest?.eden?.capabilities?.worldRpcAuth === true;
1634
+ if (!affinityEnabled && priorIdentity?.retainedDeployments !== undefined) {
1635
+ throw deploymentFailure("EVE_AFFINITY_REQUIRED", "This target retains affinity deployments; install a World package declaring deploymentAffinity before updating it.");
1636
+ }
1540
1637
  const superseded = isUpdate
1541
1638
  ? {
1542
1639
  generationId: priorIdentity?.generationId,
@@ -1552,8 +1649,12 @@ async function runEveDeployment(request, options, collected) {
1552
1649
  if (durableWorld) {
1553
1650
  resultWarnings.push({
1554
1651
  id: "EVE_UPDATE_IN_FLIGHT",
1555
- message: "This update replaces the running Container; durable World runs in flight resume on the new code — Cloudflare has no deployment pinning, so keep step names and payload shapes backward compatible.",
1556
- remediation: "Let pending approvals and long runs settle before updating, or keep step names, queue names, and serialized payloads compatible across the change.",
1652
+ message: affinityEnabled
1653
+ ? "This update retains pinned deployments until their sessions, queue messages, and transfers drain."
1654
+ : "This update replaces the running Container; an Eve version change can strand existing sessions because the installed World does not declare deployment affinity.",
1655
+ remediation: affinityEnabled
1656
+ ? "End old sessions to allow their deployment images to retire."
1657
+ : "Let sessions settle before changing Eve versions, or install an affinity-capable World package.",
1557
1658
  });
1558
1659
  }
1559
1660
  if (prior?.worldCloudflare !==
@@ -1594,6 +1695,80 @@ async function runEveDeployment(request, options, collected) {
1594
1695
  throw deploymentFailure("EVE_IMAGE_IDENTITY_UNAVAILABLE", "The deploy candidate did not provide an immutable runtime image digest.");
1595
1696
  }
1596
1697
  const targetKey = stableDeploymentKey(collected.projectId, accountId, request.environment, request.name);
1698
+ const releaseId = `eve-deploy-${randomUUID()}`;
1699
+ let previousEveVersion = priorIdentity?.eveVersion;
1700
+ if (previousEveVersion === undefined && prior !== undefined) {
1701
+ previousEveVersion = (await readInstalledManifest(join(prior.generationRoot, "container/snapshot"), "eve"))?.version;
1702
+ }
1703
+ const retainedDeployments = {
1704
+ ...priorIdentity?.retainedDeployments,
1705
+ };
1706
+ if (affinityEnabled && priorIdentity?.retainedDeployments === undefined &&
1707
+ priorIdentity?.containerImage !== undefined && priorIdentity.generationId !== undefined) {
1708
+ retainedDeployments["eden-cloudflare"] = {
1709
+ image: priorIdentity.containerImage,
1710
+ generationId: priorIdentity.generationId,
1711
+ ...(previousEveVersion === undefined ? {} : { eveVersion: previousEveVersion }),
1712
+ affinityContract: EDEN_AFFINITY_CONTRACT,
1713
+ ...(priorIdentity.runtimeRevisionHandle === undefined ? {} : { runtimeRevisionHandle: priorIdentity.runtimeRevisionHandle }),
1714
+ };
1715
+ }
1716
+ const matchingDeployment = Object.entries(retainedDeployments).find(([, entry]) => entry.eveVersion === candidate.eveVersion && entry.affinityContract === EDEN_AFFINITY_CONTRACT)?.[0];
1717
+ const deploymentId = !affinityEnabled ? releaseId : matchingDeployment ??
1718
+ `eden-affinity-${createHash("sha256").update(`${candidate.eveVersion}:${EDEN_AFFINITY_CONTRACT}`).digest("hex").slice(0, 24)}`;
1719
+ const retiringDeployments = { ...priorIdentity?.pendingRetirements };
1720
+ delete retiringDeployments[deploymentId];
1721
+ if (priorIdentity?.deploymentId !== undefined)
1722
+ delete retiringDeployments[priorIdentity.deploymentId];
1723
+ for (const id of prior?.protectedDeploymentIds ?? [])
1724
+ delete retiringDeployments[id];
1725
+ const hasRetainedNoncurrentDeployments = priorIdentity?.retainedDeployments !== undefined &&
1726
+ Object.keys(retainedDeployments).some((id) => id !== priorIdentity.deploymentId);
1727
+ const controlToken = `${randomUUID()}${randomUUID()}`;
1728
+ if (affinityEnabled && (hasRetainedNoncurrentDeployments || Object.keys(retiringDeployments).length > 0)) {
1729
+ await (options.protectedStore ?? defaultProtectedStore(request.name)).put({
1730
+ targetId: targetKey, revision: `eve-affinity-control-${randomUUID()}`,
1731
+ inputIdentity: {
1732
+ token: `eve-affinity-control-input-${randomUUID()}`,
1733
+ byteLength: Buffer.byteLength(controlToken),
1734
+ digest: createHash("sha256").update(controlToken).digest("hex"),
1735
+ },
1736
+ variableNames: ["EDEN_EVE_CONTROL_TOKEN"], values: { EDEN_EVE_CONTROL_TOKEN: controlToken },
1737
+ });
1738
+ }
1739
+ if (affinityEnabled && hasRetainedNoncurrentDeployments) {
1740
+ const ids = Object.keys(retainedDeployments);
1741
+ const raw = await (options.deploymentRpc ?? defaultDeploymentRpc)({
1742
+ origin: stableWorkersDevOrigin, operation: "deployments.drainState", deploymentIds: ids,
1743
+ controlToken,
1744
+ });
1745
+ const states = validateDeploymentDrainStates(raw, ids);
1746
+ const eligible = ids.filter((id) => id !== deploymentId && id !== priorIdentity.deploymentId &&
1747
+ !prior?.protectedDeploymentIds?.includes(id) && states[id]?.drained === true);
1748
+ const remainingCount = ids.length - eligible.length + (deploymentId in retainedDeployments ? 0 : 1);
1749
+ if (remainingCount <= 10 && eligible.length > 0) {
1750
+ // Drain snapshots are not fences. Retire only noncurrent targets, and
1751
+ // obtain the World's durable admission fence before dropping routes.
1752
+ const retired = await (options.deploymentRpc ?? defaultDeploymentRpc)({
1753
+ origin: stableWorkersDevOrigin, operation: "deployments.retire",
1754
+ deploymentIds: eligible, controlToken,
1755
+ });
1756
+ if (typeof retired !== "object" || retired === null || !("retired" in retired) ||
1757
+ !Array.isArray(retired.retired) ||
1758
+ eligible.some((id) => !retired.retired.includes(id))) {
1759
+ throw deploymentFailure("EVE_AFFINITY_RETIRE_UNPROVEN", "The World did not prove the noncurrent deployments durably fenced for retirement.");
1760
+ }
1761
+ for (const id of eligible) {
1762
+ retiringDeployments[id] = retainedDeployments[id];
1763
+ delete retainedDeployments[id];
1764
+ }
1765
+ }
1766
+ if (Object.keys(retainedDeployments).length + (deploymentId in retainedDeployments ? 0 : 1) > 10) {
1767
+ const blockers = Object.entries(states).filter(([id]) => id in retainedDeployments)
1768
+ .map(([id, state]) => `${id}: ${state.liveSessions} live sessions, ${state.liveRuns} live runs, ${state.pendingMessages} pending messages, ${state.unfinishedTransfers} unfinished transfers`);
1769
+ throw deploymentFailure("EVE_AFFINITY_LIMIT", `Deployment refused before image/code publication: at most 10 affinity deployments may be retained. Blocking deployments: ${blockers.join("; ")}`);
1770
+ }
1771
+ }
1597
1772
  let imageReference = options.containerImageReference;
1598
1773
  if (imageReference !== undefined) {
1599
1774
  if (imageDigestFromReference(imageReference) !== imageDigest) {
@@ -1606,7 +1781,7 @@ async function runEveDeployment(request, options, collected) {
1606
1781
  if (localImageReference === undefined || localImageReference.length === 0) {
1607
1782
  throw deploymentFailure("EVE_IMAGE_IDENTITY_UNAVAILABLE", "The deploy candidate did not return an exact local image handle for publication.");
1608
1783
  }
1609
- const imageRepository = `eden-eve-${targetKey}-${candidate.generationId}`;
1784
+ const imageRepository = `eden-eve-${targetKey}-${affinityEnabled ? deploymentId : candidate.generationId}`;
1610
1785
  const imageTag = `${imageRepository}:${EVE_REGISTRY_IMAGE_TAG}`;
1611
1786
  const targetImageReference = `registry.cloudflare.com/${accountId}/${imageRepository}@${imageDigest}`;
1612
1787
  const imagePublication = await (options.publishImage ??
@@ -1668,7 +1843,6 @@ async function runEveDeployment(request, options, collected) {
1668
1843
  throw deploymentFailure("DEPLOY_INDETERMINATE", reason);
1669
1844
  }
1670
1845
  }
1671
- const deploymentId = `eve-deploy-${randomUUID()}`;
1672
1846
  const containerApplicationName = boundedResourceName(request.name, "container");
1673
1847
  const stableContainerInstanceName = boundedResourceName(request.name, "instance");
1674
1848
  const runtimeVariableNames = candidate.runtimeVariableNames;
@@ -1677,6 +1851,24 @@ async function runEveDeployment(request, options, collected) {
1677
1851
  sourceDigest: candidate.sourceDigest,
1678
1852
  generationId: candidate.generationId,
1679
1853
  deploymentId,
1854
+ releaseId,
1855
+ eveVersion: candidate.eveVersion,
1856
+ ...(affinityEnabled ? {
1857
+ affinityContract: EDEN_AFFINITY_CONTRACT,
1858
+ retainedDeployments: {
1859
+ ...retainedDeployments,
1860
+ [deploymentId]: {
1861
+ image: imageReference,
1862
+ generationId: candidate.generationId,
1863
+ eveVersion: candidate.eveVersion,
1864
+ affinityContract: EDEN_AFFINITY_CONTRACT,
1865
+ releaseId,
1866
+ worldRpcAuth,
1867
+ ...(runtimeInjection?.protectedHandle === undefined ? {} : { runtimeRevisionHandle: runtimeInjection.protectedHandle }),
1868
+ },
1869
+ },
1870
+ pendingRetirements: retiringDeployments,
1871
+ } : {}),
1680
1872
  environment: request.environment,
1681
1873
  name: request.name,
1682
1874
  accountId,
@@ -1697,6 +1889,11 @@ async function runEveDeployment(request, options, collected) {
1697
1889
  containerApplicationName: identity.containerApplicationName,
1698
1890
  stableContainerInstanceName: identity.stableContainerInstanceName,
1699
1891
  deploymentId: identity.deploymentId,
1892
+ worldRpcAuth,
1893
+ ...(identity.releaseId === undefined ? {} : { releaseId: identity.releaseId }),
1894
+ ...(identity.eveVersion === undefined ? {} : { eveVersion: identity.eveVersion }),
1895
+ ...(identity.affinityContract === undefined ? {} : { affinityContract: identity.affinityContract }),
1896
+ ...(identity.retainedDeployments === undefined ? {} : { retainedDeployments: identity.retainedDeployments }),
1700
1897
  generationId: identity.generationId,
1701
1898
  stableWorkersDevOrigin: identity.stableWorkersDevOrigin,
1702
1899
  containerImage: identity.containerImage,
@@ -1752,6 +1949,7 @@ async function runEveDeployment(request, options, collected) {
1752
1949
  throw error;
1753
1950
  }
1754
1951
  };
1952
+ await writeEveDeploymentRecord(candidate, identity, hostConfig, "indeterminate", { evidence: "publication-intent" }, "deployment-intent.json");
1755
1953
  try {
1756
1954
  eveProgress(options, "publishing Worker and Container application");
1757
1955
  publication = await publishOne(publicationRequest);
@@ -1954,7 +2152,40 @@ async function runEveDeployment(request, options, collected) {
1954
2152
  await promoteEveTargetPointer(join(request.projectRoot, ".eden", "eve-deploy"), `${request.environment}-${request.name}`, candidate.generationRoot);
1955
2153
  await options.afterPromotion?.();
1956
2154
  let supersededImageCleanup;
1957
- if (isUpdate) {
2155
+ if (affinityEnabled && Object.keys(retiringDeployments).length > 0) {
2156
+ const ids = Object.keys(retiringDeployments);
2157
+ const completedRetirements = [];
2158
+ try {
2159
+ const result = await (options.deploymentRpc ?? defaultDeploymentRpc)({
2160
+ origin: stableWorkersDevOrigin, operation: "deployments.retire", deploymentIds: ids,
2161
+ controlToken,
2162
+ });
2163
+ if (typeof result !== "object" || result === null ||
2164
+ !("retired" in result) || !Array.isArray(result.retired) ||
2165
+ ids.some((id) => !result.retired.includes(id)))
2166
+ throw new Error("Retirement was not proven.");
2167
+ for (const [id, retained] of Object.entries(retiringDeployments)) {
2168
+ const repository = ownedRegistryRepository({
2169
+ ...identity, deploymentId: id, containerImage: retained.image, generationId: retained.generationId,
2170
+ }, request.environment, request.name, accountId);
2171
+ if (repository !== undefined && await (options.deleteRegistryRepository ??
2172
+ defaultRegistryRepositoryDelete)({ repository }) === "indeterminate") {
2173
+ throw new Error(`Repository deletion was not proven: ${repository}`);
2174
+ }
2175
+ completedRetirements.push(id);
2176
+ }
2177
+ supersededImageCleanup = check("VAL-LIFE-007", "passed", "Drained deployment instances and their owned registry repositories were retired.");
2178
+ }
2179
+ catch {
2180
+ supersededImageCleanup = check("VAL-LIFE-007", "failed", "The deployment is healthy, but drained resource retirement was not proven; exact pending retirement records were retained for retry.", "Retry deploy to finish only recorded drained resource cleanup.");
2181
+ }
2182
+ if (completedRetirements.length > 0) {
2183
+ // Keep the immutable deployment record as evidence, but exclude proven
2184
+ // cleanup from the next deploy's active retry set.
2185
+ await writeFile(join(candidate.generationRoot, "completed-retirements.json"), `${JSON.stringify(completedRetirements)}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" });
2186
+ }
2187
+ }
2188
+ if (isUpdate && !affinityEnabled) {
1958
2189
  // The superseded generation's registry repository is generation-scoped
1959
2190
  // and no longer referenced once CURRENT promotes this generation; only
1960
2191
  // then is it safe to delete. Every tag in the repository goes with it —
@@ -2053,6 +2284,22 @@ async function collectEvePreflight(request, options) {
2053
2284
  runtimeConfig,
2054
2285
  };
2055
2286
  }
2287
+ const sandboxProvider = await checkEveSandboxProvider(request.projectRoot);
2288
+ if (sandboxProvider !== undefined) {
2289
+ checks.push(check(sandboxProvider.code, "failed", sandboxProvider.message, sandboxProvider.remediation));
2290
+ return {
2291
+ result: {
2292
+ command: request.command === "deploy" ? "eve deploy" : "eve preflight",
2293
+ ok: false,
2294
+ environment: request.environment,
2295
+ name: request.name,
2296
+ checks,
2297
+ warnings,
2298
+ candidate: null,
2299
+ },
2300
+ runtimeConfig,
2301
+ };
2302
+ }
2056
2303
  warnings.push(...await collectEveSetupWarnings(request.projectRoot));
2057
2304
  eveProgress(options, "packaging project — snapshot, frozen install, eve build");
2058
2305
  const nodeImage = options.nodeImage ?? DEFAULT_EVE_NODE_IMAGE;
@@ -2495,7 +2742,7 @@ async function readEveDestroyRecord(projectRoot, environment, name) {
2495
2742
  if (generationRoot === undefined) {
2496
2743
  return readEveDestroyRecordFromGenerations(projectRoot, environment, name);
2497
2744
  }
2498
- const parsed = await parseEveDestroyRecord(join(generationRoot, "deployment.json"), environment, name);
2745
+ const parsed = await parseEveDestroyRecord(await deploymentRecordPath(generationRoot), environment, name);
2499
2746
  return parsed === undefined
2500
2747
  ? undefined
2501
2748
  : { ...parsed, generationRoot };
@@ -2509,7 +2756,7 @@ async function readEveDestroyRecordFromGenerations(projectRoot, environment, nam
2509
2756
  const details = await lstat(generationRoot).catch(() => undefined);
2510
2757
  if (details === undefined || !details.isDirectory())
2511
2758
  continue;
2512
- const recordDetails = await lstat(join(generationRoot, "deployment.json")).catch(() => undefined);
2759
+ const recordDetails = await lstat(await deploymentRecordPath(generationRoot)).catch(() => undefined);
2513
2760
  if (recordDetails !== undefined && recordDetails.isFile()) {
2514
2761
  withRecords.push({
2515
2762
  generationRoot,
@@ -2519,7 +2766,7 @@ async function readEveDestroyRecordFromGenerations(projectRoot, environment, nam
2519
2766
  }
2520
2767
  withRecords.sort((left, right) => right.modifiedAt - left.modifiedAt);
2521
2768
  for (const { generationRoot } of withRecords) {
2522
- const parsed = await parseEveDestroyRecord(join(generationRoot, "deployment.json"), environment, name);
2769
+ const parsed = await parseEveDestroyRecord(await deploymentRecordPath(generationRoot), environment, name);
2523
2770
  if (parsed !== undefined) {
2524
2771
  return { ...parsed, generationRoot };
2525
2772
  }
@@ -2564,7 +2811,35 @@ async function parseEveDestroyRecord(recordPath, environment, name) {
2564
2811
  if (identity.environment !== environment || identity.name !== name) {
2565
2812
  return undefined;
2566
2813
  }
2814
+ const metadata = {};
2815
+ for (const key of ["deploymentId", "eveVersion", "releaseId", "runtimeRevisionHandle"]) {
2816
+ if (typeof identity[key] === "string")
2817
+ Object.assign(metadata, { [key]: identity[key] });
2818
+ }
2819
+ if (typeof identity.affinityContract === "number")
2820
+ Object.assign(metadata, { affinityContract: identity.affinityContract });
2821
+ for (const key of ["retainedDeployments", "pendingRetirements"]) {
2822
+ const value = identity[key];
2823
+ if (value === undefined)
2824
+ continue;
2825
+ if (typeof value !== "object" || value === null || Array.isArray(value))
2826
+ return undefined;
2827
+ for (const [id, deployment] of Object.entries(value)) {
2828
+ if (!/^(?:eden-cloudflare|eden-affinity-[a-f0-9]{24})$/u.test(id) ||
2829
+ typeof deployment !== "object" || deployment === null ||
2830
+ typeof deployment.image !== "string" || imageDigestFromReference(deployment.image) === undefined ||
2831
+ typeof deployment.generationId !== "string" ||
2832
+ (deployment.eveVersion !== undefined && typeof deployment.eveVersion !== "string") ||
2833
+ (deployment.affinityContract !== undefined && typeof deployment.affinityContract !== "number") ||
2834
+ (deployment.releaseId !== undefined && typeof deployment.releaseId !== "string") ||
2835
+ (deployment.worldRpcAuth !== undefined && typeof deployment.worldRpcAuth !== "boolean") ||
2836
+ (deployment.runtimeRevisionHandle !== undefined && typeof deployment.runtimeRevisionHandle !== "string"))
2837
+ return undefined;
2838
+ }
2839
+ Object.assign(metadata, { [key]: value });
2840
+ }
2567
2841
  const verified = {
2842
+ ...metadata,
2568
2843
  workerName: identity.workerName,
2569
2844
  containerApplicationName: identity.containerApplicationName,
2570
2845
  stableContainerInstanceName: identity.stableContainerInstanceName,
@@ -2603,7 +2878,7 @@ async function readEveDeploymentRecord(projectRoot, environment, name, accountId
2603
2878
  const deploymentIds = new Set();
2604
2879
  for (const entry of entries) {
2605
2880
  const generationRoot = join(generationsRoot, entry);
2606
- const recordPath = join(generationRoot, "deployment.json");
2881
+ const recordPath = await deploymentRecordPath(generationRoot);
2607
2882
  const details = await lstat(recordPath).catch(() => undefined);
2608
2883
  if (details === undefined || !details.isFile())
2609
2884
  continue;
@@ -2636,7 +2911,7 @@ async function readEveDeploymentRecord(projectRoot, environment, name, accountId
2636
2911
  live = pinned;
2637
2912
  }
2638
2913
  else {
2639
- const parsed = await parseEveDeploymentRecord(join(generationRoot, "deployment.json"), environment, name);
2914
+ const parsed = await parseEveDeploymentRecord(await deploymentRecordPath(generationRoot), environment, name);
2640
2915
  if (parsed !== undefined &&
2641
2916
  !((accountId !== undefined &&
2642
2917
  parsed.identity.accountId !== accountId) ||
@@ -2652,10 +2927,31 @@ async function readEveDeploymentRecord(projectRoot, environment, name, accountId
2652
2927
  // Newest record carrying a usable DO history wins, regardless of its
2653
2928
  // deployment outcome: a tag it published must never be replayed.
2654
2929
  const historySource = scanned.find((entry) => entry.record.durableObjects !== undefined);
2930
+ // CURRENT is only a health/promotion pointer. A newer uncertain publication
2931
+ // may already serve sessions; preserve every such image until a later deploy
2932
+ // can prove it drained, never infer absence from a failed health check.
2933
+ const unresolved = scanned.filter((entry) => entry.modifiedAt > live.modifiedAt);
2934
+ const retainedDeployments = { ...live.record.identity.retainedDeployments };
2935
+ const pendingRetirements = { ...live.record.identity.pendingRetirements };
2936
+ const protectedDeploymentIds = new Set();
2937
+ if (live.record.identity.retainedDeployments === undefined) {
2938
+ protectedDeploymentIds.add("eden-cloudflare");
2939
+ }
2940
+ for (const entry of unresolved.slice().reverse()) {
2941
+ Object.assign(retainedDeployments, entry.record.identity.retainedDeployments);
2942
+ Object.assign(pendingRetirements, entry.record.identity.pendingRetirements);
2943
+ if (entry.record.deploymentId !== undefined)
2944
+ protectedDeploymentIds.add(entry.record.deploymentId);
2945
+ }
2655
2946
  return {
2656
2947
  recordPath: live.record.recordPath,
2657
2948
  generationRoot: live.generationRoot,
2658
- identity: live.record.identity,
2949
+ identity: {
2950
+ ...live.record.identity,
2951
+ ...(Object.keys(retainedDeployments).length === 0 ? {} : { retainedDeployments }),
2952
+ ...(Object.keys(pendingRetirements).length === 0 ? {} : { pendingRetirements }),
2953
+ },
2954
+ protectedDeploymentIds: [...protectedDeploymentIds],
2659
2955
  status: live.record.status,
2660
2956
  deploymentIds: [...deploymentIds],
2661
2957
  worldCloudflare: live.record.worldCloudflare,
@@ -2666,6 +2962,19 @@ async function parseEveDeploymentRecord(recordPath, environment, name) {
2666
2962
  const base = await parseEveDestroyRecord(recordPath, environment, name);
2667
2963
  if (base === undefined)
2668
2964
  return undefined;
2965
+ let identity = base.identity;
2966
+ try {
2967
+ const completed = JSON.parse(await readFile(join(dirname(recordPath), "completed-retirements.json"), "utf8"));
2968
+ if (Array.isArray(completed) && completed.every((id) => typeof id === "string")) {
2969
+ const pendingRetirements = { ...identity.pendingRetirements };
2970
+ for (const id of completed)
2971
+ delete pendingRetirements[id];
2972
+ identity = { ...identity, pendingRetirements };
2973
+ }
2974
+ }
2975
+ catch {
2976
+ // Missing or unreadable cleanup evidence must not suppress a retry.
2977
+ }
2669
2978
  let parsed;
2670
2979
  try {
2671
2980
  parsed = JSON.parse(await readFile(recordPath, "utf8"));
@@ -2709,7 +3018,7 @@ async function parseEveDeploymentRecord(recordPath, environment, name) {
2709
3018
  Array.isArray(rawMigrations);
2710
3019
  const invalidHistory = () => ({
2711
3020
  recordPath,
2712
- identity: base.identity,
3021
+ identity,
2713
3022
  status,
2714
3023
  deploymentId,
2715
3024
  worldCloudflare: rawVars?.EDEN_EVE_WORLD_CLOUDFLARE === true,
@@ -2736,7 +3045,7 @@ async function parseEveDeploymentRecord(recordPath, environment, name) {
2736
3045
  }
2737
3046
  return {
2738
3047
  recordPath,
2739
- identity: base.identity,
3048
+ identity,
2740
3049
  status,
2741
3050
  deploymentId,
2742
3051
  worldCloudflare: rawVars?.EDEN_EVE_WORLD_CLOUDFLARE === true,
@@ -2758,7 +3067,7 @@ async function defaultDestroyTargetRead(request) {
2758
3067
  "images",
2759
3068
  "list",
2760
3069
  "--json",
2761
- ]);
3070
+ ], WRANGLER_OPERATION_TIMEOUT_MS);
2762
3071
  const tagsByRepository = registryTagsByRepository(images.value);
2763
3072
  if (!images.failed && tagsByRepository !== undefined) {
2764
3073
  const present = [];
@@ -2837,12 +3146,13 @@ async function defaultContainerDelete(request) {
2837
3146
  * repository is deleted individually.
2838
3147
  */
2839
3148
  async function defaultRegistryRepositoryDelete(request) {
3149
+ // Catalog reads include Cloudflare snapshot tags and can exceed 30 seconds.
2840
3150
  const images = await readWranglerJson([
2841
3151
  "containers",
2842
3152
  "images",
2843
3153
  "list",
2844
3154
  "--json",
2845
- ]);
3155
+ ], WRANGLER_OPERATION_TIMEOUT_MS);
2846
3156
  const collection = jsonCollection(images.value);
2847
3157
  if (images.failed || collection === undefined)
2848
3158
  return "indeterminate";
@@ -2868,9 +3178,11 @@ async function defaultRegistryRepositoryDelete(request) {
2868
3178
  deletedAny = true;
2869
3179
  continue;
2870
3180
  }
2871
- if (!/(?:does not exist|not found|404)/iu.test(result.stderr)) {
2872
- return "indeterminate";
2873
- }
3181
+ }
3182
+ const after = await readWranglerJson(["containers", "images", "list", "--json"], WRANGLER_OPERATION_TIMEOUT_MS);
3183
+ const remaining = after.failed ? undefined : registryTagsByRepository(after.value);
3184
+ if (remaining === undefined || (remaining.get(request.repository)?.length ?? 0) > 0) {
3185
+ return "indeterminate";
2874
3186
  }
2875
3187
  return deletedAny ? "deleted" : "absent";
2876
3188
  }
@@ -2888,13 +3200,13 @@ function ownedRegistryRepository(identity, environment, name, accountId) {
2888
3200
  identity.accountId !== accountId) {
2889
3201
  return undefined;
2890
3202
  }
2891
- const repository = `eden-eve-${stableDeploymentKey(identity.projectId, identity.accountId, environment, name)}-${identity.generationId}`;
2892
- const expected = `registry.cloudflare.com/${identity.accountId}/${repository}@`;
2893
- if (!identity.containerImage.startsWith(expected) ||
2894
- imageDigestFromReference(identity.containerImage) === undefined) {
3203
+ const prefix = `eden-eve-${stableDeploymentKey(identity.projectId, identity.accountId, environment, name)}-`;
3204
+ const suffixes = [identity.generationId];
3205
+ if (identity.retainedDeployments !== undefined && identity.deploymentId !== undefined)
3206
+ suffixes.push(identity.deploymentId);
3207
+ if (imageDigestFromReference(identity.containerImage) === undefined)
2895
3208
  return undefined;
2896
- }
2897
- return repository;
3209
+ return suffixes.map((suffix) => `${prefix}${suffix}`).find((repository) => identity.containerImage.startsWith(`registry.cloudflare.com/${identity.accountId}/${repository}@`));
2898
3210
  }
2899
3211
  /**
2900
3212
  * The registry repository an indeterminate deploy attempt provably pushed.
@@ -2947,17 +3259,31 @@ async function collectEveDestroyRegistryRepositories(projectRoot, environment, n
2947
3259
  const generationsRoot = join(projectRoot, ".eden", "eve-deploy", "generations");
2948
3260
  const entries = await readdir(generationsRoot).catch(() => []);
2949
3261
  const repositories = new Set();
3262
+ const addRetained = (identity) => {
3263
+ for (const [id, retained] of Object.entries({
3264
+ ...identity.retainedDeployments, ...identity.pendingRetirements,
3265
+ })) {
3266
+ const repository = ownedRegistryRepository({
3267
+ ...identity, deploymentId: id, containerImage: retained.image, generationId: retained.generationId,
3268
+ }, environment, name, record.accountId);
3269
+ if (repository !== undefined)
3270
+ repositories.add(repository);
3271
+ }
3272
+ };
3273
+ addRetained(record);
2950
3274
  const targetKey = record.projectId === undefined
2951
3275
  ? undefined
2952
3276
  : stableDeploymentKey(record.projectId, record.accountId, environment, name);
2953
3277
  for (const entry of entries) {
2954
3278
  const generationRoot = join(generationsRoot, entry);
2955
- const parsed = await parseEveDestroyRecord(join(generationRoot, "deployment.json"), environment, name);
3279
+ const parsed = await parseEveDestroyRecord(await deploymentRecordPath(generationRoot), environment, name);
2956
3280
  const deployedRepository = parsed === undefined
2957
3281
  ? undefined
2958
3282
  : ownedRegistryRepository(parsed.identity, environment, name, record.accountId);
2959
3283
  if (deployedRepository !== undefined)
2960
3284
  repositories.add(deployedRepository);
3285
+ if (parsed !== undefined)
3286
+ addRetained(parsed.identity);
2961
3287
  if (targetKey === undefined)
2962
3288
  continue;
2963
3289
  const attemptRepository = await attemptRegistryRepository(join(generationRoot, "deployment-attempt.json"), record.accountId, targetKey);
@@ -1,5 +1,5 @@
1
1
  import { type EveRuntimeInputIdentity } from "./eve-runtime-config.js";
2
- export type EvePackagingCode = "ROOT_INVALID" | "UNSUPPORTED_TOOLCHAIN" | "DEPENDENCY_AMBIGUITY" | "SOURCE_RACE" | "EVE_BUILD_FAILED" | "UNSUPPORTED_EVE_OUTPUT" | "RUNTIME_CLOSURE_INCOMPLETE" | "SECRET_EXCLUSION_FAILED" | "DOCKER_PLATFORM_BLOCKED" | "UNSUPPORTED_HOST_REQUIREMENT" | "EVE_HEALTH_FAILED" | "WORLD_MIGRATION_FAILED" | "CLEANUP_UNVERIFIED";
2
+ export type EvePackagingCode = "ROOT_INVALID" | "UNSUPPORTED_TOOLCHAIN" | "DEPENDENCY_AMBIGUITY" | "SOURCE_RACE" | "EVE_BUILD_FAILED" | "EVE_SANDBOX_PROVIDER" | "UNSUPPORTED_EVE_OUTPUT" | "RUNTIME_CLOSURE_INCOMPLETE" | "SECRET_EXCLUSION_FAILED" | "DOCKER_PLATFORM_BLOCKED" | "UNSUPPORTED_HOST_REQUIREMENT" | "EVE_HEALTH_FAILED" | "WORLD_MIGRATION_FAILED" | "CLEANUP_UNVERIFIED";
3
3
  export interface EveRuntimeConfigExclusion {
4
4
  /**
5
5
  * This path is opaque. The packaging worker only uses its lexical identity
@@ -228,9 +228,10 @@ export interface EveProjectPackagingResult {
228
228
  readonly checks: readonly EvePackagingCheck[];
229
229
  /**
230
230
  * Outcome of the builder's temporary eve#4440 sandbox-resume patch:
231
- * `applied` when the shipped just-bash binding carries it, `skipped` when
232
- * the binding is a version Eden does not know, `absent` when this Eve
233
- * build has no just-bash binding or packaging never reached the image.
231
+ * `applied` when the shipped just-bash binding carries it, `fixed` when the
232
+ * installed Eve already includes the upstream fix, `skipped` when the
233
+ * binding is a version Eden does not know, `absent` when this Eve build has
234
+ * no just-bash binding or packaging never reached the image.
234
235
  */
235
236
  readonly sandboxResumePatch: EveSandboxResumePatchStatus;
236
237
  readonly candidateImageId: string | null;
@@ -272,6 +273,25 @@ export declare const EVE_SANDBOX_RESUME_PATCH_KNOWN_SHA256 = "0ba0ac3a237c99af2f
272
273
  export declare const EVE_SANDBOX_RESUME_PATCH_LATEST_SHA256 = "ab8b1ad7398af8f6fd2a1e91eb0d1ff8d2b9b9afe1836234f36ce0e896791c45";
273
274
  /** Marker the patched binding always contains; proves the patch shipped. */
274
275
  export declare const EVE_SANDBOX_RESUME_PATCH_SIGNATURE = "edenRefreshJustBashSessionSkills";
276
+ /**
277
+ * First Eve release whose just-bash provider keeps a session's files across
278
+ * template changes (vercel/eve#5132), so the patch is not needed from here on.
279
+ */
280
+ export declare const EVE_SANDBOX_RESUME_FIXED_VERSION = "0.83.0";
281
+ /**
282
+ * True when an installed Eve version already ships the eve#4440 fix. A
283
+ * prerelease of the fixed version sorts before it (semver), so it may predate
284
+ * the fix and keeps the patch.
285
+ */
286
+ export declare function eveIncludesSandboxResumeFix(version: unknown): boolean;
287
+ /**
288
+ * Early, source-level rejection of sandbox setups that cannot work in Eden's
289
+ * isolated builder: a native provider named in the sandbox module, or, on Eve
290
+ * 0.83.0 and later, DefaultSandbox (no module, or one that names it without
291
+ * JustBash). Anything it cannot recognize is left to
292
+ * checkPreparedJustBashTemplate, which inspects what `eve build` produced.
293
+ */
294
+ export declare function checkEveSandboxProvider(projectRoot: string, resolvedVersion?: string): Promise<EvePackagingError | undefined>;
275
295
  /** The exact `resume()` implementation in Eve 0.68.0, 0.71.2, 0.72.1, and 0.79.1. */
276
296
  export declare 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)}";
277
297
  /**
@@ -305,7 +325,7 @@ export declare const EVE_SANDBOX_RESUME_PATCH_HELPERS = "async function edenRefr
305
325
  */
306
326
  export declare const EVE_SANDBOX_RESUME_PATCH_SCRIPT: string;
307
327
  /** Outcome of the builder's sandbox-resume patch for the packaged image. */
308
- export type EveSandboxResumePatchStatus = "applied" | "skipped" | "absent";
328
+ export type EveSandboxResumePatchStatus = "applied" | "fixed" | "skipped" | "absent";
309
329
  export declare function sha256(value: string | Buffer): string;
310
330
  export declare function jsonBytes(value: unknown): Buffer;
311
331
  export declare function isWithin(root: string, candidate: string): boolean;
@@ -48,6 +48,150 @@ export const EVE_SANDBOX_RESUME_PATCH_KNOWN_SHA256 = "0ba0ac3a237c99af2f67a6028f
48
48
  export const EVE_SANDBOX_RESUME_PATCH_LATEST_SHA256 = "ab8b1ad7398af8f6fd2a1e91eb0d1ff8d2b9b9afe1836234f36ce0e896791c45";
49
49
  /** Marker the patched binding always contains; proves the patch shipped. */
50
50
  export const EVE_SANDBOX_RESUME_PATCH_SIGNATURE = "edenRefreshJustBashSessionSkills";
51
+ /**
52
+ * First Eve release whose just-bash provider keeps a session's files across
53
+ * template changes (vercel/eve#5132), so the patch is not needed from here on.
54
+ */
55
+ export const EVE_SANDBOX_RESUME_FIXED_VERSION = "0.83.0";
56
+ /** `major.minor.patch` of a version string; prerelease and build suffixes are dropped. */
57
+ function parseEveRelease(version) {
58
+ const match = typeof version === "string"
59
+ ? /^(\d+)\.(\d+)\.(\d+)/u.exec(version)
60
+ : null;
61
+ return match === null
62
+ ? null
63
+ : [Number(match[1]), Number(match[2]), Number(match[3])];
64
+ }
65
+ /**
66
+ * True when an installed Eve version already ships the eve#4440 fix. A
67
+ * prerelease of the fixed version sorts before it (semver), so it may predate
68
+ * the fix and keeps the patch.
69
+ */
70
+ export function eveIncludesSandboxResumeFix(version) {
71
+ const installed = parseEveRelease(version);
72
+ const fixed = parseEveRelease(EVE_SANDBOX_RESUME_FIXED_VERSION);
73
+ if (installed === null || fixed === null)
74
+ return false;
75
+ for (let index = 0; index < 3; index += 1) {
76
+ if (installed[index] !== fixed[index]) {
77
+ return (installed[index] ?? 0) > (fixed[index] ?? 0);
78
+ }
79
+ }
80
+ return typeof version === "string" && !/^\d+\.\d+\.\d+-/u.test(version);
81
+ }
82
+ /** Eve 0.83.0 and later no longer fall back to just-bash in production builds. */
83
+ function eveRequiresExplicitJustBash(version) {
84
+ const release = parseEveRelease(version);
85
+ return release !== null && (release[0] > 0 || release[1] >= 83);
86
+ }
87
+ /**
88
+ * Eve reads its sandbox module from either path, with any source extension.
89
+ * When `agent/sandbox/` exists only the nested module counts: Eve 0.83–0.86
90
+ * silently ignores `agent/sandbox.ts` and 0.87+ fails the build.
91
+ */
92
+ const EVE_SANDBOX_MODULE_PATHS = ["agent/sandbox", "agent/sandbox/sandbox"].flatMap((base) => ["ts", "mts", "cts", "tsx", "js", "mjs", "cjs", "jsx"].map((extension) => `${base}.${extension}`));
93
+ function justBashRemediation(command, path) {
94
+ return `Run ${command} (production dependency), then create or replace ${path} with:\nimport { defineSandbox } from 'eve/sandbox';\nimport { JustBashSandbox } from 'eve/sandbox/just-bash';\nexport const environment = JustBashSandbox.environment();\nexport default defineSandbox(() => environment.open());\nCommit the module, package.json, and updated lockfile, then retry eden preflight. See docs/deploy.md. Do not set EVE_DEV.`;
95
+ }
96
+ /**
97
+ * Early, source-level rejection of sandbox setups that cannot work in Eden's
98
+ * isolated builder: a native provider named in the sandbox module, or, on Eve
99
+ * 0.83.0 and later, DefaultSandbox (no module, or one that names it without
100
+ * JustBash). Anything it cannot recognize is left to
101
+ * checkPreparedJustBashTemplate, which inspects what `eve build` produced.
102
+ */
103
+ export async function checkEveSandboxProvider(projectRoot, resolvedVersion) {
104
+ const source = async (path) => readFile(join(projectRoot, path), "utf8").catch(() => "");
105
+ const json = async (path) => {
106
+ try {
107
+ const value = JSON.parse(await source(path));
108
+ return value !== null && typeof value === "object" && !Array.isArray(value)
109
+ ? value
110
+ : {};
111
+ }
112
+ catch {
113
+ return {};
114
+ }
115
+ };
116
+ const manifest = await json("package.json");
117
+ const dependencies = manifest.dependencies;
118
+ const lock = await json("package-lock.json");
119
+ const packages = lock.packages;
120
+ const pnpmLock = await source("pnpm-lock.yaml");
121
+ const pnpmVersion = /^\s{6}eve:\s*\r?\n\s{8}specifier:[^\n]*\r?\n\s{8}version:\s*['"]?(\d+\.\d+\.\d+(?:-[^('"\s]+)?)/mu
122
+ .exec(pnpmLock)?.[1];
123
+ const installed = await json("node_modules/eve/package.json");
124
+ const declared = typeof dependencies?.eve === "string"
125
+ ? /^[~^]?(\d+\.\d+\.\d+)/u.exec(dependencies.eve)?.[1]
126
+ : undefined;
127
+ const version = resolvedVersion ?? packages?.["node_modules/eve"]?.version ??
128
+ pnpmVersion ?? installed.version ?? declared;
129
+ const requiresExplicit = eveRequiresExplicitJustBash(version);
130
+ const sandboxDirectory = (await lstat(join(projectRoot, "agent/sandbox")).catch(() => undefined))
131
+ ?.isDirectory() === true;
132
+ let path;
133
+ for (const candidate of EVE_SANDBOX_MODULE_PATHS) {
134
+ if ((await lstat(join(projectRoot, candidate)).catch(() => undefined))?.isFile() === true) {
135
+ path = candidate;
136
+ break;
137
+ }
138
+ }
139
+ const module = path === undefined
140
+ ? ""
141
+ : (await source(path)).replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*/gu, "");
142
+ const otherProvider = /\b(DefaultSandbox|DockerSandbox|VercelSandbox|VmSandbox|VMSandbox)\b/u
143
+ .exec(module)?.[1];
144
+ const justBash = /import\s*\{[^}]*?\bJustBashSandbox(?:\s+as\s+(\w+))?\s*(?:,|\})[^;]*?from\s*["']eve\/sandbox\/just-bash["']/u
145
+ .exec(module);
146
+ const binding = justBash?.[1] ?? "JustBashSandbox";
147
+ const explicit = justBash !== null &&
148
+ new RegExp(`\\b${binding}\\s*\\.\\s*environment\\s*\\(`, "u").test(module);
149
+ const native = otherProvider !== undefined && otherProvider !== "DefaultSandbox";
150
+ const usesDefault = path === undefined || (otherProvider === "DefaultSandbox" && !explicit);
151
+ const shadowed = requiresExplicit && sandboxDirectory && path?.startsWith("agent/sandbox.") === true;
152
+ if (!native && !shadowed && !(requiresExplicit && usesDefault))
153
+ return undefined;
154
+ const target = shadowed || (path === undefined && sandboxDirectory)
155
+ ? "agent/sandbox/sandbox.ts"
156
+ : path ?? "agent/sandbox.ts";
157
+ const command = typeof manifest.packageManager === "string" &&
158
+ manifest.packageManager.startsWith("pnpm@")
159
+ ? "pnpm add just-bash"
160
+ : "npm install just-bash";
161
+ return new EvePackagingError({
162
+ code: "EVE_SANDBOX_PROVIDER",
163
+ subject: target,
164
+ reason: shadowed
165
+ ? `${String(path)} sits beside the agent/sandbox/ directory, where Eve ${String(version)} reads only agent/sandbox/sandbox.ts: Eve 0.83–0.86 ignores ${String(path)} and falls back to DefaultSandbox, and 0.87 and later fail the build.`
166
+ : native
167
+ ? `${target} selects ${otherProvider}, which Eden does not support; use explicit JustBash instead.`
168
+ : path === undefined
169
+ ? `Eve ${String(version)} has no sandbox module, so it uses DefaultSandbox, which has no provider in Eden's isolated builder: production builds no longer fall back to just-bash.`
170
+ : `${target} selects DefaultSandbox, which has no provider in Eden's isolated builder on Eve ${String(version)}: production builds no longer fall back to just-bash.`,
171
+ remediation: shadowed
172
+ ? `Move ${String(path)} to agent/sandbox/sandbox.ts (delete it instead if agent/sandbox/sandbox.ts already exists), then retry eden preflight.`
173
+ : justBashRemediation(command, target),
174
+ });
175
+ }
176
+ /**
177
+ * Ground truth after `eve build`: on Eve 0.83.0 and later an agent whose
178
+ * sandbox resolved to no provider prepares no template and would fail to open
179
+ * its sandbox on Cloudflare, however its sandbox module is written.
180
+ */
181
+ async function checkPreparedJustBashTemplate(snapshotRoot, version) {
182
+ if (!eveRequiresExplicitJustBash(version))
183
+ return undefined;
184
+ const templatesPath = `${EVE_SANDBOX_CACHE_ROOT}/just-bash/templates`;
185
+ const templates = await readdir(join(snapshotRoot, templatesPath)).catch(() => []);
186
+ if (templates.length > 0)
187
+ return undefined;
188
+ return new EvePackagingError({
189
+ code: "EVE_SANDBOX_PROVIDER",
190
+ subject: templatesPath,
191
+ reason: `Eve ${version} built no just-bash sandbox template, so the agent's sandbox would fail to open on Cloudflare; its sandbox module does not resolve to JustBash.`,
192
+ remediation: justBashRemediation("npm install just-bash (or pnpm add just-bash)", "agent/sandbox.ts (agent/sandbox/sandbox.ts when agent/sandbox/ exists)"),
193
+ });
194
+ }
51
195
  /** Path of the just-bash binding inside the resolved Eve package root. */
52
196
  const EVE_JUST_BASH_BINDING_PATH = "dist/src/execution/sandbox/bindings/just-bash.js";
53
197
  /** The exact `resume()` implementation in Eve 0.68.0, 0.71.2, 0.72.1, and 0.79.1. */
@@ -107,9 +251,23 @@ const KNOWN_PATCHES = ${JSON.stringify({
107
251
  const SIGNATURE = ${JSON.stringify(EVE_SANDBOX_RESUME_PATCH_SIGNATURE)};
108
252
  const BINDING_PATH = ${JSON.stringify(EVE_JUST_BASH_BINDING_PATH)};
109
253
  const HELPERS = ${JSON.stringify(EVE_SANDBOX_RESUME_PATCH_HELPERS)};
254
+ const FIXED_VERSION = ${JSON.stringify(EVE_SANDBOX_RESUME_FIXED_VERSION)};
110
255
  function finish(outcome, detail) {
111
256
  console.log("eden-eve-sandbox-resume-patch: " + outcome + " (" + detail + ")");
112
257
  }
258
+ function parseRelease(version) {
259
+ const match = typeof version === "string" ? /^(\\d+)\\.(\\d+)\\.(\\d+)/.exec(version) : null;
260
+ return match === null ? null : [Number(match[1]), Number(match[2]), Number(match[3])];
261
+ }
262
+ function includesFix(version) {
263
+ const installed = parseRelease(version);
264
+ const fixed = parseRelease(FIXED_VERSION);
265
+ if (installed === null || fixed === null) return false;
266
+ for (let index = 0; index < 3; index += 1) {
267
+ if (installed[index] !== fixed[index]) return installed[index] > fixed[index];
268
+ }
269
+ return !/^\\d+\\.\\d+\\.\\d+-/.test(version);
270
+ }
113
271
  let packageJsonPath;
114
272
  try {
115
273
  packageJsonPath = fs.realpathSync("node_modules/eve/package.json");
@@ -125,6 +283,16 @@ try {
125
283
  finish("absent", "this eve build has no just-bash binding");
126
284
  process.exit(0);
127
285
  }
286
+ let eveVersion;
287
+ try {
288
+ eveVersion = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")).version;
289
+ } catch {
290
+ eveVersion = undefined;
291
+ }
292
+ if (!source.includes(SIGNATURE) && includesFix(eveVersion)) {
293
+ finish("fixed", "eve " + eveVersion + " includes the upstream fix for eve#4440");
294
+ process.exit(0);
295
+ }
128
296
  if (source.includes(SIGNATURE)) {
129
297
  finish("applied", "already patched");
130
298
  process.exit(0);
@@ -1475,6 +1643,14 @@ async function scanGeneratedSandboxCache(snapshotRoot) {
1475
1643
  const relativePath = relativeDirectory.length === 0
1476
1644
  ? entry.name
1477
1645
  : `${relativeDirectory}/${entry.name}`;
1646
+ if (relativeDirectory === "" && entry.name !== "just-bash") {
1647
+ throw new EvePackagingError({
1648
+ code: "EVE_SANDBOX_PROVIDER",
1649
+ subject: `${EVE_SANDBOX_CACHE_ROOT}/${entry.name}`,
1650
+ reason: `The Eve build prepared a ${entry.name} sandbox template; Eden supports only just-bash templates.`,
1651
+ remediation: "Replace the sandbox module with the explicit JustBash environment in docs/deploy.md, run npm install just-bash (or pnpm add just-bash), commit the module and lockfile, then retry eden preflight without EVE_DEV.",
1652
+ });
1653
+ }
1478
1654
  const candidate = join(directory, entry.name);
1479
1655
  if (entry.isSymbolicLink()) {
1480
1656
  throw new EvePackagingError({
@@ -1691,20 +1867,39 @@ function classifyNpmInstallFailure(stderr) {
1691
1867
  remediation: "Fix the reported npm failure; regenerate package-lock.json with the selected exact npm version if it is out of sync. Use public registry dependencies without .npmrc credentials or dependency build scripts.",
1692
1868
  });
1693
1869
  }
1870
+ /**
1871
+ * The failing BuildKit step's own output (`#<step> <seconds> <line>`), last 20
1872
+ * lines, so Eve's error reaches the user instead of a generic build failure.
1873
+ */
1874
+ function failingDockerStepOutput(stderr) {
1875
+ const step = [...stderr.matchAll(/^#(\d+) ERROR:/gmu)].at(-1)?.[1];
1876
+ if (step === undefined)
1877
+ return undefined;
1878
+ const lines = [...stderr.matchAll(new RegExp(`^#${step} \\d+\\.\\d+ (.*)$`, "gmu"))]
1879
+ .map((match) => (match[1] ?? "").trimEnd())
1880
+ .filter((line) => line.length > 0)
1881
+ .slice(-20);
1882
+ if (lines.length === 0)
1883
+ return undefined;
1884
+ const output = lines.join("\n");
1885
+ return output.length > 2000 ? `…${output.slice(-2000)}` : output;
1886
+ }
1694
1887
  function classifyEveBuildFailure(stderr) {
1888
+ const output = failingDockerStepOutput(stderr);
1889
+ const reported = output === undefined ? "" : `\nEve reported:\n${output}`;
1695
1890
  if (/\bjust-bash\b/u.test(stderr)) {
1696
1891
  return new EvePackagingError({
1697
1892
  code: "EVE_BUILD_FAILED",
1698
1893
  subject: "eve build",
1699
- reason: "The project-local Eve build failed inside the isolated Linux/amd64 builder because Eve's default sandbox needs just-bash where no Docker daemon or /dev/kvm exists.",
1894
+ reason: `The project-local Eve build failed inside the isolated Linux/amd64 builder because Eve's default sandbox needs just-bash where no Docker daemon or /dev/kvm exists.${reported}`,
1700
1895
  remediation: "Declare just-bash as a production dependency of the Eve project, regenerate its package-manager lockfile, and retry.",
1701
1896
  });
1702
1897
  }
1703
1898
  return new EvePackagingError({
1704
1899
  code: "EVE_BUILD_FAILED",
1705
1900
  subject: "eve build",
1706
- reason: "The project-local Eve build failed inside the isolated Linux/amd64 builder.",
1707
- remediation: "Fix the project-local Eve build and retry without changing the authored project during packaging.",
1901
+ reason: `The project-local Eve build failed inside the isolated Linux/amd64 builder.${reported}`,
1902
+ remediation: "Fix the error Eve reported (run eve build locally to reproduce it) and retry.",
1708
1903
  });
1709
1904
  }
1710
1905
  function classifyDockerBuildFailure(error) {
@@ -1818,9 +2013,18 @@ async function detectEveSandboxResumePatch(snapshotRoot) {
1818
2013
  try {
1819
2014
  const packageJsonPath = await realpath(join(snapshotRoot, "node_modules/eve/package.json"));
1820
2015
  const binding = await readFile(join(dirname(packageJsonPath), EVE_JUST_BASH_BINDING_PATH), "utf8");
1821
- return binding.includes(EVE_SANDBOX_RESUME_PATCH_SIGNATURE)
1822
- ? "applied"
1823
- : "skipped";
2016
+ if (binding.includes(EVE_SANDBOX_RESUME_PATCH_SIGNATURE))
2017
+ return "applied";
2018
+ const version = await readFile(packageJsonPath, "utf8")
2019
+ .then((text) => {
2020
+ const manifest = JSON.parse(text);
2021
+ return typeof manifest === "object" && manifest !== null &&
2022
+ "version" in manifest
2023
+ ? manifest.version
2024
+ : undefined;
2025
+ })
2026
+ .catch(() => undefined);
2027
+ return eveIncludesSandboxResumeFix(version) ? "fixed" : "skipped";
1824
2028
  }
1825
2029
  catch {
1826
2030
  return "absent";
@@ -2028,6 +2232,9 @@ export async function buildEveProjectSnapshot(options) {
2028
2232
  const afterCopyRace = await sourceRaceErrorIfChanged(roots.canonicalRoot, initialInputs, initialRuntimeInputIdentity, options.runtimeConfig, resolve(options.artifactRoot), roots.requestedRoot);
2029
2233
  if (afterCopyRace !== undefined)
2030
2234
  return blockedResult(afterCopyRace, { writtenPaths });
2235
+ const sandboxProvider = await checkEveSandboxProvider(snapshotRoot);
2236
+ if (sandboxProvider !== undefined)
2237
+ throw sandboxProvider;
2031
2238
  const nodeImage = options.nodeImage ?? options.builder.nodeImage;
2032
2239
  let actualDockerfilePath;
2033
2240
  if (nodeImage !== undefined) {
@@ -2094,9 +2301,15 @@ export async function buildEveProjectSnapshot(options) {
2094
2301
  return blockedResult(cleanupError ?? dependencyError, { writtenPaths });
2095
2302
  }
2096
2303
  const eve = await resolveProjectLocalEve(snapshotRoot);
2304
+ const sandboxProviderAfterBuild = await checkEveSandboxProvider(snapshotRoot, eve.version);
2305
+ if (sandboxProviderAfterBuild !== undefined)
2306
+ throw sandboxProviderAfterBuild;
2097
2307
  const sandboxResumePatch = await detectEveSandboxResumePatch(snapshotRoot);
2098
2308
  const output = await scanGeneratedOutput(snapshotRoot);
2099
2309
  const sandboxCache = await scanGeneratedSandboxCache(snapshotRoot);
2310
+ const preparedTemplate = await checkPreparedJustBashTemplate(snapshotRoot, eve.version);
2311
+ if (preparedTemplate !== undefined)
2312
+ throw preparedTemplate;
2100
2313
  await assertGeneratedTreesExcludeSecrets(snapshotRoot);
2101
2314
  const snapshotSourceDigest = await snapshotDigest(snapshotRoot);
2102
2315
  const runtimeManifestPath = join(generationRoot, "runtime-manifest.json");
@@ -2277,6 +2490,15 @@ export async function buildEveProjectSnapshot(options) {
2277
2490
  remediation: null,
2278
2491
  }]
2279
2492
  : []),
2493
+ ...(sandboxResumePatch === "fixed"
2494
+ ? [{
2495
+ id: "EVE_SANDBOX_RESUME_PATCH",
2496
+ status: "pass",
2497
+ subject: "eve-sandbox-resume",
2498
+ reason: `not needed: Eve ${EVE_SANDBOX_RESUME_FIXED_VERSION} and later include the fix for eve#4440`,
2499
+ remediation: null,
2500
+ }]
2501
+ : []),
2280
2502
  ],
2281
2503
  sandboxResumePatch,
2282
2504
  candidateImageId: buildResult.imageId ?? null,
@@ -1,4 +1,4 @@
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"];
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_WORLD_TOKEN", "EDEN_EVE_WORLD_RPC_AUTH", "EDEN_EVE_DEPLOYMENT_ID", "EDEN_EVE_GENERATION_ID", "EDEN_EVE_RELEASE_ID", "EDEN_EVE_DEPLOYMENTS", "EDEN_AFFINITY_CONTRACT", "EVE_SCHEDULES_DISABLED", "EDEN_EVE_CONTROL_TOKEN"];
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
4
  export declare const EVE_HOST_ENVIRONMENT: Readonly<{
@@ -10,8 +10,15 @@ export const EVE_RESERVED_HOST_VARIABLES = Object.freeze([
10
10
  "NODE_EXTRA_CA_CERTS",
11
11
  "WORKFLOW_LOCAL_BASE_URL",
12
12
  "EDEN_WORLD_URL",
13
+ "EDEN_WORLD_TOKEN",
14
+ "EDEN_EVE_WORLD_RPC_AUTH",
13
15
  "EDEN_EVE_DEPLOYMENT_ID",
14
16
  "EDEN_EVE_GENERATION_ID",
17
+ "EDEN_EVE_RELEASE_ID",
18
+ "EDEN_EVE_DEPLOYMENTS",
19
+ "EDEN_AFFINITY_CONTRACT",
20
+ "EVE_SCHEDULES_DISABLED",
21
+ "EDEN_EVE_CONTROL_TOKEN",
15
22
  ]);
16
23
  export const EVE_START_COMMAND = Object.freeze([
17
24
  "./node_modules/.bin/eve",
@@ -196,11 +203,18 @@ function parseEnvironmentText(text) {
196
203
  }
197
204
  return Object.freeze(values);
198
205
  }
206
+ /**
207
+ * Values shorter than this are not masked: substring masking of `1`, `8`, or
208
+ * `eve` garbled every message and matched Eden's own random revision IDs,
209
+ * failing deploys. Credentials are longer; short values are still delivered
210
+ * only through the protected channels, just not masked if a tool echoes them.
211
+ */
212
+ const MIN_REDACTED_RUNTIME_VALUE_LENGTH = 8;
199
213
  class EveRuntimeRedactionRegistry {
200
214
  handle = `eve-runtime-redaction-${randomUUID()}`;
201
215
  values = new Set();
202
216
  register(value) {
203
- if (value.length > 0)
217
+ if (value.length >= MIN_REDACTED_RUNTIME_VALUE_LENGTH)
204
218
  this.values.add(value);
205
219
  }
206
220
  redact(value) {
@@ -458,6 +458,7 @@ COPY node_modules /candidate/node_modules
458
458
  ${sandboxContextCopy}${sourceEntries.map((entry) => `COPY ${JSON.stringify([entry, `/candidate/${entry}`])}`).join("\n")}
459
459
 
460
460
  FROM --platform=linux/amd64 ${imageReference(nodeImage)} AS runtime
461
+ RUN apt-get update && apt-get install -y --no-install-recommends python3 && rm -rf /var/lib/apt/lists/*
461
462
  WORKDIR /workspace
462
463
  ENV HOST=0.0.0.0 \\
463
464
  NITRO_HOST=0.0.0.0 \\
@@ -582,6 +583,8 @@ function runtimeContainerEnvironment(request) {
582
583
  ...(request.workflowWorld === "@moinulmoin/eden-world-cloudflare"
583
584
  ? {
584
585
  EDEN_WORLD_URL: `${request.publicOrigin ?? "http://localhost:8080"}/__eden/world/rpc`,
586
+ EDEN_EVE_DEPLOYMENT_ID: `eden-smoke-${request.candidate.generationId}`,
587
+ EDEN_EVE_RELEASE_ID: `eden-smoke-${request.candidate.generationId}`,
585
588
  CBOR_NATIVE_ACCELERATION_DISABLED: "true",
586
589
  }
587
590
  : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moinulmoin/eden",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "Deploy existing Eve projects to your own Cloudflare account.",
5
5
  "keywords": [
6
6
  "ai",
@@ -31,7 +31,7 @@
31
31
  "NOTICE"
32
32
  ],
33
33
  "dependencies": {
34
- "@moinulmoin/eden-runtime-cloudflare": "0.8.0",
34
+ "@moinulmoin/eden-runtime-cloudflare": "0.10.0",
35
35
  "wrangler": "4.147.0"
36
36
  },
37
37
  "devDependencies": {