This commit is contained in:
Raven Scott
2026-04-03 19:39:13 -04:00
parent 02904e7523
commit 54ea903bd4
19 changed files with 435 additions and 29 deletions
+16
View File
@@ -39,6 +39,7 @@ import {
getBareServiceRuntime
} from './lib/bare-initd.js'
import { BARE_OS_CTX_API_VERSION } from './lib/bare-os-ctx-api.js'
import { buildBareOsRuntimeCaps } from './lib/bare-os-runtime-caps.js'
import './lib/bare-cron.js'
const _pkg = packageRootDir(import.meta.url)
@@ -228,6 +229,19 @@ async function executeKernel(disk, store, swarm, initSource) {
BARE_OS_CTX_API_VERSION,
0: 'bare-os'
}
const hostEnv = globalThis.process?.env
if (hostEnv && typeof hostEnv === 'object') {
for (const k of [
'BARE_OS_PIPELINE_MAX_STAGES',
'BARE_OS_PIPELINE_MAX_BYTES',
'BARE_OS_PIPELINE_MAX_LINES',
'BARE_OS_BOOT_PROFILE',
'BARE_OS_ONBOOT'
]) {
const v = hostEnv[k]
if (v != null && v !== '') shellEnv[k] = v
}
}
/** @type {{ getMounts: () => Map<string, { drive: import('hyperdrive').default, writable: boolean }> }} */
const vfsMountRef = { getMounts: () => new Map() }
const bootStartedMs = Date.now()
@@ -262,6 +276,8 @@ async function executeKernel(disk, store, swarm, initSource) {
const ctx = {
/** Documented `ctx` contract version; bump in lib/bare-os-ctx-api.js when the surface changes. */
bareOsCtxApiVersion: BARE_OS_CTX_API_VERSION,
/** Frozen snapshot: pipeline limits, pseudo-`/` paths, feature flags (see developer guide). */
bareOsRuntimeCaps: buildBareOsRuntimeCaps(shellEnv),
/** Milliseconds since Unix epoch when this session started VFS construction (for `/proc/uptime`). */
bareOsBootStartedMs: bootStartedMs,
/** True when `BARE_OS_SKIP_REPL=1` — stdin is non-interactive; `readLine` yields EOF immediately after boot. */
@@ -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.1.0'
export const BARE_OS_CTX_API_VERSION = '1.2.0'
@@ -0,0 +1,73 @@
/**
* Stable introspection for custom kernels and scripts (`ctx.bareOsRuntimeCaps`).
* Keep in sync with `vfs.js` pseudo paths and `shell.js` pipeline limits.
*/
import { BARE_OS_CTX_API_VERSION } from './bare-os-ctx-api.js'
import {
DEFAULT_PIPELINE_MAX_CAPTURE_BYTES,
DEFAULT_PIPELINE_MAX_CAPTURE_LINES,
DEFAULT_PIPELINE_MAX_STAGES,
getBareOsPipelineLimits
} from './shell.js'
/**
* Documented pseudo filesystem paths (read-mostly; some accept discard writes).
* @type {readonly string[]}
*/
export const BARE_OS_PSEUDO_FS_PATHS = Object.freeze([
'/dev',
'/dev/null',
'/dev/zero',
'/dev/urandom',
'/proc',
'/proc/bare_os_version',
'/proc/cpuinfo',
'/proc/loadavg',
'/proc/meminfo',
'/proc/self',
'/proc/self/cmdline',
'/proc/self/environ',
'/proc/self/exe',
'/proc/uptime',
'/proc/version',
'/run',
'/run/bare-os',
'/run/bare-os/units',
'/sys',
'/sys/fs',
'/sys/fs/bare_os',
'/sys/fs/bare_os/version'
])
/**
* @param {Record<string, string>} shellEnv Session environment (same object as `ctx.env` / `vfs.env`).
*/
export function buildBareOsRuntimeCaps(shellEnv) {
const pl = getBareOsPipelineLimits(shellEnv)
return Object.freeze({
ctxApiVersion: BARE_OS_CTX_API_VERSION,
pipeline: Object.freeze({
maxStages: pl.maxStages,
maxBytes: pl.maxBytes,
maxLines: pl.maxLines,
defaults: Object.freeze({
maxStages: DEFAULT_PIPELINE_MAX_STAGES,
maxBytes: DEFAULT_PIPELINE_MAX_CAPTURE_BYTES,
maxLines: DEFAULT_PIPELINE_MAX_CAPTURE_LINES
}),
envKeys: Object.freeze([
'BARE_OS_PIPELINE_MAX_STAGES',
'BARE_OS_PIPELINE_MAX_BYTES',
'BARE_OS_PIPELINE_MAX_LINES'
])
}),
pseudoFsPaths: BARE_OS_PSEUDO_FS_PATHS,
features: Object.freeze({
simulatedPipelines: true,
bareInitd: true,
vfsTwoDrive: true,
identityVault: true
})
})
}
+3 -2
View File
@@ -53,9 +53,10 @@ 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
*/
function pipelineLimitsFromEnv(env) {
export function getBareOsPipelineLimits(env) {
const o = env && typeof env === 'object' ? env : {}
const parse = (key, def) => {
const v = o[key]
@@ -650,7 +651,7 @@ function segmentHasCommand(seg) {
async function execParsedPipeline(ctx, pipeline) {
const vfs = ctx.vfs
const env = vfs.env
const lim = pipelineLimitsFromEnv(env)
const lim = getBareOsPipelineLimits(env)
if (pipeline.length > lim.maxStages) {
ctx.console.error(
`shell: pipeline exceeds BARE_OS_PIPELINE_MAX_STAGES (${lim.maxStages})`
+29 -1
View File
@@ -23,9 +23,12 @@ import {
splitTokensBySemicolon,
splitTokensByAndOr,
BARE_OS_EXIT_STATUS_ENV,
syncBareOsExitStatusEnv
syncBareOsExitStatusEnv,
getBareOsPipelineLimits,
DEFAULT_PIPELINE_MAX_STAGES
} from './lib/shell.js'
import { BARE_OS_CTX_API_VERSION } from './lib/bare-os-ctx-api.js'
import { buildBareOsRuntimeCaps } from './lib/bare-os-runtime-caps.js'
import {
fuzzyMatch,
stripAnsi,
@@ -753,6 +756,31 @@ test('BARE_OS_CTX_API_VERSION is semver-shaped', async (t) => {
t.ok(/^\d+\.\d+\.\d+$/.test(BARE_OS_CTX_API_VERSION))
})
test('getBareOsPipelineLimits reads BARE_OS_PIPELINE_* from env', async (t) => {
const def = getBareOsPipelineLimits({})
t.is(def.maxStages, DEFAULT_PIPELINE_MAX_STAGES)
const custom = getBareOsPipelineLimits({
BARE_OS_PIPELINE_MAX_STAGES: '4',
BARE_OS_PIPELINE_MAX_BYTES: '100',
BARE_OS_PIPELINE_MAX_LINES: '20'
})
t.is(custom.maxStages, 4)
t.is(custom.maxBytes, 100)
t.is(custom.maxLines, 20)
})
test('buildBareOsRuntimeCaps matches ctx API version and pipeline env', async (t) => {
const caps = buildBareOsRuntimeCaps({
BARE_OS_PIPELINE_MAX_STAGES: '8',
BARE_OS_CTX_API_VERSION
})
t.is(caps.ctxApiVersion, BARE_OS_CTX_API_VERSION)
t.is(caps.pipeline.maxStages, 8)
t.ok(Array.isArray(caps.pseudoFsPaths))
t.ok(caps.pseudoFsPaths.includes('/proc/version'))
t.is(caps.features.simulatedPipelines, true)
})
test('expandArgvAliases expands first word and keeps trailing argv', async (t) => {
t.alike(expandArgvAliases(['ll', 'z'], defaultShellAliases()), [
'ls',
+38
View File
@@ -0,0 +1,38 @@
# kernel — system image sources
Files in this directory are **read from disk by the seeder** (or copied into `packages/bare-os-seeder/kernel/` for Pear) and written into the **system Hyperdrive** with **no temporary directory** on the host.
## Staging map (seeder)
| Source | Drive path |
| ----------------- | --------------------- |
| `init.js` | `/boot/init.js` |
| `bin/<name>` | `/bin/<name>` |
| `etc/...` | `/etc/...` |
| `share/man/...` | `/share/man/...` |
| Any other file | `/<relative path>` |
## Contents
- **`init.js`** — Kernel entry: must define `async function start(ctx)`. Boot order: **`/etc/os-release`** → **`/etc/motd`** → optional **`/etc/bare-os/rc.profile.<profile>`** (profile from **`BARE_OS_BOOT_PROFILE`** or first line of **`/etc/bare-os/profile`**) → **`/etc/bare-os/rc`** → **`/etc/bare-os/rc.d/*`** (sorted; only names starting with a digit, plus skips dotfiles, `*~`, `README*`, `*.md`) → banner → when **`BARE_OS_SKIP_REPL`**, optional one line from **`BARE_OS_ONBOOT`** or **`/etc/bare-os/onboot`** → **`readLine` / `execLine`** loop (boot snippet errors are logged, not fatal). Custom kernels may call **`ctx.registerKernelShutdownHook(fn)`** before initd disposers; use **`ctx.bareOsRuntimeCaps`** for limits and pseudo path lists ([`developer-guide/02-the-context-object.md`](../developer-guide/02-the-context-object.md)).
- **`bin/`** — **Tier-1 utilities** built by [bare-os-coreutils](../packages/bare-os-coreutils/README.md). Each file is **`runtime.js`** + optional **`lib/*-engine.js`** (**`sed`**, **`awk`**) or **`lib/man-render.js`** (**`man`**) + **`async function run(ctx, argv)`** (no ESM **`import`** in **`src/`**).
- **`share/man/man.json`** — Merged manual database for **`/bin/man`** (built by **`bare-os-coreutils`**; see [handbook ch.10](../handbook/10-manpages-and-online-help.md)).
- **`etc/os-release`** — Static OS metadata (`NAME`, `VERSION`, …).
- **`etc/motd`** — Optional message printed after **`os-release`** (distributors can customize).
- **`etc/bare-os/banner`** or **`/etc/issue`** — If present on the system drive, the default kernel prints one of these instead of the built-in session hint (unless **`BARE_OS_SKIP_REPL`** shortens the banner). Set **`BARE_OS_BOOT_TRACE=1`** in the session environment to log boot phase timings on stderr.
- **`etc/bare-os/rc`** — Optional boot snippet: one **`execLine`** per non-comment line (trusted).
- **`etc/bare-os/rc.d/`** — Optional extra snippets (basename must start with a digit), same line rules, run after **`rc`** in filename order. Human-oriented notes live in **`.README`** (a dotfile so legacy **`init.js`** never executes it).
## Editing workflow
1. Change sources under `kernel/` or `packages/bare-os-coreutils/src/`.
2. Run `npm run build -w bare-os-coreutils` to refresh `kernel/bin/*`.
3. Run seeder again to re-stage the drive (or use a fresh Corestore for a clean image).
Pear bundles use the **vendored** tree under `packages/bare-os-seeder/kernel/`; keep it in sync by running the same build before `pear stage`. **`npm test`** runs **`scripts/verify-kernel-seeder-parity.mjs`** (after **`bare-os-coreutils`** build) so the two trees match byte-for-byte.
## See also
- [Handbook — Kernel and userspace](../handbook/06-kernel-and-binaries.md)
- [Handbook — POSIX utilities, shell, VFS](../handbook/09-posix-utilities-shell-and-vfs.md)
- [DOCUMENTATION.md](../DOCUMENTATION.md) §9
+87 -2
View File
@@ -2,8 +2,16 @@
* Hyperdrive-resident kernel (staged as /boot/init.js).
* Loaded by the booter with an injected ctx object (trusted replication source).
*
* Boot order: /etc/os-release → /etc/motd → /etc/bare-os/rc → /etc/bare-os/rc.d/*
* (sorted; digit-prefixed snippet names) → session banner → interactive loop.
* Boot order: /etc/os-release → /etc/motd → optional profile rc → /etc/bare-os/rc
* /etc/bare-os/rc.d/* (sorted; digit-prefixed snippet names) → session banner →
* optional oneshot onboot when BARE_OS_SKIP_REPL → interactive loop.
*
* Profile: first non-empty line of /etc/bare-os/profile, overridden by BARE_OS_BOOT_PROFILE.
* When set, runs /etc/bare-os/rc.profile.<name> if present (trusted execLine, before main rc).
*
* Non-interactive onboot: when ctx.bareOsSkipRepl, runs one line from BARE_OS_ONBOOT env
* or first non-empty, non-# line from /etc/bare-os/onboot (trusted), then readLine yields EOF.
*
* Use ctx.registerKernelShutdownHook(fn) for teardown before initd disposers.
*/
@@ -71,6 +79,80 @@ async function printMotd(ctx) {
}
}
/**
* Boot profile name: BARE_OS_BOOT_PROFILE wins over first line of /etc/bare-os/profile.
* @param {Record<string, unknown>} ctx
* @returns {Promise<string>}
*/
async function resolveBootProfileName(ctx) {
const fromEnv = ctx.env && ctx.env.BARE_OS_BOOT_PROFILE
if (fromEnv != null && String(fromEnv).trim()) return String(fromEnv).trim()
const { drive, b4a } = ctx
try {
const buf = await drive.get('/etc/bare-os/profile')
if (!buf) return ''
const line = b4a.toString(buf).split(/\r?\n/)[0] || ''
return line.trim()
} catch {
return ''
}
}
/**
* Optional trusted snippet /etc/bare-os/rc.profile.<name> (before /etc/bare-os/rc).
* @param {Record<string, unknown>} ctx
* @param {string} profileName
*/
async function runProfileRc(ctx, profileName) {
if (!profileName) return
const safe = profileName.replace(/[^a-zA-Z0-9._-]/g, '')
if (safe !== profileName) {
ctx.console.error(
'[boot] profile name contains unsupported characters; skipping rc.profile'
)
return
}
await runRcFileAt(
ctx,
`/etc/bare-os/rc.profile.${safe}`,
`rc.profile.${safe}`
)
}
/**
* When stdin is non-interactive, run a single trusted boot command (automation).
* @param {Record<string, unknown>} ctx
*/
async function runOnbootOnce(ctx) {
if (!ctx.bareOsSkipRepl) return
const { execLine, console, drive, b4a, env } = ctx
let line = ''
const fromEnv = env && env.BARE_OS_ONBOOT
if (fromEnv != null && String(fromEnv).trim()) {
line = String(fromEnv).trim()
} else {
try {
const buf = await drive.get('/etc/bare-os/onboot')
if (buf) {
for (const raw of b4a.toString(buf).split(/\r?\n/)) {
const t = raw.trim()
if (!t || t.startsWith('#')) continue
line = t
break
}
}
} catch (e) {
console.error('onboot: ' + ((e && e.message) || String(e)))
}
}
if (!line) return
try {
await execLine(line)
} catch (e) {
console.error((e && e.message) || String(e))
}
}
/**
* @param {Record<string, unknown>} ctx
* @param {string} drivePath absolute path on system drive
@@ -161,9 +243,12 @@ async function start(ctx) {
const { readLine, execLine, console } = ctx
await bootTimed(ctx, 'os-release', () => printOsRelease(ctx))
await bootTimed(ctx, 'motd', () => printMotd(ctx))
const profileName = await resolveBootProfileName(ctx)
await bootTimed(ctx, 'rc.profile', () => runProfileRc(ctx, profileName))
await bootTimed(ctx, 'rc', () => runRcFileAt(ctx, '/etc/bare-os/rc', 'rc'))
await bootTimed(ctx, 'rc.d', () => runBareOsRcDir(ctx))
await printSessionBanner(ctx)
await bootTimed(ctx, 'onboot', () => runOnbootOnce(ctx))
while (true) {
const line = await readLine('')
if (line == null) break
File diff suppressed because one or more lines are too long