This commit is contained in:
Raven Scott
2026-04-05 04:33:14 -04:00
parent 4af98ef97b
commit 904ad7e7fd
48 changed files with 1905 additions and 219 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# bare-os-bare-libs
Builds trusted IIFE bundles from [`bare-module-manifest.json`](../bare-os-booter/lib/bare-module-manifest.json) entries marked `"bundle": true`. Output:
Builds trusted IIFE bundles from [`bare-module-manifest.json`](../bare-os-booter/lib/bare-module-manifest.json) entries marked `"bundle": true`. **`npm run build`** runs **`prebuild`**: **`../../scripts/generate-bare-module-manifest-data.mjs`**, which refreshes **[`bare-module-manifest.data.mjs`](../bare-os-booter/lib/bare-module-manifest.data.mjs)** for Pear before esbuild. Output:
- `kernel/lib/bare/bundles/<ctxKey>.js`
- `kernel/lib/bare/manifest.json` (paths for the booter drive merge)
+1
View File
@@ -5,6 +5,7 @@
"type": "module",
"description": "Builds IIFE bundles under kernel/lib/bare/bundles for ctx.bare drive merge",
"scripts": {
"prebuild": "node ../../scripts/generate-bare-module-manifest-data.mjs",
"build": "node build.mjs"
},
"devDependencies": {
+4 -3
View File
@@ -33,7 +33,7 @@ node index.js
- **`bare-os-http-policy.js`** — Optional HTTP allow/deny lists for wrapped `ctx.httpFetch`
- **`bare-os-ctx-api.js` / `bare-os-ctx.d.ts`** — Semantic version of the `ctx` contract + optional TypeScript shapes
- **`bare-os-runtime-caps.js`** — Frozen caps: pipeline limits, `quotas`, pseudo paths, `features` flags
- **`bare-os-ctx-bare.js` / `bare-module-manifest.json`** — **`ctx.bare`** host imports + optional **`/lib/bare/`** drive bundle merge
- **`bare-os-ctx-bare.js` / `bare-module-manifest.json` / `bare-module-manifest.data.mjs`** — **`ctx.bare`** host imports + optional **`/lib/bare/`** drive bundle merge; Pear loads the manifest from generated **`bare-module-manifest.data.mjs`** (see [PEAR-RUN.md](../../PEAR-RUN.md), [Developer guide ch.12](../../developer-guide/12-bare-modules-and-pear-ecosystem.md))
- **`identity-session.js` / `identity-account.js`** — Guest vs unlocked user, `/.bare/account`, vault
- **`hdms-manager.js`** — Extra Hyperdrives, mounts under `/mnt`, Autopass pair/invite
- **`bare-initd.js`** — Service registry, unit drop-ins, **`units.d`** fragments, socket-activation, **`IdleSec=`** idle stop, **`ConditionPathExists=`** / **`AssertPathExists=`**, **ReadinessPath** / **`exec:`** polling, DAG snapshot **`/proc/bare_os/initd_graph.json`**, mobile suspend/resume order, `startBareInitd`, **kernel-logger**
@@ -66,11 +66,12 @@ npm test -w bare-os-booter
```
- `brittle-bare test.identity.js` — crypto account codec (Bare)
- `brittle-node test.js` — Hyperdrive, VFS, shell, kernel runner, cron matchers, etc.
- `brittle-node test.js` — Hyperdrive, VFS, shell, kernel runner, cron matchers, etc.; includes parity of **`bare-module-manifest.json`** vs **`bare-module-manifest.data.mjs`**
- Root **`npm run verify:manifest-data`** — ensures JSON and **`.data.mjs`** stay in sync (runs in **`pretest`**)
## Pear staging
`pear.stage` includes hoisted `../../node_modules`. Run **`scripts/ensure-pear-node-modules.mjs`** from the repo root before `pear run` so the app package sees symlinked deps (see root README).
`pear.stage` includes hoisted `../../node_modules`. Run **`scripts/ensure-pear-node-modules.mjs`** from the repo root before `pear run` so the app package sees symlinked deps (see root README). After changing **`bare-module-manifest.json`** or running **`sync:bare-manifest`**, confirm **`npm run verify:manifest-data`** passes, then **`pear stage` / `pear release`** so Pear ships an up-to-date **`bare-module-manifest.data.mjs`**.
## See also
+4
View File
@@ -6689,6 +6689,10 @@ async function executeKernel(disk, store, swarm, initSource) {
async bareOsVfsBatchWrite(puts) {
await bareOsVfsBatchPut(disk.drive, puts)
if (!vfs || !Array.isArray(puts)) return
/* Drive path `lib/bare/bare-module-manifest.json` updates warm-cache bundle rows.
* Pear hosts load `ctx.bare` from embedded `bare-module-manifest.data.mjs` (booter
* release); changing only the drive copy does not alter the Pear module graph until a
* new booter is staged. */
let hasManifestPut = false
/** @type {Uint8Array | ArrayBuffer | ArrayBufferView | null | undefined} */
let manifestBuf = null
File diff suppressed because it is too large Load Diff
+42 -103
View File
@@ -3,9 +3,10 @@
* plus optional merge from trusted IIFE bundles on the system drive.
*/
import { readFileSync, statSync } from '#host-fs'
import { dirname, join, resolve } from '#host-path'
import { readFileSync } from '#host-fs'
import { dirname, join } from '#host-path'
import { fileURLToPath } from 'url'
import bareModuleManifestEmbedded from './bare-module-manifest.data.mjs'
import { bareOsHostBooterWarn } from './bare-os-host-booter-log.js'
/**
@@ -61,117 +62,54 @@ export const BARE_OS_STDLIB_GLOBAL = '__bare_os_stdlib__'
const MANIFEST_NAME = 'bare-module-manifest.json'
/** @param {string} root */
function libDirIfManifestAtRoot(root) {
if (!root || root === '/' || root === '') return null
const tries = [
join(root, 'lib', MANIFEST_NAME),
join(root, 'packages', 'bare-os-booter', 'lib', MANIFEST_NAME)
]
for (const f of tries) {
try {
if (statSync(f).isFile()) return dirname(f)
} catch {
/* continue */
}
}
return null
}
/**
* `pear://app/lib/foo.js` → try **`join(mount, 'lib')`** for each bundle mount (manifest must exist).
* @param {string} href
* @param {string[]} mounts
* @param {unknown} m
* @returns {typeof bareModuleManifestEmbedded}
*/
function pearUrlLibDir(href, mounts) {
if (!href.startsWith('pear:')) return null
let u
try {
u = new URL(href)
} catch {
return null
}
const segs = u.pathname.split('/').filter(Boolean)
if (segs.length < 2) return null
segs.pop()
const rel = segs.join('/')
if (!rel) return null
for (const m of mounts) {
if (!m || m === '/') continue
const candidate = resolve(join(String(m), rel))
try {
if (statSync(join(candidate, MANIFEST_NAME)).isFile()) return candidate
} catch {
/* continue */
}
}
return null
}
/**
* Directory containing `bare-module-manifest.json`. Resolved lazily so Pear **`cwd`** / mounts are valid.
* Never fall back to **`/lib`** (happens when **`join('/', 'lib')`** after a bogus **`/`** candidate).
*/
function resolveBareCtxLibDir() {
const href = String(import.meta.url)
if (href.startsWith('file:')) {
return dirname(fileURLToPath(href))
}
const rti = globalThis.Pear?.constructor?.RTI?.mount
const swap = globalThis.Pear?.config?.swapDir
const cwdRaw =
typeof globalThis.process?.cwd === 'function'
? globalThis.process.cwd()
: ''
const cwd = cwdRaw ? resolve(String(cwdRaw)) : resolve('.')
let d = cwd
for (let i = 0; i < 48; i++) {
const found = libDirIfManifestAtRoot(d)
if (found) return found
const parent = dirname(d)
if (parent === d) break
d = parent
}
const mounts = []
const seenM = new Set()
for (const c of [rti, swap].filter(Boolean)) {
const p = resolve(String(c))
if (p === '/' || !p || seenM.has(p)) continue
seenM.add(p)
mounts.push(p)
}
const fromPear = pearUrlLibDir(href, mounts)
if (fromPear) return fromPear
const seen = new Set()
for (const c of [cwd, ...mounts].filter(Boolean)) {
const p = resolve(String(c))
if (p === '/' || !p || seen.has(p)) continue
seen.add(p)
const found = libDirIfManifestAtRoot(p)
if (found) return found
}
throw new Error(
`[bare-os-booter] Cannot find ${MANIFEST_NAME}. ` +
`cwd=${JSON.stringify(cwdRaw)} pearMount=${JSON.stringify(rti ?? null)} import.meta.url=${JSON.stringify(href.slice(0, 120))}`
function cloneBareModuleManifest(m) {
return /** @type {typeof bareModuleManifestEmbedded} */ (
JSON.parse(JSON.stringify(m))
)
}
/** @type {{ version: number, entries: Array<{ ctxKey: string, package: string, export?: string, bundle?: boolean, optional?: boolean, tier?: string }> }} */
/** @type {{ version: number, entries: Array<{ ctxKey: string, package: string, export?: string, bundle?: boolean, optional?: boolean, tier?: string }> } | null} */
let _cachedManifest = null
/**
* Embedded copy of **`bare-module-manifest.json`** for Pear (`pear://` **import.meta.url**).
* `bare-fs` coerces **`URL`** arguments through **`bare-url.fileURLToPath`**, which rejects
* non-`file:` schemes, so **`readFileSync(new URL('pear:…'))`** cannot load the JSON from the
* bundle. Static import of **`bare-module-manifest.data.mjs`** (regenerated by
* **`npm run sync:bare-manifest`**) keeps the manifest in the Pear module graph.
*
* On **`file:`** trees, the JSON on disk is preferred so edits apply without regenerating `.data.mjs`.
*/
export function loadBareModuleManifest() {
if (_cachedManifest) return _cachedManifest
const libDir = resolveBareCtxLibDir()
const raw = readFileSync(join(libDir, MANIFEST_NAME), 'utf8')
_cachedManifest = JSON.parse(raw)
const href = String(import.meta.url)
if (href.startsWith('pear:')) {
_cachedManifest = cloneBareModuleManifest(bareModuleManifestEmbedded)
return _cachedManifest
}
if (href.startsWith('file:')) {
try {
const libDir = dirname(fileURLToPath(href))
const raw = readFileSync(join(libDir, MANIFEST_NAME), 'utf8')
_cachedManifest = JSON.parse(raw)
} catch {
_cachedManifest = cloneBareModuleManifest(bareModuleManifestEmbedded)
}
return _cachedManifest
}
_cachedManifest = cloneBareModuleManifest(bareModuleManifestEmbedded)
return _cachedManifest
}
/** Exported for tests: default export object from **`bare-module-manifest.data.mjs`**. */
export function bareOsBareModuleManifestEmbeddedRef() {
return bareModuleManifestEmbedded
}
/**
* @param {Record<string, string>} shellEnv
*/
@@ -351,8 +289,9 @@ export async function maybeMergeBareFromDrive(shellEnv, vfs, target) {
const sym = BARE_OS_STDLIB_GLOBAL
g[sym] = g[sym] && typeof g[sym] === 'object' ? g[sym] : {}
const concRaw = String(shellEnv?.BARE_OS_BARE_STDLIB_RESOLVE_CONCURRENCY ?? '')
.trim()
const concRaw = String(
shellEnv?.BARE_OS_BARE_STDLIB_RESOLVE_CONCURRENCY ?? ''
).trim()
const concParsed = Number.parseInt(concRaw, 10)
const readConc =
Number.isFinite(concParsed) && concParsed >= 1
@@ -1,5 +1,8 @@
/**
* Session identity: guest vs unlocked keypair; env + vfs; vault save.
* User-facing **`ctx.console.log`** lines on register / unlock / logout / vault save are intentional session feedback (not debug).
* @see [Handbook — Identity and vault](../../../handbook/05-identity-vault-and-hdms.md)
* @see [Developer guide — Context object](../../../developer-guide/02-the-context-object.md)
*/
import b4a from 'b4a'
@@ -325,8 +328,8 @@ export async function unlockIdentity(ctx, passphrase) {
const buf = await ctx.personalDrive.get(ACCOUNT_PATH)
if (!buf) {
const e = new Error('No account — use: login --new <passphrase>')
/** @type {Error & { code?: string }} */ (e).code =
'BARE_OS_IDENTITY_NO_ACCOUNT'
/** @type {Error & { code?: string }} */
e.code = 'BARE_OS_IDENTITY_NO_ACCOUNT'
throw e
}
let publicKey
@@ -337,9 +340,9 @@ export async function unlockIdentity(ctx, passphrase) {
const e = new Error(
'Passphrase does not unlock this account (wrong passphrase or unreadable account file).'
)
const ee = /** @type {Error & { code?: string, cause?: unknown }} */ (e)
ee.code = 'BARE_OS_IDENTITY_PASSPHRASE_REJECTED'
ee.cause = err
/** @type {Error & { code?: string, cause?: unknown }} */
e.code = 'BARE_OS_IDENTITY_PASSPHRASE_REJECTED'
e.cause = err
throw e
}
wipeSecret(ctx)
+11 -3
View File
@@ -4,8 +4,14 @@ import { fileURLToPath } from 'url'
import os from 'bare-os'
function cwd() {
if (typeof globalThis.process?.cwd === 'function')
return globalThis.process.cwd()
if (typeof globalThis.process?.cwd === 'function') {
try {
const p = globalThis.process.cwd()
if (p) return p
} catch {
/* Pear/Bare sometimes exposes cwd() but returns "" */
}
}
return os.cwd()
}
@@ -36,9 +42,11 @@ export function packageRootDir(metaUrl) {
}
const rti = globalThis.Pear?.constructor?.RTI?.mount
const swap = globalThis.Pear?.config?.swapDir
const pearAppDir = globalThis.Pear?.app?.dir ?? globalThis.Pear?.config?.dir
const rtiDir = globalThis.Pear?.constructor?.RTI?.dir
const seen = new Set()
const candidates = []
for (const c of [rti, swap, cwd()].filter(Boolean)) {
for (const c of [rti, swap, pearAppDir, rtiDir, cwd()].filter(Boolean)) {
const p = path.resolve(String(c))
if (!seen.has(p)) {
seen.add(p)
+1 -1
View File
@@ -45,7 +45,7 @@
"pear": {
"name": "bare-os-booter",
"stage": {
"includes": [
"include": [
"../../node_modules"
],
"ignore": [
+24
View File
@@ -60,6 +60,7 @@ import { BARE_OS_CTX_API_VERSION } from './lib/bare-os-ctx-api.js'
import { buildBareOsRuntimeCaps } from './lib/bare-os-runtime-caps.js'
import {
bareOsBareModulesEnabled,
bareOsBareModuleManifestEmbeddedRef,
buildBareCtxObjectFromHost,
loadBareModuleManifest,
maybeMergeBareFromDrive
@@ -2711,6 +2712,29 @@ test('loadBareModuleManifest has entries', async (t) => {
t.ok(m.entries.some((e) => e.ctxKey === 'b4a'))
})
function stableStringifyManifest(obj) {
if (obj === null || typeof obj !== 'object') return JSON.stringify(obj)
if (Array.isArray(obj)) {
return '[' + obj.map((x) => stableStringifyManifest(x)).join(',') + ']'
}
const keys = Object.keys(obj).sort()
return (
'{' +
keys
.map((k) => JSON.stringify(k) + ':' + stableStringifyManifest(obj[k]))
.join(',') +
'}'
)
}
test('bare-module-manifest.data.mjs matches bare-module-manifest.json', async (t) => {
const dir = path.dirname(fileURLToPath(import.meta.url))
const jsonPath = path.join(dir, 'lib', 'bare-module-manifest.json')
const fromJson = JSON.parse(await readFile(jsonPath, 'utf8'))
const embedded = bareOsBareModuleManifestEmbeddedRef()
t.is(stableStringifyManifest(fromJson), stableStringifyManifest(embedded))
})
test('buildBareCtxObjectFromHost loads core keys on Node', async (t) => {
const target = {}
await buildBareCtxObjectFromHost({}, target)
+5 -1
View File
@@ -55,7 +55,11 @@ async function run(ctx, argv) {
if (modeOpt != null) mkOpts.mode = modeOpt
await ctx.vfs.mkdir(p, mkOpts)
} catch (e) {
ctx.console.error('mkdir: ' + p + ': ' + ((e && e.message) || e))
const code =
e && typeof e === 'object' && typeof e.code === 'string' ? e.code : ''
const base = (e && e.message) || String(e)
const tail = code ? `${code}: ${base}` : base
ctx.console.error(`mkdir: ${p}: ${tail}`)
ctx.exitCode = 1
}
}
+33 -54
View File
@@ -1,11 +1,11 @@
import Hyperswarm from 'hyperswarm'
import Protomux from 'protomux'
import AppDrive from 'pear-appdrive'
import b4a from 'b4a'
import safetyCatch from 'safety-catch'
import Hyperdrive from 'hyperdrive'
import Corestore from 'corestore'
import { readdir, readFile, stat } from '#host-fs-promises'
import path from '#host-path'
import { stat } from '#host-fs-promises'
import {
topicKey,
buildMbr,
@@ -18,7 +18,14 @@ import {
defaultKernelRoot,
defaultSeedCorestorePath
} from './lib/paths.js'
import { logPearMultisigKernelHint } from './lib/pear-multisig-hint.js'
import {
logPearMultisigKernelHint,
logPearMultisigKernelHintFromPearAppDrive
} from './lib/pear-multisig-hint.js'
import {
stageKernelTreeFromFs,
stageKernelTreeFromPearAppDrive
} from './lib/stage-kernel-tree.js'
import { buildSeederSnapshotHintsJson } from './lib/build-seeder-snapshot-hints-json.js'
import { seedLog } from './lib/host-logger.mjs'
@@ -28,55 +35,6 @@ function corestorePath() {
return defaultSeedCorestorePath(_pkg, import.meta.url)
}
/** @param {import('hyperdrive').default} drive */
async function stageKernelTree(drive, kernelRoot) {
/** @type {string[]} */
const manifestPaths = []
async function walk(rel) {
const abs = path.join(kernelRoot, rel)
const entries = await readdir(abs, { withFileTypes: true })
for (const ent of entries) {
const name = ent.name
const subRel = rel ? path.join(rel, name) : name
const subAbs = path.join(kernelRoot, subRel)
if (ent.isDirectory()) {
await walk(subRel)
} else {
const raw = await readFile(subAbs)
let drivePath
if (subRel === 'init.js') {
drivePath = '/boot/init.js'
} else if (subRel.startsWith('bin' + path.sep)) {
drivePath = '/bin/' + subRel.slice(4).split(path.sep).join('/')
} else if (subRel.startsWith('etc' + path.sep)) {
drivePath = '/etc/' + subRel.slice(4).split(path.sep).join('/')
} else {
drivePath = '/' + subRel.split(path.sep).join('/')
}
if (drivePath === '/README.md') {
seedLog(
'info',
'[skip] /README.md (kernel tree doc only; not installed on image root)'
)
continue
}
await drive.put(drivePath, b4a.from(raw))
manifestPaths.push(drivePath)
seedLog('info', `[+] ${drivePath}`)
}
}
}
const st = await stat(kernelRoot).catch(() => null)
if (!st || !st.isDirectory()) {
throw new Error('kernel directory missing: ' + kernelRoot)
}
seedLog('info', `Staging kernel from ${kernelRoot}`)
await walk('')
manifestPaths.sort()
return manifestPaths
}
async function maybeBuildCoreutilsFromSource() {
if (!import.meta.url.startsWith('file:')) {
seedLog(
@@ -115,6 +73,7 @@ async function main() {
await maybeBuildBareLibsFromSource()
const kernelRoot = defaultKernelRoot(_pkg, import.meta.url)
const metaHref = String(import.meta.url)
const store = new Corestore(corestorePath())
const drive = new Hyperdrive(store)
@@ -123,8 +82,28 @@ async function main() {
driveId: b4a.toString(drive.id, 'hex')
})
const manifestPaths = await stageKernelTree(drive, kernelRoot)
await logPearMultisigKernelHint(kernelRoot)
const kernelOnDisk = await stat(kernelRoot).then(
(s) => s.isDirectory(),
() => false
)
/** @type {string[]} */
let manifestPaths
if (kernelOnDisk) {
manifestPaths = await stageKernelTreeFromFs(drive, kernelRoot)
await logPearMultisigKernelHint(kernelRoot)
} else if (!metaHref.startsWith('file:')) {
const appDrive = new AppDrive()
await appDrive.ready()
try {
manifestPaths = await stageKernelTreeFromPearAppDrive(appDrive, drive)
await logPearMultisigKernelHintFromPearAppDrive(appDrive)
} finally {
await appDrive.close().catch(() => {})
}
} else {
throw new Error('kernel directory missing: ' + kernelRoot)
}
const localRAM = new Map()
const mbr = buildMbr(drive.key)
+5 -1
View File
@@ -144,7 +144,11 @@ async function run(ctx, argv) {
if (modeOpt != null) mkOpts.mode = modeOpt
await ctx.vfs.mkdir(p, mkOpts)
} catch (e) {
ctx.console.error('mkdir: ' + p + ': ' + ((e && e.message) || e))
const code =
e && typeof e === 'object' && typeof e.code === 'string' ? e.code : ''
const base = (e && e.message) || String(e)
const tail = code ? `${code}: ${base}` : base
ctx.console.error(`mkdir: ${p}: ${tail}`)
ctx.exitCode = 1
}
}
@@ -19,7 +19,7 @@ Self-contained **`ctx.bare` support** on the **system** Hyperdrive: Holepunch **
## What gets staged
- **`bare-module-manifest.json`** — Copy of the booter manifest (same keys and packages as host resolution). Tells the runtime which logical module names exist.
- **`bare-module-manifest.json`** — Copy of the booter manifest (same keys and packages as host resolution). Tells the runtime which logical module names exist on the **drive**. The Pear **booter** package also ships a generated sibling **`bare-module-manifest.data.mjs`** (repo: **`packages/bare-os-booter/lib/`**) used when **`import.meta.url`** is **`pear://…`** so **`ctx.bare`** host imports do not rely on **`readFile`** of **`pear:`** URLs; see [PEAR-RUN.md](../../../PEAR-RUN.md).
- **`manifest.json`** — Drive loader index: **`bundles`** lists IIFE paths that assign into **`globalThis.__bare_os_stdlib__`**; **`bundleStats`** counts attempted bundles; **`bundleDiagnostics`** lists each bundles byte size (same data as **`docs/audit/bundle-health.json`**).
- **`bundles/*.js`** — One esbuild IIFE per catalog entry. The bare-libs build is **fail-fast** (esbuild errors abort; no stub placeholders). Stale **`*.js`** left from older tiered builds are **pruned** on each successful build. Regenerate with **`npm run build -w bare-os-bare-libs`** (updates **`docs/audit/bundle-health.json`**).
@@ -52,8 +52,8 @@ The authoritative list is in the [environment appendix](../../../docs/reference/
## Regenerating bundles
1. Edit **`packages/bare-os-booter/lib/bare-module-manifest.json`** or bundle sources under **`packages/bare-os-bare-libs/`** as needed.
2. Run **`npm run build -w bare-os-bare-libs`** — output lands in **`kernel/lib/bare/`** (and CI expects **`packages/bare-os-seeder/kernel/`** to match **`kernel/`** byte-for-byte afterward).
1. Edit **`packages/bare-os-booter/lib/bare-module-manifest.json`** or bundle sources under **`packages/bare-os-bare-libs/`** as needed. After catalog merges, **`npm run sync:bare-manifest`** refreshes **`optionalDependencies`** and regenerates **`bare-module-manifest.data.mjs`** for Pear.
2. Run **`npm run build -w bare-os-bare-libs`** — **`prebuild`** runs **`generate-bare-module-manifest-data.mjs`**; output lands in **`kernel/lib/bare/`** (and CI expects **`packages/bare-os-seeder/kernel/`** to match **`kernel/`** byte-for-byte afterward).
3. Mirror **`kernel/`** into **`packages/bare-os-seeder/kernel/`** (same tree) so **`scripts/verify-kernel-seeder-parity.mjs`** passes — typically `rsync -a --delete kernel/ packages/bare-os-seeder/kernel/` from the repo root after init/bundle changes.
4. Re-run the seeder so peers replicate the updated system drive.
@@ -1,6 +1,6 @@
{
"schema": 1,
"atMs": 1775373309737,
"atMs": 1775376099873,
"commands": [
"arch",
"awk",
File diff suppressed because one or more lines are too long
@@ -0,0 +1,34 @@
import path from '#host-path'
/**
* Map a path relative to the host `kernel/` tree to the POSIX path on the system image.
* @param {string} subRel native-relative path (e.g. `bin/cat`, `init.js`)
* @returns {string} image path starting with `/`
*/
export function kernelHostRelToImagePosix(subRel) {
if (subRel === 'init.js') return '/boot/init.js'
if (subRel.startsWith('bin' + path.sep)) {
return '/bin/' + subRel.slice(4).split(path.sep).join('/')
}
if (subRel.startsWith('etc' + path.sep)) {
return '/etc/' + subRel.slice(4).split(path.sep).join('/')
}
return '/' + subRel.split(path.sep).join('/')
}
export function skipKernelImagePath(drivePath) {
return drivePath === '/README.md'
}
/**
* `/kernel/bin/cat` → host-relative `bin/cat` (native separators).
* @param {string} fullKey hyperdrive key
* @returns {string | null}
*/
export function bundleKernelKeyToHostRel(fullKey) {
const k = String(fullKey).replace(/^\/+/, '')
if (!k.startsWith('kernel/')) return null
const rest = k.slice('kernel/'.length)
if (!rest) return null
return rest.split('/').join(path.sep)
}
+11 -3
View File
@@ -4,8 +4,14 @@ import { fileURLToPath } from 'url'
import os from 'bare-os'
function cwd() {
if (typeof globalThis.process?.cwd === 'function')
return globalThis.process.cwd()
if (typeof globalThis.process?.cwd === 'function') {
try {
const p = globalThis.process.cwd()
if (p) return p
} catch {
/* Pear/Bare sometimes exposes cwd() but returns "" */
}
}
return os.cwd()
}
@@ -40,9 +46,11 @@ export function packageRootDir(metaUrl) {
}
const rti = globalThis.Pear?.constructor?.RTI?.mount
const swap = globalThis.Pear?.config?.swapDir
const pearAppDir = globalThis.Pear?.app?.dir ?? globalThis.Pear?.config?.dir
const rtiDir = globalThis.Pear?.constructor?.RTI?.dir
const seen = new Set()
const candidates = []
for (const c of [rti, swap, cwd()].filter(Boolean)) {
for (const c of [rti, swap, pearAppDir, rtiDir, cwd()].filter(Boolean)) {
const p = path.resolve(String(c))
if (!seen.has(p)) {
seen.add(p)
@@ -101,3 +101,38 @@ export async function logPearMultisigKernelHint(kernelRoot) {
console.warn('[seeder] pear.multisig.json:', e?.message || e)
}
}
/**
* Same shape checks as {@link logPearMultisigKernelHint}, reading **`/kernel/pear.multisig.json`**
* via Pear app drive IPC (**`pear-appdrive`**). Subprocess **`hyper-multisig verify`** is skipped.
* @param {{ get: (key: string) => Promise<Uint8Array | Buffer | null> }} appDrive
*/
export async function logPearMultisigKernelHintFromPearAppDrive(appDrive) {
const envOut = globalThis.process?.env
let raw
try {
const buf = await appDrive.get('/kernel/pear.multisig.json')
if (!buf) return
raw = b4a.toString(buf, 'utf8')
} catch {
return
}
try {
const j = JSON.parse(raw)
if (!pearMultisigShapeOk(j)) {
console.warn(
'[seeder] pear.multisig.json: expected { signers: string[], quorum: number } with 1 ≤ quorum ≤ signers.length'
)
if (envOut) envOut.BARE_OS_SEEDER_MULTISIG_VERIFY_RESULT = 'skipped'
return
}
const signers = j.signers
const quorum = j.quorum
console.log(
`[seeder] pear.multisig.json OK (${signers.length} signers, quorum ${quorum}) [from Pear bundle]`
)
if (envOut) envOut.BARE_OS_SEEDER_MULTISIG_VERIFY_RESULT = 'skipped'
} catch (e) {
console.warn('[seeder] pear.multisig.json (bundle):', e?.message || e)
}
}
@@ -0,0 +1,99 @@
import path from '#host-path'
import { readdir, readFile, stat } from '#host-fs-promises'
import b4a from 'b4a'
import {
bundleKernelKeyToHostRel,
kernelHostRelToImagePosix,
skipKernelImagePath
} from './kernel-layout.js'
import { seedLog } from './host-logger.mjs'
/**
* @param {import('hyperdrive').default} drive system image (writable)
* @param {string} kernelRoot host directory
*/
export async function stageKernelTreeFromFs(drive, kernelRoot) {
const st = await stat(kernelRoot).catch(() => null)
if (!st || !st.isDirectory()) {
throw new Error('kernel directory missing: ' + kernelRoot)
}
seedLog('info', `Staging kernel from ${kernelRoot}`)
/** @type {string[]} */
const manifestPaths = []
async function walk(rel) {
const abs = path.join(kernelRoot, rel)
const entries = await readdir(abs, { withFileTypes: true })
for (const ent of entries) {
const name = ent.name
const subRel = rel ? path.join(rel, name) : name
const subAbs = path.join(kernelRoot, subRel)
if (ent.isDirectory()) {
await walk(subRel)
} else {
const raw = await readFile(subAbs)
const drivePath = kernelHostRelToImagePosix(subRel)
if (skipKernelImagePath(drivePath)) {
seedLog(
'info',
'[skip] /README.md (kernel tree doc only; not installed on image root)'
)
continue
}
await drive.put(drivePath, b4a.from(raw))
manifestPaths.push(drivePath)
seedLog('info', `[+] ${drivePath}`)
}
}
}
await walk('')
manifestPaths.sort()
return manifestPaths
}
/**
* Copy `/kernel/**` from the Pear app bundle onto the system image drive.
* Uses **`pear-appdrive`** (sidecar IPC) — do not open the platform Corestore from the worker
* (RocksDB lock conflict with the sidecar).
*
* @param {{ list: (key: string, opts?: object) => AsyncIterable<{ key?: string }>, get: (key: string) => Promise<Uint8Array | Buffer | null> }} appDrive
* @param {import('hyperdrive').default} imageDrive system image (write)
*/
export async function stageKernelTreeFromPearAppDrive(appDrive, imageDrive) {
const probe = await appDrive.get('/kernel/init.js')
if (probe == null) {
throw new Error(
'Pear app drive has no /kernel/init.js (bundle missing kernel; re-stage with pear.stage.include listing kernel)'
)
}
seedLog('info', 'Staging kernel from Pear app drive via IPC (/kernel)')
/** @type {string[]} */
const manifestPaths = []
for await (const { key } of appDrive.list('/kernel')) {
if (!key) continue
const subRel = bundleKernelKeyToHostRel(key)
if (!subRel) continue
const raw = await appDrive.get(key)
if (raw == null) continue
const drivePath = kernelHostRelToImagePosix(subRel)
if (skipKernelImagePath(drivePath)) {
seedLog(
'info',
'[skip] /README.md (kernel tree doc only; not installed on image root)'
)
continue
}
await imageDrive.put(drivePath, b4a.from(raw))
manifestPaths.push(drivePath)
seedLog('info', `[+] ${drivePath}`)
}
manifestPaths.sort()
return manifestPaths
}
+4 -2
View File
@@ -21,6 +21,7 @@
"corestore": "^7.2.1",
"hyperdrive": "^13.3.2",
"hyperswarm": "^4.17.0",
"pear-appdrive": "^1.1.2",
"protomux": "^3.10.1",
"safety-catch": "^1.0.2"
},
@@ -30,8 +31,9 @@
"pear": {
"name": "bare-os-seeder",
"stage": {
"includes": [
"../../node_modules"
"include": [
"../../node_modules",
"kernel"
],
"ignore": [
".git",