Skip to content

fix(router): no-follow-redirect on fan-out/health/discovery clients + bump tmproto to v0.3.2 - #515

Merged
ohalushchak-exadel merged 4 commits into
mainfrom
ohalushchak-exadel/router-no-follow-redirect-and-tmproto-pin
Sep 8, 2026
Merged

fix(router): no-follow-redirect on fan-out/health/discovery clients + bump tmproto to v0.3.2#515
ohalushchak-exadel merged 4 commits into
mainfrom
ohalushchak-exadel/router-no-follow-redirect-and-tmproto-pin

Conversation

@ohalushchak-exadel

Copy link
Copy Markdown
Collaborator

Summary

Closes two related conformance gaps against the TMP provider-endpoint rules and picks up the tmproto artifact-scrub fixes from the just-cut submodule release.

Router-side HTTP clients no longer follow redirects. None of the three router-owned clients (fan-out in router.go, /health probe in healthcheck.go, discovery poll in discovery.go) set CheckRedirect. Spec forbids redirects on provider endpoints — a 3xx from a provider (or a provider registry, on the discovery poll) would let net/http replay the signed request body at whatever host DNS/rebind resolved. safeDialContext blocks only private destinations; a public attacker-controlled host would still resolve, so the redirect refusal is the load-bearing guarantee. This PR wires a shared noFollowRedirect helper (in safedial.go, alongside the dial-time SSRF guard) into the default clients built by NewRouter, NewHealthChecker, and NewDiscovery. Caller-overridden clients (WithHTTPClient / WithHealthCheckClient / WithDiscoveryClient) remain the caller's responsibility.

tmproto pin bumped from v0.3.0 to v0.3.2. Picks up:

  • StripAccess now clears the URL field on known assets whose access declared signed_url (previously left the credential in the query string after nil-ing Access) and scrubs access — plus url when the access declared signed_url — from UnknownAsset raw JSON.
  • The UnknownAsset scrub path uses json.Decoder.UseNumber so integers above 2^53 (IDs, epoch-ms timestamps) round-trip verbatim.

The router calls Artifact.StripAccess() on every incoming context-match request, so bumping the pin activates all of the above on the fan-out hop without any local code change on this PR.

Test plan

  • go test ./... in tmproto, targeting, router — all green
  • CI green after push
  • Manual sanity: point the router at a provider that answers 3xx and confirm the router observes the 3xx (surfaced as a provider error) and does not follow it
  • Manual sanity: a context-match request carrying a signed-URL asset fans out with the asset URL blanked
  • Manual sanity: a context-match request carrying an unknown asset type with an access object fans out with that object stripped

ohalushchak-exadel and others added 3 commits September 8, 2026 20:51
…ents

TMP forbids redirects on provider endpoints (spec §Provider Endpoints).
None of the three router-side HTTP clients set CheckRedirect, so a
provider — or a provider registry, on the discovery poll — that answers
3xx would replay the signed request body at whatever host DNS/rebind
resolved. safeDialContext blocks only private destinations; a public
attacker-controlled host would still resolve, so the redirect refusal is
the load-bearing guarantee.

Add a shared noFollowRedirect helper in safedial.go alongside the
existing dial-time SSRF guard and wire it into the default clients
constructed by NewRouter, NewHealthChecker, and NewDiscovery. Caller-
overridden clients (WithHTTPClient / WithHealthCheckClient /
WithDiscoveryClient) remain the caller's responsibility.

Tests:
* Helper unit test asserts noFollowRedirect returns
  http.ErrUseLastResponse so net/http surfaces the 3xx to the caller
  instead of following it.
* End-to-end router test: builds a router, points its client at a
  redirect-emitting httptest server whose Location targets a canary,
  asserts the router observes the 3xx and the canary receives zero
  requests.
* Wiring tests for HealthChecker and Discovery assert their default
  clients set CheckRedirect and it returns ErrUseLastResponse when
  invoked. (Their transports use safeDialContext, which blocks
  localhost, so an end-to-end variant would need a public target.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Picks up StripAccess covering signed-URL residue on known assets and
credential material carried on unknown asset types (tmproto PR #513),
plus the UnknownAsset scrub-path fix that preserves integer precision
above 2^53.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The root module's tmproto pin bump left cmd/router, cmd/identity-agent,
and e2e/stack out of sync: each pins tmproto directly AND replaces the
root adcp-go module to ../.. (or ../../..). MVS resolves against the
higher of the two, so the docker builds ran through the containerized
CI with "go: updates to go.mod needed" against the shared build cache.

Run go mod tidy in each affected sub-module. cmd/context-agent needs
no change — it does not replace adcp-go and pulls tmproto indirectly
through targeting, which still pins v0.3.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ladon verdict: Approve

Approve.

Clean, well-scoped security fix: no-follow-redirect (http.ErrUseLastResponse) wired into all three default router-side clients (fan-out, health, discovery), with end-to-end test coverage, correct 3xx-as-error handling, and body close on every path. The tmproto v0.3.2 pin is a real release with matching go.sum and conventional-commit markers.

Checked:

  • No blocking (critical/high) findings.
  • No in-diff medium findings.
  • high_risk is true only because changed files match router/**; all reasons are (modified) or (added) with no reviewer medium concern, so the modifications are presumed safe (no escalation on the flag alone).
  • gated_paths is false; author is not on a no-auto-approve team.

One non-blocking follow-up noted by the reviewer: the fourth trust-fetching client (registry sync, router/registry.go:75) lacks the same safeDialContext/CheckRedirect hardening, but it is latent (main.go constructs it with empty URLs). Worth a future pass but not a blocker here.

Decision table: no rows 1–8 fire → row 9 → approve.

aao-secretariat[bot]
aao-secretariat Bot previously approved these changes Sep 8, 2026

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ladon verdict: Approve

Approve.

Wires a shared noFollowRedirect (returns http.ErrUseLastResponse) into the default fan-out, health, and discovery clients — closing a TMP provider-endpoint redirect-replay gap not covered by safeDialContext (private-only) — plus a consistent tmproto v0.3.0→v0.3.2 pin bump across all workspace modules with matching go.sum hashes.

Checked:

  • Default-client-only scoping of the redirect suppression (no broad behavior change).
  • End-to-end + wiring coverage via new router/no_follow_redirect_test.go.
  • No adcp/schemas/** or adcp/types_gen.go changes — schema↔generated-type coherence intact.
  • No TMP signing/verification, identity-agent TEE, or protocol-managed-skill edits.
  • Consistent module resolution across all go.mod/go.sum.

high_risk is true only because files match router/**; the modifications preserve contracts and the reviewer surfaced no Critical/High/Medium findings. Three manual-sanity checkboxes are unchecked but the primary redirect path is covered by an automated test. No blocking findings, no escalation triggers, no team gate, gated_paths false — falls through to row 9.

Matches the pattern used elsewhere in router/*_test.go
(safedial_test.go:84) so the linter's errcheck rule is happy on the
new no-follow-redirect regression test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Re-approval: no reviewable delta since the prior approval; re-submitting after stale-review dismissal.

@ohalushchak-exadel
ohalushchak-exadel merged commit 6294066 into main Sep 8, 2026
22 checks passed
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