Skip to content

fix(anthropic): wire buildGuardedModelFetch into the API-key createClient branch#98017

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

fix(anthropic): wire buildGuardedModelFetch into the API-key createClient branch#98017
wangmiao0668000666 wants to merge 1 commit into
openclaw:mainfrom
wangmiao0668000666:fix/anthropic-apikey-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 API-key, 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 API-key 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 API-key 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 API-key test was extended to assert the new fetch: option is in scope:

   it("keeps API-key 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-apikey.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-apikey.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-apikey.fetch-proof.test.ts adds a second describe(...) block Anthropic API-key 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-apikey.fetch-proof.test.ts --reporter=verbose

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

 ✓ |unit| src/llm/providers/anthropic-apikey.fetch-proof.test.ts > Anthropic API-key guard-specific SSRF blocking proof > blocks a private-IP request before globalThis.fetch is called (guard-specific behavior) 2663ms
stdout | src/llm/providers/anthropic-apikey.fetch-proof.test.ts > Anthropic API-key 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=44 stop_reason=stop

 ✓ |unit| src/llm/providers/anthropic-apikey.fetch-proof.test.ts > Anthropic API-key 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 44 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=44 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 (API-key branch) now passes fetch: buildGuardedModelFetch(model) to new Anthropic({...}), so Anthropic calls routed through the API-key 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-apikey.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-apikey.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-apikey.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=44 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-apikey.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 API-key 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-apikey.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 API-key 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 API-key Anthropic requests now use guarded fetch instead of SDK-default fetch. For default API-key endpoints this is transparent.
  • Custom API-key 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 API-key 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 API-key constructor.
  • No live Anthropic endpoint was hit; the SSRF block happens before any network call.

Related

…ient branch

This brings the API-key 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:11 AM ET / 04:11 UTC.

Summary
This PR adds buildGuardedModelFetch(model) to the Anthropic API-key SDK constructor and adds constructor, SSRF-block, and loopback proof tests.

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

Reproducibility: yes. source-reproducible. Current main reaches the Anthropic API-key SDK constructor without a custom fetch option, and the pinned SDK falls back to global fetch when that option is absent.

Review metrics: 1 noteworthy metric.

  • SDK fetch hook: 1 added. One existing Anthropic API-key SDK client switches from dependency-default fetch to OpenClaw guarded provider transport, which is the compatibility-sensitive part of the merge.

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:

  • none.

Risk before merge

  • [P1] Existing custom, proxy, private-network, or unusual Anthropic API-key endpoints that worked through SDK-default fetch may now fail closed, time out, or see different response lifecycle behavior under OpenClaw guarded transport.
  • [P1] No live Anthropic endpoint was hit; the submitted proof uses the actual SDK against a local loopback server plus an SSRF negative control.

Maintainer options:

  1. Accept API-Key Guarded Transport (recommended)
    Maintainers can accept that Anthropic API-key traffic should now follow the same guarded transport policy used by sibling SDK paths.
  2. Require A Compatibility Path First
    If existing custom API-key endpoints must retain SDK-default behavior, require an explicit compatibility or migration policy before merging unconditional guarded transport.
  3. Pause For Endpoint Policy Review
    If fail-closed behavior for custom Anthropic endpoints is not yet accepted, pause this PR until maintainers decide the endpoint policy.

Next step before merge

  • No automated repair remains; the next action is maintainer review of the fail-closed custom endpoint risk and 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 the narrow API-key guarded-fetch wiring after maintainers accept the intentional fail-closed transport policy for this provider path.

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

Yes, source-reproducible. Current main reaches the Anthropic API-key SDK constructor without a custom fetch option, and the pinned SDK falls back to global fetch when that option is absent.

Is this the best way to solve the issue?

Yes, subject to maintainer compatibility acceptance. Constructor-level fetch injection is the SDK-supported seam and matches the existing managed Anthropic transport path without widening this split PR to all auth branches.

AGENTS.md: found and applied where relevant.

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

Label changes

Label justifications:

  • P2: This is normal-priority provider security hardening with a bounded Anthropic API-key blast radius and no active outage evidence.
  • merge-risk: 🚨 compatibility: The PR changes an existing provider path from SDK-default fetch behavior to OpenClaw guarded transport policy.
  • merge-risk: 🚨 availability: Guarded transport can block, time out, or otherwise fail custom endpoints that previously reached the network through SDK-default fetch.
  • 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 output from a real local HTTP server using the actual Anthropic SDK path, with positive loopback traffic and an SSRF-block negative control.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes after-fix terminal output from a real local HTTP server using the actual Anthropic SDK path, with positive loopback traffic and an SSRF-block negative control.
Evidence reviewed

PR surface:

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

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

What I checked:

  • Repository policy read: Root AGENTS.md and src/agents/AGENTS.md were read fully; provider runtime, dependency-contract, proof, and compatibility-risk guidance affected this review. (AGENTS.md:1, 85ee71223f0d)
  • Current main gap: streamAnthropic calls createClient, and the current API-key new Anthropic({...}) branch has no fetch constructor option. (src/llm/providers/anthropic.ts:998, 85ee71223f0d)
  • PR runtime change: The PR adds one import and fetch: buildGuardedModelFetch(model) to the API-key constructor only. (src/llm/providers/anthropic.ts:1011, c64e4680d14e)
  • Anthropic SDK dependency contract: The pinned @anthropic-ai/[email protected] ClientOptions exposes fetch?: Fetch, stores options.fetch ?? Shims.getDefaultFetch(), and calls this.fetch.call(undefined, url, fetchOptions) for requests. (https://unpkg.com/@anthropic-ai/[email protected]/client.js:111)
  • Shared guarded fetch contract: buildGuardedModelFetch resolves provider request policy, timeout, SSRF policy, dispatcher/proxy state, retry hints, lifecycle cleanup, and managed response handling. (src/agents/provider-transport-fetch.ts:776, 85ee71223f0d)
  • Sibling Anthropic transport invariant: The managed Anthropic transport already builds guarded fetch once and passes it into its Anthropic client path, including the API-key-style return branch. (src/agents/anthropic-transport-stream.ts:799, 85ee71223f0d)

Likely related people:

  • vincentkoc: Recent commits modified src/llm/providers/anthropic.ts around stream handling and Foundry/Claude auth policy, which are adjacent to this constructor branch. (role: recent Anthropic provider contributor; confidence: high; commits: 9e6332338855, 066700bdd0e4, 536c8a840bda; files: src/llm/providers/anthropic.ts, src/agents/anthropic-transport-stream.ts)
  • wangmiao0668000666: They authored merged guarded-fetch and provider-transport hardening work in sibling OpenAI/Azure paths, so they are relevant beyond merely opening this PR. (role: recent guarded-transport contributor; confidence: high; commits: 66ffad117610, 1bccd2930437, ff820d3942cc; files: src/agents/provider-transport-fetch.ts, src/llm/providers/azure-openai-responses.ts, src/llm/providers/openai-completions.ts)
  • steipete: Recent history shows provider transport, Anthropic request shaping, and model/provider edge-case work near this runtime boundary. (role: adjacent provider-runtime contributor; confidence: medium; commits: a02813164dd5, 9ead0ae9219, d92b3b5cc2bd; files: src/llm/providers/anthropic.ts, src/agents/provider-transport-fetch.ts)
  • Kaspre: Recent SSRF origin-trust work shaped the policy that guarded provider fetch now applies to custom and private endpoints. (role: SSRF policy contributor; confidence: medium; commits: 44840007d42d; files: 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