/** Shared helpers for /bin/agent tools (preamble for agent bundle). */ /** * Read-only base system (kernel / system Hyperdrive + virtual fs). * Everything else is writable by default (denylist, not allowlist). * @type {readonly string[]} */ var BARE_AGENT_MUTATE_DENY_PREFIXES = Object.freeze([ '/bin', '/etc', '/boot', '/lib', '/usr', '/share', '/proc', '/dev', '/sys', '/run' ]) /** * Seed list for diagnostic snapshots. Live reads allow any /proc path. * @type {readonly string[]} */ var BARE_AGENT_PROC_READ_ALLOWLIST = Object.freeze([ '/proc/bare_os/metrics_live.json', '/proc/bare_os/features', '/proc/bare_os/features.json', '/proc/bare_os/swarm.json', '/proc/bare_os/capabilities.json', '/proc/bare_os/swarm_replication_status.json', '/proc/bare_os/swarm_relay_status.json', '/proc/bare_os/swarm_datagrams_status.json', '/proc/bare_os/swarm_connection_manager_status.json', '/proc/bare_os/swarm_key_broker_status.json', '/proc/bare_os/swarm_holepunch_status.json', '/proc/bare_os/swarm_datagram_replication_status.json', '/proc/bare_os/swarm_status.json' ]) /** * @param {string} absPath * @returns {string} */ function bareAgentNormalizeAbsPath(absPath) { const p = String(absPath || '').replace(/\\/g, '/') if (!p.startsWith('/') || p.includes('..')) return '' if (p.length > 1) return p.replace(/\/+$/, '') return p } /** * Expand ~ / $HOME / relative guest paths to an absolute VFS path. * @param {string} raw * @param {string} home * @param {string} [cwd] */ function bareAgentExpandGuestPath(raw, home, cwd) { let p = String(raw == null ? '' : raw).trim() if (!p) return '' p = p.replace(/\\/g, '/') const h = String(home || '').replace(/\/+$/, '') || '/home/guest' let c = String(cwd || h).replace(/\/+$/, '') || h if (c === '~') c = h else if (c.startsWith('~/')) c = h + c.slice(1) else if (c.startsWith('$HOME/')) c = h + c.slice(5) else if (c === '$HOME') c = h if (p === '~' || p === '$HOME') return h if (p.startsWith('~/')) { p = h + '/' + p.slice(2) } else if (p.startsWith('$HOME/')) { p = h + '/' + p.slice(6) } else if (!p.startsWith('/')) { p = (c === '/' ? '/' + p : c + '/' + p) } p = p.replace(/\/{2,}/g, '/') if (p.length > 1) p = p.replace(/\/+$/, '') return p } /** * Pick a path-like argument from a tool-call blob (path / file_path / dir / …). * @param {Record} args */ function bareAgentPickPathArg(args) { if (!args || typeof args !== 'object') return '' const keys = [ 'path', 'file_path', 'file', 'filename', 'dir', 'directory', 'folder', 'from_path', 'to_path' ] for (let i = 0; i < keys.length; i++) { const v = args[keys[i]] if (typeof v === 'string' && v.trim()) return v.trim() } return '' } /** * @param {string} absPath * @param {unknown} prefixes * @returns {boolean} */ function bareAgentPrefixDenied(absPath, prefixes) { const p = bareAgentNormalizeAbsPath(absPath) if (!p) return true const list = Array.isArray(prefixes) && prefixes.length ? prefixes : BARE_AGENT_MUTATE_DENY_PREFIXES for (let i = 0; i < list.length; i++) { const pref = String(list[i] || '').replace(/\/+$/, '') if (!pref) continue if (p === pref || p.startsWith(pref + '/')) return true } return false } /** * Any absolute guest path is readable (denylist-empty). * @param {string} absPath */ function bareAgentPathAllowedRead(absPath) { return Boolean(bareAgentNormalizeAbsPath(absPath)) } /** * Writes/renames/deletes: whole VFS except the read-only base system. * @param {string} absPath * @param {unknown} [prefixes] */ function bareAgentPathAllowedMutate(absPath, prefixes) { const p = bareAgentNormalizeAbsPath(absPath) if (!p || p === '/') return false return !bareAgentPrefixDenied(p, prefixes) } /** * @param {unknown} statObj * @param {string} path */ function bareAgentSerializeStat(statObj, path) { if (!statObj || typeof statObj !== 'object') return { path, error: 'no_stat' } const s = /** @type {Record} */ (statObj) /** @type {'file' | 'dir' | 'symlink' | 'other'} */ let kind = 'other' try { if (typeof s.isDirectory === 'function' && s.isDirectory()) kind = 'dir' else if (typeof s.isSymbolicLink === 'function' && s.isSymbolicLink()) kind = 'symlink' else if (typeof s.isFile === 'function' && s.isFile()) kind = 'file' else if (typeof s.mode === 'number') { const M = Number(s.mode) if ((M & 0o170000) === 0o040000) kind = 'dir' else if ((M & 0o170000) === 0o120000) kind = 'symlink' else if ((M & 0o170000) === 0o100000) kind = 'file' } } catch { /* ignore */ } /** @type {Record} */ const out = { path, kind, size: typeof s.size === 'number' ? s.size : undefined, mode: typeof s.mode === 'number' ? s.mode : undefined, mtimeMs: typeof s.mtimeMs === 'number' ? s.mtimeMs : undefined, uid: typeof s.uid === 'number' ? s.uid : undefined, gid: typeof s.gid === 'number' ? s.gid : undefined } if (typeof s.target === 'string') out.target = s.target return out } /** * @param {string} text * @param {number} maxChars */ function bareAgentTruncateChars(text, maxChars) { const t = String(text || '') const n = Math.floor(maxChars) if (!Number.isFinite(n) || n <= 0) return '' if (t.length <= n) return t return t.slice(0, n) + '\n… truncated' } /** * @param {Record} page * @param {number} maxChars */ function bareAgentManExtractPageSlice(page, maxChars) { if (!page || typeof page !== 'object') return { error: 'bad_page' } const m = Math.min(Math.max(Math.floor(maxChars) || 8000, 500), 64_000) const name = typeof page.name === 'string' ? page.name : '' const section = typeof page.section === 'number' ? page.section : 0 const title = typeof page.title === 'string' ? page.title : '' const synopsis = Array.isArray(page.synopsis) ? page.synopsis.map((x) => String(x)).join('\n') : '' const description = bareAgentTruncateChars( typeof page.description === 'string' ? page.description : '', Math.floor(m * 0.55) ) let opts = '' if (Array.isArray(page.options)) { const lines = [] for (const o of page.options) { if (!o || typeof o !== 'object') continue const fl = typeof o.flag === 'string' ? o.flag : '' const me = typeof o.meaning === 'string' ? o.meaning : '' if (fl || me) lines.push(fl + (fl && me ? ' — ' : '') + me) } opts = bareAgentTruncateChars(lines.join('\n'), Math.floor(m * 0.35)) } const blob = name + '(' + section + ') — ' + title + '\n\nSYNOPSIS\n' + synopsis + '\n\nDESCRIPTION\n' + description + (opts ? '\n\nOPTIONS\n' + opts : '') return { name, section, title, synopsis, description, options_text: opts || undefined, text: bareAgentTruncateChars(blob, m) } } /** * Same semantics as `man -k`: substring match on indexed keywords (merged DB). * @param {unknown} db * @param {string} needle * @param {number} maxHits */ function bareAgentManAproposHits(db, needle, maxHits) { const n = String(needle || '').toLowerCase() const max = Math.min(Math.max(Math.floor(maxHits) || 40, 1), 200) if (!n || !db || typeof db !== 'object') return /** @type {{ lines: string[], truncated: boolean }} */ ({ lines: [], truncated: false }) const d = /** @type {Record} */ (db) const pages = Array.isArray(d.pages) ? d.pages : [] const apropos = Array.isArray(d.apropos) ? d.apropos : [] const seen = new Set() /** @type {string[]} */ const lines = [] let truncated = false for (const row of apropos) { if (!row || typeof row !== 'object') continue const kw = typeof row.kw === 'string' ? row.kw : '' if (!kw.includes(n)) continue const idx = row.pageRef if (typeof idx !== 'number' || !pages[idx]) continue if (seen.has(idx)) continue seen.add(idx) const p = /** @type {Record} */ (pages[idx]) const name = typeof p.name === 'string' ? p.name : '' const sec = typeof p.section === 'number' ? p.section : 0 const title = typeof p.title === 'string' ? p.title : '' lines.push(name + '(' + sec + ') - ' + title) if (lines.length >= max) { truncated = true break } } lines.sort() return { lines, truncated } } /** * @param {Record} ctx * @param {unknown} vfs * @param {{ db: unknown | null }} cacheRef * @returns {Promise} */ async function bareAgentManEnsureDbLoaded(ctx, vfs, cacheRef) { if (cacheRef.db) return cacheRef.db if (!vfs || typeof vfs.readFile !== 'function') return null try { const buf = await vfs.readFile('/share/man/man.json') if (!buf || !buf.length) return null const t = typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function' ? ctx.b4a.toString(buf) : String(new TextDecoder().decode(buf)) const parsed = JSON.parse(t) cacheRef.db = parsed return parsed } catch { return null } } /** * Resolve one manual page from merged DB (like `man [[section] name]`). * @param {unknown} db * @param {string} topic * @param {number | null} sectionExplicit */ function bareAgentManResolvePage(db, topic, sectionExplicit) { const name = String(topic || '').toLowerCase() if (!name || !db || typeof db !== 'object') return { error: 'not_found' } const d = /** @type {Record} */ (db) const index = d.index if (!index || typeof index !== 'object') return { error: 'not_found' } const idx = /** @type {Record} */ (index)[name] if (typeof idx !== 'number') return { error: 'not_found' } const pages = Array.isArray(d.pages) ? d.pages : [] const page = pages[idx] if (!page || typeof page !== 'object') return { error: 'not_found' } const sec = typeof page.section === 'number' ? page.section : 0 if (sectionExplicit !== null && sectionExplicit !== sec) { return { error: 'wrong_section', foundSection: sec } } return { page: /** @type {Record} */ (page) } } /** * @param {string} absPath * @returns {boolean} */ function bareAgentProcReadPathAllowed(absPath) { const p = bareAgentNormalizeAbsPath(absPath) return Boolean(p && (p === '/proc' || p.startsWith('/proc/'))) } /** * Kernel-runner evals guest scripts as AsyncFunction('ctx','argv', body). * Agent-authored code that redeclares ctx/argv or uses ESM export throws * "Identifier 'ctx' has already been declared" — same contract as /bin * utilities: define async function run(ctx, argv), never bind ctx yourself. * @param {string} code */ function bareAgentNormalizeGuestScript(code) { let src = String(code || '').replace(/^\uFEFF/, '') src = src.replace(/^#!.*(?:\r?\n|$)/, '') src = src.replace(/^\s*export\s*\{[^}]*\}\s*;?\s*$/gm, '') src = src.replace(/^export\s+default\s+/gm, '') src = src.replace(/^export\s+async\s+function\s+/gm, 'async function ') src = src.replace(/^export\s+function\s+/gm, 'function ') src = src.replace(/^export\s+const\s+/gm, 'const ') src = src.replace(/^export\s+let\s+/gm, 'let ') src = src.replace(/^export\s+var\s+/gm, 'var ') src = src.replace(/^(?:const|let|var)\s+ctx\b[^;\n]*;?\s*$/gm, '') src = src.replace(/^(?:const|let|var)\s+argv\b[^;\n]*;?\s*$/gm, '') src = src.replace( /^(?:const|let|var)\s*\{\s*ctx[\s,}][^;\n]*;?\s*$/gm, '' ) if ( !/\b(?:async\s+)?function\s+run\s*\(/.test(src) && !/\brun\s*=\s*(?:async\s*)?(?:function\s*)?\(/.test(src) ) { src = 'async function run(ctx, argv) {\n' + src.trim() + '\n}\n' } return src.trim() + '\n' } /** * Trim glued prose off a curl/wget URL (`orgBut` → `org`). * @param {string} cmd */ function bareAgentTrimGluedShellCommand(cmd) { let s = String(cmd || '').trim() s = s.replace(/(\.(?:org|com|net|io|dev|fyi|info|edu|co))([A-Z].*)$/i, '$1') s = s.replace(/(https?:\/\/[^\s]+?)(?=[A-Z][a-z])/, '$1') return s.trim() } /** * If a run_js_script body is actually a guest CLI (curl/wget/ip), return * the command so dispatch can divert to run_command. * @param {string} code * @returns {string} */ function bareAgentShellCommandFromJsScript(code) { const s = String(code || '') if (!s.trim()) return '' const trimmed = s.trim() const looksShell = typeof bareAgentLooksLikeShellCommand === 'function' ? bareAgentLooksLikeShellCommand(trimmed) : /^(curl|wget|ip|ping|ifconfig)\b/i.test(trimmed) if ( looksShell && !/\bfunction\b|\bclass\b|\bawait\b|\bconst\b|\blet\b|\bvar\b|\bimport\b/.test(trimmed) ) { return bareAgentTrimGluedShellCommand(trimmed) } const execRe = /\b(?:execLine|execSync|exec|spawnSync|system)\s*\(\s*(['"`])([\s\S]*?)\1/g let m while ((m = execRe.exec(s))) { const cmd = String(m[2] || '').trim() if (/^(curl|wget|ip|ping|ifconfig)\b/i.test(cmd)) { return bareAgentTrimGluedShellCommand(cmd) } } const fetchRe = /\bfetch\s*\(\s*(['"`])(https?:\/\/[^'"`]+)\1/ m = fetchRe.exec(s) if (m) return 'curl -s ' + String(m[2] || '').trim() const curlRe = /(?:^|[\n;])\s*((?:curl|wget)\s+https?:\/\/[^\s;'"`]+)/im m = curlRe.exec(s) if (m) return bareAgentTrimGluedShellCommand(m[1]) return '' } /** * task_complete used as a surrender after a failed tool, not a real finish. * @param {string} summary */ function bareAgentLooksLikeFailedTaskComplete(summary) { const s = String(summary || '').toLowerCase() if (!s.trim()) return false return ( /error occurred/.test(s) || /alternative method required/.test(s) || (/attempted to /.test(s) && /error|fail/.test(s)) || /failed due to/.test(s) || /could not (run|execute|retrieve|complete|get)/.test(s) || /not (able|allowed) to (run|execute|complete)/.test(s) || /top-level (scope|await|module)/.test(s) ) } /** * User asked to run a guest CLI (curl / IP lookup), not write JS. * @param {string} text */ function bareAgentLooksLikeGuestCliRequest(text) { const s = String(text || '').toLowerCase() if (!s.trim()) return false return ( /\b(run|execute|use)\b.{0,48}\b(curl|wget|ping|ifconfig|ip addr)\b/.test(s) || /\b(curl|wget)\b.{0,48}\b(ip address|public ip|our ip|my ip)\b/.test(s) || /\b(find|get|check|show|lookup)\b.{0,48}\b(ip address|public ip|our ip|my ip)\b/.test( s ) ) }