@evident-ai/runner-cdk 3.5.2-dev.bcf1140 → 3.5.2-dev.cffa626

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.
Files changed (36) hide show
  1. package/README.md +91 -40
  2. package/dist/controller-lambda/handler.js +74 -79
  3. package/dist/evident-scale-to-zero-construct.js +7 -0
  4. package/dist/image-version-pruner-lambda/handler.js +28250 -0
  5. package/dist/image-version-reporter-lambda/handler.js +15 -8
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.js +3 -2
  8. package/dist/microvm/constants.d.ts +1 -1
  9. package/dist/microvm/constants.js +7 -4
  10. package/dist/microvm/construct.d.ts +12 -8
  11. package/dist/microvm/construct.js +42 -33
  12. package/dist/microvm/controller/{doorbell.d.ts → doorbell-request.d.ts} +6 -6
  13. package/dist/microvm/controller/{doorbell.js → doorbell-request.js} +2 -2
  14. package/dist/microvm/controller/handle-doorbell.d.ts +2 -2
  15. package/dist/microvm/controller/handle-doorbell.js +62 -62
  16. package/dist/microvm/image/stage-context.d.ts +13 -38
  17. package/dist/microvm/image/stage-context.js +30 -120
  18. package/dist/microvm/image-version-pruner/construct.d.ts +13 -0
  19. package/dist/microvm/image-version-pruner/construct.js +79 -0
  20. package/dist/microvm/image-version-pruner/handler.d.ts +20 -0
  21. package/dist/microvm/image-version-pruner/handler.js +116 -0
  22. package/dist/microvm/image-version-reporter/construct.d.ts +1 -1
  23. package/dist/microvm/image-version-reporter/construct.js +3 -3
  24. package/dist/microvm/image-version-reporter/handler.d.ts +2 -2
  25. package/dist/microvm/image-version-reporter/handler.js +9 -7
  26. package/dist/microvm/shapes.d.ts +10 -21
  27. package/dist/microvm/shapes.js +10 -17
  28. package/dist/waker-lambda/handler.js +3 -3
  29. package/package.json +3 -3
  30. package/dist/microvm-image-context/Dockerfile +0 -190
  31. package/dist/microvm-image-context/hook-server.js +0 -286
  32. package/dist/microvm-image-context/hooks/common.sh +0 -905
  33. package/dist/microvm-image-context/hooks/resume +0 -81
  34. package/dist/microvm-image-context/hooks/run +0 -95
  35. package/dist/microvm-image-context/hooks/suspend +0 -21
  36. package/dist/microvm-image-context/hooks/terminate +0 -36
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
 
@@ -76,8 +76,8 @@ agent.selfStopRoleArn;
76
76
  agent.replicaPrefix;
77
77
  ```
78
78
 
79
- For a complete working example, see `GETTING-STARTED.md` (not published to npm); it is the
80
- construct's own dogfooding consumer.
79
+ For the full end-to-end walkthrough for this construct, see
80
+ [GETTING-STARTED.md](https://github.com/evident-run/evident-run/blob/main/aws/runner-cdk/GETTING-STARTED.md).
81
81
 
82
82
  ## Props
83
83
 
@@ -99,6 +99,26 @@ construct's own dogfooding consumer.
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
+ | `imageVersionPruning` | `boolean?` | Opts into construct-owned pruning for this deployment's images. Uses the engine defaults; omitting it creates no pruning resources. |
117
+
118
+ The `DoorbellSecret` CloudFormation logical ID remains stable for Evident's own
119
+ default stack. It identifies the deployed secret resource, not the customer-facing
120
+ name of the signing key.
121
+
102
122
  ### Two deliberate "no default" choices
103
123
 
104
124
  - **The endpoints are required.** Defaulting them would either bake *our* dev environment
@@ -116,57 +136,73 @@ built from a `Dockerfile` you control. The generic runner image lives separately
116
136
 
117
137
  ## The MicroVM image
118
138
 
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.
139
+ ### Prune image versions
123
140
 
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:
141
+ MicroVM image versions are quota-bound. Opt into construct-owned pruning with one
142
+ property:
143
+
144
+ ```ts
145
+ new EvidentMicrovmConstruct(this, 'Runner', {
146
+ // ...other required MicroVM props
147
+ imageVersionPruning: true,
148
+ });
149
+ ```
150
+
151
+ This bounds the per-image version quota (`L-F8BECE9C`) for this deployment's shapes only.
152
+ Retention follows the engine defaults, and pruning runs before the image publish for the
153
+ deploy that invokes it. The custom-resource Lambda writes per-image verdicts and a summary
154
+ to CloudWatch Logs. Use the `prune-microvm-image-versions` CLI for deployments that do not
155
+ use this construct or that need non-default retention; see the
156
+ `@evident-ai/lambda-microvm-cdk` package README for its flags and safety semantics.
157
+
158
+ `EvidentMicrovmConstruct`'s `buildContextPath` is a **local directory** holding the
159
+ Dockerfile you control and everything it copies. `cdk deploy` zips and uploads that
160
+ directory, and the Lambda MicroVM service builds the image in *your* account. Start the
161
+ Dockerfile from a pinned `ghcr.io/sroze/evident-microvm-base:X.Y.Z` tag; the base image
162
+ contains the hook server, lifecycle hooks and common agent tooling.
163
+
164
+ `stageMicrovmBuildContext()` combines your build-context directory with a full checkout
165
+ of the workspace in a fresh temporary directory. The workspace is written at
166
+ `workspace/`, and the returned path is the value for `buildContextPath`:
129
167
 
130
168
  ```ts
131
- import path from 'node:path';
132
169
  import {
133
170
  EvidentMicrovmConstruct,
134
171
  HOOKS_PORT,
135
172
  HOOK_TIMEOUT_SECONDS,
136
- stageMicrovmImageContext,
173
+ stageMicrovmBuildContext,
137
174
  } from '@evident-ai/runner-cdk';
138
175
 
176
+ const buildContextPath = stageMicrovmBuildContext({
177
+ // A full (non-shallow) clone: the agent branches, commits and opens PRs.
178
+ workspaceRepositoryPath: '/path/to/your/checkout',
179
+ // Credential-free: the token the agent pushes with arrives per-session.
180
+ workspaceOriginUrl: 'https://github.com/acme/widgets.git',
181
+ buildContextPath: '/path/to/your/microvm-build-context',
182
+ });
183
+
139
184
  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
- }),
185
+ buildContextPath,
150
186
  // Must match the published image these were baked into, so export them
151
187
  // rather than restating the numbers.
152
188
  hooksPort: HOOKS_PORT,
153
189
  hookTimeoutSeconds: HOOK_TIMEOUT_SECONDS,
154
190
  baseImageArn,
155
191
  baseImageVersion,
156
- doorbellSecret,
192
+ controllerSigningSecret,
157
193
  runnerSecret,
158
194
  runnerOpencodeConfigPath: 'opencode.runner.jsonc',
159
195
  });
160
196
  ```
161
197
 
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](../../runner/docker-images/microvm/README.md) for the full overlay contract.
198
+ Write project-specific installation and build steps in your Dockerfile. The [MicroVM base
199
+ image README](../../runner/docker-images/microvm/README.md) documents the consumer
200
+ contract, including the required `USER`, `WORKDIR`, ownership and `git reset --quiet`
201
+ steps.
166
202
 
167
- The image bakes your repository checked out but not installed and makes no assumptions about
168
- its package manager. To speed up first boot, install dependencies from your overlay's
169
- `setup-workspace`; otherwise, the agent installs them on first use.
203
+ The staged image includes your repository checkout, while your Dockerfile controls its
204
+ package manager and build warm-up. Do not copy credentials into the build context or bake
205
+ them into image environment variables.
170
206
 
171
207
  | MicroVM prop | Omitted behavior |
172
208
  | --- | --- |
@@ -174,12 +210,27 @@ its package manager. To speed up first boot, install dependencies from your over
174
210
  | `runnerOpencodeConfigPath` | OpenCode uses the baked project configuration. Relative paths resolve from the workspace; absolute paths resolve in the image. |
175
211
  | `extraImageEnvironment` | Non-secret values baked into every VM launched from this image version; every VM can read them, so never put secrets here. |
176
212
  | `gitUserName` / `gitUserEmail` | The hook uses the Evident bot defaults for git identity. |
177
- | `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. |
178
-
179
- `extraImageEnvironment` is the runtime counterpart to an overlay. Use it when an
180
- overlay-installed component needs an environment variable after the VM boots. The
181
- values are baked into every VM launched from that image version and are readable by
182
- all of them, so never put a secret in this prop.
213
+ | `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. |
214
+
215
+ `extraImageEnvironment` is the runtime environment for the image. Values are baked into
216
+ every VM launched from that image version and are readable by all of them, so never put a
217
+ secret in this prop.
218
+
219
+ ## Breaking changes in this release
220
+
221
+ - `stageMicrovmImageContext` is removed. Use `stageMicrovmBuildContext()` with your
222
+ Dockerfile and build context.
223
+ - `imageSource` is renamed to `buildContextPath`.
224
+ - `MicrovmShape.constructId` is removed. CloudFormation child identity derives from
225
+ the shape `name`.
226
+ - `doorbellSecret` is renamed to `controllerSigningSecret`.
227
+ - The default shape's `imageName` changes to `evident-microvm-standard`. The
228
+ `AWS::Lambda::MicrovmImage` resource, its auto-created build role and execution
229
+ role, and both roles' default policies are replaced with the new shape identity.
230
+ - The JSON field in the caller-supplied Secrets Manager secret changes from
231
+ `DOORBELL_SECRET` to `CONTROLLER_SIGNING_SECRET`. Rename that field in your own
232
+ secret. If you do not, the controller reads no signing key and every doorbell
233
+ returns HTTP 401 at runtime; TypeScript cannot catch this field-key change.
183
234
 
184
235
  ## Status / limitations
185
236
 
@@ -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
  }
@@ -49818,7 +49818,7 @@ function verifyWakeSignature(rawBody, signatureHeader, secret) {
49818
49818
  return (0, import_node_crypto11.timingSafeEqual)(providedBuffer, expectedBuffer);
49819
49819
  }
49820
49820
 
49821
- // src/microvm/controller/doorbell.ts
49821
+ // src/microvm/controller/doorbell-request.ts
49822
49822
  var WAKE_EVENT_TYPES = ["agent.message_queued", "agent.wake_requested"];
49823
49823
  var SUSPEND_EVENT_TYPE = "runner.suspend_requested";
49824
49824
  var SHAPES_EVENT_TYPE = "runner.shapes_requested";
@@ -49838,7 +49838,7 @@ function nonBlankString(value) {
49838
49838
  function isWakeType(value) {
49839
49839
  return WAKE_EVENT_TYPES.includes(value);
49840
49840
  }
49841
- function parseDoorbell(rawBody) {
49841
+ function parseDoorbellRequest(rawBody) {
49842
49842
  let body;
49843
49843
  try {
49844
49844
  body = JSON.parse(rawBody);
@@ -49979,9 +49979,9 @@ function decide({
49979
49979
  action: { kind: action }
49980
49980
  };
49981
49981
  }
49982
- function clientTokenFor(doorbell) {
49983
- const generation = doorbell.microvmId ?? doorbell.occurredAt;
49984
- return (0, import_node_crypto12.createHash)("sha256").update(`${doorbell.runnerId}:${generation}`).digest("hex");
49982
+ function clientTokenFor(doorbellRequest) {
49983
+ const generation = doorbellRequest.microvmId ?? doorbellRequest.occurredAt;
49984
+ return (0, import_node_crypto12.createHash)("sha256").update(`${doorbellRequest.runnerId}:${generation}`).digest("hex");
49985
49985
  }
49986
49986
  function parseImageVersion(value) {
49987
49987
  const parts = value.trim().split(".");
@@ -50001,17 +50001,17 @@ function compareImageVersions(a7, b7) {
50001
50001
  }
50002
50002
  return 0;
50003
50003
  }
50004
- async function shouldRecreateForNewerImage(doorbell, shape, runningVersion, microvm) {
50004
+ async function shouldRecreateForNewerImage(doorbellRequest, shape, runningVersion, microvm) {
50005
50005
  if (runningVersion === void 0) {
50006
50006
  console.warn(
50007
- `[doorbell] GetMicrovm reported no imageVersion for runner ${doorbell.runnerId}; resuming`
50007
+ `[doorbell] GetMicrovm reported no imageVersion for runner ${doorbellRequest.runnerId}; resuming`
50008
50008
  );
50009
50009
  return false;
50010
50010
  }
50011
50011
  const running = parseImageVersion(runningVersion);
50012
50012
  if (running === void 0) {
50013
50013
  console.warn(
50014
- `[doorbell] unparseable running imageVersion "${runningVersion}" for runner ${doorbell.runnerId}; resuming`
50014
+ `[doorbell] unparseable running imageVersion "${runningVersion}" for runner ${doorbellRequest.runnerId}; resuming`
50015
50015
  );
50016
50016
  return false;
50017
50017
  }
@@ -50020,14 +50020,14 @@ async function shouldRecreateForNewerImage(doorbell, shape, runningVersion, micr
50020
50020
  latestVersion = await microvm.latestImageVersion(shape.imageArn);
50021
50021
  } catch (error3) {
50022
50022
  console.warn(
50023
- `[doorbell] GetMicrovmImage failed for runner ${doorbell.runnerId}; resuming on the running image ${runningVersion}: ${error3 instanceof Error ? error3.message : String(error3)}`
50023
+ `[doorbell] GetMicrovmImage failed for runner ${doorbellRequest.runnerId}; resuming on the running image ${runningVersion}: ${error3 instanceof Error ? error3.message : String(error3)}`
50024
50024
  );
50025
50025
  return false;
50026
50026
  }
50027
50027
  const latest = latestVersion === void 0 ? void 0 : parseImageVersion(latestVersion);
50028
50028
  if (latest === void 0) {
50029
50029
  console.warn(
50030
- `[doorbell] no comparable latest image version (${latestVersion ?? "none"}) for runner ${doorbell.runnerId}; resuming on the running image ${runningVersion}`
50030
+ `[doorbell] no comparable latest image version (${latestVersion ?? "none"}) for runner ${doorbellRequest.runnerId}; resuming on the running image ${runningVersion}`
50031
50031
  );
50032
50032
  return false;
50033
50033
  }
@@ -50036,20 +50036,20 @@ async function shouldRecreateForNewerImage(doorbell, shape, runningVersion, micr
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",
50049
50049
  reason: REASON.invalidSignature
50050
50050
  });
50051
50051
  }
50052
- const parsed = parseDoorbell(rawBody);
50052
+ const parsed = parseDoorbellRequest(rawBody);
50053
50053
  if (!parsed.ok) {
50054
50054
  return decide({
50055
50055
  statusCode: 400,
@@ -50057,9 +50057,9 @@ async function handleDoorbell({
50057
50057
  reason: parsed.reason
50058
50058
  });
50059
50059
  }
50060
- const doorbell = parsed.doorbell;
50060
+ const doorbellRequest = parsed.doorbell;
50061
50061
  const timing = { sleep: sleep4, random };
50062
- if (doorbell.type === SHAPES_EVENT_TYPE) {
50062
+ if (doorbellRequest.type === SHAPES_EVENT_TYPE) {
50063
50063
  return decide({
50064
50064
  statusCode: 200,
50065
50065
  action: "describe",
@@ -50067,43 +50067,43 @@ async function handleDoorbell({
50067
50067
  shapes: shapes.advertise()
50068
50068
  });
50069
50069
  }
50070
- if (doorbell.type !== "runner.suspend_requested") {
50071
- const resolvedShape = shapes.resolve(doorbell.shape);
50070
+ if (doorbellRequest.type !== "runner.suspend_requested") {
50071
+ const resolvedShape = shapes.resolve(doorbellRequest.shape);
50072
50072
  if (resolvedShape === void 0) {
50073
50073
  return decide({
50074
50074
  statusCode: 400,
50075
50075
  action: "rejected",
50076
50076
  reason: REASON.unknownShape,
50077
- runnerId: doorbell.runnerId,
50078
- microvmId: doorbell.microvmId
50077
+ runnerId: doorbellRequest.runnerId,
50078
+ microvmId: doorbellRequest.microvmId
50079
50079
  });
50080
50080
  }
50081
- return handleWakeDoorbell(doorbell, resolvedShape, microvm, timing);
50081
+ return handleWakeDoorbell(doorbellRequest, resolvedShape, microvm, timing);
50082
50082
  }
50083
- return suspendMicrovm(doorbell, microvm, timing);
50083
+ return suspendMicrovm(doorbellRequest, microvm, timing);
50084
50084
  }
50085
- async function handleWakeDoorbell(doorbell, shape, microvm, timing) {
50086
- if (doorbell.microvmId === void 0) {
50087
- return runMicrovm(doorbell, shape, microvm, timing, false, REASON.noMicrovmId);
50085
+ async function handleWakeDoorbell(doorbellRequest, shape, microvm, timing) {
50086
+ if (doorbellRequest.microvmId === void 0) {
50087
+ return runMicrovm(doorbellRequest, shape, microvm, timing, false, REASON.noMicrovmId);
50088
50088
  }
50089
50089
  let described;
50090
50090
  let polled = false;
50091
50091
  try {
50092
- described = await microvm.describe(doorbell.microvmId);
50092
+ described = await microvm.describe(doorbellRequest.microvmId);
50093
50093
  if (described.state === "SUSPENDING") {
50094
- described = await pollWhileSuspending(doorbell.microvmId, microvm, timing.sleep);
50094
+ described = await pollWhileSuspending(doorbellRequest.microvmId, microvm, timing.sleep);
50095
50095
  polled = true;
50096
50096
  }
50097
50097
  } catch (error3) {
50098
50098
  console.error(
50099
- `[doorbell] GetMicrovm failed for runner ${doorbell.runnerId}: ${error3 instanceof Error ? error3.message : String(error3)}`
50099
+ `[doorbell] GetMicrovm failed for runner ${doorbellRequest.runnerId}: ${error3 instanceof Error ? error3.message : String(error3)}`
50100
50100
  );
50101
50101
  return decide({
50102
50102
  statusCode: 500,
50103
50103
  action: "none",
50104
50104
  reason: REASON.getStateFailed,
50105
- runnerId: doorbell.runnerId,
50106
- microvmId: doorbell.microvmId
50105
+ runnerId: doorbellRequest.runnerId,
50106
+ microvmId: doorbellRequest.microvmId
50107
50107
  });
50108
50108
  }
50109
50109
  const state3 = described.state;
@@ -50113,8 +50113,8 @@ async function handleWakeDoorbell(doorbell, shape, microvm, timing) {
50113
50113
  action: "none",
50114
50114
  reason: REASON.suspendingPollExhausted,
50115
50115
  state: state3,
50116
- runnerId: doorbell.runnerId,
50117
- microvmId: doorbell.microvmId,
50116
+ runnerId: doorbellRequest.runnerId,
50117
+ microvmId: doorbellRequest.microvmId,
50118
50118
  polled
50119
50119
  });
50120
50120
  }
@@ -50126,16 +50126,16 @@ async function handleWakeDoorbell(doorbell, shape, microvm, timing) {
50126
50126
  action: "none",
50127
50127
  reason: state3 === "RUNNING" ? REASON.alreadyRunning : REASON.alreadyStarting,
50128
50128
  state: state3,
50129
- runnerId: doorbell.runnerId,
50130
- microvmId: doorbell.microvmId,
50129
+ runnerId: doorbellRequest.runnerId,
50130
+ microvmId: doorbellRequest.microvmId,
50131
50131
  polled,
50132
50132
  startedAt: described.startedAt,
50133
50133
  imageVersion: described.imageVersion
50134
50134
  });
50135
50135
  case "SUSPENDED":
50136
- if (doorbell.recreateOnOutdatedImage === true && await shouldRecreateForNewerImage(doorbell, shape, described.imageVersion, microvm)) {
50136
+ if (doorbellRequest.recreateOnOutdatedImage === true && await shouldRecreateForNewerImage(doorbellRequest, shape, described.imageVersion, microvm)) {
50137
50137
  return runMicrovm(
50138
- doorbell,
50138
+ doorbellRequest,
50139
50139
  shape,
50140
50140
  microvm,
50141
50141
  timing,
@@ -50145,8 +50145,8 @@ async function handleWakeDoorbell(doorbell, shape, microvm, timing) {
50145
50145
  );
50146
50146
  }
50147
50147
  return resumeMicrovm(
50148
- doorbell,
50149
- doorbell.microvmId,
50148
+ doorbellRequest,
50149
+ doorbellRequest.microvmId,
50150
50150
  shape,
50151
50151
  microvm,
50152
50152
  timing,
@@ -50156,7 +50156,7 @@ async function handleWakeDoorbell(doorbell, shape, microvm, timing) {
50156
50156
  );
50157
50157
  default:
50158
50158
  return runMicrovm(
50159
- doorbell,
50159
+ doorbellRequest,
50160
50160
  shape,
50161
50161
  microvm,
50162
50162
  timing,
@@ -50182,14 +50182,14 @@ function isClientTokenParameterMismatch(error3) {
50182
50182
  const message = typeof e7.message === "string" ? e7.message.toLowerCase() : "";
50183
50183
  return message.includes("clienttoken") && message.includes("different request parameters");
50184
50184
  }
50185
- async function runMicrovm(doorbell, shape, microvm, timing, polled, reason, state3) {
50185
+ async function runMicrovm(doorbellRequest, shape, microvm, timing, polled, reason, state3) {
50186
50186
  let started;
50187
50187
  try {
50188
50188
  started = await withThrottleRetry(
50189
50189
  () => microvm.run({
50190
50190
  imageIdentifier: shape.imageArn,
50191
- runHookPayload: doorbell.runHookPayload,
50192
- clientToken: clientTokenFor(doorbell)
50191
+ runHookPayload: doorbellRequest.runHookPayload,
50192
+ clientToken: clientTokenFor(doorbellRequest)
50193
50193
  }),
50194
50194
  timing
50195
50195
  );
@@ -50198,26 +50198,26 @@ async function runMicrovm(doorbell, shape, microvm, timing, polled, reason, stat
50198
50198
  const name = error3 instanceof Error ? error3.name : "unknown";
50199
50199
  const message = error3 instanceof Error ? error3.message : String(error3);
50200
50200
  console.warn(
50201
- `[doorbell] RunMicrovm clientToken already used for runner ${doorbell.runnerId}: ${name}: ${message}`
50201
+ `[doorbell] RunMicrovm clientToken already used for runner ${doorbellRequest.runnerId}: ${name}: ${message}`
50202
50202
  );
50203
50203
  return decide({
50204
50204
  statusCode: 200,
50205
50205
  action: "run",
50206
50206
  reason: REASON.runAlreadyStartedForToken,
50207
50207
  state: state3,
50208
- runnerId: doorbell.runnerId,
50208
+ runnerId: doorbellRequest.runnerId,
50209
50209
  polled
50210
50210
  });
50211
50211
  }
50212
50212
  console.error(
50213
- `[doorbell] RunMicrovm failed for runner ${doorbell.runnerId}: ${error3 instanceof Error ? error3.message : String(error3)}`
50213
+ `[doorbell] RunMicrovm failed for runner ${doorbellRequest.runnerId}: ${error3 instanceof Error ? error3.message : String(error3)}`
50214
50214
  );
50215
50215
  return decide({
50216
50216
  statusCode: 500,
50217
50217
  action: "run",
50218
50218
  reason: REASON.runFailed,
50219
50219
  state: state3,
50220
- runnerId: doorbell.runnerId,
50220
+ runnerId: doorbellRequest.runnerId,
50221
50221
  polled
50222
50222
  });
50223
50223
  }
@@ -50226,22 +50226,22 @@ async function runMicrovm(doorbell, shape, microvm, timing, polled, reason, stat
50226
50226
  action: "run",
50227
50227
  reason,
50228
50228
  state: state3,
50229
- runnerId: doorbell.runnerId,
50229
+ runnerId: doorbellRequest.runnerId,
50230
50230
  microvmId: started.microvmId,
50231
50231
  polled,
50232
50232
  startedAt: started.startedAt,
50233
50233
  imageVersion: started.imageVersion
50234
50234
  });
50235
50235
  }
50236
- async function resumeMicrovm(doorbell, microvmId, shape, microvm, timing, polled, startedAt, imageVersion) {
50236
+ async function resumeMicrovm(doorbellRequest, microvmId, shape, microvm, timing, polled, startedAt, imageVersion) {
50237
50237
  try {
50238
50238
  await withThrottleRetry(() => microvm.resume(microvmId), timing);
50239
50239
  } catch (error3) {
50240
50240
  console.error(
50241
- `[doorbell] ResumeMicrovm failed for runner ${doorbell.runnerId}: ${error3 instanceof Error ? error3.message : String(error3)}`
50241
+ `[doorbell] ResumeMicrovm failed for runner ${doorbellRequest.runnerId}: ${error3 instanceof Error ? error3.message : String(error3)}`
50242
50242
  );
50243
50243
  return runMicrovm(
50244
- doorbell,
50244
+ doorbellRequest,
50245
50245
  shape,
50246
50246
  microvm,
50247
50247
  timing,
@@ -50255,21 +50255,21 @@ async function resumeMicrovm(doorbell, microvmId, shape, microvm, timing, polled
50255
50255
  action: "resume",
50256
50256
  reason: REASON.resumed,
50257
50257
  state: "SUSPENDED",
50258
- runnerId: doorbell.runnerId,
50258
+ runnerId: doorbellRequest.runnerId,
50259
50259
  microvmId,
50260
50260
  polled,
50261
50261
  startedAt,
50262
50262
  imageVersion
50263
50263
  });
50264
50264
  }
50265
- async function suspendMicrovm(doorbell, microvm, timing) {
50266
- const microvmId = doorbell.microvmId;
50265
+ async function suspendMicrovm(doorbellRequest, microvm, timing) {
50266
+ const microvmId = doorbellRequest.microvmId;
50267
50267
  if (microvmId === void 0) {
50268
50268
  return decide({
50269
50269
  statusCode: 200,
50270
50270
  action: "none",
50271
50271
  reason: REASON.suspendWithoutMicrovmId,
50272
- runnerId: doorbell.runnerId
50272
+ runnerId: doorbellRequest.runnerId
50273
50273
  });
50274
50274
  }
50275
50275
  let state3;
@@ -50277,13 +50277,13 @@ async function suspendMicrovm(doorbell, microvm, timing) {
50277
50277
  ({ state: state3 } = await microvm.describe(microvmId));
50278
50278
  } catch (error3) {
50279
50279
  console.error(
50280
- `[doorbell] GetMicrovm failed for runner ${doorbell.runnerId}: ${error3 instanceof Error ? error3.message : String(error3)}`
50280
+ `[doorbell] GetMicrovm failed for runner ${doorbellRequest.runnerId}: ${error3 instanceof Error ? error3.message : String(error3)}`
50281
50281
  );
50282
50282
  return decide({
50283
50283
  statusCode: 500,
50284
50284
  action: "none",
50285
50285
  reason: REASON.getStateFailed,
50286
- runnerId: doorbell.runnerId,
50286
+ runnerId: doorbellRequest.runnerId,
50287
50287
  microvmId
50288
50288
  });
50289
50289
  }
@@ -50293,7 +50293,7 @@ async function suspendMicrovm(doorbell, microvm, timing) {
50293
50293
  action: "none",
50294
50294
  reason: REASON.suspendAlreadyGone,
50295
50295
  state: state3,
50296
- runnerId: doorbell.runnerId,
50296
+ runnerId: doorbellRequest.runnerId,
50297
50297
  microvmId
50298
50298
  });
50299
50299
  }
@@ -50301,14 +50301,14 @@ async function suspendMicrovm(doorbell, microvm, timing) {
50301
50301
  await withThrottleRetry(() => microvm.suspend(microvmId), timing);
50302
50302
  } catch (error3) {
50303
50303
  console.error(
50304
- `[doorbell] SuspendMicrovm failed for runner ${doorbell.runnerId}: ${error3 instanceof Error ? error3.message : String(error3)}`
50304
+ `[doorbell] SuspendMicrovm failed for runner ${doorbellRequest.runnerId}: ${error3 instanceof Error ? error3.message : String(error3)}`
50305
50305
  );
50306
50306
  return decide({
50307
50307
  statusCode: 500,
50308
50308
  action: "suspend",
50309
50309
  reason: REASON.suspendFailed,
50310
50310
  state: state3,
50311
- runnerId: doorbell.runnerId,
50311
+ runnerId: doorbellRequest.runnerId,
50312
50312
  microvmId
50313
50313
  });
50314
50314
  }
@@ -50317,7 +50317,7 @@ async function suspendMicrovm(doorbell, microvm, timing) {
50317
50317
  action: "suspend",
50318
50318
  reason: REASON.suspended,
50319
50319
  state: state3,
50320
- runnerId: doorbell.runnerId,
50320
+ runnerId: doorbellRequest.runnerId,
50321
50321
  microvmId
50322
50322
  });
50323
50323
  }
@@ -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) {
@@ -50468,14 +50468,9 @@ function createRuntimeMicrovm(executionRoleArn) {
50468
50468
  // No `idlePolicy` (AC5, #732) — deliberately, not an oversight.
50469
50469
  // `maxIdleDurationSeconds` measures INBOUND endpoint traffic only; our
50470
50470
  // runner is outbound-only (it dials the Cloudflare relay), so AWS sees
50471
- // an idle endpoint no matter how busy the agent is. Phase 0 measured
50472
- // this directly: a VM with `maxIdleDurationSeconds=60` and zero inbound
50473
- // traffic was suspended after 71 s while its guest process was alive
50474
- // and working (docs/spikes/lambda-microvms-phase0/README.md). AWS's own
50475
- // Claude-agents guidance recommending `maxIdleDurationSeconds: 120`
50476
- // targets VMs reached inbound and does not apply here. Idle is instead
50471
+ // an idle endpoint no matter how busy the agent is. Idle is instead
50477
50472
  // decided IN the VM by `evident run --idle-timeout`
50478
- // (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
50479
50474
  // clean exit is what drives the controller's own `suspend()` below via
50480
50475
  // the `runner.suspend_requested` doorbell. See `idle-policy.test.ts`
50481
50476
  // in this directory, which scans this file's source for the key and
@@ -50506,9 +50501,9 @@ function requireEnv(name) {
50506
50501
  }
50507
50502
  var sleep3 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
50508
50503
  var handler = async (event) => {
50509
- const doorbellSecret = await getDoorbellSecret(
50510
- requireEnv("DOORBELL_SECRET_ARN"),
50511
- requireEnv("DOORBELL_SECRET_KEY")
50504
+ const controllerSigningSecret = await getControllerSigningSecret(
50505
+ requireEnv("CONTROLLER_SIGNING_SECRET_ARN"),
50506
+ requireEnv("CONTROLLER_SIGNING_SECRET_KEY")
50512
50507
  );
50513
50508
  const shapes = parseShapeCatalogue(requireEnv("MICROVM_SHAPES"));
50514
50509
  const executionRoleArn = requireEnv("MICROVM_EXECUTION_ROLE_ARN");
@@ -50516,7 +50511,7 @@ var handler = async (event) => {
50516
50511
  const { statusCode, body } = await handleDoorbell({
50517
50512
  rawBody,
50518
50513
  signatureHeader: event.headers?.["x-evident-signature"],
50519
- doorbellSecret,
50514
+ controllerSigningSecret,
50520
50515
  shapes,
50521
50516
  microvm: createRuntimeMicrovm(executionRoleArn),
50522
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,