@evident-ai/runner-cdk 0.1.0 → 0.1.1-dev.d96ef8a

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,108 @@
1
+ "use strict";
2
+ /**
3
+ * Parses the `MICROVM_SHAPES` environment variable (a JSON array, written by
4
+ * CloudFormation from `infrastructure/evident-microvm/src/shapes.ts` at
5
+ * deploy time — see D6/D7 in the plan) into a `ShapeCatalogue` the pure
6
+ * decision core can query, without that core ever touching `process.env`
7
+ * itself.
8
+ *
9
+ * A malformed catalogue means a broken deploy: `parseShapeCatalogue` throws
10
+ * once, loudly, at cold start rather than handing back a half-parsed
11
+ * catalogue that degrades per-request.
12
+ *
13
+ * `resolve()` never falls back to the default shape for a *named* request —
14
+ * only the absence of a name (`undefined`) means "give me the default". An
15
+ * unrecognised name returns `undefined` so the caller can reject loudly
16
+ * (AC-4) instead of silently launching the default shape a doorbell did not
17
+ * ask for.
18
+ */
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.parseShapeCatalogue = parseShapeCatalogue;
21
+ function nonBlankString(value) {
22
+ return typeof value === 'string' && value.trim().length > 0;
23
+ }
24
+ function parseShapeEntry(entry, index) {
25
+ if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
26
+ throw new Error(`MICROVM_SHAPES[${index}] is not an object.`);
27
+ }
28
+ const { name, title, description, memory_mib: memoryMiB, image_arn: imageArn, is_default: isDefault, } = entry;
29
+ if (!nonBlankString(name)) {
30
+ throw new Error(`MICROVM_SHAPES[${index}] is missing a non-blank "name".`);
31
+ }
32
+ if (!nonBlankString(title)) {
33
+ throw new Error(`Shape "${name}" is missing a non-blank "title".`);
34
+ }
35
+ if (!nonBlankString(description)) {
36
+ throw new Error(`Shape "${name}" is missing a non-blank "description".`);
37
+ }
38
+ if (typeof memoryMiB !== 'number' || !Number.isFinite(memoryMiB)) {
39
+ throw new Error(`Shape "${name}" has a non-number "memory_mib".`);
40
+ }
41
+ if (!nonBlankString(imageArn)) {
42
+ throw new Error(`Shape "${name}" is missing a non-blank "image_arn".`);
43
+ }
44
+ if (typeof isDefault !== 'boolean') {
45
+ throw new Error(`Shape "${name}" has a non-boolean "is_default".`);
46
+ }
47
+ return { name, title, description, memoryMiB, imageArn, isDefault };
48
+ }
49
+ /**
50
+ * Parses and validates the `MICROVM_SHAPES` catalogue. Mirrors
51
+ * `validateShapes`'s (`src/shapes.ts`) message vocabulary so an operator
52
+ * reading a controller cold-start failure recognises it as the same rule
53
+ * they already met at synth time.
54
+ *
55
+ * Throws on: invalid JSON, a non-array, an empty array, an entry missing a
56
+ * non-blank `name`/`title`/`description`/`image_arn`, a non-number
57
+ * `memory_mib`, a non-boolean `is_default`, a duplicate `name`, or
58
+ * zero/multiple `is_default: true` entries.
59
+ */
60
+ function parseShapeCatalogue(raw) {
61
+ let parsed;
62
+ try {
63
+ parsed = JSON.parse(raw);
64
+ }
65
+ catch {
66
+ throw new Error('MICROVM_SHAPES is not valid JSON.');
67
+ }
68
+ if (!Array.isArray(parsed)) {
69
+ throw new Error('MICROVM_SHAPES must be a JSON array.');
70
+ }
71
+ if (parsed.length === 0) {
72
+ throw new Error('MICROVM_SHAPES must not be empty.');
73
+ }
74
+ const shapes = parsed.map((entry, index) => parseShapeEntry(entry, index));
75
+ const seenNames = new Set();
76
+ for (const shape of shapes) {
77
+ if (seenNames.has(shape.name)) {
78
+ throw new Error(`Duplicate MicroVM shape name "${shape.name}" in MICROVM_SHAPES — shape names must be unique.`);
79
+ }
80
+ seenNames.add(shape.name);
81
+ }
82
+ const defaults = shapes.filter((shape) => shape.isDefault);
83
+ if (defaults.length === 0) {
84
+ throw new Error('MICROVM_SHAPES must mark exactly one shape is_default: true — none does.');
85
+ }
86
+ if (defaults.length > 1) {
87
+ throw new Error('MICROVM_SHAPES must mark exactly one shape is_default: true — ' +
88
+ `found ${defaults.length}: ${defaults.map((shape) => shape.name).join(', ')}.`);
89
+ }
90
+ const defaultShape = defaults[0];
91
+ return {
92
+ resolve(name) {
93
+ if (name === undefined) {
94
+ return defaultShape;
95
+ }
96
+ return shapes.find((shape) => shape.name === name);
97
+ },
98
+ advertise() {
99
+ return shapes.map((shape) => ({
100
+ name: shape.name,
101
+ title: shape.title,
102
+ description: shape.description,
103
+ memory_mib: shape.memoryMiB,
104
+ is_default: shape.isDefault,
105
+ }));
106
+ },
107
+ };
108
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Retry for the mutating MicroVM operations. `RunMicrovm`/`ResumeMicrovm` are 5/s
3
+ * and `SuspendMicrovm` is 2/s burst 2, so a doorbell burst throttles in normal
4
+ * operation. Full jitter over `[300, 900] ms` — worst case 1.2 s, which has to
5
+ * coexist with the `SUSPENDING` poll (≤ 2 s) inside the Lambda's 8 s timeout.
6
+ */
7
+ export type Timing = {
8
+ sleep: (ms: number) => Promise<void>;
9
+ random: () => number;
10
+ };
11
+ export declare function withThrottleRetry<T>(operation: () => Promise<T>, { sleep, random }: Timing): Promise<T>;
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ /**
3
+ * Retry for the mutating MicroVM operations. `RunMicrovm`/`ResumeMicrovm` are 5/s
4
+ * and `SuspendMicrovm` is 2/s burst 2, so a doorbell burst throttles in normal
5
+ * operation. Full jitter over `[300, 900] ms` — worst case 1.2 s, which has to
6
+ * coexist with the `SUSPENDING` poll (≤ 2 s) inside the Lambda's 8 s timeout.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.withThrottleRetry = withThrottleRetry;
10
+ const BACKOFF_MS = [300, 900];
11
+ function isThrottling(error) {
12
+ const name = error?.name;
13
+ return name === 'ThrottlingException' || name === 'TooManyRequestsException';
14
+ }
15
+ async function withThrottleRetry(operation, { sleep, random }) {
16
+ for (let attempt = 0;; attempt++) {
17
+ try {
18
+ return await operation();
19
+ }
20
+ catch (error) {
21
+ if (attempt >= BACKOFF_MS.length || !isThrottling(error)) {
22
+ throw error;
23
+ }
24
+ await sleep(Math.floor(random() * BACKOFF_MS[attempt]));
25
+ }
26
+ }
27
+ }
@@ -0,0 +1,72 @@
1
+ import { type MicrovmMemoryMiB } from '@evident-ai/lambda-microvm-cdk';
2
+ /**
3
+ * An operator-authored MicroVM shape: a named memory footprint the controller
4
+ * can launch a session on. A shape's size is fixed on its image (#723) —
5
+ * there is no launch-time resource dial — so "a bigger machine" means "a
6
+ * different shape", each backed by its own `MicrovmImage`.
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.
13
+ */
14
+ export interface MicrovmShape {
15
+ /** The stable id a doorbell asks for; crosses the wire to Evident. */
16
+ readonly name: string;
17
+ /** Operator-authored, for Evident's future shape picker. */
18
+ readonly title: string;
19
+ /**
20
+ * Operator-authored; also becomes the image's CloudFormation
21
+ * `Description`.
22
+ */
23
+ readonly description: string;
24
+ readonly memoryMiB: MicrovmMemoryMiB;
25
+ /**
26
+ * Exactly one shape must set this, and — see the ordering contract below —
27
+ * it must be the first entry in `MICROVM_SHAPES`.
28
+ */
29
+ readonly isDefault: boolean;
30
+ /** The `MicrovmImage` construct id within the stack. */
31
+ readonly constructId: string;
32
+ /** The account-unique `imageName` passed to `MicrovmImage`. */
33
+ readonly imageName: string;
34
+ }
35
+ /**
36
+ * The committed shape list. Ordering contract (load-bearing, not cosmetic):
37
+ * the list is ordered, the default shape MUST be the first entry, and the
38
+ * stack constructs shapes in list order. That is because every non-default
39
+ * shape's image is built with the default shape's own execution role (D3 in
40
+ * the plan) — so the default has to exist before any other shape can borrow
41
+ * its role. Reordering this list to put the default anywhere but index 0 is
42
+ * a synth-time error (`validateShapes`), not a cosmetic change. The same
43
+ * order is what `advertise()` (controller-side) hands Evident's future
44
+ * picker, so default-first is also the picker's ordering — a small price for
45
+ * making the construction-order contract enforceable rather than a
46
+ * convention.
47
+ *
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.
54
+ *
55
+ * "Frozen" means unchanged under OUR OWN synth (no `microvm:*` context set —
56
+ * `main.ts` passes none). `imageName` MAY carry a customer's
57
+ * `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.
62
+ */
63
+ export declare const MICROVM_SHAPES: readonly MicrovmShape[];
64
+ /**
65
+ * Validates a shape list at synth time. Deliberately lean
66
+ * (`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).
71
+ */
72
+ export declare function validateShapes(shapes: readonly MicrovmShape[]): readonly MicrovmShape[];
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MICROVM_SHAPES = void 0;
4
+ exports.validateShapes = validateShapes;
5
+ const lambda_microvm_cdk_1 = require("@evident-ai/lambda-microvm-cdk");
6
+ /**
7
+ * The committed shape list. Ordering contract (load-bearing, not cosmetic):
8
+ * the list is ordered, the default shape MUST be the first entry, and the
9
+ * stack constructs shapes in list order. That is because every non-default
10
+ * shape's image is built with the default shape's own execution role (D3 in
11
+ * the plan) — so the default has to exist before any other shape can borrow
12
+ * its role. Reordering this list to put the default anywhere but index 0 is
13
+ * a synth-time error (`validateShapes`), not a cosmetic change. The same
14
+ * order is what `advertise()` (controller-side) hands Evident's future
15
+ * picker, so default-first is also the picker's ordering — a small price for
16
+ * making the construction-order contract enforceable rather than a
17
+ * convention.
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.
25
+ *
26
+ * "Frozen" means unchanged under OUR OWN synth (no `microvm:*` context set —
27
+ * `main.ts` passes none). `imageName` MAY carry a customer's
28
+ * `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.
33
+ */
34
+ exports.MICROVM_SHAPES = [
35
+ {
36
+ name: 'standard',
37
+ title: 'Standard',
38
+ description: 'Evident per-session runner: an arm64 image that boots with no identity and acquires one at /run.',
39
+ memoryMiB: 4096,
40
+ isDefault: true,
41
+ // FROZEN — see the file-level comment. Do not rename either string.
42
+ constructId: 'Image',
43
+ imageName: 'evident-microvm',
44
+ },
45
+ ];
46
+ /**
47
+ * Validates a shape list at synth time. Deliberately lean
48
+ * (`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).
53
+ */
54
+ function validateShapes(shapes) {
55
+ if (shapes.length === 0) {
56
+ throw new Error('MICROVM_SHAPES must not be empty.');
57
+ }
58
+ const seenNames = new Set();
59
+ for (const shape of shapes) {
60
+ if (seenNames.has(shape.name)) {
61
+ throw new Error(`Duplicate MicroVM shape name "${shape.name}" — shape names must be unique.`);
62
+ }
63
+ seenNames.add(shape.name);
64
+ }
65
+ const seenImageNames = new Set();
66
+ for (const shape of shapes) {
67
+ if (seenImageNames.has(shape.imageName)) {
68
+ throw new Error(`Duplicate MicroVM image name "${shape.imageName}" (shape "${shape.name}") — ` +
69
+ 'AWS rejects this only at deploy, minutes in, so it is checked here instead.');
70
+ }
71
+ seenImageNames.add(shape.imageName);
72
+ }
73
+ const defaults = shapes.filter((shape) => shape.isDefault);
74
+ if (defaults.length === 0) {
75
+ throw new Error('MICROVM_SHAPES must mark exactly one shape isDefault: true — none does.');
76
+ }
77
+ if (defaults.length > 1) {
78
+ throw new Error('MICROVM_SHAPES must mark exactly one shape isDefault: true — ' +
79
+ `found ${defaults.length}: ${defaults.map((shape) => shape.name).join(', ')}.`);
80
+ }
81
+ if (shapes[0] !== defaults[0]) {
82
+ throw new Error(`The default shape ("${defaults[0].name}") must be the first entry in MICROVM_SHAPES, ` +
83
+ `not index ${shapes.indexOf(defaults[0])}. Every non-default shape's image is built ` +
84
+ "with the default shape's own execution role, so the default has to be constructed first.");
85
+ }
86
+ for (const shape of shapes) {
87
+ if (!lambda_microvm_cdk_1.MICROVM_MEMORY_TIERS_MIB.includes(shape.memoryMiB)) {
88
+ throw new Error(`Shape "${shape.name}" has memoryMiB ${shape.memoryMiB}, which is not one of the ` +
89
+ `MicroVM memory tiers (${lambda_microvm_cdk_1.MICROVM_MEMORY_TIERS_MIB.join(', ')} MiB).`);
90
+ }
91
+ }
92
+ return shapes;
93
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@evident-ai/runner-cdk",
3
- "version": "0.1.0",
4
- "description": "Reusable CDK construct for a single scale-to-zero Evident agent runner on Fargate (task + service + per-agent self-stop role + waker Lambda). Instantiate once per agent from your own stack.",
3
+ "version": "0.1.1-dev.d96ef8a",
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",
7
7
  "exports": {
@@ -21,12 +21,15 @@
21
21
  "lint": "eslint 'src/**/*.ts' --max-warnings=0"
22
22
  },
23
23
  "peerDependencies": {
24
+ "@evident-ai/lambda-microvm-cdk": "^0.1.1",
24
25
  "aws-cdk-lib": "^2.240.0",
25
26
  "constructs": "^10.5.0"
26
27
  },
27
28
  "devDependencies": {
28
29
  "@aws-sdk/client-ecs": "^3.682.0",
30
+ "@aws-sdk/client-lambda-microvms": "^3.1095.0",
29
31
  "@aws-sdk/client-secrets-manager": "^3.682.0",
32
+ "@evident-ai/lambda-microvm-cdk": "^0.1.1",
30
33
  "@evident/webhook-signature": "workspace:*",
31
34
  "@types/node": "^22",
32
35
  "aws-cdk-lib": "^2.240.0",