Build a Tenant-Safe Incident Copilot
Build a tenant-safe incident copilot with Amazon Bedrock AgentCore.
Introduction
30 Second Summary
During a major outage, teams need answers quickly. A rushed answer can become a data leak when one customer's incident evidence slips into another customer's conversation.
In this project, you will build and deploy a tenant-safe incident copilot on Amazon Bedrock AgentCore. The finished system keeps evidence, memory, and session ownership scoped to the correct tenant.
What You'll Build
You'll ask the live copilot about INC-100 as two different tenants and watch each tenant receive a different incident record from the same deployed system.
By the end of this project, you'll have:
- An evidence-based triage flow where you submit an incident ID and receive the correct tenant's operational evidence from a deterministic Go service using MCP.
- A tenant-scoped conversation that restores the prior turn when you reuse the same session through AgentCore Memory.
- An inspectable trust path defined with AWS CDK that shows a cross-tenant session attack ending in a traceable rejection.
- Secret Mission: Audit every execution role and map where JWT authentication plus Cedar Policy belong in the next production increment.
Are there any prerequisites?
You'll need the confirmed Ubuntu toolchain from the project prerequisites. This includes Docker, AWS CLI, Node.js, npm, uv, Git, GitHub, and an editor.
Your configured AWS sandbox also needs Amazon Bedrock model access plus permission to deploy IAM, CloudFormation, ECR, AgentCore, and Amazon CloudWatch resources.
Before We Start
Before the hands-on work begins, commit to who needs this evidence-driven incident copilot and why tenant isolation must protect incident records, conversation events, and execution sessions.
Set Up the AgentCore Architecture Workspace
The incident copilot spans containers, managed services, identities, and trust boundaries. A reproducible workspace makes every part of that architecture explicit before your sandbox receives any resources.
In this step, you will verify your Ubuntu toolchain. You will also assemble the Amazon Bedrock AgentCore repository and synthesize its AWS Cloud Development Kit deployment graph for us-west-2.
In this step, get ready to:
- Validate the local tooling and AWS sandbox identity.
- Create the repository from the supplied architecture files.
- Bootstrap the sandbox and synthesize the deployment template.
Validate the confirmed Ubuntu toolchain
This workspace depends on Docker Buildx for ARM64 container assets. Node.js drives the infrastructure project while uv runs the Python utilities later.
- Check that Docker Buildx, Node.js, npm, AWS CDK, uv, and Git are available by running these commands:
docker buildx version
node --version
npm --version
cdk --version
uv --version
git --version
What do these checks prove?
- Docker Buildx confirms that your machine can prepare the ARM64 assets required by AgentCore Runtime.
- Node.js and npm provide the runtime and package manager for the CDK project.
- AWS CDK turns the TypeScript stack into an AWS CloudFormation template.
- uv and Git support the invocation utility and repository workflow.
✔️ Every command returns a version
Your local toolchain is ready. Continue with the sandbox identity check.
ⓧ One command is not found
Install the missing tool from its official guide. Return to the version checks after the installation finishes.
- Use the Docker installation guide when Docker Buildx is unavailable.
- Use the Node.js download guide when Node.js or npm is unavailable.
- Use the AWS CDK setup guide when the CDK command is unavailable.
- Use the uv installation guide when uv is unavailable.
- Use the Git installation guide when Git is unavailable.
Your AWS CLI identity determines which account receives the deployment. The returned account and role must belong to your sandbox.
- Reveal the identity used by your configured AWS CLI by running this command:
aws sts get-caller-identity
What does this command return?
The response identifies the current AWS account and calling principal. This proves which sandbox your later CDK commands target.
- Compare the returned account with the account assigned in your sandbox instructions.
- Confirm that the returned principal is an approved sandbox role.
- Confirm that the sandbox permits IAM roles and service-linked roles.
- Confirm that the sandbox permits CloudFormation, Amazon ECR, AgentCore Runtime, Gateway, Memory, and Amazon CloudWatch resources in us-west-2.
AWS identity unavailable?
Refresh the authentication method provided with your sandbox. Run the identity command again after the session is active.
If the account or role is unfamiliar, stop before bootstrapping. Help me verify the AWS identity configured for this AgentCore sandbox.
Create the repository from the supplied source files
The repository keeps infrastructure, orchestration, deterministic tool execution, invocation, and architecture documentation in separate locations. That separation makes each execution role and trust boundary easier to audit.
- Move to your Desktop by running this command:
cd ~/Desktop
Why start on the Desktop?
A fixed starting location keeps the repository easy to find during later terminal steps.
- Create the repository directories by running these commands:
mkdir -p agentcore-incident-copilot/{bin,lib,services/agent,services/tool,scripts,docs}
cd agentcore-incident-copilot
How is the workspace divided?
- The bin and lib directories hold the CDK entry point and stack.
- The services/agent and services/tool directories hold separate Runtime container definitions.
- The scripts directory holds the deployed Runtime invocation utility.
- The docs directory records architecture decisions and trust boundaries.
- Confirm the six directories exist by running this command:
ls bin lib services/agent services/tool scripts docs
What should you see?
The output lists each named directory. Your repository now has a place for every supplied artifact.
Creating the source set is the fiddliest part of this step because several files live in nested directories. Use the paths below as your checklist.
- Create each file shown in the double-check tab with your editor's file explorer.
- Paste the matching supplied content into each file.
- Save every file inside agentcore-incident-copilot.
✔️ Awesome, I've got everything!
Great. Confirm every file is saved before installing the dependencies.
ⓧ I'd like to double check the full code
- Compare every file path and its contents with the references below.
Use this content for package.json.
{
"name": "agentcore-incident-copilot",
"version": "1.0.0",
"private": true,
"scripts": {
"synth": "cdk synth",
"deploy": "cdk deploy --require-approval=never --outputs-file outputs.json",
"destroy": "cdk destroy"
},
"dependencies": {
"aws-cdk-lib": "2.274.0",
"constructs": "10.8.1"
},
"devDependencies": {
"@types/node": "26.6.5",
"aws-cdk": "2.1146.1",
"ts-node": "10.9.2",
"typescript": "5.9.3"
}
}
What does this file control?
This manifest pins the CDK libraries and TypeScript tools. Its scripts provide one command for each infrastructure lifecycle action.
Use this content for tsconfig.json.
{
"compilerOptions": {
"target": "ES2022",
"module": "CommonJS",
"moduleResolution": "node",
"lib": ["ES2022"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "dist",
"types": ["node"]
},
"include": ["bin/**/*.ts", "lib/**/*.ts"]
}
What does the compiler check?
This configuration applies strict TypeScript checks to the CDK entry point and stack source.
Use this content for cdk.json.
{
"app": "npx ts-node --preferTsExts bin/app.ts",
"outputsFile": "outputs.json"
}
How does CDK start the app?
This configuration points CDK at the TypeScript entry point. It also names the deployment output file used by the invocation utility.
Use this content for bin/app.ts.
#!/usr/bin/env node
import { App } from "aws-cdk-lib";
import { AgentCoreStack } from "../lib/agentcore-stack";
const app = new App();
new AgentCoreStack(app, "AgentCoreMasteryStack", {
env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: "us-west-2" },
});
What does the entry point establish?
The entry point creates AgentCoreMasteryStack in the current CDK account. It fixes the target Region as us-west-2.
Use this content for lib/agentcore-stack.ts.
import * as path from "path";
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as agentcore from "aws-cdk-lib/aws-bedrockagentcore";
import * as ecrAssets from "aws-cdk-lib/aws-ecr-assets";
import * as iam from "aws-cdk-lib/aws-iam";
const ACTOR_HEADER = "X-Amzn-Bedrock-AgentCore-Runtime-Custom-Actor-Id";
const MODEL_ID = "global.anthropic.claude-haiku-4-5-20251001-v1:0";
export class AgentCoreStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const memory = new agentcore.CfnMemory(this, "IncidentMemory", {
name: "incident_copilot_memory",
description: "Seven-day short-term event memory for the incident copilot",
eventExpiryDuration: 7,
});
memory.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY);
const toolArtifact = agentcore.AgentRuntimeArtifact.fromAsset(
path.join(__dirname, "../services/tool"),
{ platform: ecrAssets.Platform.LINUX_ARM64 },
);
const toolRuntime = new agentcore.Runtime(this, "ToolRuntime", {
runtimeName: "IncidentToolRuntime",
description: "Tenant-scoped Go MCP incident evidence service",
agentRuntimeArtifact: toolArtifact,
networkConfiguration: agentcore.RuntimeNetworkConfiguration.usingPublicNetwork(),
protocolConfiguration: agentcore.ProtocolType.MCP,
platformVersion: agentcore.PlatformVersion.V2,
requestHeaderConfiguration: { allowlistedHeaders: [ACTOR_HEADER] },
tracingEnabled: true,
});
const gateway = new agentcore.Gateway(this, "IncidentGateway", {
gatewayName: "incident-copilot-gateway",
description: "IAM-authenticated MCP gateway for incident tools",
});
const cfnGateway = gateway.node.defaultChild as agentcore.CfnGateway;
cfnGateway.authorizerType = "AWS_IAM";
cfnGateway.protocolType = "MCP";
cfnGateway.protocolConfiguration = {
mcp: {
supportedVersions: ["2025-11-25"],
streamingConfiguration: { enableResponseStreaming: true },
},
};
gateway.role.addToPrincipalPolicy(new iam.PolicyStatement({
actions: ["bedrock-agentcore:InvokeAgentRuntime"],
resources: [toolRuntime.agentRuntimeArn, `${toolRuntime.agentRuntimeArn}/*`],
}));
gateway.role.addToPrincipalPolicy(new iam.PolicyStatement({
actions: ["bedrock-agentcore:InvokeGateway"],
resources: [gateway.gatewayArn],
}));
const encodedToolRuntimeArn = cdk.Fn.join(
"%2F",
cdk.Fn.split("/", cdk.Fn.join("%3A", cdk.Fn.split(":", toolRuntime.agentRuntimeArn))),
);
const toolEndpoint = cdk.Fn.join("", [
`https://bedrock-agentcore.${this.region}.amazonaws.com/runtimes/`,
encodedToolRuntimeArn,
"/invocations?qualifier=DEFAULT",
]);
const toolTarget = new agentcore.CfnGatewayTarget(this, "IncidentToolTarget", {
gatewayIdentifier: cfnGateway.attrGatewayIdentifier,
name: "incident-tools",
description: "Go MCP incident tools hosted on AgentCore Runtime",
targetConfiguration: {
mcp: { mcpServer: { endpoint: toolEndpoint, listingMode: "DEFAULT" } },
},
credentialProviderConfigurations: [{
credentialProviderType: "GATEWAY_IAM_ROLE",
credentialProvider: {
iamCredentialProvider: { service: "bedrock-agentcore", region: this.region },
},
}],
metadataConfiguration: { allowedRequestHeaders: [ACTOR_HEADER] },
});
toolTarget.node.addDependency(gateway);
toolTarget.node.addDependency(toolRuntime);
const gatewayRolePolicy = gateway.role.node.findChild("DefaultPolicy")
.node.defaultChild as cdk.CfnResource;
toolTarget.node.addDependency(gatewayRolePolicy);
const agentArtifact = agentcore.AgentRuntimeArtifact.fromAsset(
path.join(__dirname, "../services/agent"),
{ platform: ecrAssets.Platform.LINUX_ARM64 },
);
const agentRuntime = new agentcore.Runtime(this, "AgentRuntime", {
runtimeName: "IncidentCopilotRuntime",
description: "Strands incident copilot with Gateway and Memory",
agentRuntimeArtifact: agentArtifact,
networkConfiguration: agentcore.RuntimeNetworkConfiguration.usingPublicNetwork(),
protocolConfiguration: agentcore.ProtocolType.HTTP,
platformVersion: agentcore.PlatformVersion.V2,
tracingEnabled: true,
environmentVariables: {
AWS_REGION: this.region,
AWS_DEFAULT_REGION: this.region,
MODEL_ID,
GATEWAY_URL: cfnGateway.attrGatewayUrl,
MEMORY_ID: memory.attrMemoryId,
ACTOR_HEADER,
ENFORCE_SESSION_BINDING: "true",
UNIFIED_TRACES_DESTINATION_ENABLED: "true",
},
});
agentRuntime.node.addDependency(toolTarget);
agentRuntime.role.addToPrincipalPolicy(new iam.PolicyStatement({
actions: ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"],
resources: ["*"],
}));
agentRuntime.role.addToPrincipalPolicy(new iam.PolicyStatement({
actions: [
"bedrock-agentcore:CreateEvent",
"bedrock-agentcore:GetEvent",
"bedrock-agentcore:ListEvents",
"bedrock-agentcore:GetMemory",
],
resources: [memory.attrMemoryArn],
}));
agentRuntime.role.addToPrincipalPolicy(new iam.PolicyStatement({
actions: ["bedrock-agentcore:InvokeGateway"],
resources: [gateway.gatewayArn],
}));
new cdk.CfnOutput(this, "AgentRuntimeArn", { value: agentRuntime.agentRuntimeArn });
new cdk.CfnOutput(this, "ToolRuntimeArn", { value: toolRuntime.agentRuntimeArn });
new cdk.CfnOutput(this, "GatewayUrl", { value: cfnGateway.attrGatewayUrl });
new cdk.CfnOutput(this, "MemoryId", { value: memory.attrMemoryId });
}
}
What architecture does this stack describe?
The stack defines short-term Memory, separate HTTP and MCP Runtime resources, an IAM-authenticated Gateway, and an MCP target. Explicit dependencies preserve the deployment order.
Both container assets use ARM64. The trusted actor header is allowlisted through the Gateway target and the tool Runtime boundary.
Use this content for services/agent/pyproject.toml.
[project]
name = "incident-copilot-agent"
version = "1.0.0"
requires-python = ">=3.11,<3.12"
dependencies = [
"aws-opentelemetry-distro==0.21.0",
"bedrock-agentcore==1.25.0",
"boto3==1.43.118",
"mcp==1.30.0",
"mcp-proxy-for-aws==1.7.0",
"strands-agents==1.62.0"
]
Why pin the Python dependencies?
The pins keep the Runtime contract, Strands integration, MCP client, AWS SDK, and telemetry behavior reproducible.
Use this content for services/agent/Dockerfile.
FROM --platform=linux/arm64 ghcr.io/astral-sh/uv:python3.11-bookworm-slim
WORKDIR /app
COPY pyproject.toml ./
RUN uv sync
COPY app.py ./
ENV PATH="/app/.venv/bin:${PATH}"
EXPOSE 8080
USER 10001:10001
CMD ["opentelemetry-instrument", "python", "app.py"]
What contract does the agent container prepare?
The container targets ARM64 and exposes port 8080 for the AgentCore HTTP contract. It runs the Python app as a non-root user with telemetry instrumentation.
Use this content for services/tool/go.mod.
module incidenttool
go 1.25.0
require github.com/modelcontextprotocol/go-sdk v1.8.0
What does the Go module pin?
This module fixes the MCP SDK used by the deterministic incident evidence service.
Use this content for services/tool/Dockerfile.
FROM --platform=$BUILDPLATFORM golang:1.27.3-bookworm AS build
ARG TARGETOS
ARG TARGETARCH
WORKDIR /src
COPY go.mod ./
RUN go mod download
COPY main.go ./
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -trimpath -o /out/incident-tool .
FROM debian:bookworm-slim
COPY --from=build --chown=65532:65532 /out/incident-tool /incident-tool
EXPOSE 8000
USER 65532:65532
ENTRYPOINT ["/incident-tool"]
How is the tool container isolated?
The build stage compiles the Go service for the target platform. The runtime stage contains the resulting binary and runs it as a non-root user on port 8000.
Use this content for scripts/invoke.py.
import argparse
import json
import uuid
from pathlib import Path
import boto3
def stack_outputs(path: str) -> dict[str, str]:
data = json.loads(Path(path).read_text())
return data["AgentCoreMasteryStack"]
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--user", required=True)
parser.add_argument("--prompt", required=True)
parser.add_argument("--session", default=str(uuid.uuid4()))
parser.add_argument("--outputs", default="outputs.json")
args = parser.parse_args()
outputs = stack_outputs(args.outputs)
client = boto3.client("bedrock-agentcore", region_name="us-west-2")
print(f"session_id={args.session}")
try:
response = client.invoke_agent_runtime(
agentRuntimeArn=outputs["AgentRuntimeArn"],
qualifier="DEFAULT",
runtimeSessionId=args.session,
runtimeUserId=args.user,
contentType="application/json",
accept="application/json",
payload=json.dumps({"prompt": args.prompt}).encode(),
)
print(response["response"].read().decode())
except Exception as error:
print(f"invocation_failed={error}")
raise
if __name__ == "__main__":
main()
What does the invocation utility preserve?
The utility reads the deployed Runtime ARN from outputs.json. It sends the user ID, session ID, and JSON prompt as separate request fields.
Use this content for docs/architecture.md.
# Incident Copilot Architecture Decisions
## Quality attributes
- Tenant isolation is more important than tool-call convenience.
- Incident evidence must come from a deterministic system, not the model prompt.
- Every network hop must have an explicit identity and observable session key.
- Python owns orchestration. Go owns deterministic tool execution.
- Infrastructure must be reproducible through CDK and removable as one stack.
## Component map
```mermaid
flowchart LR
Caller[Trusted IAM caller] -->|runtimeUserId + runtimeSessionId| Agent[AgentCore HTTP Runtime]
Agent -->|SigV4 + trusted actor header| Gateway[AgentCore Gateway]
Gateway -->|SigV4 + allowlisted actor header| Tool[AgentCore MCP Runtime]
Tool --> Evidence[(Tenant incident map)]
Agent --> Memory[AgentCore Memory]
Agent --> Bedrock[Amazon Bedrock model]
Agent --> Telemetry[CloudWatch and AgentCore Observability]
```
## Trust boundaries
| Boundary | Trusted identity | Protected value | Enforcement |
| --- | --- | --- | --- |
| Caller to agent Runtime | IAM principal with InvokeAgentRuntimeForUser | runtimeUserId | Upstream caller plus IAM |
| Runtime session | First actor seen in the microVM | runtimeSessionId ownership | Application binding guard |
| Agent to Gateway | Agent Runtime execution role | Gateway invocation | SigV4 and IAM |
| Gateway to Go tool | Gateway execution role | Tool Runtime invocation | SigV4 and IAM |
| Gateway to tool container | Allowlisted actor header | Tenant evidence scope | GatewayTarget and Runtime header allowlists |
| Agent to Memory | Agent Runtime execution role | actorId plus sessionId events | IAM and Memory keys |
## Decisions
1. Use BedrockAgentCoreApp rather than raw FastAPI because it owns the Runtime HTTP contract, health endpoint, request context, and streaming behavior.
2. Use a native MCP Gateway target rather than a generic REST proxy so discovery and invocation remain MCP concepts.
3. Use two Runtime resources so orchestration and deterministic tools have separate roles, containers, protocols, and scaling boundaries.
4. Use runtimeUserId only as a trusted-backend development bridge. Replace it with JWT inbound authentication when end users call directly.
5. Store only short-term raw events here. Add long-term strategies only with explicit namespace and retention decisions.
6. Use Runtime V2, but keep actor binding state empty at startup so every restored microVM begins unbound.
7. Permit Bedrock model invocation on `*` in this sandbox because a global inference profile can route to multiple foundation-model resources. Replace this with the organization's approved inference-profile policy in production.
## Deliberate obstacle
With session binding disabled, the platform accepts one runtimeSessionId from two runtimeUserId values. Memory separates actor IDs and the Go tool scopes data by the propagated actor header, but the microVM context is still shared. The final control binds the first actor to the microVM and rejects conflicts.
## Follow-on mastery path
- Replace runtimeUserId delegation with Cognito or enterprise OIDC.
- Put Cedar Policy in ENFORCE mode at Gateway and add Memory fine-grained access control.
- Add AgentCore Browser and Code Interpreter behind approval boundaries.
- Add long-term Memory strategies, deletion workflows, and namespace tests.
- Add Evaluations, adversarial datasets, GitHub Actions, named Runtime endpoints, alarms, and rollback.
Why keep an architecture document?
The document records identity provenance, protected values, and enforcement points. It gives you a reference for evaluating the session-binding obstacle later.
The source files pin the infrastructure toolchain and describe each deployment boundary. The next command resolves those npm dependencies locally.
- Install the project dependencies by running this command:
npm install
What does the installation add?
npm resolves the versions declared in package.json. It creates the local dependency tree used for synthesis.
- Confirm that node_modules now exists in agentcore-incident-copilot.
- Confirm that package-lock.json now exists beside package.json.
- Check the installed local AWS CDK version by running this command:
npx aws-cdk --version
Why check the local CDK version?
The local CLI comes from this repository's dependency tree. Checking it confirms that synthesis uses the pinned project tool instead of an unrelated global installation.
✔️ I see version 2.1146.1
The repository is using the expected AWS CDK CLI version. Continue to bootstrap the sandbox.
ⓧ I see another version
The local dependency tree does not match the supplied manifest. Confirm that package.json pins 2.1146.1.
- Resolve the pinned dependency again by running this command:
npm install
What does this retry use?
The command resolves the dependency versions declared by this repository.
ⓧ Command not found
The local npm executables are unavailable. Confirm that the earlier Node.js and npm checks succeed.
Use the official Node.js download guide if either command is missing. Return to npm install after Node.js is ready.
Dependency installation failed?
Check that package.json is saved in agentcore-incident-copilot. Confirm that your network can reach the npm registry.
Help me diagnose the npm installation in this CDK repository.
Bootstrap and synthesize before deploying
CDK bootstrapping prepares shared deployment support for the target AWS environment. Synthesis then converts the TypeScript graph into a reviewable CloudFormation template without deploying the application stack.
This is the first command that changes your AWS sandbox. Confirm that the account from the identity check is correct before continuing.
- Bootstrap the current account in us-west-2 by running this command:
npx cdk bootstrap
What does bootstrapping prepare?
CDK creates or updates the shared support used to stage deployment assets. Reusing that support keeps later stack deployments reproducible.
Bootstrapping can take a few minutes while CloudFormation prepares the environment. A quiet terminal during that work does not mean the command has stalled.
- Wait for the bootstrap command to report successful completion.
Bootstrap rejected?
Confirm that the active sandbox role can create CloudFormation resources and IAM roles. Confirm that the stack entry point still targets us-west-2.
Help me troubleshoot CDK bootstrap in my approved AWS sandbox.
Before you synthesize, predict what the deployment graph should contain. How many Runtime resources do you expect to find?
- Synthesize the AgentCoreMasteryStack template by running this command:
npm run synth
What does synthesis verify?
- TypeScript compiles the stack and validates the CDK construct graph.
- CDK emits the CloudFormation template for AgentCoreMasteryStack.
- The generated graph exposes resources and execution-role policies before deployment.
- Review the synthesized template printed by the command.
- Confirm that the template contains two Runtime resources.
- Confirm that the template contains one Gateway resource.
- Confirm that the template contains one GatewayTarget resource.
- Confirm that the template contains one Memory resource.
- Confirm that the template includes IAM policies for the agent Runtime and Gateway execution paths.
- Return to lib/agentcore-stack.ts in your editor.
- Confirm that both container assets use LINUX_ARM64.
Where does the trusted header travel?
The Gateway target forwards the custom actor header through its request-header allowlist. The tool Runtime independently allowlists that header at its container boundary.
The agent Runtime passes tenant identity into that trusted path. The model never chooses the tenant identifier.
That is the workspace ready. Your source now produces a deployment graph with isolated Runtime roles, an IAM-authenticated Gateway path, short-term Memory, and ARM64 container assets.
Your AgentCore architecture is reproducible before its first deployment. Next, you will run the local reasoning loop and see exactly what a model lacks without Gateway tools or Memory.
Run the Local Reasoning Loop
Your AWS CDK stack now synthesizes the complete deployment graph. That is a meaningful milestone because every planned resource is visible in one reviewable template.
The copilot still needs a reality check before deployment. A model from Amazon Bedrock can reason about an incident request, but it has no deterministic evidence from the Gateway or MCP tool. It also cannot reconstruct an earlier conversation without AgentCore Memory.
In this step, get ready to:
- Start the local AgentCore HTTP contract on port 8080.
- Invoke the copilot without Gateway tools or Memory.
- Record why the model-only result is an architecture limitation.
Create and start the local app
The BedrockAgentCoreApp class supplies the Runtime HTTP contract. Your application only defines the reasoning entrypoint that runs behind /invocations and the health check at /ping.
- Create services/agent/app.py in your editor by copying the supplied file from the double-check tab below:
✔️ Awesome, I've got everything!
Your app.py file now contains the complete local reasoning loop. Save the file before continuing.
ⓧ I'd like to double check the full code
import os
import threading
from contextlib import ExitStack
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from bedrock_agentcore.memory.integrations.strands.config import AgentCoreMemoryConfig
from bedrock_agentcore.memory.integrations.strands.session_manager import AgentCoreMemorySessionManager
from mcp_proxy_for_aws.client import aws_iam_streamablehttp_client
from strands import Agent
from strands.models import BedrockModel
from strands.tools.mcp import MCPClient
app = BedrockAgentCoreApp()
REGION = os.environ.get("AWS_REGION", "us-west-2")
MODEL_ID = os.environ["MODEL_ID"]
ACTOR_HEADER = os.environ.get(
"ACTOR_HEADER", "X-Amzn-Bedrock-AgentCore-Runtime-Custom-Actor-Id"
)
ENFORCE_SESSION_BINDING = (
os.environ.get("ENFORCE_SESSION_BINDING", "true").lower() == "true"
)
model = BedrockModel(model_id=MODEL_ID, temperature=0.2, streaming=False)
_session_owner: str | None = None
_session_owner_lock = threading.Lock()
def require_actor_id(context) -> str:
actor_id = context.request.headers.get("x-amzn-bedrock-agentcore-runtime-user-id")
if not actor_id:
raise ValueError("runtime user id is required")
return actor_id
def bind_session(actor_id: str) -> None:
global _session_owner
if not ENFORCE_SESSION_BINDING:
return
with _session_owner_lock:
if _session_owner is None:
_session_owner = actor_id
elif _session_owner != actor_id:
raise PermissionError("session is already bound to another actor")
def memory_manager(actor_id: str, session_id: str):
memory_id = os.environ.get("MEMORY_ID")
if not memory_id:
return None
config = AgentCoreMemoryConfig(
memory_id=memory_id, actor_id=actor_id, session_id=session_id
)
return AgentCoreMemorySessionManager(
agentcore_memory_config=config, region_name=REGION
)
def gateway_factory(actor_id: str):
gateway_url = os.environ.get("GATEWAY_URL")
if not gateway_url:
return None
return lambda: aws_iam_streamablehttp_client(
endpoint=gateway_url,
aws_region=REGION,
aws_service="bedrock-agentcore",
headers={ACTOR_HEADER: actor_id},
)
@app.entrypoint
def invoke(payload, context):
prompt = payload.get("prompt")
if not isinstance(prompt, str) or not prompt.strip():
raise ValueError("prompt must be a non-empty string")
if not context.session_id:
raise ValueError("runtime session id is required")
actor_id = require_actor_id(context)
bind_session(actor_id)
session_manager = memory_manager(actor_id, context.session_id)
factory = gateway_factory(actor_id)
with ExitStack() as stack:
if session_manager is not None:
session_manager = stack.enter_context(session_manager)
tools = []
if factory is not None:
mcp_client = stack.enter_context(MCPClient(factory))
tools = mcp_client.list_tools_sync()
agent_kwargs = {}
if session_manager is not None:
agent_kwargs["session_manager"] = session_manager
agent = Agent(
model=model,
tools=tools,
system_prompt=(
"You are an evidence-driven DevOps incident copilot. "
"For an incident ID, use the available incident tool before diagnosing. "
"Never invent telemetry. If no tool is available, state that the evidence "
"path is unavailable and list the evidence you require."
),
**agent_kwargs,
)
response = agent(prompt)
return {
"actor_id": actor_id,
"session_id": context.session_id,
"tool_count": len(tools),
"response": response.message["content"][0]["text"],
}
if __name__ == "__main__":
app.run()
How does this app create the local gap?
- The Strands agent uses the configured Bedrock model for reasoning.
- The memory_manager() function returns None when MEMORY_ID is absent.
- The gateway_factory() function returns None when GATEWAY_URL is absent.
- The system prompt tells the model to report the missing evidence path instead of inventing telemetry.
- Save services/agent/app.py.
- Switch back to the terminal at the top level of agentcore-incident-copilot.
- Move into services/agent by running this command:
cd services/agent
What does this command do?
This moves your terminal into the folder containing app.py and pyproject.toml. The next commands use that Python project definition.
The pinned packages in pyproject.toml keep the local environment aligned with the future container. Install them before starting the app.
- Install the agent dependencies by running this command:
uv sync
What does this command do?
The command resolves the pinned packages from pyproject.toml and prepares the local virtual environment. The same dependency set powers the agent container.
When the command finishes, the dependency activity stops and your shell prompt returns. Your local environment is ready to run the supplied app.
Dependencies not resolving?
Confirm the terminal is inside services/agent. The folder must contain pyproject.toml.
Check that your AWS sandbox credentials remain available if package startup later reaches the Bedrock model. Help me diagnose a failed uv sync for this AgentCore Python project.
This local run deliberately omits GATEWAY_URL and MEMORY_ID. It also disables the session-binding guard so you can observe the model-only baseline.
- Start the local app with the required environment by running these commands:
export AWS_REGION=us-west-2
export MODEL_ID=global.anthropic.claude-haiku-4-5-20251001-v1:0
export ENFORCE_SESSION_BINDING=false
unset GATEWAY_URL MEMORY_ID
uv run app.py
What does this startup configuration do?
- The Region points model requests at us-west-2.
- The model ID selects the permitted global Bedrock inference profile.
- The disabled binding flag preserves the deliberately incomplete security posture for this baseline.
- The missing Gateway and Memory values keep tools and conversation reconstruction disconnected.
Keep this terminal running. BedrockAgentCoreApp now owns the local service contract on port 8080.
- Create a second terminal tab while the server continues running.
- Check the health endpoint from the second terminal by running this command:
curl http://localhost:8080/ping
What does this check prove?
The request reaches the health endpoint supplied by BedrockAgentCoreApp. A JSON health response proves the local HTTP contract is listening on port 8080.
The local contract is live. You can now test the reasoning entrypoint through the same /invocations path used by the Runtime container.
No health response?
Return to the server terminal and look for a startup failure. A missing MODEL_ID value or an unresolved dependency prevents the app from binding to port 8080.
Confirm the first terminal still has the app process running. Help me diagnose why the local AgentCore ping endpoint is unavailable.
Invoke the model-only copilot
The health check proves the server works. The next request asks the copilot to investigate INC-100 as tenant-alpha with a Runtime session ID longer than 33 characters.
Before you send the request, do you think the model can produce trustworthy incident evidence without a connected tool?
- Send the incident request from the second terminal by running this command:
curl -X POST http://localhost:8080/invocations \
-H "Content-Type: application/json" \
-H "X-Amzn-Bedrock-AgentCore-Runtime-Session-Id: tenant-alpha-local-session-000000000001" \
-H "X-Amzn-Bedrock-AgentCore-Runtime-User-Id: tenant-alpha" \
-d '{"prompt":"Investigate incident INC-100. Use operational evidence and report the likely cause."}'
What does this request test?
- The session header supplies a valid local Runtime session identifier.
- The user header identifies the caller as tenant-alpha.
- The JSON payload asks the entrypoint to investigate INC-100.
- The request reaches the real Bedrock model without adding incident evidence to the prompt.
What should I see?
The JSON response contains "actor_id": "tenant-alpha" and the session ID you supplied. It also contains "tool_count": 0 because no Gateway URL is available for tool discovery.
The model response reports that the evidence path is unavailable. Its exact wording can vary, but it should request operational evidence instead of inventing a cause for INC-100.
You now have a working reasoning loop with a visible limitation. The model can respond, but it cannot support an evidence-based diagnosis yet.
Invocation not returning a model response?
Confirm your sandbox permits access to the configured model. The server terminal should show whether the request reached Bedrock.
Check that the session value remains longer than 33 characters. Help me diagnose the failed local incident invocation.
Record the architecture limitation
A successful model response does not make this an incident copilot. The local result has no deterministic incident evidence or reconstructed conversation history.
Why not put the incident data in the prompt?
Prompt-stuffing would make the response appear more informed while bypassing the missing evidence boundary. It would conceal the need for Gateway discovery, an MCP tool, trusted identity propagation, and tenant-scoped data access.
It would also conceal the missing Memory and session-isolation controls. The visible gap gives every later resource a concrete security purpose.
- Return to docs/architecture.md in your editor.
- Under ## Decisions, add one concise note that records both findings from the callout above.
- Use your editor's Markdown preview to confirm the note mentions the model-only evidence gap and the controls that prompt-stuffing would conceal.
You should see the new local limitation beside the architecture decisions. The document now connects the failed evidence path to Gateway, MCP, trusted headers, Memory, and isolation.
Your baseline has done its job. Next, you will deploy the managed AgentCore architecture that gives this reasoning loop deterministic tools and tenant-scoped state.
Deploy the AgentCore Architecture
The local Amazon Bedrock AgentCore reasoning loop can reach a model. It also proved that model access alone cannot supply trustworthy incident evidence.
Reproducible production infrastructure makes every trust boundary explicit. This step uses AWS CDK to deploy two isolated Runtime V2 containers plus the managed services that connect them.
In this step, get ready to:
- Build the tenant-scoped Go incident evidence service.
- Confirm the Runtime, Gateway, Memory, identity propagation, and role boundaries in the CDK stack.
- Deploy AgentCoreMasteryStack with session binding deliberately disabled.
Build the tenant-scoped evidence boundary
The Go service implements a read-only MCP tool. Tenant identity comes from a trusted request header supplied by the infrastructure path.
The tool accepts only an incident ID from the model. It uses the propagated actor identity to select the correct tenant record.
- Use your editor's file explorer to create services/tool/main.go inside the existing services/tool directory.
- Add the package declaration plus the required imports by pasting this block:
package main
import (
"context"
"errors"
"fmt"
"log"
"net/http"
"time"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
What do these imports support?
- The request packages provide the context plus HTTP types used by the MCP handler.
- The error packages produce explicit failures when trusted tenant data is unavailable.
- The logging plus timing packages support the managed HTTP server.
- The MCP SDK supplies the server, tool, request, and streamable HTTP APIs.
- Save services/tool/main.go.
- Confirm that main.go now appears under services/tool in your editor's file explorer.
Can't find the new Go file?
Check that main.go sits beside go.mod plus Dockerfile. A file created elsewhere will not enter the tool container build context.
Ask for help checking the file location.
The data model keeps tenant identity outside IncidentInput. The model can request INC-100, while the trusted actor header decides which record is accessible.
- Extend main.go with the actor header plus deterministic tenant records by pasting this below the import block:
const actorHeader = "X-Amzn-Bedrock-AgentCore-Runtime-Custom-Actor-Id"
type IncidentInput struct {
IncidentID string `json:"incident_id" jsonschema:"Incident identifier such as INC-100"`
}
type Incident struct {
IncidentID string `json:"incident_id"`
Service string `json:"service"`
Severity string `json:"severity"`
Signal string `json:"signal"`
Change string `json:"recent_change"`
}
var incidents = map[string]map[string]Incident{
"tenant-alpha": {"INC-100": {
IncidentID: "INC-100", Service: "payments-api", Severity: "SEV-1",
Signal: "HTTP 5xx rate rose from 0.3% to 18% after 14:05 UTC",
Change: "payments-api release 2026.10.25.3 at 14:02 UTC",
}},
"tenant-beta": {"INC-100": {
IncidentID: "INC-100", Service: "inventory-sync", Severity: "SEV-2",
Signal: "queue age exceeded 900 seconds in two warehouses",
Change: "warehouse routing rule update at 09:40 UTC",
}},
}
How is the evidence separated?
- The custom actor header carries the tenant identity resolved before tool execution.
- The outer map separates records by actor.
- Each tenant can use the same incident ID without accessing the other tenant's record.
- The fixed records make every later isolation test repeatable.
- Save services/tool/main.go.
- Press Cmd+F (macOS) or Ctrl+F (Windows and Ubuntu) to open your editor's search control.
- Search for tenant-beta.
You should find the inventory incident under tenant-beta. The payments incident remains under tenant-alpha.
Missing one tenant record?
Compare both map keys with tenant-alpha plus tenant-beta. Check that each nested record closes with the same braces plus commas shown above.
Ask for help checking the incident map.
The tool handler reads the actor identity from MCP request metadata. It rejects missing, empty, unknown, or unmatched tenant data before returning evidence.
- Add the getIncident handler below the incident map by pasting this block:
func getIncident(
ctx context.Context,
req *mcp.CallToolRequest,
input IncidentInput,
) (*mcp.CallToolResult, Incident, error) {
if req.Extra == nil || req.Extra.Header == nil {
return nil, Incident{}, errors.New("trusted actor header is missing")
}
actorID := req.Extra.Header.Get(actorHeader)
if actorID == "" {
return nil, Incident{}, errors.New("trusted actor header is empty")
}
tenantIncidents, ok := incidents[actorID]
if !ok {
return nil, Incident{}, fmt.Errorf("unknown actor %q", actorID)
}
incident, ok := tenantIncidents[input.IncidentID]
if !ok {
return nil, Incident{}, fmt.Errorf(
"incident %q was not found for actor %q", input.IncidentID, actorID,
)
}
return nil, incident, nil
}
What does this handler enforce?
- The handler reads the custom actor header from req.Extra.Header.
- The actor chooses the tenant map before the incident ID is resolved.
- Unknown actors receive an error without any incident record.
- A valid actor receives only evidence stored within that actor's map.
- Save services/tool/main.go.
- Search the file for req.Extra.Header.Get(actorHeader).
You should find one tenant lookup in getIncident. No model-provided field supplies the actor identity.
Can't find the trusted header lookup?
Check that the handler receives req *mcp.CallToolRequest. Confirm that the lookup uses actorHeader exactly.
Ask for help checking the handler signature.
The final function registers the read-only tool. It serves stateless streamable HTTP on the Runtime MCP contract.
- Complete main.go by adding this main function below getIncident:
func main() {
server := mcp.NewServer(
&mcp.Implementation{Name: "incident-evidence", Version: "1.0.0"},
&mcp.ServerOptions{SupportedProtocolVersions: []string{"2025-11-25"}},
)
mcp.AddTool(server, &mcp.Tool{
Name: "get_incident",
Description: "Retrieve read-only incident evidence for the authenticated actor",
Annotations: &mcp.ToolAnnotations{ReadOnlyHint: true, IdempotentHint: true},
}, getIncident)
handler := mcp.NewStreamableHTTPHandler(
func(*http.Request) *mcp.Server { return server },
&mcp.StreamableHTTPOptions{Stateless: true},
)
mux := http.NewServeMux()
mux.Handle("/mcp", handler)
mux.HandleFunc("/ping", func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `{"status":"Healthy"}`)
})
httpServer := &http.Server{
Addr: "0.0.0.0:8000", Handler: mux, ReadHeaderTimeout: 5 * time.Second,
}
log.Printf("incident MCP server listening on %s", httpServer.Addr)
log.Fatal(httpServer.ListenAndServe())
}
What does this server do?
- The server publishes get_incident as a read-only tool.
- Stateless streamable HTTP lets AgentCore manage platform sessions plus load balancing.
- The MCP endpoint listens at 0.0.0.0:8000/mcp.
- The health endpoint responds through /ping.
- Save services/tool/main.go.
- Confirm that the final line in the file is the closing brace for main.
Does the file end early?
Check that mux.Handle("/mcp", handler) appears before the health handler. Confirm that httpServer.ListenAndServe() remains inside main.
Ask for help checking the completed server.
✔️ Awesome, I've got everything!
Great. Double-check that you saved services/tool/main.go.
ⓧ I'd like to double check the full code
package main
import (
"context"
"errors"
"fmt"
"log"
"net/http"
"time"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
const actorHeader = "X-Amzn-Bedrock-AgentCore-Runtime-Custom-Actor-Id"
type IncidentInput struct {
IncidentID string `json:"incident_id" jsonschema:"Incident identifier such as INC-100"`
}
type Incident struct {
IncidentID string `json:"incident_id"`
Service string `json:"service"`
Severity string `json:"severity"`
Signal string `json:"signal"`
Change string `json:"recent_change"`
}
var incidents = map[string]map[string]Incident{
"tenant-alpha": {"INC-100": {
IncidentID: "INC-100", Service: "payments-api", Severity: "SEV-1",
Signal: "HTTP 5xx rate rose from 0.3% to 18% after 14:05 UTC",
Change: "payments-api release 2026.10.25.3 at 14:02 UTC",
}},
"tenant-beta": {"INC-100": {
IncidentID: "INC-100", Service: "inventory-sync", Severity: "SEV-2",
Signal: "queue age exceeded 900 seconds in two warehouses",
Change: "warehouse routing rule update at 09:40 UTC",
}},
}
func getIncident(
ctx context.Context,
req *mcp.CallToolRequest,
input IncidentInput,
) (*mcp.CallToolResult, Incident, error) {
if req.Extra == nil || req.Extra.Header == nil {
return nil, Incident{}, errors.New("trusted actor header is missing")
}
actorID := req.Extra.Header.Get(actorHeader)
if actorID == "" {
return nil, Incident{}, errors.New("trusted actor header is empty")
}
tenantIncidents, ok := incidents[actorID]
if !ok {
return nil, Incident{}, fmt.Errorf("unknown actor %q", actorID)
}
incident, ok := tenantIncidents[input.IncidentID]
if !ok {
return nil, Incident{}, fmt.Errorf(
"incident %q was not found for actor %q", input.IncidentID, actorID,
)
}
return nil, incident, nil
}
func main() {
server := mcp.NewServer(
&mcp.Implementation{Name: "incident-evidence", Version: "1.0.0"},
&mcp.ServerOptions{SupportedProtocolVersions: []string{"2025-11-25"}},
)
mcp.AddTool(server, &mcp.Tool{
Name: "get_incident",
Description: "Retrieve read-only incident evidence for the authenticated actor",
Annotations: &mcp.ToolAnnotations{ReadOnlyHint: true, IdempotentHint: true},
}, getIncident)
handler := mcp.NewStreamableHTTPHandler(
func(*http.Request) *mcp.Server { return server },
&mcp.StreamableHTTPOptions{Stateless: true},
)
mux := http.NewServeMux()
mux.Handle("/mcp", handler)
mux.HandleFunc("/ping", func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `{"status":"Healthy"}`)
})
httpServer := &http.Server{
Addr: "0.0.0.0:8000", Handler: mux, ReadHeaderTimeout: 5 * time.Second,
}
log.Printf("incident MCP server listening on %s", httpServer.Addr)
log.Fatal(httpServer.ListenAndServe())
}
The existing Docker definition compiles this service inside its builder stage. Your local machine does not need a separate Go installation.
- Return to the existing services/tool/Dockerfile.
- Confirm that the builder image uses golang:1.27.3-bookworm.
- Confirm that the runtime stage switches to USER 65532:65532.
- Confirm that the container exposes port 8000.
These settings package the deterministic server as a non-root ARM64 Runtime container. The deployment build provides the compile check.
Confirm the managed architecture
The existing stack separates orchestration from deterministic tool execution. AgentCore Gateway connects those boundaries through SigV4 authentication.
AgentCore Memory stores seven days of short-term events. Matching header allowlists carry the trusted actor identity through Gateway to the tool Runtime.
- Switch back to lib/agentcore-stack.ts in your editor.
- Locate IncidentMemory to confirm its event expiry is 7.
- Locate ToolRuntime to confirm its protocol is ProtocolType.MCP.
- Locate AgentRuntime to confirm its protocol is ProtocolType.HTTP.
- Confirm that both container assets target Platform.LINUX_ARM64.
- Confirm that both Runtime resources use PlatformVersion.V2.
Why use two Runtime resources?
The Python Runtime owns model orchestration, Gateway access, and Memory reconstruction. The Go Runtime owns deterministic incident retrieval.
Separate resources produce distinct protocols, roles, containers, and scaling boundaries. A compromised tool process cannot inherit the agent's orchestration logic.
- Find requestHeaderConfiguration on ToolRuntime.
- Confirm that its allowlist contains X-Amzn-Bedrock-AgentCore-Runtime-Custom-Actor-Id.
- Find metadataConfiguration on IncidentToolTarget.
- Confirm that its allowed request headers contain the same custom actor header.
The matching allowlists create one explicit identity propagation path. The model cannot choose the actor value through tool arguments.
This first deployment keeps session binding disabled for the planned security demonstration. The guarded version is introduced only after the unguarded behavior has been tested.
- Find the ENFORCE_SESSION_BINDING entry in the AgentRuntime environment variables.
- Set the entry to false so the line reads:
ENFORCE_SESSION_BINDING: "false",
Why disable binding here?
The initial Runtime version preserves the incomplete security posture required for the later adversarial test. Memory actor scoping plus trusted tool headers still remain active as independent controls.
- Save lib/agentcore-stack.ts.
- Switch back to the terminal where package.json is located.
- Synthesize the updated deployment graph by running:
npm run synth
What does this command check?
The script asks CDK to translate the TypeScript stack into a CloudFormation template. Synthesis checks the construct graph before any managed resource is created.
- Review the synthesis result in the terminal.
You should see a successful AgentCoreMasteryStack synthesis. The template includes two Runtime resources plus one Gateway, one GatewayTarget, and one Memory resource.
Did synthesis fail?
Check that main.go is inside services/tool. Check that the TypeScript environment line retains its surrounding comma.
Ask for help with the synthesis output.
Deploy and verify the stack
Before you deploy
This action creates usage-based AWS resources for Runtime, model inference, Gateway, Memory, telemetry, and container storage. Keep your test invocations focused.
The cleanup section removes the application stack when you finish. Shared CDK bootstrap assets are reviewed separately.
This deployment can take several minutes because CDK builds plus publishes two container assets. A quiet terminal during an image layer upload can be normal.
Before you run the deployment, do you expect the stack to expose one execution boundary or separate agent and tool boundaries?
- Deploy AgentCoreMasteryStack by running:
npm run deploy
What does this command do?
- CDK builds the Python HTTP Runtime container.
- CDK builds the Go MCP Runtime container.
- CDK publishes both ARM64 assets for deployment.
- CDK creates the managed resource graph without an approval prompt.
- CDK writes the stack outputs to outputs.json.
You should see AgentCoreMasteryStack reach CREATE_COMPLETE. That is the infrastructure boundary established: separate agent plus tool Runtimes are now connected through the managed Gateway.
Did the deployment stop?
- Confirm that your active AWS credentials still target the sandbox account verified earlier.
- Confirm that Docker remains available for both ARM64 asset builds.
- Confirm that the deployment Region remains us-west-2.
Ask for help with the failed deployment.
- Select the generated outputs.json file in your editor.
- Confirm that AgentCoreMasteryStack contains AgentRuntimeArn.
- Confirm that it contains ToolRuntimeArn.
- Confirm that it contains GatewayUrl.
- Confirm that it contains MemoryId.
Those four values prove that the application Runtime, tool Runtime, Gateway, and Memory resource were all resolved during deployment.
- Open AWS CloudFormation in the AWS Management Console.
- Set the console Region to us-west-2.
- Select AgentCoreMasteryStack from the stack list.
- Confirm that the stack status is CREATE_COMPLETE.
The completed stack confirms that the CDK dependency graph created each managed boundary in the required order.
How are the execution roles separated?
- The agent Runtime role can invoke the configured Bedrock inference profile.
- The agent Runtime role can create plus retrieve short-term Memory events.
- The agent Runtime role can invoke the Gateway.
- The Gateway role can invoke the tool Runtime through SigV4.
- The Go tool Runtime receives the allowlisted actor header plus serves deterministic incident evidence.
The managed path is live with its first security posture preserved. Next, you will invoke it to prove that Gateway supplies tenant evidence and Memory restores a prior conversation.
Connect Gateway Tools and Tenant Memory
Your deployed Amazon Bedrock AgentCore stack can now answer real requests. Deployment alone does not prove that each tenant receives the correct incident evidence.
Now you will send calls through Amazon Bedrock AgentCore Gateway to the Go MCP tool. You will also reuse a session through Amazon Bedrock AgentCore Memory to prove that short-term context returns.
In this step, get ready to:
- Retrieve Tenant Alpha's incident evidence through the IAM-authenticated Gateway.
- Restore an earlier Tenant Alpha conversation from short-term Memory.
- Retrieve a separate incident record for Tenant Beta.
Invoke the deployed agent for Tenant Alpha
The invocation utility reads outputs.json to find the deployed agent Runtime. It sends tenant-alpha as the Runtime user identity.
The first deployed request may take a few minutes while Runtime starts the session. A quiet terminal during that wait does not mean the request has failed.
Before you run this, what tool count do you expect the deployed agent to report? Hold that prediction in mind.
- Switch back to the terminal at the repository from earlier.
- Invoke the deployed copilot for Tenant Alpha by running this command:
uv run --with boto3==1.43.118 python scripts/invoke.py --user tenant-alpha --prompt "Investigate incident INC-100. Use available tools and report the evidence."
What Does This Invocation Prove?
- The scripts/invoke.py utility generates a Runtime session ID before calling IncidentCopilotRuntime.
- The agent discovers get_incident through the IAM-authenticated Gateway.
- A tool_count value of 1 confirms that one MCP tool was available.
- The response describes the payments-api SEV-1 incident for Tenant Alpha.
- Record the value printed after session_id= here: your tenant-alpha runtime session ID.
Tool Count Still Shows Zero?
A tool_count value of 0 means the deployed Runtime did not create the Gateway client. Confirm that the deployment used the stack supplying GATEWAY_URL.
If the invocation reports a permissions problem, confirm that your sandbox caller can invoke the agent Runtime. Help me diagnose this AgentCore invocation failure.
That live evidence path now works. Tenant Alpha receives deterministic incident data through the Gateway instead of relying on model-generated facts.
Restore the Tenant Alpha conversation
Each invocation constructs a new Strands agent. AgentCoreMemorySessionManager restores earlier events by using the same actor_id with the same session_id.
Before you run the follow-up, do you expect the new agent instance to remember the earlier incident? Keep your prediction in mind.
- Reuse the Tenant Alpha session by running this command:
uv run --with boto3==1.43.118 python scripts/invoke.py --user tenant-alpha --session [[ALPHA_SESSION_ID="your tenant-alpha runtime session ID"]] --prompt "Based on the incident we just discussed, what is the likely next action?"
What Does Memory Restore?
- The matching actor and session identifiers select Tenant Alpha's earlier short-term events.
- The response should refer to the payments-api incident or its recent release without asking you to repeat INC-100.
- This proves that context survives even though the application constructs a new Strands agent for the request.
Follow-Up Lost the Earlier Context?
Confirm that ALPHA_SESSION_ID exactly matches the first command's printed session ID. Confirm that the follow-up still uses tenant-alpha.
A different actor or session selects a different Memory event stream. Help me troubleshoot why AgentCore Memory did not restore my earlier conversation.
Prove tenant-scoped tool data
Tenant Beta can request the same incident identifier without reaching Tenant Alpha's record. The Go tool selects its tenant map from the trusted X-Amzn-Bedrock-AgentCore-Runtime-Custom-Actor-Id header.
Before you run this, do you expect INC-100 to return the payments incident again? Keep your prediction in mind.
- Invoke the copilot as Tenant Beta in a generated session by running this command:
uv run --with boto3==1.43.118 python scripts/invoke.py --user tenant-beta --prompt "Investigate incident INC-100. Use available tools and report the evidence."
What Does Tenant Beta Receive?
- The missing --session argument makes scripts/invoke.py generate a new Runtime session ID.
- The response still contains a tool_count value of 1.
- The evidence now describes Tenant Beta's inventory-sync SEV-2 incident.
- The model supplies only INC-100 to get_incident. The propagated trusted header determines the tenant.
Seeing Tenant Alpha Evidence?
Confirm that the command uses --user tenant-beta. Confirm that it does not reuse ALPHA_SESSION_ID.
If the actor is correct but the evidence is still wrong, inspect the trusted-header lookup in getIncident. Help me trace why Tenant Beta received the wrong incident evidence.
You have now proved both capability boundaries. Memory restores context within Tenant Alpha's session while the trusted header keeps Tenant Beta's tool evidence separate.
Your copilot now has evidence and short-term context. Next, you will attack the remaining Runtime session boundary before enabling the application-level binding control.
Enforce the Session Boundary
Your deployed copilot now retrieves tenant-scoped evidence through Amazon Bedrock AgentCore Gateway. Amazon Bedrock AgentCore Memory restores earlier conversation events for each actor.
Amazon Bedrock AgentCore Runtime keeps a runtime session on one isolated microVM. User ownership remains an application responsibility.
In this step, get ready to:
- Test cross-tenant reuse of a shared runtime session.
- Protect session ownership in a new Runtime version.
- Correlate the guarded session through observability.
Demonstrate the cross-tenant gap
The existing Memory keys scope conversation events by actor. The trusted actor header also scopes incident evidence inside the Go tool.
This test asks a separate question. What happens when a second actor presents the exact same runtimeSessionId while enforcement remains disabled?
- Return to the terminal output from your earlier Tenant Alpha invocation.
- Find the printed session_id value.
- Record that value here: your existing tenant-alpha runtime session ID.
- Reconfirm Tenant Alpha's session by running this command:
uv run --with boto3==1.43.118 python scripts/invoke.py \
--user tenant-alpha \
--session [[ATTACK_SESSION_ID="your existing tenant-alpha runtime session ID"]] \
--prompt "Review incident INC-100 and summarize the evidence."
What does this invocation prove?
- The invocation sends tenant-alpha as the Runtime user ID.
- The --session argument reuses the Runtime session from the earlier Alpha request.
- The prompt asks the copilot to retrieve evidence for INC-100.
You'll see tool_count equal to 1. The response contains Alpha's payments-api evidence.
Did the Alpha invocation fail?
Confirm that the session ID matches the earlier Alpha output. Confirm that outputs.json remains in the project directory.
Check that your AWS sandbox credentials are still active. Help me troubleshoot the Tenant Alpha invocation.
Before you reuse the session as Tenant Beta, do you think the request will be accepted or rejected?
- Present the same Runtime session as Tenant Beta by running this command:
uv run --with boto3==1.43.118 python scripts/invoke.py \
--user tenant-beta \
--session [[ATTACK_SESSION_ID="your existing tenant-alpha runtime session ID"]] \
--prompt "Review incident INC-100 and summarize the evidence."
The Shared Session Is Accepted
You'll see a normal response for tenant-beta using the same Runtime session. The tool returns Beta's inventory-sync evidence.
Memory still separates events by actor ID. The trusted header still keeps the tool lookup tenant-scoped.
The microVM handled one Runtime session for two actors. Shared execution context remains unacceptable even when the downstream data controls limit exposure.
Was Beta Rejected Already?
Confirm that the deployed stack still sets ENFORCE_SESSION_BINDING to false. A Runtime version with enforcement enabled rejects this designed obstacle.
Help me check which Runtime version handled the request.
The accepted Beta request is architecture evidence. Capture it in the decision record before changing the control.
- In docs/architecture.md, add this section below the numbered decisions:
## Deliberate obstacle
With session binding disabled, the platform accepts one runtimeSessionId from two runtimeUserId values. Memory separates actor IDs and the Go tool scopes data by the propagated actor header, but the microVM context is still shared. The final control binds the first actor to the microVM and rejects conflicts.
What Does This Decision Record Capture?
The record separates three controls that protect different boundaries. Memory scopes conversation events while the actor header scopes deterministic tool data.
The session guard protects ownership of the shared execution context. Each control closes a distinct path.
- Save docs/architecture.md.
- Open the Markdown preview for docs/architecture.md.
You should see a Deliberate obstacle section below the numbered architecture decisions.
✔️ Awesome, I've got everything!
Your architecture record now explains why tenant-scoped data controls still need a Runtime session ownership guard.
ⓧ I'd like to double check the full code
# Incident Copilot Architecture Decisions
## Quality attributes
- Tenant isolation is more important than tool-call convenience.
- Incident evidence must come from a deterministic system, not the model prompt.
- Every network hop must have an explicit identity and observable session key.
- Python owns orchestration. Go owns deterministic tool execution.
- Infrastructure must be reproducible through CDK and removable as one stack.
## Component map
```mermaid
flowchart LR
Caller[Trusted IAM caller] -->|runtimeUserId + runtimeSessionId| Agent[AgentCore HTTP Runtime]
Agent -->|SigV4 + trusted actor header| Gateway[AgentCore Gateway]
Gateway -->|SigV4 + allowlisted actor header| Tool[AgentCore MCP Runtime]
Tool --> Evidence[(Tenant incident map)]
Agent --> Memory[AgentCore Memory]
Agent --> Bedrock[Amazon Bedrock model]
Agent --> Telemetry[CloudWatch and AgentCore Observability]
```
## Trust boundaries
| Boundary | Trusted identity | Protected value | Enforcement |
| --- | --- | --- | --- |
| Caller to agent Runtime | IAM principal with InvokeAgentRuntimeForUser | runtimeUserId | Upstream caller plus IAM |
| Runtime session | First actor seen in the microVM | runtimeSessionId ownership | Application binding guard |
| Agent to Gateway | Agent Runtime execution role | Gateway invocation | SigV4 and IAM |
| Gateway to Go tool | Gateway execution role | Tool Runtime invocation | SigV4 and IAM |
| Gateway to tool container | Allowlisted actor header | Tenant evidence scope | GatewayTarget and Runtime header allowlists |
| Agent to Memory | Agent Runtime execution role | actorId plus sessionId events | IAM and Memory keys |
## Decisions
1. Use BedrockAgentCoreApp rather than raw FastAPI because it owns the Runtime HTTP contract, health endpoint, request context, and streaming behavior.
2. Use a native MCP Gateway target rather than a generic REST proxy so discovery and invocation remain MCP concepts.
3. Use two Runtime resources so orchestration and deterministic tools have separate roles, containers, protocols, and scaling boundaries.
4. Use runtimeUserId only as a trusted-backend development bridge. Replace it with JWT inbound authentication when end users call directly.
5. Store only short-term raw events here. Add long-term strategies only with explicit namespace and retention decisions.
6. Use Runtime V2, but keep actor binding state empty at startup so every restored microVM begins unbound.
7. Permit Bedrock model invocation on `*` in this sandbox because a global inference profile can route to multiple foundation-model resources. Replace this with the organization's approved inference-profile policy in production.
## Deliberate obstacle
With session binding disabled, the platform accepts one runtimeSessionId from two runtimeUserId values. Memory separates actor IDs and the Go tool scopes data by the propagated actor header, but the microVM context is still shared. The final control binds the first actor to the microVM and rejects conflicts.
## Follow-on mastery path
- Replace runtimeUserId delegation with Cognito or enterprise OIDC.
- Put Cedar Policy in ENFORCE mode at Gateway and add Memory fine-grained access control.
- Add AgentCore Browser and Code Interpreter behind approval boundaries.
- Add long-term Memory strategies, deletion workflows, and namespace tests.
- Add Evaluations, adversarial datasets, GitHub Actions, named Runtime endpoints, alarms, and rollback.
Enable the binding guard
The application already defines bind_session() around a locked _session_owner value. The CDK environment variable decides whether that guard runs.
- In your editor, select lib/agentcore-stack.ts.
- Find ENFORCE_SESSION_BINDING inside the agent Runtime's environmentVariables block.
- Replace its current value with true so the line matches this reference:
ENFORCE_SESSION_BINDING: "true",
What Does This Setting Activate?
The enabled guard stores the first observed actor ID in _session_owner under a lock. Every later request in that microVM must present the same actor ID.
A conflicting actor triggers PermissionError before Memory reconstruction or tool discovery begins.
- Save lib/agentcore-stack.ts.
✔️ Awesome, I've got everything!
Your stack now declares session binding as enabled for the agent Runtime.
ⓧ I'd like to double check the full code
import * as path from "path";
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as agentcore from "aws-cdk-lib/aws-bedrockagentcore";
import * as ecrAssets from "aws-cdk-lib/aws-ecr-assets";
import * as iam from "aws-cdk-lib/aws-iam";
const ACTOR_HEADER = "X-Amzn-Bedrock-AgentCore-Runtime-Custom-Actor-Id";
const MODEL_ID = "global.anthropic.claude-haiku-4-5-20251001-v1:0";
export class AgentCoreStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const memory = new agentcore.CfnMemory(this, "IncidentMemory", {
name: "incident_copilot_memory",
description: "Seven-day short-term event memory for the incident copilot",
eventExpiryDuration: 7,
});
memory.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY);
const toolArtifact = agentcore.AgentRuntimeArtifact.fromAsset(
path.join(__dirname, "../services/tool"),
{ platform: ecrAssets.Platform.LINUX_ARM64 },
);
const toolRuntime = new agentcore.Runtime(this, "ToolRuntime", {
runtimeName: "IncidentToolRuntime",
description: "Tenant-scoped Go MCP incident evidence service",
agentRuntimeArtifact: toolArtifact,
networkConfiguration: agentcore.RuntimeNetworkConfiguration.usingPublicNetwork(),
protocolConfiguration: agentcore.ProtocolType.MCP,
platformVersion: agentcore.PlatformVersion.V2,
requestHeaderConfiguration: { allowlistedHeaders: [ACTOR_HEADER] },
tracingEnabled: true,
});
const gateway = new agentcore.Gateway(this, "IncidentGateway", {
gatewayName: "incident-copilot-gateway",
description: "IAM-authenticated MCP gateway for incident tools",
});
const cfnGateway = gateway.node.defaultChild as agentcore.CfnGateway;
cfnGateway.authorizerType = "AWS_IAM";
cfnGateway.protocolType = "MCP";
cfnGateway.protocolConfiguration = {
mcp: {
supportedVersions: ["2025-11-25"],
streamingConfiguration: { enableResponseStreaming: true },
},
};
gateway.role.addToPrincipalPolicy(new iam.PolicyStatement({
actions: ["bedrock-agentcore:InvokeAgentRuntime"],
resources: [toolRuntime.agentRuntimeArn, `${toolRuntime.agentRuntimeArn}/*`],
}));
gateway.role.addToPrincipalPolicy(new iam.PolicyStatement({
actions: ["bedrock-agentcore:InvokeGateway"],
resources: [gateway.gatewayArn],
}));
const encodedToolRuntimeArn = cdk.Fn.join(
"%2F",
cdk.Fn.split("/", cdk.Fn.join("%3A", cdk.Fn.split(":", toolRuntime.agentRuntimeArn))),
);
const toolEndpoint = cdk.Fn.join("", [
`https://bedrock-agentcore.${this.region}.amazonaws.com/runtimes/`,
encodedToolRuntimeArn,
"/invocations?qualifier=DEFAULT",
]);
const toolTarget = new agentcore.CfnGatewayTarget(this, "IncidentToolTarget", {
gatewayIdentifier: cfnGateway.attrGatewayIdentifier,
name: "incident-tools",
description: "Go MCP incident tools hosted on AgentCore Runtime",
targetConfiguration: {
mcp: { mcpServer: { endpoint: toolEndpoint, listingMode: "DEFAULT" } },
},
credentialProviderConfigurations: [{
credentialProviderType: "GATEWAY_IAM_ROLE",
credentialProvider: {
iamCredentialProvider: { service: "bedrock-agentcore", region: this.region },
},
}],
metadataConfiguration: { allowedRequestHeaders: [ACTOR_HEADER] },
});
toolTarget.node.addDependency(gateway);
toolTarget.node.addDependency(toolRuntime);
const gatewayRolePolicy = gateway.role.node.findChild("DefaultPolicy")
.node.defaultChild as cdk.CfnResource;
toolTarget.node.addDependency(gatewayRolePolicy);
const agentArtifact = agentcore.AgentRuntimeArtifact.fromAsset(
path.join(__dirname, "../services/agent"),
{ platform: ecrAssets.Platform.LINUX_ARM64 },
);
const agentRuntime = new agentcore.Runtime(this, "AgentRuntime", {
runtimeName: "IncidentCopilotRuntime",
description: "Strands incident copilot with Gateway and Memory",
agentRuntimeArtifact: agentArtifact,
networkConfiguration: agentcore.RuntimeNetworkConfiguration.usingPublicNetwork(),
protocolConfiguration: agentcore.ProtocolType.HTTP,
platformVersion: agentcore.PlatformVersion.V2,
tracingEnabled: true,
environmentVariables: {
AWS_REGION: this.region,
AWS_DEFAULT_REGION: this.region,
MODEL_ID,
GATEWAY_URL: cfnGateway.attrGatewayUrl,
MEMORY_ID: memory.attrMemoryId,
ACTOR_HEADER,
ENFORCE_SESSION_BINDING: "true",
UNIFIED_TRACES_DESTINATION_ENABLED: "true",
},
});
agentRuntime.node.addDependency(toolTarget);
agentRuntime.role.addToPrincipalPolicy(new iam.PolicyStatement({
actions: ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"],
resources: ["*"],
}));
agentRuntime.role.addToPrincipalPolicy(new iam.PolicyStatement({
actions: [
"bedrock-agentcore:CreateEvent",
"bedrock-agentcore:GetEvent",
"bedrock-agentcore:ListEvents",
"bedrock-agentcore:GetMemory",
],
resources: [memory.attrMemoryArn],
}));
agentRuntime.role.addToPrincipalPolicy(new iam.PolicyStatement({
actions: ["bedrock-agentcore:InvokeGateway"],
resources: [gateway.gatewayArn],
}));
new cdk.CfnOutput(this, "AgentRuntimeArn", { value: agentRuntime.agentRuntimeArn });
new cdk.CfnOutput(this, "ToolRuntimeArn", { value: toolRuntime.agentRuntimeArn });
new cdk.CfnOutput(this, "GatewayUrl", { value: cfnGateway.attrGatewayUrl });
new cdk.CfnOutput(this, "MemoryId", { value: memory.attrMemoryId });
}
}
This deployment creates a new immutable agent Runtime version. Usage-based charges continue while you run the verification requests.
The update can take several minutes while CDK builds the ARM64 asset and updates the Runtime. A quiet terminal during the container build is expected.
- Deploy the guarded Runtime version by running:
npm run deploy
What Does This Deployment Change?
The deploy script builds the updated agent container configuration. CDK publishes a new Runtime version with ENFORCE_SESSION_BINDING set to true.
Existing sessions retain the code version selected when they started. The next verification uses a fresh session so it reaches the guarded version.
You'll see CDK report a successful deployment. outputs.json continues to contain the current AgentRuntimeArn, ToolRuntimeArn, GatewayUrl, and MemoryId values.
Did the Deployment Fail?
Confirm that Docker is running before retrying the ARM64 container build. Confirm that your AWS sandbox credentials still target us-west-2.
Help me troubleshoot the guarded Runtime deployment.
Verify and trace the guarded session
A new Runtime version only applies to sessions created after deployment. The first request in a fresh session becomes the ownership test for _session_owner.
- Create a fresh guarded session as Tenant Alpha by running this command:
uv run --with boto3==1.43.118 python scripts/invoke.py \
--user tenant-alpha \
--prompt "Review incident INC-100 and summarize the evidence."
What Does the First Request Do?
The invocation script generates a fresh Runtime session ID. The first request binds _session_owner to tenant-alpha inside that session's microVM.
The successful request still reaches Memory and the Gateway-discovered tool. Session ownership now protects the execution context around those controls.
You'll see a new value after session_id=. The response contains Alpha's incident evidence with tool_count equal to 1.
- Copy the value printed after session_id=.
- Record it here: your fresh guarded runtime session ID.
Did the Fresh Session Fail?
Confirm that you omitted --session so the script could generate a new ID. An old session can remain attached to the earlier Runtime version.
Help me troubleshoot the fresh guarded session.
Before you run the boundary check, what should Alpha's ownership mean when Beta presents the same session ID?
You are about to run an intentional rejection test.
- Present the guarded session as Tenant Beta by running this command:
uv run --with boto3==1.43.118 python scripts/invoke.py \
--user tenant-beta \
--session [[GUARDED_SESSION_ID="your fresh guarded runtime session ID"]] \
--prompt "Review incident INC-100 and summarize the evidence."
The Guard Rejects Tenant Beta
The invocation fails with session is already bound to another actor.
The first Alpha request set _session_owner for this microVM. The conflicting Beta request stops before Memory reconstruction or MCP tool discovery.
Was Tenant Beta Accepted?
Confirm that the Beta command uses the fresh session ID printed by the post-deployment Alpha request. Confirm that the deployed stack sets ENFORCE_SESSION_BINDING to true.
Help me troubleshoot why the conflicting actor was accepted.
The guarded session now has two observable outcomes. Alpha completes the full reasoning path while Beta stops at the application boundary.
Amazon CloudWatch stores the telemetry used by Amazon Bedrock AgentCore Observability. The shared session ID correlates the successful path with the rejected request.
- Open the CloudWatch generative AI observability view for the guarded session.
- Search for your fresh guarded runtime session ID.
- Select the matching agent invocation.
What Should I See?
The successful Alpha path starts with the Runtime invocation. The trace carries the guarded session through session.id.
The Alpha trace includes the Bedrock inference. It also includes the Gateway and MCP tool activity.
Memory activity uses the Alpha actor ID with the same session ID. This links reconstruction to the authenticated actor.
The failed Beta request correlates to the same session ID. Its rejection marks the application boundary before downstream agent work.
That closes the boundary you exposed earlier. The same session now produces tenant-scoped evidence for its owner and rejects every conflicting actor.
Secret mission
Audit the Trust Boundaries
The guarded copilot now rejects cross-tenant session reuse. In this mission, you will audit every execution role and map the next production controls to the boundary each one protects.
Clean Up Your Resources
Clean Up Your Resources
Your deployed Amazon Bedrock AgentCore architecture can continue incurring usage-based charges. The options below let you keep the environment available, pause active work, or delete the application stack.
Cost warning
- Amazon Bedrock AgentCore Runtime V2 compute generates charges from use.
- Amazon Bedrock inference generates charges when invoked.
- Amazon Bedrock AgentCore Gateway API calls are usage-based.
- Amazon Bedrock AgentCore Memory storage can continue generating charges.
- Amazon CloudWatch telemetry incurs ingestion and storage charges.
- Amazon ECR storage can incur charges while container assets remain.
Pausing prevents new invocation activity. Deleting removes the application stack.
Resources you used:
- The AWS CloudFormation stack AgentCoreMasteryStack.
- The Runtime V2 resource IncidentCopilotRuntime.
- The Runtime V2 resource IncidentToolRuntime.
- The Gateway incident-copilot-gateway.
- The Gateway target incident-tools.
- The Memory resource incident_copilot_memory.
- The stack-specific IAM execution roles and policies.
Keep everything running
No action is needed. This option fits continued testing of the guarded incident copilot.
- Keep AgentCoreMasteryStack deployed while you continue testing the guarded copilot.
- Reuse each existing session only with its bound tenant identity.
- Monitor usage in your AWS sandbox.
- Retain your local source files and completed trust-boundary matrix.
Pause - I'll come back to this later
For this project, pausing means ending active work while keeping the deployed stack. Persistent storage can still incur charges.
- Finish any Runtime invocation that is currently in progress.
- Stop sending new invocations with scripts/invoke.py.
- Avoid creating fresh Runtime sessions until you return.
- Review persistent storage usage in Memory, CloudWatch, and ECR.
Delete - I don't want to use this again
This option removes the deployed application resources from your AWS sandbox. Your local project files remain available for reuse.
Stack deletion is permanent for the deployed resources. AWS CDK pauses for confirmation before it removes them.
- Switch back to the terminal in the directory that contains package.json.
- Start the stack deletion by running this command:
npm run destroy
What does this command do?
This npm command runs the destroy script defined in package.json.
AWS CDK removes the resources managed by AgentCoreMasteryStack.
- Approve the deletion when AWS CDK displays its confirmation request.
- Wait until AWS CDK reports that AgentCoreMasteryStack finished deleting.
- Confirm that IncidentCopilotRuntime no longer appears in your AWS sandbox.
- Confirm that IncidentToolRuntime no longer appears in your AWS sandbox.
- Confirm that incident-copilot-gateway no longer appears in your AWS sandbox.
- Confirm that incident-tools no longer appears in your AWS sandbox.
- Confirm that incident_copilot_memory no longer appears in your AWS sandbox.
- Confirm that the custom IAM roles created by AgentCoreMasteryStack no longer appear in your AWS sandbox.
That's the chargeable application stack taken care of. Your source files and trust-boundary matrix are still available locally.
What stays in your sandbox?
Shared CDK bootstrap assets in Amazon ECR and Amazon S3 can support other stacks. The application stack deletion leaves them available for separate review.
- Confirm whether another CDK stack uses the shared bootstrap assets before removing them.
- Confirm the retention status of the guarded session telemetry in Amazon CloudWatch.
Nice Work!
Nice Work!
You did it! You built a tenant-safe incident copilot on Amazon Bedrock AgentCore that defends incident evidence from cross-tenant session reuse.
You've learned how to:
- Defined the full architecture with AWS CDK. Deployed separate ARM64 HTTP containers plus MCP containers to AgentCore Runtime V2.
- Connected an IAM-authenticated AgentCore Gateway to a tenant-scoped Go evidence tool. Restored actor-scoped conversations through AgentCore Memory.
- Demonstrated unsafe runtimeSessionId reuse across tenants. Enforced first-actor session binding. Correlated the guarded execution path in CloudWatch.
- Secret Mission: Produced a role-by-role trust-boundary audit. Placed JWT validation at the direct end-user boundary. Placed Cedar Policy at Gateway. Added Memory fine-grained access control to the production roadmap.
Ready to quiz yourself?