@evident-ai/runner-cdk 3.5.2-dev.7130f36 → 3.5.2-dev.81383cc

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 (33) hide show
  1. package/README.md +89 -39
  2. package/dist/controller-lambda/handler.js +13 -13
  3. package/dist/image-version-pruner-lambda/handler.js +28250 -0
  4. package/dist/image-version-reporter-lambda/handler.js +15 -8
  5. package/dist/index.d.ts +1 -1
  6. package/dist/index.js +3 -2
  7. package/dist/microvm/constants.d.ts +1 -1
  8. package/dist/microvm/constants.js +7 -4
  9. package/dist/microvm/construct.d.ts +12 -8
  10. package/dist/microvm/construct.js +33 -26
  11. package/dist/microvm/controller/handle-doorbell.d.ts +2 -2
  12. package/dist/microvm/controller/handle-doorbell.js +2 -2
  13. package/dist/microvm/image/stage-context.d.ts +13 -38
  14. package/dist/microvm/image/stage-context.js +30 -120
  15. package/dist/microvm/image-version-pruner/construct.d.ts +13 -0
  16. package/dist/microvm/image-version-pruner/construct.js +79 -0
  17. package/dist/microvm/image-version-pruner/handler.d.ts +20 -0
  18. package/dist/microvm/image-version-pruner/handler.js +116 -0
  19. package/dist/microvm/image-version-reporter/construct.d.ts +1 -1
  20. package/dist/microvm/image-version-reporter/construct.js +3 -3
  21. package/dist/microvm/image-version-reporter/handler.d.ts +2 -2
  22. package/dist/microvm/image-version-reporter/handler.js +9 -7
  23. package/dist/microvm/shapes.d.ts +10 -21
  24. package/dist/microvm/shapes.js +10 -17
  25. package/package.json +3 -3
  26. package/dist/microvm-image-context/Dockerfile +0 -190
  27. package/dist/microvm-image-context/hook-server.js +0 -286
  28. package/dist/microvm-image-context/hooks/common.sh +0 -958
  29. package/dist/microvm-image-context/hooks/resume +0 -81
  30. package/dist/microvm-image-context/hooks/run +0 -95
  31. package/dist/microvm-image-context/hooks/session-db-maintenance.mjs +0 -69
  32. package/dist/microvm-image-context/hooks/suspend +0 -21
  33. package/dist/microvm-image-context/hooks/terminate +0 -38
@@ -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
  /**
@@ -93,6 +91,12 @@ export interface EvidentMicrovmConstructProps {
93
91
  * throws at construction rather than silently disabling the reporter.
94
92
  */
95
93
  readonly evidentRunnerId?: string;
94
+ /**
95
+ * Opts into bounding the per-image MicroVM version quota (L-F8BECE9C) for
96
+ * this deployment's images only, using the upstream engine defaults. Omitting
97
+ * this leaves the synthesised template byte-for-byte identical.
98
+ */
99
+ readonly imageVersionPruning?: boolean;
96
100
  }
97
101
  /**
98
102
  * The customer-account half of the per-session runner (#558): the MicroVM
@@ -44,6 +44,7 @@ const lambda_microvm_cdk_1 = require("@evident-ai/lambda-microvm-cdk");
44
44
  const constants_1 = require("./constants");
45
45
  const shapes_1 = require("./shapes");
46
46
  const construct_1 = require("./image-version-reporter/construct");
47
+ const construct_2 = require("./image-version-pruner/construct");
47
48
  // Image hooks build the snapshot, so they get minutes where the runtime hooks
48
49
  // get AWS's 60 s.
49
50
  const IMAGE_HOOK_TIMEOUT_SECONDS = 600;
@@ -146,42 +147,31 @@ class EvidentMicrovmConstruct extends constructs_1.Construct {
146
147
  lifecycleTimeout: cdk.Duration.seconds(props.hookTimeoutSeconds),
147
148
  buildTimeout: cdk.Duration.seconds(IMAGE_HOOK_TIMEOUT_SECONDS),
148
149
  };
149
- // Built first and with no `executionRole` prop, exactly as before this
150
- // shape catalogue existed: same construct id (`Image`, frozen on the
151
- // shape record — see shapes.ts), same `imageName`, same description. The
152
- // construct therefore creates `Image/ExecutionRole` at its current path
153
- // and current logical id — zero CloudFormation churn on the live,
154
- // hand-deployed stack (D3/D9). Every additional shape below borrows this
155
- // role rather than getting one of its own, which is why `validateShapes`
156
- // requires the default shape to be `shapes[0]`: it must exist before any
157
- // other shape can reference its execution role.
158
- const defaultImage = new lambda_microvm_cdk_1.MicrovmImage(this, defaultShape.constructId, {
150
+ // Built first without an `executionRole` prop, so every additional shape
151
+ // can borrow this image's execution role. `validateShapes` requires the
152
+ // default shape to be `shapes[0]` because that role must exist first.
153
+ const defaultImage = new lambda_microvm_cdk_1.MicrovmImage(this, defaultShape.name, {
159
154
  imageName: defaultShape.imageName,
160
155
  description: defaultShape.description,
161
156
  // A local directory, so the zip is uploaded by `cdk deploy` itself and the
162
157
  // image ARN is a stack reference rather than a deploy-time input.
163
- source: props.imageSource,
158
+ source: props.buildContextPath,
164
159
  baseImageArn: props.baseImageArn,
165
160
  baseImageVersion: props.baseImageVersion,
166
161
  memoryMiB: defaultShape.memoryMiB,
167
162
  environment: imageEnvironment,
168
163
  hooks: imageHooks,
169
164
  });
170
- // WART, accepted deliberately: every additional shape's image is built
171
- // with `defaultImage.executionRole`, so this role's construct path
172
- // (`Image/ExecutionRole`) reads as if it belongs to just the default
173
- // shape's image, when in fact it is shared by every shape. That is
174
- // intentional — see D3 in the plan. Do NOT "fix" this with
175
- // `overrideLogicalId`: renaming the logical id IS the CloudFormation
176
- // churn this design exists to avoid (a delete-and-recreate of the role a
177
- // running MicroVM has live session credentials from). The wart is one
178
- // comment; the override would undo the point of D3.
165
+ // WART: every additional shape shares `defaultImage.executionRole`, so the
166
+ // role's path under the default image reads as if it belongs to one shape
167
+ // even though it serves all of them. Keeping one role avoids replacing a
168
+ // role that running MicroVMs may still have credentials from.
179
169
  const images = new Map([[defaultShape.name, defaultImage]]);
180
170
  for (const shape of additionalShapes) {
181
- images.set(shape.name, new lambda_microvm_cdk_1.MicrovmImage(this, shape.constructId, {
171
+ images.set(shape.name, new lambda_microvm_cdk_1.MicrovmImage(this, shape.name, {
182
172
  imageName: shape.imageName,
183
173
  description: shape.description,
184
- source: props.imageSource,
174
+ source: props.buildContextPath,
185
175
  baseImageArn: props.baseImageArn,
186
176
  baseImageVersion: props.baseImageVersion,
187
177
  memoryMiB: shape.memoryMiB,
@@ -237,8 +227,8 @@ class EvidentMicrovmConstruct extends constructs_1.Construct {
237
227
  // The ARN and field name, never the value: the controller reads it from
238
228
  // Secrets Manager at runtime, so the key is not exposed to anyone with
239
229
  // lambda:GetFunctionConfiguration.
240
- DOORBELL_SECRET_ARN: props.doorbellSecret.secretArn,
241
- DOORBELL_SECRET_KEY: constants_1.DOORBELL_SECRET_KEY,
230
+ CONTROLLER_SIGNING_SECRET_ARN: props.controllerSigningSecret.secretArn,
231
+ CONTROLLER_SIGNING_SECRET_KEY: constants_1.CONTROLLER_SIGNING_SECRET_KEY,
242
232
  MICROVM_SHAPES: shapeCatalogueJson,
243
233
  // The role every shape's MicroVM shares (D3) — still one role, still
244
234
  // this same expression, now documented as shared rather than singular.
@@ -253,7 +243,7 @@ class EvidentMicrovmConstruct extends constructs_1.Construct {
253
243
  // emit N near-identical policy statements and break the "exactly one
254
244
  // statement granting X" guarantees this construct's test encodes.
255
245
  defaultImage.grantRun(controller);
256
- props.doorbellSecret.grantRead(controller);
246
+ props.controllerSigningSecret.grantRead(controller);
257
247
  const functionUrl = controller.addFunctionUrl({
258
248
  // HMAC verification of the doorbell body IS the auth, as for the ECS waker.
259
249
  authType: lambda.FunctionUrlAuthType.NONE,
@@ -263,7 +253,7 @@ class EvidentMicrovmConstruct extends constructs_1.Construct {
263
253
  // no custom resource — this branch must not run for them.
264
254
  if (props.evidentApiUrl && props.evidentRunnerId) {
265
255
  new construct_1.ImageVersionReporter(this, 'ImageVersionReporter', {
266
- doorbellSecret: props.doorbellSecret,
256
+ controllerSigningSecret: props.controllerSigningSecret,
267
257
  evidentApiUrl: props.evidentApiUrl,
268
258
  evidentRunnerId: props.evidentRunnerId,
269
259
  imageVersions: Object.fromEntries(shapes.map((shape) => [shape.name, images.get(shape.name).latestActiveImageVersion])),
@@ -274,6 +264,23 @@ class EvidentMicrovmConstruct extends constructs_1.Construct {
274
264
  buildTriggers: Object.fromEntries(shapes.map((shape) => [shape.name, images.get(shape.name).buildInputs])),
275
265
  });
276
266
  }
267
+ if (props.imageVersionPruning) {
268
+ const pruner = new construct_2.ImageVersionPruner(this, 'ImageVersionPruner', {
269
+ imageArns: shapes.map((shape) => (0, lambda_microvm_cdk_1.microvmImageArn)({
270
+ region: cdk.Stack.of(this).region,
271
+ accountId: cdk.Stack.of(this).account,
272
+ imageName: shape.imageName,
273
+ })),
274
+ buildTriggers: Object.fromEntries(shapes.map((shape) => {
275
+ const { buildRoleArn: _buildRoleArn, ...buildInputs } = images.get(shape.name).buildInputs;
276
+ return [shape.name, buildInputs];
277
+ })),
278
+ });
279
+ // Free a quota slot before the publish that consumes it.
280
+ for (const shape of shapes) {
281
+ images.get(shape.name).node.addDependency(pruner);
282
+ }
283
+ }
277
284
  this.functionUrl = functionUrl.url;
278
285
  this.shapeCatalogueJson = shapeCatalogueJson;
279
286
  this.durableStateBucket = durableState;
@@ -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',
@@ -1,42 +1,17 @@
1
- export interface StageMicrovmImageContextOptions {
2
- /**
3
- * The repository baked into the image as the agent's workspace. This is the
4
- * CONSUMER's repository, not Evident's — the image is a dev box for whatever
5
- * codebase the agent works on. Must be a full (non-shallow) clone.
6
- */
7
- readonly repositoryPath: string;
8
- /**
9
- * `origin` of the baked repository, as the VM will see it. Must be a
10
- * credential-free URL (typically `https://github.com/<owner>/<repo>.git`):
11
- * this string ships inside the shared snapshot, so a token in it would be
12
- * baked into every VM. The credential arrives per-VM instead.
13
- */
14
- readonly originUrl: string;
15
- /** Directory to write the build context to. Removed and recreated. */
16
- readonly destination: string;
17
- /**
18
- * Overlay scripts the image build runs as root and as `runner`. The overlay
19
- * ships inside the shared snapshot, so it may install software but must carry
20
- * no credential. When omitted, deterministic no-op scripts are written.
21
- */
22
- readonly overlayDir?: string;
23
- /**
24
- * The published template to copy from. Defaults to the one inside this
25
- * package; named directly by the tests, which assert against a template they
26
- * built themselves rather than whichever `dist/` happens to be present.
27
- */
28
- readonly templateDir?: string;
1
+ /** The fixed directory that contains the workspace in a staged build context. */
2
+ export declare const MICROVM_WORKSPACE_DIRECTORY = "workspace";
3
+ export interface StageMicrovmBuildContextOptions {
4
+ /** The customer's directory containing their Dockerfile and COPY sources. */
5
+ readonly buildContextPath: string;
6
+ /** A full local checkout to bake into the image's `workspace/` directory. */
7
+ readonly workspaceRepositoryPath: string;
8
+ /** Credential-free origin URL carried by the baked checkout. */
9
+ readonly workspaceOriginUrl: string;
29
10
  }
30
11
  /**
31
- * Writes the build context AWS unpacks — the Dockerfile, the hook server, the
32
- * per-phase hook scripts, the deployment overlay and the repository — and
33
- * returns its path. The result is what `EvidentMicrovmConstruct`'s `imageSource`
34
- * takes.
12
+ * Stages customer-owned image inputs in a fresh temporary directory.
35
13
  *
36
- * The Dockerfile, hook server and phase hooks come from this package's
37
- * published `dist/`, so a consumer needs no checkout of the source repository and no
38
- * copy of those files. A caller may supply an overlay; the default is
39
- * deterministic no-op scripts. `repositoryPath` is the only required caller
40
- * input.
14
+ * Staging keeps the image asset hash a pure function of the workspace commit while
15
+ * leaving the customer in control of the Dockerfile and its build context.
41
16
  */
42
- export declare function stageMicrovmImageContext(options: StageMicrovmImageContextOptions): string;
17
+ export declare function stageMicrovmBuildContext(options: StageMicrovmBuildContextOptions): string;
@@ -33,143 +33,53 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.stageMicrovmImageContext = stageMicrovmImageContext;
36
+ exports.MICROVM_WORKSPACE_DIRECTORY = void 0;
37
+ exports.stageMicrovmBuildContext = stageMicrovmBuildContext;
37
38
  const node_child_process_1 = require("node:child_process");
38
39
  const node_fs_1 = require("node:fs");
40
+ const node_os_1 = require("node:os");
39
41
  const path = __importStar(require("node:path"));
40
- /**
41
- * The image build context template published inside this package.
42
- *
43
- * Resolved from the PACKAGE ROOT, THREE directories above `__dirname` — either
44
- * compiled (`dist/microvm/image`) or via ts-node (`src/microvm/image`) — the
45
- * same way `microvm/construct.ts` resolves the controller Lambda asset, but one
46
- * `..` deeper because this file sits a directory below it. It is
47
- * written by `scripts/build.ts` at PACKAGE build time, not at stage time, so a
48
- * consumer needs no esbuild and no checkout of this repo (#1528).
49
- */
50
- const TEMPLATE_DIR = path.resolve(__dirname, '..', '..', '..', 'dist', 'microvm-image-context');
42
+ /** The fixed directory that contains the workspace in a staged build context. */
43
+ exports.MICROVM_WORKSPACE_DIRECTORY = 'workspace';
51
44
  function git(args, cwd) {
52
45
  return (0, node_child_process_1.execFileSync)('git', args, { cwd, encoding: 'utf8' }).trim();
53
46
  }
54
- /**
55
- * `git rev-parse --is-shallow-repository` throws an opaque `execFileSync`
56
- * error (e.g. "not a git repository") when `repositoryPath` isn't usable at
57
- * all, so that failure is rethrown naming the path.
58
- */
59
- function isShallowRepository(repositoryPath) {
47
+ /** Names the path when git cannot determine whether the repository is shallow. */
48
+ function isShallowRepository(sourcePath) {
60
49
  try {
61
- return git(['rev-parse', '--is-shallow-repository'], repositoryPath) === 'true';
50
+ return git(['rev-parse', '--is-shallow-repository'], sourcePath) === 'true';
62
51
  }
63
52
  catch (err) {
64
- throw new Error(`${repositoryPath} is not a usable git repository: ` +
53
+ throw new Error(`${sourcePath} is not a usable git repository: ` +
65
54
  `${err instanceof Error ? err.message : String(err)}`);
66
55
  }
67
56
  }
68
- /**
69
- * Clones the caller's repository into the build context for the Dockerfile to
70
- * bake as a checked-out but uninstalled workspace.
71
- *
72
- * `file://` rather than a plain path: a path triggers git's local-clone
73
- * optimisation, which copies the whole object store — every branch and worktree
74
- * ref on the staging machine (50 MB here). The file transport negotiates like a
75
- * network fetch and packs only what the checked-out branch reaches (9 MB), with
76
- * one branch and one remote-tracking ref. Ignored files, `node_modules` first
77
- * among them, are not part of a clone at all.
78
- */
79
- function stageRepository(repositoryPath, destination, originUrl) {
80
- if (isShallowRepository(repositoryPath)) {
81
- throw new Error(`refusing to stage a shallow checkout (${repositoryPath}): the agent branches, ` +
57
+ /** Clones a full repository while normalising the bytes that affect the image asset hash. */
58
+ function stageRepository(sourcePath, stagedWorkspacePath, workspaceOriginUrl) {
59
+ if (isShallowRepository(sourcePath)) {
60
+ throw new Error(`refusing to stage a shallow checkout (${sourcePath}): the agent branches, ` +
82
61
  'commits and opens PRs, so the image needs real history. Run `git fetch --unshallow`.');
83
62
  }
84
- // Absolute on both sides: `file://` needs an absolute path to mean anything,
85
- // and the clone's cwd is the destination's parent (the only directory both
86
- // are guaranteed to resolve against).
87
- git(['clone', '--quiet', '--single-branch', `file://${repositoryPath}`, destination], path.dirname(destination));
88
- // The clone points `origin` at this machine's filesystem; the VM's origin is
89
- // the caller's own remote over HTTPS, with no credential in it.
90
- git(['remote', 'set-url', 'origin', originUrl], destination);
91
- // The asset hash of the resulting image must be a pure function of the commit,
92
- // or every deploy publishes a new `AWS::Lambda::MicrovmImage` version and wedges
93
- // the stack at its per-image version ceiling. Two things break that purity:
94
- // - `pack-objects`' delta search is multithreaded (`pack.threads` defaults to
95
- // the CPU count) and its output varies run to run; `-f`/`-F`
96
- // (`--no-reuse-delta`/`--no-reuse-object`) additionally stop it from reusing
97
- // whatever packing the source checkout happened to have. All three together
98
- // make the pack a pure function of the object set, independent of the source.
99
- // - `.git/index` carries stat data (inode/mtime/size) and `.git/logs/**` carries
100
- // wall-clock reflog timestamps; neither is byte-stable for a fixed commit.
101
- git(['-c', 'pack.threads=1', 'repack', '-adfqF'], destination);
102
- (0, node_fs_1.rmSync)(path.join(destination, '.git', 'index'), { force: true });
103
- (0, node_fs_1.rmSync)(path.join(destination, '.git', 'logs'), { recursive: true, force: true });
104
- console.log(`[stage] repo ${git(['rev-parse', 'HEAD'], destination)}`);
105
- }
106
- function stageScriptDirectory(source, destination) {
107
- (0, node_fs_1.mkdirSync)(destination);
108
- for (const entry of (0, node_fs_1.readdirSync)(source, { withFileTypes: true })) {
109
- const sourcePath = path.join(source, entry.name);
110
- const staged = path.join(destination, entry.name);
111
- if (entry.isDirectory()) {
112
- stageScriptDirectory(sourcePath, staged);
113
- continue;
114
- }
115
- (0, node_fs_1.copyFileSync)(sourcePath, staged);
116
- // Set modes here rather than inheriting them from the template, so they stay
117
- // correct after an npm pack, zip, CI-cache or hand-copy round trip. The image
118
- // executes extensionless scripts and sources `.sh` files.
119
- (0, node_fs_1.chmodSync)(staged, entry.name.endsWith('.sh') ? 0o644 : 0o755);
120
- }
63
+ git(['clone', '--quiet', '--single-branch', `file://${sourcePath}`, stagedWorkspacePath], path.dirname(stagedWorkspacePath));
64
+ git(['remote', 'set-url', 'origin', workspaceOriginUrl], stagedWorkspacePath);
65
+ git(['-c', 'pack.threads=1', 'repack', '-adfqF'], stagedWorkspacePath);
66
+ (0, node_fs_1.rmSync)(path.join(stagedWorkspacePath, '.git', 'index'), { force: true });
67
+ (0, node_fs_1.rmSync)(path.join(stagedWorkspacePath, '.git', 'logs'), { recursive: true, force: true });
68
+ console.log(`[stage] repo ${git(['rev-parse', 'HEAD'], stagedWorkspacePath)}`);
121
69
  }
122
70
  /**
123
- * Writes the build context AWS unpacks — the Dockerfile, the hook server, the
124
- * per-phase hook scripts, the deployment overlay and the repository — and
125
- * returns its path. The result is what `EvidentMicrovmConstruct`'s `imageSource`
126
- * takes.
71
+ * Stages customer-owned image inputs in a fresh temporary directory.
127
72
  *
128
- * The Dockerfile, hook server and phase hooks come from this package's
129
- * published `dist/`, so a consumer needs no checkout of the source repository and no
130
- * copy of those files. A caller may supply an overlay; the default is
131
- * deterministic no-op scripts. `repositoryPath` is the only required caller
132
- * input.
73
+ * Staging keeps the image asset hash a pure function of the workspace commit while
74
+ * leaving the customer in control of the Dockerfile and its build context.
133
75
  */
134
- function stageMicrovmImageContext(options) {
135
- const { originUrl, overlayDir, templateDir = TEMPLATE_DIR } = options;
136
- // `file://` and the clone below only mean anything against absolute paths,
137
- // and a caller may reasonably pass either.
138
- const repositoryPath = path.resolve(options.repositoryPath);
139
- const destination = path.resolve(options.destination);
140
- if (!(0, node_fs_1.existsSync)(templateDir)) {
141
- throw new Error(`the MicroVM image context template is missing from ${templateDir}. It is written by ` +
142
- "this package's build (`pnpm --filter @evident-ai/runner-cdk build`) and ships in the " +
143
- 'published tarball, so an installed copy should always have it.');
144
- }
145
- (0, node_fs_1.rmSync)(destination, { recursive: true, force: true });
146
- (0, node_fs_1.mkdirSync)(destination, { recursive: true });
147
- (0, node_fs_1.copyFileSync)(path.join(templateDir, 'Dockerfile'), path.join(destination, 'Dockerfile'));
148
- (0, node_fs_1.copyFileSync)(path.join(templateDir, 'hook-server.js'), path.join(destination, 'hook-server.js'));
149
- const hooksSource = path.join(templateDir, 'hooks');
150
- const hooksStage = path.join(destination, 'hooks');
151
- stageScriptDirectory(hooksSource, hooksStage);
152
- const overlayStage = path.join(destination, 'overlay');
153
- if (overlayDir === undefined) {
154
- (0, node_fs_1.mkdirSync)(overlayStage);
155
- for (const name of ['setup-root', 'setup-workspace']) {
156
- const staged = path.join(overlayStage, name);
157
- (0, node_fs_1.writeFileSync)(staged, '#!/usr/bin/env bash\n# No overlay supplied.\n', { mode: 0o755 });
158
- (0, node_fs_1.chmodSync)(staged, 0o755);
159
- }
160
- }
161
- else {
162
- const resolvedOverlayDir = path.resolve(overlayDir);
163
- if (!(0, node_fs_1.existsSync)(resolvedOverlayDir)) {
164
- throw new Error(`overlay directory does not exist: ${resolvedOverlayDir}`);
165
- }
166
- for (const required of ['setup-root', 'setup-workspace']) {
167
- if (!(0, node_fs_1.existsSync)(path.join(resolvedOverlayDir, required))) {
168
- throw new Error(`overlay directory ${resolvedOverlayDir} is missing required script ${required}`);
169
- }
170
- }
171
- stageScriptDirectory(resolvedOverlayDir, overlayStage);
76
+ function stageMicrovmBuildContext(options) {
77
+ const buildContextPath = path.resolve(options.buildContextPath);
78
+ if (!(0, node_fs_1.existsSync)(buildContextPath) || !(0, node_fs_1.existsSync)(path.join(buildContextPath, 'Dockerfile'))) {
79
+ throw new Error(`MicroVM build context ${buildContextPath} does not exist or has no Dockerfile`);
172
80
  }
173
- stageRepository(repositoryPath, path.join(destination, 'repo'), originUrl);
174
- return destination;
81
+ const stagedPath = (0, node_fs_1.mkdtempSync)(path.join((0, node_os_1.tmpdir)(), 'evident-microvm-'));
82
+ (0, node_fs_1.cpSync)(buildContextPath, stagedPath, { recursive: true });
83
+ stageRepository(path.resolve(options.workspaceRepositoryPath), path.join(stagedPath, exports.MICROVM_WORKSPACE_DIRECTORY), options.workspaceOriginUrl);
84
+ return stagedPath;
175
85
  }
@@ -0,0 +1,13 @@
1
+ import { Construct } from 'constructs';
2
+ import type { MicrovmImageBuildInputs } from '@evident-ai/lambda-microvm-cdk';
3
+ export type ImageVersionPrunerProps = {
4
+ readonly imageArns: readonly string[];
5
+ /**
6
+ * Build inputs are a CloudFormation-visible invocation trigger. The build role
7
+ * ARN is excluded because it belongs to the image subtree and would create a cycle.
8
+ */
9
+ readonly buildTriggers: Record<string, Omit<MicrovmImageBuildInputs, 'buildRoleArn'>>;
10
+ };
11
+ export declare class ImageVersionPruner extends Construct {
12
+ constructor(scope: Construct, id: string, props: ImageVersionPrunerProps);
13
+ }