147 lines
4.7 KiB
JavaScript
147 lines
4.7 KiB
JavaScript
/**
|
||
* 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 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<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)
|
||
}
|
||
}
|