Skip to content

feat(agentcore): serve the built-in MCP through AgentCore Gateway (opt-in) - #613

Open
eipasteur wants to merge 5 commits into
mainfrom
feat/agentcore-gateway-mcp
Open

eipasteur wants to merge 5 commits into
mainfrom
feat/agentcore-gateway-mcp

Conversation

@eipasteur

@eipasteur eipasteur commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

In short

Opt-in path that serves the built-in aidlc MCP through AgentCore Gateway instead of inside the agent's microVM (enable_mcp_gateway, default false, requires network_mode = "VPC").

  • A stdio proxy in the runtime registers the same tool catalog and forwards each call, SigV4-signed, to an AWS_IAM Gateway. The CLIs keep the same aidlc server and tool names.
  • The Gateway's Lambda target (lambda/mcp-tools) runs the existing graph and process handlers.
  • v2-orchestrator and discussions sign an ES256 grant with an asymmetric KMS key. The key policy allows kms:Sign only to those roles; the tools Lambda holds only kms:GetPublicKey.
  • The grant names the space, intent, execution, stage lane and leg, mode and release-policy digest. It travels in a propagated header, never in tool arguments, and is not exposed to command handlers or the agent CLI.
  • On every call the Lambda verifies the grant, rejects session context outside it, re-reads the execution and stage row (a finished, cancelled or superseded leg is denied), enforces the role's tool catalog and drops unknown arguments.
  • Every allow, deny and handler error is logged with its scope.
  • With the Gateway enabled and no grant present, the runtime refuses to start the MCP instead of falling back to the local server.
  • A grant is refused once its run is retired on the execution, even while a stage row still names that run (the window between a rewind or recompose retiring the run and resetting the stage rows).

MCP core modules move to lambda/shared/mcp/; re-export shims in lambda/agentcore/mcp/ keep existing imports working.

Scope

First of four PRs: this one, then engine-data, then the credential broker and source-control alias, then the removal of the runtime role's direct data permissions. This PR changes where the built-in tools run and how each call is authorized. It does not reduce the runtime role's permissions on its own; the fourth PR removes them once the platform runs in strict mode.

Deliberately not included: an agent or persona identity in the grant. The persona is known only to the runtime, so an unsigned value would be forgeable by a compromised agent and could not be relied on for authorization or audit. Per-persona grants make sense together with CLI sandboxing and are a follow-up.

Accepted residual risks:

  • The sub-role inside a granted stage (author, reviewer, reader) is asserted by the runtime. A forged session can raise it within its own stage, never reach another stage, execution or space.
  • Discussion grants rely on a 300 s TTL.
  • Durable callbacks stay on the runtime role.
  • Older managed-environment images ignore the new env vars and keep the local MCP path.

Validation

  • Rebased on main. Full backend suite passes on this branch alone (6168 tests) and on the stack head; test:release passes, including the new active-execution check tests.
  • lint, format:check, dep:check, terraform fmt, terraform validate and the CI Terraform tests pass.
  • Deployed in the PR sandbox with the flag on, together with the follow-up PRs: Claude and OpenCode full runs to SUCCEEDED (validation and question gates, park/resume, reviewer verdicts), every tool call allowed and audited; calls with a missing or invalid grant denied and audited; cross-space reads return nothing. Kiro not exercised.

Upgrade notes

With the flag off, every new resource is count-gated and the grant key env var is not set. Enabling it creates the Gateway, target, tools Lambda, KMS key, security group and schema object, and switches the runtime to the proxy. The new roles follow iam_role_path and iam_permissions_boundary_arn.

Executions started before the flag have no grant issuer and would fail the next time they invoke the runtime. deploy-terraform.sh therefore refuses a plan that switches the runtime to the Gateway while any execution is started, running or parked, and lists them. Stop starting intents until the deployment has succeeded, let running executions park or finish, cancel the parked and queued ones, apply, then rewind each to its current stage (its first stage if it had not reached one): the rewind relaunches it with grants. AIDLC_ALLOW_ACTIVE_EXECUTIONS=1 applies anyway. A direct terraform apply skips the check.

Comment on lines +647 to +650
if (!grant) {
throw Object.assign(new Error('Gateway MCP requires a control-plane grant'), {
code: 'GATEWAY_GRANT_MISSING',
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Preserve existing executions before enabling the mandatory-grant runtime path

@eipasteur An execution started with Gateway disabled can park at a human gate, lose its warm runtime session, and then resume after enable_mcp_gateway is enabled. Its published durable orchestrator still has the old configuration and no grant issuer, while the standard runtime can restart on the new configuration: standard revisions have runtimeEndpoint: null, and resolveRuntimeTarget() does not pin the invocation using runtimeVersion. The resumed request therefore reaches this check without a grant and fails with GATEWAY_GRANT_MISSING.

This also affects executions started after deploying the new code with flags off. In #614's engine-data mode, the request fails even earlier: resolveInvocationAgentAuth() reads META through the remote store before calling the broker. My local reproduction using the real dispatcher/auth resolver/store returned HTTP 500 with No engine data grant for this invocation; broker and handler calls were both zero. Leaving enforce_runtime_grants=false does not protect that path.

Please either keep existing executions routed to a compatible pinned runtime revision/configuration, or require and enforce draining them before the first remote-path flag is enabled. The rollout guide in #616 currently drains them only after enabling Gateway and engine-data. Keep the missing-grant rejection itself; an unauthenticated fallback would weaken the boundary.

Regression/acceptance checks:

  • Start an execution with flags off, park it, stop its runtime session, enable Gateway, and resume it.
  • Repeat for engine-data transition mode with strict enforcement off.
  • Verify compatible resumption, or prevent activation while such executions remain; include parked executions in the drain condition.
  • Keep missing/forged grants rejected on the remote path.

The failure was reproduced locally at stack head 22ec812c; the live AWS migration scenario was not exercised.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, thanks for the repro. I kept the missing-grant rejection and went with draining before activation, enforced rather than documented:

  • scripts/check-active-executions.mjs runs in deploy-terraform.sh on every saved plan (and again at apply). It detects the runtime gaining the Gateway (this PR) or engine-data (feat(engine-data): route the runtime's engine data through grant-verified engine-data (opt-in) #614) configuration, so a retry after an apply that failed before the runtime was updated is still caught, and refuses the plan while any execution is CREATED, RUNNING or WAITING, listing them. Fresh installs and plans that switch nothing skip the table read.
  • Operator path: stop starting intents until the deployment has succeeded, let running executions park or finish (/cancel does not accept RUNNING), cancel the parked and queued ones, apply, then rewind each to its current stage (its first stage if it had not reached one). The rewind starts a new orchestrator run on the new configuration, so it relaunches with grants; artifacts and workspace are kept.
  • AIDLC_ALLOW_ACTIVE_EXECUTIONS=1 overrides, and still prints the executions that will fail.
  • The rollout guide in feat(terraform): remove the runtime role's direct data access under enforced runtime grants #616 now drains before each of the two flags, not after.

Tests cover activation per flag, retry after a partial apply, queued/running/parked executions vs drafts, fresh install, read failures and the override; the script was also run against a real process table. Not exercised: the live migration scenario on a deployed stack. A direct terraform apply skips the check (documented), and new starts during the apply are prevented by the procedure, not enforced. Pinning the runtime revision per execution would allow migrating without the drain, but it is a much larger change; happy to take it as a follow-up if we need hot migration.

Comment on lines +149 to +151
if (requireActive && !ACTIVE_EXECUTION_STATUSES.includes(execution.status)) {
throw new Error('MCP execution is not active');
}

@jeromevdl jeromevdl Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 — Reject grants after the execution’s run ID changes.

startup-context.js:149–151 checks the execution’s status, but compares orchestratorRunId only against the stage row. Recompose retires the execution’s run ID before resetting stage rows. During that interval, the execution can remain WAITING and the stage still carries the old run/callback IDs. The Gateway therefore accepts the retired grant. I reproduced this through the real grant signer, verifier, and Gateway handler: a signed old-run grant reached a stubbed createArtifact writer and was audited as allow/ok. DynamoDB write fencing does not protect that graph mutation, so stale work can modify artifacts after recompose has archived them.

Fix: compare the signed run ID with execution META during authorization, alongside the existing stage check. Add a regression test where META is retired but the stage still names the old run. This belongs in #613’s existing authorization logic; it does not require importing #615’s retirement machinery.

@eipasteur eipasteur Oct 27, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3463512: authorization now also compares the signed run with META.orchestratorRunId. The regression test has META retired while the stage row still names the old run; it fails without the fix. The same check is in engine-data's stage grants (#614, 84e5efe), since graph writes have no store-side fence there either.

@jeromevdl jeromevdl left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apart from the one P2, the stack boundaries are good with #614, #615 and #616., architecture is reasonable (SOLID/KISS).
Also need a rebase.

@eipasteur

Copy link
Copy Markdown
Contributor Author

Rebased on main, including #603: the new roles (mcp-tools, mcp-gateway, engine-data) follow iam_role_path and the permissions boundary, and the signer role ARNs in the grant key policy include the path.

@eipasteur
eipasteur force-pushed the feat/agentcore-gateway-mcp branch from 0bd1cc0 to 89d4922 Compare October 28, 2026 00:29
@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Complexity and test coverage

Automated quality report using code complexity and test coverage to highlight potential hot spots in this PR.
Review 🟣 and 🔴 items first: consider simplifying the code, improving test coverage, or both. This report is advisory.

File / function Complexity Δ Branch coverage Δ Uncovered branches Depending files
🟠 lambda/shared/gateway-grant.js:202 — verifyGatewayGrant — → 17 — — → 81.1% — — → 10 5
🟠 lambda/v2-orchestrator/index.js:2712 — runStage / <anonymous> — → 18 — — → 86.7% — — → 6 0
🟠 lambda/shared/mcp/startup-context.js:133 — validateStartupContext — → 28 — — → 97.6% — — → 1 3

🟢 Improving · 🟠 Needs attention · 🔴 High risk · 🟣 Highest risk

— = unavailable · n/a = not applicable · * = incomplete tests

CI run and full report artifacts

@jeromevdl jeromevdl left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eipasteur Previous findings are solved, thank you.

Two focused questions on the Gateway integration after our meeting this morning:

  • JWT validation: Is there a reason we use AWS_IAM inbound plus a custom JWT verified in the target Lambda, rather than Gateway’s native CUSTOM_JWT authorizer? I understand execution/stage checks would remain application-owned, and direct Lambda calls still need verification. Would native validation simplify the MCP path, or introduce more infrastructure than it removes?
  • Tool permissions: Did you consider AgentCore Policy for the tool allowlist? Would it replace meaningful application code, or mostly duplicate the existing shared rules? Since the runtime currently asserts the role, would trusted role claims be a prerequisite?

Please also rebase the stack.

@jeromevdl jeromevdl added area/agents AgentCore runtime: CLI drivers, MCP tools, workspace and Git operations area/backend Lambda APIs, lambda/shared, v2-orchestrator area/infra Terraform, installer and deployment area/ci GitHub Actions, hooks, lint, release tooling, E2E harness size/XL 3000+ changed lines (lockfiles excluded), consider splitting labels Oct 30, 2026
…t-in)

With enable_mcp_gateway (requires VPC networking) the runtime no longer
runs the built-in aidlc MCP server in the agent's microVM. A stdio proxy
registers the same tools and forwards each call, SigV4-signed, to an
AWS_IAM AgentCore Gateway whose Lambda target (mcp-tools) runs the existing
graph and process handlers.

The orchestrator and discussions Lambdas sign a short ES256 grant with a
KMS key only they may use. It names the space, intent, execution, stage
lane and leg, mode and release-policy digest, travels in a propagated
header and never reaches command handlers or the agent CLI. On every call
the tools Lambda verifies it, rejects session context outside it, re-reads
the execution and stage row (a finished, cancelled or superseded leg is
denied), enforces the role's tool catalog and drops unknown arguments, and
audits each allow, deny and handler error with its scope. Without a grant
the runtime refuses to start the MCP instead of falling back.

The tools Lambda gets scoped DynamoDB KMS access for customer-managed keys
and may only post to websocket connections. The MCP core moves to
lambda/shared/mcp with re-export shims. The runtime role is unchanged.
@eipasteur
eipasteur force-pushed the feat/agentcore-gateway-mcp branch from 89d4922 to ef53c76 Compare October 30, 2026 18:39
@eipasteur

Copy link
Copy Markdown
Contributor Author

Tool permissions. Not in this PR. With AWS_IAM inbound, Policy sees the runtime role's ARN, which is the same for every agent, and the role inside a stage (author, reviewer, reader) is asserted by the runtime in the session header. A Policy allowlist would duplicate the shared rules without replacing any code, so yes, trusted role claims are the prerequisite. It becomes worthwhile once the role is a signed claim and the Gateway fronts more than the built-in target, which is the custom MCP follow-up from Wednesday. I'll answer the JWT question separately.

Rebase. The stack is rebased on main, including #534 and #623. Two tests needed adapting to the new contracts; the production code did not change:

I also documented the Gateway trust boundary in architecture.md, including what the grant does and does not protect against.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/agents AgentCore runtime: CLI drivers, MCP tools, workspace and Git operations area/backend Lambda APIs, lambda/shared, v2-orchestrator area/ci GitHub Actions, hooks, lint, release tooling, E2E harness area/infra Terraform, installer and deployment size/XL 3000+ changed lines (lockfiles excluded), consider splitting

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants