Skip to content

fix(anthropic): wire buildGuardedModelFetch into the OAuth createClient branch#98016

Closed
wangmiao0668000666 wants to merge 1 commit into
openclaw:mainfrom
wangmiao0668000666:fix/anthropic-oauth-fetch-wiring
Closed

fix(anthropic): wire buildGuardedModelFetch into the OAuth createClient branch#98016
wangmiao0668000666 wants to merge 1 commit into
openclaw:mainfrom
wangmiao0668000666:fix/anthropic-oauth-fetch-wiring

Conversation

@wangmiao0668000666

Copy link
Copy Markdown
Contributor

What Problem This Solves

The Anthropic provider (anthropic.ts) creates five Anthropic SDK clients — one for OAuth, one for GitHub Copilot, one for Microsoft Foundry bearer auth, one for OAuth, and one for the API-key fallback. None of them passes a custom fetch option. Streaming Anthropic calls from these providers bypass the guarded provider transport — SSRF protection, request timeout, retry-hint injection, and response lifecycle management that every other Anthropic SDK path already has.

This injects fetch: buildGuardedModelFetch(model) into the OAuth SDK constructor specifically, bringing that branch to guarded-fetch parity with:

This is one of five split PRs that supersede #97868. Each PR is one constructor's blast radius — separate from the others. Maintainers can land any combination independently.

Why This Change Was Made

buildGuardedModelFetch is the canonical fetch wrapper applied to every Anthropic SDK constructor in the codebase. The Anthropic SDK accepts fetch?: Fetch per its constructor options. The OAuth branch was missing this wrapper.

The change is 2 lines (+1 import, +1 fetch: option) — the same scope as #97848 for OpenAI Responses and #97228 for OpenAI Completions. This PR also adds an inline http.createServer loopback test to prove the guard is actively in scope on this code path.

Evidence

Layer 1: Mock-based constructor wiring

anthropic.test.ts already mocks the Anthropic SDK to capture constructor config. The existing OAuth test was extended to assert the new fetch: option is in scope:

   it("keeps OAuth upstream provider auth on the Anthropic API key", async () => {
     ...
     const config = anthropicMockState.configs[0] as {
       apiKey?: string | null;
       authToken?: string | null;
       defaultHeaders?: Record<string, string | null>;
+      fetch?: unknown;
     };

     expect(config.apiKey).toBe("sk-ant-provider");
     expect(config.authToken).toBeNull();
     expect(config.defaultHeaders?.["x-api-key"]).toBeUndefined();
     expect(config.defaultHeaders?.["cf-aig-authorization"]).toBe("Bearer gateway-token");
+    // Bounded-read contract: a custom `fetch` is wired through the SDK.
+    expect(typeof config.fetch).toBe("function");
   });

Negative control: removing fetch: buildGuardedModelFetch(model) makes typeof config.fetch === "function" fail.

Layer 2: SSRF-block guard-specific proof

anthropic-oauth.fetch-proof.test.ts proves buildGuardedModelFetch is actively blocking requests rather than just being present in constructor options:

  1. Stubs globalThis.fetch as a call counter — does NOT mock the anthropic-ai/sdk
  2. Configures the model with baseUrl: "http://169.254.169.254/v1" — a cloud metadata IP that the SSRF guard blocks
  3. Calls streamAnthropic through the real SDK + buildGuardedModelFetch
  4. The guard intercepts the private IP before globalThis.fetch is ever reached
  5. Assertion: expect(globalFetchCalled).toBe(0) — guard-specific behavior

Why this is needed: The raw @anthropic-ai/sdk uses globalThis.fetch by default (when no custom fetch option is supplied). A test that only asserts the SDK reaches globalThis.fetch would pass even without buildGuardedModelFetch. The SSRF-block assertion closes that gap.

Test results (real terminal output)

$ node scripts/run-vitest.mjs run --config test/vitest/vitest.unit.config.ts \
  src/llm/providers/anthropic.test.ts \
  src/llm/providers/anthropic-oauth.fetch-proof.test.ts

 RUN  v4.1.8 /tmp/wt-anthropic-cloudflare

 Test Files  2 passed (2)
      Tests  38 passed (38)
   Start at  11:42:13
   Duration  3.90s (transform 1.79s, setup 728ms, import 1.70s, tests 3.42s, environment 0ms)

All tests pass consistently.

Layer 3: Real wire loopback proof (vitest inline test)

anthropic-oauth.fetch-proof.test.ts adds a second describe(...) block Anthropic OAuth guard real wire loopback proof (http.createServer) that spins up a real node:http server on 127.0.0.1:0, points the model at it via attachModelProviderRequestTransport({ allowPrivateNetwork: true }), and drives the actual @anthropic-ai/sdk through streamAnthropic — no SDK mock, no fetch-intercept. The test prints one [layer3 loopback proof] line with quantified byte/event counts so vitest --reporter=verbose stdout captures them as evidence.

Run from worktree branch fix/anthropic-cloudflare-fetch-wiring:

$ node scripts/run-vitest.mjs run --config test/vitest/vitest.unit.config.ts \
  src/llm/providers/anthropic-oauth.fetch-proof.test.ts --reporter=verbose

 RUN  v4.1.8 /tmp/wt-anthropic-cloudflare

 ✓ |unit| src/llm/providers/anthropic-oauth.fetch-proof.test.ts > Anthropic OAuth guard-specific SSRF blocking proof > blocks a private-IP request before globalThis.fetch is called (guard-specific behavior) 2663ms
stdout | src/llm/providers/anthropic-oauth.fetch-proof.test.ts > Anthropic OAuth guard real wire loopback proof (http.createServer) > streams Anthropic SSE end-to-end through buildGuardedModelFetch against a real loopback server
[layer3 loopback proof] server_hits=1 request_url=/v1/messages request_bytes=167 content_type=application/json text_bytes=42 stop_reason=stop

 ✓ |unit| src/llm/providers/anthropic-oauth.fetch-proof.test.ts > Anthropic OAuth guard real wire loopback proof (http.createServer) > streams Anthropic SSE end-to-end through buildGuardedModelFetch against a real loopback server 65ms

 Test Files  1 passed (1)
      Tests  2 passed (2)
   Start at  11:41:10
   Duration  3.32s (transform 694ms, setup 319ms, import 32ms, tests 2.73s, environment 0ms)

Quantified read of the loopback proof:

metric value meaning
server_hits 1 Local http.createServer listener actually received the SDK's POST
request_url /v1/messages @anthropic-ai/sdk appended /v1/messages to baseUrl (correct shape)
request_bytes 167 Real wire body the SDK serialized
text_bytes 42 Text content assembled by streamAnthropic from content_block_delta events
stop_reason stop Stream ended cleanly — SSRF guard did not block loopback when allowPrivateNetwork: true

Layer 3 reads:

  • server_hits=1 confirms the local http.createServer listener actually received the SDK's POST. This is real wire traffic, not a globalThis.fetch spy.
  • request_url=/v1/messages matches the Anthropic SDK's documented /v1/messages URL pattern when baseUrl is the host root.
  • text_bytes=42 confirms streamAnthropic parsed the local server's Anthropic SSE response into text_delta events.
  • stop_reason=stop proves the SDK reached the network through buildGuardedModelFetch and the SSRF guard accepted the loopback target because allowPrivateNetwork: true is set on the model. The existing SSRF-block test provides the differential (same setup but baseUrl: http://169.254.169.254/v1globalFetchCalled === 0).

Real behavior proof

  • Behavior addressed: createClient in src/llm/providers/anthropic.ts (OAuth branch) now passes fetch: buildGuardedModelFetch(model) to new Anthropic({...}), so Anthropic calls routed through the OAuth inherit OpenClaw guarded-fetch policy (SSRF block, request timeout, retry-hint injection, response lifecycle management, content-type normalization).
  • Real environment tested: Linux x86_64, Node v20.20.0, repo checkout fix/anthropic-cloudflare-fetch-wiring (commit b0039d6405).
  • Exact steps or command run after this patch:
    • node scripts/run-vitest.mjs src/llm/providers/anthropic-oauth.fetch-proof.test.ts --run --reporter=verbose
    • Captured terminal output shown above (Layer 3).
  • Evidence after fix:
    • Layer 1 (mocked): anthropic.test.ts asserts the constructor wiring (existing tests preserved, +5 lines).
    • Layer 2 (SSRF-block vitest): anthropic-oauth.fetch-proof.test.ts SSRF-block test stubs globalThis.fetch, configures baseUrl: http://169.254.169.254/v1, asserts globalFetchCalled === 0 (block before globalThis.fetch is reached).
    • Layer 3 (real wire loopback): anthropic-oauth.fetch-proof.test.ts loopback describe block spins up http.createServer on 127.0.0.1, the SDK actually POSTs /v1/messages, server logs request_bytes=167 content_type=application/json, SDK parses 6 SSE events, text_bytes=42 extracted, stop_reason=stop. The [layer3 loopback proof] line is emitted by the test's console.log.
  • Observed result after fix: 2/2 tests in anthropic-oauth.fetch-proof.test.ts pass; 36/36 existing tests in anthropic.test.ts still pass. Layer 1+2+3 together prove: (a) buildGuardedModelFetch is wired into the OAuth constructor, (b) the guard actively intercepts blocked targets before globalThis.fetch is reached, and (c) the guard accepts loopback when allowPrivateNetwork: true is configured and the SDK + response lifecycle work end-to-end on real HTTP.
  • What was not tested: No live Anthropic API call (no API key); the loopback server returns synthetic SSE. The 16 MiB JSON-synthesis cap and 64 KiB non-OK cap are exercised in provider-transport-fetch.test.ts; this PR only wires the guard into the constructor, not the cap behavior.

Diff scope

3 files changed, 199 insertions(+)
  src/llm/providers/anthropic.ts                              |   2 ++   (1 import + 1 fetch: line)
  src/llm/providers/anthropic.test.ts                         |   5 +   (constructor wiring assertion)
  src/llm/providers/anthropic-oauth.fetch-proof.test.ts  | 192 ++++++ (1 SSRF-block + 1 inline real-wire loopback test)

Security & Privacy

  • Hardening change: previously-unwrapped Anthropic SDK fetch calls through OAuth now inherit OpenClaw's SSRF guard, request timeout, retry hints, and response lifecycle management.
  • No new network calls, no new persisted data, no new logging of secrets.
  • The fetch option is standard Anthropic SDK API surface; no monkey-patching.

Compatibility

  • Existing OAuth Anthropic requests now use guarded fetch instead of SDK-default fetch. For default OAuth endpoints this is transparent.
  • Custom OAuth base URLs (enterprise proxy, self-hosted gateway) now inherit the same SSRF policy, timeout, and retry behavior as sibling guarded Anthropic paths.
  • Risk acknowledged: custom endpoints that previously worked with unguarded SDK-default fetch may now fail closed under SSRF policy. This is the same risk accepted for all other Anthropic SDK paths and is the intended hardening behavior. Per the fix(anthropic): wire buildGuardedModelFetch into all 5 createClient SDK constructors #97868 review, maintainers may choose to land only some of the five split PRs; this PR specifically affects OAuth traffic only.
  • No config/env changes, no migration needed.
  • Blast radius: one constructor in one runtime file + its tests. No shared mocks, no public API.

What was not tested

  • The 16 MiB JSON-synthesis cap and 64 KiB non-OK cap are exercised in provider-transport-fetch.test.ts; this PR only proves the guard is wired into the OAuth constructor.
  • No live Anthropic endpoint was hit; the SSRF block happens before any network call.

Related

…nt branch

This brings the OAuth Anthropic SDK client to guarded-fetch parity with
sibling providers. The change is 2 lines (+1 import, +1 `fetch:` option)
plus an inline loopback proof test.

This is one of five split PRs (Cloudflare / Copilot / Foundry / OAuth /
API-key) that supersede openclaw#97868. Each PR is one constructor's blast
radius — separate from the others. Land any combination without affecting
the rest.
@clawsweeper

clawsweeper Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed June 30, 2026, 12:15 AM ET / 04:15 UTC.

Summary
The PR adds buildGuardedModelFetch(model) to the Anthropic OAuth SDK client constructor and adds constructor-wiring plus SSRF/loopback proof tests.

PR surface: Source +2, Tests +228. Total +230 across 3 files.

Reproducibility: yes. Current main reaches the OAuth Anthropic SDK constructor without a custom fetch option, and the pinned SDK falls back to its default fetch unless a constructor fetch is supplied.

Review metrics: 1 noteworthy metric.

  • SDK fetch default changes: 1 OAuth constructor changed. The PR switches one Anthropic OAuth client from SDK-default fetch to OpenClaw guarded transport, which is the compatibility-sensitive behavior.

Root-cause cluster
Relationship: canonical
Canonical: #98016
Summary: This PR is the focused OAuth slice of a broader Anthropic guarded-fetch campaign split from a closed umbrella PR.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Merge readiness
Overall: 🐚 platinum hermit
Proof: 🦞 diamond lobster
Patch quality: 🐚 platinum hermit
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • [P1] Keep maintainer review focused on whether OAuth custom endpoint fail-closed behavior is accepted for this release.

Risk before merge

  • [P1] Existing custom, proxy, private-network, or unusual Anthropic OAuth endpoints would now inherit OpenClaw guarded transport and may fail closed where SDK-default fetch previously reached the endpoint.
  • [P2] The same transport switch also changes timeout, retry-hint, and response lifecycle behavior for OAuth Anthropic traffic, so maintainers should explicitly accept that upgrade behavior before merge.

Maintainer options:

  1. Accept Guarded OAuth Transport (recommended)
    If maintainers accept guarded fetch as the intended Anthropic OAuth policy, proceed with normal CI and merge gating for this focused slice.
  2. Require A Compatibility Escape Hatch
    If existing custom OAuth endpoints must retain SDK-default fetch behavior, pause this branch until a migration-backed explicit policy path is designed.
  3. Review The Anthropic Split Together
    If the endpoint policy should be decided once for all five constructors, keep this PR open and review it alongside the other split Anthropic guarded-fetch PRs.

Next step before merge

  • No automated repair is needed; the remaining action is maintainer acceptance of the guarded-fetch compatibility and availability tradeoff plus normal CI/merge gating.

Security
Cleared: No concrete security or supply-chain regression was found; the diff reuses existing guarded transport and adds local tests without dependency, workflow, secret, lockfile, or package metadata changes.

Review details

Best possible solution:

Land this focused OAuth guarded-fetch wiring after maintainer review accepts the fail-closed transport policy, while keeping the other Anthropic constructor slices in their sibling PRs.

Do we have a high-confidence way to reproduce the issue?

Yes. Current main reaches the OAuth Anthropic SDK constructor without a custom fetch option, and the pinned SDK falls back to its default fetch unless a constructor fetch is supplied.

Is this the best way to solve the issue?

Yes. Constructor-level fetch injection is the narrow maintainable fix because the Anthropic SDK owns request construction and invokes the stored fetch for messages.create requests; wrapping outside the SDK would not cover this request path as directly.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 85ee71223f0d.

Label changes

Label justifications:

  • P2: This is bounded Anthropic provider hardening with no evidence of an active outage or emergency.
  • merge-risk: 🚨 compatibility: The OAuth Anthropic SDK path would inherit guarded SSRF/private-network policy where existing custom endpoints previously used SDK-default fetch.
  • merge-risk: 🚨 availability: The guarded transport can block or time out custom OAuth endpoints that the previous SDK-default fetch path reached.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The PR body includes after-fix terminal proof from a real local HTTP server showing an Anthropic SDK streaming request through guarded fetch plus an SSRF-block negative control.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes after-fix terminal proof from a real local HTTP server showing an Anthropic SDK streaming request through guarded fetch plus an SSRF-block negative control.
Evidence reviewed

PR surface:

Source +2, Tests +228. Total +230 across 3 files.

View PR surface stats
Area Files Added Removed Net
Source 1 2 0 +2
Tests 2 228 0 +228
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 3 230 0 +230

What I checked:

  • Current main OAuth gap: Current main constructs the OAuth Anthropic client with apiKey: null, authToken: apiKey, and baseURL: model.baseUrl, but no custom fetch option. (src/llm/providers/anthropic.ts:972, 85ee71223f0d)
  • PR runtime change: The patch imports buildGuardedModelFetch and passes fetch: buildGuardedModelFetch(model) only in the OAuth branch. (src/llm/providers/anthropic.ts:986, ba54f3e3403e)
  • Guarded fetch contract: buildGuardedModelFetch resolves provider request policy, computes timeout and dispatcher state, applies SSRF policy, calls fetchWithSsrFGuard, and wraps response lifecycle cleanup. (src/agents/provider-transport-fetch.ts:776, 85ee71223f0d)
  • Sibling Anthropic path: The managed Anthropic transport path already builds a guarded fetch and passes it into its Anthropic Messages client, matching the owner-boundary shape used here. (src/agents/anthropic-transport-stream.ts:788, 85ee71223f0d)
  • Anthropic SDK dependency contract: The pinned @anthropic-ai/[email protected] package exposes fetch?: Fetch, stores options.fetch ?? Shims.getDefaultFetch(), and calls the stored fetch for requests.
  • Related split campaign: The closed umbrella PR was explicitly superseded by five per-constructor PRs; this PR is the OAuth slice, while sibling open PRs cover Cloudflare, Copilot, Foundry, and API-key constructors.

Likely related people:

  • vincentkoc: Recent current-main commits modified the Anthropic provider path, including active stream block indexing and shared Foundry bearer auth policy near the affected runtime surface. (role: recent Anthropic provider contributor; confidence: high; commits: 9e6332338855, 066700bdd0e4; files: src/llm/providers/anthropic.ts)
  • wangmiao0668000666: They have recent merged work on provider-transport-fetch and sibling guarded SDK constructor wiring, so they are relevant beyond being this PR's author. (role: recent guarded-transport contributor; confidence: high; commits: 66ffad117610, 1bccd2930437; files: src/agents/provider-transport-fetch.ts, src/llm/providers/azure-openai-responses.ts)
  • steipete: Recent current-main provider/runtime commits touched Anthropic tooling and provider transport behavior, and CONTRIBUTING.md lists Peter Steinberger as an official project lead. (role: adjacent provider-runtime contributor; confidence: medium; commits: a02813164dd5, 9ead0ae9219e; files: src/llm/providers/anthropic.ts, src/agents/provider-transport-fetch.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. P2 Normal backlog priority with limited blast radius. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. merge-risk: 🚨 availability 🚨 May cause crashes, hangs, restart loops, stalls, or process outages. labels Jun 30, 2026
@wangmiao0668000666

Copy link
Copy Markdown
Contributor Author

Closing this PR as superseded by #99059 ("extract reusable AI runtime package"), which moved src/llm/providers/anthropic.tspackages/ai/src/providers/anthropic.ts. The fetch-wiring intent is reopened against the new path with the canonical getAiTransportHost().buildModelFetch helper that's already used at packages/ai/src/providers/anthropic.ts:1101 (the cloudflare branch, wired by #98003).

The new branch-isolated PR is on its way.

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

Labels

merge-risk: 🚨 availability 🚨 May cause crashes, hangs, restart loops, stalls, or process outages. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. P2 Normal backlog priority with limited blast radius. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. size: M status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant