Add an agentcore_gateway model backend: AgentCore Gateway in front of LiteLLM, per-session credentials - #40
Merged
odinwang merged 4 commits intoOct 1, 2026
Conversation
…tials Adds an agentcore_gateway model backend alongside bedrock and litellm. It reaches the same goal as llm-edge — no upstream model credential ever lands in a container whose user is root — with one fewer service to run: the provider credential lives in the gateway's token vault, so there is nothing on the platform side to hold either. A session's credential is an STS session tagged with its runtime session id, minted by the backend and delivered in the existing llm_credentials block: - a session policy narrows it to InvokeGateway on the one gateway the backend points at, so a credential read out of kernel memory cannot reach any other gateway in the account; - the session tag is what revoke() conditions a Deny on, which is how one session is stopped without disturbing another. The Deny set is rebuilt from a LLMREVOKE partition and pruned once entries outlive the credential they revoke, keeping the inline policy inside its 10 KB limit; - STS cannot extend a session, so rotate() re-mints from the routing already recorded on the token item, preserving the "config edits apply at next warmup" rule the edge path already has. Claude Code cannot sign SigV4, which is exactly why the signing belongs in the kernel shim rather than the CLI subprocess: both shims now sign per request (botocore in the SDK kernel, ~40 lines of node:crypto in the contract server, which keeps its single dependency). Only a minimal header set is signed and the caller's headers are attached afterwards, so Claude Code's anthropic-* and x-stainless-* headers cannot invalidate a signature; its own x-amz-* headers are dropped rather than folded in. The SigV4 path buffers the request body because it needs the payload hash, and is capped; the edge path still streams it through unread. What this backend deliberately does not claim: IAM conditions cannot see a request body, so the per-session model allowlist is not expressible as an IAM condition. The shim's check is a fail-fast optimisation and says so — the session's user is root in that microVM and can bypass it. Real enforcement needs a gateway request interceptor or one gateway per model; the allowlist is recorded on the token item either way so the decision stays auditable. scripts/e2e_agentcore_gateway.py provisions a live gateway and drives the real Claude Code CLI through the real shim, asserting the chain end to end: a multi-turn tool-using task completes over three model calls, the credential is refused on a second gateway, and revoking one session leaves another working. Terraform for the caller role is not included yet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Folds the third model backend into the existing model-access sections rather than adding a new page: architecture.md's backend table, security-explainer §9 (which gains the second call chain), deployment.md §2 as Option A2, permissions.md's principal table, the user guide's Governance card, and the EXTENDING.md configuration index. Three things the evaluation turned up that operators need in writing: - The kernel roles already hold InvokeGateway on gateway/* so they can reach MCP tool gateways, and that wildcard covers an inference gateway too — a root user in the microVM could call it on the role's own identity, with no session tag to revoke. Enabling this backend requires an explicit Deny for the inference gateway's ARN; without it the per-session credential is decorative. Documented as a prerequisite in deployment.md and permissions.md. - A gateway interceptor's escaping exception is relayed to the caller with its full stack trace, regardless of exceptionLevel — and the caller is the tenant's microVM. Interceptors must catch everything themselves. The reassuring half: interceptor failure is fail-closed. - allowedRequestHeaders is not optional. Left unset the gateway relays the caller's own x-amz-security-token; once set, a connector target stops relaying content-type unless it is listed. Also corrects two claims §9.4 and §9.5 had been overstating, independent of this backend: the platform's quota counts invocations rather than tokens, so there is no per-user token ceiling on either gateway mode; and credential reuse across sessions is not prevented by either, because nothing proves a caller is the session it claims to be. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Repoint the gateway target from the bedrock-mantle connector to an inference *provider* target in front of LiteLLM, which is what this backend is for: the LiteLLM virtual key lives in an AgentCore Identity API-key credential provider and the gateway injects it, so neither the platform nor the container holds it and llm-edge is not needed. - Target: endpoint = LiteLLM, operations /v1/messages, /v1/chat/completions and /v1/responses with the models the virtual key permits. The gateway does no protocol translation (it matches path and model, swaps the key, relays body and SSE verbatim), so which model works on which path is LiteLLM's call; all four test models answered on all three. credentialPrefix "Bearer", no trailing space (the gateway adds it). allowedRequestHeaders keeps anthropic-beta, which LiteLLM accepts. - Gateway execution role: GetWorkloadAccessToken as well as GetResourceApiKey (the documented policy alone fails with "Failed to get workload identity token"), plus the provider's secret. - Private path: a top-level privateEndpoint.managedVpcResource places VPC Lattice resource-gateway ENIs in the LiteLLM VPC and connects to an internal ALB (routingDomain) while keeping the public cert name as SNI. Driven by LITELLM_ROUTING_DOMAIN / LITELLM_VPC_ID / LITELLM_SUBNET_IDS / LITELLM_LATTICE_SG_IDS. - No REQUEST interceptor: drop_params on the LiteLLM model entries strips Claude Code's first-party-only fields (context_management). An interceptor adds a Lambda hop per call and caps bodies at ~4.5 MB. - e2e: [multimodel] smoke over every model on the key; SSO reserved roles trusted via the account root; teardown waits for private-endpoint targets; LITELLM_ENDPOINT / LITELLM_CREDENTIAL_PROVIDER_ARN required. - deployment.md Option A2 rewritten for LiteLLM, including the 350-second Lattice idle timeout that makes long non-streaming calls hang. Verified end to end in ap-northeast-1 through the private endpoint: chain, multi-turn tool task, multi-model, failfast, scoping, revoke (8.6 s). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2 tasks done
3 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Update (maintainer, 2026-10-01): gateway now fronts LiteLLM
Two commits pushed on top: a merge of current
main, and292d41b.bedrock-mantleconnector. The LiteLLM virtual key lives in an AgentCore Identity API-key credential provider and the gateway injects it, so LiteLLM keeps its multi-provider routing and cost accounting andllm-edgeis not needed. Operations declare/v1/messages,/v1/chat/completionsand/v1/responses. The gateway does no protocol translation (it matches path +model, swaps the key, relays body and SSE verbatim), so which model works on which path is LiteLLM's call. All four test models (claude-opus-5-5,claude-sonnet-5-5,gpt-6-astra,kimi-k3) answered on all three paths.drop_paramson the LiteLLM model entries strips Claude Code'scontext_management. An interceptor adds a synchronous Lambda call per model request and caps bodies at about 4.5 MB (Lambda's 6 MB invoke payload after base64): 3.1 MB passed, 4.7 MB was refused, 26 MB went straight through without it.privateEndpoint.managedVpcResource(top-level, next totargetConfiguration) puts VPC Lattice resource-gateway ENIs in the LiteLLM VPC and connects to an internal ALB viaroutingDomain, keeping the public cert name as SNI. ALB access logs show every request arriving from the two Lattice ENIs.credentialPrefixis"Bearer"with no trailing space (the gateway adds one); the execution role also needsGetWorkloadAccessToken; the gateway rejects any path outside those three, includingcount_tokens, at target-creation time.Runtime → gateway traffic can stay inside the VPC too, via the gateway interface endpoint in #45.
What
A third model backend,
agentcore_gateway, alongsidebedrockandlitellm. It reaches the same goal asllm-edge— no upstream model credential ever lands in a container whose user is root — with one fewer service to run: the provider credential lives in the AgentCore Gateway's token vault, so there is nothing on the platform side to hold either.Two commits: code, then docs.
How a session is authorized
The credential a kernel receives is an STS session tagged with its runtime session id:
InvokeGatewayon the one gateway the backend points at, so a credential read out of kernel memory cannot reach any other gateway in the account;revoke()conditions a Deny on — that is how one session is stopped without disturbing another. The Deny set is rebuilt from anLLMREVOKEpartition and pruned once entries outlive the credential they revoke, keeping the inline policy inside its 10 KB limit;rotate()re-mints from the routing already recorded on the token item, preserving the "config edits apply at next warmup" rule the edge path has.Claude Code cannot sign SigV4, which is exactly why the signing belongs in the kernel shim rather than the CLI subprocess. Both shims now sign per request — botocore in the SDK kernel, ~40 lines of
node:cryptoin the contract server so it keeps its single dependency. Only a minimal header set is signed and the caller's headers are attached afterwards, so Claude Code'santhropic-*andx-stainless-*headers cannot invalidate a signature; its ownx-amz-*headers are dropped rather than folded in.What this backend deliberately does not claim
IAM conditions cannot see a request body, so the per-session model allowlist is not expressible as an IAM condition. The shim's check is a fail-fast optimisation and says so in the code — the session's user is root in that microVM and can bypass the shim. Real enforcement needs a gateway request interceptor or one gateway per model. The allowlist is recorded on the token item either way so the decision stays auditable.
Verification
scripts/e2e_agentcore_gateway.pyprovisions a live gateway and caller role, then drives the real Claude Code CLI through the real kernel shim and tears everything down. Nothing in the chain is stubbed; the only simulation is that the shim runs in the test process rather than inside AgentCore Runtime, which is the same place it runs in production.It asserts, and currently passes:
tool_resultblocks accumulating 0→1→2, request body growing 112 KB → 114 KB, all streamingrevoke(); session B keeps workingClaude Code sent exactly one path throughout:
POST /v1/messages?beta=true. It never calls/v1/messages/count_tokens, which AgentCore Gateway does not accept inbound — so that gap is not a blocker.Things operators need to know (all documented)
InvokeGatewayongateway/*so they can reach MCP tool gateways, and that wildcard covers an inference gateway too. A root user in the microVM could call it on the role's own identity, with no session tag to revoke. Enabling this backend requires an explicit Deny for the inference gateway's ARN — without it the per-session credential is decorative. Written up as a prerequisite indeployment.mdandpermissions.md.exceptionLevel— and the caller is the tenant's microVM. Interceptors must catch everything themselves. The reassuring half: interceptor failure is fail-closed (verified).allowedRequestHeadersis not optional. Left unset the gateway relays the caller's ownx-amz-security-token; once set, a connector target stops relayingcontent-typeunless it is listed.The docs commit also corrects two claims
security-explainer.zh.md§9.4/§9.5 had been overstating, independent of this backend: the platform's quota counts invocations rather than tokens (so there is no per-user token ceiling on either gateway mode), and credential reuse across sessions is not prevented by either mode, because nothing proves a caller is the session it claims to be.Not included
MaxSessionDuration, the gateway and the kernel-role Deny are described in the docs but not yet a module. Deliberate: I could not exercise a Terraform path end to end here.agentcore_gatewayships disabled, and with the caller role ARN unset the backend refuses to route rather than falling back to a shared credential.Note on stacking
This branch is rebased directly onto
mainand is independent of #39, which touchesllm_credentials_service.pytoo. Whichever merges second will want a look at the ownershipConditionExpressioninmint()/mint_agentcore()— the two express the same rule in their own paths.🤖 Generated with Claude Code