@evident-ai/runner-cdk 3.5.2-dev.e67eb8b → 3.5.2-dev.f258bd6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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
- need: the image build context ships inside it (see [The MicroVM image](#the-microvm-image)
14
- below), so there is no checkout of the source repository involved. See
12
+ and suspends between messages. You provide a build-context directory and a full source
13
+ checkout to `stageMicrovmBuildContext`, then pass its returned directory as
14
+ `buildContextPath` (see [The MicroVM image](#the-microvm-image) below). See
15
15
  [the AWS runner doc](https://evident.run/docs/aws-runner) for the full picture of both
16
16
  strategies.
17
17
 
@@ -99,6 +99,25 @@ For the full end-to-end walkthrough for this construct, see
99
99
  | `agentSecret`, `wakerSecret` | `secretsmanager.ISecret` | Any Secrets Manager secret. |
100
100
  | `availableSecretKeys` | `Set<string>` | Keys present in the agent secret. Every listed key is injected; `EVIDENT_RUNNER_KEY` and `EVIDENT_AGENT_KEY` are injected from the `EVIDENT_AGENT_KEY` field, and `GH_TOKEN` is also injected for adopters that do not enumerate their secret. |
101
101
 
102
+ ### MicroVM props
103
+
104
+ | Prop | Type | Notes |
105
+ | --- | --- | --- |
106
+ | `buildContextPath` | `string` | Directory containing your Dockerfile and every file it copies, normally the result of `stageMicrovmBuildContext()`. |
107
+ | `baseImageArn` / `baseImageVersion` | `string` | The AWS-managed base MicroVM image values from `aws lambda-microvms list-managed-microvm-images`. |
108
+ | `controllerSigningSecret` | `secretsmanager.ISecret` | Secret containing the shared HMAC key under the `CONTROLLER_SIGNING_SECRET` JSON field. |
109
+ | `hooksPort` / `hookTimeoutSeconds` | `number` | Values exported by the base image contract. |
110
+ | `shapes` | `readonly MicrovmShape[]?` | Shape catalogue to build. Defaults to the committed catalogue. |
111
+ | `runnerSecret` | `secretsmanager.ISecret?` | Optional JSON secret whose values are exported by `/run`. |
112
+ | `runnerOpencodeConfigPath` | `string?` | Optional baked OpenCode configuration path. |
113
+ | `gitUserName` / `gitUserEmail` | `string?` | Optional git identity for agent commits. |
114
+ | `extraImageEnvironment` | `Record<string,string>?` | Non-secret values baked into every VM launched from the image version. |
115
+ | `evidentApiUrl` / `evidentRunnerId` | `string?` | Supply both to report published image versions to Evident, or omit both. |
116
+
117
+ The `DoorbellSecret` CloudFormation logical ID remains stable for Evident's own
118
+ default stack. It identifies the deployed secret resource, not the customer-facing
119
+ name of the signing key.
120
+
102
121
  ### Two deliberate "no default" choices
103
122
 
104
123
  - **The endpoints are required.** Defaulting them would either bake *our* dev environment
@@ -116,58 +135,65 @@ built from a `Dockerfile` you control. The generic runner image lives separately
116
135
 
117
136
  ## The MicroVM image
118
137
 
119
- `EvidentMicrovmConstruct`'s `imageSource` is a **local directory** holding the image build
120
- context, which `cdk deploy` zips and uploads. The Lambda MicroVM service then builds the
121
- image in *your* account, from a base image ARN you discover there — so, unlike the Fargate
122
- strategy, there is no registry image to pull.
138
+ ### Prune image versions
139
+
140
+ MicroVM image versions are quota-bound. `@evident-ai/lambda-microvm-cdk` ships the
141
+ `prune-microvm-image-versions` CLI; enable it with one `package.json` script line:
142
+
143
+ ```json
144
+ "microvm:prune": "prune-microvm-image-versions --image-arn arn:aws:lambda:eu-west-1:000000000000:microvm-image:your-image"
145
+ ```
146
+
147
+ See the `@evident-ai/lambda-microvm-cdk` package README for the CLI flags and safety semantics.
148
+
149
+ `EvidentMicrovmConstruct`'s `buildContextPath` is a **local directory** holding the
150
+ Dockerfile you control and everything it copies. `cdk deploy` zips and uploads that
151
+ directory, and the Lambda MicroVM service builds the image in *your* account. Start the
152
+ Dockerfile from a pinned `ghcr.io/sroze/evident-microvm-base:X.Y.Z` tag; the base image
153
+ contains the hook server, lifecycle hooks and common agent tooling.
123
154
 
124
- The context has three parts: this package supplies the Dockerfile, the per-phase hook
125
- scripts and the bundled hook server, published inside the tarball at
126
- `dist/microvm-image-context/`; **your** repository is baked in as the agent's workspace;
127
- and an optional overlay supplies deployment-specific build steps. `stageMicrovmImageContext()`
128
- puts them together:
155
+ `stageMicrovmBuildContext()` combines your build-context directory with a full checkout
156
+ of the workspace in a fresh temporary directory. The workspace is written at
157
+ `workspace/`, and the returned path is the value for `buildContextPath`:
129
158
 
130
159
  ```ts
131
- import path from 'node:path';
132
160
  import {
133
161
  EvidentMicrovmConstruct,
134
162
  HOOKS_PORT,
135
163
  HOOK_TIMEOUT_SECONDS,
136
- stageMicrovmImageContext,
164
+ stageMicrovmBuildContext,
137
165
  } from '@evident-ai/runner-cdk';
138
166
 
167
+ const buildContextPath = stageMicrovmBuildContext({
168
+ // A full (non-shallow) clone: the agent branches, commits and opens PRs.
169
+ workspaceRepositoryPath: '/path/to/your/checkout',
170
+ // Credential-free: the token the agent pushes with arrives per-session.
171
+ workspaceOriginUrl: 'https://github.com/acme/widgets.git',
172
+ buildContextPath: '/path/to/your/microvm-build-context',
173
+ });
174
+
139
175
  new EvidentMicrovmConstruct(this, 'Runner', {
140
- imageSource: stageMicrovmImageContext({
141
- // A full (non-shallow) clone: the agent branches, commits and opens PRs.
142
- repositoryPath: '/path/to/your/checkout',
143
- // Credential-free — this string ships inside the shared snapshot. The
144
- // token the agent pushes with arrives per-session, never baked in.
145
- originUrl: 'https://github.com/acme/widgets.git',
146
- destination: path.join(__dirname, '..', 'build', 'image'),
147
- // Optional deployment-specific installs and build warm-up.
148
- overlayDir: '/path/to/microvm-overlay',
149
- }),
176
+ buildContextPath,
150
177
  // Must match the published image these were baked into, so export them
151
178
  // rather than restating the numbers.
152
179
  hooksPort: HOOKS_PORT,
153
180
  hookTimeoutSeconds: HOOK_TIMEOUT_SECONDS,
154
181
  baseImageArn,
155
182
  baseImageVersion,
156
- doorbellSecret,
183
+ controllerSigningSecret,
157
184
  runnerSecret,
158
185
  runnerOpencodeConfigPath: 'opencode.runner.jsonc',
159
186
  });
160
187
  ```
161
188
 
162
- Pass `overlayDir` to add deployment-specific build steps to the staged image. The
163
- directory provides `setup-root` for root-owned installs and `setup-workspace` for
164
- dependency installation and workspace build warm-up. See the [MicroVM image
165
- README](https://github.com/evident-run/evident-run/blob/main/runner/docker-images/microvm/README.md)
166
- for the full overlay contract.
189
+ Write project-specific installation and build steps in your Dockerfile. The [MicroVM base
190
+ image README](../../runner/docker-images/microvm/README.md) documents the consumer
191
+ contract, including the required `USER`, `WORKDIR`, ownership and `git reset --quiet`
192
+ steps.
167
193
 
168
- The image bakes your repository checked out but not installed and makes no assumptions about
169
- its package manager. To speed up first boot, install dependencies from your overlay's
170
- `setup-workspace`; otherwise, the agent installs them on first use.
194
+ The staged image includes your repository checkout, while your Dockerfile controls its
195
+ package manager and build warm-up. Do not copy credentials into the build context or bake
196
+ them into image environment variables.
171
197
 
172
198
  | MicroVM prop | Omitted behavior |
173
199
  | --- | --- |
@@ -175,12 +201,27 @@ its package manager. To speed up first boot, install dependencies from your over
175
201
  | `runnerOpencodeConfigPath` | OpenCode uses the baked project configuration. Relative paths resolve from the workspace; absolute paths resolve in the image. |
176
202
  | `extraImageEnvironment` | Non-secret values baked into every VM launched from this image version; every VM can read them, so never put secrets here. |
177
203
  | `gitUserName` / `gitUserEmail` | The hook uses the Evident bot defaults for git identity. |
178
- | `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. |
179
-
180
- `extraImageEnvironment` is the runtime counterpart to an overlay. Use it when an
181
- overlay-installed component needs an environment variable after the VM boots. The
182
- values are baked into every VM launched from that image version and are readable by
183
- all of them, so never put a secret in this prop.
204
+ | `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 `controllerSigningSecret`). A failure to report never fails your deploy — it warns and Evident reads that provisioner as *unknown*, never as up to date or stale. |
205
+
206
+ `extraImageEnvironment` is the runtime environment for the image. Values are baked into
207
+ every VM launched from that image version and are readable by all of them, so never put a
208
+ secret in this prop.
209
+
210
+ ## Breaking changes in this release
211
+
212
+ - `stageMicrovmImageContext` is removed. Use `stageMicrovmBuildContext()` with your
213
+ Dockerfile and build context.
214
+ - `imageSource` is renamed to `buildContextPath`.
215
+ - `MicrovmShape.constructId` is removed. CloudFormation child identity derives from
216
+ the shape `name`.
217
+ - `doorbellSecret` is renamed to `controllerSigningSecret`.
218
+ - The default shape's `imageName` changes to `evident-microvm-standard`. The
219
+ `AWS::Lambda::MicrovmImage` resource, its auto-created build role and execution
220
+ role, and both roles' default policies are replaced with the new shape identity.
221
+ - The JSON field in the caller-supplied Secrets Manager secret changes from
222
+ `DOORBELL_SECRET` to `CONTROLLER_SIGNING_SECRET`. Rename that field in your own
223
+ secret. If you do not, the controller reads no signing key and every doorbell
224
+ returns HTTP 401 at runtime; TypeScript cannot catch this field-key change.
184
225
 
185
226
  ## Status / limitations
186
227
 
@@ -49799,9 +49799,9 @@ var MICROVM_MAX_RUN_SECONDS = 28800;
49799
49799
  // src/microvm/controller/handle-doorbell.ts
49800
49800
  var import_node_crypto12 = require("node:crypto");
49801
49801
 
49802
- // ../webhook-signature/src/verify-wake-signature.ts
49802
+ // ../../sdk/src/signature.ts
49803
49803
  var import_node_crypto11 = require("node:crypto");
49804
- function verifyWakeSignature(rawBody, signatureHeader, secret) {
49804
+ function verifyEvidentSignature(rawBody, signatureHeader, secret) {
49805
49805
  if (!signatureHeader || !secret) {
49806
49806
  return false;
49807
49807
  }
@@ -50036,13 +50036,13 @@ async function shouldRecreateForNewerImage(doorbellRequest, shape, runningVersio
50036
50036
  async function handleDoorbell({
50037
50037
  rawBody,
50038
50038
  signatureHeader,
50039
- doorbellSecret,
50039
+ controllerSigningSecret,
50040
50040
  shapes,
50041
50041
  microvm,
50042
50042
  sleep: sleep4,
50043
50043
  random
50044
50044
  }) {
50045
- if (!verifyWakeSignature(rawBody, signatureHeader, doorbellSecret)) {
50045
+ if (!verifyEvidentSignature(rawBody, signatureHeader, controllerSigningSecret)) {
50046
50046
  return decide({
50047
50047
  statusCode: 401,
50048
50048
  action: "rejected",
@@ -50413,20 +50413,20 @@ function parseShapeCatalogue(raw) {
50413
50413
  // src/microvm/controller/handler.ts
50414
50414
  var microvms = new import_client_lambda_microvms.LambdaMicrovmsClient({});
50415
50415
  var secrets = new import_client_secrets_manager.SecretsManagerClient({});
50416
- var cachedDoorbellSecret;
50417
- async function getDoorbellSecret(secretArn, key) {
50418
- if (cachedDoorbellSecret !== void 0) {
50419
- return cachedDoorbellSecret;
50416
+ var cachedControllerSigningSecret;
50417
+ async function getControllerSigningSecret(secretArn, key) {
50418
+ if (cachedControllerSigningSecret !== void 0) {
50419
+ return cachedControllerSigningSecret;
50420
50420
  }
50421
50421
  const { SecretString } = await secrets.send(new import_client_secrets_manager.GetSecretValueCommand({ SecretId: secretArn }));
50422
50422
  if (!SecretString) {
50423
- throw new Error(`doorbell secret ${secretArn} has no SecretString`);
50423
+ throw new Error(`controller signing secret ${secretArn} has no SecretString`);
50424
50424
  }
50425
50425
  const value = JSON.parse(SecretString)[key];
50426
50426
  if (typeof value !== "string" || value === "") {
50427
- throw new Error(`doorbell secret ${secretArn} is missing a non-empty '${key}' field`);
50427
+ throw new Error(`controller signing secret ${secretArn} is missing a non-empty '${key}' field`);
50428
50428
  }
50429
- cachedDoorbellSecret = value;
50429
+ cachedControllerSigningSecret = value;
50430
50430
  return value;
50431
50431
  }
50432
50432
  function createRuntimeMicrovm(executionRoleArn) {
@@ -50470,7 +50470,7 @@ function createRuntimeMicrovm(executionRoleArn) {
50470
50470
  // runner is outbound-only (it dials the Cloudflare relay), so AWS sees
50471
50471
  // an idle endpoint no matter how busy the agent is. Idle is instead
50472
50472
  // decided IN the VM by `evident run --idle-timeout`
50473
- // (packages/runner-cdk/microvm-image/hooks/common.sh's `IDLE_TIMEOUT_SECONDS`), whose
50473
+ // (runner/docker-images/microvm/hooks/common.sh's `IDLE_TIMEOUT_SECONDS`), whose
50474
50474
  // clean exit is what drives the controller's own `suspend()` below via
50475
50475
  // the `runner.suspend_requested` doorbell. See `idle-policy.test.ts`
50476
50476
  // in this directory, which scans this file's source for the key and
@@ -50501,9 +50501,9 @@ function requireEnv(name) {
50501
50501
  }
50502
50502
  var sleep3 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
50503
50503
  var handler = async (event) => {
50504
- const doorbellSecret = await getDoorbellSecret(
50505
- requireEnv("DOORBELL_SECRET_ARN"),
50506
- requireEnv("DOORBELL_SECRET_KEY")
50504
+ const controllerSigningSecret = await getControllerSigningSecret(
50505
+ requireEnv("CONTROLLER_SIGNING_SECRET_ARN"),
50506
+ requireEnv("CONTROLLER_SIGNING_SECRET_KEY")
50507
50507
  );
50508
50508
  const shapes = parseShapeCatalogue(requireEnv("MICROVM_SHAPES"));
50509
50509
  const executionRoleArn = requireEnv("MICROVM_EXECUTION_ROLE_ARN");
@@ -50511,7 +50511,7 @@ var handler = async (event) => {
50511
50511
  const { statusCode, body } = await handleDoorbell({
50512
50512
  rawBody,
50513
50513
  signatureHeader: event.headers?.["x-evident-signature"],
50514
- doorbellSecret,
50514
+ controllerSigningSecret,
50515
50515
  shapes,
50516
50516
  microvm: createRuntimeMicrovm(executionRoleArn),
50517
50517
  sleep: sleep3,
@@ -157,6 +157,13 @@ class EvidentScaleToZeroConstruct extends constructs_1.Construct {
157
157
  // Object Get/Put/Delete scoped to this agent's prefix. DeleteObject is
158
158
  // required: Litestream's retention/compaction prunes old LTX files.
159
159
  replicaBucket.grantReadWrite(taskRole, `${this.replicaPrefix}/*`);
160
+ // Vault credentials are read and conditionally written, so this shared prefix
161
+ // needs no delete or list permission; conditional writes use request headers.
162
+ taskRole.addToPrincipalPolicy(new iam.PolicyStatement({
163
+ sid: 'ReadWriteCredentialVaultObjects',
164
+ actions: ['s3:GetObject', 's3:PutObject'],
165
+ resources: [replicaBucket.arnForObjects('credential-vaults/*')],
166
+ }));
160
167
  this.service = new ecs.FargateService(this, 'Service', {
161
168
  cluster,
162
169
  taskDefinition,
@@ -29,15 +29,17 @@ var import_client_secrets_manager = require("@aws-sdk/client-secrets-manager");
29
29
  var PHYSICAL_RESOURCE_ID = "evident-image-version-report";
30
30
  var REPORT_TIMEOUT_MS = 5e3;
31
31
  var secrets = new import_client_secrets_manager.SecretsManagerClient({});
32
- async function fetchDoorbellSecret(secretArn, secretKey) {
32
+ async function fetchControllerSigningSecret(secretArn, secretKey) {
33
33
  const { SecretString } = await secrets.send(new import_client_secrets_manager.GetSecretValueCommand({ SecretId: secretArn }));
34
34
  if (!SecretString) {
35
- throw new Error(`doorbell secret ${secretArn} has no SecretString`);
35
+ throw new Error(`controller signing secret ${secretArn} has no SecretString`);
36
36
  }
37
37
  const parsed = JSON.parse(SecretString);
38
38
  const value = parsed !== null && typeof parsed === "object" ? parsed[secretKey] : void 0;
39
39
  if (typeof value !== "string" || value === "") {
40
- throw new Error(`doorbell secret ${secretArn} is missing a non-empty '${secretKey}' field`);
40
+ throw new Error(
41
+ `controller signing secret ${secretArn} is missing a non-empty '${secretKey}' field`
42
+ );
41
43
  }
42
44
  return value;
43
45
  }
@@ -63,7 +65,7 @@ function response() {
63
65
  return { PhysicalResourceId: PHYSICAL_RESOURCE_ID };
64
66
  }
65
67
  var runtimeDependencies = {
66
- getSecret: fetchDoorbellSecret,
68
+ getSecret: fetchControllerSigningSecret,
67
69
  fetch: globalThis.fetch
68
70
  };
69
71
  async function handleImageVersionReport(event, getConfig, dependencies = runtimeDependencies) {
@@ -77,10 +79,15 @@ async function handleImageVersionReport(event, getConfig, dependencies = runtime
77
79
  versions = readImageVersions(event.ResourceProperties?.imageVersions);
78
80
  const config = getConfig();
79
81
  runnerId = config.evidentRunnerId;
80
- const secret = await dependencies.getSecret(config.doorbellSecretArn, config.doorbellSecretKey);
82
+ const secret = await dependencies.getSecret(
83
+ config.controllerSigningSecretArn,
84
+ config.controllerSigningSecretKey
85
+ );
81
86
  const body = JSON.stringify({
82
87
  type: "runner.microvm_image_versions_reported",
83
- versions
88
+ versions,
89
+ // The API orders reports by this value because replacing an image restarts its version counter.
90
+ reporter_clock_ms: Date.now()
84
91
  });
85
92
  const signature = (0, import_node_crypto.createHmac)("sha256", secret).update(body).digest("hex");
86
93
  const url = `${config.evidentApiUrl.replace(/\/+$/, "")}/v1/runners/${encodeURIComponent(
@@ -117,8 +124,8 @@ async function handleImageVersionReport(event, getConfig, dependencies = runtime
117
124
  return response();
118
125
  }
119
126
  var handler = (event) => handleImageVersionReport(event, () => ({
120
- doorbellSecretArn: requireEnv("DOORBELL_SECRET_ARN"),
121
- doorbellSecretKey: requireEnv("DOORBELL_SECRET_KEY"),
127
+ controllerSigningSecretArn: requireEnv("CONTROLLER_SIGNING_SECRET_ARN"),
128
+ controllerSigningSecretKey: requireEnv("CONTROLLER_SIGNING_SECRET_KEY"),
122
129
  evidentApiUrl: requireEnv("EVIDENT_API_URL"),
123
130
  evidentRunnerId: requireEnv("EVIDENT_RUNNER_ID")
124
131
  }));
package/dist/index.d.ts CHANGED
@@ -3,4 +3,4 @@ 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
5
  export { MICROVM_MAX_RUN_SECONDS, HOOKS_PORT, HOOK_TIMEOUT_SECONDS, } from './microvm/constants';
6
- export { stageMicrovmImageContext, type StageMicrovmImageContextOptions, } from './microvm/image/stage-context';
6
+ export { MICROVM_WORKSPACE_DIRECTORY, stageMicrovmBuildContext, type StageMicrovmBuildContextOptions, } 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.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;
3
+ exports.stageMicrovmBuildContext = exports.MICROVM_WORKSPACE_DIRECTORY = 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");
@@ -17,4 +17,5 @@ Object.defineProperty(exports, "MICROVM_MAX_RUN_SECONDS", { enumerable: true, ge
17
17
  Object.defineProperty(exports, "HOOKS_PORT", { enumerable: true, get: function () { return constants_1.HOOKS_PORT; } });
18
18
  Object.defineProperty(exports, "HOOK_TIMEOUT_SECONDS", { enumerable: true, get: function () { return constants_1.HOOK_TIMEOUT_SECONDS; } });
19
19
  var stage_context_1 = require("./microvm/image/stage-context");
20
- Object.defineProperty(exports, "stageMicrovmImageContext", { enumerable: true, get: function () { return stage_context_1.stageMicrovmImageContext; } });
20
+ Object.defineProperty(exports, "MICROVM_WORKSPACE_DIRECTORY", { enumerable: true, get: function () { return stage_context_1.MICROVM_WORKSPACE_DIRECTORY; } });
21
+ Object.defineProperty(exports, "stageMicrovmBuildContext", { enumerable: true, get: function () { return stage_context_1.stageMicrovmBuildContext; } });
@@ -6,4 +6,4 @@ export declare const SUSPENDING_POLL_ATTEMPTS = 4;
6
6
  export declare const SUSPENDING_POLL_INTERVAL_MS = 500;
7
7
  export declare const MICROVM_MAX_RUN_SECONDS = 28800;
8
8
  export declare const SESSION_CLEANUP_MAX_AGE = "24h";
9
- export declare const DOORBELL_SECRET_KEY = "DOORBELL_SECRET";
9
+ export declare const CONTROLLER_SIGNING_SECRET_KEY = "CONTROLLER_SIGNING_SECRET";
@@ -4,7 +4,7 @@
4
4
  // across them. The image template pins matching copies, guarded by
5
5
  // `microvm/image/dockerfile.test.ts`.
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.DOORBELL_SECRET_KEY = exports.SESSION_CLEANUP_MAX_AGE = 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;
7
+ exports.CONTROLLER_SIGNING_SECRET_KEY = exports.SESSION_CLEANUP_MAX_AGE = 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;
8
8
  // The port the image's hook server binds, baked into the published Dockerfile
9
9
  // (`ENV HOOKS_PORT`) and passed to the construct as `hooksPort`. Exported from
10
10
  // the package index because a consumer building the published image context
@@ -38,6 +38,9 @@ exports.MICROVM_MAX_RUN_SECONDS = 28800;
38
38
  // The 24h window rather than 7d keeps the session database bounded at the growth
39
39
  // rate observed in #537. The sweep runs on the default 1h interval.
40
40
  exports.SESSION_CLEANUP_MAX_AGE = '24h';
41
- // The JSON field inside the doorbell secret is shared by the Lambdas that name
42
- // it and by whatever produces the secret; this is never the secret value.
43
- exports.DOORBELL_SECRET_KEY = 'DOORBELL_SECRET';
41
+ // The JSON field inside the controller signing secret is shared by the Lambdas
42
+ // that name it and by whatever produces the secret; this is never the secret
43
+ // value. It is customer-facing and separate from persisted doorbell messages
44
+ // and telemetry. Changing the value silently stops an existing adopter's
45
+ // secret from being read.
46
+ exports.CONTROLLER_SIGNING_SECRET_KEY = 'CONTROLLER_SIGNING_SECRET';
@@ -4,12 +4,10 @@ import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
4
4
  import { type MicrovmShape } from './shapes';
5
5
  export interface EvidentMicrovmConstructProps {
6
6
  /**
7
- * Build context for the image, already staged on disk. An argument rather
8
- * than something this construct stages itself, so synthesising the stack
9
- * stays a pure function of its inputs instead of depending on how the
10
- * ambient checkout was cloned.
7
+ * Directory containing the consumer's Dockerfile and every file it copies,
8
+ * typically the return value of `stageMicrovmBuildContext()`.
11
9
  */
12
- readonly imageSource: string;
10
+ readonly buildContextPath: string;
13
11
  /**
14
12
  * ARN of the Lambda-managed base MicroVM image, from
15
13
  * `aws lambda-microvms list-managed-microvm-images`. A string, not a `CfnParameter`
@@ -33,14 +31,14 @@ export interface EvidentMicrovmConstructProps {
33
31
  readonly shapes?: readonly MicrovmShape[];
34
32
  /**
35
33
  * The secret holding the shared HMAC key the controller verifies every
36
- * doorbell against, under the field `DOORBELL_SECRET`. Any Secrets Manager
34
+ * doorbell against, under the field `CONTROLLER_SIGNING_SECRET`. Any Secrets Manager
37
35
  * secret, however the caller manages it — this construct has no opinion on
38
36
  * SOPS, KMS, or any other production mechanism. Required (not defaulted
39
37
  * here): a default would need a scope of its own, and a caller-owned
40
38
  * default keeps its own logical id — the caller builds one exactly once,
41
39
  * in its own scope, and passes the result in either case.
42
40
  */
43
- readonly doorbellSecret: secretsmanager.ISecret;
41
+ readonly controllerSigningSecret: secretsmanager.ISecret;
44
42
  /**
45
43
  * Optional runner secret whose JSON values are exported by `/run`. Omitting it
46
44
  * leaves GitHub and MCP credentials unavailable to a generic consumer.
@@ -65,7 +63,7 @@ export interface EvidentMicrovmConstructProps {
65
63
  * TCP port the image's hook server listens on, baked into both the image
66
64
  * (`HOOKS_PORT` env var) and the `MicrovmImage`'s `hooks.port` — a mismatch
67
65
  * means AWS probes a port nothing binds. Caller-supplied because the hook
68
- * server is part of `imageSource`, which this construct does not build.
66
+ * server is part of the base image, which this construct does not build.
69
67
  */
70
68
  readonly hooksPort: number;
71
69
  /**
@@ -37,6 +37,7 @@ exports.EvidentMicrovmConstruct = void 0;
37
37
  const path = __importStar(require("path"));
38
38
  const cdk = __importStar(require("aws-cdk-lib"));
39
39
  const constructs_1 = require("constructs");
40
+ const iam = __importStar(require("aws-cdk-lib/aws-iam"));
40
41
  const lambda = __importStar(require("aws-cdk-lib/aws-lambda"));
41
42
  const s3 = __importStar(require("aws-cdk-lib/aws-s3"));
42
43
  const lambda_microvm_cdk_1 = require("@evident-ai/lambda-microvm-cdk");
@@ -145,42 +146,31 @@ class EvidentMicrovmConstruct extends constructs_1.Construct {
145
146
  lifecycleTimeout: cdk.Duration.seconds(props.hookTimeoutSeconds),
146
147
  buildTimeout: cdk.Duration.seconds(IMAGE_HOOK_TIMEOUT_SECONDS),
147
148
  };
148
- // Built first and with no `executionRole` prop, exactly as before this
149
- // shape catalogue existed: same construct id (`Image`, frozen on the
150
- // shape record — see shapes.ts), same `imageName`, same description. The
151
- // construct therefore creates `Image/ExecutionRole` at its current path
152
- // and current logical id — zero CloudFormation churn on the live,
153
- // hand-deployed stack (D3/D9). Every additional shape below borrows this
154
- // role rather than getting one of its own, which is why `validateShapes`
155
- // requires the default shape to be `shapes[0]`: it must exist before any
156
- // other shape can reference its execution role.
157
- const defaultImage = new lambda_microvm_cdk_1.MicrovmImage(this, defaultShape.constructId, {
149
+ // Built first without an `executionRole` prop, so every additional shape
150
+ // can borrow this image's execution role. `validateShapes` requires the
151
+ // default shape to be `shapes[0]` because that role must exist first.
152
+ const defaultImage = new lambda_microvm_cdk_1.MicrovmImage(this, defaultShape.name, {
158
153
  imageName: defaultShape.imageName,
159
154
  description: defaultShape.description,
160
155
  // A local directory, so the zip is uploaded by `cdk deploy` itself and the
161
156
  // image ARN is a stack reference rather than a deploy-time input.
162
- source: props.imageSource,
157
+ source: props.buildContextPath,
163
158
  baseImageArn: props.baseImageArn,
164
159
  baseImageVersion: props.baseImageVersion,
165
160
  memoryMiB: defaultShape.memoryMiB,
166
161
  environment: imageEnvironment,
167
162
  hooks: imageHooks,
168
163
  });
169
- // WART, accepted deliberately: every additional shape's image is built
170
- // with `defaultImage.executionRole`, so this role's construct path
171
- // (`Image/ExecutionRole`) reads as if it belongs to just the default
172
- // shape's image, when in fact it is shared by every shape. That is
173
- // intentional — see D3 in the plan. Do NOT "fix" this with
174
- // `overrideLogicalId`: renaming the logical id IS the CloudFormation
175
- // churn this design exists to avoid (a delete-and-recreate of the role a
176
- // running MicroVM has live session credentials from). The wart is one
177
- // comment; the override would undo the point of D3.
164
+ // WART: every additional shape shares `defaultImage.executionRole`, so the
165
+ // role's path under the default image reads as if it belongs to one shape
166
+ // even though it serves all of them. Keeping one role avoids replacing a
167
+ // role that running MicroVMs may still have credentials from.
178
168
  const images = new Map([[defaultShape.name, defaultImage]]);
179
169
  for (const shape of additionalShapes) {
180
- images.set(shape.name, new lambda_microvm_cdk_1.MicrovmImage(this, shape.constructId, {
170
+ images.set(shape.name, new lambda_microvm_cdk_1.MicrovmImage(this, shape.name, {
181
171
  imageName: shape.imageName,
182
172
  description: shape.description,
183
- source: props.imageSource,
173
+ source: props.buildContextPath,
184
174
  baseImageArn: props.baseImageArn,
185
175
  baseImageVersion: props.baseImageVersion,
186
176
  memoryMiB: shape.memoryMiB,
@@ -189,13 +179,14 @@ class EvidentMicrovmConstruct extends constructs_1.Construct {
189
179
  hooks: imageHooks,
190
180
  }));
191
181
  }
192
- // Bucket-wide rather than per-prefix (as the ECS runner's task role is):
193
- // the prefix arrives at /run, so it cannot be named at synth, and one
194
- // execution role is shared by every VM of every shape (D3). So a VM can
195
- // read another session's credential store — acceptable within a single
196
- // customer account, and what Phase 4 (#263) has to tighten when this goes
197
- // multi-tenant.
198
- durableState.grantReadWrite(defaultImage.executionRole);
182
+ // The execution role serves every image shape, so per-session state is scoped
183
+ // to the `runners/` root; vault credentials need only GetObject and PutObject.
184
+ durableState.grantReadWrite(defaultImage.executionRole, 'runners/*');
185
+ defaultImage.executionRole.addToPrincipalPolicy(new iam.PolicyStatement({
186
+ sid: 'ReadWriteCredentialVaultObjects',
187
+ actions: ['s3:GetObject', 's3:PutObject'],
188
+ resources: [durableState.arnForObjects('credential-vaults/*')],
189
+ }));
199
190
  // The image receives only an ARN; `/run` reads the secret value at runtime.
200
191
  props.runnerSecret?.grantRead(defaultImage.executionRole);
201
192
  // The shape catalogue the controller resolves a doorbell's `shape` field
@@ -235,8 +226,8 @@ class EvidentMicrovmConstruct extends constructs_1.Construct {
235
226
  // The ARN and field name, never the value: the controller reads it from
236
227
  // Secrets Manager at runtime, so the key is not exposed to anyone with
237
228
  // lambda:GetFunctionConfiguration.
238
- DOORBELL_SECRET_ARN: props.doorbellSecret.secretArn,
239
- DOORBELL_SECRET_KEY: constants_1.DOORBELL_SECRET_KEY,
229
+ CONTROLLER_SIGNING_SECRET_ARN: props.controllerSigningSecret.secretArn,
230
+ CONTROLLER_SIGNING_SECRET_KEY: constants_1.CONTROLLER_SIGNING_SECRET_KEY,
240
231
  MICROVM_SHAPES: shapeCatalogueJson,
241
232
  // The role every shape's MicroVM shares (D3) — still one role, still
242
233
  // this same expression, now documented as shared rather than singular.
@@ -251,7 +242,7 @@ class EvidentMicrovmConstruct extends constructs_1.Construct {
251
242
  // emit N near-identical policy statements and break the "exactly one
252
243
  // statement granting X" guarantees this construct's test encodes.
253
244
  defaultImage.grantRun(controller);
254
- props.doorbellSecret.grantRead(controller);
245
+ props.controllerSigningSecret.grantRead(controller);
255
246
  const functionUrl = controller.addFunctionUrl({
256
247
  // HMAC verification of the doorbell body IS the auth, as for the ECS waker.
257
248
  authType: lambda.FunctionUrlAuthType.NONE,
@@ -261,7 +252,7 @@ class EvidentMicrovmConstruct extends constructs_1.Construct {
261
252
  // no custom resource — this branch must not run for them.
262
253
  if (props.evidentApiUrl && props.evidentRunnerId) {
263
254
  new construct_1.ImageVersionReporter(this, 'ImageVersionReporter', {
264
- doorbellSecret: props.doorbellSecret,
255
+ controllerSigningSecret: props.controllerSigningSecret,
265
256
  evidentApiUrl: props.evidentApiUrl,
266
257
  evidentRunnerId: props.evidentRunnerId,
267
258
  imageVersions: Object.fromEntries(shapes.map((shape) => [shape.name, images.get(shape.name).latestActiveImageVersion])),
@@ -17,10 +17,10 @@ export type DoorbellResult = {
17
17
  kind: ActionKind;
18
18
  };
19
19
  };
20
- export declare function handleDoorbell({ rawBody, signatureHeader, doorbellSecret, shapes, microvm, sleep, random, }: {
20
+ export declare function handleDoorbell({ rawBody, signatureHeader, controllerSigningSecret, shapes, microvm, sleep, random, }: {
21
21
  rawBody: string;
22
22
  signatureHeader: string | undefined;
23
- doorbellSecret: string;
23
+ controllerSigningSecret: string;
24
24
  shapes: ShapeCatalogue;
25
25
  microvm: MicrovmClient;
26
26
  } & Timing): Promise<DoorbellResult>;
@@ -174,8 +174,8 @@ async function shouldRecreateForNewerImage(doorbellRequest, shape, runningVersio
174
174
  // been deactivated, and recreating would not roll it forward. Resume.
175
175
  return compareImageVersions(running, latest) < 0;
176
176
  }
177
- async function handleDoorbell({ rawBody, signatureHeader, doorbellSecret, shapes, microvm, sleep, random, }) {
178
- if (!(0, sdk_1.verifyEvidentSignature)(rawBody, signatureHeader, doorbellSecret)) {
177
+ async function handleDoorbell({ rawBody, signatureHeader, controllerSigningSecret, shapes, microvm, sleep, random, }) {
178
+ if (!(0, sdk_1.verifyEvidentSignature)(rawBody, signatureHeader, controllerSigningSecret)) {
179
179
  return decide({
180
180
  statusCode: 401,
181
181
  action: 'rejected',