fix(anthropic): wire buildGuardedModelFetch into the GitHub Copilot createClient branch#98014
Conversation
…reateClient branch This brings the GitHub Copilot 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.
|
Codex review: needs maintainer review before merge. Reviewed June 30, 2026, 12:12 AM ET / 04:12 UTC. Summary PR surface: Source +2, Tests +222. Total +224 across 3 files. Reproducibility: yes. at source level: current main's GitHub Copilot branch omits the SDK Review metrics: 1 noteworthy metric.
Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Land the branch-specific guarded-fetch wiring after maintainer signoff on the documented custom-endpoint fail-closed behavior, leaving the other Anthropic constructors to their split PRs. Do we have a high-confidence way to reproduce the issue? Yes, at source level: current main's GitHub Copilot branch omits the SDK Is this the best way to solve the issue? Yes, this is the narrowest code fix for the split GitHub Copilot constructor: it reuses the existing guarded model fetch seam already used by sibling transport paths. The remaining question is maintainer acceptance of the compatibility risk, not a different implementation layer. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 85ee71223f0d. Label changesLabel justifications:
Evidence reviewedPR surface: Source +2, Tests +222. Total +224 across 3 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
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
|
|
Closing this PR as superseded by #99059 ("extract reusable AI runtime package"), which moved The new branch-isolated PR is on its way. |
…ient branch Reopens openclaw#98014 after packages/ai extraction (openclaw#99059) moved anthropic.ts out of src/llm/providers. Adds a single fetch line to the github-copilot createClient branch, parallel to the cloudflare branch wired by openclaw#98003 and the same-shape api-key/oauth/foundry siblings landing in this series. Bytes-cap verification is owned by buildModelFetch itself; this PR proves the constructor wiring via inline test through production streamAnthropic.
…ient branch Reopens openclaw#98014 after packages/ai extraction (openclaw#99059) moved anthropic.ts out of src/llm/providers. Adds a single fetch line to the github-copilot createClient branch, parallel to the cloudflare branch wired by openclaw#98003 and the same-shape api-key/oauth/foundry siblings landing in this series. Bytes-cap verification is owned by buildModelFetch itself; this PR proves the constructor wiring via inline test through production streamAnthropic.
…ient branch Reopens openclaw#98014 after packages/ai extraction (openclaw#99059) moved anthropic.ts out of src/llm/providers. Adds a single fetch line to the github-copilot createClient branch, parallel to the cloudflare branch wired by openclaw#98003 and the same-shape api-key/oauth/foundry siblings landing in this series. Bytes-cap verification is owned by buildModelFetch itself; this PR proves the constructor wiring via inline test through production streamAnthropic.
…ient branch Reopens openclaw#98014 after packages/ai extraction (openclaw#99059) moved anthropic.ts out of src/llm/providers. Adds a single fetch line to the github-copilot createClient branch, parallel to the cloudflare branch wired by openclaw#98003 and the same-shape api-key/oauth/foundry siblings landing in this series. Bytes-cap verification is owned by buildModelFetch itself; this PR proves the constructor wiring via inline test through production streamAnthropic.
What Problem This Solves
The Anthropic provider (
anthropic.ts) creates fiveAnthropicSDK clients — one for GitHub Copilot, 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 customfetchoption. 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 GitHub Copilot SDK constructor specifically, bringing that branch to guarded-fetch parity with:anthropic-transport-stream.ts:788(managed Anthropic transport)openai-responses.ts(fix(openai-responses): bound SSE response reads via buildGuardedModelFetch #97848)openai-completions.ts(fix(openai-completions): bound SSE response reads via buildGuardedModelFetch #97228)azure-openai-responses.tsThis 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
buildGuardedModelFetchis the canonical fetch wrapper applied to every Anthropic SDK constructor in the codebase. The Anthropic SDK acceptsfetch?: Fetchper its constructor options. The GitHub Copilot 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 inlinehttp.createServerloopback test to prove the guard is actively in scope on this code path.Evidence
Layer 1: Mock-based constructor wiring
anthropic.test.tsalready mocks the Anthropic SDK to capture constructor config. The existing GitHub Copilot test was extended to assert the newfetch:option is in scope:it("keeps GitHub Copilot 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)makestypeof config.fetch === "function"fail.Layer 2: SSRF-block guard-specific proof
anthropic-copilot.fetch-proof.test.tsprovesbuildGuardedModelFetchis actively blocking requests rather than just being present in constructor options:globalThis.fetchas a call counter — does NOT mock theanthropic-ai/sdkbaseUrl: "http://169.254.169.254/v1"— a cloud metadata IP that the SSRF guard blocksstreamAnthropicthrough the real SDK +buildGuardedModelFetchglobalThis.fetchis ever reachedexpect(globalFetchCalled).toBe(0)— guard-specific behaviorWhy this is needed: The raw
@anthropic-ai/sdkusesglobalThis.fetchby default (when no customfetchoption is supplied). A test that only asserts the SDK reachesglobalThis.fetchwould pass even withoutbuildGuardedModelFetch. The SSRF-block assertion closes that gap.Test results (real terminal output)
All tests pass consistently.
Layer 3: Real wire loopback proof (vitest inline test)
anthropic-copilot.fetch-proof.test.tsadds a seconddescribe(...)blockAnthropic GitHub Copilot guard real wire loopback proof (http.createServer)that spins up a realnode:httpserver on127.0.0.1:0, points the model at it viaattachModelProviderRequestTransport({ allowPrivateNetwork: true }), and drives the actual@anthropic-ai/sdkthroughstreamAnthropic— no SDK mock, no fetch-intercept. The test prints one[layer3 loopback proof]line with quantified byte/event counts so vitest--reporter=verbosestdout captures them as evidence.Run from worktree branch
fix/anthropic-cloudflare-fetch-wiring:Quantified read of the loopback proof:
server_hitshttp.createServerlistener actually received the SDK's POSTrequest_url/v1/messages@anthropic-ai/sdkappended/v1/messagesto baseUrl (correct shape)request_bytestext_bytesstreamAnthropicfromcontent_block_deltaeventsstop_reasonstopallowPrivateNetwork: trueLayer 3 reads:
server_hits=1confirms the localhttp.createServerlistener actually received the SDK's POST. This is real wire traffic, not aglobalThis.fetchspy.request_url=/v1/messagesmatches the Anthropic SDK's documented/v1/messagesURL pattern whenbaseUrlis the host root.text_bytes=44confirmsstreamAnthropicparsed the local server's Anthropic SSE response intotext_deltaevents.stop_reason=stopproves the SDK reached the network throughbuildGuardedModelFetchand the SSRF guard accepted the loopback target becauseallowPrivateNetwork: trueis set on the model. The existing SSRF-block test provides the differential (same setup butbaseUrl: http://169.254.169.254/v1→globalFetchCalled === 0).Real behavior proof
createClientinsrc/llm/providers/anthropic.ts(GitHub Copilot branch) now passesfetch: buildGuardedModelFetch(model)tonew Anthropic({...}), so Anthropic calls routed through the GitHub Copilot inherit OpenClaw guarded-fetch policy (SSRF block, request timeout, retry-hint injection, response lifecycle management, content-type normalization).fix/anthropic-cloudflare-fetch-wiring(commitb0039d6405).node scripts/run-vitest.mjs src/llm/providers/anthropic-copilot.fetch-proof.test.ts --run --reporter=verboseanthropic.test.tsasserts the constructor wiring (existing tests preserved, +5 lines).anthropic-copilot.fetch-proof.test.tsSSRF-block test stubsglobalThis.fetch, configuresbaseUrl: http://169.254.169.254/v1, assertsglobalFetchCalled === 0(block before globalThis.fetch is reached).anthropic-copilot.fetch-proof.test.tsloopbackdescribeblock spins uphttp.createServeron127.0.0.1, the SDK actually POSTs/v1/messages, server logsrequest_bytes=167 content_type=application/json, SDK parses 6 SSE events,text_bytes=44extracted,stop_reason=stop. The[layer3 loopback proof]line is emitted by the test'sconsole.log.anthropic-copilot.fetch-proof.test.tspass; 36/36 existing tests inanthropic.test.tsstill pass. Layer 1+2+3 together prove: (a)buildGuardedModelFetchis wired into the GitHub Copilot constructor, (b) the guard actively intercepts blocked targets beforeglobalThis.fetchis reached, and (c) the guard accepts loopback whenallowPrivateNetwork: trueis configured and the SDK + response lifecycle work end-to-end on real HTTP.provider-transport-fetch.test.ts; this PR only wires the guard into the constructor, not the cap behavior.Diff scope
Security & Privacy
fetchoption is standard Anthropic SDK API surface; no monkey-patching.Compatibility
What was not tested
provider-transport-fetch.test.ts; this PR only proves the guard is wired into the GitHub Copilot constructor.Related
buildGuardedModelFetchinsrc/agents/provider-transport-fetch.ts:776