fix(cli): narrow webfetch image rejection

Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com>
This commit is contained in:
chrarnoldus
2026-06-04 10:45:51 +00:00
co-authored by kiloconnect[bot]
parent a8a8dd8724
commit 1a1c0bd86a
4 changed files with 48 additions and 7 deletions
+1 -1
View File
@@ -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.
+7 -2
View File
@@ -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: {},
}
}
+4 -1
View File
@@ -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"
+36 -3
View File
@@ -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()
},
})