Files
bare-operating-system/packages/bare-os-booter/lib/kernel-extension-resolver.js
T
Raven Scott 8f2e3cceb0 Move editable kernel bulk from kernel/init-main.js to kernel/lib/init/
(staged as /lib/init/init-main.js); point bundle-kernel-init and verify
scripts at the new path.

Wire curl, wget, openssl, ssh-keygen, and tar through coreutils and
booter host delegates with booter-side CLI helpers; refresh related
bins, bare manifest, shell completion, and man DB (kernel + seeder).

Add booter support modules for ACL evaluation, audit chain, secret
handles, peer admission, replication priority, process table, swarm
lifecycle, boot-graph proc, metrics, monotonic time, protomux alias
registry, and swarm peer policy; extend extension resolver, VFS,
swarm connection managers, IPC, identity-account, and initd.

Harden bare-os-bare-libs build on esbuild failure; add verify scripts
for extension manifest schema and runtime incomplete markers; extend
ctx API typings, gen-ctx-client-stub, and verify-ctx-dts.

Update boot hook fragment, bundled init.js, handbook and reference
docs (incl. kernel security and VFS path classes).
2026-04-04 17:51:47 -04:00

257 lines
8.0 KiB
JavaScript

/**
* Kernel extension registry resolver: duplicate detection, capability conflicts,
* dependency ordering (topological sort), optional signer/hash policy, and cycle reporting.
*
* Enforcement at boot remains in `kernel/init.js` (`kernel.ext.d`); this module provides
* shared deterministic resolution for booter tooling, tests, and strict preflight.
*/
/**
* @param {{ extId: string, source: string }[]} entries
* @returns {{ ok: true, ordered: typeof entries } | { ok: false, duplicateIds: string[] }}
*/
export function resolveKernelExtensionIds(entries) {
const seen = new Map()
/** @type {string[]} */
const dups = []
for (const e of entries) {
const id = String(e.extId || '').trim()
if (!id) continue
if (seen.has(id)) dups.push(id)
else seen.set(id, e.source)
}
if (dups.length) return { ok: false, duplicateIds: [...new Set(dups)] }
return { ok: true, ordered: [...entries] }
}
/**
* @param {{ extId: string, capabilities?: string[] }[]} entries
* @returns {{ ok: boolean, conflicts: string[] }}
*/
export function detectKernelExtensionCapabilityConflicts(entries) {
/** @type {Map<string, string>} */
const capToId = new Map()
/** @type {string[]} */
const conflicts = []
for (const e of entries) {
const id = String(e.extId || '').trim()
if (!id) continue
const caps = Array.isArray(e.capabilities)
? e.capabilities.map((c) => String(c).trim()).filter(Boolean)
: []
for (const c of caps) {
const prev = capToId.get(c)
if (prev && prev !== id) conflicts.push(`${c}(${prev} vs ${id})`)
else capToId.set(c, id)
}
}
return { ok: conflicts.length === 0, conflicts }
}
/**
* @param {{ file: string, extId: string, scripts: string[], dependsOn: string[], beforeIds?: string[], signaturePointer?: string, signerPubHex?: string, contentSha256Hex?: string }[]} entries
* @returns {{ ok: true, ordered: typeof entries } | { ok: false, cycleExtIds: string[] }}
*/
export function topologicalOrderKernelExtensions(entries) {
const sorted = [...entries].sort((a, b) => a.file.localeCompare(b.file))
const idToFile = new Map()
for (const e of sorted) {
if (!idToFile.has(e.extId)) idToFile.set(e.extId, e.file)
}
/** @type {Map<string, string[]>} */
const adj = new Map()
/** @type {Map<string, number>} */
const indeg = new Map()
for (const e of sorted) indeg.set(e.file, 0)
for (const e of sorted) {
for (const dep of e.dependsOn) {
const from = idToFile.get(dep)
if (!from || from === e.file) continue
if (!adj.has(from)) adj.set(from, [])
adj.get(from).push(e.file)
indeg.set(e.file, (indeg.get(e.file) || 0) + 1)
}
}
/** @type {string[]} */
const q = sorted
.map((e) => e.file)
.filter((f) => (indeg.get(f) || 0) === 0)
.sort()
/** @type {string[]} */
const out = []
while (q.length) {
const f = q.shift()
if (!f) break
out.push(f)
for (const to of adj.get(f) || []) {
indeg.set(to, (indeg.get(to) || 0) - 1)
if (indeg.get(to) === 0) {
q.push(to)
q.sort()
}
}
}
if (out.length !== sorted.length) {
const stuck = sorted.filter((e) => !out.includes(e.file))
return { ok: false, cycleExtIds: stuck.map((e) => e.extId) }
}
const byFile = new Map(sorted.map((e) => [e.file, e]))
return {
ok: true,
ordered: out.map((f) => byFile.get(f)).filter(Boolean)
}
}
/**
* @param {{ extId: string, dependsOn: string[] }[]} entries
* @returns {number}
*/
export function kernelExtensionDependencyDepth(entries) {
const idToEntry = new Map(entries.map((e) => [e.extId, e]))
const memo = new Map()
/**
* @param {string} id
* @param {Set<string>} stack
*/
function depth(id, stack) {
if (memo.has(id)) return memo.get(id)
if (stack.has(id)) return 99
const e = idToEntry.get(id)
if (!e) return 0
stack.add(id)
let d = 0
for (const dep of e.dependsOn) {
d = Math.max(d, depth(dep, stack) + 1)
}
stack.delete(id)
memo.set(id, d)
return d
}
let max = 0
for (const e of entries) {
max = Math.max(max, depth(e.extId, new Set()))
}
return max
}
/**
* @param {Record<string, string | string[] | undefined>} pins extId → expected ed25519 pubkey hex (64) or list for threshold
* @param {{ extId: string, signerPubHex?: string }[]} entries
* @returns {{ ok: boolean, violations: string[] }}
*/
export function validateExtensionSignerPins(entries, pins) {
if (!pins || typeof pins !== 'object') return { ok: true, violations: [] }
/** @type {string[]} */
const violations = []
for (const e of entries) {
const id = String(e.extId || '').trim()
if (!id) continue
const need = pins[id]
if (need == null) continue
const needList = Array.isArray(need)
? need.map((x) => String(x).trim().toLowerCase())
: [String(need).trim().toLowerCase()]
const got = String(e.signerPubHex || '').trim().toLowerCase()
if (!got || got.length !== 64 || !/^[0-9a-f]+$/.test(got)) {
violations.push(`${id}: missing or invalid signerPubHex`)
continue
}
if (!needList.includes(got)) {
violations.push(`${id}: signer pin mismatch`)
}
}
return { ok: violations.length === 0, violations }
}
/**
* @param {Record<string, string>} hashPins extId → lowercase sha256 hex
* @param {{ extId: string, contentSha256Hex?: string }[]} entries
* @returns {{ ok: boolean, violations: string[] }}
*/
export function validateExtensionContentHashes(entries, hashPins) {
if (!hashPins || typeof hashPins !== 'object') return { ok: true, violations: [] }
/** @type {string[]} */
const violations = []
for (const e of entries) {
const id = String(e.extId || '').trim()
if (!id) continue
const need = hashPins[id]
if (need == null) continue
const want = String(need).trim().toLowerCase()
const got = String(e.contentSha256Hex || '').trim().toLowerCase()
if (!got || got !== want) violations.push(`${id}: content hash pin mismatch`)
}
return { ok: violations.length === 0, violations }
}
/**
* Full resolve pipeline for extension drop-in rows (same shape as kernel `kernel.ext.d` JSON + file meta).
* @param {{ file: string, extId: string, scripts: string[], dependsOn: string[], beforeIds?: string[], signaturePointer?: string, signerPubHex?: string, contentSha256Hex?: string, capabilities?: string[] }[]} entries
* @param {{ signerPins?: Record<string, string | string[]>, hashPins?: Record<string, string>, maxDepth?: number } | undefined} policy
*/
export function resolveKernelExtensionsFull(entries, policy = {}) {
const idRes = resolveKernelExtensionIds(
entries.map((e) => ({ extId: e.extId, source: e.file }))
)
if (!idRes.ok) {
return {
ok: false,
reason: 'duplicate_ids',
duplicateIds: idRes.duplicateIds,
ordered: []
}
}
const capRes = detectKernelExtensionCapabilityConflicts(entries)
if (!capRes.ok) {
return {
ok: false,
reason: 'capability_conflict',
conflicts: capRes.conflicts,
ordered: []
}
}
const maxDepth = policy.maxDepth
if (typeof maxDepth === 'number' && maxDepth >= 1) {
const d = kernelExtensionDependencyDepth(
entries.map((e) => ({ extId: e.extId, dependsOn: e.dependsOn || [] }))
)
if (d > maxDepth) {
return {
ok: false,
reason: 'max_depth',
depth: d,
maxDepth,
ordered: []
}
}
}
const pinRes = validateExtensionSignerPins(entries, policy.signerPins || {})
if (!pinRes.ok) {
return {
ok: false,
reason: 'signer_pins',
violations: pinRes.violations,
ordered: []
}
}
const hashRes = validateExtensionContentHashes(entries, policy.hashPins || {})
if (!hashRes.ok) {
return {
ok: false,
reason: 'hash_pins',
violations: hashRes.violations,
ordered: []
}
}
const topo = topologicalOrderKernelExtensions(entries)
if (!topo.ok) {
return {
ok: false,
reason: 'cycle',
cycleExtIds: topo.cycleExtIds,
ordered: []
}
}
return { ok: true, ordered: topo.ordered, reason: null }
}