@moinulmoin/eden 0.9.0 → 0.11.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/SKILL.md +215 -0
- package/dist/eve-control-plane.d.ts +28 -0
- package/dist/eve-control-plane.js +403 -35
- package/dist/eve-runtime-config.d.ts +1 -1
- package/dist/eve-runtime-config.js +7 -0
- package/dist/eve-runtime-image.js +3 -0
- package/dist/index.js +19 -0
- package/package.json +4 -3
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.11.0
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
```sh
|
|
19
|
-
pnpm add --global @moinulmoin/eden@0.
|
|
19
|
+
pnpm add --global @moinulmoin/eden@0.11.0
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
```sh
|
|
23
|
-
bun add --global @moinulmoin/eden@0.
|
|
23
|
+
bun add --global @moinulmoin/eden@0.11.0
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
Then confirm the CLI is available:
|
package/dist/SKILL.md
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: eden
|
|
3
|
+
description: Deploy, update, check, or remove a Vercel Eve agent on the user's own Cloudflare account with the Eden CLI. Use when someone wants to run an Eve project off Vercel, check whether an Eve project is ready for Cloudflare, ship a change to an Eden deployment, upgrade Eve on one, or tear one down.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Eden: run an Eve agent on Cloudflare
|
|
7
|
+
|
|
8
|
+
Eden (`@moinulmoin/eden`) builds an existing Eve project as-is and runs it in a
|
|
9
|
+
Cloudflare Container behind a Worker on the user's own account. It never
|
|
10
|
+
rewrites the project; you make any needed changes yourself, guided by
|
|
11
|
+
preflight.
|
|
12
|
+
|
|
13
|
+
`npx @moinulmoin/eden skill` prints this guide for the installed CLI version.
|
|
14
|
+
Docs: https://github.com/moinulmoin/eden (failure and warning codes:
|
|
15
|
+
`docs/troubleshooting.md`).
|
|
16
|
+
|
|
17
|
+
## The loop
|
|
18
|
+
|
|
19
|
+
1. Check the machine and account (below).
|
|
20
|
+
2. Write the env file (below).
|
|
21
|
+
3. Run preflight and read the JSON:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
npx @moinulmoin/eden preflight --env-file ~/.config/<agent>/preview.env --json
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Preflight builds and boots the agent locally and checks Cloudflare access.
|
|
28
|
+
It changes nothing on Cloudflare. It prints one JSON object:
|
|
29
|
+
`ok`, `checks[]` (`id`, `status`, `message`, `remediation`) and
|
|
30
|
+
`warnings[]` (`id`, `message`, `remediation`).
|
|
31
|
+
4. Fix every failed check, then every warning, using its `remediation`. Re-run
|
|
32
|
+
until `ok` is `true` and no warning applies. A warning does not block a
|
|
33
|
+
deploy, but each one names something that breaks or loses data on
|
|
34
|
+
Cloudflare; only leave one if the user accepts that.
|
|
35
|
+
5. A clean preflight means the project can run on Cloudflare. It does not
|
|
36
|
+
check that keys are real: before deploying, make sure the user has replaced
|
|
37
|
+
every placeholder secret in the env file.
|
|
38
|
+
6. Deploy, then verify with a real message (below). Confirm with the user
|
|
39
|
+
first for production.
|
|
40
|
+
|
|
41
|
+
## Machine and account
|
|
42
|
+
|
|
43
|
+
- macOS or Linux, Node `>=24.17.0`
|
|
44
|
+
- Docker or OrbStack running, with linux/amd64 support
|
|
45
|
+
- Cloudflare Workers Paid plan (Containers need it), and Wrangler logged in:
|
|
46
|
+
`npx wrangler@4.147.0 login` (the user must do this in a browser)
|
|
47
|
+
|
|
48
|
+
## Project setup Cloudflare needs
|
|
49
|
+
|
|
50
|
+
These are what preflight checks most often. Fix them before the first run to
|
|
51
|
+
save a build.
|
|
52
|
+
|
|
53
|
+
- **One lockfile.** Either a root `package-lock.json`, or an exact
|
|
54
|
+
`"packageManager": "pnpm@x.y.z"` with a root `pnpm-lock.yaml`. Not both, not
|
|
55
|
+
Bun. Dependency install scripts do not run.
|
|
56
|
+
- **Sandbox.** Add `just-bash` as a production dependency. On Eve `0.83.0` and
|
|
57
|
+
later, also select it explicitly in `agent/sandbox.ts` (or
|
|
58
|
+
`agent/sandbox/sandbox.ts` if an `agent/sandbox/` folder exists):
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { defineSandbox } from 'eve/sandbox';
|
|
62
|
+
import { JustBashSandbox } from 'eve/sandbox/just-bash';
|
|
63
|
+
|
|
64
|
+
export const environment = JustBashSandbox.environment();
|
|
65
|
+
export default defineSandbox(() => environment.open());
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Vercel Sandbox, Docker, and other providers do not run on Eden.
|
|
69
|
+
- **Channel auth (`EVE_AUTH_SCAFFOLD`).** The `eve init` auth in
|
|
70
|
+
`agent/channels/eve.ts` (`vercelOidc()`, `localDev()`, `placeholderAuth()`)
|
|
71
|
+
rejects every Cloudflare caller with 401. Replace it with a real
|
|
72
|
+
authenticator, for example a bearer token read from the env file. Keep
|
|
73
|
+
`localDev()` so `eve dev` still works locally; it is ignored in production:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { localDev, type AuthFn } from "eve/channels/auth";
|
|
77
|
+
import { eveChannel } from "eve/channels/eve";
|
|
78
|
+
|
|
79
|
+
const authenticate: AuthFn<Request> = (request) => {
|
|
80
|
+
const expected = process.env.AGENT_AUTH_TOKEN;
|
|
81
|
+
if (!expected || request.headers.get("authorization") !== `Bearer ${expected}`) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
attributes: {},
|
|
86
|
+
authenticator: "bearer-token",
|
|
87
|
+
principalId: "app-client",
|
|
88
|
+
principalType: "app",
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export default eveChannel({ auth: [authenticate, localDev()] });
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
- **Model key (`EVE_AI_GATEWAY_KEY_MISSING`).** A string model such as
|
|
96
|
+
`"openai/gpt-5"` goes through Vercel AI Gateway. On Vercel that
|
|
97
|
+
authenticates automatically; on Cloudflare it needs `AI_GATEWAY_API_KEY` in
|
|
98
|
+
the env file. A direct provider such as `openai("…")` from `@ai-sdk/openai`
|
|
99
|
+
needs that provider's key instead.
|
|
100
|
+
- **Vercel Connect (`EVE_VERCEL_CONNECT`).** `connect()` from
|
|
101
|
+
`@vercel/connect/eve` gets credentials using an identity only Vercel
|
|
102
|
+
provides, so those connections fail on Cloudflare. Put the service's own key
|
|
103
|
+
or bot token in the env file and replace `connect(...)` in the connection
|
|
104
|
+
with `auth: { getToken: async () => ({ token: process.env.SERVICE_TOKEN! }) }`.
|
|
105
|
+
For per-user sign-in, use `defineInteractiveAuthorization` from
|
|
106
|
+
`eve/connections` with your own OAuth app. Having `@vercel/connect` in
|
|
107
|
+
`package.json` without importing it is fine.
|
|
108
|
+
- **Durable state (`EVE_WORLD_LOCAL`).** The default local Workflow World loses
|
|
109
|
+
sessions and pending approvals on every update. Use one of:
|
|
110
|
+
- `@moinulmoin/eden-world-cloudflare`: no database; sessions also survive Eve
|
|
111
|
+
upgrades. Add it as a production dependency and set in `agent/agent.ts`:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
experimental: { workflow: { world: "@moinulmoin/eden-world-cloudflare" } },
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
- `@workflow/world-postgres` with a direct, unpooled Postgres URL in the env
|
|
118
|
+
file.
|
|
119
|
+
|
|
120
|
+
## Env file and secrets
|
|
121
|
+
|
|
122
|
+
- Lives **outside the project**, mode 600, for example
|
|
123
|
+
`~/.config/<agent>/preview.env`. Passed with `--env-file`.
|
|
124
|
+
- Exactly `KEY=value` per line: no quotes (they become part of the value), no
|
|
125
|
+
spaces around the name, `#` comment lines allowed.
|
|
126
|
+
- Holds the model key and every secret the agent reads (auth token, service
|
|
127
|
+
keys, database URL).
|
|
128
|
+
- Never print, log, or commit it, and never pass a secret as a command
|
|
129
|
+
argument. Ask the user to fill in values you do not have.
|
|
130
|
+
|
|
131
|
+
## Commands
|
|
132
|
+
|
|
133
|
+
```sh
|
|
134
|
+
# deploy to preview; the name comes from package.json
|
|
135
|
+
npx @moinulmoin/eden deploy --env-file ~/.config/<agent>/preview.env --json
|
|
136
|
+
|
|
137
|
+
# ship a change: the same command updates in place, on the same URL
|
|
138
|
+
npx @moinulmoin/eden deploy --env-file ~/.config/<agent>/preview.env --json
|
|
139
|
+
|
|
140
|
+
# production always needs an explicit --name
|
|
141
|
+
npx @moinulmoin/eden deploy --env production --name <name> --env-file <file> --json
|
|
142
|
+
|
|
143
|
+
# remove one target (always needs the exact --name deploy printed;
|
|
144
|
+
# add --env production for a production target)
|
|
145
|
+
npx @moinulmoin/eden destroy --name <name> --json
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
- `--project <dir>` selects a project other than the current directory.
|
|
149
|
+
- A fresh deploy takes a few minutes, mostly building the container. The
|
|
150
|
+
result's `deployment.stableWorkersDevOrigin` is the live URL.
|
|
151
|
+
- An `eve init` project's `npm run deploy` script still deploys to Vercel, not
|
|
152
|
+
Eden.
|
|
153
|
+
- Confirm with the user before any `destroy` or production deploy. Destroy
|
|
154
|
+
deletes the agent immediately, even mid-conversation, and with the Cloudflare
|
|
155
|
+
World also its stored sessions. An external Postgres database is not touched.
|
|
156
|
+
|
|
157
|
+
## Verify
|
|
158
|
+
|
|
159
|
+
Health only proves the server started; it needs no auth:
|
|
160
|
+
|
|
161
|
+
```sh
|
|
162
|
+
curl -s https://<url>/eve/v1/health # expect "status":"ready"
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Then prove auth, the model key, and the agent work with a real message, using
|
|
166
|
+
the channel's credential (the bearer token from the env file):
|
|
167
|
+
|
|
168
|
+
```sh
|
|
169
|
+
curl -s -X POST https://<url>/eve/v1/session \
|
|
170
|
+
-H "Authorization: Bearer $AGENT_AUTH_TOKEN" \
|
|
171
|
+
-H "Content-Type: application/json" \
|
|
172
|
+
-d '{"message":"Reply with one word: ready"}'
|
|
173
|
+
# {"ok":true,"sessionId":"wrun_…"}; then read the reply:
|
|
174
|
+
curl -s -N https://<url>/eve/v1/session/<sessionId>/stream \
|
|
175
|
+
-H "Authorization: Bearer $AGENT_AUTH_TOKEN"
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Load the token into the shell from the env file without printing it. The
|
|
179
|
+
first request after the agent sleeps takes about 10–15 seconds.
|
|
180
|
+
|
|
181
|
+
## Upgrading Eve
|
|
182
|
+
|
|
183
|
+
- Bump `eve`, then reinstall so the lockfile matches. Keep
|
|
184
|
+
`@moinulmoin/eden-world-cloudflare` on its latest version.
|
|
185
|
+
- Moving an `eve init` project to Eve `1.0` also needs `@vercel/connect`
|
|
186
|
+
`2.7.0` or later; otherwise npm fails with `ERESOLVE`.
|
|
187
|
+
- Then deploy as usual. With the current Cloudflare World, existing sessions
|
|
188
|
+
continue on the new version. With the Postgres World or an older Cloudflare
|
|
189
|
+
World they return HTTP 409 `session_stranded` and users start new ones.
|
|
190
|
+
|
|
191
|
+
## Limits to tell the user
|
|
192
|
+
|
|
193
|
+
- The sandbox is `just-bash`, a simulated shell: files, grep, sed, jq,
|
|
194
|
+
sqlite3, and curl work; python, git, and npm do not.
|
|
195
|
+
- One container per agent, `workers.dev` URL only, no custom domains yet.
|
|
196
|
+
- The container sleeps after 1 hour idle and wakes on the next request.
|
|
197
|
+
Standard 5-field cron schedules still fire while it sleeps; other cron forms
|
|
198
|
+
only fire while it is awake (`EVE_SCHEDULE_UNSUPPORTED`).
|
|
199
|
+
|
|
200
|
+
## When something fails
|
|
201
|
+
|
|
202
|
+
- Checks run before anything changes on Cloudflare. Fix the first failed
|
|
203
|
+
check by its `remediation` and run again.
|
|
204
|
+
- `VAL-CLI-007-TARGET-CONFLICT`: a resource with that name exists that Eden
|
|
205
|
+
cannot prove it owns. For a brand-new agent, pick another `--name`; never
|
|
206
|
+
delete the resource to make room.
|
|
207
|
+
- `VAL-CLI-007-CLOUDFLARE-ACCESS`: Wrangler is not logged in or the account
|
|
208
|
+
lacks Workers Paid/Containers. The user has to fix this
|
|
209
|
+
(`npx wrangler@4.147.0 whoami`).
|
|
210
|
+
- `DEPLOY_INDETERMINATE`: Eden could not prove the result; the old version
|
|
211
|
+
usually keeps serving. Check the URL's health, then retry with the exact
|
|
212
|
+
same `--project`, `--env`, and `--name`. Never switch to a new name to get
|
|
213
|
+
past it, never delete resources by prefix, and never delete the project's
|
|
214
|
+
`.eden/` folder: it is the ownership proof `destroy` needs.
|
|
215
|
+
- Look up any other code in `docs/troubleshooting.md`.
|
|
@@ -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";
|
|
@@ -11,7 +12,7 @@ import { EveCliError, EVE_NAME_PATTERN, } from "./eve.js";
|
|
|
11
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]")
|
|
@@ -214,6 +263,56 @@ function pooledPostgresReason(connectionString) {
|
|
|
214
263
|
}
|
|
215
264
|
return undefined;
|
|
216
265
|
}
|
|
266
|
+
const EVE_WORLD_LOCAL_REMEDIATION = 'For durable state without a database, add @moinulmoin/eden-world-cloudflare and set experimental: { workflow: { world: "@moinulmoin/eden-world-cloudflare" } } in agent.ts; sessions survive Eve upgrades. Alternatively, add @workflow/world-postgres, select it as the workflow world, and put WORKFLOW_POSTGRES_URL with a direct, unpooled Postgres URL in --env-file.';
|
|
267
|
+
/** Static, dynamic, side-effect, or require imports of @vercel/connect or a subpath. */
|
|
268
|
+
function importsVercelConnect(source) {
|
|
269
|
+
return /\b(?:from|import|require)\s*\(?\s*["']@vercel\/connect(?:\/[^"']*)?["']/u
|
|
270
|
+
.test(source.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*/gu, ""));
|
|
271
|
+
}
|
|
272
|
+
async function vercelConnectSourcePaths(projectRoot) {
|
|
273
|
+
const paths = [];
|
|
274
|
+
const visit = async (path) => {
|
|
275
|
+
const entries = await readdir(join(projectRoot, path), { withFileTypes: true })
|
|
276
|
+
.catch(() => []);
|
|
277
|
+
for (const entry of entries) {
|
|
278
|
+
if (entry.name === "node_modules" || entry.isSymbolicLink())
|
|
279
|
+
continue;
|
|
280
|
+
const child = `${path}/${entry.name}`;
|
|
281
|
+
if (entry.isDirectory())
|
|
282
|
+
await visit(child);
|
|
283
|
+
else if (entry.isFile() && /\.[cm]?[jt]sx?$/u.test(entry.name)) {
|
|
284
|
+
const source = await readFile(join(projectRoot, child), "utf8").catch(() => "");
|
|
285
|
+
if (importsVercelConnect(source))
|
|
286
|
+
paths.push(child);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
await visit("agent");
|
|
291
|
+
await visit("agents");
|
|
292
|
+
return paths.sort();
|
|
293
|
+
}
|
|
294
|
+
/** Eve persists its compile-time routing decision, including its default model. */
|
|
295
|
+
async function collectEveGatewayWarnings(candidate, runtimeConfig) {
|
|
296
|
+
if (runtimeConfig?.withProtectedValues((values) => (values.AI_GATEWAY_API_KEY?.trim().length ?? 0) > 0) === true)
|
|
297
|
+
return [];
|
|
298
|
+
try {
|
|
299
|
+
const manifest = JSON.parse(await readFile(join(candidate.snapshotRoot, EVE_COMPILED_MANIFEST_PATH), "utf8"));
|
|
300
|
+
if (manifest?.config?.model?.routing?.kind !== "gateway")
|
|
301
|
+
return [];
|
|
302
|
+
const source = typeof manifest.config.source?.logicalPath === "string"
|
|
303
|
+
? manifest.config.source.logicalPath
|
|
304
|
+
: "agent/agent.ts";
|
|
305
|
+
return [{
|
|
306
|
+
id: "EVE_AI_GATEWAY_KEY_MISSING",
|
|
307
|
+
message: `The model in ${source} routes through Vercel AI Gateway, but --env-file has no non-blank AI_GATEWAY_API_KEY. Cloudflare has no Vercel OIDC project identity, so model calls fail.`,
|
|
308
|
+
remediation: 'Add AI_GATEWAY_API_KEY from https://vercel.com/dashboard/ai/api-keys to your --env-file, or use a direct provider such as openai("gpt-5") from @ai-sdk/openai with OPENAI_API_KEY in that file.',
|
|
309
|
+
}];
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
// Unknown or older compiled formats are not evidence of Gateway routing.
|
|
313
|
+
return [];
|
|
314
|
+
}
|
|
315
|
+
}
|
|
217
316
|
/**
|
|
218
317
|
* Recognizes the generated channel, not arbitrary user authentication policies.
|
|
219
318
|
* The calls below match the Eve 0.82 initializer's agent/channels/eve.ts.
|
|
@@ -228,6 +327,14 @@ async function collectEveSetupWarnings(projectRoot) {
|
|
|
228
327
|
return "";
|
|
229
328
|
}
|
|
230
329
|
};
|
|
330
|
+
const connectPaths = await vercelConnectSourcePaths(projectRoot);
|
|
331
|
+
if (connectPaths.length > 0) {
|
|
332
|
+
warnings.push({
|
|
333
|
+
id: "EVE_VERCEL_CONNECT",
|
|
334
|
+
message: `Vercel Connect imports in ${connectPaths.join(", ")} get credentials using a Vercel project identity (OIDC) that only Vercel injects; these connections fail on Cloudflare.`,
|
|
335
|
+
remediation: "Put each service's own API key or bot token in --env-file and replace connect(...) in the connection with auth: { getToken: async () => ({ token: process.env.SERVICE_TOKEN! }) }. For per-user sign-in, use defineInteractiveAuthorization from eve/connections with your own OAuth app.",
|
|
336
|
+
});
|
|
337
|
+
}
|
|
231
338
|
const channel = (await source("agent/channels/eve.ts"))
|
|
232
339
|
.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*/gu, "")
|
|
233
340
|
.replace(/\s+/gu, "");
|
|
@@ -246,7 +353,7 @@ async function collectEveSetupWarnings(projectRoot) {
|
|
|
246
353
|
warnings.push({
|
|
247
354
|
id: "EVE_WORLD_LOCAL",
|
|
248
355
|
message: "Local World stores memory on container disk; updates or a lost sleep snapshot reset sessions and approvals.",
|
|
249
|
-
remediation:
|
|
356
|
+
remediation: EVE_WORLD_LOCAL_REMEDIATION,
|
|
250
357
|
});
|
|
251
358
|
}
|
|
252
359
|
if (manifest.dependencies?.["just-bash"] === undefined &&
|
|
@@ -301,7 +408,7 @@ async function collectEveWorldWarnings(candidate, runtimeConfig) {
|
|
|
301
408
|
warnings.push({
|
|
302
409
|
id: "EVE_WORLD_LOCAL",
|
|
303
410
|
message: "Local World stores memory on container disk; updates or a lost sleep snapshot reset sessions and approvals.",
|
|
304
|
-
remediation:
|
|
411
|
+
remediation: EVE_WORLD_LOCAL_REMEDIATION,
|
|
305
412
|
});
|
|
306
413
|
}
|
|
307
414
|
}
|
|
@@ -471,13 +578,13 @@ function safeDockerEnvironment() {
|
|
|
471
578
|
DOCKER_CONTEXT: process.env.DOCKER_CONTEXT,
|
|
472
579
|
};
|
|
473
580
|
}
|
|
474
|
-
async function readWranglerJson(args) {
|
|
581
|
+
async function readWranglerJson(args, timeoutMs = 30_000) {
|
|
475
582
|
try {
|
|
476
583
|
const result = await execFileAsync(process.execPath, [wranglerEntrypoint, ...args], {
|
|
477
584
|
env: safeWranglerEnvironment(),
|
|
478
585
|
cwd: wranglerCwd,
|
|
479
586
|
maxBuffer: 2 * 1024 * 1024,
|
|
480
|
-
timeout:
|
|
587
|
+
timeout: timeoutMs,
|
|
481
588
|
});
|
|
482
589
|
const parsed = JSON.parse(result.stdout.trim());
|
|
483
590
|
return { value: parsed, failed: false };
|
|
@@ -813,11 +920,33 @@ async function resolveEveTargetOwnership(request, result) {
|
|
|
813
920
|
evidence.containerNamespaceId === evidence.workerContainerNamespaceId);
|
|
814
921
|
const remoteProven = evidence !== undefined && workerProven && containerProven;
|
|
815
922
|
if (!remoteProven) {
|
|
923
|
+
const failures = [];
|
|
924
|
+
if (evidence === undefined)
|
|
925
|
+
failures.push("provider evidence unavailable");
|
|
926
|
+
if (result.target.observed?.worker === true) {
|
|
927
|
+
if (evidence?.settingsReadable !== true)
|
|
928
|
+
failures.push("Worker settings unreadable");
|
|
929
|
+
else if (evidence.workerDeploymentId === undefined)
|
|
930
|
+
failures.push("Worker deployment stamp absent");
|
|
931
|
+
else if (!recordedDeploymentIds.has(evidence.workerDeploymentId))
|
|
932
|
+
failures.push("Worker deployment stamp differs from recorded identities");
|
|
933
|
+
}
|
|
934
|
+
if (result.target.observed?.container === true) {
|
|
935
|
+
if (evidence?.containerSchedulingPolicy !== "durable_object")
|
|
936
|
+
failures.push("Container scheduling policy unproven");
|
|
937
|
+
if (evidence?.containerNamespaceId === undefined)
|
|
938
|
+
failures.push("Container namespace unreadable");
|
|
939
|
+
if (evidence?.workerContainerNamespaceId === undefined)
|
|
940
|
+
failures.push("Worker Container binding namespace unreadable");
|
|
941
|
+
if (evidence?.containerNamespaceId !== undefined && evidence.workerContainerNamespaceId !== undefined &&
|
|
942
|
+
evidence.containerNamespaceId !== evidence.workerContainerNamespaceId)
|
|
943
|
+
failures.push("Container and Worker binding namespaces differ");
|
|
944
|
+
}
|
|
816
945
|
return {
|
|
817
946
|
...result,
|
|
818
947
|
target: {
|
|
819
948
|
...result.target,
|
|
820
|
-
message:
|
|
949
|
+
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("; ")}.`,
|
|
821
950
|
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.",
|
|
822
951
|
},
|
|
823
952
|
};
|
|
@@ -995,11 +1124,17 @@ function transportIdentityMatches(expected, observed) {
|
|
|
995
1124
|
"generationId",
|
|
996
1125
|
"stableWorkersDevOrigin",
|
|
997
1126
|
"workerName",
|
|
998
|
-
"stableContainerInstanceName",
|
|
999
1127
|
];
|
|
1000
1128
|
if (!required.every((key) => observed[key] === expected[key])) {
|
|
1001
1129
|
return false;
|
|
1002
1130
|
}
|
|
1131
|
+
const expectedInstance = expected.retainedDeployments === undefined ||
|
|
1132
|
+
expected.deploymentId === "eden-cloudflare"
|
|
1133
|
+
? expected.stableContainerInstanceName
|
|
1134
|
+
: `${expected.stableContainerInstanceName}:${expected.deploymentId}`;
|
|
1135
|
+
if (observed.stableContainerInstanceName !== expectedInstance ||
|
|
1136
|
+
observed.releaseId !== expected.releaseId)
|
|
1137
|
+
return false;
|
|
1003
1138
|
return expected.runtimeRevisionHandle === undefined
|
|
1004
1139
|
? observed.runtimeRevisionHandle === undefined
|
|
1005
1140
|
: observed.runtimeRevisionHandle === expected.runtimeRevisionHandle;
|
|
@@ -1012,6 +1147,7 @@ function identityMatches(expected, observed) {
|
|
|
1012
1147
|
"sourceDigest",
|
|
1013
1148
|
"generationId",
|
|
1014
1149
|
"deploymentId",
|
|
1150
|
+
"releaseId",
|
|
1015
1151
|
"environment",
|
|
1016
1152
|
"name",
|
|
1017
1153
|
"accountId",
|
|
@@ -1039,6 +1175,11 @@ function safeIdentity(identity) {
|
|
|
1039
1175
|
sourceDigest: identity.sourceDigest,
|
|
1040
1176
|
generationId: identity.generationId,
|
|
1041
1177
|
deploymentId: identity.deploymentId,
|
|
1178
|
+
releaseId: identity.releaseId,
|
|
1179
|
+
eveVersion: identity.eveVersion,
|
|
1180
|
+
affinityContract: identity.affinityContract,
|
|
1181
|
+
retainedDeployments: identity.retainedDeployments,
|
|
1182
|
+
pendingRetirements: identity.pendingRetirements,
|
|
1042
1183
|
environment: identity.environment,
|
|
1043
1184
|
name: identity.name,
|
|
1044
1185
|
accountId: identity.accountId,
|
|
@@ -1281,6 +1422,7 @@ function responseIdentity(response, body) {
|
|
|
1281
1422
|
read("sourceDigest", "x-eden-eve-source-digest");
|
|
1282
1423
|
read("generationId", "x-eden-eve-generation-id");
|
|
1283
1424
|
read("deploymentId", "x-eden-eve-deployment-id");
|
|
1425
|
+
read("releaseId", "x-eden-eve-release-id");
|
|
1284
1426
|
read("environment", "x-eden-eve-environment");
|
|
1285
1427
|
read("name", "x-eden-eve-name");
|
|
1286
1428
|
read("accountId", "x-eden-eve-account-id");
|
|
@@ -1341,7 +1483,7 @@ function defaultHealthRunner() {
|
|
|
1341
1483
|
// must wait until the *served container* verifiably runs this
|
|
1342
1484
|
// generation's image (the host stamps the header only after
|
|
1343
1485
|
// inspecting the running instance).
|
|
1344
|
-
if (observedIdentity.startedDeploymentId !== request.identity.deploymentId) {
|
|
1486
|
+
if (observedIdentity.startedDeploymentId !== (request.identity.releaseId ?? request.identity.deploymentId)) {
|
|
1345
1487
|
lastReason =
|
|
1346
1488
|
"The served Container has not verifiably started this deployment's image yet; waiting for the rollout to land.";
|
|
1347
1489
|
await wait();
|
|
@@ -1460,8 +1602,8 @@ async function promoteEveTargetPointer(deploymentRoot, pointerKey, generationRoo
|
|
|
1460
1602
|
}
|
|
1461
1603
|
return currentPath;
|
|
1462
1604
|
}
|
|
1463
|
-
async function writeEveDeploymentRecord(candidate, identity, hostConfig, status, extra = {}) {
|
|
1464
|
-
await writeFile(join(candidate.generationRoot,
|
|
1605
|
+
async function writeEveDeploymentRecord(candidate, identity, hostConfig, status, extra = {}, filename = "deployment.json") {
|
|
1606
|
+
await writeFile(join(candidate.generationRoot, filename), `${JSON.stringify({
|
|
1465
1607
|
version: 1,
|
|
1466
1608
|
status,
|
|
1467
1609
|
identity: safeIdentity(identity),
|
|
@@ -1474,6 +1616,10 @@ async function writeEveDeploymentRecord(candidate, identity, hostConfig, status,
|
|
|
1474
1616
|
flag: "wx",
|
|
1475
1617
|
});
|
|
1476
1618
|
}
|
|
1619
|
+
async function deploymentRecordPath(generationRoot) {
|
|
1620
|
+
const path = join(generationRoot, "deployment.json");
|
|
1621
|
+
return await lstat(path).then(() => path, () => join(generationRoot, "deployment-intent.json"));
|
|
1622
|
+
}
|
|
1477
1623
|
async function writeEveIndeterminateEvidence(candidate, evidence, extra = {}) {
|
|
1478
1624
|
await writeFile(join(candidate.generationRoot, "deployment-attempt.json"), `${JSON.stringify({
|
|
1479
1625
|
version: 1,
|
|
@@ -1538,6 +1684,14 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1538
1684
|
}
|
|
1539
1685
|
const priorIdentity = prior?.identity;
|
|
1540
1686
|
const priorDurableObjects = prior?.durableObjects;
|
|
1687
|
+
const worldManifest = collected.workflowWorld === "@moinulmoin/eden-world-cloudflare"
|
|
1688
|
+
? await readInstalledManifest(candidate.snapshotRoot, collected.workflowWorld)
|
|
1689
|
+
: undefined;
|
|
1690
|
+
const affinityEnabled = worldManifest?.eden?.capabilities?.deploymentAffinity === true;
|
|
1691
|
+
const worldRpcAuth = worldManifest?.eden?.capabilities?.worldRpcAuth === true;
|
|
1692
|
+
if (!affinityEnabled && priorIdentity?.retainedDeployments !== undefined) {
|
|
1693
|
+
throw deploymentFailure("EVE_AFFINITY_REQUIRED", "This target retains affinity deployments; install a World package declaring deploymentAffinity before updating it.");
|
|
1694
|
+
}
|
|
1541
1695
|
const superseded = isUpdate
|
|
1542
1696
|
? {
|
|
1543
1697
|
generationId: priorIdentity?.generationId,
|
|
@@ -1553,8 +1707,12 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1553
1707
|
if (durableWorld) {
|
|
1554
1708
|
resultWarnings.push({
|
|
1555
1709
|
id: "EVE_UPDATE_IN_FLIGHT",
|
|
1556
|
-
message:
|
|
1557
|
-
|
|
1710
|
+
message: affinityEnabled
|
|
1711
|
+
? "This update retains pinned deployments until their sessions, queue messages, and transfers drain."
|
|
1712
|
+
: "This update replaces the running Container; an Eve version change can strand existing sessions because the installed World does not declare deployment affinity.",
|
|
1713
|
+
remediation: affinityEnabled
|
|
1714
|
+
? "End old sessions to allow their deployment images to retire."
|
|
1715
|
+
: "Let sessions settle before changing Eve versions, or install an affinity-capable World package.",
|
|
1558
1716
|
});
|
|
1559
1717
|
}
|
|
1560
1718
|
if (prior?.worldCloudflare !==
|
|
@@ -1595,6 +1753,80 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1595
1753
|
throw deploymentFailure("EVE_IMAGE_IDENTITY_UNAVAILABLE", "The deploy candidate did not provide an immutable runtime image digest.");
|
|
1596
1754
|
}
|
|
1597
1755
|
const targetKey = stableDeploymentKey(collected.projectId, accountId, request.environment, request.name);
|
|
1756
|
+
const releaseId = `eve-deploy-${randomUUID()}`;
|
|
1757
|
+
let previousEveVersion = priorIdentity?.eveVersion;
|
|
1758
|
+
if (previousEveVersion === undefined && prior !== undefined) {
|
|
1759
|
+
previousEveVersion = (await readInstalledManifest(join(prior.generationRoot, "container/snapshot"), "eve"))?.version;
|
|
1760
|
+
}
|
|
1761
|
+
const retainedDeployments = {
|
|
1762
|
+
...priorIdentity?.retainedDeployments,
|
|
1763
|
+
};
|
|
1764
|
+
if (affinityEnabled && priorIdentity?.retainedDeployments === undefined &&
|
|
1765
|
+
priorIdentity?.containerImage !== undefined && priorIdentity.generationId !== undefined) {
|
|
1766
|
+
retainedDeployments["eden-cloudflare"] = {
|
|
1767
|
+
image: priorIdentity.containerImage,
|
|
1768
|
+
generationId: priorIdentity.generationId,
|
|
1769
|
+
...(previousEveVersion === undefined ? {} : { eveVersion: previousEveVersion }),
|
|
1770
|
+
affinityContract: EDEN_AFFINITY_CONTRACT,
|
|
1771
|
+
...(priorIdentity.runtimeRevisionHandle === undefined ? {} : { runtimeRevisionHandle: priorIdentity.runtimeRevisionHandle }),
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
const matchingDeployment = Object.entries(retainedDeployments).find(([, entry]) => entry.eveVersion === candidate.eveVersion && entry.affinityContract === EDEN_AFFINITY_CONTRACT)?.[0];
|
|
1775
|
+
const deploymentId = !affinityEnabled ? releaseId : matchingDeployment ??
|
|
1776
|
+
`eden-affinity-${createHash("sha256").update(`${candidate.eveVersion}:${EDEN_AFFINITY_CONTRACT}`).digest("hex").slice(0, 24)}`;
|
|
1777
|
+
const retiringDeployments = { ...priorIdentity?.pendingRetirements };
|
|
1778
|
+
delete retiringDeployments[deploymentId];
|
|
1779
|
+
if (priorIdentity?.deploymentId !== undefined)
|
|
1780
|
+
delete retiringDeployments[priorIdentity.deploymentId];
|
|
1781
|
+
for (const id of prior?.protectedDeploymentIds ?? [])
|
|
1782
|
+
delete retiringDeployments[id];
|
|
1783
|
+
const hasRetainedNoncurrentDeployments = priorIdentity?.retainedDeployments !== undefined &&
|
|
1784
|
+
Object.keys(retainedDeployments).some((id) => id !== priorIdentity.deploymentId);
|
|
1785
|
+
const controlToken = `${randomUUID()}${randomUUID()}`;
|
|
1786
|
+
if (affinityEnabled && (hasRetainedNoncurrentDeployments || Object.keys(retiringDeployments).length > 0)) {
|
|
1787
|
+
await (options.protectedStore ?? defaultProtectedStore(request.name)).put({
|
|
1788
|
+
targetId: targetKey, revision: `eve-affinity-control-${randomUUID()}`,
|
|
1789
|
+
inputIdentity: {
|
|
1790
|
+
token: `eve-affinity-control-input-${randomUUID()}`,
|
|
1791
|
+
byteLength: Buffer.byteLength(controlToken),
|
|
1792
|
+
digest: createHash("sha256").update(controlToken).digest("hex"),
|
|
1793
|
+
},
|
|
1794
|
+
variableNames: ["EDEN_EVE_CONTROL_TOKEN"], values: { EDEN_EVE_CONTROL_TOKEN: controlToken },
|
|
1795
|
+
});
|
|
1796
|
+
}
|
|
1797
|
+
if (affinityEnabled && hasRetainedNoncurrentDeployments) {
|
|
1798
|
+
const ids = Object.keys(retainedDeployments);
|
|
1799
|
+
const raw = await (options.deploymentRpc ?? defaultDeploymentRpc)({
|
|
1800
|
+
origin: stableWorkersDevOrigin, operation: "deployments.drainState", deploymentIds: ids,
|
|
1801
|
+
controlToken,
|
|
1802
|
+
});
|
|
1803
|
+
const states = validateDeploymentDrainStates(raw, ids);
|
|
1804
|
+
const eligible = ids.filter((id) => id !== deploymentId && id !== priorIdentity.deploymentId &&
|
|
1805
|
+
!prior?.protectedDeploymentIds?.includes(id) && states[id]?.drained === true);
|
|
1806
|
+
const remainingCount = ids.length - eligible.length + (deploymentId in retainedDeployments ? 0 : 1);
|
|
1807
|
+
if (remainingCount <= 10 && eligible.length > 0) {
|
|
1808
|
+
// Drain snapshots are not fences. Retire only noncurrent targets, and
|
|
1809
|
+
// obtain the World's durable admission fence before dropping routes.
|
|
1810
|
+
const retired = await (options.deploymentRpc ?? defaultDeploymentRpc)({
|
|
1811
|
+
origin: stableWorkersDevOrigin, operation: "deployments.retire",
|
|
1812
|
+
deploymentIds: eligible, controlToken,
|
|
1813
|
+
});
|
|
1814
|
+
if (typeof retired !== "object" || retired === null || !("retired" in retired) ||
|
|
1815
|
+
!Array.isArray(retired.retired) ||
|
|
1816
|
+
eligible.some((id) => !retired.retired.includes(id))) {
|
|
1817
|
+
throw deploymentFailure("EVE_AFFINITY_RETIRE_UNPROVEN", "The World did not prove the noncurrent deployments durably fenced for retirement.");
|
|
1818
|
+
}
|
|
1819
|
+
for (const id of eligible) {
|
|
1820
|
+
retiringDeployments[id] = retainedDeployments[id];
|
|
1821
|
+
delete retainedDeployments[id];
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
if (Object.keys(retainedDeployments).length + (deploymentId in retainedDeployments ? 0 : 1) > 10) {
|
|
1825
|
+
const blockers = Object.entries(states).filter(([id]) => id in retainedDeployments)
|
|
1826
|
+
.map(([id, state]) => `${id}: ${state.liveSessions} live sessions, ${state.liveRuns} live runs, ${state.pendingMessages} pending messages, ${state.unfinishedTransfers} unfinished transfers`);
|
|
1827
|
+
throw deploymentFailure("EVE_AFFINITY_LIMIT", `Deployment refused before image/code publication: at most 10 affinity deployments may be retained. Blocking deployments: ${blockers.join("; ")}`);
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1598
1830
|
let imageReference = options.containerImageReference;
|
|
1599
1831
|
if (imageReference !== undefined) {
|
|
1600
1832
|
if (imageDigestFromReference(imageReference) !== imageDigest) {
|
|
@@ -1607,7 +1839,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1607
1839
|
if (localImageReference === undefined || localImageReference.length === 0) {
|
|
1608
1840
|
throw deploymentFailure("EVE_IMAGE_IDENTITY_UNAVAILABLE", "The deploy candidate did not return an exact local image handle for publication.");
|
|
1609
1841
|
}
|
|
1610
|
-
const imageRepository = `eden-eve-${targetKey}-${candidate.generationId}`;
|
|
1842
|
+
const imageRepository = `eden-eve-${targetKey}-${affinityEnabled ? deploymentId : candidate.generationId}`;
|
|
1611
1843
|
const imageTag = `${imageRepository}:${EVE_REGISTRY_IMAGE_TAG}`;
|
|
1612
1844
|
const targetImageReference = `registry.cloudflare.com/${accountId}/${imageRepository}@${imageDigest}`;
|
|
1613
1845
|
const imagePublication = await (options.publishImage ??
|
|
@@ -1669,7 +1901,6 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1669
1901
|
throw deploymentFailure("DEPLOY_INDETERMINATE", reason);
|
|
1670
1902
|
}
|
|
1671
1903
|
}
|
|
1672
|
-
const deploymentId = `eve-deploy-${randomUUID()}`;
|
|
1673
1904
|
const containerApplicationName = boundedResourceName(request.name, "container");
|
|
1674
1905
|
const stableContainerInstanceName = boundedResourceName(request.name, "instance");
|
|
1675
1906
|
const runtimeVariableNames = candidate.runtimeVariableNames;
|
|
@@ -1678,6 +1909,24 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1678
1909
|
sourceDigest: candidate.sourceDigest,
|
|
1679
1910
|
generationId: candidate.generationId,
|
|
1680
1911
|
deploymentId,
|
|
1912
|
+
releaseId,
|
|
1913
|
+
eveVersion: candidate.eveVersion,
|
|
1914
|
+
...(affinityEnabled ? {
|
|
1915
|
+
affinityContract: EDEN_AFFINITY_CONTRACT,
|
|
1916
|
+
retainedDeployments: {
|
|
1917
|
+
...retainedDeployments,
|
|
1918
|
+
[deploymentId]: {
|
|
1919
|
+
image: imageReference,
|
|
1920
|
+
generationId: candidate.generationId,
|
|
1921
|
+
eveVersion: candidate.eveVersion,
|
|
1922
|
+
affinityContract: EDEN_AFFINITY_CONTRACT,
|
|
1923
|
+
releaseId,
|
|
1924
|
+
worldRpcAuth,
|
|
1925
|
+
...(runtimeInjection?.protectedHandle === undefined ? {} : { runtimeRevisionHandle: runtimeInjection.protectedHandle }),
|
|
1926
|
+
},
|
|
1927
|
+
},
|
|
1928
|
+
pendingRetirements: retiringDeployments,
|
|
1929
|
+
} : {}),
|
|
1681
1930
|
environment: request.environment,
|
|
1682
1931
|
name: request.name,
|
|
1683
1932
|
accountId,
|
|
@@ -1698,6 +1947,11 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1698
1947
|
containerApplicationName: identity.containerApplicationName,
|
|
1699
1948
|
stableContainerInstanceName: identity.stableContainerInstanceName,
|
|
1700
1949
|
deploymentId: identity.deploymentId,
|
|
1950
|
+
worldRpcAuth,
|
|
1951
|
+
...(identity.releaseId === undefined ? {} : { releaseId: identity.releaseId }),
|
|
1952
|
+
...(identity.eveVersion === undefined ? {} : { eveVersion: identity.eveVersion }),
|
|
1953
|
+
...(identity.affinityContract === undefined ? {} : { affinityContract: identity.affinityContract }),
|
|
1954
|
+
...(identity.retainedDeployments === undefined ? {} : { retainedDeployments: identity.retainedDeployments }),
|
|
1701
1955
|
generationId: identity.generationId,
|
|
1702
1956
|
stableWorkersDevOrigin: identity.stableWorkersDevOrigin,
|
|
1703
1957
|
containerImage: identity.containerImage,
|
|
@@ -1753,6 +2007,7 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1753
2007
|
throw error;
|
|
1754
2008
|
}
|
|
1755
2009
|
};
|
|
2010
|
+
await writeEveDeploymentRecord(candidate, identity, hostConfig, "indeterminate", { evidence: "publication-intent" }, "deployment-intent.json");
|
|
1756
2011
|
try {
|
|
1757
2012
|
eveProgress(options, "publishing Worker and Container application");
|
|
1758
2013
|
publication = await publishOne(publicationRequest);
|
|
@@ -1955,7 +2210,40 @@ async function runEveDeployment(request, options, collected) {
|
|
|
1955
2210
|
await promoteEveTargetPointer(join(request.projectRoot, ".eden", "eve-deploy"), `${request.environment}-${request.name}`, candidate.generationRoot);
|
|
1956
2211
|
await options.afterPromotion?.();
|
|
1957
2212
|
let supersededImageCleanup;
|
|
1958
|
-
if (
|
|
2213
|
+
if (affinityEnabled && Object.keys(retiringDeployments).length > 0) {
|
|
2214
|
+
const ids = Object.keys(retiringDeployments);
|
|
2215
|
+
const completedRetirements = [];
|
|
2216
|
+
try {
|
|
2217
|
+
const result = await (options.deploymentRpc ?? defaultDeploymentRpc)({
|
|
2218
|
+
origin: stableWorkersDevOrigin, operation: "deployments.retire", deploymentIds: ids,
|
|
2219
|
+
controlToken,
|
|
2220
|
+
});
|
|
2221
|
+
if (typeof result !== "object" || result === null ||
|
|
2222
|
+
!("retired" in result) || !Array.isArray(result.retired) ||
|
|
2223
|
+
ids.some((id) => !result.retired.includes(id)))
|
|
2224
|
+
throw new Error("Retirement was not proven.");
|
|
2225
|
+
for (const [id, retained] of Object.entries(retiringDeployments)) {
|
|
2226
|
+
const repository = ownedRegistryRepository({
|
|
2227
|
+
...identity, deploymentId: id, containerImage: retained.image, generationId: retained.generationId,
|
|
2228
|
+
}, request.environment, request.name, accountId);
|
|
2229
|
+
if (repository !== undefined && await (options.deleteRegistryRepository ??
|
|
2230
|
+
defaultRegistryRepositoryDelete)({ repository }) === "indeterminate") {
|
|
2231
|
+
throw new Error(`Repository deletion was not proven: ${repository}`);
|
|
2232
|
+
}
|
|
2233
|
+
completedRetirements.push(id);
|
|
2234
|
+
}
|
|
2235
|
+
supersededImageCleanup = check("VAL-LIFE-007", "passed", "Drained deployment instances and their owned registry repositories were retired.");
|
|
2236
|
+
}
|
|
2237
|
+
catch {
|
|
2238
|
+
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.");
|
|
2239
|
+
}
|
|
2240
|
+
if (completedRetirements.length > 0) {
|
|
2241
|
+
// Keep the immutable deployment record as evidence, but exclude proven
|
|
2242
|
+
// cleanup from the next deploy's active retry set.
|
|
2243
|
+
await writeFile(join(candidate.generationRoot, "completed-retirements.json"), `${JSON.stringify(completedRetirements)}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
if (isUpdate && !affinityEnabled) {
|
|
1959
2247
|
// The superseded generation's registry repository is generation-scoped
|
|
1960
2248
|
// and no longer referenced once CURRENT promotes this generation; only
|
|
1961
2249
|
// then is it safe to delete. Every tag in the repository goes with it —
|
|
@@ -2182,6 +2470,7 @@ async function collectEvePreflight(request, options) {
|
|
|
2182
2470
|
if (localHint !== -1)
|
|
2183
2471
|
warnings.splice(localHint, 1);
|
|
2184
2472
|
warnings.push(...(await collectEveWorldWarnings(candidate, undefined)));
|
|
2473
|
+
warnings.push(...await collectEveGatewayWarnings(candidate, runtimeConfig));
|
|
2185
2474
|
const schedules = await readCompiledSchedules(candidate);
|
|
2186
2475
|
const expressible = schedules.filter((schedule) => parseEveScheduleCron(schedule.cron) !== undefined);
|
|
2187
2476
|
const unexpressible = schedules.filter((schedule) => parseEveScheduleCron(schedule.cron) === undefined);
|
|
@@ -2512,7 +2801,7 @@ async function readEveDestroyRecord(projectRoot, environment, name) {
|
|
|
2512
2801
|
if (generationRoot === undefined) {
|
|
2513
2802
|
return readEveDestroyRecordFromGenerations(projectRoot, environment, name);
|
|
2514
2803
|
}
|
|
2515
|
-
const parsed = await parseEveDestroyRecord(
|
|
2804
|
+
const parsed = await parseEveDestroyRecord(await deploymentRecordPath(generationRoot), environment, name);
|
|
2516
2805
|
return parsed === undefined
|
|
2517
2806
|
? undefined
|
|
2518
2807
|
: { ...parsed, generationRoot };
|
|
@@ -2526,7 +2815,7 @@ async function readEveDestroyRecordFromGenerations(projectRoot, environment, nam
|
|
|
2526
2815
|
const details = await lstat(generationRoot).catch(() => undefined);
|
|
2527
2816
|
if (details === undefined || !details.isDirectory())
|
|
2528
2817
|
continue;
|
|
2529
|
-
const recordDetails = await lstat(
|
|
2818
|
+
const recordDetails = await lstat(await deploymentRecordPath(generationRoot)).catch(() => undefined);
|
|
2530
2819
|
if (recordDetails !== undefined && recordDetails.isFile()) {
|
|
2531
2820
|
withRecords.push({
|
|
2532
2821
|
generationRoot,
|
|
@@ -2536,7 +2825,7 @@ async function readEveDestroyRecordFromGenerations(projectRoot, environment, nam
|
|
|
2536
2825
|
}
|
|
2537
2826
|
withRecords.sort((left, right) => right.modifiedAt - left.modifiedAt);
|
|
2538
2827
|
for (const { generationRoot } of withRecords) {
|
|
2539
|
-
const parsed = await parseEveDestroyRecord(
|
|
2828
|
+
const parsed = await parseEveDestroyRecord(await deploymentRecordPath(generationRoot), environment, name);
|
|
2540
2829
|
if (parsed !== undefined) {
|
|
2541
2830
|
return { ...parsed, generationRoot };
|
|
2542
2831
|
}
|
|
@@ -2581,7 +2870,35 @@ async function parseEveDestroyRecord(recordPath, environment, name) {
|
|
|
2581
2870
|
if (identity.environment !== environment || identity.name !== name) {
|
|
2582
2871
|
return undefined;
|
|
2583
2872
|
}
|
|
2873
|
+
const metadata = {};
|
|
2874
|
+
for (const key of ["deploymentId", "eveVersion", "releaseId", "runtimeRevisionHandle"]) {
|
|
2875
|
+
if (typeof identity[key] === "string")
|
|
2876
|
+
Object.assign(metadata, { [key]: identity[key] });
|
|
2877
|
+
}
|
|
2878
|
+
if (typeof identity.affinityContract === "number")
|
|
2879
|
+
Object.assign(metadata, { affinityContract: identity.affinityContract });
|
|
2880
|
+
for (const key of ["retainedDeployments", "pendingRetirements"]) {
|
|
2881
|
+
const value = identity[key];
|
|
2882
|
+
if (value === undefined)
|
|
2883
|
+
continue;
|
|
2884
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
2885
|
+
return undefined;
|
|
2886
|
+
for (const [id, deployment] of Object.entries(value)) {
|
|
2887
|
+
if (!/^(?:eden-cloudflare|eden-affinity-[a-f0-9]{24})$/u.test(id) ||
|
|
2888
|
+
typeof deployment !== "object" || deployment === null ||
|
|
2889
|
+
typeof deployment.image !== "string" || imageDigestFromReference(deployment.image) === undefined ||
|
|
2890
|
+
typeof deployment.generationId !== "string" ||
|
|
2891
|
+
(deployment.eveVersion !== undefined && typeof deployment.eveVersion !== "string") ||
|
|
2892
|
+
(deployment.affinityContract !== undefined && typeof deployment.affinityContract !== "number") ||
|
|
2893
|
+
(deployment.releaseId !== undefined && typeof deployment.releaseId !== "string") ||
|
|
2894
|
+
(deployment.worldRpcAuth !== undefined && typeof deployment.worldRpcAuth !== "boolean") ||
|
|
2895
|
+
(deployment.runtimeRevisionHandle !== undefined && typeof deployment.runtimeRevisionHandle !== "string"))
|
|
2896
|
+
return undefined;
|
|
2897
|
+
}
|
|
2898
|
+
Object.assign(metadata, { [key]: value });
|
|
2899
|
+
}
|
|
2584
2900
|
const verified = {
|
|
2901
|
+
...metadata,
|
|
2585
2902
|
workerName: identity.workerName,
|
|
2586
2903
|
containerApplicationName: identity.containerApplicationName,
|
|
2587
2904
|
stableContainerInstanceName: identity.stableContainerInstanceName,
|
|
@@ -2620,7 +2937,7 @@ async function readEveDeploymentRecord(projectRoot, environment, name, accountId
|
|
|
2620
2937
|
const deploymentIds = new Set();
|
|
2621
2938
|
for (const entry of entries) {
|
|
2622
2939
|
const generationRoot = join(generationsRoot, entry);
|
|
2623
|
-
const recordPath =
|
|
2940
|
+
const recordPath = await deploymentRecordPath(generationRoot);
|
|
2624
2941
|
const details = await lstat(recordPath).catch(() => undefined);
|
|
2625
2942
|
if (details === undefined || !details.isFile())
|
|
2626
2943
|
continue;
|
|
@@ -2653,7 +2970,7 @@ async function readEveDeploymentRecord(projectRoot, environment, name, accountId
|
|
|
2653
2970
|
live = pinned;
|
|
2654
2971
|
}
|
|
2655
2972
|
else {
|
|
2656
|
-
const parsed = await parseEveDeploymentRecord(
|
|
2973
|
+
const parsed = await parseEveDeploymentRecord(await deploymentRecordPath(generationRoot), environment, name);
|
|
2657
2974
|
if (parsed !== undefined &&
|
|
2658
2975
|
!((accountId !== undefined &&
|
|
2659
2976
|
parsed.identity.accountId !== accountId) ||
|
|
@@ -2669,10 +2986,31 @@ async function readEveDeploymentRecord(projectRoot, environment, name, accountId
|
|
|
2669
2986
|
// Newest record carrying a usable DO history wins, regardless of its
|
|
2670
2987
|
// deployment outcome: a tag it published must never be replayed.
|
|
2671
2988
|
const historySource = scanned.find((entry) => entry.record.durableObjects !== undefined);
|
|
2989
|
+
// CURRENT is only a health/promotion pointer. A newer uncertain publication
|
|
2990
|
+
// may already serve sessions; preserve every such image until a later deploy
|
|
2991
|
+
// can prove it drained, never infer absence from a failed health check.
|
|
2992
|
+
const unresolved = scanned.filter((entry) => entry.modifiedAt > live.modifiedAt);
|
|
2993
|
+
const retainedDeployments = { ...live.record.identity.retainedDeployments };
|
|
2994
|
+
const pendingRetirements = { ...live.record.identity.pendingRetirements };
|
|
2995
|
+
const protectedDeploymentIds = new Set();
|
|
2996
|
+
if (live.record.identity.retainedDeployments === undefined) {
|
|
2997
|
+
protectedDeploymentIds.add("eden-cloudflare");
|
|
2998
|
+
}
|
|
2999
|
+
for (const entry of unresolved.slice().reverse()) {
|
|
3000
|
+
Object.assign(retainedDeployments, entry.record.identity.retainedDeployments);
|
|
3001
|
+
Object.assign(pendingRetirements, entry.record.identity.pendingRetirements);
|
|
3002
|
+
if (entry.record.deploymentId !== undefined)
|
|
3003
|
+
protectedDeploymentIds.add(entry.record.deploymentId);
|
|
3004
|
+
}
|
|
2672
3005
|
return {
|
|
2673
3006
|
recordPath: live.record.recordPath,
|
|
2674
3007
|
generationRoot: live.generationRoot,
|
|
2675
|
-
identity:
|
|
3008
|
+
identity: {
|
|
3009
|
+
...live.record.identity,
|
|
3010
|
+
...(Object.keys(retainedDeployments).length === 0 ? {} : { retainedDeployments }),
|
|
3011
|
+
...(Object.keys(pendingRetirements).length === 0 ? {} : { pendingRetirements }),
|
|
3012
|
+
},
|
|
3013
|
+
protectedDeploymentIds: [...protectedDeploymentIds],
|
|
2676
3014
|
status: live.record.status,
|
|
2677
3015
|
deploymentIds: [...deploymentIds],
|
|
2678
3016
|
worldCloudflare: live.record.worldCloudflare,
|
|
@@ -2683,6 +3021,19 @@ async function parseEveDeploymentRecord(recordPath, environment, name) {
|
|
|
2683
3021
|
const base = await parseEveDestroyRecord(recordPath, environment, name);
|
|
2684
3022
|
if (base === undefined)
|
|
2685
3023
|
return undefined;
|
|
3024
|
+
let identity = base.identity;
|
|
3025
|
+
try {
|
|
3026
|
+
const completed = JSON.parse(await readFile(join(dirname(recordPath), "completed-retirements.json"), "utf8"));
|
|
3027
|
+
if (Array.isArray(completed) && completed.every((id) => typeof id === "string")) {
|
|
3028
|
+
const pendingRetirements = { ...identity.pendingRetirements };
|
|
3029
|
+
for (const id of completed)
|
|
3030
|
+
delete pendingRetirements[id];
|
|
3031
|
+
identity = { ...identity, pendingRetirements };
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
catch {
|
|
3035
|
+
// Missing or unreadable cleanup evidence must not suppress a retry.
|
|
3036
|
+
}
|
|
2686
3037
|
let parsed;
|
|
2687
3038
|
try {
|
|
2688
3039
|
parsed = JSON.parse(await readFile(recordPath, "utf8"));
|
|
@@ -2726,7 +3077,7 @@ async function parseEveDeploymentRecord(recordPath, environment, name) {
|
|
|
2726
3077
|
Array.isArray(rawMigrations);
|
|
2727
3078
|
const invalidHistory = () => ({
|
|
2728
3079
|
recordPath,
|
|
2729
|
-
identity
|
|
3080
|
+
identity,
|
|
2730
3081
|
status,
|
|
2731
3082
|
deploymentId,
|
|
2732
3083
|
worldCloudflare: rawVars?.EDEN_EVE_WORLD_CLOUDFLARE === true,
|
|
@@ -2753,7 +3104,7 @@ async function parseEveDeploymentRecord(recordPath, environment, name) {
|
|
|
2753
3104
|
}
|
|
2754
3105
|
return {
|
|
2755
3106
|
recordPath,
|
|
2756
|
-
identity
|
|
3107
|
+
identity,
|
|
2757
3108
|
status,
|
|
2758
3109
|
deploymentId,
|
|
2759
3110
|
worldCloudflare: rawVars?.EDEN_EVE_WORLD_CLOUDFLARE === true,
|
|
@@ -2775,7 +3126,7 @@ async function defaultDestroyTargetRead(request) {
|
|
|
2775
3126
|
"images",
|
|
2776
3127
|
"list",
|
|
2777
3128
|
"--json",
|
|
2778
|
-
]);
|
|
3129
|
+
], WRANGLER_OPERATION_TIMEOUT_MS);
|
|
2779
3130
|
const tagsByRepository = registryTagsByRepository(images.value);
|
|
2780
3131
|
if (!images.failed && tagsByRepository !== undefined) {
|
|
2781
3132
|
const present = [];
|
|
@@ -2854,12 +3205,13 @@ async function defaultContainerDelete(request) {
|
|
|
2854
3205
|
* repository is deleted individually.
|
|
2855
3206
|
*/
|
|
2856
3207
|
async function defaultRegistryRepositoryDelete(request) {
|
|
3208
|
+
// Catalog reads include Cloudflare snapshot tags and can exceed 30 seconds.
|
|
2857
3209
|
const images = await readWranglerJson([
|
|
2858
3210
|
"containers",
|
|
2859
3211
|
"images",
|
|
2860
3212
|
"list",
|
|
2861
3213
|
"--json",
|
|
2862
|
-
]);
|
|
3214
|
+
], WRANGLER_OPERATION_TIMEOUT_MS);
|
|
2863
3215
|
const collection = jsonCollection(images.value);
|
|
2864
3216
|
if (images.failed || collection === undefined)
|
|
2865
3217
|
return "indeterminate";
|
|
@@ -2885,9 +3237,11 @@ async function defaultRegistryRepositoryDelete(request) {
|
|
|
2885
3237
|
deletedAny = true;
|
|
2886
3238
|
continue;
|
|
2887
3239
|
}
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
3240
|
+
}
|
|
3241
|
+
const after = await readWranglerJson(["containers", "images", "list", "--json"], WRANGLER_OPERATION_TIMEOUT_MS);
|
|
3242
|
+
const remaining = after.failed ? undefined : registryTagsByRepository(after.value);
|
|
3243
|
+
if (remaining === undefined || (remaining.get(request.repository)?.length ?? 0) > 0) {
|
|
3244
|
+
return "indeterminate";
|
|
2891
3245
|
}
|
|
2892
3246
|
return deletedAny ? "deleted" : "absent";
|
|
2893
3247
|
}
|
|
@@ -2905,13 +3259,13 @@ function ownedRegistryRepository(identity, environment, name, accountId) {
|
|
|
2905
3259
|
identity.accountId !== accountId) {
|
|
2906
3260
|
return undefined;
|
|
2907
3261
|
}
|
|
2908
|
-
const
|
|
2909
|
-
const
|
|
2910
|
-
if (
|
|
2911
|
-
|
|
3262
|
+
const prefix = `eden-eve-${stableDeploymentKey(identity.projectId, identity.accountId, environment, name)}-`;
|
|
3263
|
+
const suffixes = [identity.generationId];
|
|
3264
|
+
if (identity.retainedDeployments !== undefined && identity.deploymentId !== undefined)
|
|
3265
|
+
suffixes.push(identity.deploymentId);
|
|
3266
|
+
if (imageDigestFromReference(identity.containerImage) === undefined)
|
|
2912
3267
|
return undefined;
|
|
2913
|
-
}
|
|
2914
|
-
return repository;
|
|
3268
|
+
return suffixes.map((suffix) => `${prefix}${suffix}`).find((repository) => identity.containerImage.startsWith(`registry.cloudflare.com/${identity.accountId}/${repository}@`));
|
|
2915
3269
|
}
|
|
2916
3270
|
/**
|
|
2917
3271
|
* The registry repository an indeterminate deploy attempt provably pushed.
|
|
@@ -2964,17 +3318,31 @@ async function collectEveDestroyRegistryRepositories(projectRoot, environment, n
|
|
|
2964
3318
|
const generationsRoot = join(projectRoot, ".eden", "eve-deploy", "generations");
|
|
2965
3319
|
const entries = await readdir(generationsRoot).catch(() => []);
|
|
2966
3320
|
const repositories = new Set();
|
|
3321
|
+
const addRetained = (identity) => {
|
|
3322
|
+
for (const [id, retained] of Object.entries({
|
|
3323
|
+
...identity.retainedDeployments, ...identity.pendingRetirements,
|
|
3324
|
+
})) {
|
|
3325
|
+
const repository = ownedRegistryRepository({
|
|
3326
|
+
...identity, deploymentId: id, containerImage: retained.image, generationId: retained.generationId,
|
|
3327
|
+
}, environment, name, record.accountId);
|
|
3328
|
+
if (repository !== undefined)
|
|
3329
|
+
repositories.add(repository);
|
|
3330
|
+
}
|
|
3331
|
+
};
|
|
3332
|
+
addRetained(record);
|
|
2967
3333
|
const targetKey = record.projectId === undefined
|
|
2968
3334
|
? undefined
|
|
2969
3335
|
: stableDeploymentKey(record.projectId, record.accountId, environment, name);
|
|
2970
3336
|
for (const entry of entries) {
|
|
2971
3337
|
const generationRoot = join(generationsRoot, entry);
|
|
2972
|
-
const parsed = await parseEveDestroyRecord(
|
|
3338
|
+
const parsed = await parseEveDestroyRecord(await deploymentRecordPath(generationRoot), environment, name);
|
|
2973
3339
|
const deployedRepository = parsed === undefined
|
|
2974
3340
|
? undefined
|
|
2975
3341
|
: ownedRegistryRepository(parsed.identity, environment, name, record.accountId);
|
|
2976
3342
|
if (deployedRepository !== undefined)
|
|
2977
3343
|
repositories.add(deployedRepository);
|
|
3344
|
+
if (parsed !== undefined)
|
|
3345
|
+
addRetained(parsed.identity);
|
|
2978
3346
|
if (targetKey === undefined)
|
|
2979
3347
|
continue;
|
|
2980
3348
|
const attemptRepository = await attemptRegistryRepository(join(generationRoot, "deployment-attempt.json"), record.accountId, targetKey);
|
|
@@ -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",
|
|
@@ -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/dist/index.js
CHANGED
|
@@ -17,6 +17,7 @@ Commands:
|
|
|
17
17
|
preflight Build and inspect a local Eve candidate without remote mutation
|
|
18
18
|
deploy Deploy the selected Eve project to the exact named target
|
|
19
19
|
destroy Remove the exact owned Eve target
|
|
20
|
+
skill Print the bundled Eden skill for coding agents
|
|
20
21
|
|
|
21
22
|
preflight and deploy default to the current directory, the preview
|
|
22
23
|
environment, and a target name derived from package.json. destroy and
|
|
@@ -177,6 +178,24 @@ export async function runEdenCli(args, options = {}) {
|
|
|
177
178
|
stdout(edenVersion());
|
|
178
179
|
return 0;
|
|
179
180
|
}
|
|
181
|
+
if (args[0] === "skill") {
|
|
182
|
+
if (args.length === 2 && (args[1] === "--help" || args[1] === "-h")) {
|
|
183
|
+
stdout("Usage: eden skill");
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
186
|
+
if (args.length !== 1) {
|
|
187
|
+
throw new EveCliError({
|
|
188
|
+
code: "EVE_ARGUMENT_UNKNOWN",
|
|
189
|
+
message: "eden skill takes no arguments or options.",
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
const skill = readFileSync(new URL("./SKILL.md", import.meta.url), "utf8");
|
|
193
|
+
if (options.stdout === undefined)
|
|
194
|
+
process.stdout.write(skill);
|
|
195
|
+
else
|
|
196
|
+
options.stdout(skill);
|
|
197
|
+
return 0;
|
|
198
|
+
}
|
|
180
199
|
if (!EVE_CLI_COMMANDS.includes(args[0] ?? "")) {
|
|
181
200
|
throw new EveCliError({
|
|
182
201
|
code: "COMMAND_UNKNOWN",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moinulmoin/eden",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Deploy existing Eve projects to your own Cloudflare account.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -27,11 +27,12 @@
|
|
|
27
27
|
"dist/**/*.js",
|
|
28
28
|
"dist/**/*.mjs",
|
|
29
29
|
"dist/**/*.d.ts",
|
|
30
|
+
"dist/SKILL.md",
|
|
30
31
|
"LICENSE",
|
|
31
32
|
"NOTICE"
|
|
32
33
|
],
|
|
33
34
|
"dependencies": {
|
|
34
|
-
"@moinulmoin/eden-runtime-cloudflare": "0.
|
|
35
|
+
"@moinulmoin/eden-runtime-cloudflare": "0.11.0",
|
|
35
36
|
"wrangler": "4.147.0"
|
|
36
37
|
},
|
|
37
38
|
"devDependencies": {
|
|
@@ -54,7 +55,7 @@
|
|
|
54
55
|
},
|
|
55
56
|
"scripts": {
|
|
56
57
|
"build": "tsc -b",
|
|
57
|
-
"postbuild": "chmod 755 dist/index.js",
|
|
58
|
+
"postbuild": "chmod 755 dist/index.js && cp ../../skills/eden/SKILL.md dist/SKILL.md",
|
|
58
59
|
"typecheck": "tsc -b --pretty false",
|
|
59
60
|
"lint": "eslint src --max-warnings 0",
|
|
60
61
|
"test": "pnpm --dir ../.. exec vitest run packages/cli/test --passWithNoTests --maxWorkers=1"
|