clawdie-ai/setup/jails.ts
Clawdie AI 29fadfe1a6 refactor(infra): Phase 1 — jail registry, shared provisioner, eliminate 7x duplication (Sam & Claude)
Introduces infra/jails.yaml as single source of truth for jail definitions,

src/jail-schema.ts with Zod validation, src/jail-registry.ts for runtime,

setup/bastille-helpers.ts as shared module replacing 7 copy-pasted

bastille()/jailExists()/detectFreeBSDRelease() wrappers.

Refactors setup/{db,cms,git,forgejo,jails,llama-cpp,ollama,skills-memory}.ts

to import from bastille-helpers. Archives infra/ansible/ to .archive/.

Net reduction: ~300 lines of duplicated code. All IPs now derive

from jails.yaml with env var overrides preserved.

Build: pass | Tests: not run (Linux)
2026-04-14 01:16:13 +02:00

155 lines
4.1 KiB
TypeScript

/**
* setup/jails.ts — Provision the worker jail(s).
*
* Current main uses a persistent worker jail as a baseline sandbox. The agent
* runtime may still execute on the host depending on profile, but creating the
* worker jail keeps the FreeBSD jail layout consistent and ready.
*/
import { execSync, spawnSync } from 'child_process';
import { AGENT_NAME, SUBNET_BASE } from '../src/config.js';
import { logger } from '../src/logger.js';
import { loadPackageList, mountPkgCacheInJail } from './packages.js';
import { commandExists, getPlatform, isRoot } from './platform.js';
import { emitStatus } from './status.js';
import { maybeEnableTailscaleInJail } from './tailscale.js';
import {
bastille,
jailExists,
detectFreeBSDRelease,
} from './bastille-helpers.js';
const LOG = 'logs/setup.log';
export async function run(args: string[]): Promise<void> {
if (getPlatform() !== 'freebsd') {
emitStatus('SETUP_JAILS', {
STATUS: 'failed',
ERROR: 'unsupported_platform',
LOG,
});
process.exit(1);
}
if (!isRoot()) {
emitStatus('SETUP_JAILS', {
STATUS: 'failed',
ERROR: 'requires_root',
LOG,
});
throw new Error('setup_jails_requires_root');
}
if (!commandExists('bastille')) {
emitStatus('SETUP_JAILS', {
STATUS: 'failed',
ERROR: 'missing_bastille',
LOG,
});
throw new Error('missing_bastille');
}
const doCreate =
args.includes('--create') || args.includes('--create-worker');
if (!doCreate) {
emitStatus('SETUP_JAILS', {
STATUS: 'skipped',
REASON: 'missing_flag',
LOG,
});
logger.info('Jails step skipped — pass --create');
return;
}
const safeAgentName = AGENT_NAME.replace(/[-_]/g, '');
const preferredJailName = `${safeAgentName}worker`;
const legacyHyphenName = `${AGENT_NAME}-worker`;
const legacyPlainName = 'worker';
let workerJail = preferredJailName;
if (jailExists(legacyHyphenName) && !jailExists(preferredJailName)) {
workerJail = legacyHyphenName;
} else if (jailExists(legacyPlainName) && !jailExists(preferredJailName)) {
workerJail = legacyPlainName;
}
const workerIp =
process.env.WORKER_JAIL_IP_START ||
process.env.WORKER_JAIL_IP ||
`${SUBNET_BASE}.101`;
const gateway = process.env.WARDEN_GATEWAY || `${SUBNET_BASE}.1`;
const bridge = process.env.WARDEN_BRIDGE || 'warden0';
const release = detectFreeBSDRelease();
try {
if (!jailExists(workerJail)) {
logger.info(
{ jail: workerJail, ip: workerIp, release },
'Creating worker jail',
);
const create = bastille(
'create',
'-T',
'-B',
'-g',
gateway,
workerJail,
release,
`${workerIp}/24`,
bridge,
);
if (!create.ok) {
throw new Error(`bastille create failed: ${create.output}`);
}
bastille(
'config',
workerJail,
'set',
'host.hostname',
`worker.${AGENT_NAME}.home.arpa`,
);
bastille('restart', workerJail);
} else {
logger.info(
{ jail: workerJail },
'Worker jail already exists, skipping creation',
);
}
mountPkgCacheInJail(workerJail);
const pkgs = loadPackageList('worker-jail.txt');
const pkg = bastille('pkg', workerJail, 'install', '-y', ...pkgs);
if (!pkg.ok) {
logger.warn(
{ output: pkg.output },
'Worker jail package install had warnings',
);
}
const chsh = bastille(
'cmd',
workerJail,
'chsh',
'-s',
'/usr/local/bin/bash',
'root',
);
if (!chsh.ok) {
logger.warn(
{ output: chsh.output },
'chsh to bash failed in worker jail',
);
}
const runBastille = (args: string[]) => bastille(...args);
maybeEnableTailscaleInJail(runBastille, workerJail, workerJail);
emitStatus('SETUP_JAILS', {
STATUS: 'success',
WORKER_JAIL_NAME: workerJail,
WORKER_JAIL_IP: workerIp,
LOG,
});
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
emitStatus('SETUP_JAILS', { STATUS: 'failed', ERROR: message, LOG });
throw err;
}
}