- setup/browser-jail.ts: NODE_PATH=/opt/browser-validation/node_modules -> /opt/clawdie/node_modules to match where ensureBrowserBackendDeps actually installs deps - docs/internal/BROWSER-JAIL.md: Decided section now says host-resident xpra over SSH, not controlplane-streamed clone
247 lines
7 KiB
TypeScript
247 lines
7 KiB
TypeScript
/**
|
|
* 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<void> {
|
|
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,
|
|
});
|
|
}
|