clawdie-ai/setup/browser-jail.ts
Operator & Claude Code 2c9c031fea Fix browser rc.d NODE_PATH and update stale Decided section (Sam & Claude)
- 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
2026-05-12 18:18:12 +02:00

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,
});
}