@evident-ai/runner-cdk 3.4.1-dev.c463782 → 3.4.1-dev.d4a79de

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 CHANGED
@@ -165,6 +165,7 @@ and the agent installs on first use.
165
165
  | `runnerSecret` | No GitHub or MCP credentials are exported at `/run`. When supplied, `/run` reads the JSON secret and exports every non-empty value whose key is a valid environment-variable name. |
166
166
  | `runnerOpencodeConfigPath` | OpenCode uses the baked project configuration. Relative paths resolve from the workspace; absolute paths resolve in the image. |
167
167
  | `gitUserName` / `gitUserEmail` | The hook uses the Evident bot defaults for git identity. |
168
+ | `evidentApiUrl` / `evidentRunnerId` | No deploy-time report of the published image version — a byte-identical template to not having these props at all. Both non-secret (a URL, a public pool UUID) and both required together: set them if you have an Evident runner id configured for this provisioner, so each deploy that publishes a new image version tells Evident about it (a CloudFormation custom resource, HMAC-signed with `doorbellSecret`). A failure to report never fails your deploy — it warns and Evident reads that provisioner as *unknown*, never as up to date or stale. |
168
169
 
169
170
  ## Status / limitations
170
171
 
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/microvm/image-version-reporter/handler.ts
21
+ var handler_exports = {};
22
+ __export(handler_exports, {
23
+ handleImageVersionReport: () => handleImageVersionReport,
24
+ handler: () => handler
25
+ });
26
+ module.exports = __toCommonJS(handler_exports);
27
+ var import_node_crypto = require("node:crypto");
28
+ var import_client_secrets_manager = require("@aws-sdk/client-secrets-manager");
29
+ var PHYSICAL_RESOURCE_ID = "evident-image-version-report";
30
+ var REPORT_TIMEOUT_MS = 5e3;
31
+ var secrets = new import_client_secrets_manager.SecretsManagerClient({});
32
+ async function fetchDoorbellSecret(secretArn, secretKey) {
33
+ const { SecretString } = await secrets.send(new import_client_secrets_manager.GetSecretValueCommand({ SecretId: secretArn }));
34
+ if (!SecretString) {
35
+ throw new Error(`doorbell secret ${secretArn} has no SecretString`);
36
+ }
37
+ const parsed = JSON.parse(SecretString);
38
+ const value = parsed !== null && typeof parsed === "object" ? parsed[secretKey] : void 0;
39
+ if (typeof value !== "string" || value === "") {
40
+ throw new Error(`doorbell secret ${secretArn} is missing a non-empty '${secretKey}' field`);
41
+ }
42
+ return value;
43
+ }
44
+ function requireEnv(name) {
45
+ const value = process.env[name];
46
+ if (!value) {
47
+ throw new Error(`missing required env var ${name}`);
48
+ }
49
+ return value;
50
+ }
51
+ function readImageVersions(value) {
52
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
53
+ throw new Error("imageVersions must be an object");
54
+ }
55
+ return Object.entries(value).map(([shape, imageVersion]) => {
56
+ if (typeof imageVersion !== "string") {
57
+ throw new Error(`imageVersions.${shape} must be a string`);
58
+ }
59
+ return { shape, image_version: imageVersion };
60
+ });
61
+ }
62
+ function response() {
63
+ return { PhysicalResourceId: PHYSICAL_RESOURCE_ID };
64
+ }
65
+ var runtimeDependencies = {
66
+ getSecret: fetchDoorbellSecret,
67
+ fetch: globalThis.fetch
68
+ };
69
+ async function handleImageVersionReport(event, getConfig, dependencies = runtimeDependencies) {
70
+ if (event.RequestType === "Delete") {
71
+ return response();
72
+ }
73
+ let runnerId = "unknown";
74
+ let versions = [];
75
+ let responseStatus;
76
+ try {
77
+ versions = readImageVersions(event.ResourceProperties?.imageVersions);
78
+ const config = getConfig();
79
+ runnerId = config.evidentRunnerId;
80
+ const secret = await dependencies.getSecret(config.doorbellSecretArn, config.doorbellSecretKey);
81
+ const body = JSON.stringify({
82
+ type: "runner.microvm_image_versions_reported",
83
+ versions
84
+ });
85
+ const signature = (0, import_node_crypto.createHmac)("sha256", secret).update(body).digest("hex");
86
+ const url = `${config.evidentApiUrl.replace(/\/+$/, "")}/v1/runners/${encodeURIComponent(
87
+ runnerId
88
+ )}/microvm-image-versions`;
89
+ const result = await dependencies.fetch(url, {
90
+ method: "POST",
91
+ headers: {
92
+ "content-type": "application/json",
93
+ "x-evident-signature": signature
94
+ },
95
+ body,
96
+ signal: AbortSignal.timeout(REPORT_TIMEOUT_MS)
97
+ });
98
+ responseStatus = result.status;
99
+ console.log("runner.microvm_image_versions_reported", {
100
+ runnerId,
101
+ versions,
102
+ status: result.status
103
+ });
104
+ if (!result.ok) {
105
+ throw new Error(`Evident returned HTTP ${result.status}`);
106
+ }
107
+ } catch (error) {
108
+ const message = error instanceof Error ? error.message : String(error);
109
+ console.warn("MicroVM image version report failed", {
110
+ operation: event.RequestType,
111
+ runnerId,
112
+ versions,
113
+ ...responseStatus === void 0 ? {} : { status: responseStatus },
114
+ error: message
115
+ });
116
+ }
117
+ return response();
118
+ }
119
+ var handler = (event) => handleImageVersionReport(event, () => ({
120
+ doorbellSecretArn: requireEnv("DOORBELL_SECRET_ARN"),
121
+ doorbellSecretKey: requireEnv("DOORBELL_SECRET_KEY"),
122
+ evidentApiUrl: requireEnv("EVIDENT_API_URL"),
123
+ evidentRunnerId: requireEnv("EVIDENT_RUNNER_ID")
124
+ }));
125
+ // Annotate the CommonJS export names for ESM import in node:
126
+ 0 && (module.exports = {
127
+ handleImageVersionReport,
128
+ handler
129
+ });
@@ -69,6 +69,24 @@ export interface EvidentMicrovmConstructProps {
69
69
  * running a hook).
70
70
  */
71
71
  readonly hookTimeoutSeconds: number;
72
+ /**
73
+ * Base URL of Evident's own API. Together with `evidentRunnerId`, opts this
74
+ * deploy into reporting each shape's published image version to Evident
75
+ * (#1903) — a CDK custom resource, triggered only when a deploy actually
76
+ * changes a version. Omitting BOTH leaves the template byte-for-byte
77
+ * identical to not having this prop at all: an adopter with no Evident
78
+ * runner id configured has nothing to report against. Supplying only one
79
+ * throws at synth — see `evidentRunnerId`.
80
+ */
81
+ readonly evidentApiUrl?: string;
82
+ /**
83
+ * The pool runner id (public, non-secret UUID) to report against — see
84
+ * `evidentApiUrl`. Mirrors `EvidentScaleToZeroConstructProps.evidentAgentId`'s
85
+ * "public (non-secret) Evident agent UUID" precedent. Must be supplied
86
+ * together with `evidentApiUrl`, or neither at all — supplying exactly one
87
+ * throws at construction rather than silently disabling the reporter.
88
+ */
89
+ readonly evidentRunnerId?: string;
72
90
  }
73
91
  /**
74
92
  * The customer-account half of the per-session runner (#558): the MicroVM
@@ -41,6 +41,7 @@ const lambda = __importStar(require("aws-cdk-lib/aws-lambda"));
41
41
  const s3 = __importStar(require("aws-cdk-lib/aws-s3"));
42
42
  const lambda_microvm_cdk_1 = require("@evident-ai/lambda-microvm-cdk");
43
43
  const shapes_1 = require("./shapes");
44
+ const construct_1 = require("./image-version-reporter/construct");
44
45
  // Image hooks build the snapshot, so they get minutes where the runtime hooks
45
46
  // get AWS's 60 s.
46
47
  const IMAGE_HOOK_TIMEOUT_SECONDS = 600;
@@ -65,6 +66,15 @@ class EvidentMicrovmConstruct extends constructs_1.Construct {
65
66
  // Synth-time failure, before any construct exists.
66
67
  const shapes = (0, shapes_1.validateShapes)(props.shapes ?? shapes_1.MICROVM_SHAPES);
67
68
  const [defaultShape, ...additionalShapes] = shapes;
69
+ // D8's "both required together" is a synth-time contract, not just a
70
+ // README sentence: a caller supplying exactly one silently gets NO
71
+ // reporter and no warning (the `&&` check below just skips it), which
72
+ // reads as "configured" when it is not. Fail loudly instead.
73
+ if (Boolean(props.evidentApiUrl) !== Boolean(props.evidentRunnerId)) {
74
+ throw new Error('EvidentMicrovmConstruct: evidentApiUrl and evidentRunnerId must be supplied ' +
75
+ 'together or not at all — supplying only one silently disables the image ' +
76
+ 'version reporter rather than reporting a misconfiguration.');
77
+ }
68
78
  // The model credentials a runner restores at /run, and the litestream
69
79
  // session replica (it is `LITESTREAM_BUCKET` below), share one per-runner
70
80
  // prefix chosen at /run — synth can't name it, and lifecycle filters take
@@ -249,6 +259,22 @@ class EvidentMicrovmConstruct extends constructs_1.Construct {
249
259
  // HMAC verification of the doorbell body IS the auth, as for the ECS waker.
250
260
  authType: lambda.FunctionUrlAuthType.NONE,
251
261
  });
262
+ // Opt-in only (D8): both values present is what creates the reporter, so an
263
+ // adopter who supplies neither gets a template with no additional Lambda and
264
+ // no custom resource — this branch must not run for them.
265
+ if (props.evidentApiUrl && props.evidentRunnerId) {
266
+ new construct_1.ImageVersionReporter(this, 'ImageVersionReporter', {
267
+ doorbellSecret: props.doorbellSecret,
268
+ evidentApiUrl: props.evidentApiUrl,
269
+ evidentRunnerId: props.evidentRunnerId,
270
+ imageVersions: Object.fromEntries(shapes.map((shape) => [shape.name, images.get(shape.name).latestActiveImageVersion])),
271
+ // `buildInputs` already covers every property that can cause AWS
272
+ // to publish a new version for THIS shape's image (source, base
273
+ // image, memory, hooks, environment, description, build role —
274
+ // see its docstring); no additional encoding needed here.
275
+ buildTriggers: Object.fromEntries(shapes.map((shape) => [shape.name, images.get(shape.name).buildInputs])),
276
+ });
277
+ }
252
278
  this.functionUrl = functionUrl.url;
253
279
  this.shapeCatalogueJson = shapeCatalogueJson;
254
280
  this.durableStateBucket = durableState;
@@ -0,0 +1,35 @@
1
+ import { Construct } from 'constructs';
2
+ import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
3
+ import type { MicrovmImageBuildInputs } from '@evident-ai/lambda-microvm-cdk';
4
+ export type ImageVersionReporterProps = {
5
+ /** The secret whose value is the same HMAC key the controller verifies every doorbell against. */
6
+ doorbellSecret: secretsmanager.ISecret;
7
+ /** Base URL of Evident's own API — non-secret. */
8
+ evidentApiUrl: string;
9
+ /** The pool runner id the report is filed against — non-secret, public UUID. */
10
+ evidentRunnerId: string;
11
+ /** Shape name -> the `Fn::GetAtt LatestActiveImageVersion` published this deploy. */
12
+ imageVersions: Record<string, string>;
13
+ /**
14
+ * Shape name -> {@link MicrovmImageBuildInputs}, a STRUCTURED object (never
15
+ * a delimiter-joined string — see its docstring for why flattening is
16
+ * unsafe) that changes whenever THIS deploy's build inputs change. Unlike
17
+ * `imageVersions`' `Fn::GetAtt` (a runtime attribute of the MicroVM image
18
+ * service, whose deploy-time propagation to a dependent resource this
19
+ * construct cannot itself guarantee), every field here is either a
20
+ * synth-time-computed literal or a plain `Ref`/`Fn::GetAtt` to a resource
21
+ * IN THIS STACK — CloudFormation diffs the full resolved property tree, so
22
+ * a change anywhere in this object is a certain, not merely likely,
23
+ * re-invocation trigger.
24
+ */
25
+ buildTriggers: Record<string, MicrovmImageBuildInputs>;
26
+ };
27
+ /**
28
+ * Deploy-time push: reports each shape's just-published MicroVM image version to
29
+ * Evident, once per deploy that actually changes it. `imageVersions` is the
30
+ * payload; `buildTriggers` is what guarantees re-invocation on exactly the
31
+ * deploy that matters, independent of `imageVersions`' own resolution timing.
32
+ */
33
+ export declare class ImageVersionReporter extends Construct {
34
+ constructor(scope: Construct, id: string, props: ImageVersionReporterProps);
35
+ }
@@ -0,0 +1,91 @@
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.ImageVersionReporter = void 0;
37
+ const path = __importStar(require("path"));
38
+ const cdk = __importStar(require("aws-cdk-lib"));
39
+ const constructs_1 = require("constructs");
40
+ const lambda = __importStar(require("aws-cdk-lib/aws-lambda"));
41
+ const cr = __importStar(require("aws-cdk-lib/custom-resources"));
42
+ /** The JSON field name inside the doorbell secret, same key the controller reads. */
43
+ const DOORBELL_SECRET_KEY = 'DOORBELL_SECRET';
44
+ /**
45
+ * Deploy-time push: reports each shape's just-published MicroVM image version to
46
+ * Evident, once per deploy that actually changes it. `imageVersions` is the
47
+ * payload; `buildTriggers` is what guarantees re-invocation on exactly the
48
+ * deploy that matters, independent of `imageVersions`' own resolution timing.
49
+ */
50
+ class ImageVersionReporter extends constructs_1.Construct {
51
+ constructor(scope, id, props) {
52
+ super(scope, id);
53
+ const handler = new lambda.Function(this, 'Function', {
54
+ runtime: lambda.Runtime.NODEJS_22_X,
55
+ // Pre-bundled at PACKAGE build time, same reasoning as the waker's and the
56
+ // controller's own `Code.fromAsset` (see their comments) — resolved from
57
+ // the PACKAGE ROOT, two directories below this file either compiled
58
+ // (`dist/microvm/image-version-reporter`) or via ts-node
59
+ // (`src/microvm/image-version-reporter`).
60
+ code: lambda.Code.fromAsset(path.join(__dirname, '..', '..', '..', 'dist', 'image-version-reporter-lambda')),
61
+ handler: 'handler.handler',
62
+ memorySize: 128,
63
+ // A deploy hook must not hold CloudFormation for its default 1 h — this is
64
+ // the hard stop; the handler's own internal REPORT_TIMEOUT_MS is well under it.
65
+ timeout: cdk.Duration.seconds(30),
66
+ environment: {
67
+ // ARN + field name only, never the value (mirrors the controller and waker).
68
+ DOORBELL_SECRET_ARN: props.doorbellSecret.secretArn,
69
+ DOORBELL_SECRET_KEY: DOORBELL_SECRET_KEY,
70
+ EVIDENT_API_URL: props.evidentApiUrl,
71
+ EVIDENT_RUNNER_ID: props.evidentRunnerId,
72
+ },
73
+ });
74
+ props.doorbellSecret.grantRead(handler);
75
+ const provider = new cr.Provider(this, 'Provider', {
76
+ onEventHandler: handler,
77
+ });
78
+ new cdk.CustomResource(this, 'Resource', {
79
+ serviceToken: provider.serviceToken,
80
+ properties: {
81
+ imageVersions: props.imageVersions,
82
+ // Not read by the handler — its only job is to be a property value
83
+ // CloudFormation can see change (a structured object, so any field
84
+ // changing anywhere is a real property diff), so the resource is
85
+ // invoked on exactly the deploy that changes a build input.
86
+ buildTriggers: props.buildTriggers,
87
+ },
88
+ });
89
+ }
90
+ }
91
+ exports.ImageVersionReporter = ImageVersionReporter;
@@ -0,0 +1,26 @@
1
+ export type ImageVersionPair = {
2
+ shape: string;
3
+ image_version: string;
4
+ };
5
+ export type ReporterEvent = {
6
+ RequestType: 'Create' | 'Update' | 'Delete';
7
+ ResourceProperties?: {
8
+ imageVersions?: unknown;
9
+ };
10
+ };
11
+ export type ReporterConfig = {
12
+ doorbellSecretArn: string;
13
+ doorbellSecretKey: string;
14
+ evidentApiUrl: string;
15
+ evidentRunnerId: string;
16
+ };
17
+ type ReporterDependencies = {
18
+ getSecret: (secretArn: string, secretKey: string) => Promise<string>;
19
+ fetch: typeof fetch;
20
+ };
21
+ type ReporterResponse = {
22
+ PhysicalResourceId: string;
23
+ };
24
+ export declare function handleImageVersionReport(event: ReporterEvent, getConfig: () => ReporterConfig, dependencies?: ReporterDependencies): Promise<ReporterResponse>;
25
+ export declare const handler: (event: ReporterEvent) => Promise<ReporterResponse>;
26
+ export {};
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handler = void 0;
4
+ exports.handleImageVersionReport = handleImageVersionReport;
5
+ const node_crypto_1 = require("node:crypto");
6
+ const client_secrets_manager_1 = require("@aws-sdk/client-secrets-manager");
7
+ const PHYSICAL_RESOURCE_ID = 'evident-image-version-report';
8
+ const REPORT_TIMEOUT_MS = 5_000;
9
+ const secrets = new client_secrets_manager_1.SecretsManagerClient({});
10
+ async function fetchDoorbellSecret(secretArn, secretKey) {
11
+ const { SecretString } = await secrets.send(new client_secrets_manager_1.GetSecretValueCommand({ SecretId: secretArn }));
12
+ if (!SecretString) {
13
+ throw new Error(`doorbell secret ${secretArn} has no SecretString`);
14
+ }
15
+ const parsed = JSON.parse(SecretString);
16
+ const value = parsed !== null && typeof parsed === 'object'
17
+ ? parsed[secretKey]
18
+ : undefined;
19
+ if (typeof value !== 'string' || value === '') {
20
+ throw new Error(`doorbell secret ${secretArn} is missing a non-empty '${secretKey}' field`);
21
+ }
22
+ return value;
23
+ }
24
+ function requireEnv(name) {
25
+ const value = process.env[name];
26
+ if (!value) {
27
+ throw new Error(`missing required env var ${name}`);
28
+ }
29
+ return value;
30
+ }
31
+ function readImageVersions(value) {
32
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
33
+ throw new Error('imageVersions must be an object');
34
+ }
35
+ return Object.entries(value).map(([shape, imageVersion]) => {
36
+ if (typeof imageVersion !== 'string') {
37
+ throw new Error(`imageVersions.${shape} must be a string`);
38
+ }
39
+ return { shape, image_version: imageVersion };
40
+ });
41
+ }
42
+ function response() {
43
+ return { PhysicalResourceId: PHYSICAL_RESOURCE_ID };
44
+ }
45
+ const runtimeDependencies = {
46
+ getSecret: fetchDoorbellSecret,
47
+ fetch: globalThis.fetch,
48
+ };
49
+ async function handleImageVersionReport(event, getConfig, dependencies = runtimeDependencies) {
50
+ if (event.RequestType === 'Delete') {
51
+ return response();
52
+ }
53
+ let runnerId = 'unknown';
54
+ let versions = [];
55
+ let responseStatus;
56
+ try {
57
+ versions = readImageVersions(event.ResourceProperties?.imageVersions);
58
+ const config = getConfig();
59
+ runnerId = config.evidentRunnerId;
60
+ const secret = await dependencies.getSecret(config.doorbellSecretArn, config.doorbellSecretKey);
61
+ const body = JSON.stringify({
62
+ type: 'runner.microvm_image_versions_reported',
63
+ versions,
64
+ });
65
+ const signature = (0, node_crypto_1.createHmac)('sha256', secret).update(body).digest('hex');
66
+ const url = `${config.evidentApiUrl.replace(/\/+$/, '')}/v1/runners/${encodeURIComponent(runnerId)}/microvm-image-versions`;
67
+ const result = await dependencies.fetch(url, {
68
+ method: 'POST',
69
+ headers: {
70
+ 'content-type': 'application/json',
71
+ 'x-evident-signature': signature,
72
+ },
73
+ body,
74
+ signal: AbortSignal.timeout(REPORT_TIMEOUT_MS),
75
+ });
76
+ responseStatus = result.status;
77
+ console.log('runner.microvm_image_versions_reported', {
78
+ runnerId,
79
+ versions,
80
+ status: result.status,
81
+ });
82
+ if (!result.ok) {
83
+ throw new Error(`Evident returned HTTP ${result.status}`);
84
+ }
85
+ }
86
+ catch (error) {
87
+ const message = error instanceof Error ? error.message : String(error);
88
+ console.warn('MicroVM image version report failed', {
89
+ operation: event.RequestType,
90
+ runnerId,
91
+ versions,
92
+ ...(responseStatus === undefined ? {} : { status: responseStatus }),
93
+ error: message,
94
+ });
95
+ }
96
+ return response();
97
+ }
98
+ const handler = (event) => handleImageVersionReport(event, () => ({
99
+ doorbellSecretArn: requireEnv('DOORBELL_SECRET_ARN'),
100
+ doorbellSecretKey: requireEnv('DOORBELL_SECRET_KEY'),
101
+ evidentApiUrl: requireEnv('EVIDENT_API_URL'),
102
+ evidentRunnerId: requireEnv('EVIDENT_RUNNER_ID'),
103
+ }));
104
+ exports.handler = handler;
@@ -95,13 +95,23 @@ ARG OPENCODE_VERSION=1.18.3
95
95
  ARG EVIDENT_CLI_VERSION=dev
96
96
  ARG CLAUDE_CODE_VERSION=latest
97
97
  ARG RUNNER_SYNCHRONISER_VERSION=dev
98
+ #
99
+ # @brave/brave-search-mcp-server is pinned (not a build arg — a single caller,
100
+ # opencode.evident.jsonc, needs one version) and baked here for the same reason
101
+ # as the ECS runner image: a cold `npx -y` install of it took ~38s in a fresh
102
+ # MicroVM, over OpenCode's ~30s MCP connect timeout, so the server silently
103
+ # failed to connect on every first turn after a boot. Baking it lets
104
+ # opencode.evident.jsonc invoke the installed `brave-search-mcp-server` binary
105
+ # directly instead of through `npx`.
98
106
  RUN npm install -g \
99
107
  "opencode-ai@${OPENCODE_VERSION}" \
100
108
  "@evident-ai/cli@${EVIDENT_CLI_VERSION}" \
101
109
  "@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" \
102
110
  "@evident-ai/runner-synchroniser@${RUNNER_SYNCHRONISER_VERSION}" \
111
+ "@brave/brave-search-mcp-server@2.1.0" \
103
112
  && opencode --version \
104
113
  && evident --version \
114
+ && brave-search-mcp-server --help >/dev/null \
105
115
  && npm cache clean --force
106
116
 
107
117
  # Playwright chromium + OS deps for E2E, baked at build time to avoid a slow
@@ -21,6 +21,12 @@ OPENCODE_PORT="${OPENCODE_PORT:-4096}"
21
21
  # /proc/<pid>/environ, which runs as that same uid. /terminate removes it.
22
22
  # shellcheck disable=SC2034 # read by the scripts that source this file
23
23
  CONTEXT_FILE="/dev/shm/evident-run-context"
24
+
25
+ # The runtime injects MICROVM_ID only into /run, never /resume. This tmpfs file
26
+ # survives suspend/resume so /resume can restore the id for every fresh CLI
27
+ # process to self-report and acknowledge a fulfilled recycle request (#1906).
28
+ # shellcheck disable=SC2034 # read by the scripts that source this file
29
+ MICROVM_ID_FILE="/dev/shm/evident-microvm-id"
24
30
  TUNNEL_PID_FILE="/dev/shm/evident-tunnel.pid"
25
31
  OPENCODE_PID_FILE="/dev/shm/evident-opencode.pid"
26
32
  LITESTREAM_PID_FILE="/dev/shm/evident-litestream.pid"
@@ -52,19 +58,20 @@ log() { echo "[hook:$(hook_name)] $*"; }
52
58
  warn() { echo "[hook:$(hook_name)] $*" >&2; }
53
59
  error() { echo "[hook:$(hook_name)] ERROR: $*" >&2; }
54
60
 
55
- # Returns the CLI's own exit code. Domain outcomes (nothing persisted yet, a
56
- # corrupt object) are LOGGED and exit 0, the predicates answer "no" with 10, and
61
+ # Returns the CLI's own exit code. `restore` logs domain outcomes and returns 0;
62
+ # a non-zero status means the tool itself failed. `sync-once` returns 40 for
63
+ # `failed`, `hashFailed`, and `localInvalid` (credentials not persisted), and 0
64
+ # for every other outcome. The predicates answer "no" with 10, and
57
65
  # `session-db-classify`'s three typed answers are 30 (fatal)/31 (replica
58
66
  # unusable)/32 (retry), extended by `session-db-verify`'s 33 (integrity
59
67
  # exhausted, replica separated and local disposed) / 34 (could not prove
60
- # separation or disposal) see `runner/synchroniser/src/cli.ts`'s own
61
- # comment for what each means, not restated here. Any OTHER non-zero status
68
+ # separation or disposal). Any status outside a command's contractual answers
62
69
  # means the tool itself broke, which is the only case worth an ERROR here.
63
70
  run_synchroniser() {
64
71
  local rc=0
65
72
  "${SYNCHRONISER}" "$@" || rc=$?
66
73
  case "${rc}" in
67
- 0 | 10 | 30 | 31 | 32 | 33 | 34) ;;
74
+ 0 | 10 | 30 | 31 | 32 | 33 | 34 | 40) ;;
68
75
  *) error "synchroniser '$*' exited ${rc}; the '${SYNCHRONISER}' command is missing from PATH, corrupt, or it threw" ;;
69
76
  esac
70
77
  return "${rc}"
@@ -39,6 +39,13 @@ fi
39
39
  read -r runner_key
40
40
  } <"${CONTEXT_FILE}"
41
41
 
42
+ # Restore the id so the resumed CLI can self-report and acknowledge any
43
+ # outstanding recycle request (#1906). Older images may not have this file.
44
+ if [ -s "${MICROVM_ID_FILE}" ]; then
45
+ MICROVM_ID="$(cat "${MICROVM_ID_FILE}")"
46
+ export MICROVM_ID
47
+ fi
48
+
42
49
  # Tolerant, never fatal: a resume that fails costs the user their whole
43
50
  # session, so a broken durable-state config degrades to "no session-DB
44
51
  # replication" rather than a failed resume. The CLI's own guards handle the
@@ -77,7 +77,14 @@ check_runner_key "${runner_key}" "${api_url}" || exit 1
77
77
  # is written. Bare: every guard inside start_credential_sync is its own `return 0`.
78
78
  start_credential_sync
79
79
 
80
- # 7 — the first per-VM identity on the wire. The subshell's umask makes the file
80
+ # 7 — preserve the VM id across suspend/resume. The runtime always supplies it
81
+ # for a /run hook, and the subshell keeps the persisted value protected.
82
+ (
83
+ umask 077
84
+ printf '%s\n' "${MICROVM_ID}" >"${MICROVM_ID_FILE}"
85
+ )
86
+
87
+ # 8 — the first per-VM identity on the wire. The subshell's umask makes the file
81
88
  # unreadable to anyone else from the moment it exists, before the key is in it.
82
89
  (
83
90
  umask 077
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evident-ai/runner-cdk",
3
- "version": "3.4.1-dev.c463782",
3
+ "version": "3.4.1-dev.d4a79de",
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",