@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 +3 -3
- package/dist/eve-control-plane.d.ts +28 -0
- package/dist/eve-control-plane.js +360 -34
- package/dist/eve-packaging.d.ts +25 -5
- package/dist/eve-packaging.js +228 -6
- package/dist/eve-runtime-config.d.ts +1 -1
- package/dist/eve-runtime-config.js +15 -1
- package/dist/eve-runtime-image.js +3 -0
- package/package.json +2 -2
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.
|
|
15
|
+
npm install --global @moinulmoin/eden@0.10.0
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
```sh
|
|
19
|
-
pnpm add --global @moinulmoin/eden@0.
|
|
19
|
+
pnpm add --global @moinulmoin/eden@0.10.0
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
```sh
|
|
23
|
-
bun add --global @moinulmoin/eden@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:
|
|
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:
|
|
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,
|
|
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:
|
|
1556
|
-
|
|
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 (
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
2872
|
-
|
|
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
|
|
2892
|
-
const
|
|
2893
|
-
if (
|
|
2894
|
-
|
|
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(
|
|
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);
|
package/dist/eve-packaging.d.ts
CHANGED
|
@@ -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, `
|
|
232
|
-
*
|
|
233
|
-
*
|
|
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;
|
package/dist/eve-packaging.js
CHANGED
|
@@ -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:
|
|
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:
|
|
1707
|
-
remediation: "Fix the
|
|
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
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
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
|
|
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.
|
|
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.
|
|
34
|
+
"@moinulmoin/eden-runtime-cloudflare": "0.10.0",
|
|
35
35
|
"wrangler": "4.147.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|