@evident-ai/runner-cdk 3.5.2-dev.7130f36 → 3.5.2-dev.81383cc

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.
Files changed (33) hide show
  1. package/README.md +89 -39
  2. package/dist/controller-lambda/handler.js +13 -13
  3. package/dist/image-version-pruner-lambda/handler.js +28250 -0
  4. package/dist/image-version-reporter-lambda/handler.js +15 -8
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.js +3 -2
  7. package/dist/microvm/constants.d.ts +1 -1
  8. package/dist/microvm/constants.js +7 -4
  9. package/dist/microvm/construct.d.ts +12 -8
  10. package/dist/microvm/construct.js +33 -26
  11. package/dist/microvm/controller/handle-doorbell.d.ts +2 -2
  12. package/dist/microvm/controller/handle-doorbell.js +2 -2
  13. package/dist/microvm/image/stage-context.d.ts +13 -38
  14. package/dist/microvm/image/stage-context.js +30 -120
  15. package/dist/microvm/image-version-pruner/construct.d.ts +13 -0
  16. package/dist/microvm/image-version-pruner/construct.js +79 -0
  17. package/dist/microvm/image-version-pruner/handler.d.ts +20 -0
  18. package/dist/microvm/image-version-pruner/handler.js +116 -0
  19. package/dist/microvm/image-version-reporter/construct.d.ts +1 -1
  20. package/dist/microvm/image-version-reporter/construct.js +3 -3
  21. package/dist/microvm/image-version-reporter/handler.d.ts +2 -2
  22. package/dist/microvm/image-version-reporter/handler.js +9 -7
  23. package/dist/microvm/shapes.d.ts +10 -21
  24. package/dist/microvm/shapes.js +10 -17
  25. package/package.json +3 -3
  26. package/dist/microvm-image-context/Dockerfile +0 -190
  27. package/dist/microvm-image-context/hook-server.js +0 -286
  28. package/dist/microvm-image-context/hooks/common.sh +0 -958
  29. package/dist/microvm-image-context/hooks/resume +0 -81
  30. package/dist/microvm-image-context/hooks/run +0 -95
  31. package/dist/microvm-image-context/hooks/session-db-maintenance.mjs +0 -69
  32. package/dist/microvm-image-context/hooks/suspend +0 -21
  33. package/dist/microvm-image-context/hooks/terminate +0 -38
@@ -0,0 +1,79 @@
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.ImageVersionPruner = void 0;
37
+ const path = __importStar(require("path"));
38
+ const cdk = __importStar(require("aws-cdk-lib"));
39
+ const iam = __importStar(require("aws-cdk-lib/aws-iam"));
40
+ const lambda = __importStar(require("aws-cdk-lib/aws-lambda"));
41
+ const cr = __importStar(require("aws-cdk-lib/custom-resources"));
42
+ const constructs_1 = require("constructs");
43
+ class ImageVersionPruner extends constructs_1.Construct {
44
+ constructor(scope, id, props) {
45
+ super(scope, id);
46
+ const handler = new lambda.Function(this, 'Function', {
47
+ runtime: lambda.Runtime.NODEJS_22_X,
48
+ code: lambda.Code.fromAsset(path.join(__dirname, '..', '..', '..', 'dist', 'image-version-pruner-lambda')),
49
+ handler: 'handler.handler',
50
+ memorySize: 256,
51
+ timeout: cdk.Duration.minutes(5),
52
+ });
53
+ handler.addToRolePolicy(new iam.PolicyStatement({
54
+ actions: [
55
+ 'lambda:GetMicrovmImage',
56
+ 'lambda:ListMicrovmImageVersions',
57
+ 'lambda:UpdateMicrovmImageVersion',
58
+ 'lambda:DeleteMicrovmImageVersion',
59
+ ],
60
+ resources: [...props.imageArns],
61
+ }));
62
+ handler.addToRolePolicy(new iam.PolicyStatement({
63
+ // Evidence-only read: this account-wide permission cannot authorize a mutation.
64
+ actions: ['lambda:ListMicrovms'],
65
+ resources: ['*'],
66
+ }));
67
+ const provider = new cr.Provider(this, 'Provider', {
68
+ onEventHandler: handler,
69
+ });
70
+ new cdk.CustomResource(this, 'Resource', {
71
+ serviceToken: provider.serviceToken,
72
+ properties: {
73
+ imageArns: props.imageArns,
74
+ buildTriggers: props.buildTriggers,
75
+ },
76
+ });
77
+ }
78
+ }
79
+ exports.ImageVersionPruner = ImageVersionPruner;
@@ -0,0 +1,20 @@
1
+ import { LambdaMicrovmsClient } from '@aws-sdk/client-lambda-microvms';
2
+ import { type PruneImageVersionsOptions, type PruneSummary } from '@evident-ai/lambda-microvm-cdk/prune';
3
+ export declare const PRUNE_DEADLINE_MS: number;
4
+ export type ImageVersionPrunerEvent = {
5
+ RequestType: 'Create' | 'Update' | 'Delete';
6
+ ResourceProperties?: {
7
+ imageArns?: unknown;
8
+ };
9
+ };
10
+ export type ImageVersionPrunerDependencies = {
11
+ pruneImageVersions: (options: PruneImageVersionsOptions) => Promise<PruneSummary>;
12
+ createClient: () => LambdaMicrovmsClient;
13
+ now: () => number;
14
+ };
15
+ type ImageVersionPrunerResponse = {
16
+ PhysicalResourceId: string;
17
+ };
18
+ export declare function handleImageVersionPrune(event: ImageVersionPrunerEvent, dependencies?: ImageVersionPrunerDependencies): Promise<ImageVersionPrunerResponse>;
19
+ export declare const handler: (event: ImageVersionPrunerEvent) => Promise<ImageVersionPrunerResponse>;
20
+ export {};
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handler = exports.PRUNE_DEADLINE_MS = void 0;
4
+ exports.handleImageVersionPrune = handleImageVersionPrune;
5
+ const client_lambda_microvms_1 = require("@aws-sdk/client-lambda-microvms");
6
+ const prune_1 = require("@evident-ai/lambda-microvm-cdk/prune");
7
+ const PHYSICAL_RESOURCE_ID = 'evident-image-version-pruner';
8
+ exports.PRUNE_DEADLINE_MS = 4 * 60 * 1_000;
9
+ const runtimeDependencies = {
10
+ pruneImageVersions: prune_1.pruneImageVersions,
11
+ createClient: () => new client_lambda_microvms_1.LambdaMicrovmsClient({}),
12
+ now: Date.now,
13
+ };
14
+ function response() {
15
+ return { PhysicalResourceId: PHYSICAL_RESOURCE_ID };
16
+ }
17
+ function log(message) {
18
+ console.log(`[image-version-pruner] ${message}`);
19
+ }
20
+ function logError(message) {
21
+ console.error(`[image-version-pruner] ${message}`);
22
+ }
23
+ function readImageArns(value) {
24
+ if (!Array.isArray(value) || !value.every((imageArn) => typeof imageArn === 'string')) {
25
+ return undefined;
26
+ }
27
+ return value;
28
+ }
29
+ async function raceWithDeadline(operation, controller) {
30
+ let timer;
31
+ const deadline = new Promise((resolve) => {
32
+ timer = setTimeout(() => resolve({ timedOut: true }), exports.PRUNE_DEADLINE_MS);
33
+ });
34
+ try {
35
+ const result = await Promise.race([
36
+ operation.then((summary) => ({ timedOut: false, summary })),
37
+ deadline,
38
+ ]);
39
+ if (result.timedOut) {
40
+ controller.abort();
41
+ try {
42
+ await operation;
43
+ }
44
+ catch (error) {
45
+ console.warn('MicroVM image version pruning cancellation observed', {
46
+ error: error instanceof Error ? error.message : String(error),
47
+ });
48
+ }
49
+ }
50
+ return result;
51
+ }
52
+ finally {
53
+ if (timer !== undefined) {
54
+ clearTimeout(timer);
55
+ }
56
+ }
57
+ }
58
+ async function handleImageVersionPrune(event, dependencies = runtimeDependencies) {
59
+ if (event.RequestType === 'Delete') {
60
+ return response();
61
+ }
62
+ const imageArns = readImageArns(event.ResourceProperties?.imageArns);
63
+ if (imageArns === undefined) {
64
+ console.warn('MicroVM image version pruning skipped', {
65
+ operation: event.RequestType,
66
+ reason: 'imageArns must be an array of strings',
67
+ });
68
+ return response();
69
+ }
70
+ if (imageArns.length === 0) {
71
+ console.warn('MicroVM image version pruning skipped', {
72
+ operation: event.RequestType,
73
+ reason: 'imageArns is empty',
74
+ });
75
+ return response();
76
+ }
77
+ try {
78
+ const startedAt = dependencies.now();
79
+ const controller = new AbortController();
80
+ const operation = dependencies.pruneImageVersions({
81
+ client: dependencies.createClient(),
82
+ imageArns,
83
+ abortSignal: controller.signal,
84
+ log,
85
+ logError,
86
+ });
87
+ const result = await raceWithDeadline(operation, controller);
88
+ if (result.timedOut) {
89
+ console.warn('MicroVM image version pruning deadline reached', {
90
+ operation: event.RequestType,
91
+ imageArns,
92
+ deadlineMs: exports.PRUNE_DEADLINE_MS,
93
+ elapsedMs: dependencies.now() - startedAt,
94
+ });
95
+ return response();
96
+ }
97
+ console.log('MicroVM image version pruning complete', {
98
+ operation: event.RequestType,
99
+ imageArns,
100
+ deactivated: result.summary.deactivated.length,
101
+ deleted: result.summary.deleted.length,
102
+ skipped: result.summary.skipped.length,
103
+ stoppedOnQuota: result.summary.stoppedOnQuota,
104
+ });
105
+ }
106
+ catch (error) {
107
+ console.warn('MicroVM image version pruning failed', {
108
+ operation: event.RequestType,
109
+ imageArns,
110
+ error: error instanceof Error ? error.message : String(error),
111
+ });
112
+ }
113
+ return response();
114
+ }
115
+ const handler = (event) => handleImageVersionPrune(event);
116
+ exports.handler = handler;
@@ -3,7 +3,7 @@ import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
3
3
  import type { MicrovmImageBuildInputs } from '@evident-ai/lambda-microvm-cdk';
4
4
  export type ImageVersionReporterProps = {
5
5
  /** The secret whose value is the same HMAC key the controller verifies every doorbell against. */
6
- doorbellSecret: secretsmanager.ISecret;
6
+ controllerSigningSecret: secretsmanager.ISecret;
7
7
  /** Base URL of Evident's own API — non-secret. */
8
8
  evidentApiUrl: string;
9
9
  /** The pool runner id the report is filed against — non-secret, public UUID. */
@@ -64,13 +64,13 @@ class ImageVersionReporter extends constructs_1.Construct {
64
64
  timeout: cdk.Duration.seconds(30),
65
65
  environment: {
66
66
  // ARN + field name only, never the value (mirrors the controller and waker).
67
- DOORBELL_SECRET_ARN: props.doorbellSecret.secretArn,
68
- DOORBELL_SECRET_KEY: constants_1.DOORBELL_SECRET_KEY,
67
+ CONTROLLER_SIGNING_SECRET_ARN: props.controllerSigningSecret.secretArn,
68
+ CONTROLLER_SIGNING_SECRET_KEY: constants_1.CONTROLLER_SIGNING_SECRET_KEY,
69
69
  EVIDENT_API_URL: props.evidentApiUrl,
70
70
  EVIDENT_RUNNER_ID: props.evidentRunnerId,
71
71
  },
72
72
  });
73
- props.doorbellSecret.grantRead(handler);
73
+ props.controllerSigningSecret.grantRead(handler);
74
74
  const provider = new cr.Provider(this, 'Provider', {
75
75
  onEventHandler: handler,
76
76
  });
@@ -9,8 +9,8 @@ export type ReporterEvent = {
9
9
  };
10
10
  };
11
11
  export type ReporterConfig = {
12
- doorbellSecretArn: string;
13
- doorbellSecretKey: string;
12
+ controllerSigningSecretArn: string;
13
+ controllerSigningSecretKey: string;
14
14
  evidentApiUrl: string;
15
15
  evidentRunnerId: string;
16
16
  };
@@ -7,17 +7,17 @@ const client_secrets_manager_1 = require("@aws-sdk/client-secrets-manager");
7
7
  const PHYSICAL_RESOURCE_ID = 'evident-image-version-report';
8
8
  const REPORT_TIMEOUT_MS = 5_000;
9
9
  const secrets = new client_secrets_manager_1.SecretsManagerClient({});
10
- async function fetchDoorbellSecret(secretArn, secretKey) {
10
+ async function fetchControllerSigningSecret(secretArn, secretKey) {
11
11
  const { SecretString } = await secrets.send(new client_secrets_manager_1.GetSecretValueCommand({ SecretId: secretArn }));
12
12
  if (!SecretString) {
13
- throw new Error(`doorbell secret ${secretArn} has no SecretString`);
13
+ throw new Error(`controller signing secret ${secretArn} has no SecretString`);
14
14
  }
15
15
  const parsed = JSON.parse(SecretString);
16
16
  const value = parsed !== null && typeof parsed === 'object'
17
17
  ? parsed[secretKey]
18
18
  : undefined;
19
19
  if (typeof value !== 'string' || value === '') {
20
- throw new Error(`doorbell secret ${secretArn} is missing a non-empty '${secretKey}' field`);
20
+ throw new Error(`controller signing secret ${secretArn} is missing a non-empty '${secretKey}' field`);
21
21
  }
22
22
  return value;
23
23
  }
@@ -43,7 +43,7 @@ function response() {
43
43
  return { PhysicalResourceId: PHYSICAL_RESOURCE_ID };
44
44
  }
45
45
  const runtimeDependencies = {
46
- getSecret: fetchDoorbellSecret,
46
+ getSecret: fetchControllerSigningSecret,
47
47
  fetch: globalThis.fetch,
48
48
  };
49
49
  async function handleImageVersionReport(event, getConfig, dependencies = runtimeDependencies) {
@@ -57,10 +57,12 @@ async function handleImageVersionReport(event, getConfig, dependencies = runtime
57
57
  versions = readImageVersions(event.ResourceProperties?.imageVersions);
58
58
  const config = getConfig();
59
59
  runnerId = config.evidentRunnerId;
60
- const secret = await dependencies.getSecret(config.doorbellSecretArn, config.doorbellSecretKey);
60
+ const secret = await dependencies.getSecret(config.controllerSigningSecretArn, config.controllerSigningSecretKey);
61
61
  const body = JSON.stringify({
62
62
  type: 'runner.microvm_image_versions_reported',
63
63
  versions,
64
+ // The API orders reports by this value because replacing an image restarts its version counter.
65
+ reporter_clock_ms: Date.now(),
64
66
  });
65
67
  const signature = (0, node_crypto_1.createHmac)('sha256', secret).update(body).digest('hex');
66
68
  const url = `${config.evidentApiUrl.replace(/\/+$/, '')}/v1/runners/${encodeURIComponent(runnerId)}/microvm-image-versions`;
@@ -96,8 +98,8 @@ async function handleImageVersionReport(event, getConfig, dependencies = runtime
96
98
  return response();
97
99
  }
98
100
  const handler = (event) => handleImageVersionReport(event, () => ({
99
- doorbellSecretArn: requireEnv('DOORBELL_SECRET_ARN'),
100
- doorbellSecretKey: requireEnv('DOORBELL_SECRET_KEY'),
101
+ controllerSigningSecretArn: requireEnv('CONTROLLER_SIGNING_SECRET_ARN'),
102
+ controllerSigningSecretKey: requireEnv('CONTROLLER_SIGNING_SECRET_KEY'),
101
103
  evidentApiUrl: requireEnv('EVIDENT_API_URL'),
102
104
  evidentRunnerId: requireEnv('EVIDENT_RUNNER_ID'),
103
105
  }));
@@ -5,11 +5,8 @@ import { type MicrovmMemoryMiB } from '@evident-ai/lambda-microvm-cdk';
5
5
  * there is no launch-time resource dial — so "a bigger machine" means "a
6
6
  * different shape", each backed by its own `MicrovmImage`.
7
7
  *
8
- * `constructId` and `imageName` are carried **explicitly** rather than
9
- * derived from `name`, because for the default shape they are frozen
10
- * deployment identity (see the comment on `MICROVM_SHAPES` below): deriving
11
- * them would let an innocent rename of `name` silently rebuild the live
12
- * image. Every other shape is free to name these however is convenient.
8
+ * `name` is also the `MicrovmImage` child id, so it identifies both the
9
+ * controller-facing shape and the image resource built for it.
13
10
  */
14
11
  export interface MicrovmShape {
15
12
  /** The stable id a doorbell asks for; crosses the wire to Evident. */
@@ -27,8 +24,6 @@ export interface MicrovmShape {
27
24
  * it must be the first entry in `MICROVM_SHAPES`.
28
25
  */
29
26
  readonly isDefault: boolean;
30
- /** The `MicrovmImage` construct id within the stack. */
31
- readonly constructId: string;
32
27
  /** The account-unique `imageName` passed to `MicrovmImage`. */
33
28
  readonly imageName: string;
34
29
  }
@@ -45,28 +40,22 @@ export interface MicrovmShape {
45
40
  * making the construction-order contract enforceable rather than a
46
41
  * convention.
47
42
  *
48
- * The default entry's `constructId` ("Image") and `imageName`
49
- * ("evident-microvm") are FROZEN: they are today's live, hand-deployed
50
- * image's identity. Changing either destroys and rebuilds it (~4m45s,
51
- * measured) plus a window with no image. Its `description` is copied
52
- * verbatim from the image that is live today so the deploy changes nothing
53
- * about it.
43
+ * The default entry's `name` ("standard") and `imageName`
44
+ * ("evident-microvm-standard") are FROZEN: `name` crosses the wire and
45
+ * identifies the image child, while `imageName` is account-unique. Changing
46
+ * either replaces the image, so both values remain stable after deployment.
54
47
  *
55
48
  * "Frozen" means unchanged under OUR OWN synth (no `microvm:*` context set —
56
49
  * `main.ts` passes none). `imageName` MAY carry a customer's
57
50
  * `microvm:namePrefix` (`deployment.ts`'s `resolveShapeCatalogue`) — that is
58
- * account-scoped naming, not a change to this module. `constructId` stays
59
- * frozen unconditionally, with no override anywhere: it is the
60
- * CloudFormation logical-id path, and prefixing it is the resource
61
- * replacement this whole file's FROZEN warning exists to prevent.
51
+ * account-scoped naming, not a change to this module.
62
52
  */
63
53
  export declare const MICROVM_SHAPES: readonly MicrovmShape[];
64
54
  /**
65
55
  * Validates a shape list at synth time. Deliberately lean
66
56
  * (`code-simplicity.mdc`): this is a hand-edited constant list in a typed
67
- * file, so it only checks what `tsc` cannot already catch — CDK itself
68
- * rejects a duplicate `constructId`, so that one needs no check here, and no
69
- * byte-budget cap is applied (the image ARNs are unresolved tokens at synth,
70
- * so a rendered size is not measurable anyway).
57
+ * file, so it only checks what `tsc` cannot already catch. No byte-budget cap
58
+ * is applied because the image ARNs are unresolved tokens at synth, so a
59
+ * rendered size is not measurable anyway.
71
60
  */
72
61
  export declare function validateShapes(shapes: readonly MicrovmShape[]): readonly MicrovmShape[];
@@ -16,20 +16,15 @@ const lambda_microvm_cdk_1 = require("@evident-ai/lambda-microvm-cdk");
16
16
  * making the construction-order contract enforceable rather than a
17
17
  * convention.
18
18
  *
19
- * The default entry's `constructId` ("Image") and `imageName`
20
- * ("evident-microvm") are FROZEN: they are today's live, hand-deployed
21
- * image's identity. Changing either destroys and rebuilds it (~4m45s,
22
- * measured) plus a window with no image. Its `description` is copied
23
- * verbatim from the image that is live today so the deploy changes nothing
24
- * about it.
19
+ * The default entry's `name` ("standard") and `imageName`
20
+ * ("evident-microvm-standard") are FROZEN: `name` crosses the wire and
21
+ * identifies the image child, while `imageName` is account-unique. Changing
22
+ * either replaces the image, so both values remain stable after deployment.
25
23
  *
26
24
  * "Frozen" means unchanged under OUR OWN synth (no `microvm:*` context set —
27
25
  * `main.ts` passes none). `imageName` MAY carry a customer's
28
26
  * `microvm:namePrefix` (`deployment.ts`'s `resolveShapeCatalogue`) — that is
29
- * account-scoped naming, not a change to this module. `constructId` stays
30
- * frozen unconditionally, with no override anywhere: it is the
31
- * CloudFormation logical-id path, and prefixing it is the resource
32
- * replacement this whole file's FROZEN warning exists to prevent.
27
+ * account-scoped naming, not a change to this module.
33
28
  */
34
29
  exports.MICROVM_SHAPES = [
35
30
  {
@@ -38,18 +33,16 @@ exports.MICROVM_SHAPES = [
38
33
  description: 'Evident per-session runner: an arm64 image that boots with no identity and acquires one at /run.',
39
34
  memoryMiB: 4096,
40
35
  isDefault: true,
41
- // FROZEN — see the file-level comment. Do not rename either string.
42
- constructId: 'Image',
43
- imageName: 'evident-microvm',
36
+ // FROZEN — name crosses the wire and imageName is account-unique.
37
+ imageName: 'evident-microvm-standard',
44
38
  },
45
39
  ];
46
40
  /**
47
41
  * Validates a shape list at synth time. Deliberately lean
48
42
  * (`code-simplicity.mdc`): this is a hand-edited constant list in a typed
49
- * file, so it only checks what `tsc` cannot already catch — CDK itself
50
- * rejects a duplicate `constructId`, so that one needs no check here, and no
51
- * byte-budget cap is applied (the image ARNs are unresolved tokens at synth,
52
- * so a rendered size is not measurable anyway).
43
+ * file, so it only checks what `tsc` cannot already catch. No byte-budget cap
44
+ * is applied because the image ARNs are unresolved tokens at synth, so a
45
+ * rendered size is not measurable anyway.
53
46
  */
54
47
  function validateShapes(shapes) {
55
48
  if (shapes.length === 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evident-ai/runner-cdk",
3
- "version": "3.5.2-dev.7130f36",
3
+ "version": "3.5.2-dev.81383cc",
4
4
  "description": "Reusable CDK constructs for an Evident agent runner: a single scale-to-zero Fargate runner (task + service + per-agent self-stop role + waker Lambda), or a per-session AWS Lambda MicroVM that boots on demand and suspends between messages. Instantiate once per agent from your own stack.",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -15,8 +15,8 @@
15
15
  ],
16
16
  "scripts": {
17
17
  "build": "pnpm run build-bundled-deps && ts-node scripts/build.ts && node -e 'const m=require(\"./package.json\").main; if (!require(\"node:fs\").existsSync(m)) { console.error(`build produced no ${m}`); process.exit(1); }'",
18
- "//build-bundled-deps": "scripts/build.ts esbuild-bundles runner/docker-images/microvm/hook-server.ts, which imports @evident-ai/lambda-microvm-runtime by its built `main`. @evident-ai/lambda-microvm-cdk is a workspace:* devDependency whose types resolve through its own dist/, so tsc --project tsconfig.build.json cannot compile src/microvm/{shapes,construct}.ts without it. Chained into `build` rather than left to turbo's `^build` because callers outside turbo invoke `pnpm --filter @evident-ai/runner-cdk build` directly, which bypasses turbo entirely.",
19
- "build-bundled-deps": "pnpm --filter @evident-ai/lambda-microvm-cdk build && pnpm --filter @evident-ai/lambda-microvm-runtime build",
18
+ "//build-bundled-deps": "Builds the workspace dependencies whose dist/ output the direct runner-cdk build chain consumes: lambda-microvm-cdk for the public construct types, lambda-microvm-runtime for runner-microvm-image's hook-server bundle, and runner-microvm-image for that bundle itself. Chained into `build` rather than left to turbo's `^build` because callers outside turbo invoke `pnpm --filter @evident-ai/runner-cdk build` directly, which bypasses turbo entirely.",
19
+ "build-bundled-deps": "pnpm --filter @evident-ai/lambda-microvm-cdk build && pnpm --filter @evident-ai/lambda-microvm-runtime build && pnpm --filter runner-microvm-image build",
20
20
  "typecheck": "tsc --noEmit",
21
21
  "test": "node --test --require ts-node/register 'src/**/*.test.ts'",
22
22
  "format": "prettier --write 'src/**/*.ts'",
@@ -1,190 +0,0 @@
1
- # Generic MicroVM runner image
2
- #
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.
7
- #
8
- # THE BOOT SPLIT — the rule that governs everything below. AWS snapshots this
9
- # image ONCE at build and every MicroVM resumes from that same snapshot, so
10
- # anything unique or secret baked in here is shared by every VM launched from
11
- # this image version. Therefore this image must NOT start the tunnel
12
- # (`evident run`), clone the repo, or carry any credential, machine ID or
13
- # generated secret. Identity arrives per-VM in the `/run` hook payload.
14
- # `aws/runner-cdk/src/microvm/image/dockerfile.test.ts` fails the build if this
15
- # regresses.
16
- #
17
- # No `--platform` on FROM: AWS builds this natively on Graviton. Build it
18
- # locally with `docker build --platform linux/arm64`.
19
- #
20
- # Node major tracks `.nvmrc` / the root `engines.node` (22) so the image runs
21
- # the same runtime as CI — `evident-runner/src/node-version.test.ts` fails if
22
- # they drift. The Debian codename is pinned explicitly (`bookworm`, what
23
- # `node:22-slim` resolves to today) so a future default bump to a newer Debian
24
- # can't silently change the apt package set this image asserts on below.
25
- FROM node:22-bookworm-slim
26
-
27
- ENV PNPM_HOME="/pnpm"
28
- ENV PATH="$PNPM_HOME:$PATH"
29
- RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
30
-
31
- # System tooling + GitHub CLI + AWS CLI, in a single layer.
32
- # - awscli (v2): required — durable-state persistence uses `aws s3`.
33
- # - procps: required at RUNTIME for process inspection and debugging. Listed
34
- # explicitly so apt keeps it.
35
- # - gnupg, unzip: BUILD-ONLY (gh apt key / AWS CLI bundle); purged in this layer.
36
- #
37
- # Purge build-only packages BY NAME — do NOT use `apt-get autoremove`, which
38
- # once cascaded into shared libs and swept `procps` out of the runner image. The
39
- # binary assertion further down fails the build if any required binary is gone.
40
- RUN apt-get update \
41
- && apt-get install -y --no-install-recommends \
42
- ca-certificates curl git jq less procps psmisc ripgrep \
43
- gnupg unzip \
44
- # --- GitHub CLI (`gh`) from the official apt repo ---
45
- && mkdir -p -m 755 /etc/apt/keyrings \
46
- && curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
47
- -o /etc/apt/keyrings/githubcli-archive-keyring.gpg \
48
- && chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
49
- && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
50
- > /etc/apt/sources.list.d/github-cli.list \
51
- && apt-get update \
52
- && apt-get install -y --no-install-recommends gh \
53
- # --- AWS CLI v2 (aarch64 bundle; this image is arm64 only) ---
54
- && curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip" -o /tmp/awscliv2.zip \
55
- && unzip -q /tmp/awscliv2.zip -d /tmp \
56
- && /tmp/aws/install \
57
- && aws --version \
58
- # --- Drop the build-only packages (BY NAME, no autoremove) + caches ---
59
- && apt-get purge -y gnupg unzip \
60
- && rm -rf /tmp/aws /tmp/awscliv2.zip /var/lib/apt/lists/*
61
-
62
- # Litestream — replicates the OpenCode SQLite session DB to S3 so history
63
- # survives VM replacement. Pinned + checksum-verified: a wrong LITESTREAM_SHA256
64
- # fails the build at `sha256sum -c`. The checksum is for the arm64 tarball and
65
- # is NOT interchangeable with the ECS runner image's. Note the release tag keeps
66
- # the `v` but the filename does not.
67
- # `/run` restores opencode.db from S3 (#812 WI-2) — one bounded `litestream
68
- # restore` attempt — then starts a backgrounded `litestream replicate` once
69
- # opencode has opened the DB (WI-3); `/suspend`/`/resume`/`/terminate` flush,
70
- # restart and stop it in turn (WI-4). Do not remove as dead code.
71
- ARG LITESTREAM_VERSION=0.5.16
72
- ARG LITESTREAM_SHA256=678022e4103145302598e35d37f8718392d42e153feeb1e2d4a64dd0cd3aaf10
73
- RUN curl -fsSL -o /tmp/litestream.tar.gz \
74
- "https://github.com/benbjohnson/litestream/releases/download/v${LITESTREAM_VERSION}/litestream-${LITESTREAM_VERSION}-linux-arm64.tar.gz" \
75
- && echo "${LITESTREAM_SHA256} /tmp/litestream.tar.gz" | sha256sum -c - \
76
- && tar -C /usr/local/bin -xzf /tmp/litestream.tar.gz litestream \
77
- && chmod +x /usr/local/bin/litestream \
78
- && rm -f /tmp/litestream.tar.gz \
79
- && litestream version
80
-
81
- # The AI agent runtime, the Evident CLI and the Claude Code CLI, installed
82
- # globally and pinned via build args so image versions are reproducible.
83
- # OPENCODE_VERSION is a DELIBERATE PIN, not a floating tag — see #537: a
84
- # floating `opencode-ai@latest` install was implicated in unbounded runner
85
- # memory growth / OOM kills. Keep it in lockstep with the ECS runner image's.
86
- # EVIDENT_CLI_VERSION is DELIBERATELY the floating `dev` tag, matching the ECS
87
- # runner image. This is a
88
- # development environment: it should track the head of the CLI so changes are
89
- # testable without a version bump in every PR that touches a hook.
90
- # RUNNER_SYNCHRONISER_VERSION floats on `dev` for the identical reason.
91
- ARG OPENCODE_VERSION=1.18.3
92
- ARG EVIDENT_CLI_VERSION=dev
93
- ARG CLAUDE_CODE_VERSION=latest
94
- ARG RUNNER_SYNCHRONISER_VERSION=dev
95
- RUN npm install -g \
96
- "opencode-ai@${OPENCODE_VERSION}" \
97
- "@evident-ai/cli@${EVIDENT_CLI_VERSION}" \
98
- "@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" \
99
- "@evident-ai/runner-synchroniser@${RUNNER_SYNCHRONISER_VERSION}" \
100
- && opencode --version \
101
- && evident --version \
102
- && npm cache clean --force
103
-
104
- # Assert every REQUIRED runtime binary survived every layer above (fail loud).
105
- # Placed after the last install so it covers the apt purge, the litestream
106
- # extraction and the global npm installs in one pass.
107
- RUN for bin in ps pgrep pkill curl aws gh git jq rg litestream runner-synchroniser opencode evident timeout; do \
108
- command -v "$bin" >/dev/null 2>&1 || { echo "FATAL: required binary '$bin' missing from image" >&2; exit 1; }; \
109
- done \
110
- && curl -fsS -o /dev/null https://cli.github.com \
111
- || { echo "FATAL: curl TLS smoke test failed (broken TLS libs)" >&2; exit 1; }
112
-
113
- # Non-root user that owns the workspace. uid/gid pinned to 10001 for stable file
114
- # ownership (opencode.db, ~/.claude/.credentials.json) across rebuilds.
115
- RUN groupadd --system --gid 10001 runner \
116
- && useradd --system --uid 10001 --gid 10001 \
117
- --create-home --home-dir /home/runner runner
118
-
119
- # The hook server: the ONLY process this image starts, and it starts nothing
120
- # itself until AWS calls a hook. It is an esbuild bundle of
121
- # src/microvm/image/hook-server.ts, placed next to this Dockerfile by
122
- # `stageMicrovmImageContext()` — which is why this file is not buildable
123
- # straight from the source tree.
124
- COPY hook-server.js /usr/local/lib/evident/hook-server.js
125
-
126
- # One executable per lifecycle phase. There is deliberately no `ready` and no
127
- # `validate` script: the snapshot is shared by every VM started from this image
128
- # version, so nothing per-VM may run before /run — which leaves those two phases
129
- # with nothing to do, and the runtime answers them 200 without running anything.
130
- COPY hooks /etc/evident/hooks
131
-
132
- # Workspace: the repository the agent works on, baked in. A resumed VM must be
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.
135
- #
136
- # This does not weaken the boot split: source, git history and node_modules are
137
- # neither secret nor per-VM-unique, so sharing them across every VM from this
138
- # snapshot is exactly right. No credential is involved either — the repo travels
139
- # in the build context (cloned by `stageMicrovmImageContext()` from a local
140
- # checkout), not through a `git clone` needing a token. `origin` is the plain
141
- # HTTPS GitHub URL; the credential that makes it fetchable and pushable arrives
142
- # per-VM, so the agent is what brings this checkout up to date.
143
- ENV WORKSPACE=/workspace
144
- ENV HOME=/home/runner
145
- #
146
- # The machine-id files are created EMPTY and runner-owned so that /run can
147
- # actually rewrite them: the hooks run as uid 10001, and neither `/etc` nor
148
- # `/var/lib/dbus` is writable by it, so without this the regeneration is a
149
- # permanent no-op and every VM from this snapshot shares one machine id. Empty
150
- # is the correct unset state — it is the absence of an identity, so nothing
151
- # per-VM-unique enters the shared snapshot.
152
- RUN mkdir -p /var/lib/dbus /home/runner/.local/state/evident \
153
- && install -o runner -g runner -m 0644 /dev/null /etc/machine-id \
154
- && install -o runner -g runner -m 0644 /dev/null /var/lib/dbus/machine-id \
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
164
- COPY --chown=10001:10001 repo ${WORKSPACE}
165
- WORKDIR ${WORKSPACE}
166
- USER runner
167
-
168
- # The staged context deliberately ships no `.git/index` (it is not byte-stable,
169
- # so shipping it would re-roll the image version on every deploy) — without this
170
- # reset the agent's workspace reports ~1400 files as staged deletions. Runs as
171
- # `runner` (not root) so the rebuilt index is runner-owned and writable later.
172
- RUN git reset --quiet
173
-
174
- # Deployment-specific dependency installation and build work belongs here. This runs
175
- # as `runner` in `${WORKSPACE}`, so everything it writes is agent-writable.
176
- #
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
180
-
181
- # Port AWS calls the MicroVM hooks on.
182
- ENV HOOKS_PORT=8080
183
- # OpenCode loopback port the CLI tunnels to (matches `evident run` default).
184
- ENV OPENCODE_PORT=4096
185
-
186
- EXPOSE 8080
187
-
188
- # No ENTRYPOINT shell: the hook server is started directly, and it must be the
189
- # only thing running until AWS calls /run.
190
- CMD ["node", "/usr/local/lib/evident/hook-server.js"]