From 1a1c0bd86a458409f9c92118311bd1119626d9dc Mon Sep 17 00:00:00 2001 From: chrarnoldus <12196001+chrarnoldus@users.noreply.github.com> Date: Thu, 4 Jun 2026 10:45:51 +0000 Subject: [PATCH] fix(cli): narrow webfetch image rejection Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> --- .changeset/calm-icons-fetch.md | 2 +- packages/opencode/src/tool/webfetch.ts | 9 ++++- packages/opencode/src/util/media.ts | 5 ++- packages/opencode/test/tool/webfetch.test.ts | 39 ++++++++++++++++++-- 4 files changed, 48 insertions(+), 7 deletions(-) diff --git a/.changeset/calm-icons-fetch.md b/.changeset/calm-icons-fetch.md index 6717f429e35..69a84091136 100644 --- a/.changeset/calm-icons-fetch.md +++ b/.changeset/calm-icons-fetch.md @@ -2,4 +2,4 @@ "@kilocode/cli": patch --- -Prevent unsupported images fetched from the web from causing provider request errors. +Prevent icon images fetched from the web from causing provider request errors. diff --git a/packages/opencode/src/tool/webfetch.ts b/packages/opencode/src/tool/webfetch.ts index dc2c2ad35d7..01279ad55de 100644 --- a/packages/opencode/src/tool/webfetch.ts +++ b/packages/opencode/src/tool/webfetch.ts @@ -126,10 +126,15 @@ export const WebFetchTool = Tool.define( } // kilocode_change start - if (mime.startsWith("image/") && mime !== "image/svg+xml") { + if ( + mime.startsWith("image/") && + !isImageAttachment(mime) && + mime !== "image/svg+xml" && + mime !== "image/vnd.fastbidsheet" + ) { return { title, - output: `Unsupported image format: ${mime}. Supported formats are JPEG, PNG, GIF, and WebP.`, + output: `Unsupported image format: ${mime}.`, metadata: {}, } } diff --git a/packages/opencode/src/util/media.ts b/packages/opencode/src/util/media.ts index f90eb30ae4c..0e855821336 100644 --- a/packages/opencode/src/util/media.ts +++ b/packages/opencode/src/util/media.ts @@ -1,4 +1,5 @@ const startsWith = (bytes: Uint8Array, prefix: number[]) => prefix.every((value, index) => bytes[index] === value) +const icons = new Set(["image/vnd.microsoft.icon", "image/x-icon", "image/x-ico", "image/ico", "image/icon"]) // kilocode_change export function isPdfAttachment(mime: string) { return mime === "application/pdf" @@ -8,9 +9,11 @@ export function isMedia(mime: string) { return mime.startsWith("image/") || isPdfAttachment(mime) } +// kilocode_change start export function isImageAttachment(mime: string) { - return ["image/jpeg", "image/png", "image/gif", "image/webp"].includes(mime) // kilocode_change + return mime.startsWith("image/") && mime !== "image/svg+xml" && mime !== "image/vnd.fastbidsheet" && !icons.has(mime) } +// kilocode_change end export function sniffAttachmentMime(bytes: Uint8Array, fallback: string) { if (startsWith(bytes, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) return "image/png" diff --git a/packages/opencode/test/tool/webfetch.test.ts b/packages/opencode/test/tool/webfetch.test.ts index 89d10881cfc..e7df83c4f7b 100644 --- a/packages/opencode/test/tool/webfetch.test.ts +++ b/packages/opencode/test/tool/webfetch.test.ts @@ -83,16 +83,49 @@ describe("tool.webfetch", () => { }) // kilocode_change start - test("rejects unsupported image attachments", async () => { + test.each(["image/x-icon", "image/vnd.microsoft.icon"])("rejects %s attachments", async (mime) => { const bytes = new Uint8Array([0, 0, 1, 0]) await withFetch( - () => new Response(bytes, { status: 200, headers: { "content-type": "image/x-icon" } }), + () => new Response(bytes, { status: 200, headers: { "content-type": mime } }), async (url) => { await WithInstance.provide({ directory: projectRoot, fn: async () => { const result = await exec({ url: new URL("/favicon.ico", url).toString(), format: "markdown" }) - expect(result.output).toContain("Unsupported image format: image/x-icon") + expect(result.output).toContain(`Unsupported image format: ${mime}`) + expect(result.attachments).toBeUndefined() + }, + }) + }, + ) + }) + + test("returns non-icon image responses as file attachments", async () => { + const bytes = new Uint8Array([0, 0, 0, 0]) + await withFetch( + () => new Response(bytes, { status: 200, headers: { "content-type": "image/avif" } }), + async (url) => { + await WithInstance.provide({ + directory: projectRoot, + fn: async () => { + const result = await exec({ url: new URL("/image.avif", url).toString(), format: "markdown" }) + expect(result.output).toBe("Image fetched successfully") + expect(result.attachments?.[0].mime).toBe("image/avif") + }, + }) + }, + ) + }) + + test("keeps fastbidsheet responses as text output", async () => { + await withFetch( + () => new Response("sheet", { status: 200, headers: { "content-type": "image/vnd.fastbidsheet" } }), + async (url) => { + await WithInstance.provide({ + directory: projectRoot, + fn: async () => { + const result = await exec({ url: new URL("/sheet", url).toString(), format: "text" }) + expect(result.output).toBe("sheet") expect(result.attachments).toBeUndefined() }, })