Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ src/assets/**/*.md
.github/scripts/prompts/
src/assets/**/*.ts
src/assets/**/*.json
src/assets/**/*.mjs
src/assets/**/*.template
19 changes: 15 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,24 @@ agentcore invoke

### Resource Management

| Command | Description |
| -------- | ---------------------------------------------------- |
| `add` | Add agents, memory, credentials, evaluators, targets |
| `remove` | Remove resources from project |
| Command | Description |
| -------- | ------------------------------------------------------------------ |
| `add` | Add agents, memory, credentials, evaluators, targets, interceptors |
| `remove` | Remove resources from project |

> **Note**: Run `agentcore deploy` after `add` or `remove` to update resources in AWS.

#### Interceptors

| Command | Description |
| ------------------------------- | ------------------------------------------------------------------- |
| `add interceptor` | Add a Lambda interceptor (managed scaffold or BYO ARN) to a gateway |
| `remove interceptor` | Remove an interceptor |
| `logs interceptor --name <n>` | Tail or search managed interceptor CloudWatch logs |
| `invoke interceptor --name <n>` | Invoke a managed interceptor with a synthetic payload |

See [docs/interceptors.md](docs/interceptors.md) for templates, schema, and the cross-account behavior.

### Observability

| Command | Description |
Expand Down
163 changes: 163 additions & 0 deletions docs/interceptors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
# Lambda Interceptors

AgentCore Gateway Interceptors are customer-owned Lambda functions that the gateway invokes on every MCP request to
inspect, transform, or short-circuit traffic. They run at one of two interception points:

- **REQUEST** — before the gateway invokes the target.
- **RESPONSE** — after the target returns, before the gateway replies to the caller.

A gateway can carry up to **2 interceptors** (one REQUEST + one RESPONSE), or a single interceptor wired to both points.

## Modes

The CLI supports two first-class modes, mirroring the existing code-based evaluator pattern:

| Mode | What the CLI owns | When to use |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------- |
| **Managed** (default) | Scaffolds a templated Lambda project under `app/<name>/`, packages it, deploys it, renders the resulting ARN into the gateway's `InterceptorConfigurations`. | You want the CLI to own the source tree and deploy artifact end-to-end. |
| **External** | You pass an already-deployed Lambda ARN with `--lambda-arn`. The CLI plugs the ARN into the gateway and grants `lambda:InvokeFunction` to the gateway role. | You have a centralized auth Lambda or a third-party-owned function. |

## Quick start — managed

```bash
# Single REQUEST-point interceptor with the JWT scope authorizer template
agentcore add interceptor \
--name auth-check \
--gateway my-gateway \
--interception-points REQUEST \
--template jwt-scope-authorizer \
--runtime python3.12

# Edit app/auth-check/handler.py with your scope rules, then:
agentcore deploy
```

## Quick start — external (BYO ARN)

```bash
agentcore add interceptor \
--name central-auth \
--gateway my-gateway \
--interception-points REQUEST \
--lambda-arn arn:aws:lambda:us-east-1:111111111111:function:central-auth-prod
```

The CLI does not scaffold any code; the only artifact is the JSON entry in `agentcore.json`.

## Dual-point on a single Lambda

A single interceptor can serve both REQUEST and RESPONSE on the same gateway:

```bash
agentcore add interceptor \
--name dual-point \
--gateway my-gateway \
--interception-points REQUEST,RESPONSE \
--template pass-through \
--runtime python3.12
```

This counts as one interceptor against the cardinality cap.

## Templates (managed mode)

| Template | Point(s) | Purpose |
| ---------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `pass-through` | REQUEST or RESPONSE | Minimal compliant handler. Demonstrates the input/output envelope and the streaming guard. |
| `jwt-scope-authorizer` | REQUEST | Decodes the inbound `Authorization` JWT and short-circuits with a structured 403 if the required scope is missing. |
| `tools-list-filter` | RESPONSE | Strips unauthorized tools from `tools/list` responses based on a customer-supplied `is_authorized()` predicate. |

Each template ships in both Python 3.12 and Node.js 22.x. Pick with `--runtime python3.12` (default) or
`--runtime nodejs22.x`.

## Operational verbs

```bash
# Tail logs for a managed interceptor
agentcore logs interceptor --name auth-check --follow

# Search logs by time window
agentcore logs interceptor --name auth-check --since 1h --until now

# Invoke synthetically with a payload file
agentcore invoke interceptor --name auth-check --payload-file ./test-event.json
```

For external interceptors, both verbs print a copy-pasteable `aws` CLI remediation and exit non-zero — the CLI doesn't
own those Lambdas.

## Cross-account external interceptors

When `--lambda-arn`'s account ID does not match the deploy target's account, the CLI emits a **warning** at preflight
(with masked account IDs) and **continues** the deploy. The deploy itself succeeds — the gateway role's identity policy
grants `lambda:InvokeFunction` on the foreign ARN. What doesn't work yet is the first invocation: AWS Lambda requires a
matching resource-based policy on the function granting the gateway role permission to invoke it.

Example warning (account IDs masked to last 4 digits):

```
WARNING: Cross-account interceptor detected for "central-auth".
Gateway account(s): ****1947
Lambda: arn:aws:lambda:us-east-1:****1111:function:central-auth-prod

Deploy will succeed, but the first interceptor invocation will fail until
you add a resource-based policy to the Lambda. Run this in the Lambda's
account (once per interceptor) before sending traffic through the gateway:

aws lambda add-permission \
--function-name <your-interceptor-function-name> \
--statement-id GatewayServiceRoleInvoke \
--action lambda:InvokeFunction \
--principal <gateway-role-arn-from-deployed-state>

Continuing with deploy...
```

Run the snippet once in the Lambda's account, before sending traffic through the gateway.

## Schema

```jsonc
{
"interceptors": [
{
"name": "auth-check",
"gatewayName": "my-gateway",
"interceptionPoints": ["REQUEST"],
"passRequestHeaders": true,
"config": {
"managed": {
"codeLocation": "app/auth-check/",
"entrypoint": "handler.lambda_handler",
"timeoutSeconds": 30,
"runtime": "python3.12",
"additionalPolicies": ["execution-role-policy.json"],
},
},
},
{
"name": "central-auth",
"gatewayName": "my-gateway",
"interceptionPoints": ["RESPONSE"],
"passRequestHeaders": true,
"config": {
"external": {
"lambdaArn": "arn:aws:lambda:us-east-1:111111111111:function:central-auth-prod",
},
},
},
],
}
```

`config.managed` and `config.external` are mutually exclusive (exactly one must be set).

## Removal

```bash
agentcore remove interceptor --name auth-check
agentcore deploy
```

Managed-mode removal also deletes the scaffolded `app/<name>/` directory. External-mode removal touches only the JSON
entry. The next `deploy` reconciles the gateway via CloudFormation — no imperative `UpdateGateway` calls.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
"@aws-sdk/client-bedrock-runtime": "^3.893.0",
"@aws-sdk/client-cloudformation": "^3.893.0",
"@aws-sdk/client-cloudwatch-logs": "^3.893.0",
"@aws-sdk/client-lambda": "^3.893.0",
"@aws-sdk/client-resource-groups-tagging-api": "^3.893.0",
"@aws-sdk/client-s3": "^3.1012.0",
"@aws-sdk/client-sts": "^3.893.0",
Expand Down
68 changes: 55 additions & 13 deletions src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,21 @@ async function main() {
// Gateway fields are stored in agentcore.json but may not yet be on the
// AgentCoreProjectSpec type from @aws/agentcore-cdk, so we read them
// dynamically and cast the resulting object.
//
// Interceptors live at root in agentcore.json (\`interceptors[]\`). The CDK
// package's AgentCoreMcpSpec carries them in the same MCP-scoped object as
// gateways/runtimes, so we copy the field across at this boundary. Empty
// arrays are normalized to undefined to keep CDK synth clean when no
// interceptors are defined.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const specAny = spec as any;
const mcpSpec = specAny.agentCoreGateways?.length
const hasMcp = specAny.agentCoreGateways?.length || specAny.interceptors?.length;
const mcpSpec = hasMcp
? {
agentCoreGateways: specAny.agentCoreGateways,
agentCoreGateways: specAny.agentCoreGateways ?? [],
mcpRuntimeTools: specAny.mcpRuntimeTools,
unassignedTargets: specAny.unassignedTargets,
interceptors: specAny.interceptors?.length ? specAny.interceptors : undefined,
}
: undefined;

Expand Down Expand Up @@ -300,15 +308,25 @@ export class AgentCoreStack extends Stack {
spec,
});

// Create AgentCoreMcp if there are gateways configured
if (mcpSpec?.agentCoreGateways && mcpSpec.agentCoreGateways.length > 0) {
new AgentCoreMcp(this, 'Mcp', {
projectName: spec.name,
mcpSpec,
agentCoreApplication: this.application,
credentials,
projectTags: spec.tags,
});
// Create AgentCoreMcp if there are gateways or interceptors configured.
// Interceptors are MCP-scoped via the gatewayName reference, so they
// never appear without gateways under valid schema, but the OR guard
// here is defensive — it prevents interceptors from silently vanishing
// if the spec ever reaches synth in a partially-validated state.
if (mcpSpec) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- the bundled CDK type may not yet declare interceptors
const interceptorsAny = (mcpSpec as any).interceptors;
const hasGateways = mcpSpec.agentCoreGateways && mcpSpec.agentCoreGateways.length > 0;
const hasInterceptors = interceptorsAny && interceptorsAny.length > 0;
if (hasGateways || hasInterceptors) {
new AgentCoreMcp(this, 'Mcp', {
projectName: spec.name,
mcpSpec,
agentCoreApplication: this.application,
credentials,
projectTags: spec.tags,
});
}
}

// Stack-level output
Expand Down Expand Up @@ -357,8 +375,8 @@ exports[`Assets Directory Snapshots > CDK assets > cdk/cdk/package.json should m
"typescript": "~5.9.3"
},
"dependencies": {
"@aws/agentcore-cdk": "^0.1.0-alpha.19",
"aws-cdk-lib": "^2.248.0",
"@aws/agentcore-cdk": "^0.1.0-alpha.29",
"aws-cdk-lib": "^2.252.0",
"constructs": "^10.0.0"
}
}
Expand Down Expand Up @@ -451,6 +469,30 @@ exports[`Assets Directory Snapshots > File listing > should match the expected f
"evaluators/python-lambda/execution-role-policy.json",
"evaluators/python-lambda/lambda_function.py",
"evaluators/python-lambda/pyproject.toml",
"interceptors/node-lambda/jwt-scope-authorizer/README.md",
"interceptors/node-lambda/jwt-scope-authorizer/execution-role-policy.json",
"interceptors/node-lambda/jwt-scope-authorizer/index.mjs",
"interceptors/node-lambda/jwt-scope-authorizer/package.json",
"interceptors/node-lambda/pass-through/README.md",
"interceptors/node-lambda/pass-through/execution-role-policy.json",
"interceptors/node-lambda/pass-through/index.mjs",
"interceptors/node-lambda/pass-through/package.json",
"interceptors/node-lambda/tools-list-filter/README.md",
"interceptors/node-lambda/tools-list-filter/execution-role-policy.json",
"interceptors/node-lambda/tools-list-filter/index.mjs",
"interceptors/node-lambda/tools-list-filter/package.json",
"interceptors/python-lambda/jwt-scope-authorizer/README.md",
"interceptors/python-lambda/jwt-scope-authorizer/execution-role-policy.json",
"interceptors/python-lambda/jwt-scope-authorizer/handler.py",
"interceptors/python-lambda/jwt-scope-authorizer/pyproject.toml",
"interceptors/python-lambda/pass-through/README.md",
"interceptors/python-lambda/pass-through/execution-role-policy.json",
"interceptors/python-lambda/pass-through/handler.py",
"interceptors/python-lambda/pass-through/pyproject.toml",
"interceptors/python-lambda/tools-list-filter/README.md",
"interceptors/python-lambda/tools-list-filter/execution-role-policy.json",
"interceptors/python-lambda/tools-list-filter/handler.py",
"interceptors/python-lambda/tools-list-filter/pyproject.toml",
"mcp/python-lambda/README.md",
"mcp/python-lambda/handler.py",
"mcp/python-lambda/pyproject.toml",
Expand Down
12 changes: 10 additions & 2 deletions src/assets/cdk/bin/cdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,21 @@ async function main() {
// Gateway fields are stored in agentcore.json but may not yet be on the
// AgentCoreProjectSpec type from @aws/agentcore-cdk, so we read them
// dynamically and cast the resulting object.
//
// Interceptors live at root in agentcore.json (`interceptors[]`). The CDK
// package's AgentCoreMcpSpec carries them in the same MCP-scoped object as
// gateways/runtimes, so we copy the field across at this boundary. Empty
// arrays are normalized to undefined to keep CDK synth clean when no
// interceptors are defined.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const specAny = spec as any;
const mcpSpec = specAny.agentCoreGateways?.length
const hasMcp = specAny.agentCoreGateways?.length || specAny.interceptors?.length;
const mcpSpec = hasMcp
? {
agentCoreGateways: specAny.agentCoreGateways,
agentCoreGateways: specAny.agentCoreGateways ?? [],
mcpRuntimeTools: specAny.mcpRuntimeTools,
unassignedTargets: specAny.unassignedTargets,
interceptors: specAny.interceptors?.length ? specAny.interceptors : undefined,
}
: undefined;

Expand Down
28 changes: 19 additions & 9 deletions src/assets/cdk/lib/cdk-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,25 @@ export class AgentCoreStack extends Stack {
spec,
});

// Create AgentCoreMcp if there are gateways configured
if (mcpSpec?.agentCoreGateways && mcpSpec.agentCoreGateways.length > 0) {
new AgentCoreMcp(this, 'Mcp', {
projectName: spec.name,
mcpSpec,
agentCoreApplication: this.application,
credentials,
projectTags: spec.tags,
});
// Create AgentCoreMcp if there are gateways or interceptors configured.
// Interceptors are MCP-scoped via the gatewayName reference, so they
// never appear without gateways under valid schema, but the OR guard
// here is defensive — it prevents interceptors from silently vanishing
// if the spec ever reaches synth in a partially-validated state.
if (mcpSpec) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- the bundled CDK type may not yet declare interceptors
const interceptorsAny = (mcpSpec as any).interceptors;
const hasGateways = mcpSpec.agentCoreGateways && mcpSpec.agentCoreGateways.length > 0;
const hasInterceptors = interceptorsAny && interceptorsAny.length > 0;
if (hasGateways || hasInterceptors) {
new AgentCoreMcp(this, 'Mcp', {
projectName: spec.name,
mcpSpec,
agentCoreApplication: this.application,
credentials,
projectTags: spec.tags,
});
}
}

// Stack-level output
Expand Down
4 changes: 2 additions & 2 deletions src/assets/cdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
"typescript": "~5.9.3"
},
"dependencies": {
"@aws/agentcore-cdk": "^0.1.0-alpha.19",
"aws-cdk-lib": "^2.248.0",
"@aws/agentcore-cdk": "^0.1.0-alpha.29",
"aws-cdk-lib": "^2.252.0",
"constructs": "^10.0.0"
}
}
Loading
Loading