@evident-ai/runner-cdk 0.1.1-dev.0fe02fb → 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.
- package/README.md +21 -16
- package/dist/controller-lambda/handler.js +50523 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +8 -1
- package/dist/microvm/constants.d.ts +4 -0
- package/dist/microvm/constants.js +24 -0
- package/dist/microvm/construct.d.ts +73 -0
- package/dist/microvm/construct.js +242 -0
- package/dist/microvm/controller/doorbell.d.ts +73 -0
- package/dist/microvm/controller/doorbell.js +107 -0
- package/dist/microvm/controller/handle-doorbell.d.ts +27 -0
- package/dist/microvm/controller/handle-doorbell.js +480 -0
- package/dist/microvm/controller/microvm-client.d.ts +75 -0
- package/dist/microvm/controller/microvm-client.js +7 -0
- package/dist/microvm/controller/shape-catalogue.d.ts +64 -0
- package/dist/microvm/controller/shape-catalogue.js +108 -0
- package/dist/microvm/controller/throttle-retry.d.ts +11 -0
- package/dist/microvm/controller/throttle-retry.js +27 -0
- package/dist/microvm/shapes.d.ts +72 -0
- package/dist/microvm/shapes.js +93 -0
- package/package.json +5 -2
|
@@ -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.1-dev.
|
|
4
|
-
"description": "Reusable CDK
|
|
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",
|