Further Agent Harness Fixes (write_file) and (create_directory) tools updates
Release rolling / release (push) Successful in 12m52s

This commit is contained in:
2026-08-19 14:27:00 -04:00
parent d047d26f0a
commit 0373bc4d25
9 changed files with 684 additions and 54 deletions
+206 -18
View File
@@ -1193,6 +1193,59 @@ function bareAgentNormalizeAbsPath(absPath) {
return p 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<string, unknown>} 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 {string} absPath
* @param {unknown} prefixes * @param {unknown} prefixes
@@ -6927,7 +6980,7 @@ function bareAgentToolDefinitions() {
function: { function: {
name: 'read_file', name: 'read_file',
description: 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: { parameters: {
type: 'object', type: 'object',
properties: { properties: {
@@ -6959,11 +7012,12 @@ function bareAgentToolDefinitions() {
function: { function: {
name: 'write_file', name: 'write_file',
description: 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: { parameters: {
type: 'object', type: 'object',
properties: { 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' } content: { type: 'string', description: 'Full file contents' }
}, },
required: ['path', 'content'] required: ['path', 'content']
@@ -7003,11 +7057,13 @@ function bareAgentToolDefinitions() {
type: 'function', type: 'function',
function: { function: {
name: 'create_directory', 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: { parameters: {
type: 'object', type: 'object',
properties: { properties: {
path: { type: 'string' } path: { type: 'string', description: 'Absolute path or ~/dir' },
dir: { type: 'string', description: 'Alias for path' }
}, },
required: ['path'] required: ['path']
} }
@@ -8582,6 +8638,54 @@ async function bareAgentDispatchTool(o) {
return bareAgentJsonResult({ ok: false, error: 'invalid_tool_arguments_json' }) return bareAgentJsonResult({ ok: false, error: 'invalid_tool_arguments_json' })
} }
const cfgNow = configRef.current || {} 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 ( if (
!internalGate && !internalGate &&
cfgNow.autonomous_active && cfgNow.autonomous_active &&
@@ -9335,13 +9439,37 @@ async function bareAgentDispatchTool(o) {
} }
if (toolName === 'write_file') { 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')) { 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('..')) { 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') { if (!vfs || typeof vfs.writeFile !== 'function' || typeof vfs.mkdir !== 'function') {
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' }) return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
@@ -9362,7 +9490,26 @@ async function bareAgentDispatchTool(o) {
? ctx.b4a.from(content) ? ctx.b4a.from(content)
: new TextEncoder().encode(content) : new TextEncoder().encode(content)
await vfs.writeFile(path, body) 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') { if (toolName === 'edit_file' || toolName === 'search_replace') {
@@ -9459,19 +9606,54 @@ async function bareAgentDispatchTool(o) {
} }
if (toolName === 'create_directory') { 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')) { if (!enforceAutonomousPath(path, 'mutate')) {
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' }) return bareAgentJsonResult({ ok: false, error: 'path_not_allowed', path })
} }
if (!path.startsWith('/')) { 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'
})
} }
if (!vfs || typeof vfs.mkdir !== 'function') { if (!vfs || typeof vfs.mkdir !== 'function') {
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' }) return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
} }
appendProgress('mkdir ' + path) appendProgress('mkdir ' + path)
await vfs.mkdir(path, { recursive: true }) 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') { if (toolName === 'search_files') {
@@ -9733,7 +9915,10 @@ async function bareAgentDispatchTool(o) {
} }
if (toolName === 'list_directory') { 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 = const maxEnt =
typeof args.max_entries === 'number' && Number.isFinite(args.max_entries) typeof args.max_entries === 'number' && Number.isFinite(args.max_entries)
? Math.min(Math.floor(args.max_entries), 2000) ? 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. 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. - 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. - 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. - 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. - 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 + paths.config +
'. Always expand ~ to ' + '. Always expand ~ to ' +
home + 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' '- node is unavailable; use run_js_script for JavaScript you author in this session.\n'
) )
} }
@@ -49,6 +49,59 @@ function bareAgentNormalizeAbsPath(absPath) {
return p 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<string, unknown>} 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 {string} absPath
* @param {unknown} prefixes * @param {unknown} prefixes
@@ -10,7 +10,7 @@ function bareAgentToolDefinitions() {
function: { function: {
name: 'read_file', name: 'read_file',
description: 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: { parameters: {
type: 'object', type: 'object',
properties: { properties: {
@@ -42,11 +42,12 @@ function bareAgentToolDefinitions() {
function: { function: {
name: 'write_file', name: 'write_file',
description: 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: { parameters: {
type: 'object', type: 'object',
properties: { 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' } content: { type: 'string', description: 'Full file contents' }
}, },
required: ['path', 'content'] required: ['path', 'content']
@@ -86,11 +87,13 @@ function bareAgentToolDefinitions() {
type: 'function', type: 'function',
function: { function: {
name: 'create_directory', 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: { parameters: {
type: 'object', type: 'object',
properties: { properties: {
path: { type: 'string' } path: { type: 'string', description: 'Absolute path or ~/dir' },
dir: { type: 'string', description: 'Alias for path' }
}, },
required: ['path'] required: ['path']
} }
@@ -37,6 +37,54 @@ async function bareAgentDispatchTool(o) {
return bareAgentJsonResult({ ok: false, error: 'invalid_tool_arguments_json' }) return bareAgentJsonResult({ ok: false, error: 'invalid_tool_arguments_json' })
} }
const cfgNow = configRef.current || {} 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 ( if (
!internalGate && !internalGate &&
cfgNow.autonomous_active && cfgNow.autonomous_active &&
@@ -790,13 +838,37 @@ async function bareAgentDispatchTool(o) {
} }
if (toolName === 'write_file') { 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')) { 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('..')) { 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') { if (!vfs || typeof vfs.writeFile !== 'function' || typeof vfs.mkdir !== 'function') {
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' }) return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
@@ -817,7 +889,26 @@ async function bareAgentDispatchTool(o) {
? ctx.b4a.from(content) ? ctx.b4a.from(content)
: new TextEncoder().encode(content) : new TextEncoder().encode(content)
await vfs.writeFile(path, body) 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') { if (toolName === 'edit_file' || toolName === 'search_replace') {
@@ -914,19 +1005,54 @@ async function bareAgentDispatchTool(o) {
} }
if (toolName === 'create_directory') { 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')) { if (!enforceAutonomousPath(path, 'mutate')) {
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' }) return bareAgentJsonResult({ ok: false, error: 'path_not_allowed', path })
} }
if (!path.startsWith('/')) { 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'
})
} }
if (!vfs || typeof vfs.mkdir !== 'function') { if (!vfs || typeof vfs.mkdir !== 'function') {
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' }) return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
} }
appendProgress('mkdir ' + path) appendProgress('mkdir ' + path)
await vfs.mkdir(path, { recursive: true }) 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') { if (toolName === 'search_files') {
@@ -1188,7 +1314,10 @@ async function bareAgentDispatchTool(o) {
} }
if (toolName === 'list_directory') { 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 = const maxEnt =
typeof args.max_entries === 'number' && Number.isFinite(args.max_entries) typeof args.max_entries === 'number' && Number.isFinite(args.max_entries)
? Math.min(Math.floor(args.max_entries), 2000) ? Math.min(Math.floor(args.max_entries), 2000)
@@ -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. 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. - 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. - 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. - 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. - 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 + paths.config +
'. Always expand ~ to ' + '. Always expand ~ to ' +
home + 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' '- node is unavailable; use run_js_script for JavaScript you author in this session.\n'
) )
} }
@@ -223,6 +223,8 @@ test('agent-tui embeds operating contract appendix', async (t) => {
t.ok(TUI.includes('YOU CAN EXECUTE')) t.ok(TUI.includes('YOU CAN EXECUTE'))
t.ok(TUI.includes('There is no bash, shell, cli, or terminal tool')) 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 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('Do not call task_complete after a failed tool'))
t.ok(TUI.includes('The user never runs your tools')) t.ok(TUI.includes('The user never runs your tools'))
t.ok(TUI.includes('discord_send_message')) t.ok(TUI.includes('discord_send_message'))
@@ -965,3 +965,53 @@ test('discord_send_message queues or uses the live DM hook', async (t) => {
t.ok(st.ok) t.ok(st.ok)
t.is(st.userId, '111') 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')
})
@@ -29,6 +29,20 @@ test('mutate paths use a denylist (base system read-only, rest writable)', async
t.absent(s.bareAgentPathAllowedMutate('/')) 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) => { test('js script wrapping curl/fetch diverts to a shell command', async (t) => {
const s = load() const s = load()
const fromJs = s.bareAgentShellCommandFromJsScript const fromJs = s.bareAgentShellCommandFromJsScript
+206 -18
View File
@@ -1193,6 +1193,59 @@ function bareAgentNormalizeAbsPath(absPath) {
return p 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<string, unknown>} 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 {string} absPath
* @param {unknown} prefixes * @param {unknown} prefixes
@@ -6927,7 +6980,7 @@ function bareAgentToolDefinitions() {
function: { function: {
name: 'read_file', name: 'read_file',
description: 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: { parameters: {
type: 'object', type: 'object',
properties: { properties: {
@@ -6959,11 +7012,12 @@ function bareAgentToolDefinitions() {
function: { function: {
name: 'write_file', name: 'write_file',
description: 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: { parameters: {
type: 'object', type: 'object',
properties: { 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' } content: { type: 'string', description: 'Full file contents' }
}, },
required: ['path', 'content'] required: ['path', 'content']
@@ -7003,11 +7057,13 @@ function bareAgentToolDefinitions() {
type: 'function', type: 'function',
function: { function: {
name: 'create_directory', 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: { parameters: {
type: 'object', type: 'object',
properties: { properties: {
path: { type: 'string' } path: { type: 'string', description: 'Absolute path or ~/dir' },
dir: { type: 'string', description: 'Alias for path' }
}, },
required: ['path'] required: ['path']
} }
@@ -8582,6 +8638,54 @@ async function bareAgentDispatchTool(o) {
return bareAgentJsonResult({ ok: false, error: 'invalid_tool_arguments_json' }) return bareAgentJsonResult({ ok: false, error: 'invalid_tool_arguments_json' })
} }
const cfgNow = configRef.current || {} 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 ( if (
!internalGate && !internalGate &&
cfgNow.autonomous_active && cfgNow.autonomous_active &&
@@ -9335,13 +9439,37 @@ async function bareAgentDispatchTool(o) {
} }
if (toolName === 'write_file') { 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')) { 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('..')) { 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') { if (!vfs || typeof vfs.writeFile !== 'function' || typeof vfs.mkdir !== 'function') {
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' }) return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
@@ -9362,7 +9490,26 @@ async function bareAgentDispatchTool(o) {
? ctx.b4a.from(content) ? ctx.b4a.from(content)
: new TextEncoder().encode(content) : new TextEncoder().encode(content)
await vfs.writeFile(path, body) 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') { if (toolName === 'edit_file' || toolName === 'search_replace') {
@@ -9459,19 +9606,54 @@ async function bareAgentDispatchTool(o) {
} }
if (toolName === 'create_directory') { 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')) { if (!enforceAutonomousPath(path, 'mutate')) {
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' }) return bareAgentJsonResult({ ok: false, error: 'path_not_allowed', path })
} }
if (!path.startsWith('/')) { 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'
})
} }
if (!vfs || typeof vfs.mkdir !== 'function') { if (!vfs || typeof vfs.mkdir !== 'function') {
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' }) return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
} }
appendProgress('mkdir ' + path) appendProgress('mkdir ' + path)
await vfs.mkdir(path, { recursive: true }) 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') { if (toolName === 'search_files') {
@@ -9733,7 +9915,10 @@ async function bareAgentDispatchTool(o) {
} }
if (toolName === 'list_directory') { 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 = const maxEnt =
typeof args.max_entries === 'number' && Number.isFinite(args.max_entries) typeof args.max_entries === 'number' && Number.isFinite(args.max_entries)
? Math.min(Math.floor(args.max_entries), 2000) ? 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. 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. - 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. - 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. - 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. - 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 + paths.config +
'. Always expand ~ to ' + '. Always expand ~ to ' +
home + 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' '- node is unavailable; use run_js_script for JavaScript you author in this session.\n'
) )
} }