@evident-ai/runner-cdk 0.1.1-dev.da88f8e → 3.4.1-dev.0ef5061
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 +24 -14
- package/dist/controller-lambda/handler.js +14 -8
- package/dist/evident-scale-to-zero-construct.d.ts +16 -4
- package/dist/evident-scale-to-zero-construct.js +19 -14
- package/dist/image-version-reporter-lambda/handler.js +129 -0
- package/dist/microvm/constants.js +2 -3
- package/dist/microvm/construct.d.ts +34 -2
- package/dist/microvm/construct.js +38 -0
- package/dist/microvm/controller/handle-doorbell.js +11 -8
- package/dist/microvm/controller/microvm-client.d.ts +7 -1
- package/dist/microvm/controller/shape-catalogue.d.ts +1 -1
- package/dist/microvm/controller/shape-catalogue.js +1 -1
- package/dist/microvm/image-version-reporter/construct.d.ts +35 -0
- package/dist/microvm/image-version-reporter/construct.js +91 -0
- package/dist/microvm/image-version-reporter/handler.d.ts +26 -0
- package/dist/microvm/image-version-reporter/handler.js +104 -0
- package/dist/microvm-image-context/Dockerfile +10 -7
- package/dist/microvm-image-context/hook-server.js +2 -6
- package/dist/microvm-image-context/hooks/common.sh +247 -545
- package/dist/microvm-image-context/hooks/resume +9 -10
- package/dist/microvm-image-context/hooks/run +16 -33
- package/dist/microvm-image-context/hooks/suspend +3 -0
- package/dist/microvm-image-context/hooks/terminate +3 -0
- package/dist/waker/construct.js +1 -1
- package/package.json +10 -8
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. Installing this package is all you
|
|
13
|
-
context ships inside it (see [The MicroVM image](#the-microvm-image)
|
|
14
|
-
no checkout of `sroze/evident` involved. See
|
|
12
|
+
and suspends between messages. Installing the `@dev` tag of this package is all you
|
|
13
|
+
need: the image build context ships inside it (see [The MicroVM image](#the-microvm-image)
|
|
14
|
+
below), so there is 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
|
|
|
@@ -34,8 +34,9 @@ cluster can host several agents.
|
|
|
34
34
|
## Getting started
|
|
35
35
|
|
|
36
36
|
For a full end-to-end walkthrough — shared infra, secrets without SOPS/KMS, the image,
|
|
37
|
-
the two grants, deploy, and wiring the wake webhook — see
|
|
38
|
-
|
|
37
|
+
the two grants, deploy, and wiring the wake webhook — see `GETTING-STARTED.md` in this
|
|
38
|
+
package's directory (not published to npm; read it from a checkout of the repo). The
|
|
39
|
+
snippet below is the short version.
|
|
39
40
|
|
|
40
41
|
```ts
|
|
41
42
|
import { EvidentScaleToZeroConstruct } from "@evident-ai/runner-cdk";
|
|
@@ -96,7 +97,7 @@ the construct's own dogfooding consumer.
|
|
|
96
97
|
| `cluster`, `securityGroup`, `taskRole`, `logGroup`, `replicaBucket` | interfaces | Shared infra you create and pass in. |
|
|
97
98
|
| `image` | `ecs.ContainerImage` | Your runner image. |
|
|
98
99
|
| `agentSecret`, `wakerSecret` | `secretsmanager.ISecret` | Any Secrets Manager secret. |
|
|
99
|
-
| `availableSecretKeys` | `Set<string>` | Keys present in the agent secret.
|
|
100
|
+
| `availableSecretKeys` | `Set<string>` | Keys present in the agent secret. Every listed key is injected; `EVIDENT_AGENT_KEY` and `GH_TOKEN` are also injected for compatibility with adopters that do not enumerate their secret. |
|
|
100
101
|
|
|
101
102
|
### Two deliberate "no default" choices
|
|
102
103
|
|
|
@@ -111,7 +112,7 @@ the construct's own dogfooding consumer.
|
|
|
111
112
|
|
|
112
113
|
This construct never builds an image. Pass any `ecs.ContainerImage` — from a registry, or
|
|
113
114
|
built from a `Dockerfile` you control. The generic runner image lives separately in
|
|
114
|
-
`
|
|
115
|
+
`runner/docker-images/fargate`, so you can adopt the image, the construct, or both.
|
|
115
116
|
|
|
116
117
|
## The MicroVM image
|
|
117
118
|
|
|
@@ -126,6 +127,7 @@ the per-phase hook scripts and the bundled hook server, published inside the tar
|
|
|
126
127
|
the agent's workspace. `stageMicrovmImageContext()` puts the two together:
|
|
127
128
|
|
|
128
129
|
```ts
|
|
130
|
+
import path from 'node:path';
|
|
129
131
|
import {
|
|
130
132
|
EvidentMicrovmConstruct,
|
|
131
133
|
HOOKS_PORT,
|
|
@@ -149,6 +151,8 @@ new EvidentMicrovmConstruct(this, 'Runner', {
|
|
|
149
151
|
baseImageArn,
|
|
150
152
|
baseImageVersion,
|
|
151
153
|
doorbellSecret,
|
|
154
|
+
runnerSecret,
|
|
155
|
+
runnerOpencodeConfigPath: 'opencode.runner.jsonc',
|
|
152
156
|
});
|
|
153
157
|
```
|
|
154
158
|
|
|
@@ -156,14 +160,20 @@ Your repo does not have to be a pnpm workspace. If it commits a `pnpm-lock.yaml`
|
|
|
156
160
|
pre-installs dependencies at build time (a faster first boot); if not, that step is skipped
|
|
157
161
|
and the agent installs on first use.
|
|
158
162
|
|
|
163
|
+
| MicroVM prop | Omitted behavior |
|
|
164
|
+
| --- | --- |
|
|
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
|
+
| `runnerOpencodeConfigPath` | OpenCode uses the baked project configuration. Relative paths resolve from the workspace; absolute paths resolve in the image. |
|
|
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. |
|
|
169
|
+
|
|
159
170
|
## Status / limitations
|
|
160
171
|
|
|
161
|
-
- **Published to npm** as `@evident-ai/runner-cdk` (MIT) —
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
dependency for dogfooding.
|
|
172
|
+
- **Published to npm** as `@evident-ai/runner-cdk` (MIT). Install the `@dev` tag —
|
|
173
|
+
`npm install @evident-ai/runner-cdk@dev` (or `pnpm add`/`yarn add`); no checkout of
|
|
174
|
+
this repo is required. It builds to a self-contained `dist/` (`pnpm --filter
|
|
175
|
+
@evident-ai/runner-cdk build`) with no `workspace:`/`@evident/*` runtime dependency.
|
|
176
|
+
This repo's own consumer (`infrastructure/evident-runner`) still consumes it as a
|
|
177
|
+
`workspace:*` dependency for dogfooding.
|
|
168
178
|
- **AWS/ECS-specific.** Non-AWS clouds are an open question on the epic, not a supported
|
|
169
179
|
path.
|
|
@@ -42866,7 +42866,7 @@ Reference: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sso.ht
|
|
|
42866
42866
|
// ../../node_modules/.pnpm/@aws-sdk+signature-v4-multi-region@3.996.42/node_modules/@aws-sdk/signature-v4-multi-region/dist-cjs/index.js
|
|
42867
42867
|
var require_dist_cjs23 = __commonJS({
|
|
42868
42868
|
"../../node_modules/.pnpm/@aws-sdk+signature-v4-multi-region@3.996.42/node_modules/@aws-sdk/signature-v4-multi-region/dist-cjs/index.js"(exports2) {
|
|
42869
|
-
var { SignatureV4: SignatureV43, signatureV4aContainer } =
|
|
42869
|
+
var { SignatureV4: SignatureV43, signatureV4aContainer } = require_dist_cjs2();
|
|
42870
42870
|
var signatureV4CrtContainer = {
|
|
42871
42871
|
CrtSignerV4: null
|
|
42872
42872
|
};
|
|
@@ -49949,6 +49949,7 @@ function decide({
|
|
|
49949
49949
|
runnerId,
|
|
49950
49950
|
microvmId,
|
|
49951
49951
|
startedAt,
|
|
49952
|
+
imageVersion,
|
|
49952
49953
|
shapes,
|
|
49953
49954
|
polled
|
|
49954
49955
|
}) {
|
|
@@ -49966,6 +49967,7 @@ function decide({
|
|
|
49966
49967
|
reason,
|
|
49967
49968
|
...microvmId === void 0 ? {} : { microvm_id: microvmId },
|
|
49968
49969
|
...startedAt === void 0 ? {} : { microvm_started_at: startedAt.toISOString() },
|
|
49970
|
+
...imageVersion === void 0 ? {} : { image_version: imageVersion },
|
|
49969
49971
|
...shapes === void 0 ? {} : { shapes },
|
|
49970
49972
|
...polled === true ? { polled: true } : {}
|
|
49971
49973
|
}),
|
|
@@ -50122,7 +50124,8 @@ async function handleWakeDoorbell(doorbell, shape, microvm, timing) {
|
|
|
50122
50124
|
runnerId: doorbell.runnerId,
|
|
50123
50125
|
microvmId: doorbell.microvmId,
|
|
50124
50126
|
polled,
|
|
50125
|
-
startedAt: described.startedAt
|
|
50127
|
+
startedAt: described.startedAt,
|
|
50128
|
+
imageVersion: described.imageVersion
|
|
50126
50129
|
});
|
|
50127
50130
|
case "SUSPENDED":
|
|
50128
50131
|
if (await shouldRecreateForNewerImage(doorbell, shape, described.imageVersion, microvm)) {
|
|
@@ -50143,7 +50146,8 @@ async function handleWakeDoorbell(doorbell, shape, microvm, timing) {
|
|
|
50143
50146
|
microvm,
|
|
50144
50147
|
timing,
|
|
50145
50148
|
polled,
|
|
50146
|
-
described.startedAt
|
|
50149
|
+
described.startedAt,
|
|
50150
|
+
described.imageVersion
|
|
50147
50151
|
);
|
|
50148
50152
|
default:
|
|
50149
50153
|
return runMicrovm(
|
|
@@ -50220,10 +50224,11 @@ async function runMicrovm(doorbell, shape, microvm, timing, polled, reason, stat
|
|
|
50220
50224
|
runnerId: doorbell.runnerId,
|
|
50221
50225
|
microvmId: started.microvmId,
|
|
50222
50226
|
polled,
|
|
50223
|
-
startedAt: started.startedAt
|
|
50227
|
+
startedAt: started.startedAt,
|
|
50228
|
+
imageVersion: started.imageVersion
|
|
50224
50229
|
});
|
|
50225
50230
|
}
|
|
50226
|
-
async function resumeMicrovm(doorbell, microvmId, shape, microvm, timing, polled, startedAt) {
|
|
50231
|
+
async function resumeMicrovm(doorbell, microvmId, shape, microvm, timing, polled, startedAt, imageVersion) {
|
|
50227
50232
|
try {
|
|
50228
50233
|
await withThrottleRetry(() => microvm.resume(microvmId), timing);
|
|
50229
50234
|
} catch (error3) {
|
|
@@ -50248,7 +50253,8 @@ async function resumeMicrovm(doorbell, microvmId, shape, microvm, timing, polled
|
|
|
50248
50253
|
runnerId: doorbell.runnerId,
|
|
50249
50254
|
microvmId,
|
|
50250
50255
|
polled,
|
|
50251
|
-
startedAt
|
|
50256
|
+
startedAt,
|
|
50257
|
+
imageVersion
|
|
50252
50258
|
});
|
|
50253
50259
|
}
|
|
50254
50260
|
async function suspendMicrovm(doorbell, microvm, timing) {
|
|
@@ -50443,7 +50449,7 @@ function createRuntimeMicrovm(executionRoleArn) {
|
|
|
50443
50449
|
return latestActiveImageVersion;
|
|
50444
50450
|
},
|
|
50445
50451
|
async run({ imageIdentifier, runHookPayload, clientToken }) {
|
|
50446
|
-
const { microvmId, startedAt } = await microvms.send(
|
|
50452
|
+
const { microvmId, startedAt, imageVersion } = await microvms.send(
|
|
50447
50453
|
new import_client_lambda_microvms.RunMicrovmCommand({
|
|
50448
50454
|
imageIdentifier,
|
|
50449
50455
|
executionRoleArn,
|
|
@@ -50476,7 +50482,7 @@ function createRuntimeMicrovm(executionRoleArn) {
|
|
|
50476
50482
|
if (!microvmId) {
|
|
50477
50483
|
throw new Error("RunMicrovm returned no microvmId");
|
|
50478
50484
|
}
|
|
50479
|
-
return { microvmId, startedAt };
|
|
50485
|
+
return { microvmId, startedAt, imageVersion };
|
|
50480
50486
|
},
|
|
50481
50487
|
async resume(microvmId) {
|
|
50482
50488
|
await microvms.send(new import_client_lambda_microvms.ResumeMicrovmCommand({ microvmIdentifier: microvmId }));
|
|
@@ -61,13 +61,25 @@ export type EvidentScaleToZeroConstructProps = {
|
|
|
61
61
|
replicaBucket: s3.IBucket;
|
|
62
62
|
image: ecs.ContainerImage;
|
|
63
63
|
/**
|
|
64
|
-
* Container secrets.
|
|
65
|
-
*
|
|
66
|
-
*
|
|
64
|
+
* Container secrets. `availableSecretKeys` are injected by name; EVIDENT_AGENT_KEY
|
|
65
|
+
* and GH_TOKEN remain available to external adopters that do not enumerate keys.
|
|
66
|
+
*
|
|
67
|
+
* `agentSecret` always supplies EVIDENT_AGENT_KEY. When `capabilitySecret` is
|
|
68
|
+
* ALSO given (#1868 WI-2), every capability key (GH_TOKEN included) is injected
|
|
69
|
+
* from THAT secret instead, and `availableSecretKeys` is ignored — the two
|
|
70
|
+
* deploy targets (this one and the MicroVM stack) then read the same
|
|
71
|
+
* capability keys from the same shared secret rather than each carrying its
|
|
72
|
+
* own copy. When `capabilitySecret` is absent, behavior is unchanged: GH_TOKEN
|
|
73
|
+
* plus every `availableSecretKeys` entry, all from `agentSecret` — an external
|
|
74
|
+
* adopter passing only `agentSecret`/`availableSecretKeys` is unaffected.
|
|
67
75
|
*/
|
|
68
76
|
agentSecret: secretsmanager.ISecret;
|
|
69
|
-
/** Key names present in the agent secret
|
|
77
|
+
/** Key names present in the agent secret. Every listed key is injected into the container. */
|
|
70
78
|
availableSecretKeys: Set<string>;
|
|
79
|
+
/** The shared capability secret (#1868 WI-2). See `agentSecret`'s doc above. */
|
|
80
|
+
capabilitySecret?: secretsmanager.ISecret;
|
|
81
|
+
/** Key names present in `capabilitySecret`. Ignored when `capabilitySecret` is absent. */
|
|
82
|
+
capabilityKeys?: Set<string>;
|
|
71
83
|
/** Waker secret (EVIDENT_WAKE_SECRET), consumed by the waker at runtime. */
|
|
72
84
|
wakerSecret: secretsmanager.ISecret;
|
|
73
85
|
};
|
|
@@ -61,7 +61,7 @@ class EvidentScaleToZeroConstruct extends constructs_1.Construct {
|
|
|
61
61
|
service;
|
|
62
62
|
constructor(scope, id, props) {
|
|
63
63
|
super(scope, id);
|
|
64
|
-
const { agentName, evidentAgentId, gitRepo, gitBranch, idleTimeoutSeconds, cpu, memoryLimitMiB, resourcePrefix = DEFAULT_RESOURCE_PREFIX, envName, evidentApiUrl, evidentTunnelUrl, extraEnvironment, cluster, securityGroup, taskRole, logGroup, replicaBucket, image, agentSecret, availableSecretKeys, wakerSecret, } = props;
|
|
64
|
+
const { agentName, evidentAgentId, gitRepo, gitBranch, idleTimeoutSeconds, cpu, memoryLimitMiB, resourcePrefix = DEFAULT_RESOURCE_PREFIX, envName, evidentApiUrl, evidentTunnelUrl, extraEnvironment, cluster, securityGroup, taskRole, logGroup, replicaBucket, image, agentSecret, availableSecretKeys, capabilitySecret, capabilityKeys, wakerSecret, } = props;
|
|
65
65
|
this.replicaPrefix = `agents/${evidentAgentId}`;
|
|
66
66
|
// A plain STRING (not service.serviceName) so the container env is static and
|
|
67
67
|
// has no construct-ordering dependency on the service.
|
|
@@ -101,7 +101,7 @@ class EvidentScaleToZeroConstruct extends constructs_1.Construct {
|
|
|
101
101
|
// --endpoint / --tunnel). The agent is resolved from EVIDENT_AGENT_KEY.
|
|
102
102
|
EVIDENT_API_URL: evidentApiUrl,
|
|
103
103
|
EVIDENT_TUNNEL_URL: evidentTunnelUrl,
|
|
104
|
-
// Litestream replica target (read by
|
|
104
|
+
// Litestream replica target (read by runner/synchroniser, which
|
|
105
105
|
// generates /etc/evident/litestream.yml at boot).
|
|
106
106
|
LITESTREAM_BUCKET: replicaBucket.bucketName,
|
|
107
107
|
LITESTREAM_PREFIX: this.replicaPrefix,
|
|
@@ -115,10 +115,9 @@ class EvidentScaleToZeroConstruct extends constructs_1.Construct {
|
|
|
115
115
|
// window (protected/active sessions are never touched). Tightened from 7d
|
|
116
116
|
// to 24h per #537: at 7d, opencode.db was observed growing ~11x (77 MB ->
|
|
117
117
|
// 849 MB) in ~24h, so 7d was far too permissive at the observed growth
|
|
118
|
-
// rate. 24h is
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
// interval and never touches a protected/active session.
|
|
118
|
+
// rate. 24h is the tighter mitigation that came out of that (see also the
|
|
119
|
+
// OPENCODE_VERSION pin above). Sweep runs on the default 1h interval and
|
|
120
|
+
// never touches a protected/active session.
|
|
122
121
|
EVIDENT_SESSION_CLEANUP_MAX_AGE: '24h',
|
|
123
122
|
CLUSTER: cluster.clusterName,
|
|
124
123
|
SERVICE: serviceName,
|
|
@@ -128,16 +127,22 @@ class EvidentScaleToZeroConstruct extends constructs_1.Construct {
|
|
|
128
127
|
...extraEnvironment,
|
|
129
128
|
};
|
|
130
129
|
const secrets = {
|
|
130
|
+
// Identifies the runner to Evident. Always agentSecret — capabilitySecret
|
|
131
|
+
// never carries this key (#1868 WI-2: the two are a deliberate split, not
|
|
132
|
+
// a duplication of the same data).
|
|
131
133
|
EVIDENT_AGENT_KEY: ecs.Secret.fromSecretsManager(agentSecret, 'EVIDENT_AGENT_KEY'),
|
|
132
|
-
GH_TOKEN: ecs.Secret.fromSecretsManager(agentSecret, 'GH_TOKEN'),
|
|
133
134
|
};
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
135
|
+
if (capabilitySecret !== undefined) {
|
|
136
|
+
for (const key of capabilityKeys ?? new Set()) {
|
|
137
|
+
secrets[key] = ecs.Secret.fromSecretsManager(capabilitySecret, key);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
// Legacy shape, unchanged: GH_TOKEN (required to clone this construct's
|
|
142
|
+
// workspace at boot) plus every availableSecretKeys entry, all from
|
|
143
|
+
// agentSecret — what every external adopter still gets.
|
|
144
|
+
secrets.GH_TOKEN = ecs.Secret.fromSecretsManager(agentSecret, 'GH_TOKEN');
|
|
145
|
+
for (const key of availableSecretKeys) {
|
|
141
146
|
secrets[key] = ecs.Secret.fromSecretsManager(agentSecret, key);
|
|
142
147
|
}
|
|
143
148
|
}
|
|
@@ -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
|
+
});
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// Shared literals for the MicroVM controller and for the image it launches.
|
|
3
3
|
// Every module imports from here rather than restating a value that must match
|
|
4
|
-
// across them
|
|
5
|
-
// image
|
|
6
|
-
// `scripts/build.ts`, so these values reach the image as inlined constants.
|
|
4
|
+
// across them. The image template pins matching copies, guarded by
|
|
5
|
+
// `microvm/image/dockerfile.test.ts`.
|
|
7
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
7
|
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
8
|
// The port the image's hook server binds, baked into the published Dockerfile
|
|
@@ -12,7 +12,7 @@ export interface EvidentMicrovmConstructProps {
|
|
|
12
12
|
readonly imageSource: string;
|
|
13
13
|
/**
|
|
14
14
|
* ARN of the Lambda-managed base MicroVM image, from
|
|
15
|
-
* `aws lambda list-managed-microvm-images`. A string, not a `CfnParameter`
|
|
15
|
+
* `aws lambda-microvms list-managed-microvm-images`. A string, not a `CfnParameter`
|
|
16
16
|
* built here: the caller creates the parameter at ITS OWN stack scope
|
|
17
17
|
* (preserving its logical id) and passes its resolved value down — a
|
|
18
18
|
* construct must not mint parameters into a consumer's stack.
|
|
@@ -20,7 +20,7 @@ export interface EvidentMicrovmConstructProps {
|
|
|
20
20
|
readonly baseImageArn: string;
|
|
21
21
|
/**
|
|
22
22
|
* Version of the Lambda-managed base MicroVM image, from
|
|
23
|
-
* `aws lambda list-managed-microvm-images`. See `baseImageArn` for why this
|
|
23
|
+
* `aws lambda-microvms list-managed-microvm-images`. See `baseImageArn` for why this
|
|
24
24
|
* is a plain string.
|
|
25
25
|
*/
|
|
26
26
|
readonly baseImageVersion: string;
|
|
@@ -41,6 +41,20 @@ export interface EvidentMicrovmConstructProps {
|
|
|
41
41
|
* in its own scope, and passes the result in either case.
|
|
42
42
|
*/
|
|
43
43
|
readonly doorbellSecret: secretsmanager.ISecret;
|
|
44
|
+
/**
|
|
45
|
+
* Optional runner secret whose JSON values are exported by `/run`. Omitting it
|
|
46
|
+
* leaves GitHub and MCP credentials unavailable to a generic consumer.
|
|
47
|
+
*/
|
|
48
|
+
readonly runnerSecret?: secretsmanager.ISecret;
|
|
49
|
+
/**
|
|
50
|
+
* Optional runner OpenCode overlay. Absolute paths are image paths; relative
|
|
51
|
+
* paths are resolved from the baked workspace by the `/run` hook.
|
|
52
|
+
*/
|
|
53
|
+
readonly runnerOpencodeConfigPath?: string;
|
|
54
|
+
/** Optional git identity the `/run` hook configures for agent commits. */
|
|
55
|
+
readonly gitUserName?: string;
|
|
56
|
+
/** Optional git email the `/run` hook configures for agent commits. */
|
|
57
|
+
readonly gitUserEmail?: string;
|
|
44
58
|
/**
|
|
45
59
|
* TCP port the image's hook server listens on, baked into both the image
|
|
46
60
|
* (`HOOKS_PORT` env var) and the `MicrovmImage`'s `hooks.port` — a mismatch
|
|
@@ -55,6 +69,24 @@ export interface EvidentMicrovmConstructProps {
|
|
|
55
69
|
* running a hook).
|
|
56
70
|
*/
|
|
57
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;
|
|
58
90
|
}
|
|
59
91
|
/**
|
|
60
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
|
|
@@ -121,6 +131,16 @@ class EvidentMicrovmConstruct extends constructs_1.Construct {
|
|
|
121
131
|
// already bakes in for its own runners (evident-scale-to-zero-construct.ts:191).
|
|
122
132
|
// Safe to bake: identical for every VM from this image version.
|
|
123
133
|
EVIDENT_SESSION_CLEANUP_MAX_AGE: '24h',
|
|
134
|
+
...(props.runnerOpencodeConfigPath
|
|
135
|
+
? { RUNNER_OPENCODE_CONFIG: props.runnerOpencodeConfigPath }
|
|
136
|
+
: {}),
|
|
137
|
+
...(props.runnerSecret
|
|
138
|
+
? {
|
|
139
|
+
RUNNER_SECRET_ARN: props.runnerSecret.secretArn,
|
|
140
|
+
}
|
|
141
|
+
: {}),
|
|
142
|
+
...(props.gitUserName ? { GIT_USER_NAME: props.gitUserName } : {}),
|
|
143
|
+
...(props.gitUserEmail ? { GIT_USER_EMAIL: props.gitUserEmail } : {}),
|
|
124
144
|
};
|
|
125
145
|
// ...and the hooks configuration every shape's image is built with.
|
|
126
146
|
const imageHooks = {
|
|
@@ -179,6 +199,8 @@ class EvidentMicrovmConstruct extends constructs_1.Construct {
|
|
|
179
199
|
// customer account, and what Phase 4 (#263) has to tighten when this goes
|
|
180
200
|
// multi-tenant.
|
|
181
201
|
durableState.grantReadWrite(defaultImage.executionRole);
|
|
202
|
+
// The image receives only an ARN; `/run` reads the secret value at runtime.
|
|
203
|
+
props.runnerSecret?.grantRead(defaultImage.executionRole);
|
|
182
204
|
// The shape catalogue the controller resolves a doorbell's `shape` field
|
|
183
205
|
// against, and advertises over the describe channel (#723). One JSON
|
|
184
206
|
// array, built with `Stack.toJsonString` — not `JSON.stringify` — because
|
|
@@ -237,6 +259,22 @@ class EvidentMicrovmConstruct extends constructs_1.Construct {
|
|
|
237
259
|
// HMAC verification of the doorbell body IS the auth, as for the ECS waker.
|
|
238
260
|
authType: lambda.FunctionUrlAuthType.NONE,
|
|
239
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
|
+
}
|
|
240
278
|
this.functionUrl = functionUrl.url;
|
|
241
279
|
this.shapeCatalogueJson = shapeCatalogueJson;
|
|
242
280
|
this.durableStateBucket = durableState;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.handleDoorbell = handleDoorbell;
|
|
4
4
|
const node_crypto_1 = require("node:crypto");
|
|
5
|
-
const
|
|
5
|
+
const sdk_1 = require("@evident/sdk");
|
|
6
6
|
const constants_1 = require("../constants");
|
|
7
7
|
const doorbell_1 = require("./doorbell");
|
|
8
8
|
const throttle_retry_1 = require("./throttle-retry");
|
|
@@ -37,7 +37,7 @@ const LOG_ACTION = {
|
|
|
37
37
|
rejected: 'reject',
|
|
38
38
|
describe: 'describe',
|
|
39
39
|
};
|
|
40
|
-
function decide({ statusCode, action, reason, state, runnerId, microvmId, startedAt, shapes, polled, }) {
|
|
40
|
+
function decide({ statusCode, action, reason, state, runnerId, microvmId, startedAt, imageVersion, shapes, polled, }) {
|
|
41
41
|
const line = `[doorbell] state=${state ?? 'none'} action=${LOG_ACTION[action]} ` +
|
|
42
42
|
`microvm_id=${microvmId ?? '-'} runner_id=${runnerId ?? '-'} reason=${reason}` +
|
|
43
43
|
(polled === undefined ? '' : ` polled=${polled}`);
|
|
@@ -56,6 +56,7 @@ function decide({ statusCode, action, reason, state, runnerId, microvmId, starte
|
|
|
56
56
|
reason,
|
|
57
57
|
...(microvmId === undefined ? {} : { microvm_id: microvmId }),
|
|
58
58
|
...(startedAt === undefined ? {} : { microvm_started_at: startedAt.toISOString() }),
|
|
59
|
+
...(imageVersion === undefined ? {} : { image_version: imageVersion }),
|
|
59
60
|
...(shapes === undefined ? {} : { shapes }),
|
|
60
61
|
...(polled === true ? { polled: true } : {}),
|
|
61
62
|
}),
|
|
@@ -175,7 +176,7 @@ async function shouldRecreateForNewerImage(doorbell, shape, runningVersion, micr
|
|
|
175
176
|
return compareImageVersions(running, latest) < 0;
|
|
176
177
|
}
|
|
177
178
|
async function handleDoorbell({ rawBody, signatureHeader, doorbellSecret, shapes, microvm, sleep, random, }) {
|
|
178
|
-
if (!(0,
|
|
179
|
+
if (!(0, sdk_1.verifyEvidentSignature)(rawBody, signatureHeader, doorbellSecret)) {
|
|
179
180
|
return decide({
|
|
180
181
|
statusCode: 401,
|
|
181
182
|
action: 'rejected',
|
|
@@ -283,6 +284,7 @@ async function handleWakeDoorbell(doorbell, shape, microvm, timing) {
|
|
|
283
284
|
microvmId: doorbell.microvmId,
|
|
284
285
|
polled,
|
|
285
286
|
startedAt: described.startedAt,
|
|
287
|
+
imageVersion: described.imageVersion,
|
|
286
288
|
});
|
|
287
289
|
case 'SUSPENDED':
|
|
288
290
|
// A newer image has been published since this VM booted: resuming would
|
|
@@ -292,7 +294,7 @@ async function handleWakeDoorbell(doorbell, shape, microvm, timing) {
|
|
|
292
294
|
if (await shouldRecreateForNewerImage(doorbell, shape, described.imageVersion, microvm)) {
|
|
293
295
|
return runMicrovm(doorbell, shape, microvm, timing, polled, REASON.imageVersionOutdated, state);
|
|
294
296
|
}
|
|
295
|
-
return resumeMicrovm(doorbell, doorbell.microvmId, shape, microvm, timing, polled, described.startedAt);
|
|
297
|
+
return resumeMicrovm(doorbell, doorbell.microvmId, shape, microvm, timing, polled, described.startedAt, described.imageVersion);
|
|
296
298
|
default:
|
|
297
299
|
// TERMINATED, TERMINATING, not_found — gone or going, so a fresh VM either
|
|
298
300
|
// way. This is what self-heals a stale `microvm_id`.
|
|
@@ -387,13 +389,13 @@ async function runMicrovm(doorbell, shape, microvm, timing, polled, reason, stat
|
|
|
387
389
|
microvmId: started.microvmId,
|
|
388
390
|
polled,
|
|
389
391
|
startedAt: started.startedAt,
|
|
392
|
+
imageVersion: started.imageVersion,
|
|
390
393
|
});
|
|
391
394
|
}
|
|
392
395
|
async function resumeMicrovm(doorbell, microvmId, shape, microvm, timing, polled,
|
|
393
|
-
//
|
|
394
|
-
//
|
|
395
|
-
|
|
396
|
-
startedAt) {
|
|
396
|
+
// Both values come from the `describe()` result that led here because
|
|
397
|
+
// `ResumeMicrovm` returns no lifecycle metadata of its own.
|
|
398
|
+
startedAt, imageVersion) {
|
|
397
399
|
try {
|
|
398
400
|
await (0, throttle_retry_1.withThrottleRetry)(() => microvm.resume(microvmId), timing);
|
|
399
401
|
}
|
|
@@ -411,6 +413,7 @@ startedAt) {
|
|
|
411
413
|
microvmId,
|
|
412
414
|
polled,
|
|
413
415
|
startedAt,
|
|
416
|
+
imageVersion,
|
|
414
417
|
});
|
|
415
418
|
}
|
|
416
419
|
async function suspendMicrovm(doorbell, microvm, timing) {
|
|
@@ -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` (runner/docker-images/microvm/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
|
*/
|
|
@@ -69,6 +69,12 @@ export interface MicrovmClient {
|
|
|
69
69
|
* none. Never synthesize one (e.g. `new Date()`) here or in any caller.
|
|
70
70
|
*/
|
|
71
71
|
startedAt?: Date;
|
|
72
|
+
/**
|
|
73
|
+
* The image version this VM booted on, straight from `RunMicrovm`'s own
|
|
74
|
+
* `imageVersion`. Absent means AWS omitted it and must be read as UNKNOWN;
|
|
75
|
+
* see `MicrovmDescription.imageVersion` above.
|
|
76
|
+
*/
|
|
77
|
+
imageVersion?: string;
|
|
72
78
|
}>;
|
|
73
79
|
resume(microvmId: string): Promise<void>;
|
|
74
80
|
suspend(microvmId: string): Promise<void>;
|