feat(registry): federate agent tools behind an AgentCore Gateway — P1 (#641) - #755
Conversation
…aws-samples#641) Implements the P1 slice of ADR-019: a single read-only Lambda tool (`abca_repo_config`) exposed through a managed AgentCore Gateway with SigV4 (AWS_IAM) inbound auth and gateway-execution-role outbound (no credential). The whole feature is context-gated — resources synthesize only under `--context enableToolGateway=true`, so the default synth is byte-for-byte unchanged and no new CFN types enter the bootstrap policy set. CDK - constructs/tool-gateway.ts: AgentCore L2 Gateway (usingAwsIam, explicit — L2 default is Cognito), one Lambda target (GATEWAY_IAM_ROLE outbound), inline tool schema, read-only RepoTable GetItem, IAM4/IAM5 nag suppressions, public grantInvoke(). - handlers/tool-repo-config.ts: curated read-only lookup (secret ARNs omitted); resolves the federated tool name, validates the owner/name slug, returns {onboarded:false} for un-onboarded repos. - stacks/agent.ts + constructs/ecs-agent-cluster.ts: context gate + substrate parity — both the AgentCore runtime role and the ECS task role get bedrock-agentcore:InvokeGateway and ABCA_TOOL_GATEWAY_URL. Agent - gateway_tools.py: in-process SigV4-signing MCP bridge (not the stdio proxy ADR-019 originally sketched — the SDK's remote MCP client can only attach a static Bearer header, so it cannot sign SigV4). Registers an SDK MCP server that signs each Streamable-HTTP request with the compute role's ambient creds (service bedrock-agentcore) and proxies to the federated tool by suffix. Inert unless ABCA_TOOL_GATEWAY_URL is set. - runner.py: register the bridge when the feature is deployed. Tests: tool-gateway (7), tool-repo-config, agent gateway bridge (18) — new files at 100%. Full CDK + agent suites pass; masking-scan fallback allowlisted with a justified inline nosemgrep. Docs: ADR-019 P1 row updated to record the in-process-bridge deviation; Starlight mirror regenerated.
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #755 +/- ##
=======================================
Coverage ? 91.97%
=======================================
Files ? 301
Lines ? 84357
Branches ? 8415
=======================================
Hits ? 77589
Misses ? 6768
Partials ? 0 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
… parity, type safety Blocking: - B1: map AWS::BedrockAgentCore::Gateway + ::GatewayTarget in the bootstrap least-privilege resource-action map (create actions covered by the existing bedrock-agentcore:* grant), matching the lambda-microvm gated-type precedent. Add a gated synth-coverage regression test proving both types are mapped. - B2: assert substrate parity — with enableToolGateway + compute_type=ecs, the ECS task defs carry ABCA_TOOL_GATEWAY_URL and the task role is granted bedrock-agentcore:InvokeGateway. This is isadeks' both-substrates ADR bar. Non-blocking: - N1: replace gateway.gatewayUrl! non-null assertions with a guarded ToolGateway.gatewayUrl accessor that throws (fails synth) instead of shipping the literal "undefined" into the env. Used at both substrate call sites. - N3: disable the agent bridge with a clear WARN when the URL is set but no AWS region is resolvable, instead of an opaque downstream SigV4 signing failure. - N4: model RepoConfigToolResult as a discriminated union keyed on `onboarded` so "config present iff onboarded" is a compile-time invariant. - N5: reframe the neutral-naming comments as a convention, not something strip_linear_mcp_servers enforces (it only scrubs on-disk .mcp.json). - N6: extract _register_gateway_server and unit-test the runner wiring (registered under GATEWAY_SERVER_NAME when enabled, absent when disabled).
…g, error visibility, type polish Non-blocking + nit fixes from the second review pass on the AgentCore Gateway P1 (aws-samples#641): - test: scope the ECS substrate-parity InvokeGateway assertion to the EcsAgentCluster TaskRole via `Roles`. The AgentCore runtime role is granted the same action in the same template, so the unscoped assertion stayed green even if the ECS grant were removed — the exact cross-substrate regression the test exists to catch. - fix(agent): log a WARN on the ImportError (SDK-missing) disable path in build_gateway_server, matching the region-unset path — an opted-in feature that can't be honored no longer disables silently. - fix(agent): classify _repo_config_impl failures — expected transport errors (OSError/TimeoutError/httpx.HTTPError/McpError) stay WARN; anything else (bad response shape, RuntimeError from _match_remote_tool, …) routes through log_error_cw so coding/deployment bugs reach APPLICATION_LOGS instead of hiding in a WARN line. Still never raises. - test(agent): cover the N3 region guard (URL-set-but-no-region → None + WARN) and the AWS_DEFAULT_REGION fallback; add expected-vs-unexpected error-classification tests. - refactor(agent): add SdkToolResult TypedDict for the {content,isError} tool-result shape so drift across return sites is type-checked. - nit(agent): tighten the _sigv4_auth rotation comment to reflect that re-freezing only picks up rotation from a RefreshableCredentials chain. - nit(cdk): use ComputeType (not string) for compute_type in RepoConfigToolResult. - nit(test): synth tool-gateway.test.ts once in beforeAll (aws-samples#366).
The SdkToolResult TypedDict added for the review nit is rejected by `ty` (the agent typecheck gate): a TypedDict is not assignable to `dict[str, Any]`, and the SDK's @tool decorator expects the closure to return `Awaitable[dict[str, Any]]`, so the type cannot flow through the decorator either. Revert to `dict[str, Any]` return annotations; the other 7 review fixes are unaffected.
mayakost
left a comment
There was a problem hiding this comment.
Principal-architect review — P1 AgentCore Gateway tool federation
Verdict: Approve. High-quality, disciplined P1 — context-gated, additive, byte-for-byte-unchanged default synth, 100%-covered new files, docs + Starlight mirror updated in the same PR. No blocking correctness or security bugs. One small pre-merge fix recommended (B1) plus non-blocking follow-ups.
Reviewed with the pr-review-toolkit agents (code-reviewer, silent-failure-hunter, type-design-analyzer, pr-test-analyzer, comment-analyzer) + a focused security pass, then principal-architect judgment.
Vision alignment
Matches ADR-019 P1 exactly: one read-only Lambda target, AWS_IAM/SigV4 inbound, gateway-execution-role outbound (no credential), off by default. Bounded blast radius (read-only, secrets omitted, no new CFN types on the default path). The one deviation — in-process SigV4 bridge instead of the .mcp.json-writer shape the ADR originally sketched (the SDK's remote MCP client can only attach a static header) — is recorded in the ADR phasing table in this same PR. 👍
Recommended pre-merge fix
- B1 — outbound Gateway call drops solution-UA attribution (#319).
agent/src/gateway_tools.py_SigV4.auth_flowsigns a rawhttpxrequest tobedrock-agentcorebut never injects themd/uksb-wt64nei4u6#agentUser-Agent segment that AGENTS.md mandates for every outbound AWS call (the bridge uses botocore only for credential resolution, bypassing theaws_sessionwiring that normally adds it). ~3 lines using the existingua.static_user_agent_extra(); User-Agent isn't a default-signed header so it won't break the signature. Attribution-only — no functional/security impact.
Non-blocking — security (data returned is non-sensitive operational metadata; secrets correctly omitted)
- No tenant/task scoping on the
repoarg (tool-repo-config.ts,gateway_tools.py): any agent can query any onboarded repo'scompute_type/model_id/build_command/lint_command. Low impact now; worth scoping (inject + validate the task's repo) or documenting the decision before P3 generalizes to N targets. grantReadDatabroader than needed (tool-gateway.ts:113): handler does a singleGetItem; consider an inlinedynamodb:GetItem-only policy.InvokeGatewaygrant resource scope unasserted in tests — confirm the L2 scopes to the gateway ARN (not*) and assertResource, so the IAM5 suppressions can't mask a wildcard.- No
https://guard onABCA_TOOL_GATEWAY_URL(gateway_tools.py): a one-line scheme check prevents SigV4X-Amz-Security-Tokenfrom ever riding a plaintext request on misconfiguration.
Non-blocking — observability (never-abort posture itself is correct)
- Unexpected-error path calls
log_error_cwwithouttask_id→ files underagent_error/unknowninstead of the task stream. Threadtask_idinto the closure. - Feature-disable paths (SDK-missing, region-unset) use
log("WARN")→ stdout-only, not APPLICATION_LOGS; an operator who opted in and finds the tool absent gets no dashboard signal. - Empty-success (
_result_to_text→""withisError=False) is indistinguishable from a silent upstream drop — a WARN/sentinel would help future tools on this bridge.
Non-blocking — types / comments / tests
RepoConfigToolResultJSDoc overclaims "config present iff onboarded" — theonboarded:truearm's fields are all optional, so the guarantee is "field slots accessible iff onboarded." Reword._expected_gateway_errors()recomputed per call and itshttpx/mcpimport guards are effectively dead (both always in the container) — cache once at module load.- Comment
gateway_tools.py:11-13: "the shape ADR-019 originally sketched" (stdio proxy) — the original ADR P1 row sketched a.mcp.jsonwriter, not a stdio proxy. Minor rewording. - Test gaps: the string context form (
enableToolGateway: 'true'— the actualcdk deploy --contextpath) is never tested (all tests use the boolean); no test thatlookupRepothrowing propagates;OSError(most common transport failure) not exercised in the expected-WARN path.
Governance
- "Closes #641" may prematurely close the umbrella issue — #641 spans P1–P4. Use "Part of #641" / "Refs #641", or ensure P2–P4 follow-ups exist.
Bootstrap / CI
Bootstrap synth-coverage passes with no artifact change required: the two new CFN types are mapped in resource-action-map.ts and their create actions fall under the pre-existing bedrock-agentcore:* grant, so no BOOTSTRAP_VERSION bump / artifact regen / DEPLOYMENT_ROLES.md change is needed (correctly absent from the diff). All 4 CI checks green. Test quality is high — real botocore crypto in the SigV4 test, WARN-vs-loud classification pinned, secret-non-leakage asserted, and the ECS parity test correctly scoped to the EcsAgentClusterTaskRole.
Nice work — clean, well-tested, well-documented slice.
What & why
Implements the P1 slice of ADR-019 (issue #641): a single read-only Lambda tool (
abca_repo_config) exposed through a managed AgentCore Gateway with SigV4 (AWS_IAM) inbound auth and gateway-execution-role outbound (no credential). This is the thin, end-to-end proving slice for tool federation — one tool, one target, both auth sides real — that the fuller registry builds on.The entire feature is context-gated: resources synthesize only under
--context enableToolGateway=true. The default synth is byte-for-byte unchanged, so no new CFN types enter the bootstrap policy set and existing deploys are unaffected.Changes
CDK
constructs/tool-gateway.ts— AgentCore L2GatewaywithGatewayAuthorizer.usingAwsIam()(explicit — the L2 default is Cognito, which would silently create a user pool). One Lambda target (GATEWAY_IAM_ROLEoutbound), inline tool schema, read-only RepoTableGetItem, IAM4/IAM5 nag suppressions, publicgrantInvoke().handlers/tool-repo-config.ts— curated read-only lookup that omits secret ARNs; resolves the federated tool name (<target>___<tool>), validates theowner/nameslug, returns{onboarded:false}for un-onboarded repos.stacks/agent.ts+constructs/ecs-agent-cluster.ts— the context gate plus substrate parity: both the AgentCore runtime role and the ECS task role getbedrock-agentcore:InvokeGatewayandABCA_TOOL_GATEWAY_URL.Agent
gateway_tools.py— an in-process SigV4-signing MCP bridge. This deviates from the stdio-proxy shape ADR-019 originally sketched: the Claude Agent SDK's remote MCP client can only attach a staticAuthorizationheader, so it cannot sign SigV4 (which signs each request's body + timestamp). The bridge registers an SDK MCP server that signs each Streamable-HTTP request with the compute role's ambient credentials (servicebedrock-agentcore) and proxies to the federated tool by suffix. Inert unlessABCA_TOOL_GATEWAY_URLis set.runner.py— registers the bridge when the feature is deployed.Tests
tool-gateway.test.ts(7),tool-repo-config.test.ts,test_gateway_tools.py(18). New files at 100% coverage.Docs
mise //docs:sync.Notes for reviewers
enableToolGateway=truewarrants a bootstrap update in a follow-up (the gated path introducesAWS::BedrockAgentCore::Gateway/GatewayTarget).security:sast:maskingscan currently flags ~15 pre-existing files unrelated to this PR — the identical scan fails the same way on pristinemain(localsemgrepis newer than the version that last cleared these under bug(security): security:sast fails on main — 2 semgrep findings from #695 block pre-push #729/fix(security): clear the two security:sast findings blocking pre-push (#729) #730;mise.tomlpinssemgrep = "latest"). This PR's new files produce zero masking findings. CI is the authoritative gate.Closes #641 (P1 scope).