chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:58:18 +08:00
commit 6d5d58c1a9
18293 changed files with 3502153 additions and 0 deletions
@@ -0,0 +1,36 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim
WORKDIR /app
# Configure UV for container environment
ENV UV_SYSTEM_PYTHON=1 \
UV_COMPILE_BYTECODE=1 \
DOCKER_CONTAINER=1 \
OTEL_PYTHON_LOG_CORRELATION=true \
PYTHONUNBUFFERED=1
# Copy and install agent-specific requirements first
COPY agents/langgraph-single-agent/requirements.txt requirements.txt
RUN uv pip install --no-cache -r requirements.txt && \
uv pip install --no-cache aws-opentelemetry-distro==0.16.0
# Create non-root user
RUN useradd -m -u 1000 bedrock_agentcore
USER bedrock_agentcore
EXPOSE 8080
# Copy agent code, tools, and shared utilities
COPY agents/langgraph-single-agent/langgraph_agent.py .
COPY agents/langgraph-single-agent/tools/ tools/
COPY agents/utils/ utils/
# Healthcheck using Python (no extra dependencies needed)
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/ping', timeout=2)" || exit 1
# Start agent with OpenTelemetry instrumentation
CMD ["opentelemetry-instrument", "python", "-m", "langgraph_agent"]
@@ -0,0 +1,159 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
import logging
import os
from ag_ui.core import RunAgentInput, RunErrorEvent
from bedrock_agentcore.identity.auth import requires_access_token
from bedrock_agentcore.runtime import BedrockAgentCoreApp, RequestContext
from copilotkit import (
CopilotKitMiddleware,
LangGraphAGUIAgent,
StateStreamingMiddleware,
StateItem,
)
from langchain.agents import create_agent
from langchain_aws import ChatBedrock
from langchain_mcp_adapters.client import MultiServerMCPClient
from langgraph_checkpoint_aws import AgentCoreMemorySaver
from utils.auth import extract_user_id_from_context
from utils.ssm import get_ssm_parameter
from tools import query_data, AgentState, todo_tools
app = BedrockAgentCoreApp()
ACTOR_ID_KEYS = ("actor_id", "actorId", "user_id", "userId", "sub")
SYSTEM_PROMPT = """You are a helpful assistant with access to tools via the Gateway and built-in data tools.
When demonstrating charts, always call the query_data tool first to fetch data from the database before calling any chart tool.
When managing todos, use manage_todos to update the list and get_todos to read the current list.
When asked about your tools, list them and explain what they do."""
@requires_access_token(
provider_name=os.environ["GATEWAY_CREDENTIAL_PROVIDER_NAME"],
auth_flow="M2M",
scopes=[],
)
async def _fetch_gateway_token(access_token: str) -> str:
return access_token
async def create_gateway_mcp_client() -> MultiServerMCPClient:
stack_name = os.environ.get("STACK_NAME")
if not stack_name:
raise ValueError("STACK_NAME environment variable is required")
if not stack_name.replace("-", "").replace("_", "").isalnum():
raise ValueError("Invalid STACK_NAME format")
gateway_url = get_ssm_parameter(f"/{stack_name}/gateway_url")
fresh_token = await _fetch_gateway_token()
return MultiServerMCPClient(
{
"gateway": {
"transport": "streamable_http",
"url": gateway_url,
"headers": {
"Authorization": f"Bearer {fresh_token}",
},
}
}
)
def _build_model(streaming: bool) -> ChatBedrock:
return ChatBedrock(
model_id="us.anthropic.claude-sonnet-4-5-20250929-v1:0",
temperature=0.1,
max_tokens=16384,
streaming=streaming,
beta_use_converse_api=True,
)
def _build_checkpointer() -> AgentCoreMemorySaver:
memory_id = os.environ.get("MEMORY_ID")
if not memory_id:
raise ValueError("MEMORY_ID environment variable is required")
return AgentCoreMemorySaver(
memory_id=memory_id,
region_name=os.environ.get("AWS_DEFAULT_REGION", "us-east-1"),
)
@app.entrypoint
async def invocations(payload: dict, context: RequestContext):
input_data = RunAgentInput.model_validate(payload)
# Extract actor identity securely from the validated JWT token.
try:
actor_id = extract_user_id_from_context(context)
except ValueError:
# Fall back to forwarded props if JWT extraction fails (e.g. local dev).
forwarded = (
input_data.forwarded_props
if isinstance(input_data.forwarded_props, dict)
else {}
)
actor_id = next(
(forwarded[k] for k in ACTOR_ID_KEYS if k in forwarded and forwarded[k]),
None,
)
if not actor_id:
raise ValueError(
"Missing actor identity. Provide forwardedProps.actor_id/user_id "
"or include sub claim in the bearer token."
)
try:
try:
mcp_client = await create_gateway_mcp_client()
gateway_tools = await mcp_client.get_tools()
except Exception as gw_err:
logging.warning("Gateway tools unavailable (running locally?): %s", gw_err)
gateway_tools = []
graph = create_agent(
model=_build_model(streaming=True),
tools=[*gateway_tools, query_data, *todo_tools],
checkpointer=_build_checkpointer(),
middleware=[
CopilotKitMiddleware(),
StateStreamingMiddleware(
StateItem(
state_key="todos", tool="manage_todos", tool_argument="todos"
)
),
],
system_prompt=SYSTEM_PROMPT,
state_schema=AgentState,
)
agent = LangGraphAGUIAgent(
name="LangGraphSingleAgent",
description="LangGraph single agent exposed via AG-UI",
graph=graph,
config={"configurable": {"actor_id": actor_id}},
)
async for event in agent.run(input_data):
if event is not None:
yield event.model_dump(mode="json", by_alias=True, exclude_none=True)
except Exception as exc:
logging.exception("Agent run failed")
yield RunErrorEvent(
message=str(exc) or type(exc).__name__,
code=type(exc).__name__,
).model_dump(mode="json", by_alias=True, exclude_none=True)
if __name__ == "__main__":
app.run()
@@ -0,0 +1,14 @@
# LangGraph agent dependencies with pinned versions
fastapi==0.115.12
uvicorn==0.34.2
ag-ui-protocol>=0.1.15
ag-ui-langgraph==0.0.33
copilotkit==0.1.87
partialjson>=0.0.8
langgraph==1.0.10rc1
langchain>=0.3.0
langchain-aws==1.0.0
langchain-mcp-adapters==0.1.13
langgraph-checkpoint-aws==1.0.5
mcp==1.23.1
bedrock-agentcore==1.0.6
@@ -0,0 +1,8 @@
# patterns/langgraph-single-agent/tools/__init__.py
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
from .query_data import query_data
from .todos import AgentState, todo_tools
__all__ = ["query_data", "AgentState", "todo_tools"]
@@ -0,0 +1,16 @@
date,category,amount,type
2026-01-05,Food,42.50,expense
2026-01-10,Transport,15.00,expense
2026-01-15,Salary,3500.00,income
2026-01-20,Entertainment,80.00,expense
2026-01-25,Utilities,120.00,expense
2026-02-03,Food,55.20,expense
2026-02-08,Freelance,800.00,income
2026-02-14,Dining,65.00,expense
2026-02-20,Transport,22.50,expense
2026-02-28,Salary,3500.00,income
2026-03-05,Groceries,95.40,expense
2026-03-10,Gym,40.00,expense
2026-03-15,Salary,3500.00,income
2026-03-18,Coffee,18.75,expense
2026-03-22,Books,35.00,expense
1 date category amount type
2 2026-01-05 Food 42.50 expense
3 2026-01-10 Transport 15.00 expense
4 2026-01-15 Salary 3500.00 income
5 2026-01-20 Entertainment 80.00 expense
6 2026-01-25 Utilities 120.00 expense
7 2026-02-03 Food 55.20 expense
8 2026-02-08 Freelance 800.00 income
9 2026-02-14 Dining 65.00 expense
10 2026-02-20 Transport 22.50 expense
11 2026-02-28 Salary 3500.00 income
12 2026-03-05 Groceries 95.40 expense
13 2026-03-10 Gym 40.00 expense
14 2026-03-15 Salary 3500.00 income
15 2026-03-18 Coffee 18.75 expense
16 2026-03-22 Books 35.00 expense
@@ -0,0 +1,25 @@
# patterns/langgraph-single-agent/tools/query_data.py
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
import csv
from pathlib import Path
from langchain.tools import tool
# Read at module load time — avoids file I/O on every tool invocation.
_csv_path = Path(__file__).parent / "db.csv"
try:
with open(_csv_path) as _f:
_cached_data = list(csv.DictReader(_f))
except (FileNotFoundError, OSError) as e:
raise RuntimeError(f"query_data: cannot load sample data from {_csv_path}") from e
@tool
def query_data(query: str) -> list[dict]:
"""
Query the database. Accepts natural language.
Always call this tool before displaying a chart or graph.
"""
return _cached_data
@@ -0,0 +1,66 @@
# patterns/langgraph-single-agent/tools/todos.py
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
import uuid
from typing import Literal, TypedDict
from langchain.agents import AgentState as BaseAgentState
from langchain.tools import ToolRuntime, tool
from langchain_core.messages import ToolMessage
from langgraph.types import Command
# ToolRuntime is confirmed available at langchain.tools (langchain >= 1.2).
# If you see an ImportError, verify your langchain version is >= 0.3.
class Todo(TypedDict):
id: str
title: str
description: str
emoji: str
status: Literal["pending", "completed"]
class AgentState(BaseAgentState):
todos: list[Todo]
def _assign_ids(todos: list[dict]) -> list[dict]:
"""Assign a uuid4 to any todo that has a missing or empty 'id'."""
for todo in todos:
if not todo.get("id"):
todo["id"] = str(uuid.uuid4())
return todos
@tool
def manage_todos(todos: list[Todo], runtime: ToolRuntime) -> Command:
"""
Manage the current todos. Replaces the entire todo list.
Assigns a unique UUID to any todo that is missing one.
"""
_assign_ids(todos) # type: ignore[arg-type]
return Command(
update={
"todos": todos,
"messages": [
ToolMessage(
content="Successfully updated todos",
tool_call_id=runtime.tool_call_id,
)
],
}
)
@tool
def get_todos(runtime: ToolRuntime) -> list[Todo]:
"""
Get the current todo list from agent state.
"""
return runtime.state.get("todos", [])
todo_tools = [manage_todos, get_todos]
@@ -0,0 +1,36 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim
WORKDIR /app
# Configure UV for container environment
ENV UV_SYSTEM_PYTHON=1 \
UV_COMPILE_BYTECODE=1 \
DOCKER_CONTAINER=1 \
OTEL_PYTHON_LOG_CORRELATION=true \
PYTHONUNBUFFERED=1
# Copy and install agent-specific requirements first
COPY agents/strands-single-agent/requirements.txt requirements.txt
RUN uv pip install --no-cache -r requirements.txt && \
uv pip install --no-cache aws-opentelemetry-distro==0.16.0
# Create non-root user
RUN useradd -m -u 1000 bedrock_agentcore
USER bedrock_agentcore
EXPOSE 8080
# Copy agent code and shared utilities
COPY agents/strands-single-agent/strands_agent.py .
COPY agents/strands-single-agent/tools/ tools/
COPY agents/utils/ utils/
# Healthcheck using Python (no extra dependencies needed)
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/ping', timeout=2)" || exit 1
# Start agent with OpenTelemetry instrumentation
CMD ["opentelemetry-instrument", "python", "-m", "strands_agent"]
@@ -0,0 +1,8 @@
# Strands agent dependencies with pinned versions
strands-agents==1.24.0
mcp==1.26.0
bedrock-agentcore[strands-agents]==1.2.0
PyJWT[crypto]>=2.10.1
ag-ui-protocol>=0.1.10
ag-ui-strands==0.1.2
fastapi>=0.115.12
@@ -0,0 +1,229 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
import json
import logging
import os
import traceback
from ag_ui.core import RunAgentInput, RunErrorEvent
from ag_ui_strands import (
StrandsAgent,
StrandsAgentConfig,
ToolBehavior,
PredictStateMapping,
)
from ag_ui_strands.config import ToolCallContext
from bedrock_agentcore.memory.integrations.strands.config import AgentCoreMemoryConfig
from bedrock_agentcore.memory.integrations.strands.session_manager import (
AgentCoreMemorySessionManager,
)
from bedrock_agentcore.runtime import BedrockAgentCoreApp, RequestContext
from mcp.client.streamable_http import streamablehttp_client
from strands import Agent
from strands.models import BedrockModel
from strands.tools.mcp import MCPClient
from tools.query_data import query_data
from tools.todos import manage_todos
from utils.auth import extract_user_id_from_context, get_gateway_access_token
from utils.ssm import get_ssm_parameter
app = BedrockAgentCoreApp()
logger = logging.getLogger(__name__)
ACTOR_ID_KEYS = ("actor_id", "actorId", "user_id", "userId", "sub")
SYSTEM_PROMPT = """You are a helpful assistant with access to tools via the Gateway and built-in data tools.
When demonstrating charts, always call the query_data tool first to fetch data from the database before calling any chart tool.
When managing todos, use manage_todos to update the list.
When asked about your tools, list them and explain what they do."""
BEDROCK_MODEL = BedrockModel(
model_id="us.anthropic.claude-sonnet-4-5-20250929-v1:0",
temperature=0.1,
)
def create_gateway_mcp_client() -> MCPClient:
"""
Create MCP client for AgentCore Gateway with OAuth2 authentication.
Calls get_gateway_access_token() inside the lambda factory to ensure a fresh
token is fetched on every MCP reconnection (avoids the closure trap).
"""
stack_name = os.environ.get("STACK_NAME")
if not stack_name:
raise ValueError("STACK_NAME environment variable is required")
if not stack_name.replace("-", "").replace("_", "").isalnum():
raise ValueError("Invalid STACK_NAME format")
gateway_url = get_ssm_parameter(f"/{stack_name}/gateway_url")
return MCPClient(
lambda: streamablehttp_client(
url=gateway_url,
headers={"Authorization": f"Bearer {get_gateway_access_token()}"},
),
prefix="gateway",
)
def create_strands_agent(actor_id: str, session_id: str) -> StrandsAgent:
"""
Create a StrandsAgent wrapping a Strands SDK agent with AgentCore memory,
Gateway MCP tools, and CopilotKit-compatible AG-UI configuration.
Memory: AgentCoreMemorySessionManager provides cloud-persistent conversation
history keyed by actor_id, matching the AgentCoreMemorySaver approach used
in the LangGraph pattern.
"""
memory_id = os.environ.get("MEMORY_ID")
if not memory_id:
raise ValueError("MEMORY_ID environment variable is required")
agentcore_memory_config = AgentCoreMemoryConfig(
memory_id=memory_id, session_id=session_id, actor_id=actor_id
)
session_manager = AgentCoreMemorySessionManager(
agentcore_memory_config=agentcore_memory_config,
region_name=os.environ.get("AWS_DEFAULT_REGION", "us-east-1"),
)
gateway_client = create_gateway_mcp_client()
# Inject current todos into the system prompt so the agent always knows
# the latest todo state without needing a separate get_todos tool.
def state_context_builder(state: dict) -> str:
todos = state.get("todos", [])
if todos:
return f"\nCurrent todos:\n{json.dumps(todos, indent=2)}"
return ""
# When manage_todos is called, emit a StateSnapshotEvent with the new todos
# so the frontend updates immediately (before the tool result arrives).
async def todos_state_from_args(ctx: ToolCallContext) -> dict:
todos = (ctx.tool_input or {}).get("todos", [])
return {"todos": todos}
# Frontend tools (generative UI / canvas controls): let the agent continue after
# calling them so it generates a proper conclusion text. The run then finishes
# naturally and ag_ui_strands sends a MessagesSnapshotEvent that preserves the
# chat history. Without continue_after_frontend_call the stream halts and
# CopilotKit v2 clears the UI because no snapshot was sent.
frontend_tool_behavior = ToolBehavior(
continue_after_frontend_call=False,
skip_messages_snapshot=False,
)
config = StrandsAgentConfig(
tool_behaviors={
"manage_todos": ToolBehavior(
state_from_args=todos_state_from_args,
predict_state=[
PredictStateMapping(
state_key="todos",
tool="manage_todos",
tool_argument="todos",
)
],
),
"pieChart": frontend_tool_behavior,
"barChart": frontend_tool_behavior,
"toggleTheme": frontend_tool_behavior,
"scheduleTime": frontend_tool_behavior,
"enableAppMode": frontend_tool_behavior,
"enableChatMode": frontend_tool_behavior,
},
state_context_builder=state_context_builder,
)
# Build the underlying Strands agent with persistent memory and tools.
core_agent = Agent(
name="FASTAgent",
system_prompt=SYSTEM_PROMPT,
tools=[gateway_client, query_data, manage_todos],
model=BEDROCK_MODEL,
session_manager=session_manager,
record_direct_tool_call=True,
trace_attributes={
"user.id": actor_id,
"session.id": session_id,
},
)
strands_agent = StrandsAgent(
agent=core_agent,
name="FASTAgent",
description="FAST Strands agent with CopilotKit generative UI support",
config=config,
)
# Pre-seed the per-thread agent cache so StrandsAgent.run() uses our
# core_agent (which has AgentCoreMemorySessionManager) rather than creating
# a new instance without it.
strands_agent._agents_by_thread[session_id] = core_agent
return strands_agent
@app.entrypoint
async def invocations(payload: dict, context: RequestContext):
"""
Main entrypoint for the Strands agent using AG-UI protocol.
Accepts RunAgentInput payloads from the CopilotKit Lambda Runtime,
streams AG-UI events back, and supports generative UI, shared state
(todos), and human-in-the-loop interactions via CopilotKit.
"""
input_data = RunAgentInput.model_validate(payload)
# Extract actor identity securely from the validated JWT token.
try:
actor_id = extract_user_id_from_context(context)
except ValueError:
# Fall back to forwarded props if JWT extraction fails (e.g. local dev).
forwarded = (
input_data.forwarded_props
if isinstance(input_data.forwarded_props, dict)
else {}
)
actor_id = next(
(forwarded[k] for k in ACTOR_ID_KEYS if k in forwarded and forwarded[k]),
None,
)
if not actor_id:
raise ValueError(
"Missing actor identity. Provide forwardedProps.actor_id/user_id "
"or include sub claim in the bearer token."
)
# Use thread_id from the request (set by CopilotKit runtime) or fall back
# to actor_id so each user gets their own persistent conversation thread.
session_id = input_data.thread_id or actor_id
# Ensure thread_id in the payload matches so StrandsAgent uses our pre-seeded agent.
input_data = input_data.model_copy(update={"thread_id": session_id})
try:
strands_agent = create_strands_agent(actor_id, session_id)
async for event in strands_agent.run(input_data):
if event is not None:
yield event.model_dump(mode="json", by_alias=True, exclude_none=True)
except Exception as exc:
logger.exception("Agent run failed")
yield RunErrorEvent(
message=str(exc) or type(exc).__name__,
code=type(exc).__name__,
).model_dump(mode="json", by_alias=True, exclude_none=True)
if __name__ == "__main__":
app.run()
@@ -0,0 +1,4 @@
from .query_data import query_data
from .todos import manage_todos, Todo
__all__ = ["query_data", "manage_todos", "Todo"]
@@ -0,0 +1,16 @@
date,category,amount,type
2026-01-05,Food,42.50,expense
2026-01-10,Transport,15.00,expense
2026-01-15,Salary,3500.00,income
2026-01-20,Entertainment,80.00,expense
2026-01-25,Utilities,120.00,expense
2026-02-03,Food,55.20,expense
2026-02-08,Freelance,800.00,income
2026-02-14,Dining,65.00,expense
2026-02-20,Transport,22.50,expense
2026-02-28,Salary,3500.00,income
2026-03-05,Groceries,95.40,expense
2026-03-10,Gym,40.00,expense
2026-03-15,Salary,3500.00,income
2026-03-18,Coffee,18.75,expense
2026-03-22,Books,35.00,expense
1 date category amount type
2 2026-01-05 Food 42.50 expense
3 2026-01-10 Transport 15.00 expense
4 2026-01-15 Salary 3500.00 income
5 2026-01-20 Entertainment 80.00 expense
6 2026-01-25 Utilities 120.00 expense
7 2026-02-03 Food 55.20 expense
8 2026-02-08 Freelance 800.00 income
9 2026-02-14 Dining 65.00 expense
10 2026-02-20 Transport 22.50 expense
11 2026-02-28 Salary 3500.00 income
12 2026-03-05 Groceries 95.40 expense
13 2026-03-10 Gym 40.00 expense
14 2026-03-15 Salary 3500.00 income
15 2026-03-18 Coffee 18.75 expense
16 2026-03-22 Books 35.00 expense
@@ -0,0 +1,21 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
import csv
import os
from strands import tool
@tool
def query_data(query: str) -> str:
"""
Query financial data from the database. Use this tool to fetch data before
rendering any charts. Returns CSV-formatted data relevant to the query.
"""
db_path = os.path.join(os.path.dirname(__file__), "db.csv")
try:
with open(db_path, "r") as f:
content = f.read()
return content
except FileNotFoundError:
return "No data available."
@@ -0,0 +1,23 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
from typing import Literal, TypedDict
from strands import tool
class Todo(TypedDict):
id: str
title: str
description: str
emoji: str
status: Literal["pending", "completed"]
@tool
def manage_todos(todos: list) -> str:
"""
Manage the current todos. Replaces the entire todo list.
Each todo should have: id (str), title (str), description (str), emoji (str), status ('pending' or 'completed').
"""
return "Todos updated successfully"
@@ -0,0 +1,100 @@
"""
Authentication utilities for agent patterns.
Provides secure user identity extraction from JWT tokens in the AgentCore Runtime
RequestContext (prevents impersonation via prompt injection).
"""
import logging
import os
import jwt
from bedrock_agentcore.identity.auth import requires_access_token
from bedrock_agentcore.runtime import RequestContext
logger = logging.getLogger(__name__)
def extract_user_id_from_context(context: RequestContext) -> str:
"""
Securely extract the user ID from the JWT token in the request context.
AgentCore Runtime validates the JWT token before passing it to the agent,
so we can safely skip signature verification here. The user ID is taken
from the token's 'sub' claim rather than from the request payload, which
prevents impersonation via prompt injection.
Args:
context (RequestContext): The request context provided by AgentCore
Runtime, containing validated request headers including the
Authorization JWT.
Returns:
str: The user ID (sub claim) extracted from the validated JWT token.
Raises:
ValueError: If the Authorization header is missing or the JWT does
not contain a 'sub' claim.
"""
request_headers = context.request_headers
if not request_headers:
raise ValueError(
"No request headers found in context. "
"Ensure the AgentCore Runtime is configured with a request header allowlist "
"that includes the Authorization header."
)
auth_header = request_headers.get("Authorization")
if not auth_header:
raise ValueError(
"No Authorization header found in request context. "
"Ensure the AgentCore Runtime is configured with JWT inbound auth "
"and the Authorization header is in the request header allowlist."
)
# Remove "Bearer " prefix to get the raw JWT token
token = (
auth_header.replace("Bearer ", "")
if auth_header.startswith("Bearer ")
else auth_header
)
# Decode without signature verification — AgentCore Runtime already validated the token.
# We use options to skip all verification since this is a trusted, pre-validated token.
claims = jwt.decode(
jwt=token,
options={"verify_signature": False},
algorithms=["RS256"],
)
user_id = claims.get("sub")
if not user_id:
raise ValueError(
"JWT token does not contain a 'sub' claim. Cannot determine user identity."
)
logger.info("Extracted user_id from JWT: %s", user_id)
return user_id
@requires_access_token(
provider_name=os.environ.get("GATEWAY_CREDENTIAL_PROVIDER_NAME", ""),
auth_flow="M2M",
scopes=[],
)
def get_gateway_access_token(access_token: str) -> str:
"""
Fetch OAuth2 access token for AgentCore Gateway authentication.
The @requires_access_token decorator handles token retrieval and refresh:
1. Token Retrieval: Calls GetResourceOauth2Token API to fetch token from Token Vault
2. Automatic Refresh: Uses refresh tokens to renew expired access tokens
3. Error Orchestration: Handles missing tokens and OAuth flow management
For M2M (Machine-to-Machine) flows, the decorator uses Client Credentials grant type.
The provider_name must match the Name field in the CDK OAuth2CredentialProvider resource.
This is synchronous because it's called during agent setup before the async
message processing loop.
"""
return access_token
@@ -0,0 +1,45 @@
"""
SSM Parameter Store utilities for agent patterns.
Provides a single shared function for fetching parameters from AWS SSM
Parameter Store, used by agents to retrieve configuration values like
Gateway URLs that are set during deployment.
"""
import logging
import os
import boto3
logger = logging.getLogger(__name__)
def get_ssm_parameter(parameter_name: str) -> str:
"""
Fetch a parameter value from AWS SSM Parameter Store.
SSM Parameter Store is AWS's service for storing configuration values
securely. This function retrieves values like Gateway URLs and other
stack-specific configuration that are set during CDK deployment.
Args:
parameter_name (str): The full SSM parameter name/path
(e.g. '/my-stack/gateway_url').
Returns:
str: The parameter value.
Raises:
ValueError: If the parameter is not found or cannot be retrieved.
"""
region = os.environ.get(
"AWS_REGION", os.environ.get("AWS_DEFAULT_REGION", "us-east-1")
)
ssm = boto3.client("ssm", region_name=region)
try:
response = ssm.get_parameter(Name=parameter_name)
return response["Parameter"]["Value"]
except ssm.exceptions.ParameterNotFound:
raise ValueError(f"SSM parameter not found: {parameter_name}")
except Exception as e:
raise ValueError(f"Failed to retrieve SSM parameter {parameter_name}: {e}")