Another pass again

This commit is contained in:
2026-08-18 17:33:36 -04:00
parent def93bd39a
commit 54d9cd5db8
12 changed files with 1596 additions and 1211 deletions
+36 -213
View File
@@ -161,6 +161,10 @@ import {
bareInitdResumeAfterBareMobile
} from './lib/bare-initd.js'
import { BARE_OS_LIFECYCLE_SCHEMA_VERSION } from './lib/bare-os-lifecycle-schema.js'
import { createBooterBootEmitter } from './lib/bare-os-boot-phases.js'
import { createBooterProcHelpers } from './lib/bare-os-booter-proc-helpers.js'
import { createBareOsVirtualSignalDeliverer } from './lib/bare-os-virtual-signal.js'
import { createKernelLoaderAuditAppend } from './lib/bare-os-loader-audit.js'
import { createBareOsQvacBridge } from './lib/bare-os-qvac-host.mjs'
import { bareOsQvacEnsureModelsHdms } from './lib/bare-os-qvac-models-store.mjs'
import { buildPearIpcRegistryJson } from './lib/bare-os-pear-ipc-registry.js'
@@ -181,9 +185,7 @@ import {
import { buildBareOsSyscallsProcJson } from './lib/bare-os-syscalls-proc-json.js'
import {
bareOsIsPosixSignalName,
bareOsNormalizeSignalName,
BARE_OS_SIGNAL_EXIT,
BARE_OS_SESSION_EXIT_SIGNALS
bareOsNormalizeSignalName
} from './lib/bare-os-posix-signals.js'
import {
bareOsWasmKernelCompile as compileBareOsWasmKernelModule,
@@ -191,10 +193,6 @@ import {
} from './lib/bare-os-wasm-kernel.js'
import { BARE_OS_CTX_API_VERSION } from './lib/bare-os-ctx-api.js'
import { buildBareOsPearCorestoreHrpcProcJson } from './lib/bare-os-proc-pear-corestore-hrpc.js'
import {
bareOsProcBlindPeerRelayHintsExposed,
buildBareOsBlindRelaySwarmRuntime
} from './lib/bare-os-proc-blind-peer-relay-gate.js'
import { buildBareOsRuntimeCaps } from './lib/bare-os-runtime-caps.js'
import {
bareOsBareModulesEnabled,
@@ -739,31 +737,15 @@ async function executeKernel(disk, store, swarm, initSource) {
/** Live logical→actual map; updated by {@link protomuxAliasRegistry}. */
const bareOsProtomuxAliasMap = protomuxAliasRegistry.legacyMapView()
function emitBooterBootPhase(phase) {
return emitBooterBootStep(phase)
}
function emitBooterBootStep(step) {
const p = String(step)
const arr = bootReadyStateRef.booterPhases
const arrSt = bootReadyStateRef.booterStages
if (!arr.includes(p)) arr.push(p)
if (!arrSt.includes(p)) arrSt.push(p)
const ev = {
type: 'boot',
phase: 'booter:' + p,
ms: Date.now() - bootStartedMs,
ts: Date.now(),
sessionId: bareOsSessionId,
lifecycleSchemaVersion: BARE_OS_LIFECYCLE_SCHEMA_VERSION,
...(bootHrtimeNowNs ? { monotonicNs: String(bootHrtimeNowNs()) } : {})
}
for (const fn of bootEventSubs) {
Promise.resolve(fn(ev)).catch(() => {})
}
for (const fn of diagnosticsSubs) {
Promise.resolve(fn({ ...ev, source: 'booter' })).catch(() => {})
}
}
const { emitBooterBootPhase, emitBooterBootStep } = createBooterBootEmitter({
bootReadyStateRef,
bootStartedMs,
bootHrtimeNowNs,
bootEventSubs,
diagnosticsSubs,
sessionId: bareOsSessionId,
lifecycleSchemaVersion: BARE_OS_LIFECYCLE_SCHEMA_VERSION
})
/** @type {{ stats: unknown }} */
const hostProcStatsRef = { stats: null }
/** Filled after `ctx` is constructed; used for `/proc` readers that need live shell job rows. */
@@ -771,39 +753,18 @@ async function executeKernel(disk, store, swarm, initSource) {
/** @type {Record<string, unknown> | null} */
ctx: null
}
function bareOsCollectLogicalFdRows() {
const c = bareOsInteractiveCtxRef.ctx
const o = c?.bareOsLogicalFds
if (!o || typeof o !== 'object') return []
return Object.keys(o)
.filter((k) => /^[0-9]+$/.test(k) && k !== '0' && k !== '1' && k !== '2')
.sort((a, b) => Number(a) - Number(b))
.map((fdNum) => ({
fd: Number(fdNum),
target: String(/** @type {Record<string, string>} */ (o)[fdNum] ?? '')
}))
}
function bareOsLiveShellJobsAndIpcStats() {
const live = bareOsInteractiveCtxRef.ctx
const shellJobs =
live &&
live.shellBackgroundJobs &&
typeof live.shellBackgroundJobs === 'object' &&
Array.isArray(live.shellBackgroundJobs.list)
? live.shellBackgroundJobs.list
: []
let ipcStats
try {
ipcStats =
live && live.bareOsIpc && typeof live.bareOsIpc.stats === 'function'
? live.bareOsIpc.stats()
: undefined
} catch {
ipcStats = undefined
}
return { shellJobs, ipcStats }
}
const {
bareOsCollectLogicalFdRows,
bareOsLiveShellJobsAndIpcStats,
bareOsBlindRelayRuntimeHintsForProc,
bareOsChatProcSnapshotRecord,
bareOsMeshdropProcSnapshotRecord
} = createBooterProcHelpers({
interactiveCtxRef: bareOsInteractiveCtxRef,
disk,
shellEnv,
hostEnv
})
/** @type {{ current: Record<string, unknown> | null }} */
const warmReadCacheStatsRef = { current: null }
/** Prior Hypercore lengths for optional replication-driven warm-cache prefix eviction. */
@@ -817,46 +778,6 @@ async function executeKernel(disk, store, swarm, initSource) {
const bootBudgetSummaryProcRef = {
text: '{"schema":1,"note":"boot_budget_summary_pending"}\n'
}
function bareOsBlindRelayRuntimeHintsForProc() {
if (!bareOsProcBlindPeerRelayHintsExposed(shellEnv)) return undefined
const peers = disk.peers ? [...disk.peers] : []
return {
swarm: buildBareOsBlindRelaySwarmRuntime(shellEnv, peers, {
hasSystemDrive: !!disk.drive,
hasPersonalDrive: !!disk.personalDrive,
auxiliaryDriveCount: Array.isArray(disk.auxiliaryDrives)
? disk.auxiliaryDrives.length
: 0
})
}
}
/**
* Same object as `/proc/bare_os/chat.json` (single source for VFS + ctx hook).
* @returns {Record<string, unknown>}
*/
function bareOsChatProcSnapshotRecord() {
const svc = disk.bareOsChatService
if (!svc) {
return {
schema: 1,
note: 'Swarm chat is off when BARE_OS_PROTOMUX_CHAT_CHANNEL=0/false/off (stock default is on).'
}
}
return {
schema: 1,
atMs: Date.now(),
protocol: svc.PROTOCOL_CHAT_CHANNEL_NAME,
metrics: svc.snapshotMetrics(),
protomuxChatRxTotal:
typeof disk.protomuxChatChannelRxTotal === 'number'
? disk.protomuxChatChannelRxTotal
: 0,
swarmPeers:
disk.peers && typeof disk.peers.size === 'number' ? disk.peers.size : 0,
muxEnabled:
bareOsChatMuxEnabled(shellEnv) || bareOsChatMuxEnabled(hostEnv || {})
}
}
const vfs = createVfs(disk.drive, disk.personalDrive, shellEnv, vfsMountRef, {
procBareOsSwarmSubsystemStatusJsonText(fileKey) {
return bareOsFormatSwarmSubsystemProcJson(fileKey, {
@@ -897,31 +818,7 @@ async function executeKernel(disk, store, swarm, initSource) {
return `${JSON.stringify(bareOsChatProcSnapshotRecord())}\n`
},
procBareOsMeshdropText() {
const svc = disk.bareOsMeshdropService
/** @type {Record<string, unknown>} */
const o = svc
? {
schema: 1,
atMs: Date.now(),
protocol: svc.PROTOCOL_MESHDROP_CHANNEL_NAME,
metrics: svc.snapshotMetrics(),
protomuxMeshdropRxTotal:
typeof disk.protomuxMeshdropChannelRxTotal === 'number'
? disk.protomuxMeshdropChannelRxTotal
: 0,
swarmPeers:
disk.peers && typeof disk.peers.size === 'number'
? disk.peers.size
: 0,
muxEnabled:
bareOsMeshdropMuxEnabled(shellEnv) ||
bareOsMeshdropMuxEnabled(hostEnv || {})
}
: {
schema: 1,
note: 'Meshdrop channel is off when BARE_OS_PROTOMUX_MESHDROP_CHANNEL=0/false/off (stock default is on).'
}
return `${JSON.stringify(o)}\n`
return `${JSON.stringify(bareOsMeshdropProcSnapshotRecord())}\n`
},
procBareOsDhtScanText() {
const peers = disk.peers?.size ?? 0
@@ -3498,45 +3395,11 @@ async function executeKernel(disk, store, swarm, initSource) {
return redactAuditShellLineFromEnv(line, shellEnv)
}
/**
* Loader audit NDJSON under /run/bare-os/loader-audit.ndjson (cap-gated via BARE_OS_LOADER_AUDIT).
* @param {Record<string, unknown>} rec
*/
async function appendKernelLoaderAuditLine(rec) {
if (
shellEnv.BARE_OS_LOADER_AUDIT !== '1' &&
shellEnv.BARE_OS_LOADER_AUDIT !== 'true'
)
return
if (
!vfs ||
typeof vfs.readFile !== 'function' ||
typeof vfs.writeFile !== 'function'
)
return
const line =
JSON.stringify({
type: 'loader_audit',
ts: Date.now(),
sessionId: bareOsSessionId,
...rec
}) + '\n'
try {
let prev = ''
try {
const buf = await vfs.readFile('/run/bare-os/loader-audit.ndjson')
prev = b4a.toString(buf)
} catch {
/* new */
}
await vfs.writeFile(
'/run/bare-os/loader-audit.ndjson',
b4a.from(prev + line)
)
} catch {
/* ignore */
}
}
const appendKernelLoaderAuditLine = createKernelLoaderAuditAppend({
env: shellEnv,
vfs,
sessionId: bareOsSessionId
})
let bareOsAdvertisedPrimaryResolved =
BARE_OS_KERNEL_FEATURES_STOCK_WORD_PRIMARY >>> 0
@@ -3555,50 +3418,10 @@ async function executeKernel(disk, store, swarm, initSource) {
let sessionExitCode = 0
let forceSessionEnd = false
/**
* @param {Record<string, unknown>} ctx
* @param {number} pid
* @param {string} sig normalized signal short name (e.g. TERM)
*/
function deliverBareOsVirtualSignalToPid(ctx, pid, sig) {
const extendedSynthetic = Number.isFinite(pid) && pid >= 4100 && pid < 9000
if (sig === '0') {
return { ok: true, delivered: false, exists: true }
}
if (
ctx.bareOsLogicalSigaction &&
ctx.bareOsLogicalSigaction[sig] === 'IGNORE'
) {
return {
ok: true,
delivered: false,
ignored: true,
atMs: Date.now()
}
}
const atMs = Date.now()
if (
pid === 3 &&
(sig === 'INT' || sig === 'TERM' || sig === 'USR1' || sig === 'USR2')
) {
Promise.resolve(dispatchShellTrapSignal(ctx, sig)).catch(() => {})
}
bareOsVirtualSignalState.set(pid, { signal: sig, atMs })
if (typeof globalThis.process?.emit === 'function') {
try {
globalThis.process.emit('bare-os:signal', { pid, signal: sig, atMs })
} catch {
/* ignore */
}
}
const noSessionExit =
extendedSynthetic || !BARE_OS_SESSION_EXIT_SIGNALS.has(sig)
if (!noSessionExit) {
const signum = BARE_OS_SIGNAL_EXIT[sig] || 15
ctx.requestBooterExit(128 + signum)
}
return { ok: true, delivered: true, atMs }
}
const deliverBareOsVirtualSignalToPid = createBareOsVirtualSignalDeliverer({
dispatchShellTrapSignal,
virtualSignalState: bareOsVirtualSignalState
})
const stockBareOsHrpcRequest = createStockBareOsHrpcRequest({
env: shellEnv,
@@ -17,3 +17,55 @@ export const BARE_OS_BOOTER_BOOT_STEPS = Object.freeze([
export function isKnownBareOsBooterBootStep(step) {
return BARE_OS_BOOTER_BOOT_STEPS.includes(String(step))
}
/**
* @param {{
* bootReadyStateRef: { booterPhases: string[], booterStages: string[] },
* bootStartedMs: number,
* bootHrtimeNowNs?: (() => bigint) | null,
* bootEventSubs: Iterable<(ev: Record<string, unknown>) => unknown>,
* diagnosticsSubs: Iterable<(ev: Record<string, unknown>) => unknown>,
* sessionId: string,
* lifecycleSchemaVersion: string | number
* }} deps
*/
export function createBooterBootEmitter(deps) {
const {
bootReadyStateRef,
bootStartedMs,
bootHrtimeNowNs = null,
bootEventSubs,
diagnosticsSubs,
sessionId,
lifecycleSchemaVersion
} = deps
function emitBooterBootStep(step) {
const p = String(step)
const arr = bootReadyStateRef.booterPhases
const arrSt = bootReadyStateRef.booterStages
if (!arr.includes(p)) arr.push(p)
if (!arrSt.includes(p)) arrSt.push(p)
const ev = {
type: 'boot',
phase: 'booter:' + p,
ms: Date.now() - bootStartedMs,
ts: Date.now(),
sessionId,
lifecycleSchemaVersion,
...(bootHrtimeNowNs ? { monotonicNs: String(bootHrtimeNowNs()) } : {})
}
for (const fn of bootEventSubs) {
Promise.resolve(fn(ev)).catch(() => {})
}
for (const fn of diagnosticsSubs) {
Promise.resolve(fn({ ...ev, source: 'booter' })).catch(() => {})
}
}
function emitBooterBootPhase(phase) {
return emitBooterBootStep(phase)
}
return { emitBooterBootStep, emitBooterBootPhase }
}
@@ -0,0 +1,134 @@
/**
* Booter /proc snapshot helpers used while constructing executeKernel ctx.
*/
import {
bareOsProcBlindPeerRelayHintsExposed,
buildBareOsBlindRelaySwarmRuntime
} from './bare-os-proc-blind-peer-relay-gate.js'
import { bareOsChatMuxEnabled } from './bare-os-chat-service.js'
import { bareOsMeshdropMuxEnabled } from './bare-os-meshdrop-service.js'
/**
* @param {{
* interactiveCtxRef: { ctx: Record<string, unknown> | null },
* disk: Record<string, unknown>,
* shellEnv: Record<string, string | undefined>,
* hostEnv?: Record<string, string | undefined> | null
* }} deps
*/
export function createBooterProcHelpers(deps) {
const { interactiveCtxRef, disk, shellEnv, hostEnv = null } = deps
function bareOsCollectLogicalFdRows() {
const c = interactiveCtxRef.ctx
const o = c?.bareOsLogicalFds
if (!o || typeof o !== 'object') return []
return Object.keys(o)
.filter((k) => /^[0-9]+$/.test(k) && k !== '0' && k !== '1' && k !== '2')
.sort((a, b) => Number(a) - Number(b))
.map((fdNum) => ({
fd: Number(fdNum),
target: String(/** @type {Record<string, string>} */ (o)[fdNum] ?? '')
}))
}
function bareOsLiveShellJobsAndIpcStats() {
const live = interactiveCtxRef.ctx
const shellJobs =
live &&
live.shellBackgroundJobs &&
typeof live.shellBackgroundJobs === 'object' &&
Array.isArray(live.shellBackgroundJobs.list)
? live.shellBackgroundJobs.list
: []
let ipcStats
try {
ipcStats =
live && live.bareOsIpc && typeof live.bareOsIpc.stats === 'function'
? live.bareOsIpc.stats()
: undefined
} catch {
ipcStats = undefined
}
return { shellJobs, ipcStats }
}
function bareOsBlindRelayRuntimeHintsForProc() {
if (!bareOsProcBlindPeerRelayHintsExposed(shellEnv)) return undefined
const peers = disk.peers ? [...disk.peers] : []
return {
swarm: buildBareOsBlindRelaySwarmRuntime(shellEnv, peers, {
hasSystemDrive: !!disk.drive,
hasPersonalDrive: !!disk.personalDrive,
auxiliaryDriveCount: Array.isArray(disk.auxiliaryDrives)
? disk.auxiliaryDrives.length
: 0
})
}
}
/**
* Same object as `/proc/bare_os/chat.json` (single source for VFS + ctx hook).
* @returns {Record<string, unknown>}
*/
function bareOsChatProcSnapshotRecord() {
const svc = disk.bareOsChatService
if (!svc) {
return {
schema: 1,
note: 'Swarm chat is off when BARE_OS_PROTOMUX_CHAT_CHANNEL=0/false/off (stock default is on).'
}
}
return {
schema: 1,
atMs: Date.now(),
protocol: svc.PROTOCOL_CHAT_CHANNEL_NAME,
metrics: svc.snapshotMetrics(),
protomuxChatRxTotal:
typeof disk.protomuxChatChannelRxTotal === 'number'
? disk.protomuxChatChannelRxTotal
: 0,
swarmPeers:
disk.peers && typeof disk.peers.size === 'number' ? disk.peers.size : 0,
muxEnabled:
bareOsChatMuxEnabled(shellEnv) || bareOsChatMuxEnabled(hostEnv || {})
}
}
/**
* Same object as `/proc/bare_os/meshdrop.json`.
* @returns {Record<string, unknown>}
*/
function bareOsMeshdropProcSnapshotRecord() {
const svc = disk.bareOsMeshdropService
if (!svc) {
return {
schema: 1,
note: 'Meshdrop channel is off when BARE_OS_PROTOMUX_MESHDROP_CHANNEL=0/false/off (stock default is on).'
}
}
return {
schema: 1,
atMs: Date.now(),
protocol: svc.PROTOCOL_MESHDROP_CHANNEL_NAME,
metrics: svc.snapshotMetrics(),
protomuxMeshdropRxTotal:
typeof disk.protomuxMeshdropChannelRxTotal === 'number'
? disk.protomuxMeshdropChannelRxTotal
: 0,
swarmPeers:
disk.peers && typeof disk.peers.size === 'number' ? disk.peers.size : 0,
muxEnabled:
bareOsMeshdropMuxEnabled(shellEnv) ||
bareOsMeshdropMuxEnabled(hostEnv || {})
}
}
return {
bareOsCollectLogicalFdRows,
bareOsLiveShellJobsAndIpcStats,
bareOsBlindRelayRuntimeHintsForProc,
bareOsChatProcSnapshotRecord,
bareOsMeshdropProcSnapshotRecord
}
}
@@ -0,0 +1,55 @@
/**
* Loader audit NDJSON under /run/bare-os/loader-audit.ndjson.
*/
import b4a from 'b4a'
/**
* @param {{
* env: Record<string, string | undefined>,
* vfs: { readFile?: Function, writeFile?: Function } | null | undefined,
* sessionId: string
* }} deps
*/
export function createKernelLoaderAuditAppend(deps) {
const { env, vfs, sessionId } = deps
/**
* Cap-gated via BARE_OS_LOADER_AUDIT.
* @param {Record<string, unknown>} rec
*/
return async function appendKernelLoaderAuditLine(rec) {
if (
env.BARE_OS_LOADER_AUDIT !== '1' &&
env.BARE_OS_LOADER_AUDIT !== 'true'
)
return
if (
!vfs ||
typeof vfs.readFile !== 'function' ||
typeof vfs.writeFile !== 'function'
)
return
const line =
JSON.stringify({
type: 'loader_audit',
ts: Date.now(),
sessionId,
...rec
}) + '\n'
try {
let prev = ''
try {
const buf = await vfs.readFile('/run/bare-os/loader-audit.ndjson')
prev = b4a.toString(buf)
} catch {
/* new */
}
await vfs.writeFile(
'/run/bare-os/loader-audit.ndjson',
b4a.from(prev + line)
)
} catch {
/* ignore */
}
}
}
@@ -0,0 +1,62 @@
/**
* Logical signal delivery for synthetic PIDs (guest has no host signals).
*/
import {
BARE_OS_SIGNAL_EXIT,
BARE_OS_SESSION_EXIT_SIGNALS
} from './bare-os-posix-signals.js'
/**
* @param {{
* dispatchShellTrapSignal: (ctx: Record<string, unknown>, sig: string) => unknown,
* virtualSignalState: Map<number, { signal: string, atMs: number }>
* }} deps
*/
export function createBareOsVirtualSignalDeliverer(deps) {
const { dispatchShellTrapSignal, virtualSignalState } = deps
/**
* @param {Record<string, unknown>} ctx
* @param {number} pid
* @param {string} sig normalized signal short name (e.g. TERM)
*/
return function deliverBareOsVirtualSignalToPid(ctx, pid, sig) {
const extendedSynthetic = Number.isFinite(pid) && pid >= 4100 && pid < 9000
if (sig === '0') {
return { ok: true, delivered: false, exists: true }
}
if (
ctx.bareOsLogicalSigaction &&
ctx.bareOsLogicalSigaction[sig] === 'IGNORE'
) {
return {
ok: true,
delivered: false,
ignored: true,
atMs: Date.now()
}
}
const atMs = Date.now()
if (
pid === 3 &&
(sig === 'INT' || sig === 'TERM' || sig === 'USR1' || sig === 'USR2')
) {
Promise.resolve(dispatchShellTrapSignal(ctx, sig)).catch(() => {})
}
virtualSignalState.set(pid, { signal: sig, atMs })
if (typeof globalThis.process?.emit === 'function') {
try {
globalThis.process.emit('bare-os:signal', { pid, signal: sig, atMs })
} catch {
/* ignore */
}
}
const noSessionExit =
extendedSynthetic || !BARE_OS_SESSION_EXIT_SIGNALS.has(sig)
if (!noSessionExit) {
const signum = BARE_OS_SIGNAL_EXIT[sig] || 15
ctx.requestBooterExit(128 + signum)
}
return { ok: true, delivered: true, atMs }
}
}
@@ -0,0 +1,224 @@
/**
* Shell builtin name table and optional POSIX `read` builtin.
*/
export const SHELL_BUILTINS = new Set([
'alias',
'unalias',
'barerc',
'cd',
'export',
'unset',
'readonly',
'umask',
'set',
':',
'command',
'type',
'logout',
'exit',
'jobs',
'fg',
'bg',
'wait',
'suspend-job',
'disown',
'trap',
'test',
'['
])
/**
* Optional POSIX-style **`read`** builtin (bounded line, IFS split). Off by default.
* @param {Record<string, string | undefined> | null | undefined} env
*/
export function bareOsShellReadBuiltinEnabled(env) {
const o = env && typeof env === 'object' ? env : {}
return o.BARE_OS_SHELL_READ_BUILTIN === '1' || o.BARE_OS_SHELL_READ_BUILTIN === 'true'
}
/**
* Sorted list of shell builtin command names for completion / UX.
* Includes **`read`** only when {@link bareOsShellReadBuiltinEnabled} is true.
* @param {Record<string, string | undefined> | null | undefined} env
* @returns {string[]}
*/
export function listBareOsShellBuiltins(env) {
const out = [...SHELL_BUILTINS]
if (bareOsShellReadBuiltinEnabled(env)) out.push('read')
out.sort()
return out
}
/**
* @param {string} cmd
* @param {Record<string, string | undefined> | null | undefined} env
*/
export function isShellBuiltin(cmd, env) {
if (SHELL_BUILTINS.has(cmd)) return true
if (cmd === 'read' && bareOsShellReadBuiltinEnabled(env)) return true
return false
}
/**
* @param {string} line
* @param {string} ifs
* @param {number} nNames
*/
export function bareOsShellReadSplitFields(line, ifs, nNames) {
const sep = ifs.length ? ifs[0] : ' '
if (nNames <= 1) return [line]
const out = []
let rest = line
for (let i = 0; i < nNames - 1; i++) {
const idx = rest.indexOf(sep)
if (idx === -1) {
out.push(rest)
rest = ''
break
}
out.push(rest.slice(0, idx))
rest = rest.slice(idx + sep.length)
}
while (out.length < nNames - 1) out.push('')
out.push(rest)
return out
}
/**
* @param {Record<string, unknown>} ctx
* @param {number} maxBytes
* @param {(m: string) => void} errFn
* @returns {Promise<string | null>} null = EOF / error
*/
export async function bareOsShellReadOneLine(ctx, maxBytes, errFn, opts = {}) {
const delimiter = typeof opts.delimiter === 'string' ? opts.delimiter : '\n'
const timeoutMs = Number.isFinite(opts.timeoutMs) ? Number(opts.timeoutMs) : 0
if (typeof ctx.shellStdin === 'string') {
const raw = ctx.shellStdin
const idx = delimiter ? raw.indexOf(delimiter) : -1
const line = idx === -1 ? raw : raw.slice(0, idx)
ctx.shellStdin = idx === -1 ? '' : raw.slice(idx + delimiter.length)
if (line.length > maxBytes) {
errFn(`read: line exceeds BARE_OS_SHELL_READ_MAX_BYTES (${maxBytes})`)
return null
}
return line
}
const rl = ctx.readLine
if (typeof rl === 'function') {
const readP = rl('')
const ln =
timeoutMs > 0
? await Promise.race([
readP,
new Promise((resolve) => setTimeout(() => resolve(null), timeoutMs))
])
: await readP
if (ln == null) return null
if (ln.length > maxBytes) {
errFn(`read: line exceeds BARE_OS_SHELL_READ_MAX_BYTES (${maxBytes})`)
return null
}
return ln
}
errFn(
'read: no input (redirect stdin, use a pipeline, or interactive readLine)'
)
return null
}
/**
* @param {Record<string, unknown>} ctx
* @param {string[]} argv
* @param {Record<string, string>} env
* @param {(m: string) => void} origErr
*/
export async function runShellReadBuiltin(ctx, argv, env, origErr) {
let i = 1
let rawMode = false
let delimiter = '\n'
let timeoutMs = 0
while (i < argv.length && argv[i].startsWith('-')) {
const a = argv[i]
if (a === '-r') {
rawMode = true
}
else if (a === '-d') {
const d = argv[i + 1]
if (d == null) {
origErr.call(ctx.console, 'read: option requires an argument -- d')
ctx.exitCode = 2
return
}
delimiter = String(d).slice(0, 1)
i++
}
else if (a === '-t') {
const v = argv[i + 1]
if (v == null) {
origErr.call(ctx.console, 'read: option requires an argument -- t')
ctx.exitCode = 2
return
}
const n = Number.parseFloat(String(v))
if (!Number.isFinite(n) || n < 0) {
origErr.call(ctx.console, 'read: invalid timeout: ' + String(v))
ctx.exitCode = 2
return
}
timeoutMs = Math.min(Math.floor(n * 1000), 120000)
i++
}
else if (a === '--') {
i++
break
} else {
origErr.call(ctx.console, 'read: unsupported option: ' + a)
ctx.exitCode = 2
return
}
i++
}
const names = argv.slice(i).filter(Boolean)
if (!names.length) names.push('REPLY')
const maxRaw = env.BARE_OS_SHELL_READ_MAX_BYTES
const maxParsed =
maxRaw != null && String(maxRaw).trim() !== ''
? Number.parseInt(String(maxRaw), 10)
: 65536
const maxBytes =
Number.isFinite(maxParsed) && maxParsed > 0
? Math.min(maxParsed, 2_000_000)
: 65536
let line = await bareOsShellReadOneLine(
ctx,
maxBytes,
(m) => origErr.call(ctx.console, m),
{ delimiter, timeoutMs }
)
if (line === null) {
for (const n of names) env[n] = ''
ctx.exitCode = 1
return
}
if (!rawMode) {
line = line.replace(/\\(.)/g, '$1')
}
const ifs =
env.IFS !== undefined && env.IFS !== null ? String(env.IFS) : ' \t\n'
const fields = bareOsShellReadSplitFields(line, ifs, names.length)
for (let j = 0; j < names.length; j++) {
const k = names[j]
if (
ctx.shellReadonlyVars instanceof Set &&
ctx.shellReadonlyVars.has(k)
) {
origErr.call(ctx.console, k + ': readonly variable')
ctx.exitCode = 1
return
}
env[k] = fields[j] ?? ''
}
ctx.exitCode = 0
}
+366
View File
@@ -0,0 +1,366 @@
/**
* Shell parameter / arithmetic word expansion (`$VAR`, `${…}`, `$((…))`).
*/
import { findArithmeticClose } from './shell-lex.js'
import { shellCheckUnboundParam } from './shell-nounset.js'
import { bareOsEvalArithmeticExpr } from './shell-arithmetic.js'
import { BARE_OS_EXIT_STATUS_ENV } from './shell-runtime.js'
/**
* @param {string} inner
* @param {Record<string, string>} env
*/
function expandParamBracedInner(inner, env, depth = 0) {
const expandNested = (s) => {
const txt = String(s ?? '')
if (!txt.includes('$')) return txt
return expandWord(txt, env, depth + 1)
}
const tr = inner.trim()
const lenParam = /^#([A-Za-z_][A-Za-z0-9_]*)$/.exec(tr)
if (lenParam) {
shellCheckUnboundParam(lenParam[1], env)
return String(String(env[lenParam[1]] ?? '').length)
}
const indirectOn =
env &&
(env.BARE_OS_SHELL_INDIRECT_EXPANSION === '1' ||
env.BARE_OS_SHELL_INDIRECT_EXPANSION === 'true')
const indirectName = /^!([A-Za-z_][A-Za-z0-9_]*)$/.exec(tr)
if (indirectOn && indirectName) {
const ref = String(env[indirectName[1]] ?? '')
shellCheckUnboundParam(ref, env)
return String(env[ref] ?? '')
}
const paramV2 =
env &&
(env.BARE_OS_SHELL_PARAM_EXPANSION_V2 === '1' ||
env.BARE_OS_SHELL_PARAM_EXPANSION_V2 === 'true')
const paramV3 =
env &&
(env.BARE_OS_SHELL_PARAM_EXPANSION_V3 === '1' ||
env.BARE_OS_SHELL_PARAM_EXPANSION_V3 === 'true')
const errIdx = inner.indexOf(':?')
if (paramV3 && errIdx > 0) {
const name = inner.slice(0, errIdx).trim()
const msg = inner.slice(errIdx + 2)
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
const v = env[name]
if (v == null || String(v) === '') {
throw new Error(expandNested(msg) || 'parameter null or unset')
}
return String(v)
}
}
const assignIdx = inner.indexOf(':=')
if (paramV2 && assignIdx > 0) {
const name = inner.slice(0, assignIdx).trim()
const alt = inner.slice(assignIdx + 2)
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
let v = env[name]
if (v == null || String(v) === '') {
const ex = expandNested(alt)
env[name] = ex
v = ex
}
return String(v ?? '')
}
}
const posixUnsetOnly =
env &&
(env.BARE_OS_SHELL_POSIX_UNSET_ONLY_DEFAULT === '1' ||
env.BARE_OS_SHELL_POSIX_UNSET_ONLY_DEFAULT === 'true')
if (posixUnsetOnly) {
const hy = inner.indexOf('-')
if (
hy > 0 &&
inner.slice(hy - 1, hy + 1) !== ':-' &&
!inner.includes(':')
) {
const m = /^([A-Za-z_][A-Za-z0-9_]*)-(.+)$/.exec(inner)
if (m && m[1] && m[2] != null) {
const name = m[1]
const alt = m[2]
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
if (!Object.prototype.hasOwnProperty.call(env, name))
return expandNested(alt)
return String(env[name] ?? '')
}
}
}
}
const idx = inner.indexOf(':-')
if (idx > 0) {
const name = inner.slice(0, idx).trim()
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
const alt = inner.slice(idx + 2)
const v = env[name]
if (v != null && String(v) !== '') return String(v)
return expandNested(alt)
}
}
const plusIdx = inner.indexOf(':+')
if (paramV3 && plusIdx > 0) {
const name = inner.slice(0, plusIdx).trim()
const alt = inner.slice(plusIdx + 2)
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
const v = env[name]
if (v != null && String(v) !== '') return expandNested(alt)
return ''
}
}
if (paramV3) {
const sliceRe = /^([A-Za-z_][A-Za-z0-9_]*):(\d+)(?::(\d+))?$/.exec(tr)
if (sliceRe) {
const name = sliceRe[1]
const off = Number.parseInt(sliceRe[2], 10)
const ln =
sliceRe[3] != null ? Number.parseInt(sliceRe[3], 10) : undefined
const v = String(env[name] ?? '')
let out = Number.isFinite(off) ? v.slice(off) : v
if (ln != null && Number.isFinite(ln)) out = out.slice(0, ln)
return out
}
const globalRepl = /^([A-Za-z_][A-Za-z0-9_]*)\/\/(.*)\/(.*)$/.exec(tr)
if (globalRepl && globalRepl[2].length <= 256 && globalRepl[3].length <= 512) {
const name = globalRepl[1]
let v = String(env[name] ?? '')
const pat = globalRepl[2]
const rep = expandNested(globalRepl[3])
try {
const re = new RegExp(pat, 'g')
v = v.replace(re, rep)
} catch {
/* invalid regex — leave value */
}
return v
}
}
if (paramV2) {
const longPref = /^([A-Za-z_][A-Za-z0-9_]*)##(.+)$/.exec(inner)
if (longPref && longPref[2].length > 0 && longPref[2].length <= 128) {
const v = String(env[longPref[1]] ?? '')
const pat = longPref[2]
if (pat === '*/') {
const i = v.lastIndexOf('/')
return i >= 0 ? v.slice(i + 1) : v
}
let end = -1
for (let i = 0; i <= v.length - pat.length; i++) {
if (v.slice(i, i + pat.length) === pat) end = i + pat.length
}
return end >= 0 ? v.slice(end) : v
}
const shortPref = /^([A-Za-z_][A-Za-z0-9_]*)#(.+)$/.exec(inner)
if (shortPref && shortPref[2].length > 0 && shortPref[2].length <= 128) {
const v = String(env[shortPref[1]] ?? '')
const pat = shortPref[2]
if (pat === '*/') {
const i = v.indexOf('/')
return i >= 0 ? v.slice(i + 1) : v
}
const i = v.indexOf(pat)
return i >= 0 ? v.slice(i + pat.length) : v
}
const longSuf = /^([A-Za-z_][A-Za-z0-9_]*)%%(.+)$/.exec(inner)
if (longSuf && longSuf[2].length > 0 && longSuf[2].length <= 128) {
const v = String(env[longSuf[1]] ?? '')
const pat = longSuf[2]
if (!/[?*[]/.test(pat) && v.endsWith(pat))
return v.slice(0, v.length - pat.length)
if (pat.includes('*') && !pat.includes('[') && !pat.includes('?')) {
const parts = pat.split('*')
if (parts.length === 2) {
const a = parts[0]
const b = parts[1]
let best = -1
for (let len = 1; len <= v.length; len++) {
const suf = v.slice(v.length - len)
if (
suf.startsWith(a) &&
suf.endsWith(b) &&
suf.length >= a.length + b.length
) {
if (best < 0 || len > best) best = len
}
}
if (best > 0) return v.slice(0, v.length - best)
}
}
return v
}
const shortSuf = /^([A-Za-z_][A-Za-z0-9_]*)%(.+)$/.exec(inner)
if (shortSuf && shortSuf[2].length > 0 && shortSuf[2].length <= 128) {
const v = String(env[shortSuf[1]] ?? '')
const pat = shortSuf[2]
if (!/[?*[]/.test(pat) && v.endsWith(pat))
return v.slice(0, v.length - pat.length)
if (pat.includes('*') && !pat.includes('[') && !pat.includes('?')) {
const parts = pat.split('*')
if (parts.length === 2) {
const a = parts[0]
const b = parts[1]
let best = -1
for (let len = 1; len <= v.length; len++) {
const suf = v.slice(v.length - len)
if (
suf.startsWith(a) &&
suf.endsWith(b) &&
suf.length >= a.length + b.length
) {
if (best < 0 || len < best) best = len
}
}
if (best > 0) return v.slice(0, v.length - best)
}
}
return v
}
}
const hash = inner.indexOf('#')
if (hash > 0) {
const name = inner.slice(0, hash).trim()
const pref = inner.slice(hash + 1)
if (
/^[A-Za-z_][A-Za-z0-9_]*$/.test(name) &&
pref.length > 0 &&
pref.length <= 128
) {
const v = String(env[name] ?? '')
return v.startsWith(pref) ? v.slice(pref.length) : v
}
}
const keySimple = inner.trim()
shellCheckUnboundParam(keySimple, env)
return env[keySimple] ?? ''
}
/**
* @param {string} s
* @param {Record<string, string>} env
*/
export function expandWord(s, env, depth = 0) {
const maxDepthRaw = Number.parseInt(
String(env?.BARE_OS_SHELL_EXPANSION_MAX_DEPTH || '32'),
10
)
const maxDepth =
Number.isFinite(maxDepthRaw) && maxDepthRaw > 0 ? Math.min(maxDepthRaw, 256) : 32
if (depth > maxDepth) {
throw new Error(`shell: expansion recursion too deep (max ${maxDepth})`)
}
const paramExpOn =
env &&
(env.BARE_OS_SHELL_PARAM_EXPANSION === '1' ||
env.BARE_OS_SHELL_PARAM_EXPANSION === 'true')
const paramV2 =
env &&
(env.BARE_OS_SHELL_PARAM_EXPANSION_V2 === '1' ||
env.BARE_OS_SHELL_PARAM_EXPANSION_V2 === 'true')
const paramV3 =
env &&
(env.BARE_OS_SHELL_PARAM_EXPANSION_V3 === '1' ||
env.BARE_OS_SHELL_PARAM_EXPANSION_V3 === 'true')
let out = ''
let j = 0
while (j < s.length) {
if (s[j] === '$') {
if (s[j + 1] === '(' && s[j + 2] === '(') {
const close = findArithmeticClose(s, j + 3)
if (close < 0) {
out += s.slice(j)
break
}
const inner = s.slice(j + 3, close)
try {
out += bareOsEvalArithmeticExpr(inner, env)
} catch (e) {
if (
env?.BARE_OS_SHELL_POSIX_MODE === '1' ||
env?.BARE_OS_SHELL_POSIX_MODE === 'true'
) {
throw new Error(
'shell: arithmetic: invalid token (POSIX mode strict arithmetic)'
)
}
throw e
}
j = close + 2
continue
}
if (s[j + 1] === '{') {
const end = s.indexOf('}', j + 2)
if (end === -1) {
out += s.slice(j)
break
}
const inner = s.slice(j + 2, end)
const tr0 = inner.trim()
const indirectOnBr =
env?.BARE_OS_SHELL_INDIRECT_EXPANSION === '1' ||
env?.BARE_OS_SHELL_INDIRECT_EXPANSION === 'true'
if (inner === '?') {
out += env[BARE_OS_EXIT_STATUS_ENV] ?? '0'
} else if (
/^#[A-Za-z_][A-Za-z0-9_]*$/.test(tr0) ||
(indirectOnBr && /^![A-Za-z_][A-Za-z0-9_]*$/.test(tr0)) ||
(paramExpOn &&
(inner.includes(':-') ||
(paramV3 && (inner.includes(':+') || inner.includes(':?'))) ||
(paramV3 &&
(/^[A-Za-z_][A-Za-z0-9_]*:\d/.test(tr0) ||
/^[A-Za-z_][A-Za-z0-9_]*\/\//.test(inner))) ||
(paramV2 &&
(inner.includes(':=') ||
/^[A-Za-z_][A-Za-z0-9_]*##/.test(inner) ||
/^[A-Za-z_][A-Za-z0-9_]*%%/.test(inner) ||
/^[A-Za-z_][A-Za-z0-9_]*%[^%]/.test(inner) ||
/^[A-Za-z_][A-Za-z0-9_]*#[^#]/.test(inner))) ||
(/^[A-Za-z_][A-Za-z0-9_]*#/.test(inner) && inner.includes('#'))))
) {
out += expandParamBracedInner(inner, env, depth + 1)
} else {
const ik = inner.trim()
shellCheckUnboundParam(ik, env)
out += env[ik] ?? ''
}
j = end + 1
continue
}
if (s[j + 1] === '?') {
out += env[BARE_OS_EXIT_STATUS_ENV] ?? '0'
j += 2
continue
}
if (/[0-9]/.test(s[j + 1] ?? '')) {
const pn = s[j + 1]
shellCheckUnboundParam(pn, env)
out += env[pn] ?? ''
j += 2
continue
}
let k = j + 1
while (k < s.length && /[A-Za-z0-9_]/.test(s[k])) k++
const name = s.slice(j + 1, k)
if (name) {
shellCheckUnboundParam(name, env)
out += env[name] ?? ''
j = k
} else {
out += '$'
j++
}
continue
}
out += s[j++]
}
return out
}
@@ -0,0 +1,146 @@
/**
* Shell runtime helpers: exit-status env, pipeline caps, child merge, audit.
*/
/** Env key mirroring last command exit status (POSIX `$?` parity). */
export const BARE_OS_EXIT_STATUS_ENV = 'BARE_OS_EXIT_STATUS'
/**
* Mirror `ctx.exitCode` into `ctx.vfs.env` so kernels and `echo $?` see last status.
* @param {Record<string, unknown>} ctx
*/
export function syncBareOsExitStatusEnv(ctx) {
const env = ctx.vfs?.env
if (!env || typeof env !== 'object') return
const n = Number(ctx.exitCode)
env[BARE_OS_EXIT_STATUS_ENV] = String(Number.isFinite(n) ? n : 0)
}
/** Default caps for simulated pipeline capture (`console.log` between stages). */
export const DEFAULT_PIPELINE_MAX_STAGES = 32
export const DEFAULT_PIPELINE_MAX_CAPTURE_BYTES = 2 * 1024 * 1024
export const DEFAULT_PIPELINE_MAX_CAPTURE_LINES = 50000
/**
* Resolved simulated pipeline limits for the current `vfs.env` / session env.
* @param {Record<string, string> | null | undefined} env
*/
export function getBareOsPipelineLimits(env) {
const o = env && typeof env === 'object' ? env : {}
const parse = (key, def) => {
const v = o[key]
if (v == null || v === '') return def
const n = Number.parseInt(String(v), 10)
return Number.isFinite(n) && n > 0 ? n : def
}
/** Upper bounds on simulated capture (after streaming multiplier); tunable for high-RAM hosts. */
const absMaxBytes = parse(
'BARE_OS_PIPELINE_ABS_MAX_BYTES',
512 * 1024 * 1024
)
const absMaxLines = parse('BARE_OS_PIPELINE_ABS_MAX_LINES', 2_000_000)
const streamOn =
o.BARE_OS_SHELL_STREAMING === '1' || o.BARE_OS_SHELL_STREAMING === 'true'
const multRaw = Number.parseFloat(
String(o.BARE_OS_SHELL_STREAMING_MULT || '4')
)
const mult =
streamOn && Number.isFinite(multRaw) && multRaw > 1
? Math.min(multRaw, 16)
: 1
const baseBytes = parse(
'BARE_OS_PIPELINE_MAX_BYTES',
DEFAULT_PIPELINE_MAX_CAPTURE_BYTES
)
const baseLines = parse(
'BARE_OS_PIPELINE_MAX_LINES',
DEFAULT_PIPELINE_MAX_CAPTURE_LINES
)
const effectiveBytes = Math.floor(baseBytes * mult)
const effectiveLines = Math.floor(baseLines * mult)
return {
maxStages: parse(
'BARE_OS_PIPELINE_MAX_STAGES',
DEFAULT_PIPELINE_MAX_STAGES
),
maxBytes: Math.min(effectiveBytes, absMaxBytes),
maxLines: Math.min(effectiveLines, absMaxLines),
streamingMultiplier: mult,
/** True when `BARE_OS_SHELL_STREAMING` relaxes caps via multiplier. */
streamingEnabled: streamOn,
/** Parsed `BARE_OS_PIPELINE_MAX_*` before multiplier (for operator snapshots). */
baseMaxBytes: baseBytes,
baseMaxLines: baseLines,
/** Hard ceilings after multiplier (`BARE_OS_PIPELINE_ABS_MAX_*`; defaults 512MiB / 2M lines). */
absCapBytes: absMaxBytes,
absCapLines: absMaxLines
}
}
/**
* When `suspend-job` sets `stopped` on a background entry, yield between statements
* until `fg` / `bg` clears it (cooperative logical job control; no host SIGTSTP).
* @param {{ stopped?: boolean }} entry
*/
export function waitWhileShellJobStopped(entry) {
if (!entry || !entry.stopped) return Promise.resolve()
return new Promise((resolve) => {
const id = setInterval(() => {
if (!entry.stopped) {
clearInterval(id)
resolve(undefined)
}
}, 10)
})
}
/**
* @param {Record<string, unknown>} ctx
* @param {string} event
* @param {Record<string, unknown>} payload
*/
export function appendShellAuditEvent(ctx, event, payload = {}) {
if (!Array.isArray(ctx.shellAuditEvents)) ctx.shellAuditEvents = []
ctx.shellAuditEvents.push({
schema: 1,
ts: Date.now(),
event,
...payload
})
}
/**
* @param {Record<string, unknown>} ctx
* @param {Record<string, unknown>} childCtx
*/
export function mergeChildExitCode(ctx, childCtx) {
if (childCtx.exitCode !== undefined && childCtx.exitCode !== null) {
ctx.exitCode = childCtx.exitCode
}
}
/**
* @param {unknown} identity
* @returns {identity is { state: unknown, publicKey: unknown, secretKey: unknown }}
*/
export function isMergeableIdentitySession(identity) {
if (!identity || typeof identity !== 'object') return false
return (
Object.prototype.hasOwnProperty.call(identity, 'state') &&
Object.prototype.hasOwnProperty.call(identity, 'publicKey') &&
Object.prototype.hasOwnProperty.call(identity, 'secretKey')
)
}
/**
* Merge shell child-command side effects we intentionally allow to flow back.
* Today this includes exit status and identity session state.
* @param {Record<string, unknown>} ctx
* @param {Record<string, unknown>} childCtx
*/
export function mergePipelineChildCtx(ctx, childCtx) {
mergeChildExitCode(ctx, childCtx)
if (isMergeableIdentitySession(childCtx.identity)) {
ctx.identity = /** @type {Record<string, unknown>} */ (childCtx.identity)
}
}
+25 -723
View File
@@ -38,376 +38,37 @@ import {
parseShellFunctionDeclaration,
casePatternMatches
} from './shell-syntax.js'
import {
isShellBuiltin,
runShellReadBuiltin
} from './shell-builtins.js'
import {
syncBareOsExitStatusEnv,
getBareOsPipelineLimits,
waitWhileShellJobStopped,
appendShellAuditEvent,
mergePipelineChildCtx
} from './shell-runtime.js'
import { expandWord } from './shell-expand.js'
export { BARE_OS_SHELL_NOUNSET_ERROR }
const SHELL_BUILTINS = new Set([
'alias',
'unalias',
'barerc',
'cd',
'export',
'unset',
'readonly',
'umask',
'set',
':',
'command',
'type',
'logout',
'exit',
'jobs',
'fg',
'bg',
'wait',
'suspend-job',
'disown',
'trap',
'test',
'['
])
/**
* Optional POSIX-style **`read`** builtin (bounded line, IFS split). Off by default.
* @param {Record<string, string | undefined> | null | undefined} env
*/
export function bareOsShellReadBuiltinEnabled(env) {
const o = env && typeof env === 'object' ? env : {}
return o.BARE_OS_SHELL_READ_BUILTIN === '1' || o.BARE_OS_SHELL_READ_BUILTIN === 'true'
}
/**
* Sorted list of shell builtin command names for completion / UX.
* Includes **`read`** only when {@link bareOsShellReadBuiltinEnabled} is true.
* @param {Record<string, string | undefined> | null | undefined} env
* @returns {string[]}
*/
export function listBareOsShellBuiltins(env) {
const out = [...SHELL_BUILTINS]
if (bareOsShellReadBuiltinEnabled(env)) out.push('read')
out.sort()
return out
}
/**
* @param {string} cmd
* @param {Record<string, string | undefined> | null | undefined} env
*/
function isShellBuiltin(cmd, env) {
if (SHELL_BUILTINS.has(cmd)) return true
if (cmd === 'read' && bareOsShellReadBuiltinEnabled(env)) return true
return false
}
/**
* @param {string} line
* @param {string} ifs
* @param {number} nNames
*/
function bareOsShellReadSplitFields(line, ifs, nNames) {
const sep = ifs.length ? ifs[0] : ' '
if (nNames <= 1) return [line]
const out = []
let rest = line
for (let i = 0; i < nNames - 1; i++) {
const idx = rest.indexOf(sep)
if (idx === -1) {
out.push(rest)
rest = ''
break
}
out.push(rest.slice(0, idx))
rest = rest.slice(idx + sep.length)
}
while (out.length < nNames - 1) out.push('')
out.push(rest)
return out
}
/**
* @param {Record<string, unknown>} ctx
* @param {number} maxBytes
* @param {(m: string) => void} errFn
* @returns {Promise<string | null>} null = EOF / error
*/
async function bareOsShellReadOneLine(ctx, maxBytes, errFn, opts = {}) {
const delimiter = typeof opts.delimiter === 'string' ? opts.delimiter : '\n'
const timeoutMs = Number.isFinite(opts.timeoutMs) ? Number(opts.timeoutMs) : 0
if (typeof ctx.shellStdin === 'string') {
const raw = ctx.shellStdin
const idx = delimiter ? raw.indexOf(delimiter) : -1
const line = idx === -1 ? raw : raw.slice(0, idx)
ctx.shellStdin = idx === -1 ? '' : raw.slice(idx + delimiter.length)
if (line.length > maxBytes) {
errFn(`read: line exceeds BARE_OS_SHELL_READ_MAX_BYTES (${maxBytes})`)
return null
}
return line
}
const rl = ctx.readLine
if (typeof rl === 'function') {
const readP = rl('')
const ln =
timeoutMs > 0
? await Promise.race([
readP,
new Promise((resolve) => setTimeout(() => resolve(null), timeoutMs))
])
: await readP
if (ln == null) return null
if (ln.length > maxBytes) {
errFn(`read: line exceeds BARE_OS_SHELL_READ_MAX_BYTES (${maxBytes})`)
return null
}
return ln
}
errFn(
'read: no input (redirect stdin, use a pipeline, or interactive readLine)'
)
return null
}
/**
* @param {Record<string, unknown>} ctx
* @param {string[]} argv
* @param {Record<string, string>} env
* @param {(m: string) => void} origErr
*/
async function runShellReadBuiltin(ctx, argv, env, origErr) {
let i = 1
let rawMode = false
let delimiter = '\n'
let timeoutMs = 0
while (i < argv.length && argv[i].startsWith('-')) {
const a = argv[i]
if (a === '-r') {
rawMode = true
}
else if (a === '-d') {
const d = argv[i + 1]
if (d == null) {
origErr.call(ctx.console, 'read: option requires an argument -- d')
ctx.exitCode = 2
return
}
delimiter = String(d).slice(0, 1)
i++
}
else if (a === '-t') {
const v = argv[i + 1]
if (v == null) {
origErr.call(ctx.console, 'read: option requires an argument -- t')
ctx.exitCode = 2
return
}
const n = Number.parseFloat(String(v))
if (!Number.isFinite(n) || n < 0) {
origErr.call(ctx.console, 'read: invalid timeout: ' + String(v))
ctx.exitCode = 2
return
}
timeoutMs = Math.min(Math.floor(n * 1000), 120000)
i++
}
else if (a === '--') {
i++
break
} else {
origErr.call(ctx.console, 'read: unsupported option: ' + a)
ctx.exitCode = 2
return
}
i++
}
const names = argv.slice(i).filter(Boolean)
if (!names.length) names.push('REPLY')
const maxRaw = env.BARE_OS_SHELL_READ_MAX_BYTES
const maxParsed =
maxRaw != null && String(maxRaw).trim() !== ''
? Number.parseInt(String(maxRaw), 10)
: 65536
const maxBytes =
Number.isFinite(maxParsed) && maxParsed > 0
? Math.min(maxParsed, 2_000_000)
: 65536
let line = await bareOsShellReadOneLine(
ctx,
maxBytes,
(m) => origErr.call(ctx.console, m),
{ delimiter, timeoutMs }
)
if (line === null) {
for (const n of names) env[n] = ''
ctx.exitCode = 1
return
}
if (!rawMode) {
line = line.replace(/\\(.)/g, '$1')
}
const ifs =
env.IFS !== undefined && env.IFS !== null ? String(env.IFS) : ' \t\n'
const fields = bareOsShellReadSplitFields(line, ifs, names.length)
for (let j = 0; j < names.length; j++) {
const k = names[j]
if (
ctx.shellReadonlyVars instanceof Set &&
ctx.shellReadonlyVars.has(k)
) {
origErr.call(ctx.console, k + ': readonly variable')
ctx.exitCode = 1
return
}
env[k] = fields[j] ?? ''
}
ctx.exitCode = 0
}
/**
* When `suspend-job` sets `stopped` on a background entry, yield between statements
* until `fg` / `bg` clears it (cooperative logical job control; no host SIGTSTP).
* @param {{ stopped?: boolean }} entry
*/
function waitWhileShellJobStopped(entry) {
if (!entry || !entry.stopped) return Promise.resolve()
return new Promise((resolve) => {
const id = setInterval(() => {
if (!entry.stopped) {
clearInterval(id)
resolve(undefined)
}
}, 10)
})
}
/**
* @param {Record<string, unknown>} ctx
* @param {string} event
* @param {Record<string, unknown>} payload
*/
function appendShellAuditEvent(ctx, event, payload = {}) {
if (!Array.isArray(ctx.shellAuditEvents)) ctx.shellAuditEvents = []
ctx.shellAuditEvents.push({
schema: 1,
ts: Date.now(),
event,
...payload
})
}
/**
* @param {Record<string, unknown>} ctx
* @param {Record<string, unknown>} childCtx
*/
function mergeChildExitCode(ctx, childCtx) {
if (childCtx.exitCode !== undefined && childCtx.exitCode !== null) {
ctx.exitCode = childCtx.exitCode
}
}
/**
* @param {unknown} identity
* @returns {identity is { state: unknown, publicKey: unknown, secretKey: unknown }}
*/
function isMergeableIdentitySession(identity) {
if (!identity || typeof identity !== 'object') return false
return (
Object.prototype.hasOwnProperty.call(identity, 'state') &&
Object.prototype.hasOwnProperty.call(identity, 'publicKey') &&
Object.prototype.hasOwnProperty.call(identity, 'secretKey')
)
}
/**
* Merge shell child-command side effects we intentionally allow to flow back.
* Today this includes exit status and identity session state.
* @param {Record<string, unknown>} ctx
* @param {Record<string, unknown>} childCtx
*/
function mergePipelineChildCtx(ctx, childCtx) {
mergeChildExitCode(ctx, childCtx)
if (isMergeableIdentitySession(childCtx.identity)) {
ctx.identity = /** @type {Record<string, unknown>} */ (childCtx.identity)
}
}
/** Env key mirroring last command exit status (POSIX `$?` parity). */
export const BARE_OS_EXIT_STATUS_ENV = 'BARE_OS_EXIT_STATUS'
/**
* Mirror `ctx.exitCode` into `ctx.vfs.env` so kernels and `echo $?` see last status.
* @param {Record<string, unknown>} ctx
*/
export function syncBareOsExitStatusEnv(ctx) {
const env = ctx.vfs?.env
if (!env || typeof env !== 'object') return
const n = Number(ctx.exitCode)
env[BARE_OS_EXIT_STATUS_ENV] = String(Number.isFinite(n) ? n : 0)
}
export {
bareOsShellReadBuiltinEnabled,
listBareOsShellBuiltins
} from './shell-builtins.js'
export {
BARE_OS_EXIT_STATUS_ENV,
syncBareOsExitStatusEnv,
DEFAULT_PIPELINE_MAX_STAGES,
DEFAULT_PIPELINE_MAX_CAPTURE_BYTES,
DEFAULT_PIPELINE_MAX_CAPTURE_LINES,
getBareOsPipelineLimits
} from './shell-runtime.js'
export { expandWord } from './shell-expand.js'
/** Max alias indirections (prevents cycles). */
const MAX_ALIAS_DEPTH = 16
/** Default caps for simulated pipeline capture (`console.log` between stages). */
export const DEFAULT_PIPELINE_MAX_STAGES = 32
export const DEFAULT_PIPELINE_MAX_CAPTURE_BYTES = 2 * 1024 * 1024
export const DEFAULT_PIPELINE_MAX_CAPTURE_LINES = 50000
/**
* Resolved simulated pipeline limits for the current `vfs.env` / session env.
* @param {Record<string, string> | null | undefined} env
*/
export function getBareOsPipelineLimits(env) {
const o = env && typeof env === 'object' ? env : {}
const parse = (key, def) => {
const v = o[key]
if (v == null || v === '') return def
const n = Number.parseInt(String(v), 10)
return Number.isFinite(n) && n > 0 ? n : def
}
/** Upper bounds on simulated capture (after streaming multiplier); tunable for high-RAM hosts. */
const absMaxBytes = parse(
'BARE_OS_PIPELINE_ABS_MAX_BYTES',
512 * 1024 * 1024
)
const absMaxLines = parse('BARE_OS_PIPELINE_ABS_MAX_LINES', 2_000_000)
const streamOn =
o.BARE_OS_SHELL_STREAMING === '1' || o.BARE_OS_SHELL_STREAMING === 'true'
const multRaw = Number.parseFloat(
String(o.BARE_OS_SHELL_STREAMING_MULT || '4')
)
const mult =
streamOn && Number.isFinite(multRaw) && multRaw > 1
? Math.min(multRaw, 16)
: 1
const baseBytes = parse(
'BARE_OS_PIPELINE_MAX_BYTES',
DEFAULT_PIPELINE_MAX_CAPTURE_BYTES
)
const baseLines = parse(
'BARE_OS_PIPELINE_MAX_LINES',
DEFAULT_PIPELINE_MAX_CAPTURE_LINES
)
const effectiveBytes = Math.floor(baseBytes * mult)
const effectiveLines = Math.floor(baseLines * mult)
return {
maxStages: parse(
'BARE_OS_PIPELINE_MAX_STAGES',
DEFAULT_PIPELINE_MAX_STAGES
),
maxBytes: Math.min(effectiveBytes, absMaxBytes),
maxLines: Math.min(effectiveLines, absMaxLines),
streamingMultiplier: mult,
/** True when `BARE_OS_SHELL_STREAMING` relaxes caps via multiplier. */
streamingEnabled: streamOn,
/** Parsed `BARE_OS_PIPELINE_MAX_*` before multiplier (for operator snapshots). */
baseMaxBytes: baseBytes,
baseMaxLines: baseLines,
/** Hard ceilings after multiplier (`BARE_OS_PIPELINE_ABS_MAX_*`; defaults 512MiB / 2M lines). */
absCapBytes: absMaxBytes,
absCapLines: absMaxLines
}
}
/**
* Baseline aliases; `~/.barerc` and `unalias -a` merge/reset from this table.
* @returns {Record<string, string>}
@@ -622,365 +283,6 @@ export function tokenize(line) {
return /** @type {Token[]} */ (lexShellLine(line))
}
/**
* @param {string} inner
* @param {Record<string, string>} env
*/
function expandParamBracedInner(inner, env, depth = 0) {
const expandNested = (s) => {
const txt = String(s ?? '')
if (!txt.includes('$')) return txt
return expandWord(txt, env, depth + 1)
}
const tr = inner.trim()
const lenParam = /^#([A-Za-z_][A-Za-z0-9_]*)$/.exec(tr)
if (lenParam) {
shellCheckUnboundParam(lenParam[1], env)
return String(String(env[lenParam[1]] ?? '').length)
}
const indirectOn =
env &&
(env.BARE_OS_SHELL_INDIRECT_EXPANSION === '1' ||
env.BARE_OS_SHELL_INDIRECT_EXPANSION === 'true')
const indirectName = /^!([A-Za-z_][A-Za-z0-9_]*)$/.exec(tr)
if (indirectOn && indirectName) {
const ref = String(env[indirectName[1]] ?? '')
shellCheckUnboundParam(ref, env)
return String(env[ref] ?? '')
}
const paramV2 =
env &&
(env.BARE_OS_SHELL_PARAM_EXPANSION_V2 === '1' ||
env.BARE_OS_SHELL_PARAM_EXPANSION_V2 === 'true')
const paramV3 =
env &&
(env.BARE_OS_SHELL_PARAM_EXPANSION_V3 === '1' ||
env.BARE_OS_SHELL_PARAM_EXPANSION_V3 === 'true')
const errIdx = inner.indexOf(':?')
if (paramV3 && errIdx > 0) {
const name = inner.slice(0, errIdx).trim()
const msg = inner.slice(errIdx + 2)
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
const v = env[name]
if (v == null || String(v) === '') {
throw new Error(expandNested(msg) || 'parameter null or unset')
}
return String(v)
}
}
const assignIdx = inner.indexOf(':=')
if (paramV2 && assignIdx > 0) {
const name = inner.slice(0, assignIdx).trim()
const alt = inner.slice(assignIdx + 2)
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
let v = env[name]
if (v == null || String(v) === '') {
const ex = expandNested(alt)
env[name] = ex
v = ex
}
return String(v ?? '')
}
}
const posixUnsetOnly =
env &&
(env.BARE_OS_SHELL_POSIX_UNSET_ONLY_DEFAULT === '1' ||
env.BARE_OS_SHELL_POSIX_UNSET_ONLY_DEFAULT === 'true')
if (posixUnsetOnly) {
const hy = inner.indexOf('-')
if (
hy > 0 &&
inner.slice(hy - 1, hy + 1) !== ':-' &&
!inner.includes(':')
) {
const m = /^([A-Za-z_][A-Za-z0-9_]*)-(.+)$/.exec(inner)
if (m && m[1] && m[2] != null) {
const name = m[1]
const alt = m[2]
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
if (!Object.prototype.hasOwnProperty.call(env, name))
return expandNested(alt)
return String(env[name] ?? '')
}
}
}
}
const idx = inner.indexOf(':-')
if (idx > 0) {
const name = inner.slice(0, idx).trim()
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
const alt = inner.slice(idx + 2)
const v = env[name]
if (v != null && String(v) !== '') return String(v)
return expandNested(alt)
}
}
const plusIdx = inner.indexOf(':+')
if (paramV3 && plusIdx > 0) {
const name = inner.slice(0, plusIdx).trim()
const alt = inner.slice(plusIdx + 2)
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
const v = env[name]
if (v != null && String(v) !== '') return expandNested(alt)
return ''
}
}
if (paramV3) {
const sliceRe = /^([A-Za-z_][A-Za-z0-9_]*):(\d+)(?::(\d+))?$/.exec(tr)
if (sliceRe) {
const name = sliceRe[1]
const off = Number.parseInt(sliceRe[2], 10)
const ln =
sliceRe[3] != null ? Number.parseInt(sliceRe[3], 10) : undefined
const v = String(env[name] ?? '')
let out = Number.isFinite(off) ? v.slice(off) : v
if (ln != null && Number.isFinite(ln)) out = out.slice(0, ln)
return out
}
const globalRepl = /^([A-Za-z_][A-Za-z0-9_]*)\/\/(.*)\/(.*)$/.exec(tr)
if (globalRepl && globalRepl[2].length <= 256 && globalRepl[3].length <= 512) {
const name = globalRepl[1]
let v = String(env[name] ?? '')
const pat = globalRepl[2]
const rep = expandNested(globalRepl[3])
try {
const re = new RegExp(pat, 'g')
v = v.replace(re, rep)
} catch {
/* invalid regex — leave value */
}
return v
}
}
if (paramV2) {
const longPref = /^([A-Za-z_][A-Za-z0-9_]*)##(.+)$/.exec(inner)
if (longPref && longPref[2].length > 0 && longPref[2].length <= 128) {
const v = String(env[longPref[1]] ?? '')
const pat = longPref[2]
if (pat === '*/') {
const i = v.lastIndexOf('/')
return i >= 0 ? v.slice(i + 1) : v
}
let end = -1
for (let i = 0; i <= v.length - pat.length; i++) {
if (v.slice(i, i + pat.length) === pat) end = i + pat.length
}
return end >= 0 ? v.slice(end) : v
}
const shortPref = /^([A-Za-z_][A-Za-z0-9_]*)#(.+)$/.exec(inner)
if (shortPref && shortPref[2].length > 0 && shortPref[2].length <= 128) {
const v = String(env[shortPref[1]] ?? '')
const pat = shortPref[2]
if (pat === '*/') {
const i = v.indexOf('/')
return i >= 0 ? v.slice(i + 1) : v
}
const i = v.indexOf(pat)
return i >= 0 ? v.slice(i + pat.length) : v
}
const longSuf = /^([A-Za-z_][A-Za-z0-9_]*)%%(.+)$/.exec(inner)
if (longSuf && longSuf[2].length > 0 && longSuf[2].length <= 128) {
const v = String(env[longSuf[1]] ?? '')
const pat = longSuf[2]
if (!/[?*[]/.test(pat) && v.endsWith(pat))
return v.slice(0, v.length - pat.length)
if (pat.includes('*') && !pat.includes('[') && !pat.includes('?')) {
const parts = pat.split('*')
if (parts.length === 2) {
const a = parts[0]
const b = parts[1]
let best = -1
for (let len = 1; len <= v.length; len++) {
const suf = v.slice(v.length - len)
if (
suf.startsWith(a) &&
suf.endsWith(b) &&
suf.length >= a.length + b.length
) {
if (best < 0 || len > best) best = len
}
}
if (best > 0) return v.slice(0, v.length - best)
}
}
return v
}
const shortSuf = /^([A-Za-z_][A-Za-z0-9_]*)%(.+)$/.exec(inner)
if (shortSuf && shortSuf[2].length > 0 && shortSuf[2].length <= 128) {
const v = String(env[shortSuf[1]] ?? '')
const pat = shortSuf[2]
if (!/[?*[]/.test(pat) && v.endsWith(pat))
return v.slice(0, v.length - pat.length)
if (pat.includes('*') && !pat.includes('[') && !pat.includes('?')) {
const parts = pat.split('*')
if (parts.length === 2) {
const a = parts[0]
const b = parts[1]
let best = -1
for (let len = 1; len <= v.length; len++) {
const suf = v.slice(v.length - len)
if (
suf.startsWith(a) &&
suf.endsWith(b) &&
suf.length >= a.length + b.length
) {
if (best < 0 || len < best) best = len
}
}
if (best > 0) return v.slice(0, v.length - best)
}
}
return v
}
}
const hash = inner.indexOf('#')
if (hash > 0) {
const name = inner.slice(0, hash).trim()
const pref = inner.slice(hash + 1)
if (
/^[A-Za-z_][A-Za-z0-9_]*$/.test(name) &&
pref.length > 0 &&
pref.length <= 128
) {
const v = String(env[name] ?? '')
return v.startsWith(pref) ? v.slice(pref.length) : v
}
}
const keySimple = inner.trim()
shellCheckUnboundParam(keySimple, env)
return env[keySimple] ?? ''
}
/**
* @param {string} s
* @param {Record<string, string>} env
*/
export function expandWord(s, env, depth = 0) {
const maxDepthRaw = Number.parseInt(
String(env?.BARE_OS_SHELL_EXPANSION_MAX_DEPTH || '32'),
10
)
const maxDepth =
Number.isFinite(maxDepthRaw) && maxDepthRaw > 0 ? Math.min(maxDepthRaw, 256) : 32
if (depth > maxDepth) {
throw new Error(`shell: expansion recursion too deep (max ${maxDepth})`)
}
const paramExpOn =
env &&
(env.BARE_OS_SHELL_PARAM_EXPANSION === '1' ||
env.BARE_OS_SHELL_PARAM_EXPANSION === 'true')
const paramV2 =
env &&
(env.BARE_OS_SHELL_PARAM_EXPANSION_V2 === '1' ||
env.BARE_OS_SHELL_PARAM_EXPANSION_V2 === 'true')
const paramV3 =
env &&
(env.BARE_OS_SHELL_PARAM_EXPANSION_V3 === '1' ||
env.BARE_OS_SHELL_PARAM_EXPANSION_V3 === 'true')
let out = ''
let j = 0
while (j < s.length) {
if (s[j] === '$') {
if (s[j + 1] === '(' && s[j + 2] === '(') {
const close = findArithmeticClose(s, j + 3)
if (close < 0) {
out += s.slice(j)
break
}
const inner = s.slice(j + 3, close)
try {
out += bareOsEvalArithmeticExpr(inner, env)
} catch (e) {
if (
env?.BARE_OS_SHELL_POSIX_MODE === '1' ||
env?.BARE_OS_SHELL_POSIX_MODE === 'true'
) {
throw new Error(
'shell: arithmetic: invalid token (POSIX mode strict arithmetic)'
)
}
throw e
}
j = close + 2
continue
}
if (s[j + 1] === '{') {
const end = s.indexOf('}', j + 2)
if (end === -1) {
out += s.slice(j)
break
}
const inner = s.slice(j + 2, end)
const tr0 = inner.trim()
const indirectOnBr =
env?.BARE_OS_SHELL_INDIRECT_EXPANSION === '1' ||
env?.BARE_OS_SHELL_INDIRECT_EXPANSION === 'true'
if (inner === '?') {
out += env[BARE_OS_EXIT_STATUS_ENV] ?? '0'
} else if (
/^#[A-Za-z_][A-Za-z0-9_]*$/.test(tr0) ||
(indirectOnBr && /^![A-Za-z_][A-Za-z0-9_]*$/.test(tr0)) ||
(paramExpOn &&
(inner.includes(':-') ||
(paramV3 && (inner.includes(':+') || inner.includes(':?'))) ||
(paramV3 &&
(/^[A-Za-z_][A-Za-z0-9_]*:\d/.test(tr0) ||
/^[A-Za-z_][A-Za-z0-9_]*\/\//.test(inner))) ||
(paramV2 &&
(inner.includes(':=') ||
/^[A-Za-z_][A-Za-z0-9_]*##/.test(inner) ||
/^[A-Za-z_][A-Za-z0-9_]*%%/.test(inner) ||
/^[A-Za-z_][A-Za-z0-9_]*%[^%]/.test(inner) ||
/^[A-Za-z_][A-Za-z0-9_]*#[^#]/.test(inner))) ||
(/^[A-Za-z_][A-Za-z0-9_]*#/.test(inner) && inner.includes('#'))))
) {
out += expandParamBracedInner(inner, env, depth + 1)
} else {
const ik = inner.trim()
shellCheckUnboundParam(ik, env)
out += env[ik] ?? ''
}
j = end + 1
continue
}
if (s[j + 1] === '?') {
out += env[BARE_OS_EXIT_STATUS_ENV] ?? '0'
j += 2
continue
}
if (/[0-9]/.test(s[j + 1] ?? '')) {
const pn = s[j + 1]
shellCheckUnboundParam(pn, env)
out += env[pn] ?? ''
j += 2
continue
}
let k = j + 1
while (k < s.length && /[A-Za-z0-9_]/.test(s[k])) k++
const name = s.slice(j + 1, k)
if (name) {
shellCheckUnboundParam(name, env)
out += env[name] ?? ''
j = k
} else {
out += '$'
j++
}
continue
}
out += s[j++]
}
return out
}
/**
* @typedef {{
* argv: Extract<Token, { type: 'word' }>[],
@@ -0,0 +1,324 @@
/**
* Linux-shaped `/proc` / `/dev` pseudo texts used by createVfs.
*/
import b4a from 'b4a'
/** UTF-8 bytes; Bare may not define global TextEncoder (see curl-cli utf8Encode). */
export function utf8Encode(str) {
return b4a.from(String(str), 'utf8')
}
/**
* @param {{
* env: Record<string, string | undefined>,
* procSnapshot?: { version?: string, cmdline?: string } | null,
* bootStartedMs?: number | null,
* hostProcStatsRef?: { stats?: unknown } | null,
* getProcSelfExtraFds?: (() => { fdNum: string, target: string }[]) | null,
* getProcSyntheticLinuxCompat?: (() => { sessionId?: string, swarmPeerCount?: number, peerIds?: unknown[] } | null | undefined) | null,
* secureRandomBytes?: ((n: number) => Uint8Array) | null,
* environKeyAllowed: (k: string) => boolean
* }} deps
*/
export function createVfsPseudoLinux(deps) {
const {
env,
procSnapshot = null,
bootStartedMs = null,
hostProcStatsRef = null,
getProcSelfExtraFds = null,
getProcSyntheticLinuxCompat = null,
secureRandomBytes = null,
environKeyAllowed
} = deps
function pseudoVersionText() {
const v = procSnapshot?.version ?? env.BARE_OS_CTX_API_VERSION ?? 'unknown'
let out = `Bare OS\nbare_os_ctx_api_version=${v}\n`
const tty = String(env.BARE_OS_PEAR_TTY_FLAGS_JSON || '').trim()
if (tty) {
out += `pear_tty_flags_json=${tty.slice(0, 512)}\n`
}
return out
}
function pseudoCmdlineText() {
return `${procSnapshot?.cmdline ?? 'bare-os'}\n`
}
function listProcSelfFdDirNames() {
const names = new Set(['0', '1', '2'])
const extra = getProcSelfExtraFds ? getProcSelfExtraFds() : []
for (const e of extra) {
if (e && e.fdNum && /^[0-9]+$/.test(String(e.fdNum))) {
names.add(String(e.fdNum))
}
}
return [...names].sort((a, b) => Number(a) - Number(b))
}
function encodeProcSelfFdSymlink(fdNumRaw) {
const n = String(fdNumRaw ?? '0')
if (n === '0' || n === '1' || n === '2') {
return utf8Encode(
`/proc/self/fd/${n} -> (bare-os stdio pseudo inode)\n`
)
}
const extra = getProcSelfExtraFds ? getProcSelfExtraFds() : []
const hit = extra.find((e) => e && String(e.fdNum) === n)
if (hit && hit.target) {
return utf8Encode(`/proc/self/fd/${n} -> ${hit.target}\n`)
}
return utf8Encode(`/proc/self/fd/${n} -> (not open)\n`)
}
function pseudoEnvironBytes() {
const parts = []
for (const k of Object.keys(env).sort()) {
if (!environKeyAllowed(k)) continue
const v = env[k]
if (typeof v !== 'string') continue
parts.push(`${k}=${v}\0`)
}
return utf8Encode(parts.join(''))
}
function pseudoUptimeText() {
const start = bootStartedMs != null ? bootStartedMs : Date.now()
const up = Math.max(0, (Date.now() - start) / 1000)
return `${up.toFixed(2)} ${up.toFixed(2)}\n`
}
function pseudoMeminfoText() {
const snap = hostProcStatsRef?.stats
const mu =
snap &&
typeof snap === 'object' &&
snap !== null &&
/** @type {{ memoryUsage?: () => unknown }} */ (snap).memoryUsage
? /** @type {{ memoryUsage: () => { rss?: number, heapTotal?: number, heapUsed?: number, external?: number } }} */ (
snap
).memoryUsage()
: null
if (mu && typeof mu === 'object') {
const rss = Number(mu.rss) || 0
const heap = Number(mu.heapUsed) || 0
const total = Math.max(rss, heap, 1)
const kb = (n) => Math.max(0, Math.floor(n / 1024))
return [
`MemTotal: ${kb(total * 2)} kB`,
`MemFree: ${kb(Math.max(0, total - heap))} kB`,
`MemAvailable: ${kb(Math.max(0, total - heap))} kB`,
'SwapTotal: 0 kB',
'SwapFree: 0 kB',
'BogoSource: bare-os host memoryUsage snapshot',
''
].join('\n')
}
return [
'MemTotal: 524288 kB',
'MemFree: 262144 kB',
'MemAvailable: 262144 kB',
'SwapTotal: 0 kB',
'SwapFree: 0 kB',
''
].join('\n')
}
function pseudoCpuinfoText() {
const snap = hostProcStatsRef?.stats
const cpus =
snap &&
typeof snap === 'object' &&
snap !== null &&
typeof (/** @type {{ cpus?: () => unknown[] }} */ (snap).cpus) ===
'function'
? /** @type {{ cpus: () => unknown[] }} */ (snap).cpus()
: null
if (Array.isArray(cpus) && cpus.length > 0) {
const lines = []
let i = 0
for (const c of cpus) {
const m =
c && typeof c === 'object' && 'model' in c
? String(/** @type {{ model?: string }} */ (c).model || '')
: ''
const sp =
c && typeof c === 'object' && 'speed' in c
? Number(/** @type {{ speed?: number }} */ (c).speed) || 0
: 0
lines.push(`processor\t: ${i}`)
lines.push(`vendor_id\t: host`)
lines.push(`model name\t: ${m || 'host CPU (bare-os snapshot)'}`)
lines.push(`cpu MHz\t\t: ${sp}`)
lines.push(`flags\t: bare_os_host_stats`)
lines.push('')
i++
}
return lines.join('\n')
}
return [
'processor\t: 0',
'vendor_id\t: BareOS',
'model name\t: Bare OS pseudo CPU (vfs-backed)',
'flags\t: bare_os',
''
].join('\n')
}
function pseudoLoadavgText() {
return '0.05 0.04 0.02 1/64 1\n'
}
function pseudoSelfExeText() {
return '/bin/sh (bare-os pseudo inode; no backing host file)\n'
}
function bareOsSanitizeCgroupSegment(s) {
return String(s || 'unknown')
.replace(/[^a-zA-Z0-9._-]/g, '_')
.slice(0, 128)
}
/** @returns {{ sessionId: string, swarmPeerCount: number, peerIds: string[] }} */
function readProcSyntheticLinuxCompat() {
let sessionId = String(env.BARE_OS_SESSION_ID || env.BARE_OS_TRACE_ID || '')
.trim()
let swarmPeerCount = 0
let peerIds = []
try {
const h = getProcSyntheticLinuxCompat ? getProcSyntheticLinuxCompat() : null
if (h && typeof h === 'object') {
if (h.sessionId != null && String(h.sessionId).trim())
sessionId = String(h.sessionId).trim()
const n = Number(h.swarmPeerCount)
if (Number.isFinite(n) && n >= 0) swarmPeerCount = Math.min(4096, n | 0)
if (Array.isArray(h.peerIds))
peerIds = h.peerIds.map((x) => String(x || '')).filter(Boolean)
}
} catch {
/* ignore */
}
if (!sessionId) sessionId = 'session'
return { sessionId, swarmPeerCount, peerIds }
}
/**
* Linux cgroup v2 single-column lines, scoped to the guest session (not host cgroups).
*/
function pseudoSelfCgroupsText() {
const { sessionId } = readProcSyntheticLinuxCompat()
const sid = bareOsSanitizeCgroupSegment(sessionId)
return [
'# bare_os synthetic cgroup v2 view (guest session + swarm scope; not the host cgroup hierarchy)',
'0::/bare-os.scope',
`0::/bare-os.session/${sid}/init`,
`0::/bare-os.session/${sid}/swarm`,
''
].join('\n')
}
/** IPv4:port as in /proc/net/tcp (little-endian quad hex + port hex). */
function bareOsProcNetIpv4PortHex(ip, port) {
const parts = String(ip || '0.0.0.0').split('.')
const a = Number(parts[0]) || 0
const b = Number(parts[1]) || 0
const c = Number(parts[2]) || 0
const d = Number(parts[3]) || 0
const addr = (a | (b << 8) | (c << 16) | (d << 24)) >>> 0
const p = port & 0xffff
return (
addr.toString(16).padStart(8, '0') +
':' +
p.toString(16).padStart(4, '0')
)
}
/**
* Logical Hyperswarm / replication streams as Linux-shaped /proc/net/tcp rows (inode encodes slot).
*/
function pseudoNetTcpText() {
const { swarmPeerCount, peerIds } = readProcSyntheticLinuxCompat()
const nPeers = Math.max(swarmPeerCount, peerIds.length)
const lines = [
' sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode',
'# bare_os: logical swarm/replication endpoints (not host TCP); st 0A=LISTEN 01=ESTABLISHED'
]
lines.push(
` 0: ${bareOsProcNetIpv4PortHex('127.0.0.1', 57800)} 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 800000`
)
let inode = 800001
for (let i = 0; i < nPeers; i++) {
const id = peerIds[i] || '0'.repeat(64)
const h = id.slice(0, 8) || '00000000'
const v = Number.parseInt(h, 16) || 0
const o1 = v & 255
const o2 = (v >>> 8) & 255
const o3 = (v >>> 16) & 255
const o4 = (v >>> 24) & 255
const remIp = `${o1}.${o2}.${o3}.${o4}`
lines.push(
` ${i + 1}: ${bareOsProcNetIpv4PortHex('127.0.0.1', 58000 + i)} ${bareOsProcNetIpv4PortHex(remIp, 57900 + i)} 01 00000000:00000000 00:00000000 00000000 0 0 ${inode}`
)
inode++
}
lines.push('')
return lines.join('\n')
}
function pseudoNetUdpText() {
const { swarmPeerCount } = readProcSyntheticLinuxCompat()
const lines = [
' sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode ref',
'# bare_os: logical UDP-shaped discovery/datagram hints (not host UDP)'
]
lines.push(
` 0: ${bareOsProcNetIpv4PortHex('0.0.0.0', 49721)} 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 810000 2`
)
if (swarmPeerCount > 0) {
lines.push(
` 1: ${bareOsProcNetIpv4PortHex('127.0.0.1', 49722)} 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 810001 2`
)
}
lines.push('')
return lines.join('\n')
}
/**
* When the booter supplies `secureRandomBytes` (e.g. bare-crypto), reads are suitable
* for cryptographic use; otherwise falls back to Math.random (not crypto-grade).
*/
function pseudoUrandomBytes() {
const n = 4096
if (secureRandomBytes) {
try {
return secureRandomBytes(n)
} catch {
/* fall through */
}
}
const u = new Uint8Array(n)
for (let i = 0; i < n; i++) u[i] = Math.floor(Math.random() * 256)
return u
}
return {
listProcSelfFdDirNames,
encodeProcSelfFdSymlink,
pseudoVersionText,
pseudoCmdlineText,
pseudoEnvironBytes,
pseudoUptimeText,
pseudoMeminfoText,
pseudoCpuinfoText,
pseudoLoadavgText,
pseudoSelfExeText,
bareOsSanitizeCgroupSegment,
readProcSyntheticLinuxCompat,
pseudoSelfCgroupsText,
bareOsProcNetIpv4PortHex,
pseudoNetTcpText,
pseudoNetUdpText,
pseudoUrandomBytes
}
}
+29 -275
View File
@@ -46,6 +46,10 @@ import {
vfsErr
} from './vfs-path.js'
import { createVfsPersonalLayout } from './vfs-personal.js'
import {
utf8Encode,
createVfsPseudoLinux
} from './vfs-pseudo-linux.js'
export {
classifyBareOsVfsPathClass,
@@ -797,6 +801,31 @@ export function createVfs(
bareOsGuestSensitivePersonalDenied,
environKeyAllowed
} = createVfsPersonalLayout(env, bareOsIdentityVfsRef)
const {
listProcSelfFdDirNames,
encodeProcSelfFdSymlink,
pseudoVersionText,
pseudoCmdlineText,
pseudoEnvironBytes,
pseudoUptimeText,
pseudoMeminfoText,
pseudoCpuinfoText,
pseudoLoadavgText,
pseudoSelfExeText,
pseudoSelfCgroupsText,
pseudoNetTcpText,
pseudoNetUdpText,
pseudoUrandomBytes
} = createVfsPseudoLinux({
env,
procSnapshot,
bootStartedMs,
hostProcStatsRef,
getProcSelfExtraFds,
getProcSyntheticLinuxCompat,
secureRandomBytes,
environKeyAllowed
})
let cwd = env.PWD || HOME()
/**
@@ -813,281 +842,6 @@ export function createVfs(
)
}
function pseudoVersionText() {
const v = procSnapshot?.version ?? env.BARE_OS_CTX_API_VERSION ?? 'unknown'
let out = `Bare OS\nbare_os_ctx_api_version=${v}\n`
const tty = String(env.BARE_OS_PEAR_TTY_FLAGS_JSON || '').trim()
if (tty) {
out += `pear_tty_flags_json=${tty.slice(0, 512)}\n`
}
return out
}
function pseudoCmdlineText() {
return `${procSnapshot?.cmdline ?? 'bare-os'}\n`
}
/** UTF-8 bytes; Bare may not define global TextEncoder (see curl-cli utf8Encode). */
function utf8Encode(str) {
return b4a.from(String(str), 'utf8')
}
function listProcSelfFdDirNames() {
const names = new Set(['0', '1', '2'])
const extra = getProcSelfExtraFds ? getProcSelfExtraFds() : []
for (const e of extra) {
if (e && e.fdNum && /^[0-9]+$/.test(String(e.fdNum))) {
names.add(String(e.fdNum))
}
}
return [...names].sort((a, b) => Number(a) - Number(b))
}
function encodeProcSelfFdSymlink(fdNumRaw) {
const n = String(fdNumRaw ?? '0')
if (n === '0' || n === '1' || n === '2') {
return utf8Encode(
`/proc/self/fd/${n} -> (bare-os stdio pseudo inode)\n`
)
}
const extra = getProcSelfExtraFds ? getProcSelfExtraFds() : []
const hit = extra.find((e) => e && String(e.fdNum) === n)
if (hit && hit.target) {
return utf8Encode(`/proc/self/fd/${n} -> ${hit.target}\n`)
}
return utf8Encode(`/proc/self/fd/${n} -> (not open)\n`)
}
function pseudoEnvironBytes() {
const parts = []
for (const k of Object.keys(env).sort()) {
if (!environKeyAllowed(k)) continue
const v = env[k]
if (typeof v !== 'string') continue
parts.push(`${k}=${v}\0`)
}
return utf8Encode(parts.join(''))
}
function pseudoUptimeText() {
const start = bootStartedMs != null ? bootStartedMs : Date.now()
const up = Math.max(0, (Date.now() - start) / 1000)
return `${up.toFixed(2)} ${up.toFixed(2)}\n`
}
function pseudoMeminfoText() {
const snap = hostProcStatsRef?.stats
const mu =
snap &&
typeof snap === 'object' &&
snap !== null &&
/** @type {{ memoryUsage?: () => unknown }} */ (snap).memoryUsage
? /** @type {{ memoryUsage: () => { rss?: number, heapTotal?: number, heapUsed?: number, external?: number } }} */ (
snap
).memoryUsage()
: null
if (mu && typeof mu === 'object') {
const rss = Number(mu.rss) || 0
const heap = Number(mu.heapUsed) || 0
const total = Math.max(rss, heap, 1)
const kb = (n) => Math.max(0, Math.floor(n / 1024))
return [
`MemTotal: ${kb(total * 2)} kB`,
`MemFree: ${kb(Math.max(0, total - heap))} kB`,
`MemAvailable: ${kb(Math.max(0, total - heap))} kB`,
'SwapTotal: 0 kB',
'SwapFree: 0 kB',
'BogoSource: bare-os host memoryUsage snapshot',
''
].join('\n')
}
return [
'MemTotal: 524288 kB',
'MemFree: 262144 kB',
'MemAvailable: 262144 kB',
'SwapTotal: 0 kB',
'SwapFree: 0 kB',
''
].join('\n')
}
function pseudoCpuinfoText() {
const snap = hostProcStatsRef?.stats
const cpus =
snap &&
typeof snap === 'object' &&
snap !== null &&
typeof (/** @type {{ cpus?: () => unknown[] }} */ (snap).cpus) ===
'function'
? /** @type {{ cpus: () => unknown[] }} */ (snap).cpus()
: null
if (Array.isArray(cpus) && cpus.length > 0) {
const lines = []
let i = 0
for (const c of cpus) {
const m =
c && typeof c === 'object' && 'model' in c
? String(/** @type {{ model?: string }} */ (c).model || '')
: ''
const sp =
c && typeof c === 'object' && 'speed' in c
? Number(/** @type {{ speed?: number }} */ (c).speed) || 0
: 0
lines.push(`processor\t: ${i}`)
lines.push(`vendor_id\t: host`)
lines.push(`model name\t: ${m || 'host CPU (bare-os snapshot)'}`)
lines.push(`cpu MHz\t\t: ${sp}`)
lines.push(`flags\t: bare_os_host_stats`)
lines.push('')
i++
}
return lines.join('\n')
}
return [
'processor\t: 0',
'vendor_id\t: BareOS',
'model name\t: Bare OS pseudo CPU (vfs-backed)',
'flags\t: bare_os',
''
].join('\n')
}
function pseudoLoadavgText() {
return '0.05 0.04 0.02 1/64 1\n'
}
function pseudoSelfExeText() {
return '/bin/sh (bare-os pseudo inode; no backing host file)\n'
}
function bareOsSanitizeCgroupSegment(s) {
return String(s || 'unknown')
.replace(/[^a-zA-Z0-9._-]/g, '_')
.slice(0, 128)
}
/** @returns {{ sessionId: string, swarmPeerCount: number, peerIds: string[] }} */
function readProcSyntheticLinuxCompat() {
let sessionId = String(env.BARE_OS_SESSION_ID || env.BARE_OS_TRACE_ID || '')
.trim()
let swarmPeerCount = 0
let peerIds = []
try {
const h = getProcSyntheticLinuxCompat ? getProcSyntheticLinuxCompat() : null
if (h && typeof h === 'object') {
if (h.sessionId != null && String(h.sessionId).trim())
sessionId = String(h.sessionId).trim()
const n = Number(h.swarmPeerCount)
if (Number.isFinite(n) && n >= 0) swarmPeerCount = Math.min(4096, n | 0)
if (Array.isArray(h.peerIds))
peerIds = h.peerIds.map((x) => String(x || '')).filter(Boolean)
}
} catch {
/* ignore */
}
if (!sessionId) sessionId = 'session'
return { sessionId, swarmPeerCount, peerIds }
}
/**
* Linux cgroup v2 single-column lines, scoped to the guest session (not host cgroups).
*/
function pseudoSelfCgroupsText() {
const { sessionId } = readProcSyntheticLinuxCompat()
const sid = bareOsSanitizeCgroupSegment(sessionId)
return [
'# bare_os synthetic cgroup v2 view (guest session + swarm scope; not the host cgroup hierarchy)',
'0::/bare-os.scope',
`0::/bare-os.session/${sid}/init`,
`0::/bare-os.session/${sid}/swarm`,
''
].join('\n')
}
/** IPv4:port as in /proc/net/tcp (little-endian quad hex + port hex). */
function bareOsProcNetIpv4PortHex(ip, port) {
const parts = String(ip || '0.0.0.0').split('.')
const a = Number(parts[0]) || 0
const b = Number(parts[1]) || 0
const c = Number(parts[2]) || 0
const d = Number(parts[3]) || 0
const addr = (a | (b << 8) | (c << 16) | (d << 24)) >>> 0
const p = port & 0xffff
return (
addr.toString(16).padStart(8, '0') +
':' +
p.toString(16).padStart(4, '0')
)
}
/**
* Logical Hyperswarm / replication streams as Linux-shaped /proc/net/tcp rows (inode encodes slot).
*/
function pseudoNetTcpText() {
const { swarmPeerCount, peerIds } = readProcSyntheticLinuxCompat()
const nPeers = Math.max(swarmPeerCount, peerIds.length)
const lines = [
' sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode',
'# bare_os: logical swarm/replication endpoints (not host TCP); st 0A=LISTEN 01=ESTABLISHED'
]
lines.push(
` 0: ${bareOsProcNetIpv4PortHex('127.0.0.1', 57800)} 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 800000`
)
let inode = 800001
for (let i = 0; i < nPeers; i++) {
const id = peerIds[i] || '0'.repeat(64)
const h = id.slice(0, 8) || '00000000'
const v = Number.parseInt(h, 16) || 0
const o1 = v & 255
const o2 = (v >>> 8) & 255
const o3 = (v >>> 16) & 255
const o4 = (v >>> 24) & 255
const remIp = `${o1}.${o2}.${o3}.${o4}`
lines.push(
` ${i + 1}: ${bareOsProcNetIpv4PortHex('127.0.0.1', 58000 + i)} ${bareOsProcNetIpv4PortHex(remIp, 57900 + i)} 01 00000000:00000000 00:00000000 00000000 0 0 ${inode}`
)
inode++
}
lines.push('')
return lines.join('\n')
}
function pseudoNetUdpText() {
const { swarmPeerCount } = readProcSyntheticLinuxCompat()
const lines = [
' sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode ref',
'# bare_os: logical UDP-shaped discovery/datagram hints (not host UDP)'
]
lines.push(
` 0: ${bareOsProcNetIpv4PortHex('0.0.0.0', 49721)} 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 810000 2`
)
if (swarmPeerCount > 0) {
lines.push(
` 1: ${bareOsProcNetIpv4PortHex('127.0.0.1', 49722)} 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 810001 2`
)
}
lines.push('')
return lines.join('\n')
}
/**
* When the booter supplies `secureRandomBytes` (e.g. bare-crypto), reads are suitable
* for cryptographic use; otherwise falls back to Math.random (not crypto-grade).
*/
function pseudoUrandomBytes() {
const n = 4096
if (secureRandomBytes) {
try {
return secureRandomBytes(n)
} catch {
/* fall through */
}
}
const u = new Uint8Array(n)
for (let i = 0; i < n; i++) u[i] = Math.floor(Math.random() * 256)
return u
}
function pseudoFileBytes(routePseudo) {
const f = routePseudo.file
const k = routePseudo.kind
@@ -34,6 +34,25 @@ import {
casePatternMatches
} from './lib/shell-syntax.js'
import { createVfsPersonalLayout } from './lib/vfs-personal.js'
import {
listBareOsShellBuiltins,
isShellBuiltin,
bareOsShellReadSplitFields
} from './lib/shell-builtins.js'
import {
getBareOsPipelineLimits,
mergePipelineChildCtx,
DEFAULT_PIPELINE_MAX_STAGES
} from './lib/shell-runtime.js'
import { expandWord } from './lib/shell-expand.js'
import {
utf8Encode,
createVfsPseudoLinux
} from './lib/vfs-pseudo-linux.js'
import { createBareOsVirtualSignalDeliverer } from './lib/bare-os-virtual-signal.js'
import { createKernelLoaderAuditAppend } from './lib/bare-os-loader-audit.js'
import { createBooterBootEmitter } from './lib/bare-os-boot-phases.js'
import { createBooterProcHelpers } from './lib/bare-os-booter-proc-helpers.js'
test('posix env flags and caps', (t) => {
t.ok(wantPosixSocketFdBridge({ BARE_OS_POSIX_SOCKET_FD_BRIDGE: '1' }))
@@ -245,3 +264,127 @@ test('vfs personal layout and guest-sensitive paths', (t) => {
t.ok(!unlocked.bareOsGuestSensitivePersonalDenied('/.bare/vault/key'))
t.is(unlocked.personalLayoutRootAbs(), '/.bare-os')
})
test('shell builtins and read field split', (t) => {
t.ok(isShellBuiltin('cd', {}))
t.ok(!isShellBuiltin('read', {}))
t.ok(isShellBuiltin('read', { BARE_OS_SHELL_READ_BUILTIN: '1' }))
t.ok(listBareOsShellBuiltins({ BARE_OS_SHELL_READ_BUILTIN: '1' }).includes('read'))
t.alike(bareOsShellReadSplitFields('a b c', ' ', 2), ['a', 'b c'])
})
test('shell runtime pipeline limits and child merge', (t) => {
t.is(getBareOsPipelineLimits({}).maxStages, DEFAULT_PIPELINE_MAX_STAGES)
t.is(getBareOsPipelineLimits({ BARE_OS_PIPELINE_MAX_STAGES: '4' }).maxStages, 4)
const ctx = { exitCode: 0 }
mergePipelineChildCtx(ctx, {
exitCode: 7,
identity: { state: 'unlocked', publicKey: 1, secretKey: 2 }
})
t.is(ctx.exitCode, 7)
t.is(ctx.identity.state, 'unlocked')
})
test('expandWord param and arithmetic', (t) => {
t.is(expandWord('x${HOME}y', { HOME: '/h' }), 'x/hy')
t.is(expandWord('$(( $a + 1 ))', { a: '2' }), '3')
})
test('vfs linux-shaped /proc texts', (t) => {
const linux = createVfsPseudoLinux({
env: {
BARE_OS_CTX_API_VERSION: '1.2.3',
BARE_OS_SESSION_ID: 'sess-1',
HOME: '/home/guest',
PASSWORD: 'nope'
},
procSnapshot: { version: '1.2.3', cmdline: 'bare-os' },
bootStartedMs: Date.now() - 2500,
environKeyAllowed: (k) => k !== 'PASSWORD',
getProcSyntheticLinuxCompat: () => ({
sessionId: 'sess-1',
swarmPeerCount: 1,
peerIds: ['aabbccdd']
})
})
t.ok(linux.pseudoVersionText().includes('1.2.3'))
t.ok(linux.pseudoMeminfoText().includes('MemTotal:'))
t.ok(linux.pseudoCpuinfoText().includes('processor'))
t.ok(linux.pseudoNetTcpText().includes('ESTABLISHED'))
t.ok(linux.pseudoSelfCgroupsText().includes('sess-1'))
t.ok(utf8Encode('hi').byteLength >= 2)
})
test('virtual signal deliverer ignore and existence probe', (t) => {
const state = new Map()
const traps = []
const deliver = createBareOsVirtualSignalDeliverer({
dispatchShellTrapSignal: (ctx, sig) => {
traps.push(sig)
},
virtualSignalState: state
})
const ctx = {
bareOsLogicalSigaction: { TERM: 'IGNORE' },
requestBooterExit() {
t.fail('should not exit when ignored')
}
}
const ignored = deliver(ctx, 3, 'TERM')
t.ok(ignored.ignored)
t.is(traps.length, 0)
const probe = deliver({ requestBooterExit() {} }, 3, '0')
t.ok(probe.exists)
t.absent(probe.delivered)
})
test('loader audit appends when enabled', async (t) => {
const files = new Map()
const append = createKernelLoaderAuditAppend({
env: { BARE_OS_LOADER_AUDIT: '1' },
sessionId: 's1',
vfs: {
async readFile(p) {
if (!files.has(p)) throw new Error('missing')
return files.get(p)
},
async writeFile(p, buf) {
files.set(p, buf)
}
}
})
await append({ phase: 'vfs' })
const txt = new TextDecoder().decode(files.get('/run/bare-os/loader-audit.ndjson'))
t.ok(txt.includes('"type":"loader_audit"'))
t.ok(txt.includes('"phase":"vfs"'))
})
test('booter boot emitter records phases', (t) => {
const bootReadyStateRef = { booterPhases: [], booterStages: [] }
const events = []
const { emitBooterBootStep } = createBooterBootEmitter({
bootReadyStateRef,
bootStartedMs: Date.now(),
bootEventSubs: [(ev) => events.push(ev)],
diagnosticsSubs: [],
sessionId: 's',
lifecycleSchemaVersion: 1
})
emitBooterBootStep('vfs')
emitBooterBootStep('vfs')
t.alike(bootReadyStateRef.booterPhases, ['vfs'])
t.is(events.length, 2)
t.is(events[0].phase, 'booter:vfs')
})
test('booter proc helpers chat/meshdrop off notes', (t) => {
const helpers = createBooterProcHelpers({
interactiveCtxRef: { ctx: { bareOsLogicalFds: { 7: '/tmp/x' } } },
disk: {},
shellEnv: {},
hostEnv: {}
})
t.alike(helpers.bareOsCollectLogicalFdRows(), [{ fd: 7, target: '/tmp/x' }])
t.ok(helpers.bareOsChatProcSnapshotRecord().note)
t.ok(helpers.bareOsMeshdropProcSnapshotRecord().note)
})