cdk-local 0.64.0 → 0.65.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.
@@ -7799,6 +7799,641 @@ async function writeProfileCredentialsFile(profileName, creds) {
7799
7799
  };
7800
7800
  }
7801
7801
 
7802
+ //#endregion
7803
+ //#region src/cli/commands/local-invoke.ts
7804
+ /**
7805
+ * `cdkl invoke <target>` — run a Lambda function locally inside a
7806
+ * Docker container that bundles the AWS Lambda Runtime Interface
7807
+ * Emulator (RIE). Modeled on `sam local invoke` but reusing cdk-local's
7808
+ * synthesis / asset / construct-path plumbing.
7809
+ */
7810
+ async function localInvokeCommand(target, options, extraStateProviders) {
7811
+ const logger = getLogger();
7812
+ if (options.verbose) logger.setLevel("debug");
7813
+ warnIfDeprecatedRegion(options);
7814
+ let imagePlan;
7815
+ let containerId;
7816
+ let stopLogs;
7817
+ let sigintHandler;
7818
+ let profileCredsFile;
7819
+ /**
7820
+ * Unified cleanup for both the success / failure unwind path AND the
7821
+ * SIGINT handler.
7822
+ */
7823
+ const cleanup = singleFlight(async () => {
7824
+ if (stopLogs) try {
7825
+ stopLogs();
7826
+ } catch (err) {
7827
+ getLogger().debug(`streamLogs stop failed: ${err instanceof Error ? err.message : String(err)}`);
7828
+ }
7829
+ if (containerId) try {
7830
+ await removeContainer(containerId);
7831
+ } catch (err) {
7832
+ getLogger().debug(`removeContainer(${containerId}) failed: ${err instanceof Error ? err.message : String(err)}`);
7833
+ }
7834
+ if (imagePlan?.inlineTmpDir) try {
7835
+ rmSync(imagePlan.inlineTmpDir, {
7836
+ recursive: true,
7837
+ force: true
7838
+ });
7839
+ } catch (err) {
7840
+ getLogger().debug(`Failed to remove inline-code tmpdir ${imagePlan.inlineTmpDir}: ${err instanceof Error ? err.message : String(err)}`);
7841
+ }
7842
+ if (imagePlan?.layersTmpDir) try {
7843
+ rmSync(imagePlan.layersTmpDir, {
7844
+ recursive: true,
7845
+ force: true
7846
+ });
7847
+ } catch (err) {
7848
+ getLogger().debug(`Failed to remove merged-layers tmpdir ${imagePlan.layersTmpDir}: ${err instanceof Error ? err.message : String(err)}`);
7849
+ }
7850
+ if (imagePlan?.layerArnTmpDirs) for (const dir of imagePlan.layerArnTmpDirs) try {
7851
+ rmSync(dir, {
7852
+ recursive: true,
7853
+ force: true
7854
+ });
7855
+ } catch (err) {
7856
+ getLogger().debug(`Failed to remove ARN-layer tmpdir ${dir}: ${err instanceof Error ? err.message : String(err)}`);
7857
+ }
7858
+ if (profileCredsFile) try {
7859
+ await profileCredsFile.dispose();
7860
+ } catch (err) {
7861
+ getLogger().debug(`Failed to remove profile credentials tmpdir ${profileCredsFile.hostPath}: ${err instanceof Error ? err.message : String(err)}`);
7862
+ }
7863
+ }, (err) => {
7864
+ getLogger().debug(`cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
7865
+ });
7866
+ try {
7867
+ await applyRoleArnIfSet({
7868
+ roleArn: options.roleArn,
7869
+ region: options.region
7870
+ });
7871
+ await ensureDockerAvailable();
7872
+ const profileCredentials = options.profile ? await resolveProfileCredentials$1(options.profile) : void 0;
7873
+ if (options.profile && profileCredentials) profileCredsFile = await writeProfileCredentialsFile(options.profile, profileCredentials);
7874
+ const appCmd = resolveApp(options.app);
7875
+ if (!appCmd) throw new Error(`No CDK app specified. Pass --app, set ${getEmbedConfig().envPrefix}_APP, or add "app" to cdk.json.`);
7876
+ logger.info("Synthesizing CDK app...");
7877
+ const synthesizer = new Synthesizer();
7878
+ const context = parseContextOptions(options.context);
7879
+ const synthOpts = {
7880
+ app: appCmd,
7881
+ output: options.output,
7882
+ ...options.region && { region: options.region },
7883
+ ...options.profile && { profile: options.profile },
7884
+ ...Object.keys(context).length > 0 && { context }
7885
+ };
7886
+ const { stacks } = await synthesizer.synthesize(synthOpts);
7887
+ const lambda = resolveLambdaTarget(await resolveSingleTarget(target, {
7888
+ entries: listTargets(stacks).lambdas,
7889
+ message: "Select a Lambda function to invoke",
7890
+ noun: "Lambda functions",
7891
+ 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")
7892
+ }), stacks);
7893
+ const targetLabel = lambda.kind === "zip" ? lambda.runtime : "container image";
7894
+ logger.info(`Target: ${lambda.stack.stackName}/${lambda.logicalId} (${targetLabel})`);
7895
+ imagePlan = await resolveImagePlan(lambda, options);
7896
+ let stateAudit;
7897
+ let templateEnv = getTemplateEnv$1(lambda.resource);
7898
+ let stateForRoleHint;
7899
+ const stateProvider = createLocalStateProvider(options, lambda.stack.stackName, await resolveCfnFallbackRegion(options, lambda.stack.region), extraStateProviders);
7900
+ if (stateProvider) try {
7901
+ const loaded = await stateProvider.load(lambda.stack.stackName, lambda.stack.region);
7902
+ if (loaded) {
7903
+ stateForRoleHint = {
7904
+ version: 1,
7905
+ stackName: lambda.stack.stackName,
7906
+ resources: loaded.resources,
7907
+ outputs: loaded.outputs,
7908
+ lastModified: 0
7909
+ };
7910
+ const subContext = {
7911
+ resources: loaded.resources,
7912
+ consumerRegion: loaded.region
7913
+ };
7914
+ if (envHasIntrinsicValue$1(templateEnv)) {
7915
+ const pseudo = await resolvePseudoParametersForInvoke(lambda.stack.region, options);
7916
+ if (pseudo) subContext.pseudoParameters = pseudo;
7917
+ }
7918
+ if (envHasIntrinsicValue$1(templateEnv) && stateProvider.resolveTemplateSsmParameters) {
7919
+ const ssmParams = await stateProvider.resolveTemplateSsmParameters(lambda.stack.template);
7920
+ if (Object.keys(ssmParams.values).length > 0) subContext.parameters = ssmParams.values;
7921
+ if (ssmParams.secureStringLogicalIds.length > 0) subContext.sensitiveParameters = new Set(ssmParams.secureStringLogicalIds);
7922
+ }
7923
+ if (envHasCrossStackIntrinsic(templateEnv)) {
7924
+ const resolver = await stateProvider.buildCrossStackResolver(loaded.region);
7925
+ if (resolver) subContext.crossStackResolver = resolver;
7926
+ }
7927
+ const { env, audit } = await substituteEnvVarsFromStateAsync(templateEnv, subContext);
7928
+ templateEnv = env;
7929
+ const label = stateProvider.label;
7930
+ for (const key of audit.resolvedKeys) logger.debug(`${label}: substituted env var ${key}`);
7931
+ let unresolved = audit.unresolved;
7932
+ const resolvedKeys = [...audit.resolvedKeys];
7933
+ if (unresolved.length > 0 && stateProvider.resolveDeployedFunctionEnv) {
7934
+ const physicalId = loaded.resources[lambda.logicalId]?.physicalId;
7935
+ if (physicalId) {
7936
+ const deployedEnv = await stateProvider.resolveDeployedFunctionEnv(physicalId);
7937
+ const fb = applyDeployedEnvFallback(templateEnv, unresolved, deployedEnv);
7938
+ templateEnv = fb.env;
7939
+ unresolved = fb.stillUnresolved;
7940
+ for (const key of fb.filled) {
7941
+ resolvedKeys.push(key);
7942
+ logger.debug(`${label}: filled env var ${key} from deployed function config`);
7943
+ }
7944
+ }
7945
+ }
7946
+ stateAudit = {
7947
+ resolvedKeys,
7948
+ unresolved,
7949
+ sensitiveKeys: audit.sensitiveKeys
7950
+ };
7951
+ 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.`);
7952
+ }
7953
+ } catch (err) {
7954
+ stateProvider.dispose();
7955
+ throw err;
7956
+ }
7957
+ const overrides = readEnvOverridesFile$4(options.envVars);
7958
+ const lambdaCdkPath = readCdkPathOrUndefined(lambda.resource);
7959
+ const envResult = resolveEnvVars(lambda.logicalId, lambdaCdkPath, templateEnv, overrides);
7960
+ for (const key of envResult.unresolved) {
7961
+ if (stateAudit && stateAudit.unresolved.some((u) => u.key === key)) continue;
7962
+ const overrideKeyExample = lambdaCdkPath?.replace(/\/Resource$/, "") ?? lambda.logicalId;
7963
+ 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.`);
7964
+ }
7965
+ let resolvedAssumeRoleArn;
7966
+ try {
7967
+ resolvedAssumeRoleArn = await resolveAssumeRoleArnForLambda(options.assumeRole, stateForRoleHint, stateProvider, lambda.logicalId);
7968
+ } finally {
7969
+ stateProvider?.dispose();
7970
+ }
7971
+ if (options.assumeRole === void 0 && stateForRoleHint) suggestAssumeRoleFromState(stateForRoleHint, lambda.logicalId);
7972
+ const event = await readEvent$1(options);
7973
+ const dockerEnv = {
7974
+ AWS_LAMBDA_FUNCTION_NAME: lambda.logicalId,
7975
+ AWS_LAMBDA_FUNCTION_MEMORY_SIZE: String(lambda.memoryMb),
7976
+ AWS_LAMBDA_FUNCTION_TIMEOUT: String(lambda.timeoutSec),
7977
+ AWS_LAMBDA_FUNCTION_VERSION: "$LATEST",
7978
+ AWS_LAMBDA_LOG_GROUP_NAME: `/aws/lambda/${lambda.logicalId}`,
7979
+ AWS_LAMBDA_LOG_STREAM_NAME: "local",
7980
+ ...envResult.resolved
7981
+ };
7982
+ let assumeSucceeded = false;
7983
+ if (resolvedAssumeRoleArn) {
7984
+ const stsRegion = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"];
7985
+ try {
7986
+ const creds = await assumeLambdaExecutionRole$1(resolvedAssumeRoleArn, stsRegion);
7987
+ dockerEnv["AWS_ACCESS_KEY_ID"] = creds.accessKeyId;
7988
+ dockerEnv["AWS_SECRET_ACCESS_KEY"] = creds.secretAccessKey;
7989
+ dockerEnv["AWS_SESSION_TOKEN"] = creds.sessionToken;
7990
+ if (stsRegion) dockerEnv["AWS_REGION"] = stsRegion;
7991
+ assumeSucceeded = true;
7992
+ } catch (err) {
7993
+ const reason = err instanceof Error ? err.message : String(err);
7994
+ logger.warn(`--assume-role: STS AssumeRole(${resolvedAssumeRoleArn}) failed: ${reason}. Falling back to the developer's shell credentials.`);
7995
+ }
7996
+ }
7997
+ if (!assumeSucceeded) {
7998
+ forwardAwsEnv$3(dockerEnv);
7999
+ applyProfileCredentialsOverlay(dockerEnv, profileCredentials, false);
8000
+ if (profileCredsFile) {
8001
+ dockerEnv["AWS_SHARED_CREDENTIALS_FILE"] = profileCredsFile.containerPath;
8002
+ dockerEnv["AWS_PROFILE"] = profileCredsFile.profileName;
8003
+ }
8004
+ }
8005
+ let debugPort;
8006
+ if (options.debugPort) {
8007
+ debugPort = Number(options.debugPort);
8008
+ if (!Number.isInteger(debugPort) || debugPort <= 0 || debugPort > 65535) throw new Error(`--debug-port must be an integer in 1..65535, got '${options.debugPort}'`);
8009
+ dockerEnv["NODE_OPTIONS"] = `--inspect-brk=0.0.0.0:${debugPort}`;
8010
+ 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.");
8011
+ }
8012
+ const hostPort = await pickFreePort();
8013
+ const containerHost = options.containerHost;
8014
+ if (lambda.layers.length > 0) logger.info(`Mounting ${lambda.layers.length} Lambda layer${lambda.layers.length === 1 ? "" : "s"} at /opt`);
8015
+ logger.info(`Starting container (image=${imagePlan.image}, port=${hostPort})...`);
8016
+ const extraMountsWithProfile = profileCredsFile ? [...imagePlan.extraMounts ?? [], {
8017
+ hostPath: profileCredsFile.hostPath,
8018
+ containerPath: profileCredsFile.containerPath,
8019
+ readOnly: true
8020
+ }] : imagePlan.extraMounts;
8021
+ containerId = await runDetached({
8022
+ image: imagePlan.image,
8023
+ mounts: imagePlan.mounts,
8024
+ extraMounts: extraMountsWithProfile,
8025
+ env: dockerEnv,
8026
+ ...stateAudit && stateAudit.sensitiveKeys.length > 0 && { sensitiveEnvKeys: new Set(stateAudit.sensitiveKeys) },
8027
+ cmd: imagePlan.cmd,
8028
+ hostPort,
8029
+ host: containerHost,
8030
+ ...debugPort !== void 0 && { debugPort },
8031
+ ...imagePlan.platform !== void 0 && { platform: imagePlan.platform },
8032
+ ...imagePlan.entryPoint !== void 0 && { entryPoint: imagePlan.entryPoint },
8033
+ ...imagePlan.workingDir !== void 0 && { workingDir: imagePlan.workingDir },
8034
+ ...imagePlan.tmpfs !== void 0 && { tmpfs: imagePlan.tmpfs }
8035
+ });
8036
+ stopLogs = streamLogs(containerId);
8037
+ sigintHandler = () => {
8038
+ cleanup().then(() => {
8039
+ process.exit(130);
8040
+ });
8041
+ };
8042
+ process.on("SIGINT", sigintHandler);
8043
+ await waitForRieReady(containerHost, hostPort, 5e3);
8044
+ const result = await invokeRie(containerHost, hostPort, event, Math.max(3e4, lambda.timeoutSec * 2 * 1e3));
8045
+ await new Promise((resolveDelay) => setTimeout(resolveDelay, 250));
8046
+ process.stdout.write(`${result.raw}\n`);
8047
+ } finally {
8048
+ if (sigintHandler) process.off("SIGINT", sigintHandler);
8049
+ await cleanup();
8050
+ }
8051
+ }
8052
+ async function resolveImagePlan(lambda, options) {
8053
+ if (lambda.kind === "zip") return resolveZipImagePlan$1(lambda, options);
8054
+ return resolveContainerImagePlan$1(lambda, options);
8055
+ }
8056
+ async function resolveZipImagePlan$1(lambda, options) {
8057
+ let inlineTmpDir;
8058
+ let codeDir = lambda.codePath;
8059
+ if (codeDir === null) {
8060
+ inlineTmpDir = materializeInlineCode$2(lambda.handler, lambda.inlineCode ?? "", resolveRuntimeFileExtension(lambda.runtime));
8061
+ codeDir = inlineTmpDir;
8062
+ }
8063
+ const image = resolveRuntimeImage(lambda.runtime);
8064
+ await pullImage(image, options.pull === false);
8065
+ const layerPlan = await materializeLambdaLayersIncludingArns(lambda.layers, options);
8066
+ const containerCodePath = resolveRuntimeCodeMountPath(lambda.runtime);
8067
+ const tmpfs = resolveTmpfsForLambda(lambda);
8068
+ return {
8069
+ image,
8070
+ mounts: [{
8071
+ hostPath: codeDir,
8072
+ containerPath: containerCodePath,
8073
+ readOnly: true
8074
+ }],
8075
+ extraMounts: layerPlan.mount ? [layerPlan.mount] : [],
8076
+ cmd: [lambda.handler],
8077
+ ...inlineTmpDir !== void 0 && { inlineTmpDir },
8078
+ ...layerPlan.tmpDir !== void 0 && { layersTmpDir: layerPlan.tmpDir },
8079
+ ...layerPlan.extraTmpDirs.length > 0 && { layerArnTmpDirs: layerPlan.extraTmpDirs },
8080
+ ...tmpfs !== void 0 && { tmpfs }
8081
+ };
8082
+ }
8083
+ async function materializeLambdaLayersIncludingArns(layers, options) {
8084
+ const extraTmpDirs = [];
8085
+ const flat = [];
8086
+ for (const layer of layers) {
8087
+ if (layer.kind === "asset") {
8088
+ flat.push({
8089
+ logicalId: layer.logicalId,
8090
+ assetPath: layer.assetPath
8091
+ });
8092
+ continue;
8093
+ }
8094
+ const dir = await materializeLayerFromArn(layer, { ...options.layerRoleArn !== void 0 && { roleArn: options.layerRoleArn } });
8095
+ extraTmpDirs.push(dir);
8096
+ flat.push({
8097
+ logicalId: layer.arn,
8098
+ assetPath: dir
8099
+ });
8100
+ }
8101
+ return {
8102
+ ...materializeLambdaLayers$1(flat),
8103
+ extraTmpDirs
8104
+ };
8105
+ }
8106
+ function resolveTmpfsForLambda(lambda) {
8107
+ if (lambda.ephemeralStorageMb === void 0) return void 0;
8108
+ const logger = getLogger();
8109
+ if (lambda.kind === "image") logger.info(`Lambda ${lambda.logicalId}: capping /tmp at ${lambda.ephemeralStorageMb} MiB via --tmpfs (overlays any base-image /tmp content)`);
8110
+ else logger.debug(`Lambda ${lambda.logicalId}: applying EphemeralStorage cap via --tmpfs /tmp:size=${lambda.ephemeralStorageMb}m`);
8111
+ return {
8112
+ target: "/tmp",
8113
+ sizeMb: lambda.ephemeralStorageMb
8114
+ };
8115
+ }
8116
+ function materializeLambdaLayers$1(layers) {
8117
+ if (layers.length === 0) return {};
8118
+ if (layers.length === 1) return { mount: {
8119
+ hostPath: layers[0].assetPath,
8120
+ containerPath: "/opt",
8121
+ readOnly: true
8122
+ } };
8123
+ const tmpDir = mkdtempSync(path.join(tmpdir(), `${getEmbedConfig().resourceNamePrefix}-invoke-layers-`));
8124
+ for (const layer of layers) cpSync(layer.assetPath, tmpDir, {
8125
+ recursive: true,
8126
+ force: true
8127
+ });
8128
+ return {
8129
+ mount: {
8130
+ hostPath: tmpDir,
8131
+ containerPath: "/opt",
8132
+ readOnly: true
8133
+ },
8134
+ tmpDir
8135
+ };
8136
+ }
8137
+ async function resolveContainerImagePlan$1(lambda, options) {
8138
+ const logger = getLogger();
8139
+ const platform = architectureToPlatform(lambda.architecture);
8140
+ const localBuild = await resolveLocalBuildPlan$1(lambda);
8141
+ let imageRef;
8142
+ if (localBuild) imageRef = await buildContainerImage(localBuild.asset, localBuild.cdkOutDir, {
8143
+ architecture: lambda.architecture,
8144
+ noBuild: options.build === false
8145
+ });
8146
+ else {
8147
+ 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.`);
8148
+ logger.info(`No matching cdk.out asset for ${lambda.imageUri}; falling back to ECR pull (same-acct/region only)...`);
8149
+ imageRef = await pullEcrImage(lambda.imageUri, {
8150
+ skipPull: options.pull === false,
8151
+ ...options.region !== void 0 && { region: options.region },
8152
+ ...options.ecrRoleArn !== void 0 && { ecrRoleArn: options.ecrRoleArn },
8153
+ ...options.profile !== void 0 && { profile: options.profile }
8154
+ });
8155
+ }
8156
+ const tmpfs = resolveTmpfsForLambda(lambda);
8157
+ return {
8158
+ image: imageRef,
8159
+ mounts: [],
8160
+ extraMounts: [],
8161
+ cmd: lambda.imageConfig.command ?? [],
8162
+ platform,
8163
+ ...lambda.imageConfig.entryPoint && lambda.imageConfig.entryPoint.length > 0 && { entryPoint: lambda.imageConfig.entryPoint },
8164
+ ...lambda.imageConfig.workingDirectory !== void 0 && { workingDir: lambda.imageConfig.workingDirectory },
8165
+ ...tmpfs !== void 0 && { tmpfs }
8166
+ };
8167
+ }
8168
+ async function resolveLocalBuildPlan$1(lambda) {
8169
+ const manifestPath = lambda.stack.assetManifestPath;
8170
+ if (!manifestPath) return void 0;
8171
+ const cdkOutDir = dirname(manifestPath);
8172
+ const manifest = await new AssetManifestLoader().loadManifest(cdkOutDir, lambda.stack.stackName);
8173
+ if (!manifest) return void 0;
8174
+ const entry = getDockerImageBySourceHash(manifest, lambda.imageUri);
8175
+ if (!entry) return void 0;
8176
+ return {
8177
+ asset: entry.asset,
8178
+ cdkOutDir
8179
+ };
8180
+ }
8181
+ function envHasIntrinsicValue$1(templateEnv) {
8182
+ if (!templateEnv) return false;
8183
+ for (const v of Object.values(templateEnv)) {
8184
+ if (v === void 0 || v === null) continue;
8185
+ if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") continue;
8186
+ return true;
8187
+ }
8188
+ return false;
8189
+ }
8190
+ function envHasCrossStackIntrinsic(templateEnv) {
8191
+ if (!templateEnv) return false;
8192
+ for (const v of Object.values(templateEnv)) {
8193
+ if (!v || typeof v !== "object") continue;
8194
+ const obj = v;
8195
+ if ("Fn::ImportValue" in obj || "Fn::GetStackOutput" in obj) return true;
8196
+ }
8197
+ return false;
8198
+ }
8199
+ async function resolvePseudoParametersForInvoke(stackRegion, options) {
8200
+ const logger = getLogger();
8201
+ const region = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? stackRegion;
8202
+ 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.`);
8203
+ let accountId;
8204
+ try {
8205
+ const { STSClient, GetCallerIdentityCommand } = await import("@aws-sdk/client-sts");
8206
+ const sts = new STSClient({ ...region && { region } });
8207
+ try {
8208
+ accountId = (await sts.send(new GetCallerIdentityCommand({}))).Account;
8209
+ } finally {
8210
+ sts.destroy();
8211
+ }
8212
+ } catch (err) {
8213
+ 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.`);
8214
+ }
8215
+ const partitionAndSuffix = region ? derivePartitionAndUrlSuffix(region) : void 0;
8216
+ const bag = {
8217
+ ...accountId !== void 0 && { accountId },
8218
+ ...region !== void 0 && { region },
8219
+ ...partitionAndSuffix && {
8220
+ partition: partitionAndSuffix.partition,
8221
+ urlSuffix: partitionAndSuffix.urlSuffix
8222
+ }
8223
+ };
8224
+ return Object.keys(bag).length === 0 ? void 0 : bag;
8225
+ }
8226
+ function getTemplateEnv$1(resource) {
8227
+ const env = (resource.Properties ?? {})["Environment"];
8228
+ if (!env || typeof env !== "object") return void 0;
8229
+ const vars = env["Variables"];
8230
+ if (!vars || typeof vars !== "object") return void 0;
8231
+ return vars;
8232
+ }
8233
+ function readEnvOverridesFile$4(filePath) {
8234
+ if (!filePath) return void 0;
8235
+ let raw;
8236
+ try {
8237
+ raw = readFileSync(filePath, "utf-8");
8238
+ } catch (err) {
8239
+ throw new Error(`Failed to read --env-vars file '${filePath}': ${err instanceof Error ? err.message : String(err)}`);
8240
+ }
8241
+ let parsed;
8242
+ try {
8243
+ parsed = JSON.parse(raw);
8244
+ } catch (err) {
8245
+ throw new Error(`Failed to parse --env-vars file '${filePath}' as JSON: ${err instanceof Error ? err.message : String(err)}`);
8246
+ }
8247
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`--env-vars file '${filePath}' must contain a JSON object at the top level.`);
8248
+ return parsed;
8249
+ }
8250
+ async function readEvent$1(options) {
8251
+ if (options.event && options.eventStdin) throw new Error("--event and --event-stdin are mutually exclusive.");
8252
+ if (options.eventStdin) return parseEvent$1(await readStdin$1(), "<stdin>");
8253
+ if (options.event) return parseEvent$1(readFileSync(options.event, "utf-8"), options.event);
8254
+ return {};
8255
+ }
8256
+ function parseEvent$1(raw, source) {
8257
+ try {
8258
+ return JSON.parse(raw);
8259
+ } catch (err) {
8260
+ throw new Error(`Failed to parse event payload from ${source} as JSON: ${err instanceof Error ? err.message : String(err)}`);
8261
+ }
8262
+ }
8263
+ async function readStdin$1() {
8264
+ const chunks = [];
8265
+ for await (const chunk of process.stdin) chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
8266
+ return Buffer.concat(chunks).toString("utf-8");
8267
+ }
8268
+ async function assumeLambdaExecutionRole$1(roleArn, region) {
8269
+ const { STSClient, AssumeRoleCommand } = await import("@aws-sdk/client-sts");
8270
+ const sts = new STSClient({ ...region && { region } });
8271
+ try {
8272
+ const creds = (await sts.send(new AssumeRoleCommand({
8273
+ RoleArn: roleArn,
8274
+ RoleSessionName: `${getEmbedConfig().resourceNamePrefix}-invoke-${Date.now()}`,
8275
+ DurationSeconds: 3600
8276
+ }))).Credentials;
8277
+ if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) throw new Error(`AssumeRole(${roleArn}) returned no usable credentials.`);
8278
+ return {
8279
+ accessKeyId: creds.AccessKeyId,
8280
+ secretAccessKey: creds.SecretAccessKey,
8281
+ sessionToken: creds.SessionToken
8282
+ };
8283
+ } finally {
8284
+ sts.destroy();
8285
+ }
8286
+ }
8287
+ function forwardAwsEnv$3(env) {
8288
+ for (const key of [
8289
+ "AWS_ACCESS_KEY_ID",
8290
+ "AWS_SECRET_ACCESS_KEY",
8291
+ "AWS_SESSION_TOKEN",
8292
+ "AWS_REGION",
8293
+ "AWS_DEFAULT_REGION"
8294
+ ]) {
8295
+ const value = process.env[key];
8296
+ if (value !== void 0) env[key] = value;
8297
+ }
8298
+ }
8299
+ /**
8300
+ * Resolve `--profile <p>` to a concrete credential set. Mirrors the
8301
+ * helper in `local-start-api.ts`.
8302
+ */
8303
+ async function resolveProfileCredentials$1(profile) {
8304
+ const { STSClient } = await import("@aws-sdk/client-sts");
8305
+ const sts = new STSClient({ profile });
8306
+ try {
8307
+ const credsProvider = sts.config.credentials;
8308
+ const creds = typeof credsProvider === "function" ? await credsProvider() : credsProvider;
8309
+ 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.");
8310
+ return {
8311
+ accessKeyId: creds.accessKeyId,
8312
+ secretAccessKey: creds.secretAccessKey,
8313
+ ...creds.sessionToken && { sessionToken: creds.sessionToken }
8314
+ };
8315
+ } finally {
8316
+ sts.destroy();
8317
+ }
8318
+ }
8319
+ function applyProfileCredentialsOverlay(env, profileCreds, assumeRoleActive) {
8320
+ if (!profileCreds) return;
8321
+ if (assumeRoleActive) return;
8322
+ env["AWS_ACCESS_KEY_ID"] = profileCreds.accessKeyId;
8323
+ env["AWS_SECRET_ACCESS_KEY"] = profileCreds.secretAccessKey;
8324
+ if (profileCreds.sessionToken) env["AWS_SESSION_TOKEN"] = profileCreds.sessionToken;
8325
+ else delete env["AWS_SESSION_TOKEN"];
8326
+ }
8327
+ function materializeInlineCode$2(handler, source, fileExtension) {
8328
+ const lastDot = handler.lastIndexOf(".");
8329
+ if (lastDot <= 0) throw new Error(`Handler '${handler}' is malformed: expected '<modulePath>.<exportName>'.`);
8330
+ const modulePath = handler.substring(0, lastDot);
8331
+ const dir = mkdtempSync(path.join(tmpdir(), `${getEmbedConfig().resourceNamePrefix}-invoke-`));
8332
+ const filePath = path.join(dir, `${modulePath}${fileExtension}`);
8333
+ mkdirSync(path.dirname(filePath), { recursive: true });
8334
+ writeFileSync(filePath, source, "utf-8");
8335
+ return dir;
8336
+ }
8337
+ function suggestAssumeRoleFromState(state, logicalId) {
8338
+ const logger = getLogger();
8339
+ const roleArn = resolveExecutionRoleArnFromState(state, logicalId);
8340
+ 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.`);
8341
+ }
8342
+ /**
8343
+ * Resolve the role ARN to assume for a Lambda invoke, honoring the three
8344
+ * `--assume-role` forms:
8345
+ *
8346
+ * - `--assume-role <arn>` (explicit) → return `<arn>`.
8347
+ * - `--assume-role` (bare, no value) → resolve from CFn state first;
8348
+ * if that misses, fall back to
8349
+ * `stateProvider.resolveLambdaExecutionRoleArn(<physicalId>)`
8350
+ * (a `lambda:GetFunctionConfiguration` call) so a sibling-stack
8351
+ * execution role still resolves (issue #181 — `ListStackResources`
8352
+ * returns the role's name, not its ARN, so `attributes.Arn` is
8353
+ * empty on the CFn state map and the state-only lookup misses).
8354
+ * - `--assume-role` absent → return undefined (no assume).
8355
+ *
8356
+ * Logs the resolution path (info on success, warn on miss) so the user
8357
+ * can tell why the container did or did not get assumed-role creds.
8358
+ *
8359
+ * Exported for unit testing.
8360
+ */
8361
+ async function resolveAssumeRoleArnForLambda(assumeRole, stateForRoleHint, stateProvider, lambdaLogicalId) {
8362
+ const logger = getLogger();
8363
+ if (typeof assumeRole === "string") return assumeRole;
8364
+ if (assumeRole !== true) return;
8365
+ if (!stateForRoleHint) {
8366
+ 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.");
8367
+ return;
8368
+ }
8369
+ const fromState = resolveExecutionRoleArnFromState(stateForRoleHint, lambdaLogicalId);
8370
+ if (fromState) {
8371
+ logger.info(`--assume-role: auto-resolved execution role from state: ${fromState}`);
8372
+ return fromState;
8373
+ }
8374
+ const fnPhysicalId = stateForRoleHint.resources[lambdaLogicalId]?.physicalId;
8375
+ if (stateProvider?.resolveLambdaExecutionRoleArn && fnPhysicalId) {
8376
+ const liveArn = await stateProvider.resolveLambdaExecutionRoleArn(fnPhysicalId);
8377
+ if (liveArn) {
8378
+ logger.info(`--assume-role: auto-resolved execution role from GetFunctionConfiguration: ${liveArn}`);
8379
+ return liveArn;
8380
+ }
8381
+ }
8382
+ 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.`);
8383
+ }
8384
+ function resolveExecutionRoleArnFromState(state, logicalId, roleProperty = "Role") {
8385
+ const lambda = state.resources[logicalId];
8386
+ if (!lambda) return void 0;
8387
+ const roleRef = lambda.properties?.[roleProperty] ?? lambda.observedProperties?.[roleProperty];
8388
+ if (typeof roleRef === "string" && roleRef.startsWith("arn:")) return roleRef;
8389
+ if (typeof roleRef === "object" && roleRef !== null) {
8390
+ const refLogicalId = pickReferencedLogicalId(roleRef);
8391
+ if (refLogicalId) {
8392
+ const cached = state.resources[refLogicalId]?.attributes?.["Arn"];
8393
+ if (typeof cached === "string" && cached.startsWith("arn:")) return cached;
8394
+ }
8395
+ }
8396
+ }
8397
+ function pickReferencedLogicalId(intrinsic) {
8398
+ if ("Ref" in intrinsic && typeof intrinsic["Ref"] === "string") return intrinsic["Ref"];
8399
+ if ("Fn::GetAtt" in intrinsic) {
8400
+ const arg = intrinsic["Fn::GetAtt"];
8401
+ if (Array.isArray(arg) && typeof arg[0] === "string") return arg[0];
8402
+ if (typeof arg === "string") return arg.split(".")[0];
8403
+ }
8404
+ }
8405
+ function createLocalInvokeCommand(opts = {}) {
8406
+ setEmbedConfig(opts.embedConfig);
8407
+ 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)").action(withErrorHandling(async (target, options) => {
8408
+ await localInvokeCommand(target, options, opts.extraStateProviders);
8409
+ }));
8410
+ addInvokeSpecificOptions(invoke);
8411
+ [
8412
+ ...commonOptions(),
8413
+ ...appOptions(),
8414
+ ...contextOptions
8415
+ ].forEach((option) => invoke.addOption(option));
8416
+ invoke.addOption(deprecatedRegionOption);
8417
+ return invoke;
8418
+ }
8419
+ /**
8420
+ * Register the option block that `cdkl invoke` adds on top of the shared
8421
+ * common / app / context option helpers. Shared between `cdkl invoke` and
8422
+ * any host CLI (e.g. cdkd's `local invoke`) that wraps the single-shot
8423
+ * RIE-backed Lambda runner, so adding or renaming an `invoke`-only flag
8424
+ * here propagates to every embedder without duplicate `.addOption(...)`
8425
+ * blocks.
8426
+ *
8427
+ * Calling order only affects `--help` presentation (Commander parses
8428
+ * insertion-order-independent). The host-CLI convention is host-specific
8429
+ * options first, then this helper, then the shared common / app / context
8430
+ * options — host flags / invoke flags / common flags grouped in three
8431
+ * `--help` clusters. Chainable: returns `cmd`.
8432
+ */
8433
+ function addInvokeSpecificOptions(cmd) {
8434
+ return cmd.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."));
8435
+ }
8436
+
7802
8437
  //#endregion
7803
8438
  //#region src/local/agentcore-code-build.ts
7804
8439
  /**
@@ -15921,7 +16556,7 @@ async function localStartApiCommand(targets, options, extraStateProviders) {
15921
16556
  await ensureDockerAvailable();
15922
16557
  const appCmd = resolveApp(options.app);
15923
16558
  if (!appCmd) throw new Error(`No CDK app specified. Pass --app, set ${getEmbedConfig().envPrefix}_APP, or add "app" to cdk.json.`);
15924
- const overrides = readEnvOverridesFile$1(options.envVars);
16559
+ const overrides = readEnvOverridesFile$3(options.envVars);
15925
16560
  const debugPortBase = options.debugPortBase ? parseDebugPort(options.debugPortBase) : void 0;
15926
16561
  const perLambdaConcurrency = parsePerLambdaConcurrency(options.perLambdaConcurrency);
15927
16562
  const inlineTmpDirs = /* @__PURE__ */ new Set();
@@ -16478,1152 +17113,2009 @@ function deriveSynthStackPrefix(targets) {
16478
17113
  if (slash === -1) return void 0;
16479
17114
  prefixes.add(t.slice(0, slash));
16480
17115
  }
16481
- return prefixes.size === 1 ? [...prefixes][0] : void 0;
17116
+ return prefixes.size === 1 ? [...prefixes][0] : void 0;
17117
+ }
17118
+ /**
17119
+ * Decide whether synth should cover ALL stacks for the given target
17120
+ * subset (item 5). Synth is scoped to one stack only when a single stack
17121
+ * can be pinned; otherwise we synth every stack and the union is filtered
17122
+ * down by `apiFilters` afterwards.
17123
+ *
17124
+ * Returns `true` (synth all) when there ARE explicit targets but no
17125
+ * single stack can be pinned from them — i.e. `--stack` is unset, an
17126
+ * explicit `--from-cfn-stack <name>` is unset, AND
17127
+ * {@link deriveSynthStackPrefix} can't resolve one shared prefix (targets
17128
+ * span stacks, or any target is a bare logical id). Returns `false` when
17129
+ * there are no targets (serve-all already synths everything via the
17130
+ * regular path) or a stack is otherwise pinned.
17131
+ *
17132
+ * @internal exported for unit tests.
17133
+ */
17134
+ function shouldSynthAllStacks(targets, stackPattern, cfnStackFallback) {
17135
+ if (targets.length === 0) return false;
17136
+ if (stackPattern !== void 0 || cfnStackFallback !== void 0) return false;
17137
+ return deriveSynthStackPrefix(targets) === void 0;
17138
+ }
17139
+ /**
17140
+ * Decide whether bare `start-api` should open the pre-selected-all
17141
+ * multi-select picker. The picker runs ONLY when:
17142
+ * - no explicit API subset was named (`apiFilters` empty — neither
17143
+ * positional `<targets...>` nor the deprecated `--api`),
17144
+ * - `--all-stacks` was not passed (that path already serves every API),
17145
+ * - AND stdin/stdout is a TTY.
17146
+ *
17147
+ * In a non-TTY (CI / pipe) this returns `false`, and the caller serves
17148
+ * EVERY discovered API without prompting — start-api's one intentional
17149
+ * asymmetry vs invoke / run-task (which error when bare in a non-TTY),
17150
+ * because start-api legitimately has a "serve all" default.
17151
+ *
17152
+ * Extracted as a pure function (TTY is passed in) so the decision is
17153
+ * unit-testable without booting the server or juggling global TTY state.
17154
+ *
17155
+ * @internal exported for unit tests.
17156
+ */
17157
+ function shouldPromptBareMultiSelect(apiFilters, allStacks, isTty) {
17158
+ return apiFilters.length === 0 && !allStacks && isTty;
17159
+ }
17160
+ /**
17161
+ * Decide whether the `--from-cfn-stack <name>` redundancy tip should
17162
+ * fire for the current invocation. Fires only when:
17163
+ * - `fromCfnStack` is a non-empty STRING (explicit value, not bare `true`)
17164
+ * - exactly ONE stack is routed
17165
+ * - the explicit value equals the routed stack's `stackName`
17166
+ *
17167
+ * Extracted as a pure function so it can be unit-tested without booting
17168
+ * the full server. See improvement A in the start-api UX PR.
17169
+ *
17170
+ * @internal exported for unit tests.
17171
+ */
17172
+ function shouldEmitFromCfnRedundancyTip(fromCfnStack, routedStackNames) {
17173
+ if (typeof fromCfnStack !== "string") return false;
17174
+ if (fromCfnStack.length === 0) return false;
17175
+ if (routedStackNames.length !== 1) return false;
17176
+ return fromCfnStack === routedStackNames[0];
17177
+ }
17178
+ /**
17179
+ * One-shot wrapper around `shouldEmitFromCfnRedundancyTip` for the
17180
+ * `--watch` hot-reload path. `synthesizeAndBuild` re-runs on every
17181
+ * reload firing, so without a gate the tip would re-emit on every
17182
+ * reload — noisy. This helper consults the caller-supplied ref:
17183
+ * - If the predicate fires AND the ref is still `false`, calls `emit`
17184
+ * and flips the ref to `true`.
17185
+ * - On subsequent invocations the ref is `true` and the helper is a
17186
+ * no-op for the rest of the ref's lifetime.
17187
+ * - When the predicate does NOT fire (no `--from-cfn-stack` value /
17188
+ * intentionally-different value / multi-stack run), the ref stays
17189
+ * `false` so a future reload whose synthesized stacks change in a
17190
+ * way that DOES make the value redundant still emits the tip once.
17191
+ *
17192
+ * The ref is owned by `localStartApiCommand` (one per server boot), so
17193
+ * independent server invocations get independent flags.
17194
+ *
17195
+ * @internal exported for unit tests.
17196
+ */
17197
+ function tryEmitFromCfnRedundancyTipOnce(fromCfnStack, routedStackNames, emittedRef, emit) {
17198
+ if (emittedRef.value) return;
17199
+ if (!shouldEmitFromCfnRedundancyTip(fromCfnStack, routedStackNames)) return;
17200
+ const routedStackName = routedStackNames[0];
17201
+ if (routedStackName === void 0) return;
17202
+ emit(routedStackName);
17203
+ emittedRef.value = true;
17204
+ }
17205
+ /**
17206
+ * Distinct, stable list of Lambda logical IDs reachable through any
17207
+ * discovered route OR referenced by a Lambda authorizer attached to one
17208
+ * of those routes. Stable order = first-occurrence order in the routes
17209
+ * list, then any newly-introduced authorizer Lambdas, which keeps the
17210
+ * route-table output deterministic.
17211
+ */
17212
+ function uniqueLambdaIds(routes, routesWithAuth, webSocketApis = []) {
17213
+ const seen = /* @__PURE__ */ new Set();
17214
+ const out = [];
17215
+ for (const r of routes) {
17216
+ if (r.unsupported || r.mockCors || r.serviceIntegration) continue;
17217
+ if (r.lambdaLogicalId.length === 0) continue;
17218
+ if (!seen.has(r.lambdaLogicalId)) {
17219
+ seen.add(r.lambdaLogicalId);
17220
+ out.push(r.lambdaLogicalId);
17221
+ }
17222
+ }
17223
+ for (const entry of routesWithAuth) {
17224
+ if (entry.route.unsupported || entry.route.mockCors || entry.route.serviceIntegration) continue;
17225
+ const auth = entry.authorizer;
17226
+ if (!auth) continue;
17227
+ if (auth.kind === "lambda-token" || auth.kind === "lambda-request") {
17228
+ if (!seen.has(auth.lambdaLogicalId)) {
17229
+ seen.add(auth.lambdaLogicalId);
17230
+ out.push(auth.lambdaLogicalId);
17231
+ }
17232
+ }
17233
+ }
17234
+ for (const api of webSocketApis) for (const r of api.routes) if (!seen.has(r.targetLambdaLogicalId)) {
17235
+ seen.add(r.targetLambdaLogicalId);
17236
+ out.push(r.targetLambdaLogicalId);
17237
+ }
17238
+ return out;
17239
+ }
17240
+ /**
17241
+ * Prefetch the JWKS for every Cognito / JWT authorizer attached to a
17242
+ * discovered route. Failures degrade to pass-through mode (verifier
17243
+ * surfaces a warn line on first hit); we still issue the prefetch so
17244
+ * the warn lands at startup rather than mid-request.
17245
+ */
17246
+ async function prewarmJwks(routesWithAuth, jwksCache) {
17247
+ const urls = /* @__PURE__ */ new Set();
17248
+ for (const entry of routesWithAuth) {
17249
+ const auth = entry.authorizer;
17250
+ if (!auth) continue;
17251
+ if (auth.kind === "cognito") for (const pool of auth.pools) urls.add(buildCognitoJwksUrl(pool.region, pool.userPoolId));
17252
+ else if (auth.kind === "jwt") {
17253
+ const url = auth.region && auth.userPoolId ? buildCognitoJwksUrl(auth.region, auth.userPoolId) : buildJwksUrlFromIssuer(auth.issuer);
17254
+ urls.add(url);
17255
+ }
17256
+ }
17257
+ await Promise.all([...urls].map((u) => jwksCache.fetchAndCache(u)));
17258
+ }
17259
+ /**
17260
+ * Emit a one-line warn for every VPC-config Lambda. The handler still
17261
+ * runs locally, but its container does not get attached to the AWS
17262
+ * VPC's subnets — calls to private RDS / ElastiCache will fail. cdk-local
17263
+ * surfaces this so the developer can pin the unexpected behavior to
17264
+ * the VPC config rather than chasing a "connection refused" rabbit
17265
+ * hole.
17266
+ */
17267
+ function warnVpcConfigLambdas(routesWithAuth, stacks) {
17268
+ const logger = getLogger();
17269
+ const seen = /* @__PURE__ */ new Set();
17270
+ const reachable = [];
17271
+ for (const entry of routesWithAuth) {
17272
+ if (!seen.has(entry.route.lambdaLogicalId)) {
17273
+ seen.add(entry.route.lambdaLogicalId);
17274
+ reachable.push(entry.route.lambdaLogicalId);
17275
+ }
17276
+ const auth = entry.authorizer;
17277
+ if (auth && (auth.kind === "lambda-token" || auth.kind === "lambda-request")) {
17278
+ if (!seen.has(auth.lambdaLogicalId)) {
17279
+ seen.add(auth.lambdaLogicalId);
17280
+ reachable.push(auth.lambdaLogicalId);
17281
+ }
17282
+ }
17283
+ }
17284
+ for (const logicalId of reachable) for (const stack of stacks) {
17285
+ const resource = stack.template.Resources?.[logicalId];
17286
+ if (!resource || resource.Type !== "AWS::Lambda::Function") continue;
17287
+ const vpcConfig = (resource.Properties ?? {})["VpcConfig"];
17288
+ if (vpcConfig && typeof vpcConfig === "object" && Object.keys(vpcConfig).length > 0) logger.warn(`Lambda ${logicalId} has VpcConfig — local container will reach external services via the host's network, NOT through the deployed VPC's NAT/private subnets. Calls to private RDS/ElastiCache will fail.`);
17289
+ break;
17290
+ }
16482
17291
  }
16483
17292
  /**
16484
- * Decide whether synth should cover ALL stacks for the given target
16485
- * subset (item 5). Synth is scoped to one stack only when a single stack
16486
- * can be pinned; otherwise we synth every stack and the union is filtered
16487
- * down by `apiFilters` afterwards.
16488
- *
16489
- * Returns `true` (synth all) when there ARE explicit targets but no
16490
- * single stack can be pinned from them — i.e. `--stack` is unset, an
16491
- * explicit `--from-cfn-stack <name>` is unset, AND
16492
- * {@link deriveSynthStackPrefix} can't resolve one shared prefix (targets
16493
- * span stacks, or any target is a bare logical id). Returns `false` when
16494
- * there are no targets (serve-all already synths everything via the
16495
- * regular path) or a stack is otherwise pinned.
17293
+ * Walk the discovered routes for `AuthorizationType: 'AWS_IAM'` and emit
17294
+ * a one-line warn naming the affected routes. Returns `true` when at
17295
+ * least one IAM route is present so the caller wires the SigV4
17296
+ * credentials loader. Re-runs across hot reloads are silent — the warn
17297
+ * fires only at initial boot (matches `warnVpcConfigLambdas`'s policy).
16496
17298
  *
16497
- * @internal exported for unit tests.
17299
+ * Implementation note: signature verification only — IAM policy
17300
+ * evaluation (resource / action / condition) is NOT emulated. See
17301
+ * `src/local/sigv4-verify.ts` and the help text in `docs/cli-reference.md`.
16498
17302
  */
16499
- function shouldSynthAllStacks(targets, stackPattern, cfnStackFallback) {
16500
- if (targets.length === 0) return false;
16501
- if (stackPattern !== void 0 || cfnStackFallback !== void 0) return false;
16502
- return deriveSynthStackPrefix(targets) === void 0;
17303
+ function warnIamRoutes(routesWithAuth) {
17304
+ const logger = getLogger();
17305
+ const iamRoutes = [];
17306
+ const oacRoutes = [];
17307
+ for (const entry of routesWithAuth) {
17308
+ if (entry.authorizer?.kind !== "iam") continue;
17309
+ if (entry.authorizer.oacFronted === true) oacRoutes.push(entry.route.declaredAt);
17310
+ else iamRoutes.push(entry.route.declaredAt);
17311
+ }
17312
+ if (iamRoutes.length === 0 && oacRoutes.length === 0) return false;
17313
+ if (iamRoutes.length > 0) {
17314
+ logger.warn(`${iamRoutes.length} route(s) declare AuthorizationType: AWS_IAM — ${getEmbedConfig().cliName} start-api verifies the SigV4 signatures it CAN (requests signed with your local AWS credentials), but cannot verify a federated / Cognito Identity Pool / cross-account signer and does NOT emulate IAM policy evaluation (resource / action / condition rules). By default such unverifiable requests warn-and-pass with a placeholder principalId — pass --strict-sigv4 to deny them instead. Downstream authorization is the dev's responsibility.`);
17315
+ for (const declaredAt of iamRoutes) logger.warn(` - ${declaredAt}`);
17316
+ }
17317
+ if (oacRoutes.length > 0) {
17318
+ logger.warn(`${oacRoutes.length} Function URL route(s) with AuthType: AWS_IAM are fronted by a CloudFront Origin Access Control. In production CloudFront re-signs the origin request, so no local client signature can be verified — ${getEmbedConfig().cliName} start-api passes these through (warn-and-pass) and they ignore --strict-sigv4. Do NOT trust the request identity in handler code.`);
17319
+ for (const declaredAt of oacRoutes) logger.warn(` - ${declaredAt}`);
17320
+ }
17321
+ return true;
16503
17322
  }
16504
17323
  /**
16505
- * Decide whether bare `start-api` should open the pre-selected-all
16506
- * multi-select picker. The picker runs ONLY when:
16507
- * - no explicit API subset was named (`apiFilters` empty — neither
16508
- * positional `<targets...>` nor the deprecated `--api`),
16509
- * - `--all-stacks` was not passed (that path already serves every API),
16510
- * - AND stdin/stdout is a TTY.
16511
- *
16512
- * In a non-TTY (CI / pipe) this returns `false`, and the caller serves
16513
- * EVERY discovered API without prompting — start-api's one intentional
16514
- * asymmetry vs invoke / run-task (which error when bare in a non-TTY),
16515
- * because start-api legitimately has a "serve all" default.
16516
- *
16517
- * Extracted as a pure function (TTY is passed in) so the decision is
16518
- * unit-testable without booting the server or juggling global TTY state.
16519
- *
16520
- * @internal exported for unit tests.
17324
+ * Build the per-Lambda container spec code dir, env vars (template +
17325
+ * --env-vars overlay), STS-issued creds when --assume-role names this
17326
+ * Lambda, optional --debug-port reservation. Errors out with a clear
17327
+ * message if the Lambda's code can't be resolved (asset directory
17328
+ * missing, runtime not supported).
16521
17329
  */
16522
- function shouldPromptBareMultiSelect(apiFilters, allStacks, isTty) {
16523
- return apiFilters.length === 0 && !allStacks && isTty;
17330
+ async function buildContainerSpec(args) {
17331
+ const { logicalId, stacks, overrides, assumeRole, containerHost, debugPort, stsRegion, inlineTmpDirs, layerTmpDirs, stateByStack, skipPull, layerRoleArn, profileCredentials, profileCredsFile, profileRegion, stackRegionOverride } = args;
17332
+ const lambda = resolveLambdaByLogicalId(logicalId, stacks);
17333
+ let codeDir;
17334
+ let optDir;
17335
+ let imageRef;
17336
+ let platform;
17337
+ if (lambda.kind === "zip") {
17338
+ codeDir = lambda.codePath ?? materializeInlineCode$1(lambda.handler, lambda.inlineCode ?? "", resolveRuntimeFileExtension(lambda.runtime), inlineTmpDirs);
17339
+ optDir = await materializeLambdaLayers(lambda.layers, layerTmpDirs, layerRoleArn);
17340
+ } else {
17341
+ imageRef = (await resolveContainerImageForStartApi(lambda, skipPull, args.profile)).imageRef;
17342
+ platform = architectureToPlatform(lambda.architecture);
17343
+ }
17344
+ let templateEnv = getTemplateEnv(lambda.resource);
17345
+ const stateBundle = stateByStack.get(lambda.stack.stackName);
17346
+ let stateAudit;
17347
+ if (stateBundle) {
17348
+ const context = { resources: stateBundle.state.resources };
17349
+ if (stateBundle.pseudoParameters) context.pseudoParameters = stateBundle.pseudoParameters;
17350
+ if (stateBundle.ssmParameters) context.parameters = stateBundle.ssmParameters;
17351
+ if (stateBundle.ssmSecureStringLogicalIds?.length) context.sensitiveParameters = new Set(stateBundle.ssmSecureStringLogicalIds);
17352
+ const { env, audit } = substituteEnvVarsFromState(templateEnv, context);
17353
+ templateEnv = env;
17354
+ for (const key of audit.resolvedKeys) getLogger().debug(`Lambda ${logicalId}: state source substituted env var ${key}`);
17355
+ let unresolved = audit.unresolved;
17356
+ const resolvedKeys = [...audit.resolvedKeys];
17357
+ const deployedEnv = stateBundle.deployedEnvByLambda?.get(logicalId);
17358
+ if (unresolved.length > 0 && deployedEnv) {
17359
+ const fb = applyDeployedEnvFallback(templateEnv, unresolved, deployedEnv);
17360
+ templateEnv = fb.env;
17361
+ unresolved = fb.stillUnresolved;
17362
+ for (const key of fb.filled) {
17363
+ resolvedKeys.push(key);
17364
+ getLogger().debug(`Lambda ${logicalId}: filled env var ${key} from deployed function config`);
17365
+ }
17366
+ }
17367
+ stateAudit = {
17368
+ resolvedKeys,
17369
+ unresolved,
17370
+ sensitiveKeys: audit.sensitiveKeys
17371
+ };
17372
+ for (const { key, reason } of unresolved) getLogger().warn(`Lambda ${logicalId}: state source could not substitute env var ${key} (${reason}). Override it via --env-vars or it will be dropped.`);
17373
+ }
17374
+ const lambdaCdkPath = readCdkPathOrUndefined(lambda.resource);
17375
+ const envResult = resolveEnvVars(logicalId, lambdaCdkPath, templateEnv, overrides);
17376
+ for (const key of envResult.unresolved) {
17377
+ if (stateAudit && stateAudit.unresolved.some((u) => u.key === key)) continue;
17378
+ const overrideKeyExample = lambdaCdkPath?.replace(/\/Resource$/, "") ?? logicalId;
17379
+ getLogger().warn(`Lambda ${logicalId}: env var ${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.`);
17380
+ }
17381
+ const dockerEnv = {
17382
+ AWS_LAMBDA_FUNCTION_NAME: logicalId,
17383
+ AWS_LAMBDA_FUNCTION_MEMORY_SIZE: String(lambda.memoryMb),
17384
+ AWS_LAMBDA_FUNCTION_TIMEOUT: String(lambda.timeoutSec),
17385
+ AWS_LAMBDA_FUNCTION_VERSION: "$LATEST",
17386
+ AWS_LAMBDA_LOG_GROUP_NAME: `/aws/lambda/${logicalId}`,
17387
+ AWS_LAMBDA_LOG_STREAM_NAME: "local",
17388
+ ...envResult.resolved
17389
+ };
17390
+ const roleArn = effectiveAssumeRoleArn(logicalId, assumeRole);
17391
+ if (roleArn) {
17392
+ const creds = await assumeLambdaExecutionRole(roleArn, stsRegion);
17393
+ dockerEnv["AWS_ACCESS_KEY_ID"] = creds.accessKeyId;
17394
+ dockerEnv["AWS_SECRET_ACCESS_KEY"] = creds.secretAccessKey;
17395
+ dockerEnv["AWS_SESSION_TOKEN"] = creds.sessionToken;
17396
+ if (stsRegion) dockerEnv["AWS_REGION"] = stsRegion;
17397
+ } else {
17398
+ forwardAwsEnv$2(dockerEnv);
17399
+ if (profileCredentials) {
17400
+ dockerEnv["AWS_ACCESS_KEY_ID"] = profileCredentials.accessKeyId;
17401
+ dockerEnv["AWS_SECRET_ACCESS_KEY"] = profileCredentials.secretAccessKey;
17402
+ if (profileCredentials.sessionToken) dockerEnv["AWS_SESSION_TOKEN"] = profileCredentials.sessionToken;
17403
+ else delete dockerEnv["AWS_SESSION_TOKEN"];
17404
+ }
17405
+ if (profileCredsFile) {
17406
+ dockerEnv["AWS_SHARED_CREDENTIALS_FILE"] = profileCredsFile.containerPath;
17407
+ dockerEnv["AWS_PROFILE"] = profileCredsFile.profileName;
17408
+ }
17409
+ }
17410
+ if (!dockerEnv["AWS_REGION"]) {
17411
+ const fallbackRegion = resolveContainerFallbackRegion({
17412
+ stackRegionOverride,
17413
+ synthRegion: lambda.stack.region,
17414
+ profileRegion
17415
+ });
17416
+ if (fallbackRegion) dockerEnv["AWS_REGION"] = fallbackRegion;
17417
+ }
17418
+ if (debugPort !== void 0) dockerEnv["NODE_OPTIONS"] = `--inspect-brk=0.0.0.0:${debugPort}`;
17419
+ const tmpfs = lambda.ephemeralStorageMb !== void 0 ? {
17420
+ target: "/tmp",
17421
+ sizeMb: lambda.ephemeralStorageMb
17422
+ } : void 0;
17423
+ const sensitiveEnvKeys = stateAudit && stateAudit.sensitiveKeys.length > 0 ? new Set(stateAudit.sensitiveKeys) : void 0;
17424
+ if (lambda.kind === "zip") return {
17425
+ kind: "zip",
17426
+ lambda,
17427
+ codeDir,
17428
+ env: dockerEnv,
17429
+ ...sensitiveEnvKeys && { sensitiveEnvKeys },
17430
+ containerHost,
17431
+ ...optDir !== void 0 && { optDir },
17432
+ ...debugPort !== void 0 && { debugPort },
17433
+ ...tmpfs !== void 0 && { tmpfs },
17434
+ ...profileCredsFile && { profileCredentialsFile: {
17435
+ hostPath: profileCredsFile.hostPath,
17436
+ containerPath: profileCredsFile.containerPath
17437
+ } }
17438
+ };
17439
+ return {
17440
+ kind: "image",
17441
+ lambda,
17442
+ image: imageRef,
17443
+ platform,
17444
+ command: lambda.imageConfig.command ?? [],
17445
+ ...lambda.imageConfig.entryPoint !== void 0 && lambda.imageConfig.entryPoint.length > 0 && { entryPoint: lambda.imageConfig.entryPoint },
17446
+ ...lambda.imageConfig.workingDirectory !== void 0 && { workingDir: lambda.imageConfig.workingDirectory },
17447
+ env: dockerEnv,
17448
+ ...sensitiveEnvKeys && { sensitiveEnvKeys },
17449
+ containerHost,
17450
+ ...debugPort !== void 0 && { debugPort },
17451
+ ...tmpfs !== void 0 && { tmpfs },
17452
+ ...profileCredsFile && { profileCredentialsFile: {
17453
+ hostPath: profileCredsFile.hostPath,
17454
+ containerPath: profileCredsFile.containerPath
17455
+ } }
17456
+ };
16524
17457
  }
16525
17458
  /**
16526
- * Decide whether the `--from-cfn-stack <name>` redundancy tip should
16527
- * fire for the current invocation. Fires only when:
16528
- * - `fromCfnStack` is a non-empty STRING (explicit value, not bare `true`)
16529
- * - exactly ONE stack is routed
16530
- * - the explicit value equals the routed stack's `stackName`
16531
- *
16532
- * Extracted as a pure function so it can be unit-tested without booting
16533
- * the full server. See improvement A in the start-api UX PR.
17459
+ * Resolve a container Lambda's local docker image — local build from
17460
+ * `cdk.out` asset manifest first, ECR-pull fallback when the asset
17461
+ * manifest has no matching entry. Mirrors `cdkl invoke`'s
17462
+ * `resolveContainerImagePlan` shape; the start-api server doesn't
17463
+ * need the no-build flag (deterministic-tag cache reuse is automatic
17464
+ * across reloads because the per-Lambda tag is content-addressed).
16534
17465
  *
16535
- * @internal exported for unit tests.
17466
+ * Same-account / same-region only on the ECR-pull path (matches the
17467
+ * `cdkl invoke` PR 5 of #224 boundary). Cross-account /
17468
+ * cross-region ECR pull is the W2-1 deferred follow-up.
16536
17469
  */
16537
- function shouldEmitFromCfnRedundancyTip(fromCfnStack, routedStackNames) {
16538
- if (typeof fromCfnStack !== "string") return false;
16539
- if (fromCfnStack.length === 0) return false;
16540
- if (routedStackNames.length !== 1) return false;
16541
- return fromCfnStack === routedStackNames[0];
17470
+ async function resolveContainerImageForStartApi(lambda, skipPull, profile) {
17471
+ const logger = getLogger();
17472
+ const localBuild = await resolveLocalBuildPlan(lambda);
17473
+ if (localBuild) return { imageRef: await buildContainerImage(localBuild.asset, localBuild.cdkOutDir, { architecture: lambda.architecture }) };
17474
+ 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.`);
17475
+ logger.info(`No matching cdk.out asset for ${lambda.imageUri}; falling back to ECR pull (same-acct/region only)...`);
17476
+ return { imageRef: await pullEcrImage(lambda.imageUri, {
17477
+ skipPull,
17478
+ ...profile !== void 0 && { profile }
17479
+ }) };
16542
17480
  }
16543
17481
  /**
16544
- * One-shot wrapper around `shouldEmitFromCfnRedundancyTip` for the
16545
- * `--watch` hot-reload path. `synthesizeAndBuild` re-runs on every
16546
- * reload firing, so without a gate the tip would re-emit on every
16547
- * reload — noisy. This helper consults the caller-supplied ref:
16548
- * - If the predicate fires AND the ref is still `false`, calls `emit`
16549
- * and flips the ref to `true`.
16550
- * - On subsequent invocations the ref is `true` and the helper is a
16551
- * no-op for the rest of the ref's lifetime.
16552
- * - When the predicate does NOT fire (no `--from-cfn-stack` value /
16553
- * intentionally-different value / multi-stack run), the ref stays
16554
- * `false` so a future reload whose synthesized stacks change in a
16555
- * way that DOES make the value redundant still emits the tip once.
16556
- *
16557
- * The ref is owned by `localStartApiCommand` (one per server boot), so
16558
- * independent server invocations get independent flags.
17482
+ * Look up the docker image asset that backs a container Lambda.
17483
+ * Returns `undefined` when the asset manifest has no matching entry —
17484
+ * the caller falls back to the ECR-pull path.
16559
17485
  *
16560
- * @internal exported for unit tests.
17486
+ * Mirrors `local-invoke.ts:resolveLocalBuildPlan`; kept separate so
17487
+ * the two commands evolve their asset-lookup heuristics independently.
16561
17488
  */
16562
- function tryEmitFromCfnRedundancyTipOnce(fromCfnStack, routedStackNames, emittedRef, emit) {
16563
- if (emittedRef.value) return;
16564
- if (!shouldEmitFromCfnRedundancyTip(fromCfnStack, routedStackNames)) return;
16565
- const routedStackName = routedStackNames[0];
16566
- if (routedStackName === void 0) return;
16567
- emit(routedStackName);
16568
- emittedRef.value = true;
17489
+ async function resolveLocalBuildPlan(lambda) {
17490
+ const manifestPath = lambda.stack.assetManifestPath;
17491
+ if (!manifestPath) return void 0;
17492
+ const cdkOutDir = path.dirname(manifestPath);
17493
+ const manifest = await new AssetManifestLoader().loadManifest(cdkOutDir, lambda.stack.stackName);
17494
+ if (!manifest) return void 0;
17495
+ const entry = getDockerImageBySourceHash(manifest, lambda.imageUri);
17496
+ if (!entry) return void 0;
17497
+ return {
17498
+ asset: entry.asset,
17499
+ cdkOutDir
17500
+ };
16569
17501
  }
16570
17502
  /**
16571
- * Distinct, stable list of Lambda logical IDs reachable through any
16572
- * discovered route OR referenced by a Lambda authorizer attached to one
16573
- * of those routes. Stable order = first-occurrence order in the routes
16574
- * list, then any newly-introduced authorizer Lambdas, which keeps the
16575
- * route-table output deterministic.
17503
+ * Build the `/opt` bind-mount source for a Lambda's layers. Mirrors
17504
+ * the helper in `src/cli/commands/local-invoke.ts` but stores the
17505
+ * merged tmpdir into the shared `layerTmpDirs` set so the server's
17506
+ * graceful shutdown path can clean it up. Returns `undefined` when
17507
+ * the function declares no layers.
17508
+ *
17509
+ * Three branches:
17510
+ * - 0 layers → `undefined` (no `/opt` mount).
17511
+ * - 1 layer → bind-mount the layer's asset dir directly (no copy)
17512
+ * when the entry is a same-stack asset. Literal-ARN entries always
17513
+ * pre-materialize first.
17514
+ * - 2+ layers → copy each into a fresh tmpdir IN ORDER (later
17515
+ * layers overwrite earlier files via `cpSync({force: true})`),
17516
+ * bind-mount the tmpdir at `/opt`. Records the tmpdir in
17517
+ * `layerTmpDirs` so `shutdown(...)` removes it.
17518
+ *
17519
+ * Issue #448: literal-ARN entries (`{kind: 'arn', ...}`) are downloaded
17520
+ * + unzipped via `lambda:GetLayerVersion` BEFORE the cpSync-merge
17521
+ * branches run. Every per-ARN tmpdir is also recorded in `layerTmpDirs`
17522
+ * so the same shutdown path cleans it up — even for the single-layer
17523
+ * fast path that bind-mounts the dir directly.
17524
+ *
17525
+ * AWS Lambda's actual runtime extracts every layer ZIP into `/opt`
17526
+ * in template order — the merge mirrors that. Docker rejects multiple
17527
+ * `-v ...:/opt:ro` entries at the same target, so cdk-local can't rely on
17528
+ * overlay layering and must produce a single merged dir on the host.
16576
17529
  */
16577
- function uniqueLambdaIds(routes, routesWithAuth, webSocketApis = []) {
16578
- const seen = /* @__PURE__ */ new Set();
16579
- const out = [];
16580
- for (const r of routes) {
16581
- if (r.unsupported || r.mockCors || r.serviceIntegration) continue;
16582
- if (r.lambdaLogicalId.length === 0) continue;
16583
- if (!seen.has(r.lambdaLogicalId)) {
16584
- seen.add(r.lambdaLogicalId);
16585
- out.push(r.lambdaLogicalId);
16586
- }
16587
- }
16588
- for (const entry of routesWithAuth) {
16589
- if (entry.route.unsupported || entry.route.mockCors || entry.route.serviceIntegration) continue;
16590
- const auth = entry.authorizer;
16591
- if (!auth) continue;
16592
- if (auth.kind === "lambda-token" || auth.kind === "lambda-request") {
16593
- if (!seen.has(auth.lambdaLogicalId)) {
16594
- seen.add(auth.lambdaLogicalId);
16595
- out.push(auth.lambdaLogicalId);
16596
- }
17530
+ async function materializeLambdaLayers(layers, layerTmpDirs, layerRoleArn) {
17531
+ if (layers.length === 0) return void 0;
17532
+ const flat = [];
17533
+ for (const layer of layers) {
17534
+ if (layer.kind === "asset") {
17535
+ flat.push({
17536
+ logicalId: layer.logicalId,
17537
+ assetPath: layer.assetPath
17538
+ });
17539
+ continue;
16597
17540
  }
17541
+ const dir = await materializeLayerFromArn(layer, { ...layerRoleArn !== void 0 && { roleArn: layerRoleArn } });
17542
+ layerTmpDirs.add(dir);
17543
+ flat.push({
17544
+ logicalId: layer.arn,
17545
+ assetPath: dir
17546
+ });
16598
17547
  }
16599
- for (const api of webSocketApis) for (const r of api.routes) if (!seen.has(r.targetLambdaLogicalId)) {
16600
- seen.add(r.targetLambdaLogicalId);
16601
- out.push(r.targetLambdaLogicalId);
17548
+ if (flat.length === 1) return flat[0].assetPath;
17549
+ const dir = mkdtempSync(path.join(tmpdir(), `${getEmbedConfig().resourceNamePrefix}-start-api-layers-`));
17550
+ for (const layer of flat) cpSync(layer.assetPath, dir, {
17551
+ recursive: true,
17552
+ force: true
17553
+ });
17554
+ layerTmpDirs.add(dir);
17555
+ return dir;
17556
+ }
17557
+ function resolveLambdaByLogicalId(logicalId, stacks) {
17558
+ for (const stack of stacks) {
17559
+ const resource = stack.template.Resources?.[logicalId];
17560
+ if (!resource || resource.Type !== "AWS::Lambda::Function") continue;
17561
+ const props = resource.Properties ?? {};
17562
+ const memoryMb = typeof props["MemorySize"] === "number" ? props["MemorySize"] : 128;
17563
+ const timeoutSec = typeof props["Timeout"] === "number" ? props["Timeout"] : 3;
17564
+ const code = props["Code"] ?? {};
17565
+ const imageUri = extractImageUri(code["ImageUri"], logicalId, stack.stackName, stack.template.Resources ?? {}, stack.region);
17566
+ if (imageUri !== void 0) return resolveImageLambda({
17567
+ stack,
17568
+ logicalId,
17569
+ resource,
17570
+ props,
17571
+ memoryMb,
17572
+ timeoutSec,
17573
+ imageUri
17574
+ });
17575
+ const runtime = typeof props["Runtime"] === "string" ? props["Runtime"] : "";
17576
+ const handler = typeof props["Handler"] === "string" ? props["Handler"] : "";
17577
+ if (!runtime) throw new Error(`Lambda '${logicalId}' has no Runtime property and no Code.ImageUri. ${getEmbedConfig().cliName} start-api cannot tell if this is a ZIP or a container Lambda.`);
17578
+ if (!handler) throw new Error(`Lambda '${logicalId}' has no Handler property.`);
17579
+ const inlineCode = typeof code["ZipFile"] === "string" ? code["ZipFile"] : void 0;
17580
+ let codePath = null;
17581
+ if (!inlineCode) codePath = resolveAssetCodePath(stack, logicalId, resource);
17582
+ const layers = resolveLambdaLayers(stack, logicalId, props);
17583
+ const ephemeralStorageMb = extractEphemeralStorageMb(props, logicalId);
17584
+ return {
17585
+ kind: "zip",
17586
+ stack,
17587
+ logicalId,
17588
+ resource,
17589
+ runtime,
17590
+ handler,
17591
+ memoryMb,
17592
+ timeoutSec,
17593
+ codePath,
17594
+ layers,
17595
+ ...inlineCode !== void 0 && { inlineCode },
17596
+ ...ephemeralStorageMb !== void 0 && { ephemeralStorageMb }
17597
+ };
16602
17598
  }
16603
- return out;
17599
+ throw new Error(`No AWS::Lambda::Function resource named '${logicalId}' found in target stacks. This is likely a synthesis bug — the route-discovery phase resolved a route to this logical ID.`);
16604
17600
  }
16605
17601
  /**
16606
- * Prefetch the JWKS for every Cognito / JWT authorizer attached to a
16607
- * discovered route. Failures degrade to pass-through mode (verifier
16608
- * surfaces a warn line on first hit); we still issue the prefetch so
16609
- * the warn lands at startup rather than mid-request.
17602
+ * Extract `Code.ImageUri` across the shapes CDK actually synthesizes.
17603
+ * Mirrors the simpler subset of `lambda-resolver.ts:extractImageUri`
17604
+ * scoped to the shapes `cdkl start-api` consumes flat string,
17605
+ * `Fn::Sub` (the canonical asset shape for
17606
+ * `lambda.DockerImageCode.fromImageAsset`), and `Fn::Join` (the
17607
+ * canonical shape for `lambda.DockerImageCode.fromImageAsset` in
17608
+ * CDK 2.x, which emits a `Fn::Join` over the literal bootstrap ECR
17609
+ * URI with `${AWS::URLSuffix}` — issues #627 + #637).
17610
+ *
17611
+ * The `Fn::Join` arm routes through the shared
17612
+ * `tryResolveImageFnJoin` helper (`src/local/intrinsic-image.ts`) used
17613
+ * by `cdkl invoke`. When the synth template recorded a deploy
17614
+ * region (`stack.region`), we derive `{ urlSuffix, partition, region }`
17615
+ * via `derivePseudoParametersFromRegion` and pass it as the resolver's
17616
+ * `pseudoParameters` block so the canonical `${AWS::URLSuffix}` shape
17617
+ * resolves cleanly (issue #637). Same-stack ECR refs still surface as
17618
+ * `needs-state` (those require `--from-state`); a Join that references
17619
+ * `${AWS::AccountId}` without state still surfaces as `not-applicable`
17620
+ * with a more specific error message naming the missing parameter.
17621
+ *
17622
+ * Returns `undefined` when the field is absent or non-recognized,
17623
+ * which routes the caller to the ZIP branch (with its existing
17624
+ * "no Runtime / no Handler" validations).
16610
17625
  */
16611
- async function prewarmJwks(routesWithAuth, jwksCache) {
16612
- const urls = /* @__PURE__ */ new Set();
16613
- for (const entry of routesWithAuth) {
16614
- const auth = entry.authorizer;
16615
- if (!auth) continue;
16616
- if (auth.kind === "cognito") for (const pool of auth.pools) urls.add(buildCognitoJwksUrl(pool.region, pool.userPoolId));
16617
- else if (auth.kind === "jwt") {
16618
- const url = auth.region && auth.userPoolId ? buildCognitoJwksUrl(auth.region, auth.userPoolId) : buildJwksUrlFromIssuer(auth.issuer);
16619
- urls.add(url);
17626
+ function extractImageUri(value, logicalId, stackName, resources, region) {
17627
+ if (typeof value === "string" && value.length > 0) return value;
17628
+ if (value && typeof value === "object" && !Array.isArray(value)) {
17629
+ const obj = value;
17630
+ const sub = obj["Fn::Sub"];
17631
+ if (typeof sub === "string" && sub.length > 0) return sub;
17632
+ if (Array.isArray(sub) && typeof sub[0] === "string") return sub[0];
17633
+ if ("Fn::Join" in obj) {
17634
+ const pseudoParameters = derivePseudoParametersFromRegion(region);
17635
+ const joinResolved = tryResolveImageFnJoin(value, resources, pseudoParameters ? { pseudoParameters } : void 0);
17636
+ if (joinResolved.kind === "resolved") return joinResolved.uri;
17637
+ if (joinResolved.kind === "needs-state") throw new Error(`Lambda '${logicalId}' in ${stackName} references same-stack ECR repository '${joinResolved.repoLogicalId}' via Fn::Join. ${getEmbedConfig().cliName} start-api cannot resolve the repository URI without state — deploy the stack first, rebuild via lambda.DockerImageCode.fromImageAsset, or pin a public image.`);
17638
+ if (joinResolved.kind === "unsupported-join") throw new Error(`Lambda '${logicalId}' in ${stackName} has an unsupported Fn::Join Code.ImageUri shape: ${joinResolved.reason}. ${getEmbedConfig().cliName} start-api recognizes the canonical CDK 2.x lambda.DockerImageCode.fromEcr Fn::Join shape (delimiter "" with nested Fn::Select/Fn::Split over an ECR Repository Arn GetAtt + Ref to the repo).`);
17639
+ const accountIdHint = pseudoParameters ? ` (likely \${AWS::AccountId}, which ${getEmbedConfig().binaryName} cannot derive without a state source or STS)` : ` (${getEmbedConfig().binaryName} could not derive AWS pseudo parameters because stack.region was undefined)`;
17640
+ throw new Error(`Lambda '${logicalId}' in ${stackName} has an Fn::Join Code.ImageUri that ${getEmbedConfig().cliName} start-api cannot resolve${accountIdHint}. Workarounds: deploy first and run with a state-source flag (e.g. --from-cfn-stack or a host-provided extension), or pin a fully-literal public image URI.`);
16620
17641
  }
16621
17642
  }
16622
- await Promise.all([...urls].map((u) => jwksCache.fetchAndCache(u)));
16623
17643
  }
16624
17644
  /**
16625
- * Emit a one-line warn for every VPC-config Lambda. The handler still
16626
- * runs locally, but its container does not get attached to the AWS
16627
- * VPC's subnets calls to private RDS / ElastiCache will fail. cdk-local
16628
- * surfaces this so the developer can pin the unexpected behavior to
16629
- * the VPC config rather than chasing a "connection refused" rabbit
16630
- * hole.
17645
+ * Build the IMAGE-variant `ResolvedStartApiLambda` from a Lambda
17646
+ * template entry with `Code.ImageUri`. Mirrors
17647
+ * `lambda-resolver.ts:extractImageLambdaProperties` but trimmed to the
17648
+ * fields `cdkl start-api` actually consumes.
16631
17649
  */
16632
- function warnVpcConfigLambdas(routesWithAuth, stacks) {
16633
- const logger = getLogger();
16634
- const seen = /* @__PURE__ */ new Set();
16635
- const reachable = [];
16636
- for (const entry of routesWithAuth) {
16637
- if (!seen.has(entry.route.lambdaLogicalId)) {
16638
- seen.add(entry.route.lambdaLogicalId);
16639
- reachable.push(entry.route.lambdaLogicalId);
16640
- }
16641
- const auth = entry.authorizer;
16642
- if (auth && (auth.kind === "lambda-token" || auth.kind === "lambda-request")) {
16643
- if (!seen.has(auth.lambdaLogicalId)) {
16644
- seen.add(auth.lambdaLogicalId);
16645
- reachable.push(auth.lambdaLogicalId);
16646
- }
16647
- }
16648
- }
16649
- for (const logicalId of reachable) for (const stack of stacks) {
16650
- const resource = stack.template.Resources?.[logicalId];
16651
- if (!resource || resource.Type !== "AWS::Lambda::Function") continue;
16652
- const vpcConfig = (resource.Properties ?? {})["VpcConfig"];
16653
- if (vpcConfig && typeof vpcConfig === "object" && Object.keys(vpcConfig).length > 0) logger.warn(`Lambda ${logicalId} has VpcConfig — local container will reach external services via the host's network, NOT through the deployed VPC's NAT/private subnets. Calls to private RDS/ElastiCache will fail.`);
16654
- break;
17650
+ function resolveImageLambda(args) {
17651
+ const { stack, logicalId, resource, props, memoryMb, timeoutSec, imageUri } = args;
17652
+ const rawImageConfig = props["ImageConfig"] ?? {};
17653
+ const imageConfig = {};
17654
+ if (Array.isArray(rawImageConfig["Command"])) imageConfig.command = rawImageConfig["Command"].filter((s) => typeof s === "string");
17655
+ if (Array.isArray(rawImageConfig["EntryPoint"])) imageConfig.entryPoint = rawImageConfig["EntryPoint"].filter((s) => typeof s === "string");
17656
+ if (typeof rawImageConfig["WorkingDirectory"] === "string") imageConfig.workingDirectory = rawImageConfig["WorkingDirectory"];
17657
+ const arches = props["Architectures"];
17658
+ let architecture = "x86_64";
17659
+ if (Array.isArray(arches) && arches.length > 0) {
17660
+ const first = arches[0];
17661
+ if (first === "arm64") architecture = "arm64";
17662
+ else if (first === "x86_64") architecture = "x86_64";
17663
+ else throw new Error(`Lambda '${logicalId}' has unsupported Architectures value '${String(first)}'. ${getEmbedConfig().cliName} start-api supports x86_64 and arm64.`);
16655
17664
  }
17665
+ const ephemeralStorageMb = extractEphemeralStorageMb(props, logicalId);
17666
+ return {
17667
+ kind: "image",
17668
+ stack,
17669
+ logicalId,
17670
+ resource,
17671
+ memoryMb,
17672
+ timeoutSec,
17673
+ imageUri,
17674
+ imageConfig,
17675
+ architecture,
17676
+ layers: [],
17677
+ ...ephemeralStorageMb !== void 0 && { ephemeralStorageMb }
17678
+ };
16656
17679
  }
16657
17680
  /**
16658
- * Walk the discovered routes for `AuthorizationType: 'AWS_IAM'` and emit
16659
- * a one-line warn naming the affected routes. Returns `true` when at
16660
- * least one IAM route is present so the caller wires the SigV4
16661
- * credentials loader. Re-runs across hot reloads are silent — the warn
16662
- * fires only at initial boot (matches `warnVpcConfigLambdas`'s policy).
17681
+ * Locate the Lambda's local code directory using the CDK-blessed
17682
+ * `Metadata['aws:asset:path']` hint. Bind-mounted directly at
17683
+ * `/var/task` (read-only) by the docker-runner.
17684
+ */
17685
+ function resolveAssetCodePath(stack, logicalId, resource) {
17686
+ const assetPath = resource.Metadata?.["aws:asset:path"];
17687
+ if (typeof assetPath !== "string" || assetPath.length === 0) throw new Error(`Lambda '${logicalId}' has no Metadata['aws:asset:path']. ${getEmbedConfig().cliName} start-api needs this hint to find the local asset directory. Re-synthesize the app and retry.`);
17688
+ const cdkOutDir = stack.assetManifestPath ? path.dirname(stack.assetManifestPath) : process.cwd();
17689
+ return path.isAbsolute(assetPath) ? assetPath : path.resolve(cdkOutDir, assetPath);
17690
+ }
17691
+ /**
17692
+ * Print the discovered route table to stdout. Format mirrors the spec
17693
+ * doc's example so verify.sh / users can read it at a glance.
16663
17694
  *
16664
- * Implementation note: signature verification only IAM policy
16665
- * evaluation (resource / action / condition) is NOT emulated. See
16666
- * `src/local/sigv4-verify.ts` and the help text in `docs/cli-reference.md`.
17695
+ * Routes with `unsupported` or `mockCors` are annotated so the user can
17696
+ * tell at a glance which routes will dispatch to a Lambda vs which
17697
+ * return 501 / 204 directly:
17698
+ * - normal: `GET /items -> Handler (HTTP API)`
17699
+ * - mockCors: `OPTIONS /items -> [MOCK CORS preflight] (REST v1, stage 'prod')`
17700
+ * - unsupported: `POST /admin -> [501 Not Implemented] (HTTP API)`
16667
17701
  */
16668
- function warnIamRoutes(routesWithAuth) {
16669
- const logger = getLogger();
16670
- const iamRoutes = [];
16671
- const oacRoutes = [];
16672
- for (const entry of routesWithAuth) {
16673
- if (entry.authorizer?.kind !== "iam") continue;
16674
- if (entry.authorizer.oacFronted === true) oacRoutes.push(entry.route.declaredAt);
16675
- else iamRoutes.push(entry.route.declaredAt);
16676
- }
16677
- if (iamRoutes.length === 0 && oacRoutes.length === 0) return false;
16678
- if (iamRoutes.length > 0) {
16679
- logger.warn(`${iamRoutes.length} route(s) declare AuthorizationType: AWS_IAM — ${getEmbedConfig().cliName} start-api verifies the SigV4 signatures it CAN (requests signed with your local AWS credentials), but cannot verify a federated / Cognito Identity Pool / cross-account signer and does NOT emulate IAM policy evaluation (resource / action / condition rules). By default such unverifiable requests warn-and-pass with a placeholder principalId — pass --strict-sigv4 to deny them instead. Downstream authorization is the dev's responsibility.`);
16680
- for (const declaredAt of iamRoutes) logger.warn(` - ${declaredAt}`);
16681
- }
16682
- if (oacRoutes.length > 0) {
16683
- logger.warn(`${oacRoutes.length} Function URL route(s) with AuthType: AWS_IAM are fronted by a CloudFront Origin Access Control. In production CloudFront re-signs the origin request, so no local client signature can be verified — ${getEmbedConfig().cliName} start-api passes these through (warn-and-pass) and they ignore --strict-sigv4. Do NOT trust the request identity in handler code.`);
16684
- for (const declaredAt of oacRoutes) logger.warn(` - ${declaredAt}`);
17702
+ function printRouteTable(routes) {
17703
+ const sorted = [...routes.map((r) => r.route)].sort((a, b) => {
17704
+ if (a.pathPattern !== b.pathPattern) return a.pathPattern.localeCompare(b.pathPattern);
17705
+ return a.method.localeCompare(b.method);
17706
+ });
17707
+ const methodWidth = Math.max(...sorted.map((r) => r.method.length), 6);
17708
+ const pathWidth = Math.max(...sorted.map((r) => r.pathPattern.length), 8);
17709
+ process.stdout.write("Discovered routes:\n");
17710
+ for (const r of sorted) {
17711
+ const sourceLabel = r.source === "http-api" ? "HTTP API" : r.source === "rest-v1" ? `REST v1, stage '${r.stage}'` : "Function URL";
17712
+ const target = r.mockCors ? "[MOCK CORS preflight]" : r.unsupported ? "[501 Not Implemented]" : r.serviceIntegration ? `[${r.serviceIntegration.subtype}]` : r.restV1Integration ? formatRestV1IntegrationLabel(r.restV1Integration) : r.lambdaLogicalId;
17713
+ process.stdout.write(` ${r.method.padEnd(methodWidth)} ${r.pathPattern.padEnd(pathWidth)} -> ${target} (${sourceLabel})\n`);
16685
17714
  }
16686
- return true;
17715
+ process.stdout.write("\n");
16687
17716
  }
16688
17717
  /**
16689
- * Build the per-Lambda container spec code dir, env vars (template +
16690
- * --env-vars overlay), STS-issued creds when --assume-role names this
16691
- * Lambda, optional --debug-port reservation. Errors out with a clear
16692
- * message if the Lambda's code can't be resolved (asset directory
16693
- * missing, runtime not supported).
17718
+ * Format the route-table label for a REST v1 non-AWS_PROXY integration.
17719
+ * `MOCK` / `HTTP` / `HTTP_PROXY` show their integration kind directly;
17720
+ * `AWS` (Lambda non-proxy) shows the Lambda logical id with an `[AWS]`
17721
+ * suffix so it's distinguishable from AWS_PROXY rows. Closes #457.
16694
17722
  */
16695
- async function buildContainerSpec(args) {
16696
- const { logicalId, stacks, overrides, assumeRole, containerHost, debugPort, stsRegion, inlineTmpDirs, layerTmpDirs, stateByStack, skipPull, layerRoleArn, profileCredentials, profileCredsFile, profileRegion, stackRegionOverride } = args;
16697
- const lambda = resolveLambdaByLogicalId(logicalId, stacks);
16698
- let codeDir;
16699
- let optDir;
16700
- let imageRef;
16701
- let platform;
16702
- if (lambda.kind === "zip") {
16703
- codeDir = lambda.codePath ?? materializeInlineCode$1(lambda.handler, lambda.inlineCode ?? "", resolveRuntimeFileExtension(lambda.runtime), inlineTmpDirs);
16704
- optDir = await materializeLambdaLayers(lambda.layers, layerTmpDirs, layerRoleArn);
16705
- } else {
16706
- imageRef = (await resolveContainerImageForStartApi(lambda, skipPull, args.profile)).imageRef;
16707
- platform = architectureToPlatform(lambda.architecture);
16708
- }
16709
- let templateEnv = getTemplateEnv(lambda.resource);
16710
- const stateBundle = stateByStack.get(lambda.stack.stackName);
16711
- let stateAudit;
16712
- if (stateBundle) {
16713
- const context = { resources: stateBundle.state.resources };
16714
- if (stateBundle.pseudoParameters) context.pseudoParameters = stateBundle.pseudoParameters;
16715
- if (stateBundle.ssmParameters) context.parameters = stateBundle.ssmParameters;
16716
- if (stateBundle.ssmSecureStringLogicalIds?.length) context.sensitiveParameters = new Set(stateBundle.ssmSecureStringLogicalIds);
16717
- const { env, audit } = substituteEnvVarsFromState(templateEnv, context);
16718
- templateEnv = env;
16719
- for (const key of audit.resolvedKeys) getLogger().debug(`Lambda ${logicalId}: state source substituted env var ${key}`);
16720
- let unresolved = audit.unresolved;
16721
- const resolvedKeys = [...audit.resolvedKeys];
16722
- const deployedEnv = stateBundle.deployedEnvByLambda?.get(logicalId);
16723
- if (unresolved.length > 0 && deployedEnv) {
16724
- const fb = applyDeployedEnvFallback(templateEnv, unresolved, deployedEnv);
16725
- templateEnv = fb.env;
16726
- unresolved = fb.stillUnresolved;
16727
- for (const key of fb.filled) {
16728
- resolvedKeys.push(key);
16729
- getLogger().debug(`Lambda ${logicalId}: filled env var ${key} from deployed function config`);
16730
- }
16731
- }
16732
- stateAudit = {
16733
- resolvedKeys,
16734
- unresolved,
16735
- sensitiveKeys: audit.sensitiveKeys
16736
- };
16737
- for (const { key, reason } of unresolved) getLogger().warn(`Lambda ${logicalId}: state source could not substitute env var ${key} (${reason}). Override it via --env-vars or it will be dropped.`);
16738
- }
16739
- const lambdaCdkPath = readCdkPathOrUndefined(lambda.resource);
16740
- const envResult = resolveEnvVars(logicalId, lambdaCdkPath, templateEnv, overrides);
16741
- for (const key of envResult.unresolved) {
16742
- if (stateAudit && stateAudit.unresolved.some((u) => u.key === key)) continue;
16743
- const overrideKeyExample = lambdaCdkPath?.replace(/\/Resource$/, "") ?? logicalId;
16744
- getLogger().warn(`Lambda ${logicalId}: env var ${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.`);
16745
- }
16746
- const dockerEnv = {
16747
- AWS_LAMBDA_FUNCTION_NAME: logicalId,
16748
- AWS_LAMBDA_FUNCTION_MEMORY_SIZE: String(lambda.memoryMb),
16749
- AWS_LAMBDA_FUNCTION_TIMEOUT: String(lambda.timeoutSec),
16750
- AWS_LAMBDA_FUNCTION_VERSION: "$LATEST",
16751
- AWS_LAMBDA_LOG_GROUP_NAME: `/aws/lambda/${logicalId}`,
16752
- AWS_LAMBDA_LOG_STREAM_NAME: "local",
16753
- ...envResult.resolved
16754
- };
16755
- const roleArn = effectiveAssumeRoleArn(logicalId, assumeRole);
16756
- if (roleArn) {
16757
- const creds = await assumeLambdaExecutionRole(roleArn, stsRegion);
16758
- dockerEnv["AWS_ACCESS_KEY_ID"] = creds.accessKeyId;
16759
- dockerEnv["AWS_SECRET_ACCESS_KEY"] = creds.secretAccessKey;
16760
- dockerEnv["AWS_SESSION_TOKEN"] = creds.sessionToken;
16761
- if (stsRegion) dockerEnv["AWS_REGION"] = stsRegion;
16762
- } else {
16763
- forwardAwsEnv$1(dockerEnv);
16764
- if (profileCredentials) {
16765
- dockerEnv["AWS_ACCESS_KEY_ID"] = profileCredentials.accessKeyId;
16766
- dockerEnv["AWS_SECRET_ACCESS_KEY"] = profileCredentials.secretAccessKey;
16767
- if (profileCredentials.sessionToken) dockerEnv["AWS_SESSION_TOKEN"] = profileCredentials.sessionToken;
16768
- else delete dockerEnv["AWS_SESSION_TOKEN"];
16769
- }
16770
- if (profileCredsFile) {
16771
- dockerEnv["AWS_SHARED_CREDENTIALS_FILE"] = profileCredsFile.containerPath;
16772
- dockerEnv["AWS_PROFILE"] = profileCredsFile.profileName;
16773
- }
16774
- }
16775
- if (!dockerEnv["AWS_REGION"]) {
16776
- const fallbackRegion = resolveContainerFallbackRegion({
16777
- stackRegionOverride,
16778
- synthRegion: lambda.stack.region,
16779
- profileRegion
16780
- });
16781
- if (fallbackRegion) dockerEnv["AWS_REGION"] = fallbackRegion;
17723
+ function formatRestV1IntegrationLabel(integration) {
17724
+ switch (integration.kind) {
17725
+ case "mock": return "[MOCK]";
17726
+ case "http-proxy": return `[HTTP_PROXY ${integration.uri}]`;
17727
+ case "http": return `[HTTP ${integration.uri}]`;
17728
+ case "aws-lambda": return `${integration.lambdaLogicalId} [AWS]`;
16782
17729
  }
16783
- if (debugPort !== void 0) dockerEnv["NODE_OPTIONS"] = `--inspect-brk=0.0.0.0:${debugPort}`;
16784
- const tmpfs = lambda.ephemeralStorageMb !== void 0 ? {
16785
- target: "/tmp",
16786
- sizeMb: lambda.ephemeralStorageMb
16787
- } : void 0;
16788
- const sensitiveEnvKeys = stateAudit && stateAudit.sensitiveKeys.length > 0 ? new Set(stateAudit.sensitiveKeys) : void 0;
16789
- if (lambda.kind === "zip") return {
16790
- kind: "zip",
16791
- lambda,
16792
- codeDir,
16793
- env: dockerEnv,
16794
- ...sensitiveEnvKeys && { sensitiveEnvKeys },
16795
- containerHost,
16796
- ...optDir !== void 0 && { optDir },
16797
- ...debugPort !== void 0 && { debugPort },
16798
- ...tmpfs !== void 0 && { tmpfs },
16799
- ...profileCredsFile && { profileCredentialsFile: {
16800
- hostPath: profileCredsFile.hostPath,
16801
- containerPath: profileCredsFile.containerPath
16802
- } }
16803
- };
16804
- return {
16805
- kind: "image",
16806
- lambda,
16807
- image: imageRef,
16808
- platform,
16809
- command: lambda.imageConfig.command ?? [],
16810
- ...lambda.imageConfig.entryPoint !== void 0 && lambda.imageConfig.entryPoint.length > 0 && { entryPoint: lambda.imageConfig.entryPoint },
16811
- ...lambda.imageConfig.workingDirectory !== void 0 && { workingDir: lambda.imageConfig.workingDirectory },
16812
- env: dockerEnv,
16813
- ...sensitiveEnvKeys && { sensitiveEnvKeys },
16814
- containerHost,
16815
- ...debugPort !== void 0 && { debugPort },
16816
- ...tmpfs !== void 0 && { tmpfs },
16817
- ...profileCredsFile && { profileCredentialsFile: {
16818
- hostPath: profileCredsFile.hostPath,
16819
- containerPath: profileCredsFile.containerPath
16820
- } }
16821
- };
16822
17730
  }
16823
17731
  /**
16824
- * Resolve a container Lambda's local docker image local build from
16825
- * `cdk.out` asset manifest first, ECR-pull fallback when the asset
16826
- * manifest has no matching entry. Mirrors `cdkl invoke`'s
16827
- * `resolveContainerImagePlan` shape; the start-api server doesn't
16828
- * need the no-build flag (deterministic-tag cache reuse is automatic
16829
- * across reloads because the per-Lambda tag is content-addressed).
16830
- *
16831
- * Same-account / same-region only on the ECR-pull path (matches the
16832
- * `cdkl invoke` PR 5 of #224 boundary). Cross-account /
16833
- * cross-region ECR pull is the W2-1 deferred follow-up.
17732
+ * Materialize an inline Lambda body (`Code.ZipFile`) to a tmpdir and
17733
+ * return the directory the container should mount at /var/task.
17734
+ * Mirrors `cdkl invoke`'s implementation; the only divergence is
17735
+ * the long-running-server lifecycle: every tmpdir created here is
17736
+ * recorded in `tmpDirsOut` so the caller's shutdown path can `rmSync`
17737
+ * them. (`cdkl invoke` runs once and `--rm` is the right model;
17738
+ * `cdkl start-api` lives across requests, so leaks compound.)
16834
17739
  */
16835
- async function resolveContainerImageForStartApi(lambda, skipPull, profile) {
16836
- const logger = getLogger();
16837
- const localBuild = await resolveLocalBuildPlan(lambda);
16838
- if (localBuild) return { imageRef: await buildContainerImage(localBuild.asset, localBuild.cdkOutDir, { architecture: lambda.architecture }) };
16839
- 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.`);
16840
- logger.info(`No matching cdk.out asset for ${lambda.imageUri}; falling back to ECR pull (same-acct/region only)...`);
16841
- return { imageRef: await pullEcrImage(lambda.imageUri, {
16842
- skipPull,
16843
- ...profile !== void 0 && { profile }
16844
- }) };
17740
+ function materializeInlineCode$1(handler, source, fileExtension, tmpDirsOut) {
17741
+ const lastDot = handler.lastIndexOf(".");
17742
+ if (lastDot <= 0) throw new Error(`Handler '${handler}' is malformed: expected '<modulePath>.<exportName>'.`);
17743
+ const modulePath = handler.substring(0, lastDot);
17744
+ const dir = mkdtempSync(path.join(tmpdir(), `${getEmbedConfig().resourceNamePrefix}-start-api-`));
17745
+ tmpDirsOut.add(dir);
17746
+ const filePath = path.join(dir, `${modulePath}${fileExtension}`);
17747
+ mkdirSync(path.dirname(filePath), { recursive: true });
17748
+ writeFileSync(filePath, source, "utf-8");
17749
+ return dir;
17750
+ }
17751
+ /** Pull `Properties.Environment.Variables` (when present). */
17752
+ function getTemplateEnv(resource) {
17753
+ const env = (resource.Properties ?? {})["Environment"];
17754
+ if (!env || typeof env !== "object") return void 0;
17755
+ const vars = env["Variables"];
17756
+ if (!vars || typeof vars !== "object") return void 0;
17757
+ return vars;
17758
+ }
17759
+ /** Read the SAM-shape `--env-vars` JSON file. */
17760
+ function readEnvOverridesFile$3(filePath) {
17761
+ if (!filePath) return void 0;
17762
+ let raw;
17763
+ try {
17764
+ raw = readFileSync(filePath, "utf-8");
17765
+ } catch (err) {
17766
+ throw new Error(`Failed to read --env-vars file '${filePath}': ${err instanceof Error ? err.message : String(err)}`);
17767
+ }
17768
+ let parsed;
17769
+ try {
17770
+ parsed = JSON.parse(raw);
17771
+ } catch (err) {
17772
+ throw new Error(`Failed to parse --env-vars file '${filePath}' as JSON: ${err instanceof Error ? err.message : String(err)}`);
17773
+ }
17774
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`--env-vars file '${filePath}' must contain a JSON object at the top level.`);
17775
+ return parsed;
16845
17776
  }
16846
17777
  /**
16847
- * Look up the docker image asset that backs a container Lambda.
16848
- * Returns `undefined` when the asset manifest has no matching entry —
16849
- * the caller falls back to the ECR-pull path.
16850
- *
16851
- * Mirrors `local-invoke.ts:resolveLocalBuildPlan`; kept separate so
16852
- * the two commands evolve their asset-lookup heuristics independently.
17778
+ * Forward the developer's AWS credentials into the container so the
17779
+ * handler's AWS SDK calls can authenticate. Used when --assume-role is
17780
+ * NOT set for that Lambda SAM-compatible default.
16853
17781
  */
16854
- async function resolveLocalBuildPlan(lambda) {
16855
- const manifestPath = lambda.stack.assetManifestPath;
16856
- if (!manifestPath) return void 0;
16857
- const cdkOutDir = path.dirname(manifestPath);
16858
- const manifest = await new AssetManifestLoader().loadManifest(cdkOutDir, lambda.stack.stackName);
16859
- if (!manifest) return void 0;
16860
- const entry = getDockerImageBySourceHash(manifest, lambda.imageUri);
16861
- if (!entry) return void 0;
16862
- return {
16863
- asset: entry.asset,
16864
- cdkOutDir
16865
- };
17782
+ function forwardAwsEnv$2(env) {
17783
+ for (const key of [
17784
+ "AWS_ACCESS_KEY_ID",
17785
+ "AWS_SECRET_ACCESS_KEY",
17786
+ "AWS_SESSION_TOKEN",
17787
+ "AWS_REGION",
17788
+ "AWS_DEFAULT_REGION"
17789
+ ]) {
17790
+ const value = process.env[key];
17791
+ if (value !== void 0) env[key] = value;
17792
+ }
16866
17793
  }
16867
17794
  /**
16868
- * Build the `/opt` bind-mount source for a Lambda's layers. Mirrors
16869
- * the helper in `src/cli/commands/local-invoke.ts` but stores the
16870
- * merged tmpdir into the shared `layerTmpDirs` set so the server's
16871
- * graceful shutdown path can clean it up. Returns `undefined` when
16872
- * the function declares no layers.
17795
+ * Issue #654: resolve `--profile <p>` to a concrete credential set
17796
+ * for forwarding to Lambda containers.
16873
17797
  *
16874
- * Three branches:
16875
- * - 0 layers `undefined` (no `/opt` mount).
16876
- * - 1 layer → bind-mount the layer's asset dir directly (no copy)
16877
- * when the entry is a same-stack asset. Literal-ARN entries always
16878
- * pre-materialize first.
16879
- * - 2+ layers → copy each into a fresh tmpdir IN ORDER (later
16880
- * layers overwrite earlier files via `cpSync({force: true})`),
16881
- * bind-mount the tmpdir at `/opt`. Records the tmpdir in
16882
- * `layerTmpDirs` so `shutdown(...)` removes it.
17798
+ * The dev's AWS credentials may live in any of:
17799
+ * - `~/.aws/sso/cache/*.json` (AWS IAM Identity Center / legacy SSO)
17800
+ * - `~/.aws/credentials` (regular long-lived access keys)
17801
+ * - `~/.aws/config` profiles with `role_arn` + `source_profile` (chained AssumeRole)
17802
+ * - `credential_process` external resolvers
16883
17803
  *
16884
- * Issue #448: literal-ARN entries (`{kind: 'arn', ...}`) are downloaded
16885
- * + unzipped via `lambda:GetLayerVersion` BEFORE the cpSync-merge
16886
- * branches run. Every per-ARN tmpdir is also recorded in `layerTmpDirs`
16887
- * so the same shutdown path cleans it up even for the single-layer
16888
- * fast path that bind-mounts the dir directly.
17804
+ * `forwardAwsEnv` only reads `process.env.AWS_*`, which is empty for
17805
+ * every shape except "user manually exported the env vars". The
17806
+ * Lambda container therefore boots without creds and the handler's
17807
+ * AWS SDK call fails with `Could not load credentials from any providers`.
16889
17808
  *
16890
- * AWS Lambda's actual runtime extracts every layer ZIP into `/opt`
16891
- * in template order the merge mirrors that. Docker rejects multiple
16892
- * `-v ...:/opt:ro` entries at the same target, so cdk-local can't rely on
16893
- * overlay layering and must produce a single merged dir on the host.
17809
+ * This helper constructs a transient `STSClient({ profile })` to drive
17810
+ * the SDK's default credential provider chain same code path the
17811
+ * host's own AWS SDK clients use when `--profile` is set, so SSO / IAM
17812
+ * Identity Center / role-assumption profiles all resolve the same way
17813
+ * they already do for the host's outbound calls. We then extract the
17814
+ * resolved `AwsCredentialIdentity` via `sts.config.credentials()` and
17815
+ * return the underlying `{ accessKeyId, secretAccessKey, sessionToken? }`
17816
+ * for env-var injection.
17817
+ *
17818
+ * Called ONCE at server boot; the resolved creds are reused for every
17819
+ * Lambda container's env overlay (when `--assume-role` is not set for
17820
+ * that Lambda — assume-role wins per the existing precedence). SSO
17821
+ * temp creds typically last 1h+, so a single resolve is fine for the
17822
+ * common dev session; long-running `--watch` sessions that outlive
17823
+ * the creds need a cdk-local restart (deferred refresh out of scope for
17824
+ * v1, see issue #654).
17825
+ *
17826
+ * Also resolves the profile's configured `region` (from `~/.aws/config`'s
17827
+ * `region = ...` for this profile) off the same client config. The
17828
+ * synthesized credentials file we mount into the container carries only
17829
+ * the credential triple — no `region =` — so without this the container
17830
+ * boots with creds but no region, and a handler's ambient-region SDK call
17831
+ * (`new XxxClient({})`) fails with "Region is missing". The caller seeds
17832
+ * the container's `AWS_REGION` fallback with it (see
17833
+ * {@link resolveContainerFallbackRegion}). Region resolution is
17834
+ * best-effort: a profile with no region (or any resolution failure)
17835
+ * yields `region: undefined` rather than throwing — a missing region is
17836
+ * not a missing-credentials error.
16894
17837
  */
16895
- async function materializeLambdaLayers(layers, layerTmpDirs, layerRoleArn) {
16896
- if (layers.length === 0) return void 0;
16897
- const flat = [];
16898
- for (const layer of layers) {
16899
- if (layer.kind === "asset") {
16900
- flat.push({
16901
- logicalId: layer.logicalId,
16902
- assetPath: layer.assetPath
16903
- });
16904
- continue;
17838
+ async function resolveProfileCredentials(profile) {
17839
+ const { STSClient } = await import("@aws-sdk/client-sts");
17840
+ const sts = new STSClient({ profile });
17841
+ try {
17842
+ const credsProvider = sts.config.credentials;
17843
+ const creds = typeof credsProvider === "function" ? await credsProvider() : credsProvider;
17844
+ 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.");
17845
+ let region;
17846
+ try {
17847
+ const regionProvider = sts.config.region;
17848
+ const resolved = typeof regionProvider === "function" ? await regionProvider() : regionProvider;
17849
+ if (typeof resolved === "string" && resolved.length > 0) region = resolved;
17850
+ } catch {
17851
+ region = void 0;
16905
17852
  }
16906
- const dir = await materializeLayerFromArn(layer, { ...layerRoleArn !== void 0 && { roleArn: layerRoleArn } });
16907
- layerTmpDirs.add(dir);
16908
- flat.push({
16909
- logicalId: layer.arn,
16910
- assetPath: dir
16911
- });
16912
- }
16913
- if (flat.length === 1) return flat[0].assetPath;
16914
- const dir = mkdtempSync(path.join(tmpdir(), `${getEmbedConfig().resourceNamePrefix}-start-api-layers-`));
16915
- for (const layer of flat) cpSync(layer.assetPath, dir, {
16916
- recursive: true,
16917
- force: true
16918
- });
16919
- layerTmpDirs.add(dir);
16920
- return dir;
16921
- }
16922
- function resolveLambdaByLogicalId(logicalId, stacks) {
16923
- for (const stack of stacks) {
16924
- const resource = stack.template.Resources?.[logicalId];
16925
- if (!resource || resource.Type !== "AWS::Lambda::Function") continue;
16926
- const props = resource.Properties ?? {};
16927
- const memoryMb = typeof props["MemorySize"] === "number" ? props["MemorySize"] : 128;
16928
- const timeoutSec = typeof props["Timeout"] === "number" ? props["Timeout"] : 3;
16929
- const code = props["Code"] ?? {};
16930
- const imageUri = extractImageUri(code["ImageUri"], logicalId, stack.stackName, stack.template.Resources ?? {}, stack.region);
16931
- if (imageUri !== void 0) return resolveImageLambda({
16932
- stack,
16933
- logicalId,
16934
- resource,
16935
- props,
16936
- memoryMb,
16937
- timeoutSec,
16938
- imageUri
16939
- });
16940
- const runtime = typeof props["Runtime"] === "string" ? props["Runtime"] : "";
16941
- const handler = typeof props["Handler"] === "string" ? props["Handler"] : "";
16942
- if (!runtime) throw new Error(`Lambda '${logicalId}' has no Runtime property and no Code.ImageUri. ${getEmbedConfig().cliName} start-api cannot tell if this is a ZIP or a container Lambda.`);
16943
- if (!handler) throw new Error(`Lambda '${logicalId}' has no Handler property.`);
16944
- const inlineCode = typeof code["ZipFile"] === "string" ? code["ZipFile"] : void 0;
16945
- let codePath = null;
16946
- if (!inlineCode) codePath = resolveAssetCodePath(stack, logicalId, resource);
16947
- const layers = resolveLambdaLayers(stack, logicalId, props);
16948
- const ephemeralStorageMb = extractEphemeralStorageMb(props, logicalId);
16949
17853
  return {
16950
- kind: "zip",
16951
- stack,
16952
- logicalId,
16953
- resource,
16954
- runtime,
16955
- handler,
16956
- memoryMb,
16957
- timeoutSec,
16958
- codePath,
16959
- layers,
16960
- ...inlineCode !== void 0 && { inlineCode },
16961
- ...ephemeralStorageMb !== void 0 && { ephemeralStorageMb }
17854
+ accessKeyId: creds.accessKeyId,
17855
+ secretAccessKey: creds.secretAccessKey,
17856
+ ...creds.sessionToken && { sessionToken: creds.sessionToken },
17857
+ ...region && { region }
16962
17858
  };
17859
+ } finally {
17860
+ sts.destroy();
16963
17861
  }
16964
- throw new Error(`No AWS::Lambda::Function resource named '${logicalId}' found in target stacks. This is likely a synthesis bug — the route-discovery phase resolved a route to this logical ID.`);
16965
17862
  }
16966
17863
  /**
16967
- * Extract `Code.ImageUri` across the shapes CDK actually synthesizes.
16968
- * Mirrors the simpler subset of `lambda-resolver.ts:extractImageUri`
16969
- * scoped to the shapes `cdkl start-api` consumes flat string,
16970
- * `Fn::Sub` (the canonical asset shape for
16971
- * `lambda.DockerImageCode.fromImageAsset`), and `Fn::Join` (the
16972
- * canonical shape for `lambda.DockerImageCode.fromImageAsset` in
16973
- * CDK 2.x, which emits a `Fn::Join` over the literal bootstrap ECR
16974
- * URI with `${AWS::URLSuffix}` — issues #627 + #637).
17864
+ * Resolve the fallback `AWS_REGION` for a Lambda container when neither
17865
+ * the assume-role STS region nor a forwarded `AWS_REGION` /
17866
+ * `AWS_DEFAULT_REGION` env var already set one.
16975
17867
  *
16976
- * The `Fn::Join` arm routes through the shared
16977
- * `tryResolveImageFnJoin` helper (`src/local/intrinsic-image.ts`) used
16978
- * by `cdkl invoke`. When the synth template recorded a deploy
16979
- * region (`stack.region`), we derive `{ urlSuffix, partition, region }`
16980
- * via `derivePseudoParametersFromRegion` and pass it as the resolver's
16981
- * `pseudoParameters` block so the canonical `${AWS::URLSuffix}` shape
16982
- * resolves cleanly (issue #637). Same-stack ECR refs still surface as
16983
- * `needs-state` (those require `--from-state`); a Join that references
16984
- * `${AWS::AccountId}` without state still surfaces as `not-applicable`
16985
- * with a more specific error message naming the missing parameter.
17868
+ * Precedence mirrors where the deployed function would actually run, so a
17869
+ * handler's ambient-region SDK call (`new XxxClient({})`) reaches the same
17870
+ * region locally as the resources it is bound to:
16986
17871
  *
16987
- * Returns `undefined` when the field is absent or non-recognized,
16988
- * which routes the caller to the ZIP branch (with its existing
16989
- * "no Runtime / no Handler" validations).
17872
+ * 1. `--stack-region` explicit; also drives the `--from-cfn-stack` CFn
17873
+ * client, so the container matches the region the bound stack was read
17874
+ * from.
17875
+ * 2. the synth-derived stack region — `env.region` on the CDK stack, read
17876
+ * from the cloud assembly manifest (`StackInfo.region`). Previously
17877
+ * used only host-side for the `--from-cfn-stack` CFn client; never
17878
+ * injected into the container.
17879
+ * 3. the `--profile`'s configured region — `~/.aws/config`'s `region =`
17880
+ * for the profile. `--profile` injected credentials but not this.
17881
+ *
17882
+ * Returns `undefined` when none is known; the caller leaves `AWS_REGION`
17883
+ * unset so the SDK's own "Region is missing" surfaces (the correct signal
17884
+ * for a region-agnostic stack with no profile region and no `AWS_REGION`
17885
+ * env).
16990
17886
  */
16991
- function extractImageUri(value, logicalId, stackName, resources, region) {
16992
- if (typeof value === "string" && value.length > 0) return value;
16993
- if (value && typeof value === "object" && !Array.isArray(value)) {
16994
- const obj = value;
16995
- const sub = obj["Fn::Sub"];
16996
- if (typeof sub === "string" && sub.length > 0) return sub;
16997
- if (Array.isArray(sub) && typeof sub[0] === "string") return sub[0];
16998
- if ("Fn::Join" in obj) {
16999
- const pseudoParameters = derivePseudoParametersFromRegion(region);
17000
- const joinResolved = tryResolveImageFnJoin(value, resources, pseudoParameters ? { pseudoParameters } : void 0);
17001
- if (joinResolved.kind === "resolved") return joinResolved.uri;
17002
- if (joinResolved.kind === "needs-state") throw new Error(`Lambda '${logicalId}' in ${stackName} references same-stack ECR repository '${joinResolved.repoLogicalId}' via Fn::Join. ${getEmbedConfig().cliName} start-api cannot resolve the repository URI without state — deploy the stack first, rebuild via lambda.DockerImageCode.fromImageAsset, or pin a public image.`);
17003
- if (joinResolved.kind === "unsupported-join") throw new Error(`Lambda '${logicalId}' in ${stackName} has an unsupported Fn::Join Code.ImageUri shape: ${joinResolved.reason}. ${getEmbedConfig().cliName} start-api recognizes the canonical CDK 2.x lambda.DockerImageCode.fromEcr Fn::Join shape (delimiter "" with nested Fn::Select/Fn::Split over an ECR Repository Arn GetAtt + Ref to the repo).`);
17004
- const accountIdHint = pseudoParameters ? ` (likely \${AWS::AccountId}, which ${getEmbedConfig().binaryName} cannot derive without a state source or STS)` : ` (${getEmbedConfig().binaryName} could not derive AWS pseudo parameters because stack.region was undefined)`;
17005
- throw new Error(`Lambda '${logicalId}' in ${stackName} has an Fn::Join Code.ImageUri that ${getEmbedConfig().cliName} start-api cannot resolve${accountIdHint}. Workarounds: deploy first and run with a state-source flag (e.g. --from-cfn-stack or a host-provided extension), or pin a fully-literal public image URI.`);
17006
- }
17887
+ function resolveContainerFallbackRegion(args) {
17888
+ return args.stackRegionOverride ?? args.synthRegion ?? args.profileRegion;
17889
+ }
17890
+ /**
17891
+ * Issue an STS AssumeRole and return temporary credentials. Mirrors
17892
+ * `cdkl invoke`'s helper byte-for-byte; lifted here so the
17893
+ * start-api command stays self-contained.
17894
+ */
17895
+ async function assumeLambdaExecutionRole(roleArn, region) {
17896
+ const { STSClient, AssumeRoleCommand } = await import("@aws-sdk/client-sts");
17897
+ const sts = new STSClient({ ...region && { region } });
17898
+ try {
17899
+ const creds = (await sts.send(new AssumeRoleCommand({
17900
+ RoleArn: roleArn,
17901
+ RoleSessionName: `${getEmbedConfig().resourceNamePrefix}-start-api-${Date.now()}`,
17902
+ DurationSeconds: 3600
17903
+ }))).Credentials;
17904
+ if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) throw new Error(`AssumeRole(${roleArn}) returned no usable credentials.`);
17905
+ return {
17906
+ accessKeyId: creds.AccessKeyId,
17907
+ secretAccessKey: creds.SecretAccessKey,
17908
+ sessionToken: creds.SessionToken
17909
+ };
17910
+ } finally {
17911
+ sts.destroy();
17007
17912
  }
17008
17913
  }
17009
17914
  /**
17010
- * Build the IMAGE-variant `ResolvedStartApiLambda` from a Lambda
17011
- * template entry with `Code.ImageUri`. Mirrors
17012
- * `lambda-resolver.ts:extractImageLambdaProperties` but trimmed to the
17013
- * fields `cdkl start-api` actually consumes.
17915
+ * Parse / clamp the `--per-lambda-concurrency` flag. Above-cap values
17916
+ * are clamped to 4 with a warn line (per the spec doc's risk-mitigation
17917
+ * row).
17014
17918
  */
17015
- function resolveImageLambda(args) {
17016
- const { stack, logicalId, resource, props, memoryMb, timeoutSec, imageUri } = args;
17017
- const rawImageConfig = props["ImageConfig"] ?? {};
17018
- const imageConfig = {};
17019
- if (Array.isArray(rawImageConfig["Command"])) imageConfig.command = rawImageConfig["Command"].filter((s) => typeof s === "string");
17020
- if (Array.isArray(rawImageConfig["EntryPoint"])) imageConfig.entryPoint = rawImageConfig["EntryPoint"].filter((s) => typeof s === "string");
17021
- if (typeof rawImageConfig["WorkingDirectory"] === "string") imageConfig.workingDirectory = rawImageConfig["WorkingDirectory"];
17022
- const arches = props["Architectures"];
17023
- let architecture = "x86_64";
17024
- if (Array.isArray(arches) && arches.length > 0) {
17025
- const first = arches[0];
17026
- if (first === "arm64") architecture = "arm64";
17027
- else if (first === "x86_64") architecture = "x86_64";
17028
- else throw new Error(`Lambda '${logicalId}' has unsupported Architectures value '${String(first)}'. ${getEmbedConfig().cliName} start-api supports x86_64 and arm64.`);
17919
+ function parsePerLambdaConcurrency(raw) {
17920
+ const parsed = parseInt(raw, 10);
17921
+ if (!Number.isFinite(parsed) || parsed < 1) throw new Error(`--per-lambda-concurrency must be a positive integer (got '${raw}')`);
17922
+ if (parsed > 4) {
17923
+ getLogger().warn(`--per-lambda-concurrency ${parsed} exceeds the v1 cap of 4; clamping to 4. (Raise this in a follow-up PR if your workload needs more.)`);
17924
+ return 4;
17029
17925
  }
17030
- const ephemeralStorageMb = extractEphemeralStorageMb(props, logicalId);
17031
- return {
17032
- kind: "image",
17033
- stack,
17034
- logicalId,
17035
- resource,
17036
- memoryMb,
17037
- timeoutSec,
17038
- imageUri,
17039
- imageConfig,
17040
- architecture,
17041
- layers: [],
17042
- ...ephemeralStorageMb !== void 0 && { ephemeralStorageMb }
17043
- };
17926
+ return parsed;
17044
17927
  }
17045
17928
  /**
17046
- * Locate the Lambda's local code directory using the CDK-blessed
17047
- * `Metadata['aws:asset:path']` hint. Bind-mounted directly at
17048
- * `/var/task` (read-only) by the docker-runner.
17929
+ * Filter the global Lambda spec map to just the Lambdas reachable from
17930
+ * one API server group. The container pool for that server is built
17931
+ * from this filtered map so per-API authorizer Lambdas + route
17932
+ * handlers stay scoped to their owning server — disposing one server's
17933
+ * pool on shutdown does NOT touch another server's still-warm
17934
+ * containers.
17935
+ *
17936
+ * Also includes any authorizer Lambdas attached to the group's routes
17937
+ * (a Lambda authorizer is a Lambda the pool needs to know about, even
17938
+ * though no route directly handles `lambdaLogicalId === auth.lambdaLogicalId`).
17049
17939
  */
17050
- function resolveAssetCodePath(stack, logicalId, resource) {
17051
- const assetPath = resource.Metadata?.["aws:asset:path"];
17052
- if (typeof assetPath !== "string" || assetPath.length === 0) throw new Error(`Lambda '${logicalId}' has no Metadata['aws:asset:path']. ${getEmbedConfig().cliName} start-api needs this hint to find the local asset directory. Re-synthesize the app and retry.`);
17053
- const cdkOutDir = stack.assetManifestPath ? path.dirname(stack.assetManifestPath) : process.cwd();
17054
- return path.isAbsolute(assetPath) ? assetPath : path.resolve(cdkOutDir, assetPath);
17940
+ function filterSpecsForGroup(group, allSpecs) {
17941
+ const ids = /* @__PURE__ */ new Set();
17942
+ for (const rwa of group.routes) {
17943
+ ids.add(rwa.route.lambdaLogicalId);
17944
+ const auth = rwa.authorizer;
17945
+ if (auth && (auth.kind === "lambda-token" || auth.kind === "lambda-request")) ids.add(auth.lambdaLogicalId);
17946
+ }
17947
+ const out = /* @__PURE__ */ new Map();
17948
+ for (const id of ids) {
17949
+ const spec = allSpecs.get(id);
17950
+ if (spec) out.set(id, spec);
17951
+ }
17952
+ return out;
17055
17953
  }
17056
17954
  /**
17057
- * Print the discovered route table to stdout. Format mirrors the spec
17058
- * doc's example so verify.sh / users can read it at a glance.
17059
- *
17060
- * Routes with `unsupported` or `mockCors` are annotated so the user can
17061
- * tell at a glance which routes will dispatch to a Lambda vs which
17062
- * return 501 / 204 directly:
17063
- * - normal: `GET /items -> Handler (HTTP API)`
17064
- * - mockCors: `OPTIONS /items -> [MOCK CORS preflight] (REST v1, stage 'prod')`
17065
- * - unsupported: `POST /admin -> [501 Not Implemented] (HTTP API)`
17955
+ * Print one route table per server, with the server's display name as
17956
+ * the section header. Replaces the pre-issue #260 single flat table
17957
+ * users now see exactly which routes belong to which API + port.
17066
17958
  */
17067
- function printRouteTable(routes) {
17068
- const sorted = [...routes.map((r) => r.route)].sort((a, b) => {
17069
- if (a.pathPattern !== b.pathPattern) return a.pathPattern.localeCompare(b.pathPattern);
17070
- return a.method.localeCompare(b.method);
17071
- });
17072
- const methodWidth = Math.max(...sorted.map((r) => r.method.length), 6);
17073
- const pathWidth = Math.max(...sorted.map((r) => r.pathPattern.length), 8);
17074
- process.stdout.write("Discovered routes:\n");
17075
- for (const r of sorted) {
17076
- const sourceLabel = r.source === "http-api" ? "HTTP API" : r.source === "rest-v1" ? `REST v1, stage '${r.stage}'` : "Function URL";
17077
- const target = r.mockCors ? "[MOCK CORS preflight]" : r.unsupported ? "[501 Not Implemented]" : r.serviceIntegration ? `[${r.serviceIntegration.subtype}]` : r.restV1Integration ? formatRestV1IntegrationLabel(r.restV1Integration) : r.lambdaLogicalId;
17078
- process.stdout.write(` ${r.method.padEnd(methodWidth)} ${r.pathPattern.padEnd(pathWidth)} -> ${target} (${sourceLabel})\n`);
17959
+ function printPerServerRouteTables(servers) {
17960
+ for (const { group, server } of servers) {
17961
+ process.stdout.write(`\n${group.displayName} (http://${server.host}:${server.port})\n`);
17962
+ printRouteTable(group.routes);
17079
17963
  }
17080
- process.stdout.write("\n");
17081
17964
  }
17082
17965
  /**
17083
- * Format the route-table label for a REST v1 non-AWS_PROXY integration.
17084
- * `MOCK` / `HTTP` / `HTTP_PROXY` show their integration kind directly;
17085
- * `AWS` (Lambda non-proxy) shows the Lambda logical id with an `[AWS]`
17086
- * suffix so it's distinguishable from AWS_PROXY rows. Closes #457.
17966
+ * Surface every `unsupported` route (deferred 501) as a startup warn so
17967
+ * the user sees what isn't reachable BEFORE they try to curl it. One
17968
+ * warn line per route the route's `unsupported.reason` already names
17969
+ * the offender + the underlying limitation, so we just prefix with
17970
+ * method + path. Returns the number of unsupported routes so the caller
17971
+ * can emit a single-line summary header above the list.
17087
17972
  */
17088
- function formatRestV1IntegrationLabel(integration) {
17089
- switch (integration.kind) {
17090
- case "mock": return "[MOCK]";
17091
- case "http-proxy": return `[HTTP_PROXY ${integration.uri}]`;
17092
- case "http": return `[HTTP ${integration.uri}]`;
17093
- case "aws-lambda": return `${integration.lambdaLogicalId} [AWS]`;
17094
- }
17973
+ function warnUnsupportedRoutes(routes, logger) {
17974
+ const unsupported = routes.filter((r) => r.unsupported);
17975
+ if (unsupported.length === 0) return 0;
17976
+ logger.warn(`${unsupported.length} route(s) will respond HTTP 501 Not Implemented when hit (boot continued):`);
17977
+ for (const r of unsupported) logger.warn(` - ${r.method} ${r.pathPattern}: ${r.unsupported.reason}`);
17978
+ return unsupported.length;
17095
17979
  }
17096
17980
  /**
17097
- * Materialize an inline Lambda body (`Code.ZipFile`) to a tmpdir and
17098
- * return the directory the container should mount at /var/task.
17099
- * Mirrors `cdkl invoke`'s implementation; the only divergence is
17100
- * the long-running-server lifecycle: every tmpdir created here is
17101
- * recorded in `tmpDirsOut` so the caller's shutdown path can `rmSync`
17102
- * them. (`cdkl invoke` runs once and `--rm` is the right model;
17103
- * `cdkl start-api` lives across requests, so leaks compound.)
17981
+ * Surface every WebSocket API tagged as unsupported at discovery as a
17982
+ * startup warn. The boot loop above skips attaching the server for
17983
+ * these APIs, so no upgrade requests are ever accepted on them —
17984
+ * mirrors `warnUnsupportedRoutes`'s shape but for the WebSocket axis.
17985
+ * Typical trigger: a Route declaring `AuthorizationType !== 'NONE'` on
17986
+ * `$connect` (cdkl v1 does not emulate WebSocket authorizers; closing
17987
+ * this gap structurally rather than silently admitting
17988
+ * unauthenticated clients matches the security-by-default precedent
17989
+ * PR #514 set for HTTP API v2 service integrations).
17104
17990
  */
17105
- function materializeInlineCode$1(handler, source, fileExtension, tmpDirsOut) {
17106
- const lastDot = handler.lastIndexOf(".");
17107
- if (lastDot <= 0) throw new Error(`Handler '${handler}' is malformed: expected '<modulePath>.<exportName>'.`);
17108
- const modulePath = handler.substring(0, lastDot);
17109
- const dir = mkdtempSync(path.join(tmpdir(), `${getEmbedConfig().resourceNamePrefix}-start-api-`));
17110
- tmpDirsOut.add(dir);
17111
- const filePath = path.join(dir, `${modulePath}${fileExtension}`);
17112
- mkdirSync(path.dirname(filePath), { recursive: true });
17113
- writeFileSync(filePath, source, "utf-8");
17114
- return dir;
17991
+ function warnUnsupportedWebSocketApis(apis, logger) {
17992
+ const unsupported = apis.filter((api) => api.unsupported);
17993
+ if (unsupported.length === 0) return 0;
17994
+ logger.warn(`${unsupported.length} WebSocket API(s) will NOT accept upgrade requests (boot continued):`);
17995
+ for (const api of unsupported) logger.warn(` - ${api.declaredAt}: ${api.unsupported.reason}`);
17996
+ return unsupported.length;
17115
17997
  }
17116
- /** Pull `Properties.Environment.Variables` (when present). */
17117
- function getTemplateEnv(resource) {
17118
- const env = (resource.Properties ?? {})["Environment"];
17119
- if (!env || typeof env !== "object") return void 0;
17120
- const vars = env["Variables"];
17121
- if (!vars || typeof vars !== "object") return void 0;
17122
- return vars;
17998
+ /**
17999
+ * Surface a one-line warn per HTTP / HTTP_PROXY integration whose
18000
+ * `Integration.Uri` points at a well-known internal address space
18001
+ * (AWS IMDS, loopback, link-local, RFC1918). PR #505 / issue #457
18002
+ * follow-up: cdk-local does NOT block these — warn-and-proceed matches the
18003
+ * cognito JWKS pass-through pattern but the user should see the
18004
+ * destination at boot so a malicious / typo'd template Uri does not
18005
+ * silently exfiltrate credentials in CI. Deduplicated per-Uri.
18006
+ */
18007
+ function warnSsrfRiskyIntegrations(routes, logger) {
18008
+ const seen = /* @__PURE__ */ new Set();
18009
+ for (const r of routes) {
18010
+ const integ = r.restV1Integration;
18011
+ if (!integ) continue;
18012
+ if (integ.kind !== "http" && integ.kind !== "http-proxy") continue;
18013
+ if (seen.has(integ.uri)) continue;
18014
+ seen.add(integ.uri);
18015
+ warnSsrfRiskyUri(integ.uri, `${r.method} ${r.pathPattern}`, (msg) => logger.warn(msg));
18016
+ }
17123
18017
  }
17124
- /** Read the SAM-shape `--env-vars` JSON file. */
17125
- function readEnvOverridesFile$1(filePath) {
17126
- if (!filePath) return void 0;
17127
- let raw;
18018
+ /**
18019
+ * One reload cycle for the multi-server topology (issue #260). The
18020
+ * watcher serializes calls via a chain promise; this function:
18021
+ *
18022
+ * 1. Re-runs `synthesizeAndBuild()` once (failure → warn + keep
18023
+ * previous version serving on every server).
18024
+ * 2. Re-groups the new routes by API server key.
18025
+ * 3. For each existing server, swaps state to the new group's
18026
+ * routes + a freshly-built pool filtered to that group's
18027
+ * Lambdas. Disposes the previous pool in the background.
18028
+ * 4. Warns about new groups (= an API was added in CDK code) and
18029
+ * vanished groups (= an API was removed) — those require a
18030
+ * server restart in v1.
18031
+ */
18032
+ async function reloadAllServers(args) {
18033
+ const { synthesizeAndBuild, servers, buildPool, logger } = args;
18034
+ let material;
17128
18035
  try {
17129
- raw = readFileSync(filePath, "utf-8");
18036
+ material = await synthesizeAndBuild();
17130
18037
  } catch (err) {
17131
- throw new Error(`Failed to read --env-vars file '${filePath}': ${err instanceof Error ? err.message : String(err)}`);
18038
+ logger.warn(`cdk synth failed during reload; keeping previous version. (${err instanceof Error ? err.message : String(err)})`);
18039
+ return;
17132
18040
  }
17133
- let parsed;
17134
- try {
17135
- parsed = JSON.parse(raw);
17136
- } catch (err) {
17137
- throw new Error(`Failed to parse --env-vars file '${filePath}' as JSON: ${err instanceof Error ? err.message : String(err)}`);
18041
+ const newGroups = groupRoutesByServer(material.routes);
18042
+ const newByKey = new Map(newGroups.map((g) => [g.serverKey, g]));
18043
+ const oldKeys = new Set(servers.map((s) => s.group.serverKey));
18044
+ const newKeys = new Set(newByKey.keys());
18045
+ const added = [...newKeys].filter((k) => !oldKeys.has(k));
18046
+ const removed = [...oldKeys].filter((k) => !newKeys.has(k));
18047
+ if (added.length > 0) logger.warn(`Reload detected new API surface(s): ${added.join(", ")}. Restart '${getEmbedConfig().cliName} start-api' to serve them.`);
18048
+ if (removed.length > 0) logger.warn(`Reload detected removed API surface(s): ${removed.join(", ")}. Their servers will keep serving stale routes until restart.`);
18049
+ for (const booted of servers) {
18050
+ const group = newByKey.get(booted.group.serverKey);
18051
+ if (!group) continue;
18052
+ const newPool = buildPool(filterSpecsForGroup(group, material.specs));
18053
+ const newState = {
18054
+ routes: group.routes,
18055
+ pool: newPool,
18056
+ corsConfigByApiId: material.corsConfigByApiId
18057
+ };
18058
+ const previousState = booted.server.setServerState(newState);
18059
+ booted.group = group;
18060
+ previousState.pool.dispose().catch((err) => {
18061
+ logger.debug(`Previous pool dispose() failed for ${group.displayName}: ${err instanceof Error ? err.message : String(err)}`);
18062
+ });
18063
+ }
18064
+ printPerServerRouteTables(servers);
18065
+ const allRoutes = servers.flatMap((s) => s.group.routes.map((r) => r.route));
18066
+ warnUnsupportedRoutes(allRoutes, logger);
18067
+ warnSsrfRiskyIntegrations(allRoutes, logger);
18068
+ }
18069
+ /**
18070
+ * Returns true when any value in the function's template env map is a
18071
+ * CFn intrinsic (non-primitive). Used to gate the pseudo-parameter STS
18072
+ * hop inside the `--from-state` flow: literal-only env maps don't need
18073
+ * the pseudo-parameter bag and shouldn't pay for an STS call. Mirrors
18074
+ * the same gating in `local-invoke.ts` (`envHasIntrinsicValue`) and
18075
+ * `ecs-task-resolver.ts` (`containerHasIntrinsicEnvOrSecret`).
18076
+ */
18077
+ function envHasIntrinsicValue(templateEnv) {
18078
+ if (!templateEnv) return false;
18079
+ for (const v of Object.values(templateEnv)) {
18080
+ if (v === void 0 || v === null) continue;
18081
+ if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") continue;
18082
+ return true;
17138
18083
  }
17139
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`--env-vars file '${filePath}' must contain a JSON object at the top level.`);
17140
- return parsed;
18084
+ return false;
17141
18085
  }
17142
18086
  /**
17143
- * Forward the developer's AWS credentials into the container so the
17144
- * handler's AWS SDK calls can authenticate. Used when --assume-role is
17145
- * NOT set for that Lambda SAM-compatible default.
18087
+ * Load deployed state for every stack that owns a routed Lambda. Once
18088
+ * per `synthesizeAndBuild` pass (initial boot + every reload), so a
18089
+ * Lambda's per-spec build does not pay one round-trip per Lambda. Per-
18090
+ * stack failures (no state, ambiguous region, bucket resolution error)
18091
+ * degrade to warn-and-fall-back via the active `LocalStateProvider` —
18092
+ * the affected stack's reachable Lambdas behave as if `--from-state` /
18093
+ * `--from-cfn-stack` were not set, while sibling stacks with loadable
18094
+ * state still substitute.
18095
+ *
18096
+ * Pseudo parameters are resolved per stack and only when at least one
18097
+ * reachable Lambda in that stack has an intrinsic-valued env entry
18098
+ * (gated via {@link envHasIntrinsicValue}). STS failures degrade to
18099
+ * warn and leave `pseudoParameters: undefined` — substitution still
18100
+ * runs for non-`AWS::*` refs.
17146
18101
  */
17147
- function forwardAwsEnv$1(env) {
17148
- for (const key of [
17149
- "AWS_ACCESS_KEY_ID",
17150
- "AWS_SECRET_ACCESS_KEY",
17151
- "AWS_SESSION_TOKEN",
17152
- "AWS_REGION",
17153
- "AWS_DEFAULT_REGION"
17154
- ]) {
17155
- const value = process.env[key];
17156
- if (value !== void 0) env[key] = value;
18102
+ /**
18103
+ * Returns true when at least one host-provided extra state-provider flag
18104
+ * is active in the options bag.
18105
+ */
18106
+ function hasExtraStateProviderActive(options, extraStateProviders) {
18107
+ if (!extraStateProviders) return false;
18108
+ for (const key of Object.keys(extraStateProviders)) if (options[key]) return true;
18109
+ return false;
18110
+ }
18111
+ async function loadStateForRoutedStacks(stacks, routes, routesWithAuth, options, extraStateProviders) {
18112
+ const logger = getLogger();
18113
+ const out = /* @__PURE__ */ new Map();
18114
+ const lambdaIds = uniqueLambdaIds(routes, routesWithAuth);
18115
+ const reachableStackNames = /* @__PURE__ */ new Set();
18116
+ for (const logicalId of lambdaIds) for (const stack of stacks) {
18117
+ const resource = stack.template.Resources?.[logicalId];
18118
+ if (resource && resource.Type === "AWS::Lambda::Function") {
18119
+ reachableStackNames.add(stack.stackName);
18120
+ break;
18121
+ }
18122
+ }
18123
+ const stackHasIntrinsicEnv = (stackName) => {
18124
+ for (const logicalId of lambdaIds) for (const stack of stacks) {
18125
+ if (stack.stackName !== stackName) continue;
18126
+ const resource = stack.template.Resources?.[logicalId];
18127
+ if (!resource || resource.Type !== "AWS::Lambda::Function") continue;
18128
+ if (envHasIntrinsicValue(getTemplateEnv(resource))) return true;
18129
+ }
18130
+ return false;
18131
+ };
18132
+ rejectExplicitCfnStackWithMultipleStacks(options, reachableStackNames.size);
18133
+ for (const stackName of reachableStackNames) {
18134
+ const stack = stacks.find((s) => s.stackName === stackName);
18135
+ if (!stack) continue;
18136
+ const provider = createLocalStateProvider(options, stack.stackName, await resolveCfnFallbackRegion(options, stack.region), extraStateProviders);
18137
+ if (!provider) continue;
18138
+ try {
18139
+ const loaded = await provider.load(stack.stackName, stack.region);
18140
+ if (!loaded) continue;
18141
+ const bundle = { state: {
18142
+ version: 1,
18143
+ stackName: stack.stackName,
18144
+ resources: loaded.resources,
18145
+ outputs: loaded.outputs,
18146
+ lastModified: 0
18147
+ } };
18148
+ if (stackHasIntrinsicEnv(stackName)) {
18149
+ const pseudo = await resolvePseudoParametersForStartApi(loaded.region, options);
18150
+ if (pseudo) bundle.pseudoParameters = pseudo;
18151
+ }
18152
+ if (provider.resolveDeployedFunctionEnv) {
18153
+ const deployedEnvByLambda = /* @__PURE__ */ new Map();
18154
+ for (const logicalId of lambdaIds) {
18155
+ const resource = stack.template.Resources?.[logicalId];
18156
+ if (!resource || resource.Type !== "AWS::Lambda::Function") continue;
18157
+ if (!envHasIntrinsicValue(getTemplateEnv(resource))) continue;
18158
+ const physicalId = loaded.resources[logicalId]?.physicalId;
18159
+ if (!physicalId) continue;
18160
+ const deployedEnv = await provider.resolveDeployedFunctionEnv(physicalId);
18161
+ if (deployedEnv) deployedEnvByLambda.set(logicalId, deployedEnv);
18162
+ }
18163
+ if (deployedEnvByLambda.size > 0) bundle.deployedEnvByLambda = deployedEnvByLambda;
18164
+ }
18165
+ if (stackHasIntrinsicEnv(stackName) && provider.resolveTemplateSsmParameters) {
18166
+ const ssmParameters = await provider.resolveTemplateSsmParameters(stack.template);
18167
+ if (Object.keys(ssmParameters.values).length > 0) bundle.ssmParameters = ssmParameters.values;
18168
+ if (ssmParameters.secureStringLogicalIds.length > 0) bundle.ssmSecureStringLogicalIds = ssmParameters.secureStringLogicalIds;
18169
+ }
18170
+ out.set(stackName, bundle);
18171
+ logger.debug(`${provider.label}: loaded state for ${stackName} (${loaded.region})`);
18172
+ } finally {
18173
+ provider.dispose();
18174
+ }
17157
18175
  }
18176
+ return out;
17158
18177
  }
17159
18178
  /**
17160
- * Issue #654: resolve `--profile <p>` to a concrete credential set
17161
- * for forwarding to Lambda containers.
17162
- *
17163
- * The dev's AWS credentials may live in any of:
17164
- * - `~/.aws/sso/cache/*.json` (AWS IAM Identity Center / legacy SSO)
17165
- * - `~/.aws/credentials` (regular long-lived access keys)
17166
- * - `~/.aws/config` profiles with `role_arn` + `source_profile` (chained AssumeRole)
17167
- * - `credential_process` external resolvers
17168
- *
17169
- * `forwardAwsEnv` only reads `process.env.AWS_*`, which is empty for
17170
- * every shape except "user manually exported the env vars". The
17171
- * Lambda container therefore boots without creds and the handler's
17172
- * AWS SDK call fails with `Could not load credentials from any providers`.
17173
- *
17174
- * This helper constructs a transient `STSClient({ profile })` to drive
17175
- * the SDK's default credential provider chain — same code path the
17176
- * host's own AWS SDK clients use when `--profile` is set, so SSO / IAM
17177
- * Identity Center / role-assumption profiles all resolve the same way
17178
- * they already do for the host's outbound calls. We then extract the
17179
- * resolved `AwsCredentialIdentity` via `sts.config.credentials()` and
17180
- * return the underlying `{ accessKeyId, secretAccessKey, sessionToken? }`
17181
- * for env-var injection.
17182
- *
17183
- * Called ONCE at server boot; the resolved creds are reused for every
17184
- * Lambda container's env overlay (when `--assume-role` is not set for
17185
- * that Lambda — assume-role wins per the existing precedence). SSO
17186
- * temp creds typically last 1h+, so a single resolve is fine for the
17187
- * common dev session; long-running `--watch` sessions that outlive
17188
- * the creds need a cdk-local restart (deferred refresh out of scope for
17189
- * v1, see issue #654).
18179
+ * Build the AWS pseudo-parameter bag for `--from-state` env-var
18180
+ * substitution. Mirrors `resolvePseudoParametersForInvoke` in
18181
+ * `local-invoke.ts` byte-for-byte — kept inlined here rather than
18182
+ * extracted into a shared helper because the two call sites differ in
18183
+ * region precedence (this one is per-stack so the resolved state
18184
+ * region takes priority).
17190
18185
  *
17191
- * Also resolves the profile's configured `region` (from `~/.aws/config`'s
17192
- * `region = ...` for this profile) off the same client config. The
17193
- * synthesized credentials file we mount into the container carries only
17194
- * the credential triple — no `region =` — so without this the container
17195
- * boots with creds but no region, and a handler's ambient-region SDK call
17196
- * (`new XxxClient({})`) fails with "Region is missing". The caller seeds
17197
- * the container's `AWS_REGION` fallback with it (see
17198
- * {@link resolveContainerFallbackRegion}). Region resolution is
17199
- * best-effort: a profile with no region (or any resolution failure)
17200
- * yields `region: undefined` rather than throwing — a missing region is
17201
- * not a missing-credentials error.
18186
+ * Region precedence: `--region` > `AWS_REGION` > `AWS_DEFAULT_REGION` >
18187
+ * the state record's region (returned by the active `LocalStateProvider`).
17202
18188
  */
17203
- async function resolveProfileCredentials(profile) {
17204
- const { STSClient } = await import("@aws-sdk/client-sts");
17205
- const sts = new STSClient({ profile });
18189
+ async function resolvePseudoParametersForStartApi(stateRegion, options) {
18190
+ const logger = getLogger();
18191
+ const region = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? stateRegion;
18192
+ let accountId;
17206
18193
  try {
17207
- const credsProvider = sts.config.credentials;
17208
- const creds = typeof credsProvider === "function" ? await credsProvider() : credsProvider;
17209
- 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.");
17210
- let region;
18194
+ const { STSClient, GetCallerIdentityCommand } = await import("@aws-sdk/client-sts");
18195
+ const sts = new STSClient({ ...region && { region } });
17211
18196
  try {
17212
- const regionProvider = sts.config.region;
17213
- const resolved = typeof regionProvider === "function" ? await regionProvider() : regionProvider;
17214
- if (typeof resolved === "string" && resolved.length > 0) region = resolved;
17215
- } catch {
17216
- region = void 0;
18197
+ accountId = (await sts.send(new GetCallerIdentityCommand({}))).Account;
18198
+ } finally {
18199
+ sts.destroy();
17217
18200
  }
17218
- return {
17219
- accessKeyId: creds.accessKeyId,
17220
- secretAccessKey: creds.secretAccessKey,
17221
- ...creds.sessionToken && { sessionToken: creds.sessionToken },
17222
- ...region && { region }
17223
- };
17224
- } finally {
17225
- sts.destroy();
18201
+ } catch (err) {
18202
+ logger.warn(`--from-state: resolver needs \${AWS::AccountId} but STS GetCallerIdentity failed: ${err instanceof Error ? err.message : String(err)}. Substitution will be skipped for AWS::AccountId; affected env entries will be dropped with per-key warnings.`);
17226
18203
  }
18204
+ const partitionAndSuffix = region ? derivePartitionAndUrlSuffix(region) : void 0;
18205
+ const bag = {
18206
+ ...accountId !== void 0 && { accountId },
18207
+ ...region !== void 0 && { region },
18208
+ ...partitionAndSuffix && {
18209
+ partition: partitionAndSuffix.partition,
18210
+ urlSuffix: partitionAndSuffix.urlSuffix
18211
+ }
18212
+ };
18213
+ return Object.keys(bag).length === 0 ? void 0 : bag;
18214
+ }
18215
+ /** Validate `--debug-port-base`. */
18216
+ function parseDebugPort(raw) {
18217
+ const parsed = parseInt(raw, 10);
18218
+ if (!Number.isFinite(parsed) || parsed < 1 || parsed > 65535) throw new Error(`--debug-port-base must be 1..65535 (got '${raw}')`);
18219
+ return parsed;
17227
18220
  }
17228
18221
  /**
17229
- * Resolve the fallback `AWS_REGION` for a Lambda container when neither
17230
- * the assume-role STS region nor a forwarded `AWS_REGION` /
17231
- * `AWS_DEFAULT_REGION` env var already set one.
17232
- *
17233
- * Precedence mirrors where the deployed function would actually run, so a
17234
- * handler's ambient-region SDK call (`new XxxClient({})`) reaches the same
17235
- * region locally as the resources it is bound to:
18222
+ * Resolve the mTLS configuration from CLI options. Returns `undefined`
18223
+ * when none of the three `--mtls-*` flags is set (the server stays
18224
+ * plain-HTTP). When any of the three is set, ALL THREE must be set —
18225
+ * partial configurations are rejected at parse time so the server
18226
+ * never boots in a half-configured state.
17236
18227
  *
17237
- * 1. `--stack-region` explicit; also drives the `--from-cfn-stack` CFn
17238
- * client, so the container matches the region the bound stack was read
17239
- * from.
17240
- * 2. the synth-derived stack region — `env.region` on the CDK stack, read
17241
- * from the cloud assembly manifest (`StackInfo.region`). Previously
17242
- * used only host-side for the `--from-cfn-stack` CFn client; never
17243
- * injected into the container.
17244
- * 3. the `--profile`'s configured region `~/.aws/config`'s `region =`
17245
- * for the profile. `--profile` injected credentials but not this.
18228
+ * Exported for unit testing.
18229
+ */
18230
+ function resolveMtlsConfig(options) {
18231
+ const present = [];
18232
+ const absent = [];
18233
+ if (options.mtlsTruststore !== void 0 && options.mtlsTruststore !== "") present.push("--mtls-truststore");
18234
+ else absent.push("--mtls-truststore");
18235
+ if (options.mtlsCert !== void 0 && options.mtlsCert !== "") present.push("--mtls-cert");
18236
+ else absent.push("--mtls-cert");
18237
+ if (options.mtlsKey !== void 0 && options.mtlsKey !== "") present.push("--mtls-key");
18238
+ else absent.push("--mtls-key");
18239
+ if (present.length === 0) return void 0;
18240
+ if (absent.length > 0) throw new Error(`mTLS configuration is incomplete: ${present.join(", ")} set but ${absent.join(", ")} missing. All three of --mtls-truststore, --mtls-cert, and --mtls-key must be set together to enable mTLS, or all three left unset for plain HTTP.`);
18241
+ return readMtlsMaterialsFromDisk({
18242
+ truststorePath: options.mtlsTruststore,
18243
+ certPath: options.mtlsCert,
18244
+ keyPath: options.mtlsKey
18245
+ });
18246
+ }
18247
+ /**
18248
+ * Builder for the `start-api` subcommand.
18249
+ */
18250
+ function createLocalStartApiCommand(opts = {}) {
18251
+ setEmbedConfig(opts.embedConfig);
18252
+ const startApi = new Command("start-api").description("Run a long-running local HTTP server that maps API Gateway routes (REST v1, HTTP API, Function URL) to Lambda invocations against the AWS Lambda Runtime Interface Emulator (Docker required). Supports Lambda TOKEN/REQUEST authorizers, Cognito User Pool / HTTP v2 JWT authorizers, and AWS_IAM auth (REST v1 `AuthorizationType: AWS_IAM` and Function URL `AuthType: AWS_IAM` — SigV4 signature verification only; IAM policy evaluation is NOT emulated). When JWKS is unreachable, JWT authorizers fall back to pass-through (every token accepted) with a warn line — local dev fallback. VPC-config Lambdas run locally and surface a warn line at startup; their containers do NOT get attached to the deployed VPC subnets, so calls to private RDS / ElastiCache will fail.").argument("[targets...]", `Optional API subset filter. Pass one or more identifiers to serve exactly that subset (the union; each on its own port). Each accepts the bare CDK logical id ('MyHttpApi'; single-stack apps only), stack-qualified logical id ('MyStack:MyHttpApi'), full CDK Construct path ('MyStack/MyHttpApi/Resource'), or an ancestor Construct path that prefix-matches ('MyStack/MyHttpApi'). When omitted in a TTY, a multi-select picker opens with every API pre-selected (Enter serves all, deselect to pick a subset); when omitted in a non-TTY (CI / pipe) every discovered API is served. Mirrors \`${getEmbedConfig().cliName} invoke\` / \`${getEmbedConfig().cliName} run-task\` target syntax.`).action(withErrorHandling(async (targets, options) => {
18253
+ await localStartApiCommand(targets, options, opts.extraStateProviders);
18254
+ }));
18255
+ addStartApiSpecificOptions(startApi);
18256
+ [
18257
+ ...commonOptions(),
18258
+ ...appOptions(),
18259
+ ...contextOptions
18260
+ ].forEach((opt) => startApi.addOption(opt));
18261
+ startApi.addOption(deprecatedRegionOption);
18262
+ return startApi;
18263
+ }
18264
+ /**
18265
+ * Register the option block that `cdkl start-api` adds on top of the shared
18266
+ * common / app / context option helpers. Shared between `cdkl start-api`
18267
+ * and any host CLI (e.g. cdkd's `local start-api`) that wraps the
18268
+ * long-running API-Gateway-fronted local HTTP server, so adding or
18269
+ * renaming a `start-api`-only flag here propagates to every embedder
18270
+ * without duplicate `.addOption(...)` blocks.
17246
18271
  *
17247
- * Returns `undefined` when none is known; the caller leaves `AWS_REGION`
17248
- * unset so the SDK's own "Region is missing" surfaces (the correct signal
17249
- * for a region-agnostic stack with no profile region and no `AWS_REGION`
17250
- * env).
18272
+ * Calling order only affects `--help` presentation (Commander parses
18273
+ * insertion-order-independent). The host-CLI convention is host-specific
18274
+ * options first, then this helper, then the shared common / app / context
18275
+ * options — host flags / start-api flags / common flags grouped in three
18276
+ * `--help` clusters. Chainable: returns `cmd`.
17251
18277
  */
17252
- function resolveContainerFallbackRegion(args) {
17253
- return args.stackRegionOverride ?? args.synthRegion ?? args.profileRegion;
18278
+ function addStartApiSpecificOptions(cmd) {
18279
+ return cmd.addOption(new Option("--port <port>", "HTTP server port (default: auto-allocate)").default("0")).addOption(new Option("--host <host>", "Bind address").default("127.0.0.1")).addOption(new Option("--stack <name>", "Stack to start (single-stack apps auto-detect)")).addOption(new Option("--all-stacks", "Serve every stack's API in a multi-stack app (each API on its own port) instead of erroring out. Mutually exclusive with a positional target subset, --stack, and an explicit --from-cfn-stack <name>; the bare --from-cfn-stack flag stays compatible (binds each routed stack to its own CFn stack).").default(false)).addOption(new Option("--warm", "Pre-start one container per Lambda at server boot").default(false)).addOption(new Option("--per-lambda-concurrency <n>", "Pool size cap per Lambda (default 2, max 4)").default("2")).addOption(new Option("--no-pull", "Skip docker pull (cached image)")).addOption(new Option("--container-host <host>", "IP the host uses to bind/probe the RIE port (must be a numeric IP — `docker run -p <ip>:<port>:8080` rejects hostnames). Defaults to 127.0.0.1.").default("127.0.0.1")).addOption(new Option("--debug-port-base <port>", "Reserve a contiguous --debug-port range (one per Lambda)")).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"LogicalId\":{\"KEY\":\"VALUE\"}, \"Parameters\": {...}})")).addOption(new Option("--assume-role <arn-or-pair>", "Assume the Lambda's execution role and forward STS-issued temp creds. Bare <arn> = global default; <LogicalId>=<arn> = per-Lambda override (repeatable). Per-Lambda > global > unset (developer creds passed through).").argParser((raw, prev) => parseAssumeRoleToken(raw, prev))).addOption(new Option("--watch", "Hot-reload: re-synth + re-discover routes when the CDK app's source changes (honors cdk.json watch.include/exclude; cdk.out, node_modules, .git are always excluded). Off by default; the server keeps the previous version serving when synth fails mid-reload.").default(false)).addOption(new Option("--stage <name>", "Select an API Gateway Stage by its 'StageName'. Default: the first Stage attached to each API. Drives event.stageVariables for both REST v1 and HTTP API v2. NOTE: For HTTP API v2 routes, requestContext.stage is always '$default' regardless of this flag (AWS-side limitation — HTTP API only exposes one stage to the integration event); only event.stageVariables is affected for v2 routes. For REST v1 routes the selected StageName is also threaded into requestContext.stage.")).addOption(new Option("--api <id>", "DEPRECATED — use the positional <targets...> argument instead. Accepts a SINGLE identifier; for a subset pass multiple positional targets. Same accepted forms (bare logical id, stack-qualified, Construct path, ancestor prefix). Will be removed in a future major release.")).addOption(new Option("--layer-role-arn <arn>", "Role to sts:AssumeRole before calling lambda:GetLayerVersion on every literal-ARN entry in Properties.Layers (issue #448). 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("--from-cfn-stack [cfn-stack-name]", "Read a deployed CloudFormation stack via ListStackResources and substitute Ref / Fn::ImportValue in Lambda 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 per routed stack; pass an explicit value when a single CFn stack should serve every routed stack. 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.")).addOption(new Option("--mtls-truststore <path>", `PEM-encoded CA bundle for client-certificate verification (mutual TLS). When set, the local server switches from HTTP to HTTPS and the TLS handshake rejects clients whose certificate doesn't chain to one of these CAs. Verified certs are surfaced on the Lambda event under requestContext.identity.clientCert (REST v1) / requestContext.authentication.clientCert (HTTP API v2). Must be set together with --mtls-cert + --mtls-key; partial flag sets are rejected. Generate a CA + server + client cert for local dev: openssl req -x509 -newkey rsa:2048 -nodes -keyout ca-key.pem -out ca.pem -subj "/CN=${getEmbedConfig().resourceNamePrefix}-ca" -days 365; openssl req -newkey rsa:2048 -nodes -keyout server-key.pem -out server-csr.pem -subj "/CN=localhost"; openssl x509 -req -in server-csr.pem -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out server-cert.pem -days 365; openssl req -newkey rsa:2048 -nodes -keyout client-key.pem -out client-csr.pem -subj "/CN=client"; openssl x509 -req -in client-csr.pem -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out client-cert.pem -days 365; curl --cacert ca.pem --cert client-cert.pem --key client-key.pem https://localhost:<port>/...`)).addOption(new Option("--mtls-cert <path>", "PEM-encoded server certificate for mutual TLS. Self-signed is fine for local dev. Must be set together with --mtls-truststore + --mtls-key.")).addOption(new Option("--mtls-key <path>", "PEM-encoded server private key matching --mtls-cert. Must be set together with --mtls-truststore + --mtls-cert.")).addOption(new Option("--strict-sigv4", "Opt-in: DENY AWS_IAM SigV4 requests that cannot be cryptographically verified (foreign access-key-id — e.g. a federated / Cognito Identity Pool / cross-account signer — OR no local AWS credentials configured) instead of the default warn-and-pass. DEFAULT off: cdk-local warn-and-passes unverifiable IAM requests with a placeholder principalId so local dev exercises app logic without reproducing an auth boundary it cannot fully emulate. OAC-fronted Function URLs always warn-and-pass regardless.").default(false));
17254
18280
  }
18281
+
18282
+ //#endregion
18283
+ //#region src/cli/commands/local-invoke-agentcore.ts
17255
18284
  /**
17256
- * Issue an STS AssumeRole and return temporary credentials. Mirrors
17257
- * `cdkl invoke`'s helper byte-for-byte; lifted here so the
17258
- * start-api command stays self-contained.
18285
+ * Parser for `--timeout <ms>`. Accepts a positive integer; rejects 0,
18286
+ * negatives, fractions, and non-numeric input.
17259
18287
  */
17260
- async function assumeLambdaExecutionRole(roleArn, region) {
17261
- const { STSClient, AssumeRoleCommand } = await import("@aws-sdk/client-sts");
17262
- const sts = new STSClient({ ...region && { region } });
18288
+ function parseTimeoutMs(raw) {
18289
+ const parsed = Number(raw);
18290
+ 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");
18291
+ return parsed;
18292
+ }
18293
+ /**
18294
+ * `cdkl invoke-agentcore <target>` — run a Bedrock AgentCore Runtime container
18295
+ * locally and invoke it once over the AgentCore HTTP contract. Resolves
18296
+ * the `AWS::BedrockAgentCore::Runtime`, pulls / builds its container,
18297
+ * starts it on port 8080, waits for `GET /ping`, POSTs the event to
18298
+ * `POST /invocations`, prints the response, and tears down. Covers the
18299
+ * container artifact and the CodeConfiguration managed-runtime artifact
18300
+ * (fromCodeAsset, built from source) on the HTTP + MCP protocols; the agent's
18301
+ * calls to real AWS go to real AWS (credentials injected like `cdkl invoke`).
18302
+ */
18303
+ async function localInvokeAgentCoreCommand(target, options, extraStateProviders) {
18304
+ const logger = getLogger();
18305
+ if (options.verbose) logger.setLevel("debug");
18306
+ warnIfDeprecatedRegion(options);
18307
+ let containerId;
18308
+ let stopLogs;
18309
+ let sigintHandler;
18310
+ let profileCredsFile;
18311
+ let stateProvider;
18312
+ const cleanup = singleFlight(async () => {
18313
+ if (stateProvider) try {
18314
+ stateProvider.dispose();
18315
+ } catch (err) {
18316
+ getLogger().debug(`state provider dispose failed: ${err instanceof Error ? err.message : String(err)}`);
18317
+ }
18318
+ if (stopLogs) try {
18319
+ stopLogs();
18320
+ } catch (err) {
18321
+ getLogger().debug(`streamLogs stop failed: ${err instanceof Error ? err.message : String(err)}`);
18322
+ }
18323
+ if (containerId) try {
18324
+ await removeContainer(containerId);
18325
+ } catch (err) {
18326
+ getLogger().debug(`removeContainer(${containerId}) failed: ${err instanceof Error ? err.message : String(err)}`);
18327
+ }
18328
+ if (profileCredsFile) try {
18329
+ await profileCredsFile.dispose();
18330
+ } catch (err) {
18331
+ getLogger().debug(`Failed to remove profile credentials tmpdir ${profileCredsFile.hostPath}: ${err instanceof Error ? err.message : String(err)}`);
18332
+ }
18333
+ }, (err) => {
18334
+ getLogger().debug(`cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
18335
+ });
17263
18336
  try {
17264
- const creds = (await sts.send(new AssumeRoleCommand({
17265
- RoleArn: roleArn,
17266
- RoleSessionName: `${getEmbedConfig().resourceNamePrefix}-start-api-${Date.now()}`,
17267
- DurationSeconds: 3600
17268
- }))).Credentials;
17269
- if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) throw new Error(`AssumeRole(${roleArn}) returned no usable credentials.`);
17270
- return {
17271
- accessKeyId: creds.AccessKeyId,
17272
- secretAccessKey: creds.SecretAccessKey,
17273
- sessionToken: creds.SessionToken
18337
+ await applyRoleArnIfSet({
18338
+ roleArn: options.roleArn,
18339
+ region: options.region
18340
+ });
18341
+ await ensureDockerAvailable();
18342
+ const profileCredentials = options.profile ? await resolveProfileCredentials(options.profile) : void 0;
18343
+ if (options.profile && profileCredentials) profileCredsFile = await writeProfileCredentialsFile(options.profile, profileCredentials);
18344
+ const appCmd = resolveApp(options.app);
18345
+ if (!appCmd) throw new Error(`No CDK app specified. Pass --app, set ${getEmbedConfig().envPrefix}_APP, or add "app" to cdk.json.`);
18346
+ logger.info("Synthesizing CDK app...");
18347
+ const synthesizer = new Synthesizer();
18348
+ const context = parseContextOptions(options.context);
18349
+ const synthOpts = {
18350
+ app: appCmd,
18351
+ output: options.output,
18352
+ ...options.region && { region: options.region },
18353
+ ...options.profile && { profile: options.profile },
18354
+ ...Object.keys(context).length > 0 && { context }
18355
+ };
18356
+ const { stacks } = await synthesizer.synthesize(synthOpts);
18357
+ const resolvedTarget = await resolveSingleTarget(target, {
18358
+ entries: listTargets(stacks).agentCoreRuntimes,
18359
+ message: "Select an AgentCore Runtime to invoke",
18360
+ noun: "AgentCore Runtimes",
18361
+ 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")
18362
+ });
18363
+ const candidate = pickAgentCoreCandidateStack(resolvedTarget, stacks);
18364
+ stateProvider = createLocalStateProvider(options, candidate?.stackName ?? "", await resolveCfnFallbackRegion(options, candidate?.region), extraStateProviders);
18365
+ const { context: imageContext, loaded: loadedState } = stateProvider && candidate ? await buildAgentCoreImageContext(candidate, stateProvider, options) : {
18366
+ context: void 0,
18367
+ loaded: void 0
18368
+ };
18369
+ const resolved = resolveAgentCoreTarget(resolvedTarget, stacks, imageContext);
18370
+ logger.info(`Target: ${resolved.stack.stackName}/${resolved.logicalId} (${resolved.protocol})`);
18371
+ const isMcp = resolved.protocol === "MCP";
18372
+ const isA2a = resolved.protocol === "A2A";
18373
+ if (resolved.protocol === "AGUI") logger.info("AGUI runtime: routing through the HTTP /invocations + /ws path (AG-UI wire is SSE / WebSocket on port 8080).");
18374
+ if ((isMcp || isA2a) && options.ws) logger.warn(`--ws applies only to the HTTP / AGUI protocols; ignoring it for this ${resolved.protocol} runtime.`);
18375
+ if (options.wsInteractive && !options.ws) logger.warn("--ws-interactive is meaningful only with --ws; ignoring.");
18376
+ 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.");
18377
+ const sessionId = options.sessionId ?? randomUUID();
18378
+ const event = await readEvent(options);
18379
+ const mcpRequest = isMcp ? buildMcpRequest(event) : void 0;
18380
+ const a2aRequest = isA2a ? buildA2aRequest(event) : void 0;
18381
+ let authorization;
18382
+ if (isMcp || isA2a) {
18383
+ if (resolved.jwtAuthorizer || options.bearerToken) {
18384
+ const pathLabel = isMcp ? MCP_PATH : "/";
18385
+ 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.`);
18386
+ }
18387
+ } else authorization = await resolveInboundAuthorization(resolved, options);
18388
+ await resolveFromS3BucketIntrinsic(resolved, stateProvider, loadedState, imageContext);
18389
+ const image = await resolveAgentCoreImage(resolved, options, loadedState, stateProvider);
18390
+ const { env: dockerEnv, sensitiveEnvKeys } = await buildContainerEnv(resolved, options, profileCredentials, profileCredsFile, stateProvider, loadedState, imageContext);
18391
+ const hostPort = await pickFreePort();
18392
+ const containerHost = options.containerHost;
18393
+ const containerName = `${getEmbedConfig().resourceNamePrefix}-agentcore-${process.pid}-${Math.random().toString(36).slice(2, 8)}`;
18394
+ const containerPort = isMcp ? MCP_CONTAINER_PORT : isA2a ? A2A_CONTAINER_PORT : void 0;
18395
+ const containerPortLabel = isMcp ? `${MCP_CONTAINER_PORT}${MCP_PATH}` : isA2a ? `${A2A_CONTAINER_PORT}${"/"}` : "8080";
18396
+ logger.info(`Starting agent container (image=${image}, port=${hostPort} -> ${containerPortLabel})...`);
18397
+ containerId = await runDetached({
18398
+ image,
18399
+ mounts: [],
18400
+ env: dockerEnv,
18401
+ cmd: [],
18402
+ hostPort,
18403
+ host: containerHost,
18404
+ platform: options.platform,
18405
+ name: containerName,
18406
+ ...containerPort !== void 0 && { containerPort },
18407
+ ...sensitiveEnvKeys.size > 0 && { sensitiveEnvKeys }
18408
+ });
18409
+ stopLogs = streamLogs(containerId);
18410
+ sigintHandler = () => {
18411
+ cleanup().then(() => process.exit(130));
17274
18412
  };
18413
+ process.on("SIGINT", sigintHandler);
18414
+ if (isMcp && mcpRequest) {
18415
+ logger.info(`MCP request: ${mcpRequest.method}`);
18416
+ const mcp = await mcpInvokeOnce(containerHost, hostPort, mcpRequest, { requestTimeoutMs: options.timeout });
18417
+ await new Promise((r) => setTimeout(r, 250));
18418
+ emitMcpResult(mcp);
18419
+ } else if (isA2a && a2aRequest) {
18420
+ logger.info(`A2A request: ${a2aRequest.method}`);
18421
+ const a2a = await a2aInvokeOnce(containerHost, hostPort, a2aRequest, { requestTimeoutMs: options.timeout });
18422
+ await new Promise((r) => setTimeout(r, 250));
18423
+ emitA2aResult(a2a);
18424
+ } else if (options.ws) {
18425
+ await waitForAgentCorePing(containerHost, hostPort);
18426
+ const frameSource = options.wsInteractive ? readStdinLines() : void 0;
18427
+ 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...");
18428
+ const wsResult = await invokeAgentCoreWs(containerHost, hostPort, event, {
18429
+ sessionId,
18430
+ timeoutMs: options.timeout,
18431
+ onMessage: (text) => process.stdout.write(text),
18432
+ ...authorization && { authorization },
18433
+ ...frameSource && { frameSource }
18434
+ });
18435
+ await new Promise((r) => setTimeout(r, 250));
18436
+ emitWsResult(wsResult);
18437
+ } else {
18438
+ await waitForAgentCorePing(containerHost, hostPort);
18439
+ const additionalHeaders = await buildSigV4HeadersIfRequested(options, resolved, loadedState, containerHost, hostPort, event, sessionId, stateProvider);
18440
+ const result = await invokeAgentCore(containerHost, hostPort, event, {
18441
+ sessionId,
18442
+ timeoutMs: options.timeout,
18443
+ onChunk: (text) => process.stdout.write(text),
18444
+ ...authorization && { authorization },
18445
+ ...additionalHeaders && { additionalHeaders }
18446
+ });
18447
+ await new Promise((r) => setTimeout(r, 250));
18448
+ emitResult(result);
18449
+ }
17275
18450
  } finally {
17276
- sts.destroy();
18451
+ if (sigintHandler) process.off("SIGINT", sigintHandler);
18452
+ await cleanup();
17277
18453
  }
17278
18454
  }
17279
18455
  /**
17280
- * Parse / clamp the `--per-lambda-concurrency` flag. Above-cap values
17281
- * are clamped to 4 with a warn line (per the spec doc's risk-mitigation
17282
- * row).
18456
+ * Enforce the runtime's inbound JWT authorizer (when declared) and return
18457
+ * the `Authorization` header to forward to `/invocations`.
18458
+ *
18459
+ * - No authorizer → forward the token verbatim if one was given (no-op
18460
+ * otherwise).
18461
+ * - `--no-verify-auth` → warn + forward without verifying (local-dev escape).
18462
+ * - Authorizer + no token → reject (AgentCore returns 401).
18463
+ * - Authorizer + token → verify against the OIDC discovery URL; reject on
18464
+ * failure (AgentCore returns 403); forward on success. An unreachable
18465
+ * discovery URL falls back to pass-through accept (offline-dev fallback in
18466
+ * {@link verifyJwtViaDiscovery}).
18467
+ *
18468
+ * Exported so a unit test can drive the gate without the full Docker pipeline.
17283
18469
  */
17284
- function parsePerLambdaConcurrency(raw) {
17285
- const parsed = parseInt(raw, 10);
17286
- if (!Number.isFinite(parsed) || parsed < 1) throw new Error(`--per-lambda-concurrency must be a positive integer (got '${raw}')`);
17287
- if (parsed > 4) {
17288
- getLogger().warn(`--per-lambda-concurrency ${parsed} exceeds the v1 cap of 4; clamping to 4. (Raise this in a follow-up PR if your workload needs more.)`);
17289
- return 4;
18470
+ async function resolveInboundAuthorization(resolved, options) {
18471
+ const logger = getLogger();
18472
+ const authorizer = resolved.jwtAuthorizer;
18473
+ const header = options.bearerToken ? `Bearer ${options.bearerToken}` : void 0;
18474
+ if (!authorizer) return header;
18475
+ if (options.verifyAuth === false) {
18476
+ logger.warn(`Runtime '${resolved.logicalId}' declares a customJwtAuthorizer, but --no-verify-auth was set — skipping inbound JWT verification (local-dev escape hatch).`);
18477
+ return header;
18478
+ }
18479
+ 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");
18480
+ if (!(await verifyJwtViaDiscovery({
18481
+ discoveryUrl: authorizer.discoveryUrl,
18482
+ ...authorizer.allowedAudience && { allowedAudience: authorizer.allowedAudience },
18483
+ ...authorizer.allowedClients && { allowedClients: authorizer.allowedClients },
18484
+ ...authorizer.allowedScopes && { allowedScopes: authorizer.allowedScopes },
18485
+ ...authorizer.customClaims && { customClaims: authorizer.customClaims }
18486
+ }, 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");
18487
+ logger.info(`Inbound JWT verified against ${authorizer.discoveryUrl}.`);
18488
+ return header;
18489
+ }
18490
+ /**
18491
+ * Compute the SigV4 headers for the `/invocations` POST when `--sigv4` is
18492
+ * requested. Returns `undefined` (no header overlay) when:
18493
+ *
18494
+ * - `--sigv4` is not set,
18495
+ * - the runtime declares a `customJwtAuthorizer` (the JWT path wins; warns),
18496
+ *
18497
+ * Throws a {@link CdkLocalError} when `--sigv4` conflicts with
18498
+ * `--bearer-token`, or when no AWS credentials are resolvable for signing.
18499
+ *
18500
+ * Exported so a unit test can drive the gate without the full Docker pipeline.
18501
+ */
18502
+ async function buildSigV4HeadersIfRequested(options, resolved, loaded, host, port, event, sessionId, stateProvider) {
18503
+ if (!options.sigv4) return void 0;
18504
+ if (options.bearerToken) throw new CdkLocalError(`--sigv4 and --bearer-token are mutually exclusive: pick one inbound auth.`, "LOCAL_INVOKE_AGENTCORE_AUTH_CONFLICT");
18505
+ if (resolved.jwtAuthorizer) {
18506
+ getLogger().warn(`Runtime '${resolved.logicalId}' declares a customJwtAuthorizer; --sigv4 ignored (JWT path takes precedence).`);
18507
+ return;
17290
18508
  }
17291
- return parsed;
18509
+ const region = options.region ?? options.stackRegion ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? resolved.stack.region;
18510
+ 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");
18511
+ const signed = await signAgentCoreInvocation({
18512
+ credentials: await resolveHostCredentialsForSigV4(options, resolved, loaded, region, stateProvider),
18513
+ region,
18514
+ host,
18515
+ port,
18516
+ path: "/invocations",
18517
+ body: JSON.stringify(event ?? {}),
18518
+ sessionId
18519
+ });
18520
+ const headers = {
18521
+ Authorization: signed.authorization,
18522
+ "X-Amz-Date": signed.amzDate,
18523
+ "X-Amz-Content-Sha256": signed.amzContentSha256
18524
+ };
18525
+ if (signed.amzSecurityToken) headers["X-Amz-Security-Token"] = signed.amzSecurityToken;
18526
+ getLogger().info(`Signed /invocations with SigV4 (region=${region}).`);
18527
+ return headers;
17292
18528
  }
17293
18529
  /**
17294
- * Filter the global Lambda spec map to just the Lambdas reachable from
17295
- * one API server group. The container pool for that server is built
17296
- * from this filtered map so per-API authorizer Lambdas + route
17297
- * handlers stay scoped to their owning server — disposing one server's
17298
- * pool on shutdown does NOT touch another server's still-warm
17299
- * containers.
18530
+ * Resolve credentials for host-side SigV4 signing. Precedence:
18531
+ * 1. `--assume-role` STS temp creds (warn + fall through on STS failure);
18532
+ * 2. `--profile` profile creds (sessionToken when the profile carries one);
18533
+ * 3. shell env (`AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` / optional
18534
+ * `AWS_SESSION_TOKEN`).
17300
18535
  *
17301
- * Also includes any authorizer Lambdas attached to the group's routes
17302
- * (a Lambda authorizer is a Lambda the pool needs to know about, even
17303
- * though no route directly handles `lambdaLogicalId === auth.lambdaLogicalId`).
18536
+ * Throws a {@link CdkLocalError} when none are available `--sigv4` cannot
18537
+ * proceed without credentials, unlike the unsigned path.
17304
18538
  */
17305
- function filterSpecsForGroup(group, allSpecs) {
17306
- const ids = /* @__PURE__ */ new Set();
17307
- for (const rwa of group.routes) {
17308
- ids.add(rwa.route.lambdaLogicalId);
17309
- const auth = rwa.authorizer;
17310
- if (auth && (auth.kind === "lambda-token" || auth.kind === "lambda-request")) ids.add(auth.lambdaLogicalId);
18539
+ async function resolveHostCredentialsForSigV4(options, resolved, loaded, region, stateProvider) {
18540
+ const logger = getLogger();
18541
+ const assumeRoleArn = await resolveAssumeRoleArn(options, resolved, loaded, stateProvider);
18542
+ if (assumeRoleArn) try {
18543
+ return await assumeAgentCoreExecutionRole(assumeRoleArn, region);
18544
+ } catch (err) {
18545
+ 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"}.`);
17311
18546
  }
17312
- const out = /* @__PURE__ */ new Map();
17313
- for (const id of ids) {
17314
- const spec = allSpecs.get(id);
17315
- if (spec) out.set(id, spec);
18547
+ if (options.profile) {
18548
+ const creds = await resolveProfileCredentials(options.profile);
18549
+ if (creds?.accessKeyId && creds.secretAccessKey) return {
18550
+ accessKeyId: creds.accessKeyId,
18551
+ secretAccessKey: creds.secretAccessKey,
18552
+ ...creds.sessionToken && { sessionToken: creds.sessionToken }
18553
+ };
17316
18554
  }
17317
- return out;
17318
- }
17319
- /**
17320
- * Print one route table per server, with the server's display name as
17321
- * the section header. Replaces the pre-issue #260 single flat table —
17322
- * users now see exactly which routes belong to which API + port.
17323
- */
17324
- function printPerServerRouteTables(servers) {
17325
- for (const { group, server } of servers) {
17326
- process.stdout.write(`\n${group.displayName} (http://${server.host}:${server.port})\n`);
17327
- printRouteTable(group.routes);
18555
+ const accessKeyId = process.env["AWS_ACCESS_KEY_ID"];
18556
+ const secretAccessKey = process.env["AWS_SECRET_ACCESS_KEY"];
18557
+ if (accessKeyId && secretAccessKey) {
18558
+ const sessionToken = process.env["AWS_SESSION_TOKEN"];
18559
+ return {
18560
+ accessKeyId,
18561
+ secretAccessKey,
18562
+ ...sessionToken && { sessionToken }
18563
+ };
17328
18564
  }
18565
+ 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");
17329
18566
  }
17330
18567
  /**
17331
- * Surface every `unsupported` route (deferred 501) as a startup warn so
17332
- * the user sees what isn't reachable BEFORE they try to curl it. One
17333
- * warn line per route the route's `unsupported.reason` already names
17334
- * the offender + the underlying limitation, so we just prefix with
17335
- * method + path. Returns the number of unsupported routes so the caller
17336
- * can emit a single-line summary header above the list.
17337
- */
17338
- function warnUnsupportedRoutes(routes, logger) {
17339
- const unsupported = routes.filter((r) => r.unsupported);
17340
- if (unsupported.length === 0) return 0;
17341
- logger.warn(`${unsupported.length} route(s) will respond HTTP 501 Not Implemented when hit (boot continued):`);
17342
- for (const r of unsupported) logger.warn(` - ${r.method} ${r.pathPattern}: ${r.unsupported.reason}`);
17343
- return unsupported.length;
17344
- }
17345
- /**
17346
- * Surface every WebSocket API tagged as unsupported at discovery as a
17347
- * startup warn. The boot loop above skips attaching the server for
17348
- * these APIs, so no upgrade requests are ever accepted on them —
17349
- * mirrors `warnUnsupportedRoutes`'s shape but for the WebSocket axis.
17350
- * Typical trigger: a Route declaring `AuthorizationType !== 'NONE'` on
17351
- * `$connect` (cdkl v1 does not emulate WebSocket authorizers; closing
17352
- * this gap structurally rather than silently admitting
17353
- * unauthenticated clients matches the security-by-default precedent
17354
- * PR #514 set for HTTP API v2 service integrations).
17355
- */
17356
- function warnUnsupportedWebSocketApis(apis, logger) {
17357
- const unsupported = apis.filter((api) => api.unsupported);
17358
- if (unsupported.length === 0) return 0;
17359
- logger.warn(`${unsupported.length} WebSocket API(s) will NOT accept upgrade requests (boot continued):`);
17360
- for (const api of unsupported) logger.warn(` - ${api.declaredAt}: ${api.unsupported.reason}`);
17361
- return unsupported.length;
17362
- }
17363
- /**
17364
- * Surface a one-line warn per HTTP / HTTP_PROXY integration whose
17365
- * `Integration.Uri` points at a well-known internal address space
17366
- * (AWS IMDS, loopback, link-local, RFC1918). PR #505 / issue #457
17367
- * follow-up: cdk-local does NOT block these — warn-and-proceed matches the
17368
- * cognito JWKS pass-through pattern — but the user should see the
17369
- * destination at boot so a malicious / typo'd template Uri does not
17370
- * silently exfiltrate credentials in CI. Deduplicated per-Uri.
18568
+ * Acquire the agent image. A CODE artifact (managed runtime) is built from
18569
+ * source a fromCodeAsset bundle from its cdk.out asset, a fromS3 bundle
18570
+ * downloaded + extracted from S3. A CONTAINER artifact mirrors the
18571
+ * container-Lambda path: build from a local cdk.out asset when the URI matches
18572
+ * one, else pull from ECR, else pull a plain registry image.
18573
+ *
18574
+ * `loaded` is the `--from-cfn-stack` state record (when available) — threaded
18575
+ * through so a bare `--assume-role` can resolve the execution-role ARN from
18576
+ * state for the fromS3 download. `stateProvider` enables the issue-#187
18577
+ * live-fallback to `bedrock-agentcore-control:GetAgentRuntime` when the
18578
+ * static state lookup misses.
17371
18579
  */
17372
- function warnSsrfRiskyIntegrations(routes, logger) {
17373
- const seen = /* @__PURE__ */ new Set();
17374
- for (const r of routes) {
17375
- const integ = r.restV1Integration;
17376
- if (!integ) continue;
17377
- if (integ.kind !== "http" && integ.kind !== "http-proxy") continue;
17378
- if (seen.has(integ.uri)) continue;
17379
- seen.add(integ.uri);
17380
- warnSsrfRiskyUri(integ.uri, `${r.method} ${r.pathPattern}`, (msg) => logger.warn(msg));
18580
+ async function resolveAgentCoreImage(resolved, options, loaded, stateProvider) {
18581
+ const logger = getLogger();
18582
+ const architecture = platformToArchitecture(options.platform);
18583
+ if (resolved.codeArtifact) return resolveAgentCoreCodeImage(resolved, resolved.codeArtifact, options, architecture, loaded, stateProvider);
18584
+ const containerUri = resolved.containerUri;
18585
+ 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");
18586
+ const manifestPath = resolved.stack.assetManifestPath;
18587
+ if (manifestPath) {
18588
+ const cdkOutDir = dirname(manifestPath);
18589
+ const manifest = await new AssetManifestLoader().loadManifest(cdkOutDir, resolved.stack.stackName);
18590
+ if (manifest) {
18591
+ const entry = getDockerImageBySourceHash(manifest, containerUri);
18592
+ if (entry) return buildContainerImage(entry.asset, cdkOutDir, {
18593
+ architecture,
18594
+ noBuild: options.build === false
18595
+ });
18596
+ }
18597
+ }
18598
+ if (parseEcrUri(containerUri)) {
18599
+ logger.info(`Pulling agent image from ECR: ${containerUri}`);
18600
+ return pullEcrImage(containerUri, {
18601
+ skipPull: options.pull === false,
18602
+ ...options.region !== void 0 && { region: options.region },
18603
+ ...options.ecrRoleArn !== void 0 && { ecrRoleArn: options.ecrRoleArn },
18604
+ ...options.profile !== void 0 && { profile: options.profile }
18605
+ });
17381
18606
  }
18607
+ await pullImage(containerUri, options.pull === false);
18608
+ return containerUri;
18609
+ }
18610
+ /**
18611
+ * Build a local image from a `CodeConfiguration` (managed-runtime) bundle.
18612
+ *
18613
+ * - fromS3 (`code.s3Source` set, a literal S3 object): download + extract the
18614
+ * bundle, then run the from-source build over the extracted dir.
18615
+ * - fromCodeAsset: locate the source dir in cdk.out via its asset hash, then
18616
+ * run the same from-source build (generated Dockerfile → install deps → run
18617
+ * EntryPoint).
18618
+ */
18619
+ async function resolveAgentCoreCodeImage(resolved, code, options, architecture, loaded, stateProvider) {
18620
+ if (code.s3Source) return resolveAgentCoreCodeImageFromS3(resolved, code, code.s3Source, options, architecture, loaded, stateProvider);
18621
+ const manifestPath = resolved.stack.assetManifestPath;
18622
+ 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");
18623
+ const cdkOutDir = dirname(manifestPath);
18624
+ const loader = new AssetManifestLoader();
18625
+ const manifest = await loader.loadManifest(cdkOutDir, resolved.stack.stackName);
18626
+ const fileAssets = manifest ? loader.getFileAssets(manifest) : void 0;
18627
+ const asset = fileAssets ? fileAssets.get(code.codeAssetHash) ?? findFileAssetByObjectKey(fileAssets, code.codeAssetHash) : void 0;
18628
+ 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");
18629
+ const sourceDir = loader.getAssetSourcePath(cdkOutDir, asset);
18630
+ 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");
18631
+ return buildAgentCoreCodeImage({
18632
+ sourceDir,
18633
+ runtime: code.runtime,
18634
+ entryPoint: code.entryPoint,
18635
+ architecture,
18636
+ noBuild: options.build === false
18637
+ });
17382
18638
  }
17383
18639
  /**
17384
- * One reload cycle for the multi-server topology (issue #260). The
17385
- * watcher serializes calls via a chain promise; this function:
18640
+ * Build a local image from a fromS3 CodeConfiguration bundle: download +
18641
+ * extract the S3 object, run the from-source build over the extracted dir, then
18642
+ * clean up the temp dir.
17386
18643
  *
17387
- * 1. Re-runs `synthesizeAndBuild()` once (failure warn + keep
17388
- * previous version serving on every server).
17389
- * 2. Re-groups the new routes by API server key.
17390
- * 3. For each existing server, swaps state to the new group's
17391
- * routes + a freshly-built pool filtered to that group's
17392
- * Lambdas. Disposes the previous pool in the background.
17393
- * 4. Warns about new groups (= an API was added in CDK code) and
17394
- * vanished groups (= an API was removed) — those require a
17395
- * server restart in v1.
18644
+ * Credentials mirror the rest of the command: an `--assume-role` ARN (explicit,
18645
+ * or resolved from `--from-cfn-stack` state for the bare form) yields STS temp
18646
+ * creds for the download; otherwise `--profile` / the default chain is used.
18647
+ * The region is `--region` / `--stack-region` / env / the stack's region.
17396
18648
  */
17397
- async function reloadAllServers(args) {
17398
- const { synthesizeAndBuild, servers, buildPool, logger } = args;
17399
- let material;
17400
- try {
17401
- material = await synthesizeAndBuild();
18649
+ async function resolveAgentCoreCodeImageFromS3(resolved, code, s3Source, options, architecture, loaded, stateProvider) {
18650
+ const logger = getLogger();
18651
+ 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");
18652
+ const location = {
18653
+ bucket: s3Source.bucket,
18654
+ key: s3Source.key,
18655
+ ...s3Source.versionId !== void 0 && { versionId: s3Source.versionId }
18656
+ };
18657
+ const region = options.region ?? options.stackRegion ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? resolved.stack.region;
18658
+ const assumeRoleArn = await resolveAssumeRoleArn(options, resolved, loaded, stateProvider);
18659
+ let credentials;
18660
+ if (assumeRoleArn) try {
18661
+ credentials = await assumeAgentCoreExecutionRole(assumeRoleArn, region);
17402
18662
  } catch (err) {
17403
- logger.warn(`cdk synth failed during reload; keeping previous version. (${err instanceof Error ? err.message : String(err)})`);
17404
- return;
18663
+ 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"}.`);
17405
18664
  }
17406
- const newGroups = groupRoutesByServer(material.routes);
17407
- const newByKey = new Map(newGroups.map((g) => [g.serverKey, g]));
17408
- const oldKeys = new Set(servers.map((s) => s.group.serverKey));
17409
- const newKeys = new Set(newByKey.keys());
17410
- const added = [...newKeys].filter((k) => !oldKeys.has(k));
17411
- const removed = [...oldKeys].filter((k) => !newKeys.has(k));
17412
- if (added.length > 0) logger.warn(`Reload detected new API surface(s): ${added.join(", ")}. Restart '${getEmbedConfig().cliName} start-api' to serve them.`);
17413
- if (removed.length > 0) logger.warn(`Reload detected removed API surface(s): ${removed.join(", ")}. Their servers will keep serving stale routes until restart.`);
17414
- for (const booted of servers) {
17415
- const group = newByKey.get(booted.group.serverKey);
17416
- if (!group) continue;
17417
- const newPool = buildPool(filterSpecsForGroup(group, material.specs));
17418
- const newState = {
17419
- routes: group.routes,
17420
- pool: newPool,
17421
- corsConfigByApiId: material.corsConfigByApiId
17422
- };
17423
- const previousState = booted.server.setServerState(newState);
17424
- booted.group = group;
17425
- previousState.pool.dispose().catch((err) => {
17426
- logger.debug(`Previous pool dispose() failed for ${group.displayName}: ${err instanceof Error ? err.message : String(err)}`);
18665
+ const bundle = await downloadAndExtractS3Bundle(location, {
18666
+ ...region !== void 0 && { region },
18667
+ ...options.profile !== void 0 && { profile: options.profile },
18668
+ ...credentials !== void 0 && { credentials }
18669
+ });
18670
+ try {
18671
+ return await buildAgentCoreCodeImage({
18672
+ sourceDir: bundle.dir,
18673
+ runtime: code.runtime,
18674
+ entryPoint: code.entryPoint,
18675
+ architecture,
18676
+ noBuild: options.build === false
17427
18677
  });
18678
+ } finally {
18679
+ await bundle.cleanup();
17428
18680
  }
17429
- printPerServerRouteTables(servers);
17430
- const allRoutes = servers.flatMap((s) => s.group.routes.map((r) => r.route));
17431
- warnUnsupportedRoutes(allRoutes, logger);
17432
- warnSsrfRiskyIntegrations(allRoutes, logger);
17433
18681
  }
17434
18682
  /**
17435
- * Returns true when any value in the function's template env map is a
17436
- * CFn intrinsic (non-primitive). Used to gate the pseudo-parameter STS
17437
- * hop inside the `--from-state` flow: literal-only env maps don't need
17438
- * the pseudo-parameter bag and shouldn't pay for an STS call. Mirrors
17439
- * the same gating in `local-invoke.ts` (`envHasIntrinsicValue`) and
17440
- * `ecs-task-resolver.ts` (`containerHasIntrinsicEnvOrSecret`).
18683
+ * Find the file asset whose destination objectKey is `<hash>.zip` (matching the
18684
+ * `Code.S3.Prefix`'s hash) when the source-hash-keyed lookup misses covers a
18685
+ * synthesizer whose source hash differs from the destination objectKey.
17441
18686
  */
17442
- function envHasIntrinsicValue(templateEnv) {
17443
- if (!templateEnv) return false;
17444
- for (const v of Object.values(templateEnv)) {
17445
- if (v === void 0 || v === null) continue;
17446
- if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") continue;
17447
- return true;
17448
- }
17449
- return false;
18687
+ function findFileAssetByObjectKey(fileAssets, hash) {
18688
+ const zip = `${hash}.zip`;
18689
+ for (const asset of fileAssets.values()) if (Object.values(asset.destinations).some((d) => d.objectKey === zip || d.objectKey.endsWith(`/${zip}`))) return asset;
17450
18690
  }
17451
18691
  /**
17452
- * Load deployed state for every stack that owns a routed Lambda. Once
17453
- * per `synthesizeAndBuild` pass (initial boot + every reload), so a
17454
- * Lambda's per-spec build does not pay one round-trip per Lambda. Per-
17455
- * stack failures (no state, ambiguous region, bucket resolution error)
17456
- * degrade to warn-and-fall-back via the active `LocalStateProvider` —
17457
- * the affected stack's reachable Lambdas behave as if `--from-state` /
17458
- * `--from-cfn-stack` were not set, while sibling stacks with loadable
17459
- * state still substitute.
18692
+ * Build the container env + the set of env keys to keep off the `docker run`
18693
+ * argv. Substitutes `--from-cfn-stack` state into the template env (reusing the
18694
+ * shared state load + image-resolution context Ref / Fn::Sub / Fn::Join +
18695
+ * SSM parameters, with decrypted SecureString values flagged sensitive),
18696
+ * applies `--env-vars` overrides, then injects AWS credentials (`--assume-role`
18697
+ * STS temp creds resolving an intrinsic RoleArn from state for bare
18698
+ * `--assume-role` else `--profile` / dev creds).
17460
18699
  *
17461
- * Pseudo parameters are resolved per stack and only when at least one
17462
- * reachable Lambda in that stack has an intrinsic-valued env entry
17463
- * (gated via {@link envHasIntrinsicValue}). STS failures degrade to
17464
- * warn and leave `pseudoParameters: undefined` — substitution still
17465
- * runs for non-`AWS::*` refs.
18700
+ * The state provider + loaded record + image context are built once by the
18701
+ * caller and shared here, so this does not re-load state.
17466
18702
  */
18703
+ async function buildContainerEnv(resolved, options, profileCredentials, profileCredsFile, stateProvider, loaded, imageContext) {
18704
+ const logger = getLogger();
18705
+ let templateEnv = resolved.environmentVariables;
18706
+ const sensitiveEnvKeys = /* @__PURE__ */ new Set();
18707
+ if (stateProvider && loaded) {
18708
+ const subContext = {
18709
+ resources: imageContext?.stateResources ?? loaded.resources,
18710
+ consumerRegion: loaded.region
18711
+ };
18712
+ const pseudo = imageContext?.pseudoParameters ?? derivePseudoParametersFromRegion(loaded.region);
18713
+ if (pseudo) subContext.pseudoParameters = pseudo;
18714
+ if (imageContext?.stateParameters) subContext.parameters = imageContext.stateParameters;
18715
+ if (imageContext?.stateSensitiveParameters?.length) subContext.sensitiveParameters = new Set(imageContext.stateSensitiveParameters);
18716
+ const resolver = await stateProvider.buildCrossStackResolver(loaded.region);
18717
+ if (resolver) subContext.crossStackResolver = resolver;
18718
+ const { env, audit } = await substituteEnvVarsFromStateAsync(templateEnv, subContext);
18719
+ templateEnv = env;
18720
+ for (const key of audit.resolvedKeys) logger.debug(`${stateProvider.label}: substituted env var ${key}`);
18721
+ for (const key of audit.sensitiveKeys) sensitiveEnvKeys.add(key);
18722
+ 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.`);
18723
+ }
18724
+ const overrides = readEnvOverridesFile$2(options.envVars);
18725
+ const cdkPath = readCdkPathOrUndefined(resolved.resource);
18726
+ const envResult = resolveEnvVars(resolved.logicalId, cdkPath, templateEnv, overrides);
18727
+ for (const key of envResult.unresolved) {
18728
+ const overrideKeyExample = cdkPath?.replace(/\/Resource$/, "") ?? resolved.logicalId;
18729
+ 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.`);
18730
+ }
18731
+ const dockerEnv = { ...envResult.resolved };
18732
+ const assumeRoleArn = await resolveAssumeRoleArn(options, resolved, loaded, stateProvider);
18733
+ await applyAgentCoreCredentialEnv(dockerEnv, {
18734
+ ...assumeRoleArn !== void 0 && { assumeRoleArn },
18735
+ ...options.region !== void 0 && { region: options.region },
18736
+ ...profileCredentials !== void 0 && { profileCredentials },
18737
+ ...profileCredsFile !== void 0 && { profileCredsFile: {
18738
+ containerPath: profileCredsFile.containerPath,
18739
+ profileName: profileCredsFile.profileName
18740
+ } }
18741
+ });
18742
+ return {
18743
+ env: dockerEnv,
18744
+ sensitiveEnvKeys
18745
+ };
18746
+ }
18747
+ /**
18748
+ * Resolve a fromS3 bundle's intrinsic `Code.S3.Bucket` to a literal bucket
18749
+ * name in place on `resolved.codeArtifact.s3Source.bucket`. Uses the SAME
18750
+ * state-substitution machinery env vars use under `--from-cfn-stack`, so
18751
+ * every cross-stack intrinsic that path supports (`Ref` / `Fn::ImportValue` /
18752
+ * `Fn::GetStackOutput`) is supported transparently here.
18753
+ *
18754
+ * No-op when there is no intrinsic to resolve. Errors when no state is
18755
+ * available, or when the substitution returns a non-string / unresolved value.
18756
+ *
18757
+ * Exported so a unit test can drive the gate without the full Docker pipeline.
18758
+ */
18759
+ async function resolveFromS3BucketIntrinsic(resolved, stateProvider, loaded, imageContext) {
18760
+ const s3Source = resolved.codeArtifact?.s3Source;
18761
+ if (!s3Source || s3Source.bucketIntrinsic === void 0) return;
18762
+ if (s3Source.bucket !== void 0) return;
18763
+ 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");
18764
+ const subContext = {
18765
+ resources: imageContext?.stateResources ?? loaded.resources,
18766
+ consumerRegion: loaded.region
18767
+ };
18768
+ const pseudo = imageContext?.pseudoParameters ?? derivePseudoParametersFromRegion(loaded.region);
18769
+ if (pseudo) subContext.pseudoParameters = pseudo;
18770
+ const crossStackResolver = await stateProvider.buildCrossStackResolver(loaded.region);
18771
+ if (crossStackResolver) subContext.crossStackResolver = crossStackResolver;
18772
+ const result = await substituteAgainstStateAsync(s3Source.bucketIntrinsic, subContext);
18773
+ 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");
18774
+ 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");
18775
+ s3Source.bucket = result.value;
18776
+ getLogger().info(`Resolved fromS3 Code.S3.Bucket from state: ${describeIntrinsic(s3Source.bucketIntrinsic)} -> ${result.value}`);
18777
+ }
18778
+ /** Render the intrinsic key for an error / log message (e.g. `Ref:Bucket1`). */
18779
+ function describeIntrinsic(value) {
18780
+ if (!value || typeof value !== "object") return String(value);
18781
+ const obj = value;
18782
+ const key = Object.keys(obj)[0] ?? "?";
18783
+ const arg = obj[key];
18784
+ if (typeof arg === "string") return `${key}:${arg}`;
18785
+ return key;
18786
+ }
17467
18787
  /**
17468
- * Returns true when at least one host-provided extra state-provider flag
17469
- * is active in the options bag.
18788
+ * Build the `--from-cfn-stack` image-resolution context + return the loaded
18789
+ * state record (loaded once, reused by env substitution + role resolution).
18790
+ * Mirrors `run-task`'s `buildEcsImageResolutionContext`: pseudo parameters
18791
+ * (region + STS account id), the deployed resources, and SSM template
18792
+ * parameters (decrypted SecureString logical ids flagged sensitive).
17470
18793
  */
17471
- function hasExtraStateProviderActive(options, extraStateProviders) {
17472
- if (!extraStateProviders) return false;
17473
- for (const key of Object.keys(extraStateProviders)) if (options[key]) return true;
17474
- return false;
17475
- }
17476
- async function loadStateForRoutedStacks(stacks, routes, routesWithAuth, options, extraStateProviders) {
18794
+ async function buildAgentCoreImageContext(candidate, stateProvider, options) {
17477
18795
  const logger = getLogger();
17478
- const out = /* @__PURE__ */ new Map();
17479
- const lambdaIds = uniqueLambdaIds(routes, routesWithAuth);
17480
- const reachableStackNames = /* @__PURE__ */ new Set();
17481
- for (const logicalId of lambdaIds) for (const stack of stacks) {
17482
- const resource = stack.template.Resources?.[logicalId];
17483
- if (resource && resource.Type === "AWS::Lambda::Function") {
17484
- reachableStackNames.add(stack.stackName);
17485
- break;
17486
- }
18796
+ const region = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? candidate.region;
18797
+ let accountId;
18798
+ try {
18799
+ accountId = await resolveCallerAccountId$2(region, options.profile);
18800
+ } catch (err) {
18801
+ 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.`);
17487
18802
  }
17488
- const stackHasIntrinsicEnv = (stackName) => {
17489
- for (const logicalId of lambdaIds) for (const stack of stacks) {
17490
- if (stack.stackName !== stackName) continue;
17491
- const resource = stack.template.Resources?.[logicalId];
17492
- if (!resource || resource.Type !== "AWS::Lambda::Function") continue;
17493
- if (envHasIntrinsicValue(getTemplateEnv(resource))) return true;
18803
+ const context = {};
18804
+ const pseudo = derivePseudoParametersFromRegion(region, accountId);
18805
+ if (pseudo) context.pseudoParameters = pseudo;
18806
+ const loaded = await stateProvider.load(candidate.stackName, candidate.region);
18807
+ if (loaded) {
18808
+ context.stateResources = loaded.resources;
18809
+ if (stateProvider.resolveTemplateSsmParameters) {
18810
+ const ssm = await stateProvider.resolveTemplateSsmParameters(candidate.template);
18811
+ if (Object.keys(ssm.values).length > 0) context.stateParameters = ssm.values;
18812
+ if (ssm.secureStringLogicalIds.length > 0) context.stateSensitiveParameters = ssm.secureStringLogicalIds;
17494
18813
  }
17495
- return false;
18814
+ }
18815
+ return {
18816
+ context,
18817
+ loaded: loaded ?? void 0
17496
18818
  };
17497
- rejectExplicitCfnStackWithMultipleStacks(options, reachableStackNames.size);
17498
- for (const stackName of reachableStackNames) {
17499
- const stack = stacks.find((s) => s.stackName === stackName);
17500
- if (!stack) continue;
17501
- const provider = createLocalStateProvider(options, stack.stackName, await resolveCfnFallbackRegion(options, stack.region), extraStateProviders);
17502
- if (!provider) continue;
18819
+ }
18820
+ /** STS `GetCallerIdentity` for the `${AWS::AccountId}` pseudo parameter (threads `--profile`). */
18821
+ async function resolveCallerAccountId$2(region, profile) {
18822
+ const { STSClient, GetCallerIdentityCommand } = await import("@aws-sdk/client-sts");
18823
+ const sts = new STSClient({
18824
+ ...region && { region },
18825
+ ...profile && { profile }
18826
+ });
18827
+ try {
18828
+ return (await sts.send(new GetCallerIdentityCommand({}))).Account;
18829
+ } finally {
18830
+ sts.destroy();
18831
+ }
18832
+ }
18833
+ /**
18834
+ * Inject AWS credentials into the container env. Precedence:
18835
+ * 1. `--assume-role` → STS-issued temp creds for the resolved ARN (on
18836
+ * STS failure, warn + fall through to dev creds).
18837
+ * 2. dev shell creds (`forwardAwsEnv`) + `--profile` overlay
18838
+ * ({@link applyProfileCredentialsOverlay}) + the bind-mounted
18839
+ * credentials-file env so handler `fromIni({ profile })` resolves.
18840
+ *
18841
+ * Exported so a unit test can lock the binding (mock STS) without driving
18842
+ * the full synth + docker pipeline.
18843
+ */
18844
+ async function applyAgentCoreCredentialEnv(dockerEnv, args) {
18845
+ const logger = getLogger();
18846
+ let assumeSucceeded = false;
18847
+ if (args.assumeRoleArn) {
18848
+ const stsRegion = args.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"];
17503
18849
  try {
17504
- const loaded = await provider.load(stack.stackName, stack.region);
17505
- if (!loaded) continue;
17506
- const bundle = { state: {
17507
- version: 1,
17508
- stackName: stack.stackName,
17509
- resources: loaded.resources,
17510
- outputs: loaded.outputs,
17511
- lastModified: 0
17512
- } };
17513
- if (stackHasIntrinsicEnv(stackName)) {
17514
- const pseudo = await resolvePseudoParametersForStartApi(loaded.region, options);
17515
- if (pseudo) bundle.pseudoParameters = pseudo;
17516
- }
17517
- if (provider.resolveDeployedFunctionEnv) {
17518
- const deployedEnvByLambda = /* @__PURE__ */ new Map();
17519
- for (const logicalId of lambdaIds) {
17520
- const resource = stack.template.Resources?.[logicalId];
17521
- if (!resource || resource.Type !== "AWS::Lambda::Function") continue;
17522
- if (!envHasIntrinsicValue(getTemplateEnv(resource))) continue;
17523
- const physicalId = loaded.resources[logicalId]?.physicalId;
17524
- if (!physicalId) continue;
17525
- const deployedEnv = await provider.resolveDeployedFunctionEnv(physicalId);
17526
- if (deployedEnv) deployedEnvByLambda.set(logicalId, deployedEnv);
17527
- }
17528
- if (deployedEnvByLambda.size > 0) bundle.deployedEnvByLambda = deployedEnvByLambda;
17529
- }
17530
- if (stackHasIntrinsicEnv(stackName) && provider.resolveTemplateSsmParameters) {
17531
- const ssmParameters = await provider.resolveTemplateSsmParameters(stack.template);
17532
- if (Object.keys(ssmParameters.values).length > 0) bundle.ssmParameters = ssmParameters.values;
17533
- if (ssmParameters.secureStringLogicalIds.length > 0) bundle.ssmSecureStringLogicalIds = ssmParameters.secureStringLogicalIds;
18850
+ const creds = await assumeAgentCoreExecutionRole(args.assumeRoleArn, stsRegion);
18851
+ dockerEnv["AWS_ACCESS_KEY_ID"] = creds.accessKeyId;
18852
+ dockerEnv["AWS_SECRET_ACCESS_KEY"] = creds.secretAccessKey;
18853
+ dockerEnv["AWS_SESSION_TOKEN"] = creds.sessionToken;
18854
+ if (stsRegion) dockerEnv["AWS_REGION"] = stsRegion;
18855
+ assumeSucceeded = true;
18856
+ } catch (err) {
18857
+ logger.warn(`--assume-role: STS AssumeRole(${args.assumeRoleArn}) failed: ${err instanceof Error ? err.message : String(err)}. Falling back to the developer's shell credentials.`);
18858
+ }
18859
+ }
18860
+ if (!assumeSucceeded) {
18861
+ forwardAwsEnv$1(dockerEnv);
18862
+ applyProfileCredentialsOverlay(dockerEnv, args.profileCredentials, false);
18863
+ if (args.profileCredsFile) {
18864
+ dockerEnv["AWS_SHARED_CREDENTIALS_FILE"] = args.profileCredsFile.containerPath;
18865
+ dockerEnv["AWS_PROFILE"] = args.profileCredsFile.profileName;
18866
+ }
18867
+ }
18868
+ }
18869
+ /**
18870
+ * Resolve the role ARN to assume, honoring the three `--assume-role` forms.
18871
+ * Bare `--assume-role` uses the runtime's literal `RoleArn`; when that is an
18872
+ * intrinsic (the common L2 case — `Fn::GetAtt` to an auto-created role) it
18873
+ * resolves the execution-role ARN from `--from-cfn-stack` state first; when
18874
+ * that misses (issue #187 `ListStackResources` returns the role's name,
18875
+ * not its ARN, so `attributes.Arn` is empty on the CFn state map) it falls
18876
+ * back to `stateProvider.resolveAgentCoreRuntimeRoleArn(<physicalId>)`
18877
+ * (a `bedrock-agentcore-control:GetAgentRuntime` call) so a same-stack
18878
+ * execution role still resolves. Only warns + falls back to dev creds
18879
+ * when all of those miss.
18880
+ */
18881
+ async function resolveAssumeRoleArn(options, resolved, loaded, stateProvider) {
18882
+ if (typeof options.assumeRole === "string") return options.assumeRole;
18883
+ if (options.assumeRole !== true) return void 0;
18884
+ if (resolved.roleArn) return resolved.roleArn;
18885
+ if (loaded) {
18886
+ const fromState = resolveExecutionRoleArnFromState(loaded, resolved.logicalId, "RoleArn");
18887
+ if (fromState) {
18888
+ getLogger().debug(`--assume-role: resolved RoleArn from state: ${fromState}`);
18889
+ return fromState;
18890
+ }
18891
+ const runtimePhysicalId = loaded.resources[resolved.logicalId]?.physicalId;
18892
+ if (stateProvider?.resolveAgentCoreRuntimeRoleArn && runtimePhysicalId) {
18893
+ const liveArn = await stateProvider.resolveAgentCoreRuntimeRoleArn(runtimePhysicalId);
18894
+ if (liveArn) {
18895
+ getLogger().info(`--assume-role: auto-resolved execution role from GetAgentRuntime: ${liveArn}`);
18896
+ return liveArn;
17534
18897
  }
17535
- out.set(stackName, bundle);
17536
- logger.debug(`${provider.label}: loaded state for ${stackName} (${loaded.region})`);
17537
- } finally {
17538
- provider.dispose();
17539
18898
  }
17540
18899
  }
17541
- return out;
18900
+ 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.");
18901
+ }
18902
+ function emitResult(result) {
18903
+ const logger = getLogger();
18904
+ if (result.status >= 400) {
18905
+ logger.warn(`Agent /invocations returned HTTP ${result.status}.`);
18906
+ process.exitCode = 1;
18907
+ }
18908
+ if (result.streamed) {
18909
+ process.stdout.write("\n");
18910
+ return;
18911
+ }
18912
+ process.stdout.write(`${result.raw}\n`);
17542
18913
  }
17543
18914
  /**
17544
- * Build the AWS pseudo-parameter bag for `--from-state` env-var
17545
- * substitution. Mirrors `resolvePseudoParametersForInvoke` in
17546
- * `local-invoke.ts` byte-for-byte kept inlined here rather than
17547
- * extracted into a shared helper because the two call sites differ in
17548
- * region precedence (this one is per-stack so the resolved state
17549
- * region takes priority).
18915
+ * Finish a `/ws` exchange: the frames were already streamed to stdout via the
18916
+ * onMessage sink, so just terminate with a newline (so the shell prompt resumes
18917
+ * cleanly) and note the frame count at debug level.
18918
+ */
18919
+ function emitWsResult(result) {
18920
+ process.stdout.write("\n");
18921
+ getLogger().debug(`Agent /ws closed after ${result.frames} frame(s).`);
18922
+ }
18923
+ /**
18924
+ * Build the JSON-RPC request to send to an MCP runtime from `--event`:
18925
+ * - no `--event` (empty object) → `tools/list` (discover the server's tools),
18926
+ * - an object with a string `method` → that method + its `params`,
18927
+ * - anything else → a fail-fast error.
17550
18928
  *
17551
- * Region precedence: `--region` > `AWS_REGION` > `AWS_DEFAULT_REGION` >
17552
- * the state record's region (returned by the active `LocalStateProvider`).
18929
+ * Exported for unit testing.
17553
18930
  */
17554
- async function resolvePseudoParametersForStartApi(stateRegion, options) {
17555
- const logger = getLogger();
17556
- const region = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? stateRegion;
17557
- let accountId;
18931
+ function buildMcpRequest(event) {
18932
+ if (event === void 0 || event === null) return {
18933
+ method: "tools/list",
18934
+ params: {}
18935
+ };
18936
+ 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");
18937
+ const obj = event;
18938
+ if (Object.keys(obj).length === 0) return {
18939
+ method: "tools/list",
18940
+ params: {}
18941
+ };
18942
+ 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");
18943
+ return {
18944
+ method: obj["method"],
18945
+ ...obj["params"] !== void 0 && { params: obj["params"] }
18946
+ };
18947
+ }
18948
+ /** Print the MCP JSON-RPC response; exit 1 when it carried a JSON-RPC error. */
18949
+ function emitMcpResult(result) {
18950
+ if (!result.ok) {
18951
+ getLogger().warn("MCP server returned a JSON-RPC error.");
18952
+ process.exitCode = 1;
18953
+ }
18954
+ process.stdout.write(`${result.raw}\n`);
18955
+ }
18956
+ /**
18957
+ * Build the JSON-RPC request to send to an A2A runtime from `--event`:
18958
+ * - no `--event` (empty object) → `agent/getCard` (discover the agent's card),
18959
+ * - an object with a string `method` → that method + its `params`,
18960
+ * - anything else → a fail-fast error.
18961
+ *
18962
+ * Exported for unit testing.
18963
+ */
18964
+ function buildA2aRequest(event) {
18965
+ if (event === void 0 || event === null) return {
18966
+ method: "agent/getCard",
18967
+ params: {}
18968
+ };
18969
+ 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");
18970
+ const obj = event;
18971
+ if (Object.keys(obj).length === 0) return {
18972
+ method: "agent/getCard",
18973
+ params: {}
18974
+ };
18975
+ 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");
18976
+ return {
18977
+ method: obj["method"],
18978
+ ...obj["params"] !== void 0 && { params: obj["params"] }
18979
+ };
18980
+ }
18981
+ /** Print the A2A JSON-RPC response; exit 1 when it carried a JSON-RPC error. */
18982
+ function emitA2aResult(result) {
18983
+ if (!result.ok) {
18984
+ getLogger().warn("A2A server returned a JSON-RPC error.");
18985
+ process.exitCode = 1;
18986
+ }
18987
+ process.stdout.write(`${result.raw}\n`);
18988
+ }
18989
+ /** Map a `--platform` value to the architecture `buildContainerImage` expects. */
18990
+ function platformToArchitecture(platform) {
18991
+ return platform === "linux/amd64" ? "x86_64" : "arm64";
18992
+ }
18993
+ function forwardAwsEnv$1(env) {
18994
+ for (const key of [
18995
+ "AWS_ACCESS_KEY_ID",
18996
+ "AWS_SECRET_ACCESS_KEY",
18997
+ "AWS_SESSION_TOKEN",
18998
+ "AWS_REGION",
18999
+ "AWS_DEFAULT_REGION"
19000
+ ]) {
19001
+ const value = process.env[key];
19002
+ if (value !== void 0) env[key] = value;
19003
+ }
19004
+ }
19005
+ async function assumeAgentCoreExecutionRole(roleArn, region) {
19006
+ const { STSClient, AssumeRoleCommand } = await import("@aws-sdk/client-sts");
19007
+ const sts = new STSClient({ ...region && { region } });
17558
19008
  try {
17559
- const { STSClient, GetCallerIdentityCommand } = await import("@aws-sdk/client-sts");
17560
- const sts = new STSClient({ ...region && { region } });
19009
+ const creds = (await sts.send(new AssumeRoleCommand({
19010
+ RoleArn: roleArn,
19011
+ RoleSessionName: `${getEmbedConfig().resourceNamePrefix}-invoke-agentcore-${Date.now()}`,
19012
+ DurationSeconds: 3600
19013
+ }))).Credentials;
19014
+ if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) throw new Error(`AssumeRole(${roleArn}) returned no usable credentials.`);
19015
+ return {
19016
+ accessKeyId: creds.AccessKeyId,
19017
+ secretAccessKey: creds.SecretAccessKey,
19018
+ sessionToken: creds.SessionToken
19019
+ };
19020
+ } finally {
19021
+ sts.destroy();
19022
+ }
19023
+ }
19024
+ async function readEvent(options) {
19025
+ if (options.event && options.eventStdin) throw new Error("--event and --event-stdin are mutually exclusive.");
19026
+ if (options.eventStdin) return parseEvent(await readStdin(), "<stdin>");
19027
+ if (options.event) {
19028
+ let raw;
17561
19029
  try {
17562
- accountId = (await sts.send(new GetCallerIdentityCommand({}))).Account;
17563
- } finally {
17564
- sts.destroy();
19030
+ raw = readFileSync(options.event, "utf-8");
19031
+ } catch (err) {
19032
+ throw new Error(`Failed to read --event file '${options.event}': ${err instanceof Error ? err.message : String(err)}`);
17565
19033
  }
19034
+ return parseEvent(raw, options.event);
19035
+ }
19036
+ return {};
19037
+ }
19038
+ function parseEvent(raw, source) {
19039
+ try {
19040
+ return JSON.parse(raw);
17566
19041
  } catch (err) {
17567
- logger.warn(`--from-state: resolver needs \${AWS::AccountId} but STS GetCallerIdentity failed: ${err instanceof Error ? err.message : String(err)}. Substitution will be skipped for AWS::AccountId; affected env entries will be dropped with per-key warnings.`);
19042
+ throw new Error(`Failed to parse event payload from ${source} as JSON: ${err instanceof Error ? err.message : String(err)}`);
17568
19043
  }
17569
- const partitionAndSuffix = region ? derivePartitionAndUrlSuffix(region) : void 0;
17570
- const bag = {
17571
- ...accountId !== void 0 && { accountId },
17572
- ...region !== void 0 && { region },
17573
- ...partitionAndSuffix && {
17574
- partition: partitionAndSuffix.partition,
17575
- urlSuffix: partitionAndSuffix.urlSuffix
17576
- }
17577
- };
17578
- return Object.keys(bag).length === 0 ? void 0 : bag;
17579
19044
  }
17580
- /** Validate `--debug-port-base`. */
17581
- function parseDebugPort(raw) {
17582
- const parsed = parseInt(raw, 10);
17583
- if (!Number.isFinite(parsed) || parsed < 1 || parsed > 65535) throw new Error(`--debug-port-base must be 1..65535 (got '${raw}')`);
17584
- return parsed;
19045
+ async function readStdin() {
19046
+ const chunks = [];
19047
+ for await (const chunk of process.stdin) chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
19048
+ return Buffer.concat(chunks).toString("utf-8");
17585
19049
  }
17586
19050
  /**
17587
- * Resolve the mTLS configuration from CLI options. Returns `undefined`
17588
- * when none of the three `--mtls-*` flags is set (the server stays
17589
- * plain-HTTP). When any of the three is set, ALL THREE must be set —
17590
- * partial configurations are rejected at parse time so the server
17591
- * never boots in a half-configured state.
19051
+ * Read `process.stdin` line-buffered and yield each line as a string (trailing
19052
+ * `\r?\n` stripped). The async iterable completes when stdin EOFs (Ctrl-D /
19053
+ * end-of-pipe), and surrenders the underlying stream when its `return()` is
19054
+ * called so the WS client can close down the source when the server closes
19055
+ * first without leaving stdin held open.
17592
19056
  *
17593
- * Exported for unit testing.
19057
+ * Exported so a unit test can drive the iterable shape directly.
17594
19058
  */
17595
- function resolveMtlsConfig(options) {
17596
- const present = [];
17597
- const absent = [];
17598
- if (options.mtlsTruststore !== void 0 && options.mtlsTruststore !== "") present.push("--mtls-truststore");
17599
- else absent.push("--mtls-truststore");
17600
- if (options.mtlsCert !== void 0 && options.mtlsCert !== "") present.push("--mtls-cert");
17601
- else absent.push("--mtls-cert");
17602
- if (options.mtlsKey !== void 0 && options.mtlsKey !== "") present.push("--mtls-key");
17603
- else absent.push("--mtls-key");
17604
- if (present.length === 0) return void 0;
17605
- if (absent.length > 0) throw new Error(`mTLS configuration is incomplete: ${present.join(", ")} set but ${absent.join(", ")} missing. All three of --mtls-truststore, --mtls-cert, and --mtls-key must be set together to enable mTLS, or all three left unset for plain HTTP.`);
17606
- return readMtlsMaterialsFromDisk({
17607
- truststorePath: options.mtlsTruststore,
17608
- certPath: options.mtlsCert,
17609
- keyPath: options.mtlsKey
19059
+ async function* readStdinLines() {
19060
+ const { createInterface } = await import("node:readline");
19061
+ const rl = createInterface({
19062
+ input: process.stdin,
19063
+ crlfDelay: Infinity
17610
19064
  });
19065
+ try {
19066
+ for await (const line of rl) yield line;
19067
+ } finally {
19068
+ rl.close();
19069
+ }
17611
19070
  }
17612
- /**
17613
- * Builder for the `start-api` subcommand.
17614
- */
17615
- function createLocalStartApiCommand(opts = {}) {
19071
+ function readEnvOverridesFile$2(filePath) {
19072
+ if (!filePath) return void 0;
19073
+ let raw;
19074
+ try {
19075
+ raw = readFileSync(filePath, "utf-8");
19076
+ } catch (err) {
19077
+ throw new Error(`Failed to read --env-vars file '${filePath}': ${err instanceof Error ? err.message : String(err)}`);
19078
+ }
19079
+ let parsed;
19080
+ try {
19081
+ parsed = JSON.parse(raw);
19082
+ } catch (err) {
19083
+ throw new Error(`Failed to parse --env-vars file '${filePath}' as JSON: ${err instanceof Error ? err.message : String(err)}`);
19084
+ }
19085
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`--env-vars file '${filePath}' must contain a JSON object at the top level.`);
19086
+ return parsed;
19087
+ }
19088
+ function createLocalInvokeAgentCoreCommand(opts = {}) {
17616
19089
  setEmbedConfig(opts.embedConfig);
17617
- const startApi = new Command("start-api").description("Run a long-running local HTTP server that maps API Gateway routes (REST v1, HTTP API, Function URL) to Lambda invocations against the AWS Lambda Runtime Interface Emulator (Docker required). Supports Lambda TOKEN/REQUEST authorizers, Cognito User Pool / HTTP v2 JWT authorizers, and AWS_IAM auth (REST v1 `AuthorizationType: AWS_IAM` and Function URL `AuthType: AWS_IAM` — SigV4 signature verification only; IAM policy evaluation is NOT emulated). When JWKS is unreachable, JWT authorizers fall back to pass-through (every token accepted) with a warn line — local dev fallback. VPC-config Lambdas run locally and surface a warn line at startup; their containers do NOT get attached to the deployed VPC subnets, so calls to private RDS / ElastiCache will fail.").argument("[targets...]", `Optional API subset filter. Pass one or more identifiers to serve exactly that subset (the union; each on its own port). Each accepts the bare CDK logical id ('MyHttpApi'; single-stack apps only), stack-qualified logical id ('MyStack:MyHttpApi'), full CDK Construct path ('MyStack/MyHttpApi/Resource'), or an ancestor Construct path that prefix-matches ('MyStack/MyHttpApi'). When omitted in a TTY, a multi-select picker opens with every API pre-selected (Enter serves all, deselect to pick a subset); when omitted in a non-TTY (CI / pipe) every discovered API is served. Mirrors \`${getEmbedConfig().cliName} invoke\` / \`${getEmbedConfig().cliName} run-task\` target syntax.`).addOption(new Option("--port <port>", "HTTP server port (default: auto-allocate)").default("0")).addOption(new Option("--host <host>", "Bind address").default("127.0.0.1")).addOption(new Option("--stack <name>", "Stack to start (single-stack apps auto-detect)")).addOption(new Option("--all-stacks", "Serve every stack's API in a multi-stack app (each API on its own port) instead of erroring out. Mutually exclusive with a positional target subset, --stack, and an explicit --from-cfn-stack <name>; the bare --from-cfn-stack flag stays compatible (binds each routed stack to its own CFn stack).").default(false)).addOption(new Option("--warm", "Pre-start one container per Lambda at server boot").default(false)).addOption(new Option("--per-lambda-concurrency <n>", "Pool size cap per Lambda (default 2, max 4)").default("2")).addOption(new Option("--no-pull", "Skip docker pull (cached image)")).addOption(new Option("--container-host <host>", "IP the host uses to bind/probe the RIE port (must be a numeric IP — `docker run -p <ip>:<port>:8080` rejects hostnames). Defaults to 127.0.0.1.").default("127.0.0.1")).addOption(new Option("--debug-port-base <port>", "Reserve a contiguous --debug-port range (one per Lambda)")).addOption(new Option("--env-vars <file>", "JSON env-var overrides (SAM-compatible: {\"LogicalId\":{\"KEY\":\"VALUE\"}, \"Parameters\": {...}})")).addOption(new Option("--assume-role <arn-or-pair>", "Assume the Lambda's execution role and forward STS-issued temp creds. Bare <arn> = global default; <LogicalId>=<arn> = per-Lambda override (repeatable). Per-Lambda > global > unset (developer creds passed through).").argParser((raw, prev) => parseAssumeRoleToken(raw, prev))).addOption(new Option("--watch", "Hot-reload: re-synth + re-discover routes when the CDK app's source changes (honors cdk.json watch.include/exclude; cdk.out, node_modules, .git are always excluded). Off by default; the server keeps the previous version serving when synth fails mid-reload.").default(false)).addOption(new Option("--stage <name>", "Select an API Gateway Stage by its 'StageName'. Default: the first Stage attached to each API. Drives event.stageVariables for both REST v1 and HTTP API v2. NOTE: For HTTP API v2 routes, requestContext.stage is always '$default' regardless of this flag (AWS-side limitation — HTTP API only exposes one stage to the integration event); only event.stageVariables is affected for v2 routes. For REST v1 routes the selected StageName is also threaded into requestContext.stage.")).addOption(new Option("--api <id>", "DEPRECATED — use the positional <targets...> argument instead. Accepts a SINGLE identifier; for a subset pass multiple positional targets. Same accepted forms (bare logical id, stack-qualified, Construct path, ancestor prefix). Will be removed in a future major release.")).addOption(new Option("--layer-role-arn <arn>", "Role to sts:AssumeRole before calling lambda:GetLayerVersion on every literal-ARN entry in Properties.Layers (issue #448). 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("--from-cfn-stack [cfn-stack-name]", "Read a deployed CloudFormation stack via ListStackResources and substitute Ref / Fn::ImportValue in Lambda 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 per routed stack; pass an explicit value when a single CFn stack should serve every routed stack. 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.")).addOption(new Option("--mtls-truststore <path>", `PEM-encoded CA bundle for client-certificate verification (mutual TLS). When set, the local server switches from HTTP to HTTPS and the TLS handshake rejects clients whose certificate doesn't chain to one of these CAs. Verified certs are surfaced on the Lambda event under requestContext.identity.clientCert (REST v1) / requestContext.authentication.clientCert (HTTP API v2). Must be set together with --mtls-cert + --mtls-key; partial flag sets are rejected. Generate a CA + server + client cert for local dev: openssl req -x509 -newkey rsa:2048 -nodes -keyout ca-key.pem -out ca.pem -subj "/CN=${getEmbedConfig().resourceNamePrefix}-ca" -days 365; openssl req -newkey rsa:2048 -nodes -keyout server-key.pem -out server-csr.pem -subj "/CN=localhost"; openssl x509 -req -in server-csr.pem -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out server-cert.pem -days 365; openssl req -newkey rsa:2048 -nodes -keyout client-key.pem -out client-csr.pem -subj "/CN=client"; openssl x509 -req -in client-csr.pem -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out client-cert.pem -days 365; curl --cacert ca.pem --cert client-cert.pem --key client-key.pem https://localhost:<port>/...`)).addOption(new Option("--mtls-cert <path>", "PEM-encoded server certificate for mutual TLS. Self-signed is fine for local dev. Must be set together with --mtls-truststore + --mtls-key.")).addOption(new Option("--mtls-key <path>", "PEM-encoded server private key matching --mtls-cert. Must be set together with --mtls-truststore + --mtls-cert.")).addOption(new Option("--strict-sigv4", "Opt-in: DENY AWS_IAM SigV4 requests that cannot be cryptographically verified (foreign access-key-id — e.g. a federated / Cognito Identity Pool / cross-account signer — OR no local AWS credentials configured) instead of the default warn-and-pass. DEFAULT off: cdk-local warn-and-passes unverifiable IAM requests with a placeholder principalId so local dev exercises app logic without reproducing an auth boundary it cannot fully emulate. OAC-fronted Function URLs always warn-and-pass regardless.").default(false)).action(withErrorHandling(async (targets, options) => {
17618
- await localStartApiCommand(targets, options, opts.extraStateProviders);
19090
+ 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)").action(withErrorHandling(async (target, options) => {
19091
+ await localInvokeAgentCoreCommand(target, options, opts.extraStateProviders);
17619
19092
  }));
19093
+ addInvokeAgentCoreSpecificOptions(cmd);
17620
19094
  [
17621
19095
  ...commonOptions(),
17622
19096
  ...appOptions(),
17623
19097
  ...contextOptions
17624
- ].forEach((opt) => startApi.addOption(opt));
17625
- startApi.addOption(deprecatedRegionOption);
17626
- return startApi;
19098
+ ].forEach((opt) => cmd.addOption(opt));
19099
+ cmd.addOption(deprecatedRegionOption);
19100
+ return cmd;
19101
+ }
19102
+ /**
19103
+ * Register the option block that `cdkl invoke-agentcore` adds on top of
19104
+ * the shared common / app / context option helpers. Shared between
19105
+ * `cdkl invoke-agentcore` and any host CLI (e.g. cdkd's
19106
+ * `local invoke-agentcore`) that wraps the single-shot AgentCore Runtime
19107
+ * container runner, so adding or renaming an `invoke-agentcore`-only flag
19108
+ * here propagates to every embedder without duplicate `.addOption(...)`
19109
+ * blocks.
19110
+ *
19111
+ * Calling order only affects `--help` presentation (Commander parses
19112
+ * insertion-order-independent). The host-CLI convention is host-specific
19113
+ * options first, then this helper, then the shared common / app / context
19114
+ * options — host flags / invoke-agentcore flags / common flags grouped
19115
+ * in three `--help` clusters. Chainable: returns `cmd`.
19116
+ */
19117
+ function addInvokeAgentCoreSpecificOptions(cmd) {
19118
+ return cmd.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."));
17627
19119
  }
17628
19120
 
17629
19121
  //#endregion
@@ -18653,23 +20145,332 @@ function shellJoin(parts) {
18653
20145
  }
18654
20146
 
18655
20147
  //#endregion
18656
- //#region src/local/ecs-service-resolver.ts
20148
+ //#region src/cli/commands/local-run-task.ts
20149
+ /**
20150
+ * `cdkl run-task <target>` — Phase 1 of the ECS local-execution
20151
+ * trilogy. Synthesizes the CDK app, locates the target
20152
+ * `AWS::ECS::TaskDefinition`, stands up a per-task docker network with
20153
+ * the AWS-published `amazon-ecs-local-container-endpoints` sidecar, and
20154
+ * starts every container in `dependsOn` order. The essential
20155
+ * container's exit code drives the CLI's exit.
20156
+ */
20157
+ async function localRunTaskCommand(target, options, extraStateProviders) {
20158
+ const logger = getLogger();
20159
+ if (options.verbose) logger.setLevel("debug");
20160
+ warnIfDeprecatedRegion(options);
20161
+ const state = createEcsRunState();
20162
+ let sigintHandler;
20163
+ let sigintCount = 0;
20164
+ let stateProvider;
20165
+ let profileCredsFile;
20166
+ let cleanupPromise;
20167
+ const cleanup = async () => {
20168
+ if (!cleanupPromise) cleanupPromise = (async () => {
20169
+ try {
20170
+ await cleanupEcsRun(state, { keepRunning: options.keepRunning });
20171
+ } catch (err) {
20172
+ getLogger().debug(`cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
20173
+ }
20174
+ if (profileCredsFile) try {
20175
+ await profileCredsFile.dispose();
20176
+ } catch (err) {
20177
+ getLogger().debug(`Failed to remove profile credentials tmpdir ${profileCredsFile.hostPath}: ${err instanceof Error ? err.message : String(err)}`);
20178
+ }
20179
+ })();
20180
+ await cleanupPromise;
20181
+ };
20182
+ try {
20183
+ await applyRoleArnIfSet({
20184
+ roleArn: options.roleArn,
20185
+ region: options.region
20186
+ });
20187
+ await ensureDockerAvailable();
20188
+ const appCmd = resolveApp(options.app);
20189
+ if (!appCmd) throw new Error(`No CDK app specified. Pass --app, set ${getEmbedConfig().envPrefix}_APP, or add "app" to cdk.json.`);
20190
+ logger.info("Synthesizing CDK app...");
20191
+ const synthesizer = new Synthesizer();
20192
+ const context = parseContextOptions(options.context);
20193
+ const synthOpts = {
20194
+ app: appCmd,
20195
+ output: options.output,
20196
+ ...options.region && { region: options.region },
20197
+ ...options.profile && { profile: options.profile },
20198
+ ...Object.keys(context).length > 0 && { context }
20199
+ };
20200
+ const { stacks } = await synthesizer.synthesize(synthOpts);
20201
+ const resolvedTarget = await resolveSingleTarget(target, {
20202
+ entries: listTargets(stacks).ecsTaskDefinitions,
20203
+ message: "Select an ECS task definition to run",
20204
+ noun: "ECS task definitions",
20205
+ 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")
20206
+ });
20207
+ const candidate = pickCandidateStack$1(parseEcsTarget(resolvedTarget).stackPattern, stacks);
20208
+ stateProvider = createLocalStateProvider(options, candidate?.stackName ?? "", await resolveCfnFallbackRegion(options, candidate?.region), extraStateProviders);
20209
+ const imageContext = await buildEcsImageResolutionContext$1(candidate, stateProvider, options);
20210
+ const task = resolveEcsTaskTarget(resolvedTarget, stacks, imageContext);
20211
+ logger.info(`Target: ${task.stack.stackName}/${task.taskDefinitionLogicalId} (family=${task.family}, containers=${task.containers.length})`);
20212
+ const taskNeeds = detectEcsImageResolutionNeeds(stacks.find((s) => s.stackName === task.stack.stackName) ?? task.stack);
20213
+ if (stateProvider && taskNeeds.needsCrossStackResolver) {
20214
+ const consumerRegion = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? task.stack.region ?? "us-east-1";
20215
+ const resolver = await stateProvider.buildCrossStackResolver(consumerRegion);
20216
+ if (resolver) await applyCrossStackResolverToTask(task, {
20217
+ resources: imageContext?.stateResources ?? {},
20218
+ ...imageContext?.pseudoParameters && { pseudoParameters: imageContext.pseudoParameters },
20219
+ ...imageContext?.stateParameters && { parameters: imageContext.stateParameters },
20220
+ ...imageContext?.stateSensitiveParameters?.length && { sensitiveParameters: new Set(imageContext.stateSensitiveParameters) },
20221
+ consumerRegion,
20222
+ crossStackResolver: resolver
20223
+ });
20224
+ } 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.");
20225
+ sigintHandler = () => {
20226
+ sigintCount += 1;
20227
+ if (sigintCount >= 2) {
20228
+ process.stderr.write("Force-exit on second ^C; container cleanup skipped.\n");
20229
+ process.exit(130);
20230
+ }
20231
+ logger.info("Stopping task...");
20232
+ cleanup().then(() => process.exit(130));
20233
+ };
20234
+ process.on("SIGINT", sigintHandler);
20235
+ let assumedCredentials;
20236
+ let resolvedRoleArn;
20237
+ if (options.assumeTaskRole === true) {
20238
+ 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>`);
20239
+ resolvedRoleArn = await resolvePlaceholderAccount$1(task.taskRoleArn, options.region);
20240
+ assumedCredentials = await assumeTaskRole$1(resolvedRoleArn, options.region);
20241
+ } else if (typeof options.assumeTaskRole === "string") {
20242
+ resolvedRoleArn = options.assumeTaskRole;
20243
+ assumedCredentials = await assumeTaskRole$1(resolvedRoleArn, options.region);
20244
+ }
20245
+ const sidecarCredentials = await resolveSidecarCredentials(options, assumedCredentials);
20246
+ if (options.profile && sidecarCredentials && !assumedCredentials) profileCredsFile = await writeProfileCredentialsFile(options.profile, sidecarCredentials);
20247
+ const envOverrides = readEnvOverridesFile$1(options.envVars);
20248
+ const runOpts = {
20249
+ cluster: options.cluster,
20250
+ containerHost: options.containerHost,
20251
+ skipPull: options.pull === false,
20252
+ keepRunning: options.keepRunning,
20253
+ detach: options.detach
20254
+ };
20255
+ if (envOverrides) runOpts.envOverrides = envOverrides;
20256
+ if (sidecarCredentials) runOpts.taskCredentials = sidecarCredentials;
20257
+ if (resolvedRoleArn) runOpts.taskRoleArn = resolvedRoleArn;
20258
+ if (options.platform) runOpts.platformOverride = options.platform;
20259
+ if (options.region) runOpts.region = options.region;
20260
+ if (options.ecrRoleArn) runOpts.ecrRoleArn = options.ecrRoleArn;
20261
+ if (options.profile) runOpts.profile = options.profile;
20262
+ const hostPortOverrides = parseHostPortOverrides(options.hostPort);
20263
+ if (Object.keys(hostPortOverrides).length > 0) runOpts.hostPortOverrides = hostPortOverrides;
20264
+ if (profileCredsFile) runOpts.profileCredentialsFile = {
20265
+ hostPath: profileCredsFile.hostPath,
20266
+ containerPath: profileCredsFile.containerPath,
20267
+ profileName: profileCredsFile.profileName
20268
+ };
20269
+ const result = await runEcsTask(task, runOpts, state);
20270
+ if (options.detach) {
20271
+ logger.info(`Task containers started in detached mode; ${getEmbedConfig().binaryName} is exiting.`);
20272
+ logger.info(`Use 'docker ps --filter network=${result.state.network?.networkName ?? "<network>"}' to inspect; tear down with 'docker rm -f' and 'docker network rm'.`);
20273
+ sigintCount = 99;
20274
+ return;
20275
+ }
20276
+ if (result.essentialContainerName) logger.info(`Essential container '${result.essentialContainerName}' exited with code ${result.exitCode}.`);
20277
+ if (result.exitCode !== 0) process.exitCode = result.exitCode;
20278
+ } finally {
20279
+ if (sigintHandler) process.off("SIGINT", sigintHandler);
20280
+ if (stateProvider) stateProvider.dispose();
20281
+ if (!options.detach) await cleanup();
20282
+ }
20283
+ }
20284
+ /**
20285
+ * If `arn` contains the `${AWS::AccountId}` placeholder emitted by the
20286
+ * resolver for inline same-stack IAM Roles, substitute the live caller
20287
+ * account via STS `GetCallerIdentity`. Otherwise pass through unchanged.
20288
+ */
20289
+ async function resolvePlaceholderAccount$1(arn, region) {
20290
+ if (!arn.includes("${AWS::AccountId}")) return arn;
20291
+ const { STSClient, GetCallerIdentityCommand } = await import("@aws-sdk/client-sts");
20292
+ const sts = new STSClient({ ...region && { region } });
20293
+ try {
20294
+ const account = (await sts.send(new GetCallerIdentityCommand({}))).Account;
20295
+ 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>`);
20296
+ return arn.split(TASK_ROLE_ACCOUNT_PLACEHOLDER).join(account);
20297
+ } finally {
20298
+ sts.destroy();
20299
+ }
20300
+ }
20301
+ /**
20302
+ * Assume `roleArn` and return temp credentials.
20303
+ */
20304
+ async function assumeTaskRole$1(roleArn, region) {
20305
+ const { STSClient, AssumeRoleCommand } = await import("@aws-sdk/client-sts");
20306
+ const sts = new STSClient({ ...region && { region } });
20307
+ try {
20308
+ const creds = (await sts.send(new AssumeRoleCommand({
20309
+ RoleArn: roleArn,
20310
+ RoleSessionName: `${getEmbedConfig().resourceNamePrefix}-run-task-${Date.now()}`,
20311
+ DurationSeconds: 3600
20312
+ }))).Credentials;
20313
+ if (!creds?.AccessKeyId || !creds.SecretAccessKey || !creds.SessionToken) throw new Error(`AssumeRole(${roleArn}) returned no usable credentials.`);
20314
+ return {
20315
+ accessKeyId: creds.AccessKeyId,
20316
+ secretAccessKey: creds.SecretAccessKey,
20317
+ sessionToken: creds.SessionToken
20318
+ };
20319
+ } finally {
20320
+ sts.destroy();
20321
+ }
20322
+ }
18657
20323
  /**
18658
- * Walk the synth template to locate an `AWS::ECS::Service` by display
18659
- * path or stack-qualified logical id, resolve its `TaskDefinition`
18660
- * reference, and chain into the existing `resolveEcsTaskTarget` machinery
18661
- * to produce a `ResolvedEcsService` carrying both the service knobs and
18662
- * the underlying task descriptor.
20324
+ * Build the substitution context the ECS task resolver consumes.
20325
+ * Returns `undefined` when no container's `Image` field needs
20326
+ * substitution the resolver behaves as before in that case.
20327
+ */
20328
+ async function buildEcsImageResolutionContext$1(candidate, stateProvider, options) {
20329
+ const logger = getLogger();
20330
+ if (!candidate) return void 0;
20331
+ const needs = detectEcsImageResolutionNeeds(candidate);
20332
+ if (!needs.needsPseudoParameters && !needs.needsStateResources && !needs.needsEnvOrSecretSubstitution) return;
20333
+ const ctx = {};
20334
+ const wantsPseudoForEnvOrSecret = !!stateProvider && needs.needsEnvOrSecretSubstitution;
20335
+ if (needs.needsPseudoParameters || wantsPseudoForEnvOrSecret) {
20336
+ const region = options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"] ?? candidate.region;
20337
+ 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.`);
20338
+ let accountId;
20339
+ try {
20340
+ accountId = await resolveCallerAccountId$1(region, options.profile);
20341
+ } catch (err) {
20342
+ 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.`);
20343
+ }
20344
+ const partitionAndSuffix = region ? derivePartitionAndUrlSuffix(region) : void 0;
20345
+ ctx.pseudoParameters = {
20346
+ ...accountId !== void 0 && { accountId },
20347
+ ...region !== void 0 && { region },
20348
+ ...partitionAndSuffix && {
20349
+ partition: partitionAndSuffix.partition,
20350
+ urlSuffix: partitionAndSuffix.urlSuffix
20351
+ }
20352
+ };
20353
+ }
20354
+ const wantsState = needs.needsStateResources || needs.needsEnvOrSecretSubstitution;
20355
+ if (stateProvider && wantsState) {
20356
+ const loaded = await stateProvider.load(candidate.stackName, candidate.region);
20357
+ if (loaded) ctx.stateResources = loaded.resources;
20358
+ if (needs.needsEnvOrSecretSubstitution && stateProvider.resolveTemplateSsmParameters) {
20359
+ const ssmParameters = await stateProvider.resolveTemplateSsmParameters(candidate.template);
20360
+ if (Object.keys(ssmParameters.values).length > 0) ctx.stateParameters = ssmParameters.values;
20361
+ if (ssmParameters.secureStringLogicalIds.length > 0) ctx.stateSensitiveParameters = ssmParameters.secureStringLogicalIds;
20362
+ }
20363
+ } 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.");
20364
+ 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).");
20365
+ return ctx;
20366
+ }
20367
+ function pickCandidateStack$1(stackPattern, stacks) {
20368
+ if (stackPattern === null) {
20369
+ if (stacks.length === 1) return stacks[0];
20370
+ return;
20371
+ }
20372
+ const matched = matchStacks(stacks, [stackPattern]);
20373
+ if (matched.length === 1) return matched[0];
20374
+ }
20375
+ async function resolveCallerAccountId$1(region, profile) {
20376
+ const { STSClient, GetCallerIdentityCommand } = await import("@aws-sdk/client-sts");
20377
+ const sts = new STSClient({
20378
+ ...region && { region },
20379
+ ...profile && { profile }
20380
+ });
20381
+ try {
20382
+ return (await sts.send(new GetCallerIdentityCommand({}))).Account;
20383
+ } finally {
20384
+ sts.destroy();
20385
+ }
20386
+ }
20387
+ /**
20388
+ * Read the `--env-vars` JSON file using the same SAM-style shape as
20389
+ * `cdkl invoke --env-vars`: top-level keys are container names, with
20390
+ * `Parameters` reserved for global entries.
20391
+ */
20392
+ function readEnvOverridesFile$1(filePath) {
20393
+ if (!filePath) return void 0;
20394
+ let raw;
20395
+ try {
20396
+ raw = readFileSync(filePath, "utf-8");
20397
+ } catch (err) {
20398
+ throw new Error(`Failed to read --env-vars file '${filePath}': ${err instanceof Error ? err.message : String(err)}`);
20399
+ }
20400
+ let parsed;
20401
+ try {
20402
+ parsed = JSON.parse(raw);
20403
+ } catch (err) {
20404
+ throw new Error(`Failed to parse --env-vars file '${filePath}' as JSON: ${err instanceof Error ? err.message : String(err)}`);
20405
+ }
20406
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`--env-vars file '${filePath}' must contain a JSON object at the top level.`);
20407
+ return parsed;
20408
+ }
20409
+ /**
20410
+ * Pick the credentials forwarded to the AWS-published
20411
+ * `amazon-ecs-local-container-endpoints` sidecar. Precedence:
20412
+ * 1. `--assume-task-role <arn>` (or bare `--assume-task-role` against
20413
+ * a resolvable `TaskRoleArn`) → STS-assumed temp creds. Highest
20414
+ * priority — when the user opted in to IAM emulation, those creds
20415
+ * drive the sidecar regardless of `--profile`.
20416
+ * 2. `--profile <p>` → resolved via {@link resolveProfileCredentials}
20417
+ * (the SDK's default credential provider chain — SSO / IAM
20418
+ * Identity Center / fromIni / role-assumption). NEW in this PR.
20419
+ * 3. Neither set → `undefined`; the sidecar runs with its own
20420
+ * default credential chain (typically empty inside a fresh
20421
+ * container — user containers will get 4xx from the credentials
20422
+ * endpoint, mimicking IAM-misconfigured prod).
20423
+ *
20424
+ * Extracted as an exported helper so a unit test can exercise every
20425
+ * branch without having to mock the full Synth + Docker + AWS pipeline
20426
+ * (the strategy used for the Lambda container path).
20427
+ */
20428
+ async function resolveSidecarCredentials(options, assumedCredentials) {
20429
+ if (assumedCredentials) return assumedCredentials;
20430
+ if (options.profile) return resolveProfileCredentials(options.profile);
20431
+ }
20432
+ function createLocalRunTaskCommand(opts = {}) {
20433
+ setEmbedConfig(opts.embedConfig);
20434
+ 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)").action(withErrorHandling(async (target, options) => {
20435
+ await localRunTaskCommand(target, options, opts.extraStateProviders);
20436
+ }));
20437
+ addRunTaskSpecificOptions(cmd);
20438
+ [
20439
+ ...commonOptions(),
20440
+ ...appOptions(),
20441
+ ...contextOptions
20442
+ ].forEach((opt) => cmd.addOption(opt));
20443
+ cmd.addOption(deprecatedRegionOption);
20444
+ return cmd;
20445
+ }
20446
+ /**
20447
+ * Register the option block that `cdkl run-task` adds on top of the shared
20448
+ * common / app / context option helpers. Shared between `cdkl run-task` and
20449
+ * any host CLI (e.g. cdkd's `local run-task`) that wraps the single-task
20450
+ * ECS local runner, so adding or renaming a `run-task`-only flag here
20451
+ * propagates to every embedder without duplicate `.addOption(...)` blocks.
18663
20452
  *
18664
- * Target shape mirrors `cdkl run-task`: `<Stack>/<DisplayPath>` or
18665
- * `<Stack>:<LogicalId>`; single-stack apps may omit the stack prefix.
20453
+ * Calling order only affects `--help` presentation (Commander parses
20454
+ * insertion-order-independent). The host-CLI convention is host-specific
20455
+ * options first, then this helper, then the shared common / app / context
20456
+ * options — host flags / run-task flags / common flags grouped in three
20457
+ * `--help` clusters. Chainable: returns `cmd`.
18666
20458
  *
18667
- * Optional `context` (same as the task resolver) carries the ECR image
18668
- * substitution data pseudo parameters (Tier 1) + state-recorded
18669
- * resources (Tier 2). The CLI builds it lazily when the candidate
18670
- * service's task definition actually needs substitution.
20459
+ * NOTE: `run-task` does NOT compose with {@link addCommonEcsServiceOptions}
20460
+ * even though many flags overlap. The two ECS surfaces (single-task vs
20461
+ * multi-replica service) have intentionally divergent defaults
20462
+ * (`run-task` has no `--max-tasks` / `--restart-policy`; `start-service`
20463
+ * / `start-alb` have no `--host-port` / `--keep-running` / `--detach`),
20464
+ * and folding `run-task` into the service common block would mutate the
20465
+ * surface non-trivially. Each command keeps its own helper.
18671
20466
  */
18672
- function resolveEcsServiceTarget(target, stacks, context) {
20467
+ function addRunTaskSpecificOptions(cmd) {
20468
+ return cmd.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."));
20469
+ }
20470
+
20471
+ //#endregion
20472
+ //#region src/local/ecs-service-resolver.ts
20473
+ function resolveEcsServiceTarget(target, stacks, context, options) {
18673
20474
  if (stacks.length === 0) throw new EcsTaskResolutionError("No stacks found in the synthesized assembly.");
18674
20475
  const parsed = parseEcsTarget(target);
18675
20476
  const stack = pickStack$1(parsed, stacks);
@@ -18691,14 +20492,14 @@ function resolveEcsServiceTarget(target, stacks, context) {
18691
20492
  if (!serviceLogicalId || !serviceResource) throw notFoundError(target, stack, resources, parsed);
18692
20493
  if (serviceResource.Type === "AWS::ECS::TaskDefinition") throw new EcsTaskResolutionError(`Resource '${serviceLogicalId}' in ${stack.stackName} is an ECS TaskDefinition, not a Service. Use \`${getEmbedConfig().cliName} run-task\` for one-shot tasks; \`${getEmbedConfig().cliName} start-service\` is Service-only.`);
18693
20494
  if (serviceResource.Type !== "AWS::ECS::Service") throw new EcsTaskResolutionError(`Resource '${serviceLogicalId}' in ${stack.stackName} is ${serviceResource.Type}, not an AWS::ECS::Service.`);
18694
- return extractServiceProperties(stack, serviceLogicalId, serviceResource, stacks, context);
20495
+ return extractServiceProperties(stack, serviceLogicalId, serviceResource, stacks, context, options);
18695
20496
  }
18696
20497
  /**
18697
20498
  * Pure-functional extraction from the synth resource. Exposed for unit
18698
20499
  * testing the per-field resolution rules (DesiredCount default, missing
18699
20500
  * TaskDefinition, intrinsic shapes).
18700
20501
  */
18701
- function extractServiceProperties(stack, serviceLogicalId, resource, stacks, context) {
20502
+ function extractServiceProperties(stack, serviceLogicalId, resource, stacks, context, options) {
18702
20503
  const props = resource.Properties ?? {};
18703
20504
  const warnings = [];
18704
20505
  const taskDefRef = props["TaskDefinition"];
@@ -18708,7 +20509,10 @@ function extractServiceProperties(stack, serviceLogicalId, resource, stacks, con
18708
20509
  const desiredCount = parseDesiredCount(props["DesiredCount"], serviceLogicalId);
18709
20510
  const healthCheckGracePeriodSeconds = parseHealthCheckGrace(props["HealthCheckGracePeriodSeconds"], serviceLogicalId);
18710
20511
  const serviceName = parseServiceName(props["ServiceName"], serviceLogicalId);
18711
- if (Array.isArray(props["LoadBalancers"]) && props["LoadBalancers"].length > 0) warnings.push(`ECS Service '${serviceLogicalId}' declares LoadBalancers, but local load-balancer emulation is deferred to a follow-up PR. Containers are NOT registered to a local listener; reach them via their published ports.`);
20512
+ if (!options?.suppressLoadBalancerWarning && Array.isArray(props["LoadBalancers"]) && props["LoadBalancers"].length > 0) {
20513
+ const { cliName } = getEmbedConfig();
20514
+ warnings.push(`ECS Service '${serviceLogicalId}' declares LoadBalancers, but \`${cliName} start-service\` runs the replicas only; no local listener fronts them. Reach the containers via their published ports, or run \`${cliName} start-alb <Stack>/<Alb>\` to boot the same replicas behind a local front-door that round-robins the listener rules.`);
20515
+ }
18712
20516
  const serviceConnect = extractServiceConnect(props["ServiceConnectConfiguration"], task);
18713
20517
  const out = {
18714
20518
  stack,
@@ -18999,11 +20803,14 @@ async function getPublishedHostPort(containerId, containerPort, protocol = "tcp"
18999
20803
  * design's §O5 "--no-envoy by default" recommendation.
19000
20804
  *
19001
20805
  * Deferred to follow-up PRs:
19002
- * - Local load-balancer emulation (LB listener + target-group health
19003
- * check + round-robin) — separate PR per the issue's PR-split.
19004
20806
  * - Envoy sidecar for Service Connect L7 routing / retries / circuit
19005
20807
  * breaking (Cloud Map DNS-only mode ships now).
19006
20808
  * - Rolling deployment (`--reload` / `--watch`).
20809
+ *
20810
+ * Local load-balancer emulation is now first-class via `start-alb`, which
20811
+ * boots the same replicas behind a local front-door that mirrors the
20812
+ * deployed ALB's listener-rule / forward / redirect / fixed-response /
20813
+ * authenticate-* surface; `start-service` runs the replicas only.
19007
20814
  */
19008
20815
  var EcsServiceRunnerError = class EcsServiceRunnerError extends Error {
19009
20816
  constructor(message) {
@@ -21555,7 +23362,7 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
21555
23362
  };
21556
23363
  process.on("SIGINT", sigintHandler);
21557
23364
  process.on("SIGTERM", sigintHandler);
21558
- for (const pt of perTarget) pt.controller = await bootOneTarget(pt.boot, pt.runState, stacks, options, discovery, skipPull, extraStateProviders, profileCredsFile, frontDoorByService.get(pt.boot.target));
23365
+ for (const pt of perTarget) pt.controller = await bootOneTarget(pt.boot, pt.runState, stacks, options, discovery, skipPull, extraStateProviders, profileCredsFile, frontDoorByService.get(pt.boot.target), strategy.suppressLoadBalancerWarning === true);
21559
23366
  if (perTarget.length > 0) {
21560
23367
  const summary = perTarget.map((pt) => `${pt.controller.service.serviceName} (${pt.controller.activeReplicaCount()} replica(s))`).join(", ");
21561
23368
  logger.info(`Service(s) running: ${summary}.`);
@@ -21572,20 +23379,20 @@ async function runEcsServiceEmulator(targets, options, strategy, extraStateProvi
21572
23379
  await cleanup();
21573
23380
  }
21574
23381
  }
21575
- async function bootOneTarget(boot, runState, stacks, options, discovery, skipPull, extraStateProviders, profileCredsFile, frontDoorPools) {
23382
+ async function bootOneTarget(boot, runState, stacks, options, discovery, skipPull, extraStateProviders, profileCredsFile, frontDoorPools, suppressLoadBalancerWarning) {
21576
23383
  const candidate = pickCandidateStack(parseEcsTarget(boot.target).stackPattern, stacks);
21577
23384
  const stateProvider = createLocalStateProvider(options, candidate?.stackName ?? "", await resolveCfnFallbackRegion(options, candidate?.region), extraStateProviders);
21578
23385
  try {
21579
- return await runOneTarget(boot, runState, stacks, options, discovery, skipPull, stateProvider, profileCredsFile, frontDoorPools);
23386
+ return await runOneTarget(boot, runState, stacks, options, discovery, skipPull, stateProvider, profileCredsFile, frontDoorPools, suppressLoadBalancerWarning);
21580
23387
  } finally {
21581
23388
  if (stateProvider) stateProvider.dispose();
21582
23389
  }
21583
23390
  }
21584
- async function runOneTarget(boot, runState, stacks, options, discovery, skipPull, stateProvider, profileCredsFile, frontDoorPools) {
23391
+ async function runOneTarget(boot, runState, stacks, options, discovery, skipPull, stateProvider, profileCredsFile, frontDoorPools, suppressLoadBalancerWarning) {
21585
23392
  const logger = getLogger();
21586
23393
  const target = boot.target;
21587
23394
  const imageContext = await buildEcsImageResolutionContext(target, stacks, options, stateProvider);
21588
- const service = resolveEcsServiceTarget(target, stacks, imageContext);
23395
+ const service = resolveEcsServiceTarget(target, stacks, imageContext, { suppressLoadBalancerWarning });
21589
23396
  logger.info(`Target: ${service.stack.stackName}/${service.serviceLogicalId} (service=${service.serviceName}, desiredCount=${service.desiredCount}, task=${service.task.taskDefinitionLogicalId})`);
21590
23397
  if (service.serviceConnect) logger.info(`Service Connect: namespace='${service.serviceConnect.namespaceName}', ${service.serviceConnect.services.length} service(s) registered for peer discovery.`);
21591
23398
  if (service.serviceRegistries.length > 0) logger.info(`Cloud Map: ${service.serviceRegistries.length} ServiceRegistry binding(s).`);
@@ -22852,7 +24659,8 @@ function albStrategy(options) {
22852
24659
  warnings
22853
24660
  };
22854
24661
  },
22855
- lbPortOverrides
24662
+ lbPortOverrides,
24663
+ suppressLoadBalancerWarning: true
22856
24664
  };
22857
24665
  }
22858
24666
  /**
@@ -22890,5 +24698,97 @@ function addAlbSpecificOptions(cmd) {
22890
24698
  }
22891
24699
 
22892
24700
  //#endregion
22893
- export { VtlEvaluationError as $, substituteImagePlaceholders as $n, pickFreePort as $t, availableApiIdentifiers as A, resolveSsmParameters as An, parseSseForJsonRpc as At, computeRequestIdentityHash as B, discoverRoutes as Bn, renderCodeDockerfile as Bt, createWatchPredicates as C, isCfnFlagPresent as Cn, A2A_CONTAINER_PORT as Ct, createFileWatcher as D, resolveCfnStackName as Dn, MCP_PATH as Dt, createAuthorizerCache as E, resolveCfnRegion as En, MCP_CONTAINER_PORT as Et, startApiServer as F, countTargets as Fn, waitForAgentCorePing as Ft, translateLambdaResponse as G, AGENTCORE_HTTP_PROTOCOL as Gn, getDockerImageBySourceHash as Gt, invokeRequestAuthorizer as H, resolveLambdaArnIntrinsic as Hn, writeProfileCredentialsFile as Ht, resolveSelectionExpression as I, listTargets as In, downloadAndExtractS3Bundle as It, buildRestV1Event as J, AgentCoreResolutionError as Jn, architectureToPlatform as Jt, applyAuthorizerOverlay as K, AGENTCORE_MCP_PROTOCOL as Kn, invokeRie as Kt, resolveServiceIntegrationParameters as L, discoverWebSocketApis as Ln, SUPPORTED_CODE_RUNTIMES as Lt, filterRoutesByApiIdentifiers as M, resolveWatchConfig as Mn, signAgentCoreInvocation as Mt, groupRoutesByServer as N, Synthesizer as Nn, AGENTCORE_SESSION_ID_HEADER as Nt, attachStageContext as O, CfnLocalStateProvider as On, MCP_PROTOCOL_VERSION as Ot, readMtlsMaterialsFromDisk as P, resolveSingleTarget as Pn, invokeAgentCore as Pt, tryParseStatus as Q, derivePseudoParametersFromRegion as Qn, ensureDockerAvailable as Qt, defaultCredentialsLoader as R, discoverWebSocketApisOrThrow as Rn, buildAgentCoreCodeImage as Rt, createLocalStartApiCommand as S, createLocalStateProvider as Sn, invokeAgentCoreWs as St, resolveProfileCredentials as T, resolveCfnFallbackRegion as Tn, a2aInvokeOnce as Tt, invokeTokenAuthorizer as U, AGENTCORE_A2A_PROTOCOL as Un, singleFlight as Ut, evaluateCachedLambdaPolicy as V, pickRefLogicalId as Vn, toCmdArgv as Vt, matchRoute as W, AGENTCORE_AGUI_PROTOCOL as Wn, AssetManifestLoader as Wt, pickResponseTemplate as X, resolveAgentCoreTarget as Xn, parseEcrUri as Xt, evaluateResponseParameters as Y, pickAgentCoreCandidateStack as Yn, buildContainerImage as Yt, selectIntegrationResponse as Z, resolveLambdaTarget as Zn, pullEcrImage as Zt, getContainerNetworkIp as _, substituteEnvVarsFromState as _n, applyCorsResponseHeaders as _t, resolveAlbTarget as a, resolveRuntimeFileExtension as an, LocalStartServiceError as ar, handleConnectionsRequest as at, parseHostPortOverrides as b, materializeLayerFromArn as bn, isFunctionUrlOacFronted as bt, MAX_TASKS_SUBNET_RANGE_CAP as c, TASK_ROLE_ACCOUNT_PLACEHOLDER as cn, appOptions as cr, buildDisconnectEvent as ct, parseMaxTasks as d, detectEcsImageResolutionNeeds as dn, deprecatedRegionOption as dr, buildJwksUrlFromIssuer as dt, pullImage as en, tryResolveImageFnJoin as er, HOST_GATEWAY_MIN_VERSION as et, parseRestartPolicy as f, parseEcsTarget as fn, parseContextOptions as fr, createJwksCache as ft, CloudMapRegistry as g, substituteAgainstStateAsync as gn, attachAuthorizers as gt, buildCloudMapIndex as h, substituteAgainstState as hn, verifyJwtViaDiscovery as ht, parseLbPortOverrides as i, resolveRuntimeCodeMountPath as in, LocalInvokeBuildError as ir, buildMgmtEndpointEnvUrl as it, filterRoutesByApiIdentifier as j, resolveApp as jn, AGENTCORE_SIGV4_SERVICE as jt, buildStageMap as k, collectSsmParameterRefs as kn, mcpInvokeOnce as kt, addCommonEcsServiceOptions as l, applyCrossStackResolverToTask as ln, commonOptions as lr, buildMessageEvent as lt, runEcsServiceEmulator as m, applyDeployedEnvFallback as mn, verifyJwtAuthorizer as mt, albStrategy as n, runDetached as nn, readCdkPathOrUndefined as nr, bufferToBody as nt, isApplicationLoadBalancer as o, resolveRuntimeImage as on, withErrorHandling as or, parseConnectionsPath as ot, resolveSharedSidecarCredentials as p, resolveEcsTaskTarget as pn, warnIfDeprecatedRegion as pr, verifyCognitoJwt as pt, buildHttpApiV2Event as q, AGENTCORE_RUNTIME_TYPE as qn, waitForRieReady as qt, createLocalStartAlbCommand as r, streamLogs as rn, CdkLocalError as rr, ConnectionRegistry as rt, resolveAlbFrontDoor as s, EcsTaskResolutionError as sn, applyRoleArnIfSet as sr, buildConnectEvent as st, addAlbSpecificOptions as t, removeContainer as tn, matchStacks as tr, probeHostGatewaySupport as tt, buildEcsImageResolutionContext as u, derivePartitionAndUrlSuffix as un, contextOptions as ur, buildCognitoJwksUrl as ut, cleanupEcsRun as v, substituteEnvVarsFromStateAsync as vn, buildCorsConfigByApiId as vt, resolveApiTargetSubset as w, rejectExplicitCfnStackWithMultipleStacks as wn, A2A_PATH as wt, runEcsTask as x, LocalStateSourceError as xn, matchPreflight as xt, createEcsRunState as y, resolveEnvVars as yn, buildCorsConfigFromCloudFrontChain as yt, buildMethodArn as z, parseSelectionExpressionPath as zn, computeCodeImageTag as zt };
22894
- //# sourceMappingURL=local-start-alb-CIE0TMpn.js.map
24701
+ //#region src/cli/commands/local-list.ts
24702
+ async function localListCommand(options) {
24703
+ const logger = getLogger();
24704
+ if (options.verbose) logger.setLevel("debug");
24705
+ warnIfDeprecatedRegion(options);
24706
+ await applyRoleArnIfSet({
24707
+ roleArn: options.roleArn,
24708
+ region: void 0
24709
+ });
24710
+ const appCmd = resolveApp(options.app);
24711
+ if (!appCmd) throw new Error(`No CDK app specified. Pass --app, set ${getEmbedConfig().envPrefix}_APP, or add "app" to cdk.json.`);
24712
+ process.stderr.write("Synthesizing CDK app...\n");
24713
+ const synthesizer = new Synthesizer();
24714
+ const context = parseContextOptions(options.context);
24715
+ const synthOpts = {
24716
+ app: appCmd,
24717
+ output: options.output,
24718
+ ...options.profile && { profile: options.profile },
24719
+ ...Object.keys(context).length > 0 && { context }
24720
+ };
24721
+ const { stacks } = await synthesizer.synthesize(synthOpts);
24722
+ const listing = listTargets(stacks);
24723
+ process.stdout.write(`${formatTargetListing(listing, getEmbedConfig().cliName, { long: options.long })}\n`);
24724
+ }
24725
+ /**
24726
+ * Render a {@link TargetListing} as the grouped text list `cdkl list`
24727
+ * prints. Each non-empty category is preceded by a blank line and a
24728
+ * header naming the command that runs it, then one target per line by
24729
+ * CDK display path. With {@link FormatTargetListingOptions.long}, each
24730
+ * target's stack-qualified logical ID is printed on an indented line
24731
+ * beneath it. Exported so a unit test can assert the output shape
24732
+ * without running synthesis.
24733
+ */
24734
+ function formatTargetListing(listing, cliName, options = {}) {
24735
+ if (countTargets(listing) === 0) return `No runnable targets (Lambda functions, APIs, ECS services / tasks, AgentCore Runtimes, load balancers) found in this CDK app.`;
24736
+ const long = options.long ?? false;
24737
+ return "\n" + [
24738
+ formatSection("Lambda Functions", `${cliName} invoke <target>`, listing.lambdas, long),
24739
+ formatSection("APIs", `${cliName} start-api [target...]`, listing.apis, long),
24740
+ formatSection("ECS Services", `${cliName} start-service <target...>`, listing.ecsServices, long),
24741
+ formatSection("ECS Task Definitions", `${cliName} run-task <target>`, listing.ecsTaskDefinitions, long),
24742
+ formatSection("AgentCore Runtimes", `${cliName} invoke-agentcore <target>`, listing.agentCoreRuntimes, long),
24743
+ formatSection("Application Load Balancers", `${cliName} start-alb <target...>`, listing.loadBalancers, long)
24744
+ ].filter((lines) => lines.length > 0).map((lines) => lines.join("\n")).join("\n\n");
24745
+ }
24746
+ function formatSection(title, command, entries, long) {
24747
+ if (entries.length === 0) return [];
24748
+ const lines = [`${title} -> ${command}`];
24749
+ for (const entry of entries) {
24750
+ const primary = entry.displayPath ?? entry.qualifiedId;
24751
+ lines.push(entry.kind ? ` ${primary} (${entry.kind})` : ` ${primary}`);
24752
+ if (long && entry.displayPath) lines.push(` ${entry.qualifiedId}`);
24753
+ }
24754
+ return lines;
24755
+ }
24756
+ function createLocalListCommand(opts = {}) {
24757
+ setEmbedConfig(opts.embedConfig);
24758
+ 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.").action(withErrorHandling(async (options) => {
24759
+ await localListCommand(options);
24760
+ }));
24761
+ addListSpecificOptions(cmd);
24762
+ [
24763
+ ...commonOptions(),
24764
+ ...appOptions(),
24765
+ ...contextOptions
24766
+ ].forEach((opt) => cmd.addOption(opt));
24767
+ cmd.addOption(deprecatedRegionOption);
24768
+ return cmd;
24769
+ }
24770
+ /**
24771
+ * Register the option block that `cdkl list` adds on top of the shared
24772
+ * common / app / context option helpers. Shared between `cdkl list` and any
24773
+ * host CLI (e.g. cdkd's `local list`) that wraps the synthesis-driven
24774
+ * target enumeration, so adding or renaming a `list`-only flag here
24775
+ * propagates to every embedder without duplicate `.addOption(...)` blocks.
24776
+ *
24777
+ * Calling order only affects `--help` presentation (Commander parses
24778
+ * insertion-order-independent). The host-CLI convention is host-specific
24779
+ * options first, then this helper, then the shared common / app / context
24780
+ * options — host flags / list flags / common flags grouped in three
24781
+ * `--help` clusters. Chainable: returns `cmd`.
24782
+ *
24783
+ * Today `cdkl list` only contributes one non-common flag (`-l, --long`),
24784
+ * but the helper is still exposed so the surface-contract test pattern
24785
+ * (helper + common == createLocalListCommand) is uniform across every
24786
+ * `add<Cmd>SpecificOptions` extraction.
24787
+ */
24788
+ function addListSpecificOptions(cmd) {
24789
+ return cmd.addOption(new Option("-l, --long", "Also print each target's stack-qualified logical ID (<Stack>:<LogicalId>) beneath it").default(false));
24790
+ }
24791
+
24792
+ //#endregion
24793
+ export { pickResponseTemplate as $, substituteAgainstState as $t, createFileWatcher as A, AgentCoreResolutionError as An, MCP_PATH as At, resolveServiceIntegrationParameters as B, SUPPORTED_CODE_RUNTIMES as Bt, addInvokeAgentCoreSpecificOptions as C, pickRefLogicalId as Cn, isFunctionUrlOacFronted as Ct, createWatchPredicates as D, AGENTCORE_HTTP_PROTOCOL as Dn, A2A_PATH as Dt, createLocalStartApiCommand as E, AGENTCORE_AGUI_PROTOCOL as En, A2A_CONTAINER_PORT as Et, filterRoutesByApiIdentifiers as F, tryResolveImageFnJoin as Fn, signAgentCoreInvocation as Ft, invokeRequestAuthorizer as G, addInvokeSpecificOptions as Gt, buildMethodArn as H, computeCodeImageTag as Ht, groupRoutesByServer as I, LocalInvokeBuildError as In, AGENTCORE_SESSION_ID_HEADER as It, translateLambdaResponse as J, buildContainerImage as Jt, invokeTokenAuthorizer as K, createLocalInvokeCommand as Kt, readMtlsMaterialsFromDisk as L, LocalStartServiceError as Ln, invokeAgentCore as Lt, buildStageMap as M, resolveAgentCoreTarget as Mn, mcpInvokeOnce as Mt, availableApiIdentifiers as N, derivePseudoParametersFromRegion as Nn, parseSseForJsonRpc as Nt, resolveApiTargetSubset as O, AGENTCORE_MCP_PROTOCOL as On, a2aInvokeOnce as Ot, filterRoutesByApiIdentifier as P, substituteImagePlaceholders as Pn, AGENTCORE_SIGV4_SERVICE as Pt, evaluateResponseParameters as Q, EcsTaskResolutionError as Qt, startApiServer as R, withErrorHandling as Rn, waitForAgentCorePing as Rt, createLocalRunTaskCommand as S, discoverRoutes as Sn, buildCorsConfigFromCloudFrontChain as St, addStartApiSpecificOptions as T, AGENTCORE_A2A_PROTOCOL as Tn, invokeAgentCoreWs as Tt, computeRequestIdentityHash as U, renderCodeDockerfile as Ut, defaultCredentialsLoader as V, buildAgentCoreCodeImage as Vt, evaluateCachedLambdaPolicy as W, toCmdArgv as Wt, buildHttpApiV2Event as X, resolveRuntimeFileExtension as Xt, applyAuthorizerOverlay as Y, resolveRuntimeCodeMountPath as Yt, buildRestV1Event as Z, resolveRuntimeImage as Zt, runEcsServiceEmulator as _, countTargets as _n, verifyJwtAuthorizer as _t, albStrategy as a, LocalStateSourceError as an, bufferToBody as at, getContainerNetworkIp as b, discoverWebSocketApisOrThrow as bn, applyCorsResponseHeaders as bt, resolveAlbTarget as c, rejectExplicitCfnStackWithMultipleStacks as cn, handleConnectionsRequest as ct, MAX_TASKS_SUBNET_RANGE_CAP as d, resolveCfnStackName as dn, buildDisconnectEvent as dt, substituteAgainstStateAsync as en, selectIntegrationResponse as et, addCommonEcsServiceOptions as f, CfnLocalStateProvider as fn, buildMessageEvent as ft, resolveSharedSidecarCredentials as g, resolveSingleTarget as gn, verifyCognitoJwt as gt, parseRestartPolicy as h, resolveWatchConfig as hn, createJwksCache as ht, addAlbSpecificOptions as i, materializeLayerFromArn as in, probeHostGatewaySupport as it, attachStageContext as j, pickAgentCoreCandidateStack as jn, MCP_PROTOCOL_VERSION as jt, createAuthorizerCache as k, AGENTCORE_RUNTIME_TYPE as kn, MCP_CONTAINER_PORT as kt, isApplicationLoadBalancer as l, resolveCfnFallbackRegion as ln, parseConnectionsPath as lt, parseMaxTasks as m, resolveSsmParameters as mn, buildJwksUrlFromIssuer as mt, createLocalListCommand as n, substituteEnvVarsFromStateAsync as nn, VtlEvaluationError as nt, createLocalStartAlbCommand as o, createLocalStateProvider as on, ConnectionRegistry as ot, buildEcsImageResolutionContext as p, collectSsmParameterRefs as pn, buildCognitoJwksUrl as pt, matchRoute as q, architectureToPlatform as qt, formatTargetListing as r, resolveEnvVars as rn, HOST_GATEWAY_MIN_VERSION as rt, parseLbPortOverrides as s, isCfnFlagPresent as sn, buildMgmtEndpointEnvUrl as st, addListSpecificOptions as t, substituteEnvVarsFromState as tn, tryParseStatus as tt, resolveAlbFrontDoor as u, resolveCfnRegion as un, buildConnectEvent as ut, buildCloudMapIndex as v, listTargets as vn, verifyJwtViaDiscovery as vt, createLocalInvokeAgentCoreCommand as w, resolveLambdaArnIntrinsic as wn, matchPreflight as wt, addRunTaskSpecificOptions as x, parseSelectionExpressionPath as xn, buildCorsConfigByApiId as xt, CloudMapRegistry as y, discoverWebSocketApis as yn, attachAuthorizers as yt, resolveSelectionExpression as z, downloadAndExtractS3Bundle as zt };
24794
+ //# sourceMappingURL=local-list-JDGfdnip.js.map