From 46ec327774fb623f337f778e53aa2dd9ec7d0ab0 Mon Sep 17 00:00:00 2001 From: Jackie Chui Date: Fri, 1 May 2026 16:25:55 -0700 Subject: [PATCH] added normalize content to fix potential read grep and glob issues with mimeTypes and Uint8Array, made sure edit and grep must have minimum string in it before performing action --- .../sandbox/backends/StateBackend.test.ts | 13 +++++++++++++ .../SmartAgent/sandbox/backends/StateBackend.ts | 9 ++++++--- .../nodes/agentflow/SmartAgent/sandbox/tools/fs.ts | 4 ++-- .../nodes/agentflow/SmartAgent/sandbox/utils.ts | 9 +++++++++ 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/packages/components/nodes/agentflow/SmartAgent/sandbox/backends/StateBackend.test.ts b/packages/components/nodes/agentflow/SmartAgent/sandbox/backends/StateBackend.test.ts index 6ea07494f..2e9cf2e7f 100644 --- a/packages/components/nodes/agentflow/SmartAgent/sandbox/backends/StateBackend.test.ts +++ b/packages/components/nodes/agentflow/SmartAgent/sandbox/backends/StateBackend.test.ts @@ -43,6 +43,19 @@ describe('StateBackend — write/read', () => { } }) + it('write of Uint8Array to a text-mimeType path is decoded to a string on read', async () => { + const backend = new StateBackend() + const bytes = new TextEncoder().encode('hi from bytes') + await backend.write('/workspace/decoded.txt', bytes) + const result = await backend.read('/workspace/decoded.txt') + expect('content' in result).toBe(true) + if ('content' in result) { + expect(typeof result.content).toBe('string') + expect(result.content).toBe('hi from bytes') + expect(result.mimeType).toBe('text/plain') + } + }) + it('constructor accepts initialFiles and makes them readable', async () => { const now = Date.now() const initialFiles: Record = { diff --git a/packages/components/nodes/agentflow/SmartAgent/sandbox/backends/StateBackend.ts b/packages/components/nodes/agentflow/SmartAgent/sandbox/backends/StateBackend.ts index 23874dc6a..301ec4b08 100644 --- a/packages/components/nodes/agentflow/SmartAgent/sandbox/backends/StateBackend.ts +++ b/packages/components/nodes/agentflow/SmartAgent/sandbox/backends/StateBackend.ts @@ -13,7 +13,7 @@ import { ReadResult, WriteResult } from '../BackendProtocol' -import { escapeRegex, getMimeType, globToRegex, isTextMimeType, paginateLines } from '../utils' +import { escapeRegex, getMimeType, globToRegex, isTextMimeType, normalizeContent, paginateLines } from '../utils' type FileStore = Record @@ -21,7 +21,10 @@ export class StateBackend implements BackendProtocol { protected files: FileStore constructor(initialFiles: FileStore = {}) { - this.files = { ...initialFiles } + this.files = {} + for (const [path, data] of Object.entries(initialFiles)) { + this.files[path] = { ...data, content: normalizeContent(data.content, data.mimeType) } + } } async write(filePath: string, content: string | Uint8Array): Promise { @@ -30,7 +33,7 @@ export class StateBackend implements BackendProtocol { } const mimeType = getMimeType(filePath) const now = Date.now() - const data: FileData = { content, mimeType, created_at: now, modified_at: now } + const data: FileData = { content: normalizeContent(content, mimeType), mimeType, created_at: now, modified_at: now } this.files[filePath] = data const filesUpdate: FilesUpdate = { [filePath]: data } diff --git a/packages/components/nodes/agentflow/SmartAgent/sandbox/tools/fs.ts b/packages/components/nodes/agentflow/SmartAgent/sandbox/tools/fs.ts index c2261ff9c..428bd8be7 100644 --- a/packages/components/nodes/agentflow/SmartAgent/sandbox/tools/fs.ts +++ b/packages/components/nodes/agentflow/SmartAgent/sandbox/tools/fs.ts @@ -42,7 +42,7 @@ export function buildFsTools(backend: BackendProtocol, onFilesUpdate?: (update: 'Edit an existing text file by replacing a specific string. Fails if the string appears more than once unless replace_all is true.', schema: z.object({ file_path: z.string().describe('Absolute path of the file to edit (e.g. /workspace/hello.txt)'), - old_str: z.string().describe('Exact string to find and replace'), + old_str: z.string().min(1).describe('Exact string to find and replace'), new_str: z.string().describe('String to replace it with'), replace_all: z.boolean().optional().describe('Replace every occurrence instead of requiring exactly one (default false)') }), @@ -87,7 +87,7 @@ export function buildFsTools(backend: BackendProtocol, onFilesUpdate?: (update: name: 'grep_files', description: 'Search file contents for a regex pattern. Returns matching lines with file path and line number.', schema: z.object({ - pattern: z.string().describe('Regex pattern to search for (e.g. "function|const")'), + pattern: z.string().min(1).describe('Regex pattern to search for (e.g. "function|const")'), dir_path: z.string().optional().describe('Directory to search in (default /). Pass null to search all files.'), glob: z.string().optional().describe('Optional glob to filter filenames (e.g. *.ts)') }), diff --git a/packages/components/nodes/agentflow/SmartAgent/sandbox/utils.ts b/packages/components/nodes/agentflow/SmartAgent/sandbox/utils.ts index ba325102d..febb38d60 100644 --- a/packages/components/nodes/agentflow/SmartAgent/sandbox/utils.ts +++ b/packages/components/nodes/agentflow/SmartAgent/sandbox/utils.ts @@ -93,6 +93,15 @@ export function isTextMimeType(mimeType: string): boolean { return TEXT_MIME_PREFIXES.some((p) => mimeType.startsWith(p)) || TEXT_MIME_EXACT.has(mimeType) } +// Coerce content to match its mimeType so backends can rely on +// "text mimeType ⇒ string content, binary mimeType ⇒ Uint8Array content" as an invariant. +export function normalizeContent(content: string | Uint8Array, mimeType: string): string | Uint8Array { + if (isTextMimeType(mimeType)) { + return typeof content === 'string' ? content : new TextDecoder().decode(content) + } + return content instanceof Uint8Array ? content : new TextEncoder().encode(content) +} + export function paginateLines(text: string, offset: number, limit: number): { content: string; truncated: boolean } { const lines = text.split('\n') const truncated = lines.length > offset + limit