Summary
Inside an AgentCore Runtime container, BedrockAgentCoreContext.get_workload_access_token() returns None whenever the agent is served by a plain FastAPI/Starlette ASGI app rather than BedrockAgentCoreApp. This breaks @requires_api_key and @requires_access_token, plus any imperative IdentityClient.get_api_key() callers.
While debugging, I found three sequential silent failures that all need fixing for the documented identity flow to actually work end-to-end. Filing as one issue because they layer — masking each other if you only fix the top one.
Reproduction
- Build an AgentCore Runtime image whose ASGI entrypoint is a plain
fastapi.FastAPI() app exposing POST /invocations (rather than BedrockAgentCoreApp + @app.entrypoint).
- The agent does work in a background thread spawned via
threading.Thread(target=run_pipeline, ...) (common pattern for return-200-fast async work).
- Inside the pipeline, call
BedrockAgentCoreContext.get_workload_access_token(). Observe None.
- Caller-side: orchestrator Lambda calls
InvokeAgentRuntimeCommand({ ..., runtimeUserId: 'cognito-sub' }) with all required IAM (InvokeAgentRuntime + InvokeAgentRuntimeForUser).
- Runtime role has
bedrock-agentcore:GetResourceApiKey + GetWorkloadAccessToken* + bedrock-agentcore-identity!* SLR exists.
Three silent failures, in the order you hit them
1. Plain FastAPI bypasses _build_request_context
The SDK populates the ContextVar via _build_request_context in bedrock_agentcore/runtime/app.py (called from BedrockAgentCoreApp._handle_invocation lines 537+). Plain FastAPI/Starlette apps bypass this entirely — the middleware never installs, so BedrockAgentCoreContext.set_workload_access_token(...) is never called even though the platform delivered the token on the wire.
Header diagnostic (logged from inside our handler against a SigV4-authorized runtime in us-east-1, 2026-05-18) — the platform sends the token under TWO header spellings on the same request:
HEADER (token-shaped) workloadaccesstoken: AgV4diWP... # matches ACCESS_TOKEN_HEADER constant
HEADER (token-shaped) x-amzn-bedrock-agentcore-runtime-workload-accesstoken: AgV4diWP... # undocumented
The undocumented long-form should either be removed or aliased explicitly. Today, code that reads only WorkloadAccessToken per the SDK constant is correct; code that reads the long form is relying on undocumented behavior.
2. ContextVar is per-thread
After bridging the header in user middleware, get_workload_access_token() still returned None in our pipeline. Cause: Python ContextVar storage is per-thread, not propagated across threading.Thread boundaries. Setting the token in the request handler thread doesn't reach a pipeline thread spawned from the handler.
This isn't called out in the BedrockAgentCoreContext docstring or in the runtime-oauth.html documentation. Issue #219 alludes to it (ContextVar is "safe under ASGI") but doesn't surface the threading caveat that bites users running long pipelines outside the request task.
3. Runtime role needs secretsmanager:GetSecretValue on bedrock-agentcore-identity!*
After (1) and (2) were fixed, IdentityClient.get_api_key() actually fired and got:
AccessDeniedException: ... not authorized to perform: secretsmanager:GetSecretValue
AgentCore Identity stores api-key credentials in Secrets Manager under reserved prefix bedrock-agentcore-identity!*. The GetResourceApiKey API surfaces the underlying secret to the caller, and AWS verifies the caller role (the runtime execution role) has GetSecretValue on the actual secret resource — not the SLR. This is not documented in https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-oauth.html or in the GetResourceApiKey API reference.
Tightly-scoped fix:
runtime_role.addToPrincipalPolicy(new iam.PolicyStatement({
actions: ['secretsmanager:GetSecretValue'],
resources: [
f'arn:aws:secretsmanager:{region}:{account}:secret:bedrock-agentcore-identity!*',
],
}))
This is an SDK-adjacent issue rather than a strict SDK bug — but the right place to surface it is alongside the other Identity-flow gotchas, and the SDK's IdentityClient.get_api_key could include the canonical IAM grant in its docstring.
Workaround
# 1. Read the workload token off the request and thread it explicitly into
# the background pipeline so the new thread can re-set the ContextVar.
def _extract_workload_access_token(request: Request) -> str:
return (
request.headers.get("WorkloadAccessToken")
or request.headers.get("x-amzn-bedrock-agentcore-runtime-workload-accesstoken")
or ""
)
@app.post("/invocations")
async def invoke(request: Request, body: ...):
token = _extract_workload_access_token(request)
# ... start background thread, pass token as kwarg ...
spawn(target=run_pipeline, kwargs={..., 'workload_access_token': token})
return {"accepted": True}
def run_pipeline(*, workload_access_token: str = "", **kwargs):
if workload_access_token:
from bedrock_agentcore.runtime.context import BedrockAgentCoreContext
BedrockAgentCoreContext.set_workload_access_token(workload_access_token)
# ... rest of pipeline runs in this thread; @requires_api_key works now ...
Plus IAM grant on the runtime execution role (above).
Expected behavior
At least one of:
-
An exported, opt-in middleware: bedrock_agentcore.runtime.middleware.BedrockAgentCoreContextMiddleware users can app.add_middleware(...) to get parity with BedrockAgentCoreApp for the request-handler path. Combined with a clearly-documented per-thread propagation pattern in the docstring.
-
OR a BedrockAgentCoreContext API that doesn't depend on ContextVar: e.g., BedrockAgentCoreContext.from_request(request) that reads the headers directly and returns a context object the user can pass into @requires_api_key(context=...). Sidesteps both the middleware gap and the threading gap.
-
AND: docstring updates on BedrockAgentCoreContext, IdentityClient, and the Identity getting-started doc that explicitly call out:
- "ContextVar is per-thread; if you spawn background threads, re-set the token on entry"
- "Caller IAM principal must have
secretsmanager:GetSecretValue on bedrock-agentcore-identity!*"
- The undocumented long-form header — either remove or formally alias
Environment
bedrock-agentcore 1.9.1 (PyPI)
- AgentCore Runtime: ARM64 container, Python 3.13, FastAPI 0.136.1, uvicorn 0.47.0
- Region:
us-east-1
- Inbound auth: SigV4 (default IAM, no
AuthorizerConfiguration set)
PR proposal
I'm willing to send a PR for the middleware + docstring updates. Two questions before I draft:
- Shape preference? Opt-in
BedrockAgentCoreContextMiddleware that users explicitly add (option 1 above) vs. helper API on BedrockAgentCoreContext (option 2)? The middleware is a smaller diff but only fixes the request-handler path; the helper is more invasive but addresses the threading gotcha cleanly.
- Should the threading caveat live in code (e.g., a
BedrockAgentCoreContext.copy_for_thread() helper) or just docs?
Happy to send whichever shape maintainers prefer.
Summary
Inside an AgentCore Runtime container,
BedrockAgentCoreContext.get_workload_access_token()returnsNonewhenever the agent is served by a plainFastAPI/StarletteASGI app rather thanBedrockAgentCoreApp. This breaks@requires_api_keyand@requires_access_token, plus any imperativeIdentityClient.get_api_key()callers.While debugging, I found three sequential silent failures that all need fixing for the documented identity flow to actually work end-to-end. Filing as one issue because they layer — masking each other if you only fix the top one.
Reproduction
fastapi.FastAPI()app exposingPOST /invocations(rather thanBedrockAgentCoreApp+@app.entrypoint).threading.Thread(target=run_pipeline, ...)(common pattern for return-200-fast async work).BedrockAgentCoreContext.get_workload_access_token(). ObserveNone.InvokeAgentRuntimeCommand({ ..., runtimeUserId: 'cognito-sub' })with all required IAM (InvokeAgentRuntime+InvokeAgentRuntimeForUser).bedrock-agentcore:GetResourceApiKey+GetWorkloadAccessToken*+bedrock-agentcore-identity!*SLR exists.Three silent failures, in the order you hit them
1. Plain FastAPI bypasses
_build_request_contextThe SDK populates the ContextVar via
_build_request_contextinbedrock_agentcore/runtime/app.py(called fromBedrockAgentCoreApp._handle_invocationlines 537+). Plain FastAPI/Starlette apps bypass this entirely — the middleware never installs, soBedrockAgentCoreContext.set_workload_access_token(...)is never called even though the platform delivered the token on the wire.Header diagnostic (logged from inside our handler against a SigV4-authorized runtime in us-east-1, 2026-05-18) — the platform sends the token under TWO header spellings on the same request:
The undocumented long-form should either be removed or aliased explicitly. Today, code that reads only
WorkloadAccessTokenper the SDK constant is correct; code that reads the long form is relying on undocumented behavior.2. ContextVar is per-thread
After bridging the header in user middleware,
get_workload_access_token()still returned None in our pipeline. Cause: PythonContextVarstorage is per-thread, not propagated acrossthreading.Threadboundaries. Setting the token in the request handler thread doesn't reach a pipeline thread spawned from the handler.This isn't called out in the
BedrockAgentCoreContextdocstring or in the runtime-oauth.html documentation. Issue #219 alludes to it (ContextVaris "safe under ASGI") but doesn't surface the threading caveat that bites users running long pipelines outside the request task.3. Runtime role needs
secretsmanager:GetSecretValueonbedrock-agentcore-identity!*After (1) and (2) were fixed,
IdentityClient.get_api_key()actually fired and got:AgentCore Identity stores api-key credentials in Secrets Manager under reserved prefix
bedrock-agentcore-identity!*. TheGetResourceApiKeyAPI surfaces the underlying secret to the caller, and AWS verifies the caller role (the runtime execution role) hasGetSecretValueon the actual secret resource — not the SLR. This is not documented in https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-oauth.html or in theGetResourceApiKeyAPI reference.Tightly-scoped fix:
This is an SDK-adjacent issue rather than a strict SDK bug — but the right place to surface it is alongside the other Identity-flow gotchas, and the SDK's
IdentityClient.get_api_keycould include the canonical IAM grant in its docstring.Workaround
Plus IAM grant on the runtime execution role (above).
Expected behavior
At least one of:
An exported, opt-in middleware:
bedrock_agentcore.runtime.middleware.BedrockAgentCoreContextMiddlewareusers canapp.add_middleware(...)to get parity withBedrockAgentCoreAppfor the request-handler path. Combined with a clearly-documented per-thread propagation pattern in the docstring.OR a
BedrockAgentCoreContextAPI that doesn't depend on ContextVar: e.g.,BedrockAgentCoreContext.from_request(request)that reads the headers directly and returns a context object the user can pass into@requires_api_key(context=...). Sidesteps both the middleware gap and the threading gap.AND: docstring updates on
BedrockAgentCoreContext,IdentityClient, and the Identity getting-started doc that explicitly call out:secretsmanager:GetSecretValueonbedrock-agentcore-identity!*"Environment
bedrock-agentcore1.9.1(PyPI)us-east-1AuthorizerConfigurationset)PR proposal
I'm willing to send a PR for the middleware + docstring updates. Two questions before I draft:
BedrockAgentCoreContextMiddlewarethat users explicitly add (option 1 above) vs. helper API onBedrockAgentCoreContext(option 2)? The middleware is a smaller diff but only fixes the request-handler path; the helper is more invasive but addresses the threading gotcha cleanly.BedrockAgentCoreContext.copy_for_thread()helper) or just docs?Happy to send whichever shape maintainers prefer.