Files
bare-operating-system/packages/bare-os-coreutils/lib/agent/agent-helpers.js
T
2026-08-18 18:11:28 -04:00

291 lines
8.8 KiB
JavaScript

/** 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<string, unknown>} */ (statObj)
/** @type {'file' | 'dir' | 'symlink' | 'other'} */
let kind = 'other'
try {
if (typeof s.isDirectory === 'function' && s.isDirectory()) kind = 'dir'
else if (typeof s.isSymbolicLink === 'function' && s.isSymbolicLink()) kind = 'symlink'
else if (typeof s.isFile === 'function' && s.isFile()) kind = 'file'
else if (typeof s.mode === 'number') {
const M = Number(s.mode)
if ((M & 0o170000) === 0o040000) kind = 'dir'
else if ((M & 0o170000) === 0o120000) kind = 'symlink'
else if ((M & 0o170000) === 0o100000) kind = 'file'
}
} catch {
/* ignore */
}
/** @type {Record<string, unknown>} */
const out = {
path,
kind,
size: typeof s.size === 'number' ? s.size : undefined,
mode: typeof s.mode === 'number' ? s.mode : undefined,
mtimeMs: typeof s.mtimeMs === 'number' ? s.mtimeMs : undefined,
uid: typeof s.uid === 'number' ? s.uid : undefined,
gid: typeof s.gid === 'number' ? s.gid : undefined
}
if (typeof s.target === 'string') out.target = s.target
return out
}
/**
* @param {string} text
* @param {number} maxChars
*/
function bareAgentTruncateChars(text, maxChars) {
const t = String(text || '')
const n = Math.floor(maxChars)
if (!Number.isFinite(n) || n <= 0) return ''
if (t.length <= n) return t
return t.slice(0, n) + '\n… truncated'
}
/**
* @param {Record<string, unknown>} page
* @param {number} maxChars
*/
function bareAgentManExtractPageSlice(page, maxChars) {
if (!page || typeof page !== 'object')
return { error: 'bad_page' }
const m = Math.min(Math.max(Math.floor(maxChars) || 8000, 500), 64_000)
const name = typeof page.name === 'string' ? page.name : ''
const section = typeof page.section === 'number' ? page.section : 0
const title = typeof page.title === 'string' ? page.title : ''
const synopsis = Array.isArray(page.synopsis)
? page.synopsis.map((x) => String(x)).join('\n')
: ''
const description = bareAgentTruncateChars(
typeof page.description === 'string' ? page.description : '',
Math.floor(m * 0.55)
)
let opts = ''
if (Array.isArray(page.options)) {
const lines = []
for (const o of page.options) {
if (!o || typeof o !== 'object') continue
const fl = typeof o.flag === 'string' ? o.flag : ''
const me = typeof o.meaning === 'string' ? o.meaning : ''
if (fl || me) lines.push(fl + (fl && me ? ' — ' : '') + me)
}
opts = bareAgentTruncateChars(lines.join('\n'), Math.floor(m * 0.35))
}
const blob =
name +
'(' +
section +
') — ' +
title +
'\n\nSYNOPSIS\n' +
synopsis +
'\n\nDESCRIPTION\n' +
description +
(opts ? '\n\nOPTIONS\n' + opts : '')
return {
name,
section,
title,
synopsis,
description,
options_text: opts || undefined,
text: bareAgentTruncateChars(blob, m)
}
}
/**
* Same semantics as `man -k`: substring match on indexed keywords (merged DB).
* @param {unknown} db
* @param {string} needle
* @param {number} maxHits
*/
function bareAgentManAproposHits(db, needle, maxHits) {
const n = String(needle || '').toLowerCase()
const max = Math.min(Math.max(Math.floor(maxHits) || 40, 1), 200)
if (!n || !db || typeof db !== 'object')
return /** @type {{ lines: string[], truncated: boolean }} */ ({
lines: [],
truncated: false
})
const d = /** @type {Record<string, unknown>} */ (db)
const pages = Array.isArray(d.pages) ? d.pages : []
const apropos = Array.isArray(d.apropos) ? d.apropos : []
const seen = new Set()
/** @type {string[]} */
const lines = []
let truncated = false
for (const row of apropos) {
if (!row || typeof row !== 'object') continue
const kw = typeof row.kw === 'string' ? row.kw : ''
if (!kw.includes(n)) continue
const idx = row.pageRef
if (typeof idx !== 'number' || !pages[idx]) continue
if (seen.has(idx)) continue
seen.add(idx)
const p = /** @type {Record<string, unknown>} */ (pages[idx])
const name = typeof p.name === 'string' ? p.name : ''
const sec = typeof p.section === 'number' ? p.section : 0
const title = typeof p.title === 'string' ? p.title : ''
lines.push(name + '(' + sec + ') - ' + title)
if (lines.length >= max) {
truncated = true
break
}
}
lines.sort()
return { lines, truncated }
}
/**
* @param {Record<string, unknown>} ctx
* @param {unknown} vfs
* @param {{ db: unknown | null }} cacheRef
* @returns {Promise<unknown | null>}
*/
async function bareAgentManEnsureDbLoaded(ctx, vfs, cacheRef) {
if (cacheRef.db) return cacheRef.db
if (!vfs || typeof vfs.readFile !== 'function') return null
try {
const buf = await vfs.readFile('/share/man/man.json')
if (!buf || !buf.length) return null
const t =
typeof ctx.b4a !== 'undefined' && ctx.b4a && typeof ctx.b4a.toString === 'function'
? ctx.b4a.toString(buf)
: String(new TextDecoder().decode(buf))
const parsed = JSON.parse(t)
cacheRef.db = parsed
return parsed
} catch {
return null
}
}
/**
* Resolve one manual page from merged DB (like `man [[section] name]`).
* @param {unknown} db
* @param {string} topic
* @param {number | null} sectionExplicit
*/
function bareAgentManResolvePage(db, topic, sectionExplicit) {
const name = String(topic || '').toLowerCase()
if (!name || !db || typeof db !== 'object')
return { error: 'not_found' }
const d = /** @type {Record<string, unknown>} */ (db)
const index = d.index
if (!index || typeof index !== 'object') return { error: 'not_found' }
const idx = /** @type {Record<string, unknown>} */ (index)[name]
if (typeof idx !== 'number') return { error: 'not_found' }
const pages = Array.isArray(d.pages) ? d.pages : []
const page = pages[idx]
if (!page || typeof page !== 'object') return { error: 'not_found' }
const sec = typeof page.section === 'number' ? page.section : 0
if (sectionExplicit !== null && sectionExplicit !== sec) {
return { error: 'wrong_section', foundSection: sec }
}
return { page: /** @type {Record<string, unknown>} */ (page) }
}
/**
* @param {string} absPath
* @returns {boolean}
*/
function bareAgentProcReadPathAllowed(absPath) {
const p = bareAgentNormalizeAbsPath(absPath)
return Boolean(p && (p === '/proc' || p.startsWith('/proc/')))
}