/** * Mirrors semantics in lib/agent-helpers.js (apropos matching, mutate paths, proc allowlist). * Keep aligned when editing helpers. */ import test from 'brittle' /** @type {readonly string[]} */ const PROC_ALLOW = [ '/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_connection_manager_status.json' ] function pathAllowedMutate(p) { const x = String(p || '').replace(/\\/g, '/') if (!x.startsWith('/') || x.includes('..')) return false return ( x.startsWith('/home/') || x.startsWith('/tmp/') || x === '/tmp' || x.startsWith('/root/') || x.startsWith('/mnt/') ) } function procAllowed(p) { const x = String(p || '').replace(/\\/g, '/') if (!x.startsWith('/') || x.includes('..')) return false return PROC_ALLOW.includes(x) } function aproposHits(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 { 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 } } test('mutate paths allow guest areas only', async (t) => { t.ok(pathAllowedMutate('/home/guest/x')) t.ok(pathAllowedMutate('/tmp/a')) t.absent(pathAllowedMutate('/bin/foo')) t.absent(pathAllowedMutate('/etc/passwd')) t.absent(pathAllowedMutate('/proc/x')) }) test('proc allowlist exact paths', async (t) => { t.ok(procAllowed('/proc/bare_os/features')) t.ok(procAllowed('/proc/bare_os/swarm.json')) t.ok(procAllowed('/proc/bare_os/swarm_connection_manager_status.json')) t.absent(procAllowed('/proc/bare_os/other.json')) t.absent(procAllowed('/proc/../proc/bare_os/swarm.json')) }) test('apropos substring matches kw like man -k', async (t) => { const db = { pages: [ { name: 'grep', section: 1, title: 'pattern search' }, { name: 'true', section: 1, title: 'exit successfully' } ], apropos: [ { kw: 'grep', pageRef: 0 }, { kw: 'pattern', pageRef: 0 }, { kw: 'true', pageRef: 1 } ] } const r = aproposHits(db, 'pat', 10) t.is(r.lines.length, 1) t.ok(r.lines[0].startsWith('grep(')) })