/** 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 } /** * @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/'))) }