Repository navigation
fix(agent): pin custom MCP connections to the validated address - #184
Conversation
🤖 AI Code Review (two independent reviewers)_Reviewer responses: codex/ALL claude/ALL _ Status: BLOCKED — CRITICAL/MAJOR issues must be resolved before merge All disputed claims are now verified. The final synthesized review follows. Chair review — PR #184: fix(agent): pin custom MCP connections to the validated addressIMAGE_COVERAGE: NOT_REQUIRED 1. SummaryThe core change is a genuine security improvement: it replaces the documented resolve-and-recheck DNS-rebinding window on the custom MCP integration transport with real IP pinning (validated address set, preserved Host/SNI, certificate verification, in-transport redirect rejection), plus a header-injection gate on stored credentials — and it is well-tested, including against a real TLS server. However, the pinning transport breaks IPv6-only endpoints, the new timeout wiring silently collapses the integration read budget from 300s to 8s, and the new 2. Issues per lensL2 — Code correctnessMAJOR (claude flagged; chair-confirmed) — Bare IPv6 pinned addresses break AAAA-only endpoints. MAJOR (claude flagged; chair-confirmed) — Integration read budget collapses from 300s to 8s. MINOR (claude) — L3 — Security / AWS mutation safetyNo CRITICAL/MAJOR. The patch adds no AWS-mutating call, IAM statement, or hardcoded secret; the only AWS API touched is the pre-existing read-only Rejected (codex flagged MAJOR) — "Custom external MCP registration contradicts ADR-007 / frozen BYO-MCP." This is a false positive. BYO-MCP is an ADR-004 capability tier explicitly listed in MINOR (claude) — the comment at L4 — Observability / data-integration correctnessNo Steampipe/CIS/diagnosis code is modified; the findings are contradictions between this PR's new documentation and the data-integration code it describes. MAJOR (claude flagged; chair-confirmed) — the "search tools receive start/end" / "selected bounds are forwarded" contract is false for four of eight kinds. MAJOR (claude flagged; chair-confirmed) — Jaeger limit disclosure does not exist. The doc claims "Jaeger discloses its requested limit and retained count; reaching the requested limit is potentially partial." MINOR (claude) — the doc's 4 MiB success / 64 KiB error body caps have no implementation; L5 — Docs/ADR consistencyMAJOR (claude flagged; chair-confirmed) — MAJOR (claude flagged; chair-confirmed) — the 55-second "form shows the effective maximum" claim is false, and the 55s clamp is ClickHouse-only. MAJOR (claude flagged; chair-confirmed) — " These doc findings gate because MINOR (claude) — 3. Suggestions
4. VerdictThe security direction is right and the transport tests are strong, but two chair-confirmed functional regressions (IPv6-only endpoint breakage, 8s read-budget collapse) and multiple chair-confirmed contradictions between the PR's own new "source-supported" documentation and the shipped code require a revision cycle. Triggered by commit |
55ad447 to
ca77596
Compare
55b8c7d to
06759fd
Compare
ca77596 to
8b66c8d
Compare
Custom MCP transports connect to the address set validated at request time while preserving Host, TLS SNI and certificate verification, and reject every 3xx. IPv6 addresses are bracketed so AAAA-only endpoints connect. The transport deadline and the MCP session read timeout follow a 60s read budget separate from the 8s connect budget. A malformed port is an SSRF-classified rejection, discarded per-address failures are logged, and pasted trailing whitespace in an api_key value is stripped. The pin composes with the default-off custom MCP governance: spec validation and the attested secret version run first, then the validated addresses are pinned for that session. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Status note for reviewers:
Agent suites on this head: 189 passed ( |
8b66c8d to
cf85a32
Compare
* fix(agent): pin custom MCP connections to the validated address (#184) Custom MCP transports connect to the address set validated at request time while preserving Host, TLS SNI and certificate verification, and reject every 3xx. IPv6 addresses are bracketed so AAAA-only endpoints connect. The transport deadline and the MCP session read timeout follow a 60s read budget separate from the 8s connect budget. A malformed port is an SSRF-classified rejection, discarded per-address failures are logged, and pasted trailing whitespace in an api_key value is stripped. The pin composes with the default-off custom MCP governance: spec validation and the attested secret version run first, then the validated addresses are pinned for that session. Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> * fix: address review feedback (iteration 1/5) * docs: add the pinned-transport suite to the required agent test commands Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…183) * fix(datasources): bound connector reads and verify real query access Datasource Lambdas cap success (4 MiB) and error (64 KiB) bodies before parsing and close streams on every exit. Missing explicit instance IDs fail closed. Connection tests run the real bounded query operation, and Jaeger/Dynatrace/Datadog/Loki report partial or unverified results instead of absence. Prometheus/Mimir enforce the saved execution ceiling inside the connector. Optional Jaeger/Dynatrace VPC attachment stays default-off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(datasources): share one request deadline and disclose limit saturation Review follow-ups for the connector bounds: connect, headers and body share one monotonic deadline so a slow upstream cannot double the aligned HTTP timeout past the Lambda wall; oversized or slow error bodies keep their HTTP status. Loki and Jaeger report limit saturation as partial with requested/returned counts, separately from byte-bound loss. Connection probes return latency_ms, accept empty or warned results, and keep the SSRF classification. The default kind mirror is stamped with its instance id, the id-keyed lookup accepts only that default's mirror (legacy unstamped mirrors keep working with a warning until the next save), and timeoutS rides the console conn config for every kind. Restores the resolve-and-recheck disclosure that the stacked agent change replaces. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(web): expect the stamped instance id on default re-mirrors Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: bind datasource credentials to their registered identity * fix: address review feedback (iteration 2/5) * fix(agent): pin custom MCP connections to the validated address (#184) Custom MCP transports connect to the address set validated at request time while preserving Host, TLS SNI and certificate verification, and reject every 3xx. IPv6 addresses are bracketed so AAAA-only endpoints connect. The transport deadline and the MCP session read timeout follow a 60s read budget separate from the 8s connect budget. A malformed port is an SSRF-classified rejection, discarded per-address failures are logged, and pasted trailing whitespace in an api_key value is stripped. The pin composes with the default-off custom MCP governance: spec validation and the attested secret version run first, then the validated addresses are pinned for that session. Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> * fix(web): show datasource completeness, limits and shared scope (#185) * fix(web): show datasource completeness, limits and shared scope The datasource API and UI report instance-specific configuration only, show effective execution/response limits, pass the selected window to trace and SaaS kinds, reject unsupported SQL range shapes, and label shared default datasources. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web): align datasource guides with the stacked connector contract Review follow-ups: the datasource guides (ko/en/ja/zh) and API reference describe the shared timeout contract, the real-query connection test and the Mimir API prefix; the reference doc records Loki saturation disclosure, the shared request deadline and the oauth_3lo posture. The form no longer offers oauth_3lo. A Datadog instance's legacy key pair counts as configured and survives a settings-only PATCH regardless of the row's authType. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: address review feedback (iteration 1/5) * fix: address review feedback (iteration 2/5) * fix(web): defer oauth_3lo enablement to the custom MCP governance gate The enable-time transport check is superseded by the merged default-off governance gate, which admits only api_key and oauth_client_credentials; registration validation and the form still reject oauth_3lo. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * revert: move the web/datasource contract change out of this PR Reverts the #185 squash (b5bfb94). It was merged into this branch instead of dev, which pushed this PR's diff to 245 KB — past the AI review input limit, so the reviewers were never called. The web and documentation change returns as a dev-based PR after this one merges. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * chore: move the web credential-contract changes to the web PR The instance-id stamp on the default kind mirror, the console-path timeoutS forwarding and the default/delete mirror handling are web changes that ship with the datasource web contract PR. Keeping them here pushed this PR against the review input limit; the connector side already fails closed without an id-bound credential, and the runbook's backfill (re-save the default) writes the id key with today's web code. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: address review feedback (iteration 2/5) * fix: address review feedback (iteration 3/5) * fix: address review feedback (iteration 4/5) * chore: keep this PR within the review input limit The optional Jaeger/Dynatrace VPC flags move to their own PR and the datasource-development FAQ wording moves to the web contract PR; both are independent of the connector changes reviewed here. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * chore: carry the connector CHANGELOG entry in the web contract PR The feature-level bullet describes the net behavior of the connector and web changes together, so it ships with the final PR of the pair; this keeps the connector PR within the review input limit. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: address review feedback (iteration 5/5) * chore: move connector regression tests to a stacked follow-up PR The AI review input limit is 128 KiB and this PR's diff was 131 KiB, so the review never reached a model. The 544-line test_external_connections.py moves unchanged to a follow-up PR stacked on this branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: address review feedback (iteration 6/10) * fix: address review feedback (iteration 7/10) * fix: address review feedback (iteration 8/10) * fix: address review feedback (iteration 9/10) * fix: address review feedback (iteration 10/10) * chore: keep this PR within the review input limit Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: address review feedback (iteration 11/12) * chore: move the Dynatrace connector regressions to the stacked test PR Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
#191) * fix(datasources): bound connector reads and verify real query access Datasource Lambdas cap success (4 MiB) and error (64 KiB) bodies before parsing and close streams on every exit. Missing explicit instance IDs fail closed. Connection tests run the real bounded query operation, and Jaeger/Dynatrace/Datadog/Loki report partial or unverified results instead of absence. Prometheus/Mimir enforce the saved execution ceiling inside the connector. Optional Jaeger/Dynatrace VPC attachment stays default-off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(datasources): share one request deadline and disclose limit saturation Review follow-ups for the connector bounds: connect, headers and body share one monotonic deadline so a slow upstream cannot double the aligned HTTP timeout past the Lambda wall; oversized or slow error bodies keep their HTTP status. Loki and Jaeger report limit saturation as partial with requested/returned counts, separately from byte-bound loss. Connection probes return latency_ms, accept empty or warned results, and keep the SSRF classification. The default kind mirror is stamped with its instance id, the id-keyed lookup accepts only that default's mirror (legacy unstamped mirrors keep working with a warning until the next save), and timeoutS rides the console conn config for every kind. Restores the resolve-and-recheck disclosure that the stacked agent change replaces. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(web): expect the stamped instance id on default re-mirrors Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: bind datasource credentials to their registered identity * fix: address review feedback (iteration 2/5) * fix(agent): pin custom MCP connections to the validated address (#184) Custom MCP transports connect to the address set validated at request time while preserving Host, TLS SNI and certificate verification, and reject every 3xx. IPv6 addresses are bracketed so AAAA-only endpoints connect. The transport deadline and the MCP session read timeout follow a 60s read budget separate from the 8s connect budget. A malformed port is an SSRF-classified rejection, discarded per-address failures are logged, and pasted trailing whitespace in an api_key value is stripped. The pin composes with the default-off custom MCP governance: spec validation and the attested secret version run first, then the validated addresses are pinned for that session. Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> * fix(web): show datasource completeness, limits and shared scope (#185) * fix(web): show datasource completeness, limits and shared scope The datasource API and UI report instance-specific configuration only, show effective execution/response limits, pass the selected window to trace and SaaS kinds, reject unsupported SQL range shapes, and label shared default datasources. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web): align datasource guides with the stacked connector contract Review follow-ups: the datasource guides (ko/en/ja/zh) and API reference describe the shared timeout contract, the real-query connection test and the Mimir API prefix; the reference doc records Loki saturation disclosure, the shared request deadline and the oauth_3lo posture. The form no longer offers oauth_3lo. A Datadog instance's legacy key pair counts as configured and survives a settings-only PATCH regardless of the row's authType. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: address review feedback (iteration 1/5) * fix: address review feedback (iteration 2/5) * fix(web): defer oauth_3lo enablement to the custom MCP governance gate The enable-time transport check is superseded by the merged default-off governance gate, which admits only api_key and oauth_client_credentials; registration validation and the form still reject oauth_3lo. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> * revert: move the web/datasource contract change out of this PR Reverts the #185 squash (b5bfb94). It was merged into this branch instead of dev, which pushed this PR's diff to 245 KB — past the AI review input limit, so the reviewers were never called. The web and documentation change returns as a dev-based PR after this one merges. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * chore: move the web credential-contract changes to the web PR The instance-id stamp on the default kind mirror, the console-path timeoutS forwarding and the default/delete mirror handling are web changes that ship with the datasource web contract PR. Keeping them here pushed this PR against the review input limit; the connector side already fails closed without an id-bound credential, and the runbook's backfill (re-save the default) writes the id key with today's web code. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: address review feedback (iteration 2/5) * fix: address review feedback (iteration 3/5) * fix: address review feedback (iteration 4/5) * chore: keep this PR within the review input limit The optional Jaeger/Dynatrace VPC flags move to their own PR and the datasource-development FAQ wording moves to the web contract PR; both are independent of the connector changes reviewed here. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * chore: carry the connector CHANGELOG entry in the web contract PR The feature-level bullet describes the net behavior of the connector and web changes together, so it ships with the final PR of the pair; this keeps the connector PR within the review input limit. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix: address review feedback (iteration 5/5) * chore: move connector regression tests to a stacked follow-up PR The AI review input limit is 128 KiB and this PR's diff was 131 KiB, so the review never reached a model. The 544-line test_external_connections.py moves unchanged to a follow-up PR stacked on this branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(datasources): add connection identity and bounded I/O regressions Restores test_external_connections.py, split from the connector-bounds PR to keep that PR under the AI review input limit. Covers connection identity, bounded reads, query proof and result-completion behavior (121 tests). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: address review feedback (iteration 6/10) * test(datasources): count only emitted Jaeger traces in returnedCount Matches the connector change in the base PR: when the byte bound drops every trace, returnedCount is now the length of the emitted (empty) traces array. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: address review feedback (iteration 7/10) * fix: address review feedback (iteration 8/10) * fix: address review feedback (iteration 9/10) * fix: address review feedback (iteration 10/10) * chore: keep this PR within the review input limit Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix: address review feedback (iteration 11/12) * chore: move the Dynatrace connector regressions to the stacked test PR Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(datasources): carry the Dynatrace connector regressions Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Summary
Part 2 of 3, stacked on #183 (
fix/datasource-connector-bounds). Merge after #183, then retarget #185 todev. This PR is agent-core only; the reference doc and guides moved to #185 so they describe code that exists below them.agent/pinned_http.py, used bystreamable_http_sigv4.py) connects to the address set validated for that invocation, preserving Host, TLS SNI and certificate verification, and rejects every 3xx before the MCP SDK can follow it. IPv6 pins are bracketed, so AAAA-only endpoints connect.CONNECT_TIMEOUT = 8per pinned address,READ_TIMEOUT = 60for time-to-headers / between SSE events (sse_read_timeout), inside the 120s chat budget. The transport deadline follows the read budget, never the connect budget. Discarded per-address failures are logged so a certificate failure on one address is not hidden behind the last one.agent.pySSRF checks return the validated addresses, use the endpoint's port, classify a malformed port asSsrfBlocked, and reject backslashes. api_key header names and values and stored bearer tokens are validated; a pasted trailing newline in a value is stripped, not rejected.requirements.txt, Dockerfile) — the versions the SNI extension contract was tested against.datasource_http.py's docstring now describes both pinned transports (this PR lands the module the sentence refers to).py/insecure-protocol).Takes effect after the next Deploy AgentCore.
Review follow-ups (round 1)
IPv6 breakage → bracketed host; 300s→8s read collapse → separate
READ_TIMEOUT; hardcoded8→ module constants imported byagent.py;parsed.portValueError→SsrfBlocked; "request-local" → "invocation-local"; discarded errors logged; 3xx message no longer says "redirect" for Location-less responses. The doc contradictions (apiPrefix, per-kind windows, Jaeger limit disclosure, 55s form, oauth_3lo) are addressed by stacking: those docs now live in #185 above the code they describe. The "custom MCP contradicts ADR-007" finding was rejected by the chair and is also settled by #177 (merged).Tests
bash scripts/v2/merge-verify.shon Python 3.12: web vitest PASS, Terraform fmt PASS, deployment Node tests PASS; only the codec-sandbox tests fail for the known environmental reason (same ondev).cd agent && pytest test_pinned_http.py test_agent.py test_readiness.py tests/test_health_tools.py tests/test_anthropic_loop.py→ 182 passed, with new IPv6, read-budget, factory-timeout, malformed-port and whitespace-strip cases. Needs Python ≥ 3.11 (asyncio.timeout).🤖 Generated with Claude Code