cdk-local 0.61.1 → 0.62.1

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.
@@ -0,0 +1,2036 @@
1
+ import { c as getEmbedConfig, s as getLogger, u as setEmbedConfig } from "./docker-cmd-voNPrcRh.js";
2
+ import { $n as resolveCdkPathToLogicalIds, $t as resolveRuntimeCodeMountPath, An as countTargets, At as waitForAgentCorePing, Bt as getDockerImageBySourceHash, Dt as signAgentCoreInvocation, En as resolveApp, Gn as resolveAgentCoreTarget, Gt as parseEcrUri, Ht as waitForRieReady, Jt as pickFreePort, Kn as resolveLambdaTarget, Kt as pullEcrImage, Lt as writeProfileCredentialsFile, Nt as buildAgentCoreCodeImage, On as Synthesizer, Qn as readCdkPathOrUndefined, Qt as streamLogs, Rn as AGENTCORE_A2A_PROTOCOL, Rt as singleFlight, St as MCP_PATH, Ut as architectureToPlatform, Vn as AGENTCORE_MCP_PROTOCOL, Vt as invokeRie, Wn as pickAgentCoreCandidateStack, Wt as buildContainerImage, Xn as matchStacks, Xt as removeContainer, Yt as pullImage, Zn as buildCdkPathIndex, Zt as runDetached, _n as createLocalStateProvider, _t as invokeAgentCoreWs, an as derivePartitionAndUrlSuffix, ar as appOptions, b as resolveProfileCredentials$1, bn as resolveCfnFallbackRegion, bt as a2aInvokeOnce, cn as resolveEcsTaskTarget, cr as deprecatedRegionOption, dn as substituteAgainstStateAsync, en as resolveRuntimeFileExtension, er as CdkLocalError, g as runEcsTask, h as parseHostPortOverrides, hn as materializeLayerFromArn, i as addCommonEcsServiceOptions, in as applyCrossStackResolverToTask, ir as applyRoleArnIfSet, jn as listTargets, jt as downloadAndExtractS3Bundle, kn as resolveSingleTarget, kt as invokeAgentCore, l as runEcsServiceEmulator, ln as applyDeployedEnvFallback, lr as parseContextOptions, m as createEcsRunState, mn as resolveEnvVars, n as resolveAlbFrontDoor, nr as LocalStartServiceError, on as detectEcsImageResolutionNeeds, or as commonOptions, p as cleanupEcsRun, pn as substituteEnvVarsFromStateAsync, qn as derivePseudoParametersFromRegion, qt as ensureDockerAvailable, rn as TASK_ROLE_ACCOUNT_PLACEHOLDER, rr as withErrorHandling, sn as parseEcsTarget, sr as contextOptions, st as createJwksCache, t as isApplicationLoadBalancer, tn as resolveRuntimeImage, ur as warnIfDeprecatedRegion, ut as verifyJwtViaDiscovery, vt as A2A_CONTAINER_PORT, wt as mcpInvokeOnce, xt as MCP_CONTAINER_PORT, yt as A2A_PATH, zn as AGENTCORE_AGUI_PROTOCOL, zt as AssetManifestLoader } from "./elb-front-door-resolver-CsCmMUqz.js";
3
+ import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import * as path from "node:path";
6
+ import { dirname } from "node:path";
7
+ import { Command, Option } from "commander";
8
+ import { randomUUID } from "node:crypto";
9
+
10
+ //#region src/cli/commands/local-invoke.ts
11
+ /**
12
+ * `cdkl invoke <target>` — run a Lambda function locally inside a
13
+ * Docker container that bundles the AWS Lambda Runtime Interface
14
+ * Emulator (RIE). Modeled on `sam local invoke` but reusing cdk-local's
15
+ * synthesis / asset / construct-path plumbing.
16
+ */
17
+ async function localInvokeCommand(target, options, extraStateProviders) {
18
+ const logger = getLogger();
19
+ if (options.verbose) logger.setLevel("debug");
20
+ warnIfDeprecatedRegion(options);
21
+ let imagePlan;
22
+ let containerId;
23
+ let stopLogs;
24
+ let sigintHandler;
25
+ let profileCredsFile;
26
+ /**
27
+ * Unified cleanup for both the success / failure unwind path AND the
28
+ * SIGINT handler.
29
+ */
30
+ const cleanup = singleFlight(async () => {
31
+ if (stopLogs) try {
32
+ stopLogs();
33
+ } catch (err) {
34
+ getLogger().debug(`streamLogs stop failed: ${err instanceof Error ? err.message : String(err)}`);
35
+ }
36
+ if (containerId) try {
37
+ await removeContainer(containerId);
38
+ } catch (err) {
39
+ getLogger().debug(`removeContainer(${containerId}) failed: ${err instanceof Error ? err.message : String(err)}`);
40
+ }
41
+ if (imagePlan?.inlineTmpDir) try {
42
+ rmSync(imagePlan.inlineTmpDir, {
43
+ recursive: true,
44
+ force: true
45
+ });
46
+ } catch (err) {
47
+ getLogger().debug(`Failed to remove inline-code tmpdir ${imagePlan.inlineTmpDir}: ${err instanceof Error ? err.message : String(err)}`);
48
+ }
49
+ if (imagePlan?.layersTmpDir) try {
50
+ rmSync(imagePlan.layersTmpDir, {
51
+ recursive: true,
52
+ force: true
53
+ });
54
+ } catch (err) {
55
+ getLogger().debug(`Failed to remove merged-layers tmpdir ${imagePlan.layersTmpDir}: ${err instanceof Error ? err.message : String(err)}`);
56
+ }
57
+ if (imagePlan?.layerArnTmpDirs) for (const dir of imagePlan.layerArnTmpDirs) try {
58
+ rmSync(dir, {
59
+ recursive: true,
60
+ force: true
61
+ });
62
+ } catch (err) {
63
+ getLogger().debug(`Failed to remove ARN-layer tmpdir ${dir}: ${err instanceof Error ? err.message : String(err)}`);
64
+ }
65
+ if (profileCredsFile) try {
66
+ await profileCredsFile.dispose();
67
+ } catch (err) {
68
+ getLogger().debug(`Failed to remove profile credentials tmpdir ${profileCredsFile.hostPath}: ${err instanceof Error ? err.message : String(err)}`);
69
+ }
70
+ }, (err) => {
71
+ getLogger().debug(`cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
72
+ });
73
+ try {
74
+ await applyRoleArnIfSet({
75
+ roleArn: options.roleArn,
76
+ region: options.region
77
+ });
78
+ await ensureDockerAvailable();
79
+ const profileCredentials = options.profile ? await resolveProfileCredentials(options.profile) : void 0;
80
+ if (options.profile && profileCredentials) profileCredsFile = await writeProfileCredentialsFile(options.profile, profileCredentials);
81
+ const appCmd = resolveApp(options.app);
82
+ if (!appCmd) throw new Error(`No CDK app specified. Pass --app, set ${getEmbedConfig().envPrefix}_APP, or add "app" to cdk.json.`);
83
+ logger.info("Synthesizing CDK app...");
84
+ const synthesizer = new Synthesizer();
85
+ const context = parseContextOptions(options.context);
86
+ const synthOpts = {
87
+ app: appCmd,
88
+ output: options.output,
89
+ ...options.region && { region: options.region },
90
+ ...options.profile && { profile: options.profile },
91
+ ...Object.keys(context).length > 0 && { context }
92
+ };
93
+ const { stacks } = await synthesizer.synthesize(synthOpts);
94
+ const lambda = resolveLambdaTarget(await resolveSingleTarget(target, {
95
+ entries: listTargets(stacks).lambdas,
96
+ message: "Select a Lambda function to invoke",
97
+ noun: "Lambda functions",
98
+ onMissing: () => new CdkLocalError(`${getEmbedConfig().cliName} invoke requires a <target> (a Lambda display path or logical ID). Run \`${getEmbedConfig().cliName} list\` to see them, or run it in a TTY to pick interactively.`, "LOCAL_INVOKE_TARGET_REQUIRED")
99
+ }), stacks);
100
+ const targetLabel = lambda.kind === "zip" ? lambda.runtime : "container image";
101
+ logger.info(`Target: ${lambda.stack.stackName}/${lambda.logicalId} (${targetLabel})`);
102
+ imagePlan = await resolveImagePlan(lambda, options);
103
+ let stateAudit;
104
+ let templateEnv = getTemplateEnv(lambda.resource);
105
+ let stateForRoleHint;
106
+ const stateProvider = createLocalStateProvider(options, lambda.stack.stackName, await resolveCfnFallbackRegion(options, lambda.stack.region), extraStateProviders);
107
+ if (stateProvider) try {
108
+ const loaded = await stateProvider.load(lambda.stack.stackName, lambda.stack.region);
109
+ if (loaded) {
110
+ stateForRoleHint = {
111
+ version: 1,
112
+ stackName: lambda.stack.stackName,
113
+ resources: loaded.resources,
114
+ outputs: loaded.outputs,
115
+ lastModified: 0
116
+ };
117
+ const subContext = {
118
+ resources: loaded.resources,
119
+ consumerRegion: loaded.region
120
+ };
121
+ if (envHasIntrinsicValue(templateEnv)) {
122
+ const pseudo = await resolvePseudoParametersForInvoke(lambda.stack.region, options);
123
+ if (pseudo) subContext.pseudoParameters = pseudo;
124
+ }
125
+ if (envHasIntrinsicValue(templateEnv) && stateProvider.resolveTemplateSsmParameters) {
126
+ const ssmParams = await stateProvider.resolveTemplateSsmParameters(lambda.stack.template);
127
+ if (Object.keys(ssmParams.values).length > 0) subContext.parameters = ssmParams.values;
128
+ if (ssmParams.secureStringLogicalIds.length > 0) subContext.sensitiveParameters = new Set(ssmParams.secureStringLogicalIds);
129
+ }
130
+ if (envHasCrossStackIntrinsic(templateEnv)) {
131
+ const resolver = await stateProvider.buildCrossStackResolver(loaded.region);
132
+ if (resolver) subContext.crossStackResolver = resolver;
133
+ }
134
+ const { env, audit } = await substituteEnvVarsFromStateAsync(templateEnv, subContext);
135
+ templateEnv = env;
136
+ const label = stateProvider.label;
137
+ for (const key of audit.resolvedKeys) logger.debug(`${label}: substituted env var ${key}`);
138
+ let unresolved = audit.unresolved;
139
+ const resolvedKeys = [...audit.resolvedKeys];
140
+ if (unresolved.length > 0 && stateProvider.resolveDeployedFunctionEnv) {
141
+ const physicalId = loaded.resources[lambda.logicalId]?.physicalId;
142
+ if (physicalId) {
143
+ const deployedEnv = await stateProvider.resolveDeployedFunctionEnv(physicalId);
144
+ const fb = applyDeployedEnvFallback(templateEnv, unresolved, deployedEnv);
145
+ templateEnv = fb.env;
146
+ unresolved = fb.stillUnresolved;
147
+ for (const key of fb.filled) {
148
+ resolvedKeys.push(key);
149
+ logger.debug(`${label}: filled env var ${key} from deployed function config`);
150
+ }
151
+ }
152
+ }
153
+ stateAudit = {
154
+ resolvedKeys,
155
+ unresolved,
156
+ sensitiveKeys: audit.sensitiveKeys
157
+ };
158
+ for (const { key, reason } of unresolved) logger.warn(`${label}: could not substitute env var ${key} (${reason}). Override it via --env-vars or it will be dropped.`);
159
+ }
160
+ } catch (err) {
161
+ stateProvider.dispose();
162
+ throw err;
163
+ }
164
+ const overrides = readEnvOverridesFile$2(options.envVars);
165
+ const lambdaCdkPath = readCdkPathOrUndefined(lambda.resource);
166
+ const envResult = resolveEnvVars(lambda.logicalId, lambdaCdkPath, templateEnv, overrides);
167
+ for (const key of envResult.unresolved) {
168
+ if (stateAudit && stateAudit.unresolved.some((u) => u.key === key)) continue;
169
+ const overrideKeyExample = lambdaCdkPath?.replace(/\/Resource$/, "") ?? lambda.logicalId;
170
+ logger.warn(`Environment variable ${key} contains a CloudFormation intrinsic and was dropped. Override it with --env-vars (e.g. {"${overrideKeyExample}":{"${key}":"<literal>"}}), or pass a state-source flag (e.g. --from-cfn-stack or a host-provided extension) to recover deployed values.`);
171
+ }
172
+ let resolvedAssumeRoleArn;
173
+ try {
174
+ resolvedAssumeRoleArn = await resolveAssumeRoleArnForLambda(options.assumeRole, stateForRoleHint, stateProvider, lambda.logicalId);
175
+ } finally {
176
+ stateProvider?.dispose();
177
+ }
178
+ if (options.assumeRole === void 0 && stateForRoleHint) suggestAssumeRoleFromState(stateForRoleHint, lambda.logicalId);
179
+ const event = await readEvent$1(options);
180
+ const dockerEnv = {
181
+ AWS_LAMBDA_FUNCTION_NAME: lambda.logicalId,
182
+ AWS_LAMBDA_FUNCTION_MEMORY_SIZE: String(lambda.memoryMb),
183
+ AWS_LAMBDA_FUNCTION_TIMEOUT: String(lambda.timeoutSec),
184
+ AWS_LAMBDA_FUNCTION_VERSION: "$LATEST",
185
+ AWS_LAMBDA_LOG_GROUP_NAME: `/aws/lambda/${lambda.logicalId}`,
186
+ AWS_LAMBDA_LOG_STREAM_NAME: "local",
187
+ ...envResult.resolved
188
+ };
189
+ let assumeSucceeded = false;
190
+ if (resolvedAssumeRoleArn) {
191
+ const stsRegion = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"];
192
+ try {
193
+ const creds = await assumeLambdaExecutionRole(resolvedAssumeRoleArn, stsRegion);
194
+ dockerEnv["AWS_ACCESS_KEY_ID"] = creds.accessKeyId;
195
+ dockerEnv["AWS_SECRET_ACCESS_KEY"] = creds.secretAccessKey;
196
+ dockerEnv["AWS_SESSION_TOKEN"] = creds.sessionToken;
197
+ if (stsRegion) dockerEnv["AWS_REGION"] = stsRegion;
198
+ assumeSucceeded = true;
199
+ } catch (err) {
200
+ const reason = err instanceof Error ? err.message : String(err);
201
+ logger.warn(`--assume-role: STS AssumeRole(${resolvedAssumeRoleArn}) failed: ${reason}. Falling back to the developer's shell credentials.`);
202
+ }
203
+ }
204
+ if (!assumeSucceeded) {
205
+ forwardAwsEnv$1(dockerEnv);
206
+ applyProfileCredentialsOverlay(dockerEnv, profileCredentials, false);
207
+ if (profileCredsFile) {
208
+ dockerEnv["AWS_SHARED_CREDENTIALS_FILE"] = profileCredsFile.containerPath;
209
+ dockerEnv["AWS_PROFILE"] = profileCredsFile.profileName;
210
+ }
211
+ }
212
+ let debugPort;
213
+ if (options.debugPort) {
214
+ debugPort = Number(options.debugPort);
215
+ if (!Number.isInteger(debugPort) || debugPort <= 0 || debugPort > 65535) throw new Error(`--debug-port must be an integer in 1..65535, got '${options.debugPort}'`);
216
+ dockerEnv["NODE_OPTIONS"] = `--inspect-brk=0.0.0.0:${debugPort}`;
217
+ if (lambda.kind === "image") logger.warn("--debug-port sets NODE_OPTIONS unconditionally on container Lambdas. If the image's runtime is not Node.js, this flag is a no-op.");
218
+ }
219
+ const hostPort = await pickFreePort();
220
+ const containerHost = options.containerHost;
221
+ if (lambda.layers.length > 0) logger.info(`Mounting ${lambda.layers.length} Lambda layer${lambda.layers.length === 1 ? "" : "s"} at /opt`);
222
+ logger.info(`Starting container (image=${imagePlan.image}, port=${hostPort})...`);
223
+ const extraMountsWithProfile = profileCredsFile ? [...imagePlan.extraMounts ?? [], {
224
+ hostPath: profileCredsFile.hostPath,
225
+ containerPath: profileCredsFile.containerPath,
226
+ readOnly: true
227
+ }] : imagePlan.extraMounts;
228
+ containerId = await runDetached({
229
+ image: imagePlan.image,
230
+ mounts: imagePlan.mounts,
231
+ extraMounts: extraMountsWithProfile,
232
+ env: dockerEnv,
233
+ ...stateAudit && stateAudit.sensitiveKeys.length > 0 && { sensitiveEnvKeys: new Set(stateAudit.sensitiveKeys) },
234
+ cmd: imagePlan.cmd,
235
+ hostPort,
236
+ host: containerHost,
237
+ ...debugPort !== void 0 && { debugPort },
238
+ ...imagePlan.platform !== void 0 && { platform: imagePlan.platform },
239
+ ...imagePlan.entryPoint !== void 0 && { entryPoint: imagePlan.entryPoint },
240
+ ...imagePlan.workingDir !== void 0 && { workingDir: imagePlan.workingDir },
241
+ ...imagePlan.tmpfs !== void 0 && { tmpfs: imagePlan.tmpfs }
242
+ });
243
+ stopLogs = streamLogs(containerId);
244
+ sigintHandler = () => {
245
+ cleanup().then(() => {
246
+ process.exit(130);
247
+ });
248
+ };
249
+ process.on("SIGINT", sigintHandler);
250
+ await waitForRieReady(containerHost, hostPort, 5e3);
251
+ const result = await invokeRie(containerHost, hostPort, event, Math.max(3e4, lambda.timeoutSec * 2 * 1e3));
252
+ await new Promise((resolveDelay) => setTimeout(resolveDelay, 250));
253
+ process.stdout.write(`${result.raw}\n`);
254
+ } finally {
255
+ if (sigintHandler) process.off("SIGINT", sigintHandler);
256
+ await cleanup();
257
+ }
258
+ }
259
+ async function resolveImagePlan(lambda, options) {
260
+ if (lambda.kind === "zip") return resolveZipImagePlan(lambda, options);
261
+ return resolveContainerImagePlan(lambda, options);
262
+ }
263
+ async function resolveZipImagePlan(lambda, options) {
264
+ let inlineTmpDir;
265
+ let codeDir = lambda.codePath;
266
+ if (codeDir === null) {
267
+ inlineTmpDir = materializeInlineCode(lambda.handler, lambda.inlineCode ?? "", resolveRuntimeFileExtension(lambda.runtime));
268
+ codeDir = inlineTmpDir;
269
+ }
270
+ const image = resolveRuntimeImage(lambda.runtime);
271
+ await pullImage(image, options.pull === false);
272
+ const layerPlan = await materializeLambdaLayersIncludingArns(lambda.layers, options);
273
+ const containerCodePath = resolveRuntimeCodeMountPath(lambda.runtime);
274
+ const tmpfs = resolveTmpfsForLambda(lambda);
275
+ return {
276
+ image,
277
+ mounts: [{
278
+ hostPath: codeDir,
279
+ containerPath: containerCodePath,
280
+ readOnly: true
281
+ }],
282
+ extraMounts: layerPlan.mount ? [layerPlan.mount] : [],
283
+ cmd: [lambda.handler],
284
+ ...inlineTmpDir !== void 0 && { inlineTmpDir },
285
+ ...layerPlan.tmpDir !== void 0 && { layersTmpDir: layerPlan.tmpDir },
286
+ ...layerPlan.extraTmpDirs.length > 0 && { layerArnTmpDirs: layerPlan.extraTmpDirs },
287
+ ...tmpfs !== void 0 && { tmpfs }
288
+ };
289
+ }
290
+ async function materializeLambdaLayersIncludingArns(layers, options) {
291
+ const extraTmpDirs = [];
292
+ const flat = [];
293
+ for (const layer of layers) {
294
+ if (layer.kind === "asset") {
295
+ flat.push({
296
+ logicalId: layer.logicalId,
297
+ assetPath: layer.assetPath
298
+ });
299
+ continue;
300
+ }
301
+ const dir = await materializeLayerFromArn(layer, { ...options.layerRoleArn !== void 0 && { roleArn: options.layerRoleArn } });
302
+ extraTmpDirs.push(dir);
303
+ flat.push({
304
+ logicalId: layer.arn,
305
+ assetPath: dir
306
+ });
307
+ }
308
+ return {
309
+ ...materializeLambdaLayers(flat),
310
+ extraTmpDirs
311
+ };
312
+ }
313
+ function resolveTmpfsForLambda(lambda) {
314
+ if (lambda.ephemeralStorageMb === void 0) return void 0;
315
+ const logger = getLogger();
316
+ if (lambda.kind === "image") logger.info(`Lambda ${lambda.logicalId}: capping /tmp at ${lambda.ephemeralStorageMb} MiB via --tmpfs (overlays any base-image /tmp content)`);
317
+ else logger.debug(`Lambda ${lambda.logicalId}: applying EphemeralStorage cap via --tmpfs /tmp:size=${lambda.ephemeralStorageMb}m`);
318
+ return {
319
+ target: "/tmp",
320
+ sizeMb: lambda.ephemeralStorageMb
321
+ };
322
+ }
323
+ function materializeLambdaLayers(layers) {
324
+ if (layers.length === 0) return {};
325
+ if (layers.length === 1) return { mount: {
326
+ hostPath: layers[0].assetPath,
327
+ containerPath: "/opt",
328
+ readOnly: true
329
+ } };
330
+ const tmpDir = mkdtempSync(path.join(tmpdir(), `${getEmbedConfig().resourceNamePrefix}-invoke-layers-`));
331
+ for (const layer of layers) cpSync(layer.assetPath, tmpDir, {
332
+ recursive: true,
333
+ force: true
334
+ });
335
+ return {
336
+ mount: {
337
+ hostPath: tmpDir,
338
+ containerPath: "/opt",
339
+ readOnly: true
340
+ },
341
+ tmpDir
342
+ };
343
+ }
344
+ async function resolveContainerImagePlan(lambda, options) {
345
+ const logger = getLogger();
346
+ const platform = architectureToPlatform(lambda.architecture);
347
+ const localBuild = await resolveLocalBuildPlan(lambda);
348
+ let imageRef;
349
+ if (localBuild) imageRef = await buildContainerImage(localBuild.asset, localBuild.cdkOutDir, {
350
+ architecture: lambda.architecture,
351
+ noBuild: options.build === false
352
+ });
353
+ else {
354
+ if (!parseEcrUri(lambda.imageUri)) throw new Error(`Container Lambda '${lambda.logicalId}' has no matching asset in cdk.out, and Code.ImageUri '${lambda.imageUri}' is not an ECR URI ${getEmbedConfig().binaryName} can authenticate against. Re-synthesize the CDK app (so cdk.out includes the build context) or deploy the image to ECR first.`);
355
+ logger.info(`No matching cdk.out asset for ${lambda.imageUri}; falling back to ECR pull (same-acct/region only)...`);
356
+ imageRef = await pullEcrImage(lambda.imageUri, {
357
+ skipPull: options.pull === false,
358
+ ...options.region !== void 0 && { region: options.region },
359
+ ...options.ecrRoleArn !== void 0 && { ecrRoleArn: options.ecrRoleArn },
360
+ ...options.profile !== void 0 && { profile: options.profile }
361
+ });
362
+ }
363
+ const tmpfs = resolveTmpfsForLambda(lambda);
364
+ return {
365
+ image: imageRef,
366
+ mounts: [],
367
+ extraMounts: [],
368
+ cmd: lambda.imageConfig.command ?? [],
369
+ platform,
370
+ ...lambda.imageConfig.entryPoint && lambda.imageConfig.entryPoint.length > 0 && { entryPoint: lambda.imageConfig.entryPoint },
371
+ ...lambda.imageConfig.workingDirectory !== void 0 && { workingDir: lambda.imageConfig.workingDirectory },
372
+ ...tmpfs !== void 0 && { tmpfs }
373
+ };
374
+ }
375
+ async function resolveLocalBuildPlan(lambda) {
376
+ const manifestPath = lambda.stack.assetManifestPath;
377
+ if (!manifestPath) return void 0;
378
+ const cdkOutDir = dirname(manifestPath);
379
+ const manifest = await new AssetManifestLoader().loadManifest(cdkOutDir, lambda.stack.stackName);
380
+ if (!manifest) return void 0;
381
+ const entry = getDockerImageBySourceHash(manifest, lambda.imageUri);
382
+ if (!entry) return void 0;
383
+ return {
384
+ asset: entry.asset,
385
+ cdkOutDir
386
+ };
387
+ }
388
+ function envHasIntrinsicValue(templateEnv) {
389
+ if (!templateEnv) return false;
390
+ for (const v of Object.values(templateEnv)) {
391
+ if (v === void 0 || v === null) continue;
392
+ if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") continue;
393
+ return true;
394
+ }
395
+ return false;
396
+ }
397
+ function envHasCrossStackIntrinsic(templateEnv) {
398
+ if (!templateEnv) return false;
399
+ for (const v of Object.values(templateEnv)) {
400
+ if (!v || typeof v !== "object") continue;
401
+ const obj = v;
402
+ if ("Fn::ImportValue" in obj || "Fn::GetStackOutput" in obj) return true;
403
+ }
404
+ return false;
405
+ }
406
+ async function resolvePseudoParametersForInvoke(stackRegion, options) {
407
+ const logger = getLogger();
408
+ const region = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? stackRegion;
409
+ if (!region) logger.warn(`Resolver references \${AWS::Region} but ${getEmbedConfig().binaryName} could not determine the target region. Pass --region, set AWS_REGION, or declare env.region on the CDK stack.`);
410
+ let accountId;
411
+ try {
412
+ const { STSClient, GetCallerIdentityCommand } = await import("@aws-sdk/client-sts");
413
+ const sts = new STSClient({ ...region && { region } });
414
+ try {
415
+ accountId = (await sts.send(new GetCallerIdentityCommand({}))).Account;
416
+ } finally {
417
+ sts.destroy();
418
+ }
419
+ } catch (err) {
420
+ logger.warn(`Resolver needs \${AWS::AccountId} but STS GetCallerIdentity failed: ${err instanceof Error ? err.message : String(err)}. Substitution will be skipped; affected env entries will be dropped with per-key warnings.`);
421
+ }
422
+ const partitionAndSuffix = region ? derivePartitionAndUrlSuffix(region) : void 0;
423
+ const bag = {
424
+ ...accountId !== void 0 && { accountId },
425
+ ...region !== void 0 && { region },
426
+ ...partitionAndSuffix && {
427
+ partition: partitionAndSuffix.partition,
428
+ urlSuffix: partitionAndSuffix.urlSuffix
429
+ }
430
+ };
431
+ return Object.keys(bag).length === 0 ? void 0 : bag;
432
+ }
433
+ function getTemplateEnv(resource) {
434
+ const env = (resource.Properties ?? {})["Environment"];
435
+ if (!env || typeof env !== "object") return void 0;
436
+ const vars = env["Variables"];
437
+ if (!vars || typeof vars !== "object") return void 0;
438
+ return vars;
439
+ }
440
+ function readEnvOverridesFile$2(filePath) {
441
+ if (!filePath) return void 0;
442
+ let raw;
443
+ try {
444
+ raw = readFileSync(filePath, "utf-8");
445
+ } catch (err) {
446
+ throw new Error(`Failed to read --env-vars file '${filePath}': ${err instanceof Error ? err.message : String(err)}`);
447
+ }
448
+ let parsed;
449
+ try {
450
+ parsed = JSON.parse(raw);
451
+ } catch (err) {
452
+ throw new Error(`Failed to parse --env-vars file '${filePath}' as JSON: ${err instanceof Error ? err.message : String(err)}`);
453
+ }
454
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`--env-vars file '${filePath}' must contain a JSON object at the top level.`);
455
+ return parsed;
456
+ }
457
+ async function readEvent$1(options) {
458
+ if (options.event && options.eventStdin) throw new Error("--event and --event-stdin are mutually exclusive.");
459
+ if (options.eventStdin) return parseEvent$1(await readStdin$1(), "<stdin>");
460
+ if (options.event) return parseEvent$1(readFileSync(options.event, "utf-8"), options.event);
461
+ return {};
462
+ }
463
+ function parseEvent$1(raw, source) {
464
+ try {
465
+ return JSON.parse(raw);
466
+ } catch (err) {
467
+ throw new Error(`Failed to parse event payload from ${source} as JSON: ${err instanceof Error ? err.message : String(err)}`);
468
+ }
469
+ }
470
+ async function readStdin$1() {
471
+ const chunks = [];
472
+ for await (const chunk of process.stdin) chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
473
+ return Buffer.concat(chunks).toString("utf-8");
474
+ }
475
+ async function assumeLambdaExecutionRole(roleArn, region) {
476
+ const { STSClient, AssumeRoleCommand } = await import("@aws-sdk/client-sts");
477
+ const sts = new STSClient({ ...region && { region } });
478
+ try {
479
+ const creds = (await sts.send(new AssumeRoleCommand({
480
+ RoleArn: roleArn,
481
+ RoleSessionName: `${getEmbedConfig().resourceNamePrefix}-invoke-${Date.now()}`,
482
+ DurationSeconds: 3600
483
+ }))).Credentials;
484
+ if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) throw new Error(`AssumeRole(${roleArn}) returned no usable credentials.`);
485
+ return {
486
+ accessKeyId: creds.AccessKeyId,
487
+ secretAccessKey: creds.SecretAccessKey,
488
+ sessionToken: creds.SessionToken
489
+ };
490
+ } finally {
491
+ sts.destroy();
492
+ }
493
+ }
494
+ function forwardAwsEnv$1(env) {
495
+ for (const key of [
496
+ "AWS_ACCESS_KEY_ID",
497
+ "AWS_SECRET_ACCESS_KEY",
498
+ "AWS_SESSION_TOKEN",
499
+ "AWS_REGION",
500
+ "AWS_DEFAULT_REGION"
501
+ ]) {
502
+ const value = process.env[key];
503
+ if (value !== void 0) env[key] = value;
504
+ }
505
+ }
506
+ /**
507
+ * Resolve `--profile <p>` to a concrete credential set. Mirrors the
508
+ * helper in `local-start-api.ts`.
509
+ */
510
+ async function resolveProfileCredentials(profile) {
511
+ const { STSClient } = await import("@aws-sdk/client-sts");
512
+ const sts = new STSClient({ profile });
513
+ try {
514
+ const credsProvider = sts.config.credentials;
515
+ const creds = typeof credsProvider === "function" ? await credsProvider() : credsProvider;
516
+ if (!creds || !creds.accessKeyId || !creds.secretAccessKey) throw new Error(`--profile '${profile}': credential provider chain resolved without usable credentials. Check \`aws sso login --profile ` + profile + "` for SSO profiles, or `~/.aws/credentials` / `~/.aws/config` for regular profiles.");
517
+ return {
518
+ accessKeyId: creds.accessKeyId,
519
+ secretAccessKey: creds.secretAccessKey,
520
+ ...creds.sessionToken && { sessionToken: creds.sessionToken }
521
+ };
522
+ } finally {
523
+ sts.destroy();
524
+ }
525
+ }
526
+ function applyProfileCredentialsOverlay(env, profileCreds, assumeRoleActive) {
527
+ if (!profileCreds) return;
528
+ if (assumeRoleActive) return;
529
+ env["AWS_ACCESS_KEY_ID"] = profileCreds.accessKeyId;
530
+ env["AWS_SECRET_ACCESS_KEY"] = profileCreds.secretAccessKey;
531
+ if (profileCreds.sessionToken) env["AWS_SESSION_TOKEN"] = profileCreds.sessionToken;
532
+ else delete env["AWS_SESSION_TOKEN"];
533
+ }
534
+ function materializeInlineCode(handler, source, fileExtension) {
535
+ const lastDot = handler.lastIndexOf(".");
536
+ if (lastDot <= 0) throw new Error(`Handler '${handler}' is malformed: expected '<modulePath>.<exportName>'.`);
537
+ const modulePath = handler.substring(0, lastDot);
538
+ const dir = mkdtempSync(path.join(tmpdir(), `${getEmbedConfig().resourceNamePrefix}-invoke-`));
539
+ const filePath = path.join(dir, `${modulePath}${fileExtension}`);
540
+ mkdirSync(path.dirname(filePath), { recursive: true });
541
+ writeFileSync(filePath, source, "utf-8");
542
+ return dir;
543
+ }
544
+ function suggestAssumeRoleFromState(state, logicalId) {
545
+ const logger = getLogger();
546
+ const roleArn = resolveExecutionRoleArnFromState(state, logicalId);
547
+ if (roleArn) logger.info(`Hint: the deployed function uses execution role ${roleArn}. Re-run with --assume-role to invoke under the deployed function's narrow permissions.`);
548
+ }
549
+ /**
550
+ * Resolve the role ARN to assume for a Lambda invoke, honoring the three
551
+ * `--assume-role` forms:
552
+ *
553
+ * - `--assume-role <arn>` (explicit) → return `<arn>`.
554
+ * - `--assume-role` (bare, no value) → resolve from CFn state first;
555
+ * if that misses, fall back to
556
+ * `stateProvider.resolveLambdaExecutionRoleArn(<physicalId>)`
557
+ * (a `lambda:GetFunctionConfiguration` call) so a sibling-stack
558
+ * execution role still resolves (issue #181 — `ListStackResources`
559
+ * returns the role's name, not its ARN, so `attributes.Arn` is
560
+ * empty on the CFn state map and the state-only lookup misses).
561
+ * - `--assume-role` absent → return undefined (no assume).
562
+ *
563
+ * Logs the resolution path (info on success, warn on miss) so the user
564
+ * can tell why the container did or did not get assumed-role creds.
565
+ *
566
+ * Exported for unit testing.
567
+ */
568
+ async function resolveAssumeRoleArnForLambda(assumeRole, stateForRoleHint, stateProvider, lambdaLogicalId) {
569
+ const logger = getLogger();
570
+ if (typeof assumeRole === "string") return assumeRole;
571
+ if (assumeRole !== true) return;
572
+ if (!stateForRoleHint) {
573
+ logger.warn("--assume-role passed without an ARN, but no state was loaded. Pair it with a state-source flag, or pass the ARN explicitly: --assume-role <arn>. Falling back to the developer's shell credentials.");
574
+ return;
575
+ }
576
+ const fromState = resolveExecutionRoleArnFromState(stateForRoleHint, lambdaLogicalId);
577
+ if (fromState) {
578
+ logger.info(`--assume-role: auto-resolved execution role from state: ${fromState}`);
579
+ return fromState;
580
+ }
581
+ const fnPhysicalId = stateForRoleHint.resources[lambdaLogicalId]?.physicalId;
582
+ if (stateProvider?.resolveLambdaExecutionRoleArn && fnPhysicalId) {
583
+ const liveArn = await stateProvider.resolveLambdaExecutionRoleArn(fnPhysicalId);
584
+ if (liveArn) {
585
+ logger.info(`--assume-role: auto-resolved execution role from GetFunctionConfiguration: ${liveArn}`);
586
+ return liveArn;
587
+ }
588
+ }
589
+ logger.warn(`--assume-role: could not resolve the execution role ARN for '${lambdaLogicalId}'. Pass the ARN explicitly: --assume-role <arn>. Falling back to the developer's shell credentials.`);
590
+ }
591
+ function resolveExecutionRoleArnFromState(state, logicalId, roleProperty = "Role") {
592
+ const lambda = state.resources[logicalId];
593
+ if (!lambda) return void 0;
594
+ const roleRef = lambda.properties?.[roleProperty] ?? lambda.observedProperties?.[roleProperty];
595
+ if (typeof roleRef === "string" && roleRef.startsWith("arn:")) return roleRef;
596
+ if (typeof roleRef === "object" && roleRef !== null) {
597
+ const refLogicalId = pickReferencedLogicalId(roleRef);
598
+ if (refLogicalId) {
599
+ const cached = state.resources[refLogicalId]?.attributes?.["Arn"];
600
+ if (typeof cached === "string" && cached.startsWith("arn:")) return cached;
601
+ }
602
+ }
603
+ }
604
+ function pickReferencedLogicalId(intrinsic) {
605
+ if ("Ref" in intrinsic && typeof intrinsic["Ref"] === "string") return intrinsic["Ref"];
606
+ if ("Fn::GetAtt" in intrinsic) {
607
+ const arg = intrinsic["Fn::GetAtt"];
608
+ if (Array.isArray(arg) && typeof arg[0] === "string") return arg[0];
609
+ if (typeof arg === "string") return arg.split(".")[0];
610
+ }
611
+ }
612
+ function createLocalInvokeCommand(opts = {}) {
613
+ setEmbedConfig(opts.embedConfig);
614
+ const invoke = new Command("invoke").description("Run a Lambda function locally in a Docker container (RIE-backed). Target accepts a CDK display path (MyStack/MyApi/Handler) or stack-qualified logical ID (MyStack:MyApiHandler1234ABCD). Single-stack apps may omit the stack prefix. Omit <target> in an interactive terminal to pick the Lambda from a list.").argument("[target]", "CDK display path or stack-qualified logical ID of the Lambda to invoke (omit to pick interactively in a TTY)").addOption(new Option("-e, --event <file>", "JSON event payload file (default: {})")).addOption(new Option("--event-stdin", "Read event JSON from stdin").default(false)).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"LogicalId\":{\"KEY\":\"VALUE\"}})")).addOption(new Option("--no-pull", "Skip docker pull (use cached image) — no-op for IMAGE local-build path; `docker build` does not pull base layers by default")).addOption(new Option("--no-build", "Skip docker build on the IMAGE local-build path (use the previously-built tag). Requires the deterministic tag to already be in the local registry; errors with an actionable message when missing. No-op for ZIP Lambdas and the IMAGE ECR-pull path. Compatible with --no-pull.")).addOption(new Option("--debug-port <port>", "Node --inspect-brk port (default: off)")).addOption(new Option("--container-host <host>", "Host to bind the RIE port to").default("127.0.0.1")).addOption(new Option("--assume-role [arn]", "Assume the Lambda's deployed execution role and forward STS-issued temp credentials to the container so the handler runs with the deployed function's narrow permissions. Three forms: (1) `--assume-role <arn>` assumes the explicit ARN; (2) `--assume-role` (bare) auto-resolves the function's execution role ARN from state (requires an active state source); (3) `--no-assume-role` explicitly opts out. Off by default — when omitted, the developer's shell credentials are forwarded unchanged (SAM-compatible default). STS failures degrade to a warn + dev-creds fallback.")).addOption(new Option("--layer-role-arn <arn>", "Role to sts:AssumeRole before calling lambda:GetLayerVersion on every literal-ARN entry in Properties.Layers. Use only when the dev credentials cannot read the layer — typically cross-account layers. AWS-published public layers (e.g. Lambda Powertools) are readable from every account and need no role.")).addOption(new Option("--ecr-role-arn <arn>", "Role ARN to assume before authenticating against ECR for cross-account / centralized registries. Issues sts:AssumeRole via the default credential chain and uses the temporary credentials for ecr:GetAuthorizationToken + docker pull. Required when the caller does not have direct cross-account access to the target repository. Same-account / same-region pulls do not need this flag.")).addOption(new Option("--from-cfn-stack [cfn-stack-name]", "Read a deployed CloudFormation stack via ListStackResources and substitute Ref / Fn::ImportValue in env vars with the deployed physical IDs / exports. Use for CDK apps deployed via the upstream CDK CLI (`cdk deploy`). Bare form uses the resolved stack name; pass an explicit value when CFn stack name differs. Fn::GetAtt is warn-and-dropped in v1 (CFn ListStackResources does not return per-attribute values).")).addOption(new Option("--stack-region <region>", "Region of the state record to read. Used with --from-cfn-stack as the CFn client region.")).action(withErrorHandling(async (target, options) => {
615
+ await localInvokeCommand(target, options, opts.extraStateProviders);
616
+ }));
617
+ [
618
+ ...commonOptions(),
619
+ ...appOptions(),
620
+ ...contextOptions
621
+ ].forEach((option) => invoke.addOption(option));
622
+ invoke.addOption(deprecatedRegionOption);
623
+ return invoke;
624
+ }
625
+
626
+ //#endregion
627
+ //#region src/cli/commands/local-invoke-agentcore.ts
628
+ /**
629
+ * Parser for `--timeout <ms>`. Accepts a positive integer; rejects 0,
630
+ * negatives, fractions, and non-numeric input.
631
+ */
632
+ function parseTimeoutMs(raw) {
633
+ const parsed = Number(raw);
634
+ if (!Number.isInteger(parsed) || parsed <= 0) throw new CdkLocalError(`--timeout must be a positive integer number of milliseconds (got '${raw}').`, "LOCAL_INVOKE_AGENTCORE_TIMEOUT_INVALID");
635
+ return parsed;
636
+ }
637
+ /**
638
+ * `cdkl invoke-agentcore <target>` — run a Bedrock AgentCore Runtime container
639
+ * locally and invoke it once over the AgentCore HTTP contract. Resolves
640
+ * the `AWS::BedrockAgentCore::Runtime`, pulls / builds its container,
641
+ * starts it on port 8080, waits for `GET /ping`, POSTs the event to
642
+ * `POST /invocations`, prints the response, and tears down. Covers the
643
+ * container artifact and the CodeConfiguration managed-runtime artifact
644
+ * (fromCodeAsset, built from source) on the HTTP + MCP protocols; the agent's
645
+ * calls to real AWS go to real AWS (credentials injected like `cdkl invoke`).
646
+ */
647
+ async function localInvokeAgentCoreCommand(target, options, extraStateProviders) {
648
+ const logger = getLogger();
649
+ if (options.verbose) logger.setLevel("debug");
650
+ warnIfDeprecatedRegion(options);
651
+ let containerId;
652
+ let stopLogs;
653
+ let sigintHandler;
654
+ let profileCredsFile;
655
+ let stateProvider;
656
+ const cleanup = singleFlight(async () => {
657
+ if (stateProvider) try {
658
+ stateProvider.dispose();
659
+ } catch (err) {
660
+ getLogger().debug(`state provider dispose failed: ${err instanceof Error ? err.message : String(err)}`);
661
+ }
662
+ if (stopLogs) try {
663
+ stopLogs();
664
+ } catch (err) {
665
+ getLogger().debug(`streamLogs stop failed: ${err instanceof Error ? err.message : String(err)}`);
666
+ }
667
+ if (containerId) try {
668
+ await removeContainer(containerId);
669
+ } catch (err) {
670
+ getLogger().debug(`removeContainer(${containerId}) failed: ${err instanceof Error ? err.message : String(err)}`);
671
+ }
672
+ if (profileCredsFile) try {
673
+ await profileCredsFile.dispose();
674
+ } catch (err) {
675
+ getLogger().debug(`Failed to remove profile credentials tmpdir ${profileCredsFile.hostPath}: ${err instanceof Error ? err.message : String(err)}`);
676
+ }
677
+ }, (err) => {
678
+ getLogger().debug(`cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
679
+ });
680
+ try {
681
+ await applyRoleArnIfSet({
682
+ roleArn: options.roleArn,
683
+ region: options.region
684
+ });
685
+ await ensureDockerAvailable();
686
+ const profileCredentials = options.profile ? await resolveProfileCredentials$1(options.profile) : void 0;
687
+ if (options.profile && profileCredentials) profileCredsFile = await writeProfileCredentialsFile(options.profile, profileCredentials);
688
+ const appCmd = resolveApp(options.app);
689
+ if (!appCmd) throw new Error(`No CDK app specified. Pass --app, set ${getEmbedConfig().envPrefix}_APP, or add "app" to cdk.json.`);
690
+ logger.info("Synthesizing CDK app...");
691
+ const synthesizer = new Synthesizer();
692
+ const context = parseContextOptions(options.context);
693
+ const synthOpts = {
694
+ app: appCmd,
695
+ output: options.output,
696
+ ...options.region && { region: options.region },
697
+ ...options.profile && { profile: options.profile },
698
+ ...Object.keys(context).length > 0 && { context }
699
+ };
700
+ const { stacks } = await synthesizer.synthesize(synthOpts);
701
+ const resolvedTarget = await resolveSingleTarget(target, {
702
+ entries: listTargets(stacks).agentCoreRuntimes,
703
+ message: "Select an AgentCore Runtime to invoke",
704
+ noun: "AgentCore Runtimes",
705
+ onMissing: () => new CdkLocalError(`${getEmbedConfig().cliName} invoke-agentcore requires a <target> (an AgentCore Runtime display path or logical ID). Run \`${getEmbedConfig().cliName} list\` to see them, or run it in a TTY to pick interactively.`, "LOCAL_INVOKE_AGENTCORE_TARGET_REQUIRED")
706
+ });
707
+ const candidate = pickAgentCoreCandidateStack(resolvedTarget, stacks);
708
+ stateProvider = createLocalStateProvider(options, candidate?.stackName ?? "", await resolveCfnFallbackRegion(options, candidate?.region), extraStateProviders);
709
+ const { context: imageContext, loaded: loadedState } = stateProvider && candidate ? await buildAgentCoreImageContext(candidate, stateProvider, options) : {
710
+ context: void 0,
711
+ loaded: void 0
712
+ };
713
+ const resolved = resolveAgentCoreTarget(resolvedTarget, stacks, imageContext);
714
+ logger.info(`Target: ${resolved.stack.stackName}/${resolved.logicalId} (${resolved.protocol})`);
715
+ const isMcp = resolved.protocol === "MCP";
716
+ const isA2a = resolved.protocol === "A2A";
717
+ if (resolved.protocol === "AGUI") logger.info("AGUI runtime: routing through the HTTP /invocations + /ws path (AG-UI wire is SSE / WebSocket on port 8080).");
718
+ if ((isMcp || isA2a) && options.ws) logger.warn(`--ws applies only to the HTTP / AGUI protocols; ignoring it for this ${resolved.protocol} runtime.`);
719
+ if (options.wsInteractive && !options.ws) logger.warn("--ws-interactive is meaningful only with --ws; ignoring.");
720
+ if (options.sigv4 && (isMcp || isA2a || options.ws)) logger.warn("--sigv4 signs the HTTP /invocations request only; ignoring it for the " + (isMcp ? "MCP" : isA2a ? "A2A" : "/ws WebSocket") + " path.");
721
+ const sessionId = options.sessionId ?? randomUUID();
722
+ const event = await readEvent(options);
723
+ const mcpRequest = isMcp ? buildMcpRequest(event) : void 0;
724
+ const a2aRequest = isA2a ? buildA2aRequest(event) : void 0;
725
+ let authorization;
726
+ if (isMcp || isA2a) {
727
+ if (resolved.jwtAuthorizer || options.bearerToken) {
728
+ const pathLabel = isMcp ? MCP_PATH : "/";
729
+ logger.info(`${resolved.protocol} runtime: invoking the local container's ${pathLabel} directly (vanilla ${resolved.protocol}). An inbound JWT / --bearer-token is an AgentCore managed-plane concern and is not applied locally.`);
730
+ }
731
+ } else authorization = await resolveInboundAuthorization(resolved, options);
732
+ await resolveFromS3BucketIntrinsic(resolved, stateProvider, loadedState, imageContext);
733
+ const image = await resolveAgentCoreImage(resolved, options, loadedState, stateProvider);
734
+ const { env: dockerEnv, sensitiveEnvKeys } = await buildContainerEnv(resolved, options, profileCredentials, profileCredsFile, stateProvider, loadedState, imageContext);
735
+ const hostPort = await pickFreePort();
736
+ const containerHost = options.containerHost;
737
+ const containerName = `${getEmbedConfig().resourceNamePrefix}-agentcore-${process.pid}-${Math.random().toString(36).slice(2, 8)}`;
738
+ const containerPort = isMcp ? MCP_CONTAINER_PORT : isA2a ? A2A_CONTAINER_PORT : void 0;
739
+ const containerPortLabel = isMcp ? `${MCP_CONTAINER_PORT}${MCP_PATH}` : isA2a ? `${A2A_CONTAINER_PORT}${"/"}` : "8080";
740
+ logger.info(`Starting agent container (image=${image}, port=${hostPort} -> ${containerPortLabel})...`);
741
+ containerId = await runDetached({
742
+ image,
743
+ mounts: [],
744
+ env: dockerEnv,
745
+ cmd: [],
746
+ hostPort,
747
+ host: containerHost,
748
+ platform: options.platform,
749
+ name: containerName,
750
+ ...containerPort !== void 0 && { containerPort },
751
+ ...sensitiveEnvKeys.size > 0 && { sensitiveEnvKeys }
752
+ });
753
+ stopLogs = streamLogs(containerId);
754
+ sigintHandler = () => {
755
+ cleanup().then(() => process.exit(130));
756
+ };
757
+ process.on("SIGINT", sigintHandler);
758
+ if (isMcp && mcpRequest) {
759
+ logger.info(`MCP request: ${mcpRequest.method}`);
760
+ const mcp = await mcpInvokeOnce(containerHost, hostPort, mcpRequest, { requestTimeoutMs: options.timeout });
761
+ await new Promise((r) => setTimeout(r, 250));
762
+ emitMcpResult(mcp);
763
+ } else if (isA2a && a2aRequest) {
764
+ logger.info(`A2A request: ${a2aRequest.method}`);
765
+ const a2a = await a2aInvokeOnce(containerHost, hostPort, a2aRequest, { requestTimeoutMs: options.timeout });
766
+ await new Promise((r) => setTimeout(r, 250));
767
+ emitA2aResult(a2a);
768
+ } else if (options.ws) {
769
+ await waitForAgentCorePing(containerHost, hostPort);
770
+ const frameSource = options.wsInteractive ? readStdinLines() : void 0;
771
+ logger.info(options.wsInteractive ? "Opening the agent /ws WebSocket (interactive — stdin lines = follow-up frames; Ctrl-D to end)..." : "Opening the agent /ws WebSocket and streaming frames...");
772
+ const wsResult = await invokeAgentCoreWs(containerHost, hostPort, event, {
773
+ sessionId,
774
+ timeoutMs: options.timeout,
775
+ onMessage: (text) => process.stdout.write(text),
776
+ ...authorization && { authorization },
777
+ ...frameSource && { frameSource }
778
+ });
779
+ await new Promise((r) => setTimeout(r, 250));
780
+ emitWsResult(wsResult);
781
+ } else {
782
+ await waitForAgentCorePing(containerHost, hostPort);
783
+ const additionalHeaders = await buildSigV4HeadersIfRequested(options, resolved, loadedState, containerHost, hostPort, event, sessionId, stateProvider);
784
+ const result = await invokeAgentCore(containerHost, hostPort, event, {
785
+ sessionId,
786
+ timeoutMs: options.timeout,
787
+ onChunk: (text) => process.stdout.write(text),
788
+ ...authorization && { authorization },
789
+ ...additionalHeaders && { additionalHeaders }
790
+ });
791
+ await new Promise((r) => setTimeout(r, 250));
792
+ emitResult(result);
793
+ }
794
+ } finally {
795
+ if (sigintHandler) process.off("SIGINT", sigintHandler);
796
+ await cleanup();
797
+ }
798
+ }
799
+ /**
800
+ * Enforce the runtime's inbound JWT authorizer (when declared) and return
801
+ * the `Authorization` header to forward to `/invocations`.
802
+ *
803
+ * - No authorizer → forward the token verbatim if one was given (no-op
804
+ * otherwise).
805
+ * - `--no-verify-auth` → warn + forward without verifying (local-dev escape).
806
+ * - Authorizer + no token → reject (AgentCore returns 401).
807
+ * - Authorizer + token → verify against the OIDC discovery URL; reject on
808
+ * failure (AgentCore returns 403); forward on success. An unreachable
809
+ * discovery URL falls back to pass-through accept (offline-dev fallback in
810
+ * {@link verifyJwtViaDiscovery}).
811
+ *
812
+ * Exported so a unit test can drive the gate without the full Docker pipeline.
813
+ */
814
+ async function resolveInboundAuthorization(resolved, options) {
815
+ const logger = getLogger();
816
+ const authorizer = resolved.jwtAuthorizer;
817
+ const header = options.bearerToken ? `Bearer ${options.bearerToken}` : void 0;
818
+ if (!authorizer) return header;
819
+ if (options.verifyAuth === false) {
820
+ logger.warn(`Runtime '${resolved.logicalId}' declares a customJwtAuthorizer, but --no-verify-auth was set — skipping inbound JWT verification (local-dev escape hatch).`);
821
+ return header;
822
+ }
823
+ if (!header) throw new CdkLocalError(`Runtime '${resolved.logicalId}' requires an inbound JWT (customJwtAuthorizer). Pass --bearer-token <jwt>, or --no-verify-auth to skip verification for local dev.`, "LOCAL_INVOKE_AGENTCORE_AUTH_REQUIRED");
824
+ if (!(await verifyJwtViaDiscovery({
825
+ discoveryUrl: authorizer.discoveryUrl,
826
+ ...authorizer.allowedAudience && { allowedAudience: authorizer.allowedAudience },
827
+ ...authorizer.allowedClients && { allowedClients: authorizer.allowedClients },
828
+ ...authorizer.allowedScopes && { allowedScopes: authorizer.allowedScopes },
829
+ ...authorizer.customClaims && { customClaims: authorizer.customClaims }
830
+ }, header, createJwksCache(), { warned: /* @__PURE__ */ new Set() })).allow) throw new CdkLocalError(`Inbound JWT rejected by the runtime's customJwtAuthorizer (signature / issuer / expiry / audience check failed against ${authorizer.discoveryUrl}).`, "LOCAL_INVOKE_AGENTCORE_AUTH_DENIED");
831
+ logger.info(`Inbound JWT verified against ${authorizer.discoveryUrl}.`);
832
+ return header;
833
+ }
834
+ /**
835
+ * Compute the SigV4 headers for the `/invocations` POST when `--sigv4` is
836
+ * requested. Returns `undefined` (no header overlay) when:
837
+ *
838
+ * - `--sigv4` is not set,
839
+ * - the runtime declares a `customJwtAuthorizer` (the JWT path wins; warns),
840
+ *
841
+ * Throws a {@link CdkLocalError} when `--sigv4` conflicts with
842
+ * `--bearer-token`, or when no AWS credentials are resolvable for signing.
843
+ *
844
+ * Exported so a unit test can drive the gate without the full Docker pipeline.
845
+ */
846
+ async function buildSigV4HeadersIfRequested(options, resolved, loaded, host, port, event, sessionId, stateProvider) {
847
+ if (!options.sigv4) return void 0;
848
+ if (options.bearerToken) throw new CdkLocalError(`--sigv4 and --bearer-token are mutually exclusive: pick one inbound auth.`, "LOCAL_INVOKE_AGENTCORE_AUTH_CONFLICT");
849
+ if (resolved.jwtAuthorizer) {
850
+ getLogger().warn(`Runtime '${resolved.logicalId}' declares a customJwtAuthorizer; --sigv4 ignored (JWT path takes precedence).`);
851
+ return;
852
+ }
853
+ const region = options.region ?? options.stackRegion ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? resolved.stack.region;
854
+ if (!region) throw new CdkLocalError("--sigv4: no region resolved for the AgentCore signing scope. Pass --region <region>, set AWS_REGION, or use --from-cfn-stack with a region-bound stack.", "LOCAL_INVOKE_AGENTCORE_SIGV4_NO_REGION");
855
+ const signed = await signAgentCoreInvocation({
856
+ credentials: await resolveHostCredentialsForSigV4(options, resolved, loaded, region, stateProvider),
857
+ region,
858
+ host,
859
+ port,
860
+ path: "/invocations",
861
+ body: JSON.stringify(event ?? {}),
862
+ sessionId
863
+ });
864
+ const headers = {
865
+ Authorization: signed.authorization,
866
+ "X-Amz-Date": signed.amzDate,
867
+ "X-Amz-Content-Sha256": signed.amzContentSha256
868
+ };
869
+ if (signed.amzSecurityToken) headers["X-Amz-Security-Token"] = signed.amzSecurityToken;
870
+ getLogger().info(`Signed /invocations with SigV4 (region=${region}).`);
871
+ return headers;
872
+ }
873
+ /**
874
+ * Resolve credentials for host-side SigV4 signing. Precedence:
875
+ * 1. `--assume-role` → STS temp creds (warn + fall through on STS failure);
876
+ * 2. `--profile` → profile creds (sessionToken when the profile carries one);
877
+ * 3. shell env (`AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` / optional
878
+ * `AWS_SESSION_TOKEN`).
879
+ *
880
+ * Throws a {@link CdkLocalError} when none are available — `--sigv4` cannot
881
+ * proceed without credentials, unlike the unsigned path.
882
+ */
883
+ async function resolveHostCredentialsForSigV4(options, resolved, loaded, region, stateProvider) {
884
+ const logger = getLogger();
885
+ const assumeRoleArn = await resolveAssumeRoleArn(options, resolved, loaded, stateProvider);
886
+ if (assumeRoleArn) try {
887
+ return await assumeAgentCoreExecutionRole(assumeRoleArn, region);
888
+ } catch (err) {
889
+ logger.warn(`--assume-role: STS AssumeRole(${assumeRoleArn}) failed for --sigv4 signing: ${err instanceof Error ? err.message : String(err)}. Falling back to ${options.profile ? `--profile ${options.profile}` : "shell credentials"}.`);
890
+ }
891
+ if (options.profile) {
892
+ const creds = await resolveProfileCredentials$1(options.profile);
893
+ if (creds?.accessKeyId && creds.secretAccessKey) return {
894
+ accessKeyId: creds.accessKeyId,
895
+ secretAccessKey: creds.secretAccessKey,
896
+ ...creds.sessionToken && { sessionToken: creds.sessionToken }
897
+ };
898
+ }
899
+ const accessKeyId = process.env["AWS_ACCESS_KEY_ID"];
900
+ const secretAccessKey = process.env["AWS_SECRET_ACCESS_KEY"];
901
+ if (accessKeyId && secretAccessKey) {
902
+ const sessionToken = process.env["AWS_SESSION_TOKEN"];
903
+ return {
904
+ accessKeyId,
905
+ secretAccessKey,
906
+ ...sessionToken && { sessionToken }
907
+ };
908
+ }
909
+ throw new CdkLocalError("--sigv4: no AWS credentials available to sign the request. Set AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY, pass --profile <name>, or pass --assume-role <arn>.", "LOCAL_INVOKE_AGENTCORE_SIGV4_NO_CREDENTIALS");
910
+ }
911
+ /**
912
+ * Acquire the agent image. A CODE artifact (managed runtime) is built from
913
+ * source — a fromCodeAsset bundle from its cdk.out asset, a fromS3 bundle
914
+ * downloaded + extracted from S3. A CONTAINER artifact mirrors the
915
+ * container-Lambda path: build from a local cdk.out asset when the URI matches
916
+ * one, else pull from ECR, else pull a plain registry image.
917
+ *
918
+ * `loaded` is the `--from-cfn-stack` state record (when available) — threaded
919
+ * through so a bare `--assume-role` can resolve the execution-role ARN from
920
+ * state for the fromS3 download. `stateProvider` enables the issue-#187
921
+ * live-fallback to `bedrock-agentcore-control:GetAgentRuntime` when the
922
+ * static state lookup misses.
923
+ */
924
+ async function resolveAgentCoreImage(resolved, options, loaded, stateProvider) {
925
+ const logger = getLogger();
926
+ const architecture = platformToArchitecture(options.platform);
927
+ if (resolved.codeArtifact) return resolveAgentCoreCodeImage(resolved, resolved.codeArtifact, options, architecture, loaded, stateProvider);
928
+ const containerUri = resolved.containerUri;
929
+ if (containerUri === void 0) throw new CdkLocalError(`AgentCore Runtime '${resolved.logicalId}' has neither a container image nor a code artifact to run.`, "LOCAL_INVOKE_AGENTCORE_NO_ARTIFACT");
930
+ const manifestPath = resolved.stack.assetManifestPath;
931
+ if (manifestPath) {
932
+ const cdkOutDir = dirname(manifestPath);
933
+ const manifest = await new AssetManifestLoader().loadManifest(cdkOutDir, resolved.stack.stackName);
934
+ if (manifest) {
935
+ const entry = getDockerImageBySourceHash(manifest, containerUri);
936
+ if (entry) return buildContainerImage(entry.asset, cdkOutDir, {
937
+ architecture,
938
+ noBuild: options.build === false
939
+ });
940
+ }
941
+ }
942
+ if (parseEcrUri(containerUri)) {
943
+ logger.info(`Pulling agent image from ECR: ${containerUri}`);
944
+ return pullEcrImage(containerUri, {
945
+ skipPull: options.pull === false,
946
+ ...options.region !== void 0 && { region: options.region },
947
+ ...options.ecrRoleArn !== void 0 && { ecrRoleArn: options.ecrRoleArn },
948
+ ...options.profile !== void 0 && { profile: options.profile }
949
+ });
950
+ }
951
+ await pullImage(containerUri, options.pull === false);
952
+ return containerUri;
953
+ }
954
+ /**
955
+ * Build a local image from a `CodeConfiguration` (managed-runtime) bundle.
956
+ *
957
+ * - fromS3 (`code.s3Source` set, a literal S3 object): download + extract the
958
+ * bundle, then run the from-source build over the extracted dir.
959
+ * - fromCodeAsset: locate the source dir in cdk.out via its asset hash, then
960
+ * run the same from-source build (generated Dockerfile → install deps → run
961
+ * EntryPoint).
962
+ */
963
+ async function resolveAgentCoreCodeImage(resolved, code, options, architecture, loaded, stateProvider) {
964
+ if (code.s3Source) return resolveAgentCoreCodeImageFromS3(resolved, code, code.s3Source, options, architecture, loaded, stateProvider);
965
+ const manifestPath = resolved.stack.assetManifestPath;
966
+ if (!manifestPath) throw new CdkLocalError(`AgentCore Runtime '${resolved.logicalId}' uses a code artifact, but its stack has no asset manifest in cdk.out to read the bundle source from.`, "LOCAL_INVOKE_AGENTCORE_CODE_NO_MANIFEST");
967
+ const cdkOutDir = dirname(manifestPath);
968
+ const loader = new AssetManifestLoader();
969
+ const manifest = await loader.loadManifest(cdkOutDir, resolved.stack.stackName);
970
+ const fileAssets = manifest ? loader.getFileAssets(manifest) : void 0;
971
+ const asset = fileAssets ? fileAssets.get(code.codeAssetHash) ?? findFileAssetByObjectKey(fileAssets, code.codeAssetHash) : void 0;
972
+ if (!asset) throw new CdkLocalError(`AgentCore Runtime '${resolved.logicalId}' code bundle (asset ${code.codeAssetHash}) was not found in the cdk.out asset manifest. ${getEmbedConfig().cliName} invoke-agentcore runs a local from-source build of a fromCodeAsset bundle — re-synthesize the app so the asset is staged in cdk.out and retry. (A fromS3 bundle is downloaded from S3 instead; this runtime has no literal Code.S3.Bucket.)`, "LOCAL_INVOKE_AGENTCORE_CODE_ASSET_NOT_FOUND");
973
+ const sourceDir = loader.getAssetSourcePath(cdkOutDir, asset);
974
+ if (!existsSync(sourceDir) || !statSync(sourceDir).isDirectory()) throw new CdkLocalError(`AgentCore Runtime '${resolved.logicalId}' code bundle source '${sourceDir}' does not exist or is not a directory. Re-synthesize the app and retry.`, "LOCAL_INVOKE_AGENTCORE_CODE_SOURCE_MISSING");
975
+ return buildAgentCoreCodeImage({
976
+ sourceDir,
977
+ runtime: code.runtime,
978
+ entryPoint: code.entryPoint,
979
+ architecture,
980
+ noBuild: options.build === false
981
+ });
982
+ }
983
+ /**
984
+ * Build a local image from a fromS3 CodeConfiguration bundle: download +
985
+ * extract the S3 object, run the from-source build over the extracted dir, then
986
+ * clean up the temp dir.
987
+ *
988
+ * Credentials mirror the rest of the command: an `--assume-role` ARN (explicit,
989
+ * or resolved from `--from-cfn-stack` state for the bare form) yields STS temp
990
+ * creds for the download; otherwise `--profile` / the default chain is used.
991
+ * The region is `--region` / `--stack-region` / env / the stack's region.
992
+ */
993
+ async function resolveAgentCoreCodeImageFromS3(resolved, code, s3Source, options, architecture, loaded, stateProvider) {
994
+ const logger = getLogger();
995
+ if (typeof s3Source.bucket !== "string" || s3Source.bucket.length === 0) throw new CdkLocalError(`AgentCore Runtime '${resolved.logicalId}' fromS3 bundle reached the image step with no literal bucket. This is a cdk-local bug — please report it.`, "LOCAL_INVOKE_AGENTCORE_FROMS3_BUCKET_UNRESOLVED");
996
+ const location = {
997
+ bucket: s3Source.bucket,
998
+ key: s3Source.key,
999
+ ...s3Source.versionId !== void 0 && { versionId: s3Source.versionId }
1000
+ };
1001
+ const region = options.region ?? options.stackRegion ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? resolved.stack.region;
1002
+ const assumeRoleArn = await resolveAssumeRoleArn(options, resolved, loaded, stateProvider);
1003
+ let credentials;
1004
+ if (assumeRoleArn) try {
1005
+ credentials = await assumeAgentCoreExecutionRole(assumeRoleArn, region);
1006
+ } catch (err) {
1007
+ logger.warn(`--assume-role: STS AssumeRole(${assumeRoleArn}) failed for the fromS3 bundle download: ${err instanceof Error ? err.message : String(err)}. Falling back to ${options.profile ? `--profile ${options.profile}` : "the default credentials"}.`);
1008
+ }
1009
+ const bundle = await downloadAndExtractS3Bundle(location, {
1010
+ ...region !== void 0 && { region },
1011
+ ...options.profile !== void 0 && { profile: options.profile },
1012
+ ...credentials !== void 0 && { credentials }
1013
+ });
1014
+ try {
1015
+ return await buildAgentCoreCodeImage({
1016
+ sourceDir: bundle.dir,
1017
+ runtime: code.runtime,
1018
+ entryPoint: code.entryPoint,
1019
+ architecture,
1020
+ noBuild: options.build === false
1021
+ });
1022
+ } finally {
1023
+ await bundle.cleanup();
1024
+ }
1025
+ }
1026
+ /**
1027
+ * Find the file asset whose destination objectKey is `<hash>.zip` (matching the
1028
+ * `Code.S3.Prefix`'s hash) when the source-hash-keyed lookup misses — covers a
1029
+ * synthesizer whose source hash differs from the destination objectKey.
1030
+ */
1031
+ function findFileAssetByObjectKey(fileAssets, hash) {
1032
+ const zip = `${hash}.zip`;
1033
+ for (const asset of fileAssets.values()) if (Object.values(asset.destinations).some((d) => d.objectKey === zip || d.objectKey.endsWith(`/${zip}`))) return asset;
1034
+ }
1035
+ /**
1036
+ * Build the container env + the set of env keys to keep off the `docker run`
1037
+ * argv. Substitutes `--from-cfn-stack` state into the template env (reusing the
1038
+ * shared state load + image-resolution context — Ref / Fn::Sub / Fn::Join +
1039
+ * SSM parameters, with decrypted SecureString values flagged sensitive),
1040
+ * applies `--env-vars` overrides, then injects AWS credentials (`--assume-role`
1041
+ * STS temp creds — resolving an intrinsic RoleArn from state for bare
1042
+ * `--assume-role` — else `--profile` / dev creds).
1043
+ *
1044
+ * The state provider + loaded record + image context are built once by the
1045
+ * caller and shared here, so this does not re-load state.
1046
+ */
1047
+ async function buildContainerEnv(resolved, options, profileCredentials, profileCredsFile, stateProvider, loaded, imageContext) {
1048
+ const logger = getLogger();
1049
+ let templateEnv = resolved.environmentVariables;
1050
+ const sensitiveEnvKeys = /* @__PURE__ */ new Set();
1051
+ if (stateProvider && loaded) {
1052
+ const subContext = {
1053
+ resources: imageContext?.stateResources ?? loaded.resources,
1054
+ consumerRegion: loaded.region
1055
+ };
1056
+ const pseudo = imageContext?.pseudoParameters ?? derivePseudoParametersFromRegion(loaded.region);
1057
+ if (pseudo) subContext.pseudoParameters = pseudo;
1058
+ if (imageContext?.stateParameters) subContext.parameters = imageContext.stateParameters;
1059
+ if (imageContext?.stateSensitiveParameters?.length) subContext.sensitiveParameters = new Set(imageContext.stateSensitiveParameters);
1060
+ const resolver = await stateProvider.buildCrossStackResolver(loaded.region);
1061
+ if (resolver) subContext.crossStackResolver = resolver;
1062
+ const { env, audit } = await substituteEnvVarsFromStateAsync(templateEnv, subContext);
1063
+ templateEnv = env;
1064
+ for (const key of audit.resolvedKeys) logger.debug(`${stateProvider.label}: substituted env var ${key}`);
1065
+ for (const key of audit.sensitiveKeys) sensitiveEnvKeys.add(key);
1066
+ for (const { key, reason } of audit.unresolved) logger.warn(`${stateProvider.label}: could not substitute env var ${key} (${reason}). Override it via --env-vars or it will be dropped.`);
1067
+ }
1068
+ const overrides = readEnvOverridesFile$1(options.envVars);
1069
+ const cdkPath = readCdkPathOrUndefined(resolved.resource);
1070
+ const envResult = resolveEnvVars(resolved.logicalId, cdkPath, templateEnv, overrides);
1071
+ for (const key of envResult.unresolved) {
1072
+ const overrideKeyExample = cdkPath?.replace(/\/Resource$/, "") ?? resolved.logicalId;
1073
+ logger.warn(`Environment variable ${key} contains a CloudFormation intrinsic and was dropped. Override it with --env-vars (e.g. {"${overrideKeyExample}":{"${key}":"<literal>"}}), or pass a state-source flag (e.g. --from-cfn-stack) to recover deployed values.`);
1074
+ }
1075
+ const dockerEnv = { ...envResult.resolved };
1076
+ const assumeRoleArn = await resolveAssumeRoleArn(options, resolved, loaded, stateProvider);
1077
+ await applyAgentCoreCredentialEnv(dockerEnv, {
1078
+ ...assumeRoleArn !== void 0 && { assumeRoleArn },
1079
+ ...options.region !== void 0 && { region: options.region },
1080
+ ...profileCredentials !== void 0 && { profileCredentials },
1081
+ ...profileCredsFile !== void 0 && { profileCredsFile: {
1082
+ containerPath: profileCredsFile.containerPath,
1083
+ profileName: profileCredsFile.profileName
1084
+ } }
1085
+ });
1086
+ return {
1087
+ env: dockerEnv,
1088
+ sensitiveEnvKeys
1089
+ };
1090
+ }
1091
+ /**
1092
+ * Resolve a fromS3 bundle's intrinsic `Code.S3.Bucket` to a literal bucket
1093
+ * name in place on `resolved.codeArtifact.s3Source.bucket`. Uses the SAME
1094
+ * state-substitution machinery env vars use under `--from-cfn-stack`, so
1095
+ * every cross-stack intrinsic that path supports (`Ref` / `Fn::ImportValue` /
1096
+ * `Fn::GetStackOutput`) is supported transparently here.
1097
+ *
1098
+ * No-op when there is no intrinsic to resolve. Errors when no state is
1099
+ * available, or when the substitution returns a non-string / unresolved value.
1100
+ *
1101
+ * Exported so a unit test can drive the gate without the full Docker pipeline.
1102
+ */
1103
+ async function resolveFromS3BucketIntrinsic(resolved, stateProvider, loaded, imageContext) {
1104
+ const s3Source = resolved.codeArtifact?.s3Source;
1105
+ if (!s3Source || s3Source.bucketIntrinsic === void 0) return;
1106
+ if (s3Source.bucket !== void 0) return;
1107
+ if (!stateProvider || !loaded) throw new CdkLocalError(`AgentCore Runtime '${resolved.logicalId}' fromS3 bundle's Code.S3.Bucket is an unresolved intrinsic (${describeIntrinsic(s3Source.bucketIntrinsic)}). Pass --from-cfn-stack so its physical bucket name can be resolved against the deployed stack state.`, "LOCAL_INVOKE_AGENTCORE_FROMS3_BUCKET_INTRINSIC_NO_STATE");
1108
+ const subContext = {
1109
+ resources: imageContext?.stateResources ?? loaded.resources,
1110
+ consumerRegion: loaded.region
1111
+ };
1112
+ const pseudo = imageContext?.pseudoParameters ?? derivePseudoParametersFromRegion(loaded.region);
1113
+ if (pseudo) subContext.pseudoParameters = pseudo;
1114
+ const crossStackResolver = await stateProvider.buildCrossStackResolver(loaded.region);
1115
+ if (crossStackResolver) subContext.crossStackResolver = crossStackResolver;
1116
+ const result = await substituteAgainstStateAsync(s3Source.bucketIntrinsic, subContext);
1117
+ if (result.kind !== "literal") throw new CdkLocalError(`Could not resolve AgentCore Runtime '${resolved.logicalId}' fromS3 Code.S3.Bucket intrinsic (${describeIntrinsic(s3Source.bucketIntrinsic)}) against the --from-cfn-stack state: ${result.reason}. Confirm the referenced resource / export exists in the deployed stack.`, "LOCAL_INVOKE_AGENTCORE_FROMS3_BUCKET_INTRINSIC_UNRESOLVED");
1118
+ if (typeof result.value !== "string" || result.value.length === 0) throw new CdkLocalError(`AgentCore Runtime '${resolved.logicalId}' fromS3 Code.S3.Bucket intrinsic resolved to a ${typeof result.value} value, not a bucket name string. (${describeIntrinsic(s3Source.bucketIntrinsic)})`, "LOCAL_INVOKE_AGENTCORE_FROMS3_BUCKET_INTRINSIC_NOT_STRING");
1119
+ s3Source.bucket = result.value;
1120
+ getLogger().info(`Resolved fromS3 Code.S3.Bucket from state: ${describeIntrinsic(s3Source.bucketIntrinsic)} -> ${result.value}`);
1121
+ }
1122
+ /** Render the intrinsic key for an error / log message (e.g. `Ref:Bucket1`). */
1123
+ function describeIntrinsic(value) {
1124
+ if (!value || typeof value !== "object") return String(value);
1125
+ const obj = value;
1126
+ const key = Object.keys(obj)[0] ?? "?";
1127
+ const arg = obj[key];
1128
+ if (typeof arg === "string") return `${key}:${arg}`;
1129
+ return key;
1130
+ }
1131
+ /**
1132
+ * Build the `--from-cfn-stack` image-resolution context + return the loaded
1133
+ * state record (loaded once, reused by env substitution + role resolution).
1134
+ * Mirrors `run-task`'s `buildEcsImageResolutionContext`: pseudo parameters
1135
+ * (region + STS account id), the deployed resources, and SSM template
1136
+ * parameters (decrypted SecureString logical ids flagged sensitive).
1137
+ */
1138
+ async function buildAgentCoreImageContext(candidate, stateProvider, options) {
1139
+ const logger = getLogger();
1140
+ const region = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? candidate.region;
1141
+ let accountId;
1142
+ try {
1143
+ accountId = await resolveCallerAccountId$1(region, options.profile);
1144
+ } catch (err) {
1145
+ logger.warn(`--from-cfn-stack: STS GetCallerIdentity failed: ${err instanceof Error ? err.message : String(err)}. A same-stack ECR image URI referencing \${AWS::AccountId} may not resolve.`);
1146
+ }
1147
+ const context = {};
1148
+ const pseudo = derivePseudoParametersFromRegion(region, accountId);
1149
+ if (pseudo) context.pseudoParameters = pseudo;
1150
+ const loaded = await stateProvider.load(candidate.stackName, candidate.region);
1151
+ if (loaded) {
1152
+ context.stateResources = loaded.resources;
1153
+ if (stateProvider.resolveTemplateSsmParameters) {
1154
+ const ssm = await stateProvider.resolveTemplateSsmParameters(candidate.template);
1155
+ if (Object.keys(ssm.values).length > 0) context.stateParameters = ssm.values;
1156
+ if (ssm.secureStringLogicalIds.length > 0) context.stateSensitiveParameters = ssm.secureStringLogicalIds;
1157
+ }
1158
+ }
1159
+ return {
1160
+ context,
1161
+ loaded: loaded ?? void 0
1162
+ };
1163
+ }
1164
+ /** STS `GetCallerIdentity` for the `${AWS::AccountId}` pseudo parameter (threads `--profile`). */
1165
+ async function resolveCallerAccountId$1(region, profile) {
1166
+ const { STSClient, GetCallerIdentityCommand } = await import("@aws-sdk/client-sts");
1167
+ const sts = new STSClient({
1168
+ ...region && { region },
1169
+ ...profile && { profile }
1170
+ });
1171
+ try {
1172
+ return (await sts.send(new GetCallerIdentityCommand({}))).Account;
1173
+ } finally {
1174
+ sts.destroy();
1175
+ }
1176
+ }
1177
+ /**
1178
+ * Inject AWS credentials into the container env. Precedence:
1179
+ * 1. `--assume-role` → STS-issued temp creds for the resolved ARN (on
1180
+ * STS failure, warn + fall through to dev creds).
1181
+ * 2. dev shell creds (`forwardAwsEnv`) + `--profile` overlay
1182
+ * ({@link applyProfileCredentialsOverlay}) + the bind-mounted
1183
+ * credentials-file env so handler `fromIni({ profile })` resolves.
1184
+ *
1185
+ * Exported so a unit test can lock the binding (mock STS) without driving
1186
+ * the full synth + docker pipeline.
1187
+ */
1188
+ async function applyAgentCoreCredentialEnv(dockerEnv, args) {
1189
+ const logger = getLogger();
1190
+ let assumeSucceeded = false;
1191
+ if (args.assumeRoleArn) {
1192
+ const stsRegion = args.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"];
1193
+ try {
1194
+ const creds = await assumeAgentCoreExecutionRole(args.assumeRoleArn, stsRegion);
1195
+ dockerEnv["AWS_ACCESS_KEY_ID"] = creds.accessKeyId;
1196
+ dockerEnv["AWS_SECRET_ACCESS_KEY"] = creds.secretAccessKey;
1197
+ dockerEnv["AWS_SESSION_TOKEN"] = creds.sessionToken;
1198
+ if (stsRegion) dockerEnv["AWS_REGION"] = stsRegion;
1199
+ assumeSucceeded = true;
1200
+ } catch (err) {
1201
+ logger.warn(`--assume-role: STS AssumeRole(${args.assumeRoleArn}) failed: ${err instanceof Error ? err.message : String(err)}. Falling back to the developer's shell credentials.`);
1202
+ }
1203
+ }
1204
+ if (!assumeSucceeded) {
1205
+ forwardAwsEnv(dockerEnv);
1206
+ applyProfileCredentialsOverlay(dockerEnv, args.profileCredentials, false);
1207
+ if (args.profileCredsFile) {
1208
+ dockerEnv["AWS_SHARED_CREDENTIALS_FILE"] = args.profileCredsFile.containerPath;
1209
+ dockerEnv["AWS_PROFILE"] = args.profileCredsFile.profileName;
1210
+ }
1211
+ }
1212
+ }
1213
+ /**
1214
+ * Resolve the role ARN to assume, honoring the three `--assume-role` forms.
1215
+ * Bare `--assume-role` uses the runtime's literal `RoleArn`; when that is an
1216
+ * intrinsic (the common L2 case — `Fn::GetAtt` to an auto-created role) it
1217
+ * resolves the execution-role ARN from `--from-cfn-stack` state first; when
1218
+ * that misses (issue #187 — `ListStackResources` returns the role's name,
1219
+ * not its ARN, so `attributes.Arn` is empty on the CFn state map) it falls
1220
+ * back to `stateProvider.resolveAgentCoreRuntimeRoleArn(<physicalId>)`
1221
+ * (a `bedrock-agentcore-control:GetAgentRuntime` call) so a same-stack
1222
+ * execution role still resolves. Only warns + falls back to dev creds
1223
+ * when all of those miss.
1224
+ */
1225
+ async function resolveAssumeRoleArn(options, resolved, loaded, stateProvider) {
1226
+ if (typeof options.assumeRole === "string") return options.assumeRole;
1227
+ if (options.assumeRole !== true) return void 0;
1228
+ if (resolved.roleArn) return resolved.roleArn;
1229
+ if (loaded) {
1230
+ const fromState = resolveExecutionRoleArnFromState(loaded, resolved.logicalId, "RoleArn");
1231
+ if (fromState) {
1232
+ getLogger().debug(`--assume-role: resolved RoleArn from state: ${fromState}`);
1233
+ return fromState;
1234
+ }
1235
+ const runtimePhysicalId = loaded.resources[resolved.logicalId]?.physicalId;
1236
+ if (stateProvider?.resolveAgentCoreRuntimeRoleArn && runtimePhysicalId) {
1237
+ const liveArn = await stateProvider.resolveAgentCoreRuntimeRoleArn(runtimePhysicalId);
1238
+ if (liveArn) {
1239
+ getLogger().info(`--assume-role: auto-resolved execution role from GetAgentRuntime: ${liveArn}`);
1240
+ return liveArn;
1241
+ }
1242
+ }
1243
+ }
1244
+ getLogger().warn("--assume-role passed without an ARN, but the runtime's RoleArn is not a literal ARN in the template " + (loaded ? "and could not be resolved from the deployed stack state. " : "and no --from-cfn-stack state is available to resolve it. ") + "Pass the ARN explicitly: --assume-role <arn>. Falling back to the developer's shell credentials.");
1245
+ }
1246
+ function emitResult(result) {
1247
+ const logger = getLogger();
1248
+ if (result.status >= 400) {
1249
+ logger.warn(`Agent /invocations returned HTTP ${result.status}.`);
1250
+ process.exitCode = 1;
1251
+ }
1252
+ if (result.streamed) {
1253
+ process.stdout.write("\n");
1254
+ return;
1255
+ }
1256
+ process.stdout.write(`${result.raw}\n`);
1257
+ }
1258
+ /**
1259
+ * Finish a `/ws` exchange: the frames were already streamed to stdout via the
1260
+ * onMessage sink, so just terminate with a newline (so the shell prompt resumes
1261
+ * cleanly) and note the frame count at debug level.
1262
+ */
1263
+ function emitWsResult(result) {
1264
+ process.stdout.write("\n");
1265
+ getLogger().debug(`Agent /ws closed after ${result.frames} frame(s).`);
1266
+ }
1267
+ /**
1268
+ * Build the JSON-RPC request to send to an MCP runtime from `--event`:
1269
+ * - no `--event` (empty object) → `tools/list` (discover the server's tools),
1270
+ * - an object with a string `method` → that method + its `params`,
1271
+ * - anything else → a fail-fast error.
1272
+ *
1273
+ * Exported for unit testing.
1274
+ */
1275
+ function buildMcpRequest(event) {
1276
+ if (event === void 0 || event === null) return {
1277
+ method: "tools/list",
1278
+ params: {}
1279
+ };
1280
+ if (typeof event !== "object" || Array.isArray(event)) throw new CdkLocalError("MCP --event must be a JSON object describing a JSON-RPC request (e.g. {\"method\":\"tools/call\",\"params\":{\"name\":\"...\",\"arguments\":{...}}}).", "LOCAL_INVOKE_AGENTCORE_MCP_EVENT_INVALID");
1281
+ const obj = event;
1282
+ if (Object.keys(obj).length === 0) return {
1283
+ method: "tools/list",
1284
+ params: {}
1285
+ };
1286
+ if (typeof obj["method"] !== "string") throw new CdkLocalError(`MCP --event must include a string "method" (a JSON-RPC method such as "tools/list" or "tools/call"). Got keys: ${Object.keys(obj).join(", ")}.`, "LOCAL_INVOKE_AGENTCORE_MCP_EVENT_INVALID");
1287
+ return {
1288
+ method: obj["method"],
1289
+ ...obj["params"] !== void 0 && { params: obj["params"] }
1290
+ };
1291
+ }
1292
+ /** Print the MCP JSON-RPC response; exit 1 when it carried a JSON-RPC error. */
1293
+ function emitMcpResult(result) {
1294
+ if (!result.ok) {
1295
+ getLogger().warn("MCP server returned a JSON-RPC error.");
1296
+ process.exitCode = 1;
1297
+ }
1298
+ process.stdout.write(`${result.raw}\n`);
1299
+ }
1300
+ /**
1301
+ * Build the JSON-RPC request to send to an A2A runtime from `--event`:
1302
+ * - no `--event` (empty object) → `agent/getCard` (discover the agent's card),
1303
+ * - an object with a string `method` → that method + its `params`,
1304
+ * - anything else → a fail-fast error.
1305
+ *
1306
+ * Exported for unit testing.
1307
+ */
1308
+ function buildA2aRequest(event) {
1309
+ if (event === void 0 || event === null) return {
1310
+ method: "agent/getCard",
1311
+ params: {}
1312
+ };
1313
+ if (typeof event !== "object" || Array.isArray(event)) throw new CdkLocalError("A2A --event must be a JSON object describing a JSON-RPC request (e.g. {\"method\":\"tasks/send\",\"params\":{\"id\":\"...\",\"message\":{...}}}).", "LOCAL_INVOKE_AGENTCORE_A2A_EVENT_INVALID");
1314
+ const obj = event;
1315
+ if (Object.keys(obj).length === 0) return {
1316
+ method: "agent/getCard",
1317
+ params: {}
1318
+ };
1319
+ if (typeof obj["method"] !== "string") throw new CdkLocalError(`A2A --event must include a string "method" (a JSON-RPC method such as "agent/getCard" or "tasks/send"). Got keys: ${Object.keys(obj).join(", ")}.`, "LOCAL_INVOKE_AGENTCORE_A2A_EVENT_INVALID");
1320
+ return {
1321
+ method: obj["method"],
1322
+ ...obj["params"] !== void 0 && { params: obj["params"] }
1323
+ };
1324
+ }
1325
+ /** Print the A2A JSON-RPC response; exit 1 when it carried a JSON-RPC error. */
1326
+ function emitA2aResult(result) {
1327
+ if (!result.ok) {
1328
+ getLogger().warn("A2A server returned a JSON-RPC error.");
1329
+ process.exitCode = 1;
1330
+ }
1331
+ process.stdout.write(`${result.raw}\n`);
1332
+ }
1333
+ /** Map a `--platform` value to the architecture `buildContainerImage` expects. */
1334
+ function platformToArchitecture(platform) {
1335
+ return platform === "linux/amd64" ? "x86_64" : "arm64";
1336
+ }
1337
+ function forwardAwsEnv(env) {
1338
+ for (const key of [
1339
+ "AWS_ACCESS_KEY_ID",
1340
+ "AWS_SECRET_ACCESS_KEY",
1341
+ "AWS_SESSION_TOKEN",
1342
+ "AWS_REGION",
1343
+ "AWS_DEFAULT_REGION"
1344
+ ]) {
1345
+ const value = process.env[key];
1346
+ if (value !== void 0) env[key] = value;
1347
+ }
1348
+ }
1349
+ async function assumeAgentCoreExecutionRole(roleArn, region) {
1350
+ const { STSClient, AssumeRoleCommand } = await import("@aws-sdk/client-sts");
1351
+ const sts = new STSClient({ ...region && { region } });
1352
+ try {
1353
+ const creds = (await sts.send(new AssumeRoleCommand({
1354
+ RoleArn: roleArn,
1355
+ RoleSessionName: `${getEmbedConfig().resourceNamePrefix}-invoke-agentcore-${Date.now()}`,
1356
+ DurationSeconds: 3600
1357
+ }))).Credentials;
1358
+ if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) throw new Error(`AssumeRole(${roleArn}) returned no usable credentials.`);
1359
+ return {
1360
+ accessKeyId: creds.AccessKeyId,
1361
+ secretAccessKey: creds.SecretAccessKey,
1362
+ sessionToken: creds.SessionToken
1363
+ };
1364
+ } finally {
1365
+ sts.destroy();
1366
+ }
1367
+ }
1368
+ async function readEvent(options) {
1369
+ if (options.event && options.eventStdin) throw new Error("--event and --event-stdin are mutually exclusive.");
1370
+ if (options.eventStdin) return parseEvent(await readStdin(), "<stdin>");
1371
+ if (options.event) {
1372
+ let raw;
1373
+ try {
1374
+ raw = readFileSync(options.event, "utf-8");
1375
+ } catch (err) {
1376
+ throw new Error(`Failed to read --event file '${options.event}': ${err instanceof Error ? err.message : String(err)}`);
1377
+ }
1378
+ return parseEvent(raw, options.event);
1379
+ }
1380
+ return {};
1381
+ }
1382
+ function parseEvent(raw, source) {
1383
+ try {
1384
+ return JSON.parse(raw);
1385
+ } catch (err) {
1386
+ throw new Error(`Failed to parse event payload from ${source} as JSON: ${err instanceof Error ? err.message : String(err)}`);
1387
+ }
1388
+ }
1389
+ async function readStdin() {
1390
+ const chunks = [];
1391
+ for await (const chunk of process.stdin) chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
1392
+ return Buffer.concat(chunks).toString("utf-8");
1393
+ }
1394
+ /**
1395
+ * Read `process.stdin` line-buffered and yield each line as a string (trailing
1396
+ * `\r?\n` stripped). The async iterable completes when stdin EOFs (Ctrl-D /
1397
+ * end-of-pipe), and surrenders the underlying stream when its `return()` is
1398
+ * called — so the WS client can close down the source when the server closes
1399
+ * first without leaving stdin held open.
1400
+ *
1401
+ * Exported so a unit test can drive the iterable shape directly.
1402
+ */
1403
+ async function* readStdinLines() {
1404
+ const { createInterface } = await import("node:readline");
1405
+ const rl = createInterface({
1406
+ input: process.stdin,
1407
+ crlfDelay: Infinity
1408
+ });
1409
+ try {
1410
+ for await (const line of rl) yield line;
1411
+ } finally {
1412
+ rl.close();
1413
+ }
1414
+ }
1415
+ function readEnvOverridesFile$1(filePath) {
1416
+ if (!filePath) return void 0;
1417
+ let raw;
1418
+ try {
1419
+ raw = readFileSync(filePath, "utf-8");
1420
+ } catch (err) {
1421
+ throw new Error(`Failed to read --env-vars file '${filePath}': ${err instanceof Error ? err.message : String(err)}`);
1422
+ }
1423
+ let parsed;
1424
+ try {
1425
+ parsed = JSON.parse(raw);
1426
+ } catch (err) {
1427
+ throw new Error(`Failed to parse --env-vars file '${filePath}' as JSON: ${err instanceof Error ? err.message : String(err)}`);
1428
+ }
1429
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`--env-vars file '${filePath}' must contain a JSON object at the top level.`);
1430
+ return parsed;
1431
+ }
1432
+ function createLocalInvokeAgentCoreCommand(opts = {}) {
1433
+ setEmbedConfig(opts.embedConfig);
1434
+ const cmd = new Command("invoke-agentcore").description("Run a Bedrock AgentCore Runtime container locally and invoke it once over its protocol contract: HTTP (POST /invocations + GET /ping on 8080) or MCP (POST /mcp Streamable HTTP on 8000). Resolves the AWS::BedrockAgentCore::Runtime, pulls/builds its container, injects env vars + AWS credentials, and prints the response. For an MCP runtime, runs the session handshake then sends one JSON-RPC request (tools/list by default, or the method/params from --event). Target accepts a CDK display path (MyStack/MyAgent) or stack-qualified logical ID (MyStack:MyAgentRuntime1234). Single-stack apps may omit the stack prefix. Omit <target> in an interactive terminal to pick from a list. Supports the container artifact and the CodeConfiguration managed-runtime artifact (fromCodeAsset, built from source) on the HTTP + MCP protocols; the agent calls real AWS for managed services.").argument("[target]", "CDK display path or stack-qualified logical ID of the AgentCore Runtime to invoke (omit to pick interactively in a TTY)").addOption(new Option("-e, --event <file>", "JSON event payload file (default: {})")).addOption(new Option("--event-stdin", "Read event JSON from stdin").default(false)).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"LogicalId\":{\"KEY\":\"VALUE\"}})")).addOption(new Option("--session-id <id>", "AgentCore runtime session id header value (default: a random UUID)")).addOption(new Option("--ws", "Stream over the HTTP-protocol agent's bidirectional /ws WebSocket endpoint (on 8080) instead of POST /invocations: send --event as the first frame and print every received frame to stdout until the agent closes. Ignored for an MCP runtime.").default(false)).addOption(new Option("--ws-interactive", "REPL mode for --ws: after the initial --event frame, read additional frames from stdin (one frame per line, trailing newline stripped) and send each as a text frame until stdin EOFs (Ctrl-D) or the agent closes. Only meaningful with --ws.").default(false)).addOption(new Option("--bearer-token <jwt>", "Bearer JWT to present when the runtime declares a customJwtAuthorizer. Verified against the runtime OIDC discovery URL (signature / issuer / expiry / audience) before the container starts, then forwarded to /invocations as Authorization: Bearer <jwt>.")).addOption(new Option("--no-verify-auth", "Skip inbound JWT verification even when the runtime declares a customJwtAuthorizer (local-dev escape hatch). A --bearer-token, if given, is still forwarded.")).addOption(new Option("--sigv4", "Sign the /invocations POST with AWS SigV4 (service bedrock-agentcore) using the resolved credentials, matching the cloud default when the runtime declares no customJwtAuthorizer. Opt-in: default unsigned. Mutually exclusive with --bearer-token; ignored on a JWT-protected runtime.").default(false)).addOption(new Option("--platform <platform>", "docker --platform for the agent container (linux/amd64 or linux/arm64)").choices(["linux/amd64", "linux/arm64"]).default("linux/arm64")).addOption(new Option("--no-pull", "Skip docker pull (use cached image) — no-op for the local-build path")).addOption(new Option("--no-build", "Skip docker build on the local-asset path (use the previously-built tag). No-op for the ECR / registry pull paths.")).addOption(new Option("--container-host <host>", "Host to bind the agent port to").default("127.0.0.1")).addOption(new Option("--timeout <ms>", "Per-request timeout in milliseconds. Applied to POST /invocations, POST /mcp, and the /ws open-to-close window. Raise this for long-running agent calls that exceed the default.").default(12e4).argParser(parseTimeoutMs)).addOption(new Option("--assume-role [arn]", "Assume the runtime's execution role and forward STS-issued temp credentials to the container so the agent runs with the deployed role. Three forms: (1) `--assume-role <arn>` assumes the explicit ARN; (2) `--assume-role` (bare) uses the runtime's RoleArn when it is a literal ARN; (3) `--no-assume-role` opts out. Off by default — the developer's shell credentials are forwarded unchanged.")).addOption(new Option("--ecr-role-arn <arn>", "Role ARN to assume before authenticating against ECR for cross-account / centralized registries. Same-account / same-region pulls do not need this flag.")).addOption(new Option("--from-cfn-stack [cfn-stack-name]", "Read a deployed CloudFormation stack via ListStackResources and substitute Ref / Fn::ImportValue in env vars with the deployed physical IDs / exports. Bare form uses the resolved stack name; pass an explicit value when the CFn stack name differs.")).addOption(new Option("--stack-region <region>", "Region of the state record to read. Used with --from-cfn-stack as the CFn client region.")).action(withErrorHandling(async (target, options) => {
1435
+ await localInvokeAgentCoreCommand(target, options, opts.extraStateProviders);
1436
+ }));
1437
+ [
1438
+ ...commonOptions(),
1439
+ ...appOptions(),
1440
+ ...contextOptions
1441
+ ].forEach((opt) => cmd.addOption(opt));
1442
+ cmd.addOption(deprecatedRegionOption);
1443
+ return cmd;
1444
+ }
1445
+
1446
+ //#endregion
1447
+ //#region src/cli/commands/local-run-task.ts
1448
+ /**
1449
+ * `cdkl run-task <target>` — Phase 1 of the ECS local-execution
1450
+ * trilogy. Synthesizes the CDK app, locates the target
1451
+ * `AWS::ECS::TaskDefinition`, stands up a per-task docker network with
1452
+ * the AWS-published `amazon-ecs-local-container-endpoints` sidecar, and
1453
+ * starts every container in `dependsOn` order. The essential
1454
+ * container's exit code drives the CLI's exit.
1455
+ */
1456
+ async function localRunTaskCommand(target, options, extraStateProviders) {
1457
+ const logger = getLogger();
1458
+ if (options.verbose) logger.setLevel("debug");
1459
+ warnIfDeprecatedRegion(options);
1460
+ const state = createEcsRunState();
1461
+ let sigintHandler;
1462
+ let sigintCount = 0;
1463
+ let stateProvider;
1464
+ let profileCredsFile;
1465
+ let cleanupPromise;
1466
+ const cleanup = async () => {
1467
+ if (!cleanupPromise) cleanupPromise = (async () => {
1468
+ try {
1469
+ await cleanupEcsRun(state, { keepRunning: options.keepRunning });
1470
+ } catch (err) {
1471
+ getLogger().debug(`cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
1472
+ }
1473
+ if (profileCredsFile) try {
1474
+ await profileCredsFile.dispose();
1475
+ } catch (err) {
1476
+ getLogger().debug(`Failed to remove profile credentials tmpdir ${profileCredsFile.hostPath}: ${err instanceof Error ? err.message : String(err)}`);
1477
+ }
1478
+ })();
1479
+ await cleanupPromise;
1480
+ };
1481
+ try {
1482
+ await applyRoleArnIfSet({
1483
+ roleArn: options.roleArn,
1484
+ region: options.region
1485
+ });
1486
+ await ensureDockerAvailable();
1487
+ const appCmd = resolveApp(options.app);
1488
+ if (!appCmd) throw new Error(`No CDK app specified. Pass --app, set ${getEmbedConfig().envPrefix}_APP, or add "app" to cdk.json.`);
1489
+ logger.info("Synthesizing CDK app...");
1490
+ const synthesizer = new Synthesizer();
1491
+ const context = parseContextOptions(options.context);
1492
+ const synthOpts = {
1493
+ app: appCmd,
1494
+ output: options.output,
1495
+ ...options.region && { region: options.region },
1496
+ ...options.profile && { profile: options.profile },
1497
+ ...Object.keys(context).length > 0 && { context }
1498
+ };
1499
+ const { stacks } = await synthesizer.synthesize(synthOpts);
1500
+ const resolvedTarget = await resolveSingleTarget(target, {
1501
+ entries: listTargets(stacks).ecsTaskDefinitions,
1502
+ message: "Select an ECS task definition to run",
1503
+ noun: "ECS task definitions",
1504
+ onMissing: () => new CdkLocalError(`${getEmbedConfig().cliName} run-task requires a <target> (an ECS task definition display path or logical ID). Run \`${getEmbedConfig().cliName} list\` to see them, or run it in a TTY to pick interactively.`, "LOCAL_RUN_TASK_TARGET_REQUIRED")
1505
+ });
1506
+ const candidate = pickCandidateStack(parseEcsTarget(resolvedTarget).stackPattern, stacks);
1507
+ stateProvider = createLocalStateProvider(options, candidate?.stackName ?? "", await resolveCfnFallbackRegion(options, candidate?.region), extraStateProviders);
1508
+ const imageContext = await buildEcsImageResolutionContext(candidate, stateProvider, options);
1509
+ const task = resolveEcsTaskTarget(resolvedTarget, stacks, imageContext);
1510
+ logger.info(`Target: ${task.stack.stackName}/${task.taskDefinitionLogicalId} (family=${task.family}, containers=${task.containers.length})`);
1511
+ const taskNeeds = detectEcsImageResolutionNeeds(stacks.find((s) => s.stackName === task.stack.stackName) ?? task.stack);
1512
+ if (stateProvider && taskNeeds.needsCrossStackResolver) {
1513
+ const consumerRegion = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? task.stack.region ?? "us-east-1";
1514
+ const resolver = await stateProvider.buildCrossStackResolver(consumerRegion);
1515
+ if (resolver) await applyCrossStackResolverToTask(task, {
1516
+ resources: imageContext?.stateResources ?? {},
1517
+ ...imageContext?.pseudoParameters && { pseudoParameters: imageContext.pseudoParameters },
1518
+ ...imageContext?.stateParameters && { parameters: imageContext.stateParameters },
1519
+ ...imageContext?.stateSensitiveParameters?.length && { sensitiveParameters: new Set(imageContext.stateSensitiveParameters) },
1520
+ consumerRegion,
1521
+ crossStackResolver: resolver
1522
+ });
1523
+ } else if (!stateProvider && taskNeeds.needsCrossStackResolver) logger.warn("Container Environment / Secrets entries contain Fn::ImportValue / Fn::GetStackOutput intrinsics. Pass a state-source flag (e.g. --from-cfn-stack or a host-provided extension) to substitute them against deployed state.");
1524
+ sigintHandler = () => {
1525
+ sigintCount += 1;
1526
+ if (sigintCount >= 2) {
1527
+ process.stderr.write("Force-exit on second ^C; container cleanup skipped.\n");
1528
+ process.exit(130);
1529
+ }
1530
+ logger.info("Stopping task...");
1531
+ cleanup().then(() => process.exit(130));
1532
+ };
1533
+ process.on("SIGINT", sigintHandler);
1534
+ let assumedCredentials;
1535
+ let resolvedRoleArn;
1536
+ if (options.assumeTaskRole === true) {
1537
+ if (!task.taskRoleArn) throw new Error(`--assume-task-role passed without an ARN but the task definition has no resolvable TaskRoleArn. Either the task definition does not set TaskRoleArn, or it points at a resource ${getEmbedConfig().binaryName} cannot resolve to an IAM Role at synth time. Pass the ARN explicitly: --assume-task-role <arn>`);
1538
+ resolvedRoleArn = await resolvePlaceholderAccount(task.taskRoleArn, options.region);
1539
+ assumedCredentials = await assumeTaskRole(resolvedRoleArn, options.region);
1540
+ } else if (typeof options.assumeTaskRole === "string") {
1541
+ resolvedRoleArn = options.assumeTaskRole;
1542
+ assumedCredentials = await assumeTaskRole(resolvedRoleArn, options.region);
1543
+ }
1544
+ const sidecarCredentials = await resolveSidecarCredentials(options, assumedCredentials);
1545
+ if (options.profile && sidecarCredentials && !assumedCredentials) profileCredsFile = await writeProfileCredentialsFile(options.profile, sidecarCredentials);
1546
+ const envOverrides = readEnvOverridesFile(options.envVars);
1547
+ const runOpts = {
1548
+ cluster: options.cluster,
1549
+ containerHost: options.containerHost,
1550
+ skipPull: options.pull === false,
1551
+ keepRunning: options.keepRunning,
1552
+ detach: options.detach
1553
+ };
1554
+ if (envOverrides) runOpts.envOverrides = envOverrides;
1555
+ if (sidecarCredentials) runOpts.taskCredentials = sidecarCredentials;
1556
+ if (resolvedRoleArn) runOpts.taskRoleArn = resolvedRoleArn;
1557
+ if (options.platform) runOpts.platformOverride = options.platform;
1558
+ if (options.region) runOpts.region = options.region;
1559
+ if (options.ecrRoleArn) runOpts.ecrRoleArn = options.ecrRoleArn;
1560
+ if (options.profile) runOpts.profile = options.profile;
1561
+ const hostPortOverrides = parseHostPortOverrides(options.hostPort);
1562
+ if (Object.keys(hostPortOverrides).length > 0) runOpts.hostPortOverrides = hostPortOverrides;
1563
+ if (profileCredsFile) runOpts.profileCredentialsFile = {
1564
+ hostPath: profileCredsFile.hostPath,
1565
+ containerPath: profileCredsFile.containerPath,
1566
+ profileName: profileCredsFile.profileName
1567
+ };
1568
+ const result = await runEcsTask(task, runOpts, state);
1569
+ if (options.detach) {
1570
+ logger.info(`Task containers started in detached mode; ${getEmbedConfig().binaryName} is exiting.`);
1571
+ logger.info(`Use 'docker ps --filter network=${result.state.network?.networkName ?? "<network>"}' to inspect; tear down with 'docker rm -f' and 'docker network rm'.`);
1572
+ sigintCount = 99;
1573
+ return;
1574
+ }
1575
+ if (result.essentialContainerName) logger.info(`Essential container '${result.essentialContainerName}' exited with code ${result.exitCode}.`);
1576
+ if (result.exitCode !== 0) process.exitCode = result.exitCode;
1577
+ } finally {
1578
+ if (sigintHandler) process.off("SIGINT", sigintHandler);
1579
+ if (stateProvider) stateProvider.dispose();
1580
+ if (!options.detach) await cleanup();
1581
+ }
1582
+ }
1583
+ /**
1584
+ * If `arn` contains the `${AWS::AccountId}` placeholder emitted by the
1585
+ * resolver for inline same-stack IAM Roles, substitute the live caller
1586
+ * account via STS `GetCallerIdentity`. Otherwise pass through unchanged.
1587
+ */
1588
+ async function resolvePlaceholderAccount(arn, region) {
1589
+ if (!arn.includes("${AWS::AccountId}")) return arn;
1590
+ const { STSClient, GetCallerIdentityCommand } = await import("@aws-sdk/client-sts");
1591
+ const sts = new STSClient({ ...region && { region } });
1592
+ try {
1593
+ const account = (await sts.send(new GetCallerIdentityCommand({}))).Account;
1594
+ if (!account) throw new Error(`--assume-task-role: GetCallerIdentity returned no Account; cannot resolve placeholder ARN '${arn}'. Pass the ARN explicitly: --assume-task-role <arn>`);
1595
+ return arn.split(TASK_ROLE_ACCOUNT_PLACEHOLDER).join(account);
1596
+ } finally {
1597
+ sts.destroy();
1598
+ }
1599
+ }
1600
+ /**
1601
+ * Assume `roleArn` and return temp credentials.
1602
+ */
1603
+ async function assumeTaskRole(roleArn, region) {
1604
+ const { STSClient, AssumeRoleCommand } = await import("@aws-sdk/client-sts");
1605
+ const sts = new STSClient({ ...region && { region } });
1606
+ try {
1607
+ const creds = (await sts.send(new AssumeRoleCommand({
1608
+ RoleArn: roleArn,
1609
+ RoleSessionName: `${getEmbedConfig().resourceNamePrefix}-run-task-${Date.now()}`,
1610
+ DurationSeconds: 3600
1611
+ }))).Credentials;
1612
+ if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) throw new Error(`AssumeRole(${roleArn}) returned no usable credentials.`);
1613
+ return {
1614
+ accessKeyId: creds.AccessKeyId,
1615
+ secretAccessKey: creds.SecretAccessKey,
1616
+ sessionToken: creds.SessionToken
1617
+ };
1618
+ } finally {
1619
+ sts.destroy();
1620
+ }
1621
+ }
1622
+ /**
1623
+ * Build the substitution context the ECS task resolver consumes.
1624
+ * Returns `undefined` when no container's `Image` field needs
1625
+ * substitution — the resolver behaves as before in that case.
1626
+ */
1627
+ async function buildEcsImageResolutionContext(candidate, stateProvider, options) {
1628
+ const logger = getLogger();
1629
+ if (!candidate) return void 0;
1630
+ const needs = detectEcsImageResolutionNeeds(candidate);
1631
+ if (!needs.needsPseudoParameters && !needs.needsStateResources && !needs.needsEnvOrSecretSubstitution) return;
1632
+ const ctx = {};
1633
+ const wantsPseudoForEnvOrSecret = !!stateProvider && needs.needsEnvOrSecretSubstitution;
1634
+ if (needs.needsPseudoParameters || wantsPseudoForEnvOrSecret) {
1635
+ const region = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? candidate.region;
1636
+ if (!region) logger.warn(`Resolver references \${AWS::Region} but ${getEmbedConfig().binaryName} could not determine the target region. Pass --region, set AWS_REGION, or declare env.region on the CDK stack.`);
1637
+ let accountId;
1638
+ try {
1639
+ accountId = await resolveCallerAccountId(region, options.profile);
1640
+ } catch (err) {
1641
+ logger.warn(`Resolver needs \${AWS::AccountId} but STS GetCallerIdentity failed: ${err instanceof Error ? err.message : String(err)}. Substitution will be skipped; affected env / secret entries will be dropped with per-key warnings.`);
1642
+ }
1643
+ const partitionAndSuffix = region ? derivePartitionAndUrlSuffix(region) : void 0;
1644
+ ctx.pseudoParameters = {
1645
+ ...accountId !== void 0 && { accountId },
1646
+ ...region !== void 0 && { region },
1647
+ ...partitionAndSuffix && {
1648
+ partition: partitionAndSuffix.partition,
1649
+ urlSuffix: partitionAndSuffix.urlSuffix
1650
+ }
1651
+ };
1652
+ }
1653
+ const wantsState = needs.needsStateResources || needs.needsEnvOrSecretSubstitution;
1654
+ if (stateProvider && wantsState) {
1655
+ const loaded = await stateProvider.load(candidate.stackName, candidate.region);
1656
+ if (loaded) ctx.stateResources = loaded.resources;
1657
+ if (needs.needsEnvOrSecretSubstitution && stateProvider.resolveTemplateSsmParameters) {
1658
+ const ssmParameters = await stateProvider.resolveTemplateSsmParameters(candidate.template);
1659
+ if (Object.keys(ssmParameters.values).length > 0) ctx.stateParameters = ssmParameters.values;
1660
+ if (ssmParameters.secureStringLogicalIds.length > 0) ctx.stateSensitiveParameters = ssmParameters.secureStringLogicalIds;
1661
+ }
1662
+ } else if (!stateProvider && needs.needsStateResources) logger.warn("Container Image references a same-stack AWS::ECR::Repository. Pass a state-source flag (e.g. --from-cfn-stack or a host-provided extension) to substitute the deployed repository URI. Otherwise the resolver will surface its existing error.");
1663
+ else if (!stateProvider && needs.needsEnvOrSecretSubstitution) logger.warn("Container Environment / Secrets entries contain CloudFormation intrinsics (Ref / Fn::GetAtt / Fn::Sub / Fn::Join). Pass a state-source flag (e.g. --from-cfn-stack or a host-provided extension) to substitute them against deployed state. Without a state source these entries are dropped (per-key warnings will follow).");
1664
+ return ctx;
1665
+ }
1666
+ function pickCandidateStack(stackPattern, stacks) {
1667
+ if (stackPattern === null) {
1668
+ if (stacks.length === 1) return stacks[0];
1669
+ return;
1670
+ }
1671
+ const matched = matchStacks(stacks, [stackPattern]);
1672
+ if (matched.length === 1) return matched[0];
1673
+ }
1674
+ async function resolveCallerAccountId(region, profile) {
1675
+ const { STSClient, GetCallerIdentityCommand } = await import("@aws-sdk/client-sts");
1676
+ const sts = new STSClient({
1677
+ ...region && { region },
1678
+ ...profile && { profile }
1679
+ });
1680
+ try {
1681
+ return (await sts.send(new GetCallerIdentityCommand({}))).Account;
1682
+ } finally {
1683
+ sts.destroy();
1684
+ }
1685
+ }
1686
+ /**
1687
+ * Read the `--env-vars` JSON file using the same SAM-style shape as
1688
+ * `cdkl invoke --env-vars`: top-level keys are container names, with
1689
+ * `Parameters` reserved for global entries.
1690
+ */
1691
+ function readEnvOverridesFile(filePath) {
1692
+ if (!filePath) return void 0;
1693
+ let raw;
1694
+ try {
1695
+ raw = readFileSync(filePath, "utf-8");
1696
+ } catch (err) {
1697
+ throw new Error(`Failed to read --env-vars file '${filePath}': ${err instanceof Error ? err.message : String(err)}`);
1698
+ }
1699
+ let parsed;
1700
+ try {
1701
+ parsed = JSON.parse(raw);
1702
+ } catch (err) {
1703
+ throw new Error(`Failed to parse --env-vars file '${filePath}' as JSON: ${err instanceof Error ? err.message : String(err)}`);
1704
+ }
1705
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`--env-vars file '${filePath}' must contain a JSON object at the top level.`);
1706
+ return parsed;
1707
+ }
1708
+ /**
1709
+ * Pick the credentials forwarded to the AWS-published
1710
+ * `amazon-ecs-local-container-endpoints` sidecar. Precedence:
1711
+ * 1. `--assume-task-role <arn>` (or bare `--assume-task-role` against
1712
+ * a resolvable `TaskRoleArn`) → STS-assumed temp creds. Highest
1713
+ * priority — when the user opted in to IAM emulation, those creds
1714
+ * drive the sidecar regardless of `--profile`.
1715
+ * 2. `--profile <p>` → resolved via {@link resolveProfileCredentials}
1716
+ * (the SDK's default credential provider chain — SSO / IAM
1717
+ * Identity Center / fromIni / role-assumption). NEW in this PR.
1718
+ * 3. Neither set → `undefined`; the sidecar runs with its own
1719
+ * default credential chain (typically empty inside a fresh
1720
+ * container — user containers will get 4xx from the credentials
1721
+ * endpoint, mimicking IAM-misconfigured prod).
1722
+ *
1723
+ * Extracted as an exported helper so a unit test can exercise every
1724
+ * branch without having to mock the full Synth + Docker + AWS pipeline
1725
+ * (the strategy used for the Lambda container path).
1726
+ */
1727
+ async function resolveSidecarCredentials(options, assumedCredentials) {
1728
+ if (assumedCredentials) return assumedCredentials;
1729
+ if (options.profile) return resolveProfileCredentials$1(options.profile);
1730
+ }
1731
+ function createLocalRunTaskCommand(opts = {}) {
1732
+ setEmbedConfig(opts.embedConfig);
1733
+ const cmd = new Command("run-task").description("Run an AWS::ECS::TaskDefinition locally — pulls/builds images, sets up a per-task docker network with the AWS-published metadata-endpoints sidecar, and starts every container in dependsOn order. Target accepts a CDK display path (MyStack/MyService/TaskDef) or stack-qualified logical ID (MyStack:MyServiceTaskDefXYZ1234). Single-stack apps may omit the stack prefix. Omit <target> in an interactive terminal to pick the task definition from a list.").argument("[target]", "CDK display path or stack-qualified logical ID of the AWS::ECS::TaskDefinition to run (omit to pick interactively in a TTY)").addOption(new Option("--cluster <name>", "Cluster name surfaced to ECS_CONTAINER_METADATA_URI_V4 and used as the docker network prefix").default(getEmbedConfig().resourceNamePrefix)).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"ContainerName\":{\"KEY\":\"VALUE\"}, \"Parameters\":{}})")).addOption(new Option("--container-host <ip>", "Host IP to bind published container ports to. Must be a numeric IP (Docker rejects hostnames here)").default("127.0.0.1")).addOption(new Option("--host-port <containerPort=hostPort...>", "Publish a container port on a specific host port (e.g. 80=8080); repeatable. Default: host port == container port. Use this on macOS to map a privileged container port (< 1024) to a non-privileged host port and avoid the Docker Desktop admin-password prompt.")).addOption(new Option("--assume-task-role [arn]", "Assume the task definition's TaskRoleArn (or the supplied ARN) and forward STS-issued temp credentials via the metadata sidecar so containers run with the deployed function role. Bare flag uses the template's TaskRoleArn; pass an explicit ARN to override.")).addOption(new Option("--no-pull", "Skip docker pull for every container image and the metadata sidecar")).addOption(new Option("--ecr-role-arn <arn>", "Role ARN to assume before authenticating against ECR for cross-account / centralized registries. Issues sts:AssumeRole via the default credential chain and uses the temporary credentials for ecr:GetAuthorizationToken + docker pull. Required when the caller does not have direct cross-account access to the target repository. Same-account / same-region pulls do not need this flag.")).addOption(new Option("--platform <platform>", "Force docker --platform (linux/amd64 or linux/arm64). Default: inferred from task RuntimePlatform.CpuArchitecture")).addOption(new Option("--keep-running", "Don't docker rm -f the user containers on task exit (network + sidecar are still torn down). Use when you want to docker exec into a stopped container for post-mortems.").default(false)).addOption(new Option("--detach", "Start the containers in the background and exit (skip log streaming + auto teardown). Useful in CI smoke tests; caller manages container lifecycle.").default(false)).addOption(new Option("--from-cfn-stack [cfn-stack-name]", `Read a deployed CloudFormation stack via ListStackResources and substitute Ref / Fn::ImportValue in container env vars / secrets / image URIs with the deployed physical IDs / exports. Use for CDK apps deployed via the upstream CDK CLI (\`cdk deploy\`). Bare form uses the ${getEmbedConfig().binaryName} stack name; pass an explicit value when the CFn stack name differs. Fn::GetAtt is warn-and-dropped in v1 (CFn ListStackResources does not return per-attribute values).`)).addOption(new Option("--stack-region <region>", "Region of the state record to read. Used with --from-cfn-stack as the CFn client region.")).action(withErrorHandling(async (target, options) => {
1734
+ await localRunTaskCommand(target, options, opts.extraStateProviders);
1735
+ }));
1736
+ [
1737
+ ...commonOptions(),
1738
+ ...appOptions(),
1739
+ ...contextOptions
1740
+ ].forEach((opt) => cmd.addOption(opt));
1741
+ cmd.addOption(deprecatedRegionOption);
1742
+ return cmd;
1743
+ }
1744
+
1745
+ //#endregion
1746
+ //#region src/cli/commands/local-start-service.ts
1747
+ /**
1748
+ * `cdkl start-service` strategy — a pure ECS replica runner. It picks
1749
+ * `AWS::ECS::Service` targets and boots each with NO front-door (the ALB
1750
+ * front-door is its own command, `cdkl start-alb`). This keeps `start-service`
1751
+ * a leaf compute runner, symmetric with `invoke` / `run-task`.
1752
+ */
1753
+ function serviceStrategy() {
1754
+ return {
1755
+ pickEntries: (stacks) => listTargets(stacks).ecsServices,
1756
+ pickerMessage: "Select one or more ECS services to run",
1757
+ pickerNoun: "ECS services",
1758
+ onMissing: () => new LocalStartServiceError(`${getEmbedConfig().cliName} start-service requires at least one <target>. Pass one or more service paths like 'Stack/Orders' 'Stack/Frontend', or run it in a TTY to pick interactively.`),
1759
+ resolveBoots: (_stacks, chosenTargets) => ({
1760
+ boots: chosenTargets.map((target) => ({ target })),
1761
+ warnings: []
1762
+ }),
1763
+ lbPortOverrides: {}
1764
+ };
1765
+ }
1766
+ /**
1767
+ * `cdkl start-service <Stack/Service>` — Phase 2 of #262. Spins up
1768
+ * `DesiredCount` task replicas locally (clamped by `--max-tasks`) using the
1769
+ * existing `ecs-task-runner` per replica. Long-running; ^C cleans every replica
1770
+ * + sidecar + shared network. Pure compute: to put a local ALB front-door in
1771
+ * front of an ALB-fronted service, use `cdkl start-alb`.
1772
+ */
1773
+ function createLocalStartServiceCommand(opts = {}) {
1774
+ setEmbedConfig(opts.embedConfig);
1775
+ return addCommonEcsServiceOptions(new Command("start-service").description(`Run one or more AWS::ECS::Service resources locally as a long-running emulator. Spins up DesiredCount task replicas per service (clamped by --max-tasks) using the same per-task docker network + metadata sidecar pattern as \`${getEmbedConfig().cliName} run-task\`, then keeps each replica running and restarts it on exit per --restart-policy. ^C tears every replica + sidecar + network down. Each <target> accepts a CDK display path (MyStack/MyService) or stack-qualified logical ID (MyStack:MyServiceXYZ); single-stack apps may omit the stack prefix. When two or more <target>s are supplied, every service is booted into a shared Cloud Map / Service Connect registry so peer services discover each other via docker --add-host overlay. Omit <targets> in an interactive terminal to multi-select the services from a list. To put a local ALB front-door in front of an ALB-fronted service, use \`${getEmbedConfig().cliName} start-alb\` instead.`).argument("[targets...]", "One or more CDK display paths or stack-qualified logical IDs of the AWS::ECS::Service resources to run (omit to multi-select interactively in a TTY)").addOption(new Option("--host-port <containerPort=hostPort...>", "Publish a container port on a specific host port (e.g. 80=8080); repeatable. Default: host port == container port. Use this on macOS to map a privileged container port (< 1024) to a non-privileged host port and avoid the Docker Desktop admin-password prompt. (Single-replica services only — multi-replica services do not publish host ports.)")).action(withErrorHandling(async (targets, options) => {
1776
+ await runEcsServiceEmulator(targets, options, serviceStrategy(), opts.extraStateProviders);
1777
+ })));
1778
+ }
1779
+
1780
+ //#endregion
1781
+ //#region src/cli/commands/local-start-alb.ts
1782
+ /**
1783
+ * Issue #86 v1 — parse `--lb-port <listenerPort>=<hostPort>` overrides into a
1784
+ * `listenerPort -> hostPort` map. The local ALB front-door binds the listener
1785
+ * port on the host by default, but a privileged listener port (e.g. 80 / 443)
1786
+ * fails to bind as non-root on macOS, so the user opts in to a non-privileged
1787
+ * host port (e.g. `--lb-port 80=8080`). Repeatable; each value is
1788
+ * `<listenerPort>=<hostPort>` with both in 1-65535.
1789
+ */
1790
+ function parseLbPortOverrides(values) {
1791
+ const out = {};
1792
+ for (const raw of values ?? []) {
1793
+ const m = /^(\d+)=(\d+)$/.exec(raw.trim());
1794
+ if (!m) throw new LocalStartServiceError(`Invalid --lb-port '${raw}'. Expected <listenerPort>=<hostPort> (e.g. 80=8080).`);
1795
+ const listenerPort = Number(m[1]);
1796
+ const hostPort = Number(m[2]);
1797
+ for (const [label, p] of [["listener", listenerPort], ["host", hostPort]]) if (p < 1 || p > 65535) throw new LocalStartServiceError(`Invalid --lb-port '${raw}': ${label} port must be 1-65535.`);
1798
+ out[listenerPort] = hostPort;
1799
+ }
1800
+ return out;
1801
+ }
1802
+ /**
1803
+ * Resolve an ALB target string (`Stack/Path` display path or `Stack:LogicalId`)
1804
+ * to its stack + `AWS::ElasticLoadBalancingV2::LoadBalancer` logical id. Mirrors
1805
+ * the ECS service resolver's target grammar.
1806
+ */
1807
+ function resolveAlbTarget(target, stacks) {
1808
+ if (stacks.length === 0) throw new LocalStartServiceError("No stacks found in the synthesized assembly.");
1809
+ const parsed = parseEcsTarget(target);
1810
+ const stack = pickStack(parsed.stackPattern, stacks, target);
1811
+ const resources = stack.template.Resources ?? {};
1812
+ if (parsed.isPath) {
1813
+ const index = buildCdkPathIndex(stack.template);
1814
+ const albs = resolveCdkPathToLogicalIds(parsed.pathOrId, index).filter(({ logicalId }) => {
1815
+ const r = resources[logicalId];
1816
+ return r !== void 0 && isApplicationLoadBalancer(r);
1817
+ });
1818
+ if (albs.length === 0) throw notFound(target, stack, resources);
1819
+ if (albs.length > 1) throw new LocalStartServiceError(`Target '${target}' matches ${albs.length} load balancers in ${stack.stackName}: ${albs.map((a) => a.logicalId).join(", ")}. Refine the path or use the stack:LogicalId form.`);
1820
+ return {
1821
+ stack,
1822
+ albLogicalId: albs[0].logicalId
1823
+ };
1824
+ }
1825
+ const res = resources[parsed.pathOrId];
1826
+ if (!res || !isApplicationLoadBalancer(res)) throw notFound(target, stack, resources);
1827
+ return {
1828
+ stack,
1829
+ albLogicalId: parsed.pathOrId
1830
+ };
1831
+ }
1832
+ function pickStack(stackPattern, stacks, target) {
1833
+ if (stackPattern === null) {
1834
+ if (stacks.length === 1) return stacks[0];
1835
+ throw new LocalStartServiceError(`Target '${target}' has no stack prefix, and the assembly contains ${stacks.length} stacks: ${stacks.map((s) => s.stackName).join(", ")}. Pass it as 'Stack/Path' or 'Stack:LogicalId'.`);
1836
+ }
1837
+ const matched = matchStacks(stacks, [stackPattern]);
1838
+ if (matched.length === 0) throw new LocalStartServiceError(`No stack matches '${stackPattern}'. Available stacks: ${stacks.map((s) => s.stackName).join(", ")}.`);
1839
+ if (matched.length > 1) throw new LocalStartServiceError(`Multiple stacks match '${stackPattern}': ${matched.map((s) => s.stackName).join(", ")}. Refine the pattern.`);
1840
+ return matched[0];
1841
+ }
1842
+ function notFound(target, stack, resources) {
1843
+ const albs = Object.entries(resources).filter(([, r]) => r.Type === "AWS::ElasticLoadBalancingV2::LoadBalancer").map(([logicalId]) => logicalId);
1844
+ const available = albs.length > 0 ? ` Available load balancers in ${stack.stackName}: ${albs.join(", ")}.` : ` ${stack.stackName} declares no AWS::ElasticLoadBalancingV2::LoadBalancer resources.`;
1845
+ return new LocalStartServiceError(`Target '${target}' did not match an application Load Balancer in ${stack.stackName}.${available}`);
1846
+ }
1847
+ /**
1848
+ * `cdkl start-alb` strategy — name the ALB, boot the ECS service(s) behind it,
1849
+ * and expose each listener via a local front-door. Mirrors how `start-api`
1850
+ * names the API and serves its backing Lambdas.
1851
+ */
1852
+ function albStrategy(options) {
1853
+ const lbPortOverrides = parseLbPortOverrides(options.lbPort);
1854
+ return {
1855
+ pickEntries: (stacks) => listTargets(stacks).loadBalancers,
1856
+ pickerMessage: "Select one or more Application Load Balancers to run",
1857
+ pickerNoun: "Application Load Balancers",
1858
+ onMissing: () => new LocalStartServiceError(`${getEmbedConfig().cliName} start-alb requires at least one <target>. Pass one or more ALB paths like 'Stack/MyAlb', or run it in a TTY to pick interactively.`),
1859
+ resolveBoots: (stacks, chosenTargets) => {
1860
+ const warnings = [];
1861
+ const serviceTargets = /* @__PURE__ */ new Set();
1862
+ const listeners = [];
1863
+ const claimedHostPorts = /* @__PURE__ */ new Map();
1864
+ for (const albTarget of chosenTargets) {
1865
+ const { stack, albLogicalId } = resolveAlbTarget(albTarget, stacks);
1866
+ const resolution = resolveAlbFrontDoor(stack, albLogicalId);
1867
+ warnings.push(...resolution.warnings);
1868
+ const qualifyTarget = (t) => {
1869
+ if (t.kind === "lambda") return {
1870
+ kind: "lambda",
1871
+ lambda: resolveLambdaTarget(`${stack.stackName}:${t.lambdaLogicalId}`, stacks),
1872
+ targetGroupArn: `${stack.stackName}:${t.targetGroupLogicalId}`,
1873
+ multiValueHeaders: t.multiValueHeaders,
1874
+ weight: t.weight
1875
+ };
1876
+ const serviceTarget = `${stack.stackName}:${t.serviceLogicalId}`;
1877
+ serviceTargets.add(serviceTarget);
1878
+ return {
1879
+ kind: "ecs",
1880
+ serviceTarget,
1881
+ targetContainerName: t.targetContainerName,
1882
+ targetContainerPort: t.targetContainerPort,
1883
+ weight: t.weight
1884
+ };
1885
+ };
1886
+ const qualify = (action) => {
1887
+ if (action.kind === "forward") return {
1888
+ kind: "forward",
1889
+ targets: action.targets.map(qualifyTarget)
1890
+ };
1891
+ if (action.kind === "redirect") return {
1892
+ kind: "redirect",
1893
+ statusCode: action.statusCode,
1894
+ ...action.protocol !== void 0 && { protocol: action.protocol },
1895
+ ...action.host !== void 0 && { host: action.host },
1896
+ ...action.port !== void 0 && { port: action.port },
1897
+ ...action.path !== void 0 && { path: action.path },
1898
+ ...action.query !== void 0 && { query: action.query }
1899
+ };
1900
+ return {
1901
+ kind: "fixed-response",
1902
+ statusCode: action.statusCode,
1903
+ ...action.contentType !== void 0 && { contentType: action.contentType },
1904
+ ...action.messageBody !== void 0 && { messageBody: action.messageBody }
1905
+ };
1906
+ };
1907
+ for (const listener of resolution.listeners) {
1908
+ const hostPort = lbPortOverrides[listener.listenerPort] ?? listener.listenerPort;
1909
+ const claimedBy = claimedHostPorts.get(hostPort);
1910
+ if (claimedBy !== void 0) {
1911
+ warnings.push(`Listener port ${listener.listenerPort} would bind host port ${hostPort}, already claimed by listener port ${claimedBy}; the local front-door fronts only the first. Use --lb-port to remap one of them.`);
1912
+ continue;
1913
+ }
1914
+ claimedHostPorts.set(hostPort, listener.listenerPort);
1915
+ listeners.push({
1916
+ listenerPort: listener.listenerPort,
1917
+ hostPort,
1918
+ protocol: listener.listenerProtocol,
1919
+ ...listener.defaultAction ? { defaultAction: qualify(listener.defaultAction) } : {},
1920
+ ...listener.defaultAuthGuard ? { defaultAuthGuard: listener.defaultAuthGuard } : {},
1921
+ rules: listener.rules.map((r) => ({
1922
+ priority: r.priority,
1923
+ pathPatterns: r.pathPatterns,
1924
+ hostPatterns: r.hostPatterns,
1925
+ httpHeaderConditions: r.httpHeaderConditions,
1926
+ httpRequestMethods: r.httpRequestMethods,
1927
+ queryStringConditions: r.queryStringConditions,
1928
+ sourceIpCidrs: r.sourceIpCidrs,
1929
+ action: qualify(r.action),
1930
+ ...r.authGuard ? { authGuard: r.authGuard } : {}
1931
+ }))
1932
+ });
1933
+ }
1934
+ }
1935
+ const boots = [...serviceTargets].map((target) => ({ target }));
1936
+ const resolvedPorts = new Set(listeners.map((l) => l.listenerPort));
1937
+ for (const portStr of Object.keys(lbPortOverrides)) {
1938
+ const port = Number(portStr);
1939
+ if (!resolvedPorts.has(port)) warnings.push(`--lb-port override for listener port ${port} matched no ALB listener resolved for the named target(s); it was ignored.`);
1940
+ }
1941
+ return {
1942
+ boots,
1943
+ ...listeners.length > 0 ? { frontDoor: { listeners } } : {},
1944
+ warnings
1945
+ };
1946
+ },
1947
+ lbPortOverrides
1948
+ };
1949
+ }
1950
+ /**
1951
+ * `cdkl start-alb <Stack/Alb>` — Issue #86 v1. Names an
1952
+ * `AWS::ElasticLoadBalancingV2::LoadBalancer`, discovers the ECS service(s)
1953
+ * behind its HTTP `forward` listeners, boots their replicas, and stands up a
1954
+ * local front-door on each listener port that round-robins across the replicas.
1955
+ * The symmetric ALB counterpart of `start-api`.
1956
+ */
1957
+ function createLocalStartAlbCommand(opts = {}) {
1958
+ setEmbedConfig(opts.embedConfig);
1959
+ return addCommonEcsServiceOptions(new Command("start-alb").description("Run an Application Load Balancer locally: name the ALB, and cdk-local boots the ECS service(s) behind its listeners and stands up a local front-door on each listener port that round-robins across the running replicas and routes its listener rules across the backing services — a stable host endpoint, like behind a real load balancer. The symmetric ALB counterpart of `start-api`. Each <target> accepts a CDK display path (MyStack/MyAlb) or stack-qualified logical ID; single-stack apps may omit the stack prefix. Supports HTTP and HTTPS listeners (TLS terminated locally with --tls-cert/--tls-key or an auto-generated self-signed cert); all six ALB rule-condition fields (path-pattern / host-header / http-header / http-request-method / query-string / source-ip); forward (single and weighted), redirect, and fixed-response actions; and ECS or Lambda targets (a Lambda target group is invoked locally via the Lambda RIE). authenticate-cognito / authenticate-oidc actions enforce a local Bearer-JWT check (or AWSELBAuthSessionCookie pass-through) against the same JWKS / OIDC discovery URL the deployed ALB would; use --bearer-token <jwt> to inject a default token or --no-verify-auth to disable the guard. Omit <targets> in an interactive terminal to multi-select the load balancers from a list.").argument("[targets...]", "One or more CDK display paths or stack-qualified logical IDs of the AWS::ElasticLoadBalancingV2::LoadBalancer resources to run (omit to multi-select interactively in a TTY)").addOption(new Option("--lb-port <listenerPort=hostPort...>", "Bind the local front-door on a specific host port (e.g. 80=8080); repeatable. Default: host port == ALB listener port. Use this on macOS to remap a privileged listener port (< 1024) to a non-privileged host port.")).addOption(new Option("--tls-cert <path>", "PEM-encoded server certificate for HTTPS front-door listeners. Must be set together with --tls-key. Omit both flags to auto-generate a self-signed cert (cached under $XDG_CACHE_HOME/cdk-local/alb-https/, default ~/.cache/cdk-local/alb-https/); requires openssl on PATH. The deployed Listener Certificates[] are NOT fetched (ACM private keys are not retrievable by design). The auto-generated cert lists DNS:localhost,IP:127.0.0.1 as SubjectAltName, so a client validating a non-loopback --container-host will fail the SAN check — pass --tls-cert / --tls-key with a SAN covering that host instead.")).addOption(new Option("--tls-key <path>", "PEM-encoded server private key matching --tls-cert. Must be set together with --tls-cert.")).addOption(new Option("--no-verify-auth", "Disable local enforcement of authenticate-cognito / authenticate-oidc actions. Every request is served as if the auth check passed. Useful for local dev where you do not want to mint a Bearer token at all.")).addOption(new Option("--bearer-token <jwt>", "Default Bearer JWT injected as Authorization: Bearer <jwt> when the inbound request has none. Verified against the same JWKS / OIDC discovery URL the deployed ALB would (signature + iss + aud + exp). Local-dev convenience; cookie pass-through (AWSELBAuthSessionCookie-*) also works.")).action(withErrorHandling(async (targets, options) => {
1960
+ await runEcsServiceEmulator(targets, options, albStrategy(options), opts.extraStateProviders);
1961
+ })));
1962
+ }
1963
+
1964
+ //#endregion
1965
+ //#region src/cli/commands/local-list.ts
1966
+ async function localListCommand(options) {
1967
+ const logger = getLogger();
1968
+ if (options.verbose) logger.setLevel("debug");
1969
+ warnIfDeprecatedRegion(options);
1970
+ await applyRoleArnIfSet({
1971
+ roleArn: options.roleArn,
1972
+ region: void 0
1973
+ });
1974
+ const appCmd = resolveApp(options.app);
1975
+ if (!appCmd) throw new Error(`No CDK app specified. Pass --app, set ${getEmbedConfig().envPrefix}_APP, or add "app" to cdk.json.`);
1976
+ process.stderr.write("Synthesizing CDK app...\n");
1977
+ const synthesizer = new Synthesizer();
1978
+ const context = parseContextOptions(options.context);
1979
+ const synthOpts = {
1980
+ app: appCmd,
1981
+ output: options.output,
1982
+ ...options.profile && { profile: options.profile },
1983
+ ...Object.keys(context).length > 0 && { context }
1984
+ };
1985
+ const { stacks } = await synthesizer.synthesize(synthOpts);
1986
+ const listing = listTargets(stacks);
1987
+ process.stdout.write(`${formatTargetListing(listing, getEmbedConfig().cliName, { long: options.long })}\n`);
1988
+ }
1989
+ /**
1990
+ * Render a {@link TargetListing} as the grouped text list `cdkl list`
1991
+ * prints. Each non-empty category is preceded by a blank line and a
1992
+ * header naming the command that runs it, then one target per line by
1993
+ * CDK display path. With {@link FormatTargetListingOptions.long}, each
1994
+ * target's stack-qualified logical ID is printed on an indented line
1995
+ * beneath it. Exported so a unit test can assert the output shape
1996
+ * without running synthesis.
1997
+ */
1998
+ function formatTargetListing(listing, cliName, options = {}) {
1999
+ if (countTargets(listing) === 0) return `No runnable targets (Lambda functions, APIs, ECS services / tasks, AgentCore Runtimes, load balancers) found in this CDK app.`;
2000
+ const long = options.long ?? false;
2001
+ return "\n" + [
2002
+ formatSection("Lambda Functions", `${cliName} invoke <target>`, listing.lambdas, long),
2003
+ formatSection("APIs", `${cliName} start-api [target...]`, listing.apis, long),
2004
+ formatSection("ECS Services", `${cliName} start-service <target...>`, listing.ecsServices, long),
2005
+ formatSection("ECS Task Definitions", `${cliName} run-task <target>`, listing.ecsTaskDefinitions, long),
2006
+ formatSection("AgentCore Runtimes", `${cliName} invoke-agentcore <target>`, listing.agentCoreRuntimes, long),
2007
+ formatSection("Application Load Balancers", `${cliName} start-alb <target...>`, listing.loadBalancers, long)
2008
+ ].filter((lines) => lines.length > 0).map((lines) => lines.join("\n")).join("\n\n");
2009
+ }
2010
+ function formatSection(title, command, entries, long) {
2011
+ if (entries.length === 0) return [];
2012
+ const lines = [`${title} -> ${command}`];
2013
+ for (const entry of entries) {
2014
+ const primary = entry.displayPath ?? entry.qualifiedId;
2015
+ lines.push(entry.kind ? ` ${primary} (${entry.kind})` : ` ${primary}`);
2016
+ if (long && entry.displayPath) lines.push(` ${entry.qualifiedId}`);
2017
+ }
2018
+ return lines;
2019
+ }
2020
+ function createLocalListCommand(opts = {}) {
2021
+ setEmbedConfig(opts.embedConfig);
2022
+ const cmd = new Command("list").alias("ls").description("List the runnable targets in the synthesized CDK app, grouped by the command that runs them: Lambda functions (invoke), API Gateway REST v1 / HTTP v2 / Function URL / WebSocket surfaces (start-api), ECS services (start-service), ECS task definitions (run-task), AgentCore Runtimes (invoke-agentcore), and Application Load Balancers (start-alb). Each target is shown by its CDK display path; pass -l to also print the stack-qualified logical ID. Tip: you usually do not need to copy these — just run the command (e.g. `invoke`) with no target in a terminal and pick from the list.").addOption(new Option("-l, --long", "Also print each target's stack-qualified logical ID (<Stack>:<LogicalId>) beneath it").default(false)).action(withErrorHandling(async (options) => {
2023
+ await localListCommand(options);
2024
+ }));
2025
+ [
2026
+ ...commonOptions(),
2027
+ ...appOptions(),
2028
+ ...contextOptions
2029
+ ].forEach((opt) => cmd.addOption(opt));
2030
+ cmd.addOption(deprecatedRegionOption);
2031
+ return cmd;
2032
+ }
2033
+
2034
+ //#endregion
2035
+ export { createLocalRunTaskCommand as a, createLocalStartServiceCommand as i, formatTargetListing as n, createLocalInvokeAgentCoreCommand as o, createLocalStartAlbCommand as r, createLocalInvokeCommand as s, createLocalListCommand as t };
2036
+ //# sourceMappingURL=local-list-BPbd4EMs.js.map