Skip to content

feat: add Lambda interceptors for AgentCore gateways#1330

Open
aidandaly24 wants to merge 6 commits into
aws:mainfrom
aidandaly24:feat/interceptor
Open

feat: add Lambda interceptors for AgentCore gateways#1330
aidandaly24 wants to merge 6 commits into
aws:mainfrom
aidandaly24:feat/interceptor

Conversation

@aidandaly24
Copy link
Copy Markdown
Contributor

@aidandaly24 aidandaly24 commented May 20, 2026

Description

Adds end-to-end support for Lambda interceptors at gateway REQUEST and RESPONSE points: a new InterceptorPrimitive, three vended templates (pass-through, jwt-scope-authorizer, tools-list-filter) for both Python and Node.js, deploy-side preflight, and logs interceptor / invoke interceptor verbs.

Primitive surface

  • `agentcore add interceptor --name --gateway --interception-points --template --runtime [--timeout] [--no-pass-request-headers] [--additional-policies]` for managed mode
  • External mode via direct `agentcore.json` edit referencing a customer-provided unqualified Lambda ARN
  • `agentcore remove interceptor --name` reconciles deploy state on next `agentcore deploy`
  • `agentcore logs interceptor --name [--follow|--since|--until]` tails CloudWatch for managed interceptors; external interceptors short-circuit to a remediation message
  • `agentcore invoke interceptor --name [--payload]` invokes the underlying Lambda for managed interceptors

Schema

  • `interceptors` array on `AgentCoreProjectSpec` with cross-field `superRefine` for cardinality (max 1 per point per gateway, max 2 per gateway) and gateway-name reference checks
  • Lambda ARN regex tightened to reject version/alias qualifiers
  • `passRequestHeaders`, `entrypoint`, `timeoutSeconds`, `runtime` are optional with consumption-time defaults so user-edited `agentcore.json` stays sparse across CLI round-trips

Deploy

  • `validateInterceptors()` checks managed-mode codeLocation existence and emits a masked cross-account WARN for external mode (with full `aws lambda add-permission` remediation snippet); also wired into `agentcore validate` so the pre-check works without invoking deploy
  • `validateGatewayTargetLambdas()` does a best-effort `lambda:GetFunction` per `lambda-function-arn` target and WARNs on any failure (NotFound, AccessDenied, throttle) so a typo'd ARN surfaces before CFN ROLLBACK
  • Account IDs go through `maskAccountId()` for all user-visible warning output (PII masking)

Templates

  • `pass-through`, `jwt-scope-authorizer`, `tools-list-filter` for Python (`pyproject.toml` + hatchling) and Node.js (`index.mjs` + `package.json`)

Bug fixes

  • ANSI cursor-show escape (`\x1b[?25h`) was being written to stdout on every CLI invocation by ink/cli-cursor on App unmount, corrupting machine-parseable output (broke `--json` JSON parsing). Fixed at the CLI entry by stripping cursor show/hide escapes from non-TTY stdout/stderr writes.

Out of scope (P0)

The following were deliberately excluded from this PR. None are bugs — flagged here so reviewers don't ask "where is X":

  • PII-redaction template — patterns are wildly customer-specific (a bank's PII isn't a hospital's). Better as a future config-driven template, or roll your own using `pass-through` as a skeleton.
  • Audit-logging template (OpenSearch / S3) — non-trivial destination wiring (IAM, batching, SIEM plug-in points). Ship later when there's a clearer customer ask.
  • Provisioned concurrency — adds CFN complexity and runs $$$ even when idle. Wait for actual latency-sensitive customer demand.
  • Multi-gateway shared interceptor pools — schema is currently 1 interceptor → 1 gateway. Sharing across gateways needs schema redesign (`gatewayNames: string[]` or a separate attach relation) and harder cardinality validation.
  • Console UX parity — separate team's mission. CLI guarantees programmatic parity, not click-for-click visual matching.
  • Streaming-aware first-invocation guard — shipped as commented-in code in the pass-through Node template (`if (invocationIndex > 0) { ... }`). Activating by default would change non-streaming behavior; customers opt in when needed.

Related Issue

Closes #

Documentation PR

Type of Change

  • New feature

Testing

  • I ran `npm run test:unit` and `npm run test:integ`
  • I ran `npm run typecheck`
  • I ran `npm run lint`
  • If I modified `src/assets/`, I ran `npm run test:update-snapshots` and committed the updated snapshots

Additionally:

  • End-to-end bug bash conducted across 60+ matrix cases against live AWS account, with 20+ parallel subagents covering: managed Python + Node templates × all 3 interception points, external same-account at REQUEST/RESPONSE/dual-point, cross-account preflight WARN, full deploy lifecycle (deploy → redeploy → remove → teardown), real MCP traffic with interceptor invocation proven via CloudWatch logs, JWT scope-authorizer allow/deny, RESPONSE-side `statusCode` mutation, tools-list filtering, additionalPolicies as file path AND managed ARN, IAM Sid hash uniqueness + size under 10240 bytes, schema rejects (qualifier ARN, unknown gateway, duplicate name, over-length name), --json output validity, TUI integration verified via tui-harness MCP

Checklist

  • I have read the CONTRIBUTING document
  • I have added any necessary tests that prove my fix is effective or my feature works
  • I have updated the documentation accordingly
  • I have added an appropriate example to the documentation to outline the feature, or no new docs are needed
  • My changes generate no new warnings
  • Any dependent changes have been merged and published — depends on aws/agentcore-l3-cdk-constructs#225

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@aidandaly24 aidandaly24 requested a review from a team May 20, 2026 21:44
@github-actions github-actions Bot added the size/xl PR size: XL label May 20, 2026
@agentcore-devx-automation agentcore-devx-automation Bot added the claude-security-reviewing Claude Code /security-review in progress label May 20, 2026
@github-actions github-actions Bot added the agentcore-harness-reviewing AgentCore Harness review in progress label May 20, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 20, 2026

Package Tarball

aws-agentcore-0.14.1.tgz

How to install

gh release download pr-1330-tarball --repo aws/agentcore-cli --pattern "*.tgz" --dir /tmp/pr-tarball
npm install -g /tmp/pr-tarball/aws-agentcore-0.14.1.tgz

@agentcore-devx-automation
Copy link
Copy Markdown
Contributor

Claude Security Review: no high-confidence findings. (run)

@agentcore-devx-automation agentcore-devx-automation Bot removed the claude-security-reviewing Claude Code /security-review in progress label May 20, 2026
@github-actions github-actions Bot removed the agentcore-harness-reviewing AgentCore Harness review in progress label May 20, 2026
Adds end-to-end support for Lambda interceptors at gateway REQUEST and
RESPONSE points: a new InterceptorPrimitive, three vended templates
(pass-through, jwt-scope-authorizer, tools-list-filter) for both Python
and Node.js, deploy-side preflight, and `logs interceptor` /
`invoke interceptor` verbs.

Primitive surface
-----------------
- `agentcore add interceptor --name --gateway --interception-points
  --template --runtime [--timeout] [--no-pass-request-headers]
  [--additional-policies]` for managed mode
- External mode via direct `agentcore.json` edit referencing a
  customer-provided unqualified Lambda ARN
- `agentcore remove interceptor --name` reconciles deploy state on next
  `agentcore deploy`
- `agentcore logs interceptor --name [--follow|--since|--until]` tails
  CloudWatch for managed interceptors; external interceptors short-
  circuit to a remediation message pointing at `aws logs tail`
- `agentcore invoke interceptor --name [--payload]` invokes the
  underlying Lambda for managed interceptors with the same external-
  mode remediation message

Schema
------
- `interceptors` array on `AgentCoreProjectSpec` with cross-field
  superRefine for cardinality (max 1 per point per gateway, max 2 per
  gateway) and gateway-name reference checks
- Lambda ARN regex tightened to reject version/alias qualifiers
- `passRequestHeaders`, `entrypoint`, `timeoutSeconds`, `runtime` are
  optional with consumption-time defaults so user-edited
  `agentcore.json` stays sparse across CLI round-trips

Deploy
------
- `validateInterceptors()` checks managed-mode codeLocation existence
  and emits a masked cross-account WARN for external mode (with full
  `aws lambda add-permission` remediation snippet); also wired into
  `agentcore validate` so the pre-check works without invoking deploy
- `validateGatewayTargetLambdas()` does a best-effort
  `lambda:GetFunction` per `lambda-function-arn` target and WARNs on
  any failure (NotFound, AccessDenied, throttle) so a typo'd ARN
  surfaces before CFN ROLLBACK
- Account IDs go through `maskAccountId()` for all user-visible warning
  output (PII masking)

Templates
---------
- `pass-through`, `jwt-scope-authorizer`, `tools-list-filter` for
  Python (`pyproject.toml` + hatchling) and Node.js (`index.mjs` +
  `package.json`)
- `PackageName` Handlebars var for NPM-safe lowercase package naming

Telemetry
---------
- `has_cross_account_warning` boolean on the InterceptorPrimitive
  command-run telemetry schema
ink (via cli-cursor) writes \x1b[?25h to its render stream on App unmount.
The cli-cursor isTTY check was passing because ink's stdout wrapper
inherits isTTY from the underlying stream object, even when stdout is
piped or redirected. The trailing escape corrupted machine-parseable
output for every CLI invocation that captured stdout — most
visibly --json (broke JSON parsing) but also any pipe/redirect
workflow.

Found by the round-2 bug bash (E6) when an agent tried to
`json.load()` the output of `agentcore add interceptor --json` and got
"Extra data" — the bytes after the JSON were the cursor-show escape.

Fix: at the CLI entry, wrap process.stdout and process.stderr writes
to strip \x1b[?25[hl] (cursor show / hide) when the stream is NOT a
TTY. Real TTYs are untouched so interactive terminal behavior — cursor
hiding during spinners, restoring on exit — keeps working.

Verified live with the bundled CLI:
- agentcore --version: ends with newline only, no escape
- agentcore validate: ends with newline only
- agentcore add gateway --json: produces valid JSON, json.load() succeeds
@github-actions github-actions Bot removed the size/xl PR size: XL label May 21, 2026
@github-actions github-actions Bot added the size/xl PR size: XL label May 21, 2026
@agentcore-devx-automation agentcore-devx-automation Bot added the claude-security-reviewing Claude Code /security-review in progress label May 21, 2026
@agentcore-devx-automation
Copy link
Copy Markdown
Contributor

Claude Security Review: the review run failed before completing. See the run for details.

@agentcore-devx-automation agentcore-devx-automation Bot removed the claude-security-reviewing Claude Code /security-review in progress label May 21, 2026
The 'Limitations / out of scope (P0)' section captured deferred features
for internal review context, not customer-facing guidance. Move it to the
PR description so reviewers still see what was deliberately excluded.
@github-actions github-actions Bot removed the size/xl PR size: XL label May 21, 2026
@github-actions github-actions Bot added the size/xl PR size: XL label May 21, 2026
@agentcore-devx-automation agentcore-devx-automation Bot added the claude-security-reviewing Claude Code /security-review in progress label May 21, 2026
@agentcore-devx-automation
Copy link
Copy Markdown
Contributor

Claude Security Review: the review run failed before completing. See the run for details.

@agentcore-devx-automation agentcore-devx-automation Bot removed the claude-security-reviewing Claude Code /security-review in progress label May 21, 2026
Three fixes for CI failures on the open PR:

- schemas/agentcore.schema.v1.json: revert to origin/main. The CI gate
  rejects any change to this file because it is regenerated and
  released automatically — my rebase had carried my staged version.
- .prettierignore: add `src/assets/**/*.mjs`. The new Node interceptor
  templates ship as raw .mjs that's vended to users; we shouldn't
  reformat them via prettier on the way in.
- npm-shrinkwrap.json: revert to origin/main. My local `npm install`
  during the rebase produced a different ordering than what's on
  upstream — keeping upstream's authoritative version avoids drift.
@github-actions github-actions Bot removed the size/xl PR size: XL label May 21, 2026
@github-actions github-actions Bot added the size/xl PR size: XL label May 21, 2026
@agentcore-devx-automation agentcore-devx-automation Bot added the claude-security-reviewing Claude Code /security-review in progress label May 21, 2026
@agentcore-devx-automation
Copy link
Copy Markdown
Contributor

Claude Security Review: the review run failed before completing. See the run for details.

@agentcore-devx-automation agentcore-devx-automation Bot removed the claude-security-reviewing Claude Code /security-review in progress label May 21, 2026
Four unit tests asserted against pre-FIX-BATCH-3 behavior. Bringing them
in line with the shipped code:

1. logs/__tests__/interceptor.test.ts — error message no longer mentions
   the internal field name `interceptorFunctionName`. Assert against the
   new "has no Lambda function" copy that the user actually sees.
2. shared/__tests__/interceptor-mode-check.test.ts — error message no
   longer leaks `deployed-state.json`. Assert against the new
   "is not deployed" copy.
3. schema/.../interceptor.test.ts — schema fields are .optional() and
   defaults are applied at consumption time (not parse time), so a
   sparse parse leaves them undefined rather than filling them in.
4. operations/deploy/__tests__/preflight.test.ts — the new
   validateGatewayTargetLambdas() preflight imports getCredentialProvider
   from aws/account.js. Stub it in the existing vi.mock so existing
   fixtures (which have no lambda-function-arn targets) keep working.
@github-actions github-actions Bot added size/xl PR size: XL and removed size/xl PR size: XL labels May 21, 2026
@agentcore-devx-automation agentcore-devx-automation Bot added the claude-security-reviewing Claude Code /security-review in progress label May 21, 2026
@agentcore-devx-automation
Copy link
Copy Markdown
Contributor

Claude Security Review: the review run failed before completing. See the run for details.

@agentcore-devx-automation agentcore-devx-automation Bot removed the claude-security-reviewing Claude Code /security-review in progress label May 21, 2026
The vended CDK project at src/assets/cdk/ runs against whichever
@aws/agentcore-cdk version the user installs from npm. Until the CDK
PR merges and publishes a new alpha, the registry version doesn't
declare `interceptors` on the project spec type — so the e2e job
fails with TS2353 when building the vended cdk/ project.

Drop the explicit `interceptors: []` from the test object literal.
Zod's default fills in `[]` at parse time when the new CDK ships, and
the field's absence is harmless in the meantime. The synth assertion
itself doesn't care about interceptors at all.

Snapshot updated to match.
@github-actions github-actions Bot removed the size/xl PR size: XL label May 21, 2026
@github-actions github-actions Bot added the size/xl PR size: XL label May 21, 2026
@agentcore-devx-automation agentcore-devx-automation Bot added the claude-security-reviewing Claude Code /security-review in progress label May 21, 2026
@agentcore-devx-automation
Copy link
Copy Markdown
Contributor

Claude Security Review: the review run failed before completing. See the run for details.

@agentcore-devx-automation agentcore-devx-automation Bot removed the claude-security-reviewing Claude Code /security-review in progress label May 21, 2026
Copy link
Copy Markdown
Contributor

@tejaskash tejaskash left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Big PR but tight execution — the schema/CDK mirror, account masking, structured external-mode remediation, and cursor-escape fix are all well-handled. Four inline notes; none are blocking.

try {
const project = await this.readProjectSpec();
project.interceptors = project.interceptors.filter(i => i.name !== options.name);
await this.writeProjectSpec(project);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rollback only un-edits agentcore.json — if renderInterceptorTemplate throws after partial writes, the half-rendered files in app/<name>/ stay on disk. A subsequent agentcore add interceptor --name <same> then fails at scaffolding because the directory exists. Either rm the partial tree on rollback, or document that the user has to clean it up before retrying.

error: new ValidationError(`Cannot read --payload-file "${options.payloadFile}": ${msg}`),
};
}
} else if (options.payload) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--payload and --payload-file aren't mutually exclusive — passing both silently prefers --payload-file. Most CLIs reject the combo with a clear error. A user passing both probably has a bug they'd want to know about.

}

try {
await client.send(new GetFunctionCommand({ FunctionName: arn }));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sequential GetFunction across all gateways × targets. For projects with many targets (and especially across regions, since each region gets its own client), the latency adds up on every deploy. Promise.all over a flattened (gateway, target) list would parallelize without changing the warn-only semantics — bonus: a single SDK throttle event won't serialize the rest.

Comment thread src/cli/index.ts
return originalWrite(chunk as never, ...(rest as [never]));
}) as typeof stream.write;
}
patchStream(process.stdout);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Module-import side effect that monkey-patches process.stdout.write and process.stderr.write for the lifetime of the process. Scope is correct (non-TTY only, just \x1b[?25[hl]) and the motivation is real, but it's the kind of thing a future contributor will be very surprised by. Worth either moving to an explicit installCursorFilter() called from main() so it's greppable, or making the comment block more prominent so it doesn't read like a stray import-time bootstrap.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/xl PR size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants