@evident-ai/runner-cdk 0.1.1-dev.da88f8e → 3.4.1-dev.0ef5061

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Parses the `MICROVM_SHAPES` environment variable (a JSON array, written by
3
- * CloudFormation from `infrastructure/evident-microvm/src/shapes.ts` at
3
+ * CloudFormation from `aws/runner-cdk/src/microvm/shapes.ts` at
4
4
  * deploy time — see D6/D7 in the plan) into a `ShapeCatalogue` the pure
5
5
  * decision core can query, without that core ever touching `process.env`
6
6
  * itself.
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  /**
3
3
  * Parses the `MICROVM_SHAPES` environment variable (a JSON array, written by
4
- * CloudFormation from `infrastructure/evident-microvm/src/shapes.ts` at
4
+ * CloudFormation from `aws/runner-cdk/src/microvm/shapes.ts` at
5
5
  * deploy time — see D6/D7 in the plan) into a `ShapeCatalogue` the pure
6
6
  * decision core can query, without that core ever touching `process.env`
7
7
  * itself.
@@ -0,0 +1,35 @@
1
+ import { Construct } from 'constructs';
2
+ import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
3
+ import type { MicrovmImageBuildInputs } from '@evident-ai/lambda-microvm-cdk';
4
+ export type ImageVersionReporterProps = {
5
+ /** The secret whose value is the same HMAC key the controller verifies every doorbell against. */
6
+ doorbellSecret: secretsmanager.ISecret;
7
+ /** Base URL of Evident's own API — non-secret. */
8
+ evidentApiUrl: string;
9
+ /** The pool runner id the report is filed against — non-secret, public UUID. */
10
+ evidentRunnerId: string;
11
+ /** Shape name -> the `Fn::GetAtt LatestActiveImageVersion` published this deploy. */
12
+ imageVersions: Record<string, string>;
13
+ /**
14
+ * Shape name -> {@link MicrovmImageBuildInputs}, a STRUCTURED object (never
15
+ * a delimiter-joined string — see its docstring for why flattening is
16
+ * unsafe) that changes whenever THIS deploy's build inputs change. Unlike
17
+ * `imageVersions`' `Fn::GetAtt` (a runtime attribute of the MicroVM image
18
+ * service, whose deploy-time propagation to a dependent resource this
19
+ * construct cannot itself guarantee), every field here is either a
20
+ * synth-time-computed literal or a plain `Ref`/`Fn::GetAtt` to a resource
21
+ * IN THIS STACK — CloudFormation diffs the full resolved property tree, so
22
+ * a change anywhere in this object is a certain, not merely likely,
23
+ * re-invocation trigger.
24
+ */
25
+ buildTriggers: Record<string, MicrovmImageBuildInputs>;
26
+ };
27
+ /**
28
+ * Deploy-time push: reports each shape's just-published MicroVM image version to
29
+ * Evident, once per deploy that actually changes it. `imageVersions` is the
30
+ * payload; `buildTriggers` is what guarantees re-invocation on exactly the
31
+ * deploy that matters, independent of `imageVersions`' own resolution timing.
32
+ */
33
+ export declare class ImageVersionReporter extends Construct {
34
+ constructor(scope: Construct, id: string, props: ImageVersionReporterProps);
35
+ }
@@ -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;
@@ -37,7 +37,7 @@ RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
37
37
  # binary assertion further down fails the build if any required binary is gone.
38
38
  RUN apt-get update \
39
39
  && apt-get install -y --no-install-recommends \
40
- ca-certificates curl git jq less procps ripgrep \
40
+ ca-certificates curl git jq less procps psmisc ripgrep \
41
41
  postgresql postgresql-contrib direnv \
42
42
  gnupg unzip \
43
43
  # --- GitHub CLI (`gh`) from the official apt repo ---
@@ -71,8 +71,8 @@ RUN apt-get update \
71
71
  # restore` attempt — then starts a backgrounded `litestream replicate` once
72
72
  # opencode has opened the DB (WI-3); `/suspend`/`/resume`/`/terminate` flush,
73
73
  # restart and stop it in turn (WI-4). Do not remove as dead code.
74
- ARG LITESTREAM_VERSION=0.5.13
75
- ARG LITESTREAM_SHA256=ef47997794ce8dd87a64b44622d556b3a693b135fd72e0cf47cc42ac2e979051
74
+ ARG LITESTREAM_VERSION=0.5.16
75
+ ARG LITESTREAM_SHA256=678022e4103145302598e35d37f8718392d42e153feeb1e2d4a64dd0cd3aaf10
76
76
  RUN curl -fsSL -o /tmp/litestream.tar.gz \
77
77
  "https://github.com/benbjohnson/litestream/releases/download/v${LITESTREAM_VERSION}/litestream-${LITESTREAM_VERSION}-linux-arm64.tar.gz" \
78
78
  && echo "${LITESTREAM_SHA256} /tmp/litestream.tar.gz" | sha256sum -c - \
@@ -171,10 +171,10 @@ ENV HOME=/home/runner
171
171
  # permanent no-op and every VM from this snapshot shares one machine id. Empty
172
172
  # is the correct unset state — it is the absence of an identity, so nothing
173
173
  # per-VM-unique enters the shared snapshot.
174
- RUN mkdir -p /var/lib/runner-pg /var/lib/dbus \
174
+ RUN mkdir -p /var/lib/runner-pg /var/lib/dbus /home/runner/.local/state/evident \
175
175
  && install -o runner -g runner -m 0644 /dev/null /etc/machine-id \
176
176
  && 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
177
+ && chown -R runner:runner /var/lib/runner-pg /home/runner
178
178
  COPY --chown=10001:10001 repo ${WORKSPACE}
179
179
  WORKDIR ${WORKSPACE}
180
180
  USER runner
@@ -204,9 +204,12 @@ RUN git reset --quiet
204
204
  # database stay per-VM (see entrypoint.sh's pre-warm in evident-runner).
205
205
  RUN if [ -f pnpm-lock.yaml ]; then \
206
206
  pnpm install --frozen-lockfile \
207
- && pnpm run build --filter='./packages/*'; \
207
+ && pnpm run build --filter='./packages/*' \
208
+ --filter='./aws/runner-cdk' \
209
+ --filter='./aws/lambda-microvm-cdk' \
210
+ --filter='./aws/lambda-microvm-runtime'; \
208
211
  else \
209
- echo "[workspace-prep] no pnpm-lock.yaml in the baked repository — skipping the dependency install and the packages build; the agent installs on first use."; \
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."; \
210
213
  fi
211
214
 
212
215
  # Data dir for the local dev Postgres cluster; listens on 5433 to match this
@@ -275,14 +275,10 @@ var require_dist = __commonJS({
275
275
  }
276
276
  });
277
277
 
278
- // src/microvm/image/hook-server.ts
278
+ // ../../runner/docker-images/microvm/hook-server.ts
279
279
  var import_lambda_microvm_runtime = __toESM(require_dist());
280
-
281
- // src/microvm/constants.ts
282
- var HOOKS_PORT = 8080;
283
280
  var HOOKS_DIR = "/etc/evident/hooks";
284
-
285
- // src/microvm/image/hook-server.ts
281
+ var HOOKS_PORT = 8080;
286
282
  var server = (0, import_lambda_microvm_runtime.createNodeRuntime)({ hooksDir: HOOKS_DIR });
287
283
  var port = Number(process.env.HOOKS_PORT ?? HOOKS_PORT);
288
284
  server.listen(port, "0.0.0.0", () => {