@evident-ai/runner-cdk 3.5.2-dev.43244a2 → 3.5.2-dev.4f96188
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 +82 -40
- package/dist/controller-lambda/handler.js +74 -79
- package/dist/evident-scale-to-zero-construct.js +9 -9
- package/dist/image-version-reporter-lambda/handler.js +12 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -2
- package/dist/microvm/constants.d.ts +2 -0
- package/dist/microvm/constants.js +14 -2
- package/dist/microvm/construct.d.ts +6 -8
- package/dist/microvm/construct.js +26 -39
- package/dist/microvm/controller/{doorbell.d.ts → doorbell-request.d.ts} +6 -6
- package/dist/microvm/controller/{doorbell.js → doorbell-request.js} +2 -2
- package/dist/microvm/controller/handle-doorbell.d.ts +2 -2
- package/dist/microvm/controller/handle-doorbell.js +62 -62
- package/dist/microvm/image/stage-context.d.ts +13 -38
- package/dist/microvm/image/stage-context.js +30 -120
- package/dist/microvm/image-version-reporter/construct.d.ts +1 -1
- package/dist/microvm/image-version-reporter/construct.js +4 -5
- package/dist/microvm/image-version-reporter/handler.d.ts +2 -2
- package/dist/microvm/image-version-reporter/handler.js +7 -7
- package/dist/microvm/shapes.d.ts +10 -21
- package/dist/microvm/shapes.js +10 -17
- package/dist/waker-lambda/handler.js +3 -3
- package/package.json +3 -3
- package/dist/microvm-image-context/Dockerfile +0 -190
- package/dist/microvm-image-context/hook-server.js +0 -286
- package/dist/microvm-image-context/hooks/common.sh +0 -905
- package/dist/microvm-image-context/hooks/resume +0 -81
- package/dist/microvm-image-context/hooks/run +0 -95
- package/dist/microvm-image-context/hooks/suspend +0 -21
- 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.
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
80
|
-
|
|
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,25 @@ 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
|
+
|
|
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,57 +135,65 @@ built from a `Dockerfile` you control. The generic runner image lives separately
|
|
|
116
135
|
|
|
117
136
|
## The MicroVM image
|
|
118
137
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
`
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
183
|
+
controllerSigningSecret,
|
|
157
184
|
runnerSecret,
|
|
158
185
|
runnerOpencodeConfigPath: 'opencode.runner.jsonc',
|
|
159
186
|
});
|
|
160
187
|
```
|
|
161
188
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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.
|
|
166
193
|
|
|
167
|
-
The image
|
|
168
|
-
|
|
169
|
-
|
|
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.
|
|
170
197
|
|
|
171
198
|
| MicroVM prop | Omitted behavior |
|
|
172
199
|
| --- | --- |
|
|
@@ -174,12 +201,27 @@ its package manager. To speed up first boot, install dependencies from your over
|
|
|
174
201
|
| `runnerOpencodeConfigPath` | OpenCode uses the baked project configuration. Relative paths resolve from the workspace; absolute paths resolve in the image. |
|
|
175
202
|
| `extraImageEnvironment` | Non-secret values baked into every VM launched from this image version; every VM can read them, so never put secrets here. |
|
|
176
203
|
| `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 `
|
|
178
|
-
|
|
179
|
-
`extraImageEnvironment` is the runtime
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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.
|
|
183
225
|
|
|
184
226
|
## Status / limitations
|
|
185
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
|
-
//
|
|
49802
|
+
// ../../sdk/src/signature.ts
|
|
49803
49803
|
var import_node_crypto11 = require("node:crypto");
|
|
49804
|
-
function
|
|
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
|
|
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(
|
|
49983
|
-
const generation =
|
|
49984
|
-
return (0, import_node_crypto12.createHash)("sha256").update(`${
|
|
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(
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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
|
-
|
|
50039
|
+
controllerSigningSecret,
|
|
50040
50040
|
shapes,
|
|
50041
50041
|
microvm,
|
|
50042
50042
|
sleep: sleep4,
|
|
50043
50043
|
random
|
|
50044
50044
|
}) {
|
|
50045
|
-
if (!
|
|
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 =
|
|
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
|
|
50060
|
+
const doorbellRequest = parsed.doorbell;
|
|
50061
50061
|
const timing = { sleep: sleep4, random };
|
|
50062
|
-
if (
|
|
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 (
|
|
50071
|
-
const resolvedShape = shapes.resolve(
|
|
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:
|
|
50078
|
-
microvmId:
|
|
50077
|
+
runnerId: doorbellRequest.runnerId,
|
|
50078
|
+
microvmId: doorbellRequest.microvmId
|
|
50079
50079
|
});
|
|
50080
50080
|
}
|
|
50081
|
-
return handleWakeDoorbell(
|
|
50081
|
+
return handleWakeDoorbell(doorbellRequest, resolvedShape, microvm, timing);
|
|
50082
50082
|
}
|
|
50083
|
-
return suspendMicrovm(
|
|
50083
|
+
return suspendMicrovm(doorbellRequest, microvm, timing);
|
|
50084
50084
|
}
|
|
50085
|
-
async function handleWakeDoorbell(
|
|
50086
|
-
if (
|
|
50087
|
-
return runMicrovm(
|
|
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(
|
|
50092
|
+
described = await microvm.describe(doorbellRequest.microvmId);
|
|
50093
50093
|
if (described.state === "SUSPENDING") {
|
|
50094
|
-
described = await pollWhileSuspending(
|
|
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 ${
|
|
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:
|
|
50106
|
-
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:
|
|
50117
|
-
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:
|
|
50130
|
-
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 (
|
|
50136
|
+
if (doorbellRequest.recreateOnOutdatedImage === true && await shouldRecreateForNewerImage(doorbellRequest, shape, described.imageVersion, microvm)) {
|
|
50137
50137
|
return runMicrovm(
|
|
50138
|
-
|
|
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
|
-
|
|
50149
|
-
|
|
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
|
-
|
|
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(
|
|
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:
|
|
50192
|
-
clientToken: clientTokenFor(
|
|
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 ${
|
|
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:
|
|
50208
|
+
runnerId: doorbellRequest.runnerId,
|
|
50209
50209
|
polled
|
|
50210
50210
|
});
|
|
50211
50211
|
}
|
|
50212
50212
|
console.error(
|
|
50213
|
-
`[doorbell] RunMicrovm failed for runner ${
|
|
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:
|
|
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:
|
|
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(
|
|
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 ${
|
|
50241
|
+
`[doorbell] ResumeMicrovm failed for runner ${doorbellRequest.runnerId}: ${error3 instanceof Error ? error3.message : String(error3)}`
|
|
50242
50242
|
);
|
|
50243
50243
|
return runMicrovm(
|
|
50244
|
-
|
|
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:
|
|
50258
|
+
runnerId: doorbellRequest.runnerId,
|
|
50259
50259
|
microvmId,
|
|
50260
50260
|
polled,
|
|
50261
50261
|
startedAt,
|
|
50262
50262
|
imageVersion
|
|
50263
50263
|
});
|
|
50264
50264
|
}
|
|
50265
|
-
async function suspendMicrovm(
|
|
50266
|
-
const 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:
|
|
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 ${
|
|
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:
|
|
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:
|
|
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 ${
|
|
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:
|
|
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:
|
|
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
|
|
50417
|
-
async function
|
|
50418
|
-
if (
|
|
50419
|
-
return
|
|
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(`
|
|
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(`
|
|
50427
|
+
throw new Error(`controller signing secret ${secretArn} is missing a non-empty '${key}' field`);
|
|
50428
50428
|
}
|
|
50429
|
-
|
|
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.
|
|
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
|
-
// (
|
|
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
|
|
50510
|
-
requireEnv("
|
|
50511
|
-
requireEnv("
|
|
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
|
-
|
|
50514
|
+
controllerSigningSecret,
|
|
50520
50515
|
shapes,
|
|
50521
50516
|
microvm: createRuntimeMicrovm(executionRoleArn),
|
|
50522
50517
|
sleep: sleep3,
|