- Align posix-conformance-matrix bareOsSyscallOps with getconf BARE_OS_SYSCALL_OPS - Add verify-boot-policy-extension-signer-pins.mjs to pretest; document in scripts/README - Document extensionSignerPinsV2–V5 env wiring in OTA_AND_BUNDLES; vault multisig sketch - Note telemetry NDJSON redaction limits in environment appendix - Expand booter CHANGELOG 1.40.0 (boot-perf, shell POSIX mode, subprocess errors, etc.) - Holepunch sync README: NDJSON summary path Seeder/kernel parity and pretest already green for bundled changes.
262 lines
9.2 KiB
JavaScript
262 lines
9.2 KiB
JavaScript
/**
|
|
* Append-only UTF-8 logs under logical `/var/log/…` (VFS maps to the personal Hyperdrive).
|
|
*/
|
|
|
|
import {
|
|
BARE_OS_LIFECYCLE_SCHEMA_VERSION,
|
|
BARE_OS_TELEMETRY_SCHEMA_VERSION
|
|
} from './bare-os-lifecycle-schema.js'
|
|
|
|
export const BARE_OS_VAR_LOG_DIR = '/var/log/bare-os'
|
|
|
|
export const KERNEL_CONSOLE_LOG = `${BARE_OS_VAR_LOG_DIR}/kernel-console.log`
|
|
|
|
export const CRON_LOG = `${BARE_OS_VAR_LOG_DIR}/cron.log`
|
|
|
|
export const INITD_LOG = `${BARE_OS_VAR_LOG_DIR}/initd.log`
|
|
|
|
export const AUDIT_LOG = `${BARE_OS_VAR_LOG_DIR}/audit.log`
|
|
|
|
/** Structured JSON lines from `/bin/logger` (syslog-style metadata). */
|
|
export const LOGGER_JSON_LOG = `${BARE_OS_VAR_LOG_DIR}/logger.jsonl`
|
|
|
|
const README_REL = `${BARE_OS_VAR_LOG_DIR}/README`
|
|
|
|
const README_TEXT = `Bare OS session logs (mirrored on your personal drive under /.bare-os/var/log/<HOME-basename>/).
|
|
kernel-console.log — console.log / console.error from the kernel session
|
|
cron.log — bare-cron job errors
|
|
initd.log — bare-initd service start failures
|
|
audit.log — optional execLine audit when BARE_OS_AUDIT=1
|
|
`
|
|
|
|
/**
|
|
* Strip long hex / obvious secret-shaped tokens from telemetry mirrors (best-effort).
|
|
* @param {string} s
|
|
*/
|
|
function bareOsTelemetryRedactString(s) {
|
|
return String(s)
|
|
.replace(/\b[0-9a-f]{128,}\b/gi, '<redacted_hex>')
|
|
.replace(/\bBearer\s+\S+/gi, 'Bearer <redacted>')
|
|
.replace(/\bsk-[a-zA-Z0-9]{16,}\b/g, '<redacted_sk>')
|
|
}
|
|
|
|
/**
|
|
* @param {Record<string, unknown>} rec
|
|
*/
|
|
function bareOsTelemetrySanitizeRec(rec) {
|
|
if (!rec || typeof rec !== 'object') return {}
|
|
/** @type {Record<string, unknown>} */
|
|
const o = {}
|
|
for (const [k, v] of Object.entries(rec)) {
|
|
if (typeof v === 'string') o[k] = bareOsTelemetryRedactString(v)
|
|
else o[k] = v
|
|
}
|
|
return o
|
|
}
|
|
|
|
/** Max size before trimming older log bytes (best-effort). */
|
|
const LOG_MAX_BYTES = 512 * 1024
|
|
|
|
/** After trim, keep this many trailing bytes plus a notice line. */
|
|
const LOG_KEEP_BYTES = 256 * 1024
|
|
|
|
/**
|
|
* Ensure `/var/log/bare-os` exists and a short README is present. Best-effort; never throws.
|
|
* @param {Record<string, unknown>} ctx
|
|
*/
|
|
export async function ensureBareOsVarLogTree(ctx) {
|
|
try {
|
|
const vfs = ctx.vfs
|
|
if (!vfs || typeof vfs.mkdir !== 'function') return
|
|
await vfs.mkdir(BARE_OS_VAR_LOG_DIR, { recursive: true })
|
|
if (
|
|
typeof vfs.readFile !== 'function' ||
|
|
typeof vfs.writeFile !== 'function'
|
|
)
|
|
return
|
|
const existing = await vfs.readFile(README_REL)
|
|
if (existing && ctx.b4a.from(existing).length > 0) return
|
|
await vfs.writeFile(README_REL, ctx.b4a.from(README_TEXT))
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Append one UTF-8 line to a logical log file under `/var/log/…`. Best-effort; never throws.
|
|
* @param {Record<string, unknown>} ctx
|
|
* @param {string} logicalFilePath e.g. `/var/log/bare-os/cron.log`
|
|
* @param {string} kind
|
|
* @param {string} line
|
|
*/
|
|
export async function appendVarLog(ctx, logicalFilePath, kind, line) {
|
|
try {
|
|
const vfs = ctx.vfs
|
|
if (!vfs || typeof vfs.readFile !== 'function') return
|
|
const prev = await vfs.readFile(logicalFilePath)
|
|
const ts = new Date().toISOString()
|
|
const chunk = ctx.b4a.from(`[${ts}] [${kind}] ${line}\n`)
|
|
let merged = prev ? ctx.b4a.concat([prev, chunk]) : chunk
|
|
if (merged.length > LOG_MAX_BYTES) {
|
|
const start = Math.max(0, merged.length - LOG_KEEP_BYTES)
|
|
const tail = merged.subarray(start)
|
|
const notice = ctx.b4a.from(
|
|
`[${ts}] [bare-os] log truncated (kept last ${LOG_KEEP_BYTES} bytes): ${logicalFilePath}\n`
|
|
)
|
|
merged = ctx.b4a.concat([notice, tail])
|
|
}
|
|
await vfs.writeFile(logicalFilePath, merged)
|
|
await mirrorBareOsTelemetryNdjson(ctx, {
|
|
type: 'varLog',
|
|
path: logicalFilePath,
|
|
kind,
|
|
line: String(line).slice(0, 4000)
|
|
})
|
|
await mirrorBareOsOtelJsonl(ctx, 'varLog', {
|
|
path: logicalFilePath,
|
|
kind,
|
|
line: String(line).slice(0, 4000)
|
|
})
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Record<string, unknown>} ctx
|
|
* @param {Record<string, unknown>} rec
|
|
*/
|
|
async function mirrorBareOsTelemetryNdjson(ctx, rec) {
|
|
try {
|
|
const env = /** @type {Record<string, string>} */ (
|
|
ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
|
)
|
|
const raw = env.BARE_OS_TELEMETRY_NDJSON
|
|
if (raw == null || raw === '' || raw === '0' || raw === 'false') return
|
|
const dest = String(raw).trim()
|
|
if (!dest.startsWith('/') && !dest.startsWith('~/')) return
|
|
const vfs = ctx.vfs
|
|
if (!vfs || typeof vfs.readFile !== 'function') return
|
|
const lineage = String(env.BARE_OS_TELEMETRY_SESSION_LINEAGE_ID || '').trim()
|
|
const bootAttemptId = String(env.BARE_OS_BOOT_ATTEMPT_ID || '').trim()
|
|
const bareModuleCryptoStagingProbeId = String(env.BARE_OS_PROBE_ID_BARE_MODULE_CRYPTO_STAGING || '').trim()
|
|
const pearInspectLoggerTlsProbeId = String(env.BARE_OS_PROBE_ID_PEAR_INSPECT_LOGGER_TLS || '').trim()
|
|
const hypercorePackHrpcLifecycleProbeId = String(env.BARE_OS_PROBE_ID_HYPERCORE_PACK_HRPC_LIFECYCLE || '').trim()
|
|
const traceId = String(env.BARE_OS_TRACE_ID || '').trim()
|
|
const safeRec = bareOsTelemetrySanitizeRec(
|
|
/** @type {Record<string, unknown>} */ (rec)
|
|
)
|
|
const line =
|
|
JSON.stringify({
|
|
telemetrySchemaVersion: BARE_OS_TELEMETRY_SCHEMA_VERSION,
|
|
lifecycleSchemaVersion: BARE_OS_LIFECYCLE_SCHEMA_VERSION,
|
|
ts: Date.now(),
|
|
traceId: traceId ? traceId.slice(0, 128) : undefined,
|
|
bootAttemptId: bootAttemptId ? bootAttemptId.slice(0, 128) : undefined,
|
|
bareModuleCryptoStagingProbeId: bareModuleCryptoStagingProbeId ? bareModuleCryptoStagingProbeId.slice(0, 128) : undefined,
|
|
pearInspectLoggerTlsProbeId: pearInspectLoggerTlsProbeId ? pearInspectLoggerTlsProbeId.slice(0, 128) : undefined,
|
|
hypercorePackHrpcLifecycleProbeId: hypercorePackHrpcLifecycleProbeId ? hypercorePackHrpcLifecycleProbeId.slice(0, 128) : undefined,
|
|
bareRpcProbeClass: String(
|
|
env.BARE_OS_BARE_RPC_PROBE_CLASS || ''
|
|
)
|
|
.trim()
|
|
.slice(0, 64) || undefined,
|
|
bareModuleProbeClass: String(
|
|
env.BARE_OS_BARE_MODULE_PROBE_CLASS || ''
|
|
).trim().slice(0, 64) || undefined,
|
|
sessionLineageId: lineage ? lineage.slice(0, 128) : undefined,
|
|
hdmsPairingBackoffCount: (() => {
|
|
const n = Number.parseInt(
|
|
env.BARE_OS_HDMS_PAIRING_BACKOFF_COUNT || '',
|
|
10
|
|
)
|
|
return Number.isFinite(n) ? Math.min(1e6, n) : undefined
|
|
})(),
|
|
...safeRec
|
|
}) + '\n'
|
|
const chunk = ctx.b4a.from(line)
|
|
const prev = await vfs.readFile(dest)
|
|
let merged = prev ? ctx.b4a.concat([prev, chunk]) : chunk
|
|
const cap = 2 * 1024 * 1024
|
|
if (merged.length > cap) {
|
|
merged = merged.subarray(merged.length - cap)
|
|
}
|
|
await vfs.writeFile(dest, merged)
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Optional OpenTelemetry-inspired JSON line export (`BARE_OS_TELEMETRY_OTEL_JSONL` logical path).
|
|
* @param {Record<string, unknown>} ctx
|
|
* @param {string} eventName
|
|
* @param {Record<string, unknown>} attrs
|
|
*/
|
|
async function mirrorBareOsOtelJsonl(ctx, eventName, attrs) {
|
|
try {
|
|
const env = /** @type {Record<string, string>} */ (
|
|
ctx.env && typeof ctx.env === 'object' ? ctx.env : {}
|
|
)
|
|
const raw = env.BARE_OS_TELEMETRY_OTEL_JSONL
|
|
if (raw == null || raw === '' || raw === '0' || raw === 'false') return
|
|
const dest = String(raw).trim()
|
|
if (!dest.startsWith('/') && !dest.startsWith('~/')) return
|
|
const vfs = ctx.vfs
|
|
if (!vfs || typeof vfs.readFile !== 'function') return
|
|
const safeAttrs = bareOsTelemetrySanitizeRec(
|
|
/** @type {Record<string, unknown>} */ (attrs)
|
|
)
|
|
const line =
|
|
JSON.stringify({
|
|
otlSchemaVersion: 8,
|
|
resourceLogs: [
|
|
{
|
|
resource: {
|
|
attributes: [
|
|
{ key: 'service.name', value: { stringValue: 'bare-os' } },
|
|
{
|
|
key: 'telemetry.sdk.language',
|
|
value: { stringValue: 'javascript' }
|
|
}
|
|
]
|
|
},
|
|
scopeLogs: [
|
|
{
|
|
scope: { name: 'bare-os-booter', version: '1.0.0' },
|
|
logRecords: [
|
|
{
|
|
timeUnixNano: String(Date.now() * 1e6),
|
|
severityNumber: 9,
|
|
severityText: 'INFO',
|
|
body: { stringValue: eventName },
|
|
events: [],
|
|
links: [],
|
|
exemplars: [],
|
|
attributes: Object.entries(safeAttrs).map(([k, v]) => ({
|
|
key: k,
|
|
value:
|
|
typeof v === 'string'
|
|
? { stringValue: v }
|
|
: { stringValue: JSON.stringify(v) }
|
|
}))
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}) + '\n'
|
|
const chunk = ctx.b4a.from(line)
|
|
const prev = await vfs.readFile(dest)
|
|
let merged = prev ? ctx.b4a.concat([prev, chunk]) : chunk
|
|
const cap = 2 * 1024 * 1024
|
|
if (merged.length > cap) {
|
|
merged = merged.subarray(merged.length - cap)
|
|
}
|
|
await vfs.writeFile(dest, merged)
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
}
|