/** * setup/browser-jail.ts — Provision the fixed thick browser template jail. * * Creates the credential-free `browser` substrate at WARDEN_BROWSER_IP, * installs Chromium + Node tooling, applies the initial ZFS quota, and leaves * the jail stopped with boot disabled so clones can be created from a clean * template. */ import { execFileSync } from 'child_process'; import fs from 'fs'; import { BROWSER_JAIL_IP, PLATFORM_INTERNAL_BASE, SUBNET_BASE, ZFS_PREFIX, } from '../src/config.js'; import { logger } from '../src/logger.js'; import { platformServiceDomain } from '../src/platform-layout.js'; import { readEnvFile } from '../src/env.js'; import { bastille, detectFreeBSDRelease, jailExists, jailRoot, } from './bastille-helpers.js'; import { loadJailRegistry } from '../src/jail-schema.js'; import { loadPackageList, mountPkgCacheInJail, unmountPkgCacheInJail, } from './packages.js'; import { commandExists, getPlatform, isRoot } from './platform.js'; import { emitStatus } from './status.js'; const LOG = 'logs/setup.log'; const JAIL_NAME = 'browser'; const BROWSER_VALIDATION_DIR = '/opt/browser-validation'; const BROWSER_BACKEND_DEPS_DIR = '/opt/clawdie'; const PUPPETEER_CORE_VERSION = '24.43.0'; const ZOD_VERSION = '4.3.6'; const BROWSER_JAIL_QUOTA = '10G'; function applyBrowserJailQuota(jailName: string): void { const envValues = readEnvFile(['ZFS_POOL']); const zfsPool = (process.env.ZFS_POOL || envValues.ZFS_POOL || 'zroot').trim() || 'zroot'; const dataset = `${zfsPool}/${ZFS_PREFIX}/jails/${jailName}`; try { execFileSync('zfs', ['list', '-H', dataset], { stdio: ['ignore', 'ignore', 'ignore'], }); } catch { logger.warn( { dataset }, 'Browser jail dataset missing; skipping quota application', ); return; } execFileSync('zfs', ['set', `quota=${BROWSER_JAIL_QUOTA}`, dataset], { stdio: ['ignore', 'ignore', 'pipe'], }); } function browserBackendRcdScript(): string { return [ '#!/bin/sh', '#', '# PROVIDE: clawdie_browser', '# REQUIRE: NETWORKING LOGIN', '# KEYWORD: shutdown', '', '. /etc/rc.subr', '', 'name="clawdie_browser"', 'rcvar="clawdie_browser_enable"', 'command="/usr/sbin/daemon"', 'pidfile="/var/run/clawdie-browser.pid"', 'child_pidfile="/var/run/clawdie-browser-node.pid"', 'command_args="-P ${pidfile} -p ${child_pidfile} -r -o /var/log/clawdie-browser.log /usr/bin/env NODE_PATH=/opt/clawdie/node_modules /usr/local/bin/node /opt/clawdie/browser-backend/index.js"', '', 'load_rc_config $name', ': ${clawdie_browser_enable:="NO"}', '', 'run_rc_command "$1"', '', ].join('\n'); } function installBrowserBackendService(jailName: string): void { const root = jailRoot(jailName); const rcdDir = `${root}/usr/local/etc/rc.d`; fs.mkdirSync(rcdDir, { recursive: true }); fs.writeFileSync(`${rcdDir}/clawdie_browser`, browserBackendRcdScript(), { mode: 0o755, }); } function ensureBrowserBackendDeps(jailName: string): void { const install = bastille( 'cmd', jailName, '/bin/sh', '-lc', [ `mkdir -p ${BROWSER_BACKEND_DEPS_DIR}`, `cd ${BROWSER_BACKEND_DEPS_DIR}`, '[ -f package.json ] || npm init -y >/dev/null 2>&1', `npm install --no-save puppeteer-core@${PUPPETEER_CORE_VERSION} zod@${ZOD_VERSION}`, ].join(' && '), ); if (!install.ok) { throw new Error( `browser backend dependency install failed in ${jailName}: ${install.output}`, ); } } function ensurePuppeteerCore(jailName: string): void { const install = bastille( 'cmd', jailName, '/bin/sh', '-lc', [ `mkdir -p ${BROWSER_VALIDATION_DIR}`, `cd ${BROWSER_VALIDATION_DIR}`, '[ -f package.json ] || npm init -y >/dev/null 2>&1', `npm install --no-save puppeteer-core@${PUPPETEER_CORE_VERSION}`, ].join(' && '), ); if (!install.ok) { throw new Error( `puppeteer-core install failed in ${jailName}: ${install.output}`, ); } } export async function run(_args: string[]): Promise { if (getPlatform() !== 'freebsd') { emitStatus('SETUP_BROWSER_JAIL', { STATUS: 'failed', ERROR: 'unsupported_platform', LOG, }); process.exit(1); } if (!isRoot()) { emitStatus('SETUP_BROWSER_JAIL', { STATUS: 'failed', ERROR: 'requires_root', LOG, }); throw new Error('setup_browser_jail_requires_root'); } if (!commandExists('bastille')) { emitStatus('SETUP_BROWSER_JAIL', { STATUS: 'failed', ERROR: 'missing_bastille', LOG, }); throw new Error('missing_bastille'); } const registry = loadJailRegistry(); const jailDef = registry.jails[JAIL_NAME]; if (!jailDef) { throw new Error('browser jail missing from infra/jails.yaml'); } const gateway = process.env.WARDEN_GATEWAY || `${SUBNET_BASE}.1`; const bridge = process.env.WARDEN_BRIDGE || registry.bridge; const release = detectFreeBSDRelease(); const hostname = platformServiceDomain('browser', PLATFORM_INTERNAL_BASE); const created = !jailExists(JAIL_NAME); try { if (created) { logger.info( { jailName: JAIL_NAME, ip: BROWSER_JAIL_IP, release }, 'Creating browser jail', ); const create = bastille( 'create', ...(jailDef.thick ? ['-T'] : []), ...(jailDef.vnet ? ['-B'] : []), '-g', gateway, JAIL_NAME, release, `${BROWSER_JAIL_IP}/24`, bridge, ); if (!create.ok) { throw new Error(`bastille create failed: ${create.output}`); } } else { logger.info({ jailName: JAIL_NAME }, 'Browser jail already exists'); const start = bastille('start', JAIL_NAME); if (!start.ok && !/already running/i.test(start.output)) { throw new Error(`bastille start failed: ${start.output}`); } } bastille('config', JAIL_NAME, 'set', 'host.hostname', hostname); const restart = bastille('restart', JAIL_NAME); if (!restart.ok) { throw new Error(`bastille restart failed: ${restart.output}`); } mountPkgCacheInJail(JAIL_NAME); const packages = loadPackageList('browser-jail.txt'); const pkg = bastille('pkg', JAIL_NAME, 'install', '-y', ...packages); if (!pkg.ok) { throw new Error(`browser package install failed: ${pkg.output}`); } ensurePuppeteerCore(JAIL_NAME); ensureBrowserBackendDeps(JAIL_NAME); installBrowserBackendService(JAIL_NAME); applyBrowserJailQuota(JAIL_NAME); } catch (error) { const message = error instanceof Error ? error.message : String(error); emitStatus('SETUP_BROWSER_JAIL', { STATUS: 'failed', ERROR: message, LOG, }); throw error; } finally { unmountPkgCacheInJail(JAIL_NAME); bastille('stop', JAIL_NAME); bastille('config', JAIL_NAME, 'set', 'boot', 'off'); } emitStatus('SETUP_BROWSER_JAIL', { STATUS: 'success', JAIL_NAME, JAIL_IP: BROWSER_JAIL_IP, JAIL_QUOTA: BROWSER_JAIL_QUOTA, BOOT: 'off', LOG, }); }