cdk-local 0.74.0 → 0.76.0

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.
@@ -113,6 +113,35 @@ function parseAssumeRoleToken(raw, previous) {
113
113
  return acc;
114
114
  }
115
115
  /**
116
+ * Compose `--assume-role` (value-form accumulator) and the separate
117
+ * `--assume-role-auto` boolean flag into the in-process
118
+ * representation `localStartApiCommand` consumes downstream.
119
+ *
120
+ * - both unset -> `undefined` (pass dev creds through)
121
+ * - only `--assume-role-auto` -> `{ perLambda: {}, bareAutoResolve: true }`
122
+ * (per-Lambda auto-resolve for every routed Lambda)
123
+ * - only `--assume-role` -> the value-form accumulator (global ARN
124
+ * and/or per-Lambda map) as-is
125
+ * - both -> accumulator with `bareAutoResolve = true` overlaid;
126
+ * the per-Lambda map still wins for named Lambdas, auto-resolve
127
+ * fills in for every Lambda the map does NOT name
128
+ *
129
+ * Enforces the mutual-exclusion guard: `--assume-role-auto` and a
130
+ * global ARN (`--assume-role <arn>`) both occupy the "default for
131
+ * every other Lambda" slot, so the combination is ambiguous and
132
+ * rejected at boot with a clear error. The per-Lambda map IS
133
+ * compatible with either side.
134
+ */
135
+ function normalizeStartApiAssumeRole(raw, autoResolve) {
136
+ if (raw === void 0) return autoResolve ? {
137
+ perLambda: {},
138
+ bareAutoResolve: true
139
+ } : void 0;
140
+ if (autoResolve && raw.globalArn) throw new Error(`--assume-role-auto auto-resolves EACH routed Lambda's own execution role, but --assume-role ${raw.globalArn} also names a single global default. These are mutually exclusive on the global slot. Either drop the global ARN to keep --assume-role-auto for every Lambda, or drop --assume-role-auto to keep the global default. Per-Lambda overrides (--assume-role <LogicalId>=<arn>) are compatible with either side.`);
141
+ if (autoResolve) raw.bareAutoResolve = true;
142
+ return raw;
143
+ }
144
+ /**
116
145
  * Resolve the effective IAM role ARN for a given Lambda. Per-Lambda
117
146
  * override wins; otherwise the global default; otherwise `undefined`
118
147
  * (no role to assume — pass developer creds through).
@@ -6454,7 +6483,7 @@ function resolveRuntimeCodeMountPath(runtime) {
6454
6483
 
6455
6484
  //#endregion
6456
6485
  //#region src/local/docker-runner.ts
6457
- const execFileAsync$4 = promisify(execFile);
6486
+ const execFileAsync$5 = promisify(execFile);
6458
6487
  /**
6459
6488
  * Wraps `docker pull` / `docker run` / `docker rm` for `cdkl invoke`.
6460
6489
  *
@@ -6551,7 +6580,7 @@ async function runDetached(opts) {
6551
6580
  args.push(opts.image, ...entryPointTail, ...opts.cmd);
6552
6581
  getLogger().child("docker").debug(`${getDockerCmd()} ${redactAwsCredentialsInArgs(args).join(" ")}`);
6553
6582
  try {
6554
- const { stdout } = await execFileAsync$4(getDockerCmd(), args, {
6583
+ const { stdout } = await execFileAsync$5(getDockerCmd(), args, {
6555
6584
  maxBuffer: 10 * 1024 * 1024,
6556
6585
  ...execEnvForSecrets(passthroughEnv)
6557
6586
  });
@@ -6591,7 +6620,7 @@ async function removeContainer(containerId) {
6591
6620
  if (!containerId) return;
6592
6621
  const logger = getLogger().child("docker");
6593
6622
  try {
6594
- await execFileAsync$4(getDockerCmd(), [
6623
+ await execFileAsync$5(getDockerCmd(), [
6595
6624
  "rm",
6596
6625
  "-f",
6597
6626
  containerId
@@ -6610,7 +6639,7 @@ async function removeContainer(containerId) {
6610
6639
  async function ensureDockerAvailable() {
6611
6640
  const cmd = getDockerCmd();
6612
6641
  try {
6613
- await execFileAsync$4(cmd, [
6642
+ await execFileAsync$5(cmd, [
6614
6643
  "version",
6615
6644
  "--format",
6616
6645
  "{{.Server.Version}}"
@@ -15265,6 +15294,9 @@ async function localStartApiCommand(targets, options, extraStateProviders) {
15265
15294
  const interactivePicked = { value: false };
15266
15295
  const allStacksConflictList = allStacksConflicts(options, targets, apiFilters);
15267
15296
  if (allStacksConflictList.length > 0) throw new Error(`--all-stacks serves every stack's API and cannot be combined with a target subset selector (${allStacksConflictList.join(", ")}). Drop --all-stacks to target specific APIs, or drop the selector to serve them all. The bare --from-cfn-stack flag (no value) IS compatible with --all-stacks.`);
15297
+ const normalizedAssumeRole = normalizeStartApiAssumeRole(options.assumeRole, options.assumeRoleAuto ?? false);
15298
+ if (normalizedAssumeRole === void 0) delete options.assumeRole;
15299
+ else options.assumeRole = normalizedAssumeRole;
15268
15300
  await applyRoleArnIfSet({
15269
15301
  roleArn: options.roleArn,
15270
15302
  region: options.region,
@@ -15376,7 +15408,7 @@ async function localStartApiCommand(targets, options, extraStateProviders) {
15376
15408
  logicalId,
15377
15409
  stacks: targetStacks,
15378
15410
  overrides,
15379
- assumeRole: options.assumeRole,
15411
+ assumeRole: normalizedAssumeRole,
15380
15412
  containerHost: options.containerHost,
15381
15413
  ...debugPortBase !== void 0 && { debugPort: debugPortBase + i },
15382
15414
  stsRegion: options.region ?? process.env["AWS_REGION"] ?? process.env["AWS_DEFAULT_REGION"],
@@ -16041,6 +16073,59 @@ function warnIamRoutes(routesWithAuth) {
16041
16073
  return true;
16042
16074
  }
16043
16075
  /**
16076
+ * Resolve the effective IAM role ARN to assume for a single routed
16077
+ * Lambda, honoring the four `--assume-role` forms of issue #256
16078
+ * Option 1 (non-breaking step):
16079
+ *
16080
+ * - per-Lambda override (`<LogicalId>=<arn>`) — wins over every other
16081
+ * form for the named Lambda.
16082
+ * - global default (bare ARN) — used when the per-Lambda map misses.
16083
+ * - bare-auto-resolve (`--assume-role` with no value) — when neither
16084
+ * the per-Lambda map nor a global default applies, resolves THIS
16085
+ * Lambda's own `Role` (its `AWS::Lambda::Function`'s `Role`
16086
+ * property -> role ARN). Resolution sources, in order:
16087
+ * 1. literal-ARN form of `Properties.Role` in the SYNTHESIZED
16088
+ * template (rare in CDK output but possible);
16089
+ * 2. deployed state (`stateBundle.state.resources`) when
16090
+ * `--from-cfn-stack` (or a host-provided extension) is
16091
+ * active, via the same shared helper
16092
+ * `resolveExecutionRoleArnFromState` `cdkl invoke`'s bare-
16093
+ * form uses.
16094
+ * - flag absent / `--no-assume-role-auto` — returns `undefined`
16095
+ * (per-Lambda map and global default already filtered out by the
16096
+ * null-check at the top; bare-auto-resolve = false short-circuits
16097
+ * before the state lookup).
16098
+ *
16099
+ * The shape-level mutual exclusion (bare-auto-resolve + global ARN
16100
+ * together) is caught at boot by
16101
+ * {@link normalizeStartApiAssumeRole}; this resolver only sees a
16102
+ * well-formed `AssumeRoleOption`.
16103
+ *
16104
+ * Bare-auto-resolve misses (no literal-ARN, no state, state did not
16105
+ * carry the role attribute) warn-and-fall-through to the
16106
+ * dev-creds-passed-through branch — same trade-off `cdkl invoke
16107
+ * --assume-role` makes when bare auto-resolve cannot find an ARN.
16108
+ *
16109
+ * Exported for unit testing.
16110
+ */
16111
+ function resolveStartApiAssumeRoleArn(args) {
16112
+ const { logicalId, assumeRole, lambdaResource, stateBundle } = args;
16113
+ if (!assumeRole) return void 0;
16114
+ const explicit = effectiveAssumeRoleArn(logicalId, assumeRole);
16115
+ if (explicit) return explicit;
16116
+ if (!assumeRole.bareAutoResolve) return void 0;
16117
+ const roleProp = (lambdaResource.Properties ?? {})["Role"];
16118
+ if (typeof roleProp === "string" && roleProp.startsWith("arn:")) return roleProp;
16119
+ if (stateBundle) {
16120
+ const fromState = resolveExecutionRoleArnFromState(stateBundle.state, logicalId);
16121
+ if (fromState) {
16122
+ getLogger().info(`--assume-role: auto-resolved execution role for '${logicalId}' from state: ${fromState}`);
16123
+ return fromState;
16124
+ }
16125
+ }
16126
+ getLogger().warn(`--assume-role: could not auto-resolve the execution role ARN for '${logicalId}'. Pair --assume-role with --from-cfn-stack (or a host-provided state-source extension) so the deployed Role's ARN can be looked up, OR pin the ARN explicitly with --assume-role ${logicalId}=<arn>. Falling back to the developer's shell credentials for this Lambda.`);
16127
+ }
16128
+ /**
16044
16129
  * Build the per-Lambda container spec — code dir, env vars (template +
16045
16130
  * --env-vars overlay), STS-issued creds when --assume-role names this
16046
16131
  * Lambda, optional --debug-port reservation. Errors out with a clear
@@ -16111,7 +16196,12 @@ async function buildContainerSpec(args) {
16111
16196
  AWS_LAMBDA_LOG_STREAM_NAME: "local",
16112
16197
  ...envResult.resolved
16113
16198
  };
16114
- const roleArn = effectiveAssumeRoleArn(logicalId, assumeRole);
16199
+ const roleArn = resolveStartApiAssumeRoleArn({
16200
+ logicalId,
16201
+ assumeRole,
16202
+ lambdaResource: lambda.resource,
16203
+ stateBundle
16204
+ });
16115
16205
  if (roleArn) {
16116
16206
  const creds = await assumeLambdaExecutionRole$1(roleArn, stsRegion, profile);
16117
16207
  dockerEnv["AWS_ACCESS_KEY_ID"] = creds.accessKeyId;
@@ -16944,7 +17034,7 @@ function createLocalStartApiCommand(opts = {}) {
16944
17034
  * `--help` clusters. Chainable: returns `cmd`.
16945
17035
  */
16946
17036
  function addStartApiSpecificOptions(cmd) {
16947
- 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("--no-build", "Skip docker build on every container Lambda local-asset build (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("--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("--debug-port <port>", "Alias of --debug-port-base for parity with `cdkl invoke --debug-port`. Reserves a contiguous --debug-port range (one per Lambda) starting at <port>.")).addOption(new Option("--ecr-role-arn <arn>", "Role ARN to assume before authenticating against ECR for cross-account / centralized registries on the container-Lambda ECR-pull fallback. 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("--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));
17037
+ 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("--no-build", "Skip docker build on every container Lambda local-asset build (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("--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("--debug-port <port>", "Alias of --debug-port-base for parity with `cdkl invoke --debug-port`. Reserves a contiguous --debug-port range (one per Lambda) starting at <port>.")).addOption(new Option("--ecr-role-arn <arn>", "Role ARN to assume before authenticating against ECR for cross-account / centralized registries on the container-Lambda ECR-pull fallback. 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("--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. Two forms: (1) `--assume-role <arn>` sets a single global default ARN used for every routed Lambda; (2) `--assume-role <LogicalId>=<arn>` (repeatable) is a per-Lambda override (wins over the global default and over --assume-role-auto for the named Lambda). Pair with --assume-role-auto to auto-resolve every other Lambda. Precedence: per-Lambda > (--assume-role-auto OR global default) > unset (developer creds passed through).").argParser((raw, prev) => parseAssumeRoleToken(raw, prev))).addOption(new Option("--assume-role-auto", "Auto-resolve EACH routed Lambda's own execution role per-Lambda (literal ARN / Fn::GetAtt against template / --from-cfn-stack state). Slower boot — one STS call per Lambda — but the right shape when each Lambda's deployed role differs. Mutually exclusive with `--assume-role <arn>` (global default); errors at boot. Compatible with `--assume-role <LogicalId>=<arn>` per-Lambda overrides — the map wins for named Lambdas, auto-resolve handles the rest.").default(false)).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));
16948
17038
  }
16949
17039
 
16950
17040
  //#endregion
@@ -17576,6 +17666,167 @@ function addInvokeSpecificOptions(cmd) {
17576
17666
  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\"}, \"Parameters\": {...}})")).addOption(new Option("--no-pull", "Skip docker pull. Semantics differ by code path: ZIP Lambdas skip pulling the public Lambda base image; Container Lambdas on the local-build path are a no-op (docker build does not refresh the FROM cache by default); Container Lambdas on the ECR-pull fallback skip docker pull AND error if the image is not in the local cache (re-run without --no-pull or pre-pull manually).")).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 IP the host uses to bind the RIE port to. Must be a numeric IP (Docker rejects hostnames here). Defaults to 127.0.0.1.").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."));
17577
17667
  }
17578
17668
 
17669
+ //#endregion
17670
+ //#region src/local/source-change-classifier.ts
17671
+ /**
17672
+ * Dependency-manifest basenames recognized by the classifier. A change
17673
+ * to any of these forces a rebuild because the running container's
17674
+ * pre-built dependency layer is no longer in sync with the source.
17675
+ *
17676
+ * Coverage: the package managers commonly used inside a Lambda /
17677
+ * container image — Node (pnpm / npm / yarn), Python (pip / poetry /
17678
+ * pipenv), Ruby (bundler), Go (modules), Rust (cargo), Java / Kotlin
17679
+ * (Maven, Gradle). Adding a new ecosystem? Append its lockfile +
17680
+ * manifest here and add a classifier test row.
17681
+ */
17682
+ const REBUILD_TRIGGER_BASENAMES = new Set([
17683
+ "package.json",
17684
+ "package-lock.json",
17685
+ "pnpm-lock.yaml",
17686
+ "yarn.lock",
17687
+ "npm-shrinkwrap.json",
17688
+ "requirements.txt",
17689
+ "requirements-dev.txt",
17690
+ "pyproject.toml",
17691
+ "poetry.lock",
17692
+ "Pipfile",
17693
+ "Pipfile.lock",
17694
+ "uv.lock",
17695
+ "Gemfile",
17696
+ "Gemfile.lock",
17697
+ "go.mod",
17698
+ "go.sum",
17699
+ "Cargo.toml",
17700
+ "Cargo.lock",
17701
+ "pom.xml",
17702
+ "build.gradle",
17703
+ "build.gradle.kts",
17704
+ "settings.gradle",
17705
+ "settings.gradle.kts",
17706
+ "Makefile",
17707
+ "CMakeLists.txt"
17708
+ ]);
17709
+ /**
17710
+ * Compiled-language source extensions that require a build step
17711
+ * inside `docker build` (typically a `RUN go build` / `cargo build` /
17712
+ * `mvn package` etc.). A copy of the source alone would leave the
17713
+ * running binary stale, so the user's intent must be a rebuild.
17714
+ *
17715
+ * TypeScript source (`.ts` / `.tsx` / `.mts` / `.cts`) is treated as
17716
+ * compiled because the dominant production-container pattern is to
17717
+ * pre-compile the source via a Dockerfile `RUN tsc` / `RUN yarn build`
17718
+ * step, with the runtime executing the emitted `dist/*.js`. Soft-reload
17719
+ * would `docker cp` the new `.ts` into the container's WORKDIR while
17720
+ * the running process keeps reading the OLD `dist/` — a silent
17721
+ * stale-code failure that violates the file's "slow-but-correct beats
17722
+ * fast-but-stale" default policy (lines 14-22). Setups that transpile
17723
+ * at runtime lose the soft-reload fast path under this default; an
17724
+ * opt-in flag to restore it is a possible follow-up but is not in
17725
+ * scope here.
17726
+ *
17727
+ * Interpreted-language runtimes (Node — `.js` / `.mjs` / `.cjs`,
17728
+ * Python — `.py`, Ruby — `.rb`, shell — `.sh`) read source at process
17729
+ * start, so a `docker cp` + `docker restart` cycle picks them up.
17730
+ * Those extensions are NOT in this set.
17731
+ */
17732
+ const COMPILED_LANGUAGE_EXTENSIONS = new Set([
17733
+ ".go",
17734
+ ".rs",
17735
+ ".java",
17736
+ ".kt",
17737
+ ".kts",
17738
+ ".scala",
17739
+ ".cs",
17740
+ ".swift",
17741
+ ".fs",
17742
+ ".fsx",
17743
+ ".c",
17744
+ ".cc",
17745
+ ".cpp",
17746
+ ".cxx",
17747
+ ".h",
17748
+ ".hpp",
17749
+ ".zig",
17750
+ ".ml",
17751
+ ".mli",
17752
+ ".elm",
17753
+ ".hs",
17754
+ ".dart",
17755
+ ".ts",
17756
+ ".tsx",
17757
+ ".mts",
17758
+ ".cts"
17759
+ ]);
17760
+ /**
17761
+ * Classify a single watcher firing into rebuild vs soft-reload. Pure
17762
+ * + synchronous. The caller (emulator's reload pathway) invokes this
17763
+ * once per target per firing AFTER `cdk synth` has run and the new
17764
+ * asset manifest is on disk.
17765
+ *
17766
+ * Branching:
17767
+ * 1. No asset context (image isn't a CDK asset, or asset lookup
17768
+ * failed) → `rebuild`.
17769
+ * 2. The asset hash didn't change between old and new synths
17770
+ * (`oldAssetHash === newAssetHash`, or `oldAssetHash` missing)
17771
+ * → `rebuild`. Load-bearing guard for "user edited a CDK
17772
+ * construct file (e.g. `lib/stack.ts`) that flipped the task
17773
+ * spec but didn't touch the asset content". Soft-reload would
17774
+ * `docker cp` identical files and `docker restart` the
17775
+ * container with the OLD task spec (env / memory / mounts /
17776
+ * added sidecars are set at `docker create` time, not on
17777
+ * restart) — the user's intent would silently NOT apply.
17778
+ * Forcing rebuild keeps Phase 1-3 semantics exactly for this
17779
+ * case: the rolling primitive boots a shadow with the new task
17780
+ * spec, the user sees their construct edit take effect.
17781
+ * 3. No changed paths (the watcher fired on a debounce flush with
17782
+ * an empty pending set — shouldn't happen in practice, but
17783
+ * defensive) → `rebuild`.
17784
+ * 4. Any changed path's basename matches the Dockerfile or a
17785
+ * dependency manifest → `rebuild`.
17786
+ * 5. Any changed path's extension is a compiled-language source →
17787
+ * `rebuild`.
17788
+ * 6. Else → `soft-reload`.
17789
+ */
17790
+ function classifySourceChange(changedPaths, ctx) {
17791
+ if (!ctx) return {
17792
+ kind: "rebuild",
17793
+ reason: "target image is not a CDK docker-image asset"
17794
+ };
17795
+ if (!ctx.oldAssetHash || ctx.oldAssetHash === ctx.newAssetHash) return {
17796
+ kind: "rebuild",
17797
+ reason: "asset hash unchanged across the synth (CDK construct edit or unrelated file) — task-spec changes need a fresh `docker create`, which only the rebuild path runs"
17798
+ };
17799
+ if (changedPaths.length === 0) return {
17800
+ kind: "rebuild",
17801
+ reason: "no changed paths reported (defensive default)"
17802
+ };
17803
+ for (const p of changedPaths) {
17804
+ const basename = path.basename(p);
17805
+ if (basename === ctx.dockerFile) return {
17806
+ kind: "rebuild",
17807
+ reason: `Dockerfile edit (${basename})`
17808
+ };
17809
+ if (basename.startsWith("Dockerfile.")) return {
17810
+ kind: "rebuild",
17811
+ reason: `Dockerfile.* edit (${basename})`
17812
+ };
17813
+ if (REBUILD_TRIGGER_BASENAMES.has(basename)) return {
17814
+ kind: "rebuild",
17815
+ reason: `dependency manifest edit (${basename})`
17816
+ };
17817
+ const ext = path.extname(p).toLowerCase();
17818
+ if (COMPILED_LANGUAGE_EXTENSIONS.has(ext)) return {
17819
+ kind: "rebuild",
17820
+ reason: `compiled-language source edit (${basename}) — soft-reload would leave the built binary stale`
17821
+ };
17822
+ }
17823
+ return {
17824
+ kind: "soft-reload",
17825
+ reason: `${changedPaths.length} source-only path(s) — skipping rebuild`,
17826
+ newAssetSourceDir: ctx.newAssetSourceDir
17827
+ };
17828
+ }
17829
+
17579
17830
  //#endregion
17580
17831
  //#region src/local/agentcore-code-build.ts
17581
17832
  /**
@@ -18344,6 +18595,17 @@ async function invokeAgentCoreWs(host, port, event, options) {
18344
18595
  reject(/* @__PURE__ */ new Error(`AgentCore /ws at ${url} timed out after ${options.timeoutMs}ms. The agent may be hung or may not close the stream; check container logs.`));
18345
18596
  });
18346
18597
  }, options.timeoutMs);
18598
+ const onAbort = () => {
18599
+ finish(() => {
18600
+ stopIterator();
18601
+ try {
18602
+ ws.close();
18603
+ } catch {}
18604
+ resolve({ frames });
18605
+ });
18606
+ };
18607
+ if (options.abortSignal) if (options.abortSignal.aborted) onAbort();
18608
+ else options.abortSignal.addEventListener("abort", onAbort, { once: true });
18347
18609
  let iterator;
18348
18610
  const stopIterator = () => {
18349
18611
  if (iterator?.return) try {
@@ -18490,6 +18752,12 @@ async function localInvokeAgentCoreCommand(target, options, extraStateProviders)
18490
18752
  if ((isMcp || isA2a) && options.ws) logger.warn(`--ws applies only to the HTTP / AGUI protocols; ignoring it for this ${resolved.protocol} runtime.`);
18491
18753
  if (options.wsInteractive && !options.ws) logger.warn("--ws-interactive is meaningful only with --ws; ignoring.");
18492
18754
  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.");
18755
+ const watchEligible = options.ws === true && !isMcp && !isA2a;
18756
+ const watchActive = options.watch === true && watchEligible;
18757
+ if (options.watch === true && !watchEligible) {
18758
+ const ignoredFor = isMcp ? "MCP" : isA2a ? "A2A" : "single-shot HTTP POST /invocations";
18759
+ logger.warn(`--watch is meaningful only with the long-running --ws / --ws-interactive session paths; ignoring it for the ${ignoredFor} path (single-shot invocations run once and exit).`);
18760
+ }
18493
18761
  const sessionId = options.sessionId ?? randomUUID();
18494
18762
  const event = await readEvent(options);
18495
18763
  const mcpRequest = isMcp ? buildMcpRequest(event) : void 0;
@@ -18537,7 +18805,70 @@ async function localInvokeAgentCoreCommand(target, options, extraStateProviders)
18537
18805
  const a2a = await a2aInvokeOnce(containerHost, hostPort, a2aRequest, { requestTimeoutMs: options.timeout });
18538
18806
  await new Promise((r) => setTimeout(r, 250));
18539
18807
  emitA2aResult(a2a);
18540
- } else if (options.ws) {
18808
+ } else if (options.ws) if (watchActive) await runAgentCoreWatchLoop({
18809
+ containerHost,
18810
+ hostPort,
18811
+ event,
18812
+ sessionId,
18813
+ timeoutMs: options.timeout,
18814
+ wsInteractive: options.wsInteractive === true,
18815
+ options,
18816
+ resolvedTarget,
18817
+ resolved,
18818
+ synthesizer,
18819
+ synthOpts,
18820
+ stacks,
18821
+ ...authorization && { authorization },
18822
+ rebuild: async () => {
18823
+ if (stopLogs) {
18824
+ try {
18825
+ stopLogs();
18826
+ } catch {}
18827
+ stopLogs = void 0;
18828
+ }
18829
+ if (containerId) {
18830
+ await removeContainer(containerId);
18831
+ containerId = void 0;
18832
+ }
18833
+ const { stacks: newStacks } = await synthesizer.synthesize(synthOpts);
18834
+ const newCandidate = pickAgentCoreCandidateStack(resolvedTarget, newStacks);
18835
+ const { context: newImageContext, loaded: newLoaded } = stateProvider && newCandidate ? await buildAgentCoreImageContext(newCandidate, stateProvider, options) : {
18836
+ context: void 0,
18837
+ loaded: void 0
18838
+ };
18839
+ const newResolved = resolveAgentCoreTarget(resolvedTarget, newStacks, newImageContext);
18840
+ await resolveFromS3BucketIntrinsic(newResolved, stateProvider, newLoaded, newImageContext);
18841
+ const newImage = await resolveAgentCoreImage(newResolved, options, newLoaded, stateProvider);
18842
+ const { env: newEnv, sensitiveEnvKeys: newSensitive } = await buildContainerEnv(newResolved, options, profileCredentials, profileCredsFile, stateProvider, newLoaded, newImageContext);
18843
+ const newHostPort = await pickFreePort();
18844
+ const newName = `${getEmbedConfig().resourceNamePrefix}-agentcore-${process.pid}-${Math.random().toString(36).slice(2, 8)}`;
18845
+ logger.info(`Reload: starting agent container (image=${newImage}, port=${newHostPort} -> 8080)...`);
18846
+ containerId = await runDetached({
18847
+ image: newImage,
18848
+ mounts: [],
18849
+ env: newEnv,
18850
+ cmd: [],
18851
+ hostPort: newHostPort,
18852
+ host: containerHost,
18853
+ platform: options.platform,
18854
+ name: newName,
18855
+ ...newSensitive.size > 0 && { sensitiveEnvKeys: newSensitive }
18856
+ });
18857
+ stopLogs = streamLogs(containerId);
18858
+ return {
18859
+ containerId,
18860
+ hostPort: newHostPort,
18861
+ stacks: newStacks
18862
+ };
18863
+ },
18864
+ softReload: async (newSourceDir) => {
18865
+ if (!containerId) throw new CdkLocalError("softReload: no live container to docker cp / docker restart into.", "LOCAL_INVOKE_AGENTCORE_WATCH_NO_CONTAINER");
18866
+ await softReloadAgentContainer(containerId, newSourceDir);
18867
+ const { stacks: newStacks } = await synthesizer.synthesize(synthOpts);
18868
+ return { stacks: newStacks };
18869
+ }
18870
+ });
18871
+ else {
18541
18872
  await waitForAgentCorePing(containerHost, hostPort);
18542
18873
  const frameSource = options.wsInteractive ? readStdinLines() : void 0;
18543
18874
  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...");
@@ -18550,7 +18881,8 @@ async function localInvokeAgentCoreCommand(target, options, extraStateProviders)
18550
18881
  });
18551
18882
  await new Promise((r) => setTimeout(r, 250));
18552
18883
  emitWsResult(wsResult);
18553
- } else {
18884
+ }
18885
+ else {
18554
18886
  await waitForAgentCorePing(containerHost, hostPort);
18555
18887
  const additionalHeaders = await buildSigV4HeadersIfRequested(options, resolved, loadedState, containerHost, hostPort, event, sessionId, stateProvider);
18556
18888
  const result = await invokeAgentCore(containerHost, hostPort, event, {
@@ -19208,6 +19540,309 @@ function readEnvOverridesFile$2(filePath) {
19208
19540
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`--env-vars file '${filePath}' must contain a JSON object at the top level.`);
19209
19541
  return parsed;
19210
19542
  }
19543
+ /**
19544
+ * Issue #255 — `cdkl invoke-agentcore --watch` main loop. Wraps the
19545
+ * `/ws` dispatch in a reload-driven loop:
19546
+ *
19547
+ * 1. Open the WebSocket against the current container; stream frames
19548
+ * to stdout. The connection is bound to an abort signal the
19549
+ * watcher fires when a source change is detected.
19550
+ * 2. When the agent closes the stream naturally (no reload pending),
19551
+ * the loop exits — `--watch` does NOT auto-reconnect on a benign
19552
+ * close (the user can `^C` to leave or just re-run the command).
19553
+ * 3. When a reload fires, the abort signal closes the WS cleanly
19554
+ * (resolves with the current frame count, not a reject), then the
19555
+ * classifier picks `'rebuild'` vs `'soft-reload'` and the
19556
+ * corresponding container-swap helper runs. After the swap, the
19557
+ * loop re-opens the WS against the new / restarted container with
19558
+ * the same `--event` first frame + a fresh stdin reader (for
19559
+ * `--ws-interactive`).
19560
+ *
19561
+ * Active `/ws` socket handling: the AgentCore `/ws` protocol does not
19562
+ * define mid-session container handoff, so the only honest local-dev
19563
+ * semantic is "close the socket cleanly and let the client reconnect."
19564
+ * The reload firing logs that loudly so the user sees it.
19565
+ *
19566
+ * `softReload` and `rebuild` are passed as callbacks so the watch loop
19567
+ * itself stays uncoupled from the boot pipeline + the docker helpers
19568
+ * the outer command owns. The host-side teardown (state provider
19569
+ * dispose, container removal, profile creds-file dispose) is the
19570
+ * outer command's `finally` cleanup — the watch loop only swaps the
19571
+ * one agent container.
19572
+ *
19573
+ * @internal — exported so a unit test can drive the loop without the
19574
+ * full Docker + chokidar pipeline.
19575
+ */
19576
+ async function runAgentCoreWatchLoop(args) {
19577
+ const logger = getLogger();
19578
+ const wsInvoker = args.__wsInvoker ?? invokeAgentCoreWs;
19579
+ const waitForPing = args.__waitForPing ?? waitForAgentCorePing;
19580
+ let currentHostPort = args.hostPort;
19581
+ let currentStacks = args.stacks;
19582
+ let reloadChain = Promise.resolve();
19583
+ let currentAbort;
19584
+ let pendingReload = false;
19585
+ let reloadFailed = false;
19586
+ const watcherFactory = args.__watcherFactory ?? ((onChange) => {
19587
+ const watchRoot = process.cwd();
19588
+ const { ignored, shouldTrigger, excludePatterns } = createWatchPredicates({
19589
+ watchRoot,
19590
+ output: args.options.output,
19591
+ watchConfig: resolveWatchConfig()
19592
+ });
19593
+ logger.info(`Watching ${watchRoot} for source changes (excluding ${excludePatterns.join(", ")}).`);
19594
+ return createFileWatcher({
19595
+ paths: [watchRoot],
19596
+ ignored,
19597
+ shouldTrigger,
19598
+ onChange
19599
+ });
19600
+ });
19601
+ const cdkOutDir = args.options.output;
19602
+ const assetLoader = new AssetManifestLoader();
19603
+ const watcher = watcherFactory((changedPaths) => {
19604
+ reloadChain = reloadChain.then(async () => {
19605
+ let verdict = {
19606
+ kind: "rebuild",
19607
+ reason: "classifier not consulted"
19608
+ };
19609
+ let assetCtx;
19610
+ try {
19611
+ if (args.__classifierContext) assetCtx = await args.__classifierContext(changedPaths);
19612
+ else {
19613
+ const { stacks: freshStacks } = await args.synthesizer.synthesize(args.synthOpts);
19614
+ const oldAssetHash = await deriveOldAssetHash({
19615
+ resolvedTarget: args.resolvedTarget,
19616
+ resolved: args.resolved,
19617
+ stacks: currentStacks,
19618
+ cdkOutDir,
19619
+ assetLoader
19620
+ });
19621
+ assetCtx = await loadAgentCoreAssetContext({
19622
+ resolvedTarget: args.resolvedTarget,
19623
+ resolved: args.resolved,
19624
+ stacks: freshStacks,
19625
+ cdkOutDir,
19626
+ assetLoader,
19627
+ oldAssetHash
19628
+ });
19629
+ }
19630
+ verdict = classifySourceChange(changedPaths, assetCtx);
19631
+ logger.info(`Detected source change (${changedPaths.length} path(s)); verdict=${verdict.kind} (${verdict.reason}).`);
19632
+ } catch (err) {
19633
+ logger.warn(`Reload: classifier context unavailable (${err instanceof Error ? err.message : String(err)}); falling back to rebuild.`);
19634
+ verdict = {
19635
+ kind: "rebuild",
19636
+ reason: "classifier context unavailable; falling back to rebuild"
19637
+ };
19638
+ }
19639
+ pendingReload = true;
19640
+ logger.warn("cdkl invoke-agentcore --watch: source change detected; closing the active /ws socket so the client can reconnect to the rebuilt container.");
19641
+ currentAbort?.abort();
19642
+ try {
19643
+ if (verdict.kind === "soft-reload") {
19644
+ const { stacks: newStacks } = await args.softReload(verdict.newAssetSourceDir);
19645
+ currentStacks = newStacks;
19646
+ logger.info("Reload: soft-reloaded the agent container (docker cp + docker restart; container ID + host port preserved).");
19647
+ } else {
19648
+ const { containerId: _newId, hostPort: newHostPort, stacks: newStacks } = await args.rebuild();
19649
+ currentHostPort = newHostPort;
19650
+ currentStacks = newStacks;
19651
+ logger.info(`Reload: rebuilt the agent container.`);
19652
+ }
19653
+ } catch (err) {
19654
+ logger.error(`Reload failed: ${err instanceof Error ? err.message : String(err)}. The previous container may already be torn down; exiting --watch loop. Re-run cdkl invoke-agentcore --watch to recover.`);
19655
+ reloadFailed = true;
19656
+ currentAbort?.abort();
19657
+ }
19658
+ }).catch((err) => {
19659
+ logger.error(`Reload chain threw: ${err instanceof Error ? err.message : String(err)}`);
19660
+ });
19661
+ });
19662
+ try {
19663
+ let firstIteration = true;
19664
+ while (true) {
19665
+ if (reloadFailed) break;
19666
+ await waitForPing(args.containerHost, currentHostPort);
19667
+ if (firstIteration) {
19668
+ logger.info(args.wsInteractive ? "Opening the agent /ws WebSocket (interactive — stdin lines = follow-up frames; Ctrl-D to end)..." : "Opening the agent /ws WebSocket and streaming frames...");
19669
+ firstIteration = false;
19670
+ } else logger.info(args.wsInteractive ? "Re-opening the agent /ws WebSocket (interactive) against the rebuilt container..." : "Re-opening the agent /ws WebSocket against the rebuilt container...");
19671
+ const abort = new AbortController();
19672
+ currentAbort = abort;
19673
+ pendingReload = false;
19674
+ const frameSource = args.wsInteractive ? readStdinLines() : void 0;
19675
+ try {
19676
+ const result = await wsInvoker(args.containerHost, currentHostPort, args.event, {
19677
+ sessionId: args.sessionId,
19678
+ timeoutMs: args.timeoutMs,
19679
+ onMessage: (text) => process.stdout.write(text),
19680
+ abortSignal: abort.signal,
19681
+ ...args.authorization && { authorization: args.authorization },
19682
+ ...frameSource && { frameSource }
19683
+ });
19684
+ await new Promise((r) => setTimeout(r, 250));
19685
+ emitWsResult(result);
19686
+ } finally {
19687
+ currentAbort = void 0;
19688
+ }
19689
+ if (!pendingReload) {
19690
+ await reloadChain.catch(() => void 0);
19691
+ if (!pendingReload) break;
19692
+ }
19693
+ await reloadChain.catch(() => void 0);
19694
+ }
19695
+ } finally {
19696
+ try {
19697
+ await watcher.close();
19698
+ } catch (err) {
19699
+ logger.warn(`Watcher close failed: ${err instanceof Error ? err.message : String(err)}`);
19700
+ }
19701
+ }
19702
+ }
19703
+ /**
19704
+ * Issue #255 — soft-reload the agent container in place. `docker cp`
19705
+ * the freshly-synthed asset directory contents into the running
19706
+ * container's WORKDIR + `docker restart` the container.
19707
+ *
19708
+ * - WORKDIR is resolved from the live container's image config via
19709
+ * `docker inspect --format '{{.Config.WorkingDir}}'`. An empty
19710
+ * WORKDIR (Docker runtime default) maps to `/`. For
19711
+ * CodeConfiguration runtimes the generated Dockerfile sets
19712
+ * `WORKDIR /app`, so the copy lands there.
19713
+ * - The trailing `/.` on the source ensures CONTENTS are copied
19714
+ * (not the directory itself); the trailing `/` on the dest forces
19715
+ * docker cp to treat it as a directory.
19716
+ * - `docker restart` cycles PID 1 — the new source is picked up by
19717
+ * the interpreted-language runtime on its next startup. The
19718
+ * container ID + host port + network are preserved across the
19719
+ * restart, so the surrounding watch loop's host-port reference
19720
+ * stays valid.
19721
+ *
19722
+ * @internal — exported for unit tests of the docker-cp + docker-restart
19723
+ * shape without standing up a real container.
19724
+ */
19725
+ const execFileAsync$4 = promisify(execFile);
19726
+ function describeExecError(err) {
19727
+ if (!(err instanceof Error)) return String(err);
19728
+ const stderr = err.stderr;
19729
+ const stderrText = typeof stderr === "string" ? stderr.trim() : stderr instanceof Buffer ? stderr.toString("utf8").trim() : "";
19730
+ return stderrText ? `${err.message}\n${stderrText}` : err.message;
19731
+ }
19732
+ async function softReloadAgentContainer(containerId, newAssetSourceDir) {
19733
+ const logger = getLogger();
19734
+ const dockerCmd = getDockerCmd();
19735
+ let workdir;
19736
+ try {
19737
+ const { stdout } = await execFileAsync$4(dockerCmd, [
19738
+ "inspect",
19739
+ "--format",
19740
+ "{{.Config.WorkingDir}}",
19741
+ containerId
19742
+ ]);
19743
+ workdir = stdout.trim() || "/";
19744
+ } catch (err) {
19745
+ throw new CdkLocalError(`softReloadAgentContainer: docker inspect of container '${containerId}' failed: ${describeExecError(err)}.`, "LOCAL_INVOKE_AGENTCORE_WATCH_SOFT_RELOAD_INSPECT_FAILED");
19746
+ }
19747
+ const workdirDest = workdir.endsWith("/") ? workdir : `${workdir}/`;
19748
+ logger.info(`Soft-reload: docker cp ${newAssetSourceDir} -> ${containerId}:${workdirDest}; restart.`);
19749
+ try {
19750
+ await execFileAsync$4(dockerCmd, [
19751
+ "cp",
19752
+ `${newAssetSourceDir}/.`,
19753
+ `${containerId}:${workdirDest}`
19754
+ ], { maxBuffer: 64 * 1024 * 1024 });
19755
+ } catch (err) {
19756
+ throw new CdkLocalError(`softReloadAgentContainer: docker cp into '${containerId}:${workdir}' failed: ${describeExecError(err)}.`, "LOCAL_INVOKE_AGENTCORE_WATCH_SOFT_RELOAD_CP_FAILED");
19757
+ }
19758
+ try {
19759
+ await execFileAsync$4(dockerCmd, ["restart", containerId]);
19760
+ } catch (err) {
19761
+ throw new CdkLocalError(`softReloadAgentContainer: docker restart of '${containerId}' failed: ${describeExecError(err)}.`, "LOCAL_INVOKE_AGENTCORE_WATCH_SOFT_RELOAD_RESTART_FAILED");
19762
+ }
19763
+ }
19764
+ /**
19765
+ * Issue #255 — build the per-firing classifier context for the agent
19766
+ * container. Mirrors the ECS service emulator's `loadAssetContextForTarget`
19767
+ * shape: returns `undefined` (and the classifier defaults to `'rebuild'`)
19768
+ * when the runtime's image is not a CDK docker-image asset, or when the
19769
+ * asset manifest lookup misses for either the OLD or NEW synth.
19770
+ *
19771
+ * For a CodeConfiguration (`fromCodeAsset`) runtime we treat the bundle's
19772
+ * `codeAssetHash` as the asset hash + the staged source directory as
19773
+ * `newAssetSourceDir`, and synthesize a `Dockerfile` basename that never
19774
+ * matches a real file (the generated Dockerfile lives in a build tmpdir,
19775
+ * not the source tree, so chokidar can't observe an edit to it). A
19776
+ * `fromS3` bundle has no local source tree, so it returns `undefined` and
19777
+ * the classifier defaults to rebuild.
19778
+ *
19779
+ * Container artifacts go through the same `AssetManifestLoader` lookup
19780
+ * the ECS path uses, then read `source.directory` + `source.dockerFile`
19781
+ * off the matched docker-image entry.
19782
+ *
19783
+ * @internal — exported for the watch loop's classifier dispatch test.
19784
+ */
19785
+ async function loadAgentCoreAssetContext(args) {
19786
+ const { resolvedTarget, resolved, stacks, cdkOutDir, assetLoader, oldAssetHash } = args;
19787
+ const newCandidate = pickAgentCoreCandidateStack(resolvedTarget, stacks);
19788
+ if (!newCandidate) return void 0;
19789
+ if (resolved.codeArtifact) {
19790
+ if (resolved.codeArtifact.s3Source) return void 0;
19791
+ const manifest = await assetLoader.loadManifest(cdkOutDir, newCandidate.stackName);
19792
+ if (!manifest) return void 0;
19793
+ const asset = assetLoader.getFileAssets(manifest).get(resolved.codeArtifact.codeAssetHash);
19794
+ if (!asset) return void 0;
19795
+ const sourceDir = assetLoader.getAssetSourcePath(cdkOutDir, asset);
19796
+ return {
19797
+ ...oldAssetHash !== void 0 && { oldAssetHash },
19798
+ newAssetHash: resolved.codeArtifact.codeAssetHash,
19799
+ newAssetSourceDir: sourceDir,
19800
+ dockerFile: ".cdkl-agentcore-generated-Dockerfile"
19801
+ };
19802
+ }
19803
+ if (resolved.containerUri === void 0) return void 0;
19804
+ const manifest = await assetLoader.loadManifest(cdkOutDir, newCandidate.stackName);
19805
+ if (!manifest) return void 0;
19806
+ const dockerImageEntry = getDockerImageBySourceHash(manifest, resolved.containerUri);
19807
+ if (!dockerImageEntry) return void 0;
19808
+ const newAssetHash = dockerImageEntry.hash;
19809
+ const newDockerImage = dockerImageEntry.asset;
19810
+ if (!newDockerImage.source.directory) return void 0;
19811
+ const newAssetSourceDir = path.resolve(cdkOutDir, newDockerImage.source.directory);
19812
+ return {
19813
+ ...oldAssetHash !== void 0 && { oldAssetHash },
19814
+ newAssetHash,
19815
+ newAssetSourceDir,
19816
+ dockerFile: path.basename(newDockerImage.source.dockerFile ?? "Dockerfile")
19817
+ };
19818
+ }
19819
+ /**
19820
+ * Helper: derive the current asset hash from the live (pre-reload)
19821
+ * stacks for the classifier's `oldAssetHash` field. Returns `undefined`
19822
+ * when the runtime's image is not resolvable as a CDK asset against the
19823
+ * old stacks — the classifier defaults to rebuild in that case, which
19824
+ * is the right answer.
19825
+ */
19826
+ /**
19827
+ * Async helper: derive the OLD (pre-reload) asset hash for the
19828
+ * classifier's `oldAssetHash` field. Code-artifact runtimes carry the
19829
+ * hash on the boot-time `resolved.codeArtifact.codeAssetHash`; container
19830
+ * runtimes need a manifest lookup against the previous-synth stacks to
19831
+ * extract it. Returns `undefined` when the OLD hash can't be derived
19832
+ * (image is not a CDK asset, manifest unreadable, no candidate stack) —
19833
+ * the classifier treats `undefined` as "force rebuild", which is the
19834
+ * conservative default.
19835
+ */
19836
+ async function deriveOldAssetHash(args) {
19837
+ const { resolvedTarget, resolved, stacks, cdkOutDir, assetLoader } = args;
19838
+ if (resolved.codeArtifact) return resolved.codeArtifact.codeAssetHash;
19839
+ if (resolved.containerUri === void 0) return void 0;
19840
+ const candidate = pickAgentCoreCandidateStack(resolvedTarget, stacks);
19841
+ if (!candidate) return void 0;
19842
+ const manifest = await assetLoader.loadManifest(cdkOutDir, candidate.stackName);
19843
+ if (!manifest) return void 0;
19844
+ return getDockerImageBySourceHash(manifest, resolved.containerUri)?.hash;
19845
+ }
19211
19846
  function createLocalInvokeAgentCoreCommand(opts = {}) {
19212
19847
  setEmbedConfig(opts.embedConfig);
19213
19848
  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; SSE / WebSocket are HTTP wire-shape variants on the same port), MCP (POST /mcp Streamable HTTP on 8000), A2A, or AGUI. 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 all four 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) => {
@@ -19238,7 +19873,7 @@ function createLocalInvokeAgentCoreCommand(opts = {}) {
19238
19873
  * in three `--help` clusters. Chainable: returns `cmd`.
19239
19874
  */
19240
19875
  function addInvokeAgentCoreSpecificOptions(cmd) {
19241
- 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\"}, \"Parameters\": {...}})")).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 this command SUPPLIES (the supplier role) when the runtime declares a customJwtAuthorizer — `cdkl invoke-agentcore` is the local-dev client making the outbound call, so it always presents this token to its own invocation. Verified against the runtime's OIDC discovery URL (signature / issuer / expiry / audience) before the container starts, then forwarded to /invocations as Authorization: Bearer <jwt>. Contrast with `cdkl start-alb --bearer-token`, where the role is reversed — the ALB front-door RECEIVES inbound requests and injects this token only as a default when an inbound request has none (default-when-missing).")).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). Defaults to linux/arm64 because the cloud AgentCore Runtime requires arm64. Override to linux/amd64 only when iterating against an amd64 dev container locally; note the image will not run on the cloud runtime as-is.").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 IP the host uses to bind the agent port to. Must be a numeric IP (Docker rejects hostnames here). Defaults to 127.0.0.1.").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."));
19876
+ 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\"}, \"Parameters\": {...}})")).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 this command SUPPLIES (the supplier role) when the runtime declares a customJwtAuthorizer — `cdkl invoke-agentcore` is the local-dev client making the outbound call, so it always presents this token to its own invocation. Verified against the runtime's OIDC discovery URL (signature / issuer / expiry / audience) before the container starts, then forwarded to /invocations as Authorization: Bearer <jwt>. Contrast with `cdkl start-alb --bearer-token`, where the role is reversed — the ALB front-door RECEIVES inbound requests and injects this token only as a default when an inbound request has none (default-when-missing).")).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). Defaults to linux/arm64 because the cloud AgentCore Runtime requires arm64. Override to linux/amd64 only when iterating against an amd64 dev container locally; note the image will not run on the cloud runtime as-is.").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 IP the host uses to bind the agent port to. Must be a numeric IP (Docker rejects hostnames here). Defaults to 127.0.0.1.").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.")).addOption(new Option("--watch", "Re-synth and reload the agent container on CDK source changes. Only meaningful with the long-running /ws session paths (--ws / --ws-interactive); single-shot POST /invocations, MCP, and A2A invocations run once and exit, so --watch is logged as a no-op WARN for them and the single shot proceeds. The active /ws socket is closed cleanly on every reload firing so the next session connects to the rebuilt container — the honest local-dev semantic.").default(false));
19242
19877
  }
19243
19878
 
19244
19879
  //#endregion
@@ -21849,167 +22484,6 @@ function sleep(ms) {
21849
22484
  return sleepImpl(ms);
21850
22485
  }
21851
22486
 
21852
- //#endregion
21853
- //#region src/local/source-change-classifier.ts
21854
- /**
21855
- * Dependency-manifest basenames recognized by the classifier. A change
21856
- * to any of these forces a rebuild because the running container's
21857
- * pre-built dependency layer is no longer in sync with the source.
21858
- *
21859
- * Coverage: the package managers commonly used inside a Lambda /
21860
- * container image — Node (pnpm / npm / yarn), Python (pip / poetry /
21861
- * pipenv), Ruby (bundler), Go (modules), Rust (cargo), Java / Kotlin
21862
- * (Maven, Gradle). Adding a new ecosystem? Append its lockfile +
21863
- * manifest here and add a classifier test row.
21864
- */
21865
- const REBUILD_TRIGGER_BASENAMES = new Set([
21866
- "package.json",
21867
- "package-lock.json",
21868
- "pnpm-lock.yaml",
21869
- "yarn.lock",
21870
- "npm-shrinkwrap.json",
21871
- "requirements.txt",
21872
- "requirements-dev.txt",
21873
- "pyproject.toml",
21874
- "poetry.lock",
21875
- "Pipfile",
21876
- "Pipfile.lock",
21877
- "uv.lock",
21878
- "Gemfile",
21879
- "Gemfile.lock",
21880
- "go.mod",
21881
- "go.sum",
21882
- "Cargo.toml",
21883
- "Cargo.lock",
21884
- "pom.xml",
21885
- "build.gradle",
21886
- "build.gradle.kts",
21887
- "settings.gradle",
21888
- "settings.gradle.kts",
21889
- "Makefile",
21890
- "CMakeLists.txt"
21891
- ]);
21892
- /**
21893
- * Compiled-language source extensions that require a build step
21894
- * inside `docker build` (typically a `RUN go build` / `cargo build` /
21895
- * `mvn package` etc.). A copy of the source alone would leave the
21896
- * running binary stale, so the user's intent must be a rebuild.
21897
- *
21898
- * TypeScript source (`.ts` / `.tsx` / `.mts` / `.cts`) is treated as
21899
- * compiled because the dominant production-container pattern is to
21900
- * pre-compile the source via a Dockerfile `RUN tsc` / `RUN yarn build`
21901
- * step, with the runtime executing the emitted `dist/*.js`. Soft-reload
21902
- * would `docker cp` the new `.ts` into the container's WORKDIR while
21903
- * the running process keeps reading the OLD `dist/` — a silent
21904
- * stale-code failure that violates the file's "slow-but-correct beats
21905
- * fast-but-stale" default policy (lines 14-22). Setups that transpile
21906
- * at runtime lose the soft-reload fast path under this default; an
21907
- * opt-in flag to restore it is a possible follow-up but is not in
21908
- * scope here.
21909
- *
21910
- * Interpreted-language runtimes (Node — `.js` / `.mjs` / `.cjs`,
21911
- * Python — `.py`, Ruby — `.rb`, shell — `.sh`) read source at process
21912
- * start, so a `docker cp` + `docker restart` cycle picks them up.
21913
- * Those extensions are NOT in this set.
21914
- */
21915
- const COMPILED_LANGUAGE_EXTENSIONS = new Set([
21916
- ".go",
21917
- ".rs",
21918
- ".java",
21919
- ".kt",
21920
- ".kts",
21921
- ".scala",
21922
- ".cs",
21923
- ".swift",
21924
- ".fs",
21925
- ".fsx",
21926
- ".c",
21927
- ".cc",
21928
- ".cpp",
21929
- ".cxx",
21930
- ".h",
21931
- ".hpp",
21932
- ".zig",
21933
- ".ml",
21934
- ".mli",
21935
- ".elm",
21936
- ".hs",
21937
- ".dart",
21938
- ".ts",
21939
- ".tsx",
21940
- ".mts",
21941
- ".cts"
21942
- ]);
21943
- /**
21944
- * Classify a single watcher firing into rebuild vs soft-reload. Pure
21945
- * + synchronous. The caller (emulator's reload pathway) invokes this
21946
- * once per target per firing AFTER `cdk synth` has run and the new
21947
- * asset manifest is on disk.
21948
- *
21949
- * Branching:
21950
- * 1. No asset context (image isn't a CDK asset, or asset lookup
21951
- * failed) → `rebuild`.
21952
- * 2. The asset hash didn't change between old and new synths
21953
- * (`oldAssetHash === newAssetHash`, or `oldAssetHash` missing)
21954
- * → `rebuild`. Load-bearing guard for "user edited a CDK
21955
- * construct file (e.g. `lib/stack.ts`) that flipped the task
21956
- * spec but didn't touch the asset content". Soft-reload would
21957
- * `docker cp` identical files and `docker restart` the
21958
- * container with the OLD task spec (env / memory / mounts /
21959
- * added sidecars are set at `docker create` time, not on
21960
- * restart) — the user's intent would silently NOT apply.
21961
- * Forcing rebuild keeps Phase 1-3 semantics exactly for this
21962
- * case: the rolling primitive boots a shadow with the new task
21963
- * spec, the user sees their construct edit take effect.
21964
- * 3. No changed paths (the watcher fired on a debounce flush with
21965
- * an empty pending set — shouldn't happen in practice, but
21966
- * defensive) → `rebuild`.
21967
- * 4. Any changed path's basename matches the Dockerfile or a
21968
- * dependency manifest → `rebuild`.
21969
- * 5. Any changed path's extension is a compiled-language source →
21970
- * `rebuild`.
21971
- * 6. Else → `soft-reload`.
21972
- */
21973
- function classifySourceChange(changedPaths, ctx) {
21974
- if (!ctx) return {
21975
- kind: "rebuild",
21976
- reason: "target image is not a CDK docker-image asset"
21977
- };
21978
- if (!ctx.oldAssetHash || ctx.oldAssetHash === ctx.newAssetHash) return {
21979
- kind: "rebuild",
21980
- reason: "asset hash unchanged across the synth (CDK construct edit or unrelated file) — task-spec changes need a fresh `docker create`, which only the rebuild path runs"
21981
- };
21982
- if (changedPaths.length === 0) return {
21983
- kind: "rebuild",
21984
- reason: "no changed paths reported (defensive default)"
21985
- };
21986
- for (const p of changedPaths) {
21987
- const basename = path.basename(p);
21988
- if (basename === ctx.dockerFile) return {
21989
- kind: "rebuild",
21990
- reason: `Dockerfile edit (${basename})`
21991
- };
21992
- if (basename.startsWith("Dockerfile.")) return {
21993
- kind: "rebuild",
21994
- reason: `Dockerfile.* edit (${basename})`
21995
- };
21996
- if (REBUILD_TRIGGER_BASENAMES.has(basename)) return {
21997
- kind: "rebuild",
21998
- reason: `dependency manifest edit (${basename})`
21999
- };
22000
- const ext = path.extname(p).toLowerCase();
22001
- if (COMPILED_LANGUAGE_EXTENSIONS.has(ext)) return {
22002
- kind: "rebuild",
22003
- reason: `compiled-language source edit (${basename}) — soft-reload would leave the built binary stale`
22004
- };
22005
- }
22006
- return {
22007
- kind: "soft-reload",
22008
- reason: `${changedPaths.length} source-only path(s) — skipping rebuild`,
22009
- newAssetSourceDir: ctx.newAssetSourceDir
22010
- };
22011
- }
22012
-
22013
22487
  //#endregion
22014
22488
  //#region src/local/cloud-map-registry.ts
22015
22489
  /**
@@ -27349,5 +27823,5 @@ function addListSpecificOptions(cmd) {
27349
27823
  }
27350
27824
 
27351
27825
  //#endregion
27352
- export { MCP_PROTOCOL_VERSION as $, pickAgentCoreCandidateStack as $n, applyAuthorizerOverlay as $t, mergeForService as A, rejectExplicitCfnStackWithMultipleStacks as An, startApiServer as At, DEFAULT_SHADOW_READY_TIMEOUT_MS as B, listTargets as Bn, buildMethodArn as Bt, parseRestartPolicy as C, substituteAgainstState as Cn, materializeLayerFromArn as Ct, ImageOverrideError as D, LocalStateSourceError as Dn, filterRoutesByApiIdentifiers as Dt, runEcsServiceEmulator as E, substituteEnvVarsFromStateAsync as En, filterRoutesByApiIdentifier as Et, isLocalCdkAssetImage as F, collectSsmParameterRefs as Fn, buildJwksUrlFromIssuer as Ft, addInvokeAgentCoreSpecificOptions as G, pickRefLogicalId as Gn, attachAuthorizers as Gt, setShadowReadyTimeoutMs as H, discoverWebSocketApisOrThrow as Hn, evaluateCachedLambdaPolicy as Ht, listPinnedTargets as I, resolveSsmParameters as In, createJwksCache as It, A2A_CONTAINER_PORT as J, AGENTCORE_AGUI_PROTOCOL as Jn, buildCorsConfigFromCloudFrontChain as Jt, createLocalInvokeAgentCoreCommand as K, resolveLambdaArnIntrinsic as Kn, applyCorsResponseHeaders as Kt, buildCloudMapIndex as L, resolveWatchConfig as Ln, verifyCognitoJwt as Lt, resolveImageOverrides as M, resolveCfnRegion as Mn, resolveServiceIntegrationParameters as Mt, runImageOverrideBuilds as N, resolveCfnStackName as Nn, defaultCredentialsLoader as Nt, buildImageOverrideTag as O, createLocalStateProvider as On, groupRoutesByServer as Ot, describePinnedImageUri as P, CfnLocalStateProvider as Pn, buildCognitoJwksUrl as Pt, MCP_PATH as Q, AgentCoreResolutionError as Qn, translateLambdaResponse as Qt, CloudMapRegistry as R, resolveSingleTarget as Rn, verifyJwtAuthorizer as Rt, parseMaxTasks as S, EcsTaskResolutionError as Sn, buildStageMap as St, resolveSharedSidecarCredentials as T, substituteEnvVarsFromState as Tn, availableApiIdentifiers as Tt, getContainerNetworkIp as U, parseSelectionExpressionPath as Un, invokeRequestAuthorizer as Ut, SOFT_RELOAD_COMPLETION_LOG_SUFFIX as V, discoverWebSocketApis as Vn, computeRequestIdentityHash as Vt, attachContainerLogStreamer as W, discoverRoutes as Wn, invokeTokenAuthorizer as Wt, a2aInvokeOnce as X, AGENTCORE_MCP_PROTOCOL as Xn, matchPreflight as Xt, A2A_PATH as Y, AGENTCORE_HTTP_PROTOCOL as Yn, isFunctionUrlOacFronted as Yt, MCP_CONTAINER_PORT as Z, AGENTCORE_RUNTIME_TYPE as Zn, matchRoute as Zt, addCommonEcsServiceOptions as _, architectureToPlatform as _n, createWatchPredicates as _t, albStrategy as a, tryParseStatus as an, LocalInvokeBuildError as ar, invokeAgentCore as at, buildEcsImageResolutionContext$1 as b, resolveRuntimeFileExtension as bn, createFileWatcher as bt, resolveAlbTarget as c, probeHostGatewaySupport as cn, SUPPORTED_CODE_RUNTIMES as ct, addStartServiceSpecificOptions as d, buildMgmtEndpointEnvUrl as dn, renderCodeDockerfile as dt, buildHttpApiV2Event as en, resolveAgentCoreTarget as er, mcpInvokeOnce as et, createLocalStartServiceCommand as f, handleConnectionsRequest as fn, toCmdArgv as ft, MAX_TASKS_SUBNET_RANGE_CAP as g, buildMessageEvent as gn, createLocalStartApiCommand as gt, createLocalRunTaskCommand as h, buildDisconnectEvent as hn, addStartApiSpecificOptions as ht, addAlbSpecificOptions as i, selectIntegrationResponse as in, tryResolveImageFnJoin as ir, AGENTCORE_SESSION_ID_HEADER as it, parseImageOverrideFlags as j, resolveCfnFallbackRegion as jn, resolveSelectionExpression as jt, enforceImageOverrideOrphans as k, isCfnFlagPresent as kn, readMtlsMaterialsFromDisk as kt, isApplicationLoadBalancer as l, bufferToBody as ln, buildAgentCoreCodeImage as lt, addRunTaskSpecificOptions as m, buildConnectEvent as mn, createLocalInvokeCommand as mt, createLocalListCommand as n, evaluateResponseParameters as nn, formatStateRemedy as nr, AGENTCORE_SIGV4_SERVICE as nt, createLocalStartAlbCommand as o, VtlEvaluationError as on, buildStsClientConfig as or, waitForAgentCorePing as ot, serviceStrategy as p, parseConnectionsPath as pn, addInvokeSpecificOptions as pt, invokeAgentCoreWs as q, AGENTCORE_A2A_PROTOCOL as qn, buildCorsConfigByApiId as qt, formatTargetListing as r, pickResponseTemplate as rn, substituteImagePlaceholders as rr, signAgentCoreInvocation as rt, parseLbPortOverrides as s, HOST_GATEWAY_MIN_VERSION as sn, resolveProfileCredentials as sr, downloadAndExtractS3Bundle as st, addListSpecificOptions as t, buildRestV1Event as tn, derivePseudoParametersFromRegion as tr, parseSseForJsonRpc as tt, resolveAlbFrontDoor as u, ConnectionRegistry as un, computeCodeImageTag as ut, addEcsAssumeRoleOptions as v, buildContainerImage as vn, resolveApiTargetSubset as vt, resolveEcsAssumeRoleOption as w, substituteAgainstStateAsync as wn, resolveEnvVars as wt, ecsClusterOption as x, resolveRuntimeImage as xn, attachStageContext as xt, addImageOverrideOptions as y, resolveRuntimeCodeMountPath as yn, createAuthorizerCache as yt, classifySourceChange as z, countTargets as zn, verifyJwtViaDiscovery as zt };
27353
- //# sourceMappingURL=local-list-Dn0Al276.js.map
27826
+ export { mcpInvokeOnce as $, pickAgentCoreCandidateStack as $n, applyAuthorizerOverlay as $t, mergeForService as A, rejectExplicitCfnStackWithMultipleStacks as An, startApiServer as At, SOFT_RELOAD_COMPLETION_LOG_SUFFIX as B, listTargets as Bn, buildMethodArn as Bt, parseRestartPolicy as C, substituteAgainstState as Cn, materializeLayerFromArn as Ct, ImageOverrideError as D, LocalStateSourceError as Dn, filterRoutesByApiIdentifiers as Dt, runEcsServiceEmulator as E, substituteEnvVarsFromStateAsync as En, filterRoutesByApiIdentifier as Et, isLocalCdkAssetImage as F, collectSsmParameterRefs as Fn, buildJwksUrlFromIssuer as Ft, createLocalInvokeAgentCoreCommand as G, pickRefLogicalId as Gn, attachAuthorizers as Gt, getContainerNetworkIp as H, discoverWebSocketApisOrThrow as Hn, evaluateCachedLambdaPolicy as Ht, listPinnedTargets as I, resolveSsmParameters as In, createJwksCache as It, A2A_PATH as J, AGENTCORE_AGUI_PROTOCOL as Jn, buildCorsConfigFromCloudFrontChain as Jt, invokeAgentCoreWs as K, resolveLambdaArnIntrinsic as Kn, applyCorsResponseHeaders as Kt, buildCloudMapIndex as L, resolveWatchConfig as Ln, verifyCognitoJwt as Lt, resolveImageOverrides as M, resolveCfnRegion as Mn, resolveServiceIntegrationParameters as Mt, runImageOverrideBuilds as N, resolveCfnStackName as Nn, defaultCredentialsLoader as Nt, buildImageOverrideTag as O, createLocalStateProvider as On, groupRoutesByServer as Ot, describePinnedImageUri as P, CfnLocalStateProvider as Pn, buildCognitoJwksUrl as Pt, MCP_PROTOCOL_VERSION as Q, AgentCoreResolutionError as Qn, translateLambdaResponse as Qt, CloudMapRegistry as R, resolveSingleTarget as Rn, verifyJwtAuthorizer as Rt, parseMaxTasks as S, EcsTaskResolutionError as Sn, buildStageMap as St, resolveSharedSidecarCredentials as T, substituteEnvVarsFromState as Tn, availableApiIdentifiers as Tt, attachContainerLogStreamer as U, parseSelectionExpressionPath as Un, invokeRequestAuthorizer as Ut, setShadowReadyTimeoutMs as V, discoverWebSocketApis as Vn, computeRequestIdentityHash as Vt, addInvokeAgentCoreSpecificOptions as W, discoverRoutes as Wn, invokeTokenAuthorizer as Wt, MCP_CONTAINER_PORT as X, AGENTCORE_MCP_PROTOCOL as Xn, matchPreflight as Xt, a2aInvokeOnce as Y, AGENTCORE_HTTP_PROTOCOL as Yn, isFunctionUrlOacFronted as Yt, MCP_PATH as Z, AGENTCORE_RUNTIME_TYPE as Zn, matchRoute as Zt, addCommonEcsServiceOptions as _, architectureToPlatform as _n, createWatchPredicates as _t, albStrategy as a, tryParseStatus as an, LocalInvokeBuildError as ar, waitForAgentCorePing as at, buildEcsImageResolutionContext$1 as b, resolveRuntimeFileExtension as bn, createFileWatcher as bt, resolveAlbTarget as c, probeHostGatewaySupport as cn, buildAgentCoreCodeImage as ct, addStartServiceSpecificOptions as d, buildMgmtEndpointEnvUrl as dn, toCmdArgv as dt, buildHttpApiV2Event as en, resolveAgentCoreTarget as er, parseSseForJsonRpc as et, createLocalStartServiceCommand as f, handleConnectionsRequest as fn, classifySourceChange as ft, MAX_TASKS_SUBNET_RANGE_CAP as g, buildMessageEvent as gn, createLocalStartApiCommand as gt, createLocalRunTaskCommand as h, buildDisconnectEvent as hn, addStartApiSpecificOptions as ht, addAlbSpecificOptions as i, selectIntegrationResponse as in, tryResolveImageFnJoin as ir, invokeAgentCore as it, parseImageOverrideFlags as j, resolveCfnFallbackRegion as jn, resolveSelectionExpression as jt, enforceImageOverrideOrphans as k, isCfnFlagPresent as kn, readMtlsMaterialsFromDisk as kt, isApplicationLoadBalancer as l, bufferToBody as ln, computeCodeImageTag as lt, addRunTaskSpecificOptions as m, buildConnectEvent as mn, createLocalInvokeCommand as mt, createLocalListCommand as n, evaluateResponseParameters as nn, formatStateRemedy as nr, signAgentCoreInvocation as nt, createLocalStartAlbCommand as o, VtlEvaluationError as on, buildStsClientConfig as or, downloadAndExtractS3Bundle as ot, serviceStrategy as p, parseConnectionsPath as pn, addInvokeSpecificOptions as pt, A2A_CONTAINER_PORT as q, AGENTCORE_A2A_PROTOCOL as qn, buildCorsConfigByApiId as qt, formatTargetListing as r, pickResponseTemplate as rn, substituteImagePlaceholders as rr, AGENTCORE_SESSION_ID_HEADER as rt, parseLbPortOverrides as s, HOST_GATEWAY_MIN_VERSION as sn, resolveProfileCredentials as sr, SUPPORTED_CODE_RUNTIMES as st, addListSpecificOptions as t, buildRestV1Event as tn, derivePseudoParametersFromRegion as tr, AGENTCORE_SIGV4_SERVICE as tt, resolveAlbFrontDoor as u, ConnectionRegistry as un, renderCodeDockerfile as ut, addEcsAssumeRoleOptions as v, buildContainerImage as vn, resolveApiTargetSubset as vt, resolveEcsAssumeRoleOption as w, substituteAgainstStateAsync as wn, resolveEnvVars as wt, ecsClusterOption as x, resolveRuntimeImage as xn, attachStageContext as xt, addImageOverrideOptions as y, resolveRuntimeCodeMountPath as yn, createAuthorizerCache as yt, DEFAULT_SHADOW_READY_TIMEOUT_MS as z, countTargets as zn, verifyJwtViaDiscovery as zt };
27827
+ //# sourceMappingURL=local-list-Dh6xjxGf.js.map