Expand bounded awk/expr/test toward Issue 7; refresh man, profile 1.0.18,

posix matrix/dashboard, and syscalls/process_table schema alignment (v8).

Booter: replication_operator_sketch/corestore hints, HRPC allowlist tests,
Protomux cap channel 65536-byte bound + export, Wasm posix_profile_peek,
swarm-disk and security_posture docs.

Coreutils/kernel: pkg-swarm-index pathCapabilityEnvelopeVerify on get;
pathcap-verify --trusted failure hint; rebuild bins and sync seeder.

Docs: KERNEL_CONTRACT, kernel-extensions, capabilities index, environment
appendix (warm-cache tuning, cap channel, Wasm env), handbook observability,
vault threat model (multisig), developer-guide ctx/HRPC/Wasm, DOCUMENTATION
release-checklist note, release-checklist optional tier1 drift.

Changelog maintenance in bare-os-booter and bare-os-protocol.
This commit is contained in:
Raven Scott
2026-04-05 23:01:54 -04:00
parent 8f61d0bea8
commit 071edccfb3
73 changed files with 1730 additions and 832 deletions
+2
View File
@@ -14,6 +14,8 @@ Run from repo root:
npm run build -w bare-os-bare-libs
```
**Catalog bump pipeline (maintainers):** refresh Holepunch package metadata with **`npm run gen:bare-catalog`** at the repo root (updates **`docs/bare-holepunch-catalog.json`**), then rebuild this workspace, run **`npm test`** ( **`verify-bundle-health`**, **`verify-bundle-markers`**, **`verify-bundle-throws`**), and sync **`kernel/lib/bare/`** → seeder per **`verify-kernel-seeder-parity`**. Intentional large bundle size shifts should add a row under **Bundle health log** below.
The booter loads these only when `BARE_OS_BARE_MODULES` is enabled and `BARE_OS_BARE_DRIVE_BUNDLES` is not disabled; see the developer guide.
Manifest rows may include optional **`tier`** (**`core`** default when omitted) and **`risk`** (**`low`** / **`medium`** / **`high`**) so distributors can filter bundles without reading upstream READMEs. The catalog sync scripts preserve unknown fields when merging from **`docs/bare-holepunch-catalog.json`**.
+3 -1
View File
@@ -7,7 +7,9 @@ Authoritative **version alignment** with protocol and telemetry schema numbers l
- **`baretop` snapshot**: **`ctx.bareOsReadBareTopSnapshot({ lite: true })`** returns a reduced **`files`** map (see **`BARE_TOP_SNAPSHOT_LITE_ENTRIES`** in **`packages/bare-os-coreutils/lib/baretop-snapshot.js`**). Full batch includes **`securityPosture`** → **`/proc/bare_os/security_posture.json`**. **`metrics_live`** JSON uses **`schema: 4`** with embedded **`processTable`** when present; kernel counters include optional **`vfs.readfile.samples`** ( **`BARE_OS_VFS_READ_METRICS`** ), **`initd.unit_failed_final`**, and **`shell.pipeline_last_stages`** gauge from the shell.
- **`/proc/bare_os/net_summary.json`**: **`schemaVersion` 2** adds scalar **`replicationQueueDepth`**, firewall session totals, optional **`BARE_OS_BSD_SOCKET_BRIDGE_STATS_JSON`** merge into **`bsdSocketGuestBridge`**; **`peer_firewall_stats`** field alignment documented in **`bare-os-protocol`** channel handler.
- **Host filesystem**: first-party booter and seeder sources use **`import … from '#host-fs'`** / **`#host-path`** (and seeder **`#host-fs-promises`**) so Bare resolves **`bare-fs`** / **`bare-path`** and Node resolves the built-in **`fs`** / **`path`** modules via the default **`imports`** condition. **`bare-fs`** is a direct **`dependencies`** entry of **`bare-os-booter`**. CI: **`scripts/verify-pear-no-static-node-import.mjs`** rejects bare **`fs`** / **`path`** specifiers.
- **POSIX / proc**: **`/proc/bare_os/syscalls.json`** schema **11** with **`susv4Refs`** on **`opsDetail`**, **`posixXsh` schema 3**, **`socketMsgSurface` schema 5** (`logicalScmRightsOnSend`); includes **`posix_fadvise`** simulated, **`fdModel`**, **`errnoHints`**; JSON text **cached** until **`vfs.bareOsClearWarmReadCaches`**. **`/proc/bare_os/process_table.json`** schema **7**; **`/proc/bare_os/protomux.json`** wire text schema **2**; **`/dev/shm`** in-memory named segments. **`/proc/bare_os/security_posture.json`** — **`blindRelayPosture`**, **`blindPeeringOperatorSketch`** env gate pointers; **`vaultCryptoPrimitives`**, **`bareCryptoReportedVersion`**, **`peerAdmission`**. **`/proc/bare_os/boot_budget_summary.json`** mirrors kernel **`/run/bare-os/boot-budget-summary.json`**. **`/proc/bare_os_features`**: **`protomuxAppChannel`**, optional **`protomuxCapChannel`**, **`pearIpcConservativeAdvertisement`**. Protocol export **`BARE_OS_POSIX_PROFILE_*`**.
- **POSIX / proc**: **`/proc/bare_os/syscalls.json`** schema **11** with **`susv4Refs`** on **`opsDetail`**, **`posixXsh` schema 3**, **`socketMsgSurface` schema 5** (`logicalScmRightsOnSend`); includes **`posix_fadvise`** simulated, **`fdModel.processTableSchema` 8**, **`errnoHints`**; JSON text **cached** until **`vfs.bareOsClearWarmReadCaches`**. **`/proc/bare_os/process_table.json`** schema **8**; **`/proc/bare_os/protomux.json`** wire text schema **2**; **`/dev/shm`** in-memory named segments. **`/proc/bare_os/security_posture.json`** — **`blindRelayPosture`**, **`blindPeeringOperatorSketch`** env gate pointers; **`vaultCryptoPrimitives`**, **`bareCryptoReportedVersion`**, **`peerAdmission`**. **`/proc/bare_os/boot_budget_summary.json`** mirrors kernel **`/run/bare-os/boot-budget-summary.json`**. **`/proc/bare_os_features`**: **`protomuxAppChannel`**, optional **`protomuxCapChannel`**, **`pearIpcConservativeAdvertisement`**. Protocol export **`BARE_OS_POSIX_PROFILE_*`**.
- **Protomux cap channel**: inbound **`bare-os-cap-v1`** messages are accepted only up to **65536** bytes (**`BARE_OS_PROTOMUX_CAP_CHANNEL_MESSAGE_MAX_BYTES`** in **`lib/swarm-disk.js`**); larger payloads log **`protomux_cap_payload_oversized`** and do not increment **`protomuxCapChannelRxTotal`**.
- **Wasm (sync imports)**: optional **`BARE_OS_WASM_KERNEL_POSIX_PROFILE_PEEK`** adds **`env.bare_os_posix_profile_peek`** (declared **`BARE_OS_POSIX_PROFILE_VERSION`** string, bounded).
- **Blind-relay / pairing / geo proc** (default): **`bareOsProcBlindPeerRelayHintsMinimal`** returns **schema 1** with **`operatorRedacted: true`**. The legacy export **`bareOsProcBlindPeerRelayHintsStub`** was removed; use **`bareOsProcBlindPeerRelayHintsMinimal`** only.
- **Vendored bundles**: post-esbuild **`scripts/sanitize-bare-bundles.mjs`** (from **`bare-os-bare-libs/build.mjs`**) strips CI-forbidden markers and replaces **`node:`** requires in **`bareDev.js`** with **`bare-*`** modules.
- **Boot hooks (naming)**: canonical **`bareOsRegisterBootStepHook`**, **`bareOsInvokeBootStepHooks`**, **`bareOsEmitBareBootStepHint`**; legacy **`*BootPhase*`** methods remain thin wrappers (**no `bareOsCtxApiVersion` bump**). See [docs/reference/naming-alias-matrix.md](../../docs/reference/naming-alias-matrix.md).
+10
View File
@@ -657,6 +657,7 @@ async function executeKernel(disk, store, swarm, initSource) {
'BARE_OS_WASM_KERNEL_MONOTONIC_MS',
'BARE_OS_WASM_KERNEL_HOSTNAME_IMPORT',
'BARE_OS_WASM_KERNEL_CTX_API_PEEK',
'BARE_OS_WASM_KERNEL_POSIX_PROFILE_PEEK',
'BARE_OS_POSIX_FD_SIM',
'BARE_OS_POSIX_FD_SIM_MAX_BYTES',
'BARE_OS_POSIX_SOCKET_FD_BRIDGE',
@@ -1686,6 +1687,15 @@ async function executeKernel(disk, store, swarm, initSource) {
shellEnv.BARE_OS_BLIND_PEER_TOPOLOGY_V3_JSON &&
String(shellEnv.BARE_OS_BLIND_PEER_TOPOLOGY_V3_JSON).trim()
),
upstreamHolepunchPackages: {
schema: 1,
blindPeer: 'blind-peer',
blindPeerRouter: 'blind-peer-router',
blindPeering: 'blind-peering',
blindRelayService: 'blind-relay-service',
note:
'npm package names on the Holepunch org; host operators merge these with BARE_OS_BLIND_* env JSON — proc never echoes secret topology payloads.'
},
note:
'Non-secret env gates only; JSON bodies are not echoed here. Wire blind-peering / blind-peer-router operator configs on the host, then read sketches via disk.os replication_operator_sketch.'
},
@@ -450,6 +450,13 @@ export function createBareOsDiskOsBridge(opts) {
corestoreSnapshotUxHint: bareOsCorestoreSnapshotOperatorHint(
opts.corestore ?? null
),
corestoreSnapshotPackageRef: {
schema: 1,
npmPackage: 'corestore-snapshot',
upstreamOrg: 'holepunchto',
role:
'Host-side optional workflow; pair BARE_OS_CORESTORE_SNAPSHOT_JSON with BARE_OS_CORESTORE_STATS_JSON — corestoreSnapshotUxHint compares non-secret counts for operator rollback UX.'
},
corestoreSnapshotPaused:
String(env?.BARE_OS_CORESTORE_SNAPSHOT_PAUSED || '') === '1' ||
String(env?.BARE_OS_CORESTORE_SNAPSHOT_PAUSED || '') === 'true',
@@ -78,7 +78,7 @@ export function buildBareOsSyscallsProcJson(p) {
stdio: [0, 1, 2],
register: 'ctx.bareOsRegisterLogicalFd',
processTablePath: '/proc/bare_os/process_table.json',
processTableSchema: 7,
processTableSchema: 8,
posixLike: {
schema: 1,
note: 'POSIX open/read/write/close/pipe/dup/fcntl analogs; see docs/architecture/POSIX_DECLARED_PROFILE.md.',
@@ -2,6 +2,7 @@
* Optional WebAssembly compile / instantiate for feature-gated kernel extensions.
* Uses global WebAssembly when present (Bare/V8); no Node built-ins.
*/
import { BARE_OS_POSIX_PROFILE_VERSION } from 'bare-os-protocol/bare-os-posix-profile.js'
import { bareOsMonotonicNowMs } from './bare-os-monotonic-time.js'
const DEFAULT_MAX = 512 * 1024
@@ -83,6 +84,7 @@ function writeCStrToMemory(memory, ptr, outCap, text) {
* With **`BARE_OS_WASM_KERNEL_MONOTONIC_MS=1`**, adds **`env.bare_os_monotonic_ms`**: floored monotonic milliseconds (`performance.now` when available), i32-shaped.
* With **`BARE_OS_WASM_KERNEL_HOSTNAME_IMPORT=1`** (requires syscall imports), adds **`env.bare_os_hostname_peek`**: writes session **`HOSTNAME`** (or **`bare-os`**) NUL-terminated into Wasm memory (**bounded**).
* With **`BARE_OS_WASM_KERNEL_CTX_API_PEEK=1`** (requires syscall imports), adds **`env.bare_os_ctx_api_version_peek`**: writes **`ctx.bareOsCtxApiVersion`** NUL-terminated (**bounded**).
* With **`BARE_OS_WASM_KERNEL_POSIX_PROFILE_PEEK=1`** (requires syscall imports), adds **`env.bare_os_posix_profile_peek`**: writes declared **`BARE_OS_POSIX_PROFILE_VERSION`** NUL-terminated (**bounded**).
*
* @param {Uint8Array} source
* @param {Record<string, unknown>} ctx
@@ -154,6 +156,10 @@ export async function bareOsWasmKernelInstantiate(source, ctx, opts = {}) {
wantSyscall &&
(shellEnv.BARE_OS_WASM_KERNEL_POSIX_BRIDGE_PEEK === '1' ||
shellEnv.BARE_OS_WASM_KERNEL_POSIX_BRIDGE_PEEK === 'true')
const wantPosixProfilePeek =
wantSyscall &&
(shellEnv.BARE_OS_WASM_KERNEL_POSIX_PROFILE_PEEK === '1' ||
shellEnv.BARE_OS_WASM_KERNEL_POSIX_PROFILE_PEEK === 'true')
const pathconfFn = ctx && typeof ctx.bareOsPathconf === 'function'
? ctx.bareOsPathconf.bind(ctx)
: null
@@ -245,6 +251,12 @@ export async function bareOsWasmKernelInstantiate(source, ctx, opts = {}) {
return flags | 0
}
}
if (wantPosixProfilePeek) {
envImports.bare_os_posix_profile_peek = (outPtr, outCap) => {
const v = String(BARE_OS_POSIX_PROFILE_VERSION || '').slice(0, 32)
return writeCStrToMemory(memory, Number(outPtr), Number(outCap), v)
}
}
}
const importObject = {
env: envImports
@@ -274,7 +286,8 @@ export async function bareOsWasmKernelInstantiate(source, ctx, opts = {}) {
wasmMonotonicMsImport: !!wantMonotonicMs,
wasmHostnamePeekImport: !!(wantSyscall && wantHostname),
wasmCtxApiVersionPeekImport: !!(wantSyscall && wantCtxApiPeek),
wasmPosixBridgePeekImport: !!(wantSyscall && wantPosixBridgePeek)
wasmPosixBridgePeekImport: !!(wantSyscall && wantPosixBridgePeek),
wasmPosixProfilePeekImport: !!(wantSyscall && wantPosixProfilePeek)
}
} catch (e) {
return { ok: false, reason: String(e && e.message ? e.message : e) }
+12 -1
View File
@@ -49,6 +49,9 @@ function emitSwarmDiskHostLog(level, message, detail = null) {
/** Default max wait for block 0 (MBR) before failing boot (override **`BARE_OS_MBR_READ_TIMEOUT_MS`**). */
const MBR_READ_TIMEOUT_MS_DEFAULT = 60_000
/** Cap-channel (`bare-os-cap-v1`) message size bound when **`BARE_OS_PROTOMUX_CAP_CHANNEL`** is enabled. */
export const BARE_OS_PROTOMUX_CAP_CHANNEL_MESSAGE_MAX_BYTES = 65536
/**
* Max wait (ms) for block 0 / MBR replication before boot failure.
* Exported for microbench / unit introspection (same rules as booter).
@@ -444,8 +447,16 @@ export class SwarmDisk {
mux.pair({ protocol: PROTOCOL_CAP_CHANNEL_NAME }, (cchan) => {
cchan.addMessage({
encoding: c.buffer,
onmessage: () => {
onmessage: (buf) => {
try {
const n = buf ? buf.byteLength : 0
if (n > BARE_OS_PROTOMUX_CAP_CHANNEL_MESSAGE_MAX_BYTES) {
emitSwarmDiskHostLog('warn', 'protomux_cap_payload_oversized', {
byteLength: n,
maxBytes: BARE_OS_PROTOMUX_CAP_CHANNEL_MESSAGE_MAX_BYTES
})
return
}
this.protomuxCapChannelRxTotal++
} catch {
/* ignore */
@@ -34,3 +34,15 @@ test('bareOsHrpcAllowlistDeniesRoute wildcard edges', (t) => {
t.absent(bareOsHrpcAllowlistDeniesRoute(exact, 'vfs', 'readText'))
t.ok(bareOsHrpcAllowlistDeniesRoute(exact, 'vfs', 'other'))
})
test('bareOsHrpcAllowlistDeniesRoute bare_os service wildcard', (t) => {
const s = new Set(['bare_os.*'])
t.absent(bareOsHrpcAllowlistDeniesRoute(s, 'bare_os', 'pkg_index_get'))
t.ok(bareOsHrpcAllowlistDeniesRoute(s, 'kernel', 'ping'))
})
test('parseBareOsHrpcAllowlistJson empty array is null allow', (t) => {
const r = parseBareOsHrpcAllowlistJson('[]')
t.absent(r.allow)
t.absent(r.parseError)
})
+32
View File
@@ -1586,6 +1586,7 @@ test('disk.os replication_snapshot and replication_operator_sketch RPCs', async
t.is(oj.schema, 7)
t.is(oj.protomuxOperatorSketch?.schema, 2)
t.is(oj.corestoreSnapshotUxHint?.schema, 3)
t.is(oj.corestoreSnapshotPackageRef?.npmPackage, 'corestore-snapshot')
t.is(oj.seedReplicationStatus?.depth, 1)
t.is(oj.swarmPeerCount, null)
const badIntent = await bridge.execRpc(
@@ -7014,6 +7015,37 @@ test('bareOsWasmKernelInstantiate optional bare_os_hostname_peek import flag', a
t.ok(r.wasmHostnamePeekImport)
})
test('bareOsWasmKernelInstantiate optional bare_os_posix_profile_peek import flag', async (t) => {
const { bareOsWasmKernelInstantiate } = await import('./lib/bare-os-wasm-kernel.js')
if (typeof WebAssembly === 'undefined' || !WebAssembly.compile) {
t.pass('skip: no WebAssembly')
return
}
const wasmMinimal = new Uint8Array([
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00
])
const ctx = { bareOsPathconf: () => '4096' }
const r = await bareOsWasmKernelInstantiate(wasmMinimal, ctx, {
wasmSyscallImports: true,
shellEnv: {
BARE_OS_WASM_KERNEL_SYSCALL: '1',
BARE_OS_WASM_KERNEL_POSIX_PROFILE_PEEK: '1'
}
})
if (!r.ok) {
t.pass('instantiate may fail on minimal module')
return
}
t.ok(r.wasmPosixProfilePeekImport)
})
test('Protomux cap channel inbound message size bound is 65536 bytes', async (t) => {
const { BARE_OS_PROTOMUX_CAP_CHANNEL_MESSAGE_MAX_BYTES } = await import(
'./lib/swarm-disk.js'
)
t.is(BARE_OS_PROTOMUX_CAP_CHANNEL_MESSAGE_MAX_BYTES, 65536)
})
test('bareOsAppendVaultRotationCheckpoint appends vault_save row', async (t) => {
const { bareOsAppendVaultRotationCheckpoint } = await import(
'./lib/bare-os-vault-rotation-audit.js'
+171 -95
View File
@@ -2,7 +2,8 @@
* POSIX-oriented awk interpreter for Bare OS (no import; concatenated before src/awk.js).
* Supports: BEGIN/END, /re/, line patterns, {}, print/printf, if/else, while, for(;;), for(x in a),
* delete arr[idx], next, exit, break, continue, ++/--, arrays, strings, regex ~ !~, builtins (length, substr, index,
* split, sprintf, sub, gsub, match, int, tolower, toupper, rand, srand), getline from stdin/files,
* split, sprintf, sub, gsub, match, int, atan2, cos, sin, exp, log, sqrt, tolower, toupper, rand, srand),
* getline from stdin, files (getline x < \"path\"), unary minus,
* optional fixed-width fields via FIELDWIDTHS when BARE_OS_AWK_FIELDWIDTHS is enabled,
* -F FS, NF, NR, FNR, $0..$n, OFS, ORS, RS, ARGC, ARGV, FILENAME, ENVIRON (read-only mirror).
* GNU-style empty fields: consecutive FS delimiters still advance $1..$NF where the grammar allows.
@@ -581,7 +582,11 @@ class BareAwkParser {
}
parseUnary() {
if (this.peek().value === '!' || this.peek().value === '+') {
if (
this.peek().value === '!' ||
this.peek().value === '+' ||
this.peek().value === '-'
) {
const op = String(this.peek().value)
this.p++
return { t: 'unop', op, a: this.parseUnary() }
@@ -730,6 +735,8 @@ class BareAwkRuntime {
this._stdinIx = 0
/** @type {{ path: string, data: string, append: boolean }[]} */
this.pendingWrites = []
/** @type {Map<string, { lines: string[], i: number }>} */
this._getlineFileState = new Map()
}
splitFields() {
@@ -800,14 +807,14 @@ class BareAwkRuntime {
/**
* @param {BareAwkNode | null} pat
*/
patternMatch(pat) {
async patternMatch(pat) {
if (pat == null) return true
if (pat.t === 'pat') return false
if (pat.t === 'patRe') {
pat.rx.lastIndex = 0
return pat.rx.test(this.$0)
}
if (pat.t === 'patExpr') return this.truthy(this.evalExpr(pat.e))
if (pat.t === 'patExpr') return this.truthy(await this.evalExpr(pat.e))
return false
}
@@ -821,7 +828,7 @@ class BareAwkRuntime {
/**
* @param {BareAwkNode} n
*/
evalExpr(n) {
async evalExpr(n) {
if (!n) return ''
switch (n.t) {
case 'num':
@@ -843,9 +850,9 @@ class BareAwkRuntime {
return this.vars[name] != null ? this.vars[name] : ''
}
case 'field':
return this.fieldNum(this.evalExpr(n.e))
return this.fieldNum(await this.evalExpr(n.e))
case 'index': {
const k = String(this.evalExpr(n.idx))
const k = String(await this.evalExpr(n.idx))
if (n.name === 'ENVIRON') {
const v = this.environ[k]
return v != null ? v : ''
@@ -855,8 +862,8 @@ class BareAwkRuntime {
return arr[k] != null ? arr[k] : ''
}
case 'binop': {
const a = this.evalExpr(n.a)
const b = this.evalExpr(n.b)
const a = await this.evalExpr(n.a)
const b = await this.evalExpr(n.b)
const an = Number(a)
const bn = Number(b)
switch (n.op) {
@@ -889,77 +896,78 @@ class BareAwkRuntime {
}
}
case 'unop': {
const a = this.evalExpr(n.a)
const a = await this.evalExpr(n.a)
if (n.op === '!') return this.truthy(a) ? 0 : 1
if (n.op === '+') return Number(a) || 0
if (n.op === '-') return -(Number(a) || 0)
return 0
}
case '||':
return this.truthy(this.evalExpr(n.a))
return this.truthy(await this.evalExpr(n.a))
? 1
: this.truthy(this.evalExpr(n.b))
: this.truthy(await this.evalExpr(n.b))
? 1
: 0
case '&&':
return this.truthy(this.evalExpr(n.a)) &&
this.truthy(this.evalExpr(n.b))
return this.truthy(await this.evalExpr(n.a)) &&
this.truthy(await this.evalExpr(n.b))
? 1
: 0
case 'match': {
const s = String(this.evalExpr(n.a))
const s = String(await this.evalExpr(n.a))
const rhs = n.b
let rx
if (rhs.t === 'rxLit') rx = rhs.rx
else {
const t = String(this.evalExpr(rhs))
const t = String(await this.evalExpr(rhs))
rx = bareAwkMakeRx(t, '')
}
rx.lastIndex = 0
return rx.test(s) ? 1 : 0
}
case 'nmatch': {
const s = String(this.evalExpr(n.a))
const s = String(await this.evalExpr(n.a))
const rhs = n.b
let rx
if (rhs.t === 'rxLit') rx = rhs.rx
else {
const t = String(this.evalExpr(rhs))
const t = String(await this.evalExpr(rhs))
rx = bareAwkMakeRx(t, '')
}
rx.lastIndex = 0
return rx.test(s) ? 0 : 1
}
case '?:':
return this.truthy(this.evalExpr(n.n))
? this.evalExpr(n.a)
: this.evalExpr(n.b)
return this.truthy(await this.evalExpr(n.n))
? await this.evalExpr(n.a)
: await this.evalExpr(n.b)
case 'assign': {
const v = this.evalExpr(n.right)
this.assign(n.left, n.op, v)
const v = await this.evalExpr(n.right)
await this.assign(n.left, n.op, v)
return v
}
case 'pre': {
const cur = Number(this.evalExpr(n.x))
const cur = Number(await this.evalExpr(n.x))
const next = n.op === '++' ? cur + 1 : cur - 1
this.assignScalar(n.x, next)
await this.assignScalar(n.x, next)
return next
}
case 'post': {
const cur = Number(this.evalExpr(n.x))
const cur = Number(await this.evalExpr(n.x))
const next = n.op === '++' ? cur + 1 : cur - 1
this.assignScalar(n.x, next)
await this.assignScalar(n.x, next)
return cur
}
case 'call':
return this.callBuiltin(n.name, n.args)
return await this.callBuiltin(n.name, n.args)
case 'getline':
return this.doGetline(n)
return await this.doGetline(n)
default:
return ''
}
}
assignScalar(x, v) {
async assignScalar(x, v) {
if (x.t === 'var') {
const name = x.name
if (name === 'FS') this.FS = String(v)
@@ -968,7 +976,7 @@ class BareAwkRuntime {
else if (name === 'RS') this.RS = String(v)
else this.vars[name] = v
} else if (x.t === 'field') {
this.setField(this.evalExpr(x.e), v)
this.setField(await this.evalExpr(x.e), v)
}
}
@@ -977,10 +985,10 @@ class BareAwkRuntime {
* @param {string} op
* @param {unknown} v
*/
assign(left, op, v) {
async assign(left, op, v) {
let base = v
if (op !== '=') {
const cur = Number(this.evalExpr(left))
const cur = Number(await this.evalExpr(left))
const nv = Number(v)
if (op === '+=') base = cur + nv
else if (op === '-=') base = cur - nv
@@ -991,32 +999,52 @@ class BareAwkRuntime {
if (left.t === 'index') {
const arr =
this.arrays[left.name] || (this.arrays[left.name] = Object.create(null))
arr[String(this.evalExpr(left.idx))] = base
arr[String(await this.evalExpr(left.idx))] = base
return
}
this.assignScalar(left, base)
await this.assignScalar(left, base)
}
/**
* @param {string} path
*/
async _ensureGetlineFile(path) {
let st = this._getlineFileState.get(path)
if (st) return st
const buf = await this.io.readFile(path)
const text =
buf && this.io.bytesToString ? this.io.bytesToString(buf) : ''
const lines = text.split(/\r?\n/)
if (lines.length && lines[lines.length - 1] === '') lines.pop()
st = { lines, i: 0 }
this._getlineFileState.set(path, st)
return st
}
/**
* @param {string} name
* @param {BareAwkNode[]} args
*/
callBuiltin(name, args) {
const a = (i) => (args[i] ? this.evalExpr(args[i]) : '')
async callBuiltin(name, args) {
const a = async (i) =>
args[i] != null ? await this.evalExpr(args[i]) : ''
switch (name) {
case 'length':
if (!args.length) return this.$0.length
return String(a(0)).length
case 'substr':
return String(a(0)).slice(
Math.max(0, Math.trunc(Number(a(1))) - 1),
Math.max(0, Math.trunc(Number(a(1))) - 1 + Math.trunc(Number(a(2))))
)
return String(await a(0)).length
case 'substr': {
const s = String(await a(0))
const start = Math.max(0, Math.trunc(Number(await a(1))) - 1)
if (args.length < 3) return s.slice(start)
const ln = Math.trunc(Number(await a(2)))
if (!Number.isFinite(ln) || ln < 0) return ''
return s.slice(start, start + ln)
}
case 'index':
return String(a(0)).indexOf(String(a(1))) + 1 || 0
return String(await a(0)).indexOf(String(await a(1))) + 1 || 0
case 'split': {
const s = String(a(0))
const fs = args[1] ? String(this.evalExpr(args[1])) : this.FS
const s = String(await a(0))
const fs = args[1] ? String(await this.evalExpr(args[1])) : this.FS
let rx = fs.length === 1 ? null : bareAwkMakeRx(fs, '')
const parts = rx ? s.split(rx) : s.split(fs)
const aname = /** @type {BareAwkNode} */ (args[2])
@@ -1027,17 +1055,35 @@ class BareAwkRuntime {
}
return parts.length
}
case 'sprintf':
return bareAwkSprintf(
String(a(0)),
args.slice(1).map((x) => this.evalExpr(x))
)
case 'sprintf': {
const fmt = String(await a(0))
const rest = []
for (let j = 1; j < args.length; j++)
rest.push(await this.evalExpr(args[j]))
return bareAwkSprintf(fmt, rest)
}
case 'int':
return Math.trunc(Number(a(0)))
return Math.trunc(Number(await a(0)))
case 'log': {
const x = Number(await a(0))
return x > 0 ? Math.log(x) : Number.NaN
}
case 'sqrt': {
const x = Number(await a(0))
return x >= 0 ? Math.sqrt(x) : Number.NaN
}
case 'sin':
return Math.sin(Number(await a(0)))
case 'cos':
return Math.cos(Number(await a(0)))
case 'exp':
return Math.exp(Number(await a(0)))
case 'atan2':
return Math.atan2(Number(await a(0)), Number(await a(1)))
case 'tolower':
return String(a(0)).toLowerCase()
return String(await a(0)).toLowerCase()
case 'toupper':
return String(a(0)).toUpperCase()
return String(await a(0)).toUpperCase()
case 'rand': {
this.randSeed = (this.randSeed * 1103515245 + 12345) & 0x7fffffff
return this.randSeed / 0x7fffffff
@@ -1051,9 +1097,11 @@ class BareAwkRuntime {
case 'sub':
case 'gsub': {
const rx =
args[0].t === 'rxLit' ? args[0].rx : bareAwkMakeRx(String(a(0)), '')
const rep = String(a(1))
let target = args[2] ? String(a(2)) : this.$0
args[0].t === 'rxLit'
? args[0].rx
: bareAwkMakeRx(String(await a(0)), '')
const rep = String(await a(1))
let target = args[2] ? String(await a(2)) : this.$0
let n = 0
if (name === 'sub') {
rx.lastIndex = 0
@@ -1078,9 +1126,11 @@ class BareAwkRuntime {
return n
}
case 'match': {
const s = String(a(0))
const s = String(await a(0))
const rx =
args[1].t === 'rxLit' ? args[1].rx : bareAwkMakeRx(String(a(1)), '')
args[1].t === 'rxLit'
? args[1].rx
: bareAwkMakeRx(String(await a(1)), '')
rx.lastIndex = 0
const m = rx.exec(s)
if (!m) {
@@ -1094,7 +1144,7 @@ class BareAwkRuntime {
}
default:
if (this.funcs.has(name)) {
return this.callUser(name, args)
return await this.callUser(name, args)
}
bareAwkError('unknown function ' + name)
}
@@ -1104,19 +1154,19 @@ class BareAwkRuntime {
* @param {string} name
* @param {BareAwkNode[]} args
*/
callUser(name, args) {
async callUser(name, args) {
const f = this.funcs.get(name)
if (!f) return ''
const frame = { ...this.vars }
for (let i = 0; i < f.params.length; i++) {
frame[f.params[i]] = args[i] ? this.evalExpr(args[i]) : ''
frame[f.params[i]] = args[i] ? await this.evalExpr(args[i]) : ''
}
const prev = this.vars
this.vars = frame
let ret = ''
try {
for (const st of f.body) {
const r = this.execStmt(st)
const r = await this.execStmt(st)
if (r && r.t === 'return') {
ret = r.v != null ? r.v : ''
break
@@ -1131,12 +1181,27 @@ class BareAwkRuntime {
/**
* @param {BareAwkNode} n
*/
doGetline(n) {
async doGetline(n) {
let line = null
if (n.from) {
const path = String(this.evalExpr(n.from))
/* sync read — awk.js should preload or use async wrapper; runtime uses promise in run loop */
return 0
const path = String(await this.evalExpr(n.from))
const st = await this._ensureGetlineFile(path)
if (st.i < st.lines.length) line = st.lines[st.i++]
else line = null
if (line == null) {
if (n.var) this.vars[n.var] = ''
else {
this.$0 = ''
this.splitFields()
}
return 0
}
if (n.var) this.vars[n.var] = line
else {
this.$0 = line
this.splitFields()
}
return 1
}
if (this._stdinIx < this.io.stdinLines.length) {
line = this.io.stdinLines[this._stdinIx++]
@@ -1156,23 +1221,24 @@ class BareAwkRuntime {
/**
* @param {BareAwkNode} st
* @returns {{ t: string, v?: unknown } | void}
* @returns {Promise<{ t: string, v?: unknown } | void>}
*/
execStmt(st) {
async execStmt(st) {
switch (st.t) {
case 'block':
for (const s of st.stmts) {
const r = this.execStmt(s)
const r = await this.execStmt(s)
if (r) return r
}
break
case 'if':
if (this.truthy(this.evalExpr(st.cond))) return this.execStmt(st.then)
if (st.else) return this.execStmt(st.else)
if (this.truthy(await this.evalExpr(st.cond)))
return await this.execStmt(st.then)
if (st.else) return await this.execStmt(st.else)
break
case 'while':
while (this.truthy(this.evalExpr(st.cond))) {
const r = this.execStmt(st.body)
while (this.truthy(await this.evalExpr(st.cond))) {
const r = await this.execStmt(st.body)
if (r?.t === 'break') break
if (r?.t === 'continue') continue
if (r?.t === 'next' || r?.t === 'nextfile' || r?.t === 'exit')
@@ -1180,25 +1246,25 @@ class BareAwkRuntime {
}
break
case 'for':
if (st.init) this.evalExpr(st.init)
if (st.init) await this.evalExpr(st.init)
while (true) {
if (st.cond && !this.truthy(this.evalExpr(st.cond))) break
const r = this.execStmt(st.body)
if (st.cond && !this.truthy(await this.evalExpr(st.cond))) break
const r = await this.execStmt(st.body)
if (r?.t === 'break') break
if (r?.t === 'continue') {
if (st.step) this.evalExpr(st.step)
if (st.step) await this.evalExpr(st.step)
continue
}
if (r?.t === 'next' || r?.t === 'nextfile' || r?.t === 'exit')
return r
if (st.step) this.evalExpr(st.step)
if (st.step) await this.evalExpr(st.step)
}
break
case 'forin': {
const arr = this.arrays[st.arr] || {}
for (const k of Object.keys(arr)) {
this.vars[st.iv] = k
const r = this.execStmt(st.body)
const r = await this.execStmt(st.body)
if (r?.t === 'break') break
if (r?.t === 'continue') continue
if (r?.t === 'next' || r?.t === 'nextfile' || r?.t === 'exit')
@@ -1209,7 +1275,7 @@ class BareAwkRuntime {
case 'delete': {
const tbl =
this.arrays[st.arr] || (this.arrays[st.arr] = Object.create(null))
const ky = String(this.evalExpr(st.key))
const ky = String(await this.evalExpr(st.key))
delete tbl[ky]
break
}
@@ -1222,19 +1288,23 @@ class BareAwkRuntime {
case 'continue':
return { t: 'continue' }
case 'exit':
this.exitCode = Math.trunc(Number(this.evalExpr(st.code))) || 0
this.exitCode =
Math.trunc(Number(await this.evalExpr(st.code))) || 0
this.exitPending = true
return { t: 'exit' }
case 'return':
return { t: 'return', v: st.e ? this.evalExpr(st.e) : '' }
return {
t: 'return',
v: st.e ? await this.evalExpr(st.e) : ''
}
case 'print':
this.queuePrint(st.args, st.redir, false)
await this.queuePrint(st.args, st.redir, false)
break
case 'printf':
this.queuePrint([st.fmt, ...st.args], st.redir, true)
await this.queuePrint([st.fmt, ...st.args], st.redir, true)
break
case 'expr':
this.evalExpr(st.e)
await this.evalExpr(st.e)
break
}
}
@@ -1244,17 +1314,23 @@ class BareAwkRuntime {
* @param {{ op: string, file: BareAwkNode } | null} redir
* @param {boolean} isPrintf
*/
queuePrint(args, redir, isPrintf) {
async queuePrint(args, redir, isPrintf) {
let s = ''
if (isPrintf) {
const fmt = String(this.evalExpr(args[0]))
const rest = args.slice(1).map((x) => this.evalExpr(x))
const fmt = String(await this.evalExpr(args[0]))
const rest = []
for (let j = 1; j < args.length; j++)
rest.push(await this.evalExpr(args[j]))
s = bareAwkSprintf(fmt, rest)
} else if (!args.length) s = this.$0
else s = args.map((x) => String(this.evalExpr(x))).join(this.OFS)
else {
const parts = []
for (const x of args) parts.push(String(await this.evalExpr(x)))
s = parts.join(this.OFS)
}
s += this.ORS
if (redir) {
const path = String(this.evalExpr(redir.file))
const path = String(await this.evalExpr(redir.file))
this.pendingWrites.push({
path,
data: s,
@@ -1317,7 +1393,7 @@ export async function bareAwkRun(program, opts, io) {
rule.pattern.k === 'BEGIN'
) {
for (const st of rule.stmts) {
const r = rt.execStmt(st)
const r = await rt.execStmt(st)
await rt.flushWrites()
if (r?.t === 'exit') return rt.exitCode
}
@@ -1343,9 +1419,9 @@ export async function bareAwkRun(program, opts, io) {
rt.splitFields()
for (const rule of ast.rules) {
if (rule.pattern && rule.pattern.t === 'pat') continue
if (!rt.patternMatch(rule.pattern)) continue
if (!(await rt.patternMatch(rule.pattern))) continue
for (const st of rule.stmts) {
const r = rt.execStmt(st)
const r = await rt.execStmt(st)
await rt.flushWrites()
if (r?.t === 'next') break
if (r?.t === 'nextfile') break lines
@@ -1371,7 +1447,7 @@ export async function bareAwkRun(program, opts, io) {
for (const rule of ast.rules) {
if (rule.pattern && rule.pattern.t === 'pat' && rule.pattern.k === 'END') {
for (const st of rule.stmts) {
rt.execStmt(st)
await rt.execStmt(st)
await rt.flushWrites()
}
}
@@ -5,7 +5,7 @@
"synopsis": [
"awk [OPTION]... [OPERAND]..."
],
"description": "Pattern-directed scanning and processing. Engine in lib/awk-engine.js; not full POSIX awk.",
"description": "Pattern-directed scanning and processing. Engine in lib/awk-engine.js; Issue 7oriented subset (BEGIN/END, patterns, arrays, printf, POSIX math builtins atan2/cos/sin/exp/log/sqrt, getline from stdin or file via getline var < path, next/nextfile, delete). See handbook ch.9 §7 for lexer and timing divergences.",
"options": [],
"keywords": [
"awk",
@@ -5,7 +5,8 @@
"synopsis": [
"expr [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of expr. Full behavior is defined in packages/bare-os-coreutils/src/expr.js.",
"description": "Integer and string expressions: + - * / % (truncating division), comparisons, = and != for strings or numbers, and POSIX : (match length) with ECMA RegExp body after ^(?:…).",
"bareOsNotes": "Exit 2 on errors; : pattern is not full BRE — ECMA syntax inside (?:).",
"options": [],
"keywords": [
"expr",
@@ -5,7 +5,8 @@
"synopsis": [
"test [OPTION]... [OPERAND]..."
],
"description": "Bare OS implementation of evaluate a condition. Full behavior is defined in packages/bare-os-coreutils/src/test.js.",
"description": "File and string predicates: -e -f -d -r -w -x -s -h/-L, -z -n, string = !=, integer -eq -ne -lt -le -gt -ge (decimal integers only), file mtime FILE1 -nt FILE2 / -ot and same-inode -ef when stat exposes dev+ino.",
"bareOsNotes": "-ef falls back to path string compare if dev/ino missing.",
"options": [],
"keywords": [
"test",
+1 -1
View File
@@ -6,6 +6,6 @@
"description": "Build JS /bin utilities for bare-operating-system (concat + stage to kernel/)",
"scripts": {
"build": "node ./scripts/ensure-man-pages.mjs && node ./build.mjs",
"test": "node ./test/help-bin-list.test.mjs && node ./test/edit-key-parse.test.mjs && node ./test/edit-teardown.test.mjs && node ./test/baretop-bundle.test.mjs && node ./test/baretop-fixture.test.mjs && node ./test/baretop-compose.test.mjs && node ./test/baretop-incremental.test.mjs && node ./test/baretop-ui-helpers.test.mjs && node ./test/posix-test-int-compare.test.mjs && node ./test/posix-utils-edge.test.mjs && node ./test/posix-golden-issue7.test.mjs && node ./test/getconf-pathconf-union-mirror.test.mjs && node ./test/getconf-posix-shm.test.mjs && node ./test/awk-sed-posix-smoke.test.mjs && node ./test/xattr-acl-utils.test.mjs && node ./test/xcu-issue7-sweep.test.mjs && node ./test/expand-tab-stops.test.mjs"
"test": "node ./test/help-bin-list.test.mjs && node ./test/edit-key-parse.test.mjs && node ./test/edit-teardown.test.mjs && node ./test/baretop-bundle.test.mjs && node ./test/baretop-fixture.test.mjs && node ./test/baretop-compose.test.mjs && node ./test/baretop-incremental.test.mjs && node ./test/baretop-ui-helpers.test.mjs && node ./test/posix-test-int-compare.test.mjs && node ./test/posix-utils-edge.test.mjs && node ./test/posix-golden-issue7.test.mjs && node ./test/getconf-pathconf-union-mirror.test.mjs && node ./test/getconf-posix-shm.test.mjs && node ./test/awk-sed-posix-smoke.test.mjs && node ./test/xattr-acl-utils.test.mjs && node ./test/xcu-issue7-sweep.test.mjs && node ./test/expand-tab-stops.test.mjs && node ./test/pkg-swarm-index-pathcap.test.mjs"
}
}
+23 -3
View File
@@ -2,7 +2,7 @@ async function run(ctx, argv) {
const tokens = argv.slice(1)
if (!tokens.length || tokens[0] === '--help' || tokens[0] === '-h') {
ctx.console.log(
'usage: expr EXPRESSION\nInteger arithmetic (+ - * / %), comparisons, and string = / !=.'
'usage: expr EXPRESSION\nInteger arithmetic (+ - * / %), comparisons, string = / !=, and POSIX : (regex match length; pattern is ECMA ^(?:…))'
)
return
}
@@ -19,11 +19,31 @@ async function run(ctx, argv) {
if (/^-?\d+$/.test(t)) return { kind: 'n', v: parseInt(t, 10) }
return { kind: 's', v: t }
}
function parseMul() {
/** POSIX-style `:` regex match length (pattern is ECMA RegExp body after ^). */
function parseColon() {
let left = parsePrimary()
while (peek() === ':') {
take()
const right = parsePrimary()
const s = left.kind === 'n' ? String(left.v) : String(left.v)
const pat = right.kind === 'n' ? String(right.v) : String(right.v)
let n = 0
try {
const re = new RegExp('^(?:' + pat + ')')
const m = re.exec(s)
if (m) n = m[0].length
} catch (_) {
n = 0
}
left = { kind: 'n', v: n }
}
return left
}
function parseMul() {
let left = parseColon()
while (peek() === '*' || peek() === '/' || peek() === '%') {
const op = take()
const right = parsePrimary()
const right = parseColon()
if (left.kind !== 'n' || right.kind !== 'n') throw new Error('non-numeric')
if (op === '*') left = { kind: 'n', v: left.v * right.v }
else if (op === '/') {
@@ -83,5 +83,10 @@ async function run(ctx, argv) {
return
}
ctx.console.error('pathcap-verify: FAIL ' + r.reason)
if (trusted) {
ctx.console.error(
'pathcap-verify: hint: extend BARE_OS_PATH_CAPABILITY_TRUSTED_PUBKEYS_HEX with the issuer Ed25519 pubkey (64 hex) when the envelope is otherwise well-formed.'
)
}
ctx.exitCode = 1
}
@@ -102,6 +102,27 @@ ${topic ? 'Topic pin prefix: ' + topic.slice(0, 16) + '…' : 'Topic pin unset.'
return
}
const r = await pkgIndexLookup(ctx, keyArg)
const ent =
r && typeof r === 'object' && r.entry && typeof r.entry === 'object'
? r.entry
: null
if (
ent &&
ent.pathCapabilityEnvelope != null &&
typeof ctx.bareOsVerifyPathCapabilityEnvelope === 'function'
) {
try {
r.pathCapabilityEnvelopeVerify =
ctx.bareOsVerifyPathCapabilityEnvelope(
ent.pathCapabilityEnvelope
)
} catch (e) {
r.pathCapabilityEnvelopeVerify = {
ok: false,
error: (e && e.message) || String(e)
}
}
}
ctx.console.log(JSON.stringify(r, null, 2))
if (!r.ok) ctx.exitCode = 1
return
+19
View File
@@ -67,6 +67,25 @@ async function evalTest(ctx, args) {
}
if (op === '=') return a === b
if (op === '!=') return a !== b
if (op === '-nt' || op === '-ot' || op === '-ef') {
const s1 = await ctx.vfs.stat(a)
const s2 = await ctx.vfs.stat(b)
if (!s1 || !s2) return false
if (op === '-ef') {
const i1 = s1.ino != null ? String(s1.ino) : ''
const i2 = s2.ino != null ? String(s2.ino) : ''
const d1 = s1.dev != null ? String(s1.dev) : ''
const d2 = s2.dev != null ? String(s2.dev) : ''
if (i1 && i2 && d1 && d2) return i1 === i2 && d1 === d2
return a === b
}
const t1 = Number(s1.mtimeMs)
const t2 = Number(s2.mtimeMs)
const m1 = Number.isFinite(t1) ? t1 : 0
const m2 = Number.isFinite(t2) ? t2 : 0
if (op === '-nt') return m1 > m2
if (op === '-ot') return m1 < m2
}
return false
}
if (args.length === 2) {
@@ -59,3 +59,78 @@ test('awk nextfile skips remainder of first file', async (t) => {
)
t.is(lines.join('\n'), 'A1\nB1\nB2')
})
test('awk getline from file reads lines', async (t) => {
/** @type {string[]} */
const lines = []
const enc = new TextEncoder()
const io = {
stdinLines: [],
bytesToString(buf) {
return buf ? new TextDecoder().decode(buf) : ''
},
print(s) {
lines.push(String(s).replace(/\n$/, ''))
},
async writeFile() {},
readFile(p) {
if (p === 'data.txt') return enc.encode('one\ntwo\n')
return null
}
}
await bareAwkRun(
'BEGIN { while ((getline x < "data.txt") > 0) print x }',
{ fs: ' ', argc: 1, argv: ['awk'], environ: {} },
io
)
t.is(lines.join('\n'), 'one\ntwo')
})
test('awk atan2 and unary minus', async (t) => {
/** @type {string[]} */
const lines = []
const io = {
stdinLines: [''],
bytesToString() {
return ''
},
print(s) {
lines.push(String(s).replace(/\n$/, ''))
},
async writeFile() {},
async readFile() {
return null
}
}
await bareAwkRun(
'BEGIN { print int(atan2(0,-1)*1000); print -3+1 }',
{ fs: ' ', argc: 1, argv: ['awk'], environ: {} },
io
)
t.ok(lines[0].includes('3141') || lines[0] === '3141' || Number(lines[0]) > 3000)
t.is(lines[1], '-2')
})
test('awk substr two-arg is suffix from start', async (t) => {
/** @type {string[]} */
const lines = []
const io = {
stdinLines: [''],
bytesToString() {
return ''
},
print(s) {
lines.push(String(s).replace(/\n$/, ''))
},
async writeFile() {},
async readFile() {
return null
}
}
await bareAwkRun(
'BEGIN { print substr("abcdef", 3) }',
{ fs: ' ', argc: 1, argv: ['awk'], environ: {} },
io
)
t.is(lines[0], 'cdef')
})
@@ -0,0 +1,95 @@
/**
* pkg-swarm-index path-capability envelope verify on `get`.
*/
import { readFile } from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import test from 'brittle'
import b4a from 'b4a'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor
async function loadBin() {
const runtime = await readFile(
path.join(__dirname, '../lib/runtime.js'),
'utf8'
)
let body = await readFile(
path.join(__dirname, '../src/pkg-swarm-index.js'),
'utf8'
)
body = body.replace(/\nexport \{ run \}\s*$/m, '\n')
return new AsyncFunction(
'ctx',
'argv',
`${runtime}\n${body}\nif (typeof run === 'function') return await run(ctx, argv)\n`
)
}
const sampleIndex = {
packages: {
'[email protected]': {
driveKey: 'a'.repeat(64),
manifestHash: 'b'.repeat(64),
pathCapabilityEnvelope: { stub: true }
}
}
}
test('pkg-swarm-index get adds pathCapabilityEnvelopeVerify when ctx hook exists', async (t) => {
const run = await loadBin()
const logs = []
const ctx = {
b4a,
exitCode: 0,
env: {},
vfs: {
async readFile(p) {
t.is(p, '/etc/bare-os/pkg-index.json')
return b4a.from(JSON.stringify(sampleIndex))
}
},
bareOsVerifyPathCapabilityEnvelope(env) {
return { ok: true, envelope: env }
},
console: {
log: (m) => logs.push(String(m)),
error: (m) => logs.push(String(m))
}
}
await run(ctx, ['pkg-swarm-index', 'get', '[email protected]'])
t.is(ctx.exitCode, 0)
const out = JSON.parse(logs[0])
t.ok(out.pathCapabilityEnvelopeVerify)
t.is(out.pathCapabilityEnvelopeVerify.ok, true)
})
test('pkg-swarm-index get captures verify errors in pathCapabilityEnvelopeVerify', async (t) => {
const run = await loadBin()
const logs = []
const ctx = {
b4a,
exitCode: 0,
env: {},
vfs: {
async readFile() {
return b4a.from(JSON.stringify(sampleIndex))
}
},
bareOsVerifyPathCapabilityEnvelope() {
throw new Error('bad envelope')
},
console: {
log: (m) => logs.push(String(m)),
error: (m) => logs.push(String(m))
}
}
await run(ctx, ['pkg-swarm-index', 'get', '[email protected]'])
t.is(ctx.exitCode, 0)
const out = JSON.parse(logs[0])
t.ok(out.pathCapabilityEnvelopeVerify)
t.is(out.pathCapabilityEnvelopeVerify.ok, false)
t.ok(String(out.pathCapabilityEnvelopeVerify.error).includes('bad envelope'))
})
@@ -71,3 +71,41 @@ test('/bin/printf %c uses first character of string arg', async (t) => {
await run(ctx, ['printf', '%c', 'xy'])
t.is(ctx._logs.join(''), 'x')
})
test('/bin/expr : returns match length (POSIX-shaped)', async (t) => {
const run = await loadBin('expr')
const ctx = createCtx()
await run(ctx, ['expr', 'foobar', ':', 'foo'])
t.is(ctx._logs.join(''), '3')
const ctx2 = createCtx()
await run(ctx2, ['expr', 'foo', ':', 'bar'])
t.is(ctx2._logs.join(''), '0')
})
test('/bin/test -nt and -ot use mtimeMs', async (t) => {
const run = await loadBin('test')
const ctx = {
b4a,
exitCode: 0,
vfs: {
async stat(p) {
if (p === 'older')
return { type: 'file', mtimeMs: 1000, ino: 1, dev: 0 }
if (p === 'newer')
return { type: 'file', mtimeMs: 2000, ino: 2, dev: 0 }
return null
}
},
console: { log() {}, error() {} }
}
await run(ctx, ['test', 'newer', '-nt', 'older'])
t.is(ctx.exitCode, 0)
const ctx2 = {
b4a,
exitCode: 0,
vfs: ctx.vfs,
console: { log() {}, error() {} }
}
await run(ctx2, ['test', 'older', '-ot', 'newer'])
t.is(ctx2.exitCode, 0)
})
+1
View File
@@ -4,6 +4,7 @@ Cross-package **version alignment** (ctx API, feature-bits doc, lifecycle schema
## Documentation (rolling)
- **Protomux cap channel** — With **`BARE_OS_PROTOMUX_CAP_CHANNEL`**, the booter accepts **`bare-os-cap-v1`** messages up to **65536** bytes; larger payloads are dropped (**`protomux_cap_payload_oversized`**) without incrementing **`protomuxCapChannelRxTotal`** ([`packages/bare-os-booter/lib/swarm-disk.js`](../packages/bare-os-booter/lib/swarm-disk.js)).
- **`peer_firewall_stats` / `net_summary`** — [`lib/channel.js`](lib/channel.js) comment ties default firewall counters to **`/proc/bare_os/net_summary.json`** scalars (**`peerFirewallBlockedSessions`**, **`peerFirewallAllowedSessions`**) and the **`peerFirewallStats`** object when present.
- **`0.9.1` / `BARE_OS_KERNEL_FEATURE_BITS_DOC` `16`** — Protomux cap-channel constant (**`PROTOCOL_CAP_CHANNEL_NAME`**) + **`BARE_OS_FEATURE8_PROTOMUX_CAP_CHANNEL`** documentation bump (stock **`bare-os-booter`** surfaces); **`stock-booter-capability-contract.json`** **`expectedFeatureBitsDoc`** aligned.
- **Wire stack pins** — Re-verified **2026-04-05**: root **`package-lock.json`** hoisted **`protomux`** / **`hyperswarm`** versions still match [`packages/bare-os-booter/fixtures/protomux-hyperswarm-lock.json`](../packages/bare-os-booter/fixtures/protomux-hyperswarm-lock.json) (**`BARE_OS_PROTOMUX_CHANNEL_SCHEMA_VERSION`** unchanged).
@@ -4,7 +4,7 @@
*/
/** Semver for the documented POSIX-like surface (handbook ch.9 + environment appendix). */
export const BARE_OS_POSIX_PROFILE_VERSION = '1.0.17'
export const BARE_OS_POSIX_PROFILE_VERSION = '1.0.18'
/** Short identifier for telemetry and `/proc` mirrors. */
export const BARE_OS_POSIX_PROFILE_ID = 'bare-os-posix-like'
+171 -95
View File
@@ -91,7 +91,8 @@ function bareOsEmitRaw(ctx, chunk) {
* POSIX-oriented awk interpreter for Bare OS (no import; concatenated before src/awk.js).
* Supports: BEGIN/END, /re/, line patterns, {}, print/printf, if/else, while, for(;;), for(x in a),
* delete arr[idx], next, exit, break, continue, ++/--, arrays, strings, regex ~ !~, builtins (length, substr, index,
* split, sprintf, sub, gsub, match, int, tolower, toupper, rand, srand), getline from stdin/files,
* split, sprintf, sub, gsub, match, int, atan2, cos, sin, exp, log, sqrt, tolower, toupper, rand, srand),
* getline from stdin, files (getline x < \"path\"), unary minus,
* optional fixed-width fields via FIELDWIDTHS when BARE_OS_AWK_FIELDWIDTHS is enabled,
* -F FS, NF, NR, FNR, $0..$n, OFS, ORS, RS, ARGC, ARGV, FILENAME, ENVIRON (read-only mirror).
* GNU-style empty fields: consecutive FS delimiters still advance $1..$NF where the grammar allows.
@@ -670,7 +671,11 @@ class BareAwkParser {
}
parseUnary() {
if (this.peek().value === '!' || this.peek().value === '+') {
if (
this.peek().value === '!' ||
this.peek().value === '+' ||
this.peek().value === '-'
) {
const op = String(this.peek().value)
this.p++
return { t: 'unop', op, a: this.parseUnary() }
@@ -819,6 +824,8 @@ class BareAwkRuntime {
this._stdinIx = 0
/** @type {{ path: string, data: string, append: boolean }[]} */
this.pendingWrites = []
/** @type {Map<string, { lines: string[], i: number }>} */
this._getlineFileState = new Map()
}
splitFields() {
@@ -889,14 +896,14 @@ class BareAwkRuntime {
/**
* @param {BareAwkNode | null} pat
*/
patternMatch(pat) {
async patternMatch(pat) {
if (pat == null) return true
if (pat.t === 'pat') return false
if (pat.t === 'patRe') {
pat.rx.lastIndex = 0
return pat.rx.test(this.$0)
}
if (pat.t === 'patExpr') return this.truthy(this.evalExpr(pat.e))
if (pat.t === 'patExpr') return this.truthy(await this.evalExpr(pat.e))
return false
}
@@ -910,7 +917,7 @@ class BareAwkRuntime {
/**
* @param {BareAwkNode} n
*/
evalExpr(n) {
async evalExpr(n) {
if (!n) return ''
switch (n.t) {
case 'num':
@@ -932,9 +939,9 @@ class BareAwkRuntime {
return this.vars[name] != null ? this.vars[name] : ''
}
case 'field':
return this.fieldNum(this.evalExpr(n.e))
return this.fieldNum(await this.evalExpr(n.e))
case 'index': {
const k = String(this.evalExpr(n.idx))
const k = String(await this.evalExpr(n.idx))
if (n.name === 'ENVIRON') {
const v = this.environ[k]
return v != null ? v : ''
@@ -944,8 +951,8 @@ class BareAwkRuntime {
return arr[k] != null ? arr[k] : ''
}
case 'binop': {
const a = this.evalExpr(n.a)
const b = this.evalExpr(n.b)
const a = await this.evalExpr(n.a)
const b = await this.evalExpr(n.b)
const an = Number(a)
const bn = Number(b)
switch (n.op) {
@@ -978,77 +985,78 @@ class BareAwkRuntime {
}
}
case 'unop': {
const a = this.evalExpr(n.a)
const a = await this.evalExpr(n.a)
if (n.op === '!') return this.truthy(a) ? 0 : 1
if (n.op === '+') return Number(a) || 0
if (n.op === '-') return -(Number(a) || 0)
return 0
}
case '||':
return this.truthy(this.evalExpr(n.a))
return this.truthy(await this.evalExpr(n.a))
? 1
: this.truthy(this.evalExpr(n.b))
: this.truthy(await this.evalExpr(n.b))
? 1
: 0
case '&&':
return this.truthy(this.evalExpr(n.a)) &&
this.truthy(this.evalExpr(n.b))
return this.truthy(await this.evalExpr(n.a)) &&
this.truthy(await this.evalExpr(n.b))
? 1
: 0
case 'match': {
const s = String(this.evalExpr(n.a))
const s = String(await this.evalExpr(n.a))
const rhs = n.b
let rx
if (rhs.t === 'rxLit') rx = rhs.rx
else {
const t = String(this.evalExpr(rhs))
const t = String(await this.evalExpr(rhs))
rx = bareAwkMakeRx(t, '')
}
rx.lastIndex = 0
return rx.test(s) ? 1 : 0
}
case 'nmatch': {
const s = String(this.evalExpr(n.a))
const s = String(await this.evalExpr(n.a))
const rhs = n.b
let rx
if (rhs.t === 'rxLit') rx = rhs.rx
else {
const t = String(this.evalExpr(rhs))
const t = String(await this.evalExpr(rhs))
rx = bareAwkMakeRx(t, '')
}
rx.lastIndex = 0
return rx.test(s) ? 0 : 1
}
case '?:':
return this.truthy(this.evalExpr(n.n))
? this.evalExpr(n.a)
: this.evalExpr(n.b)
return this.truthy(await this.evalExpr(n.n))
? await this.evalExpr(n.a)
: await this.evalExpr(n.b)
case 'assign': {
const v = this.evalExpr(n.right)
this.assign(n.left, n.op, v)
const v = await this.evalExpr(n.right)
await this.assign(n.left, n.op, v)
return v
}
case 'pre': {
const cur = Number(this.evalExpr(n.x))
const cur = Number(await this.evalExpr(n.x))
const next = n.op === '++' ? cur + 1 : cur - 1
this.assignScalar(n.x, next)
await this.assignScalar(n.x, next)
return next
}
case 'post': {
const cur = Number(this.evalExpr(n.x))
const cur = Number(await this.evalExpr(n.x))
const next = n.op === '++' ? cur + 1 : cur - 1
this.assignScalar(n.x, next)
await this.assignScalar(n.x, next)
return cur
}
case 'call':
return this.callBuiltin(n.name, n.args)
return await this.callBuiltin(n.name, n.args)
case 'getline':
return this.doGetline(n)
return await this.doGetline(n)
default:
return ''
}
}
assignScalar(x, v) {
async assignScalar(x, v) {
if (x.t === 'var') {
const name = x.name
if (name === 'FS') this.FS = String(v)
@@ -1057,7 +1065,7 @@ class BareAwkRuntime {
else if (name === 'RS') this.RS = String(v)
else this.vars[name] = v
} else if (x.t === 'field') {
this.setField(this.evalExpr(x.e), v)
this.setField(await this.evalExpr(x.e), v)
}
}
@@ -1066,10 +1074,10 @@ class BareAwkRuntime {
* @param {string} op
* @param {unknown} v
*/
assign(left, op, v) {
async assign(left, op, v) {
let base = v
if (op !== '=') {
const cur = Number(this.evalExpr(left))
const cur = Number(await this.evalExpr(left))
const nv = Number(v)
if (op === '+=') base = cur + nv
else if (op === '-=') base = cur - nv
@@ -1080,32 +1088,52 @@ class BareAwkRuntime {
if (left.t === 'index') {
const arr =
this.arrays[left.name] || (this.arrays[left.name] = Object.create(null))
arr[String(this.evalExpr(left.idx))] = base
arr[String(await this.evalExpr(left.idx))] = base
return
}
this.assignScalar(left, base)
await this.assignScalar(left, base)
}
/**
* @param {string} path
*/
async _ensureGetlineFile(path) {
let st = this._getlineFileState.get(path)
if (st) return st
const buf = await this.io.readFile(path)
const text =
buf && this.io.bytesToString ? this.io.bytesToString(buf) : ''
const lines = text.split(/\r?\n/)
if (lines.length && lines[lines.length - 1] === '') lines.pop()
st = { lines, i: 0 }
this._getlineFileState.set(path, st)
return st
}
/**
* @param {string} name
* @param {BareAwkNode[]} args
*/
callBuiltin(name, args) {
const a = (i) => (args[i] ? this.evalExpr(args[i]) : '')
async callBuiltin(name, args) {
const a = async (i) =>
args[i] != null ? await this.evalExpr(args[i]) : ''
switch (name) {
case 'length':
if (!args.length) return this.$0.length
return String(a(0)).length
case 'substr':
return String(a(0)).slice(
Math.max(0, Math.trunc(Number(a(1))) - 1),
Math.max(0, Math.trunc(Number(a(1))) - 1 + Math.trunc(Number(a(2))))
)
return String(await a(0)).length
case 'substr': {
const s = String(await a(0))
const start = Math.max(0, Math.trunc(Number(await a(1))) - 1)
if (args.length < 3) return s.slice(start)
const ln = Math.trunc(Number(await a(2)))
if (!Number.isFinite(ln) || ln < 0) return ''
return s.slice(start, start + ln)
}
case 'index':
return String(a(0)).indexOf(String(a(1))) + 1 || 0
return String(await a(0)).indexOf(String(await a(1))) + 1 || 0
case 'split': {
const s = String(a(0))
const fs = args[1] ? String(this.evalExpr(args[1])) : this.FS
const s = String(await a(0))
const fs = args[1] ? String(await this.evalExpr(args[1])) : this.FS
let rx = fs.length === 1 ? null : bareAwkMakeRx(fs, '')
const parts = rx ? s.split(rx) : s.split(fs)
const aname = /** @type {BareAwkNode} */ (args[2])
@@ -1116,17 +1144,35 @@ class BareAwkRuntime {
}
return parts.length
}
case 'sprintf':
return bareAwkSprintf(
String(a(0)),
args.slice(1).map((x) => this.evalExpr(x))
)
case 'sprintf': {
const fmt = String(await a(0))
const rest = []
for (let j = 1; j < args.length; j++)
rest.push(await this.evalExpr(args[j]))
return bareAwkSprintf(fmt, rest)
}
case 'int':
return Math.trunc(Number(a(0)))
return Math.trunc(Number(await a(0)))
case 'log': {
const x = Number(await a(0))
return x > 0 ? Math.log(x) : Number.NaN
}
case 'sqrt': {
const x = Number(await a(0))
return x >= 0 ? Math.sqrt(x) : Number.NaN
}
case 'sin':
return Math.sin(Number(await a(0)))
case 'cos':
return Math.cos(Number(await a(0)))
case 'exp':
return Math.exp(Number(await a(0)))
case 'atan2':
return Math.atan2(Number(await a(0)), Number(await a(1)))
case 'tolower':
return String(a(0)).toLowerCase()
return String(await a(0)).toLowerCase()
case 'toupper':
return String(a(0)).toUpperCase()
return String(await a(0)).toUpperCase()
case 'rand': {
this.randSeed = (this.randSeed * 1103515245 + 12345) & 0x7fffffff
return this.randSeed / 0x7fffffff
@@ -1140,9 +1186,11 @@ class BareAwkRuntime {
case 'sub':
case 'gsub': {
const rx =
args[0].t === 'rxLit' ? args[0].rx : bareAwkMakeRx(String(a(0)), '')
const rep = String(a(1))
let target = args[2] ? String(a(2)) : this.$0
args[0].t === 'rxLit'
? args[0].rx
: bareAwkMakeRx(String(await a(0)), '')
const rep = String(await a(1))
let target = args[2] ? String(await a(2)) : this.$0
let n = 0
if (name === 'sub') {
rx.lastIndex = 0
@@ -1167,9 +1215,11 @@ class BareAwkRuntime {
return n
}
case 'match': {
const s = String(a(0))
const s = String(await a(0))
const rx =
args[1].t === 'rxLit' ? args[1].rx : bareAwkMakeRx(String(a(1)), '')
args[1].t === 'rxLit'
? args[1].rx
: bareAwkMakeRx(String(await a(1)), '')
rx.lastIndex = 0
const m = rx.exec(s)
if (!m) {
@@ -1183,7 +1233,7 @@ class BareAwkRuntime {
}
default:
if (this.funcs.has(name)) {
return this.callUser(name, args)
return await this.callUser(name, args)
}
bareAwkError('unknown function ' + name)
}
@@ -1193,19 +1243,19 @@ class BareAwkRuntime {
* @param {string} name
* @param {BareAwkNode[]} args
*/
callUser(name, args) {
async callUser(name, args) {
const f = this.funcs.get(name)
if (!f) return ''
const frame = { ...this.vars }
for (let i = 0; i < f.params.length; i++) {
frame[f.params[i]] = args[i] ? this.evalExpr(args[i]) : ''
frame[f.params[i]] = args[i] ? await this.evalExpr(args[i]) : ''
}
const prev = this.vars
this.vars = frame
let ret = ''
try {
for (const st of f.body) {
const r = this.execStmt(st)
const r = await this.execStmt(st)
if (r && r.t === 'return') {
ret = r.v != null ? r.v : ''
break
@@ -1220,12 +1270,27 @@ class BareAwkRuntime {
/**
* @param {BareAwkNode} n
*/
doGetline(n) {
async doGetline(n) {
let line = null
if (n.from) {
const path = String(this.evalExpr(n.from))
/* sync read — awk.js should preload or use async wrapper; runtime uses promise in run loop */
return 0
const path = String(await this.evalExpr(n.from))
const st = await this._ensureGetlineFile(path)
if (st.i < st.lines.length) line = st.lines[st.i++]
else line = null
if (line == null) {
if (n.var) this.vars[n.var] = ''
else {
this.$0 = ''
this.splitFields()
}
return 0
}
if (n.var) this.vars[n.var] = line
else {
this.$0 = line
this.splitFields()
}
return 1
}
if (this._stdinIx < this.io.stdinLines.length) {
line = this.io.stdinLines[this._stdinIx++]
@@ -1245,23 +1310,24 @@ class BareAwkRuntime {
/**
* @param {BareAwkNode} st
* @returns {{ t: string, v?: unknown } | void}
* @returns {Promise<{ t: string, v?: unknown } | void>}
*/
execStmt(st) {
async execStmt(st) {
switch (st.t) {
case 'block':
for (const s of st.stmts) {
const r = this.execStmt(s)
const r = await this.execStmt(s)
if (r) return r
}
break
case 'if':
if (this.truthy(this.evalExpr(st.cond))) return this.execStmt(st.then)
if (st.else) return this.execStmt(st.else)
if (this.truthy(await this.evalExpr(st.cond)))
return await this.execStmt(st.then)
if (st.else) return await this.execStmt(st.else)
break
case 'while':
while (this.truthy(this.evalExpr(st.cond))) {
const r = this.execStmt(st.body)
while (this.truthy(await this.evalExpr(st.cond))) {
const r = await this.execStmt(st.body)
if (r?.t === 'break') break
if (r?.t === 'continue') continue
if (r?.t === 'next' || r?.t === 'nextfile' || r?.t === 'exit')
@@ -1269,25 +1335,25 @@ class BareAwkRuntime {
}
break
case 'for':
if (st.init) this.evalExpr(st.init)
if (st.init) await this.evalExpr(st.init)
while (true) {
if (st.cond && !this.truthy(this.evalExpr(st.cond))) break
const r = this.execStmt(st.body)
if (st.cond && !this.truthy(await this.evalExpr(st.cond))) break
const r = await this.execStmt(st.body)
if (r?.t === 'break') break
if (r?.t === 'continue') {
if (st.step) this.evalExpr(st.step)
if (st.step) await this.evalExpr(st.step)
continue
}
if (r?.t === 'next' || r?.t === 'nextfile' || r?.t === 'exit')
return r
if (st.step) this.evalExpr(st.step)
if (st.step) await this.evalExpr(st.step)
}
break
case 'forin': {
const arr = this.arrays[st.arr] || {}
for (const k of Object.keys(arr)) {
this.vars[st.iv] = k
const r = this.execStmt(st.body)
const r = await this.execStmt(st.body)
if (r?.t === 'break') break
if (r?.t === 'continue') continue
if (r?.t === 'next' || r?.t === 'nextfile' || r?.t === 'exit')
@@ -1298,7 +1364,7 @@ class BareAwkRuntime {
case 'delete': {
const tbl =
this.arrays[st.arr] || (this.arrays[st.arr] = Object.create(null))
const ky = String(this.evalExpr(st.key))
const ky = String(await this.evalExpr(st.key))
delete tbl[ky]
break
}
@@ -1311,19 +1377,23 @@ class BareAwkRuntime {
case 'continue':
return { t: 'continue' }
case 'exit':
this.exitCode = Math.trunc(Number(this.evalExpr(st.code))) || 0
this.exitCode =
Math.trunc(Number(await this.evalExpr(st.code))) || 0
this.exitPending = true
return { t: 'exit' }
case 'return':
return { t: 'return', v: st.e ? this.evalExpr(st.e) : '' }
return {
t: 'return',
v: st.e ? await this.evalExpr(st.e) : ''
}
case 'print':
this.queuePrint(st.args, st.redir, false)
await this.queuePrint(st.args, st.redir, false)
break
case 'printf':
this.queuePrint([st.fmt, ...st.args], st.redir, true)
await this.queuePrint([st.fmt, ...st.args], st.redir, true)
break
case 'expr':
this.evalExpr(st.e)
await this.evalExpr(st.e)
break
}
}
@@ -1333,17 +1403,23 @@ class BareAwkRuntime {
* @param {{ op: string, file: BareAwkNode } | null} redir
* @param {boolean} isPrintf
*/
queuePrint(args, redir, isPrintf) {
async queuePrint(args, redir, isPrintf) {
let s = ''
if (isPrintf) {
const fmt = String(this.evalExpr(args[0]))
const rest = args.slice(1).map((x) => this.evalExpr(x))
const fmt = String(await this.evalExpr(args[0]))
const rest = []
for (let j = 1; j < args.length; j++)
rest.push(await this.evalExpr(args[j]))
s = bareAwkSprintf(fmt, rest)
} else if (!args.length) s = this.$0
else s = args.map((x) => String(this.evalExpr(x))).join(this.OFS)
else {
const parts = []
for (const x of args) parts.push(String(await this.evalExpr(x)))
s = parts.join(this.OFS)
}
s += this.ORS
if (redir) {
const path = String(this.evalExpr(redir.file))
const path = String(await this.evalExpr(redir.file))
this.pendingWrites.push({
path,
data: s,
@@ -1406,7 +1482,7 @@ async function bareAwkRun(program, opts, io) {
rule.pattern.k === 'BEGIN'
) {
for (const st of rule.stmts) {
const r = rt.execStmt(st)
const r = await rt.execStmt(st)
await rt.flushWrites()
if (r?.t === 'exit') return rt.exitCode
}
@@ -1432,9 +1508,9 @@ async function bareAwkRun(program, opts, io) {
rt.splitFields()
for (const rule of ast.rules) {
if (rule.pattern && rule.pattern.t === 'pat') continue
if (!rt.patternMatch(rule.pattern)) continue
if (!(await rt.patternMatch(rule.pattern))) continue
for (const st of rule.stmts) {
const r = rt.execStmt(st)
const r = await rt.execStmt(st)
await rt.flushWrites()
if (r?.t === 'next') break
if (r?.t === 'nextfile') break lines
@@ -1460,7 +1536,7 @@ async function bareAwkRun(program, opts, io) {
for (const rule of ast.rules) {
if (rule.pattern && rule.pattern.t === 'pat' && rule.pattern.k === 'END') {
for (const st of rule.stmts) {
rt.execStmt(st)
await rt.execStmt(st)
await rt.flushWrites()
}
}
+23 -3
View File
@@ -91,7 +91,7 @@ async function run(ctx, argv) {
const tokens = argv.slice(1)
if (!tokens.length || tokens[0] === '--help' || tokens[0] === '-h') {
ctx.console.log(
'usage: expr EXPRESSION\nInteger arithmetic (+ - * / %), comparisons, and string = / !=.'
'usage: expr EXPRESSION\nInteger arithmetic (+ - * / %), comparisons, string = / !=, and POSIX : (regex match length; pattern is ECMA ^(?:…))'
)
return
}
@@ -108,11 +108,31 @@ async function run(ctx, argv) {
if (/^-?\d+$/.test(t)) return { kind: 'n', v: parseInt(t, 10) }
return { kind: 's', v: t }
}
function parseMul() {
/** POSIX-style `:` regex match length (pattern is ECMA RegExp body after ^). */
function parseColon() {
let left = parsePrimary()
while (peek() === ':') {
take()
const right = parsePrimary()
const s = left.kind === 'n' ? String(left.v) : String(left.v)
const pat = right.kind === 'n' ? String(right.v) : String(right.v)
let n = 0
try {
const re = new RegExp('^(?:' + pat + ')')
const m = re.exec(s)
if (m) n = m[0].length
} catch (_) {
n = 0
}
left = { kind: 'n', v: n }
}
return left
}
function parseMul() {
let left = parseColon()
while (peek() === '*' || peek() === '/' || peek() === '%') {
const op = take()
const right = parsePrimary()
const right = parseColon()
if (left.kind !== 'n' || right.kind !== 'n') throw new Error('non-numeric')
if (op === '*') left = { kind: 'n', v: left.v * right.v }
else if (op === '/') {
@@ -172,5 +172,10 @@ async function run(ctx, argv) {
return
}
ctx.console.error('pathcap-verify: FAIL ' + r.reason)
if (trusted) {
ctx.console.error(
'pathcap-verify: hint: extend BARE_OS_PATH_CAPABILITY_TRUSTED_PUBKEYS_HEX with the issuer Ed25519 pubkey (64 hex) when the envelope is otherwise well-formed.'
)
}
ctx.exitCode = 1
}
@@ -191,6 +191,27 @@ ${topic ? 'Topic pin prefix: ' + topic.slice(0, 16) + '…' : 'Topic pin unset.'
return
}
const r = await pkgIndexLookup(ctx, keyArg)
const ent =
r && typeof r === 'object' && r.entry && typeof r.entry === 'object'
? r.entry
: null
if (
ent &&
ent.pathCapabilityEnvelope != null &&
typeof ctx.bareOsVerifyPathCapabilityEnvelope === 'function'
) {
try {
r.pathCapabilityEnvelopeVerify =
ctx.bareOsVerifyPathCapabilityEnvelope(
ent.pathCapabilityEnvelope
)
} catch (e) {
r.pathCapabilityEnvelopeVerify = {
ok: false,
error: (e && e.message) || String(e)
}
}
}
ctx.console.log(JSON.stringify(r, null, 2))
if (!r.ok) ctx.exitCode = 1
return
+19
View File
@@ -156,6 +156,25 @@ async function evalTest(ctx, args) {
}
if (op === '=') return a === b
if (op === '!=') return a !== b
if (op === '-nt' || op === '-ot' || op === '-ef') {
const s1 = await ctx.vfs.stat(a)
const s2 = await ctx.vfs.stat(b)
if (!s1 || !s2) return false
if (op === '-ef') {
const i1 = s1.ino != null ? String(s1.ino) : ''
const i2 = s2.ino != null ? String(s2.ino) : ''
const d1 = s1.dev != null ? String(s1.dev) : ''
const d2 = s2.dev != null ? String(s2.dev) : ''
if (i1 && i2 && d1 && d2) return i1 === i2 && d1 === d2
return a === b
}
const t1 = Number(s1.mtimeMs)
const t2 = Number(s2.mtimeMs)
const m1 = Number.isFinite(t1) ? t1 : 0
const m2 = Number.isFinite(t2) ? t2 : 0
if (op === '-nt') return m1 > m2
if (op === '-ot') return m1 < m2
}
return false
}
if (args.length === 2) {
@@ -7,6 +7,20 @@
"driveKeyHex": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"manifestSha256Hex": "optional content hash of package root",
"publisherPubkeyHex": "optional ed25519 pubkey that signed this row"
},
"[email protected]": {
"driveKeyHex": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"pathCapabilityEnvelope": {
"schema": 1,
"note": "Illustrative only; replace pubkeyHex/signatureHex for real issuance. When present, pkg-swarm-index get calls ctx.bareOsVerifyPathCapabilityEnvelope and prints pathCapabilityEnvelopeVerify.",
"pubkeyHex": "0000000000000000000000000000000000000000000000000000000000000000",
"signatureHex": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"payload": {
"expMs": 1893456000000,
"ops": ["read"],
"prefix": "/tmp/"
}
}
}
}
}
@@ -1,7 +1,7 @@
{
"schema": 2,
"profileId": "bare-os-posix-like",
"generatedAt": "2026-04-06T01:43:58.480Z",
"generatedAt": "2026-04-06T03:01:17.010Z",
"note": "Sparse POSIX Issue 7 coverage hints for /bin utilities. Omitted command names are not yet profiled here.",
"commandIndex": [
{
@@ -3,7 +3,7 @@
"ctxApiVersion": "1.53.0",
"posixProfile": {
"id": "bare-os-posix-like",
"version": "1.0.17"
"version": "1.0.18"
},
"ops": ["readFile", "writeFile", "socket"],
"opsDetail": [
@@ -60,7 +60,7 @@
"fdModel": {
"schema": 1,
"stdio": [0, 1, 2],
"processTableSchema": 7
"processTableSchema": 8
},
"signalModel": {
"schema": 1,
+38 -13
View File
@@ -1322,7 +1322,7 @@ async function applyBootPolicyFile(ctx) {
/**
* @param {{ file: string, extId: string, scripts: string[], dependsOn: string[], signaturePointer?: string }[]} entries
* @returns {{ ok: true, ordered: typeof entries } | { ok: false, cycleExtIds: string[] }}
* @returns {{ ok: true, ordered: typeof entries } | { ok: false, cycleExtIds: string[], cycleEdges: string[] }}
*/
function topologicalOrderKernelExtEntries(entries) {
const sorted = [...entries].sort((a, b) => a.file.localeCompare(b.file))
@@ -1364,7 +1364,21 @@ function topologicalOrderKernelExtEntries(entries) {
}
if (out.length !== sorted.length) {
const stuck = sorted.filter((e) => !out.includes(e.file))
return { ok: false, cycleExtIds: stuck.map((e) => e.extId) }
const stuckIds = new Set(stuck.map((e) => e.extId))
/** @type {string[]} */
const cycleEdges = []
for (const e of stuck) {
for (const dep of e.dependsOn) {
if (stuckIds.has(dep))
cycleEdges.push(String(e.extId) + ' -> dependsOn:' + String(dep))
}
}
cycleEdges.sort()
return {
ok: false,
cycleExtIds: stuck.map((e) => e.extId),
cycleEdges: cycleEdges.slice(0, 48)
}
}
const byFile = new Map(sorted.map((e) => [e.file, e]))
return {
@@ -1936,17 +1950,23 @@ async function runKernelExtDropins(ctx, opts = {}) {
/** @type {typeof collected} */
let ordered
if (!topo.ok) {
const edgeHint =
topo.cycleEdges && topo.cycleEdges.length
? '; cycle edges (stuck->dep): ' + topo.cycleEdges.join('; ')
: ''
bootStructuredLog(
ctx,
'error',
'kernelExt.cycle',
'[kernel.ext.d] dependency cycle in extension drop-ins; ext ids: ' +
topo.cycleExtIds.join(', ')
topo.cycleExtIds.join(', ') +
edgeHint
)
if (strictPol) {
await appendKernelExtAuditNdjson(ctx, {
event: 'dependency_cycle',
cycleExtIds: [...topo.cycleExtIds].sort()
cycleExtIds: [...topo.cycleExtIds].sort(),
cycleEdges: topo.cycleEdges || []
})
return false
}
@@ -1987,6 +2007,7 @@ async function runKernelExtDropins(ctx, opts = {}) {
? {
kind: 'dependency_cycle',
cycleExtIds: cycleSorted,
cycleEdges: topo.cycleEdges || [],
provenance: 'kernel.ext.d topological sort (init-main)'
}
: null,
@@ -3747,24 +3768,21 @@ async function runKernelSelftest(ctx) {
return true
}
/**
* @param {Record<string, unknown>} ctx
* @param {number} bootT0
* @param {string[]} stageLog
*/
/**
* @param {Record<string, unknown>} ctx
* @param {number} bootT0
* @param {{ kind: string, code: string, wallMs: number, limitMs: number }[]} violations
* @param {boolean} coldExceeded
* @param {boolean} stdlibExceeded
* @param {string[]} [stageLog]
*/
async function maybeWriteBootBudgetSummaryJson(
ctx,
bootT0,
violations,
coldExceeded,
stdlibExceeded
stdlibExceeded,
stageLog
) {
const vfs = ctx.vfs
const b4 = ctx.b4a
@@ -3772,15 +3790,21 @@ async function maybeWriteBootBudgetSummaryJson(
const wall = Date.now() - bootT0
const row =
JSON.stringify({
schema: 1,
schema: 2,
atMs: Date.now(),
coldWallMs: wall,
coldExceeded,
bareStdlibExceeded: stdlibExceeded,
violationCodes: violations.map((v) => v.code).filter(Boolean),
violations,
bootStageCount:
Array.isArray(stageLog) && stageLog.length ? stageLog.length : null,
bootStageTail:
Array.isArray(stageLog) && stageLog.length
? stageLog.slice(-16)
: undefined,
procHint: '/proc/bare_os/boot_budget_summary.json',
note: 'Written every boot; operators mirror into proc via booter VFS provider.'
note: 'Written every boot; operators mirror into proc via booter VFS provider. Schema 2 adds bootStageCount/bootStageTail from kernel stage log.'
}) + '\n'
try {
await vfs.writeFile('/run/bare-os/boot-budget-summary.json', b4.from(row))
@@ -4306,7 +4330,8 @@ async function start(ctx) {
bootT0,
bootBudgetViolationsForSummary,
coldExceeded,
stdlibExceeded
stdlibExceeded,
stageLog
)
if (budgetStrict && polStrict && (coldExceeded || stdlibExceeded)) {
bootStructuredLog(
@@ -1,18 +1,6 @@
{
"version": 1,
"bundles": [
{
"path": "/lib/bare/bundles/hypercoreIdEncoding.js",
"keys": [
"hypercoreIdEncoding"
]
},
{
"path": "/lib/bare/bundles/safetyCatch.js",
"keys": [
"safetyCatch"
]
},
{
"path": "/lib/bare/bundles/b4a.js",
"keys": [
@@ -25,6 +13,18 @@
"compactEncoding"
]
},
{
"path": "/lib/bare/bundles/safetyCatch.js",
"keys": [
"safetyCatch"
]
},
{
"path": "/lib/bare/bundles/hypercoreIdEncoding.js",
"keys": [
"hypercoreIdEncoding"
]
},
{
"path": "/lib/bare/bundles/protomux.js",
"keys": [
@@ -43,18 +43,18 @@
"barePath"
]
},
{
"path": "/lib/bare/bundles/bareEvents.js",
"keys": [
"bareEvents"
]
},
{
"path": "/lib/bare/bundles/bareEncoding.js",
"keys": [
"bareEncoding"
]
},
{
"path": "/lib/bare/bundles/bareEvents.js",
"keys": [
"bareEvents"
]
},
{
"path": "/lib/bare/bundles/bareAbort.js",
"keys": [
@@ -91,30 +91,18 @@
"bareCrypto"
]
},
{
"path": "/lib/bare/bundles/bareAsyncHooks.js",
"keys": [
"bareAsyncHooks"
]
},
{
"path": "/lib/bare/bundles/bareAtomics.js",
"keys": [
"bareAtomics"
]
},
{
"path": "/lib/bare/bundles/bareApk.js",
"keys": [
"bareApk"
]
},
{
"path": "/lib/bare/bundles/bareAppKit.js",
"keys": [
"bareAppKit"
]
},
{
"path": "/lib/bare/bundles/bareAsyncHooks.js",
"keys": [
"bareAsyncHooks"
]
},
{
"path": "/lib/bare/bundles/bareAssert.js",
"keys": [
@@ -122,9 +110,15 @@
]
},
{
"path": "/lib/bare/bundles/fetch.js",
"path": "/lib/bare/bundles/bareApk.js",
"keys": [
"fetch"
"bareApk"
]
},
{
"path": "/lib/bare/bundles/bareAtomics.js",
"keys": [
"bareAtomics"
]
},
{
@@ -134,15 +128,9 @@
]
},
{
"path": "/lib/bare/bundles/bareBundleCompile.js",
"path": "/lib/bare/bundles/fetch.js",
"keys": [
"bareBundleCompile"
]
},
{
"path": "/lib/bare/bundles/bareBundle.js",
"keys": [
"bareBundle"
"fetch"
]
},
{
@@ -152,9 +140,9 @@
]
},
{
"path": "/lib/bare/bundles/bareBundleEvaluate.js",
"path": "/lib/bare/bundles/bareBundleCompile.js",
"keys": [
"bareBundleEvaluate"
"bareBundleCompile"
]
},
{
@@ -163,6 +151,24 @@
"bareBluetoothApple"
]
},
{
"path": "/lib/bare/bundles/bareBundle.js",
"keys": [
"bareBundle"
]
},
{
"path": "/lib/bare/bundles/bareBundleEvaluate.js",
"keys": [
"bareBundleEvaluate"
]
},
{
"path": "/lib/bare/bundles/bareConsole.js",
"keys": [
"bareConsole"
]
},
{
"path": "/lib/bare/bundles/bareBoot.js",
"keys": [
@@ -175,12 +181,6 @@
"bareBundleId"
]
},
{
"path": "/lib/bare/bundles/bareConsole.js",
"keys": [
"bareConsole"
]
},
{
"path": "/lib/bare/bundles/bareChannel.js",
"keys": [
@@ -193,12 +193,6 @@
"bareDebugLog"
]
},
{
"path": "/lib/bare/bundles/bareDaemon.js",
"keys": [
"bareDaemon"
]
},
{
"path": "/lib/bare/bundles/bareDelta.js",
"keys": [
@@ -206,9 +200,9 @@
]
},
{
"path": "/lib/bare/bundles/bareDns.js",
"path": "/lib/bare/bundles/bareDaemon.js",
"keys": [
"bareDns"
"bareDaemon"
]
},
{
@@ -217,6 +211,12 @@
"bareDiagnosticsChannel"
]
},
{
"path": "/lib/bare/bundles/bareDns.js",
"keys": [
"bareDns"
]
},
{
"path": "/lib/bare/bundles/bareEnv.js",
"keys": [
@@ -229,18 +229,18 @@
"bareExif"
]
},
{
"path": "/lib/bare/bundles/bareDgram.js",
"keys": [
"bareDgram"
]
},
{
"path": "/lib/bare/bundles/bareFfmpeg.js",
"keys": [
"bareFfmpeg"
]
},
{
"path": "/lib/bare/bundles/bareDgram.js",
"keys": [
"bareDgram"
]
},
{
"path": "/lib/bare/bundles/bareCov.js",
"keys": [
@@ -254,9 +254,9 @@
]
},
{
"path": "/lib/bare/bundles/bareFileLogger.js",
"path": "/lib/bare/bundles/bareFormat.js",
"keys": [
"bareFileLogger"
"bareFormat"
]
},
{
@@ -266,33 +266,9 @@
]
},
{
"path": "/lib/bare/bundles/bareFormat.js",
"path": "/lib/bare/bundles/bareFileLogger.js",
"keys": [
"bareFormat"
]
},
{
"path": "/lib/bare/bundles/bareGif.js",
"keys": [
"bareGif"
]
},
{
"path": "/lib/bare/bundles/bareHeif.js",
"keys": [
"bareHeif"
]
},
{
"path": "/lib/bare/bundles/bareGtk.js",
"keys": [
"bareGtk"
]
},
{
"path": "/lib/bare/bundles/bareHrtime.js",
"keys": [
"bareHrtime"
"bareFileLogger"
]
},
{
@@ -302,15 +278,33 @@
]
},
{
"path": "/lib/bare/bundles/bareHttpParser.js",
"path": "/lib/bare/bundles/bareGif.js",
"keys": [
"bareHttpParser"
"bareGif"
]
},
{
"path": "/lib/bare/bundles/bareIco.js",
"path": "/lib/bare/bundles/bareHrtime.js",
"keys": [
"bareIco"
"bareHrtime"
]
},
{
"path": "/lib/bare/bundles/bareGtk.js",
"keys": [
"bareGtk"
]
},
{
"path": "/lib/bare/bundles/bareHeif.js",
"keys": [
"bareHeif"
]
},
{
"path": "/lib/bare/bundles/bareHttpParser.js",
"keys": [
"bareHttpParser"
]
},
{
@@ -320,9 +314,9 @@
]
},
{
"path": "/lib/bare/bundles/bareInspect.js",
"path": "/lib/bare/bundles/bareIco.js",
"keys": [
"bareInspect"
"bareIco"
]
},
{
@@ -331,6 +325,12 @@
"bareHttp1"
]
},
{
"path": "/lib/bare/bundles/bareInspect.js",
"keys": [
"bareInspect"
]
},
{
"path": "/lib/bare/bundles/bareHttps.js",
"keys": [
@@ -355,12 +355,6 @@
"bareIntl"
]
},
{
"path": "/lib/bare/bundles/bareInspector.js",
"keys": [
"bareInspector"
]
},
{
"path": "/lib/bare/bundles/bareLief.js",
"keys": [
@@ -374,15 +368,15 @@
]
},
{
"path": "/lib/bare/bundles/bareLink.js",
"path": "/lib/bare/bundles/bareInspector.js",
"keys": [
"bareLink"
"bareInspector"
]
},
{
"path": "/lib/bare/bundles/bareModuleLexer.js",
"path": "/lib/bare/bundles/bareLink.js",
"keys": [
"bareModuleLexer"
"bareLink"
]
},
{
@@ -391,6 +385,12 @@
"bareMake"
]
},
{
"path": "/lib/bare/bundles/bareModule.js",
"keys": [
"bareModule"
]
},
{
"path": "/lib/bare/bundles/bareModuleResolve.js",
"keys": [
@@ -398,9 +398,9 @@
]
},
{
"path": "/lib/bare/bundles/bareModule.js",
"path": "/lib/bare/bundles/bareModuleLexer.js",
"keys": [
"bareModule"
"bareModuleLexer"
]
},
{
@@ -415,18 +415,6 @@
"bareModuleTraverse"
]
},
{
"path": "/lib/bare/bundles/bareMedia.js",
"keys": [
"bareMedia"
]
},
{
"path": "/lib/bare/bundles/bareNodeFetch.js",
"keys": [
"bareNodeFetch"
]
},
{
"path": "/lib/bare/bundles/bareNative.js",
"keys": [
@@ -434,15 +422,9 @@
]
},
{
"path": "/lib/bare/bundles/bareOpen.js",
"path": "/lib/bare/bundles/bareMedia.js",
"keys": [
"bareOpen"
]
},
{
"path": "/lib/bare/bundles/bareOs.js",
"keys": [
"bareOs"
"bareMedia"
]
},
{
@@ -452,9 +434,21 @@
]
},
{
"path": "/lib/bare/bundles/barePerformance.js",
"path": "/lib/bare/bundles/bareNodeFetch.js",
"keys": [
"barePerformance"
"bareNodeFetch"
]
},
{
"path": "/lib/bare/bundles/bareOs.js",
"keys": [
"bareOs"
]
},
{
"path": "/lib/bare/bundles/bareOpen.js",
"keys": [
"bareOpen"
]
},
{
@@ -470,9 +464,9 @@
]
},
{
"path": "/lib/bare/bundles/barePipe.js",
"path": "/lib/bare/bundles/barePerformance.js",
"keys": [
"barePipe"
"barePerformance"
]
},
{
@@ -482,15 +476,9 @@
]
},
{
"path": "/lib/bare/bundles/bareNodeRuntime.js",
"path": "/lib/bare/bundles/barePipe.js",
"keys": [
"bareNodeRuntime"
]
},
{
"path": "/lib/bare/bundles/barePunycode.js",
"keys": [
"barePunycode"
"barePipe"
]
},
{
@@ -500,15 +488,9 @@
]
},
{
"path": "/lib/bare/bundles/bareDev.js",
"path": "/lib/bare/bundles/bareNodeRuntime.js",
"keys": [
"bareDev"
]
},
{
"path": "/lib/bare/bundles/bareQuerystring.js",
"keys": [
"bareQuerystring"
"bareNodeRuntime"
]
},
{
@@ -518,9 +500,21 @@
]
},
{
"path": "/lib/bare/bundles/bareQueueMicrotask.js",
"path": "/lib/bare/bundles/bareQuerystring.js",
"keys": [
"bareQueueMicrotask"
"bareQuerystring"
]
},
{
"path": "/lib/bare/bundles/bareDev.js",
"keys": [
"bareDev"
]
},
{
"path": "/lib/bare/bundles/barePunycode.js",
"keys": [
"barePunycode"
]
},
{
@@ -529,6 +523,12 @@
"bareRealm"
]
},
{
"path": "/lib/bare/bundles/bareQueueMicrotask.js",
"keys": [
"bareQueueMicrotask"
]
},
{
"path": "/lib/bare/bundles/bareRuntime.js",
"keys": [
@@ -553,6 +553,12 @@
"bareRepl"
]
},
{
"path": "/lib/bare/bundles/bareSdl.js",
"keys": [
"bareSdl"
]
},
{
"path": "/lib/bare/bundles/bareSemver.js",
"keys": [
@@ -565,30 +571,12 @@
"bareRun"
]
},
{
"path": "/lib/bare/bundles/bareSdl.js",
"keys": [
"bareSdl"
]
},
{
"path": "/lib/bare/bundles/bareSignals.js",
"keys": [
"bareSignals"
]
},
{
"path": "/lib/bare/bundles/bareSidecar.js",
"keys": [
"bareSidecar"
]
},
{
"path": "/lib/bare/bundles/bareStdio.js",
"keys": [
"bareStdio"
]
},
{
"path": "/lib/bare/bundles/bareStringDecoder.js",
"keys": [
@@ -596,9 +584,9 @@
]
},
{
"path": "/lib/bare/bundles/bareStorage.js",
"path": "/lib/bare/bundles/bareSidecar.js",
"keys": [
"bareStorage"
"bareSidecar"
]
},
{
@@ -608,9 +596,15 @@
]
},
{
"path": "/lib/bare/bundles/bareSvg.js",
"path": "/lib/bare/bundles/bareStorage.js",
"keys": [
"bareSvg"
"bareStorage"
]
},
{
"path": "/lib/bare/bundles/bareStdio.js",
"keys": [
"bareStdio"
]
},
{
@@ -620,21 +614,9 @@
]
},
{
"path": "/lib/bare/bundles/bareSubprocess.js",
"path": "/lib/bare/bundles/bareSvg.js",
"keys": [
"bareSubprocess"
]
},
{
"path": "/lib/bare/bundles/bareSystemLogger.js",
"keys": [
"bareSystemLogger"
]
},
{
"path": "/lib/bare/bundles/bareTiff.js",
"keys": [
"bareTiff"
"bareSvg"
]
},
{
@@ -644,9 +626,21 @@
]
},
{
"path": "/lib/bare/bundles/bareTcp.js",
"path": "/lib/bare/bundles/bareSystemLogger.js",
"keys": [
"bareTcp"
"bareSystemLogger"
]
},
{
"path": "/lib/bare/bundles/bareSubprocess.js",
"keys": [
"bareSubprocess"
]
},
{
"path": "/lib/bare/bundles/bareTiff.js",
"keys": [
"bareTiff"
]
},
{
@@ -667,6 +661,12 @@
"bareTpl"
]
},
{
"path": "/lib/bare/bundles/bareTcp.js",
"keys": [
"bareTcp"
]
},
{
"path": "/lib/bare/bundles/bareType.js",
"keys": [
@@ -686,9 +686,9 @@
]
},
{
"path": "/lib/bare/bundles/bareV8.js",
"path": "/lib/bare/bundles/bareTty.js",
"keys": [
"bareV8"
"bareTty"
]
},
{
@@ -698,15 +698,9 @@
]
},
{
"path": "/lib/bare/bundles/bareTty.js",
"path": "/lib/bare/bundles/bareV8.js",
"keys": [
"bareTty"
]
},
{
"path": "/lib/bare/bundles/bareWalkHandles.js",
"keys": [
"bareWalkHandles"
"bareV8"
]
},
{
@@ -715,6 +709,12 @@
"bareVm"
]
},
{
"path": "/lib/bare/bundles/bareWalkHandles.js",
"keys": [
"bareWalkHandles"
]
},
{
"path": "/lib/bare/bundles/bareUnionBundle.js",
"keys": [
@@ -734,9 +734,9 @@
]
},
{
"path": "/lib/bare/bundles/bareUtils.js",
"path": "/lib/bare/bundles/bareV8ToIstanbul.js",
"keys": [
"bareUtils"
"bareV8ToIstanbul"
]
},
{
@@ -745,24 +745,18 @@
"bareWebKitGtk"
]
},
{
"path": "/lib/bare/bundles/bareUtils.js",
"keys": [
"bareUtils"
]
},
{
"path": "/lib/bare/bundles/bareWhich.js",
"keys": [
"bareWhich"
]
},
{
"path": "/lib/bare/bundles/bareV8ToIstanbul.js",
"keys": [
"bareV8ToIstanbul"
]
},
{
"path": "/lib/bare/bundles/bareXdiff.js",
"keys": [
"bareXdiff"
]
},
{
"path": "/lib/bare/bundles/bareWinUi.js",
"keys": [
@@ -770,9 +764,15 @@
]
},
{
"path": "/lib/bare/bundles/bareWs.js",
"path": "/lib/bare/bundles/bareXdiff.js",
"keys": [
"bareWs"
"bareXdiff"
]
},
{
"path": "/lib/bare/bundles/bareZmq.js",
"keys": [
"bareZmq"
]
},
{
@@ -782,9 +782,9 @@
]
},
{
"path": "/lib/bare/bundles/bareZmq.js",
"path": "/lib/bare/bundles/bareWs.js",
"keys": [
"bareZmq"
"bareWs"
]
},
{
@@ -1595,8 +1595,8 @@
],
"bundleProvenance": {
"schemaVersion": 1,
"generatedAt": "2026-04-05T21:35:39.414Z",
"gitCommit": "f3c8ef1fc50e9c895629cfae96cdcfd644903132",
"generatedAt": "2026-04-06T03:01:17.869Z",
"gitCommit": "8f61d0bea8efc674a1063df176d7383a09f85b46",
"nodeVersion": "v22.22.0",
"bundleTier": "all",
"normativeManifest": "packages/bare-os-booter/lib/bare-module-manifest.json",
@@ -1,6 +1,6 @@
{
"schema": 1,
"atMs": 1775439838480,
"atMs": 1775444477009,
"commands": [
"arch",
"awk",
@@ -8,7 +8,7 @@
/**
* @param {{ file: string, extId: string, scripts: string[], dependsOn: string[], signaturePointer?: string }[]} entries
* @returns {{ ok: true, ordered: typeof entries } | { ok: false, cycleExtIds: string[] }}
* @returns {{ ok: true, ordered: typeof entries } | { ok: false, cycleExtIds: string[], cycleEdges: string[] }}
*/
function topologicalOrderKernelExtEntries(entries) {
const sorted = [...entries].sort((a, b) => a.file.localeCompare(b.file))
@@ -50,7 +50,21 @@ function topologicalOrderKernelExtEntries(entries) {
}
if (out.length !== sorted.length) {
const stuck = sorted.filter((e) => !out.includes(e.file))
return { ok: false, cycleExtIds: stuck.map((e) => e.extId) }
const stuckIds = new Set(stuck.map((e) => e.extId))
/** @type {string[]} */
const cycleEdges = []
for (const e of stuck) {
for (const dep of e.dependsOn) {
if (stuckIds.has(dep))
cycleEdges.push(String(e.extId) + ' -> dependsOn:' + String(dep))
}
}
cycleEdges.sort()
return {
ok: false,
cycleExtIds: stuck.map((e) => e.extId),
cycleEdges: cycleEdges.slice(0, 48)
}
}
const byFile = new Map(sorted.map((e) => [e.file, e]))
return {
@@ -622,17 +636,23 @@ async function runKernelExtDropins(ctx, opts = {}) {
/** @type {typeof collected} */
let ordered
if (!topo.ok) {
const edgeHint =
topo.cycleEdges && topo.cycleEdges.length
? '; cycle edges (stuck->dep): ' + topo.cycleEdges.join('; ')
: ''
bootStructuredLog(
ctx,
'error',
'kernelExt.cycle',
'[kernel.ext.d] dependency cycle in extension drop-ins; ext ids: ' +
topo.cycleExtIds.join(', ')
topo.cycleExtIds.join(', ') +
edgeHint
)
if (strictPol) {
await appendKernelExtAuditNdjson(ctx, {
event: 'dependency_cycle',
cycleExtIds: [...topo.cycleExtIds].sort()
cycleExtIds: [...topo.cycleExtIds].sort(),
cycleEdges: topo.cycleEdges || []
})
return false
}
@@ -673,6 +693,7 @@ async function runKernelExtDropins(ctx, opts = {}) {
? {
kind: 'dependency_cycle',
cycleExtIds: cycleSorted,
cycleEdges: topo.cycleEdges || [],
provenance: 'kernel.ext.d topological sort (init-main)'
}
: null,
@@ -1622,24 +1622,21 @@ async function runKernelSelftest(ctx) {
return true
}
/**
* @param {Record<string, unknown>} ctx
* @param {number} bootT0
* @param {string[]} stageLog
*/
/**
* @param {Record<string, unknown>} ctx
* @param {number} bootT0
* @param {{ kind: string, code: string, wallMs: number, limitMs: number }[]} violations
* @param {boolean} coldExceeded
* @param {boolean} stdlibExceeded
* @param {string[]} [stageLog]
*/
async function maybeWriteBootBudgetSummaryJson(
ctx,
bootT0,
violations,
coldExceeded,
stdlibExceeded
stdlibExceeded,
stageLog
) {
const vfs = ctx.vfs
const b4 = ctx.b4a
@@ -1647,15 +1644,21 @@ async function maybeWriteBootBudgetSummaryJson(
const wall = Date.now() - bootT0
const row =
JSON.stringify({
schema: 1,
schema: 2,
atMs: Date.now(),
coldWallMs: wall,
coldExceeded,
bareStdlibExceeded: stdlibExceeded,
violationCodes: violations.map((v) => v.code).filter(Boolean),
violations,
bootStageCount:
Array.isArray(stageLog) && stageLog.length ? stageLog.length : null,
bootStageTail:
Array.isArray(stageLog) && stageLog.length
? stageLog.slice(-16)
: undefined,
procHint: '/proc/bare_os/boot_budget_summary.json',
note: 'Written every boot; operators mirror into proc via booter VFS provider.'
note: 'Written every boot; operators mirror into proc via booter VFS provider. Schema 2 adds bootStageCount/bootStageTail from kernel stage log.'
}) + '\n'
try {
await vfs.writeFile('/run/bare-os/boot-budget-summary.json', b4.from(row))
@@ -2181,7 +2184,8 @@ async function start(ctx) {
bootT0,
bootBudgetViolationsForSummary,
coldExceeded,
stdlibExceeded
stdlibExceeded,
stageLog
)
if (budgetStrict && polStrict && (coldExceeded || stdlibExceeded)) {
bootStructuredLog(
File diff suppressed because one or more lines are too long