/** * 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} 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 | 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 512 MiB / 2 M 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} ctx * @param {string} event * @param {Record} 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} ctx * @param {Record} 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} ctx * @param {Record} childCtx */ export function mergePipelineChildCtx(ctx, childCtx) { mergeChildExitCode(ctx, childCtx) if (isMergeableIdentitySession(childCtx.identity)) { ctx.identity = /** @type {Record} */ (childCtx.identity) } }