2761 lines
103 KiB
JavaScript
2761 lines
103 KiB
JavaScript
/** Tool dispatch for /bin/agent (preamble; no import). */
|
|
|
|
/**
|
|
* @param {{
|
|
* ctx: Record<string, unknown>,
|
|
* toolName: string,
|
|
* argsJson: string,
|
|
* paths: { dir: string, config: string, cmdOut: string, workspace?: string, workspaceSkills?: string, skillsGlobal?: string },
|
|
* signal?: AbortSignal,
|
|
* appendProgress: (line: string) => void,
|
|
* home: string,
|
|
* configRef: { current: Record<string, unknown> },
|
|
* manCacheRef?: { db: unknown | null },
|
|
* onTaskComplete: (summary: string) => void
|
|
* }} o
|
|
*/
|
|
async function bareAgentDispatchTool(o) {
|
|
const {
|
|
ctx,
|
|
toolName,
|
|
argsJson,
|
|
paths,
|
|
signal,
|
|
appendProgress,
|
|
home,
|
|
configRef,
|
|
manCacheRef,
|
|
onTaskComplete
|
|
} = o
|
|
const internalGate = Boolean(o.internalGate)
|
|
const manDbCache = manCacheRef || { db: null }
|
|
/** @type {Record<string, unknown>} */
|
|
let args = {}
|
|
try {
|
|
args = /** @type {Record<string, unknown>} */ (JSON.parse(argsJson || '{}'))
|
|
} catch {
|
|
return bareAgentJsonResult({ ok: false, error: 'invalid_tool_arguments_json' })
|
|
}
|
|
const cfgNow = configRef.current || {}
|
|
if (
|
|
!internalGate &&
|
|
cfgNow.autonomous_active &&
|
|
Array.isArray(cfgNow.autonomous_deny_ops) &&
|
|
cfgNow.autonomous_deny_ops.map((x) => String(x)).includes(toolName)
|
|
) {
|
|
return bareAgentJsonResult({ ok: false, error: 'autonomous_op_denied', tool: toolName })
|
|
}
|
|
if (
|
|
!internalGate &&
|
|
cfgNow.plan_mode_active &&
|
|
typeof bareAgentPlanModeToolAllowed === 'function' &&
|
|
!bareAgentPlanModeToolAllowed(toolName, args, paths)
|
|
) {
|
|
return bareAgentJsonResult({
|
|
ok: false,
|
|
error: 'plan_mode_readonly',
|
|
tool: toolName,
|
|
hint: 'exit_plan_mode before mutating, or write only ' + String(paths.plan || '~/.agent/plan.md')
|
|
})
|
|
}
|
|
if (!internalGate && typeof bareAgentRunPreToolHooks === 'function' && paths.hooks) {
|
|
const hookReason = await bareAgentRunPreToolHooks(ctx, paths.hooks, toolName, args)
|
|
if (hookReason) {
|
|
return bareAgentJsonResult({
|
|
ok: false,
|
|
error: 'hook_denied',
|
|
tool: toolName,
|
|
reason: hookReason
|
|
})
|
|
}
|
|
}
|
|
if (toolName === 'list_dir') {
|
|
return bareAgentDispatchTool({ ...o, toolName: 'list_directory' })
|
|
}
|
|
if (toolName === 'glob') {
|
|
return bareAgentDispatchTool({ ...o, toolName: 'glob_files' })
|
|
}
|
|
if (toolName === 'ripgrep') {
|
|
return bareAgentDispatchTool({ ...o, toolName: 'grep' })
|
|
}
|
|
if (toolName === 'remember') {
|
|
return bareAgentDispatchTool({
|
|
...o,
|
|
toolName: 'memory_append',
|
|
argsJson: JSON.stringify({ ...args, kind: 'FACT' })
|
|
})
|
|
}
|
|
|
|
const vfs = ctx.vfs
|
|
const mutateDenyPrefixes = (function () {
|
|
const cfg = configRef.current || {}
|
|
if (Array.isArray(cfg.mutate_deny_prefixes) && cfg.mutate_deny_prefixes.length) {
|
|
return cfg.mutate_deny_prefixes.map((x) => String(x || '')).filter(Boolean)
|
|
}
|
|
return typeof BARE_AGENT_MUTATE_DENY_PREFIXES !== 'undefined'
|
|
? BARE_AGENT_MUTATE_DENY_PREFIXES
|
|
: ['/bin', '/etc', '/boot', '/lib', '/usr', '/share', '/proc', '/dev', '/sys', '/run']
|
|
})()
|
|
const AUTONOMOUS_CHECK_ALLOW = {
|
|
'coreutils-test': 'npm test -w bare-os-coreutils',
|
|
'verify-kernel-seeder-parity': 'node scripts/verify-kernel-seeder-parity.mjs',
|
|
'verify-man-coverage': 'node scripts/verify-man-coverage.mjs',
|
|
'verify-ctx-api-feature-bits': 'node scripts/verify-ctx-api-feature-bits.mjs'
|
|
}
|
|
const execLine =
|
|
typeof ctx.execLine === 'function'
|
|
? /** @type {(s: string, opts?: unknown) => Promise<unknown>} */ (
|
|
ctx.execLine.bind(ctx)
|
|
)
|
|
: null
|
|
|
|
/**
|
|
* @param {string} line
|
|
* @param {number | undefined} timeoutMs
|
|
* @param {{ captureExit?: boolean }} [captureOpts]
|
|
*/
|
|
async function captureExec(line, timeoutMs, captureOpts) {
|
|
const outPath = paths.cmdOut
|
|
const captureExit = Boolean(captureOpts && captureOpts.captureExit)
|
|
const trimmed = String(line || '').trim()
|
|
const compoundShell =
|
|
/\n/.test(trimmed) ||
|
|
/(^|[;\s])(for|if|while|until|case|function)\b/.test(trimmed) ||
|
|
/\b(do|done|then|else|fi|esac)\b/.test(trimmed) ||
|
|
/&&|\|\||\(\(|\{|\}/.test(trimmed)
|
|
/**
|
|
* Do not wrap with `{ cmd ; }` — Bare OS `splitTokensBySemicolon` splits on every `;`
|
|
* at depth 0 and does not treat `{ … }` as a compound, so `{` became argv[0]
|
|
* (`unknown command: {`). Redirect only; read exit from env after `execLine`.
|
|
*/
|
|
const wrapped =
|
|
(compoundShell
|
|
? 'sh -c ' + bareAgentShellQuote(trimmed)
|
|
: line) +
|
|
' > ' +
|
|
bareAgentShellQuote(outPath) +
|
|
' 2>&1'
|
|
const opts =
|
|
signal || timeoutMs
|
|
? {
|
|
signal,
|
|
timeoutMs: timeoutMs || undefined
|
|
}
|
|
: undefined
|
|
try {
|
|
if (opts) await execLine(wrapped, opts)
|
|
else await execLine(wrapped)
|
|
} catch (e) {
|
|
const msg = e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
|
return { ok: false, exitNote: msg }
|
|
}
|
|
let captured = ''
|
|
try {
|
|
if (vfs && typeof vfs.readFile === 'function') {
|
|
const buf = await vfs.readFile(outPath)
|
|
if (buf && buf.length) {
|
|
captured =
|
|
typeof ctx.b4a !== 'undefined' &&
|
|
ctx.b4a &&
|
|
typeof ctx.b4a.toString === 'function'
|
|
? ctx.b4a.toString(buf)
|
|
: String(new TextDecoder().decode(buf))
|
|
}
|
|
}
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
if (captureExit) {
|
|
const env = ctx.vfs?.env && typeof ctx.vfs.env === 'object' ? ctx.vfs.env : null
|
|
const rawEc =
|
|
env && env.BARE_OS_EXIT_STATUS != null && env.BARE_OS_EXIT_STATUS !== ''
|
|
? env.BARE_OS_EXIT_STATUS
|
|
: ctx.exitCode
|
|
const n = Number(rawEc)
|
|
const codeStr = String(Number.isFinite(n) ? n : 0)
|
|
const exitLine = '\nEXIT:' + codeStr + '\n'
|
|
captured += exitLine
|
|
try {
|
|
if (vfs?.readFile && vfs?.writeFile && ctx.b4a && typeof ctx.b4a.concat === 'function') {
|
|
let prev = await vfs.readFile(outPath)
|
|
const prevBytes =
|
|
prev && prev.length
|
|
? prev instanceof Uint8Array
|
|
? prev
|
|
: ctx.b4a.from(prev)
|
|
: ctx.b4a.from('')
|
|
await vfs.writeFile(
|
|
outPath,
|
|
ctx.b4a.concat([prevBytes, ctx.b4a.from(exitLine)])
|
|
)
|
|
}
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
const max = 120_000
|
|
if (captured.length > max) captured = captured.slice(0, max) + '\n… truncated'
|
|
return { ok: true, stdout_stderr: captured }
|
|
}
|
|
|
|
/**
|
|
* @param {string} text
|
|
*/
|
|
function redactSensitiveText(text) {
|
|
return String(text || '')
|
|
.replace(/\bBearer\s+\S+/gi, 'Bearer <redacted>')
|
|
.replace(/\bsk-[a-zA-Z0-9]{16,}\b/g, '<redacted_sk>')
|
|
.replace(
|
|
/\b([A-Z0-9_]*(KEY|TOKEN|SECRET|PASS|PASSWORD)[A-Z0-9_]*)\s*=\s*([^\s]+)/gi,
|
|
'$1=<redacted>'
|
|
)
|
|
}
|
|
|
|
/**
|
|
* @param {string} path
|
|
* @param {number} maxChars
|
|
* @param {boolean} tailOnly
|
|
* @param {boolean} redact
|
|
*/
|
|
async function readBoundedText(path, maxChars, tailOnly, redact) {
|
|
if (!vfs || typeof vfs.readFile !== 'function') {
|
|
return { ok: false, error: 'vfs unavailable' }
|
|
}
|
|
try {
|
|
const b = await vfs.readFile(path)
|
|
if (!b || !b.length) return { ok: false, error: 'empty_or_missing' }
|
|
let t =
|
|
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function'
|
|
? ctx.b4a.toString(b)
|
|
: String(new TextDecoder().decode(b))
|
|
if (redact) t = redactSensitiveText(t)
|
|
const out = tailOnly ? t.slice(-maxChars) : bareAgentTruncateChars(t, maxChars)
|
|
return { ok: true, path, text: out, truncated: t.length > out.length }
|
|
} catch (e) {
|
|
const msg =
|
|
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
|
return { ok: false, error: msg }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} p
|
|
*/
|
|
function autonomousPathAllowed(p) {
|
|
const s = String(p || '').trim()
|
|
if (!s) return true
|
|
return bareAgentPathAllowed(s)
|
|
}
|
|
|
|
/**
|
|
* @param {string} p
|
|
* @param {'read' | 'mutate'} [kind]
|
|
*/
|
|
function enforceAutonomousPath(p, kind) {
|
|
const cfg = configRef.current || {}
|
|
const path = String(p || '').trim()
|
|
if (!path) return true
|
|
if (kind === 'mutate') {
|
|
if (
|
|
typeof bareAgentPathAllowedMutate === 'function' &&
|
|
!bareAgentPathAllowedMutate(path, mutateDenyPrefixes)
|
|
) {
|
|
return false
|
|
}
|
|
} else if (!bareAgentPathAllowed(path)) {
|
|
return false
|
|
}
|
|
if (!cfg.autonomous_active) return true
|
|
if (kind !== 'mutate') return true
|
|
const allowList = Array.isArray(cfg.autonomous_allow_paths)
|
|
? cfg.autonomous_allow_paths.map((x) => String(x || '').trim()).filter(Boolean)
|
|
: []
|
|
if (!allowList.length || allowList.includes('*')) return true
|
|
for (const pref of allowList) {
|
|
if (path === pref || path.startsWith(pref.endsWith('/') ? pref : pref + '/')) return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
try {
|
|
if (toolName === 'read_skill') {
|
|
const skill = typeof args.skill === 'string' ? args.skill.trim() : ''
|
|
const maxB =
|
|
typeof args.max_bytes === 'number' && Number.isFinite(args.max_bytes)
|
|
? Math.min(Math.floor(args.max_bytes), 512_000)
|
|
: 256_000
|
|
if (!skill) {
|
|
return bareAgentJsonResult({ ok: false, error: 'skill_required' })
|
|
}
|
|
const skillPaths = {
|
|
workspaceSkills:
|
|
typeof paths.workspaceSkills === 'string'
|
|
? paths.workspaceSkills
|
|
: paths.dir + '/workspace/skills',
|
|
skillsGlobal:
|
|
typeof paths.skillsGlobal === 'string' ? paths.skillsGlobal : paths.dir + '/skills'
|
|
}
|
|
appendProgress('read_skill ' + skill)
|
|
const loaded = await bareAgentLoadSkillMarkdown(ctx, skillPaths, skill)
|
|
if (!loaded.ok) {
|
|
return bareAgentJsonResult({
|
|
ok: false,
|
|
error: loaded.error || 'load_failed',
|
|
skill
|
|
})
|
|
}
|
|
let content = loaded.content
|
|
if (content.length > maxB) content = content.slice(0, maxB) + '\n… truncated'
|
|
return bareAgentJsonResult({
|
|
ok: true,
|
|
id: loaded.id,
|
|
name: loaded.skill,
|
|
path: loaded.path,
|
|
source: loaded.source,
|
|
content
|
|
})
|
|
}
|
|
|
|
if (toolName === 'glob_files') {
|
|
const pattern = typeof args.pattern === 'string' ? args.pattern.trim() : ''
|
|
if (!pattern) return bareAgentJsonResult({ ok: false, error: 'pattern_required' })
|
|
const root =
|
|
typeof args.root === 'string' && args.root.trim()
|
|
? args.root.trim()
|
|
: home || '/home'
|
|
if (!bareAgentPathAllowed(root)) {
|
|
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
|
}
|
|
const maxResults =
|
|
typeof args.max_results === 'number' && Number.isFinite(args.max_results)
|
|
? Math.min(Math.max(Math.floor(args.max_results), 1), 400)
|
|
: 100
|
|
if (typeof bareAgentGlobFiles !== 'function') {
|
|
return bareAgentJsonResult({ ok: false, error: 'glob_unavailable' })
|
|
}
|
|
appendProgress('glob_files ' + pattern + ' @ ' + root)
|
|
const files = await bareAgentGlobFiles(ctx, root, pattern, {
|
|
maxFiles: Math.max(maxResults * 4, 200),
|
|
maxDepth: 10
|
|
})
|
|
return bareAgentJsonResult({
|
|
ok: true,
|
|
root,
|
|
pattern,
|
|
count: Math.min(files.length, maxResults),
|
|
truncated: files.length > maxResults,
|
|
files: files.slice(0, maxResults)
|
|
})
|
|
}
|
|
|
|
if (toolName === 'todo_write') {
|
|
const todosPath = paths.todos || paths.dir + '/todos.json'
|
|
const merge = args.merge !== false
|
|
const prev =
|
|
typeof bareAgentLoadTodos === 'function'
|
|
? await bareAgentLoadTodos(ctx, todosPath)
|
|
: []
|
|
let next
|
|
try {
|
|
next = bareAgentTodoApply(args.todos, { merge }, prev)
|
|
} catch (e) {
|
|
const msg =
|
|
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
|
return bareAgentJsonResult({ ok: false, error: msg })
|
|
}
|
|
if (typeof bareAgentSaveTodos === 'function') {
|
|
await bareAgentSaveTodos(ctx, todosPath, next)
|
|
}
|
|
const summary = bareAgentTodoSummarize(next)
|
|
appendProgress('todo_write open=' + String(summary.open) + '/' + String(summary.total))
|
|
return bareAgentJsonResult({
|
|
ok: true,
|
|
merge,
|
|
todos: next,
|
|
summary
|
|
})
|
|
}
|
|
|
|
if (toolName === 'memory_search') {
|
|
const query = typeof args.query === 'string' ? args.query.trim() : ''
|
|
if (!query) return bareAgentJsonResult({ ok: false, error: 'query_required' })
|
|
const maxHits =
|
|
typeof args.max_hits === 'number' && Number.isFinite(args.max_hits)
|
|
? Math.min(Math.max(Math.floor(args.max_hits), 1), 24)
|
|
: 8
|
|
const memDir =
|
|
typeof paths.workspaceMemory === 'string'
|
|
? paths.workspaceMemory
|
|
: paths.dir + '/workspace/memory'
|
|
const workspace =
|
|
typeof paths.workspace === 'string' ? paths.workspace : paths.dir + '/workspace'
|
|
const seeds = []
|
|
if (typeof bareAgentVfsWalkFiles === 'function') {
|
|
const walked = await bareAgentVfsWalkFiles(ctx, memDir, {
|
|
maxFiles: 80,
|
|
maxDepth: 4
|
|
})
|
|
for (let i = 0; i < walked.length; i++) seeds.push(walked[i])
|
|
}
|
|
seeds.push(workspace + '/MEMORY.md')
|
|
if (paths.compact) seeds.push(paths.compact)
|
|
else seeds.push(paths.dir + '/compact.md')
|
|
const uniq = []
|
|
const seenMem = Object.create(null)
|
|
for (let i = 0; i < seeds.length; i++) {
|
|
if (seenMem[seeds[i]]) continue
|
|
seenMem[seeds[i]] = 1
|
|
uniq.push(seeds[i])
|
|
}
|
|
appendProgress('memory_search ' + query.slice(0, 80))
|
|
const hits =
|
|
typeof bareAgentMemorySearchFiles === 'function'
|
|
? await bareAgentMemorySearchFiles(ctx, uniq, query, { maxHits })
|
|
: []
|
|
return bareAgentJsonResult({ ok: true, query, hits })
|
|
}
|
|
|
|
if (toolName === 'memory_get') {
|
|
const rawPath = typeof args.path === 'string' ? args.path.trim() : ''
|
|
if (!rawPath) return bareAgentJsonResult({ ok: false, error: 'path_required' })
|
|
const memDir =
|
|
typeof paths.workspaceMemory === 'string'
|
|
? paths.workspaceMemory
|
|
: paths.dir + '/workspace/memory'
|
|
const workspace =
|
|
typeof paths.workspace === 'string' ? paths.workspace : paths.dir + '/workspace'
|
|
let path = rawPath
|
|
if (!path.startsWith('/')) path = memDir + '/' + path.replace(/^\/+/, '')
|
|
const allowed =
|
|
path === workspace + '/MEMORY.md' ||
|
|
path === (paths.compact || paths.dir + '/compact.md') ||
|
|
path === memDir ||
|
|
path.indexOf(memDir + '/') === 0
|
|
if (!allowed) {
|
|
return bareAgentJsonResult({
|
|
ok: false,
|
|
error: 'memory_path_denied',
|
|
hint: 'path must be under ' + memDir
|
|
})
|
|
}
|
|
const maxB =
|
|
typeof args.max_bytes === 'number' && Number.isFinite(args.max_bytes)
|
|
? Math.min(Math.floor(args.max_bytes), 256_000)
|
|
: 64_000
|
|
appendProgress('memory_get ' + path)
|
|
let text = await bareAgentReadTextFile(ctx, path)
|
|
if (!text) return bareAgentJsonResult({ ok: false, error: 'empty_or_missing', path })
|
|
const truncated = text.length > maxB
|
|
if (truncated) text = text.slice(0, maxB) + '\n… truncated'
|
|
return bareAgentJsonResult({ ok: true, path, content: text, truncated })
|
|
}
|
|
|
|
if (toolName === 'enter_plan_mode') {
|
|
const planPath = paths.plan || paths.dir + '/plan.md'
|
|
configRef.current = { ...(configRef.current || {}), plan_mode_active: true }
|
|
if (typeof bareAgentSaveConfigFromTools === 'function') {
|
|
await bareAgentSaveConfigFromTools(ctx, paths, configRef.current)
|
|
}
|
|
const note = typeof args.note === 'string' ? args.note.trim() : ''
|
|
let prev = ''
|
|
try {
|
|
prev = await bareAgentReadTextFile(ctx, planPath)
|
|
} catch {
|
|
prev = ''
|
|
}
|
|
if (!prev.trim()) {
|
|
const starter =
|
|
'# Plan\n\n' +
|
|
(note ? note + '\n' : '- Investigate with read-only tools.\n- Write the plan here.\n- Call exit_plan_mode when ready to implement.\n')
|
|
await bareAgentWriteTextFile(ctx, planPath, starter)
|
|
} else if (note) {
|
|
await bareAgentWriteTextFile(ctx, planPath, prev.replace(/\s*$/, '') + '\n\n' + note + '\n')
|
|
}
|
|
appendProgress('enter_plan_mode ' + planPath)
|
|
return bareAgentJsonResult({
|
|
ok: true,
|
|
plan_mode_active: true,
|
|
plan: planPath
|
|
})
|
|
}
|
|
|
|
if (toolName === 'exit_plan_mode') {
|
|
const summary = typeof args.summary === 'string' ? args.summary.trim() : ''
|
|
configRef.current = { ...(configRef.current || {}), plan_mode_active: false }
|
|
if (typeof bareAgentSaveConfigFromTools === 'function') {
|
|
await bareAgentSaveConfigFromTools(ctx, paths, configRef.current)
|
|
}
|
|
if (summary && paths.plan) {
|
|
const prev = await bareAgentReadTextFile(ctx, paths.plan)
|
|
await bareAgentWriteTextFile(
|
|
ctx,
|
|
paths.plan,
|
|
(prev ? prev.replace(/\s*$/, '') + '\n\n' : '') + '## Exit\n' + summary + '\n'
|
|
)
|
|
}
|
|
appendProgress('exit_plan_mode')
|
|
return bareAgentJsonResult({
|
|
ok: true,
|
|
plan_mode_active: false,
|
|
summary: summary || null
|
|
})
|
|
}
|
|
|
|
if (toolName === 'ask_user_question') {
|
|
const rows = Array.isArray(args.questions) ? args.questions : []
|
|
if (!rows.length) {
|
|
return bareAgentJsonResult({ ok: false, error: 'questions_required' })
|
|
}
|
|
/** @type {Record<string, unknown>[]} */
|
|
const questions = []
|
|
for (let i = 0; i < rows.length; i++) {
|
|
const row = rows[i] && typeof rows[i] === 'object' ? rows[i] : {}
|
|
const question = String(row.question || '').trim()
|
|
if (!question) continue
|
|
const opts = Array.isArray(row.options) ? row.options : []
|
|
questions.push({
|
|
question,
|
|
multi_select: Boolean(row.multi_select),
|
|
options: opts.map(function (opt) {
|
|
if (opt && typeof opt === 'object') {
|
|
return {
|
|
label: String(opt.label || ''),
|
|
description: String(opt.description || '')
|
|
}
|
|
}
|
|
return { label: String(opt || ''), description: '' }
|
|
})
|
|
})
|
|
}
|
|
if (!questions.length) {
|
|
return bareAgentJsonResult({ ok: false, error: 'questions_required' })
|
|
}
|
|
const askPath = paths.ask || paths.dir + '/ask.json'
|
|
const payload = {
|
|
asked_at: new Date().toISOString(),
|
|
questions
|
|
}
|
|
if (typeof bareAgentWriteJsonFile === 'function') {
|
|
await bareAgentWriteJsonFile(ctx, askPath, payload)
|
|
}
|
|
const text = questions
|
|
.map(function (q, i) {
|
|
const opts = Array.isArray(q.options)
|
|
? q.options
|
|
.map(function (o, j) {
|
|
return (
|
|
' ' +
|
|
String(j + 1) +
|
|
') ' +
|
|
String(o.label || '') +
|
|
(o.description ? ' — ' + String(o.description) : '')
|
|
)
|
|
})
|
|
.join('\n')
|
|
: ''
|
|
return (
|
|
String(i + 1) +
|
|
'. ' +
|
|
q.question +
|
|
(q.multi_select ? ' (multi-select)' : '') +
|
|
(opts ? '\n' + opts : '')
|
|
)
|
|
})
|
|
.join('\n')
|
|
appendProgress('ask_user_question n=' + String(questions.length))
|
|
return bareAgentJsonResult({
|
|
ok: true,
|
|
ask: askPath,
|
|
questions,
|
|
text,
|
|
hint: 'Wait for the user to answer these questions on the next turn.'
|
|
})
|
|
}
|
|
|
|
if (toolName === 'task_complete') {
|
|
const summary = typeof args.summary === 'string' ? args.summary : ''
|
|
appendProgress('task_complete: ' + summary.slice(0, 200))
|
|
onTaskComplete(summary || '(done)')
|
|
return bareAgentJsonResult({
|
|
ok: true,
|
|
completed: true,
|
|
summary
|
|
})
|
|
}
|
|
|
|
if (toolName === 'autonomous_run') {
|
|
const goal = typeof args.goal === 'string' ? args.goal.trim() : ''
|
|
const scopePath = typeof args.scope_path === 'string' ? args.scope_path.trim() : ''
|
|
const cfg = configRef.current || {}
|
|
if (!goal) return bareAgentJsonResult({ ok: false, error: 'goal_required' })
|
|
if (scopePath && !autonomousPathAllowed(scopePath)) {
|
|
return bareAgentJsonResult({ ok: false, error: 'scope_path_denied' })
|
|
}
|
|
const maxRuntimeMsRaw =
|
|
typeof args.max_runtime_ms === 'number' && Number.isFinite(args.max_runtime_ms)
|
|
? args.max_runtime_ms
|
|
: cfg.autonomous_max_runtime_ms
|
|
const requiredChecks = Array.isArray(args.required_checks)
|
|
? args.required_checks.map((x) => String(x || '').trim()).filter(Boolean)
|
|
: []
|
|
const unknown = requiredChecks.filter((x) => !Object.prototype.hasOwnProperty.call(AUTONOMOUS_CHECK_ALLOW, x))
|
|
if (unknown.length) {
|
|
return bareAgentJsonResult({ ok: false, error: 'unknown_required_checks', unknown, allowlist: Object.keys(AUTONOMOUS_CHECK_ALLOW) })
|
|
}
|
|
const started =
|
|
typeof bareAgentBeginAutonomousRun === 'function'
|
|
? bareAgentBeginAutonomousRun(cfg, {
|
|
goal,
|
|
maxRuntimeMs: maxRuntimeMsRaw,
|
|
requiredChecks
|
|
})
|
|
: {
|
|
...cfg,
|
|
autonomous_mode_enabled: true,
|
|
autonomous_active: true,
|
|
autonomous_stop_requested: false,
|
|
autonomous_started_at_ms: Date.now(),
|
|
autonomous_goal: goal,
|
|
autonomous_status: 'running',
|
|
autonomous_last_error: '',
|
|
autonomous_max_runtime_ms: Math.min(
|
|
Math.max(Math.floor(Number(maxRuntimeMsRaw) || 0), 60000),
|
|
7_200_000
|
|
),
|
|
autonomous_completion_required_checks: requiredChecks
|
|
}
|
|
const merged = bareAgentMergeConfigPatch(cfg, started)
|
|
await bareAgentSaveConfigFromTools(ctx, paths, merged)
|
|
configRef.current = merged
|
|
appendProgress('autonomous_run start goal=' + goal.slice(0, 160))
|
|
return bareAgentJsonResult({
|
|
ok: true,
|
|
active: true,
|
|
enabled: true,
|
|
goal,
|
|
scope_path: scopePath || null,
|
|
max_runtime_ms: merged.autonomous_max_runtime_ms,
|
|
required_checks: requiredChecks,
|
|
status: 'running'
|
|
})
|
|
}
|
|
|
|
if (toolName === 'autonomous_run_status') {
|
|
const cfg = configRef.current || {}
|
|
const started = Number(cfg.autonomous_started_at_ms) || 0
|
|
const elapsed = started > 0 ? Math.max(0, Date.now() - started) : 0
|
|
const maxRuntime = Number(cfg.autonomous_max_runtime_ms) || 0
|
|
return bareAgentJsonResult({
|
|
ok: true,
|
|
autonomous_mode_enabled: Boolean(cfg.autonomous_mode_enabled),
|
|
active: Boolean(cfg.autonomous_active),
|
|
stop_requested: Boolean(cfg.autonomous_stop_requested),
|
|
goal: String(cfg.autonomous_goal || ''),
|
|
status: String(cfg.autonomous_status || 'idle'),
|
|
last_error: String(cfg.autonomous_last_error || ''),
|
|
started_at_ms: started,
|
|
elapsed_ms: elapsed,
|
|
max_runtime_ms: maxRuntime,
|
|
remaining_ms: maxRuntime > 0 ? Math.max(0, maxRuntime - elapsed) : 0,
|
|
required_checks: Array.isArray(cfg.autonomous_completion_required_checks)
|
|
? cfg.autonomous_completion_required_checks
|
|
: []
|
|
})
|
|
}
|
|
|
|
if (toolName === 'autonomous_run_stop') {
|
|
const cfg = configRef.current || {}
|
|
const reason = typeof args.reason === 'string' ? args.reason.trim() : ''
|
|
const stopped =
|
|
typeof bareAgentStopAutonomousRun === 'function'
|
|
? bareAgentStopAutonomousRun(cfg, reason)
|
|
: {
|
|
...cfg,
|
|
autonomous_stop_requested: true,
|
|
autonomous_status: 'stopping',
|
|
autonomous_last_error: reason || String(cfg.autonomous_last_error || '')
|
|
}
|
|
const merged = bareAgentMergeConfigPatch(cfg, stopped)
|
|
await bareAgentSaveConfigFromTools(ctx, paths, merged)
|
|
configRef.current = merged
|
|
appendProgress('autonomous_run_stop ' + (reason || 'requested'))
|
|
return bareAgentJsonResult({ ok: true, stop_requested: true, reason: reason || null })
|
|
}
|
|
|
|
if (toolName === 'read_file') {
|
|
const path =
|
|
typeof bareAgentToolPathArg === 'function'
|
|
? bareAgentToolPathArg(args)
|
|
: typeof args.path === 'string'
|
|
? args.path
|
|
: ''
|
|
if (!enforceAutonomousPath(path, 'read')) {
|
|
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
|
}
|
|
const maxB =
|
|
typeof args.max_bytes === 'number' && Number.isFinite(args.max_bytes)
|
|
? Math.min(Math.floor(args.max_bytes), 1_000_000)
|
|
: 256_000
|
|
if (!bareAgentPathAllowed(path)) {
|
|
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
|
}
|
|
if (!vfs || typeof vfs.readFile !== 'function') {
|
|
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
|
}
|
|
appendProgress('read_file ' + path)
|
|
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))
|
|
if (t.length > maxB) t = t.slice(0, maxB) + '\n… truncated'
|
|
const hasSlice =
|
|
(typeof args.offset === 'number' && Number.isFinite(args.offset)) ||
|
|
(typeof args.limit === 'number' && Number.isFinite(args.limit))
|
|
if (hasSlice && typeof bareAgentSliceFileLines === 'function') {
|
|
const sliced = bareAgentSliceFileLines(t, {
|
|
offset:
|
|
typeof args.offset === 'number' && Number.isFinite(args.offset)
|
|
? args.offset
|
|
: 1,
|
|
limit:
|
|
typeof args.limit === 'number' && Number.isFinite(args.limit)
|
|
? args.limit
|
|
: undefined,
|
|
numbered: args.numbered !== false
|
|
})
|
|
return bareAgentJsonResult({ ok: true, path, ...sliced })
|
|
}
|
|
return bareAgentJsonResult({ ok: true, path, content: t })
|
|
}
|
|
|
|
if (toolName === 'write_file') {
|
|
const path = typeof args.path === 'string' ? args.path : ''
|
|
if (!enforceAutonomousPath(path, 'mutate')) {
|
|
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
|
}
|
|
const content = typeof args.content === 'string' ? args.content : ''
|
|
if (!path.startsWith('/') || path.includes('..')) {
|
|
return bareAgentJsonResult({ ok: false, error: 'bad_path' })
|
|
}
|
|
if (!vfs || typeof vfs.writeFile !== 'function' || typeof vfs.mkdir !== 'function') {
|
|
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
|
}
|
|
appendProgress('write_file ' + path)
|
|
if (typeof bareAgentPushEdit === 'function' && paths.edits) {
|
|
try {
|
|
const prev = await bareAgentReadTextFile(ctx, path)
|
|
await bareAgentPushEdit(ctx, paths.edits, { path, prev, tool: 'write_file' })
|
|
} catch {
|
|
/* new file */
|
|
}
|
|
}
|
|
let dir = path.replace(/\/[^/]+$/, '')
|
|
if (dir && dir !== path) await vfs.mkdir(dir, { recursive: true })
|
|
const body =
|
|
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function'
|
|
? ctx.b4a.from(content)
|
|
: new TextEncoder().encode(content)
|
|
await vfs.writeFile(path, body)
|
|
return bareAgentJsonResult({ ok: true, bytes: body.length })
|
|
}
|
|
|
|
if (toolName === 'edit_file' || toolName === 'search_replace') {
|
|
const path =
|
|
typeof bareAgentToolPathArg === 'function'
|
|
? bareAgentToolPathArg(args)
|
|
: typeof args.path === 'string'
|
|
? args.path
|
|
: ''
|
|
if (!enforceAutonomousPath(path, 'mutate')) {
|
|
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
|
}
|
|
if (!vfs?.readFile || !vfs?.writeFile) {
|
|
return bareAgentJsonResult({ ok: false, error: 'path_or_vfs' })
|
|
}
|
|
appendProgress(toolName + ' ' + path)
|
|
const buf = await vfs.readFile(path)
|
|
if (typeof bareAgentPushEdit === 'function' && paths.edits) {
|
|
try {
|
|
const snap =
|
|
buf && buf.length
|
|
? typeof ctx.b4a !== 'undefined' &&
|
|
ctx.b4a &&
|
|
typeof ctx.b4a.toString === 'function'
|
|
? ctx.b4a.toString(buf)
|
|
: String(new TextDecoder().decode(buf))
|
|
: ''
|
|
await bareAgentPushEdit(ctx, paths.edits, { path, prev: snap, tool: toolName })
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
let prev =
|
|
buf && buf.length
|
|
? typeof ctx.b4a !== 'undefined' &&
|
|
ctx.b4a &&
|
|
typeof ctx.b4a.toString === 'function'
|
|
? ctx.b4a.toString(buf)
|
|
: String(new TextDecoder().decode(buf))
|
|
: ''
|
|
const full =
|
|
toolName === 'search_replace'
|
|
? ''
|
|
: typeof args.content === 'string'
|
|
? args.content
|
|
: ''
|
|
const oldStr = typeof args.old_string === 'string' ? args.old_string : ''
|
|
const newStr = typeof args.new_string === 'string' ? args.new_string : ''
|
|
const replaceAll = Boolean(args.replace_all)
|
|
let next = prev
|
|
let replacements = 0
|
|
if (full.length > 0) {
|
|
next = full
|
|
replacements = 1
|
|
} else if (typeof bareAgentSearchReplaceApply === 'function') {
|
|
const applied = bareAgentSearchReplaceApply(prev, oldStr, newStr, replaceAll)
|
|
if (!applied.ok) {
|
|
return bareAgentJsonResult({
|
|
ok: false,
|
|
error: applied.error,
|
|
count: applied.count,
|
|
hint: applied.hint
|
|
})
|
|
}
|
|
next = applied.next
|
|
replacements = applied.replacements || 0
|
|
} else if (oldStr) {
|
|
if (!prev.includes(oldStr)) {
|
|
return bareAgentJsonResult({ ok: false, error: 'old_string not found' })
|
|
}
|
|
next = replaceAll ? prev.split(oldStr).join(newStr) : prev.replace(oldStr, newStr)
|
|
replacements = 1
|
|
} else {
|
|
return bareAgentJsonResult({
|
|
ok: false,
|
|
error: 'need content or old_string+new_string'
|
|
})
|
|
}
|
|
const body =
|
|
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function'
|
|
? ctx.b4a.from(next)
|
|
: new TextEncoder().encode(next)
|
|
let dir = path.replace(/\/[^/]+$/, '')
|
|
if (dir && dir !== path && typeof vfs.mkdir === 'function') {
|
|
await vfs.mkdir(dir, { recursive: true })
|
|
}
|
|
await vfs.writeFile(path, body)
|
|
return bareAgentJsonResult({
|
|
ok: true,
|
|
bytes: body.length,
|
|
replacements,
|
|
replace_all: replaceAll
|
|
})
|
|
}
|
|
|
|
if (toolName === 'create_directory') {
|
|
const path = typeof args.path === 'string' ? args.path : ''
|
|
if (!enforceAutonomousPath(path, 'mutate')) {
|
|
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
|
}
|
|
if (!path.startsWith('/')) {
|
|
return bareAgentJsonResult({ ok: false, error: 'bad_path' })
|
|
}
|
|
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 })
|
|
}
|
|
|
|
if (toolName === 'search_files') {
|
|
const pattern = typeof args.pattern === 'string' ? args.pattern : ''
|
|
const root = typeof args.root === 'string' ? args.root : '/home'
|
|
const maxLines =
|
|
typeof args.max_lines === 'number' && Number.isFinite(args.max_lines)
|
|
? Math.min(Math.floor(args.max_lines), 500)
|
|
: 200
|
|
if (!execLine) {
|
|
return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' })
|
|
}
|
|
appendProgress('search_files ' + pattern + ' @ ' + root)
|
|
const cmd =
|
|
'grep -Rnl -- ' +
|
|
bareAgentShellQuote(pattern) +
|
|
' ' +
|
|
bareAgentShellQuote(root) +
|
|
' 2>/dev/null | head -n ' +
|
|
maxLines
|
|
const r = await captureExec(cmd, 60000)
|
|
return bareAgentJsonResult(
|
|
r.ok === false ? r : { ok: true, stdout_stderr: r.stdout_stderr }
|
|
)
|
|
}
|
|
|
|
if (toolName === 'run_command') {
|
|
const command = typeof args.command === 'string' ? args.command : ''
|
|
const deniedBy = bareAgentCommandDeniedByList(
|
|
command,
|
|
(configRef.current || {}).command_deny
|
|
)
|
|
if (deniedBy) {
|
|
return bareAgentJsonResult({
|
|
ok: false,
|
|
error: 'command_denied',
|
|
matched: deniedBy
|
|
})
|
|
}
|
|
const timeoutMs =
|
|
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' })
|
|
}
|
|
let cwd = typeof args.cwd === 'string' ? args.cwd.trim() : ''
|
|
if (!cwd && paths.lastCwd) {
|
|
try {
|
|
cwd = (await bareAgentReadTextFile(ctx, paths.lastCwd)).trim()
|
|
} catch {
|
|
cwd = ''
|
|
}
|
|
}
|
|
let line = command
|
|
if (cwd) {
|
|
if (!bareAgentPathAllowed(cwd)) {
|
|
return bareAgentJsonResult({ ok: false, error: 'cwd_not_allowed' })
|
|
}
|
|
line = 'cd ' + bareAgentShellQuote(cwd) + ' && ' + command
|
|
try {
|
|
if (paths.lastCwd) await bareAgentWriteTextFile(ctx, paths.lastCwd, cwd + '\n')
|
|
} catch {
|
|
/* optional */
|
|
}
|
|
}
|
|
appendProgress('run_command ' + line.slice(0, 160))
|
|
const r = await captureExec(line, timeoutMs, { captureExit })
|
|
return bareAgentJsonResult(
|
|
r.ok === false ? r : { ok: true, cwd: cwd || null, stdout_stderr: r.stdout_stderr }
|
|
)
|
|
}
|
|
|
|
if (toolName === 'run_js_script') {
|
|
const code = typeof args.code === 'string' ? args.code : ''
|
|
const scriptPath = paths.dir + '/_tmp_agent_run.mjs'
|
|
if (!vfs?.writeFile || !execLine) {
|
|
return bareAgentJsonResult({ ok: false, error: 'vfs or execLine' })
|
|
}
|
|
appendProgress('run_js_script (' + code.length + ' chars)')
|
|
const body =
|
|
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.from === 'function'
|
|
? ctx.b4a.from(code)
|
|
: new TextEncoder().encode(code)
|
|
await vfs.writeFile(scriptPath, body)
|
|
/** Absolute path → kernel-runner runs .mjs like `./script.mjs` (no host `node` binary). captureExec adds stdout redirect. */
|
|
const cmd = bareAgentShellQuote(scriptPath)
|
|
const r = await captureExec(cmd, 60000)
|
|
return bareAgentJsonResult(
|
|
r.ok === false ? r : { ok: true, stdout_stderr: r.stdout_stderr }
|
|
)
|
|
}
|
|
|
|
if (toolName === 'get_system_info') {
|
|
/** @type {Record<string, unknown>} */
|
|
const info = {}
|
|
try {
|
|
info.ctxApiVersion =
|
|
typeof ctx.ctxApiVersion === 'string'
|
|
? ctx.ctxApiVersion
|
|
: typeof ctx.ctxApiVersion === 'number'
|
|
? String(ctx.ctxApiVersion)
|
|
: undefined
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
const want =
|
|
typeof args.want === 'string' ? args.want : 'summary'
|
|
if (want === 'summary') {
|
|
info.discovery_hint =
|
|
'For live kernel feature flags use read_proc_file on /proc/bare_os/features (or features.json); apropos_man only searches man-page text, not runtime state. Otherwise 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')
|
|
if (b && b.length) {
|
|
info.capabilities_json =
|
|
typeof ctx.b4a !== 'undefined' &&
|
|
ctx.b4a &&
|
|
typeof ctx.b4a.toString === 'function'
|
|
? ctx.b4a.toString(b)
|
|
: String(new TextDecoder().decode(b))
|
|
}
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
if (want === 'swarm' && vfs?.readFile) {
|
|
try {
|
|
const b = await vfs.readFile('/proc/bare_os/swarm.json')
|
|
if (b && b.length) {
|
|
info.swarm =
|
|
typeof ctx.b4a !== 'undefined' &&
|
|
ctx.b4a &&
|
|
ctx.b4a.toString
|
|
? ctx.b4a.toString(b)
|
|
: String(new TextDecoder().decode(b))
|
|
}
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
try {
|
|
if (execLine) await execLine('uname -a > ' + bareAgentShellQuote(paths.cmdOut) + ' 2>&1')
|
|
if (vfs?.readFile) {
|
|
const buf = await vfs.readFile(paths.cmdOut)
|
|
if (buf && buf.length) {
|
|
info.uname =
|
|
typeof ctx.b4a !== 'undefined' &&
|
|
ctx.b4a &&
|
|
typeof ctx.b4a.toString === 'function'
|
|
? ctx.b4a.toString(buf).trim()
|
|
: String(new TextDecoder().decode(buf)).trim()
|
|
}
|
|
}
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
appendProgress('get_system_info ' + want)
|
|
return bareAgentJsonResult({ ok: true, want, info })
|
|
}
|
|
|
|
if (toolName === 'edit_agent_config') {
|
|
const patch = args.patch
|
|
if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {
|
|
return bareAgentJsonResult({ ok: false, error: 'bad patch' })
|
|
}
|
|
const merged = bareAgentMergeConfigPatch(configRef.current, patch)
|
|
configRef.current = merged
|
|
await bareAgentSaveConfigFromTools(ctx, paths, merged)
|
|
if (
|
|
Object.prototype.hasOwnProperty.call(patch, 'owner_name') ||
|
|
Object.prototype.hasOwnProperty.call(patch, 'agent_label')
|
|
) {
|
|
const workspace =
|
|
typeof paths.workspace === 'string'
|
|
? paths.workspace
|
|
: paths.dir + '/workspace'
|
|
await bareAgentSyncWorkspaceFromConfig(ctx, { workspace }, merged)
|
|
}
|
|
appendProgress('edit_agent_config')
|
|
return bareAgentJsonResult({ ok: true, saved: true })
|
|
}
|
|
|
|
if (toolName === 'list_bin') {
|
|
const limit =
|
|
typeof args.limit === 'number' && Number.isFinite(args.limit)
|
|
? Math.min(Math.floor(args.limit), 800)
|
|
: 400
|
|
if (!vfs || typeof vfs.readdir !== 'function') {
|
|
return bareAgentJsonResult({ ok: false, error: 'readdir unavailable' })
|
|
}
|
|
appendProgress('list_bin')
|
|
try {
|
|
const names = await vfs.readdir('/bin')
|
|
const arr = Array.isArray(names) ? [...names].slice(0, limit) : []
|
|
arr.sort()
|
|
return bareAgentJsonResult({
|
|
ok: true,
|
|
count: arr.length,
|
|
names: arr
|
|
})
|
|
} catch (e) {
|
|
const msg =
|
|
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
|
return bareAgentJsonResult({ ok: false, error: msg })
|
|
}
|
|
}
|
|
|
|
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 (Boolean(args.tree) && typeof bareAgentRenderTree === 'function') {
|
|
appendProgress('list_directory tree ' + dir)
|
|
const tree = await bareAgentRenderTree(ctx, dir, {
|
|
max: maxEnt,
|
|
maxDepth: args.max_depth
|
|
})
|
|
return bareAgentJsonResult(tree)
|
|
}
|
|
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, mutateDenyPrefixes) ||
|
|
!bareAgentPathAllowedMutate(to, mutateDenyPrefixes) ||
|
|
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: 'allow_delete is on by default; this home set it false'
|
|
})
|
|
}
|
|
if (reqTok && token !== reqTok) {
|
|
return bareAgentJsonResult({ ok: false, error: 'confirm_token_required' })
|
|
}
|
|
if (!bareAgentPathAllowedMutate(path, mutateDenyPrefixes) || path.includes('..')) {
|
|
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
|
}
|
|
if (!vfs) {
|
|
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
|
}
|
|
appendProgress('delete_path ' + path)
|
|
if (typeof bareAgentPushEdit === 'function' && paths.edits) {
|
|
try {
|
|
const prev = await bareAgentReadTextFile(ctx, path)
|
|
await bareAgentPushEdit(ctx, paths.edits, { path, prev, tool: 'delete_path' })
|
|
} catch {
|
|
/* missing */
|
|
}
|
|
}
|
|
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',
|
|
hint: 'read_proc_file accepts any /proc path'
|
|
})
|
|
}
|
|
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 === 'list_services') {
|
|
appendProgress('list_services')
|
|
if (!vfs || typeof vfs.readFile !== 'function') {
|
|
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
|
}
|
|
let readinessText = ''
|
|
/** @type {Record<string, unknown> | null} */
|
|
let readinessJson = null
|
|
try {
|
|
const b = await vfs.readFile('/proc/bare_os/initd_readiness.json')
|
|
if (b && b.length) {
|
|
readinessText =
|
|
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function'
|
|
? ctx.b4a.toString(b)
|
|
: String(new TextDecoder().decode(b))
|
|
try {
|
|
const parsed = JSON.parse(readinessText)
|
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
|
|
readinessJson = /** @type {Record<string, unknown>} */ (parsed)
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
const units = Array.isArray(readinessJson?.units)
|
|
? /** @type {unknown[]} */ (readinessJson.units)
|
|
: []
|
|
const rows = units
|
|
.filter((u) => u && typeof u === 'object')
|
|
.map((u) => {
|
|
const o = /** @type {Record<string, unknown>} */ (u)
|
|
return {
|
|
name: String(o.name || ''),
|
|
phase: String(o.phase || ''),
|
|
startedAtMs:
|
|
typeof o.startedAtMs === 'number' ? o.startedAtMs : undefined,
|
|
error: typeof o.error === 'string' ? o.error : undefined
|
|
}
|
|
})
|
|
.filter((r) => r.name)
|
|
return bareAgentJsonResult({
|
|
ok: true,
|
|
source: '/proc/bare_os/initd_readiness.json',
|
|
count: rows.length,
|
|
units: rows,
|
|
note:
|
|
rows.length > 0
|
|
? 'Runtime units from initd readiness snapshot.'
|
|
: 'No parsed readiness units available.'
|
|
})
|
|
}
|
|
|
|
if (toolName === 'service_status') {
|
|
const name = typeof args.name === 'string' ? args.name.trim() : ''
|
|
if (!name) return bareAgentJsonResult({ ok: false, error: 'name_required' })
|
|
appendProgress('service_status ' + name)
|
|
if (!vfs || typeof vfs.readFile !== 'function') {
|
|
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
|
}
|
|
try {
|
|
const b = await vfs.readFile('/proc/bare_os/initd_readiness.json')
|
|
const t =
|
|
b && b.length
|
|
? typeof ctx.b4a !== 'undefined' &&
|
|
ctx.b4a &&
|
|
typeof ctx.b4a.toString === 'function'
|
|
? ctx.b4a.toString(b)
|
|
: String(new TextDecoder().decode(b))
|
|
: ''
|
|
const parsed = JSON.parse(t)
|
|
const units = Array.isArray(parsed?.units) ? parsed.units : []
|
|
const hit =
|
|
units.find((u) => u && typeof u === 'object' && String(u.name || '') === name) ||
|
|
null
|
|
if (!hit) {
|
|
return bareAgentJsonResult({
|
|
ok: false,
|
|
error: 'not_found',
|
|
source: '/proc/bare_os/initd_readiness.json'
|
|
})
|
|
}
|
|
const o = /** @type {Record<string, unknown>} */ (hit)
|
|
return bareAgentJsonResult({
|
|
ok: true,
|
|
status: {
|
|
name: String(o.name || ''),
|
|
phase: String(o.phase || ''),
|
|
startedAtMs:
|
|
typeof o.startedAtMs === 'number' ? o.startedAtMs : undefined,
|
|
error: typeof o.error === 'string' ? o.error : undefined
|
|
},
|
|
journal_hint: '/run/bare-os/unit-journal/' + name + '.ndjson'
|
|
})
|
|
} catch (e) {
|
|
const msg =
|
|
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
|
return bareAgentJsonResult({ ok: false, error: msg })
|
|
}
|
|
}
|
|
|
|
if (toolName === 'list_timers') {
|
|
const includePreview = Boolean(args.include_preview)
|
|
const maxEntries =
|
|
typeof args.max_entries === 'number' && Number.isFinite(args.max_entries)
|
|
? Math.min(Math.max(Math.floor(args.max_entries), 1), 512)
|
|
: 128
|
|
appendProgress('list_timers')
|
|
if (!vfs || typeof vfs.readdir !== 'function' || typeof vfs.readFile !== 'function') {
|
|
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
|
}
|
|
const dir = home + '/.config/bare-os/timers'
|
|
let names = []
|
|
try {
|
|
names = await vfs.readdir(dir)
|
|
} catch (e) {
|
|
const msg =
|
|
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
|
return bareAgentJsonResult({ ok: false, error: msg, path: dir })
|
|
}
|
|
const timerNames = names
|
|
.filter((n) => typeof n === 'string' && n.endsWith('.timer'))
|
|
.sort()
|
|
.slice(0, maxEntries)
|
|
/** @type {unknown[]} */
|
|
const timers = []
|
|
for (const name of timerNames) {
|
|
const path = dir + '/' + name
|
|
/** @type {Record<string, unknown>} */
|
|
const row = { name, path }
|
|
if (includePreview) {
|
|
try {
|
|
const b = await vfs.readFile(path)
|
|
const txt =
|
|
b && b.length
|
|
? typeof ctx.b4a !== 'undefined' &&
|
|
ctx.b4a &&
|
|
typeof ctx.b4a.toString === 'function'
|
|
? ctx.b4a.toString(b)
|
|
: String(new TextDecoder().decode(b))
|
|
: ''
|
|
row.preview = bareAgentTruncateChars(txt, 1200)
|
|
} catch (e) {
|
|
row.preview_error =
|
|
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
|
}
|
|
}
|
|
timers.push(row)
|
|
}
|
|
return bareAgentJsonResult({
|
|
ok: true,
|
|
path: dir,
|
|
count: timers.length,
|
|
timers
|
|
})
|
|
}
|
|
|
|
if (toolName === 'read_cron_log' || toolName === 'read_audit_log') {
|
|
const maxChars =
|
|
typeof args.max_chars === 'number' && Number.isFinite(args.max_chars)
|
|
? Math.min(Math.max(Math.floor(args.max_chars), 200), 80_000)
|
|
: 12_000
|
|
const tailOnly = Boolean(args.tail_only)
|
|
const redact = toolName === 'read_audit_log' ? args.redact !== false : false
|
|
const path =
|
|
toolName === 'read_audit_log'
|
|
? '/var/log/bare-os/audit.log'
|
|
: '/var/log/bare-os/cron.log'
|
|
appendProgress(toolName + ' ' + path)
|
|
if (!vfs || typeof vfs.readFile !== 'function') {
|
|
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
|
}
|
|
try {
|
|
const b = await vfs.readFile(path)
|
|
if (!b || !b.length) return bareAgentJsonResult({ ok: false, error: 'empty_or_missing' })
|
|
let t =
|
|
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function'
|
|
? ctx.b4a.toString(b)
|
|
: String(new TextDecoder().decode(b))
|
|
if (redact) {
|
|
t = t
|
|
.replace(/\bBearer\s+\S+/gi, 'Bearer <redacted>')
|
|
.replace(/\bsk-[a-zA-Z0-9]{16,}\b/g, '<redacted_sk>')
|
|
.replace(/\b([A-Z0-9_]*(KEY|TOKEN|SECRET|PASS)[A-Z0-9_]*)\s*=\s*([^\s]+)/gi, '$1=<redacted>')
|
|
}
|
|
const out = tailOnly ? t.slice(-maxChars) : bareAgentTruncateChars(t, maxChars)
|
|
return bareAgentJsonResult({
|
|
ok: true,
|
|
path,
|
|
text: out,
|
|
truncated: t.length > out.length
|
|
})
|
|
} 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_boot_policy' || toolName === 'read_kernel_extension_resolution') {
|
|
const maxChars =
|
|
typeof args.max_chars === 'number' && Number.isFinite(args.max_chars)
|
|
? Math.min(Math.max(Math.floor(args.max_chars), 200), 120_000)
|
|
: 20_000
|
|
const path =
|
|
toolName === 'read_boot_policy'
|
|
? '/etc/bare-os/boot.policy.json'
|
|
: '/run/bare-os/kernel-ext-resolution.json'
|
|
appendProgress(toolName + ' ' + path)
|
|
if (!vfs || typeof vfs.readFile !== 'function') {
|
|
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
|
}
|
|
try {
|
|
const b = await vfs.readFile(path)
|
|
if (!b || !b.length) return bareAgentJsonResult({ ok: false, error: 'empty_or_missing' })
|
|
const t =
|
|
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function'
|
|
? ctx.b4a.toString(b)
|
|
: String(new TextDecoder().decode(b))
|
|
/** @type {unknown} */
|
|
let json = null
|
|
try {
|
|
json = JSON.parse(t)
|
|
} catch {
|
|
json = null
|
|
}
|
|
return bareAgentJsonResult({
|
|
ok: true,
|
|
path,
|
|
text: bareAgentTruncateChars(t, maxChars),
|
|
json
|
|
})
|
|
} 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_initd_graph') {
|
|
appendProgress('get_initd_graph')
|
|
if (!vfs || typeof vfs.readFile !== 'function') {
|
|
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
|
}
|
|
/** @type {Record<string, unknown>} */
|
|
const out = { ok: true }
|
|
for (const p of ['/proc/bare_os/initd_dag.json', '/proc/bare_os/initd_readiness.json']) {
|
|
try {
|
|
const b = await vfs.readFile(p)
|
|
if (!b || !b.length) continue
|
|
const t =
|
|
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function'
|
|
? ctx.b4a.toString(b)
|
|
: String(new TextDecoder().decode(b))
|
|
try {
|
|
out[p] = JSON.parse(t)
|
|
} catch {
|
|
out[p] = bareAgentTruncateChars(t, 8000)
|
|
}
|
|
} catch {
|
|
/* ignore missing */
|
|
}
|
|
}
|
|
return bareAgentJsonResult(out)
|
|
}
|
|
|
|
if (toolName === 'read_unit_journal') {
|
|
const unit = typeof args.unit === 'string' ? args.unit.trim() : ''
|
|
if (!/^[a-zA-Z0-9._-]{1,96}$/.test(unit)) {
|
|
return bareAgentJsonResult({ ok: false, error: 'invalid_unit' })
|
|
}
|
|
const maxChars =
|
|
typeof args.max_chars === 'number' && Number.isFinite(args.max_chars)
|
|
? Math.min(Math.max(Math.floor(args.max_chars), 200), 120_000)
|
|
: 12_000
|
|
const tailOnly = Boolean(args.tail_only)
|
|
const p = '/run/bare-os/unit-journal/' + unit + '.ndjson'
|
|
appendProgress('read_unit_journal ' + unit)
|
|
return bareAgentJsonResult(await readBoundedText(p, maxChars, tailOnly, true))
|
|
}
|
|
|
|
if (toolName === 'inspect_ipc_backpressure') {
|
|
appendProgress('inspect_ipc_backpressure')
|
|
if (!vfs || typeof vfs.readFile !== 'function') {
|
|
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
|
}
|
|
const candidates = [
|
|
'/proc/bare_os/ipc_backpressure.json',
|
|
'/proc/bare_os/replication_operator_sketch.json',
|
|
'/proc/bare_os/metrics_live.json'
|
|
]
|
|
/** @type {Record<string, unknown>} */
|
|
const out = { ok: true, sources: [] }
|
|
for (const p of candidates) {
|
|
try {
|
|
const b = await vfs.readFile(p)
|
|
if (!b || !b.length) continue
|
|
const t =
|
|
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function'
|
|
? ctx.b4a.toString(b)
|
|
: String(new TextDecoder().decode(b))
|
|
out.sources.push(p)
|
|
try {
|
|
out[p] = JSON.parse(t)
|
|
} catch {
|
|
out[p] = bareAgentTruncateChars(t, 6000)
|
|
}
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
return bareAgentJsonResult(out)
|
|
}
|
|
|
|
if (toolName === 'get_network_summary') {
|
|
appendProgress('get_network_summary')
|
|
if (!vfs || typeof vfs.readFile !== 'function') {
|
|
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
|
}
|
|
const candidates = [
|
|
'/proc/bare_os/net_summary.json',
|
|
'/proc/bare_os/swarm.json',
|
|
'/proc/bare_os/swarm_status.json',
|
|
'/proc/bare_os/swarm_connection_manager_status.json'
|
|
]
|
|
/** @type {Record<string, unknown>} */
|
|
const out = { ok: true, sources: [] }
|
|
for (const p of candidates) {
|
|
try {
|
|
const b = await vfs.readFile(p)
|
|
if (!b || !b.length) continue
|
|
const t =
|
|
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function'
|
|
? ctx.b4a.toString(b)
|
|
: String(new TextDecoder().decode(b))
|
|
out.sources.push(p)
|
|
try {
|
|
out[p] = JSON.parse(t)
|
|
} catch {
|
|
out[p] = bareAgentTruncateChars(t, 6000)
|
|
}
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
return bareAgentJsonResult(out)
|
|
}
|
|
|
|
if (toolName === 'tail_telemetry_streams') {
|
|
const maxChars =
|
|
typeof args.max_chars === 'number' && Number.isFinite(args.max_chars)
|
|
? Math.min(Math.max(Math.floor(args.max_chars), 200), 60_000)
|
|
: 8000
|
|
appendProgress('tail_telemetry_streams')
|
|
const pathsToRead = [
|
|
'/var/log/bare-os/audit.log',
|
|
'/var/log/bare-os/logger.jsonl',
|
|
'/var/log/bare-os/initd.log',
|
|
'/var/log/bare-os/cron.log'
|
|
]
|
|
/** @type {Record<string, unknown>} */
|
|
const out = { ok: true, streams: {} }
|
|
for (const p of pathsToRead) {
|
|
out.streams[p] = await readBoundedText(p, maxChars, true, true)
|
|
}
|
|
return bareAgentJsonResult(out)
|
|
}
|
|
|
|
if (toolName === 'pkg_index_lookup') {
|
|
const key = typeof args.key === 'string' ? args.key.trim() : ''
|
|
if (!key) return bareAgentJsonResult({ ok: false, error: 'key_required' })
|
|
appendProgress('pkg_index_lookup ' + key.slice(0, 80))
|
|
if (!execLine) return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' })
|
|
const cmd = 'pkg-swarm-index get --key ' + bareAgentShellQuote(key)
|
|
const r = await captureExec(cmd, 90000)
|
|
if (r.ok === false) return bareAgentJsonResult(r)
|
|
const txt = typeof r.stdout_stderr === 'string' ? r.stdout_stderr : ''
|
|
let json = null
|
|
try {
|
|
json = JSON.parse(txt)
|
|
} catch {
|
|
json = null
|
|
}
|
|
return bareAgentJsonResult({ ok: true, key, json, text: bareAgentTruncateChars(txt, 12000) })
|
|
}
|
|
|
|
if (toolName === 'list_verification_scripts') {
|
|
appendProgress('list_verification_scripts')
|
|
if (!vfs || typeof vfs.readdir !== 'function') {
|
|
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
|
}
|
|
const out = []
|
|
for (const dir of ['/scripts', '/home/guest/scripts']) {
|
|
try {
|
|
const names = await vfs.readdir(dir)
|
|
const rows = names
|
|
.filter((n) => typeof n === 'string' && (n.endsWith('.mjs') || n.endsWith('.js')))
|
|
.sort()
|
|
.slice(0, 400)
|
|
.map((n) => dir + '/' + n)
|
|
out.push(...rows)
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
return bareAgentJsonResult({ ok: true, scripts: out })
|
|
}
|
|
|
|
if (toolName === 'run_maintenance_gate') {
|
|
const command = typeof args.command === 'string' ? args.command.trim() : ''
|
|
const timeoutMs =
|
|
typeof args.timeout_ms === 'number' && Number.isFinite(args.timeout_ms)
|
|
? Math.min(Math.max(Math.floor(args.timeout_ms), 1000), 900000)
|
|
: 180000
|
|
appendProgress('run_maintenance_gate ' + command)
|
|
const allow = {
|
|
'verify-kernel-seeder-parity': 'node scripts/verify-kernel-seeder-parity.mjs',
|
|
'verify-man-coverage': 'node scripts/verify-man-coverage.mjs',
|
|
'verify-ctx-api-feature-bits': 'node scripts/verify-ctx-api-feature-bits.mjs',
|
|
'coreutils-test': 'npm test -w bare-os-coreutils'
|
|
}
|
|
if (!Object.prototype.hasOwnProperty.call(allow, command)) {
|
|
return bareAgentJsonResult({ ok: false, error: 'command_not_allowlisted', allowlist: Object.keys(allow) })
|
|
}
|
|
if (!execLine) return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' })
|
|
const cmd = /** @type {Record<string, string>} */ (allow)[command]
|
|
const r = await captureExec(cmd, timeoutMs, { captureExit: true })
|
|
return bareAgentJsonResult(r)
|
|
}
|
|
|
|
if (toolName === 'run_contract_checks') {
|
|
const profile = typeof args.profile === 'string' ? args.profile.trim() : ''
|
|
appendProgress('run_contract_checks ' + profile)
|
|
const mapping = {
|
|
core: 'node scripts/verify-kernel-seeder-parity.mjs && node scripts/verify-man-coverage.mjs',
|
|
docs: 'node scripts/verify-doc-links.mjs && node scripts/verify-doc-contracts.mjs',
|
|
parity: 'node scripts/verify-kernel-seeder-parity.mjs'
|
|
}
|
|
if (!Object.prototype.hasOwnProperty.call(mapping, profile)) {
|
|
return bareAgentJsonResult({ ok: false, error: 'unknown_profile', profiles: Object.keys(mapping) })
|
|
}
|
|
if (!execLine) return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' })
|
|
const cmd = /** @type {Record<string, string>} */ (mapping)[profile]
|
|
const r = await captureExec(cmd, 300000, { captureExit: true })
|
|
return bareAgentJsonResult(r)
|
|
}
|
|
|
|
if (toolName === 'summarize_build_drift') {
|
|
appendProgress('summarize_build_drift')
|
|
if (!execLine) return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' })
|
|
const r = await captureExec('git status --short', 60000)
|
|
return bareAgentJsonResult(r)
|
|
}
|
|
|
|
if (toolName === 'get_hrpc_bridge_health') {
|
|
appendProgress('get_hrpc_bridge_health')
|
|
/** @type {Record<string, unknown>} */
|
|
const out = {
|
|
ok: true,
|
|
hostCapabilities: {
|
|
hrpcBridge:
|
|
typeof ctx.bareOsHostCapability === 'function'
|
|
? Boolean(ctx.bareOsHostCapability('hrpcBridge'))
|
|
: false
|
|
}
|
|
}
|
|
if (vfs && typeof vfs.readFile === 'function') {
|
|
for (const p of ['/proc/bare_os/hrpc_route_table.json', '/proc/bare_os/hrpc_health.json']) {
|
|
try {
|
|
const b = await vfs.readFile(p)
|
|
if (!b || !b.length) continue
|
|
const t =
|
|
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function'
|
|
? ctx.b4a.toString(b)
|
|
: String(new TextDecoder().decode(b))
|
|
try {
|
|
out[p] = JSON.parse(t)
|
|
} catch {
|
|
out[p] = bareAgentTruncateChars(t, 6000)
|
|
}
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
}
|
|
return bareAgentJsonResult(out)
|
|
}
|
|
|
|
if (toolName === 'get_hrpc_allowlist_status') {
|
|
appendProgress('get_hrpc_allowlist_status')
|
|
if (!execLine) return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' })
|
|
const r = await captureExec('hrpc probe', 60000)
|
|
return bareAgentJsonResult(r)
|
|
}
|
|
|
|
if (toolName === 'emit_host_notification' || toolName === 'request_host_action') {
|
|
const cfg = configRef.current || {}
|
|
if (cfg && cfg.emergency_stop_mutations) {
|
|
return bareAgentJsonResult({ ok: false, error: 'emergency_stop_mutations_enabled' })
|
|
}
|
|
if (toolName === 'emit_host_notification' && !cfg.allow_host_notifications) {
|
|
return bareAgentJsonResult({ ok: false, error: 'host_notifications_disabled' })
|
|
}
|
|
if (toolName === 'request_host_action' && !cfg.allow_host_actions) {
|
|
return bareAgentJsonResult({ ok: false, error: 'host_actions_disabled' })
|
|
}
|
|
if (!cfg.allow_bridge_mutations) {
|
|
return bareAgentJsonResult({ ok: false, error: 'bridge_mutations_disabled' })
|
|
}
|
|
if (typeof ctx.bareOsHrpcRequest !== 'function') {
|
|
return bareAgentJsonResult({ ok: false, error: 'bareOsHrpcRequest unavailable' })
|
|
}
|
|
try {
|
|
if (toolName === 'emit_host_notification') {
|
|
const payload = {
|
|
title: String(args.title || '').slice(0, 200),
|
|
message: String(args.message || '').slice(0, 2000),
|
|
level: typeof args.level === 'string' ? args.level : 'info'
|
|
}
|
|
appendProgress('emit_host_notification ' + payload.title)
|
|
const res = await ctx.bareOsHrpcRequest('bare_os', 'host_notify', payload)
|
|
return bareAgentJsonResult({ ok: true, result: res })
|
|
}
|
|
const action = String(args.action || '').trim()
|
|
if (!/^[a-zA-Z0-9._-]{1,64}$/.test(action)) {
|
|
return bareAgentJsonResult({ ok: false, error: 'invalid_action' })
|
|
}
|
|
const payload =
|
|
args.payload && typeof args.payload === 'object' && !Array.isArray(args.payload)
|
|
? args.payload
|
|
: {}
|
|
appendProgress('request_host_action ' + action)
|
|
const res = await ctx.bareOsHrpcRequest('bare_os', 'host_action', {
|
|
action,
|
|
payload
|
|
})
|
|
return bareAgentJsonResult({ ok: true, result: res })
|
|
} 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 }
|
|
)
|
|
}
|
|
|
|
if (toolName === 'verification_hints') {
|
|
const topic = typeof args.topic === 'string' ? args.topic : ''
|
|
const pathsTouch =
|
|
typeof args.paths_touched === 'string' ? args.paths_touched : ''
|
|
appendProgress('verification_hints')
|
|
const combined = topic + ' ' + pathsTouch.replace(/,/g, ' ')
|
|
const hints = bareAgentVerificationHintsList(combined)
|
|
return bareAgentJsonResult({
|
|
ok: true,
|
|
scope_note:
|
|
'Suggested commands apply to the Bare OS git checkout on the host (npm/node at repo root). They do not run automatically.',
|
|
suggested_commands: hints
|
|
})
|
|
}
|
|
|
|
if (toolName === 'runtime_diagnostic_bundle') {
|
|
appendProgress('runtime_diagnostic_bundle')
|
|
/** @type {Record<string, unknown>} */
|
|
const bundle = {}
|
|
bundle.bareOsCtxApiVersion =
|
|
typeof ctx.bareOsCtxApiVersion !== 'undefined' ? ctx.bareOsCtxApiVersion : null
|
|
try {
|
|
if (typeof ctx.bareOsGetResourceStatus === 'function')
|
|
bundle.resources = ctx.bareOsGetResourceStatus()
|
|
} catch {
|
|
bundle.resources_error = true
|
|
}
|
|
/** @type {Record<string, unknown>} */
|
|
const procParts = {}
|
|
/** @type {string[]} */
|
|
let list = [...BARE_AGENT_PROC_READ_ALLOWLIST]
|
|
if (vfs && typeof vfs.readdir === 'function') {
|
|
try {
|
|
const names = await vfs.readdir('/proc/bare_os')
|
|
if (Array.isArray(names)) {
|
|
for (let i = 0; i < names.length; i++) {
|
|
const n = String(names[i] || '')
|
|
if (!n || n === '.' || n === '..') continue
|
|
const full = '/proc/bare_os/' + n
|
|
if (list.indexOf(full) === -1) list.push(full)
|
|
}
|
|
}
|
|
} catch {
|
|
/* keep seed list */
|
|
}
|
|
}
|
|
if (vfs && typeof vfs.readFile === 'function') {
|
|
for (let i = 0; i < list.length; i++) {
|
|
const procPath = list[i]
|
|
try {
|
|
const buf = await vfs.readFile(procPath)
|
|
if (!buf || !buf.length) continue
|
|
let t =
|
|
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function'
|
|
? ctx.b4a.toString(buf)
|
|
: String(new TextDecoder().decode(buf))
|
|
if (t.length > 80_000) t = t.slice(0, 80_000) + '\n… truncated'
|
|
try {
|
|
procParts[procPath] = JSON.parse(t)
|
|
} catch {
|
|
procParts[procPath] = t
|
|
}
|
|
} catch {
|
|
/* missing path */
|
|
}
|
|
}
|
|
}
|
|
bundle.proc = procParts
|
|
return bareAgentJsonResult({ ok: true, bundle })
|
|
}
|
|
|
|
if (toolName === 'web_fetch') {
|
|
const url = typeof args.url === 'string' ? args.url : ''
|
|
let hostHint = ''
|
|
try {
|
|
hostHint = new URL(url).hostname
|
|
} catch {
|
|
hostHint = ''
|
|
}
|
|
appendProgress('web_fetch ' + (hostHint || url.slice(0, 80)))
|
|
try {
|
|
const out = await bareWebRunTool({
|
|
ctx,
|
|
url,
|
|
method: typeof args.method === 'string' ? args.method : undefined,
|
|
headers:
|
|
args.headers &&
|
|
typeof args.headers === 'object' &&
|
|
!Array.isArray(args.headers)
|
|
? /** @type {Record<string, unknown>} */ (args.headers)
|
|
: undefined,
|
|
body: typeof args.body === 'string' ? args.body : undefined,
|
|
content_type:
|
|
typeof args.content_type === 'string' ? args.content_type : undefined,
|
|
max_response_bytes:
|
|
typeof args.max_response_bytes === 'number'
|
|
? args.max_response_bytes
|
|
: undefined,
|
|
max_redirects:
|
|
typeof args.max_redirects === 'number' ? args.max_redirects : undefined,
|
|
timeout_ms:
|
|
typeof args.timeout_ms === 'number' ? args.timeout_ms : undefined,
|
|
format: typeof args.format === 'string' ? args.format : undefined,
|
|
max_links:
|
|
typeof args.max_links === 'number' ? args.max_links : undefined,
|
|
signal
|
|
})
|
|
return bareAgentJsonResult(out)
|
|
} catch (e) {
|
|
const msg = bareWebFmtErr(e)
|
|
return bareAgentJsonResult({ ok: false, error: msg })
|
|
}
|
|
}
|
|
|
|
if (toolName === 'grep') {
|
|
const pattern = typeof args.pattern === 'string' ? args.pattern : ''
|
|
if (!pattern) return bareAgentJsonResult({ ok: false, error: 'pattern_required' })
|
|
if (typeof bareAgentGrepFiles !== 'function') {
|
|
return bareAgentJsonResult({ ok: false, error: 'grep_unavailable' })
|
|
}
|
|
const root =
|
|
(typeof args.path === 'string' && args.path.trim()) ||
|
|
(typeof args.root === 'string' && args.root.trim()) ||
|
|
home ||
|
|
'/home'
|
|
if (!bareAgentPathAllowed(root)) {
|
|
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
|
}
|
|
appendProgress('grep ' + pattern + ' @ ' + root)
|
|
const out = await bareAgentGrepFiles(ctx, {
|
|
pattern,
|
|
root,
|
|
glob: typeof args.glob === 'string' ? args.glob : '',
|
|
ignore_case: Boolean(args.ignore_case),
|
|
before: args.before,
|
|
after: args.after,
|
|
context: args.context,
|
|
max_matches: args.max_matches,
|
|
files_with_matches: Boolean(args.files_with_matches),
|
|
output_mode: typeof args.output_mode === 'string' ? args.output_mode : ''
|
|
})
|
|
return bareAgentJsonResult(out)
|
|
}
|
|
|
|
if (toolName === 'apply_patch') {
|
|
const patch =
|
|
(typeof args.patch === 'string' && args.patch) ||
|
|
(typeof args.input === 'string' && args.input) ||
|
|
''
|
|
if (!patch.trim()) return bareAgentJsonResult({ ok: false, error: 'patch_required' })
|
|
if (typeof bareAgentParseApplyPatch !== 'function') {
|
|
return bareAgentJsonResult({ ok: false, error: 'apply_patch_unavailable' })
|
|
}
|
|
const parsed = bareAgentParseApplyPatch(patch)
|
|
if (!parsed.ok) return bareAgentJsonResult(parsed)
|
|
appendProgress('apply_patch ops=' + String((parsed.ops || []).length))
|
|
if (typeof bareAgentPushEdit === 'function' && paths.edits && parsed.ops) {
|
|
for (let i = 0; i < parsed.ops.length; i++) {
|
|
const op = parsed.ops[i]
|
|
const p = String((op && op.path) || '')
|
|
if (!p) continue
|
|
try {
|
|
const prev = await bareAgentReadTextFile(ctx, p)
|
|
await bareAgentPushEdit(ctx, paths.edits, { path: p, prev, tool: 'apply_patch' })
|
|
} catch {
|
|
/* new file */
|
|
}
|
|
}
|
|
}
|
|
const out = await bareAgentApplyPatchOps(ctx, parsed.ops, {
|
|
home,
|
|
denyPrefixes: mutateDenyPrefixes
|
|
})
|
|
return bareAgentJsonResult(out)
|
|
}
|
|
|
|
if (toolName === 'update_goal') {
|
|
const message = typeof args.message === 'string' ? args.message.trim() : ''
|
|
const blocked =
|
|
typeof args.blocked_reason === 'string' ? args.blocked_reason.trim() : ''
|
|
const completed = Boolean(args.completed)
|
|
const next = { ...(configRef.current || {}) }
|
|
if (message) next.autonomous_last_error = ''
|
|
if (blocked) {
|
|
next.autonomous_status = 'blocked'
|
|
next.autonomous_last_error = blocked
|
|
next.autonomous_active = false
|
|
} else if (completed) {
|
|
next.autonomous_status = 'completed'
|
|
next.autonomous_active = false
|
|
if (message) next.autonomous_goal = String(next.autonomous_goal || '')
|
|
} else {
|
|
next.autonomous_status = 'running'
|
|
}
|
|
configRef.current = next
|
|
if (typeof bareAgentSaveConfigFromTools === 'function') {
|
|
await bareAgentSaveConfigFromTools(ctx, paths, next)
|
|
}
|
|
appendProgress(
|
|
'update_goal ' +
|
|
(completed ? 'completed' : blocked ? 'blocked' : 'progress')
|
|
)
|
|
return bareAgentJsonResult({
|
|
ok: true,
|
|
completed,
|
|
blocked: Boolean(blocked),
|
|
message: message || null,
|
|
blocked_reason: blocked || null,
|
|
status: next.autonomous_status
|
|
})
|
|
}
|
|
|
|
if (toolName === 'web_search') {
|
|
const query = typeof args.query === 'string' ? args.query.trim() : ''
|
|
if (!query) return bareAgentJsonResult({ ok: false, error: 'query_required' })
|
|
const maxResults =
|
|
typeof args.max_results === 'number' && Number.isFinite(args.max_results)
|
|
? Math.min(Math.max(Math.floor(args.max_results), 1), 16)
|
|
: 8
|
|
const url =
|
|
'https://api.duckduckgo.com/?q=' +
|
|
encodeURIComponent(query) +
|
|
'&format=json&no_html=1&skip_disambig=1'
|
|
appendProgress('web_search ' + query.slice(0, 80))
|
|
try {
|
|
const raw = await bareWebRunTool({
|
|
ctx,
|
|
url,
|
|
format: 'json',
|
|
timeout_ms: 20000,
|
|
max_response_bytes: 200000,
|
|
signal
|
|
})
|
|
if (!raw || raw.ok === false) {
|
|
return bareAgentJsonResult({
|
|
ok: false,
|
|
error: (raw && raw.error) || 'web_search_failed',
|
|
hint: 'HTTP policy may block api.duckduckgo.com; try web_fetch on a known URL'
|
|
})
|
|
}
|
|
let payload = raw.extract && raw.extract.json
|
|
if (payload == null && raw.extract && typeof raw.extract.text_slice === 'string') {
|
|
try {
|
|
payload = JSON.parse(raw.extract.text_slice)
|
|
} catch {
|
|
payload = null
|
|
}
|
|
}
|
|
const results =
|
|
typeof bareAgentParseSearchResults === 'function'
|
|
? bareAgentParseSearchResults(payload, maxResults)
|
|
: []
|
|
return bareAgentJsonResult({
|
|
ok: true,
|
|
query,
|
|
results,
|
|
abstract: payload && payload.Abstract ? String(payload.Abstract) : '',
|
|
count: results.length
|
|
})
|
|
} catch (e) {
|
|
const msg = typeof bareWebFmtErr === 'function' ? bareWebFmtErr(e) : String(e)
|
|
return bareAgentJsonResult({ ok: false, error: msg })
|
|
}
|
|
}
|
|
|
|
if (toolName === 'git_status') {
|
|
const cwd =
|
|
(typeof args.cwd === 'string' && args.cwd.trim()) ||
|
|
(ctx.env && typeof ctx.env === 'object'
|
|
? String(ctx.env.PWD || ctx.env.CWD || home || '').trim()
|
|
: '') ||
|
|
home ||
|
|
'/home'
|
|
if (!execLine) {
|
|
return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' })
|
|
}
|
|
appendProgress('git_status ' + cwd)
|
|
const cmd =
|
|
'git -C ' +
|
|
bareAgentShellQuote(cwd) +
|
|
' status --short && echo --- && git -C ' +
|
|
bareAgentShellQuote(cwd) +
|
|
' diff --stat && echo --- && git -C ' +
|
|
bareAgentShellQuote(cwd) +
|
|
' log --oneline -8'
|
|
const r = await captureExec(cmd, 30000)
|
|
return bareAgentJsonResult(
|
|
r.ok === false
|
|
? r
|
|
: { ok: true, cwd, stdout_stderr: r.stdout_stderr }
|
|
)
|
|
}
|
|
|
|
if (toolName === 'memory_append') {
|
|
const text = typeof args.text === 'string' ? args.text.trim() : ''
|
|
if (!text) return bareAgentJsonResult({ ok: false, error: 'text_required' })
|
|
const kindRaw = String(args.kind || 'NOTE').trim().toUpperCase()
|
|
const kind =
|
|
kindRaw === 'FACT' ||
|
|
kindRaw === 'CHECK' ||
|
|
kindRaw === 'RISK' ||
|
|
kindRaw === 'HANDOFF' ||
|
|
kindRaw === 'NOTE'
|
|
? kindRaw
|
|
: 'NOTE'
|
|
const workspace =
|
|
typeof paths.workspace === 'string' ? paths.workspace : paths.dir + '/workspace'
|
|
const memDir =
|
|
typeof paths.workspaceMemory === 'string'
|
|
? paths.workspaceMemory
|
|
: workspace + '/memory'
|
|
const dest = args.daily
|
|
? memDir +
|
|
'/' +
|
|
(typeof bareAgentWorkspaceUtcYmd === 'function'
|
|
? bareAgentWorkspaceUtcYmd()
|
|
: new Date().toISOString().slice(0, 10)) +
|
|
'.md'
|
|
: workspace + '/MEMORY.md'
|
|
const stamp = new Date().toISOString()
|
|
const line = '- ' + kind + ' — ' + text.replace(/\s+/g, ' ').trim()
|
|
let prev = ''
|
|
try {
|
|
prev = await bareAgentReadTextFile(ctx, dest)
|
|
} catch {
|
|
prev = ''
|
|
}
|
|
const next =
|
|
(prev ? prev.replace(/\s*$/, '') + '\n' : '# Memory\n\n') +
|
|
line +
|
|
' \n _' +
|
|
stamp +
|
|
'_\n'
|
|
await bareAgentWriteTextFile(ctx, dest, next)
|
|
appendProgress('memory_append ' + dest)
|
|
return bareAgentJsonResult({ ok: true, path: dest, kind, appended: line })
|
|
}
|
|
|
|
if (toolName === 'list_skills') {
|
|
const max =
|
|
typeof args.max === 'number' && Number.isFinite(args.max)
|
|
? Math.min(Math.max(Math.floor(args.max), 1), 80)
|
|
: 40
|
|
if (typeof bareAgentDiscoverSkills !== 'function') {
|
|
return bareAgentJsonResult({ ok: false, error: 'skills_unavailable' })
|
|
}
|
|
const skillPaths = {
|
|
workspaceSkills:
|
|
typeof paths.workspaceSkills === 'string'
|
|
? paths.workspaceSkills
|
|
: paths.dir + '/workspace/skills',
|
|
skillsGlobal:
|
|
typeof paths.skillsGlobal === 'string'
|
|
? paths.skillsGlobal
|
|
: paths.dir + '/skills'
|
|
}
|
|
const skills = await bareAgentDiscoverSkills(ctx, skillPaths)
|
|
appendProgress('list_skills ' + String(skills.length))
|
|
return bareAgentJsonResult({
|
|
ok: true,
|
|
count: Math.min(skills.length, max),
|
|
truncated: skills.length > max,
|
|
skills: skills.slice(0, max)
|
|
})
|
|
}
|
|
|
|
if (toolName === 'schedule_task') {
|
|
const prompt = typeof args.prompt === 'string' ? args.prompt.trim() : ''
|
|
if (!prompt) return bareAgentJsonResult({ ok: false, error: 'prompt_required' })
|
|
const parsed =
|
|
typeof bareAgentParseScheduleInterval === 'function'
|
|
? bareAgentParseScheduleInterval(args.interval)
|
|
: { error: 'schedule_unavailable' }
|
|
if (parsed.error) return bareAgentJsonResult({ ok: false, error: parsed.error, hint: parsed.hint })
|
|
const id =
|
|
typeof bareAgentScheduleId === 'function'
|
|
? bareAgentScheduleId(args.id || prompt.slice(0, 24))
|
|
: 'agent-task'
|
|
const dir = (home || '/home') + '/.config/bare-os/timers'
|
|
if (!vfs || typeof vfs.readdir !== 'function') {
|
|
return bareAgentJsonResult({ ok: false, error: 'vfs unavailable' })
|
|
}
|
|
try {
|
|
if (typeof vfs.mkdir === 'function') await vfs.mkdir(dir, { recursive: true })
|
|
} catch {
|
|
/* exists */
|
|
}
|
|
let names = []
|
|
try {
|
|
names = await vfs.readdir(dir)
|
|
} catch {
|
|
names = []
|
|
}
|
|
const existing = Array.isArray(names)
|
|
? names.filter(function (n) {
|
|
return typeof n === 'string' && n.endsWith('.timer')
|
|
})
|
|
: []
|
|
const dest = dir + '/' + id + '.timer'
|
|
const already = existing.indexOf(id + '.timer') >= 0
|
|
if (!already && existing.length >= 8) {
|
|
return bareAgentJsonResult({
|
|
ok: false,
|
|
error: 'timer_limit',
|
|
max: 8,
|
|
hint: 'unschedule_task an existing id first'
|
|
})
|
|
}
|
|
const quoted = bareAgentShellQuote(prompt)
|
|
const line = args.auto === false ? 'agent ' + quoted : 'agent --auto ' + quoted
|
|
const body =
|
|
parsed.kind === 'everyMs'
|
|
? '[Timer]\nEveryMs=' + String(parsed.everyMs) + '\nExecLine=' + line + '\n'
|
|
: '[Timer]\nOnCalendar=' + parsed.onCalendar + '\nExecLine=' + line + '\n'
|
|
await bareAgentWriteTextFile(ctx, dest, body)
|
|
appendProgress('schedule_task ' + id)
|
|
return bareAgentJsonResult({
|
|
ok: true,
|
|
id,
|
|
path: dest,
|
|
interval: parsed,
|
|
exec: line
|
|
})
|
|
}
|
|
|
|
if (toolName === 'unschedule_task') {
|
|
const id =
|
|
typeof bareAgentScheduleId === 'function'
|
|
? bareAgentScheduleId(args.id)
|
|
: String(args.id || '')
|
|
if (!id) return bareAgentJsonResult({ ok: false, error: 'id_required' })
|
|
const dest = (home || '/home') + '/.config/bare-os/timers/' + id + '.timer'
|
|
if (!vfs || typeof vfs.unlink !== 'function') {
|
|
return bareAgentJsonResult({ ok: false, error: 'vfs.unlink unavailable' })
|
|
}
|
|
try {
|
|
await vfs.unlink(dest)
|
|
} catch (e) {
|
|
const msg =
|
|
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
|
return bareAgentJsonResult({ ok: false, error: msg, path: dest })
|
|
}
|
|
appendProgress('unschedule_task ' + id)
|
|
return bareAgentJsonResult({ ok: true, id, path: dest })
|
|
}
|
|
|
|
if (toolName === 'list_scheduled') {
|
|
const dir = (home || '/home') + '/.config/bare-os/timers'
|
|
let names = []
|
|
try {
|
|
names = vfs && typeof vfs.readdir === 'function' ? await vfs.readdir(dir) : []
|
|
} catch {
|
|
names = []
|
|
}
|
|
const rows = []
|
|
const list = Array.isArray(names) ? names : []
|
|
for (let i = 0; i < list.length; i++) {
|
|
const name = String(list[i] || '')
|
|
if (!name.endsWith('.timer') || name.indexOf('agent-') !== 0) continue
|
|
const path = dir + '/' + name
|
|
const preview = await bareAgentReadTextFile(ctx, path)
|
|
rows.push({ id: name.replace(/\.timer$/, ''), path, preview: preview.slice(0, 400) })
|
|
}
|
|
appendProgress('list_scheduled ' + String(rows.length))
|
|
return bareAgentJsonResult({ ok: true, count: rows.length, timers: rows })
|
|
}
|
|
|
|
if (toolName === 'fuzzy_find') {
|
|
const query = typeof args.query === 'string' ? args.query.trim() : ''
|
|
if (!query) return bareAgentJsonResult({ ok: false, error: 'query_required' })
|
|
const root =
|
|
(typeof args.root === 'string' && args.root.trim()) || home || '/home'
|
|
if (typeof bareAgentFuzzyFind !== 'function') {
|
|
return bareAgentJsonResult({ ok: false, error: 'fuzzy_unavailable' })
|
|
}
|
|
appendProgress('fuzzy_find ' + query)
|
|
const hits = await bareAgentFuzzyFind(ctx, root, query, { max: args.max })
|
|
return bareAgentJsonResult({ ok: true, query, root, hits })
|
|
}
|
|
|
|
if (toolName === 'read_many') {
|
|
const pathsIn = Array.isArray(args.paths) ? args.paths : []
|
|
if (!pathsIn.length) return bareAgentJsonResult({ ok: false, error: 'paths_required' })
|
|
const maxB =
|
|
typeof args.max_bytes_each === 'number' && Number.isFinite(args.max_bytes_each)
|
|
? Math.min(Math.max(Math.floor(args.max_bytes_each), 200), 200000)
|
|
: 32000
|
|
const files = []
|
|
for (let i = 0; i < pathsIn.length && i < 16; i++) {
|
|
const path = String(pathsIn[i] || '').trim()
|
|
if (!path || !bareAgentPathAllowed(path)) {
|
|
files.push({ path, ok: false, error: 'path_not_allowed' })
|
|
continue
|
|
}
|
|
let text = await bareAgentReadTextFile(ctx, path)
|
|
const truncated = text.length > maxB
|
|
if (truncated) text = text.slice(0, maxB) + '\n… truncated'
|
|
files.push({ path, ok: true, content: text, truncated })
|
|
}
|
|
appendProgress('read_many ' + String(files.length))
|
|
return bareAgentJsonResult({ ok: true, files })
|
|
}
|
|
|
|
if (toolName === 'wait_for') {
|
|
const pattern = typeof args.pattern === 'string' ? args.pattern : ''
|
|
if (!pattern) return bareAgentJsonResult({ ok: false, error: 'pattern_required' })
|
|
let re
|
|
try {
|
|
re = new RegExp(pattern, args.ignore_case ? 'i' : '')
|
|
} catch (e) {
|
|
return bareAgentJsonResult({ ok: false, error: 'bad_regex' })
|
|
}
|
|
const timeoutMs = Math.min(
|
|
120000,
|
|
Math.max(200, Math.floor(Number(args.timeout_ms) || 15000))
|
|
)
|
|
const intervalMs = Math.min(
|
|
5000,
|
|
Math.max(100, Math.floor(Number(args.interval_ms) || 400))
|
|
)
|
|
const path = typeof args.path === 'string' ? args.path.trim() : ''
|
|
const command = typeof args.command === 'string' ? args.command.trim() : ''
|
|
if (!path && !command) {
|
|
return bareAgentJsonResult({ ok: false, error: 'path_or_command_required' })
|
|
}
|
|
appendProgress('wait_for ' + pattern)
|
|
const deadline = Date.now() + timeoutMs
|
|
let last = ''
|
|
while (Date.now() <= deadline) {
|
|
if (path) last = await bareAgentReadTextFile(ctx, path)
|
|
else if (command && execLine) {
|
|
const r = await captureExec(command, Math.min(intervalMs + 2000, 15000))
|
|
last = r && r.stdout_stderr ? String(r.stdout_stderr) : ''
|
|
}
|
|
re.lastIndex = 0
|
|
if (re.test(last)) {
|
|
return bareAgentJsonResult({
|
|
ok: true,
|
|
matched: true,
|
|
elapsed_ms: timeoutMs - Math.max(0, deadline - Date.now()),
|
|
sample: last.slice(0, 800)
|
|
})
|
|
}
|
|
if (Date.now() + intervalMs > deadline) break
|
|
await new Promise(function (resolve) {
|
|
setTimeout(resolve, intervalMs)
|
|
})
|
|
}
|
|
return bareAgentJsonResult({
|
|
ok: false,
|
|
error: 'wait_timeout',
|
|
sample: last.slice(0, 400)
|
|
})
|
|
}
|
|
|
|
if (toolName === 'undo_last_edit') {
|
|
const editsPath = paths.edits || paths.dir + '/edits.json'
|
|
if (typeof bareAgentPopEdit !== 'function') {
|
|
return bareAgentJsonResult({ ok: false, error: 'undo_unavailable' })
|
|
}
|
|
const rec = await bareAgentPopEdit(ctx, editsPath)
|
|
if (!rec || !rec.path) {
|
|
return bareAgentJsonResult({ ok: false, error: 'nothing_to_undo' })
|
|
}
|
|
await bareAgentWriteTextFile(ctx, String(rec.path), String(rec.prev || ''))
|
|
appendProgress('undo_last_edit ' + rec.path)
|
|
return bareAgentJsonResult({ ok: true, path: rec.path, tool: rec.tool || null })
|
|
}
|
|
|
|
if (toolName === 'git_diff') {
|
|
const cwd =
|
|
(typeof args.cwd === 'string' && args.cwd.trim()) ||
|
|
(ctx.env && typeof ctx.env === 'object'
|
|
? String(ctx.env.PWD || ctx.env.CWD || home || '').trim()
|
|
: '') ||
|
|
home ||
|
|
'/home'
|
|
if (!execLine) return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' })
|
|
const p = typeof args.path === 'string' ? args.path.trim() : ''
|
|
const stat = Boolean(args.stat)
|
|
const cmd =
|
|
'git -C ' +
|
|
bareAgentShellQuote(cwd) +
|
|
' diff' +
|
|
(stat ? ' --stat' : '') +
|
|
(p ? ' -- ' + bareAgentShellQuote(p) : '')
|
|
appendProgress('git_diff ' + cwd)
|
|
const r = await captureExec(cmd, 30000)
|
|
return bareAgentJsonResult(
|
|
r.ok === false ? r : { ok: true, cwd, stdout_stderr: r.stdout_stderr }
|
|
)
|
|
}
|
|
|
|
if (toolName === 'history_search') {
|
|
const query = typeof args.query === 'string' ? args.query.trim() : ''
|
|
if (!query) return bareAgentJsonResult({ ok: false, error: 'query_required' })
|
|
const histPath = paths.history || paths.dir + '/history.json'
|
|
const raw = await bareAgentReadJsonFile(ctx, histPath, [])
|
|
const hits =
|
|
typeof bareAgentHistorySearch === 'function'
|
|
? bareAgentHistorySearch(Array.isArray(raw) ? raw : [], query, args.max)
|
|
: []
|
|
appendProgress('history_search ' + query)
|
|
return bareAgentJsonResult({ ok: true, query, hits })
|
|
}
|
|
|
|
if (toolName === 'git_log' || toolName === 'git_show' || toolName === 'git_blame') {
|
|
const cwd =
|
|
(typeof args.cwd === 'string' && args.cwd.trim()) ||
|
|
(ctx.env && typeof ctx.env === 'object'
|
|
? String(ctx.env.PWD || ctx.env.CWD || home || '').trim()
|
|
: '') ||
|
|
home ||
|
|
'/home'
|
|
if (!execLine) return bareAgentJsonResult({ ok: false, error: 'execLine unavailable' })
|
|
const quoted = bareAgentShellQuote(cwd)
|
|
let cmd = 'git -C ' + quoted + ' '
|
|
if (toolName === 'git_log') {
|
|
const max = Math.min(80, Math.max(1, Math.floor(Number(args.max) || 20)))
|
|
const p = typeof args.path === 'string' ? args.path.trim() : ''
|
|
cmd += 'log --oneline -' + String(max) + (p ? ' -- ' + bareAgentShellQuote(p) : '')
|
|
} else if (toolName === 'git_show') {
|
|
const rev = typeof args.rev === 'string' && args.rev.trim() ? args.rev.trim() : 'HEAD'
|
|
const p = typeof args.path === 'string' ? args.path.trim() : ''
|
|
cmd +=
|
|
'show ' +
|
|
(args.stat ? '--stat ' : '') +
|
|
bareAgentShellQuote(rev) +
|
|
(p ? ' -- ' + bareAgentShellQuote(p) : '')
|
|
} else {
|
|
const p = typeof args.path === 'string' ? args.path.trim() : ''
|
|
if (!p) return bareAgentJsonResult({ ok: false, error: 'path_required' })
|
|
cmd += 'blame -- ' + bareAgentShellQuote(p)
|
|
}
|
|
appendProgress(toolName + ' ' + cwd)
|
|
const r = await captureExec(cmd, 30000)
|
|
return bareAgentJsonResult(
|
|
r.ok === false ? r : { ok: true, cwd, stdout_stderr: r.stdout_stderr }
|
|
)
|
|
}
|
|
|
|
if (toolName === 'copy_path') {
|
|
const from = typeof args.from_path === 'string' ? args.from_path.trim() : ''
|
|
const to = typeof args.to_path === 'string' ? args.to_path.trim() : ''
|
|
if (
|
|
!from ||
|
|
!to ||
|
|
from.includes('..') ||
|
|
to.includes('..') ||
|
|
!bareAgentPathAllowed(from) ||
|
|
!bareAgentPathAllowedMutate(to, mutateDenyPrefixes)
|
|
) {
|
|
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
|
}
|
|
if (typeof bareAgentCopyPath !== 'function') {
|
|
return bareAgentJsonResult({ ok: false, error: 'copy_unavailable' })
|
|
}
|
|
appendProgress('copy_path ' + from)
|
|
const out = await bareAgentCopyPath(ctx, from, to)
|
|
return bareAgentJsonResult(out)
|
|
}
|
|
|
|
if (toolName === 'diff_files') {
|
|
const from = typeof args.from_path === 'string' ? args.from_path.trim() : ''
|
|
const to = typeof args.to_path === 'string' ? args.to_path.trim() : ''
|
|
if (!from || !to || !bareAgentPathAllowed(from) || !bareAgentPathAllowed(to)) {
|
|
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
|
}
|
|
if (typeof bareAgentUnifiedDiff !== 'function') {
|
|
return bareAgentJsonResult({ ok: false, error: 'diff_unavailable' })
|
|
}
|
|
const a = await bareAgentReadTextFile(ctx, from)
|
|
const b = await bareAgentReadTextFile(ctx, to)
|
|
appendProgress('diff_files ' + from)
|
|
const out = bareAgentUnifiedDiff(a, b, {
|
|
from,
|
|
to,
|
|
context: args.context
|
|
})
|
|
return bareAgentJsonResult(out)
|
|
}
|
|
|
|
if (toolName === 'find_symbol') {
|
|
const name = typeof args.name === 'string' ? args.name.trim() : ''
|
|
if (!name) return bareAgentJsonResult({ ok: false, error: 'name_required' })
|
|
const root =
|
|
(typeof args.root === 'string' && args.root.trim()) || home || '/home'
|
|
if (!bareAgentPathAllowed(root) || typeof bareAgentFindSymbol !== 'function') {
|
|
return bareAgentJsonResult({ ok: false, error: 'find_symbol_unavailable' })
|
|
}
|
|
appendProgress('find_symbol ' + name)
|
|
const out = await bareAgentFindSymbol(ctx, root, name, {
|
|
glob: typeof args.glob === 'string' ? args.glob : '',
|
|
max: args.max
|
|
})
|
|
return bareAgentJsonResult(out)
|
|
}
|
|
|
|
if (toolName === 'create_skill') {
|
|
const id = String(args.id || '')
|
|
.trim()
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9-]+/g, '-')
|
|
.replace(/^-+|-+$/g, '')
|
|
if (!id) return bareAgentJsonResult({ ok: false, error: 'id_required' })
|
|
const destRoot =
|
|
(typeof paths.workspaceSkills === 'string' && paths.workspaceSkills) ||
|
|
(typeof paths.workspace === 'string' ? paths.workspace + '/skills' : paths.dir + '/workspace/skills')
|
|
const dest = destRoot + '/' + id + '/SKILL.md'
|
|
if (!bareAgentPathAllowedMutate(dest, mutateDenyPrefixes)) {
|
|
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
|
}
|
|
const md =
|
|
typeof bareAgentSkillMarkdown === 'function'
|
|
? bareAgentSkillMarkdown({
|
|
name: typeof args.name === 'string' ? args.name : id,
|
|
description: typeof args.description === 'string' ? args.description : id,
|
|
body: typeof args.body === 'string' ? args.body : ''
|
|
})
|
|
: String(args.body || '')
|
|
await bareAgentWriteTextFile(ctx, dest, md)
|
|
appendProgress('create_skill ' + id)
|
|
return bareAgentJsonResult({ ok: true, id, path: dest })
|
|
}
|
|
|
|
if (toolName === 'rewind_session') {
|
|
const histPath = paths.history || paths.dir + '/history.json'
|
|
const raw = await bareAgentReadJsonFile(ctx, histPath, [])
|
|
if (typeof bareAgentRewindHistory !== 'function') {
|
|
return bareAgentJsonResult({ ok: false, error: 'rewind_unavailable' })
|
|
}
|
|
const out = bareAgentRewindHistory(Array.isArray(raw) ? raw : [], {
|
|
steps: args.steps,
|
|
userIndex: args.user_index,
|
|
keep_user: args.keep_user
|
|
})
|
|
if (!out.ok) return bareAgentJsonResult(out)
|
|
await bareAgentSaveHistory(ctx, histPath, out.messages)
|
|
appendProgress('rewind_session dropped=' + String(out.dropped))
|
|
return bareAgentJsonResult({
|
|
ok: true,
|
|
dropped: out.dropped,
|
|
remaining: out.messages.length,
|
|
target: out.target || null,
|
|
keep_user: Boolean(out.keep_user)
|
|
})
|
|
}
|
|
|
|
if (toolName === 'export_session') {
|
|
const histPath = paths.history || paths.dir + '/history.json'
|
|
const dest =
|
|
(typeof args.path === 'string' && args.path.trim()) ||
|
|
(paths.dir ? paths.dir + '/export.md' : '/tmp/agent-export.md')
|
|
if (!bareAgentPathAllowedMutate(dest, mutateDenyPrefixes)) {
|
|
return bareAgentJsonResult({ ok: false, error: 'path_not_allowed' })
|
|
}
|
|
const raw = await bareAgentReadJsonFile(ctx, histPath, [])
|
|
const md =
|
|
typeof bareAgentExportTranscript === 'function'
|
|
? bareAgentExportTranscript(Array.isArray(raw) ? raw : [])
|
|
: ''
|
|
await bareAgentWriteTextFile(ctx, dest, md)
|
|
appendProgress('export_session ' + dest)
|
|
return bareAgentJsonResult({
|
|
ok: true,
|
|
path: dest,
|
|
messages: Array.isArray(raw) ? raw.length : 0
|
|
})
|
|
}
|
|
|
|
return bareAgentJsonResult({ ok: false, error: 'unknown_tool ' + toolName })
|
|
} catch (e) {
|
|
const msg =
|
|
e && typeof e === 'object' && 'message' in e ? String(e.message) : String(e)
|
|
appendProgress('tool_error ' + toolName + ': ' + msg.slice(0, 200))
|
|
return bareAgentJsonResult({ ok: false, error: msg })
|
|
}
|
|
}
|