/** * Append-only UTF-8 logs under logical `/var/log/…` (VFS maps to the personal Hyperdrive). * * Until the first successful `mkdir`/`writeFile`, `readdir('/var/log')` may be empty even though * `lstat('/var/log')` looks like a directory — logs live under `/var/log/bare-os/` on the logical * tree and on the personal drive under `/.bare-os/…/var/log//` (see `vfs.js` routing). * * Failures are best-effort silent unless **`BARE_OS_VAR_LOG_DEBUG=1`** (or `true`): then messages * go to **`process.stderr`** and **`ctx.console.error`**. Kernel metrics: `varlog.append.*`, * `varlog.ensure.*` (see `bareOsKernelMetricsSnapshot()`). */ import { BARE_OS_LIFECYCLE_SCHEMA_VERSION, BARE_OS_TELEMETRY_SCHEMA_VERSION } from './bare-os-lifecycle-schema.js' import { bareOsKernelMetricInc } from './bare-os-kernel-metrics.js' /** * @param {Record} ctx * @param {string} msg */ function varLogDebug(ctx, msg) { try { const env = ctx && ctx.env && typeof ctx.env === 'object' ? /** @type {Record} */ (ctx.env) : /** @type {Record} */ ({}) const on = env.BARE_OS_VAR_LOG_DEBUG === '1' || env.BARE_OS_VAR_LOG_DEBUG === 'true' if (!on) return const line = `[bare-os:var-log] ${msg}` try { globalThis.process?.stderr?.write?.(line + '\n') } catch { /* ignore */ } try { ctx?.console?.error?.(line) } catch { /* ignore */ } } catch { /* ignore */ } } 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` export const BOOT_LOG = `${BARE_OS_VAR_LOG_DIR}/boot.log` export const CLOSE_JOURNAL_LOG = `${BARE_OS_VAR_LOG_DIR}/close-journal.ndjson` export const CRASH_LOG = `${BARE_OS_VAR_LOG_DIR}/crash.ndjson` /** 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//). kernel-console.log — console.log / console.error from the kernel session cron.log — bare-cron job errors openssh.log — bare-openssh / sshd listen and auth errors holesail.log — bare-holesail / early kernel-path Holesail (AGPL-3.0 upstream) www.log — bare-os-www static HTTP for ~/.www initd.log — bare-initd service start failures audit.log — optional execLine audit when BARE_OS_AUDIT=1 boot.log — boot-to-login timeline milestones (best-effort) ` /** * 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, '') .replace(/\bBearer\s+\S+/gi, 'Bearer ') .replace(/\bsk-[a-zA-Z0-9]{16,}\b/g, '') .replace(/\b(password|secret|token|apikey|api_key)\s*[:=]\s*\S+/gi, '$1=') .replace(/\b[A-Z0-9_]*(TOKEN|SECRET|PASSWORD|APIKEY)[A-Z0-9_]*=\S+/gi, '') } /** * @param {Record} rec */ function bareOsTelemetrySanitizeRec(rec) { if (!rec || typeof rec !== 'object') return {} /** @type {Record} */ 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 const TELEMETRY_LOG_CAP_BYTES = 2 * 1024 * 1024 /** * @param {Record} ctx * @param {string} path * @param {Uint8Array} chunk * @param {number} capBytes */ async function appendLineWithOptionalCap(ctx, path, chunk, capBytes) { const vfs = ctx.vfs if (!vfs) return if (typeof vfs.appendFile === 'function') { await vfs.appendFile(path, chunk) return } if (typeof vfs.readFile !== 'function' || typeof vfs.writeFile !== 'function') return const prev = await vfs.readFile(path) let merged = prev ? ctx.b4a.concat([prev, chunk]) : chunk if (merged.length > capBytes) { merged = merged.subarray(merged.length - capBytes) } await vfs.writeFile(path, merged) } /** * Ensure `/var/log/bare-os` exists and a short README is present. Best-effort; never throws. * @param {Record} ctx */ export async function ensureBareOsVarLogTree(ctx) { try { const vfs = ctx.vfs if (!vfs || typeof vfs.mkdir !== 'function') { bareOsKernelMetricInc('varlog.ensure.skip_no_vfs_mkdir') varLogDebug(ctx, 'ensureBareOsVarLogTree: no ctx.vfs.mkdir') return } await vfs.mkdir(BARE_OS_VAR_LOG_DIR, { recursive: true }) if ( typeof vfs.readFile !== 'function' || typeof vfs.writeFile !== 'function' ) { bareOsKernelMetricInc('varlog.ensure.skip_no_vfs_rw') varLogDebug(ctx, 'ensureBareOsVarLogTree: missing vfs.readFile/writeFile') 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)) const env = ctx && typeof ctx.env === 'object' ? ctx.env : {} const unsafe = (String(env.BARE_OS_PEER_ALLOW_ALL || '') === '1') || (String(env.BARE_OS_DELEGATE_ALLOW || '').trim() === '') || (String(env.BARE_OS_PATH_CAPABILITY_REQUIRE_TRUSTED_SIGNER || '').trim() === '') if (unsafe) { await appendVarLog( ctx, BOOT_LOG, 'security', 'unsafe posture detected: review peer allow-all, delegate allowlist, and trusted signer enforcement' ) } } catch (e) { bareOsKernelMetricInc('varlog.ensure.error') varLogDebug( ctx, `ensureBareOsVarLogTree: ${e && e.message ? e.message : String(e)}` ) } } /** * Append one UTF-8 line to a logical log file under `/var/log/…`. Best-effort; never throws. * @param {Record} 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') { bareOsKernelMetricInc('varlog.append.skip_no_vfs_readfile') varLogDebug( ctx, 'appendVarLog: no vfs.readFile for ' + String(logicalFilePath) ) return } const ts = new Date().toISOString() const chunk = ctx.b4a.from(`[${ts}] [${kind}] ${line}\n`) const fastAppend = (ctx.env?.BARE_OS_VAR_LOG_FAST_APPEND === '1' || ctx.env?.BARE_OS_VAR_LOG_FAST_APPEND === 'true') && typeof vfs.appendFile === 'function' if (fastAppend) { await appendLineWithOptionalCap(ctx, logicalFilePath, chunk, LOG_MAX_BYTES) 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) }) return } const prev = await vfs.readFile(logicalFilePath) 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 (e) { bareOsKernelMetricInc('varlog.append.error') varLogDebug( ctx, `appendVarLog(${String(logicalFilePath)}): ${ e && e.message ? e.message : String(e) }` ) } } /** * @param {Record} ctx * @param {{ resource: string, phase: string, ok: boolean, durationMs?: number, retries?: number, error?: string }} row */ export async function appendBareOsCloseJournal(ctx, row) { await appendVarLog( ctx, CLOSE_JOURNAL_LOG, 'close', JSON.stringify({ schema: 1, atMs: Date.now(), resource: String(row.resource || '').slice(0, 128), phase: String(row.phase || '').slice(0, 64), ok: row.ok === true, durationMs: typeof row.durationMs === 'number' && Number.isFinite(row.durationMs) ? Math.max(0, Math.floor(row.durationMs)) : undefined, retries: typeof row.retries === 'number' && Number.isFinite(row.retries) ? Math.max(0, Math.floor(row.retries)) : undefined, error: row.error ? String(row.error).slice(0, 512) : undefined }) ) } /** * @param {Record} ctx * @param {Record} crash */ export async function appendBareOsCrashEvent(ctx, crash) { const safe = bareOsTelemetrySanitizeRec(crash || {}) await appendVarLog( ctx, CRASH_LOG, 'crash', JSON.stringify({ schema: 1, atMs: Date.now(), ...safe }) ) } /** * @param {Record} ctx * @param {Record} rec */ /** @internal Mirrored telemetry sink; exported for structured host events (e.g. swarm chat). */ export async function mirrorBareOsTelemetryNdjson(ctx, rec) { try { const env = /** @type {Record} */ ( 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) 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} */ (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) await appendLineWithOptionalCap(ctx, dest, chunk, TELEMETRY_LOG_CAP_BYTES) } catch { /* ignore */ } } /** * Optional OpenTelemetry-inspired JSON line export (`BARE_OS_TELEMETRY_OTEL_JSONL` logical path). * @param {Record} ctx * @param {string} eventName * @param {Record} attrs */ async function mirrorBareOsOtelJsonl(ctx, eventName, attrs) { try { const env = /** @type {Record} */ ( 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) return const safeAttrs = bareOsTelemetrySanitizeRec( /** @type {Record} */ (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) await appendLineWithOptionalCap(ctx, dest, chunk, TELEMETRY_LOG_CAP_BYTES) } catch { /* ignore */ } }