Updates
This commit is contained in:
@@ -5,7 +5,7 @@ Authoritative **version alignment** with protocol and telemetry schema numbers l
|
||||
## Maintenance
|
||||
|
||||
- **`bare-os-www` initd** — Stock static HTTP server for **`~/.www`** on **`127.0.0.1:8088`** ( **`bare-os-www-initd.js`**, **`bare-os-www-holesail.js`** ); managed **Holesail** entry **`bare-www-<port>`** with **`host: 127.0.0.1`**; **`ensureBareOsWwwHomeDefaults`** after **`login`** (creates **`~/.www`** when missing); **`maybeRestartBareOsWwwAfterIdentity`** restarts the unit after unlock/register/`applyLoginKeys` and after **`logout`** so the listener tracks the current session **`HOME`** (not a stale closed-over **`ctx`**). Handbook [ch.4 § bare-os-www](../../handbook/04-the-booter-runtime.md#bare-os-www-static-http-for-www); env **`BARE_OS_WWW_*`** in [environment appendix](../../docs/reference/environment-and-posix-appendix.md).
|
||||
- **Managed Holesail (`bare-holesail-managed.js`)** — When a **server** row in **`~/.holesail/state.json`** has no **`key`**, the booter persists the minted **`hs://…`** URL from the running **`Holesail`** instance after **`ready()`**, and **`holesail start …`** backfills the same when the process was already live. Restores stable tunnel URLs across boot and login.
|
||||
- **Managed Holesail (`bare-holesail-managed.js` + `vfs.js`)** — Stock **`BARE_OS_HOLESAIL_STATE`** defaults to **`/.bare/holesail/state.json`** (personal drive root) so **guest** and **logged-in** sessions share one tunnel list; with **`BARE_OS_PERSONAL_ACCT_PREFIX`**, **`/.bare/holesail/**`** is exempt from per-account **`acct/…`** layout (other **`/.bare/**`** paths stay isolated). Legacy **`~/.holesail/state.json`** is merged into the stable file on first read when empty. **Server** rows without **`key`** still get **`hs://…`** persisted after **`ready()`** / **`holesail start`** backfill.
|
||||
- **Shell completion / Fish REPL:** canonical documentation is [`docs/reference/shell-completion-and-repl-editor.md`](../../docs/reference/shell-completion-and-repl-editor.md) (engine in **`lib/completion-engine.js`**, UI in **`lib/fish-readline.js`**, **`BARE_OS_FISH`**, **`BARE_OS_COMPACT_MENU`**).
|
||||
- **`baretop` snapshot**: **`ctx.bareOsReadBareTopSnapshot({ lite: true })`** returns a reduced **`files`** map (see **`BARE_TOP_SNAPSHOT_LITE_ENTRIES`** in **`packages/bare-os-coreutils/lib/baretop-snapshot.js`**). Full batch includes **`securityPosture`** → **`/proc/bare_os/security_posture.json`**. **`metrics_live`** JSON uses **`schema: 4`** with embedded **`processTable`** when present; kernel counters include optional **`vfs.readfile.samples`** ( **`BARE_OS_VFS_READ_METRICS`** ), **`initd.unit_failed_final`**, and **`shell.pipeline_last_stages`** gauge from the shell.
|
||||
- **`/proc/bare_os/net_summary.json`**: **`schemaVersion` 2** adds scalar **`replicationQueueDepth`**, firewall session totals, optional **`BARE_OS_BSD_SOCKET_BRIDGE_STATS_JSON`** merge into **`bsdSocketGuestBridge`**; **`peer_firewall_stats`** field alignment documented in **`bare-os-protocol`** channel handler.
|
||||
|
||||
@@ -821,6 +821,12 @@ async function executeKernel(disk, store, swarm, initSource) {
|
||||
) {
|
||||
shellEnv.BARE_OS_HOLESAIL_MANAGED = '1'
|
||||
}
|
||||
if (
|
||||
shellEnv.BARE_OS_HOLESAIL_STATE === undefined ||
|
||||
shellEnv.BARE_OS_HOLESAIL_STATE === ''
|
||||
) {
|
||||
shellEnv.BARE_OS_HOLESAIL_STATE = '/.bare/holesail/state.json'
|
||||
}
|
||||
let bootProfileResolved = ''
|
||||
const bpfEarly = shellEnv.BARE_OS_BOOT_PROFILE
|
||||
if (bpfEarly != null && String(bpfEarly).trim()) {
|
||||
|
||||
@@ -4,15 +4,21 @@ import { dirname } from '#host-path'
|
||||
* Multi-tunnel Holesail manager: persisted state on the VFS, one Holesail instance per entry.
|
||||
* Used when managed mode is on (stock default with BARE_OS_HOLESAIL_INITD/MANAGED).
|
||||
*
|
||||
* State path: BARE_OS_HOLESAIL_STATE or default ~/.holesail/state.json (VFS expands ~/ under $HOME).
|
||||
* State path: BARE_OS_HOLESAIL_STATE or default `/.bare/holesail/state.json` (personal drive root;
|
||||
* stable across guest vs unlocked `$HOME`, including when `BARE_OS_PERSONAL_ACCT_PREFIX` isolates other `/.bare/**` paths).
|
||||
* Upstream holesail is AGPL-3.0.
|
||||
*/
|
||||
import { bareHolesailEnvTruthy } from './bare-holesail-env.js'
|
||||
import { loadHolesailConstructor } from './bare-holesail-loader.js'
|
||||
import { appendVarLog, BARE_OS_VAR_LOG_DIR } from './bare-os-var-log.js'
|
||||
|
||||
export const BARE_HOLESAIL_STATE_DIR = '~/.holesail'
|
||||
export const BARE_HOLESAIL_STATE_DEFAULT = `${BARE_HOLESAIL_STATE_DIR}/state.json`
|
||||
/** Legacy default (per-session `$HOME`); migration reads this when upgrading to {@link BARE_HOLESAIL_STATE_STABLE}. */
|
||||
export const BARE_HOLESAIL_STATE_LEGACY = '~/.holesail/state.json'
|
||||
|
||||
/** Stock managed state file — same logical path for guest and logged-in users. */
|
||||
export const BARE_HOLESAIL_STATE_STABLE = '/.bare/holesail/state.json'
|
||||
|
||||
export const BARE_HOLESAIL_STATE_DEFAULT = BARE_HOLESAIL_STATE_STABLE
|
||||
|
||||
const HOLESAIL_LOG = `${BARE_OS_VAR_LOG_DIR}/holesail.log`
|
||||
|
||||
@@ -143,6 +149,84 @@ function coerceState(parsed) {
|
||||
return { version: 1, connections }
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ version: number, connections: Record<string, unknown> }} a
|
||||
* @param {{ version: number, connections: Record<string, unknown> }} b
|
||||
*/
|
||||
function mergeCoercedHolesailStates(a, b) {
|
||||
const conns = { ...a.connections }
|
||||
for (const [id, raw] of Object.entries(b.connections || {})) {
|
||||
if (!raw || typeof raw !== 'object') continue
|
||||
const cur = conns[id]
|
||||
if (!cur || typeof cur !== 'object') {
|
||||
conns[id] = raw
|
||||
continue
|
||||
}
|
||||
const c = /** @type {Record<string, unknown>} */ (cur)
|
||||
const n = /** @type {Record<string, unknown>} */ (raw)
|
||||
const ck = String(c.key ?? '').trim()
|
||||
const nk = String(n.key ?? '').trim()
|
||||
if (nk && !ck) conns[id] = { ...c, ...n, key: nk }
|
||||
else if (ck && !nk) conns[id] = { ...n, ...c, key: ck }
|
||||
else conns[id] = { ...c, ...n }
|
||||
}
|
||||
return { version: 1, connections: conns }
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {{ readFile: (p: string) => Promise<unknown> }} vfs
|
||||
* @param {string} logicalPath
|
||||
*/
|
||||
async function tryReadCoercedStateFile(ctx, vfs, logicalPath) {
|
||||
try {
|
||||
const buf = await vfs.readFile(logicalPath)
|
||||
const text = ctx.b4a
|
||||
? ctx.b4a.toString(buf, 'utf8')
|
||||
: Buffer.from(buf).toString('utf8')
|
||||
return coerceState(JSON.parse(text))
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One-time upgrade: merge legacy per-`$HOME` `~/.holesail/state.json` (guest + current session)
|
||||
* into the stable `/.bare/holesail/state.json` when the latter is missing or empty.
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {Record<string, string | undefined>} env
|
||||
* @param {string} primaryPath
|
||||
* @returns {Promise<{ version: number, connections: Record<string, unknown> } | null>}
|
||||
*/
|
||||
async function bareHolesailManagedTryMigrateLegacyState(ctx, env, primaryPath) {
|
||||
if (primaryPath !== BARE_HOLESAIL_STATE_STABLE) return null
|
||||
const vfs = ctx.vfs
|
||||
if (!vfs || typeof vfs.readFile !== 'function' || typeof vfs.resolveLogical !== 'function')
|
||||
return null
|
||||
|
||||
/** @type {{ version: number, connections: Record<string, unknown> }} */
|
||||
let merged = { version: 1, connections: {} }
|
||||
const seen = new Set()
|
||||
/** @type {string[]} */
|
||||
const paths = ['/home/guest/.holesail/state.json']
|
||||
try {
|
||||
paths.push(vfs.resolveLogical('~/.holesail/state.json'))
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
for (const p of paths) {
|
||||
const norm = String(p || '').trim()
|
||||
if (!norm || seen.has(norm)) continue
|
||||
seen.add(norm)
|
||||
const chunk = await tryReadCoercedStateFile(ctx, vfs, norm)
|
||||
if (chunk && Object.keys(chunk.connections).length > 0) {
|
||||
merged = mergeCoercedHolesailStates(merged, chunk)
|
||||
}
|
||||
}
|
||||
if (Object.keys(merged.connections).length === 0) return null
|
||||
return merged
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, unknown>} ctx
|
||||
* @param {Record<string, string | undefined>} env
|
||||
@@ -157,10 +241,21 @@ export async function bareHolesailManagedReadState(ctx, env) {
|
||||
const buf = await vfs.readFile(path)
|
||||
const text = ctx.b4a ? ctx.b4a.toString(buf, 'utf8') : Buffer.from(buf).toString('utf8')
|
||||
const parsed = JSON.parse(text)
|
||||
return { path, state: coerceState(parsed) }
|
||||
const state = coerceState(parsed)
|
||||
if (Object.keys(state.connections).length > 0) return { path, state }
|
||||
} catch {
|
||||
return { path, state: { version: 1, connections: {} } }
|
||||
/* missing or corrupt */
|
||||
}
|
||||
const migrated = await bareHolesailManagedTryMigrateLegacyState(ctx, env, path)
|
||||
if (migrated && Object.keys(migrated.connections).length > 0) {
|
||||
try {
|
||||
await bareHolesailManagedWriteState(ctx, env, migrated)
|
||||
} catch {
|
||||
/* return merged in-memory even if persist fails */
|
||||
}
|
||||
return { path, state: migrated }
|
||||
}
|
||||
return { path, state: { version: 1, connections: {} } }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Drive-resident /bin/holesail → ctx.bareOsRunHolesailCli (booter).
|
||||
* Manages persisted tunnels under BARE_OS_HOLESAIL_STATE (default ~/.holesail/state.json).
|
||||
* Manages persisted tunnels under BARE_OS_HOLESAIL_STATE (default `/.bare/holesail/state.json`).
|
||||
* Live start/stop uses the bare-holesail initd unit (managed mode is the stock default).
|
||||
*/
|
||||
import {
|
||||
|
||||
@@ -3966,8 +3966,13 @@ export function createVfs(
|
||||
? '/.bare'
|
||||
: absPath.replace(/\/+$/, '') || absPath
|
||||
if (usePersonalAcctPrefix()) {
|
||||
const layoutRoot = personalLayoutRootAbs()
|
||||
const tail = bareTop === '/.bare' ? '' : p.slice('/.bare'.length)
|
||||
// Managed Holesail persistence: keep `/.bare/holesail/**` on the personal drive root so
|
||||
// guest vs unlocked sessions share one `state.json` (not under per-session `acct/…` layout).
|
||||
if (tail === '/holesail' || tail.startsWith('/holesail/')) {
|
||||
return { drive: personalDrive, path: p }
|
||||
}
|
||||
const layoutRoot = personalLayoutRootAbs()
|
||||
const physical = layoutRoot + '/.bare' + tail
|
||||
return { drive: personalDrive, path: physical }
|
||||
}
|
||||
@@ -4276,6 +4281,63 @@ export function createVfs(
|
||||
|
||||
async function assertTraverseTo(abs, finalOp) {
|
||||
const { uid: euid, gid: egid } = parseUidGid(env)
|
||||
const rTr = route(abs)
|
||||
if (
|
||||
personalDrive &&
|
||||
rTr.drive === personalDrive &&
|
||||
typeof rTr.path === 'string'
|
||||
) {
|
||||
const rp = rTr.path.replace(/\/+$/, '') || '/'
|
||||
if (rp === '/.bare/holesail' || rp.startsWith('/.bare/holesail/')) {
|
||||
const phys = rTr.path.replace(/\/+$/, '') || rTr.path
|
||||
const physPrefixes = pathPrefixes(phys)
|
||||
for (let i = 0; i < physPrefixes.length; i++) {
|
||||
const ppre = physPrefixes[i]
|
||||
const isLast = i === physPrefixes.length - 1
|
||||
if (ppre === '/') continue
|
||||
const st = await lstatPersonalDrivePhysicalAny(ppre)
|
||||
if (!isLast) {
|
||||
if (!st) throw new Error('ENOENT: ' + abs)
|
||||
if (st.type !== 'directory') {
|
||||
throw new Error('Not a directory: ' + ppre)
|
||||
}
|
||||
if (!modeAllows(st, euid, egid, 'x', 'directory')) {
|
||||
throw new Error('EACCES: cannot traverse ' + ppre)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (!st) {
|
||||
if (finalOp === 'read') return
|
||||
throw new Error('ENOENT: ' + abs)
|
||||
}
|
||||
if (st.type === 'directory') {
|
||||
if (finalOp === 'readdir') {
|
||||
if (!modeAllows(st, euid, egid, 'r', 'directory')) {
|
||||
throw new Error('EACCES: cannot read directory ' + ppre)
|
||||
}
|
||||
if (!modeAllows(st, euid, egid, 'x', 'directory')) {
|
||||
throw new Error('EACCES: cannot access directory ' + ppre)
|
||||
}
|
||||
} else if (finalOp === 'chdir') {
|
||||
if (!modeAllows(st, euid, egid, 'x', 'directory')) {
|
||||
throw new Error('EACCES: permission denied: ' + ppre)
|
||||
}
|
||||
}
|
||||
} else if (st.type === 'file' || st.type === 'symlink') {
|
||||
if (finalOp === 'read') {
|
||||
if (!modeAllows(st, euid, egid, 'r', 'file')) {
|
||||
throw new Error('EACCES: cannot read ' + ppre)
|
||||
}
|
||||
} else if (finalOp === 'write') {
|
||||
if (!modeAllows(st, euid, egid, 'w', 'file')) {
|
||||
throw new Error('EACCES: cannot write ' + ppre)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
const prefixes = pathPrefixes(abs)
|
||||
for (let i = 0; i < prefixes.length; i++) {
|
||||
const pre = prefixes[i]
|
||||
@@ -4347,9 +4409,177 @@ export function createVfs(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `/.bare/holesail/**` on the personal drive: parent checks and traverse must walk **physical**
|
||||
* paths on that drive — logical `/.bare` may map to a different backing path under
|
||||
* {@link usePersonalAcctPrefix}, and `/` is skipped as a virtual mount point.
|
||||
*/
|
||||
async function lstatPersonalDrivePhysicalDir(physPath) {
|
||||
if (!personalDrive) return null
|
||||
const pnorm = String(physPath || '').replace(/\/+$/, '') || '/'
|
||||
if (pnorm === '/') {
|
||||
return { ...synthesizeStat('/', true, env, 'directory'), path: pnorm }
|
||||
}
|
||||
const e = await entryOn(personalDrive, pnorm, { follow: false })
|
||||
const fromVal = statFromEntryValue(
|
||||
pnorm,
|
||||
{ drive: personalDrive, path: pnorm },
|
||||
true,
|
||||
e
|
||||
)
|
||||
if (fromVal) return { ...fromVal, path: pnorm }
|
||||
const names = []
|
||||
try {
|
||||
const stream = personalDrive.readdir(pnorm)
|
||||
for await (const n of stream) names.push(n)
|
||||
} catch {
|
||||
/* missing parent */
|
||||
}
|
||||
if (names.length) {
|
||||
if (names.includes(DIR_MARKER)) {
|
||||
const mPath = `${pnorm}/${DIR_MARKER}`.replace(/\/{2,}/g, '/')
|
||||
const me = await entryOn(personalDrive, mPath, { follow: false })
|
||||
const mv = me?.value
|
||||
if (mv?.blob) {
|
||||
const bo = extractBareOs(mv)
|
||||
if (bo) {
|
||||
let perm = bo.mode & 0o777
|
||||
if (perm & 0o400) perm |= 0o100
|
||||
if (perm & 0o040) perm |= 0o010
|
||||
if (perm & 0o004) perm |= 0o001
|
||||
return statFromBareOs(
|
||||
{ ...bo, mode: S_IFDIR | perm },
|
||||
'directory',
|
||||
0,
|
||||
pnorm
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return { ...synthesizeStat(pnorm, true, env, 'directory'), path: pnorm }
|
||||
}
|
||||
if (e) return { ...synthesizeStat(pnorm, true, env, 'directory'), path: pnorm }
|
||||
return null
|
||||
}
|
||||
|
||||
/** Directory or regular file (or symlink) on the personal drive by absolute path on that drive. */
|
||||
async function lstatPersonalDrivePhysicalAny(physPath) {
|
||||
if (!personalDrive) return null
|
||||
const pnorm = String(physPath || '').replace(/\/+$/, '') || '/'
|
||||
if (pnorm === '/') {
|
||||
return { ...synthesizeStat('/', true, env, 'directory'), path: pnorm }
|
||||
}
|
||||
const e = await entryOn(personalDrive, pnorm, { follow: false })
|
||||
const rf = statFromEntryValue(
|
||||
pnorm,
|
||||
{ drive: personalDrive, path: pnorm },
|
||||
true,
|
||||
e
|
||||
)
|
||||
if (rf) return rf
|
||||
return await lstatPersonalDrivePhysicalDir(pnorm)
|
||||
}
|
||||
|
||||
async function assertPersonalDriveAncestorWritableForCreate(physPath) {
|
||||
if (!personalDrive) {
|
||||
throw new Error('ENOENT: ' + String(physPath || ''))
|
||||
}
|
||||
const { uid: euid, gid: egid } = parseUidGid(env)
|
||||
const norm = String(physPath || '').replace(/\/+$/, '') || '/'
|
||||
let probe = dirnameAbs(norm)
|
||||
/** @type {{ pre: string, st: Awaited<ReturnType<typeof lstatFromAbs>> } | null} */
|
||||
let deepest = null
|
||||
while (probe && probe !== '/') {
|
||||
const st = await lstatPersonalDrivePhysicalDir(probe)
|
||||
if (st && st.type === 'directory') {
|
||||
deepest = { pre: probe, st }
|
||||
break
|
||||
}
|
||||
probe = dirnameAbs(probe)
|
||||
}
|
||||
if (!deepest) {
|
||||
deepest = {
|
||||
pre: '/',
|
||||
st: await lstatPersonalDrivePhysicalDir('/')
|
||||
}
|
||||
}
|
||||
if (!deepest.st || !modeAllows(deepest.st, euid, egid, 'w', 'directory')) {
|
||||
throw new Error('EACCES: cannot create in ' + String(deepest?.pre ?? ''))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} logicalAbs policy / guest messaging
|
||||
* @param {string} physPath path on {@link personalDrive}
|
||||
*/
|
||||
async function putPersonalDrivePhysical(logicalAbs, physPath, buf, opts = {}) {
|
||||
assertNotBootPolicyDenyVfs(logicalAbs, 'write')
|
||||
assertUnionWriteNotDenied(logicalAbs)
|
||||
if (!personalDrive) {
|
||||
throw new Error('Read-only path (not under $HOME): ' + logicalAbs)
|
||||
}
|
||||
if (await bareOsVfsAclDeniesDriveOp(env, personalDrive, physPath, 'write')) {
|
||||
throw new Error('EACCES: ACL enforces deny write: ' + logicalAbs)
|
||||
}
|
||||
assertGuestSensitivePersonalOp(personalDrive, physPath, 'write', logicalAbs)
|
||||
const existing = await entryOn(personalDrive, physPath, { follow: false })
|
||||
const hadBlob = !!existing?.value?.blob
|
||||
if (!hadBlob) await assertPersonalDriveAncestorWritableForCreate(physPath)
|
||||
const value = existing?.value
|
||||
const prevBare = extractBareOs(value)
|
||||
const bareOs = mergeBareOsOnWrite(prevBare, env, {
|
||||
executable: opts.executable,
|
||||
bumpMtime: opts.bumpMtime !== false,
|
||||
touchCtime: opts.touchCtime === true,
|
||||
legacyExecutable: !!value?.executable,
|
||||
mtimeMs: opts.mtimeMs,
|
||||
ctimeMs: opts.ctimeMs,
|
||||
posixModeBits: opts.posixModeBits
|
||||
})
|
||||
const executable =
|
||||
opts.executable !== undefined ? !!opts.executable : !!value?.executable
|
||||
const metadata = mergeEntryMetadata(value?.metadata, bareOs)
|
||||
return personalDrive.put(physPath, buf, { executable, metadata })
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure `/.bare` and `/.bare/holesail` DIR_MARKER entries exist on the personal drive for stable
|
||||
* Holesail paths (needed when `/` is skipped as a virtual mount point in parent checks, and when
|
||||
* `/.bare` is not the same backing path as `/.bare/holesail/**` under {@link usePersonalAcctPrefix}).
|
||||
*/
|
||||
async function ensureBareHolesailStablePersonalDirTree(physPath, logicalAbs) {
|
||||
if (!personalDrive) return
|
||||
const norm = String(physPath || '').replace(/\/+$/, '') || '/'
|
||||
const d = dirnameAbs(norm)
|
||||
if (d === '/' || d === '') return
|
||||
const physPrefs = pathPrefixes(d)
|
||||
const empty = new Uint8Array(0)
|
||||
for (let i = 1; i < physPrefs.length; i++) {
|
||||
const physPre = physPrefs[i]
|
||||
const st = await lstatPersonalDrivePhysicalDir(physPre)
|
||||
if (st) continue
|
||||
const markerPath =
|
||||
physPre === '/' ? `/${DIR_MARKER}` : `${physPre}/${DIR_MARKER}`
|
||||
await putPersonalDrivePhysical(logicalAbs, markerPath, empty, {})
|
||||
}
|
||||
}
|
||||
|
||||
async function assertParentWritableForCreate(abs) {
|
||||
const parent = dirnameAbs(abs)
|
||||
if (parent === abs) return
|
||||
const rH = route(abs)
|
||||
if (
|
||||
personalDrive &&
|
||||
rH.drive === personalDrive &&
|
||||
typeof rH.path === 'string'
|
||||
) {
|
||||
const rp = rH.path.replace(/\/+$/, '') || '/'
|
||||
if (rp === '/.bare/holesail' || rp.startsWith('/.bare/holesail/')) {
|
||||
const physPath = rH.path.replace(/\/+$/, '') || rH.path
|
||||
await assertPersonalDriveAncestorWritableForCreate(physPath)
|
||||
return
|
||||
}
|
||||
}
|
||||
const { uid: euid, gid: egid } = parseUidGid(env)
|
||||
const prefixes = pathPrefixes(parent)
|
||||
/** @type {{ pre: string, st: Awaited<ReturnType<typeof lstatFromAbs>> } | null} */
|
||||
@@ -5114,7 +5344,16 @@ export function createVfs(
|
||||
drive === systemDrive &&
|
||||
isWarmReadCachePath(abs)
|
||||
if (!skipModeTraverse) await assertTraverseTo(abs, 'write')
|
||||
} else await assertParentWritableForCreate(abs)
|
||||
} else {
|
||||
await assertParentWritableForCreate(abs)
|
||||
if (personalDrive && drive === personalDrive && typeof p === 'string') {
|
||||
const rp = p.replace(/\/+$/, '') || '/'
|
||||
if (rp === '/.bare/holesail' || rp.startsWith('/.bare/holesail/')) {
|
||||
const phys = p.replace(/\/+$/, '') || p
|
||||
await ensureBareHolesailStablePersonalDirTree(phys, abs)
|
||||
}
|
||||
}
|
||||
}
|
||||
const value = existing?.value
|
||||
const prevBare = extractBareOs(value)
|
||||
const bareOs = mergeBareOsOnWrite(prevBare, env, {
|
||||
@@ -5803,6 +6042,31 @@ export function createVfs(
|
||||
await writeFileAtAbs(joinLogical(abs, DIR_MARKER), empty, markerPutOpts)
|
||||
return
|
||||
}
|
||||
const rMk = route(abs)
|
||||
if (
|
||||
recursive &&
|
||||
personalDrive &&
|
||||
rMk.drive === personalDrive &&
|
||||
typeof rMk.path === 'string'
|
||||
) {
|
||||
const rp = rMk.path.replace(/\/+$/, '') || '/'
|
||||
if (rp === '/.bare/holesail' || rp.startsWith('/.bare/holesail/')) {
|
||||
const norm = rMk.path.replace(/\/+$/, '') || rMk.path
|
||||
const physPrefs = pathPrefixes(norm)
|
||||
for (let i = 1; i < physPrefs.length; i++) {
|
||||
const physPre = physPrefs[i]
|
||||
const st = await lstatPersonalDrivePhysicalDir(physPre)
|
||||
if (st) {
|
||||
if (st.type === 'directory') continue
|
||||
throw new Error('mkdir: File exists')
|
||||
}
|
||||
const markerPath =
|
||||
physPre === '/' ? `/${DIR_MARKER}` : `${physPre}/${DIR_MARKER}`
|
||||
await putPersonalDrivePhysical(abs, markerPath, empty, markerPutOpts)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
const prefs = pathPrefixes(abs)
|
||||
for (let i = 1; i < prefs.length; i++) {
|
||||
const pre = prefs[i]
|
||||
|
||||
@@ -162,7 +162,9 @@ test('ensureBareOsWwwHolesailTunnel persists managed server entry', async (t) =>
|
||||
const personal = new Hyperdrive(store.namespace('pwwhs'))
|
||||
await sys.ready()
|
||||
await personal.ready()
|
||||
const ctx = testCtx(sys, personal, { BARE_OS_HOLESAIL_STATE: '~/.holesail/state.json' })
|
||||
const ctx = testCtx(sys, personal, {
|
||||
BARE_OS_HOLESAIL_STATE: '/.bare/holesail/state.json'
|
||||
})
|
||||
const p = 18188
|
||||
const id = bareOsWwwHolesailConnectionId(p)
|
||||
await ctx.vfs.mkdir(ctx.vfs.resolveLogical('~/.holesail'), { recursive: true })
|
||||
|
||||
@@ -2708,6 +2708,52 @@ test('BARE_OS_PERSONAL_ACCT_PREFIX nests guest tmp on personal drive', async (t)
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('BARE_OS_PERSONAL_ACCT_PREFIX: /.bare/holesail shares personal drive root across guest and unlocked', async (t) => {
|
||||
const dir = testCorestoreDir('vfsholesailstable')
|
||||
const store = new Corestore(dir)
|
||||
const sys = new Hyperdrive(store)
|
||||
const personal = new Hyperdrive(store.namespace('pvhsstable'))
|
||||
await sys.ready()
|
||||
await personal.ready()
|
||||
const envG = {
|
||||
HOME: '/home/guest',
|
||||
PWD: '/home/guest',
|
||||
PATH: '/bin',
|
||||
USER: 'guest',
|
||||
UID: '65534',
|
||||
GID: '65534',
|
||||
BARE_OS_PERSONAL_ACCT_PREFIX: '1'
|
||||
}
|
||||
const vfsG = createVfs(sys, personal, envG)
|
||||
vfsG.bareOsIdentitySession = 'guest'
|
||||
const payload = JSON.stringify({
|
||||
version: 1,
|
||||
connections: { t: { server: true } }
|
||||
})
|
||||
await vfsG.writeFile('/.bare/holesail/state.json', b4a.from(payload))
|
||||
t.alike(JSON.parse(b4a.toString(await personal.get('/.bare/holesail/state.json'))), {
|
||||
version: 1,
|
||||
connections: { t: { server: true } }
|
||||
})
|
||||
const envU = {
|
||||
HOME: '/home/alicehs',
|
||||
PWD: '/home/alicehs',
|
||||
PATH: '/bin',
|
||||
USER: 'alicehs',
|
||||
UID: '1001',
|
||||
GID: '1001',
|
||||
BARE_OS_PERSONAL_ACCT_PREFIX: '1'
|
||||
}
|
||||
const vfsU = createVfs(sys, personal, envU)
|
||||
vfsU.bareOsIdentitySession = 'unlocked'
|
||||
t.alike(JSON.parse(b4a.toString(await vfsU.readFile('/.bare/holesail/state.json'))), {
|
||||
version: 1,
|
||||
connections: { t: { server: true } }
|
||||
})
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('vfs /mnt lists HDMS mounts and allows writable put', async (t) => {
|
||||
const dir = testCorestoreDir('vfsmnt')
|
||||
const store = new Corestore(dir)
|
||||
|
||||
@@ -21,7 +21,7 @@ Use when the user asks about **Holesail** in this repo: exposing TCP/UDP through
|
||||
|
||||
## Managed vs single-tunnel mode
|
||||
|
||||
- **Managed (default)** — **`BARE_OS_HOLESAIL_MANAGED=1`**. Persisted tunnel list in **`BARE_OS_HOLESAIL_STATE`** (default **`~/.holesail/state.json`**, on the **personal** drive under the session **`$HOME`**). On unit start, each **enabled** entry gets its own **`holesail`** instance. **Server** rows without **`key`** receive the minted **`hs://…`** string in **`state.json`** after **`ready()`** so the URL survives reboot/login (**`bare-holesail-managed.js`**). Operator CLI: **`holesail list`**, **`add`**, **`remove`**, **`start`**, **`stop`**, **`restart`**, **`enable`**, **`disable`**, plus **`path`** (prints resolved state path) and **`help`**.
|
||||
- **Managed (default)** — **`BARE_OS_HOLESAIL_MANAGED=1`**. Persisted tunnel list in **`BARE_OS_HOLESAIL_STATE`** (default **`/.bare/holesail/state.json`** — personal drive root, **shared** guest ↔ unlocked; not per-**`$HOME`** **`~/.holesail`**). On unit start, each **enabled** entry gets its own **`holesail`** instance. **Server** rows without **`key`** receive the minted **`hs://…`** string in **`state.json`** after **`ready()`** (**`bare-holesail-managed.js`**). Operator CLI: **`holesail list`**, **`add`**, **`remove`**, **`start`**, **`stop`**, **`restart`**, **`enable`**, **`disable`**, plus **`path`** (prints resolved state path) and **`help`**.
|
||||
- **Single tunnel** — Set **`BARE_OS_HOLESAIL_MANAGED=0`**, then exactly one of **`BARE_OS_HOLESAIL_SERVER=1`** or **`BARE_OS_HOLESAIL_CLIENT=1`**, and in client mode **`BARE_OS_HOLESAIL_KEY=…`**. Optional: **`BARE_OS_HOLESAIL_SECURE`**, **`PORT`**, **`HOST`**, **`UDP`**, **`LOG`**.
|
||||
|
||||
## Early booter (“kernel-path”) instance
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
# BARE_OS_HOLESAIL_SECURE=1
|
||||
#
|
||||
# Multi-tunnel (default; persisted state via /bin/holesail):
|
||||
# # optional: BARE_OS_HOLESAIL_STATE=~/.holesail/state.json
|
||||
# # optional: BARE_OS_HOLESAIL_STATE=/.bare/holesail/state.json
|
||||
#
|
||||
# Optional ordering override:
|
||||
[Unit]
|
||||
|
||||
@@ -21,7 +21,7 @@ Use when the user asks about **Holesail** in this repo: exposing TCP/UDP through
|
||||
|
||||
## Managed vs single-tunnel mode
|
||||
|
||||
- **Managed (default)** — **`BARE_OS_HOLESAIL_MANAGED=1`**. Persisted tunnel list in **`BARE_OS_HOLESAIL_STATE`** (default **`~/.holesail/state.json`**, on the **personal** drive under the session **`$HOME`**). On unit start, each **enabled** entry gets its own **`holesail`** instance. **Server** rows without **`key`** receive the minted **`hs://…`** string in **`state.json`** after **`ready()`** so the URL survives reboot/login (**`bare-holesail-managed.js`**). Operator CLI: **`holesail list`**, **`add`**, **`remove`**, **`start`**, **`stop`**, **`restart`**, **`enable`**, **`disable`**, plus **`path`** (prints resolved state path) and **`help`**.
|
||||
- **Managed (default)** — **`BARE_OS_HOLESAIL_MANAGED=1`**. Persisted tunnel list in **`BARE_OS_HOLESAIL_STATE`** (default **`/.bare/holesail/state.json`** — personal drive root, **shared** guest ↔ unlocked; not per-**`$HOME`** **`~/.holesail`**). On unit start, each **enabled** entry gets its own **`holesail`** instance. **Server** rows without **`key`** receive the minted **`hs://…`** string in **`state.json`** after **`ready()`** (**`bare-holesail-managed.js`**). Operator CLI: **`holesail list`**, **`add`**, **`remove`**, **`start`**, **`stop`**, **`restart`**, **`enable`**, **`disable`**, plus **`path`** (prints resolved state path) and **`help`**.
|
||||
- **Single tunnel** — Set **`BARE_OS_HOLESAIL_MANAGED=0`**, then exactly one of **`BARE_OS_HOLESAIL_SERVER=1`** or **`BARE_OS_HOLESAIL_CLIENT=1`**, and in client mode **`BARE_OS_HOLESAIL_KEY=…`**. Optional: **`BARE_OS_HOLESAIL_SECURE`**, **`PORT`**, **`HOST`**, **`UDP`**, **`LOG`**.
|
||||
|
||||
## Early booter (“kernel-path”) instance
|
||||
|
||||
Reference in New Issue
Block a user