From 105c2e8153b2138581c22bd8b600a0de0cbff256 Mon Sep 17 00:00:00 2001 From: lex00 <121451605+lex00@users.noreply.github.com> Date: Sun, 2 Aug 2026 22:34:01 -0600 Subject: [PATCH] feat(aws): export the MicroVM service limits MicrovmApp enforces (#1374) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The composite encodes the service's real limits — the five memory tiers, the name pattern and length, the environment-variable cap, the connector bounds — verified against the upstream CDK construct and the AWS docs. All of it was module-private, so a consumer driving the same service through a different control plane had to copy the numbers. kubemicrovm-ops does exactly that: its tier profile declares the five memory sizes a second time and its lint pack a third, while its own design page names this composite as the source of truth for two planned rules it cannot import. One named object rather than seven loose constants, so the shape is discoverable and a new limit has an obvious home. The composite now reads its own validation out of it, which is what keeps the export honest — a test probes the limits through MicrovmApp's public API rather than restating them, so the two cannot drift while both look right. Worth checking whether other composites hold service limits the same way. This is unlikely to be the only one, and the general shape — a composite knowing something real that nothing else can reach — is the thing. --- lexicons/aws/src/composites/index.ts | 4 +- .../aws/src/composites/microvm-app.test.ts | 40 ++++++++++++++++- lexicons/aws/src/composites/microvm-app.ts | 43 ++++++++++++++----- lexicons/aws/src/index.ts | 4 +- package-lock.json | 9 ++-- 5 files changed, 81 insertions(+), 19 deletions(-) diff --git a/lexicons/aws/src/composites/index.ts b/lexicons/aws/src/composites/index.ts index 50b969298..9ec728b5e 100644 --- a/lexicons/aws/src/composites/index.ts +++ b/lexicons/aws/src/composites/index.ts @@ -32,7 +32,7 @@ export { Ec2InstanceRole } from "./ec2-instance-role"; export type { Ec2InstanceRoleProps } from "./ec2-instance-role"; export { MinimalVpc } from "./minimal-vpc"; export type { MinimalVpcProps } from "./minimal-vpc"; -export { MicrovmApp } from "./microvm-app"; -export type { MicrovmAppProps, MicrovmAppResult, MicrovmAppBuildConnectorProps } from "./microvm-app"; +export { MicrovmApp, MICROVM_LIMITS } from "./microvm-app"; +export type { MicrovmAppProps, MicrovmAppResult, MicrovmAppBuildConnectorProps, MicrovmMemoryMiB } from "./microvm-app"; export { AgentCoreAgent } from "./agentcore-agent"; export type { AgentCoreAgentProps, AgentCoreAgentResult } from "./agentcore-agent"; diff --git a/lexicons/aws/src/composites/microvm-app.test.ts b/lexicons/aws/src/composites/microvm-app.test.ts index f66d3783d..6ff96ef2d 100644 --- a/lexicons/aws/src/composites/microvm-app.test.ts +++ b/lexicons/aws/src/composites/microvm-app.test.ts @@ -3,7 +3,7 @@ import { expandComposite } from "@intentius/chant"; import { AttrRef } from "@intentius/chant/attrref"; import { resolveAttrRefs } from "@intentius/chant/discovery/resolve"; import { awsSerializer } from "../serializer"; -import { MicrovmApp } from "./microvm-app"; +import { MicrovmApp, MICROVM_LIMITS } from "./microvm-app"; const baseProps = { name: "worker-image", @@ -232,3 +232,41 @@ describe("MicrovmApp", () => { }); }); }); + +describe("MICROVM_LIMITS", () => { + // The composite validates against these; a consumer driving the same service + // through a different control plane needs the same numbers, and copying them + // is how two sources of truth start (#1374). + test("is reachable from the package root", async () => { + const root = await import("../index"); + expect(root.MICROVM_LIMITS).toBe(MICROVM_LIMITS); + }); + + test("is what the composite actually enforces", () => { + // Not a restatement of the constants — a probe through the public API, so + // the two cannot drift apart while both look right. + expect(() => MicrovmApp({ ...baseProps, memoryMiB: 3072 as never })).toThrow( + new RegExp(MICROVM_LIMITS.memoryMiB.join(", ")), + ); + expect(() => MicrovmApp({ ...baseProps, name: "no spaces allowed" })).toThrow(/name must match/); + expect(() => + MicrovmApp({ ...baseProps, name: "x".repeat(MICROVM_LIMITS.maxNameLength + 1) }), + ).toThrow(/≤64 chars/); + expect(() => + MicrovmApp({ ...baseProps, environment: { AWS_REGION: "us-east-1" } }), + ).toThrow(/AWS_REGION/); + }); + + test("names every limit the composite checks", () => { + // A limit enforced and not named here is one a consumer cannot see. + expect(Object.keys(MICROVM_LIMITS).sort()).toEqual([ + "connectorSubnets", + "maxEgressConnectors", + "maxEnvironmentVariables", + "maxNameLength", + "memoryMiB", + "namePattern", + "reservedEnvironmentKeys", + ]); + }); +}); diff --git a/lexicons/aws/src/composites/microvm-app.ts b/lexicons/aws/src/composites/microvm-app.ts index 95ab831d8..ff624c0db 100644 --- a/lexicons/aws/src/composites/microvm-app.ts +++ b/lexicons/aws/src/composites/microvm-app.ts @@ -47,17 +47,38 @@ import { Sub } from "../intrinsics"; const MICROVM_SERVICE_PRINCIPAL = "lambda.amazonaws.com"; const CONNECTOR_MANAGED_RESOURCE_OPERATOR = "network-connectors.lambda.amazonaws.com"; -/** Documented baseline memory tiers (MiB) — the CFN schema types it as an open int, but the service accepts only these five; vCPU auto-scales with the tier. */ -const VALID_MEMORY_MIB = [512, 1024, 2048, 4096, 8192] as const; -type MicrovmMemoryMiB = (typeof VALID_MEMORY_MIB)[number]; - -const NAME_PATTERN = /^[a-zA-Z0-9-_]+$/; -const MAX_NAME_LENGTH = 64; -const RESERVED_ENV_KEYS = new Set(["AWS_REGION"]); -const MAX_ENVIRONMENT_VARIABLES = 50; -const MAX_EGRESS_CONNECTORS = 10; -const MIN_CONNECTOR_SUBNETS = 1; -const MAX_CONNECTOR_SUBNETS = 16; +/** + * The Lambda MicroVMs service's real limits, verified against the upstream + * `ran-isenberg/lambda-microvm-cdk-python` construct and the AWS docs. + * + * Exported because a consumer driving the same service through a different + * control plane needs the same numbers, and copying them is how two sources of + * truth start (#1374). The CFN schema types most of these as open ints and + * strings, so nothing but this object knows them. + */ +export const MICROVM_LIMITS = { + /** Documented baseline memory tiers (MiB). The schema says int; the service accepts five values. vCPU auto-scales with the tier. */ + memoryMiB: [512, 1024, 2048, 4096, 8192], + namePattern: /^[a-zA-Z0-9-_]+$/, + maxNameLength: 64, + /** Set by the service on every MicroVM; supplying it is rejected. */ + reservedEnvironmentKeys: ["AWS_REGION"], + maxEnvironmentVariables: 50, + maxEgressConnectors: 10, + connectorSubnets: { min: 1, max: 16 }, +} as const; + +/** One of the five memory tiers {@link MICROVM_LIMITS} names. */ +export type MicrovmMemoryMiB = (typeof MICROVM_LIMITS.memoryMiB)[number]; + +const VALID_MEMORY_MIB = MICROVM_LIMITS.memoryMiB; +const NAME_PATTERN = MICROVM_LIMITS.namePattern; +const MAX_NAME_LENGTH = MICROVM_LIMITS.maxNameLength; +const RESERVED_ENV_KEYS = new Set(MICROVM_LIMITS.reservedEnvironmentKeys); +const MAX_ENVIRONMENT_VARIABLES = MICROVM_LIMITS.maxEnvironmentVariables; +const MAX_EGRESS_CONNECTORS = MICROVM_LIMITS.maxEgressConnectors; +const MIN_CONNECTOR_SUBNETS = MICROVM_LIMITS.connectorSubnets.min; +const MAX_CONNECTOR_SUBNETS = MICROVM_LIMITS.connectorSubnets.max; /** * `sts:AssumeRole` + `sts:TagSession` trust for `lambda.amazonaws.com` — the diff --git a/lexicons/aws/src/index.ts b/lexicons/aws/src/index.ts index 75891424d..b51e6d76f 100644 --- a/lexicons/aws/src/index.ts +++ b/lexicons/aws/src/index.ts @@ -112,7 +112,7 @@ export { EfsWithAccessPoint, Ec2InstanceRole, MinimalVpc, SolrFargateService, - MicrovmApp, + MicrovmApp, MICROVM_LIMITS, AgentCoreAgent, } from "./composites/index"; export type { @@ -122,7 +122,7 @@ export type { EfsWithAccessPointProps, Ec2InstanceRoleProps, MinimalVpcProps, SolrFargateServiceProps, - MicrovmAppProps, MicrovmAppResult, MicrovmAppBuildConnectorProps, + MicrovmAppProps, MicrovmAppResult, MicrovmAppBuildConnectorProps, MicrovmMemoryMiB, AgentCoreAgentProps, AgentCoreAgentResult, } from "./composites/index"; diff --git a/package-lock.json b/package-lock.json index 2fe523d39..fd1d835a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -111,7 +111,8 @@ }, "peerDependencies": { "@intentius/chant": "^0.38.0", - "@intentius/chant-lexicon-github": "^0.38.0" + "@intentius/chant-lexicon-github": "^0.38.0", + "zod": "^4.3.6" } }, "lexicons/fountain": { @@ -214,7 +215,8 @@ }, "peerDependencies": { "@intentius/chant": "^0.38.0", - "typescript": "^5.9.3" + "typescript": "^5.9.3", + "zod": "^4.3.6" } }, "lexicons/temporal": { @@ -233,7 +235,8 @@ }, "peerDependencies": { "@intentius/chant": "^0.38.0", - "typescript": "^5.9.3" + "typescript": "^5.9.3", + "zod": "^4.3.6" } }, "node_modules/@cdktf/hcl2json": {