This commit is contained in:
Raven Scott
2026-04-22 04:41:19 -04:00
parent eebf6a0e23
commit 6b8f28bdfd
15 changed files with 433 additions and 19 deletions
+1 -1
View File
@@ -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.
+6
View File
@@ -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 -1
View File
@@ -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 {
+266 -2
View File
@@ -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]
+3 -1
View File
@@ -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 })
+46
View File
@@ -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