This commit is contained in:
2026-08-18 18:11:28 -04:00
parent 0e9a650fa2
commit bbaf47028f
259 changed files with 0 additions and 0 deletions
@@ -0,0 +1,250 @@
/**
* Load vendored `bare-discord-js` onto `ctx.bare.discordJS`.
*
* discord.js is CJS and remaps Node builtins through `bare-node-runtime` on Bare.
* Do not ESM-import the package from the booter (its ESM entry uses `node:module`).
*/
import { readFileSync } from '#host-fs'
import { tryRequireFromBooter } from './bare-os-ctx-bare.js'
import { bareOsHostBooterWarn } from './bare-os-host-booter-log.js'
import { takePackedDiscordJs } from './bare-os-ctx-discord-registry.js'
/** @type {Record<string, unknown> | undefined} */
let discordJsCache
let discordJsTried = false
/** @type {string} */
let discordJsLastError = ''
/** Last load failure (empty when unused or successful). */
export function getBareDiscordJsLoadError() {
return discordJsLastError
}
function noteDiscordLoadError(err) {
const msg =
err && typeof err === 'object' && 'message' in err
? String(/** @type {{ message?: unknown }} */ (err).message || err)
: String(err || 'unknown error')
discordJsLastError = msg.slice(0, 800)
return discordJsLastError
}
function bareRuntime() {
if (typeof globalThis.Bare !== 'undefined') return true
const v = globalThis.process?.versions
return Boolean(v && typeof v.bare === 'string')
}
/**
* Discord rejects tokens with trailing newlines, BOM, or copy-paste invisibles.
* @param {unknown} raw
* @returns {string}
*/
export function normalizeDiscordToken(raw) {
if (typeof raw !== 'string') return ''
let t = raw.trim()
if (t.charCodeAt(0) === 0xfeff) t = t.slice(1)
t = t.replace(/[\u200B-\u200D\uFEFF]/g, '')
return t.trim()
}
/**
* @param {string} line
* @returns {{ key: string, value: string } | null}
*/
export function parseDotEnvLine(line) {
let s = String(line)
.replace(/^\uFEFF/, '')
.trim()
if (!s || s.startsWith('#')) return null
if (s.startsWith('export ')) s = s.slice(7).trim()
const eq = s.indexOf('=')
if (eq === -1) return null
const key = s.slice(0, eq).trim()
if (!key) return null
let val = s.slice(eq + 1).trim()
if (
(val.startsWith('"') && val.endsWith('"')) ||
(val.startsWith("'") && val.endsWith("'"))
) {
val = val.slice(1, -1)
}
return { key, value: val }
}
/**
* @param {string} text
* @returns {Record<string, string>}
*/
export function parseDotEnvText(text) {
/** @type {Record<string, string>} */
const out = {}
if (typeof text !== 'string' || !text) return out
let body = text
if (body.charCodeAt(0) === 0xfeff) body = body.slice(1)
for (const line of body.split(/\r?\n/)) {
const parsed = parseDotEnvLine(line)
if (!parsed) continue
out[parsed.key] = parsed.value
}
return out
}
/**
* Copy host Discord token / env-file settings into the guest session.
* When `DISCORD_ENV_FILE` (or `BARE_OS_DISCORD_ENV_FILE`) is a **host** path,
* read it here so the guest does not need host filesystem access.
*
* @param {Record<string, string | undefined> | NodeJS.ProcessEnv | null | undefined} hostEnv
* @param {Record<string, string>} shellEnv
*/
export function applyDiscordHostEnvToShellEnv(hostEnv, shellEnv) {
if (!hostEnv || !shellEnv) return
for (const k of [
'BARE_OS_DISCORD',
'DISCORD_TOKEN',
'DISCORD_GUILD_ID',
'DISCORD_ID_WHITELIST',
'DISCORD_USER_INSTALL',
'DISCORD_ENV_FILE',
'BARE_OS_DISCORD_ENV_FILE'
]) {
const v = hostEnv[k]
if (v != null && String(v).trim()) shellEnv[k] = String(v)
}
const envFile = String(
hostEnv.DISCORD_ENV_FILE || hostEnv.BARE_OS_DISCORD_ENV_FILE || ''
).trim()
if (!envFile) return
try {
const parsed = parseDotEnvText(readFileSync(envFile, 'utf8'))
const token = normalizeDiscordToken(parsed.DISCORD_TOKEN || '')
if (token && !String(shellEnv.DISCORD_TOKEN || '').trim()) {
shellEnv.DISCORD_TOKEN = token
}
const guild = String(parsed.DISCORD_GUILD_ID || '').trim()
if (guild && !String(shellEnv.DISCORD_GUILD_ID || '').trim()) {
shellEnv.DISCORD_GUILD_ID = guild
}
const whitelist = String(parsed.DISCORD_ID_WHITELIST || '').trim()
if (whitelist && !String(shellEnv.DISCORD_ID_WHITELIST || '').trim()) {
shellEnv.DISCORD_ID_WHITELIST = whitelist
}
const userInstall = String(
parsed.DISCORD_USER_INSTALL || parsed.BARE_OS_DISCORD_USER_INSTALL || ''
).trim()
if (userInstall && !String(shellEnv.DISCORD_USER_INSTALL || '').trim()) {
shellEnv.DISCORD_USER_INSTALL = userInstall
}
} catch {
// Guest may still read the same path from the VFS (`--env`).
}
}
function unwrapDiscordModule(mod) {
if (!mod || typeof mod !== 'object') return undefined
const direct = /** @type {Record<string, unknown>} */ (mod)
if (typeof direct.Client === 'function') return direct
const def = direct.default
if (def && typeof def === 'object' && typeof def.Client === 'function') {
return /** @type {Record<string, unknown>} */ (def)
}
return undefined
}
function applyBareProcessEmitWarning() {
const proc = globalThis.process
if (!proc || typeof proc.emitWarning === 'function') return
proc.emitWarning = function emitWarning(warning, type, code) {
const msg = warning instanceof Error ? warning.message : String(warning)
const name = typeof type === 'string' ? type : 'Warning'
const id = typeof code === 'string' ? code : ''
const line = id ? name + ' [' + id + ']: ' + msg : name + ': ' + msg
try {
if (typeof proc.emit === 'function') proc.emit('warning', warning)
} catch {
/* ignore */
}
try {
console.error(line)
} catch {
/* ignore */
}
}
}
function applyBareTlsCompat() {
const proc = globalThis.process
if (!proc || !proc.env) return
if (!proc.env.NODE_TLS_REJECT_UNAUTHORIZED) {
proc.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
}
try {
const req = globalThis.require
if (typeof req !== 'function') return
const bareHttps = req('bare-https')
if (!bareHttps || typeof bareHttps.Agent !== 'function') return
const insecureAgent = new bareHttps.Agent({ rejectUnauthorized: false })
bareHttps.globalAgent = insecureAgent
if (bareHttps.Agent) bareHttps.Agent.global = insecureAgent
} catch {
/* optional */
}
}
/**
* @param {Record<string, unknown> | null} [_ctx]
* @returns {Promise<Record<string, unknown> | undefined>}
*/
export async function loadBareDiscordJs(_ctx) {
void _ctx
if (discordJsTried) return discordJsCache
discordJsTried = true
discordJsLastError = ''
if (bareRuntime()) applyBareTlsCompat()
applyBareProcessEmitWarning()
/** @type {unknown} */
let lastErr = null
try {
const packed = unwrapDiscordModule(takePackedDiscordJs())
if (packed) {
discordJsCache = packed
return discordJsCache
}
} catch (err) {
lastErr = err
}
try {
const mod = await tryRequireFromBooter('bare-discord-js')
const discord = unwrapDiscordModule(mod)
if (discord) {
discordJsCache = discord
return discordJsCache
}
if (!lastErr) {
lastErr = new Error(
String(import.meta.url || '').startsWith('bare:')
? 'standalone pack did not register discord.js (bare-os-ctx-discord-packed.js)'
: 'require("bare-discord-js") returned no Client'
)
}
} catch (err) {
lastErr = err
}
const detail = noteDiscordLoadError(
lastErr || 'vendored bare-discord-js unresolved'
)
bareOsHostBooterWarn(
'ctx_bare_discord_js_load_failed',
'ctx.bare.discordJS: failed to load vendored bare-discord-js',
detail
)
discordJsCache = undefined
return undefined
}
@@ -0,0 +1,23 @@
/**
* SSH PTY console newline helpers (no bare-ssh2 import — safe for brittle-node tests).
*/
/**
* Normalize embedded newlines to CRLF for SSH PTY output (multiline `console.log`, e.g. `man`).
* @param {unknown} s
* @returns {string}
*/
export function bareOpensshFormatConsoleTextForPty(s) {
return String(s).replace(/\r?\n/g, '\r\n')
}
/**
* @param {import('stream').Duplex} stream
* @param {unknown[]} args
*/
export function bareOpensshWritePtyConsoleLine(stream, args) {
const body = args.map(String).join(' ')
const normalized = bareOpensshFormatConsoleTextForPty(body)
const out = normalized.endsWith('\r\n') ? normalized : normalized + '\r\n'
if (stream.writable) stream.write(out)
}
@@ -0,0 +1,584 @@
/**
* Minimal SFTP over Bare OS VFS (bare-ssh2 SFTP server).
* Readable paths match interactive shell / VFS (home, /mnt, system image, pseudo, …).
* Writes are denied on typical read-only system prefixes; other paths defer to VFS ACLs.
*/
import {
S_IFDIR,
S_IFLNK,
S_IFREG,
formatLsMtime,
formatModeString
} from './vfs-posix-meta.js'
/**
* @typedef {{ type: string, mode?: number, size?: number, uid?: number, gid?: number, user?: string, group?: string, mtimeMs?: number }} VfsStatRow
*/
/**
* Logical paths visible over SFTP (parity with shell `createVfs` routing).
* @param {string} abs resolved absolute logical path
* @param {string} homeLogical e.g. /home/guest
*/
export function bareOsSftpAllowedLogicalPath(abs, homeLogical) {
const h = String(homeLogical || '/').replace(/\/+$/, '') || '/'
const a = String(abs || '').replace(/\/+/g, '/') || '/'
if (a === h || a.startsWith(h + '/')) return true
/** @type {string[]} */
const roots = [
'/mnt',
'/media',
'/mount',
'/mirror',
'/bin',
'/boot',
'/lib',
'/etc',
'/usr',
'/var',
'/proc',
'/sys',
'/dev',
'/run',
'/tmp',
'/snapshots',
'/root',
'/home',
'/opt'
]
for (const pre of roots) {
if (a === pre || a.startsWith(pre + '/')) return true
}
return false
}
/**
* Deny OPEN when the client requests mutating access on typical system-image paths.
* Personal home, /tmp, /var, /root, and /mnt are left to VFS (mounts may be writable).
* @param {string} abs
* @param {string} homeLogical
* @param {number} flags SFTP OPEN_MODE bitmask
* @param {{ READ: number, WRITE: number, APPEND: number, CREAT: number, TRUNC: number }} OPEN_MODE
*/
export function bareOsSftpDeniesWriteOpen(abs, homeLogical, flags, OPEN_MODE) {
const h = String(homeLogical || '/').replace(/\/+$/, '') || '/'
const a = String(abs || '').replace(/\/+/g, '/') || '/'
const wantMutate =
!!(flags & OPEN_MODE.WRITE) ||
!!(flags & OPEN_MODE.APPEND) ||
!!(flags & OPEN_MODE.CREAT) ||
!!(flags & OPEN_MODE.TRUNC)
if (!wantMutate) return false
if (a === h || a.startsWith(h + '/')) return false
if (a === '/tmp' || a.startsWith('/tmp/')) return false
if (a === '/var' || a.startsWith('/var/')) return false
if (a === '/root' || a.startsWith('/root/')) return false
const readOnlyRoots = [
'/bin',
'/boot',
'/lib',
'/etc',
'/usr',
'/proc',
'/sys',
'/dev',
'/run',
'/snapshots',
'/opt',
'/mirror'
]
for (const pre of readOnlyRoots) {
if (a === pre || a.startsWith(pre + '/')) return true
}
return false
}
/**
* @param {VfsStatRow} st
* @param {number} [sizeOverride]
*/
function vfsStatToSftpAttrs(st, sizeOverride) {
const t = st.type
const isDir = t === 'directory'
const isLink = t === 'symlink'
const mode =
(isDir ? S_IFDIR : isLink ? S_IFLNK : S_IFREG) |
((typeof st.mode === 'number' ? st.mode : 0) & 0o777)
const sz =
sizeOverride !== undefined && sizeOverride !== null
? Number(sizeOverride)
: Number(st.size || 0)
const mtimeMs =
typeof st.mtimeMs === 'number' && Number.isFinite(st.mtimeMs)
? st.mtimeMs
: Date.now()
return {
mode,
uid: typeof st.uid === 'number' ? st.uid : 1000,
gid: typeof st.gid === 'number' ? st.gid : 1000,
size: sz,
atime: mtimeMs,
mtime: mtimeMs
}
}
/**
* @param {VfsStatRow} st
* @param {string} filename
* @param {number} [sizeOverride]
*/
function vfsStatToSftpLongname(st, filename, sizeOverride) {
const t = st.type
const modeStr = formatModeString(
typeof st.mode === 'number' ? st.mode : 0o644,
t === 'directory' ? 'directory' : t === 'symlink' ? 'symlink' : 'file'
)
const u = String(st.user || 'user')
const g = String(st.group || 'user')
const sz =
sizeOverride !== undefined && sizeOverride !== null
? Number(sizeOverride)
: Number(st.size || 0)
const mtimeMs =
typeof st.mtimeMs === 'number' && Number.isFinite(st.mtimeMs)
? st.mtimeMs
: Date.now()
const mt = formatLsMtime(mtimeMs)
return `${modeStr} 1 ${u} ${g} ${String(sz).padStart(8, ' ')} ${mt} ${filename}`
}
/**
* @param {string} dirAbs
* @param {string} name
*/
function joinDirEntry(dirAbs, name) {
const d = String(dirAbs || '/').replace(/\/+$/, '') || '/'
const n = String(name || '')
if (!n) return d
return d === '/' ? '/' + n : d + '/' + n
}
/**
* Coerce vfs.readFile results for SFTP OPEN/READ. Empty string is falsy and used to make
* READ return SSH_FX_FAILURE; ArrayBuffer and non-Buffer views may lack `.subarray`.
* @param {unknown} x
* @returns {Buffer | null}
*/
function normalizeVfsBytesForSftp(x) {
if (x == null) return null
if (typeof x === 'string') return Buffer.from(x, 'utf8')
if (Buffer.isBuffer(x)) return x
if (x instanceof ArrayBuffer) return Buffer.from(x)
if (ArrayBuffer.isView(x))
return Buffer.from(x.buffer, x.byteOffset, x.byteLength)
return null
}
/**
* SFTP inbound handles come from bare-ssh2 bufferSlice → often Uint8Array / safer-buffer views
* that do not satisfy `Buffer.isBuffer` (see vendor utils.js bufferSlice comment).
* @param {unknown} h
* @returns {number | null}
*/
function sftpHandleId(h) {
if (h == null) return null
if (Buffer.isBuffer(h) && h.length >= 4) return h.readUInt32BE(0)
if (ArrayBuffer.isView(h) && h.byteLength >= 4) {
return new DataView(h.buffer, h.byteOffset, 4).getUint32(0, false)
}
return null
}
/**
* @param {import('events').EventEmitter} sftp
* @param {Record<string, unknown>} vfs
* @param {string} homeLogical absolute logical home (e.g. /home/guest)
* @param {(p: string) => string} resolveLogical
* @param {{ OPEN_MODE: Record<string, number>, STATUS_CODE: Record<string, number> }} sftpConsts
*/
export function attachBareOsSftp(
sftp,
vfs,
homeLogical,
resolveLogical,
sftpConsts
) {
const { OPEN_MODE, STATUS_CODE } = sftpConsts
/** @type {Map<number, { path: string, buf?: Buffer | null, pos: number, write?: boolean }>} */
const handles = new Map()
/** @type {Map<number, { path: string, entries?: string[], idx: number }>} */
const dirs = new Map()
/** Single counter so directory and file handles never share an id */
let nextHandleId = 1
/**
* Strict SFTP clients (e.g. FileZilla) expect outbound replies in the same order as
* requests were received; overlapping async handlers otherwise reorder packets and
* trigger "request ID mismatch". After serializing replies here, a rejected handler
* that sent no packet could stall clients; thrown errors during SFTP encode have been
* seen as native `bad_optional_access` (-fno-exceptions) killing sshd — always emit
* STATUS FAILURE on handler failure.
*/
/** @type {Promise<void>} */
let sftpReplyChain = Promise.resolve()
/**
* @param {number} reqid
* @param {() => void | Promise<void>} fn
*/
function enqueueSftpReply(reqid, fn) {
const next = sftpReplyChain.then(async () => {
try {
await Promise.resolve().then(fn)
} catch {
try {
sftp.status(reqid, STATUS_CODE.FAILURE)
} catch {
/* ignore */
}
}
})
sftpReplyChain = next.catch(() => {})
}
function ok(reqid) {
sftp.status(reqid, STATUS_CODE.OK)
}
/**
* @param {string} p
* @returns {string | null}
*/
function mapPath(p) {
const norm = String(p || '').replace(/\\/g, '/').replace(/\/+/g, '/')
let logical
if (norm === '.' || norm === '') logical = homeLogical
else if (norm.startsWith('/')) logical = resolveLogical(norm)
else logical = resolveLogical(`${homeLogical}/${norm}`.replace(/\/+/g, '/'))
const abs = logical
if (!bareOsSftpAllowedLogicalPath(abs, homeLogical)) return null
return abs
}
async function statLike(reqid, p, _isLstat) {
const abs = mapPath(p)
if (!abs) return sftp.status(reqid, STATUS_CODE.PERMISSION_DENIED)
try {
const st = await vfs.lstat(abs)
if (!st) return sftp.status(reqid, STATUS_CODE.NO_SUCH_FILE)
const attrs = vfsStatToSftpAttrs(/** @type {VfsStatRow} */ (st), undefined)
sftp.attrs(reqid, attrs)
} catch {
sftp.status(reqid, STATUS_CODE.FAILURE)
}
}
/**
* @param {string} abs already-resolved logical path (no mapPath)
*/
async function statAbsolute(reqid, abs, sizeOverride) {
try {
const st = await vfs.lstat(abs)
if (!st) return sftp.status(reqid, STATUS_CODE.NO_SUCH_FILE)
const attrs = vfsStatToSftpAttrs(/** @type {VfsStatRow} */ (st), sizeOverride)
sftp.attrs(reqid, attrs)
} catch {
sftp.status(reqid, STATUS_CODE.FAILURE)
}
}
sftp.on('REALPATH', (reqid, p) => {
enqueueSftpReply(reqid, async () => {
const abs = mapPath(p) || homeLogical
try {
const st = await vfs.lstat(abs)
const attrs = st
? vfsStatToSftpAttrs(/** @type {VfsStatRow} */ (st), undefined)
: {}
const name = [
{
filename: abs,
longname: st
? vfsStatToSftpLongname(
/** @type {VfsStatRow} */ (st),
abs.split('/').pop() || abs,
undefined
)
: 'drwxr-xr-x 1 user user 0 Jan 1 00:00 ' + abs,
attrs
}
]
sftp.name(reqid, name)
} catch {
sftp.name(reqid, [
{
filename: abs,
longname: 'drwxr-xr-x 1 user user 0 Jan 1 00:00 ' + abs,
attrs: {}
}
])
}
})
})
sftp.on('STAT', (reqid, p) => {
enqueueSftpReply(reqid, () => statLike(reqid, p, false))
})
sftp.on('LSTAT', (reqid, p) => {
enqueueSftpReply(reqid, () => statLike(reqid, p, true))
})
sftp.on('OPENDIR', (reqid, p) => {
enqueueSftpReply(reqid, async () => {
const abs = mapPath(p)
if (!abs) return sftp.status(reqid, STATUS_CODE.PERMISSION_DENIED)
try {
const list = await vfs.readdir(abs)
if (!Array.isArray(list)) return sftp.status(reqid, STATUS_CODE.FAILURE)
const h = Buffer.alloc(4)
const id = nextHandleId++
h.writeUInt32BE(id, 0)
dirs.set(id, { path: abs, entries: list, idx: 0 })
sftp.handle(reqid, h)
} catch {
sftp.status(reqid, STATUS_CODE.FAILURE)
}
})
})
sftp.on('READDIR', (reqid, handle) => {
enqueueSftpReply(reqid, async () => {
const id = sftpHandleId(handle)
if (id == null) return sftp.status(reqid, STATUS_CODE.FAILURE)
const d = dirs.get(id)
if (!d || !d.entries) return sftp.status(reqid, STATUS_CODE.FAILURE)
if (d.idx >= d.entries.length) return sftp.status(reqid, STATUS_CODE.EOF)
const chunk = d.entries.slice(d.idx, d.idx + 32)
d.idx += chunk.length
const names = []
for (const ent of chunk) {
const childAbs = joinDirEntry(d.path, ent)
try {
const st = await vfs.lstat(childAbs)
if (!st) {
names.push({
filename: ent,
longname: '-rw-r--r-- 1 user user 0 Jan 1 00:00 ' + ent,
attrs: {}
})
continue
}
const bl = /** @type {VfsStatRow} */ (st)
names.push({
filename: ent,
longname: vfsStatToSftpLongname(bl, ent, undefined),
attrs: vfsStatToSftpAttrs(bl, undefined)
})
} catch {
names.push({
filename: ent,
longname: '-rw-r--r-- 1 user user 0 Jan 1 00:00 ' + ent,
attrs: {}
})
}
}
sftp.name(reqid, names)
})
})
sftp.on('OPEN', (reqid, filename, flags) => {
enqueueSftpReply(reqid, async () => {
const abs = mapPath(filename)
if (!abs) return sftp.status(reqid, STATUS_CODE.PERMISSION_DENIED)
if (bareOsSftpDeniesWriteOpen(abs, homeLogical, flags, OPEN_MODE)) {
return sftp.status(reqid, STATUS_CODE.PERMISSION_DENIED)
}
const wantRead = !!(flags & OPEN_MODE.READ)
const wantWrite =
!!(flags & OPEN_MODE.WRITE) || !!(flags & OPEN_MODE.APPEND)
const wantCreat = !!(flags & OPEN_MODE.CREAT)
try {
let buf = null
if (!wantCreat) {
try {
buf = await vfs.readFile(abs)
} catch {
buf = null
}
}
buf = normalizeVfsBytesForSftp(buf)
if (buf == null && !wantCreat && wantRead) {
return sftp.status(reqid, STATUS_CODE.NO_SUCH_FILE)
}
if (buf == null && wantCreat) buf = Buffer.alloc(0)
if (buf == null) buf = Buffer.alloc(0)
const h = Buffer.alloc(4)
const id = nextHandleId++
h.writeUInt32BE(id, 0)
handles.set(id, {
path: abs,
buf,
pos: 0,
write: wantWrite || wantCreat
})
sftp.handle(reqid, h)
} catch {
sftp.status(reqid, STATUS_CODE.FAILURE)
}
})
})
sftp.on('READ', (reqid, handle, offset, length) => {
enqueueSftpReply(reqid, () => {
try {
const id = sftpHandleId(handle)
if (id == null) return sftp.status(reqid, STATUS_CODE.FAILURE)
const f = handles.get(id)
if (!f || f.buf == null) return sftp.status(reqid, STATUS_CODE.FAILURE)
const raw = f.buf
const body =
Buffer.isBuffer(raw) || ArrayBuffer.isView(raw)
? raw
: typeof raw === 'string'
? Buffer.from(raw, 'utf8')
: null
if (!body) return sftp.status(reqid, STATUS_CODE.FAILURE)
const off = Number(offset)
const len = Number(length)
if (!Number.isFinite(off) || !Number.isFinite(len)) {
return sftp.status(reqid, STATUS_CODE.FAILURE)
}
const end = Math.min(off + len, body.length)
if (off >= body.length) return sftp.status(reqid, STATUS_CODE.EOF)
let slice = body.subarray(off, end)
if (!Buffer.isBuffer(slice)) {
slice = Buffer.from(slice.buffer, slice.byteOffset, slice.byteLength)
}
sftp.data(reqid, slice)
} catch {
sftp.status(reqid, STATUS_CODE.FAILURE)
}
})
})
sftp.on('FSTAT', (reqid, handle) => {
enqueueSftpReply(reqid, async () => {
const id = sftpHandleId(handle)
if (id == null) return sftp.status(reqid, STATUS_CODE.FAILURE)
const f = handles.get(id)
if (f) {
const hint = f.buf ? f.buf.length : undefined
return statAbsolute(reqid, f.path, hint)
}
const d = dirs.get(id)
if (d && d.path) return statAbsolute(reqid, d.path, undefined)
sftp.status(reqid, STATUS_CODE.FAILURE)
})
})
sftp.on('WRITE', (reqid, handle, offset, data) => {
enqueueSftpReply(reqid, async () => {
const id = sftpHandleId(handle)
if (id == null) return sftp.status(reqid, STATUS_CODE.FAILURE)
const f = handles.get(id)
if (!f || !f.write) return sftp.status(reqid, STATUS_CODE.PERMISSION_DENIED)
try {
const u8 = data instanceof Uint8Array ? data : new Uint8Array(data)
const prev = f.buf ? new Uint8Array(f.buf) : new Uint8Array(0)
const need = offset + u8.length
const out = new Uint8Array(Math.max(prev.length, need))
out.set(prev)
out.set(u8, offset)
f.buf = out
await vfs.writeFile(f.path, out)
ok(reqid)
} catch {
sftp.status(reqid, STATUS_CODE.FAILURE)
}
})
})
sftp.on('CLOSE', (reqid, handle) => {
enqueueSftpReply(reqid, () => {
const id = sftpHandleId(handle)
if (id == null) return sftp.status(reqid, STATUS_CODE.FAILURE)
handles.delete(id)
dirs.delete(id)
ok(reqid)
})
})
sftp.on('REMOVE', (reqid, p) => {
enqueueSftpReply(reqid, async () => {
const abs = mapPath(p)
if (!abs) return sftp.status(reqid, STATUS_CODE.PERMISSION_DENIED)
try {
await vfs.unlink(abs)
ok(reqid)
} catch {
sftp.status(reqid, STATUS_CODE.FAILURE)
}
})
})
sftp.on('MKDIR', (reqid, p) => {
enqueueSftpReply(reqid, async () => {
const abs = mapPath(p)
if (!abs) return sftp.status(reqid, STATUS_CODE.PERMISSION_DENIED)
try {
await vfs.mkdir(abs, { recursive: true })
ok(reqid)
} catch {
sftp.status(reqid, STATUS_CODE.FAILURE)
}
})
})
sftp.on('RMDIR', (reqid, p) => {
enqueueSftpReply(reqid, async () => {
const abs = mapPath(p)
if (!abs) return sftp.status(reqid, STATUS_CODE.PERMISSION_DENIED)
try {
await vfs.rmdir(abs)
ok(reqid)
} catch {
sftp.status(reqid, STATUS_CODE.FAILURE)
}
})
})
sftp.on('SETSTAT', (reqid) => {
enqueueSftpReply(reqid, () => {
sftp.status(reqid, STATUS_CODE.OP_UNSUPPORTED)
})
})
sftp.on('FSETSTAT', (reqid) => {
enqueueSftpReply(reqid, () => {
sftp.status(reqid, STATUS_CODE.OP_UNSUPPORTED)
})
})
sftp.on('EXTENDED', (reqid) => {
enqueueSftpReply(reqid, () => {
sftp.status(reqid, STATUS_CODE.OP_UNSUPPORTED)
})
})
sftp.on('RENAME', (reqid) => {
enqueueSftpReply(reqid, () => {
sftp.status(reqid, STATUS_CODE.OP_UNSUPPORTED)
})
})
sftp.on('READLINK', (reqid) => {
enqueueSftpReply(reqid, () => {
sftp.status(reqid, STATUS_CODE.OP_UNSUPPORTED)
})
})
sftp.on('SYMLINK', (reqid) => {
enqueueSftpReply(reqid, () => {
sftp.status(reqid, STATUS_CODE.OP_UNSUPPORTED)
})
})
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,43 @@
/**
* Initd marker for swarm chat (`bare-os-chat-v1`): transport stays in the booter/swarm-disk;
* this unit records that chat is intended to be active when env allows.
*/
import { registerBareService } from './bare-initd.js'
import { bareOsChatMuxEnabled } from './bare-os-chat-service.js'
/** @type {boolean} */
let bareOsChatInitdRegistered = false
/** @param {Record<string, string | undefined>} env */
export function bareOsChatInitdEnabled(env) {
if (!bareOsChatMuxEnabled(env)) return false
const v = env.BARE_OS_CHAT_INITD
if (v === '0' || v === 'false') return false
return true
}
/**
* @param {Record<string, unknown>} ctx
*/
async function startBareOsChatInitd(ctx) {
if (!bareOsChatMuxEnabled(globalThis.process?.env)) return
ctx.console?.log?.(
'[bare-os-chat] Protomux bare-os-chat-v1 enabled (join general on peer connect)'
)
}
async function stopBareOsChatInitd(_ctx) {}
/** @param {Record<string, string | undefined>} env */
export function maybeRegisterBareOsChatInitd(env) {
if (bareOsChatInitdRegistered) return
if (!bareOsChatInitdEnabled(env || {})) return
bareOsChatInitdRegistered = true
registerBareService({
name: 'bare-os-chat',
description:
'Swarm chat (bare-os-chat-v1); stock default on — set BARE_OS_PROTOMUX_CHAT_CHANNEL=0 or BARE_OS_CHAT_INITD=0 to disable',
start: startBareOsChatInitd,
stop: stopBareOsChatInitd
})
}
@@ -0,0 +1,404 @@
import b4a from 'b4a'
import { randomBytes } from 'bare-crypto'
import {
setupBareOsChatChannel,
BARE_OS_CHAT_WIRE_SCHEMA_VERSION,
BARE_OS_CHAT_EVT_TEXT,
PROTOCOL_CHAT_CHANNEL_NAME,
bareOsProtMuxChatChannelEnabled
} from 'bare-os-protocol'
/** @typedef {{ chan: import('protomux').Channel, mux: import('protomux').Protomux, socket: any, id: string | null, chatChan?: import('protomux').Channel | null }} SwarmPeer */
/**
* Stock default: swarm chat is **on** unless disabled via env (see `bare-os-protocol` `bareOsProtMuxChatChannelEnabled`).
* @param {Record<string, string | undefined>} [env]
*/
export function bareOsChatMuxEnabled(env = globalThis.process?.env) {
return bareOsProtMuxChatChannelEnabled(env || {})
}
/**
* Swarm Protomux chat transport on `disk` (receive + relay + broadcast). Stock-on when mux env allows.
* Independent of the **`bare-os-chat`** initd unit (that unit is only started after identity unlock).
*
* @param {import('./swarm-disk.js').SwarmDisk} disk
* @param {Record<string, string | undefined>} [env]
*/
export function ensureDiskBareOsChatTransport(disk, env = {}) {
if (!disk || !bareOsChatMuxEnabled(globalThis.process?.env)) return
const merged = {
.../** @type {Record<string, string | undefined>} */ (
globalThis.process?.env || {}
),
...env
}
disk.bareOsChatService = createBareOsChatService({ env: merged })
}
/**
* @param {Record<string, string | undefined>} env
*/
function chatGossipTtlDefault(env) {
const raw = String(env.BARE_OS_CHAT_GOSSIP_TTL ?? '').trim()
const n = raw ? Number.parseInt(raw, 10) : NaN
if (Number.isFinite(n) && n >= 0 && n <= 32) return n
return 4
}
/**
* @param {Record<string, string | undefined>} env
*/
function chatHistoryMax(env) {
const raw = String(env.BARE_OS_CHAT_HISTORY_MAX ?? '').trim()
const n = raw ? Number.parseInt(raw, 10) : NaN
if (Number.isFinite(n) && n >= 16 && n <= 10000) return n
return 512
}
/**
* @param {Record<string, string | undefined>} env
*/
function chatMaxBodyBytes(env) {
const raw = String(env.BARE_OS_CHAT_MAX_BODY_BYTES ?? '').trim()
const n = raw ? Number.parseInt(raw, 10) : NaN
if (Number.isFinite(n) && n >= 256 && n <= 65536) return n
return 4096
}
/**
* Global swarm chat (host-side): fan-out, gossip, dedupe, metrics.
*/
export function createBareOsChatService(opts = {}) {
const env = opts.env || globalThis.process?.env || {}
const gossipTtlMax = chatGossipTtlDefault(
/** @type {Record<string, string | undefined>} */ (env)
)
const historyMax = chatHistoryMax(
/** @type {Record<string, string | undefined>} */ (env)
)
const maxBodyBytes = chatMaxBodyBytes(
/** @type {Record<string, string | undefined>} */ (env)
)
/** @type {Set<(ev: Record<string, unknown>) => void>} */
const subscribers = new Set()
/** @type {Map<string, number>} evt dedupe -> expireAtMs */
const seenEvt = new Map()
/** @type {Array<Record<string, unknown>>} */
const history = []
/** @type {{ rxEvent: number, txEvent: number, droppedRate: number, droppedVerify: number }} */
const metrics = {
rxEvent: 0,
txEvent: 0,
droppedRate: 0,
droppedVerify: 0
}
/** @type {Map<string, { tokens: number, resetAt: number }>} */
const ratePeer = new Map()
const RATE_WINDOW_MS = 2000
const RATE_MAX_MSG = 24
function trimDedupe() {
const now = Date.now()
for (const [k, exp] of seenEvt) {
if (exp < now) seenEvt.delete(k)
}
}
/**
* @param {string} peerKey
*/
function allowRate(peerKey) {
const now = Date.now()
let r = ratePeer.get(peerKey)
if (!r || r.resetAt < now) {
r = { tokens: RATE_MAX_MSG, resetAt: now + RATE_WINDOW_MS }
ratePeer.set(peerKey, r)
}
if (r.tokens <= 0) {
metrics.droppedRate++
return false
}
r.tokens--
return true
}
function pushHistory(rec) {
history.push(rec)
while (history.length > historyMax) history.shift()
}
/**
* @param {Uint8Array} pk
* @param {Uint8Array} evtId
*/
function dedupeKey(pk, evtId) {
return `${b4a.toString(pk, 'hex')}:${b4a.toString(evtId, 'hex')}`
}
/**
* @param {import('./swarm-disk.js').SwarmDisk} disk
* @param {SwarmPeer} fromPeer
* @param {Record<string, unknown>} evt
*/
function relayEvent(disk, fromPeer, evt) {
const ttl = typeof evt.ttl === 'number' ? evt.ttl : gossipTtlMax
if (ttl <= 0) return
const next = { ...evt, ttl: ttl - 1 }
for (const p of disk.peers) {
if (p === fromPeer) continue
const ch = p.chatChan
if (!ch || !ch.messages || !ch.messages[4]) continue
try {
ch.messages[4].send(next)
metrics.txEvent++
} catch {
/* ignore */
}
}
}
/**
* @param {import('./swarm-disk.js').SwarmDisk} disk
* @param {SwarmPeer} fromPeer
* @param {Record<string, unknown>} evt
*/
function ingestEvent(disk, fromPeer, evt) {
const senderPk = /** @type {Uint8Array | undefined} */ (evt.senderPk)
const evtId = /** @type {Uint8Array | undefined} */ (evt.evtId)
if (
!senderPk ||
senderPk.byteLength !== 32 ||
!evtId ||
evtId.byteLength !== 16
) {
return
}
const body = String(evt.body ?? '')
if (body.byteLength > maxBodyBytes) return
const sock = fromPeer.socket
const ttlMaxGossip = Math.max(1, gossipTtlMax)
if (
sock &&
sock.remotePublicKey &&
senderPk &&
senderPk.byteLength === 32
) {
if (!b4a.equals(sock.remotePublicKey, senderPk)) {
const ttlNum = typeof evt.ttl === 'number' ? evt.ttl : -1
// Direct frames: Noise remote PK must equal claimed senderPk.
// Relayed hops: ttl is decremented before forward; mux peer ≠ origin — allow ttl < configured max TTL.
if (!(ttlNum >= 0 && ttlNum < ttlMaxGossip)) {
metrics.droppedVerify++
return
}
}
}
const pk = fromPeer.id || b4a.toString(senderPk, 'hex')
if (!allowRate(pk)) return
trimDedupe()
const dk = dedupeKey(senderPk, evtId)
const now = Date.now()
if (seenEvt.has(dk)) return
seenEvt.set(dk, now + 120_000)
metrics.rxEvent++
const rec = {
...evt,
body,
receivedAtMs: now,
fromPeerKey: pk
}
pushHistory(rec)
for (const fn of subscribers) {
try {
fn(rec)
} catch {
/* ignore */
}
}
relayEvent(disk, fromPeer, evt)
}
return {
PROTOCOL_CHAT_CHANNEL_NAME,
metrics,
history() {
return [...history]
},
presence() {
/** @type {Record<string, { displayName: string, roomId: string }>} */
const out = {}
return out
},
rooms() {
return ['general']
},
subscribe(fn) {
subscribers.add(fn)
return () => subscribers.delete(fn)
},
/**
* @param {import('./swarm-disk.js').SwarmDisk} disk
* @param {import('protomux').Protomux} mux
* @param {any} socket
* @param {SwarmPeer} peer
*/
pairOnMux(disk, mux, socket, peer) {
setupBareOsChatChannel(mux, {
onHello(_m, chan) {
try {
chan.messages[1].send({
chatSchemaVersion: 1,
maxPayloadBytes: 16384
})
} catch {
/* ignore */
}
},
onHelloAck(_m, chan) {
try {
const id16 = randomBytes(16)
let pk = b4a.alloc(32)
if (socket?.publicKey && socket.publicKey.byteLength === 32) {
pk = b4a.from(socket.publicKey)
} else if (
disk.localNoiseWirePk &&
disk.localNoiseWirePk.byteLength === 32
) {
pk = disk.localNoiseWirePk
}
chan.messages[2].send({
roomId: 'general',
displayName: String(env.USER || env.LOGNAME || 'peer'),
senderPk: pk,
clientInstanceId: id16
})
} catch {
/* ignore */
}
},
onJoin(_m, _chan) {},
onLeave(_m, _chan) {},
onEvent(m, _chan) {
try {
disk.protomuxChatChannelRxTotal =
(disk.protomuxChatChannelRxTotal || 0) + 1
} catch {
/* ignore */
}
ingestEvent(disk, peer, m)
},
onControl(_m, _chan) {},
onChannelOpened(chan) {
peer.chatChan = chan
try {
chan.messages[0].send({
chatSchemaVersion: 1,
swarmChatCapability: true
})
} catch {
/* ignore */
}
mux.stream?.once?.('close', () => {
peer.chatChan = null
})
}
})
},
/**
* @param {import('./swarm-disk.js').SwarmDisk} disk
* @param {string} text
* @param {{ displayName?: string, senderPk?: Uint8Array }} [meta]
*/
broadcastLocal(disk, text, meta = {}) {
const evtId = randomBytes(16)
let pk =
meta.senderPk && meta.senderPk.byteLength === 32 ? meta.senderPk : null
if (!pk) {
for (const p of disk.peers) {
const sk = p.socket?.publicKey
if (sk && sk.byteLength === 32) {
pk = b4a.from(sk)
break
}
}
}
if (
!pk &&
disk.localNoiseWirePk &&
disk.localNoiseWirePk.byteLength === 32
) {
pk = disk.localNoiseWirePk
}
if (!pk || pk.byteLength !== 32) {
pk = b4a.alloc(32)
}
const evt = {
schemaVersion: BARE_OS_CHAT_WIRE_SCHEMA_VERSION,
evtKind: BARE_OS_CHAT_EVT_TEXT,
roomId: 'general',
evtId,
tsMs: Date.now(),
senderPk: pk,
displayName: meta.displayName || String(env.USER || 'local'),
body: text,
ttl: gossipTtlMax,
sigDetached: null
}
pushHistory({ ...evt, local: true, receivedAtMs: Date.now() })
for (const fn of subscribers) {
try {
fn({ ...evt, local: true })
} catch {
/* ignore */
}
}
/** @type {Promise<void>[]} */
const sends = []
for (const p of disk.peers) {
const ch = p.chatChan
if (!ch || !ch.messages || !ch.messages[4]) continue
const ready =
typeof ch.fullyOpened === 'function'
? Promise.race([
ch.fullyOpened(),
new Promise((resolve) => {
setTimeout(
() => resolve(ch.opened !== false && !ch.destroyed),
250
)
})
])
: true
sends.push(
Promise.resolve(ready).then((opened) => {
if (opened === false || ch.destroyed) return
if (p.chatChan !== ch) return
try {
ch.messages[4].send(evt)
metrics.txEvent++
} catch {
/* ignore */
}
})
)
}
return Promise.all(sends)
},
snapshotMetrics() {
return {
...metrics,
gossipTtlDefault: gossipTtlMax,
historyMax,
maxBodyBytes,
protocol: PROTOCOL_CHAT_CHANNEL_NAME
}
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,471 @@
/**
* Optional Discord bot initd unit (`bare-os-discord`).
* Registered only when the guest VFS has ~/.discord/.env with DISCORD_TOKEN=.
* Hidden from systemctl when that file is missing.
*/
import {
findBareServiceDefinition,
registerBareService,
startBareService,
stopBareService,
unregisterBareService
} from './bare-initd.js'
import { appendVarLog, BARE_OS_VAR_LOG_DIR } from './bare-os-var-log.js'
import {
normalizeDiscordToken,
parseDotEnvText
} from './bare-discord-js-loader.js'
// Static CJS import so bare-pack rewrites the binding into app.bundle.
// Runtime require of a sibling path is MODULE_NOT_FOUND under bare:/app.bundle/.
import discordCommandsNs from './bare-os-discord-commands-guest.cjs'
const discordCommands =
discordCommandsNs &&
(discordCommandsNs.dispatchInteraction || discordCommandsNs.buildSlashCommands)
? discordCommandsNs
: discordCommandsNs && discordCommandsNs.default
? discordCommandsNs.default
: discordCommandsNs
export const BARE_OS_DISCORD_SERVICE_ENV = '~/.discord/.env'
export const BARE_OS_DISCORD_LOG = `${BARE_OS_VAR_LOG_DIR}/discord.log`
export const BARE_OS_DISCORD_UNIT = 'bare-os-discord'
/** Extra guest files scanned for DISCORD_ID_WHITELIST / guild / user-install. */
export const BARE_OS_DISCORD_EXTRA_ENV_FILES = [
'~/.discord/.env',
'~/.discord.env',
'~/discord.env',
'~/.env',
'./.env'
]
/** @type {import('discord.js').Client | null} */
let discordServiceClient = null
/** Live session ctx (updated after login so commands are not stuck on guest). */
let discordServiceCtx = null
export function bindBareOsDiscordSessionCtx(ctx) {
if (ctx) discordServiceCtx = ctx
return discordServiceCtx
}
/** @param {Record<string, string | undefined>} env */
export function bareOsDiscordInitdEnabled(env) {
const v = env && (env.BARE_OS_DISCORD_INITD || env.BARE_OS_DISCORD)
if (v === '0' || v === 'false') return false
return true
}
function discordVfsText(ctx, buf) {
if (!buf) return ''
if (typeof ctx.b4a?.toString === 'function') return ctx.b4a.toString(buf)
return Buffer.from(buf).toString('utf8')
}
async function readBareOsDiscordDotEnvFile(ctx, path) {
const vfs = ctx && ctx.vfs
if (!vfs || typeof vfs.readFile !== 'function') return null
let buf
try {
buf = await vfs.readFile(path)
} catch {
return null
}
if (!buf) return null
return parseDotEnvText(discordVfsText(ctx, buf))
}
function discordSessionEnvMaps(ctx) {
if (!ctx.env || typeof ctx.env !== 'object') ctx.env = {}
const maps = [ctx.env]
if (ctx.vfs && ctx.vfs.env && typeof ctx.vfs.env === 'object' && ctx.vfs.env !== ctx.env) {
maps.push(ctx.vfs.env)
}
return maps
}
/**
* Copy non-empty Discord extras onto both `ctx.env` and `ctx.vfs.env`.
* Session values win; empty strings do not block a file value.
*/
export function applyDiscordExtrasToSession(ctx, parsed) {
if (!parsed || !ctx) return
const maps = discordSessionEnvMaps(ctx)
const pairs = [
['DISCORD_GUILD_ID', parsed.DISCORD_GUILD_ID],
[
'DISCORD_ID_WHITELIST',
parsed.DISCORD_ID_WHITELIST || parsed.BARE_OS_DISCORD_ID_WHITELIST
],
[
'DISCORD_USER_INSTALL',
parsed.DISCORD_USER_INSTALL || parsed.BARE_OS_DISCORD_USER_INSTALL
]
]
for (const [key, raw] of pairs) {
const v = raw == null ? '' : String(raw).trim()
if (!v) continue
for (const env of maps) {
if (!String(env[key] || '').trim()) env[key] = v
}
}
}
function discordSessionWhitelist(ctx) {
const maps = []
if (ctx && ctx.env && typeof ctx.env === 'object') maps.push(ctx.env)
if (ctx && ctx.vfs && ctx.vfs.env && typeof ctx.vfs.env === 'object') {
maps.push(ctx.vfs.env)
}
for (const env of maps) {
const v = String(
env.DISCORD_ID_WHITELIST || env.BARE_OS_DISCORD_ID_WHITELIST || ''
).trim()
if (v) return v
}
return ''
}
export async function readBareOsDiscordServiceEnv(ctx) {
const parsed = await readBareOsDiscordDotEnvFile(ctx, BARE_OS_DISCORD_SERVICE_ENV)
if (!parsed) return null
const token = normalizeDiscordToken(
parsed.DISCORD_TOKEN || parsed.BOT_TOKEN || ''
)
if (!token) return null
return {
token,
guildId: String(parsed.DISCORD_GUILD_ID || '').trim(),
whitelist: String(
parsed.DISCORD_ID_WHITELIST || parsed.BARE_OS_DISCORD_ID_WHITELIST || ''
).trim(),
userInstall: String(
parsed.DISCORD_USER_INSTALL || parsed.BARE_OS_DISCORD_USER_INSTALL || ''
).trim()
}
}
/**
* Load token from ~/.discord/.env, then fill whitelist / guild / user-install
* from that file, sibling env files (~/.env, …), and both session env maps.
*/
export async function hydrateBareOsDiscordSessionEnv(ctx) {
const creds = await readBareOsDiscordServiceEnv(ctx)
if (creds) applyDiscordExtrasToSession(ctx, creds)
const seen = Object.create(null)
for (const path of BARE_OS_DISCORD_EXTRA_ENV_FILES) {
if (!path || seen[path]) continue
seen[path] = true
const parsed = await readBareOsDiscordDotEnvFile(ctx, path)
if (parsed) applyDiscordExtrasToSession(ctx, parsed)
}
if (creds) {
const wl = discordSessionWhitelist(ctx)
if (wl) creds.whitelist = wl
const guild = String(
(ctx.env && ctx.env.DISCORD_GUILD_ID) || creds.guildId || ''
).trim()
if (guild) creds.guildId = guild
const ui = String(
(ctx.env && ctx.env.DISCORD_USER_INSTALL) || creds.userInstall || ''
).trim()
if (ui) creds.userInstall = ui
}
return creds
}
function discordServiceLog(ctx, line) {
try {
ctx.console?.log?.('[bare-os-discord] ' + line)
} catch {
/* ignore */
}
void appendVarLog(ctx, BARE_OS_DISCORD_LOG, 'discord', line)
}
async function startBareOsDiscord(ctx) {
const creds = await hydrateBareOsDiscordSessionEnv(ctx)
if (!creds) {
throw new Error('missing ' + BARE_OS_DISCORD_SERVICE_ENV + ' (DISCORD_TOKEN=)')
}
const dj = ctx.bare && ctx.bare.discordJS
if (!dj || typeof dj.Client !== 'function') {
throw new Error('ctx.bare.discordJS is unavailable')
}
discordServiceCtx = ctx
if (discordServiceClient) return
applyDiscordExtrasToSession(ctx, creds)
const osName =
(typeof process !== 'undefined' && process.platform) || 'darwin'
const client = new dj.Client({
intents: [dj.GatewayIntentBits.Guilds],
ws: {
identifyProperties: {
os: osName,
browser: 'bare-os',
device: 'bare-os'
}
}
})
if (dj.Events && dj.Events.Error) {
client.on(dj.Events.Error, function (err) {
discordServiceLog(ctx, 'client error: ' + ((err && err.message) || err))
})
}
if (dj.Events && dj.Events.InteractionCreate) {
client.on(dj.Events.InteractionCreate, async function (interaction) {
const live = discordServiceCtx || ctx
if (
discordCommands &&
typeof discordCommands.syncSessionIdentity === 'function'
) {
discordCommands.syncSessionIdentity(live)
}
const dispatch =
discordCommands &&
(discordCommands.dispatchInteraction ||
(discordCommands.default &&
discordCommands.default.dispatchInteraction))
if (typeof dispatch === 'function') {
await dispatch(live, interaction)
return
}
if (
!interaction.isChatInputCommand ||
!interaction.isChatInputCommand()
) {
return
}
if (interaction.commandName !== 'ping') return
const uid =
interaction.user && interaction.user.id ? String(interaction.user.id) : ''
const allowed =
discordCommands && typeof discordCommands.userAllowed === 'function'
? discordCommands.userAllowed(live, uid, interaction)
: true
if (!allowed) {
try {
await interaction.reply({
content:
'Access denied. Your Discord user id is not on DISCORD_ID_WHITELIST.',
flags: 64
})
} catch (err) {
discordServiceLog(
ctx,
'deny failed: ' + ((err && err.message) || err)
)
}
return
}
try {
await interaction.reply({ content: 'pong' })
} catch (err) {
discordServiceLog(
ctx,
'/ping failed: ' + ((err && err.message) || err)
)
}
})
}
await client.login(creds.token)
discordServiceClient = client
const tag =
client.user && client.user.tag ? client.user.tag : String(client.user)
discordServiceLog(ctx, 'logged in as ' + tag)
if (typeof dj.REST === 'function' && dj.Routes) {
try {
const rest = new dj.REST().setToken(creds.token)
const build =
discordCommands &&
(discordCommands.buildSlashCommands ||
(discordCommands.default &&
discordCommands.default.buildSlashCommands))
const body =
typeof build === 'function'
? await Promise.resolve(build(dj, ctx))
: [
new dj.SlashCommandBuilder()
.setName('ping')
.setDescription('Replies with pong.')
.toJSON()
]
const setReg =
discordCommands &&
(discordCommands.setPluginRegistrar ||
(discordCommands.default && discordCommands.default.setPluginRegistrar))
const userInstallOn =
discordCommands &&
typeof discordCommands.userInstallEnabled === 'function'
? discordCommands.userInstallEnabled(ctx)
: true
async function registerBody(next) {
if (
userInstallOn &&
discordCommands &&
typeof discordCommands.enableUserInstallApp === 'function'
) {
try {
await discordCommands.enableUserInstallApp(rest, dj.Routes)
} catch (err) {
discordServiceLog(
ctx,
'user-install app config failed: ' +
((err && err.message) || err) +
' (enable User Install under Developer Portal → Installation)'
)
}
}
if (
discordCommands &&
typeof discordCommands.putSlashCommands === 'function'
) {
const put = await discordCommands.putSlashCommands(
rest,
dj.Routes,
client.user.id,
next,
{ guildId: creds.guildId, userInstall: userInstallOn }
)
if (put.global) {
discordServiceLog(
ctx,
'registered ' +
next.length +
' global commands' +
(userInstallOn ? ' (user-install + guild)' : '')
)
}
if (put.guild) {
discordServiceLog(
ctx,
'registered ' +
next.length +
' commands for guild ' +
creds.guildId
)
}
return
}
if (creds.guildId) {
await rest.put(
dj.Routes.applicationGuildCommands(client.user.id, creds.guildId),
{ body: next }
)
} else {
await rest.put(dj.Routes.applicationCommands(client.user.id), {
body: next
})
}
}
if (typeof setReg === 'function') {
setReg(async function () {
const next = await Promise.resolve(build(dj, ctx))
await registerBody(next)
})
}
await registerBody(body)
if (userInstallOn) {
const url =
discordCommands &&
typeof discordCommands.userInstallAuthorizeUrl === 'function'
? discordCommands.userInstallAuthorizeUrl(client.user.id)
: ''
if (url) {
discordServiceLog(ctx, 'add to your Discord profile: ' + url)
}
const wlN =
discordCommands &&
typeof discordCommands.whitelistCount === 'function'
? discordCommands.whitelistCount(ctx)
: 0
if (wlN) {
discordServiceLog(
ctx,
'DISCORD_ID_WHITELIST active (' + wlN + ' user id(s))'
)
} else {
discordServiceLog(
ctx,
'user-install is on and DISCORD_ID_WHITELIST is empty — user-install / DM commands are denied'
)
}
}
} catch (err) {
discordServiceLog(
ctx,
'slash register failed: ' + ((err && err.message) || err)
)
}
}
}
async function stopBareOsDiscord(_ctx) {
const c = discordServiceClient
discordServiceClient = null
discordServiceCtx = null
if (!c) return
await Promise.resolve(c.destroy()).catch(function () {})
}
/**
* Register or drop the unit from systemctl based on ~/.discord/.env.
* @param {Record<string, unknown>} ctx
*/
export async function syncBareOsDiscordInitd(ctx) {
const env =
ctx && ctx.env && typeof ctx.env === 'object'
? /** @type {Record<string, string | undefined>} */ (ctx.env)
: {}
const want =
bareOsDiscordInitdEnabled(env) && (await readBareOsDiscordServiceEnv(ctx))
const have = Boolean(findBareServiceDefinition(BARE_OS_DISCORD_UNIT))
if (!want) {
if (have) {
try {
await stopBareService(ctx, BARE_OS_DISCORD_UNIT)
} catch {
/* may already be inactive */
}
unregisterBareService(BARE_OS_DISCORD_UNIT)
}
return false
}
registerBareService({
name: BARE_OS_DISCORD_UNIT,
description:
'Bare OS Discord bot (/bare /sys /svc /fs /net …); requires ~/.discord/.env',
logPath: BARE_OS_DISCORD_LOG,
start: startBareOsDiscord,
stop: stopBareOsDiscord
})
return true
}
/**
* After identity unlock: show+start the unit only when ~/.discord/.env exists.
* @param {Record<string, unknown>} ctx
*/
export async function maybeStartBareOsDiscordAfterIdentity(ctx) {
bindBareOsDiscordSessionCtx(ctx)
const shown = await syncBareOsDiscordInitd(ctx)
if (!shown) return
try {
await startBareService(ctx, BARE_OS_DISCORD_UNIT)
} catch (e) {
const msg = (e && /** @type {{ message?: string }} */ (e).message) || String(e)
discordServiceLog(ctx, 'start failed: ' + msg)
}
}
/** Drop the unit on logout so it does not appear for the next session. */
export async function stopBareOsDiscordInitd(ctx) {
if (!findBareServiceDefinition(BARE_OS_DISCORD_UNIT)) return
try {
await stopBareService(ctx, BARE_OS_DISCORD_UNIT)
} catch {
/* ignore */
}
unregisterBareService(BARE_OS_DISCORD_UNIT)
}
@@ -0,0 +1,15 @@
/**
* MUST load before any discord.js / @discordjs/ws evaluation in the standalone
* pack graph. Forces WHATWG WebSocket (bare-ws) so IDENTIFY produces READY.
* Marker: bare-os-discord-gateway-ws
*/
import wsAdapter from '../vendor/bare-discord-js/src/adapters/whatwg-ws.cjs'
const install =
(wsAdapter && wsAdapter.installBareOsDiscordGatewayWs) ||
(wsAdapter &&
wsAdapter.default &&
wsAdapter.default.installBareOsDiscordGatewayWs)
if (typeof install === 'function') install()
export {}
@@ -0,0 +1,88 @@
/**
* IRC dial policy for ctx.bareOsTlsConnect / /bin/irc.
* BARE_OS_IRC=0 disables. BARE_OS_IRC_DENYLIST always wins.
* Unset allowlist defaults to irc.libera.chat and irc.*.libera.chat.
*/
import { parseHostPatternList } from './bare-os-http-policy.js'
export const BARE_OS_IRC_DEFAULT_ALLOW = [
'irc.libera.chat',
'irc.*.libera.chat'
]
/**
* @param {string} pattern
* @param {string} host
*/
export function bareOsIrcHostMatchesGlob(pattern, host) {
const p = String(pattern || '').toLowerCase()
const h = String(host || '').toLowerCase()
if (p === '*' || p === '*:*') return true
const esc = p
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
.replace(/\*/g, '.*')
.replace(/\?/g, '.')
try {
return new RegExp('^' + esc + '$').test(h)
} catch {
return h === p
}
}
/**
* @param {string} host
*/
export function bareOsIrcIsLiberaHost(host) {
const h = String(host || '')
.trim()
.toLowerCase()
return h === 'libera.chat' || h.endsWith('.libera.chat')
}
/**
* @param {string} host
* @param {Record<string, string | undefined> | null | undefined} env
* @returns {{ ok: boolean, reason?: string }}
*/
export function bareOsIrcHostAllowed(host, env) {
const e = env && typeof env === 'object' ? env : {}
const v = e.BARE_OS_IRC
if (v === '0' || v === 'false') return { ok: false, reason: 'disabled' }
const h = String(host || '')
.trim()
.toLowerCase()
if (!h) return { ok: false, reason: 'missing host' }
const deny = parseHostPatternList(e.BARE_OS_IRC_DENYLIST)
for (const pat of deny) {
if (bareOsIrcHostMatchesGlob(pat, h))
return { ok: false, reason: 'denylist' }
}
const rawAllow = e.BARE_OS_IRC_ALLOWLIST
const allow =
rawAllow == null || String(rawAllow).trim() === ''
? BARE_OS_IRC_DEFAULT_ALLOW
: parseHostPatternList(rawAllow)
if (!allow.length) return { ok: true }
for (const pat of allow) {
if (bareOsIrcHostMatchesGlob(pat, h)) return { ok: true }
}
return { ok: false, reason: 'not in allowlist' }
}
/**
* Libera must not be dialed in plaintext unless the operator forces it twice.
* @param {string} host
* @param {{ tls?: boolean, insecurePlain?: boolean }} opts
*/
export function bareOsIrcPlaintextAllowed(host, opts) {
const o = opts || {}
if (o.tls !== false) return { ok: true }
if (bareOsIrcIsLiberaHost(host) && o.insecurePlain !== true) {
return {
ok: false,
reason: 'libera requires TLS (use --insecure-plain to override)'
}
}
return { ok: true }
}
@@ -0,0 +1,127 @@
/**
* Diagnose a GGUF on disk after load failure (magic / size / sha256).
*/
/** @type {Record<string, { size: number, sha256: string }>} */
export const BARE_OS_QVAC_KNOWN_GGUF = {
'Qwen3-0.6B-Q4_0.gguf': {
size: 382156480,
sha256: '33bcc57074ec7b6eada5a90651ee546ec0c2b271002c22baf9f1b2dd1e8f75cb'
},
'Qwen3-1.7B-Q4_0.gguf': {
size: 1056782912,
sha256: 'c876f159707a4e4f70e045106c69db15bfc935a4981706fd4f65c6e7ea1e81c5'
}
}
/**
* @param {string} filePath
* @returns {Promise<string>}
*/
export async function bareOsQvacDiagnoseGguf(filePath) {
const p = String(filePath || '').trim()
if (!p) return 'gguf: (no path)'
/** @type {string[]} */
const lines = ['gguf path: ' + p]
try {
let fs
try {
fs = await import('bare-fs')
} catch {
fs = await import('fs')
}
const fsp = fs.promises || fs
const st = await fsp.stat(p)
lines.push('size: ' + st.size + ' bytes')
const base =
p.split(/[/\\]/).pop() ||
''
const known = BARE_OS_QVAC_KNOWN_GGUF[base.replace(/^[0-9a-f]+_/i, '')] ||
Object.entries(BARE_OS_QVAC_KNOWN_GGUF).find(([name]) =>
base.endsWith(name)
)?.[1]
if (known) {
lines.push(
'expectedSize: ' +
known.size +
(st.size === known.size ? ' (ok)' : ' (MISMATCH)')
)
}
const buf = Buffer.alloc(4)
let fh
try {
if (typeof fsp.open === 'function') {
fh = await fsp.open(p, 'r')
if (typeof fh.read === 'function') {
await fh.read(buf, 0, 4, 0)
} else if (typeof fs.read === 'function' && fh.fd != null) {
await new Promise((resolve, reject) => {
fs.read(fh.fd, buf, 0, 4, 0, (err) => (err ? reject(err) : resolve()))
})
}
} else {
const fd = fs.openSync(p, 'r')
fs.readSync(fd, buf, 0, 4, 0)
fs.closeSync(fd)
}
} finally {
try {
if (fh && typeof fh.close === 'function') await fh.close()
} catch {
/* ignore */
}
}
const magic = buf.toString('utf8')
lines.push('magic: ' + JSON.stringify(magic) + (magic === 'GGUF' ? ' (ok)' : ' (BAD)'))
if (known && st.size === known.size && st.size <= 2 * 1024 * 1024 * 1024) {
try {
let crypto
try {
crypto = await import('bare-crypto')
} catch {
crypto = await import('crypto')
}
const createHash = crypto.createHash || crypto.default?.createHash
if (typeof createHash === 'function') {
const hash = createHash('sha256')
const stream = fs.createReadStream
? fs.createReadStream(p)
: null
if (stream) {
await new Promise((resolve, reject) => {
stream.on('data', (c) => hash.update(c))
stream.on('error', reject)
stream.on('end', resolve)
})
const dig = hash.digest('hex')
lines.push(
'sha256: ' +
dig +
(dig === known.sha256 ? ' (ok)' : ' (MISMATCH want ' + known.sha256 + ')')
)
}
}
} catch (e) {
lines.push('sha256: (skip ' + String(e && e.message ? e.message : e) + ')')
}
}
} catch (e) {
lines.push('diagnose error: ' + String(e && e.message ? e.message : e))
}
return lines.join('; ')
}
/**
* Extract a GGUF path from a QVAC / llama error string.
* @param {string} msg
* @returns {string}
*/
export function bareOsQvacExtractGgufPath(msg) {
const m = /failed to load model '([^']+\.gguf)'/i.exec(String(msg || ''))
if (m) return m[1]
const m2 = /(\/[^\s']+\.gguf)/i.exec(String(msg || ''))
return m2 ? m2[1] : ''
}
@@ -0,0 +1,768 @@
/**
* Probe host GPUs for QVAC load ordering (highest device-local memory first).
* Uses vulkaninfo when available; nvidia-smi / sysfs as weak fallbacks.
*
* Also prefers hardware Vulkan ICDs (NVIDIA/Intel/AMD) and disables Mesa
* lavapipe/llvmpipe so llama.cpp does not "succeed" on a CPU Vulkan device.
*
* Static bare-subprocess import: dynamic import() from this module was not
* linked in the bare-pack resolution map (MODULE_NOT_FOUND → sdk worker crash).
*/
import bareSubprocess from 'bare-subprocess'
import hostFs from '#host-fs'
import path from '#host-path'
import os from 'bare-os'
/** Standard Vulkan ICD manifest directories (Linux). */
export const BARE_OS_QVAC_VULKAN_ICD_DIRS = [
'/usr/share/vulkan/icd.d',
'/etc/vulkan/icd.d',
'/usr/local/share/vulkan/icd.d'
]
/**
* @typedef {{
* index: number,
* name: string,
* memoryBytes: number,
* deviceType: string,
* source: string
* }} BareOsQvacGpuInfo
*/
/**
* Run a short host command; return stdout (utf8) or null.
* @param {string} file
* @param {string[]} args
* @param {{ timeoutMs?: number }} [opts]
* @returns {Promise<string | null>}
*/
export async function bareOsQvacRunHostCommand(file, args, opts = {}) {
const timeoutMs = Math.max(500, Math.min(30000, Number(opts.timeoutMs) || 8000))
const argv = Array.isArray(args) ? args.map((x) => String(x)) : []
/**
* @param {{ spawn: Function }} sp
*/
async function viaSpawn(sp) {
return await new Promise((resolve) => {
let settled = false
/** @type {string[]} */
const chunks = []
let child
try {
child = sp.spawn(file, argv, {
stdio: ['ignore', 'pipe', 'pipe']
})
} catch {
resolve(null)
return
}
const finish = (out) => {
if (settled) return
settled = true
try {
if (child && typeof child.kill === 'function') child.kill()
} catch {
/* ignore */
}
resolve(out)
}
const timer = setTimeout(() => finish(null), timeoutMs)
try {
if (child.stdout && typeof child.stdout.on === 'function') {
child.stdout.on('data', (buf) => {
chunks.push(Buffer.isBuffer(buf) ? buf.toString('utf8') : String(buf))
})
}
} catch {
/* ignore */
}
const onDone = (code) => {
clearTimeout(timer)
if (code === 0 || chunks.length) finish(chunks.join(''))
else finish(null)
}
try {
child.on('exit', onDone)
child.on('error', () => {
clearTimeout(timer)
finish(null)
})
} catch {
clearTimeout(timer)
finish(null)
}
})
}
try {
const sp =
bareSubprocess &&
(typeof bareSubprocess.spawn === 'function'
? bareSubprocess
: bareSubprocess.default &&
typeof bareSubprocess.default.spawn === 'function'
? bareSubprocess.default
: null)
if (sp) {
const out = await viaSpawn(sp)
if (out != null) return out
}
} catch {
/* try node next */
}
try {
const cp = await import('child_process')
const execFile = cp.execFile || (cp.default && cp.default.execFile)
if (typeof execFile !== 'function') return null
return await new Promise((resolve) => {
execFile(
file,
argv,
{ timeout: timeoutMs, maxBuffer: 8 * 1024 * 1024, encoding: 'utf8' },
(err, stdout) => {
if (err && !stdout) resolve(null)
else resolve(stdout != null ? String(stdout) : null)
}
)
})
} catch {
return null
}
}
/**
* Parse `vulkaninfo` text for GPUs + device-local heap sizes.
* @param {string} text
* @returns {BareOsQvacGpuInfo[]}
*/
export function bareOsQvacParseVulkaninfo(text) {
const src = String(text || '')
if (!src.trim()) return []
/** @type {BareOsQvacGpuInfo[]} */
const out = []
// Split on GPU N: headers (vulkaninfo --summary and full dump).
const parts = src.split(/(?=^GPU\d+:)/m)
for (const part of parts) {
const mIdx = /^GPU(\d+):/m.exec(part)
if (!mIdx) continue
const index = Number.parseInt(mIdx[1], 10)
if (!Number.isFinite(index)) continue
const nameMatch =
/deviceName\s*=\s*(.+)$/m.exec(part) ||
/deviceName\s*=\s*(.+)$/im.exec(part)
const name = nameMatch ? String(nameMatch[1]).trim() : 'GPU' + index
const typeMatch = /deviceType\s*=\s*(\S+)/m.exec(part)
const deviceType = typeMatch ? String(typeMatch[1]).trim() : ''
let memoryBytes = 0
// Full vulkaninfo: memoryHeaps[i] blocks with DEVICE_LOCAL
const heapBlocks = part.split(/(?=memoryHeaps\[\d+\]:)/i)
for (const block of heapBlocks) {
if (!/memoryHeaps\[\d+\]:/i.test(block)) continue
const szMatch = /\bsize\s*=\s*(\d+)/i.exec(block)
if (!szMatch) continue
const sz = Number(szMatch[1])
if (!Number.isFinite(sz) || sz <= 0) continue
if (/DEVICE_LOCAL/i.test(block) || sz > memoryBytes) {
memoryBytes = Math.max(memoryBytes, sz)
}
}
// Alternate: "heapSize = N"
if (!memoryBytes) {
const hs = [...part.matchAll(/heapSize\s*=\s*(\d+)/gi)]
for (const x of hs) {
const sz = Number(x[1])
if (Number.isFinite(sz) && sz > memoryBytes) memoryBytes = sz
}
}
// Summary-only: no heaps — score by type so discrete still ranks above integrated.
if (!memoryBytes) {
if (/DISCRETE/i.test(deviceType)) memoryBytes = 8 * 1024 * 1024 * 1024
else if (/INTEGRATED/i.test(deviceType)) memoryBytes = 512 * 1024 * 1024
else memoryBytes = 256 * 1024 * 1024
}
out.push({
index,
name,
memoryBytes,
deviceType,
source: 'vulkaninfo'
})
}
return out
}
/**
* Parse nvidia-smi CSV: name, memory.total (MiB).
* Indices are NVIDIA order, NOT Vulkan — used only as a presence / size hint.
* @param {string} text
* @returns {{ name: string, memoryBytes: number }[]}
*/
export function bareOsQvacParseNvidiaSmi(text) {
/** @type {{ name: string, memoryBytes: number }[]} */
const out = []
for (const line of String(text || '').split(/\r?\n/)) {
const t = line.trim()
if (!t) continue
const parts = t.split(',').map((x) => x.trim())
if (parts.length < 2) continue
const name = parts[0]
const mib = Number(parts[1])
if (!name || !Number.isFinite(mib) || mib <= 0) continue
out.push({ name, memoryBytes: Math.floor(mib * 1024 * 1024) })
}
return out
}
/**
* Merge nvidia memory hints onto vulkan devices by fuzzy name match.
* @param {BareOsQvacGpuInfo[]} vulkan
* @param {{ name: string, memoryBytes: number }[]} nvidia
*/
export function bareOsQvacMergeNvidiaHints(vulkan, nvidia) {
if (!vulkan.length || !nvidia.length) return vulkan
return vulkan.map((g) => {
const gName = g.name.toLowerCase()
let best = null
let bestScore = 0
for (const n of nvidia) {
const nName = n.name.toLowerCase()
if (gName.includes(nName) || nName.includes(gName) || gName.includes('nvidia')) {
const score = n.memoryBytes
if (score > bestScore) {
bestScore = score
best = n
}
}
}
if (best && best.memoryBytes > g.memoryBytes) {
return {
...g,
memoryBytes: best.memoryBytes,
source: g.source + '+nvidia-smi'
}
}
return g
})
}
/**
* True for Mesa lavapipe / llvmpipe / SwiftShader / Vulkan CPU devices.
* @param {{ name?: string, deviceType?: string } | null | undefined} g
*/
export function bareOsQvacIsSoftwareVulkanDevice(g) {
if (!g || typeof g !== 'object') return false
const name = String(g.name || '').toLowerCase()
const type = String(g.deviceType || '').toLowerCase()
if (
/llvmpipe|lavapipe|swiftshader|softpipe|cpu rasterizer|microsoft basic render/.test(
name
)
) {
return true
}
if (
type === 'cpu' ||
type.includes('physical_device_type_cpu') ||
/(^|_)cpu($|_)/.test(type)
) {
return true
}
return false
}
/**
* @param {BareOsQvacGpuInfo[]} gpus
* @returns {BareOsQvacGpuInfo[]}
*/
export function bareOsQvacHardwareGpus(gpus) {
return (Array.isArray(gpus) ? gpus : []).filter(
(g) => !bareOsQvacIsSoftwareVulkanDevice(g)
)
}
/**
* Score an ICD JSON filename — higher is preferred; software drivers are negative.
* @param {string} filename
*/
export function bareOsQvacScoreVulkanIcdFilename(filename) {
const n = String(filename || '').toLowerCase()
if (/lvp|llvmpipe|lavapipe|swiftshader/.test(n)) return -100
if (/gfxstream|virtio/.test(n)) return -50
if (/nvidia/.test(n)) return 100
if (/radeon|amd|radv/.test(n)) return 80
if (/intel|anv|iris/.test(n)) return 50
if (/moltenvk|apple/.test(n)) return 40
if (/asahi/.test(n)) return 20
if (/nouveau/.test(n)) return 15
return 10
}
/**
* Vendor family for a Vulkan ICD manifest filename.
* @param {string} filename
* @returns {'software' | 'virtual' | 'nvidia' | 'amd' | 'intel' | 'apple' | 'asahi' | 'nouveau' | 'other'}
*/
export function bareOsQvacVulkanIcdVendor(filename) {
const n = String(filename || '').toLowerCase()
if (/lvp|llvmpipe|lavapipe|swiftshader/.test(n)) return 'software'
if (/gfxstream|virtio/.test(n)) return 'virtual'
if (/nvidia/.test(n)) return 'nvidia'
if (/radeon|amd|radv/.test(n)) return 'amd'
if (/intel|anv|hasvk|iris/.test(n)) return 'intel'
if (/moltenvk/.test(n)) return 'apple'
if (/asahi/.test(n)) return 'asahi'
if (/nouveau/.test(n)) return 'nouveau'
return 'other'
}
/**
* VK_LOADER_DRIVERS_SELECT glob for a preferred vendor.
* @param {string} vendor
*/
export function bareOsQvacVulkanVendorSelectGlob(vendor) {
switch (vendor) {
case 'nvidia':
return '*nvidia*'
case 'amd':
return '*radeon*,*amd*,*radv*'
case 'intel':
return '*intel*'
case 'apple':
return '*moltenvk*'
case 'asahi':
return '*asahi*'
case 'nouveau':
return '*nouveau*'
default:
return ''
}
}
/**
* Keep ICDs for the highest-scored vendor so ggml does not enumerate unused
* Mesa/emulator manifests (asahi/gfxstream/nouveau/virtio) next to RADV.
* `icds` must already be score-sorted (best first), as from discover.
* @param {string[]} icds
* @returns {{ vendor: string, icds: string[], selectGlob: string }}
*/
export function bareOsQvacPreferredVulkanIcds(icds) {
const list = (Array.isArray(icds) ? icds : []).filter(
(p) => typeof p === 'string' && p
)
if (!list.length) return { vendor: '', icds: [], selectGlob: '' }
const vendor = bareOsQvacVulkanIcdVendor(path.basename(list[0]))
if (vendor === 'software' || vendor === 'virtual' || vendor === 'other') {
return { vendor, icds: list, selectGlob: '' }
}
const filtered = list.filter(
(p) => bareOsQvacVulkanIcdVendor(path.basename(p)) === vendor
)
return {
vendor,
icds: filtered.length ? filtered : list,
selectGlob: bareOsQvacVulkanVendorSelectGlob(vendor)
}
}
/**
* List hardware Vulkan ICD manifest paths (software ICDs excluded).
* @param {{
* dirs?: string[],
* readdirSync?: (dir: string) => string[],
* existsSync?: (p: string) => boolean
* }} [opts]
* @returns {string[]}
*/
export function bareOsQvacDiscoverHardwareVulkanIcds(opts = {}) {
const dirs = Array.isArray(opts.dirs) ? opts.dirs : BARE_OS_QVAC_VULKAN_ICD_DIRS
const exists =
typeof opts.existsSync === 'function'
? opts.existsSync
: (p) => {
try {
return hostFs.existsSync(p)
} catch {
return false
}
}
const readdir =
typeof opts.readdirSync === 'function'
? opts.readdirSync
: (dir) => {
try {
return hostFs.readdirSync(dir)
} catch {
return []
}
}
/** @type {{ path: string, score: number }[]} */
const found = []
for (const dir of dirs) {
if (!exists(dir)) continue
let names = []
try {
names = readdir(dir) || []
} catch {
continue
}
for (const name of names) {
const base = String(name)
if (!base.toLowerCase().endsWith('.json')) continue
const score = bareOsQvacScoreVulkanIcdFilename(base)
if (score < 0) continue
found.push({ path: path.join(dir, base), score })
}
}
found.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path))
return found.map((x) => x.path)
}
/**
* Prefer real GPU Vulkan drivers before llama.cpp / vulkaninfo enumerate.
* Disables lavapipe and, on Optimus laptops, nudges NVIDIA offload.
*
* @param {Record<string, unknown>} [env] guest/host env overlay (BARE_OS_*)
* @param {{
* procEnv?: Record<string, string | undefined> | null,
* icds?: string[],
* hasNvidiaSmi?: boolean
* }} [opts]
* @returns {{
* note: string,
* icds: string[],
* disabledSoftware: boolean,
* preferNvidia: boolean,
* preferAmd: boolean,
* vendor: string
* }}
*/
export function bareOsQvacApplyHardwareVulkanEnv(env = {}, opts = {}) {
const procEnv =
opts.procEnv !== undefined
? opts.procEnv
: globalThis.process && globalThis.process.env
? globalThis.process.env
: null
const empty = {
note: '',
icds: /** @type {string[]} */ ([]),
disabledSoftware: false,
preferNvidia: false,
preferAmd: false,
vendor: ''
}
if (!procEnv) return empty
const keep = String(
env.BARE_OS_QVAC_KEEP_VK_ENV ?? procEnv.BARE_OS_QVAC_KEEP_VK_ENV ?? ''
)
.trim()
.toLowerCase()
if (keep === '1' || keep === 'true') {
return { ...empty, note: 'BARE_OS_QVAC_KEEP_VK_ENV — left Vulkan env unchanged' }
}
const softwareDisableGlob = '*lvp*,*lavapipe*,*swiftshader*'
const prevDisable = String(procEnv.VK_LOADER_DRIVERS_DISABLE || '').trim()
if (!prevDisable) {
procEnv.VK_LOADER_DRIVERS_DISABLE = softwareDisableGlob
} else if (!/lvp|lavapipe|swiftshader/i.test(prevDisable)) {
procEnv.VK_LOADER_DRIVERS_DISABLE = prevDisable + ',' + softwareDisableGlob
}
const discovered =
Array.isArray(opts.icds) && opts.icds.length
? opts.icds
: bareOsQvacDiscoverHardwareVulkanIcds()
const preferred = bareOsQvacPreferredVulkanIcds(discovered)
const preferNvidia =
preferred.vendor === 'nvidia' || Boolean(opts.hasNvidiaSmi)
const preferAmd = preferred.vendor === 'amd' && !preferNvidia
const vendor = preferNvidia ? 'nvidia' : preferred.vendor
// nvidia-smi without a listed NVIDIA ICD (unusual install): keep the full
// hardware list and let VK_LOADER_DRIVERS_SELECT=*nvidia* filter at load.
const icds =
preferNvidia && preferred.vendor !== 'nvidia'
? discovered
: preferred.icds
const userDrivers = String(
env.BARE_OS_QVAC_VK_DRIVER_FILES ??
procEnv.BARE_OS_QVAC_VK_DRIVER_FILES ??
''
).trim()
const alreadyForced = Boolean(
String(procEnv.VK_DRIVER_FILES || '').trim() ||
String(procEnv.VK_ICD_FILENAMES || '').trim() ||
userDrivers
)
if (userDrivers && !String(procEnv.VK_DRIVER_FILES || '').trim()) {
procEnv.VK_DRIVER_FILES = userDrivers
} else if (!alreadyForced && icds.length) {
// One vendor's manifests only — AMD boxes often ship unused Mesa ICDs
// (intel/asahi/nouveau/virtio/gfxstream) that confuse ggml enumeration.
procEnv.VK_DRIVER_FILES = icds.join(':')
}
const selectMode = String(
env.BARE_OS_QVAC_VK_SELECT ?? procEnv.BARE_OS_QVAC_VK_SELECT ?? 'auto'
)
.trim()
.toLowerCase()
// Hybrid Intel+NVIDIA/AMD: bind the discrete vendor so ggml never lands on
// iGPU/llvmpipe. Set BARE_OS_QVAC_VK_SELECT=off to keep every hardware ICD.
const selectGlob = preferNvidia
? '*nvidia*'
: preferred.selectGlob
const wantSelect =
selectGlob &&
(selectMode === 'auto' ||
selectMode === vendor ||
(preferNvidia && selectMode === 'nvidia'))
if (wantSelect && !String(procEnv.VK_LOADER_DRIVERS_SELECT || '').trim()) {
procEnv.VK_LOADER_DRIVERS_SELECT = selectGlob
}
if (preferNvidia) {
if (!String(procEnv.__NV_PRIME_RENDER_OFFLOAD || '').trim()) {
procEnv.__NV_PRIME_RENDER_OFFLOAD = '1'
}
if (!String(procEnv.__GLX_VENDOR_LIBRARY_NAME || '').trim()) {
procEnv.__GLX_VENDOR_LIBRARY_NAME = 'nvidia'
}
if (!String(procEnv.__VK_LAYER_NV_optimus || '').trim()) {
procEnv.__VK_LAYER_NV_optimus = 'NVIDIA_only'
}
}
const noteParts = ['disabled software Vulkan ICDs (lavapipe/llvmpipe)']
if (icds.length) {
noteParts.push(
'hardware ICDs=' + icds.map((p) => path.basename(p)).join(',')
)
}
if (preferNvidia) noteParts.push('NVIDIA Optimus offload env set')
if (preferAmd) noteParts.push('RADV/AMD ICD select')
const selectNow = String(procEnv.VK_LOADER_DRIVERS_SELECT || '')
if (selectNow) {
noteParts.push('VK_LOADER_DRIVERS_SELECT=' + selectNow)
}
return {
note: noteParts.join('; '),
icds,
disabledSoftware: true,
preferNvidia,
preferAmd,
vendor
}
}
/**
* True on macOS. ggml-metal is the GPU backend there (no Vulkan runtime).
* @returns {boolean}
*/
export function bareOsQvacHostIsDarwin() {
try {
if (typeof os.platform === 'function') return os.platform() === 'darwin'
} catch {
/* fall back to process below */
}
return Boolean(
globalThis.process && globalThis.process.platform === 'darwin'
)
}
/**
* Parse a Metal VRAM field ("18 GB", "Shared system memory: 18 GB", or a
* nested `{ spdisplays_vram: "16 GB" }`) to bytes. Returns 0 when absent.
* @param {unknown} v
* @returns {number}
*/
export function bareOsQvacParseMetalMemory(v) {
const raw =
v && typeof v === 'object' && !Array.isArray(v)
? String(
/** @type {Record<string, unknown>} */ (v).spdisplays_vram || ''
)
: String(v || '')
const m = /(\d+(?:\.\d+)?)\s*(MB|GB|TB)/i.exec(raw)
if (!m) return 0
const n = Number(m[1])
const unit = String(m[2]).toUpperCase()
const mult =
unit === 'GB'
? 1024 * 1024 * 1024
: unit === 'TB'
? 1024 * 1024 * 1024 * 1024
: 1024 * 1024
return Math.floor(n * mult)
}
/**
* Parse `system_profiler SPDisplaysDataType -json` for Apple GPUs.
* @param {string} text
* @returns {BareOsQvacGpuInfo[]}
*/
export function bareOsQvacParseSystemProfilerDisplays(text) {
/** @type {BareOsQvacGpuInfo[]} */
const out = []
let data
try {
data = JSON.parse(String(text || ''))
} catch {
return out
}
const list = Array.isArray(data && data.SPDisplaysDataType)
? /** @type {Array<Record<string, unknown>>} */ (data.SPDisplaysDataType)
: []
const seen = new Set()
for (const item of list) {
if (!item || typeof item !== 'object') continue
const name = String(
item.chipset_model || item._name || ''
).trim()
if (!name || seen.has(name)) continue
seen.add(name)
const vramRaw = item.spdisplays_vram ?? item.spdisplays_vram_shared ?? ''
const memoryBytes = bareOsQvacParseMetalMemory(vramRaw)
const eGpu = /egpu|external/i.test(String(item.sppci_device_type || ''))
const discrete = eGpu || Boolean(item.spdisplays_vram)
out.push({
index: out.length,
name,
memoryBytes,
deviceType: discrete
? 'PHYSICAL_DEVICE_TYPE_DISCRETE_GPU'
: 'PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU',
source: 'metal'
})
}
return out
}
/**
* Sort GPUs for load attempts: highest memory first; discrete before integrated on ties.
* Software Vulkan devices are ranked last.
* @param {BareOsQvacGpuInfo[]} gpus
* @returns {BareOsQvacGpuInfo[]}
*/
export function bareOsQvacRankGpus(gpus) {
return [...gpus].sort((a, b) => {
const as = bareOsQvacIsSoftwareVulkanDevice(a) ? 1 : 0
const bs = bareOsQvacIsSoftwareVulkanDevice(b) ? 1 : 0
if (as !== bs) return as - bs
if (b.memoryBytes !== a.memoryBytes) return b.memoryBytes - a.memoryBytes
const ad = /DISCRETE/i.test(a.deviceType) ? 1 : 0
const bd = /DISCRETE/i.test(b.deviceType) ? 1 : 0
if (bd !== ad) return bd - ad
return a.index - b.index
})
}
/**
* Build ordered main-gpu candidates for auto mode.
* @param {BareOsQvacGpuInfo[]} ranked
* @returns {Array<string | number>}
*/
export function bareOsQvacMainGpuCandidates(ranked) {
/** @type {Array<string | number>} */
const out = []
const seen = new Set()
const push = (v) => {
const k = String(v)
if (seen.has(k)) return
seen.add(k)
out.push(v)
}
const hardware = bareOsQvacHardwareGpus(ranked)
const vulkanBacked = hardware.filter((g) =>
String(g.source || '').includes('vulkaninfo')
)
if (vulkanBacked.length) {
for (const g of vulkanBacked) push(g.index)
push('dedicated')
return out
}
// nvidia-smi indices ≠ Vulkan indices on hybrid laptops — prefer class + common slots.
if (hardware.length) {
push('dedicated')
push(1)
push(0)
push(2)
push(3)
return out
}
push('dedicated')
push(1)
push(0)
push(2)
push(3)
return out
}
/**
* Probe host GPUs (best-effort). Never throws.
* @param {{ runCommand?: typeof bareOsQvacRunHostCommand }} [opts]
* @returns {Promise<BareOsQvacGpuInfo[]>}
*/
export async function bareOsQvacProbeGpus(opts = {}) {
try {
const run = opts.runCommand || bareOsQvacRunHostCommand
// macOS: no vulkaninfo/nvidia-smi. Metal is the only GPU backend, so probe
// the Apple GPU via system_profiler for name/VRAM reporting.
if (bareOsQvacHostIsDarwin()) {
const sp = await run('system_profiler', ['SPDisplaysDataType', '-json'], {
timeoutMs: 15000
})
const metal = bareOsQvacParseSystemProfilerDisplays(sp || '')
return bareOsQvacRankGpus(metal)
}
/** @type {BareOsQvacGpuInfo[]} */
let vulkan = []
const full =
(await run('vulkaninfo', [], { timeoutMs: 12000 })) ||
(await run('vulkaninfo', ['--summary'], { timeoutMs: 8000 }))
if (full) vulkan = bareOsQvacParseVulkaninfo(full)
const smi = await run(
'nvidia-smi',
['--query-gpu=name,memory.total', '--format=csv,noheader,nounits'],
{ timeoutMs: 5000 }
)
const nvidia = smi ? bareOsQvacParseNvidiaSmi(smi) : []
if (vulkan.length && nvidia.length) {
vulkan = bareOsQvacMergeNvidiaHints(vulkan, nvidia)
} else if (!vulkan.length && nvidia.length) {
// No Vulkan enumeration — synthesize placeholders; load path still tries dedicated + indices.
vulkan = nvidia.map((n, i) => ({
index: i,
name: n.name,
memoryBytes: n.memoryBytes,
deviceType: 'PHYSICAL_DEVICE_TYPE_DISCRETE_GPU',
source: 'nvidia-smi'
}))
}
// Drop software adapters from the preferred list (keep rank helper for tests).
return bareOsQvacRankGpus(vulkan)
} catch {
return []
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,600 @@
/**
* QVAC GGUF storage: HDMS /mnt/models (durable) + host cache for llama.cpp load.
*/
import path from '#host-path'
import fs from '#host-fs'
import {
bareOsQvacHostModelsDir,
bareOsQvacHostConfigPath
} from './paths.js'
export const BARE_OS_QVAC_MODELS_HDMS_LABEL = 'models'
export const BARE_OS_QVAC_MODELS_GUEST_ROOT = '/mnt/models'
/**
* @param {string} [hostCacheDir]
* @returns {string}
*/
export function bareOsQvacModelsHostCacheDir(hostCacheDir) {
const d = String(hostCacheDir || '').trim()
return d || bareOsQvacHostModelsDir()
}
/**
* Guest path for a GGUF basename under /mnt/models.
* @param {string} fileName
*/
export function bareOsQvacModelsGuestPath(fileName) {
const base = String(fileName || '')
.replace(/^\/+/, '')
.split('/')
.pop()
return BARE_OS_QVAC_MODELS_GUEST_ROOT + '/' + (base || 'model.gguf')
}
/**
* Host path for a GGUF basename under the cache dir.
* @param {string} fileName
* @param {string} [hostCacheDir]
*/
export function bareOsQvacModelsHostPath(fileName, hostCacheDir) {
const base = String(fileName || '')
.replace(/^\/+/, '')
.split('/')
.pop()
return path.join(bareOsQvacModelsHostCacheDir(hostCacheDir), base || 'model.gguf')
}
/**
* Strip leading content-hash prefix used by QVAC cache filenames.
* `5b8aae816570a09d_Qwen3-0.6B-Q4_0.gguf` → `Qwen3-0.6B-Q4_0.gguf`
* @param {string} fileName
*/
export function bareOsQvacModelsStripHashPrefix(fileName) {
const base =
String(fileName || '')
.replace(/^\/+/, '')
.split('/')
.pop() || ''
const m = /^[0-9a-f]{8,}_(.+)$/i.exec(base)
return m ? m[1] : base
}
/**
* Whether host file size matches an expected guest size (skip materialize).
* @param {string} hostPath
* @param {number} guestSize
*/
export function bareOsQvacHostGgufMatchesSize(hostPath, guestSize) {
const want = Number(guestSize)
if (!Number.isFinite(want) || want <= 0) return false
try {
const st = fs.statSync(hostPath)
return st.isFile() && st.size === want
} catch {
return false
}
}
/**
* True for a host filesystem path Bare's module resolver cannot load from
* `bare:/app.bundle` (absolute POSIX / Windows, not a URL specifier).
* @param {string} filePath
*/
export function bareOsQvacIsHostJsonConfigPath(filePath) {
const p = String(filePath || '')
if (!/\.json$/i.test(p)) return false
// Windows drive paths (`C:\...`) look like a URL scheme if checked first.
if (/^[A-Za-z]:[\\/]/.test(p)) return true
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/i.test(p)) return false
return p.startsWith('/')
}
/**
* Read a host JSON file for QVAC config. Returns undefined when the path is
* not a host .json file or the read/parse fails (caller may delegate).
* @param {unknown} id
* @returns {unknown | undefined}
*/
export function bareOsQvacTryReadHostJsonConfig(id) {
const filePath = String(id || '')
if (!bareOsQvacIsHostJsonConfigPath(filePath)) return undefined
try {
const raw = fs.readFileSync(filePath, 'utf8')
return JSON.parse(raw)
} catch {
return undefined
}
}
/**
* Bare's `@qvac/sdk` resolve-config.bare.js loads config via free `require(path)`.
* In standalone that identifier is `globalThis.require` from ctx-bare's
* `createRequire(bare:/app.bundle/.../bare-os-ctx-bare.js)`. That resolver
* cannot see host files (`MODULE_NOT_FOUND` with `bare:/home/...` candidates).
* Always wrap — do not skip when require already exists.
*/
export function bareOsQvacInstallConfigRequireShim() {
const g = globalThis
const prev = typeof g.require === 'function' ? g.require : null
if (prev && prev.__bareOsQvacConfigRequire) return prev
function bareOsQvacConfigRequire(id, ...rest) {
const parsed = bareOsQvacTryReadHostJsonConfig(id)
if (parsed !== undefined) return parsed
if (typeof prev === 'function') {
return Reflect.apply(prev, this, [id, ...rest])
}
throw new ReferenceError(
'require is not defined (Bare: only host .json config paths are shimmed)'
)
}
bareOsQvacConfigRequire.__bareOsQvacConfigRequire = true
if (prev) {
for (const key of ['cache', 'extensions', 'main', 'resolve', 'addon', 'asset']) {
try {
if (prev[key] !== undefined) bareOsQvacConfigRequire[key] = prev[key]
} catch {
/* ignore */
}
}
}
g.require = bareOsQvacConfigRequire
return bareOsQvacConfigRequire
}
/**
* Write runtime qvac.config.json with absolute cacheDirectory and set QVAC_CONFIG_PATH.
* Must run before `@qvac/sdk` plugins() so setSDKConfig sees it once.
* @param {{
* cacheDir?: string,
* loggerLevel?: string,
* loggerConsoleOutput?: boolean
* }} [opts]
* @returns {{ cacheDir: string, configPath: string }}
*/
export function bareOsQvacPrepareHostCacheConfig(opts = {}) {
bareOsQvacInstallConfigRequireShim()
const cacheDir = bareOsQvacModelsHostCacheDir(opts.cacheDir)
const configPath = bareOsQvacHostConfigPath()
const qvacRoot = path.dirname(configPath)
fs.mkdirSync(cacheDir, { recursive: true })
fs.mkdirSync(qvacRoot, { recursive: true })
const procEnv =
globalThis.process && globalThis.process.env
? globalThis.process.env
: null
// Quiet by default — SDK debug + llamacpp chatter floods the agent TTY.
// Override with BARE_OS_QVAC_LOG_LEVEL / QVAC_LOG_LEVEL (e.g. debug).
const envLevel = procEnv
? String(
procEnv.BARE_OS_QVAC_LOG_LEVEL || procEnv.QVAC_LOG_LEVEL || ''
).trim()
: ''
const loggerLevel = opts.loggerLevel || envLevel || 'error'
let loggerConsoleOutput = opts.loggerConsoleOutput
if (loggerConsoleOutput === undefined && procEnv) {
const ec = String(procEnv.BARE_OS_QVAC_LOGGER_CONSOLE || '').trim()
if (ec === '1' || ec === 'true') loggerConsoleOutput = true
else if (ec === '0' || ec === 'false') loggerConsoleOutput = false
}
if (loggerConsoleOutput === undefined) {
// Keep console on so real errors still surface; level gates noise.
loggerConsoleOutput = true
}
const body = {
plugins: ['@qvac/sdk/llamacpp-completion/plugin'],
cacheDirectory: cacheDir,
loggerLevel,
loggerConsoleOutput: Boolean(loggerConsoleOutput),
httpDownloadConcurrency: 3,
httpConnectionTimeoutMs: 15000
}
fs.writeFileSync(configPath, JSON.stringify(body, null, 2) + '\n')
if (procEnv) {
procEnv.QVAC_CONFIG_PATH = configPath
// Also pin env so @qvac/logging picks the same level before config apply.
if (!procEnv.QVAC_LOG_LEVEL) procEnv.QVAC_LOG_LEVEL = loggerLevel
}
return { cacheDir, configPath }
}
/**
* Ensure writable HDMS label `models` exists (best-effort).
* @param {{
* ctx?: Record<string, unknown> | null,
* hdmsController?: {
* active?: boolean,
* byLabel?: Map<string, unknown>,
* create?: (ctx: Record<string, unknown>, label: string) => Promise<void>
* } | null
* }} opts
* @returns {Promise<{ ok: boolean, created?: boolean, reason?: string, drive?: unknown }>}
*/
export async function bareOsQvacEnsureModelsHdms(opts = {}) {
const hc = opts.hdmsController
const ctx = opts.ctx
if (!hc || typeof hc.create !== 'function') {
return { ok: false, reason: 'no_hdms_controller' }
}
if (!hc.active) return { ok: false, reason: 'hdms_inactive' }
if (hc.byLabel && hc.byLabel.has(BARE_OS_QVAC_MODELS_HDMS_LABEL)) {
const slot = /** @type {{ drive?: unknown }} */ (
hc.byLabel.get(BARE_OS_QVAC_MODELS_HDMS_LABEL)
)
return { ok: true, created: false, drive: slot && slot.drive }
}
if (!ctx || typeof ctx !== 'object') {
return { ok: false, reason: 'no_ctx' }
}
try {
await hc.create(ctx, BARE_OS_QVAC_MODELS_HDMS_LABEL)
const slot = hc.byLabel
? /** @type {{ drive?: unknown }} */ (
hc.byLabel.get(BARE_OS_QVAC_MODELS_HDMS_LABEL)
)
: null
return { ok: true, created: true, drive: slot && slot.drive }
} catch (e) {
const msg =
e && typeof e === 'object' && 'message' in e
? String(/** @type {{ message: unknown }} */ (e).message)
: String(e)
if (/already exists/i.test(msg)) {
const slot = hc.byLabel
? /** @type {{ drive?: unknown }} */ (
hc.byLabel.get(BARE_OS_QVAC_MODELS_HDMS_LABEL)
)
: null
return { ok: true, created: false, drive: slot && slot.drive }
}
return { ok: false, reason: msg }
}
}
/**
* @param {unknown} vfs
* @param {string} guestPath
* @returns {Promise<{ size: number } | null>}
*/
export async function bareOsQvacGuestGgufStat(vfs, guestPath) {
if (!vfs || typeof vfs !== 'object') return null
const v = /** @type {Record<string, any>} */ (vfs)
try {
if (typeof v.stat === 'function') {
const st = await v.stat(guestPath)
const size = Number(st && (st.size ?? st.byteLength))
if (Number.isFinite(size) && size > 0) return { size }
}
} catch {
/* missing */
}
return null
}
/**
* Stream Hyperdrive path → host file (avoid buffering full GGUF).
* @param {{ createReadStream?: (p: string) => import('stream').Readable, get?: Function }} drive
* @param {string} drivePath e.g. /Qwen3-0.6B-Q4_0.gguf
* @param {string} hostPath
* @returns {Promise<number>} bytes written
*/
export async function bareOsQvacStreamDriveToHost(drive, drivePath, hostPath) {
const rel = String(drivePath || '').startsWith('/')
? String(drivePath)
: '/' + String(drivePath || '').replace(/^\/+/, '')
if (drive && typeof drive.createReadStream === 'function') {
await new Promise((resolve, reject) => {
let settled = false
const fail = (err) => {
if (settled) return
settled = true
reject(err)
}
const done = () => {
if (settled) return
settled = true
resolve(undefined)
}
try {
const rs = drive.createReadStream(rel)
const ws = fs.createWriteStream(hostPath)
rs.on('error', fail)
ws.on('error', fail)
ws.on('finish', done)
if (typeof rs.pipe === 'function') {
rs.pipe(ws)
} else {
rs.on('data', (chunk) => {
ws.write(chunk)
})
rs.on('end', () => ws.end())
}
} catch (e) {
fail(e)
}
})
return fs.statSync(hostPath).size
}
if (drive && typeof drive.get === 'function') {
const buf = await drive.get(rel)
if (!buf || !(buf.byteLength > 0 || buf.length > 0)) {
throw new Error('empty_drive_get')
}
const u8 = Buffer.isBuffer(buf)
? buf
: Buffer.from(
buf.buffer || buf,
buf.byteOffset || 0,
buf.byteLength || buf.length
)
fs.writeFileSync(hostPath, u8)
return u8.byteLength
}
throw new Error('drive_no_stream')
}
/**
* Materialize guest GGUF → host cache path.
* Prefers Hyperdrive createReadStream when `drive` is provided; else vfs.readFile.
* Skips when host size already matches guest size.
* @param {{
* vfs?: Record<string, any> | null,
* guestPath: string,
* hostPath: string,
* drive?: { createReadStream?: Function, get?: Function } | null,
* drivePath?: string,
* guestSize?: number
* }} o
* @returns {Promise<{ ok: boolean, bytes?: number, reason?: string, skipped?: boolean }>}
*/
export async function bareOsQvacMaterializeGgufToHost(o) {
const guestPath = String(o.guestPath || '')
const hostPath = String(o.hostPath || '')
if (!guestPath || !hostPath) {
return { ok: false, reason: 'missing_args' }
}
try {
fs.mkdirSync(path.dirname(hostPath), { recursive: true })
} catch {
/* ignore */
}
let guestSize = Number(o.guestSize)
if (!(Number.isFinite(guestSize) && guestSize > 0) && o.vfs) {
const st = await bareOsQvacGuestGgufStat(o.vfs, guestPath)
if (st) guestSize = st.size
}
if (
Number.isFinite(guestSize) &&
guestSize > 0 &&
bareOsQvacHostGgufMatchesSize(hostPath, guestSize)
) {
return { ok: true, bytes: guestSize, skipped: true, reason: 'size_match' }
}
const base =
guestPath.replace(/^\/+/, '').split('/').pop() || path.basename(hostPath)
const drivePath =
o.drivePath ||
(base.startsWith('/') ? base : '/' + base)
if (o.drive) {
try {
const bytes = await bareOsQvacStreamDriveToHost(
o.drive,
drivePath,
hostPath
)
return { ok: true, bytes }
} catch (e) {
const msg =
e && typeof e === 'object' && 'message' in e
? String(/** @type {{ message: unknown }} */ (e).message)
: String(e)
// Fall through to vfs.readFile
if (!o.vfs || typeof o.vfs.readFile !== 'function') {
return { ok: false, reason: msg }
}
}
}
try {
if (!o.vfs || typeof o.vfs.readFile !== 'function') {
return { ok: false, reason: 'vfs_no_readFile' }
}
const buf = await o.vfs.readFile(guestPath)
if (!buf || !(buf.byteLength > 0 || buf.length > 0)) {
return { ok: false, reason: 'empty_guest' }
}
const u8 = Buffer.isBuffer(buf)
? buf
: Buffer.from(
buf.buffer || buf,
buf.byteOffset || 0,
buf.byteLength || buf.length
)
fs.writeFileSync(hostPath, u8)
return { ok: true, bytes: u8.byteLength }
} catch (e) {
const msg =
e && typeof e === 'object' && 'message' in e
? String(/** @type {{ message: unknown }} */ (e).message)
: String(e)
return { ok: false, reason: msg }
}
}
/**
* Mirror host GGUF into /mnt/models/<basename>.
* Skips when guest size already matches host.
* @param {{
* vfs: Record<string, any>,
* hostPath: string,
* guestName?: string
* }} o
* @returns {Promise<{ ok: boolean, guestPath?: string, reason?: string, skipped?: boolean }>}
*/
export async function bareOsQvacMirrorHostGgufToHdms(o) {
const hostPath = String(o.hostPath || '')
if (!hostPath || !o.vfs || typeof o.vfs.writeFile !== 'function') {
return { ok: false, reason: 'missing_args' }
}
const base =
String(o.guestName || '')
.replace(/^\/+/, '')
.split('/')
.pop() || path.basename(hostPath)
const guestPath = bareOsQvacModelsGuestPath(base)
let hostSize = 0
try {
hostSize = fs.statSync(hostPath).size
} catch {
return { ok: false, reason: 'host_missing' }
}
if (!(hostSize > 0)) return { ok: false, reason: 'empty_host' }
const guest = await bareOsQvacGuestGgufStat(o.vfs, guestPath)
if (guest && guest.size === hostSize) {
return { ok: true, guestPath, skipped: true, reason: 'size_match' }
}
try {
const buf = fs.readFileSync(hostPath)
if (!buf || !buf.byteLength) return { ok: false, reason: 'empty_host' }
await o.vfs.writeFile(guestPath, buf)
return { ok: true, guestPath }
} catch (e) {
const msg =
e && typeof e === 'object' && 'message' in e
? String(/** @type {{ message: unknown }} */ (e).message)
: String(e)
return { ok: false, reason: msg }
}
}
/**
* List GGUF basenames under /mnt/models (best-effort).
* @param {Record<string, any>} vfs
* @returns {Promise<string[]>}
*/
export async function bareOsQvacListHdmsGgufs(vfs) {
if (!vfs || typeof vfs.readdir !== 'function') return []
try {
const entries = await vfs.readdir(BARE_OS_QVAC_MODELS_GUEST_ROOT)
if (!Array.isArray(entries)) return []
return entries
.map((e) => {
if (typeof e === 'string') return e
if (e && typeof e === 'object' && 'name' in e) return String(e.name)
return ''
})
.filter((n) => /\.gguf$/i.test(n))
} catch {
return []
}
}
/**
* List GGUF basenames in host cache dir.
* @param {string} [hostCacheDir]
* @returns {string[]}
*/
export function bareOsQvacListHostGgufs(hostCacheDir) {
const dir = bareOsQvacModelsHostCacheDir(hostCacheDir)
try {
return fs
.readdirSync(dir)
.filter((n) => typeof n === 'string' && /\.gguf$/i.test(n))
} catch {
return []
}
}
/**
* Find a guest GGUF matching model id / cache basename.
* @param {Record<string, any>} vfs
* @param {string} hintName e.g. Qwen3-0.6B-Q4_0.gguf or hashed cache name
* @returns {Promise<string | null>} guest absolute path
*/
export async function bareOsQvacFindHdmsGguf(vfs, hintName) {
const names = await bareOsQvacListHdmsGgufs(vfs)
if (!names.length) return null
const hint = String(hintName || '')
const stripped = bareOsQvacModelsStripHashPrefix(hint)
for (const n of names) {
if (n === hint || n === stripped) return bareOsQvacModelsGuestPath(n)
if (stripped && n.endsWith(stripped)) return bareOsQvacModelsGuestPath(n)
if (hint && n.includes(stripped)) return bareOsQvacModelsGuestPath(n)
}
return null
}
/**
* Copy any HDMS GGUFs missing/mismatched on the host cache (before loadModel).
* @param {{
* vfs: Record<string, any>,
* hostCacheDir?: string,
* drive?: { createReadStream?: Function, get?: Function } | null
* }} o
* @returns {Promise<{ materialized: string[], skipped: string[], errors: string[] }>}
*/
export async function bareOsQvacMaterializeHdmsModelsToHost(o) {
/** @type {string[]} */
const materialized = []
/** @type {string[]} */
const skipped = []
/** @type {string[]} */
const errors = []
const cacheDir = bareOsQvacModelsHostCacheDir(o.hostCacheDir)
try {
fs.mkdirSync(cacheDir, { recursive: true })
} catch {
/* ignore */
}
const names = await bareOsQvacListHdmsGgufs(o.vfs)
for (const name of names) {
const guestPath = bareOsQvacModelsGuestPath(name)
const hostPath = bareOsQvacModelsHostPath(name, cacheDir)
const r = await bareOsQvacMaterializeGgufToHost({
vfs: o.vfs,
guestPath,
hostPath,
drive: o.drive || null,
drivePath: '/' + name
})
if (r.ok && r.skipped) skipped.push(name)
else if (r.ok) materialized.push(name)
else errors.push(name + ': ' + (r.reason || 'fail'))
}
return { materialized, skipped, errors }
}
/**
* Mirror host-cache GGUFs into HDMS after download/load (best-effort).
* @param {{
* vfs: Record<string, any>,
* hostCacheDir?: string
* }} o
* @returns {Promise<{ mirrored: string[], skipped: string[], errors: string[] }>}
*/
export async function bareOsQvacMirrorHostModelsToHdms(o) {
/** @type {string[]} */
const mirrored = []
/** @type {string[]} */
const skipped = []
/** @type {string[]} */
const errors = []
const names = bareOsQvacListHostGgufs(o.hostCacheDir)
for (const name of names) {
const hostPath = bareOsQvacModelsHostPath(name, o.hostCacheDir)
const r = await bareOsQvacMirrorHostGgufToHdms({
vfs: o.vfs,
hostPath,
guestName: name
})
if (r.ok && r.skipped) skipped.push(name)
else if (r.ok) mirrored.push(name)
else errors.push(name + ': ' + (r.reason || 'fail'))
}
return { mirrored, skipped, errors }
}
@@ -0,0 +1,16 @@
/**
* Pack graph anchor for QVAC.
* Static import so bare-pack embeds the bind module + @qvac/sdk (dynamic
* import() alone was not present in app.bundle at runtime).
*
* Loaded from pack-imports / host only; SDK JS evaluates when this module loads.
* When packing with --skip-qvac, `@qvac/sdk` is remapped to stubs.
*/
import * as qvacSdkBind from './bare-os-qvac-sdk-bind.mjs'
export { qvacSdkBind }
/** @deprecated kept for pack-imports reference */
export function bareOsQvacEnsurePackGraph() {
return qvacSdkBind
}
@@ -0,0 +1,473 @@
/**
* Static @qvac/sdk binding for Bare standalone pack + in-process Bare.
*
* Imported only via relative dynamic `import('./bare-os-qvac-sdk-bind.mjs')` so
* booter startup does not evaluate the SDK (avoids CodeRange OOM). bare-pack
* still embeds this module because pack-anchor / host reference the literal path.
*
* When packing with --skip-qvac, `@qvac/sdk` resolves to build/stubs (see bare-standalone).
*/
import * as sdkNs from '@qvac/sdk'
import { llmPlugin } from '@qvac/sdk/llamacpp-completion/plugin'
import LlmLlamacpp from '@qvac/llm-llamacpp'
import barePath from 'bare-path'
import hostFs from '#host-fs'
import os from 'bare-os'
import { createRequire } from 'module'
import { bareOsQvacPrepareHostCacheConfig } from './bare-os-qvac-models-store.mjs'
import {
bareOsQvacHostBackendsRoot,
bundledExecCandidates
} from './paths.js'
const requireFromBind = createRequire(import.meta.url)
/** Well-known ggml backend filenames when bundle readdir fails. */
const KNOWN_GGML_BACKEND_NAMES = [
'libqvac-ggml-cpu-x64.so',
'libqvac-ggml-cpu-sse42.so',
'libqvac-ggml-cpu-haswell.so',
'libqvac-ggml-cpu-ivybridge.so',
'libqvac-ggml-cpu-sandybridge.so',
'libqvac-ggml-cpu-skylake.so',
'libqvac-ggml-cpu-skylakex.so',
'libqvac-ggml-cpu-alderlake.so',
'libqvac-ggml-cpu-cannonlake.so',
'libqvac-ggml-cpu-cascadelake.so',
'libqvac-ggml-cpu-cooperlake.so',
'libqvac-ggml-cpu-icelake.so',
'libqvac-ggml-cpu-sapphirerapids.so',
'libqvac-ggml-cpu-piledriver.so',
'libqvac-ggml-cpu-zen4.so',
'libqvac-ggml-vulkan.so',
'libqvac-ggml-cpu.dylib',
'libqvac-ggml-metal.dylib',
'qvac-ggml-cpu.dll',
'qvac-ggml-vulkan.dll'
]
/**
* @param {string} probeDir absolute path to `<host>/qvac__llm-llamacpp`
* @returns {boolean}
*/
function backendsSlotHasCpu(probeDir) {
try {
if (!hostFs.existsSync(probeDir)) return false
return hostFs
.readdirSync(probeDir)
.some((n) => typeof n === 'string' && /ggml-cpu/i.test(n))
} catch {
return false
}
}
/**
* @param {string} root prebuilds-equivalent root (contains `<host>/qvac__llm-llamacpp`)
* @param {string} slot `<host>/qvac__llm-llamacpp`
* @returns {string} root if valid, else ''
*/
function acceptBackendsRoot(root, slot) {
if (!root) return ''
const abs = barePath.resolve(root)
return backendsSlotHasCpu(barePath.join(abs, slot)) ? abs : ''
}
/**
* Copy ggml backends from srcDir into host cache; return host root or ''.
* @param {string} srcDir
* @param {string} slot
* @param {string[]} [names]
* @returns {string}
*/
function materializeBackendsToHost(srcDir, slot, names) {
/** @type {string[]} */
let fileNames = names && names.length ? names.slice() : []
if (!fileNames.length) {
try {
fileNames = hostFs
.readdirSync(srcDir)
.filter((n) => typeof n === 'string' && /\.(so|dylib|dll)$/i.test(n))
} catch {
fileNames = []
}
}
if (!fileNames.length) {
for (const name of KNOWN_GGML_BACKEND_NAMES) {
try {
if (hostFs.existsSync(barePath.join(srcDir, name))) fileNames.push(name)
} catch {
/* ignore */
}
}
}
if (!fileNames.length) return ''
const hostRoot = bareOsQvacHostBackendsRoot()
const destDir = barePath.join(hostRoot, slot)
try {
hostFs.mkdirSync(destDir, { recursive: true })
} catch {
return ''
}
for (const name of fileNames) {
const from = barePath.join(srcDir, name)
const to = barePath.join(destDir, name)
try {
let srcSize = -1
try {
srcSize = hostFs.statSync(from).size
} catch {
continue
}
try {
if (hostFs.statSync(to).size === srcSize) continue
} catch {
/* copy */
}
if (typeof hostFs.copyFileSync === 'function') {
try {
hostFs.copyFileSync(from, to)
continue
} catch {
/* fall through */
}
}
hostFs.writeFileSync(to, hostFs.readFileSync(from))
} catch {
/* best-effort per file */
}
}
return acceptBackendsRoot(hostRoot, slot)
}
/**
* Prefer Bare `require.asset()` so packed `/app.bundle` assets extract to real paths.
* @param {string} host
* @param {string} slot
* @returns {string}
*/
function materializeBackendsViaAsset(host, slot) {
let llmReq
try {
const main = requireFromBind.resolve('@qvac/llm-llamacpp')
llmReq = createRequire(main)
} catch {
return ''
}
if (typeof llmReq.asset !== 'function') return ''
const hostRoot = bareOsQvacHostBackendsRoot()
const destDir = barePath.join(hostRoot, slot)
try {
hostFs.mkdirSync(destDir, { recursive: true })
} catch {
return ''
}
let wrote = 0
/** @type {string} */
let realAssetDir = ''
for (const name of KNOWN_GGML_BACKEND_NAMES) {
const rel = `./prebuilds/${host}/qvac__llm-llamacpp/${name}`
try {
const assetPath = String(llmReq.asset(rel) || '')
if (!assetPath) continue
const bundleLike = /(?:^|\/)app\.bundle(?:\/|$)/i.test(assetPath)
try {
const buf = hostFs.readFileSync(assetPath)
if (buf && buf.byteLength) {
hostFs.writeFileSync(barePath.join(destDir, name), buf)
wrote += 1
if (!bundleLike) realAssetDir = barePath.dirname(assetPath)
}
} catch {
if (!bundleLike) {
try {
if (typeof hostFs.copyFileSync === 'function') {
hostFs.copyFileSync(assetPath, barePath.join(destDir, name))
wrote += 1
realAssetDir = barePath.dirname(assetPath)
}
} catch {
/* ignore */
}
}
}
} catch {
/* asset missing for this name */
}
}
void wrote
const hostHit = acceptBackendsRoot(hostRoot, slot)
if (hostHit) return hostHit
// If assets landed in a real prebuilds tree, use that root directly.
if (realAssetDir && backendsSlotHasCpu(realAssetDir)) {
const prebuildsRoot = barePath.resolve(realAssetDir, '..', '..')
const hit = acceptBackendsRoot(prebuildsRoot, slot)
if (hit) return hit
}
return ''
}
const sdk =
sdkNs?.default && typeof sdkNs.default === 'object'
? { ...sdkNs, ...sdkNs.default }
: sdkNs
/** True when pack remapped @qvac/sdk to the CI stub (e.g. win32-arm64). */
export const qvacBindIsStub = Boolean(
sdk &&
(sdk.BARE_OS_QVAC_SDK_STUB === true ||
(llmPlugin &&
typeof llmPlugin === 'object' &&
/** @type {{ name?: string }} */ (llmPlugin).name === 'qvac-llm-plugin-stub'))
)
let llmLoadPatched = false
/**
* Host id matching `@qvac/llm-llamacpp/prebuilds/<host>/`.
* Uses bare-os's native platform/arch (the runtime may not expose a
* `globalThis.process`).
* @returns {string}
*/
export function bareOsQvacNativeHostId() {
let platform = ''
let arch = ''
try {
platform = typeof os.platform === 'function' ? os.platform() : ''
} catch {
platform = ''
}
try {
arch = typeof os.arch === 'function' ? os.arch() : ''
} catch {
arch = ''
}
const p = globalThis.process || {}
if (!platform) platform = String(p.platform || 'linux')
if (!arch) arch = String(p.arch || 'x64')
if (arch === 'x86_64') arch = 'x64'
if (arch === 'aarch64') arch = 'arm64'
return platform + '-' + arch
}
/**
* Package root for `@qvac/llm-llamacpp` (may be `/app.bundle/...` in standalone).
* @returns {string}
*/
export function bareOsQvacLlmPackageRoot() {
try {
return barePath.dirname(requireFromBind.resolve('@qvac/llm-llamacpp'))
} catch {
try {
return barePath.dirname(
requireFromBind.resolve('@qvac/llm-llamacpp/package.json')
)
} catch {
return ''
}
}
}
/**
* Hosts whose ggml backends (CPU + Metal) are statically linked into the addon
* and ship no external backend dylibs (the prebuilds dir only has the .bare).
* External `backendsDir` loading is unnecessary there.
* @returns {boolean}
*/
export function bareOsQvacHostHasBuiltinBackends() {
const host = bareOsQvacNativeHostId()
return host.startsWith('darwin-') || host.startsWith('ios-')
}
/**
* Materialize ggml backend .so files to a real host path.
* Bare standalone embeds them under `/app.bundle/...` which dlopen cannot open;
* llama.cpp then fails with `make_cpu_buft_list: no CPU backend found`.
*
* Returns the prebuilds-equivalent root to pass as `backendsDir`
* (native appends `<host>/qvac__llm-llamacpp`).
* @returns {string}
*/
export function bareOsQvacEnsureHostBackendsDir() {
const host = bareOsQvacNativeHostId()
const slot = barePath.join(host, 'qvac__llm-llamacpp')
if (bareOsQvacHostHasBuiltinBackends()) {
// CPU + Metal are built into the addon; no external ggml backend dylibs
// are shipped for darwin/ios. When a real prebuilds tree exists on disk,
// point backendsDir at it so ggml's load_all_from_path no-ops on the
// missing slot dir (it tolerates absent directories).
const pkgRoot = bareOsQvacLlmPackageRoot()
if (pkgRoot) {
const prebuilds = barePath.join(pkgRoot, 'prebuilds')
try {
if (hostFs.existsSync(prebuilds)) return prebuilds
} catch {
/* ignore */
}
}
return ''
}
const envOverride = String(
globalThis.process?.env?.BARE_OS_QVAC_BACKENDS_DIR || ''
).trim()
if (envOverride) {
const hit = acceptBackendsRoot(envOverride, slot)
if (hit) return hit
}
// Already materialized under ~/.bare-os/qvac/backends
{
const hit = acceptBackendsRoot(bareOsQvacHostBackendsRoot(), slot)
if (hit) return hit
}
// Sidecar next to the standalone binary (pack + install.sh).
const sidecarRoots = []
for (const dir of bundledExecCandidates()) {
sidecarRoots.push(barePath.join(dir, 'qvac-backends'))
}
try {
const home = String(
globalThis.process?.env?.HOME ||
globalThis.process?.env?.USERPROFILE ||
''
)
if (home) {
sidecarRoots.push(
barePath.join(home, '.local', 'share', 'bare-os', 'booter', 'qvac-backends')
)
sidecarRoots.push(
barePath.join(home, '.bare-os', 'qvac', 'backends')
)
}
} catch {
/* ignore */
}
for (const beside of sidecarRoots) {
const hit = acceptBackendsRoot(beside, slot)
if (hit) return hit
}
// Bare require.asset() → real host path (or readable bytes).
{
const hit = materializeBackendsViaAsset(host, slot)
if (hit) return hit
}
const pkgRoot = bareOsQvacLlmPackageRoot()
if (!pkgRoot) return ''
const srcDir = barePath.join(pkgRoot, 'prebuilds', slot)
const srcPrebuilds = barePath.join(pkgRoot, 'prebuilds')
const bundleLike = /(?:^|\/)app\.bundle(?:\/|$)/i.test(srcDir)
// Dev / unpacked: use package prebuilds directly when they exist on disk.
if (!bundleLike) {
const hit = acceptBackendsRoot(srcPrebuilds, slot)
if (hit) return hit
}
// Copy from package/bundle path into host cache when readable.
return materializeBackendsToHost(srcDir, slot)
}
/**
* Absolute backendsDir for llama.cpp (real host path after materialize).
* @returns {string}
*/
export function bareOsQvacResolveBackendsDir() {
return bareOsQvacEnsureHostBackendsDir()
}
/**
* llama.cpp `--fit` trial-loads often fail on hybrid Vulkan (and then CPU
* inherits the same opaque error). SDK schema rejects `fit`, so inject into
* the native config map after transform via the addon constructor path.
*/
export function bareOsQvacPatchLlmLoadDefaults() {
if (llmLoadPatched || qvacBindIsStub) return
const proto = LlmLlamacpp && LlmLlamacpp.prototype
if (!proto || typeof proto._load !== 'function') return
llmLoadPatched = true
const orig = proto._load
proto._load = async function bareOsQvacPatchedLlmLoad() {
const prev =
this._config && typeof this._config === 'object' ? this._config : {}
/** @type {Record<string, string>} */
const next = { ...prev }
// This fabric build rejects --no-mmap; only inject fit (not in SDK schema).
const fitEnv = String(
globalThis.process?.env?.BARE_OS_QVAC_FIT ?? 'off'
)
.trim()
.toLowerCase()
if (next.fit == null || next.fit === '') {
next.fit = fitEnv === 'on' ? 'on' : 'off'
}
delete next.no_mmap
delete next['no-mmap']
// Re-resolve every load — first patch-time resolve can miss a late sidecar.
const backendsDir = bareOsQvacResolveBackendsDir()
if (backendsDir) {
next.backendsDir = backendsDir
} else if (bareOsQvacHostHasBuiltinBackends()) {
// Builtin CPU + Metal on darwin/ios: no external backend dylibs to load.
} else if (next.backendsDir == null || next.backendsDir === '') {
// Avoid silent /app.bundle fallback: leave unset and let native fail loudly,
// but surface a clear JS warning once.
try {
const g = /** @type {any} */ (globalThis)
if (!g.__bareOsQvacBackendsDirWarned) {
g.__bareOsQvacBackendsDirWarned = true
console.warn(
'[qvac] ggml backendsDir unresolved — deploy qvac-backends/ beside bare-os-booter, or set BARE_OS_QVAC_BACKENDS_DIR (else: no CPU backend found)'
)
}
} catch {
/* ignore */
}
}
this._config = next
return orig.call(this)
}
}
/**
* Register llamacpp completion and return bound SDK surface (registry + load/completion).
* Writes Bare-OS host cache config before plugins() so downloads land under
* `$BARE_OS_HOST_DATA/qvac/models` instead of `~/.qvac/models`.
* @param {{ cacheDir?: string }} [opts]
* @returns {Record<string, any>}
*/
export function createBoundQvacApi(opts = {}) {
if (qvacBindIsStub) {
throw new Error(
'QVAC natives not packed for this host (use agent --config REST, or rebuild without --skip-qvac on a supported host)'
)
}
const prepared = bareOsQvacPrepareHostCacheConfig({
cacheDir: opts.cacheDir
})
bareOsQvacPatchLlmLoadDefaults()
const pluginsFn = sdk.plugins
if (typeof pluginsFn !== 'function') {
throw new Error('@qvac/sdk plugins() unavailable')
}
const bound = pluginsFn([llmPlugin])
return {
...sdk,
loadModel: bound.loadModel || sdk.loadModel,
completion: bound.completion || sdk.completion,
unloadModel: bound.unloadModel || sdk.unloadModel,
cancel: bound.cancel || sdk.cancel,
__bareOsQvacCacheDir: prepared.cacheDir,
__bareOsQvacConfigPath: prepared.configPath,
__bareOsQvacBackendsDir: bareOsQvacResolveBackendsDir()
}
}
@@ -0,0 +1,193 @@
/**
* Idempotent managed Holesail entry for the stock bare-openssh listen address (see `bare-openssh.js`).
* Same persistence rules as **`bare-os-www-holesail.js`** (`~/.holesail/state.json`).
*/
import { bareHolesailEnvTruthy } from './bare-holesail-env.js'
import {
bareHolesailManagedEnsureServerSeedPersisted,
bareHolesailManagedGenerateSeedHex,
bareHolesailManagedReadState,
bareHolesailManagedRuntimeRunning,
bareHolesailManagedServiceIsRunning,
bareHolesailManagedStartOne,
bareHolesailManagedStopOne,
bareHolesailManagedWriteState
} from './bare-holesail-managed.js'
export const BARE_OS_SSH_HOLESAIL_CONNECTION_ID_PREFIX = 'bare-ssh-'
/**
* @param {number} port
*/
export function bareOsSshHolesailConnectionId(port) {
return `${BARE_OS_SSH_HOLESAIL_CONNECTION_ID_PREFIX}${port}`
}
/**
* Resolve canonical SSH connection id (always `bare-ssh-<live_port>`), while selecting
* one existing row to carry forward for key/seed reuse and listing stale ids to prune.
* @param {{ connections: Record<string, unknown> }} state
* @param {number} port
*/
function bareOsSshResolveManagedIdentity(state, port) {
const exactId = bareOsSshHolesailConnectionId(port)
const allIds = Object.keys(state.connections).filter((id) =>
id.startsWith(BARE_OS_SSH_HOLESAIL_CONNECTION_ID_PREFIX)
)
if (allIds.includes(exactId)) {
return { id: exactId, sourceId: exactId, staleIds: allIds.filter((id) => id !== exactId) }
}
if (allIds.length === 0) return { id: exactId, sourceId: exactId, staleIds: [] }
const idsSorted = allIds.sort()
const sourceId = idsSorted[0]
return { id: exactId, sourceId, staleIds: idsSorted }
}
/**
* Default on (set **`BARE_OS_SSH_HOLESAIL=0`** or **`false`** to disable).
* @param {Record<string, string | undefined>} env
*/
export function bareOsSshHolesailAutoEnabled(env) {
const v = env.BARE_OS_SSH_HOLESAIL
if (v === '0' || v === 'false') return false
return true
}
/**
* Normalize bind host for stable comparison (matches openssh listen string).
* @param {string} host
*/
function normalizeWantHost(host) {
const h = String(host ?? '').trim()
return h || '127.0.0.1'
}
/**
* Persist a managed server tunnel for SSH `port` / `host` and start it if bare-holesail is already active.
* Call after **`sshServer.listen`** succeeds with the **actual** bound port and host.
* @param {Record<string, unknown>} ctx
* @param {Record<string, string | undefined>} env
* @param {number} port
* @param {string} [host]
*/
export async function ensureBareOsSshHolesailTunnel(ctx, env, port, host) {
if (!bareOsSshHolesailAutoEnabled(env)) return
const p =
Number.isFinite(port) && port > 0 && port < 65536
? Math.floor(port)
: 2222
const wantHost = normalizeWantHost(host)
const vfs = ctx.vfs
if (!vfs || typeof vfs.readFile !== 'function' || typeof vfs.writeFile !== 'function') {
return
}
if (!ctx.b4a || typeof ctx.b4a.from !== 'function') return
const { path, state } = await bareHolesailManagedReadState(
ctx,
/** @type {Record<string, string | undefined>} */ (env)
)
const { id, sourceId, staleIds } = bareOsSshResolveManagedIdentity(state, p)
/** @type {Record<string, unknown>} */
const want = { server: true, port: p, host: wantHost, enabled: true }
const existing = state.connections[sourceId]
let shouldRestartRuntime = false
if (existing && typeof existing === 'object') {
const ex = /** @type {Record<string, unknown>} */ (existing)
const curPort = Number.parseInt(String(ex.port ?? ''), 10)
const server = bareHolesailEnvTruthy(ex.server)
const enabled =
ex.enabled === undefined || ex.enabled === null || String(ex.enabled) === ''
? true
: bareHolesailEnvTruthy(ex.enabled)
const rawHost = String(ex.host ?? '').trim()
const hostOk = rawHost === wantHost
shouldRestartRuntime = !(curPort === p && hostOk && server && enabled)
if (server && curPort === p && enabled && hostOk) {
await bareHolesailManagedEnsureServerSeedPersisted(
ctx,
/** @type {Record<string, string | undefined>} */ (env),
id
)
if (
bareHolesailManagedServiceIsRunning() &&
!bareHolesailManagedRuntimeRunning(id)
) {
try {
await bareHolesailManagedStartOne(
ctx,
/** @type {Record<string, string | undefined>} */ (env),
id
)
} catch {
/* may already run */
}
}
return
}
}
/** @type {Record<string, unknown>} */
const next = { ...want }
if (existing && typeof existing === 'object') {
const ex = /** @type {Record<string, unknown>} */ (existing)
const k = String(ex.key ?? '').trim()
if (k) next.key = k
const s = String(ex.seed ?? '').trim()
if (s) next.seed = s
}
const legacyHsOnly =
/^hs:\/\//i.test(String(next.key ?? '').trim()) && !String(next.seed ?? '').trim()
if (!legacyHsOnly && !String(next.seed ?? '').trim()) {
next.seed = bareHolesailManagedGenerateSeedHex()
}
state.connections[id] = next
for (const staleId of staleIds) delete state.connections[staleId]
await bareHolesailManagedWriteState(
ctx,
/** @type {Record<string, string | undefined>} */ (env),
state
)
await bareHolesailManagedEnsureServerSeedPersisted(
ctx,
/** @type {Record<string, string | undefined>} */ (env),
id
)
void path
if (shouldRestartRuntime && bareHolesailManagedRuntimeRunning(id)) {
try {
await bareHolesailManagedStopOne(ctx, id)
} catch {
/* ignore */
}
}
for (const staleId of staleIds) {
if (bareHolesailManagedRuntimeRunning(staleId)) {
try {
await bareHolesailManagedStopOne(ctx, staleId)
} catch {
/* ignore */
}
}
}
if (
bareHolesailManagedServiceIsRunning() &&
!bareHolesailManagedRuntimeRunning(id)
) {
try {
await bareHolesailManagedStartOne(
ctx,
/** @type {Record<string, string | undefined>} */ (env),
id
)
} catch (e) {
try {
ctx.console?.error?.(
`[bare-os-ssh] holesail start ${id}: ${(e && /** @type {{ message?: string }} */ (e).message) || String(e)}`
)
} catch {
/* ignore */
}
}
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,55 @@
/**
* Attach first-party guest TUI / SDK onto ctx (raw-JS IIFE, no guest import).
*/
import { BARE_OS_TUI_SDK_SOURCE } from './bare-os-tui-sdk.data.mjs'
/**
* @param {Record<string, string> | null | undefined} env
*/
export function bareOsTuiEnabled(env) {
const v = env && env.BARE_OS_TUI
return v !== '0' && v !== 'false'
}
/**
* @param {Record<string, unknown>} ctx
* @param {{ source?: string }} [opts]
* @returns {boolean}
*/
export function attachBareOsTuiSdk(ctx, opts = {}) {
if (!ctx || typeof ctx !== 'object') return false
const env =
ctx.env && typeof ctx.env === 'object'
? /** @type {Record<string, string>} */ (ctx.env)
: {}
if (!bareOsTuiEnabled(env)) return false
if (
typeof ctx.bareOsIsCtxMethodAllowed === 'function' &&
ctx.bareOsIsCtxMethodAllowed('tui') === false
) {
return false
}
const src =
typeof opts.source === 'string' && opts.source
? opts.source
: BARE_OS_TUI_SDK_SOURCE
if (!src) return false
try {
const factory = new Function(
'ctx',
'"use strict"; return (' + src + ')(ctx)'
)
const boxed = factory(ctx)
if (!boxed || typeof boxed !== 'object' || !boxed.tui) return false
ctx.tui = Object.freeze(boxed.tui)
ctx.sdk = Object.freeze(boxed.sdk)
return true
} catch (e) {
if (typeof ctx.console?.error === 'function') {
ctx.console.error(
'bare-os-tui: attach failed: ' + (e && /** @type {Error} */ (e).message)
)
}
return false
}
}
@@ -0,0 +1,180 @@
/**
* Idempotent managed Holesail entry for the stock www static server port.
*/
import { bareHolesailEnvTruthy } from './bare-holesail-env.js'
import {
bareHolesailManagedEnsureServerSeedPersisted,
bareHolesailManagedGenerateSeedHex,
bareHolesailManagedReadState,
bareHolesailManagedRuntimeRunning,
bareHolesailManagedServiceIsRunning,
bareHolesailManagedStartOne,
bareHolesailManagedStopOne,
bareHolesailManagedWriteState
} from './bare-holesail-managed.js'
export const BARE_OS_WWW_HOLESAIL_CONNECTION_ID_PREFIX = 'bare-www-'
/**
* @param {Record<string, string | undefined>} env
* @param {number} port
*/
export function bareOsWwwHolesailConnectionId(port) {
return `${BARE_OS_WWW_HOLESAIL_CONNECTION_ID_PREFIX}${port}`
}
/**
* Pick one persisted WWW holesail entry to reuse and return any duplicates to prune.
* @param {{ connections: Record<string, unknown> }} state
* @param {number} port
*/
function bareOsWwwResolveManagedIdentity(state, port) {
const exactId = bareOsWwwHolesailConnectionId(port)
const allIds = Object.keys(state.connections).filter((id) =>
id.startsWith(BARE_OS_WWW_HOLESAIL_CONNECTION_ID_PREFIX)
)
if (allIds.includes(exactId)) {
return { id: exactId, staleIds: allIds.filter((id) => id !== exactId) }
}
if (allIds.length === 0) return { id: exactId, staleIds: [] }
const idsSorted = allIds.sort()
return { id: idsSorted[0], staleIds: idsSorted.slice(1) }
}
/**
* @param {Record<string, string | undefined>} env
*/
export function bareOsWwwHolesailAutoEnabled(env) {
const v = env.BARE_OS_WWW_HOLESAIL
if (v === '0' || v === 'false') return false
return true
}
/**
* Persist a managed server tunnel for `port` and start it if bare-holesail is already active.
* @param {Record<string, unknown>} ctx
* @param {Record<string, string | undefined>} env
* @param {number} port
*/
export async function ensureBareOsWwwHolesailTunnel(ctx, env, port) {
if (!bareOsWwwHolesailAutoEnabled(env)) return
const p =
Number.isFinite(port) && port > 0 && port < 65536
? Math.floor(port)
: 8088
const vfs = ctx.vfs
if (!vfs || typeof vfs.readFile !== 'function' || typeof vfs.writeFile !== 'function') {
return
}
if (!ctx.b4a || typeof ctx.b4a.from !== 'function') return
const { path, state } = await bareHolesailManagedReadState(
ctx,
/** @type {Record<string, string | undefined>} */ (env)
)
const { id, staleIds } = bareOsWwwResolveManagedIdentity(state, p)
/** Holesail server mode requires an explicit loopback host. */
const wantHost = '127.0.0.1'
/** @type {Record<string, unknown>} */
const want = { server: true, port: p, host: wantHost, enabled: true }
const existing = state.connections[id]
let shouldRestartRuntime = false
if (existing && typeof existing === 'object') {
const ex = /** @type {Record<string, unknown>} */ (existing)
const curPort = Number.parseInt(String(ex.port ?? ''), 10)
const server = bareHolesailEnvTruthy(ex.server)
const enabled =
ex.enabled === undefined || ex.enabled === null || String(ex.enabled) === ''
? true
: bareHolesailEnvTruthy(ex.enabled)
const rawHost = String(ex.host ?? '').trim()
const hostOk = rawHost === wantHost
shouldRestartRuntime = !(curPort === p && hostOk && server && enabled)
if (server && curPort === p && enabled && hostOk) {
await bareHolesailManagedEnsureServerSeedPersisted(
ctx,
/** @type {Record<string, string | undefined>} */ (env),
id
)
if (
bareHolesailManagedServiceIsRunning() &&
!bareHolesailManagedRuntimeRunning(id)
) {
try {
await bareHolesailManagedStartOne(
ctx,
/** @type {Record<string, string | undefined>} */ (env),
id
)
} catch {
/* may already run */
}
}
return
}
}
/** @type {Record<string, unknown>} */
const next = { ...want }
if (existing && typeof existing === 'object') {
const ex = /** @type {Record<string, unknown>} */ (existing)
const k = String(ex.key ?? '').trim()
if (k) next.key = k
const s = String(ex.seed ?? '').trim()
if (s) next.seed = s
}
const legacyHsOnly =
/^hs:\/\//i.test(String(next.key ?? '').trim()) && !String(next.seed ?? '').trim()
if (!legacyHsOnly && !String(next.seed ?? '').trim()) {
next.seed = bareHolesailManagedGenerateSeedHex()
}
state.connections[id] = next
for (const staleId of staleIds) delete state.connections[staleId]
await bareHolesailManagedWriteState(
ctx,
/** @type {Record<string, string | undefined>} */ (env),
state
)
await bareHolesailManagedEnsureServerSeedPersisted(
ctx,
/** @type {Record<string, string | undefined>} */ (env),
id
)
void path
if (shouldRestartRuntime && bareHolesailManagedRuntimeRunning(id)) {
try {
await bareHolesailManagedStopOne(ctx, id)
} catch {
/* ignore */
}
}
for (const staleId of staleIds) {
if (bareHolesailManagedRuntimeRunning(staleId)) {
try {
await bareHolesailManagedStopOne(ctx, staleId)
} catch {
/* ignore */
}
}
}
if (
bareHolesailManagedServiceIsRunning() &&
!bareHolesailManagedRuntimeRunning(id)
) {
try {
await bareHolesailManagedStartOne(
ctx,
/** @type {Record<string, string | undefined>} */ (env),
id
)
} catch (e) {
try {
ctx.console?.error?.(
`[bare-os-www] holesail start ${id}: ${(e && /** @type {{ message?: string }} */ (e).message) || String(e)}`
)
} catch {
/* ignore */
}
}
}
}
@@ -0,0 +1,530 @@
/**
* bare-os-www — static HTTP server on loopback for ~/.www (initd unit `bare-os-www`).
* Uses `bare-node-http` so both Node and Bare/Pear paths stay on one host-import surface.
*/
import bareNodeHttp from 'bare-node-http'
const http = bareNodeHttp
import {
findBareServiceDefinition,
registerBareService,
restartBareService
} from './bare-initd.js'
import { appendVarLog, BARE_OS_VAR_LOG_DIR } from './bare-os-var-log.js'
import { ensureBareOsWwwHolesailTunnel } from './bare-os-www-holesail.js'
import { listenTcpWithFallback } from './bare-os-bind-fallback.js'
export const BARE_OS_WWW_LOG = `${BARE_OS_VAR_LOG_DIR}/www.log`
/** @type {ReturnType<typeof http.createServer> | null} */
let wwwServer = null
const MIME = Object.freeze({
html: 'text/html; charset=utf-8',
htm: 'text/html; charset=utf-8',
css: 'text/css; charset=utf-8',
js: 'text/javascript; charset=utf-8',
mjs: 'text/javascript; charset=utf-8',
json: 'application/json; charset=utf-8',
svg: 'image/svg+xml',
txt: 'text/plain; charset=utf-8',
md: 'text/markdown; charset=utf-8',
png: 'image/png',
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
gif: 'image/gif',
webp: 'image/webp',
ico: 'image/x-icon',
woff2: 'font/woff2',
woff: 'font/woff',
map: 'application/json; charset=utf-8'
})
export const BARE_OS_WWW_DEFAULT_INDEX = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Bare OS</title>
<style>
:root {
color-scheme: dark light;
--bg: #0c0d10;
--panel: #151821;
--line: #2a3142;
--text: #e8ecf4;
--muted: #8b95a8;
--accent: #6ee7b7;
--glow: rgba(110, 231, 183, 0.15);
}
@media (prefers-color-scheme: light) {
:root {
--bg: #f4f6fb;
--panel: #ffffff;
--line: #d8dee9;
--text: #1c2433;
--muted: #5c6a82;
--accent: #0d9488;
--glow: rgba(13, 148, 136, 0.12);
}
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif;
background: radial-gradient(1200px 600px at 10% -10%, var(--glow), transparent 55%),
radial-gradient(900px 500px at 90% 0%, var(--glow), transparent 50%),
var(--bg);
color: var(--text);
line-height: 1.6;
}
main {
max-width: 52rem;
margin: 0 auto;
padding: clamp(2rem, 5vw, 4rem) clamp(1.25rem, 4vw, 2rem);
}
.card {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 1rem;
padding: clamp(1.75rem, 4vw, 2.5rem);
box-shadow: 0 24px 60px rgba(0,0,0,0.25);
}
h1 {
font-size: clamp(1.75rem, 4vw, 2.35rem);
font-weight: 700;
letter-spacing: -0.03em;
margin: 0 0 0.5rem;
}
.tag {
display: inline-block;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--accent);
margin-bottom: 1rem;
}
p { margin: 0.75rem 0; color: var(--muted); }
p.lead { font-size: 1.1rem; color: var(--text); }
ul { margin: 1rem 0 0; padding-left: 1.2rem; color: var(--muted); }
li { margin: 0.35rem 0; }
code {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 0.9em;
background: var(--bg);
padding: 0.15em 0.45em;
border-radius: 0.35rem;
border: 1px solid var(--line);
}
footer {
margin-top: 2.5rem;
font-size: 0.85rem;
color: var(--muted);
}
</style>
</head>
<body>
<main>
<div class="card">
<div class="tag">Welcome</div>
<h1>Bare OS</h1>
<p class="lead">Your personal site lives on the Hyperdrive-backed path <code>~/.www</code>. Drop files here to serve them from this host.</p>
<p>Static pages are served over HTTP on the session port (default <code>8088</code>). When Holesail managed mode is enabled, a tunnel can expose this port to peers.</p>
<ul>
<li>Edit <code>index.html</code> under <code>~/.www</code> to replace this page.</li>
<li>Optional HDMS label <code>www</code> mounts at <code>/mnt/www</code>; with that drive mounted, <code>~/.www</code> routes there.</li>
</ul>
<footer>Bare operating system — minimal, replicated, yours.</footer>
</div>
</main>
</body>
</html>
`
/**
* @param {Record<string, string | undefined>} [env]
*/
export function bareOsWwwInitdEnabled(env = globalThis.process?.env) {
const v = env?.BARE_OS_WWW_INITD
if (v === '0' || v === 'false') return false
return true
}
/**
* @param {Record<string, string | undefined>} env
*/
function parseWwwPort(env) {
const raw = String(env.BARE_OS_WWW_PORT ?? '').trim()
const n = raw ? Number.parseInt(raw, 10) : 8088
if (!Number.isFinite(n) || n < 1 || n > 65535) return 8088
return n
}
/**
* @param {Record<string, string | undefined>} env
*/
function parseWwwHost(env) {
const h = String(env.BARE_OS_WWW_HOST ?? '').trim()
return h || '127.0.0.1'
}
/**
* @param {string} docrootNorm absolute, no trailing slash except root
* @param {string} pathname request URL pathname
* @returns {string | null} absolute vfs path
*/
export function bareOsWwwResolveStaticPath(docrootNorm, pathname) {
let p = pathname || '/'
try {
p = decodeURIComponent(p)
} catch {
return null
}
if (!p.startsWith('/')) return null
let rel = p.replace(/^\/+/, '')
if (!rel || rel.endsWith('/')) {
rel = rel.replace(/\/+$/, '')
rel = rel ? `${rel}/index.html` : 'index.html'
}
const segs = rel.split('/').filter(Boolean)
if (segs.some((s) => s === '..')) return null
const d = docrootNorm.replace(/\/+$/, '') || '/'
const tail = segs.join('/')
const abs = tail ? `${d}/${tail}` : `${d}/index.html`
if (d === '/') {
if (!abs.startsWith('/') || abs.includes('//')) return null
return abs
}
if (!(abs === d || abs.startsWith(`${d}/`))) return null
return abs
}
/**
* `…/dir/index.html` under docroot when `dirAbs` is a served directory (path traversal guard).
* @param {string} docrootNorm
* @param {string} dirAbs
* @returns {string | null}
*/
function bareOsWwwJoinIndexInDirectory(docrootNorm, dirAbs) {
const d = docrootNorm.replace(/\/+$/, '') || '/'
const base = String(dirAbs || '').replace(/\/+$/, '')
if (!base) return null
const idx = `${base}/index.html`
if (d === '/') {
if (!idx.startsWith('/') || idx.includes('//')) return null
return idx
}
if (!(idx === d || idx.startsWith(`${d}/`))) return null
return idx
}
/**
* @param {string} absPath
*/
function mimeForPath(absPath) {
const i = absPath.lastIndexOf('.')
if (i <= 0) return 'application/octet-stream'
const ext = absPath.slice(i + 1).toLowerCase()
return MIME[/** @type {keyof typeof MIME} */ (ext)] || 'application/octet-stream'
}
/**
* @param {Record<string, unknown>} ctx
* @param {string} line
*/
function wwwLog(ctx, line) {
void appendVarLog(ctx, BARE_OS_WWW_LOG, 'www', line)
}
/**
* Ensure `~/.www/` exists and contains default `index.html` when missing (idempotent).
* Called from identity unlock/register and from the `bare-os-www` initd unit.
* @param {Record<string, unknown>} ctx
*/
export async function ensureBareOsWwwHomeDefaults(ctx) {
const vfs = ctx.vfs
if (!vfs || typeof vfs.resolveLogical !== 'function') return
try {
await vfs.mkdir(vfs.resolveLogical('~/.www'), { recursive: true })
} catch {
/* ignore */
}
const idx = vfs.resolveLogical('~/.www/index.html')
try {
const cur = await vfs.readFile(idx)
if (cur != null) {
const u8 =
cur instanceof Uint8Array
? cur
: ctx.b4a && typeof ctx.b4a.from === 'function'
? ctx.b4a.from(cur)
: null
if (u8 && u8.byteLength > 0) return
}
} catch {
/* missing or unreadable */
}
try {
if (!ctx.b4a || typeof ctx.b4a.from !== 'function') return
const body = ctx.b4a.from(BARE_OS_WWW_DEFAULT_INDEX, 'utf8')
await vfs.writeFile(idx, body)
} catch (e) {
try {
ctx.console?.error?.(
`[bare-os-www] seed index: ${(e && /** @type {{ message?: string }} */ (e).message) || String(e)}`
)
} catch {
/* ignore */
}
}
}
/**
* @param {Record<string, unknown>} ctx
*/
async function ensureDefaultIndex(ctx) {
await ensureBareOsWwwHomeDefaults(ctx)
}
/**
* @param {import('http').IncomingMessage} req
* @param {import('http').ServerResponse} res
* @param {Record<string, unknown>} ctx
* @param {string} docrootNorm
*/
async function handleWwwRequest(req, res, ctx, docrootNorm) {
if (req.method !== 'GET' && req.method !== 'HEAD') {
res.statusCode = 405
res.setHeader('Content-Type', 'text/plain; charset=utf-8')
res.end('Method Not Allowed\n')
return
}
const u = new URL(req.url || '/', 'http://127.0.0.1')
let abs = bareOsWwwResolveStaticPath(docrootNorm, u.pathname)
if (!abs) {
res.statusCode = 400
res.setHeader('Content-Type', 'text/plain; charset=utf-8')
res.end('Bad path\n')
return
}
const vfs = ctx.vfs
if (
!vfs ||
typeof vfs.readFile !== 'function' ||
typeof vfs.stat !== 'function'
) {
res.statusCode = 500
res.end('VFS unavailable\n')
return
}
try {
let st = await vfs.stat(abs)
if (st && st.type === 'directory') {
// Without a trailing slash, browsers resolve relative URLs against / not /site/
// (RFC 3986 — "site" is treated as a single path segment, not a directory).
if (!u.pathname.endsWith('/')) {
const loc = `${u.pathname}/${u.search || ''}`
res.statusCode = 308
res.setHeader('Location', loc)
res.end()
return
}
const idx = bareOsWwwJoinIndexInDirectory(docrootNorm, abs)
if (!idx) {
res.statusCode = 404
res.setHeader('Content-Type', 'text/html; charset=utf-8')
res.end(
'<!DOCTYPE html><html><head><meta charset="utf-8"/><title>404</title></head><body><h1>404</h1></body></html>\n'
)
return
}
abs = idx
try {
st = await vfs.stat(abs)
} catch (e) {
const code = /** @type {{ code?: string }} */ (e)?.code
if (code === 'ENOENT') {
res.statusCode = 404
res.setHeader('Content-Type', 'text/html; charset=utf-8')
res.end(
'<!DOCTYPE html><html><head><meta charset="utf-8"/><title>404</title></head><body><h1>404</h1></body></html>\n'
)
return
}
throw e
}
if (!st || st.type === 'directory') {
res.statusCode = 404
res.setHeader('Content-Type', 'text/html; charset=utf-8')
res.end(
'<!DOCTYPE html><html><head><meta charset="utf-8"/><title>404</title></head><body><h1>404</h1></body></html>\n'
)
return
}
}
if (req.method === 'HEAD') {
if (!st || st.type === 'directory') {
res.statusCode = 404
res.setHeader('Content-Type', 'text/html; charset=utf-8')
res.end(
'<!DOCTYPE html><html><head><meta charset="utf-8"/><title>404</title></head><body><h1>404</h1></body></html>\n'
)
return
}
const len = Number(st.size) || 0
res.statusCode = 200
res.setHeader('Content-Type', mimeForPath(abs))
res.setHeader('Cache-Control', 'no-cache')
res.setHeader('Content-Length', String(len))
res.end()
return
}
const buf = await vfs.readFile(abs)
if (buf == null) {
res.statusCode = 404
res.setHeader('Content-Type', 'text/html; charset=utf-8')
res.end(
'<!DOCTYPE html><html><head><meta charset="utf-8"/><title>404</title></head><body><h1>404</h1></body></html>\n'
)
return
}
const u8 =
buf instanceof Uint8Array
? buf
: ctx.b4a && typeof ctx.b4a.from === 'function'
? ctx.b4a.from(buf)
: new Uint8Array()
res.statusCode = 200
res.setHeader('Content-Type', mimeForPath(abs))
res.setHeader('Cache-Control', 'no-cache')
res.setHeader('Content-Length', String(u8.byteLength))
res.end(u8)
} catch (e) {
const code = /** @type {{ code?: string }} */ (e)?.code
if (code === 'ENOENT') {
res.statusCode = 404
res.setHeader('Content-Type', 'text/html; charset=utf-8')
res.end(
'<!DOCTYPE html><html><head><meta charset="utf-8"/><title>404</title></head><body><h1>404</h1></body></html>\n'
)
return
}
res.statusCode = 500
res.setHeader('Content-Type', 'text/plain; charset=utf-8')
res.end('read error\n')
}
}
/**
* @param {Record<string, unknown>} ctx
*/
async function startBareOsWww(ctx) {
const env =
ctx.env && typeof ctx.env === 'object'
? /** @type {Record<string, string | undefined>} */ (ctx.env)
: /** @type {Record<string, string | undefined>} */ ({})
const port = parseWwwPort(env)
const host = parseWwwHost(env)
const vfs = ctx.vfs
if (!vfs || typeof vfs.resolveLogical !== 'function') {
wwwLog(ctx, 'skip: no vfs.resolveLogical')
return
}
await ensureDefaultIndex(ctx)
const docroot = vfs.resolveLogical('~/.www')
const docrootNorm = docroot.replace(/\/+$/, '') || '/'
if (wwwServer) {
wwwLog(ctx, 'already running')
return
}
const server = http.createServer((req, res) => {
void handleWwwRequest(req, res, ctx, docrootNorm)
})
let boundPort = port
try {
;({ port: boundPort } = await listenTcpWithFallback(server, port, host, env))
} catch (e) {
const msg = (e && /** @type {{ message?: string }} */ (e).message) || String(e)
wwwLog(ctx, `listen error: ${msg}`)
try {
server.close()
} catch {
/* ignore */
}
throw e
}
wwwServer = server
wwwLog(ctx, `listening http://${host}:${boundPort} docroot=${docrootNorm}`)
try {
ctx.console?.log?.(`[bare-os-www] http://${host}:${boundPort}${docrootNorm}`)
} catch {
/* ignore */
}
await ensureBareOsWwwHolesailTunnel(ctx, env, boundPort)
}
/**
* @param {Record<string, unknown>} ctx
*/
async function stopBareOsWww(ctx) {
const s = wwwServer
wwwServer = null
if (!s) return
await new Promise((resolve) => {
try {
s.close(() => resolve(null))
} catch {
resolve(null)
}
})
wwwLog(ctx, 'stopped')
}
let bareOsWwwInitdRegistered = false
/**
* @param {Record<string, string | undefined>} env
*/
/**
* After `HOME` / session identity changes (`login`, `logout`, `applyLoginKeys`, register),
* recreate the HTTP listener — `http.createServer` closed over the previous `ctx`.
* No-op when the unit is not registered or `BARE_OS_WWW_INITD` disables the service.
* @param {Record<string, unknown>} ctx
*/
export async function maybeRestartBareOsWwwAfterIdentity(ctx) {
const env =
ctx.env && typeof ctx.env === 'object'
? /** @type {Record<string, string | undefined>} */ (ctx.env)
: /** @type {Record<string, string | undefined>} */ ({})
if (!bareOsWwwInitdEnabled(env)) return
if (!findBareServiceDefinition('bare-os-www')) return
try {
await restartBareService(ctx, 'bare-os-www')
} catch (e) {
try {
ctx.console?.error?.(
`[bare-os-www] restart after identity: ${(e && /** @type {{ message?: string }} */ (e).message) || String(e)}`
)
} catch {
/* ignore */
}
}
}
export function maybeRegisterBareOsWwwInitd(env) {
if (bareOsWwwInitdRegistered) return
if (!bareOsWwwInitdEnabled(env || {})) return
bareOsWwwInitdRegistered = true
registerBareService({
name: 'bare-os-www',
description:
'Static HTTP for ~/.www (default port 8088, loopback); BARE_OS_WWW_INITD=0 to disable; BARE_OS_WWW_PORT / BARE_OS_WWW_HOST',
logPath: BARE_OS_WWW_LOG,
start: startBareOsWww,
stop: stopBareOsWww
})
}
@@ -0,0 +1,173 @@
/**
* Ed25519 key generation for Bare OS using bare-crypto (not OpenSSH wire private format).
* Writes OpenSSH-compatible `.pub` line + a Bare-OS JSON private envelope.
*
* Usage: ssh-keygen -t ed25519 -f PATH [-N pass] [-C comment]
* Encrypted private keys use bareOsKeySchema 2 (PBKDF2-SHA256 + ChaCha20-Poly1305), aligned with account crypto profile.
*/
import bareCrypto from 'bare-crypto'
import b4a from 'b4a'
import { PBKDF2_ITERATIONS, sealBytes } from './identity-account.js'
const { pbkdf2Sync, randomFillSync } = bareCrypto
/**
* @param {string} str
*/
function utf8Bytes(str) {
return b4a.from(String(str), 'utf8')
}
/**
* @param {Uint8Array} pub32
*/
function sshEd25519PublicBlob(pub32) {
const enc = (s) => {
const b = typeof s === 'string' ? utf8Bytes(s) : s
const out = new Uint8Array(4 + b.length)
new DataView(out.buffer).setUint32(0, b.length, false)
out.set(b, 4)
return out
}
const a = enc('ssh-ed25519')
const b = enc(pub32)
const merged = new Uint8Array(a.length + b.length)
merged.set(a, 0)
merged.set(b, a.length)
return merged
}
function b64(buf) {
if (typeof Buffer !== 'undefined') return Buffer.from(buf).toString('base64')
let bin = ''
for (let i = 0; i < buf.length; i++) bin += String.fromCharCode(buf[i])
// eslint-disable-next-line no-undef
return btoa(bin)
}
/**
* @param {Record<string, unknown>} ctx
* @param {string[]} argv
*/
export async function runSshKeygenCli(ctx, argv) {
const args = argv.slice(1)
if (
args.includes('-h') ||
args.includes('--help') ||
args.includes('-?') ||
args.length === 0
) {
ctx.console.log(
'Usage: ssh-keygen -t ed25519 -f KEYFILE [-N passphrase] [-C comment]\n' +
'Bare OS: writes KEYFILE (JSON private envelope) and KEYFILE.pub (ssh-ed25519 line).\n' +
'Passphrase: schema 2 sealed key (PBKDF2 + ChaCha20-Poly1305); empty -N for plaintext schema 1.\n'
)
return
}
let type = ''
let keyPath = ''
let pass = ''
let comment = ''
for (let i = 0; i < args.length; i++) {
const a = args[i]
if (a === '-t') type = String(args[++i] || '')
else if (a === '-f') keyPath = String(args[++i] || '')
else if (a === '-N') pass = String(args[++i] ?? '')
else if (a === '-C') comment = String(args[++i] || '')
else if (a.startsWith('-')) {
ctx.console.error('ssh-keygen: unsupported option ' + a)
ctx.exitCode = 1
return
}
}
if (type !== 'ed25519') {
ctx.console.error('ssh-keygen: only -t ed25519 is supported')
ctx.exitCode = 1
return
}
if (!keyPath) {
ctx.console.error('ssh-keygen: -f KEYFILE is required')
ctx.exitCode = 1
return
}
const vfs = ctx.vfs
const b4 = ctx.b4a
if (!vfs || typeof vfs.resolveLogical !== 'function' || !b4) {
ctx.console.error('ssh-keygen: vfs unavailable')
ctx.exitCode = 1
return
}
const { publicKey, privateKey } = bareCrypto.generateKeyPair('ed25519')
const pubRaw =
typeof publicKey.export === 'function' ? publicKey.export() : publicKey._key
const secRaw =
typeof privateKey.export === 'function'
? privateKey.export()
: privateKey._key
const blob = sshEd25519PublicBlob(pubRaw)
const pubLine =
'ssh-ed25519 ' + b64(blob) + ' ' + (comment || 'bare-os-ed25519') + '\n'
/** @type {Record<string, unknown>} */
let privEnvelope
if (pass !== '') {
const salt = new Uint8Array(16)
randomFillSync(salt)
const dk = pbkdf2Sync(
utf8Bytes(pass),
salt,
PBKDF2_ITERATIONS,
32,
'sha256'
)
const inner = JSON.stringify({
sk: b64(secRaw),
pk: b64(pubRaw)
})
const sealed = sealBytes(dk, utf8Bytes(inner))
privEnvelope = {
bareOsKeySchema: 2,
kty: 'ed25519',
kdf: 'pbkdf2-sha256',
aead: 'chacha20-poly1305',
iterations: PBKDF2_ITERATIONS,
salt: b64(salt),
sealed: b64(sealed),
comment: comment || 'bare-os-ed25519',
atMs: Date.now()
}
} else {
privEnvelope = {
bareOsKeySchema: 1,
kty: 'ed25519',
sk: b64(secRaw),
pk: b64(pubRaw),
comment: comment || 'bare-os-ed25519',
atMs: Date.now()
}
}
const privJson = JSON.stringify(privEnvelope, null, 2) + '\n'
const absKey = vfs.resolveLogical(keyPath)
const absPub = vfs.resolveLogical(keyPath + '.pub')
try {
if (typeof vfs.writeFile !== 'function') {
throw new Error('writeFile missing')
}
await vfs.writeFile(absKey, b4.from(privJson))
await vfs.writeFile(absPub, b4.from(pubLine))
ctx.console.log(
'generated keypair: ' + keyPath + ' and ' + keyPath + '.pub'
)
} catch (e) {
ctx.console.error(
'ssh-keygen: ' + ((e && /** @type {Error} */ (e).message) || String(e))
)
ctx.exitCode = 1
}
}
@@ -0,0 +1,129 @@
/**
* Minimal sshd_config subset parser (key value pairs and repeats).
* Lives under bare-os-booter so Pear bundles it with the booter (not bare-os-openssh).
*/
/** Default `AuthorizedKeysFile` when sshd_config omits it (OpenSSH-style relative → $HOME). */
export const SSHD_DEFAULT_AUTHORIZED_KEYS_FILE = '.ssh/authorized_keys'
/**
* Build the logical path bare-openssh uses before `vfs.resolveLogical` (must stay in sync).
* @param {string} [home]
* @param {string} [authKeysRel]
*/
export function logicalAuthorizedKeysPath(home, authKeysRel) {
const h = String(home || '/home/guest').replace(/\/+$/, '')
const rel = String(authKeysRel ?? SSHD_DEFAULT_AUTHORIZED_KEYS_FILE).trim()
if (rel.startsWith('/') || rel.startsWith('~/')) return rel
return `${h}/${rel.replace(/^\/+/, '')}`
}
/**
* @param {string} text
* @returns {{
* port: number,
* listenAddress: string,
* hostKeyPaths: string[],
* passwordAuthentication: boolean,
* pubkeyAuthentication: boolean,
* permitRootLogin: boolean,
* allowTcpForwarding: boolean,
* maxAuthTries: number,
* clientAliveInterval: number,
* authorizedKeysFile: string,
* subsystemSftp: string
* }}
*/
export function parseSshdConfig(text) {
const out = {
// >1024: Pear/Bare guests bind without root (port 22 is permission denied).
port: 2222,
listenAddress: '127.0.0.1',
hostKeyPaths: [] /** @type {string[]} */,
passwordAuthentication: true,
pubkeyAuthentication: true,
permitRootLogin: false,
allowTcpForwarding: false,
maxAuthTries: 6,
clientAliveInterval: 0,
authorizedKeysFile: SSHD_DEFAULT_AUTHORIZED_KEYS_FILE,
subsystemSftp: 'internal-sftp'
}
const lines = String(text || '').split(/\r?\n/)
for (const raw of lines) {
const line = raw.replace(/#.*$/, '').trim()
if (!line) continue
const m = /^([A-Za-z0-9]+)\s+(.+)$/.exec(line)
if (!m) continue
const key = m[1].toLowerCase()
let val = m[2].trim()
if (
(val.startsWith('"') && val.endsWith('"')) ||
(val.startsWith("'") && val.endsWith("'"))
) {
val = val.slice(1, -1)
}
switch (key) {
case 'port': {
const n = Number.parseInt(val, 10)
if (Number.isFinite(n) && n > 0 && n < 65536) out.port = n
break
}
case 'listenaddress':
out.listenAddress = val
break
case 'hostkey':
out.hostKeyPaths.push(val)
break
case 'passwordauthentication':
out.passwordAuthentication = isYes(val)
break
case 'pubkeyauthentication':
out.pubkeyAuthentication = isYes(val)
break
case 'permitrootlogin':
out.permitRootLogin = val.toLowerCase() === 'yes' || val === 'without-password'
break
case 'allowtcpforwarding':
out.allowTcpForwarding = isYes(val)
break
case 'maxauthtries': {
const n = Number.parseInt(val, 10)
if (Number.isFinite(n) && n > 0) out.maxAuthTries = Math.min(n, 32)
break
}
case 'clientaliveinterval': {
const n = Number.parseInt(val, 10)
if (Number.isFinite(n) && n >= 0) out.clientAliveInterval = n
break
}
case 'authorizedkeysfile':
out.authorizedKeysFile = val
break
case 'subsystem':
if (val.toLowerCase().startsWith('sftp')) {
const parts = val.split(/\s+/)
out.subsystemSftp = parts.slice(1).join(' ') || 'internal-sftp'
}
break
default:
break
}
}
if (out.hostKeyPaths.length === 0) {
out.hostKeyPaths.push(
'~/.config/bare-os/ssh/host/ssh_host_ed25519_key'
)
}
return out
}
/** @param {string} v */
function isYes(v) {
const s = String(v).toLowerCase()
return s === 'yes' || s === 'true'
}