@evident-ai/runner-cdk 0.1.1-dev.d96ef8a → 0.1.1-dev.da88f8e
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 +46 -3
- package/dist/controller-lambda/handler.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +7 -1
- package/dist/microvm/constants.d.ts +3 -0
- package/dist/microvm/constants.js +20 -9
- package/dist/microvm/construct.js +4 -1
- package/dist/microvm/controller/microvm-client.d.ts +1 -1
- package/dist/microvm/image/stage-context.d.ts +33 -0
- package/dist/microvm/image/stage-context.js +148 -0
- package/dist/microvm-image-context/Dockerfile +224 -0
- package/dist/microvm-image-context/hook-server.js +290 -0
- package/dist/microvm-image-context/hooks/common.sh +1255 -0
- package/dist/microvm-image-context/hooks/resume +79 -0
- package/dist/microvm-image-context/hooks/run +111 -0
- package/dist/microvm-image-context/hooks/suspend +19 -0
- package/dist/microvm-image-context/hooks/terminate +34 -0
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -9,9 +9,9 @@ Reusable AWS CDK constructs for running an [Evident](https://evident.run) agent
|
|
|
9
9
|
`desiredCount 0`, and an HMAC-authenticated waker Lambda scales it back up on the next
|
|
10
10
|
message. You pay for the time the agent is actually working.
|
|
11
11
|
- **`EvidentMicrovmConstruct`** — a per-session AWS Lambda MicroVM that boots on demand
|
|
12
|
-
and suspends between messages.
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
and suspends between messages. Installing this package is all you need: the image build
|
|
13
|
+
context ships inside it (see [The MicroVM image](#the-microvm-image) below), so there is
|
|
14
|
+
no checkout of `sroze/evident` involved. See
|
|
15
15
|
[the AWS runner doc](https://evident.run/docs/aws-runner) for the full picture of both
|
|
16
16
|
strategies.
|
|
17
17
|
|
|
@@ -113,6 +113,49 @@ This construct never builds an image. Pass any `ecs.ContainerImage` — from a r
|
|
|
113
113
|
built from a `Dockerfile` you control. The generic runner image lives separately in
|
|
114
114
|
`packages/runner-image`, so you can adopt the image, the construct, or both.
|
|
115
115
|
|
|
116
|
+
## The MicroVM image
|
|
117
|
+
|
|
118
|
+
`EvidentMicrovmConstruct`'s `imageSource` is a **local directory** holding the image build
|
|
119
|
+
context, which `cdk deploy` zips and uploads. The Lambda MicroVM service then builds the
|
|
120
|
+
image in *your* account, from a base image ARN you discover there — so, unlike the Fargate
|
|
121
|
+
strategy, there is no registry image to pull.
|
|
122
|
+
|
|
123
|
+
The context has two halves, and this package ships the half that is ours: the Dockerfile,
|
|
124
|
+
the per-phase hook scripts and the bundled hook server, published inside the tarball at
|
|
125
|
+
`dist/microvm-image-context/`. The other half is **your** repository, which is baked in as
|
|
126
|
+
the agent's workspace. `stageMicrovmImageContext()` puts the two together:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
import {
|
|
130
|
+
EvidentMicrovmConstruct,
|
|
131
|
+
HOOKS_PORT,
|
|
132
|
+
HOOK_TIMEOUT_SECONDS,
|
|
133
|
+
stageMicrovmImageContext,
|
|
134
|
+
} from '@evident-ai/runner-cdk';
|
|
135
|
+
|
|
136
|
+
new EvidentMicrovmConstruct(this, 'Runner', {
|
|
137
|
+
imageSource: stageMicrovmImageContext({
|
|
138
|
+
// A full (non-shallow) clone: the agent branches, commits and opens PRs.
|
|
139
|
+
repositoryPath: '/path/to/your/checkout',
|
|
140
|
+
// Credential-free — this string ships inside the shared snapshot. The
|
|
141
|
+
// token the agent pushes with arrives per-session, never baked in.
|
|
142
|
+
originUrl: 'https://github.com/acme/widgets.git',
|
|
143
|
+
destination: path.join(__dirname, '..', 'build', 'image'),
|
|
144
|
+
}),
|
|
145
|
+
// Must match the published image these were baked into, so export them
|
|
146
|
+
// rather than restating the numbers.
|
|
147
|
+
hooksPort: HOOKS_PORT,
|
|
148
|
+
hookTimeoutSeconds: HOOK_TIMEOUT_SECONDS,
|
|
149
|
+
baseImageArn,
|
|
150
|
+
baseImageVersion,
|
|
151
|
+
doorbellSecret,
|
|
152
|
+
});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Your repo does not have to be a pnpm workspace. If it commits a `pnpm-lock.yaml` the image
|
|
156
|
+
pre-installs dependencies at build time (a faster first boot); if not, that step is skipped
|
|
157
|
+
and the agent installs on first use.
|
|
158
|
+
|
|
116
159
|
## Status / limitations
|
|
117
160
|
|
|
118
161
|
- **Published to npm** as `@evident-ai/runner-cdk` (MIT) — see
|
|
@@ -50464,7 +50464,7 @@ function createRuntimeMicrovm(executionRoleArn) {
|
|
|
50464
50464
|
// Claude-agents guidance recommending `maxIdleDurationSeconds: 120`
|
|
50465
50465
|
// targets VMs reached inbound and does not apply here. Idle is instead
|
|
50466
50466
|
// decided IN the VM by `evident run --idle-timeout`
|
|
50467
|
-
// (
|
|
50467
|
+
// (packages/runner-cdk/microvm-image/hooks/common.sh's `IDLE_TIMEOUT_SECONDS`), whose
|
|
50468
50468
|
// clean exit is what drives the controller's own `suspend()` below via
|
|
50469
50469
|
// the `runner.suspend_requested` doorbell. See `idle-policy.test.ts`
|
|
50470
50470
|
// in this directory, which scans this file's source for the key and
|
package/dist/index.d.ts
CHANGED
|
@@ -2,4 +2,5 @@ export { EvidentScaleToZeroConstruct, type EvidentScaleToZeroConstructProps, } f
|
|
|
2
2
|
export { EvidentWaker, type EvidentWakerProps } from './waker/construct';
|
|
3
3
|
export { EvidentMicrovmConstruct, type EvidentMicrovmConstructProps } from './microvm/construct';
|
|
4
4
|
export { MICROVM_SHAPES, validateShapes, type MicrovmShape } from './microvm/shapes';
|
|
5
|
-
export { MICROVM_MAX_RUN_SECONDS } from './microvm/constants';
|
|
5
|
+
export { MICROVM_MAX_RUN_SECONDS, HOOKS_PORT, HOOK_TIMEOUT_SECONDS, } from './microvm/constants';
|
|
6
|
+
export { stageMicrovmImageContext, type StageMicrovmImageContextOptions, } from './microvm/image/stage-context';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.MICROVM_MAX_RUN_SECONDS = exports.validateShapes = exports.MICROVM_SHAPES = exports.EvidentMicrovmConstruct = exports.EvidentWaker = exports.EvidentScaleToZeroConstruct = void 0;
|
|
3
|
+
exports.stageMicrovmImageContext = exports.HOOK_TIMEOUT_SECONDS = exports.HOOKS_PORT = exports.MICROVM_MAX_RUN_SECONDS = exports.validateShapes = exports.MICROVM_SHAPES = exports.EvidentMicrovmConstruct = exports.EvidentWaker = exports.EvidentScaleToZeroConstruct = void 0;
|
|
4
4
|
var evident_scale_to_zero_construct_1 = require("./evident-scale-to-zero-construct");
|
|
5
5
|
Object.defineProperty(exports, "EvidentScaleToZeroConstruct", { enumerable: true, get: function () { return evident_scale_to_zero_construct_1.EvidentScaleToZeroConstruct; } });
|
|
6
6
|
var construct_1 = require("./waker/construct");
|
|
@@ -12,3 +12,9 @@ Object.defineProperty(exports, "MICROVM_SHAPES", { enumerable: true, get: functi
|
|
|
12
12
|
Object.defineProperty(exports, "validateShapes", { enumerable: true, get: function () { return shapes_1.validateShapes; } });
|
|
13
13
|
var constants_1 = require("./microvm/constants");
|
|
14
14
|
Object.defineProperty(exports, "MICROVM_MAX_RUN_SECONDS", { enumerable: true, get: function () { return constants_1.MICROVM_MAX_RUN_SECONDS; } });
|
|
15
|
+
// The two values a consumer MUST pass to `EvidentMicrovmConstruct` for them
|
|
16
|
+
// to match the published image they were baked into (#1528).
|
|
17
|
+
Object.defineProperty(exports, "HOOKS_PORT", { enumerable: true, get: function () { return constants_1.HOOKS_PORT; } });
|
|
18
|
+
Object.defineProperty(exports, "HOOK_TIMEOUT_SECONDS", { enumerable: true, get: function () { return constants_1.HOOK_TIMEOUT_SECONDS; } });
|
|
19
|
+
var stage_context_1 = require("./microvm/image/stage-context");
|
|
20
|
+
Object.defineProperty(exports, "stageMicrovmImageContext", { enumerable: true, get: function () { return stage_context_1.stageMicrovmImageContext; } });
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
export declare const HOOKS_PORT = 8080;
|
|
2
|
+
export declare const HOOKS_DIR = "/etc/evident/hooks";
|
|
3
|
+
export declare const HOOK_TIMEOUT_SECONDS = 60;
|
|
1
4
|
export declare const RUN_HOOK_PAYLOAD_MAX_BYTES = 16384;
|
|
2
5
|
export declare const SUSPENDING_POLL_ATTEMPTS = 4;
|
|
3
6
|
export declare const SUSPENDING_POLL_INTERVAL_MS = 500;
|
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// Shared literals for the MicroVM controller
|
|
3
|
-
// from here rather than restating a value that must match
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// there because `image/hook-server.ts` — bundled into the Docker image by a
|
|
8
|
-
// different pipeline — imports them too and cannot take an npm dependency on
|
|
9
|
-
// this construct package.
|
|
2
|
+
// Shared literals for the MicroVM controller and for the image it launches.
|
|
3
|
+
// Every module imports from here rather than restating a value that must match
|
|
4
|
+
// across them — including `microvm/image/hook-server.ts`, which runs inside the
|
|
5
|
+
// image and is esbuild-bundled into the published build context by
|
|
6
|
+
// `scripts/build.ts`, so these values reach the image as inlined constants.
|
|
10
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
-
exports.MICROVM_MAX_RUN_SECONDS = exports.SUSPENDING_POLL_INTERVAL_MS = exports.SUSPENDING_POLL_ATTEMPTS = exports.RUN_HOOK_PAYLOAD_MAX_BYTES = void 0;
|
|
8
|
+
exports.MICROVM_MAX_RUN_SECONDS = exports.SUSPENDING_POLL_INTERVAL_MS = exports.SUSPENDING_POLL_ATTEMPTS = exports.RUN_HOOK_PAYLOAD_MAX_BYTES = exports.HOOK_TIMEOUT_SECONDS = exports.HOOKS_DIR = exports.HOOKS_PORT = void 0;
|
|
9
|
+
// The port the image's hook server binds, baked into the published Dockerfile
|
|
10
|
+
// (`ENV HOOKS_PORT`) and passed to the construct as `hooksPort`. Exported from
|
|
11
|
+
// the package index because a consumer building the published image context
|
|
12
|
+
// MUST pass the value it was baked with — the construct's own docstring warns
|
|
13
|
+
// that a mismatch means AWS probes a port nothing binds.
|
|
14
|
+
exports.HOOKS_PORT = 8080;
|
|
15
|
+
// One executable per lifecycle phase. A phase with no script is a no-op, which
|
|
16
|
+
// is why the image ships none for `ready` or `validate`: nothing Evident-specific
|
|
17
|
+
// may run before the snapshot, so there is nothing for them to do.
|
|
18
|
+
exports.HOOKS_DIR = '/etc/evident/hooks';
|
|
19
|
+
// Seconds AWS allows a Run/Resume/Suspend/Terminate hook, passed to the
|
|
20
|
+
// construct as `hookTimeoutSeconds`. Exported for the same reason as
|
|
21
|
+
// `HOOKS_PORT`: the hooks are built against it.
|
|
22
|
+
exports.HOOK_TIMEOUT_SECONDS = 60;
|
|
12
23
|
// AWS caps `runHookPayload` at 16 KiB; anything larger is rejected at its API,
|
|
13
24
|
// so the controller checks it before spending a call.
|
|
14
25
|
exports.RUN_HOOK_PAYLOAD_MAX_BYTES = 16384;
|
|
@@ -81,7 +81,9 @@ class EvidentMicrovmConstruct extends constructs_1.Construct {
|
|
|
81
81
|
// ANDed with the age condition and counts newer noncurrent versions of
|
|
82
82
|
// the SAME key, but litestream's LTX keys are txid-unique and churn by
|
|
83
83
|
// DELETE, so a churned key has exactly one noncurrent version and zero
|
|
84
|
-
// newer ones.
|
|
84
|
+
// newer ones. `expiredObjectDeleteMarker` reaps the delete marker itself once
|
|
85
|
+
// its noncurrent version is gone — without it, the version index grows
|
|
86
|
+
// unboundedly, one marker per churned LTX key.
|
|
85
87
|
const durableState = new s3.Bucket(this, 'DurableState', {
|
|
86
88
|
encryption: s3.BucketEncryption.S3_MANAGED,
|
|
87
89
|
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
|
|
@@ -98,6 +100,7 @@ class EvidentMicrovmConstruct extends constructs_1.Construct {
|
|
|
98
100
|
{
|
|
99
101
|
id: 'ExpireNoncurrentVersions',
|
|
100
102
|
noncurrentVersionExpiration: cdk.Duration.days(7),
|
|
103
|
+
expiredObjectDeleteMarker: true,
|
|
101
104
|
},
|
|
102
105
|
],
|
|
103
106
|
});
|
|
@@ -49,7 +49,7 @@ export interface MicrovmClient {
|
|
|
49
49
|
* (docs/spikes/lambda-microvms-phase0/README.md). AWS's own Claude-agents
|
|
50
50
|
* guidance recommending `maxIdleDurationSeconds: 120` targets VMs reached
|
|
51
51
|
* inbound and does not apply here. Idle is instead decided IN the VM by
|
|
52
|
-
* `evident run --idle-timeout` (
|
|
52
|
+
* `evident run --idle-timeout` (packages/runner-cdk/microvm-image/hooks/common.sh's
|
|
53
53
|
* `IDLE_TIMEOUT_SECONDS`), whose clean exit drives this
|
|
54
54
|
* client's own `suspend()` via the `runner.suspend_requested` doorbell.
|
|
55
55
|
*/
|
|
@@ -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,224 @@
|
|
|
1
|
+
# Evident per-session MicroVM image (#558, epic #556)
|
|
2
|
+
#
|
|
3
|
+
# Same installed footprint as the ECS runner image, built for linux/arm64
|
|
4
|
+
# (Lambda MicroVMs run on Graviton only) and with ONE process: the hook server.
|
|
5
|
+
#
|
|
6
|
+
# THE BOOT SPLIT — the rule that governs everything below. AWS snapshots this
|
|
7
|
+
# image ONCE at build and every MicroVM resumes from that same snapshot, so
|
|
8
|
+
# anything unique or secret baked in here is shared by every VM launched from
|
|
9
|
+
# this image version. Therefore this image must NOT start the tunnel
|
|
10
|
+
# (`evident run`), clone the repo, or carry any credential, machine ID or
|
|
11
|
+
# generated secret. Identity arrives per-VM in the `/run` hook payload.
|
|
12
|
+
# `src/image/dockerfile.test.ts` fails the build if this regresses.
|
|
13
|
+
#
|
|
14
|
+
# No `--platform` on FROM: AWS builds this natively on Graviton. Build it
|
|
15
|
+
# locally with `docker build --platform linux/arm64`.
|
|
16
|
+
#
|
|
17
|
+
# Node major tracks `.nvmrc` / the root `engines.node` (22) so the image runs
|
|
18
|
+
# the same runtime as CI — `evident-runner/src/node-version.test.ts` fails if
|
|
19
|
+
# they drift. The Debian codename is pinned explicitly (`bookworm`, what
|
|
20
|
+
# `node:22-slim` resolves to today) so a future default bump to a newer Debian
|
|
21
|
+
# can't silently change the apt package set this image asserts on below.
|
|
22
|
+
FROM node:22-bookworm-slim
|
|
23
|
+
|
|
24
|
+
ENV PNPM_HOME="/pnpm"
|
|
25
|
+
ENV PATH="$PNPM_HOME:$PATH"
|
|
26
|
+
RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
|
|
27
|
+
|
|
28
|
+
# System tooling + GitHub CLI + AWS CLI, in a single layer.
|
|
29
|
+
# - awscli (v2): required — durable-state persistence uses `aws s3`.
|
|
30
|
+
# - procps, postgresql(+contrib), direnv: required at RUNTIME so the VM is a
|
|
31
|
+
# full dev box for this repo (local Postgres on 5433 as the non-root runner,
|
|
32
|
+
# no sudo). Listed explicitly so apt keeps them.
|
|
33
|
+
# - gnupg, unzip: BUILD-ONLY (gh apt key / AWS CLI bundle); purged in this layer.
|
|
34
|
+
#
|
|
35
|
+
# Purge build-only packages BY NAME — do NOT use `apt-get autoremove`, which
|
|
36
|
+
# once cascaded into shared libs and swept `procps` out of the runner image. The
|
|
37
|
+
# binary assertion further down fails the build if any required binary is gone.
|
|
38
|
+
RUN apt-get update \
|
|
39
|
+
&& apt-get install -y --no-install-recommends \
|
|
40
|
+
ca-certificates curl git jq less procps ripgrep \
|
|
41
|
+
postgresql postgresql-contrib direnv \
|
|
42
|
+
gnupg unzip \
|
|
43
|
+
# --- GitHub CLI (`gh`) from the official apt repo ---
|
|
44
|
+
&& mkdir -p -m 755 /etc/apt/keyrings \
|
|
45
|
+
&& curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
|
|
46
|
+
-o /etc/apt/keyrings/githubcli-archive-keyring.gpg \
|
|
47
|
+
&& chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
|
|
48
|
+
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
|
|
49
|
+
> /etc/apt/sources.list.d/github-cli.list \
|
|
50
|
+
&& apt-get update \
|
|
51
|
+
&& apt-get install -y --no-install-recommends gh \
|
|
52
|
+
# --- AWS CLI v2 (aarch64 bundle; this image is arm64 only) ---
|
|
53
|
+
&& curl -fsSL "https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip" -o /tmp/awscliv2.zip \
|
|
54
|
+
&& unzip -q /tmp/awscliv2.zip -d /tmp \
|
|
55
|
+
&& /tmp/aws/install \
|
|
56
|
+
&& aws --version \
|
|
57
|
+
# --- Put the PostgreSQL server binaries on PATH ---
|
|
58
|
+
# On Debian initdb/pg_ctl/postgres/psql live under /usr/lib/postgresql/
|
|
59
|
+
# <ver>/bin, not on PATH; symlink them into /usr/local/bin.
|
|
60
|
+
&& ln -s /usr/lib/postgresql/*/bin/* /usr/local/bin/ 2>/dev/null || true \
|
|
61
|
+
# --- Drop the build-only packages (BY NAME, no autoremove) + caches ---
|
|
62
|
+
&& apt-get purge -y gnupg unzip \
|
|
63
|
+
&& rm -rf /tmp/aws /tmp/awscliv2.zip /var/lib/apt/lists/*
|
|
64
|
+
|
|
65
|
+
# Litestream — replicates the OpenCode SQLite session DB to S3 so history
|
|
66
|
+
# survives VM replacement. Pinned + checksum-verified: a wrong LITESTREAM_SHA256
|
|
67
|
+
# fails the build at `sha256sum -c`. The checksum is for the arm64 tarball and
|
|
68
|
+
# is NOT interchangeable with the ECS runner image's. Note the release tag keeps
|
|
69
|
+
# the `v` but the filename does not.
|
|
70
|
+
# `/run` restores opencode.db from S3 (#812 WI-2) — one bounded `litestream
|
|
71
|
+
# restore` attempt — then starts a backgrounded `litestream replicate` once
|
|
72
|
+
# opencode has opened the DB (WI-3); `/suspend`/`/resume`/`/terminate` flush,
|
|
73
|
+
# restart and stop it in turn (WI-4). Do not remove as dead code.
|
|
74
|
+
ARG LITESTREAM_VERSION=0.5.13
|
|
75
|
+
ARG LITESTREAM_SHA256=ef47997794ce8dd87a64b44622d556b3a693b135fd72e0cf47cc42ac2e979051
|
|
76
|
+
RUN curl -fsSL -o /tmp/litestream.tar.gz \
|
|
77
|
+
"https://github.com/benbjohnson/litestream/releases/download/v${LITESTREAM_VERSION}/litestream-${LITESTREAM_VERSION}-linux-arm64.tar.gz" \
|
|
78
|
+
&& echo "${LITESTREAM_SHA256} /tmp/litestream.tar.gz" | sha256sum -c - \
|
|
79
|
+
&& tar -C /usr/local/bin -xzf /tmp/litestream.tar.gz litestream \
|
|
80
|
+
&& chmod +x /usr/local/bin/litestream \
|
|
81
|
+
&& rm -f /tmp/litestream.tar.gz \
|
|
82
|
+
&& litestream version
|
|
83
|
+
|
|
84
|
+
# The AI agent runtime, the Evident CLI and the Claude Code CLI, installed
|
|
85
|
+
# globally and pinned via build args so image versions are reproducible.
|
|
86
|
+
# OPENCODE_VERSION is a DELIBERATE PIN, not a floating tag — see #537: a
|
|
87
|
+
# floating `opencode-ai@latest` install was implicated in unbounded runner
|
|
88
|
+
# memory growth / OOM kills. Keep it in lockstep with the ECS runner image's.
|
|
89
|
+
# EVIDENT_CLI_VERSION is DELIBERATELY the floating `dev` tag, matching the ECS
|
|
90
|
+
# runner image (infrastructure/evident-runner/src/base-image.ts). This is a
|
|
91
|
+
# development environment: it should track the head of the CLI so changes are
|
|
92
|
+
# testable without a version bump in every PR that touches a hook.
|
|
93
|
+
# RUNNER_SYNCHRONISER_VERSION floats on `dev` for the identical reason.
|
|
94
|
+
ARG OPENCODE_VERSION=1.18.3
|
|
95
|
+
ARG EVIDENT_CLI_VERSION=dev
|
|
96
|
+
ARG CLAUDE_CODE_VERSION=latest
|
|
97
|
+
ARG RUNNER_SYNCHRONISER_VERSION=dev
|
|
98
|
+
RUN npm install -g \
|
|
99
|
+
"opencode-ai@${OPENCODE_VERSION}" \
|
|
100
|
+
"@evident-ai/cli@${EVIDENT_CLI_VERSION}" \
|
|
101
|
+
"@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" \
|
|
102
|
+
"@evident-ai/runner-synchroniser@${RUNNER_SYNCHRONISER_VERSION}" \
|
|
103
|
+
&& opencode --version \
|
|
104
|
+
&& evident --version \
|
|
105
|
+
&& npm cache clean --force
|
|
106
|
+
|
|
107
|
+
# Playwright chromium + OS deps for E2E, baked at build time to avoid a slow
|
|
108
|
+
# first-boot download.
|
|
109
|
+
#
|
|
110
|
+
# CRITICAL: this version MUST match the Playwright version the repo resolves for
|
|
111
|
+
# `@playwright/test` (pnpm-lock.yaml → 1.58.0). The browser build is coupled to
|
|
112
|
+
# the package version; if it drifts, a boot-time `playwright install`
|
|
113
|
+
# re-downloads a different chromium. Bump in lockstep with @playwright/test.
|
|
114
|
+
#
|
|
115
|
+
# Browsers go to a SHARED PLAYWRIGHT_BROWSERS_PATH readable by the runtime
|
|
116
|
+
# runner user (uid 10001), not root's ~/.cache.
|
|
117
|
+
ENV PLAYWRIGHT_BROWSERS_PATH=/opt/ms-playwright
|
|
118
|
+
RUN npx playwright@1.58.0 install --with-deps chromium \
|
|
119
|
+
&& chown -R 10001:10001 /opt/ms-playwright \
|
|
120
|
+
# Assert the browsers landed in the shared path (guard against a silent failure).
|
|
121
|
+
&& test -n "$(ls -A /opt/ms-playwright 2>/dev/null)" \
|
|
122
|
+
|| { echo "FATAL: Playwright browsers dir /opt/ms-playwright missing or empty after install" >&2; exit 1; }
|
|
123
|
+
|
|
124
|
+
# Assert every REQUIRED runtime binary survived every layer above (fail loud).
|
|
125
|
+
# Placed after the last install so it covers the apt purge, the litestream
|
|
126
|
+
# extraction and the global npm installs in one pass.
|
|
127
|
+
RUN for bin in ps pgrep pkill curl aws gh git jq rg psql pg_ctl initdb direnv litestream runner-synchroniser opencode evident timeout; do \
|
|
128
|
+
command -v "$bin" >/dev/null 2>&1 || { echo "FATAL: required binary '$bin' missing from image" >&2; exit 1; }; \
|
|
129
|
+
done \
|
|
130
|
+
&& curl -fsS -o /dev/null https://cli.github.com \
|
|
131
|
+
|| { echo "FATAL: curl TLS smoke test failed (broken TLS libs)" >&2; exit 1; }
|
|
132
|
+
|
|
133
|
+
# Non-root user that owns the workspace. uid/gid pinned to 10001 for stable file
|
|
134
|
+
# ownership (opencode.db, ~/.claude/.credentials.json) across rebuilds.
|
|
135
|
+
RUN groupadd --system --gid 10001 runner \
|
|
136
|
+
&& useradd --system --uid 10001 --gid 10001 \
|
|
137
|
+
--create-home --home-dir /home/runner runner
|
|
138
|
+
|
|
139
|
+
# The hook server: the ONLY process this image starts, and it starts nothing
|
|
140
|
+
# itself until AWS calls a hook. It is an esbuild bundle of
|
|
141
|
+
# src/microvm/image/hook-server.ts, placed next to this Dockerfile by
|
|
142
|
+
# `stageMicrovmImageContext()` — which is why this file is not buildable
|
|
143
|
+
# straight from the source tree.
|
|
144
|
+
COPY hook-server.js /usr/local/lib/evident/hook-server.js
|
|
145
|
+
|
|
146
|
+
# One executable per lifecycle phase. There is deliberately no `ready` and no
|
|
147
|
+
# `validate` script: the snapshot is shared by every VM started from this image
|
|
148
|
+
# version, so nothing per-VM may run before /run — which leaves those two phases
|
|
149
|
+
# with nothing to do, and the runtime answers them 200 without running anything.
|
|
150
|
+
COPY hooks /etc/evident/hooks
|
|
151
|
+
|
|
152
|
+
# Workspace: the repository the agent works on, baked in. A resumed VM must be
|
|
153
|
+
# useful in milliseconds, and a clone + install costs minutes — so both happen
|
|
154
|
+
# here, and the image is deliberately large in exchange.
|
|
155
|
+
#
|
|
156
|
+
# This does not weaken the boot split: source, git history and node_modules are
|
|
157
|
+
# neither secret nor per-VM-unique, so sharing them across every VM from this
|
|
158
|
+
# snapshot is exactly right. No credential is involved either — the repo travels
|
|
159
|
+
# in the build context (cloned by `stageMicrovmImageContext()` from a local
|
|
160
|
+
# checkout), not through a `git clone` needing a token. `origin` is the plain
|
|
161
|
+
# HTTPS GitHub URL; the credential that makes it fetchable and pushable arrives
|
|
162
|
+
# per-VM, so the agent is what brings this checkout up to date.
|
|
163
|
+
ENV WORKSPACE=/workspace
|
|
164
|
+
ENV HOME=/home/runner
|
|
165
|
+
# Runner-owned PGDATA parent for the local dev Postgres cluster (initdb +
|
|
166
|
+
# pg_ctl at boot, no sudo).
|
|
167
|
+
#
|
|
168
|
+
# The machine-id files are created EMPTY and runner-owned so that /run can
|
|
169
|
+
# actually rewrite them: the hooks run as uid 10001, and neither `/etc` nor
|
|
170
|
+
# `/var/lib/dbus` is writable by it, so without this the regeneration is a
|
|
171
|
+
# permanent no-op and every VM from this snapshot shares one machine id. Empty
|
|
172
|
+
# is the correct unset state — it is the absence of an identity, so nothing
|
|
173
|
+
# per-VM-unique enters the shared snapshot.
|
|
174
|
+
RUN mkdir -p /var/lib/runner-pg /var/lib/dbus \
|
|
175
|
+
&& install -o runner -g runner -m 0644 /dev/null /etc/machine-id \
|
|
176
|
+
&& install -o runner -g runner -m 0644 /dev/null /var/lib/dbus/machine-id \
|
|
177
|
+
&& chown -R runner:runner /var/lib/runner-pg /home/runner
|
|
178
|
+
COPY --chown=10001:10001 repo ${WORKSPACE}
|
|
179
|
+
WORKDIR ${WORKSPACE}
|
|
180
|
+
USER runner
|
|
181
|
+
|
|
182
|
+
# The staged context deliberately ships no `.git/index` (it is not byte-stable,
|
|
183
|
+
# so shipping it would re-roll the image version on every deploy) — without this
|
|
184
|
+
# reset the agent's workspace reports ~1400 files as staged deletions. Runs as
|
|
185
|
+
# `runner` (not root) so the rebuilt index is runner-owned and writable later.
|
|
186
|
+
RUN git reset --quiet
|
|
187
|
+
|
|
188
|
+
# Dependencies and the shared builds everything else depends on. As the runner
|
|
189
|
+
# user, so node_modules is writable by the agent's own later installs.
|
|
190
|
+
#
|
|
191
|
+
# ONE layer on purpose: pnpm hard-links node_modules into its content-addressed
|
|
192
|
+
# store (which it puts at ${WORKSPACE}/.pnpm-store, gitignored), and hard links
|
|
193
|
+
# only survive within a single layer — split in two and the image carries both
|
|
194
|
+
# copies. `--frozen-lockfile` because the committed lockfile is the whole reason
|
|
195
|
+
# this is bakeable at all: installing against anything else would bake a
|
|
196
|
+
# dependency set nobody committed.
|
|
197
|
+
#
|
|
198
|
+
# Guarded on a committed `pnpm-lock.yaml` so `microvm:repositoryPath` can point
|
|
199
|
+
# at a repository that is not a pnpm workspace at all: it is baked uninstalled
|
|
200
|
+
# instead of failing the build, and the agent installs on first use.
|
|
201
|
+
#
|
|
202
|
+
# Nothing environment-specific is built: `vite build` inlines VITE_* at build
|
|
203
|
+
# time and the migrations need a running Postgres, so the app builds and the dev
|
|
204
|
+
# database stay per-VM (see entrypoint.sh's pre-warm in evident-runner).
|
|
205
|
+
RUN if [ -f pnpm-lock.yaml ]; then \
|
|
206
|
+
pnpm install --frozen-lockfile \
|
|
207
|
+
&& pnpm run build --filter='./packages/*'; \
|
|
208
|
+
else \
|
|
209
|
+
echo "[workspace-prep] no pnpm-lock.yaml in the baked repository — skipping the dependency install and the packages build; the agent installs on first use."; \
|
|
210
|
+
fi
|
|
211
|
+
|
|
212
|
+
# Data dir for the local dev Postgres cluster; listens on 5433 to match this
|
|
213
|
+
# repo's .envrc DSN postgres://postgres:postgres@localhost:5433/evident.
|
|
214
|
+
ENV PGDATA=/var/lib/runner-pg/data
|
|
215
|
+
# Port AWS calls the MicroVM hooks on.
|
|
216
|
+
ENV HOOKS_PORT=8080
|
|
217
|
+
# OpenCode loopback port the CLI tunnels to (matches `evident run` default).
|
|
218
|
+
ENV OPENCODE_PORT=4096
|
|
219
|
+
|
|
220
|
+
EXPOSE 8080
|
|
221
|
+
|
|
222
|
+
# No ENTRYPOINT shell: the hook server is started directly, and it must be the
|
|
223
|
+
# only thing running until AWS calls /run.
|
|
224
|
+
CMD ["node", "/usr/local/lib/evident/hook-server.js"]
|