diff --git a/packages/core/src/tool/plugin/webfetch.ts b/packages/core/src/tool/plugin/webfetch.ts index cbbe6a224cc..9f04d216c8e 100644 --- a/packages/core/src/tool/plugin/webfetch.ts +++ b/packages/core/src/tool/plugin/webfetch.ts @@ -56,8 +56,8 @@ const headers = (format: Format, userAgent: string) => ({ "Accept-Language": "en-US,en;q=0.9", }) -const browserUserAgent = - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36" +const openCodeUserAgent = + "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; OpenCode-User/1.0; +https://opencode.ai" const isCloudflareChallenge = (error: unknown) => { if (!error || typeof error !== "object" || !("reason" in error)) return false @@ -74,14 +74,14 @@ const isCloudflareChallenge = (error: unknown) => { return response.status === 403 && response.headers["cf-mitigated"] === "challenge" } -const request = (url: string, format: Format, userAgent = browserUserAgent) => +const request = (url: string, format: Format, userAgent = openCodeUserAgent) => HttpClientRequest.get(url).pipe(HttpClientRequest.setHeaders(headers(format, userAgent))) const assertHttpUrl = (url: URL) => { if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("URL must use http:// or https://") } -const execute = (http: HttpClient.HttpClient, url: string, format: Format, userAgent = browserUserAgent) => +const execute = (http: HttpClient.HttpClient, url: string, format: Format, userAgent = openCodeUserAgent) => http.execute(request(url, format, userAgent)).pipe(Effect.flatMap(HttpClientResponse.filterStatusOk)) const collectBody = (response: HttpClientResponse.HttpClientResponse) => diff --git a/packages/core/test/tool-webfetch.test.ts b/packages/core/test/tool-webfetch.test.ts index b2abb31e40a..6a1617b3d74 100644 --- a/packages/core/test/tool-webfetch.test.ts +++ b/packages/core/test/tool-webfetch.test.ts @@ -23,6 +23,8 @@ const webFetchToolNode = makeLocationNode({ }) const sessionID = Session.ID.make("ses_webfetch_test") +const webFetchUserAgent = + "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; OpenCode-User/1.0; +https://opencode.ai" const requests: Array<{ readonly url: string; readonly headers: Record }> = [] const assertions: Permission.AssertInput[] = [] let respond = (_request: HttpClientRequest.HttpClientRequest) => @@ -376,7 +378,17 @@ describe("WebFetchTool registration", () => { expect(assertions).toMatchObject([ { sessionID, action: "webfetch", resources: [url], save: ["*"], metadata: { url, format: "text", timeout: 4 } }, ]) - expect(requests).toMatchObject([{ url, headers: { accept: expect.stringContaining("text/plain;q=1.0") } }]) + expect(requests).toMatchObject([ + { + url, + headers: { + accept: "text/plain;q=1.0, text/markdown;q=0.9, text/html;q=0.8, */*;q=0.1", + "accept-language": "en-US,en;q=0.9", + "user-agent": webFetchUserAgent, + }, + }, + ]) + expect(requests[0]?.headers).not.toHaveProperty("sec-fetch-mode") }), ) @@ -397,15 +409,23 @@ describe("WebFetchTool registration", () => { }), ) - live.effect("follows redirects while approving only the requested URL", () => - Effect.acquireUseRelease( + live.effect("follows redirects while approving only the requested URL", () => { + const received: Array> = [] + return Effect.acquireUseRelease( Effect.sync(() => Bun.serve({ port: 0, - fetch: (request) => - new URL(request.url).pathname === "/redirect" - ? new Response("", { status: 302, headers: { location: "/target" } }) - : new Response("redirected", { headers: { "content-type": "text/plain" } }), + fetch: (request) => { + received.push({ + accept: request.headers.get("accept"), + "accept-language": request.headers.get("accept-language"), + "sec-fetch-mode": request.headers.get("sec-fetch-mode"), + "user-agent": request.headers.get("user-agent"), + }) + if (new URL(request.url).pathname === "/redirect") + return new Response("", { status: 302, headers: { location: "/target" } }) + return new Response("redirected", { headers: { "content-type": "text/plain" } }) + }, }), ), (server) => @@ -421,10 +441,18 @@ describe("WebFetchTool registration", () => { expect(assertions).toMatchObject([ { sessionID, action: "webfetch", resources: [url], save: ["*"], metadata: { url, format: "text" } }, ]) + expect(received).toEqual( + Array.from({ length: 2 }, () => ({ + accept: "text/plain;q=1.0, text/markdown;q=0.9, text/html;q=0.8, */*;q=0.1", + "accept-language": "en-US,en;q=0.9", + "sec-fetch-mode": null, + "user-agent": webFetchUserAgent, + })), + ) }), (server) => Effect.promise(() => server.stop(true)), - ), - ) + ) + }) it.effect("rejects non-HTTP schemes before permission or transport", () => Effect.gen(function* () { @@ -549,7 +577,7 @@ describe("WebFetchTool registration", () => { content: [{ type: "text", text: "ok" }], }) expect(requests).toHaveLength(2) - expect(requests[0]?.headers["user-agent"]).toContain("Mozilla/5.0") + expect(requests[0]?.headers["user-agent"]).toBe(webFetchUserAgent) expect(requests[1]?.headers["user-agent"]).toBe("opencode") }), )