From 0373bc4d2557a5d96a6c3d904f72280f3260d119 Mon Sep 17 00:00:00 2001 From: Raven Scott Date: Wed, 19 Aug 2026 14:27:00 -0400 Subject: [PATCH] Further Agent Harness Fixes (write_file) and (create_directory) tools updates --- kernel/bin/agent | 224 ++++++++++++++++-- .../lib/agent/agent-helpers.js | 53 +++++ .../lib/agent/agent-tool-definitions.js | 13 +- .../lib/agent/agent-tool-dispatch.js | 151 +++++++++++- .../bare-os-coreutils/lib/agent/agent-tui.js | 7 +- .../test/agent-config-surface.test.mjs | 2 + .../test/agent-grok-port.test.mjs | 50 ++++ .../test/agent-helpers.test.mjs | 14 ++ packages/bare-os-seeder/kernel/bin/agent | 224 ++++++++++++++++-- 9 files changed, 684 insertions(+), 54 deletions(-) diff --git a/kernel/bin/agent b/kernel/bin/agent index 00de640c..2483e7d7 100644 --- a/kernel/bin/agent +++ b/kernel/bin/agent @@ -1193,6 +1193,59 @@ function bareAgentNormalizeAbsPath(absPath) { return p } +/** + * Expand ~ / $HOME / relative guest paths to an absolute VFS path. + * @param {string} raw + * @param {string} home + * @param {string} [cwd] + */ +function bareAgentExpandGuestPath(raw, home, cwd) { + let p = String(raw == null ? '' : raw).trim() + if (!p) return '' + p = p.replace(/\\/g, '/') + const h = String(home || '').replace(/\/+$/, '') || '/home/guest' + let c = String(cwd || h).replace(/\/+$/, '') || h + if (c === '~') c = h + else if (c.startsWith('~/')) c = h + c.slice(1) + else if (c.startsWith('$HOME/')) c = h + c.slice(5) + else if (c === '$HOME') c = h + if (p === '~' || p === '$HOME') return h + if (p.startsWith('~/')) { + p = h + '/' + p.slice(2) + } else if (p.startsWith('$HOME/')) { + p = h + '/' + p.slice(6) + } else if (!p.startsWith('/')) { + p = (c === '/' ? '/' + p : c + '/' + p) + } + p = p.replace(/\/{2,}/g, '/') + if (p.length > 1) p = p.replace(/\/+$/, '') + return p +} + +/** + * Pick a path-like argument from a tool-call blob (path / file_path / dir / …). + * @param {Record} args + */ +function bareAgentPickPathArg(args) { + if (!args || typeof args !== 'object') return '' + const keys = [ + 'path', + 'file_path', + 'file', + 'filename', + 'dir', + 'directory', + 'folder', + 'from_path', + 'to_path' + ] + for (let i = 0; i < keys.length; i++) { + const v = args[keys[i]] + if (typeof v === 'string' && v.trim()) return v.trim() + } + return '' +} + /** * @param {string} absPath * @param {unknown} prefixes @@ -6927,7 +6980,7 @@ function bareAgentToolDefinitions() { function: { name: 'read_file', description: - 'Read a UTF-8 text file from the VFS. Path must be absolute (e.g. /home/guest/...). Optional offset/limit return numbered line slices (Grok-style).', + 'Read a UTF-8 text file from the VFS. Path may be absolute or ~/... (tilde is expanded). Optional offset/limit return numbered line slices (Grok-style).', parameters: { type: 'object', properties: { @@ -6959,11 +7012,12 @@ function bareAgentToolDefinitions() { function: { name: 'write_file', description: - 'Create a new file or overwrite an existing one at any writable guest path. Parent directories are created as needed. Do not ask permission.', + 'Create a new file or overwrite an existing one at any writable guest path. Path may be absolute or ~/... (tilde is expanded to the session home). Parent directories are created as needed. Returns the expanded path — verify with list_directory or file_stat before claiming the file exists. Do not ask permission.', parameters: { type: 'object', properties: { - path: { type: 'string' }, + path: { type: 'string', description: 'Absolute path or ~/file' }, + file_path: { type: 'string', description: 'Alias for path' }, content: { type: 'string', description: 'Full file contents' } }, required: ['path', 'content'] @@ -7003,11 +7057,13 @@ function bareAgentToolDefinitions() { type: 'function', function: { name: 'create_directory', - description: 'Create a directory (recursive).', + description: + 'Create a directory (recursive). Path may be absolute or ~/... (tilde is expanded). Returns the expanded path — list_directory it before claiming it exists.', parameters: { type: 'object', properties: { - path: { type: 'string' } + path: { type: 'string', description: 'Absolute path or ~/dir' }, + dir: { type: 'string', description: 'Alias for path' } }, required: ['path'] } @@ -8582,6 +8638,54 @@ async function bareAgentDispatchTool(o) { return bareAgentJsonResult({ ok: false, error: 'invalid_tool_arguments_json' }) } const cfgNow = configRef.current || {} + const resolvedHome = + String(home || '').trim() || + (typeof bareAgentResolveHome === 'function' ? bareAgentResolveHome(ctx) : '') || + '/home/guest' + if (typeof bareAgentExpandGuestPath === 'function') { + const PATH_KEYS = [ + 'path', + 'file_path', + 'file', + 'filename', + 'dir', + 'directory', + 'folder', + 'root', + 'cwd', + 'from_path', + 'to_path', + 'from', + 'to', + 'src', + 'source', + 'dest', + 'destination', + 'target', + 'old_path', + 'new_path' + ] + let cwdForExpand = resolvedHome + if (paths && paths.lastCwd && typeof bareAgentReadTextFile === 'function') { + try { + const t = String(await bareAgentReadTextFile(ctx, paths.lastCwd) || '').trim() + if (t) cwdForExpand = t + } catch { + /* optional */ + } + } + if (typeof args.cwd === 'string' && args.cwd.trim()) { + args.cwd = bareAgentExpandGuestPath(args.cwd, resolvedHome, resolvedHome) + cwdForExpand = String(args.cwd || cwdForExpand) + } + for (let i = 0; i < PATH_KEYS.length; i++) { + const k = PATH_KEYS[i] + if (k === 'cwd') continue + if (typeof args[k] === 'string' && args[k].trim()) { + args[k] = bareAgentExpandGuestPath(args[k], resolvedHome, cwdForExpand) + } + } + } if ( !internalGate && cfgNow.autonomous_active && @@ -9335,13 +9439,37 @@ async function bareAgentDispatchTool(o) { } if (toolName === 'write_file') { - const path = typeof args.path === 'string' ? args.path : '' + const path = + (typeof args.path === 'string' && args.path.trim()) || + (typeof args.file_path === 'string' && args.file_path.trim()) || + (typeof args.file === 'string' && args.file.trim()) || + (typeof bareAgentPickPathArg === 'function' ? bareAgentPickPathArg(args) : '') || + '' if (!enforceAutonomousPath(path, 'mutate')) { - return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' }) + return bareAgentJsonResult({ ok: false, error: 'path_not_allowed', path }) } - const content = typeof args.content === 'string' ? args.content : '' + const content = + typeof args.content === 'string' + ? args.content + : typeof args.contents === 'string' + ? args.contents + : typeof args.text === 'string' + ? args.text + : typeof args.body === 'string' + ? args.body + : '' if (!path.startsWith('/') || path.includes('..')) { - return bareAgentJsonResult({ ok: false, error: 'bad_path' }) + return bareAgentJsonResult({ + ok: false, + error: 'bad_path', + path, + hint: + 'Use an absolute path or ~/... (tilde expands to ' + + resolvedHome + + '). Example: ' + + resolvedHome + + '/test_work/hello.js' + }) } if (!vfs || typeof vfs.writeFile !== 'function' || typeof vfs.mkdir !== 'function') { return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' }) @@ -9362,7 +9490,26 @@ async function bareAgentDispatchTool(o) { ? ctx.b4a.from(content) : new TextEncoder().encode(content) await vfs.writeFile(path, body) - return bareAgentJsonResult({ ok: true, bytes: body.length }) + let verified = false + try { + if (typeof vfs.lstat === 'function') verified = Boolean(await vfs.lstat(path)) + else if (typeof vfs.stat === 'function') verified = Boolean(await vfs.stat(path)) + else if (typeof vfs.readFile === 'function') { + const got = await vfs.readFile(path) + verified = got != null + } else verified = true + } catch { + verified = false + } + if (!verified) { + return bareAgentJsonResult({ + ok: false, + error: 'write_not_visible', + path, + hint: 'write_file returned but the path is not visible in the VFS. Retry with the expanded absolute path.' + }) + } + return bareAgentJsonResult({ ok: true, path, bytes: body.length, verified: true }) } if (toolName === 'edit_file' || toolName === 'search_replace') { @@ -9459,19 +9606,54 @@ async function bareAgentDispatchTool(o) { } if (toolName === 'create_directory') { - const path = typeof args.path === 'string' ? args.path : '' + const path = + (typeof args.path === 'string' && args.path.trim()) || + (typeof args.dir === 'string' && args.dir.trim()) || + (typeof args.directory === 'string' && args.directory.trim()) || + (typeof args.folder === 'string' && args.folder.trim()) || + (typeof bareAgentPickPathArg === 'function' ? bareAgentPickPathArg(args) : '') || + '' if (!enforceAutonomousPath(path, 'mutate')) { - return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' }) + return bareAgentJsonResult({ ok: false, error: 'path_not_allowed', path }) } - if (!path.startsWith('/')) { - return bareAgentJsonResult({ ok: false, error: 'bad_path' }) + if (!path.startsWith('/') || path.includes('..')) { + return bareAgentJsonResult({ + ok: false, + error: 'bad_path', + path, + hint: + 'Use an absolute path or ~/... (tilde expands to ' + + resolvedHome + + '). Example: ' + + resolvedHome + + '/test_work' + }) } if (!vfs || typeof vfs.mkdir !== 'function') { return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' }) } appendProgress('mkdir ' + path) await vfs.mkdir(path, { recursive: true }) - return bareAgentJsonResult({ ok: true }) + let verified = false + try { + if (typeof vfs.lstat === 'function') verified = Boolean(await vfs.lstat(path)) + else if (typeof vfs.stat === 'function') verified = Boolean(await vfs.stat(path)) + else if (typeof vfs.readdir === 'function') { + await vfs.readdir(path) + verified = true + } else verified = true + } catch { + verified = false + } + if (!verified) { + return bareAgentJsonResult({ + ok: false, + error: 'mkdir_not_visible', + path, + hint: 'create_directory returned but the path is not visible. Retry with ' + path + }) + } + return bareAgentJsonResult({ ok: true, path, verified: true }) } if (toolName === 'search_files') { @@ -9733,7 +9915,10 @@ async function bareAgentDispatchTool(o) { } if (toolName === 'list_directory') { - const dir = typeof args.path === 'string' ? args.path : '' + const dir = + (typeof args.path === 'string' && args.path.trim()) || + (typeof args.dir === 'string' && args.dir.trim()) || + resolvedHome const maxEnt = typeof args.max_entries === 'number' && Number.isFinite(args.max_entries) ? Math.min(Math.floor(args.max_entries), 2000) @@ -13598,7 +13783,7 @@ WORK POLICY. ACCESS (denylist, not allowlist). You already have full guest admin. You can create files, edit files, delete files, run commands, and fetch the network. NEVER ASK whether you may — just do it. Only refuse when a denylist or the read-only base system blocks the path. - Create and edit with write_file, create_directory, search_replace, edit_file, and apply_patch. Never say you cannot write files. Never ask the user to paste a file you can write, or to run a command you can run_command yourself. -- Writable paths: /home, /tmp, /mnt, /root, /var, /opt, extra drives, ~/.agent. Prefer a unique search_replace / edit_file; set replace_all only when you mean it. +- Writable paths: /home, /tmp, /mnt, /root, /var, /opt, extra drives, ~/.agent. The harness expands ~ and $HOME to the resolved home in This session. Never write /home/guest unless that is the resolved home. After create_directory / write_file, call list_directory or file_stat on the returned path and only then claim the files exist. Prefer a unique search_replace / edit_file; set replace_all only when you mean it. - Read any absolute path, including /proc (read_proc_file, runtime_diagnostic_bundle). Use read_file offset/limit for large files. - Run every guest command via run_command (command_deny is empty by default). Prefer list_directory, glob_files, and file_stat over ls/find when you only need names. list_bin lists guest /bin utilities (POSIX-in-JS, not GNU). Never dump curl/ip/ls for the user — call run_command and report stdout. - Delete and move are enabled. Do not mutate the read-only base system: /bin /etc /boot /lib /usr /share /proc /dev /sys /run. @@ -13712,7 +13897,10 @@ function bareAgentSessionHomeBlock(ctx, home, paths) { paths.config + '. Always expand ~ to ' + home + - ' when constructing absolute paths for tools.\n' + + ' when constructing absolute paths for tools. ~/test_work is ' + + home + + '/test_work. Never use /home/guest unless that is this resolved home.\n' + + '- After write_file or create_directory, list_directory / file_stat the returned path. Do not claim files exist from the tool name alone.\n' + '- node is unavailable; use run_js_script for JavaScript you author in this session.\n' ) } diff --git a/packages/bare-os-coreutils/lib/agent/agent-helpers.js b/packages/bare-os-coreutils/lib/agent/agent-helpers.js index 061ff3e3..43a66bc0 100644 --- a/packages/bare-os-coreutils/lib/agent/agent-helpers.js +++ b/packages/bare-os-coreutils/lib/agent/agent-helpers.js @@ -49,6 +49,59 @@ function bareAgentNormalizeAbsPath(absPath) { return p } +/** + * Expand ~ / $HOME / relative guest paths to an absolute VFS path. + * @param {string} raw + * @param {string} home + * @param {string} [cwd] + */ +function bareAgentExpandGuestPath(raw, home, cwd) { + let p = String(raw == null ? '' : raw).trim() + if (!p) return '' + p = p.replace(/\\/g, '/') + const h = String(home || '').replace(/\/+$/, '') || '/home/guest' + let c = String(cwd || h).replace(/\/+$/, '') || h + if (c === '~') c = h + else if (c.startsWith('~/')) c = h + c.slice(1) + else if (c.startsWith('$HOME/')) c = h + c.slice(5) + else if (c === '$HOME') c = h + if (p === '~' || p === '$HOME') return h + if (p.startsWith('~/')) { + p = h + '/' + p.slice(2) + } else if (p.startsWith('$HOME/')) { + p = h + '/' + p.slice(6) + } else if (!p.startsWith('/')) { + p = (c === '/' ? '/' + p : c + '/' + p) + } + p = p.replace(/\/{2,}/g, '/') + if (p.length > 1) p = p.replace(/\/+$/, '') + return p +} + +/** + * Pick a path-like argument from a tool-call blob (path / file_path / dir / …). + * @param {Record} args + */ +function bareAgentPickPathArg(args) { + if (!args || typeof args !== 'object') return '' + const keys = [ + 'path', + 'file_path', + 'file', + 'filename', + 'dir', + 'directory', + 'folder', + 'from_path', + 'to_path' + ] + for (let i = 0; i < keys.length; i++) { + const v = args[keys[i]] + if (typeof v === 'string' && v.trim()) return v.trim() + } + return '' +} + /** * @param {string} absPath * @param {unknown} prefixes diff --git a/packages/bare-os-coreutils/lib/agent/agent-tool-definitions.js b/packages/bare-os-coreutils/lib/agent/agent-tool-definitions.js index 7eb2ab82..7576adce 100644 --- a/packages/bare-os-coreutils/lib/agent/agent-tool-definitions.js +++ b/packages/bare-os-coreutils/lib/agent/agent-tool-definitions.js @@ -10,7 +10,7 @@ function bareAgentToolDefinitions() { function: { name: 'read_file', description: - 'Read a UTF-8 text file from the VFS. Path must be absolute (e.g. /home/guest/...). Optional offset/limit return numbered line slices (Grok-style).', + 'Read a UTF-8 text file from the VFS. Path may be absolute or ~/... (tilde is expanded). Optional offset/limit return numbered line slices (Grok-style).', parameters: { type: 'object', properties: { @@ -42,11 +42,12 @@ function bareAgentToolDefinitions() { function: { name: 'write_file', description: - 'Create a new file or overwrite an existing one at any writable guest path. Parent directories are created as needed. Do not ask permission.', + 'Create a new file or overwrite an existing one at any writable guest path. Path may be absolute or ~/... (tilde is expanded to the session home). Parent directories are created as needed. Returns the expanded path — verify with list_directory or file_stat before claiming the file exists. Do not ask permission.', parameters: { type: 'object', properties: { - path: { type: 'string' }, + path: { type: 'string', description: 'Absolute path or ~/file' }, + file_path: { type: 'string', description: 'Alias for path' }, content: { type: 'string', description: 'Full file contents' } }, required: ['path', 'content'] @@ -86,11 +87,13 @@ function bareAgentToolDefinitions() { type: 'function', function: { name: 'create_directory', - description: 'Create a directory (recursive).', + description: + 'Create a directory (recursive). Path may be absolute or ~/... (tilde is expanded). Returns the expanded path — list_directory it before claiming it exists.', parameters: { type: 'object', properties: { - path: { type: 'string' } + path: { type: 'string', description: 'Absolute path or ~/dir' }, + dir: { type: 'string', description: 'Alias for path' } }, required: ['path'] } diff --git a/packages/bare-os-coreutils/lib/agent/agent-tool-dispatch.js b/packages/bare-os-coreutils/lib/agent/agent-tool-dispatch.js index 8e74fb8c..fb02fa0d 100644 --- a/packages/bare-os-coreutils/lib/agent/agent-tool-dispatch.js +++ b/packages/bare-os-coreutils/lib/agent/agent-tool-dispatch.js @@ -37,6 +37,54 @@ async function bareAgentDispatchTool(o) { return bareAgentJsonResult({ ok: false, error: 'invalid_tool_arguments_json' }) } const cfgNow = configRef.current || {} + const resolvedHome = + String(home || '').trim() || + (typeof bareAgentResolveHome === 'function' ? bareAgentResolveHome(ctx) : '') || + '/home/guest' + if (typeof bareAgentExpandGuestPath === 'function') { + const PATH_KEYS = [ + 'path', + 'file_path', + 'file', + 'filename', + 'dir', + 'directory', + 'folder', + 'root', + 'cwd', + 'from_path', + 'to_path', + 'from', + 'to', + 'src', + 'source', + 'dest', + 'destination', + 'target', + 'old_path', + 'new_path' + ] + let cwdForExpand = resolvedHome + if (paths && paths.lastCwd && typeof bareAgentReadTextFile === 'function') { + try { + const t = String(await bareAgentReadTextFile(ctx, paths.lastCwd) || '').trim() + if (t) cwdForExpand = t + } catch { + /* optional */ + } + } + if (typeof args.cwd === 'string' && args.cwd.trim()) { + args.cwd = bareAgentExpandGuestPath(args.cwd, resolvedHome, resolvedHome) + cwdForExpand = String(args.cwd || cwdForExpand) + } + for (let i = 0; i < PATH_KEYS.length; i++) { + const k = PATH_KEYS[i] + if (k === 'cwd') continue + if (typeof args[k] === 'string' && args[k].trim()) { + args[k] = bareAgentExpandGuestPath(args[k], resolvedHome, cwdForExpand) + } + } + } if ( !internalGate && cfgNow.autonomous_active && @@ -790,13 +838,37 @@ async function bareAgentDispatchTool(o) { } if (toolName === 'write_file') { - const path = typeof args.path === 'string' ? args.path : '' + const path = + (typeof args.path === 'string' && args.path.trim()) || + (typeof args.file_path === 'string' && args.file_path.trim()) || + (typeof args.file === 'string' && args.file.trim()) || + (typeof bareAgentPickPathArg === 'function' ? bareAgentPickPathArg(args) : '') || + '' if (!enforceAutonomousPath(path, 'mutate')) { - return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' }) + return bareAgentJsonResult({ ok: false, error: 'path_not_allowed', path }) } - const content = typeof args.content === 'string' ? args.content : '' + const content = + typeof args.content === 'string' + ? args.content + : typeof args.contents === 'string' + ? args.contents + : typeof args.text === 'string' + ? args.text + : typeof args.body === 'string' + ? args.body + : '' if (!path.startsWith('/') || path.includes('..')) { - return bareAgentJsonResult({ ok: false, error: 'bad_path' }) + return bareAgentJsonResult({ + ok: false, + error: 'bad_path', + path, + hint: + 'Use an absolute path or ~/... (tilde expands to ' + + resolvedHome + + '). Example: ' + + resolvedHome + + '/test_work/hello.js' + }) } if (!vfs || typeof vfs.writeFile !== 'function' || typeof vfs.mkdir !== 'function') { return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' }) @@ -817,7 +889,26 @@ async function bareAgentDispatchTool(o) { ? ctx.b4a.from(content) : new TextEncoder().encode(content) await vfs.writeFile(path, body) - return bareAgentJsonResult({ ok: true, bytes: body.length }) + let verified = false + try { + if (typeof vfs.lstat === 'function') verified = Boolean(await vfs.lstat(path)) + else if (typeof vfs.stat === 'function') verified = Boolean(await vfs.stat(path)) + else if (typeof vfs.readFile === 'function') { + const got = await vfs.readFile(path) + verified = got != null + } else verified = true + } catch { + verified = false + } + if (!verified) { + return bareAgentJsonResult({ + ok: false, + error: 'write_not_visible', + path, + hint: 'write_file returned but the path is not visible in the VFS. Retry with the expanded absolute path.' + }) + } + return bareAgentJsonResult({ ok: true, path, bytes: body.length, verified: true }) } if (toolName === 'edit_file' || toolName === 'search_replace') { @@ -914,19 +1005,54 @@ async function bareAgentDispatchTool(o) { } if (toolName === 'create_directory') { - const path = typeof args.path === 'string' ? args.path : '' + const path = + (typeof args.path === 'string' && args.path.trim()) || + (typeof args.dir === 'string' && args.dir.trim()) || + (typeof args.directory === 'string' && args.directory.trim()) || + (typeof args.folder === 'string' && args.folder.trim()) || + (typeof bareAgentPickPathArg === 'function' ? bareAgentPickPathArg(args) : '') || + '' if (!enforceAutonomousPath(path, 'mutate')) { - return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' }) + return bareAgentJsonResult({ ok: false, error: 'path_not_allowed', path }) } - if (!path.startsWith('/')) { - return bareAgentJsonResult({ ok: false, error: 'bad_path' }) + if (!path.startsWith('/') || path.includes('..')) { + return bareAgentJsonResult({ + ok: false, + error: 'bad_path', + path, + hint: + 'Use an absolute path or ~/... (tilde expands to ' + + resolvedHome + + '). Example: ' + + resolvedHome + + '/test_work' + }) } if (!vfs || typeof vfs.mkdir !== 'function') { return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' }) } appendProgress('mkdir ' + path) await vfs.mkdir(path, { recursive: true }) - return bareAgentJsonResult({ ok: true }) + let verified = false + try { + if (typeof vfs.lstat === 'function') verified = Boolean(await vfs.lstat(path)) + else if (typeof vfs.stat === 'function') verified = Boolean(await vfs.stat(path)) + else if (typeof vfs.readdir === 'function') { + await vfs.readdir(path) + verified = true + } else verified = true + } catch { + verified = false + } + if (!verified) { + return bareAgentJsonResult({ + ok: false, + error: 'mkdir_not_visible', + path, + hint: 'create_directory returned but the path is not visible. Retry with ' + path + }) + } + return bareAgentJsonResult({ ok: true, path, verified: true }) } if (toolName === 'search_files') { @@ -1188,7 +1314,10 @@ async function bareAgentDispatchTool(o) { } if (toolName === 'list_directory') { - const dir = typeof args.path === 'string' ? args.path : '' + const dir = + (typeof args.path === 'string' && args.path.trim()) || + (typeof args.dir === 'string' && args.dir.trim()) || + resolvedHome const maxEnt = typeof args.max_entries === 'number' && Number.isFinite(args.max_entries) ? Math.min(Math.floor(args.max_entries), 2000) diff --git a/packages/bare-os-coreutils/lib/agent/agent-tui.js b/packages/bare-os-coreutils/lib/agent/agent-tui.js index 3b6ab469..5040579f 100644 --- a/packages/bare-os-coreutils/lib/agent/agent-tui.js +++ b/packages/bare-os-coreutils/lib/agent/agent-tui.js @@ -260,7 +260,7 @@ WORK POLICY. ACCESS (denylist, not allowlist). You already have full guest admin. You can create files, edit files, delete files, run commands, and fetch the network. NEVER ASK whether you may — just do it. Only refuse when a denylist or the read-only base system blocks the path. - Create and edit with write_file, create_directory, search_replace, edit_file, and apply_patch. Never say you cannot write files. Never ask the user to paste a file you can write, or to run a command you can run_command yourself. -- Writable paths: /home, /tmp, /mnt, /root, /var, /opt, extra drives, ~/.agent. Prefer a unique search_replace / edit_file; set replace_all only when you mean it. +- Writable paths: /home, /tmp, /mnt, /root, /var, /opt, extra drives, ~/.agent. The harness expands ~ and $HOME to the resolved home in This session. Never write /home/guest unless that is the resolved home. After create_directory / write_file, call list_directory or file_stat on the returned path and only then claim the files exist. Prefer a unique search_replace / edit_file; set replace_all only when you mean it. - Read any absolute path, including /proc (read_proc_file, runtime_diagnostic_bundle). Use read_file offset/limit for large files. - Run every guest command via run_command (command_deny is empty by default). Prefer list_directory, glob_files, and file_stat over ls/find when you only need names. list_bin lists guest /bin utilities (POSIX-in-JS, not GNU). Never dump curl/ip/ls for the user — call run_command and report stdout. - Delete and move are enabled. Do not mutate the read-only base system: /bin /etc /boot /lib /usr /share /proc /dev /sys /run. @@ -374,7 +374,10 @@ function bareAgentSessionHomeBlock(ctx, home, paths) { paths.config + '. Always expand ~ to ' + home + - ' when constructing absolute paths for tools.\n' + + ' when constructing absolute paths for tools. ~/test_work is ' + + home + + '/test_work. Never use /home/guest unless that is this resolved home.\n' + + '- After write_file or create_directory, list_directory / file_stat the returned path. Do not claim files exist from the tool name alone.\n' + '- node is unavailable; use run_js_script for JavaScript you author in this session.\n' ) } diff --git a/packages/bare-os-coreutils/test/agent-config-surface.test.mjs b/packages/bare-os-coreutils/test/agent-config-surface.test.mjs index eef931c2..5c8d1ed5 100644 --- a/packages/bare-os-coreutils/test/agent-config-surface.test.mjs +++ b/packages/bare-os-coreutils/test/agent-config-surface.test.mjs @@ -223,6 +223,8 @@ test('agent-tui embeds operating contract appendix', async (t) => { t.ok(TUI.includes('YOU CAN EXECUTE')) t.ok(TUI.includes('There is no bash, shell, cli, or terminal tool')) t.ok(TUI.includes('never wrap them in run_js_script')) + t.ok(TUI.includes('Never write /home/guest unless that is the resolved home')) + t.ok(TUI.includes('list_directory or file_stat on the returned path')) t.ok(TUI.includes('Do not call task_complete after a failed tool')) t.ok(TUI.includes('The user never runs your tools')) t.ok(TUI.includes('discord_send_message')) diff --git a/packages/bare-os-coreutils/test/agent-grok-port.test.mjs b/packages/bare-os-coreutils/test/agent-grok-port.test.mjs index 6e9d9f22..f8de524c 100644 --- a/packages/bare-os-coreutils/test/agent-grok-port.test.mjs +++ b/packages/bare-os-coreutils/test/agent-grok-port.test.mjs @@ -965,3 +965,53 @@ test('discord_send_message queues or uses the live DM hook', async (t) => { t.ok(st.ok) t.is(st.userId, '111') }) + +test('write_file and create_directory expand ~/ onto the session home', async (t) => { + const s = loadDispatch() + const home = '/home/e16d7bc4ce14' + const { vfs, files, dirs, b4a } = makeVfs({}) + const ctx = { vfs, b4a } + const paths = { + dir: home + '/.agent', + config: home + '/.agent/config.json', + cmdOut: home + '/.agent/last_command_out.txt', + edits: home + '/.agent/edits.json' + } + const configRef = { current: {} } + + const mkdir = await dispatch(s, { + ctx, + paths, + toolName: 'create_directory', + args: { path: '~/test_work' }, + configRef, + home + }) + t.ok(mkdir.ok) + t.is(mkdir.path, home + '/test_work') + t.ok(dirs.has(home + '/test_work')) + + const wrote = await dispatch(s, { + ctx, + paths, + toolName: 'write_file', + args: { path: '~/test_work/hello.js', content: 'console.log("hi")\n' }, + configRef, + home + }) + t.ok(wrote.ok) + t.is(wrote.path, home + '/test_work/hello.js') + t.is(files.get(home + '/test_work/hello.js'), 'console.log("hi")\n') + + const alias = await dispatch(s, { + ctx, + paths, + toolName: 'write_file', + args: { file_path: '~/test_work/welcome.js', contents: 'ok\n' }, + configRef, + home + }) + t.ok(alias.ok) + t.is(alias.path, home + '/test_work/welcome.js') + t.is(files.get(home + '/test_work/welcome.js'), 'ok\n') +}) diff --git a/packages/bare-os-coreutils/test/agent-helpers.test.mjs b/packages/bare-os-coreutils/test/agent-helpers.test.mjs index bd45289a..3c0b6e93 100644 --- a/packages/bare-os-coreutils/test/agent-helpers.test.mjs +++ b/packages/bare-os-coreutils/test/agent-helpers.test.mjs @@ -29,6 +29,20 @@ test('mutate paths use a denylist (base system read-only, rest writable)', async t.absent(s.bareAgentPathAllowedMutate('/')) }) +test('expandGuestPath maps tilde and $HOME onto the session home', async (t) => { + const s = load() + const exp = s.bareAgentExpandGuestPath + const home = '/home/e16d7bc4ce14' + t.is(exp('~/test_work', home), home + '/test_work') + t.is(exp('~/test_work/hello.js', home), home + '/test_work/hello.js') + t.is(exp('~', home), home) + t.is(exp('$HOME/test_work', home), home + '/test_work') + t.is(exp('/tmp/x', home), '/tmp/x') + t.is(exp('rel/file.js', home, home + '/.agent/workspace'), home + '/.agent/workspace/rel/file.js') + t.is(s.bareAgentPickPathArg({ file_path: '~/a.js' }), '~/a.js') + t.is(s.bareAgentPickPathArg({ dir: '/tmp/d' }), '/tmp/d') +}) + test('js script wrapping curl/fetch diverts to a shell command', async (t) => { const s = load() const fromJs = s.bareAgentShellCommandFromJsScript diff --git a/packages/bare-os-seeder/kernel/bin/agent b/packages/bare-os-seeder/kernel/bin/agent index 00de640c..2483e7d7 100644 --- a/packages/bare-os-seeder/kernel/bin/agent +++ b/packages/bare-os-seeder/kernel/bin/agent @@ -1193,6 +1193,59 @@ function bareAgentNormalizeAbsPath(absPath) { return p } +/** + * Expand ~ / $HOME / relative guest paths to an absolute VFS path. + * @param {string} raw + * @param {string} home + * @param {string} [cwd] + */ +function bareAgentExpandGuestPath(raw, home, cwd) { + let p = String(raw == null ? '' : raw).trim() + if (!p) return '' + p = p.replace(/\\/g, '/') + const h = String(home || '').replace(/\/+$/, '') || '/home/guest' + let c = String(cwd || h).replace(/\/+$/, '') || h + if (c === '~') c = h + else if (c.startsWith('~/')) c = h + c.slice(1) + else if (c.startsWith('$HOME/')) c = h + c.slice(5) + else if (c === '$HOME') c = h + if (p === '~' || p === '$HOME') return h + if (p.startsWith('~/')) { + p = h + '/' + p.slice(2) + } else if (p.startsWith('$HOME/')) { + p = h + '/' + p.slice(6) + } else if (!p.startsWith('/')) { + p = (c === '/' ? '/' + p : c + '/' + p) + } + p = p.replace(/\/{2,}/g, '/') + if (p.length > 1) p = p.replace(/\/+$/, '') + return p +} + +/** + * Pick a path-like argument from a tool-call blob (path / file_path / dir / …). + * @param {Record} args + */ +function bareAgentPickPathArg(args) { + if (!args || typeof args !== 'object') return '' + const keys = [ + 'path', + 'file_path', + 'file', + 'filename', + 'dir', + 'directory', + 'folder', + 'from_path', + 'to_path' + ] + for (let i = 0; i < keys.length; i++) { + const v = args[keys[i]] + if (typeof v === 'string' && v.trim()) return v.trim() + } + return '' +} + /** * @param {string} absPath * @param {unknown} prefixes @@ -6927,7 +6980,7 @@ function bareAgentToolDefinitions() { function: { name: 'read_file', description: - 'Read a UTF-8 text file from the VFS. Path must be absolute (e.g. /home/guest/...). Optional offset/limit return numbered line slices (Grok-style).', + 'Read a UTF-8 text file from the VFS. Path may be absolute or ~/... (tilde is expanded). Optional offset/limit return numbered line slices (Grok-style).', parameters: { type: 'object', properties: { @@ -6959,11 +7012,12 @@ function bareAgentToolDefinitions() { function: { name: 'write_file', description: - 'Create a new file or overwrite an existing one at any writable guest path. Parent directories are created as needed. Do not ask permission.', + 'Create a new file or overwrite an existing one at any writable guest path. Path may be absolute or ~/... (tilde is expanded to the session home). Parent directories are created as needed. Returns the expanded path — verify with list_directory or file_stat before claiming the file exists. Do not ask permission.', parameters: { type: 'object', properties: { - path: { type: 'string' }, + path: { type: 'string', description: 'Absolute path or ~/file' }, + file_path: { type: 'string', description: 'Alias for path' }, content: { type: 'string', description: 'Full file contents' } }, required: ['path', 'content'] @@ -7003,11 +7057,13 @@ function bareAgentToolDefinitions() { type: 'function', function: { name: 'create_directory', - description: 'Create a directory (recursive).', + description: + 'Create a directory (recursive). Path may be absolute or ~/... (tilde is expanded). Returns the expanded path — list_directory it before claiming it exists.', parameters: { type: 'object', properties: { - path: { type: 'string' } + path: { type: 'string', description: 'Absolute path or ~/dir' }, + dir: { type: 'string', description: 'Alias for path' } }, required: ['path'] } @@ -8582,6 +8638,54 @@ async function bareAgentDispatchTool(o) { return bareAgentJsonResult({ ok: false, error: 'invalid_tool_arguments_json' }) } const cfgNow = configRef.current || {} + const resolvedHome = + String(home || '').trim() || + (typeof bareAgentResolveHome === 'function' ? bareAgentResolveHome(ctx) : '') || + '/home/guest' + if (typeof bareAgentExpandGuestPath === 'function') { + const PATH_KEYS = [ + 'path', + 'file_path', + 'file', + 'filename', + 'dir', + 'directory', + 'folder', + 'root', + 'cwd', + 'from_path', + 'to_path', + 'from', + 'to', + 'src', + 'source', + 'dest', + 'destination', + 'target', + 'old_path', + 'new_path' + ] + let cwdForExpand = resolvedHome + if (paths && paths.lastCwd && typeof bareAgentReadTextFile === 'function') { + try { + const t = String(await bareAgentReadTextFile(ctx, paths.lastCwd) || '').trim() + if (t) cwdForExpand = t + } catch { + /* optional */ + } + } + if (typeof args.cwd === 'string' && args.cwd.trim()) { + args.cwd = bareAgentExpandGuestPath(args.cwd, resolvedHome, resolvedHome) + cwdForExpand = String(args.cwd || cwdForExpand) + } + for (let i = 0; i < PATH_KEYS.length; i++) { + const k = PATH_KEYS[i] + if (k === 'cwd') continue + if (typeof args[k] === 'string' && args[k].trim()) { + args[k] = bareAgentExpandGuestPath(args[k], resolvedHome, cwdForExpand) + } + } + } if ( !internalGate && cfgNow.autonomous_active && @@ -9335,13 +9439,37 @@ async function bareAgentDispatchTool(o) { } if (toolName === 'write_file') { - const path = typeof args.path === 'string' ? args.path : '' + const path = + (typeof args.path === 'string' && args.path.trim()) || + (typeof args.file_path === 'string' && args.file_path.trim()) || + (typeof args.file === 'string' && args.file.trim()) || + (typeof bareAgentPickPathArg === 'function' ? bareAgentPickPathArg(args) : '') || + '' if (!enforceAutonomousPath(path, 'mutate')) { - return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' }) + return bareAgentJsonResult({ ok: false, error: 'path_not_allowed', path }) } - const content = typeof args.content === 'string' ? args.content : '' + const content = + typeof args.content === 'string' + ? args.content + : typeof args.contents === 'string' + ? args.contents + : typeof args.text === 'string' + ? args.text + : typeof args.body === 'string' + ? args.body + : '' if (!path.startsWith('/') || path.includes('..')) { - return bareAgentJsonResult({ ok: false, error: 'bad_path' }) + return bareAgentJsonResult({ + ok: false, + error: 'bad_path', + path, + hint: + 'Use an absolute path or ~/... (tilde expands to ' + + resolvedHome + + '). Example: ' + + resolvedHome + + '/test_work/hello.js' + }) } if (!vfs || typeof vfs.writeFile !== 'function' || typeof vfs.mkdir !== 'function') { return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' }) @@ -9362,7 +9490,26 @@ async function bareAgentDispatchTool(o) { ? ctx.b4a.from(content) : new TextEncoder().encode(content) await vfs.writeFile(path, body) - return bareAgentJsonResult({ ok: true, bytes: body.length }) + let verified = false + try { + if (typeof vfs.lstat === 'function') verified = Boolean(await vfs.lstat(path)) + else if (typeof vfs.stat === 'function') verified = Boolean(await vfs.stat(path)) + else if (typeof vfs.readFile === 'function') { + const got = await vfs.readFile(path) + verified = got != null + } else verified = true + } catch { + verified = false + } + if (!verified) { + return bareAgentJsonResult({ + ok: false, + error: 'write_not_visible', + path, + hint: 'write_file returned but the path is not visible in the VFS. Retry with the expanded absolute path.' + }) + } + return bareAgentJsonResult({ ok: true, path, bytes: body.length, verified: true }) } if (toolName === 'edit_file' || toolName === 'search_replace') { @@ -9459,19 +9606,54 @@ async function bareAgentDispatchTool(o) { } if (toolName === 'create_directory') { - const path = typeof args.path === 'string' ? args.path : '' + const path = + (typeof args.path === 'string' && args.path.trim()) || + (typeof args.dir === 'string' && args.dir.trim()) || + (typeof args.directory === 'string' && args.directory.trim()) || + (typeof args.folder === 'string' && args.folder.trim()) || + (typeof bareAgentPickPathArg === 'function' ? bareAgentPickPathArg(args) : '') || + '' if (!enforceAutonomousPath(path, 'mutate')) { - return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' }) + return bareAgentJsonResult({ ok: false, error: 'path_not_allowed', path }) } - if (!path.startsWith('/')) { - return bareAgentJsonResult({ ok: false, error: 'bad_path' }) + if (!path.startsWith('/') || path.includes('..')) { + return bareAgentJsonResult({ + ok: false, + error: 'bad_path', + path, + hint: + 'Use an absolute path or ~/... (tilde expands to ' + + resolvedHome + + '). Example: ' + + resolvedHome + + '/test_work' + }) } if (!vfs || typeof vfs.mkdir !== 'function') { return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' }) } appendProgress('mkdir ' + path) await vfs.mkdir(path, { recursive: true }) - return bareAgentJsonResult({ ok: true }) + let verified = false + try { + if (typeof vfs.lstat === 'function') verified = Boolean(await vfs.lstat(path)) + else if (typeof vfs.stat === 'function') verified = Boolean(await vfs.stat(path)) + else if (typeof vfs.readdir === 'function') { + await vfs.readdir(path) + verified = true + } else verified = true + } catch { + verified = false + } + if (!verified) { + return bareAgentJsonResult({ + ok: false, + error: 'mkdir_not_visible', + path, + hint: 'create_directory returned but the path is not visible. Retry with ' + path + }) + } + return bareAgentJsonResult({ ok: true, path, verified: true }) } if (toolName === 'search_files') { @@ -9733,7 +9915,10 @@ async function bareAgentDispatchTool(o) { } if (toolName === 'list_directory') { - const dir = typeof args.path === 'string' ? args.path : '' + const dir = + (typeof args.path === 'string' && args.path.trim()) || + (typeof args.dir === 'string' && args.dir.trim()) || + resolvedHome const maxEnt = typeof args.max_entries === 'number' && Number.isFinite(args.max_entries) ? Math.min(Math.floor(args.max_entries), 2000) @@ -13598,7 +13783,7 @@ WORK POLICY. ACCESS (denylist, not allowlist). You already have full guest admin. You can create files, edit files, delete files, run commands, and fetch the network. NEVER ASK whether you may — just do it. Only refuse when a denylist or the read-only base system blocks the path. - Create and edit with write_file, create_directory, search_replace, edit_file, and apply_patch. Never say you cannot write files. Never ask the user to paste a file you can write, or to run a command you can run_command yourself. -- Writable paths: /home, /tmp, /mnt, /root, /var, /opt, extra drives, ~/.agent. Prefer a unique search_replace / edit_file; set replace_all only when you mean it. +- Writable paths: /home, /tmp, /mnt, /root, /var, /opt, extra drives, ~/.agent. The harness expands ~ and $HOME to the resolved home in This session. Never write /home/guest unless that is the resolved home. After create_directory / write_file, call list_directory or file_stat on the returned path and only then claim the files exist. Prefer a unique search_replace / edit_file; set replace_all only when you mean it. - Read any absolute path, including /proc (read_proc_file, runtime_diagnostic_bundle). Use read_file offset/limit for large files. - Run every guest command via run_command (command_deny is empty by default). Prefer list_directory, glob_files, and file_stat over ls/find when you only need names. list_bin lists guest /bin utilities (POSIX-in-JS, not GNU). Never dump curl/ip/ls for the user — call run_command and report stdout. - Delete and move are enabled. Do not mutate the read-only base system: /bin /etc /boot /lib /usr /share /proc /dev /sys /run. @@ -13712,7 +13897,10 @@ function bareAgentSessionHomeBlock(ctx, home, paths) { paths.config + '. Always expand ~ to ' + home + - ' when constructing absolute paths for tools.\n' + + ' when constructing absolute paths for tools. ~/test_work is ' + + home + + '/test_work. Never use /home/guest unless that is this resolved home.\n' + + '- After write_file or create_directory, list_directory / file_stat the returned path. Do not claim files exist from the tool name alone.\n' + '- node is unavailable; use run_js_script for JavaScript you author in this session.\n' ) }