@evident-ai/runner-cdk 0.1.1-dev.0fe02fb → 0.1.1-dev.15755a4
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 +76 -19
- package/dist/controller-lambda/handler.js +50523 -0
- package/dist/evident-scale-to-zero-construct.js +4 -5
- package/dist/index.d.ts +4 -0
- package/dist/index.js +14 -1
- package/dist/microvm/constants.d.ts +7 -0
- package/dist/microvm/constants.js +34 -0
- package/dist/microvm/construct.d.ts +87 -0
- package/dist/microvm/construct.js +253 -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/image/stage-context.d.ts +33 -0
- package/dist/microvm/image/stage-context.js +148 -0
- package/dist/microvm/shapes.d.ts +72 -0
- package/dist/microvm/shapes.js +93 -0
- package/dist/microvm-image-context/Dockerfile +227 -0
- package/dist/microvm-image-context/hook-server.js +286 -0
- package/dist/microvm-image-context/hooks/common.sh +1359 -0
- package/dist/microvm-image-context/hooks/resume +79 -0
- package/dist/microvm-image-context/hooks/run +117 -0
- package/dist/microvm-image-context/hooks/suspend +19 -0
- package/dist/microvm-image-context/hooks/terminate +34 -0
- package/dist/waker/construct.js +1 -1
- package/package.json +15 -7
|
@@ -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,33 @@
|
|
|
1
|
+
export interface StageMicrovmImageContextOptions {
|
|
2
|
+
/**
|
|
3
|
+
* The repository baked into the image as the agent's workspace. This is the
|
|
4
|
+
* CONSUMER's repository, not Evident's — the image is a dev box for whatever
|
|
5
|
+
* codebase the agent works on. Must be a full (non-shallow) clone.
|
|
6
|
+
*/
|
|
7
|
+
readonly repositoryPath: string;
|
|
8
|
+
/**
|
|
9
|
+
* `origin` of the baked repository, as the VM will see it. Must be a
|
|
10
|
+
* credential-free URL (typically `https://github.com/<owner>/<repo>.git`):
|
|
11
|
+
* this string ships inside the shared snapshot, so a token in it would be
|
|
12
|
+
* baked into every VM. The credential arrives per-VM instead.
|
|
13
|
+
*/
|
|
14
|
+
readonly originUrl: string;
|
|
15
|
+
/** Directory to write the build context to. Removed and recreated. */
|
|
16
|
+
readonly destination: string;
|
|
17
|
+
/**
|
|
18
|
+
* The published template to copy from. Defaults to the one inside this
|
|
19
|
+
* package; named directly by the tests, which assert against a template they
|
|
20
|
+
* built themselves rather than whichever `dist/` happens to be present.
|
|
21
|
+
*/
|
|
22
|
+
readonly templateDir?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Writes the build context AWS unpacks — the Dockerfile, the hook server, the
|
|
26
|
+
* per-phase hook scripts and the repository — and returns its path. The result
|
|
27
|
+
* is what `EvidentMicrovmConstruct`'s `imageSource` takes.
|
|
28
|
+
*
|
|
29
|
+
* The first three come from this package's published `dist/`, so a consumer
|
|
30
|
+
* needs no checkout of `sroze/evident` and no copy of the Dockerfile or hook
|
|
31
|
+
* scripts (#1528). Only `repositoryPath` is theirs to supply.
|
|
32
|
+
*/
|
|
33
|
+
export declare function stageMicrovmImageContext(options: StageMicrovmImageContextOptions): string;
|
|
@@ -0,0 +1,148 @@
|
|
|
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.stageMicrovmImageContext = stageMicrovmImageContext;
|
|
37
|
+
const node_child_process_1 = require("node:child_process");
|
|
38
|
+
const node_fs_1 = require("node:fs");
|
|
39
|
+
const path = __importStar(require("node:path"));
|
|
40
|
+
/**
|
|
41
|
+
* The image build context template published inside this package.
|
|
42
|
+
*
|
|
43
|
+
* Resolved from the PACKAGE ROOT, THREE directories above `__dirname` — either
|
|
44
|
+
* compiled (`dist/microvm/image`) or via ts-node (`src/microvm/image`) — the
|
|
45
|
+
* same way `microvm/construct.ts` resolves the controller Lambda asset, but one
|
|
46
|
+
* `..` deeper because this file sits a directory below it. It is
|
|
47
|
+
* written by `scripts/build.ts` at PACKAGE build time, not at stage time, so a
|
|
48
|
+
* consumer needs no esbuild and no checkout of this repo (#1528).
|
|
49
|
+
*/
|
|
50
|
+
const TEMPLATE_DIR = path.resolve(__dirname, '..', '..', '..', 'dist', 'microvm-image-context');
|
|
51
|
+
function git(args, cwd) {
|
|
52
|
+
return (0, node_child_process_1.execFileSync)('git', args, { cwd, encoding: 'utf8' }).trim();
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* `git rev-parse --is-shallow-repository` throws an opaque `execFileSync`
|
|
56
|
+
* error (e.g. "not a git repository") when `repositoryPath` isn't usable at
|
|
57
|
+
* all, so that failure is rethrown naming the path.
|
|
58
|
+
*/
|
|
59
|
+
function isShallowRepository(repositoryPath) {
|
|
60
|
+
try {
|
|
61
|
+
return git(['rev-parse', '--is-shallow-repository'], repositoryPath) === 'true';
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
throw new Error(`${repositoryPath} is not a usable git repository: ` +
|
|
65
|
+
`${err instanceof Error ? err.message : String(err)}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Clones the caller's repository into the build context, so the Dockerfile can
|
|
70
|
+
* ship the workspace already installed.
|
|
71
|
+
*
|
|
72
|
+
* `file://` rather than a plain path: a path triggers git's local-clone
|
|
73
|
+
* optimisation, which copies the whole object store — every branch and worktree
|
|
74
|
+
* ref on the staging machine (50 MB here). The file transport negotiates like a
|
|
75
|
+
* network fetch and packs only what the checked-out branch reaches (9 MB), with
|
|
76
|
+
* one branch and one remote-tracking ref. Ignored files, `node_modules` first
|
|
77
|
+
* among them, are not part of a clone at all.
|
|
78
|
+
*/
|
|
79
|
+
function stageRepository(repositoryPath, destination, originUrl) {
|
|
80
|
+
if (isShallowRepository(repositoryPath)) {
|
|
81
|
+
throw new Error(`refusing to stage a shallow checkout (${repositoryPath}): the agent branches, ` +
|
|
82
|
+
'commits and opens PRs, so the image needs real history. Run `git fetch --unshallow`.');
|
|
83
|
+
}
|
|
84
|
+
// Absolute on both sides: `file://` needs an absolute path to mean anything,
|
|
85
|
+
// and the clone's cwd is the destination's parent (the only directory both
|
|
86
|
+
// are guaranteed to resolve against).
|
|
87
|
+
git(['clone', '--quiet', '--single-branch', `file://${repositoryPath}`, destination], path.dirname(destination));
|
|
88
|
+
// The clone points `origin` at this machine's filesystem; the VM's origin is
|
|
89
|
+
// the caller's own remote over HTTPS, with no credential in it.
|
|
90
|
+
git(['remote', 'set-url', 'origin', originUrl], destination);
|
|
91
|
+
// The asset hash of the resulting image must be a pure function of the commit,
|
|
92
|
+
// or every deploy publishes a new `AWS::Lambda::MicrovmImage` version and wedges
|
|
93
|
+
// the stack at its per-image version ceiling. Two things break that purity:
|
|
94
|
+
// - `pack-objects`' delta search is multithreaded (`pack.threads` defaults to
|
|
95
|
+
// the CPU count) and its output varies run to run; `-f`/`-F`
|
|
96
|
+
// (`--no-reuse-delta`/`--no-reuse-object`) additionally stop it from reusing
|
|
97
|
+
// whatever packing the source checkout happened to have. All three together
|
|
98
|
+
// make the pack a pure function of the object set, independent of the source.
|
|
99
|
+
// - `.git/index` carries stat data (inode/mtime/size) and `.git/logs/**` carries
|
|
100
|
+
// wall-clock reflog timestamps; neither is byte-stable for a fixed commit.
|
|
101
|
+
git(['-c', 'pack.threads=1', 'repack', '-adfqF'], destination);
|
|
102
|
+
(0, node_fs_1.rmSync)(path.join(destination, '.git', 'index'), { force: true });
|
|
103
|
+
(0, node_fs_1.rmSync)(path.join(destination, '.git', 'logs'), { recursive: true, force: true });
|
|
104
|
+
console.log(`[stage] repo ${git(['rev-parse', 'HEAD'], destination)}`);
|
|
105
|
+
if (!(0, node_fs_1.existsSync)(path.join(destination, 'pnpm-lock.yaml'))) {
|
|
106
|
+
console.log(`[stage] ${repositoryPath} has no pnpm-lock.yaml — the image build will skip dependency installation`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Writes the build context AWS unpacks — the Dockerfile, the hook server, the
|
|
111
|
+
* per-phase hook scripts and the repository — and returns its path. The result
|
|
112
|
+
* is what `EvidentMicrovmConstruct`'s `imageSource` takes.
|
|
113
|
+
*
|
|
114
|
+
* The first three come from this package's published `dist/`, so a consumer
|
|
115
|
+
* needs no checkout of `sroze/evident` and no copy of the Dockerfile or hook
|
|
116
|
+
* scripts (#1528). Only `repositoryPath` is theirs to supply.
|
|
117
|
+
*/
|
|
118
|
+
function stageMicrovmImageContext(options) {
|
|
119
|
+
const { originUrl, templateDir = TEMPLATE_DIR } = options;
|
|
120
|
+
// `file://` and the clone below only mean anything against absolute paths,
|
|
121
|
+
// and a caller may reasonably pass either.
|
|
122
|
+
const repositoryPath = path.resolve(options.repositoryPath);
|
|
123
|
+
const destination = path.resolve(options.destination);
|
|
124
|
+
if (!(0, node_fs_1.existsSync)(templateDir)) {
|
|
125
|
+
throw new Error(`the MicroVM image context template is missing from ${templateDir}. It is written by ` +
|
|
126
|
+
"this package's build (`pnpm --filter @evident-ai/runner-cdk build`) and ships in the " +
|
|
127
|
+
'published tarball, so an installed copy should always have it.');
|
|
128
|
+
}
|
|
129
|
+
(0, node_fs_1.rmSync)(destination, { recursive: true, force: true });
|
|
130
|
+
(0, node_fs_1.mkdirSync)(destination, { recursive: true });
|
|
131
|
+
(0, node_fs_1.copyFileSync)(path.join(templateDir, 'Dockerfile'), path.join(destination, 'Dockerfile'));
|
|
132
|
+
(0, node_fs_1.copyFileSync)(path.join(templateDir, 'hook-server.js'), path.join(destination, 'hook-server.js'));
|
|
133
|
+
const hooksSource = path.join(templateDir, 'hooks');
|
|
134
|
+
const hooksStage = path.join(destination, 'hooks');
|
|
135
|
+
(0, node_fs_1.mkdirSync)(hooksStage);
|
|
136
|
+
for (const entry of (0, node_fs_1.readdirSync)(hooksSource)) {
|
|
137
|
+
const staged = path.join(hooksStage, entry);
|
|
138
|
+
(0, node_fs_1.copyFileSync)(path.join(hooksSource, entry), staged);
|
|
139
|
+
// The runtime only runs a hook it can execute. Set here rather than
|
|
140
|
+
// inherited from the template, so the staged mode is a property of THIS
|
|
141
|
+
// function rather than of however the template reached disk — `npm pack`
|
|
142
|
+
// does carry the bit, but a zip-based vendoring, a CI cache restore or a
|
|
143
|
+
// hand-copied directory need not.
|
|
144
|
+
(0, node_fs_1.chmodSync)(staged, entry.endsWith('.sh') ? 0o644 : 0o755);
|
|
145
|
+
}
|
|
146
|
+
stageRepository(repositoryPath, path.join(destination, 'repo'), originUrl);
|
|
147
|
+
return destination;
|
|
148
|
+
}
|
|
@@ -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
|
+
}
|