@evident-ai/runner-cdk 3.4.1-dev.c463782 → 3.4.1-dev.d0469f4

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.
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.ImageVersionReporter = void 0;
37
+ const path = __importStar(require("path"));
38
+ const cdk = __importStar(require("aws-cdk-lib"));
39
+ const constructs_1 = require("constructs");
40
+ const lambda = __importStar(require("aws-cdk-lib/aws-lambda"));
41
+ const cr = __importStar(require("aws-cdk-lib/custom-resources"));
42
+ /** The JSON field name inside the doorbell secret, same key the controller reads. */
43
+ const DOORBELL_SECRET_KEY = 'DOORBELL_SECRET';
44
+ /**
45
+ * Deploy-time push: reports each shape's just-published MicroVM image version to
46
+ * Evident, once per deploy that actually changes it. `imageVersions` is the
47
+ * payload; `buildTriggers` is what guarantees re-invocation on exactly the
48
+ * deploy that matters, independent of `imageVersions`' own resolution timing.
49
+ */
50
+ class ImageVersionReporter extends constructs_1.Construct {
51
+ constructor(scope, id, props) {
52
+ super(scope, id);
53
+ const handler = new lambda.Function(this, 'Function', {
54
+ runtime: lambda.Runtime.NODEJS_22_X,
55
+ // Pre-bundled at PACKAGE build time, same reasoning as the waker's and the
56
+ // controller's own `Code.fromAsset` (see their comments) — resolved from
57
+ // the PACKAGE ROOT, two directories below this file either compiled
58
+ // (`dist/microvm/image-version-reporter`) or via ts-node
59
+ // (`src/microvm/image-version-reporter`).
60
+ code: lambda.Code.fromAsset(path.join(__dirname, '..', '..', '..', 'dist', 'image-version-reporter-lambda')),
61
+ handler: 'handler.handler',
62
+ memorySize: 128,
63
+ // A deploy hook must not hold CloudFormation for its default 1 h — this is
64
+ // the hard stop; the handler's own internal REPORT_TIMEOUT_MS is well under it.
65
+ timeout: cdk.Duration.seconds(30),
66
+ environment: {
67
+ // ARN + field name only, never the value (mirrors the controller and waker).
68
+ DOORBELL_SECRET_ARN: props.doorbellSecret.secretArn,
69
+ DOORBELL_SECRET_KEY: DOORBELL_SECRET_KEY,
70
+ EVIDENT_API_URL: props.evidentApiUrl,
71
+ EVIDENT_RUNNER_ID: props.evidentRunnerId,
72
+ },
73
+ });
74
+ props.doorbellSecret.grantRead(handler);
75
+ const provider = new cr.Provider(this, 'Provider', {
76
+ onEventHandler: handler,
77
+ });
78
+ new cdk.CustomResource(this, 'Resource', {
79
+ serviceToken: provider.serviceToken,
80
+ properties: {
81
+ imageVersions: props.imageVersions,
82
+ // Not read by the handler — its only job is to be a property value
83
+ // CloudFormation can see change (a structured object, so any field
84
+ // changing anywhere is a real property diff), so the resource is
85
+ // invoked on exactly the deploy that changes a build input.
86
+ buildTriggers: props.buildTriggers,
87
+ },
88
+ });
89
+ }
90
+ }
91
+ exports.ImageVersionReporter = ImageVersionReporter;
@@ -0,0 +1,26 @@
1
+ export type ImageVersionPair = {
2
+ shape: string;
3
+ image_version: string;
4
+ };
5
+ export type ReporterEvent = {
6
+ RequestType: 'Create' | 'Update' | 'Delete';
7
+ ResourceProperties?: {
8
+ imageVersions?: unknown;
9
+ };
10
+ };
11
+ export type ReporterConfig = {
12
+ doorbellSecretArn: string;
13
+ doorbellSecretKey: string;
14
+ evidentApiUrl: string;
15
+ evidentRunnerId: string;
16
+ };
17
+ type ReporterDependencies = {
18
+ getSecret: (secretArn: string, secretKey: string) => Promise<string>;
19
+ fetch: typeof fetch;
20
+ };
21
+ type ReporterResponse = {
22
+ PhysicalResourceId: string;
23
+ };
24
+ export declare function handleImageVersionReport(event: ReporterEvent, getConfig: () => ReporterConfig, dependencies?: ReporterDependencies): Promise<ReporterResponse>;
25
+ export declare const handler: (event: ReporterEvent) => Promise<ReporterResponse>;
26
+ export {};
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handler = void 0;
4
+ exports.handleImageVersionReport = handleImageVersionReport;
5
+ const node_crypto_1 = require("node:crypto");
6
+ const client_secrets_manager_1 = require("@aws-sdk/client-secrets-manager");
7
+ const PHYSICAL_RESOURCE_ID = 'evident-image-version-report';
8
+ const REPORT_TIMEOUT_MS = 5_000;
9
+ const secrets = new client_secrets_manager_1.SecretsManagerClient({});
10
+ async function fetchDoorbellSecret(secretArn, secretKey) {
11
+ const { SecretString } = await secrets.send(new client_secrets_manager_1.GetSecretValueCommand({ SecretId: secretArn }));
12
+ if (!SecretString) {
13
+ throw new Error(`doorbell secret ${secretArn} has no SecretString`);
14
+ }
15
+ const parsed = JSON.parse(SecretString);
16
+ const value = parsed !== null && typeof parsed === 'object'
17
+ ? parsed[secretKey]
18
+ : undefined;
19
+ if (typeof value !== 'string' || value === '') {
20
+ throw new Error(`doorbell secret ${secretArn} is missing a non-empty '${secretKey}' field`);
21
+ }
22
+ return value;
23
+ }
24
+ function requireEnv(name) {
25
+ const value = process.env[name];
26
+ if (!value) {
27
+ throw new Error(`missing required env var ${name}`);
28
+ }
29
+ return value;
30
+ }
31
+ function readImageVersions(value) {
32
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
33
+ throw new Error('imageVersions must be an object');
34
+ }
35
+ return Object.entries(value).map(([shape, imageVersion]) => {
36
+ if (typeof imageVersion !== 'string') {
37
+ throw new Error(`imageVersions.${shape} must be a string`);
38
+ }
39
+ return { shape, image_version: imageVersion };
40
+ });
41
+ }
42
+ function response() {
43
+ return { PhysicalResourceId: PHYSICAL_RESOURCE_ID };
44
+ }
45
+ const runtimeDependencies = {
46
+ getSecret: fetchDoorbellSecret,
47
+ fetch: globalThis.fetch,
48
+ };
49
+ async function handleImageVersionReport(event, getConfig, dependencies = runtimeDependencies) {
50
+ if (event.RequestType === 'Delete') {
51
+ return response();
52
+ }
53
+ let runnerId = 'unknown';
54
+ let versions = [];
55
+ let responseStatus;
56
+ try {
57
+ versions = readImageVersions(event.ResourceProperties?.imageVersions);
58
+ const config = getConfig();
59
+ runnerId = config.evidentRunnerId;
60
+ const secret = await dependencies.getSecret(config.doorbellSecretArn, config.doorbellSecretKey);
61
+ const body = JSON.stringify({
62
+ type: 'runner.microvm_image_versions_reported',
63
+ versions,
64
+ });
65
+ const signature = (0, node_crypto_1.createHmac)('sha256', secret).update(body).digest('hex');
66
+ const url = `${config.evidentApiUrl.replace(/\/+$/, '')}/v1/runners/${encodeURIComponent(runnerId)}/microvm-image-versions`;
67
+ const result = await dependencies.fetch(url, {
68
+ method: 'POST',
69
+ headers: {
70
+ 'content-type': 'application/json',
71
+ 'x-evident-signature': signature,
72
+ },
73
+ body,
74
+ signal: AbortSignal.timeout(REPORT_TIMEOUT_MS),
75
+ });
76
+ responseStatus = result.status;
77
+ console.log('runner.microvm_image_versions_reported', {
78
+ runnerId,
79
+ versions,
80
+ status: result.status,
81
+ });
82
+ if (!result.ok) {
83
+ throw new Error(`Evident returned HTTP ${result.status}`);
84
+ }
85
+ }
86
+ catch (error) {
87
+ const message = error instanceof Error ? error.message : String(error);
88
+ console.warn('MicroVM image version report failed', {
89
+ operation: event.RequestType,
90
+ runnerId,
91
+ versions,
92
+ ...(responseStatus === undefined ? {} : { status: responseStatus }),
93
+ error: message,
94
+ });
95
+ }
96
+ return response();
97
+ }
98
+ const handler = (event) => handleImageVersionReport(event, () => ({
99
+ doorbellSecretArn: requireEnv('DOORBELL_SECRET_ARN'),
100
+ doorbellSecretKey: requireEnv('DOORBELL_SECRET_KEY'),
101
+ evidentApiUrl: requireEnv('EVIDENT_API_URL'),
102
+ evidentRunnerId: requireEnv('EVIDENT_RUNNER_ID'),
103
+ }));
104
+ exports.handler = handler;
@@ -1,7 +1,9 @@
1
- # Evident per-session MicroVM image (#558, epic #556)
1
+ # Generic MicroVM runner image
2
2
  #
3
- # Same installed footprint as the ECS runner image, built for linux/arm64
4
- # (Lambda MicroVMs run on Graviton only) and with ONE process: the hook server.
3
+ # Built for linux/arm64 (Lambda MicroVMs run on Graviton only) and with ONE
4
+ # process: the hook server. `overlay/setup-root` and `overlay/setup-workspace`
5
+ # are the extension point for deployment-specific packages, dependency
6
+ # installation and build warm-up.
5
7
  #
6
8
  # THE BOOT SPLIT — the rule that governs everything below. AWS snapshots this
7
9
  # image ONCE at build and every MicroVM resumes from that same snapshot, so
@@ -9,7 +11,8 @@
9
11
  # this image version. Therefore this image must NOT start the tunnel
10
12
  # (`evident run`), clone the repo, or carry any credential, machine ID or
11
13
  # generated secret. Identity arrives per-VM in the `/run` hook payload.
12
- # `src/image/dockerfile.test.ts` fails the build if this regresses.
14
+ # `aws/runner-cdk/src/microvm/image/dockerfile.test.ts` fails the build if this
15
+ # regresses.
13
16
  #
14
17
  # No `--platform` on FROM: AWS builds this natively on Graviton. Build it
15
18
  # locally with `docker build --platform linux/arm64`.
@@ -27,9 +30,8 @@ RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
27
30
 
28
31
  # System tooling + GitHub CLI + AWS CLI, in a single layer.
29
32
  # - awscli (v2): required — durable-state persistence uses `aws s3`.
30
- # - procps, postgresql(+contrib), direnv: required at RUNTIME so the VM is a
31
- # full dev box for this repo (local Postgres on 5433 as the non-root runner,
32
- # no sudo). Listed explicitly so apt keeps them.
33
+ # - procps: required at RUNTIME for process inspection and debugging. Listed
34
+ # explicitly so apt keeps it.
33
35
  # - gnupg, unzip: BUILD-ONLY (gh apt key / AWS CLI bundle); purged in this layer.
34
36
  #
35
37
  # Purge build-only packages BY NAME — do NOT use `apt-get autoremove`, which
@@ -38,7 +40,6 @@ RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
38
40
  RUN apt-get update \
39
41
  && apt-get install -y --no-install-recommends \
40
42
  ca-certificates curl git jq less procps psmisc ripgrep \
41
- postgresql postgresql-contrib direnv \
42
43
  gnupg unzip \
43
44
  # --- GitHub CLI (`gh`) from the official apt repo ---
44
45
  && mkdir -p -m 755 /etc/apt/keyrings \
@@ -54,10 +55,6 @@ RUN apt-get update \
54
55
  && unzip -q /tmp/awscliv2.zip -d /tmp \
55
56
  && /tmp/aws/install \
56
57
  && aws --version \
57
- # --- Put the PostgreSQL server binaries on PATH ---
58
- # On Debian initdb/pg_ctl/postgres/psql live under /usr/lib/postgresql/
59
- # <ver>/bin, not on PATH; symlink them into /usr/local/bin.
60
- && ln -s /usr/lib/postgresql/*/bin/* /usr/local/bin/ 2>/dev/null || true \
61
58
  # --- Drop the build-only packages (BY NAME, no autoremove) + caches ---
62
59
  && apt-get purge -y gnupg unzip \
63
60
  && rm -rf /tmp/aws /tmp/awscliv2.zip /var/lib/apt/lists/*
@@ -87,7 +84,7 @@ RUN curl -fsSL -o /tmp/litestream.tar.gz \
87
84
  # floating `opencode-ai@latest` install was implicated in unbounded runner
88
85
  # memory growth / OOM kills. Keep it in lockstep with the ECS runner image's.
89
86
  # EVIDENT_CLI_VERSION is DELIBERATELY the floating `dev` tag, matching the ECS
90
- # runner image (infrastructure/evident-runner/src/base-image.ts). This is a
87
+ # runner image. This is a
91
88
  # development environment: it should track the head of the CLI so changes are
92
89
  # testable without a version bump in every PR that touches a hook.
93
90
  # RUNNER_SYNCHRONISER_VERSION floats on `dev` for the identical reason.
@@ -104,27 +101,10 @@ RUN npm install -g \
104
101
  && evident --version \
105
102
  && npm cache clean --force
106
103
 
107
- # Playwright chromium + OS deps for E2E, baked at build time to avoid a slow
108
- # first-boot download.
109
- #
110
- # CRITICAL: this version MUST match the Playwright version the repo resolves for
111
- # `@playwright/test` (pnpm-lock.yaml → 1.58.0). The browser build is coupled to
112
- # the package version; if it drifts, a boot-time `playwright install`
113
- # re-downloads a different chromium. Bump in lockstep with @playwright/test.
114
- #
115
- # Browsers go to a SHARED PLAYWRIGHT_BROWSERS_PATH readable by the runtime
116
- # runner user (uid 10001), not root's ~/.cache.
117
- ENV PLAYWRIGHT_BROWSERS_PATH=/opt/ms-playwright
118
- RUN npx playwright@1.58.0 install --with-deps chromium \
119
- && chown -R 10001:10001 /opt/ms-playwright \
120
- # Assert the browsers landed in the shared path (guard against a silent failure).
121
- && test -n "$(ls -A /opt/ms-playwright 2>/dev/null)" \
122
- || { echo "FATAL: Playwright browsers dir /opt/ms-playwright missing or empty after install" >&2; exit 1; }
123
-
124
104
  # Assert every REQUIRED runtime binary survived every layer above (fail loud).
125
105
  # Placed after the last install so it covers the apt purge, the litestream
126
106
  # extraction and the global npm installs in one pass.
127
- RUN for bin in ps pgrep pkill curl aws gh git jq rg psql pg_ctl initdb direnv litestream runner-synchroniser opencode evident timeout; do \
107
+ RUN for bin in ps pgrep pkill curl aws gh git jq rg litestream runner-synchroniser opencode evident timeout; do \
128
108
  command -v "$bin" >/dev/null 2>&1 || { echo "FATAL: required binary '$bin' missing from image" >&2; exit 1; }; \
129
109
  done \
130
110
  && curl -fsS -o /dev/null https://cli.github.com \
@@ -150,8 +130,8 @@ COPY hook-server.js /usr/local/lib/evident/hook-server.js
150
130
  COPY hooks /etc/evident/hooks
151
131
 
152
132
  # Workspace: the repository the agent works on, baked in. A resumed VM must be
153
- # useful in milliseconds, and a clone + install costs minutes — so both happen
154
- # here, and the image is deliberately large in exchange.
133
+ # useful in milliseconds, while a clone costs minutes — so the checkout happens
134
+ # here; dependency installation and build warm-up belong to the deployment overlay.
155
135
  #
156
136
  # This does not weaken the boot split: source, git history and node_modules are
157
137
  # neither secret nor per-VM-unique, so sharing them across every VM from this
@@ -162,8 +142,6 @@ COPY hooks /etc/evident/hooks
162
142
  # per-VM, so the agent is what brings this checkout up to date.
163
143
  ENV WORKSPACE=/workspace
164
144
  ENV HOME=/home/runner
165
- # Runner-owned PGDATA parent for the local dev Postgres cluster (initdb +
166
- # pg_ctl at boot, no sudo).
167
145
  #
168
146
  # The machine-id files are created EMPTY and runner-owned so that /run can
169
147
  # actually rewrite them: the hooks run as uid 10001, and neither `/etc` nor
@@ -171,10 +149,18 @@ ENV HOME=/home/runner
171
149
  # permanent no-op and every VM from this snapshot shares one machine id. Empty
172
150
  # is the correct unset state — it is the absence of an identity, so nothing
173
151
  # per-VM-unique enters the shared snapshot.
174
- RUN mkdir -p /var/lib/runner-pg /var/lib/dbus /home/runner/.local/state/evident \
152
+ RUN mkdir -p /var/lib/dbus /home/runner/.local/state/evident \
175
153
  && install -o runner -g runner -m 0644 /dev/null /etc/machine-id \
176
154
  && install -o runner -g runner -m 0644 /dev/null /var/lib/dbus/machine-id \
177
- && chown -R runner:runner /var/lib/runner-pg /home/runner
155
+ && chown -R runner:runner /home/runner
156
+
157
+ # Deployment additions are always present because staging writes no-op defaults
158
+ # when no overlay is supplied.
159
+ COPY overlay /etc/evident/overlay
160
+ # HOME is already /home/runner, so root-warmed npx/npm state must be runner-owned
161
+ # or the runner's first npm/pnpm call would fail with EACCES.
162
+ RUN /etc/evident/overlay/setup-root \
163
+ && chown -R runner:runner /home/runner
178
164
  COPY --chown=10001:10001 repo ${WORKSPACE}
179
165
  WORKDIR ${WORKSPACE}
180
166
  USER runner
@@ -185,36 +171,13 @@ USER runner
185
171
  # `runner` (not root) so the rebuilt index is runner-owned and writable later.
186
172
  RUN git reset --quiet
187
173
 
188
- # Dependencies and the shared builds everything else depends on. As the runner
189
- # user, so node_modules is writable by the agent's own later installs.
190
- #
191
- # ONE layer on purpose: pnpm hard-links node_modules into its content-addressed
192
- # store (which it puts at ${WORKSPACE}/.pnpm-store, gitignored), and hard links
193
- # only survive within a single layer — split in two and the image carries both
194
- # copies. `--frozen-lockfile` because the committed lockfile is the whole reason
195
- # this is bakeable at all: installing against anything else would bake a
196
- # dependency set nobody committed.
197
- #
198
- # Guarded on a committed `pnpm-lock.yaml` so `microvm:repositoryPath` can point
199
- # at a repository that is not a pnpm workspace at all: it is baked uninstalled
200
- # instead of failing the build, and the agent installs on first use.
174
+ # Deployment-specific dependency installation and build work belongs here. This runs
175
+ # as `runner` in `${WORKSPACE}`, so everything it writes is agent-writable.
201
176
  #
202
- # Nothing environment-specific is built: `vite build` inlines VITE_* at build
203
- # time and the migrations need a running Postgres, so the app builds and the dev
204
- # database stay per-VM (see entrypoint.sh's pre-warm in evident-runner).
205
- RUN if [ -f pnpm-lock.yaml ]; then \
206
- pnpm install --frozen-lockfile \
207
- && pnpm run build --filter='./packages/*' \
208
- --filter='./aws/runner-cdk' \
209
- --filter='./aws/lambda-microvm-cdk' \
210
- --filter='./aws/lambda-microvm-runtime'; \
211
- else \
212
- echo "[workspace-prep] no pnpm-lock.yaml in the baked repository — skipping the dependency install and the workspace build; the agent installs on first use."; \
213
- fi
177
+ # Keep the overlay's install and build in one layer: pnpm hard-links `node_modules`
178
+ # into its content-addressed store, and hard links only survive within a single layer.
179
+ RUN /etc/evident/overlay/setup-workspace
214
180
 
215
- # Data dir for the local dev Postgres cluster; listens on 5433 to match this
216
- # repo's .envrc DSN postgres://postgres:postgres@localhost:5433/evident.
217
- ENV PGDATA=/var/lib/runner-pg/data
218
181
  # Port AWS calls the MicroVM hooks on.
219
182
  ENV HOOKS_PORT=8080
220
183
  # OpenCode loopback port the CLI tunnels to (matches `evident run` default).