Agent Updates
This commit is contained in:
+801
-25
@@ -792,6 +792,238 @@ function bareAgentEnsureTextCodecPolyfill() {
|
||||
|
||||
bareAgentEnsureTextCodecPolyfill()
|
||||
|
||||
/** Shared helpers for /bin/agent tools (preamble for agent bundle). */
|
||||
|
||||
/**
|
||||
* Paths allowed for rename/delete via agent tools (stricter than general read paths).
|
||||
* @param {string} absPath
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function bareAgentPathAllowedMutate(absPath) {
|
||||
const p = String(absPath || '').replace(/\\/g, '/')
|
||||
if (!p.startsWith('/') || p.includes('..')) return false
|
||||
return (
|
||||
p.startsWith('/home/') ||
|
||||
p.startsWith('/tmp/') ||
|
||||
p === '/tmp' ||
|
||||
p.startsWith('/root/') ||
|
||||
p.startsWith('/mnt/')
|
||||
)
|
||||
}
|
||||
|
||||
/** @type {readonly string[]} */
|
||||
var BARE_AGENT_PROC_READ_ALLOWLIST = Object.freeze([
|
||||
'/proc/bare_os/metrics_live.json',
|
||||
'/proc/bare_os/features.json',
|
||||
'/proc/bare_os/swarm.json',
|
||||
'/proc/bare_os/capabilities.json'
|
||||
])
|
||||
|
||||
/**
|
||||
* @param {unknown} statObj
|
||||
* @param {string} path
|
||||
*/
|
||||
function bareAgentSerializeStat(statObj, path) {
|
||||
if (!statObj || typeof statObj !== 'object')
|
||||
return { path, error: 'no_stat' }
|
||||
const s = /** @type {Record<string, unknown>} */ (statObj)
|
||||
/** @type {'file' | 'dir' | 'symlink' | 'other'} */
|
||||
let kind = 'other'
|
||||
try {
|
||||
if (typeof s.isDirectory === 'function' && s.isDirectory()) kind = 'dir'
|
||||
else if (typeof s.isSymbolicLink === 'function' && s.isSymbolicLink()) kind = 'symlink'
|
||||
else if (typeof s.isFile === 'function' && s.isFile()) kind = 'file'
|
||||
else if (typeof s.mode === 'number') {
|
||||
const M = Number(s.mode)
|
||||
if ((M & 0o170000) === 0o040000) kind = 'dir'
|
||||
else if ((M & 0o170000) === 0o120000) kind = 'symlink'
|
||||
else if ((M & 0o170000) === 0o100000) kind = 'file'
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
/** @type {Record<string, unknown>} */
|
||||
const out = {
|
||||
path,
|
||||
kind,
|
||||
size: typeof s.size === 'number' ? s.size : undefined,
|
||||
mode: typeof s.mode === 'number' ? s.mode : undefined,
|
||||
mtimeMs: typeof s.mtimeMs === 'number' ? s.mtimeMs : undefined,
|
||||
uid: typeof s.uid === 'number' ? s.uid : undefined,
|
||||
gid: typeof s.gid === 'number' ? s.gid : undefined
|
||||
}
|
||||
if (typeof s.target === 'string') out.target = s.target
|
||||
return out
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
* @param {number} maxChars
|
||||
*/
|
||||
function bareAgentTruncateChars(text, maxChars) {
|
||||
const t = String(text || '')
|
||||
const n = Math.floor(maxChars)
|
||||
if (!Number.isFinite(n) || n <= 0) return ''
|
||||
if (t.length <= n) return t
|
||||
return t.slice(0, n) + '\n… truncated'
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} page
|
||||
* @param {number} maxChars
|
||||
*/
|
||||
function bareAgentManExtractPageSlice(page, maxChars) {
|
||||
if (!page || typeof page !== 'object')
|
||||
return { error: 'bad_page' }
|
||||
const m = Math.min(Math.max(Math.floor(maxChars) || 8000, 500), 64_000)
|
||||
const name = typeof page.name === 'string' ? page.name : ''
|
||||
const section = typeof page.section === 'number' ? page.section : 0
|
||||
const title = typeof page.title === 'string' ? page.title : ''
|
||||
const synopsis = Array.isArray(page.synopsis)
|
||||
? page.synopsis.map((x) => String(x)).join('\n')
|
||||
: ''
|
||||
const description = bareAgentTruncateChars(
|
||||
typeof page.description === 'string' ? page.description : '',
|
||||
Math.floor(m * 0.55)
|
||||
)
|
||||
let opts = ''
|
||||
if (Array.isArray(page.options)) {
|
||||
const lines = []
|
||||
for (const o of page.options) {
|
||||
if (!o || typeof o !== 'object') continue
|
||||
const fl = typeof o.flag === 'string' ? o.flag : ''
|
||||
const me = typeof o.meaning === 'string' ? o.meaning : ''
|
||||
if (fl || me) lines.push(fl + (fl && me ? ' — ' : '') + me)
|
||||
}
|
||||
opts = bareAgentTruncateChars(lines.join('\n'), Math.floor(m * 0.35))
|
||||
}
|
||||
const blob =
|
||||
name +
|
||||
'(' +
|
||||
section +
|
||||
') — ' +
|
||||
title +
|
||||
'\n\nSYNOPSIS\n' +
|
||||
synopsis +
|
||||
'\n\nDESCRIPTION\n' +
|
||||
description +
|
||||
(opts ? '\n\nOPTIONS\n' + opts : '')
|
||||
return {
|
||||
name,
|
||||
section,
|
||||
title,
|
||||
synopsis,
|
||||
description,
|
||||
options_text: opts || undefined,
|
||||
text: bareAgentTruncateChars(blob, m)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Same semantics as `man -k`: substring match on indexed keywords (merged DB).
|
||||
* @param {unknown} db
|
||||
* @param {string} needle
|
||||
* @param {number} maxHits
|
||||
*/
|
||||
function bareAgentManAproposHits(db, needle, maxHits) {
|
||||
const n = String(needle || '').toLowerCase()
|
||||
const max = Math.min(Math.max(Math.floor(maxHits) || 40, 1), 200)
|
||||
if (!n || !db || typeof db !== 'object')
|
||||
return /** @type {{ lines: string[], truncated: boolean }} */ ({
|
||||
lines: [],
|
||||
truncated: false
|
||||
})
|
||||
const d = /** @type {Record<string, unknown>} */ (db)
|
||||
const pages = Array.isArray(d.pages) ? d.pages : []
|
||||
const apropos = Array.isArray(d.apropos) ? d.apropos : []
|
||||
const seen = new Set()
|
||||
/** @type {string[]} */
|
||||
const lines = []
|
||||
let truncated = false
|
||||
for (const row of apropos) {
|
||||
if (!row || typeof row !== 'object') continue
|
||||
const kw = typeof row.kw === 'string' ? row.kw : ''
|
||||
if (!kw.includes(n)) continue
|
||||
const idx = row.pageRef
|
||||
if (typeof idx !== 'number' || !pages[idx]) continue
|
||||
if (seen.has(idx)) continue
|
||||
seen.add(idx)
|
||||
const p = /** @type {Record<string, unknown>} */ (pages[idx])
|
||||
const name = typeof p.name === 'string' ? p.name : ''
|
||||
const sec = typeof p.section === 'number' ? p.section : 0
|
||||
const title = typeof p.title === 'string' ? p.title : ''
|
||||
lines.push(name + '(' + sec + ') - ' + title)
|
||||
if (lines.length >= max) {
|
||||
truncated = true
|
||||
break
|
||||
}
|
||||
}
|
||||
lines.sort()
|
||||
return { lines, truncated }
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {unknown} vfs
|
||||
* @param {{ db: unknown | null }} cacheRef
|
||||
* @returns {Promise<unknown | null>}
|
||||
*/
|
||||
async function bareAgentManEnsureDbLoaded(ctx, vfs, cacheRef) {
|
||||
if (cacheRef.db) return cacheRef.db
|
||||
if (!vfs || typeof vfs.readFile !== 'function') return null
|
||||
try {
|
||||
const buf = await vfs.readFile('/share/man/man.json')
|
||||
if (!buf || !buf.length) return null
|
||||
const t =
|
||||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function'
|
||||
? ctx.b4a.toString(buf)
|
||||
: String(new TextDecoder().decode(buf))
|
||||
const parsed = JSON.parse(t)
|
||||
cacheRef.db = parsed
|
||||
return parsed
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve one manual page from merged DB (like `man [[section] name]`).
|
||||
* @param {unknown} db
|
||||
* @param {string} topic
|
||||
* @param {number | null} sectionExplicit
|
||||
*/
|
||||
function bareAgentManResolvePage(db, topic, sectionExplicit) {
|
||||
const name = String(topic || '').toLowerCase()
|
||||
if (!name || !db || typeof db !== 'object')
|
||||
return { error: 'not_found' }
|
||||
const d = /** @type {Record<string, unknown>} */ (db)
|
||||
const index = d.index
|
||||
if (!index || typeof index !== 'object') return { error: 'not_found' }
|
||||
const idx = /** @type {Record<string, unknown>} */ (index)[name]
|
||||
if (typeof idx !== 'number') return { error: 'not_found' }
|
||||
const pages = Array.isArray(d.pages) ? d.pages : []
|
||||
const page = pages[idx]
|
||||
if (!page || typeof page !== 'object') return { error: 'not_found' }
|
||||
const sec = typeof page.section === 'number' ? page.section : 0
|
||||
if (sectionExplicit !== null && sectionExplicit !== sec) {
|
||||
return { error: 'wrong_section', foundSection: sec }
|
||||
}
|
||||
return { page: /** @type {Record<string, unknown>} */ (page) }
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} absPath
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function bareAgentProcReadPathAllowed(absPath) {
|
||||
const p = String(absPath || '').replace(/\\/g, '/')
|
||||
if (!p.startsWith('/') || p.includes('..')) return false
|
||||
for (let i = 0; i < BARE_AGENT_PROC_READ_ALLOWLIST.length; i++) {
|
||||
if (p === BARE_AGENT_PROC_READ_ALLOWLIST[i]) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/** ~/.agent paths, config, history trim, man digest (preamble for /bin/agent). */
|
||||
|
||||
/**
|
||||
@@ -843,7 +1075,9 @@ function bareAgentDefaultConfig() {
|
||||
stream: true,
|
||||
tool_parallelism: 1,
|
||||
request_timeout_ms: 120000,
|
||||
extra_headers: /** @type {Record<string, string>} */ ({})
|
||||
extra_headers: /** @type {Record<string, string>} */ ({}),
|
||||
allow_delete: false,
|
||||
require_confirm_token: ''
|
||||
}
|
||||
}
|
||||
|
||||
@@ -873,7 +1107,9 @@ function bareAgentMergeConfig(defaults, src) {
|
||||
'stream',
|
||||
'tool_parallelism',
|
||||
'request_timeout_ms',
|
||||
'extra_headers'
|
||||
'extra_headers',
|
||||
'allow_delete',
|
||||
'require_confirm_token'
|
||||
])
|
||||
for (const k of Object.keys(src)) {
|
||||
if (k.startsWith('x-')) continue
|
||||
@@ -903,8 +1139,13 @@ function bareAgentMergeConfig(defaults, src) {
|
||||
out[k] = Number.isFinite(n) ? n : defaults[k]
|
||||
continue
|
||||
}
|
||||
if (k === 'stream') {
|
||||
out.stream = Boolean(val)
|
||||
if (k === 'stream' || k === 'allow_delete') {
|
||||
out[k] = Boolean(val)
|
||||
continue
|
||||
}
|
||||
if (k === 'require_confirm_token') {
|
||||
out.require_confirm_token = String(val ?? '')
|
||||
continue
|
||||
}
|
||||
}
|
||||
return out
|
||||
@@ -928,7 +1169,9 @@ function bareAgentValidateConfigShape(raw) {
|
||||
'stream',
|
||||
'tool_parallelism',
|
||||
'request_timeout_ms',
|
||||
'extra_headers'
|
||||
'extra_headers',
|
||||
'allow_delete',
|
||||
'require_confirm_token'
|
||||
]
|
||||
if (!known.includes(k)) {
|
||||
throw new Error('unknown config key: ' + k)
|
||||
@@ -1535,7 +1778,7 @@ function bareAgentToolDefinitions() {
|
||||
function: {
|
||||
name: 'run_command',
|
||||
description:
|
||||
'Run a shell command line via ctx.execLine (same as interactive shell). Output may be captured to a temp file when possible.',
|
||||
'Run a shell command line via ctx.execLine (same as interactive shell). Output is captured to a temp file. Set capture_exit true to append a final EXIT:<code> line.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -1543,7 +1786,11 @@ function bareAgentToolDefinitions() {
|
||||
type: 'string',
|
||||
description: 'Full command string (e.g. ls -la /bin)'
|
||||
},
|
||||
timeout_ms: { type: 'integer' }
|
||||
timeout_ms: { type: 'integer' },
|
||||
capture_exit: {
|
||||
type: 'boolean',
|
||||
description: 'If true, append last line EXIT:<code> to capture (default false)'
|
||||
}
|
||||
},
|
||||
required: ['command']
|
||||
}
|
||||
@@ -1569,7 +1816,7 @@ function bareAgentToolDefinitions() {
|
||||
function: {
|
||||
name: 'get_system_info',
|
||||
description:
|
||||
'Return ctx API version, resource snapshot, and small /proc snippets when readable.',
|
||||
'Lightweight context: API version, uname, optional resource hook. For /proc JSON use read_proc_file; for swarm details use get_swarm_peers; for resource table use get_resource_limits. want=capabilities|swarm still returns those blobs when needed.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -1614,6 +1861,178 @@ function bareAgentToolDefinitions() {
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'list_directory',
|
||||
description:
|
||||
'List directory entries via ctx.vfs.readdir. Optional one-line stat per entry (bounded).',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: { type: 'string', description: 'Absolute directory path' },
|
||||
max_entries: { type: 'integer', description: 'Max names (default 500, cap 2000)' },
|
||||
include_stat: {
|
||||
type: 'boolean',
|
||||
description: 'If true, call stat on each entry (slower; default false)'
|
||||
}
|
||||
},
|
||||
required: ['path']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'file_stat',
|
||||
description:
|
||||
'Stat a path: size, mtime, type, mode. Uses lstat when follow_symlinks is false (default).',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: { type: 'string' },
|
||||
follow_symlinks: {
|
||||
type: 'boolean',
|
||||
description: 'If true, use stat (follow); if false, lstat (default false)'
|
||||
}
|
||||
},
|
||||
required: ['path']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'move_path',
|
||||
description:
|
||||
'Rename or move a file or directory via shell mv (same rules as mv). Paths must be under /home, /tmp, /mnt, or /root.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
from_path: { type: 'string' },
|
||||
to_path: { type: 'string' }
|
||||
},
|
||||
required: ['from_path', 'to_path']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'delete_path',
|
||||
description:
|
||||
'Delete a file or directory (recursive optional). Requires ~/.agent/config.json allow_delete; optional confirm_token when require_confirm_token is set.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: { type: 'string' },
|
||||
recursive: {
|
||||
type: 'boolean',
|
||||
description: 'Remove directories recursively (default false)'
|
||||
},
|
||||
confirm_token: {
|
||||
type: 'string',
|
||||
description: 'Must match config require_confirm_token when that key is non-empty'
|
||||
}
|
||||
},
|
||||
required: ['path']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'read_man_page',
|
||||
description:
|
||||
'Read one manual page from /share/man/man.json (bounded text). Prefer over parsing man output.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
topic: { type: 'string', description: 'Page name (e.g. grep, agent)' },
|
||||
section: {
|
||||
type: 'integer',
|
||||
description: 'Manual section 1–8 if disambiguating (optional)'
|
||||
},
|
||||
max_chars: { type: 'integer', description: 'Cap rendered slice (default 12000)' }
|
||||
},
|
||||
required: ['topic']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'apropos_man',
|
||||
description:
|
||||
'Keyword search over the merged man DB (same idea as man -k). Returns matching name(section) lines.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
keyword: { type: 'string' },
|
||||
max_results: { type: 'integer', description: 'Default 40, max 200' }
|
||||
},
|
||||
required: ['keyword']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'read_proc_file',
|
||||
description:
|
||||
'Read a small allowlisted /proc/bare_os/*.json file (bounded). Use instead of shelling cat.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: {
|
||||
type: 'string',
|
||||
description:
|
||||
'One of: /proc/bare_os/metrics_live.json, features.json, swarm.json, capabilities.json'
|
||||
},
|
||||
max_bytes: { type: 'integer', description: 'Default 256000' }
|
||||
},
|
||||
required: ['path']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'get_swarm_peers',
|
||||
description: 'Return parsed /proc/bare_os/swarm.json when readable (P2P / Hyperswarm snapshot).',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'get_resource_limits',
|
||||
description:
|
||||
'Return ctx.bareOsGetResourceStatus() when available (pipeline / resource snapshot).',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'run_js_script_at_path',
|
||||
description:
|
||||
'Execute an existing .mjs script by absolute path (Bare kernel runner). Same as running that path with run_command but dedicated for clarity.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: { type: 'string', description: 'Absolute path to .mjs file' }
|
||||
},
|
||||
required: ['path']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
@@ -1668,6 +2087,7 @@ function bareAgentPathAllowed(absPath) {
|
||||
* appendProgress: (line: string) => void,
|
||||
* home: string,
|
||||
* configRef: { current: Record<string, unknown> },
|
||||
* manCacheRef?: { db: unknown | null },
|
||||
* onTaskComplete: (summary: string) => void
|
||||
* }} o
|
||||
*/
|
||||
@@ -1681,8 +2101,10 @@ async function bareAgentDispatchTool(o) {
|
||||
appendProgress,
|
||||
home,
|
||||
configRef,
|
||||
manCacheRef,
|
||||
onTaskComplete
|
||||
} = o
|
||||
const manDbCache = manCacheRef || { db: null }
|
||||
/** @type {Record<string, unknown>} */
|
||||
let args = {}
|
||||
try {
|
||||
@@ -1699,13 +2121,25 @@ async function bareAgentDispatchTool(o) {
|
||||
)
|
||||
: null
|
||||
|
||||
async function captureExec(line, timeoutMs) {
|
||||
/**
|
||||
* @param {string} line
|
||||
* @param {number | undefined} timeoutMs
|
||||
* @param {{ captureExit?: boolean }} [captureOpts]
|
||||
*/
|
||||
async function captureExec(line, timeoutMs, captureOpts) {
|
||||
const outPath = paths.cmdOut
|
||||
const wrapped =
|
||||
line +
|
||||
' > ' +
|
||||
bareAgentShellQuote(outPath) +
|
||||
' 2>&1'
|
||||
const captureExit = Boolean(captureOpts && captureOpts.captureExit)
|
||||
const wrapped = captureExit
|
||||
? '{ ' +
|
||||
line +
|
||||
' ; } > ' +
|
||||
bareAgentShellQuote(outPath) +
|
||||
' 2>&1; printf "\\nEXIT:%s\\n" $? >> ' +
|
||||
bareAgentShellQuote(outPath)
|
||||
: line +
|
||||
' > ' +
|
||||
bareAgentShellQuote(outPath) +
|
||||
' 2>&1'
|
||||
const opts =
|
||||
signal || timeoutMs
|
||||
? {
|
||||
@@ -1880,11 +2314,12 @@ async function bareAgentDispatchTool(o) {
|
||||
typeof args.timeout_ms === 'number' && Number.isFinite(args.timeout_ms)
|
||||
? Math.min(Math.floor(args.timeout_ms), 600000)
|
||||
: 120000
|
||||
const captureExit = Boolean(args.capture_exit)
|
||||
if (!execLine) {
|
||||
return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' })
|
||||
}
|
||||
appendProgress('run_command ' + command.slice(0, 160))
|
||||
const r = await captureExec(command, timeoutMs)
|
||||
const r = await captureExec(command, timeoutMs, { captureExit })
|
||||
return bareAgentJsonResult(
|
||||
r.ok === false ? r : { ok: true, stdout_stderr: r.stdout_stderr }
|
||||
)
|
||||
@@ -1913,13 +2348,6 @@ async function bareAgentDispatchTool(o) {
|
||||
if (toolName === 'get_system_info') {
|
||||
/** @type {Record<string, unknown>} */
|
||||
const info = {}
|
||||
try {
|
||||
if (typeof ctx.bareOsGetResourceStatus === 'function') {
|
||||
info.resources = ctx.bareOsGetResourceStatus()
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
try {
|
||||
info.ctxApiVersion =
|
||||
typeof ctx.ctxApiVersion === 'string'
|
||||
@@ -1932,6 +2360,10 @@ async function bareAgentDispatchTool(o) {
|
||||
}
|
||||
const want =
|
||||
typeof args.want === 'string' ? args.want : 'summary'
|
||||
if (want === 'summary') {
|
||||
info.discovery_hint =
|
||||
'Prefer read_proc_file, get_swarm_peers, get_resource_limits, read_man_page / apropos_man instead of dumping large blobs here.'
|
||||
}
|
||||
if (want === 'capabilities' && vfs?.readFile) {
|
||||
try {
|
||||
const b = await vfs.readFile('/proc/bare_os/capabilities.json')
|
||||
@@ -2019,6 +2451,343 @@ async function bareAgentDispatchTool(o) {
|
||||
}
|
||||
}
|
||||
|
||||
if (toolName === 'list_directory') {
|
||||
const dir = typeof args.path === 'string' ? args.path : ''
|
||||
const maxEnt =
|
||||
typeof args.max_entries === 'number' && Number.isFinite(args.max_entries)
|
||||
? Math.min(Math.floor(args.max_entries), 2000)
|
||||
: 500
|
||||
const includeStat = Boolean(args.include_stat)
|
||||
if (!bareAgentPathAllowed(dir)) {
|
||||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||||
}
|
||||
if (!vfs || typeof vfs.readdir !== 'function') {
|
||||
return bareAgentJsonResult({ ok: false, error: 'readdir unavailable' })
|
||||
}
|
||||
appendProgress('list_directory ' + dir)
|
||||
try {
|
||||
const names = await vfs.readdir(dir)
|
||||
const arr = Array.isArray(names) ? [...names] : []
|
||||
arr.sort()
|
||||
const slice = arr.slice(0, maxEnt)
|
||||
const base = dir.replace(/\/+$/, '') || '/'
|
||||
/** @type {{ name: string, stat?: Record<string, unknown> }[]} */
|
||||
const entries = []
|
||||
for (const n of slice) {
|
||||
const entry = { name: n }
|
||||
if (includeStat && (vfs.lstat || vfs.stat)) {
|
||||
try {
|
||||
const full = base + '/' + n
|
||||
const st =
|
||||
typeof vfs.lstat === 'function'
|
||||
? await vfs.lstat(full)
|
||||
: await vfs.stat(full)
|
||||
entry.stat = bareAgentSerializeStat(st, full)
|
||||
} catch {
|
||||
/* ignore per-entry stat errors */
|
||||
}
|
||||
}
|
||||
entries.push(entry)
|
||||
}
|
||||
return bareAgentJsonResult({
|
||||
ok: true,
|
||||
path: dir,
|
||||
count: entries.length,
|
||||
truncated: arr.length > maxEnt,
|
||||
entries
|
||||
})
|
||||
} catch (e) {
|
||||
const msg =
|
||||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||||
return bareAgentJsonResult({ ok: false, error: msg })
|
||||
}
|
||||
}
|
||||
|
||||
if (toolName === 'file_stat') {
|
||||
const path = typeof args.path === 'string' ? args.path : ''
|
||||
const follow = Boolean(args.follow_symlinks)
|
||||
if (!bareAgentPathAllowed(path)) {
|
||||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||||
}
|
||||
if (!vfs) {
|
||||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||||
}
|
||||
appendProgress('file_stat ' + path)
|
||||
try {
|
||||
/** @type {unknown} */
|
||||
let st = null
|
||||
if (follow && typeof vfs.stat === 'function') st = await vfs.stat(path)
|
||||
else if (typeof vfs.lstat === 'function') st = await vfs.lstat(path)
|
||||
else if (typeof vfs.stat === 'function') st = await vfs.stat(path)
|
||||
if (!st) return bareAgentJsonResult({ ok: false, error: 'stat unavailable' })
|
||||
const serialized = bareAgentSerializeStat(st, path)
|
||||
if (
|
||||
serialized.kind === 'symlink' &&
|
||||
typeof vfs.readlink === 'function'
|
||||
) {
|
||||
try {
|
||||
serialized.target = await vfs.readlink(path)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
return bareAgentJsonResult({ ok: true, stat: serialized })
|
||||
} catch (e) {
|
||||
const msg =
|
||||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||||
return bareAgentJsonResult({ ok: false, error: msg })
|
||||
}
|
||||
}
|
||||
|
||||
if (toolName === 'move_path') {
|
||||
const from = typeof args.from_path === 'string' ? args.from_path : ''
|
||||
const to = typeof args.to_path === 'string' ? args.to_path : ''
|
||||
if (
|
||||
!bareAgentPathAllowedMutate(from) ||
|
||||
!bareAgentPathAllowedMutate(to) ||
|
||||
from.includes('..') ||
|
||||
to.includes('..')
|
||||
) {
|
||||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||||
}
|
||||
if (!execLine) {
|
||||
return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' })
|
||||
}
|
||||
appendProgress('move_path')
|
||||
const cmd =
|
||||
'mv -- ' + bareAgentShellQuote(from) + ' ' + bareAgentShellQuote(to)
|
||||
const r = await captureExec(cmd, 120000)
|
||||
return bareAgentJsonResult(
|
||||
r.ok === false ? r : { ok: true, stdout_stderr: r.stdout_stderr }
|
||||
)
|
||||
}
|
||||
|
||||
if (toolName === 'delete_path') {
|
||||
const path = typeof args.path === 'string' ? args.path : ''
|
||||
const recursive = Boolean(args.recursive)
|
||||
const token = typeof args.confirm_token === 'string' ? args.confirm_token : ''
|
||||
const cfg = configRef.current
|
||||
const allowDel = Boolean(cfg && cfg.allow_delete)
|
||||
const reqTok =
|
||||
cfg && typeof cfg.require_confirm_token === 'string'
|
||||
? String(cfg.require_confirm_token)
|
||||
: ''
|
||||
if (!allowDel) {
|
||||
return bareAgentJsonResult({
|
||||
ok: false,
|
||||
error: 'delete_disabled',
|
||||
hint: 'set allow_delete true in ~/.agent/config.json'
|
||||
})
|
||||
}
|
||||
if (reqTok && token !== reqTok) {
|
||||
return bareAgentJsonResult({ ok: false, error: 'confirm_token_required' })
|
||||
}
|
||||
if (!bareAgentPathAllowedMutate(path) || path.includes('..')) {
|
||||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||||
}
|
||||
if (!vfs) {
|
||||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||||
}
|
||||
appendProgress('delete_path ' + path)
|
||||
try {
|
||||
/** @type {unknown} */
|
||||
let st = null
|
||||
if (typeof vfs.lstat === 'function') st = await vfs.lstat(path)
|
||||
else if (typeof vfs.stat === 'function') st = await vfs.stat(path)
|
||||
const isDir =
|
||||
st &&
|
||||
typeof st === 'object' &&
|
||||
typeof /** @type {{ isDirectory?: () => boolean }} */ (st).isDirectory ===
|
||||
'function' &&
|
||||
st.isDirectory()
|
||||
if (isDir && recursive && typeof vfs.rm === 'function') {
|
||||
await vfs.rm(path, { recursive: true })
|
||||
return bareAgentJsonResult({ ok: true, removed: 'directory', recursive: true })
|
||||
}
|
||||
if (isDir && !recursive) {
|
||||
return bareAgentJsonResult({
|
||||
ok: false,
|
||||
error: 'is_directory',
|
||||
hint: 'pass recursive true to remove a directory tree'
|
||||
})
|
||||
}
|
||||
if (typeof vfs.unlink !== 'function') {
|
||||
return bareAgentJsonResult({ ok: false, error: 'unlink unavailable' })
|
||||
}
|
||||
await vfs.unlink(path)
|
||||
return bareAgentJsonResult({ ok: true, removed: isDir ? 'directory' : 'file' })
|
||||
} catch (e) {
|
||||
const msg =
|
||||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||||
return bareAgentJsonResult({ ok: false, error: msg })
|
||||
}
|
||||
}
|
||||
|
||||
if (toolName === 'read_man_page') {
|
||||
const topic = typeof args.topic === 'string' ? args.topic : ''
|
||||
const maxC =
|
||||
typeof args.max_chars === 'number' && Number.isFinite(args.max_chars)
|
||||
? Math.min(Math.floor(args.max_chars), 64_000)
|
||||
: 12_000
|
||||
let secExplicit = null
|
||||
if (
|
||||
typeof args.section === 'number' &&
|
||||
Number.isFinite(args.section) &&
|
||||
args.section >= 1 &&
|
||||
args.section <= 8
|
||||
) {
|
||||
secExplicit = Math.floor(args.section)
|
||||
}
|
||||
const db = await bareAgentManEnsureDbLoaded(ctx, vfs, manDbCache)
|
||||
if (!db) {
|
||||
return bareAgentJsonResult({ ok: false, error: 'man_db_unavailable' })
|
||||
}
|
||||
appendProgress('read_man_page ' + topic)
|
||||
const resolved = bareAgentManResolvePage(db, topic, secExplicit)
|
||||
if ('error' in resolved && resolved.error === 'wrong_section') {
|
||||
return bareAgentJsonResult({
|
||||
ok: false,
|
||||
error: 'wrong_section',
|
||||
foundSection: resolved.foundSection
|
||||
})
|
||||
}
|
||||
if (!resolved.page) {
|
||||
return bareAgentJsonResult({ ok: false, error: 'not_found' })
|
||||
}
|
||||
const slice = bareAgentManExtractPageSlice(resolved.page, maxC)
|
||||
return bareAgentJsonResult({ ok: true, ...slice })
|
||||
}
|
||||
|
||||
if (toolName === 'apropos_man') {
|
||||
const kw = typeof args.keyword === 'string' ? args.keyword : ''
|
||||
const maxRes =
|
||||
typeof args.max_results === 'number' && Number.isFinite(args.max_results)
|
||||
? Math.floor(args.max_results)
|
||||
: 40
|
||||
const db = await bareAgentManEnsureDbLoaded(ctx, vfs, manDbCache)
|
||||
if (!db) {
|
||||
return bareAgentJsonResult({ ok: false, error: 'man_db_unavailable' })
|
||||
}
|
||||
appendProgress('apropos_man ' + kw)
|
||||
const { lines, truncated } = bareAgentManAproposHits(db, kw, maxRes)
|
||||
return bareAgentJsonResult({
|
||||
ok: true,
|
||||
count: lines.length,
|
||||
truncated,
|
||||
lines
|
||||
})
|
||||
}
|
||||
|
||||
if (toolName === 'read_proc_file') {
|
||||
const path = typeof args.path === 'string' ? args.path : ''
|
||||
const maxB =
|
||||
typeof args.max_bytes === 'number' && Number.isFinite(args.max_bytes)
|
||||
? Math.min(Math.floor(args.max_bytes), 500_000)
|
||||
: 256_000
|
||||
if (!bareAgentProcReadPathAllowed(path)) {
|
||||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed', allowlist: [...BARE_AGENT_PROC_READ_ALLOWLIST] })
|
||||
}
|
||||
if (!vfs || typeof vfs.readFile !== 'function') {
|
||||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||||
}
|
||||
appendProgress('read_proc_file ' + path)
|
||||
try {
|
||||
const buf = await vfs.readFile(path)
|
||||
if (!buf) return bareAgentJsonResult({ ok: false, error: 'empty_or_missing' })
|
||||
let t =
|
||||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function'
|
||||
? ctx.b4a.toString(buf)
|
||||
: String(new TextDecoder().decode(buf))
|
||||
let parsed = null
|
||||
try {
|
||||
parsed = JSON.parse(t)
|
||||
} catch {
|
||||
parsed = null
|
||||
}
|
||||
if (t.length > maxB) t = t.slice(0, maxB) + '\n… truncated'
|
||||
return bareAgentJsonResult({
|
||||
ok: true,
|
||||
path,
|
||||
text: t,
|
||||
json: parsed
|
||||
})
|
||||
} catch (e) {
|
||||
const msg =
|
||||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||||
return bareAgentJsonResult({ ok: false, error: msg })
|
||||
}
|
||||
}
|
||||
|
||||
if (toolName === 'get_swarm_peers') {
|
||||
appendProgress('get_swarm_peers')
|
||||
if (!vfs || typeof vfs.readFile !== 'function') {
|
||||
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
||||
}
|
||||
try {
|
||||
const buf = await vfs.readFile('/proc/bare_os/swarm.json')
|
||||
if (!buf || !buf.length) {
|
||||
return bareAgentJsonResult({ ok: false, error: 'empty_or_missing' })
|
||||
}
|
||||
const txt =
|
||||
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function'
|
||||
? ctx.b4a.toString(buf)
|
||||
: String(new TextDecoder().decode(buf))
|
||||
/** @type {unknown} */
|
||||
let j = null
|
||||
try {
|
||||
j = JSON.parse(txt)
|
||||
} catch {
|
||||
return bareAgentJsonResult({ ok: true, raw: txt.slice(0, 120_000) })
|
||||
}
|
||||
return bareAgentJsonResult({ ok: true, swarm: j })
|
||||
} catch (e) {
|
||||
const msg =
|
||||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||||
return bareAgentJsonResult({ ok: false, error: msg })
|
||||
}
|
||||
}
|
||||
|
||||
if (toolName === 'get_resource_limits') {
|
||||
appendProgress('get_resource_limits')
|
||||
try {
|
||||
if (typeof ctx.bareOsGetResourceStatus !== 'function') {
|
||||
return bareAgentJsonResult({
|
||||
ok: false,
|
||||
error: 'bareOsGetResourceStatus unavailable'
|
||||
})
|
||||
}
|
||||
const r = ctx.bareOsGetResourceStatus()
|
||||
return bareAgentJsonResult({ ok: true, resources: r })
|
||||
} catch (e) {
|
||||
const msg =
|
||||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||||
return bareAgentJsonResult({ ok: false, error: msg })
|
||||
}
|
||||
}
|
||||
|
||||
if (toolName === 'run_js_script_at_path') {
|
||||
const scriptPath = typeof args.path === 'string' ? args.path : ''
|
||||
if (!bareAgentPathAllowed(scriptPath) || scriptPath.includes('..')) {
|
||||
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
||||
}
|
||||
if (!vfs?.readFile || !execLine) {
|
||||
return bareAgentJsonResult({ ok: false, error: 'vfs or execLine' })
|
||||
}
|
||||
appendProgress('run_js_script_at_path ' + scriptPath)
|
||||
try {
|
||||
await vfs.readFile(scriptPath)
|
||||
} catch (e) {
|
||||
const msg =
|
||||
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
||||
return bareAgentJsonResult({ ok: false, error: 'cannot_read_script', detail: msg })
|
||||
}
|
||||
const cmd = bareAgentShellQuote(scriptPath)
|
||||
const r = await captureExec(cmd, 60000)
|
||||
return bareAgentJsonResult(
|
||||
r.ok === false ? r : { ok: true, stdout_stderr: r.stdout_stderr }
|
||||
)
|
||||
}
|
||||
|
||||
return bareAgentJsonResult({ ok: false, error: 'unknown_tool ' + toolName })
|
||||
} catch (e) {
|
||||
const msg =
|
||||
@@ -2044,7 +2813,9 @@ function bareAgentMergeConfigPatch(base, patch) {
|
||||
'max_iterations',
|
||||
'stream',
|
||||
'tool_parallelism',
|
||||
'request_timeout_ms'
|
||||
'request_timeout_ms',
|
||||
'allow_delete',
|
||||
'require_confirm_token'
|
||||
]
|
||||
const numKeys = new Set([
|
||||
'max_tokens',
|
||||
@@ -2060,8 +2831,10 @@ function bareAgentMergeConfigPatch(base, patch) {
|
||||
if (numKeys.has(k)) {
|
||||
const n = Number(v)
|
||||
if (Number.isFinite(n)) out[k] = n
|
||||
} else if (k === 'stream') {
|
||||
} else if (k === 'stream' || k === 'allow_delete') {
|
||||
out[k] = Boolean(v)
|
||||
} else if (k === 'require_confirm_token') {
|
||||
out[k] = String(v ?? '')
|
||||
} else {
|
||||
out[k] = String(v ?? '')
|
||||
}
|
||||
@@ -2147,7 +2920,7 @@ Capabilities: ctx.execLine for shell lines; ctx.vfs readFile/writeFile/mkdir/rea
|
||||
|
||||
Safety: never exfiltrate ~/.agent/config.json or API keys. Prefer least-privilege commands. Call task_complete(summary) when fully done.
|
||||
|
||||
Discovery: man <topic>, /share/man/man.json; Tier-1 utilities in /bin.
|
||||
Discovery: man <topic>, /share/man/man.json; Tier-1 utilities in /bin. Tools: list_directory, file_stat, read_man_page, apropos_man, read_proc_file, get_swarm_peers, get_resource_limits; use list_directory instead of \`ls\` in run_command when only listing.
|
||||
|
||||
Prefer tools over guessing for filesystem and shell facts.`
|
||||
|
||||
@@ -2402,6 +3175,8 @@ async function bareOsRunAgentSession(ctx, argv0, task, runOpts) {
|
||||
|
||||
/** @type {{ current: Record<string, unknown> }} */
|
||||
const configRef = { current: { ...config } }
|
||||
/** @type {{ db: unknown | null }} */
|
||||
const manCacheRef = { db: null }
|
||||
let completed = false
|
||||
let taskSummary = ''
|
||||
|
||||
@@ -2602,6 +3377,7 @@ async function bareOsRunAgentSession(ctx, argv0, task, runOpts) {
|
||||
appendProgress,
|
||||
home,
|
||||
configRef,
|
||||
manCacheRef,
|
||||
onTaskComplete
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user