This commit is contained in:
Raven Scott
2026-04-03 23:42:47 -04:00
parent d8e580d1af
commit 2ef56ac314
43 changed files with 1941 additions and 553 deletions
+1
View File
@@ -4,6 +4,7 @@
| Version | Booter (workspace) | Notes |
| ------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 1.9.0 | 0.1.0 | **`bareOsVerifyBootManifestSignature`**, signed manifest (**`BARE_OS_BOOT_MANIFEST_SIGN`**, **`/etc/bare-os/boot.manifest.sig`**), **`/proc/bare_os_replication`**, **`/proc/bare_os_capabilities`** (+ **`.json`**), **`/proc/bare_os_bootstrap`**, **`/run/bare-os/unit-journal/*.ndjson`**, initd **`BARE_OS_INITD_MAX_PARALLEL`**, unit journal + **`OnFailure=`** / **`FailureAction=`** / **`Before=`**, **`bareOsIpc.duplexJsonRoundTrip`**, **`BARE_OS_VFS_BIN_CACHE`**, **`bareOsRequestMirror`**, **`bareOsExportPersonalSnapshot`**, **`bareOsPearIpcEmit`**, sandbox worker hint (**`BARE_OS_SANDBOX_WORKER`**). Seeder: optional **`BARE_OS_HYPER_MULTISIG_VERIFY`** + **`hyper-multisig`**. Coreutils: **`grep -r`** **`--include`/`--exclude`/`--exclude-dir`**, **`sed -z`**. |
| 1.8.0 | 0.1.0 | **`ctx.bareOsSandboxRunScript`**, **`bareOsBootFileSha256Hex`**, boot phase hooks (**`bareOsRegisterBootPhaseHook`** / **`bareOsInvokeBootPhaseHooks`**), virtual file meta (**`bareOsInvalidateVirtualFile`**, **`bareOsUpdateVirtualFileMeta`**), **`bareOsIpc.createDuplexBridge`**, **`/proc/bare_os_swarm`**, VFS union read (**`BARE_OS_VFS_UNION_PREFIXES`**), **`BARE_OS_URANDOM_CRYPTO=0`**, async **`bareOsRequestPearReload`**, **`bareOsPublishBootReady.subsystems`**, **`verifyBareModuleLockfile`**. Kernel: **`BARE_OS_BOOT_MANIFEST`**, **`/etc/bare-os/selftest.d/`**, stock init hooks. |
| 1.7.0 | 0.1.0 | **`ctx.bare`**: frozen map of Holepunch-style npm modules for in-image scripts (manifest-driven host `import()` + optional trusted **`/lib/bare/bundles/*.js`** merge). Caps **`bareCtxModules`**, **`bareDriveBundles`**. Env **`BARE_OS_BARE_MODULES`**, **`BARE_OS_BARE_DRIVE_BUNDLES`**. Workspace **`bare-os-bare-libs`** builds seeded bundles. |
| 1.6.0 | 0.1.0 | Abort/timeout on `execLine`, `readLine`, `runBinCommand`, VFS `readFile`/`writeFile`; IPC fan-out + JSON-RPC token/line limits; HTTP allow/deny + audit; `/proc/bare_os_resources`, `/proc/bare_os_features`; `/run/bare-os/virtual/*`; booter boot phases in `boot.json` (`booterPhases`); optional `ctx.bareOsHostStats`, `ctx.httpFetch` policy wrapper; initd `ReadinessPath` / `ReadinessTimeoutSec`; Pear/sandbox stubs. |
+159 -2
View File
@@ -18,6 +18,8 @@ import {
runBinCommand,
runUserScriptFromSource
} from './lib/kernel-runner.js'
import { verifyBootManifestEd25519 } from '#bare-os-boot-manifest-sig'
import { getBareInitdJournalNdjson } from './lib/bare-initd-journal.js'
import { createBareOsSandboxContext } from './lib/bare-os-sandbox.js'
import { createVfs } from './lib/vfs.js'
import { createBareOsIpc } from './lib/bare-os-ipc.js'
@@ -76,7 +78,7 @@ import {
bareOsListThemeNames
} from './lib/bare-os-theme-presets.js'
import './lib/bare-cron.js'
import { createHash } from 'node:crypto'
import { createHash } from 'bare-crypto'
const { randomUUID, randomBytes } = bareCrypto
@@ -319,7 +321,19 @@ async function executeKernel(disk, store, swarm, initSource) {
'BARE_OS_YES_MAX_LINES',
'BARE_OS_SHUF_MAX_LINES',
'BARE_OS_SPLIT_MAX_FILES',
'BARE_OS_NPROC'
'BARE_OS_NPROC',
'BARE_OS_BOOT_MANIFEST',
'BARE_OS_BOOT_MANIFEST_SIGN',
'BARE_OS_BOOT_MANIFEST_PUBKEY_HEX',
'BARE_OS_SANDBOX_SCRIPT',
'BARE_OS_VFS_UNION_PREFIXES',
'BARE_OS_URANDOM_CRYPTO',
'BARE_OS_INITD_MAX_PARALLEL',
'BARE_OS_VFS_BIN_CACHE',
'BARE_OS_SANDBOX_WORKER',
'BARE_OS_BLIND_BOOTSTRAP_URL',
'BARE_OS_BLIND_BOOTSTRAP_JSON',
'BARE_OS_MIRROR_READ_KEY'
]) {
const v = hostEnv[k]
if (v != null && v !== '') shellEnv[k] = v
@@ -538,6 +552,57 @@ async function executeKernel(disk, store, swarm, initSource) {
protocol: 'bare-os-v1'
})}\n`
},
procBareOsReplicationText() {
const tk = topicKey()
const peers = disk.peers?.size ?? 0
const mk = shellEnv.BARE_OS_MIRROR_READ_KEY
return `${JSON.stringify({
topicHex: b4a.toString(tk, 'hex'),
peerCount: peers,
session: sessionStatsRef,
mirrorHint: !!(mk && String(mk).trim()),
atMs: Date.now()
})}\n`
},
procBareOsCapabilitiesText() {
const c = buildBareOsRuntimeCaps(shellEnv)
const paths = (c.pseudoFsPaths || []).join('\n ')
return [
'# bare_os_capabilities (summary; see .json for machine-readable)',
`ctxApiVersion: ${c.ctxApiVersion}`,
'pipeline: ' + JSON.stringify(c.pipeline),
'quotas: ' + JSON.stringify(c.quotas),
'pseudoFsPaths:',
' ' + paths,
'features: ' + JSON.stringify(c.features),
''
].join('\n')
},
procBareOsCapabilitiesJsonText() {
return `${JSON.stringify(buildBareOsRuntimeCaps(shellEnv), null, 2)}\n`
},
procBareOsBootstrapText() {
const url = shellEnv.BARE_OS_BLIND_BOOTSTRAP_URL
const j = shellEnv.BARE_OS_BLIND_BOOTSTRAP_JSON
if (url && String(url).trim()) {
return `${JSON.stringify({
blindBootstrapUrl: String(url).trim(),
source: 'BARE_OS_BLIND_BOOTSTRAP_URL'
})}\n`
}
if (j && String(j).trim()) {
try {
return `${JSON.stringify({
blindBootstrap: JSON.parse(String(j)),
source: 'BARE_OS_BLIND_BOOTSTRAP_JSON'
})}\n`
} catch {
return `${JSON.stringify({ error: 'invalid BARE_OS_BLIND_BOOTSTRAP_JSON' })}\n`
}
}
return '{}\n'
},
getUnitJournalNdjson: (unit) => getBareInitdJournalNdjson(unit),
getVirtualReaders: () => virtualReaderEntries,
unionReadPrefixes: parseUnionReadPrefixes(shellEnv),
sysClassNetLoText() {
@@ -888,6 +953,18 @@ async function executeKernel(disk, store, swarm, initSource) {
if (shellEnv.BARE_OS_SANDBOX_SCRIPT === '0') {
throw new Error('bareOsSandboxRunScript: disabled by BARE_OS_SANDBOX_SCRIPT=0')
}
if (
shellEnv.BARE_OS_SANDBOX_WORKER === '1' ||
shellEnv.BARE_OS_SANDBOX_WORKER === 'true'
) {
try {
this.console?.log?.(
'[bare-os] BARE_OS_SANDBOX_WORKER: running inline (bare-thread needs a file entrypoint)'
)
} catch {
/* ignore */
}
}
const sb = createBareOsSandboxContext(this)
const { raceWithAbortAndTimeout } = await import('./lib/bare-os-abort.js')
return raceWithAbortAndTimeout(
@@ -957,6 +1034,86 @@ async function executeKernel(disk, store, swarm, initSource) {
bareOsBootFileSha256Hex(buf) {
const u8 = buf instanceof Uint8Array ? buf : new Uint8Array(buf)
return createHash('sha256').update(u8).digest('hex')
},
/**
* Ed25519 verify for `/etc/bare-os/boot.manifest.json` when `BARE_OS_BOOT_MANIFEST_SIGN=1`.
* @param {Uint8Array | ArrayBuffer | null | undefined} manifestBytes
* @param {Uint8Array | ArrayBuffer | null | undefined} signatureBytes
* @param {string} [publicKeyHex] optional override; else `BARE_OS_BOOT_MANIFEST_PUBKEY_HEX`
*/
bareOsVerifyBootManifestSignature(manifestBytes, signatureBytes, publicKeyHex) {
const pub =
publicKeyHex != null && String(publicKeyHex).trim()
? String(publicKeyHex).trim()
: shellEnv.BARE_OS_BOOT_MANIFEST_PUBKEY_HEX || ''
const m =
manifestBytes instanceof Uint8Array
? manifestBytes
: manifestBytes
? new Uint8Array(manifestBytes)
: null
const s =
signatureBytes instanceof Uint8Array
? signatureBytes
: signatureBytes
? new Uint8Array(signatureBytes)
: null
return verifyBootManifestEd25519(m, s, pub)
},
/**
* Ask host to attach a mirror read key (Pear / Holepunch); emits process event when available.
* @param {{ key?: string, label?: string }} [opts]
*/
async bareOsRequestMirror(opts = {}) {
if (typeof globalThis.process?.emit === 'function') {
try {
globalThis.process.emit('bare-os:mirror-request', {
key: opts.key != null ? String(opts.key) : '',
label: opts.label != null ? String(opts.label) : ''
})
} catch {
/* ignore */
}
}
return { ok: true, hint: 'Host should listen for process "bare-os:mirror-request".' }
},
/**
* Hint host to export a personal-drive snapshot (corestore-snapshot style).
* @param {{ label?: string }} [opts]
*/
async bareOsExportPersonalSnapshot(opts = {}) {
if (typeof globalThis.process?.emit === 'function') {
try {
globalThis.process.emit('bare-os:export-personal-snapshot', {
label: opts.label != null ? String(opts.label) : 'default',
ts: Date.now()
})
} catch {
/* ignore */
}
}
return {
ok: true,
hint: 'Host listens for process "bare-os:export-personal-snapshot".'
}
},
/**
* Passthrough for Pear IPCstyle host bridges (see developer guide).
* @param {string} channel
* @param {Record<string, unknown>} payload
*/
bareOsPearIpcEmit(channel, payload) {
if (typeof globalThis.process?.emit !== 'function') return false
try {
globalThis.process.emit('bare-os:pear-ipc', {
channel: String(channel || ''),
payload: payload && typeof payload === 'object' ? payload : {},
ts: Date.now()
})
return true
} catch {
return false
}
}
}
@@ -0,0 +1,50 @@
/**
* In-memory structured initd journal (NDJSON lines) exposed under /run/bare-os/unit-journal/.
*/
const MAX_LINES_PER_UNIT = 400
const MAX_LINE_UTF8 = 4096
/** @type {Map<string, string[]>} */
const linesByUnit = new Map()
/**
* @param {string} unit
* @param {Record<string, unknown>} rec
*/
export function appendBareInitdJournal(unit, rec) {
if (!unit || !/^[a-zA-Z0-9._-]+$/.test(unit)) return
const payload = { ts: Date.now(), unit, ...rec }
let line
try {
line = JSON.stringify(payload) + '\n'
} catch {
return
}
if (line.length > MAX_LINE_UTF8) return
let arr = linesByUnit.get(unit)
if (!arr) {
arr = []
linesByUnit.set(unit, arr)
}
arr.push(line)
while (arr.length > MAX_LINES_PER_UNIT) arr.shift()
}
/**
* @param {string} unit
* @returns {string}
*/
export function getBareInitdJournalNdjson(unit) {
const arr = linesByUnit.get(unit)
return arr ? arr.join('') : ''
}
/** @returns {string[]} */
export function listBareInitdJournalUnits() {
return [...linesByUnit.keys()].sort()
}
export function clearBareInitdJournalForTests() {
linesByUnit.clear()
}
+33 -4
View File
@@ -12,7 +12,9 @@ export const BARE_INITD_DISABLED_FILE = '~/.config/bare-os/initd/disabled.txt'
* Optional `name.unit` files under this directory. Supports `[Unit]` keys:
* After=, Requires=, Wants=, TimeoutStartSec=, TimeoutStopSec=, Restart=, RestartSec=, ExecStartPost=, SocketActivationIpc=,
* ReadinessPath= (VFS path until exists), ReadinessTimeoutSec=,
* ExecHealthCmd=, HealthIntervalSec=, HealthFailureThreshold=, BareMaxExecDepth=.
* ExecHealthCmd=, HealthIntervalSec=, HealthFailureThreshold=, BareMaxExecDepth=,
* Before= (reverse edge: listed units start after this one), OnFailure= (execLine after restart exhausted),
* FailureAction=exec|none (default exec when OnFailure is set).
*/
export const BARE_INITD_UNITS_DIR = '~/.config/bare-os/units'
@@ -43,7 +45,10 @@ export const BARE_INITD_DEFAULT_AFTER = Object.freeze({
* execHealthCmd: string | null,
* healthIntervalSec: number | null,
* healthFailureThreshold: number | null,
* bareMaxExecDepth: number | null
* bareMaxExecDepth: number | null,
* before: string[],
* onFailure: string | null,
* failureAction: 'exec' | 'none'
* }} BareInitdUnitDropIn
*/
@@ -64,7 +69,10 @@ export function emptyUnitDropIn() {
execHealthCmd: null,
healthIntervalSec: null,
healthFailureThreshold: null,
bareMaxExecDepth: null
bareMaxExecDepth: null,
before: [],
onFailure: null,
failureAction: 'exec'
}
}
@@ -133,11 +141,20 @@ export function parseUnitDropInText(text) {
} else if (key === 'baremaxexecdepth') {
const n = Number.parseInt(val, 10)
if (Number.isFinite(n) && n > 0) out.bareMaxExecDepth = n
} else if (key === 'before') {
out.before.push(...parseList(val))
} else if (key === 'onfailure') {
if (val.trim()) out.onFailure = val
} else if (key === 'failureaction') {
const v = val.toLowerCase()
if (v === 'none') out.failureAction = 'none'
else out.failureAction = 'exec'
}
}
out.after = [...new Set(out.after)]
out.requires = [...new Set(out.requires)]
out.wants = [...new Set(out.wants)]
out.before = [...new Set(out.before)]
return out
}
@@ -166,7 +183,10 @@ export function mergeUnitDropIns(base, user) {
healthIntervalSec: user.healthIntervalSec ?? base.healthIntervalSec,
healthFailureThreshold:
user.healthFailureThreshold ?? base.healthFailureThreshold,
bareMaxExecDepth: user.bareMaxExecDepth ?? base.bareMaxExecDepth
bareMaxExecDepth: user.bareMaxExecDepth ?? base.bareMaxExecDepth,
before: pickArr(user.before, base.before),
onFailure: user.onFailure ?? base.onFailure,
failureAction: user.failureAction ?? base.failureAction
}
}
@@ -271,6 +291,15 @@ export async function loadInitdUnitDropIns(vfs, services, defaultAfter) {
fromFile.after = [...new Set([...def, ...fromFile.after])]
map.set(s.name, fromFile)
}
for (const [name, di] of map) {
for (const b of di.before) {
if (!/^[a-zA-Z0-9._-]+$/.test(b)) continue
const target = map.get(b)
if (target) {
target.after = [...new Set([...target.after, name])]
}
}
}
return map
}
+171 -67
View File
@@ -9,6 +9,7 @@ import {
INITD_LOG,
KERNEL_CONSOLE_LOG
} from './bare-os-var-log.js'
import { appendBareInitdJournal } from './bare-initd-journal.js'
import {
BARE_INITD_DEFAULT_AFTER,
emptyUnitDropIn,
@@ -287,6 +288,7 @@ function scheduleUnitHealth(ctx, name, dropIn) {
/* ignore */
}
void appendVarLog(ctx, INITD_LOG, name, msg)
appendBareInitdJournal(name, { event: 'health_failed', error: msg })
}
})()
}, intervalMs)
@@ -370,6 +372,94 @@ export async function restartBareService(ctx, name) {
}
}
/**
* @param {BareService} s
* @param {import('./bare-initd-user.js').BareInitdUnitDropIn} dropIn
*/
async function runOnFailureHookForUnit(ctx, s, dropIn) {
if (!dropIn.onFailure?.trim() || dropIn.failureAction === 'none') return
if (typeof ctx.execLine !== 'function') return
try {
await ctx.execLine(dropIn.onFailure.trim())
appendBareInitdJournal(s.name, { event: 'on_failure_ran' })
} catch (e) {
const msg = e?.message || String(e)
try {
ctx.console?.error?.(`[bare-initd] ${s.name} OnFailure: ${msg}`)
} catch {
/* ignore */
}
}
}
/**
* @param {Record<string, unknown>} ctx
* @param {BareService} s
* @param {import('./bare-initd-user.js').BareInitdUnitDropIn} dropIn
*/
async function startNormalBareInitdUnit(ctx, s, dropIn) {
const t0 = Date.now()
const startSec = dropIn.timeoutStartSec
const maxAttempts =
dropIn.restart === 'on-failure' || dropIn.restart === 'always' ? 3 : 1
const restartDelayMs =
dropIn.restartSec != null && dropIn.restartSec >= 0
? Math.round(dropIn.restartSec * 1000)
: 1000
appendBareInitdJournal(s.name, { event: 'start_scheduled' })
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
if (attempt > 0) {
appendBareInitdJournal(s.name, { event: 'restart_attempt', attempt })
await new Promise((r) => setTimeout(r, restartDelayMs))
}
await withTimeoutSec(s.start(ctx), startSec, `start ${s.name}`)
if (dropIn.readinessPath) {
const rsec = dropIn.readinessTimeoutSec ?? 30
await waitForReadinessPath(ctx, dropIn.readinessPath, rsec)
}
runtime.set(s.name, { phase: 'active', startedAtMs: t0 })
appendBareInitdJournal(s.name, { event: 'active', attempt })
const post = dropIn.execStartPost
if (post && typeof ctx.execLine === 'function' && post.trim()) {
try {
await ctx.execLine(post.trim())
} catch (e) {
const msg = e?.message || String(e)
try {
ctx.console?.error?.(
`[bare-initd] ${s.name} ExecStartPost: ${msg}`
)
} catch {
/* ignore */
}
void appendVarLog(ctx, INITD_LOG, s.name, 'ExecStartPost: ' + msg)
}
}
scheduleUnitHealth(ctx, s.name, dropIn)
return
} catch (e) {
const msg = e?.message || String(e)
appendBareInitdJournal(s.name, {
event: 'start_error',
attempt,
error: msg
})
if (attempt === maxAttempts - 1) {
runtime.set(s.name, { phase: 'failed', startedAtMs: t0, error: msg })
try {
ctx.console?.error?.(`[bare-initd] ${s.name}: ${msg}`)
} catch {
/* ignore */
}
void appendVarLog(ctx, INITD_LOG, s.name, msg)
await runOnFailureHookForUnit(ctx, s, dropIn)
appendBareInitdJournal(s.name, { event: 'failed_final', error: msg })
}
}
}
}
/**
* @param {Record<string, unknown>} ctx
*/
@@ -377,10 +467,12 @@ export async function startBareInitd(ctx) {
runtime.clear()
await ensureBareOsVarLogTree(ctx)
const vfs = ctx.vfs
/** @type {Map<string, import('./bare-initd-user.js').BareInitdUnitDropIn>} */
let dropInsMap = new Map()
let ordered = registry
if (vfs && typeof vfs.readFile === 'function') {
const disabled = await readInitdDisabledSet(vfs)
const dropIns = await loadInitdUnitDropIns(
dropInsMap = await loadInitdUnitDropIns(
vfs,
registry,
BARE_INITD_DEFAULT_AFTER
@@ -391,7 +483,7 @@ export async function startBareInitd(ctx) {
const wantsMap = new Map()
/** @type {Map<string, string[]>} */
const afterMap = new Map()
for (const [name, di] of dropIns) {
for (const [name, di] of dropInsMap) {
afterMap.set(name, di.after)
requiresMap.set(name, di.requires)
wantsMap.set(name, di.wants)
@@ -404,34 +496,88 @@ export async function startBareInitd(ctx) {
wantsMap
)
}
for (const s of ordered) {
const t0 = Date.now()
/** @type {import('./bare-initd-user.js').BareInitdUnitDropIn} */
const dropIn =
vfs && typeof vfs.readFile === 'function'
? await readUnitDropIn(vfs, s.name)
: emptyUnitDropIn()
if (dropIn.socketActivationIpc && ctx.bareOsIpc) {
const rawPar = ctx.env && ctx.env.BARE_OS_INITD_MAX_PARALLEL
const maxP = Math.max(
1,
Math.min(32, Number.parseInt(String(rawPar ?? '1'), 10) || 1)
)
const activeNames = new Set(ordered.map((s) => s.name))
/** @type {Map<string, Set<string>>} */
const prereq = new Map()
for (const s of ordered) {
const di = dropInsMap.get(s.name) || emptyUnitDropIn()
const inc = new Set()
for (const a of di.after) if (activeNames.has(a)) inc.add(a)
for (const r of di.requires) if (activeNames.has(r)) inc.add(r)
for (const w of di.wants) if (activeNames.has(w)) inc.add(w)
prereq.set(s.name, inc)
}
/** @param {string[]} names @returns {string[][]} */
function computeLevels(names) {
const remaining = new Set(names)
/** @type {string[][]} */
const levels = []
while (remaining.size) {
const ready = [...remaining].filter((n) => {
for (const p of prereq.get(n) || []) {
if (remaining.has(p)) return false
}
return true
})
if (!ready.length) {
const n = [...remaining].sort()[0]
levels.push([n])
remaining.delete(n)
continue
}
ready.sort()
levels.push(ready)
for (const n of ready) remaining.delete(n)
}
return levels
}
const levels = computeLevels(ordered.map((s) => s.name))
for (const level of levels) {
/** @type {{ s: BareService, di: import('./bare-initd-user.js').BareInitdUnitDropIn }[]} */
const sockets = []
/** @type {{ s: BareService, di: import('./bare-initd-user.js').BareInitdUnitDropIn }[]} */
const normals = []
for (const name of level) {
const s = registry.find((x) => x.name === name)
if (!s) continue
const di = dropInsMap.get(s.name) || emptyUnitDropIn()
if (di.socketActivationIpc && ctx.bareOsIpc) sockets.push({ s, di })
else normals.push({ s, di })
}
for (const { s, di } of sockets) {
const t0 = Date.now()
appendBareInitdJournal(s.name, { event: 'socket_wait' })
try {
ctx.bareOsIpc.create(dropIn.socketActivationIpc)
ctx.bareOsIpc.create(di.socketActivationIpc)
} catch {
/* exists */
}
runtime.set(s.name, { phase: 'inactive', startedAtMs: t0 })
const ipcName = dropIn.socketActivationIpc
const startSec = dropIn.timeoutStartSec
const ipcName = di.socketActivationIpc
const startSec = di.timeoutStartSec
void (async () => {
try {
await ctx.bareOsIpc.take(ipcName)
const t1 = Date.now()
await withTimeoutSec(s.start(ctx), startSec, `start ${s.name}`)
if (dropIn.readinessPath) {
const rsec = dropIn.readinessTimeoutSec ?? 30
await waitForReadinessPath(ctx, dropIn.readinessPath, rsec)
if (di.readinessPath) {
const rsec = di.readinessTimeoutSec ?? 30
await waitForReadinessPath(ctx, di.readinessPath, rsec)
}
runtime.set(s.name, { phase: 'active', startedAtMs: t1 })
const post = dropIn.execStartPost
appendBareInitdJournal(s.name, { event: 'active', socket: true })
const post = di.execStartPost
if (post && typeof ctx.execLine === 'function' && post.trim()) {
try {
await ctx.execLine(post.trim())
@@ -447,7 +593,7 @@ export async function startBareInitd(ctx) {
void appendVarLog(ctx, INITD_LOG, s.name, 'ExecStartPost: ' + msg)
}
}
scheduleUnitHealth(ctx, s.name, dropIn)
scheduleUnitHealth(ctx, s.name, di)
} catch (e) {
const msg = e?.message || String(e)
runtime.set(s.name, { phase: 'failed', startedAtMs: t0, error: msg })
@@ -457,59 +603,17 @@ export async function startBareInitd(ctx) {
/* ignore */
}
void appendVarLog(ctx, INITD_LOG, s.name, msg)
appendBareInitdJournal(s.name, { event: 'failed_socket', error: msg })
await runOnFailureHookForUnit(ctx, s, di)
}
})()
continue
}
const startSec = dropIn.timeoutStartSec
const maxAttempts =
dropIn.restart === 'on-failure' || dropIn.restart === 'always' ? 3 : 1
const restartDelayMs =
dropIn.restartSec != null && dropIn.restartSec >= 0
? Math.round(dropIn.restartSec * 1000)
: 1000
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
if (attempt > 0) {
await new Promise((r) => setTimeout(r, restartDelayMs))
}
await withTimeoutSec(s.start(ctx), startSec, `start ${s.name}`)
if (dropIn.readinessPath) {
const rsec = dropIn.readinessTimeoutSec ?? 30
await waitForReadinessPath(ctx, dropIn.readinessPath, rsec)
}
runtime.set(s.name, { phase: 'active', startedAtMs: t0 })
const post = dropIn.execStartPost
if (post && typeof ctx.execLine === 'function' && post.trim()) {
try {
await ctx.execLine(post.trim())
} catch (e) {
const msg = e?.message || String(e)
try {
ctx.console?.error?.(
`[bare-initd] ${s.name} ExecStartPost: ${msg}`
)
} catch {
/* ignore */
}
void appendVarLog(ctx, INITD_LOG, s.name, 'ExecStartPost: ' + msg)
}
}
scheduleUnitHealth(ctx, s.name, dropIn)
break
} catch (e) {
const msg = e?.message || String(e)
if (attempt === maxAttempts - 1) {
runtime.set(s.name, { phase: 'failed', startedAtMs: t0, error: msg })
try {
ctx.console?.error?.(`[bare-initd] ${s.name}: ${msg}`)
} catch {
/* ignore */
}
void appendVarLog(ctx, INITD_LOG, s.name, msg)
}
}
for (let i = 0; i < normals.length; i += maxP) {
const chunk = normals.slice(i, i + maxP)
await Promise.all(
chunk.map(({ s, di }) => startNormalBareInitdUnit(ctx, s, di))
)
}
}
}
@@ -0,0 +1,9 @@
'use strict'
/**
* CJS bridge: `require('bare-crypto/lib/key.js')` bypasses package `exports` on Node and avoids
* pulling Bare-only addon graphs into ESM static analysis. Pear/Bare executes this file with CJS
* `require` (no Node `module` built-in needed from our ESM entry).
*/
const { verify } = require('bare-crypto')
const { Ed25519PublicKey } = require('bare-crypto/lib/key.js')
module.exports = { verify, Ed25519PublicKey }
@@ -0,0 +1,44 @@
/**
* Pear / Bare: load crypto via CJS bridge (`require` provided by runtime). No `node:module`.
*/
import b4a from 'b4a'
import bridge from './bare-os-boot-manifest-sig-bridge.cjs'
const { verify, Ed25519PublicKey } = bridge
/**
* @param {Uint8Array | null | undefined} manifestBytes
* @param {Uint8Array | null | undefined} signatureBytes
* @param {string} publicKeyHex 64 hex chars (32-byte Ed25519 public key)
* @returns {boolean}
*/
export function verifyBootManifestEd25519(manifestBytes, signatureBytes, publicKeyHex) {
if (!manifestBytes?.length || !signatureBytes?.length) return false
const hex = String(publicKeyHex || '').trim().toLowerCase().replace(/^0x/, '')
if (!/^[0-9a-f]{64}$/.test(hex)) return false
/** @type {Uint8Array} */
let sig
if (signatureBytes.length === 64) {
sig = signatureBytes
} else {
const t = b4a.toString(signatureBytes, 'utf8').trim()
if (/^[0-9a-f]{128}$/i.test(t)) {
sig = b4a.from(t, 'hex')
} else {
try {
sig = b4a.from(t, 'base64')
} catch {
return false
}
}
}
if (sig.length !== 64) return false
try {
const pub = b4a.from(hex, 'hex')
const key = new Ed25519PublicKey(pub)
return verify('ed25519', manifestBytes, key, sig)
} catch {
return false
}
}
@@ -0,0 +1,47 @@
/**
* Node (brittle-node tests / `node index.js`): lazy `require('bare-crypto')` inside try/catch.
* `bare-crypto` is Bare-oriented and typically throws under Node; callers still get a boolean.
*/
import b4a from 'b4a'
import { createRequire } from 'node:module'
const require = createRequire(import.meta.url)
/**
* @param {Uint8Array | null | undefined} manifestBytes
* @param {Uint8Array | null | undefined} signatureBytes
* @param {string} publicKeyHex 64 hex chars (32-byte Ed25519 public key)
* @returns {boolean}
*/
export function verifyBootManifestEd25519(manifestBytes, signatureBytes, publicKeyHex) {
if (!manifestBytes?.length || !signatureBytes?.length) return false
const hex = String(publicKeyHex || '').trim().toLowerCase().replace(/^0x/, '')
if (!/^[0-9a-f]{64}$/.test(hex)) return false
/** @type {Uint8Array} */
let sig
if (signatureBytes.length === 64) {
sig = signatureBytes
} else {
const t = b4a.toString(signatureBytes, 'utf8').trim()
if (/^[0-9a-f]{128}$/i.test(t)) {
sig = b4a.from(t, 'hex')
} else {
try {
sig = b4a.from(t, 'base64')
} catch {
return false
}
}
}
if (sig.length !== 64) return false
try {
const { verify } = require('bare-crypto')
const { Ed25519PublicKey } = require('bare-crypto/lib/key.js')
const pub = b4a.from(hex, 'hex')
const key = new Ed25519PublicKey(pub)
return verify('ed25519', manifestBytes, key, sig)
} catch {
return false
}
}
@@ -2,4 +2,4 @@
* Semantic version of the booter `ctx` contract for custom kernels.
* Bump when adding/removing/renaming documented `ctx` fields or changing behavior.
*/
export const BARE_OS_CTX_API_VERSION = '1.8.0'
export const BARE_OS_CTX_API_VERSION = '1.9.0'
+18
View File
@@ -56,6 +56,11 @@ export interface BareOsIpc {
take: () => Promise<Uint8Array>
}
}
duplexJsonRoundTrip(
baseName: string,
payload: Record<string, unknown>,
opts?: { timeoutMs?: number }
): Promise<Record<string, unknown>>
}
export interface BareOsHostStats {
@@ -120,6 +125,19 @@ export interface BareOsKernelContext {
opts?: BareOsAbortOpts
): Promise<void>
bareOsBootFileSha256Hex(buf: Uint8Array | ArrayBuffer): string
bareOsVerifyBootManifestSignature(
manifestBytes: Uint8Array | ArrayBuffer | null | undefined,
signatureBytes: Uint8Array | ArrayBuffer | null | undefined,
publicKeyHex?: string
): boolean
bareOsRequestMirror(opts?: {
key?: string
label?: string
}): Promise<{ ok: boolean; hint: string }>
bareOsExportPersonalSnapshot(opts?: {
label?: string
}): Promise<{ ok: boolean; hint: string }>
bareOsPearIpcEmit(channel: string, payload: Record<string, unknown>): boolean
bareOsRequestPearReload(opts?: {
persistRequest?: boolean
}): Promise<{
@@ -295,6 +295,31 @@ export function createBareOsIpc(opts = {}) {
take: () => ba.take()
}
return { left, right }
},
/**
* One JSON line request/response over a {@link createDuplexBridge} side (`bareOsRpc: "2"`).
* @param {{ push: (buf: Uint8Array | ArrayBuffer) => void, take: () => Promise<Uint8Array> }} side
* @param {Record<string, unknown>} request
*/
async duplexJsonRoundTrip(side, request) {
if (!request || typeof request !== 'object')
throw new Error('bare-os ipc: invalid duplex request')
const line =
JSON.stringify({
bareOsRpc: '2',
...request
}) + '\n'
if (line.length > maxJsonLine) {
throw new Error('bare-os ipc: duplex JSON line exceeds max length')
}
side.push(b4a.from(line, 'utf8'))
const u8 = await side.take()
const res = b4a.toString(u8, 'utf8').trim()
if (res.length > maxJsonLine) {
throw new Error('bare-os ipc: duplex response too large')
}
return /** @type {Record<string, unknown>} */ (JSON.parse(res))
}
}
}
@@ -30,6 +30,10 @@ export const BARE_OS_PSEUDO_FS_PATHS = Object.freeze([
'/proc/bare_os_quotas',
'/proc/bare_os_resources',
'/proc/bare_os_swarm',
'/proc/bare_os_replication',
'/proc/bare_os_capabilities',
'/proc/bare_os_capabilities.json',
'/proc/bare_os_bootstrap',
'/proc/bare_os_session_stats',
'/proc/bare_os_version',
'/proc/cpuinfo',
@@ -52,6 +56,7 @@ export const BARE_OS_PSEUDO_FS_PATHS = Object.freeze([
'/run/bare-os/ready',
'/run/bare-os/session',
'/run/bare-os/units',
'/run/bare-os/unit-journal',
'/run/bare-os/virtual',
'/run/bare-os/ipc',
'/sys',
@@ -165,7 +170,29 @@ export function buildBareOsRuntimeCaps(shellEnv) {
shellEnv.BARE_OS_BOOT_MANIFEST === '1' ||
shellEnv.BARE_OS_BOOT_MANIFEST === 'true',
bareModuleLockfile: true,
oidcPublishHook: true
oidcPublishHook: true,
ipcDuplexJsonRpc: true,
procReplicationSnapshot: true,
procCapabilitiesExport: true,
blindBootstrapProc: true,
initdUnitJournal: true,
initdParallelStart:
Number.parseInt(
String(shellEnv.BARE_OS_INITD_MAX_PARALLEL || '1'),
10
) > 1,
vfsBinReadCache:
shellEnv.BARE_OS_VFS_BIN_CACHE === '1' ||
shellEnv.BARE_OS_VFS_BIN_CACHE === 'true',
bootManifestEd25519:
shellEnv.BARE_OS_BOOT_MANIFEST_SIGN === '1' ||
shellEnv.BARE_OS_BOOT_MANIFEST_SIGN === 'true',
sandboxWorkerHint:
shellEnv.BARE_OS_SANDBOX_WORKER === '1' ||
shellEnv.BARE_OS_SANDBOX_WORKER === 'true',
mirrorRequestHook: true,
personalSnapshotExportHook: true,
pearIpcEmitHook: true
})
})
}
+142 -1
View File
@@ -367,6 +367,16 @@ export function tokenize(line) {
}
continue
}
if (c === '(') {
tokens.push({ type: 'op', value: '(' })
i++
continue
}
if (c === ')') {
tokens.push({ type: 'op', value: ')' })
i++
continue
}
if (c === '<') {
if (line[i + 1] === '<' && line[i + 2] === '<') {
tokens.push({ type: 'op', value: '<<<' })
@@ -418,7 +428,9 @@ export function tokenize(line) {
ch === '>' ||
ch === '<' ||
ch === ';' ||
ch === '&'
ch === '&' ||
ch === '(' ||
ch === ')'
)
break
word += ch
@@ -1175,6 +1187,8 @@ function splitTopLevelStatements(tokens) {
else if (t.value === 'fi') depth = Math.max(0, depth - 1)
else if (t.value === 'while' || t.value === 'for') depth++
else if (t.value === 'done') depth = Math.max(0, depth - 1)
else if (t.value === 'case') depth++
else if (t.value === 'esac') depth = Math.max(0, depth - 1)
}
if (t.type === 'op' && t.value === ';' && depth === 0) {
if (cur.length) out.push(cur)
@@ -1204,6 +1218,8 @@ function splitTopLevelByAmpersand(tokens) {
else if (t.value === 'fi') depth = Math.max(0, depth - 1)
else if (t.value === 'while' || t.value === 'for') depth++
else if (t.value === 'done') depth = Math.max(0, depth - 1)
else if (t.value === 'case') depth++
else if (t.value === 'esac') depth = Math.max(0, depth - 1)
}
if (t.type === 'op' && t.value === '&' && depth === 0) {
out.push(cur)
@@ -1447,6 +1463,129 @@ async function execForConstruct(ctx, tokens) {
return 'ok'
}
/**
* @param {Token[]} toks
* @param {Record<string, string>} env
* @returns {string[]}
*/
function casePatternList(toks, env) {
/** @type {string[]} */
const out = []
/** @type {Token[]} */
let cur = []
for (const t of toks) {
if (t.type === 'op' && t.value === '|') {
if (cur.length) {
const s = cur.map((w) => w.value).join(' ')
out.push(expandWord(s.trim(), env))
cur = []
}
} else if (t.type === 'word') {
cur.push(t)
}
}
if (cur.length) {
const s = cur.map((w) => w.value).join(' ')
out.push(expandWord(s.trim(), env))
}
return out.filter(Boolean)
}
/**
* @param {string} subject
* @param {string} pat
*/
function casePatternMatches(subject, pat) {
if (pat === '*') return true
return subject === pat
}
/**
* `case WORD in pattern) list ;; … esac` — bounded branches; patterns support `|` alternation and `*`.
* @param {Record<string, unknown>} ctx
* @param {Token[]} tokens
* @returns {Promise<'exit' | 'ok'>}
*/
async function execCaseConstruct(ctx, tokens) {
const last = tokens[tokens.length - 1]
if (last.type !== 'word' || last.value !== 'esac') {
ctx.console.error('shell: case: missing esac')
ctx.exitCode = 2
return 'ok'
}
if (
tokens.length < 5 ||
tokens[1].type !== 'word' ||
tokens[2].type !== 'word' ||
tokens[2].value !== 'in'
) {
ctx.console.error('shell: case: expected `case WORD in`')
ctx.exitCode = 2
return 'ok'
}
const env = ctx.vfs.env
const subj = expandWord(tokens[1].value, env)
const maxBranches = Number.parseInt(
ctx.vfs?.env?.BARE_OS_SHELL_CASE_MAX_BRANCHES || '32',
10
)
const cap = Number.isFinite(maxBranches) && maxBranches > 0 ? maxBranches : 32
let i = 3
let branches = 0
while (i < tokens.length - 1) {
if (++branches > cap) {
ctx.console.error('shell: case: too many branches (see BARE_OS_SHELL_CASE_MAX_BRANCHES)')
ctx.exitCode = 2
return 'ok'
}
let paren = -1
for (let k = i; k < tokens.length - 1; k++) {
const t = tokens[k]
if (t.type === 'op' && t.value === ')') {
paren = k
break
}
}
if (paren < 0) {
ctx.console.error('shell: case: expected )')
ctx.exitCode = 2
return 'ok'
}
const patToks = tokens.slice(i, paren)
let dsemi = -1
for (let k = paren + 1; k < tokens.length - 1; k++) {
const t = tokens[k]
const n = tokens[k + 1]
if (
t.type === 'op' &&
t.value === ';' &&
n &&
n.type === 'op' &&
n.value === ';'
) {
dsemi = k
break
}
}
if (dsemi < 0) {
ctx.console.error('shell: case: expected ;;')
ctx.exitCode = 2
return 'ok'
}
const bodyToks = tokens.slice(paren + 1, dsemi)
const pats = casePatternList(patToks, env)
const matched = pats.some((p) => casePatternMatches(subj, p))
if (matched) {
const r = await execSemicolonLists(ctx, bodyToks)
if (r === 'exit') return 'exit'
return 'ok'
}
i = dsemi + 2
}
ctx.exitCode = 0
return 'ok'
}
/**
* @param {Record<string, unknown>} ctx
* @param {Token[]} stmt
@@ -1460,6 +1599,8 @@ async function dispatchShellStatement(ctx, stmt) {
return execWhileConstruct(ctx, stmt)
if (head?.type === 'word' && head.value === 'for')
return execForConstruct(ctx, stmt)
if (head?.type === 'word' && head.value === 'case')
return execCaseConstruct(ctx, stmt)
return execAndOrList(ctx, stmt)
}
@@ -16,6 +16,7 @@ import {
readInitdDisabledSet,
writeInitdDisabledSet
} from './bare-initd-user.js'
import { getBareInitdJournalNdjson } from './bare-initd-journal.js'
/** @param {Uint8Array | null} buf @param {number} maxLines */
function tailUtf8Lines(buf, maxLines) {
@@ -112,6 +113,13 @@ export async function runSystemctlCli(ctx, argv) {
return
}
await printLogTail(ctx, def.logPath, lines, 'journalctl')
const jtext = getBareInitdJournalNdjson(unit)
if (jtext.trim()) {
const jl = jtext.trimEnd().split(/\r?\n/)
const tailJ = jl.slice(-lines).join('\n')
ctx.console.log('--- unit journal (NDJSON tail) ---')
ctx.console.log(tailJ)
}
ctx.exitCode = ctx.exitCode ?? 0
return
}
+148 -1
View File
@@ -1,6 +1,7 @@
import unixPathResolve from 'unix-path-resolve'
import b4a from 'b4a'
import { raceWithAbortAndTimeout } from './bare-os-abort.js'
import { listBareInitdJournalUnits } from './bare-initd-journal.js'
import {
extractBareOs,
identityNames,
@@ -48,6 +49,11 @@ const DIR_MARKER = '.bareos_empty'
* procBareOsResourcesText?: () => string,
* procBareOsFeaturesText?: () => string,
* procBareOsSwarmText?: () => string,
* procBareOsReplicationText?: () => string,
* procBareOsCapabilitiesText?: () => string,
* procBareOsCapabilitiesJsonText?: () => string,
* procBareOsBootstrapText?: () => string,
* getUnitJournalNdjson?: (unit: string) => string,
* getVirtualReaders?: () => Map<string, unknown>,
* unionReadPrefixes?: readonly string[],
* sysClassNetLoText?: () => string
@@ -118,6 +124,26 @@ export function createVfs(
typeof vfsOptions.procBareOsSwarmText === 'function'
? vfsOptions.procBareOsSwarmText
: null
const procBareOsReplicationText =
typeof vfsOptions.procBareOsReplicationText === 'function'
? vfsOptions.procBareOsReplicationText
: null
const procBareOsCapabilitiesText =
typeof vfsOptions.procBareOsCapabilitiesText === 'function'
? vfsOptions.procBareOsCapabilitiesText
: null
const procBareOsCapabilitiesJsonText =
typeof vfsOptions.procBareOsCapabilitiesJsonText === 'function'
? vfsOptions.procBareOsCapabilitiesJsonText
: null
const procBareOsBootstrapText =
typeof vfsOptions.procBareOsBootstrapText === 'function'
? vfsOptions.procBareOsBootstrapText
: null
const getUnitJournalNdjson =
typeof vfsOptions.getUnitJournalNdjson === 'function'
? vfsOptions.getUnitJournalNdjson
: null
const getVirtualReaders =
typeof vfsOptions.getVirtualReaders === 'function'
? vfsOptions.getVirtualReaders
@@ -127,6 +153,11 @@ export function createVfs(
(s) => typeof s === 'string' && s.startsWith('/')
)
: []
const binCacheEnabled =
env.BARE_OS_VFS_BIN_CACHE === '1' || env.BARE_OS_VFS_BIN_CACHE === 'true'
/** @type {Map<string, Uint8Array>} */
const binReadCache = binCacheEnabled ? new Map() : null
const BIN_READ_CACHE_MAX = 64
const sysClassNetLoText =
typeof vfsOptions.sysClassNetLoText === 'function'
? vfsOptions.sysClassNetLoText
@@ -321,6 +352,30 @@ export function createVfs(
const t = procBareOsSwarmText ? procBareOsSwarmText() : '{}\n'
return utf8Encode(t)
}
if (f === 'bare_os_replication') {
const t = procBareOsReplicationText
? procBareOsReplicationText()
: '{}\n'
return utf8Encode(t)
}
if (f === 'bare_os_capabilities') {
const t = procBareOsCapabilitiesText
? procBareOsCapabilitiesText()
: '(no snapshot)\n'
return utf8Encode(t)
}
if (f === 'bare_os_capabilities_json') {
const t = procBareOsCapabilitiesJsonText
? procBareOsCapabilitiesJsonText()
: '{}\n'
return utf8Encode(t)
}
if (f === 'bare_os_bootstrap') {
const t = procBareOsBootstrapText
? procBareOsBootstrapText()
: '{}\n'
return utf8Encode(t)
}
if (f === 'net_dev') {
const t = procNetDevText
? procNetDevText()
@@ -338,6 +393,13 @@ export function createVfs(
: '# bare-initd: no snapshot provider\n'
return utf8Encode(t)
}
if (k === 'run' && f === 'unit_journal' && getUnitJournalNdjson) {
const u =
/** @type {{ unitJournalName?: string }} */ (routePseudo).unitJournalName ||
''
const t = getUnitJournalNdjson(u)
return utf8Encode(t || '')
}
if (k === 'run' && f === 'boot_profile') {
const t = bootProfileText ? bootProfileText() : '\n'
return utf8Encode(t)
@@ -489,6 +551,38 @@ export function createVfs(
file: 'bare_os_swarm'
}
}
if (sub === 'bare_os_replication') {
return {
virtualPseudo: true,
kind: 'proc',
node: 'file',
file: 'bare_os_replication'
}
}
if (sub === 'bare_os_capabilities') {
return {
virtualPseudo: true,
kind: 'proc',
node: 'file',
file: 'bare_os_capabilities'
}
}
if (sub === 'bare_os_capabilities.json') {
return {
virtualPseudo: true,
kind: 'proc',
node: 'file',
file: 'bare_os_capabilities_json'
}
}
if (sub === 'bare_os_bootstrap') {
return {
virtualPseudo: true,
kind: 'proc',
node: 'file',
file: 'bare_os_bootstrap'
}
}
if (sub === 'net' || sub === 'net/') {
return { virtualPseudo: true, kind: 'proc', node: 'dir', dir: 'net' }
}
@@ -512,6 +606,29 @@ export function createVfs(
if (n === '/run/bare-os/units') {
return { virtualPseudo: true, kind: 'run', node: 'file', file: 'units' }
}
if (n === '/run/bare-os/unit-journal' || n === '/run/bare-os/unit-journal/') {
return {
virtualPseudo: true,
kind: 'run',
node: 'dir',
dir: 'unit_journal_root'
}
}
{
const uj = '/run/bare-os/unit-journal/'
if (n.startsWith(uj)) {
const seg = n.slice(uj.length).replace(/\/+$/, '')
if (/^[a-zA-Z0-9._-]+\.ndjson$/.test(seg)) {
return {
virtualPseudo: true,
kind: 'run',
node: 'file',
file: 'unit_journal',
unitJournalName: seg.replace(/\.ndjson$/, '')
}
}
}
}
if (n === '/run/bare-os/boot_profile') {
return {
virtualPseudo: true,
@@ -1197,8 +1314,12 @@ export function createVfs(
}
if (pr.kind === 'proc' && pr.node === 'root') {
return [
'bare_os_bootstrap',
'bare_os_capabilities',
'bare_os_capabilities.json',
'bare_os_features',
'bare_os_quotas',
'bare_os_replication',
'bare_os_resources',
'bare_os_session_stats',
'bare_os_swarm',
@@ -1245,11 +1366,20 @@ export function createVfs(
'ipc',
'ready',
'session',
'unit-journal',
'units',
'virtual'
]
return bareOsIpc ? base : base.filter((x) => x !== 'ipc')
}
if (
pr.kind === 'run' &&
pr.node === 'dir' &&
pr.dir === 'unit_journal_root' &&
getUnitJournalNdjson
) {
return listBareInitdJournalUnits().map((u) => `${u}.ndjson`)
}
if (
pr.kind === 'run' &&
pr.node === 'dir' &&
@@ -1546,7 +1676,24 @@ export function createVfs(
}
}
}
return drive.get(p, { follow: true })
if (binReadCache && drive === systemDrive && abs.startsWith('/bin/')) {
const hit = binReadCache.get(abs)
if (hit) return new Uint8Array(hit)
}
const got = await drive.get(p, { follow: true })
if (
binReadCache &&
drive === systemDrive &&
abs.startsWith('/bin/') &&
got
) {
if (binReadCache.size >= BIN_READ_CACHE_MAX) {
const first = binReadCache.keys().next().value
binReadCache.delete(first)
}
binReadCache.set(abs, new Uint8Array(got))
}
return got
})(),
abortOpts,
'vfs.readFile'
+4
View File
@@ -75,6 +75,10 @@
"node:url": {
"bare": "bare-url",
"default": "node:url"
},
"#bare-os-boot-manifest-sig": {
"bare": "./lib/bare-os-boot-manifest-sig.bare.js",
"default": "./lib/bare-os-boot-manifest-sig.node.js"
}
},
"optionalDependencies": {
+63
View File
@@ -855,8 +855,12 @@ test('vfs /proc /sys read-only pseudo files; write rejected', async (t) => {
t.ok(root.includes('run'))
t.ok(root.includes('dev'))
t.alike(await vfs.readdir('/proc').then((a) => [...a].sort()), [
'bare_os_bootstrap',
'bare_os_capabilities',
'bare_os_capabilities.json',
'bare_os_features',
'bare_os_quotas',
'bare_os_replication',
'bare_os_resources',
'bare_os_session_stats',
'bare_os_swarm',
@@ -917,6 +921,7 @@ test('vfs /proc /sys read-only pseudo files; write rejected', async (t) => {
'ipc',
'ready',
'session',
'unit-journal',
'units',
'virtual'
])
@@ -1199,6 +1204,7 @@ test('buildBareOsRuntimeCaps matches ctx API version and pipeline env', async (t
t.ok(caps.pseudoFsPaths.includes('/proc/mounts'))
t.ok(caps.pseudoFsPaths.includes('/proc/bare_os_resources'))
t.ok(caps.pseudoFsPaths.includes('/run/bare-os/virtual'))
t.ok(caps.pseudoFsPaths.includes('/run/bare-os/unit-journal'))
t.is(caps.features.simulatedPipelines, true)
t.is(caps.features.httpDelegate, true)
t.is(caps.features.gitDelegate, true)
@@ -3904,6 +3910,63 @@ test('coreutils gnu-gap batch: paste tac rev md5sum expr tsort numfmt truncate i
rmSync(dir, { recursive: true, force: true })
})
test('verifyBootManifestEd25519 rejects invalid inputs', async (t) => {
const { verifyBootManifestEd25519 } = await import('#bare-os-boot-manifest-sig')
const msg = b4a.from('manifest-bytes', 'utf8')
t.absent(verifyBootManifestEd25519(msg, null, ''))
t.absent(verifyBootManifestEd25519(msg, msg, '00ff'))
const z64 = '0'.repeat(64)
t.absent(verifyBootManifestEd25519(msg, new Uint8Array(64), z64))
})
test('bareOsIpc.duplexJsonRoundTrip', async (t) => {
const ipc = createBareOsIpc()
const { left, right } = ipc.createDuplexBridge('dupjx')
const respP = ipc.duplexJsonRoundTrip(right, { id: 7, method: 'ping' })
const u8 = await left.take()
const req = JSON.parse(b4a.toString(u8, 'utf8'))
t.is(req.method, 'ping')
left.push(
b4a.from(
JSON.stringify({ bareOsRpc: '2', id: 7, result: 'pong' }) + '\n',
'utf8'
)
)
const out = await respP
t.is(out.result, 'pong')
})
test('unit journal exposed under /run/bare-os/unit-journal', async (t) => {
const {
appendBareInitdJournal,
clearBareInitdJournalForTests,
getBareInitdJournalNdjson
} = await import('./lib/bare-initd-journal.js')
clearBareInitdJournalForTests()
appendBareInitdJournal('demo', { event: 'unit_test' })
const dir = testCorestoreDir('vj')
const store = new Corestore(dir)
const sys = new Hyperdrive(store)
const personal = new Hyperdrive(store.namespace('pvj'))
await sys.ready()
await personal.ready()
const env = {
HOME: '/home/guest',
PWD: '/home/guest',
PATH: '/bin',
USER: 'guest'
}
const vfs = createVfs(sys, personal, env, null, {
getUnitJournalNdjson: (u) => getBareInitdJournalNdjson(u)
})
t.ok((await vfs.readdir('/run/bare-os/unit-journal')).includes('demo.ndjson'))
const j = b4a.toString(await vfs.readFile('/run/bare-os/unit-journal/demo.ndjson'))
t.ok(j.includes('unit_test'))
clearBareInitdJournalForTests()
await store.close()
rmSync(dir, { recursive: true, force: true })
})
async function readBuiltBin(name) {
const fs = await import('node:fs/promises')
const p = path.join(__dirname, '../../kernel/bin', name)