/** Tool dispatch for /bin/agent (preamble; no import). */ /** * @param {{ * ctx: Record, * 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 }, * 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} */ let args = {} try { args = /** @type {Record} */ (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} */ ( 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 ') .replace(/\bsk-[a-zA-Z0-9]{16,}\b/g, '') .replace( /\b([A-Z0-9_]*(KEY|TOKEN|SECRET|PASS|PASSWORD)[A-Z0-9_]*)\s*=\s*([^\s]+)/gi, '$1=' ) } /** * @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[]} */ 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} */ 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 }[]} */ 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 | 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} */ (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} */ (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} */ (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} */ 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 ') .replace(/\bsk-[a-zA-Z0-9]{16,}\b/g, '') .replace(/\b([A-Z0-9_]*(KEY|TOKEN|SECRET|PASS)[A-Z0-9_]*)\s*=\s*([^\s]+)/gi, '$1=') } 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} */ 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} */ 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} */ 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} */ 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} */ (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} */ (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} */ 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} */ 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} */ 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} */ (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 }) } }