@evident-ai/runner-cdk 3.5.2-dev.e22029a → 3.5.2-dev.f258bd6
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 +69 -39
- package/dist/controller-lambda/handler.js +13 -13
- package/dist/image-version-reporter-lambda/handler.js +15 -8
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -2
- package/dist/microvm/constants.d.ts +1 -1
- package/dist/microvm/constants.js +7 -4
- package/dist/microvm/construct.d.ts +6 -8
- package/dist/microvm/construct.js +15 -26
- package/dist/microvm/controller/handle-doorbell.d.ts +2 -2
- package/dist/microvm/controller/handle-doorbell.js +2 -2
- package/dist/microvm/image/stage-context.d.ts +13 -38
- package/dist/microvm/image/stage-context.js +30 -120
- package/dist/microvm/image-version-reporter/construct.d.ts +1 -1
- package/dist/microvm/image-version-reporter/construct.js +3 -3
- package/dist/microvm/image-version-reporter/handler.d.ts +2 -2
- package/dist/microvm/image-version-reporter/handler.js +9 -7
- package/dist/microvm/shapes.d.ts +10 -21
- package/dist/microvm/shapes.js +10 -17
- package/package.json +3 -3
- package/dist/microvm-image-context/Dockerfile +0 -190
- package/dist/microvm-image-context/hook-server.js +0 -286
- package/dist/microvm-image-context/hooks/common.sh +0 -958
- package/dist/microvm-image-context/hooks/resume +0 -81
- package/dist/microvm-image-context/hooks/run +0 -95
- package/dist/microvm-image-context/hooks/session-db-maintenance.mjs +0 -69
- package/dist/microvm-image-context/hooks/suspend +0 -21
- package/dist/microvm-image-context/hooks/terminate +0 -38
|
@@ -33,143 +33,53 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.
|
|
36
|
+
exports.MICROVM_WORKSPACE_DIRECTORY = void 0;
|
|
37
|
+
exports.stageMicrovmBuildContext = stageMicrovmBuildContext;
|
|
37
38
|
const node_child_process_1 = require("node:child_process");
|
|
38
39
|
const node_fs_1 = require("node:fs");
|
|
40
|
+
const node_os_1 = require("node:os");
|
|
39
41
|
const path = __importStar(require("node:path"));
|
|
40
|
-
/**
|
|
41
|
-
|
|
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');
|
|
42
|
+
/** The fixed directory that contains the workspace in a staged build context. */
|
|
43
|
+
exports.MICROVM_WORKSPACE_DIRECTORY = 'workspace';
|
|
51
44
|
function git(args, cwd) {
|
|
52
45
|
return (0, node_child_process_1.execFileSync)('git', args, { cwd, encoding: 'utf8' }).trim();
|
|
53
46
|
}
|
|
54
|
-
/**
|
|
55
|
-
|
|
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) {
|
|
47
|
+
/** Names the path when git cannot determine whether the repository is shallow. */
|
|
48
|
+
function isShallowRepository(sourcePath) {
|
|
60
49
|
try {
|
|
61
|
-
return git(['rev-parse', '--is-shallow-repository'],
|
|
50
|
+
return git(['rev-parse', '--is-shallow-repository'], sourcePath) === 'true';
|
|
62
51
|
}
|
|
63
52
|
catch (err) {
|
|
64
|
-
throw new Error(`${
|
|
53
|
+
throw new Error(`${sourcePath} is not a usable git repository: ` +
|
|
65
54
|
`${err instanceof Error ? err.message : String(err)}`);
|
|
66
55
|
}
|
|
67
56
|
}
|
|
68
|
-
/**
|
|
69
|
-
|
|
70
|
-
|
|
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, ` +
|
|
57
|
+
/** Clones a full repository while normalising the bytes that affect the image asset hash. */
|
|
58
|
+
function stageRepository(sourcePath, stagedWorkspacePath, workspaceOriginUrl) {
|
|
59
|
+
if (isShallowRepository(sourcePath)) {
|
|
60
|
+
throw new Error(`refusing to stage a shallow checkout (${sourcePath}): the agent branches, ` +
|
|
82
61
|
'commits and opens PRs, so the image needs real history. Run `git fetch --unshallow`.');
|
|
83
62
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
}
|
|
106
|
-
function stageScriptDirectory(source, destination) {
|
|
107
|
-
(0, node_fs_1.mkdirSync)(destination);
|
|
108
|
-
for (const entry of (0, node_fs_1.readdirSync)(source, { withFileTypes: true })) {
|
|
109
|
-
const sourcePath = path.join(source, entry.name);
|
|
110
|
-
const staged = path.join(destination, entry.name);
|
|
111
|
-
if (entry.isDirectory()) {
|
|
112
|
-
stageScriptDirectory(sourcePath, staged);
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
(0, node_fs_1.copyFileSync)(sourcePath, staged);
|
|
116
|
-
// Set modes here rather than inheriting them from the template, so they stay
|
|
117
|
-
// correct after an npm pack, zip, CI-cache or hand-copy round trip. The image
|
|
118
|
-
// executes extensionless scripts and sources `.sh` files.
|
|
119
|
-
(0, node_fs_1.chmodSync)(staged, entry.name.endsWith('.sh') ? 0o644 : 0o755);
|
|
120
|
-
}
|
|
63
|
+
git(['clone', '--quiet', '--single-branch', `file://${sourcePath}`, stagedWorkspacePath], path.dirname(stagedWorkspacePath));
|
|
64
|
+
git(['remote', 'set-url', 'origin', workspaceOriginUrl], stagedWorkspacePath);
|
|
65
|
+
git(['-c', 'pack.threads=1', 'repack', '-adfqF'], stagedWorkspacePath);
|
|
66
|
+
(0, node_fs_1.rmSync)(path.join(stagedWorkspacePath, '.git', 'index'), { force: true });
|
|
67
|
+
(0, node_fs_1.rmSync)(path.join(stagedWorkspacePath, '.git', 'logs'), { recursive: true, force: true });
|
|
68
|
+
console.log(`[stage] repo ${git(['rev-parse', 'HEAD'], stagedWorkspacePath)}`);
|
|
121
69
|
}
|
|
122
70
|
/**
|
|
123
|
-
*
|
|
124
|
-
* per-phase hook scripts, the deployment overlay and the repository — and
|
|
125
|
-
* returns its path. The result is what `EvidentMicrovmConstruct`'s `imageSource`
|
|
126
|
-
* takes.
|
|
71
|
+
* Stages customer-owned image inputs in a fresh temporary directory.
|
|
127
72
|
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
* copy of those files. A caller may supply an overlay; the default is
|
|
131
|
-
* deterministic no-op scripts. `repositoryPath` is the only required caller
|
|
132
|
-
* input.
|
|
73
|
+
* Staging keeps the image asset hash a pure function of the workspace commit while
|
|
74
|
+
* leaving the customer in control of the Dockerfile and its build context.
|
|
133
75
|
*/
|
|
134
|
-
function
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const repositoryPath = path.resolve(options.repositoryPath);
|
|
139
|
-
const destination = path.resolve(options.destination);
|
|
140
|
-
if (!(0, node_fs_1.existsSync)(templateDir)) {
|
|
141
|
-
throw new Error(`the MicroVM image context template is missing from ${templateDir}. It is written by ` +
|
|
142
|
-
"this package's build (`pnpm --filter @evident-ai/runner-cdk build`) and ships in the " +
|
|
143
|
-
'published tarball, so an installed copy should always have it.');
|
|
144
|
-
}
|
|
145
|
-
(0, node_fs_1.rmSync)(destination, { recursive: true, force: true });
|
|
146
|
-
(0, node_fs_1.mkdirSync)(destination, { recursive: true });
|
|
147
|
-
(0, node_fs_1.copyFileSync)(path.join(templateDir, 'Dockerfile'), path.join(destination, 'Dockerfile'));
|
|
148
|
-
(0, node_fs_1.copyFileSync)(path.join(templateDir, 'hook-server.js'), path.join(destination, 'hook-server.js'));
|
|
149
|
-
const hooksSource = path.join(templateDir, 'hooks');
|
|
150
|
-
const hooksStage = path.join(destination, 'hooks');
|
|
151
|
-
stageScriptDirectory(hooksSource, hooksStage);
|
|
152
|
-
const overlayStage = path.join(destination, 'overlay');
|
|
153
|
-
if (overlayDir === undefined) {
|
|
154
|
-
(0, node_fs_1.mkdirSync)(overlayStage);
|
|
155
|
-
for (const name of ['setup-root', 'setup-workspace']) {
|
|
156
|
-
const staged = path.join(overlayStage, name);
|
|
157
|
-
(0, node_fs_1.writeFileSync)(staged, '#!/usr/bin/env bash\n# No overlay supplied.\n', { mode: 0o755 });
|
|
158
|
-
(0, node_fs_1.chmodSync)(staged, 0o755);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
else {
|
|
162
|
-
const resolvedOverlayDir = path.resolve(overlayDir);
|
|
163
|
-
if (!(0, node_fs_1.existsSync)(resolvedOverlayDir)) {
|
|
164
|
-
throw new Error(`overlay directory does not exist: ${resolvedOverlayDir}`);
|
|
165
|
-
}
|
|
166
|
-
for (const required of ['setup-root', 'setup-workspace']) {
|
|
167
|
-
if (!(0, node_fs_1.existsSync)(path.join(resolvedOverlayDir, required))) {
|
|
168
|
-
throw new Error(`overlay directory ${resolvedOverlayDir} is missing required script ${required}`);
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
stageScriptDirectory(resolvedOverlayDir, overlayStage);
|
|
76
|
+
function stageMicrovmBuildContext(options) {
|
|
77
|
+
const buildContextPath = path.resolve(options.buildContextPath);
|
|
78
|
+
if (!(0, node_fs_1.existsSync)(buildContextPath) || !(0, node_fs_1.existsSync)(path.join(buildContextPath, 'Dockerfile'))) {
|
|
79
|
+
throw new Error(`MicroVM build context ${buildContextPath} does not exist or has no Dockerfile`);
|
|
172
80
|
}
|
|
173
|
-
|
|
174
|
-
|
|
81
|
+
const stagedPath = (0, node_fs_1.mkdtempSync)(path.join((0, node_os_1.tmpdir)(), 'evident-microvm-'));
|
|
82
|
+
(0, node_fs_1.cpSync)(buildContextPath, stagedPath, { recursive: true });
|
|
83
|
+
stageRepository(path.resolve(options.workspaceRepositoryPath), path.join(stagedPath, exports.MICROVM_WORKSPACE_DIRECTORY), options.workspaceOriginUrl);
|
|
84
|
+
return stagedPath;
|
|
175
85
|
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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.
|
|
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
|
-
|
|
13
|
-
|
|
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
|
|
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(`
|
|
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(`
|
|
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:
|
|
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.
|
|
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
|
-
|
|
100
|
-
|
|
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
|
}));
|
package/dist/microvm/shapes.d.ts
CHANGED
|
@@ -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
|
-
* `
|
|
9
|
-
*
|
|
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 `
|
|
49
|
-
* ("evident-microvm") are FROZEN:
|
|
50
|
-
*
|
|
51
|
-
*
|
|
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.
|
|
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
|
|
68
|
-
*
|
|
69
|
-
*
|
|
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[];
|
package/dist/microvm/shapes.js
CHANGED
|
@@ -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 `
|
|
20
|
-
* ("evident-microvm") are FROZEN:
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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.
|
|
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 —
|
|
42
|
-
|
|
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
|
|
50
|
-
*
|
|
51
|
-
*
|
|
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.
|
|
3
|
+
"version": "3.5.2-dev.f258bd6",
|
|
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": "
|
|
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"]
|