Skip to content

Add an agentcore_gateway model backend: per-session credentials, no key on the platform side - #40

Open
zzkamzn wants to merge 2 commits into
aws-samples:mainfrom
zzkamzn:feat/agentcore-gateway-model-backend
Open

zzkamzn wants to merge 2 commits into
aws-samples:mainfrom
zzkamzn:feat/agentcore-gateway-model-backend

Conversation

@zzkamzn

@zzkamzn zzkamzn commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

What

A third model backend, agentcore_gateway, 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 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:

  • 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 — that is how one session is stopped without disturbing another. The Deny set is rebuilt from an 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 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 so it 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.

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.py provisions a live gateway, interceptor Lambda 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:

single turn Claude Code completes it through the chain, and the shim observed the request (so it cannot be satisfied by some ambient credential)
multi-turn task with tools write a file → run it → report a line: 3 model calls, tool_result blocks accumulating 0→1→2, request body growing 112 KB → 114 KB, all streaming
scoping the same credential is refused (403) on a second gateway
revocation session A stops working ~9 s after revoke(); session B keeps working
fail-fast the shim refuses a model the session was not routed to (403)

Claude 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)

  • ⚠️ 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. Written up 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 (verified).
  • ⚠️ 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.

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

  • Terraform for the caller role. The role, its 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.
  • Nothing changes for existing deployments. agentcore_gateway ships 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 main and is independent of #39, which touches llm_credentials_service.py too. Whichever merges second will want a look at the ownership ConditionExpression in mint() / mint_agentcore() — the two express the same rule in their own paths.

🤖 Generated with Claude Code

zzkamzn and others added 2 commits September 18, 2026 03:02
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant