From 5ca2c50fb1507c45521c27086e4d48f10b6e0890 Mon Sep 17 00:00:00 2001 From: zerob13 Date: Sun, 9 Aug 2026 16:20:50 +0800 Subject: [PATCH 1/2] fix(agent): allow octet stream text reads --- src/main/lib/binaryReadGuard.ts | 36 ++----------------- .../tool/agentTools/agentFileSystemHandler.ts | 10 +++++- src/main/tool/agentTools/agentToolManager.ts | 4 +-- test/main/lib/binaryReadGuard.test.ts | 33 ++++++----------- .../agentTools/agentToolManagerRead.test.ts | 18 ++++++++++ 5 files changed, 43 insertions(+), 58 deletions(-) diff --git a/src/main/lib/binaryReadGuard.ts b/src/main/lib/binaryReadGuard.ts index c7cf808b90..69cd8dee8e 100644 --- a/src/main/lib/binaryReadGuard.ts +++ b/src/main/lib/binaryReadGuard.ts @@ -11,17 +11,6 @@ const TEXT_LIKE_MIMES = new Set([ 'application/x-sh' ]) -const DOCUMENT_MIMES = new Set([ - 'application/pdf', - 'application/msword', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'application/vnd.ms-powerpoint', - 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'application/vnd.ms-excel', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'application/vnd.oasis.opendocument.spreadsheet' -]) - const ALWAYS_BINARY_MIMES = new Set([ 'application/zip', 'application/x-zip', @@ -36,10 +25,6 @@ export function isTextLikeMime(mimeType: string): boolean { return mimeType.startsWith('text/') || TEXT_LIKE_MIMES.has(mimeType) } -export function isDocumentMime(mimeType: string): boolean { - return DOCUMENT_MIMES.has(mimeType) -} - export async function shouldRejectAcpTextRead(filePath: string): Promise<{ reject: boolean mimeType: string @@ -58,31 +43,16 @@ export async function shouldRejectAcpTextRead(filePath: string): Promise<{ return { reject: true, mimeType } } -export async function shouldRejectAgentBinaryRead( - filePath: string, - mimeType: string -): Promise { +export function shouldRejectAgentBinaryRead(mimeType: string): boolean { if (mimeType.startsWith('image/')) { return false } - if (isTextLikeMime(mimeType) || isDocumentMime(mimeType) || mimeType === 'text/csv') { - return false - } - - if ( + return ( ALWAYS_BINARY_MIMES.has(mimeType) || mimeType.startsWith('audio/') || mimeType.startsWith('video/') - ) { - return true - } - - if (mimeType === 'application/octet-stream') { - return !(await isLikelyTextFile(filePath)) - } - - return false + ) } export function buildBinaryReadGuidance( diff --git a/src/main/tool/agentTools/agentFileSystemHandler.ts b/src/main/tool/agentTools/agentFileSystemHandler.ts index 281da2c5e8..7f1ea243a6 100644 --- a/src/main/tool/agentTools/agentFileSystemHandler.ts +++ b/src/main/tool/agentTools/agentFileSystemHandler.ts @@ -804,7 +804,15 @@ export class AgentFileSystemHandler { enforceAllowed: false, accessType: 'read' }) - const fullContent = await fs.readFile(validPath, 'utf-8') + const bytes = await fs.readFile(validPath) + let fullContent: string + if (bytes[0] === 0xff && bytes[1] === 0xfe) { + fullContent = bytes.subarray(2).toString('utf16le') + } else if (bytes[0] === 0xfe && bytes[1] === 0xff) { + fullContent = new TextDecoder('utf-16be').decode(bytes.subarray(2)) + } else { + fullContent = bytes.toString('utf8').replace(/^\uFEFF/, '') + } const totalLength = fullContent.length // Determine effective limit diff --git a/src/main/tool/agentTools/agentToolManager.ts b/src/main/tool/agentTools/agentToolManager.ts index 0e94eaa393..ec3db9b1b2 100644 --- a/src/main/tool/agentTools/agentToolManager.ts +++ b/src/main/tool/agentTools/agentToolManager.ts @@ -1240,7 +1240,7 @@ export class AgentToolManager { ) const mimeType = await this.getFileService().getMimeType(validPath) - if (await shouldRejectAgentBinaryRead(validPath, mimeType)) { + if (shouldRejectAgentBinaryRead(mimeType)) { return { content: buildBinaryReadGuidance(validPath, mimeType, 'agent') } @@ -1678,7 +1678,7 @@ export class AgentToolManager { if (mimeType === 'text/csv') { return false } - if (mimeType.startsWith('text/')) { + if (mimeType.startsWith('text/') || mimeType === 'application/octet-stream') { return true } diff --git a/test/main/lib/binaryReadGuard.test.ts b/test/main/lib/binaryReadGuard.test.ts index 9a4246237f..f14ecdf16c 100644 --- a/test/main/lib/binaryReadGuard.test.ts +++ b/test/main/lib/binaryReadGuard.test.ts @@ -1,30 +1,19 @@ -import { describe, expect, it, vi, beforeEach } from 'vitest' +import { describe, expect, it } from 'vitest' import { shouldRejectAgentBinaryRead } from '../../../src/main/lib/binaryReadGuard' -import { isLikelyTextFile } from '@/file/mime' - -vi.mock('@/file/mime', () => ({ - detectMimeType: vi.fn(), - isLikelyTextFile: vi.fn() -})) describe('binaryReadGuard', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - it('falls back to text detection for application/octet-stream', async () => { - vi.mocked(isLikelyTextFile).mockResolvedValue(true) - - await expect( - shouldRejectAgentBinaryRead('/tmp/maybe-text.bin', 'application/octet-stream') - ).resolves.toBe(false) + it('allows application/octet-stream without binary sniffing', () => { + expect(shouldRejectAgentBinaryRead('application/octet-stream')).toBe(false) }) - it('still rejects octet-stream files that do not look like text', async () => { - vi.mocked(isLikelyTextFile).mockResolvedValue(false) + it.each(['application/zip', 'application/wasm', 'audio/mpeg', 'video/mp4'])( + 'still rejects known binary MIME %s', + (mimeType) => { + expect(shouldRejectAgentBinaryRead(mimeType)).toBe(true) + } + ) - await expect( - shouldRejectAgentBinaryRead('/tmp/blob.bin', 'application/octet-stream') - ).resolves.toBe(true) + it('keeps images available for vision reads', () => { + expect(shouldRejectAgentBinaryRead('image/png')).toBe(false) }) }) diff --git a/test/main/tool/agentTools/agentToolManagerRead.test.ts b/test/main/tool/agentTools/agentToolManagerRead.test.ts index 8bf1baba69..fdcd01f42f 100644 --- a/test/main/tool/agentTools/agentToolManagerRead.test.ts +++ b/test/main/tool/agentTools/agentToolManagerRead.test.ts @@ -221,6 +221,24 @@ describe('AgentToolManager read routing', () => { expect(fileService.prepareFileCompletely).not.toHaveBeenCalled() }) + it('reads UTF-16 code files reported as application/octet-stream', async () => { + const filePath = path.join(workspaceDir, '.tmp-change.diff') + await fs.writeFile( + filePath, + Buffer.from(`\uFEFFdiff --git a/file.ts b/file.ts\n+const value = 1\n`, 'utf16le') + ) + fileService.getMimeType.mockResolvedValue('application/octet-stream') + + const result = (await manager.callTool('read', { path: '.tmp-change.diff' }, 'conv1')) as { + content: string + } + + expect(result.content).toContain('diff --git a/file.ts b/file.ts') + expect(result.content).toContain('+const value = 1') + expect(result.content).not.toContain('\u0000') + expect(fileService.prepareFileCompletely).not.toHaveBeenCalled() + }) + it('uses the Agent auto-truncate limit while preserving an explicit read limit', async () => { const filePath = path.join(workspaceDir, 'large-note.txt') await fs.writeFile(filePath, 'x'.repeat(1_500), 'utf-8') From db4eaba49a50baa61952d46094ee7267f351b828 Mon Sep 17 00:00:00 2001 From: zerob13 Date: Sun, 9 Aug 2026 16:34:10 +0800 Subject: [PATCH 2/2] test(agent): cover text read encodings --- .../agentTools/agentToolManagerRead.test.ts | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/test/main/tool/agentTools/agentToolManagerRead.test.ts b/test/main/tool/agentTools/agentToolManagerRead.test.ts index fdcd01f42f..4f47f29314 100644 --- a/test/main/tool/agentTools/agentToolManagerRead.test.ts +++ b/test/main/tool/agentTools/agentToolManagerRead.test.ts @@ -221,20 +221,37 @@ describe('AgentToolManager read routing', () => { expect(fileService.prepareFileCompletely).not.toHaveBeenCalled() }) - it('reads UTF-16 code files reported as application/octet-stream', async () => { - const filePath = path.join(workspaceDir, '.tmp-change.diff') - await fs.writeFile( - filePath, + it.each([ + [ + 'UTF-16LE', + '.tmp-change-le.diff', Buffer.from(`\uFEFFdiff --git a/file.ts b/file.ts\n+const value = 1\n`, 'utf16le') - ) + ], + [ + 'UTF-16BE', + '.tmp-change-be.diff', + Buffer.from(`\uFEFFdiff --git a/file.ts b/file.ts\n+const value = 1\n`, 'utf16le').swap16() + ], + [ + 'UTF-8 BOM', + '.tmp-change-utf8.diff', + Buffer.concat([ + Buffer.from([0xef, 0xbb, 0xbf]), + Buffer.from(`diff --git a/file.ts b/file.ts\n+const value = 1\n`, 'utf8') + ]) + ] + ])('reads %s code files reported as application/octet-stream', async (_encoding, name, bytes) => { + const filePath = path.join(workspaceDir, name) + await fs.writeFile(filePath, bytes) fileService.getMimeType.mockResolvedValue('application/octet-stream') - const result = (await manager.callTool('read', { path: '.tmp-change.diff' }, 'conv1')) as { + const result = (await manager.callTool('read', { path: name }, 'conv1')) as { content: string } expect(result.content).toContain('diff --git a/file.ts b/file.ts') expect(result.content).toContain('+const value = 1') + expect(result.content).not.toContain('\uFEFF') expect(result.content).not.toContain('\u0000') expect(fileService.prepareFileCompletely).not.toHaveBeenCalled() })