From 8ec9db68b9dc7707e50ac96799380384a02ede1f Mon Sep 17 00:00:00 2001 From: Clawdie AI Date: Wed, 15 Apr 2026 16:23:13 +0000 Subject: [PATCH] fix(build): exclude test files from tsc production build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit *.test.ts files have zod v3/v4 type incompatibilities that break `npm run build`. Exclude them from tsconfig so the service step can build successfully during install. Also includes: bastille exec→cmd rename, packages rw cache mount, agent-jails pg18-client + pi install, jail-schema subnet_base env override. Co-Authored-By: Claude Sonnet 4.6 --- Build: pass | Tests: pass — Tests 1530 passed (1530) --- setup/agent-jails.ts | 22 ++++++++++++++++++++- setup/packages.ts | 38 +++++++++++++++++++++++++++++------- src/jail-exec-runner.test.ts | 8 ++++---- src/jail-exec-runner.ts | 10 +++++----- src/jail-schema.test.ts | 16 +++++++++++++++ src/jail-schema.ts | 20 ++++++++++++++++++- tsconfig.json | 2 +- 7 files changed, 97 insertions(+), 19 deletions(-) diff --git a/setup/agent-jails.ts b/setup/agent-jails.ts index 4e779e7..3f28d75 100644 --- a/setup/agent-jails.ts +++ b/setup/agent-jails.ts @@ -69,11 +69,29 @@ const DOMAIN_KEY_ENV_MAP: Record = { }; const EXTRA_PACKAGES: Record = { - 'db-admin': ['postgresql17-client'], + 'db-admin': ['postgresql18-client'], 'git-admin': [], coordinator: [], }; +function ensurePiInstalled(jailName: string): void { + const exists = bastille('cmd', jailName, '/bin/sh', '-lc', 'command -v pi'); + if (exists.ok) return; + + logger.info({ jailName }, 'Installing pi inside agent jail'); + const install = bastille( + 'cmd', + jailName, + 'npm', + 'install', + '-g', + '@mariozechner/pi-coding-agent', + ); + if (!install.ok) { + throw new Error(`pi install failed in ${jailName}: ${install.output}`); + } +} + function buildJailEnv( specialist: string, allEnv: Record, @@ -211,6 +229,8 @@ export async function run(args: string[]): Promise { } } + ensurePiInstalled(jailName); + const chsh = bastille( 'cmd', jailName, diff --git a/setup/packages.ts b/setup/packages.ts index 23e1ff1..e5f5e59 100644 --- a/setup/packages.ts +++ b/setup/packages.ts @@ -32,7 +32,7 @@ export function loadPackageList(name: PackageListName): string[] { } /** - * Nullfs-mount the shared host pkg cache read-only into a jail. + * Nullfs-mount the shared host pkg cache into a jail. * Idempotent — checks the jail fstab before adding the mount. * Silently skips if the cache dataset or bastille fstab don't exist yet. * Call this after jail creation and before any bastille pkg install. @@ -40,15 +40,39 @@ export function loadPackageList(name: PackageListName): string[] { export function mountPkgCacheInJail(jailName: string): void { const fstabPath = `/usr/local/bastille/jails/${jailName}/fstab`; if (!fs.existsSync('/var/cache/pkg')) return; - if ( - fs.existsSync(fstabPath) && - fs.readFileSync(fstabPath, 'utf-8').includes('/var/cache/pkg') - ) { - return; + + const desiredLine = `/var/cache/pkg /usr/local/bastille/jails/${jailName}/root/var/cache/pkg nullfs rw 0 0`; + if (fs.existsSync(fstabPath)) { + const existing = fs.readFileSync(fstabPath, 'utf-8'); + if (existing.includes(desiredLine)) return; + + if (existing.includes('/var/cache/pkg') && existing.includes(' nullfs ro ')) { + const updated = existing.replace( + /^\/var\/cache\/pkg\s+.*?\/var\/cache\/pkg\s+nullfs\s+ro\s+0\s+0\s*$/gmu, + desiredLine, + ); + fs.writeFileSync(fstabPath, updated); + try { + execSync(`bastille umount -a ${jailName} /var/cache/pkg`, { stdio: 'ignore' }); + } catch { + // ignore — may not be mounted yet + } + try { + execSync(`bastille mount ${jailName} /var/cache/pkg /var/cache/pkg nullfs rw 0 0`, { + stdio: 'ignore', + }); + } catch { + // mount failed — jail pkg installs will fall back to network downloads + } + return; + } + + if (existing.includes('/var/cache/pkg')) return; } + try { execSync( - `bastille mount ${jailName} /var/cache/pkg /var/cache/pkg nullfs ro 0 0`, + `bastille mount ${jailName} /var/cache/pkg /var/cache/pkg nullfs rw 0 0`, { stdio: 'ignore', }, diff --git a/src/jail-exec-runner.test.ts b/src/jail-exec-runner.test.ts index ec57f21..281f531 100644 --- a/src/jail-exec-runner.test.ts +++ b/src/jail-exec-runner.test.ts @@ -75,12 +75,12 @@ describe('jailExecSync', () => { mockSpawnSync.mockReset(); }); - it('calls bastille exec with correct jail and command', () => { + it('calls bastille cmd with correct jail and command', () => { mockSpawnSync.mockReturnValue({ status: 0, stdout: 'ok', stderr: '', pid: 1, signal: null, output: [] } as any); jailExecSync('my-jail', 'bastille', ['list']); expect(mockSpawnSync).toHaveBeenCalledWith( 'bastille', - ['exec', 'my-jail', 'bastille', 'list'], + ['cmd', 'my-jail', 'bastille', 'list'], expect.objectContaining({ encoding: 'utf-8' }), ); }); @@ -152,12 +152,12 @@ describe('jailExec', () => { expect(result.output).toBeNull(); }); - it('uses bastille exec as the command', async () => { + it('uses bastille cmd as the command', async () => { mockSpawn.mockReturnValue(makeMockProc({ stdout: 'ok', exitCode: 0 }) as any); await jailExec({ jailName: 'my-jail', command: 'pi', prompt: 'hello' }); expect(mockSpawn).toHaveBeenCalledWith( 'bastille', - expect.arrayContaining(['exec', 'my-jail']), + expect.arrayContaining(['cmd', 'my-jail']), expect.anything(), ); }); diff --git a/src/jail-exec-runner.ts b/src/jail-exec-runner.ts index bebcb03..2856d23 100644 --- a/src/jail-exec-runner.ts +++ b/src/jail-exec-runner.ts @@ -3,7 +3,7 @@ * * Instead of spawning on the host, this runner: * 1. Writes prompt + system context to a temp file inside the jail - * 2. Runs `bastille exec pi ...` (or aider) + * 2. Runs `bastille cmd pi ...` (or aider) * 3. Captures stdout/stderr and returns the result * * Session files are stored inside the jail (persisted via nullfs mounts). @@ -98,14 +98,14 @@ function cleanupJailTemp(jailName: string, ...jailRelPaths: string[]): void { } } -// ── Bastille exec ────────────────────────────────────────────────────────── +// ── Bastille cmd ─────────────────────────────────────────────────────────── export function jailExecSync( jailName: string, command: string, args: string[] = [], ): { ok: boolean; output: string } { - const result = spawnSync('bastille', ['exec', jailName, command, ...args], { + const result = spawnSync('bastille', ['cmd', jailName, command, ...args], { encoding: 'utf-8', timeout: 30_000, }); @@ -134,7 +134,7 @@ export async function jailExec(opts: JailExecOptions): Promise { const fullCommand = command === 'pi' ? 'pi' : 'aider'; const bastilleArgs = [ - 'exec', + 'cmd', jailName, '/bin/sh', '-c', @@ -182,7 +182,7 @@ export async function jailExec(opts: JailExecOptions): Promise { output, error: code !== 0 - ? stderr.trim() || `bastille exec exited with code ${code}` + ? stderr.trim() || `bastille cmd exited with code ${code}` : null, exitCode: code, tokensUsed: Math.max(1, Math.ceil((output?.length || 0) / 4)), diff --git a/src/jail-schema.test.ts b/src/jail-schema.test.ts index 659d1a5..0451ebd 100644 --- a/src/jail-schema.test.ts +++ b/src/jail-schema.test.ts @@ -194,6 +194,22 @@ describe('resolveJailIp', () => { expect(resolveJailIp(r, 'web')).toBe('10.0.1.7'); }); + it('prefers WARDEN_SUBNET_BASE/AGENT_SUBNET_BASE when set', () => { + const priorWardenSubnetBase = process.env.WARDEN_SUBNET_BASE; + const priorAgentSubnetBase = process.env.AGENT_SUBNET_BASE; + process.env.WARDEN_SUBNET_BASE = '10.9.9'; + delete process.env.AGENT_SUBNET_BASE; + try { + const r = loadJailRegistry(fixtureYaml); + expect(resolveJailIp(r, 'db')).toBe('10.9.9.3'); + } finally { + if (priorWardenSubnetBase) process.env.WARDEN_SUBNET_BASE = priorWardenSubnetBase; + else delete process.env.WARDEN_SUBNET_BASE; + if (priorAgentSubnetBase) process.env.AGENT_SUBNET_BASE = priorAgentSubnetBase; + else delete process.env.AGENT_SUBNET_BASE; + } + }); + it('throws for unknown jail role', () => { const r = loadJailRegistry(fixtureYaml); expect(() => resolveJailIp(r, 'no_such_jail')).toThrow('Unknown jail role'); diff --git a/src/jail-schema.ts b/src/jail-schema.ts index 72dfd24..ed4296f 100644 --- a/src/jail-schema.ts +++ b/src/jail-schema.ts @@ -2,6 +2,7 @@ import { z } from 'zod'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; +import { readEnvFile } from './env.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -145,6 +146,19 @@ export function loadJailRegistry(registryPath?: string): JailRegistry { const registry = JailRegistrySchema.parse(parsed); if (!registryPath) { + const env = readEnvFile([ + 'AGENT_SUBNET_BASE', + 'WARDEN_SUBNET_BASE', + 'WARDEN_GATEWAY', + 'WARDEN_BRIDGE', + 'WARDEN_RELEASE', + ]); + registry.subnet_base = + env.WARDEN_SUBNET_BASE || env.AGENT_SUBNET_BASE || registry.subnet_base; + registry.gateway = env.WARDEN_GATEWAY || registry.gateway; + registry.bridge = env.WARDEN_BRIDGE || registry.bridge; + registry.release = env.WARDEN_RELEASE || registry.release; + cachedRegistry = registry; } return registry; @@ -153,7 +167,11 @@ export function loadJailRegistry(registryPath?: string): JailRegistry { export function resolveJailIp(registry: JailRegistry, role: string): string { const jail = registry.jails[role]; if (!jail) throw new Error(`Unknown jail role: ${role}`); - return `${registry.subnet_base}.${jail.ip_suffix}`; + const subnetBase = + process.env.WARDEN_SUBNET_BASE || + process.env.AGENT_SUBNET_BASE || + registry.subnet_base; + return `${subnetBase}.${jail.ip_suffix}`; } export function resolveAgentJailName( diff --git a/tsconfig.json b/tsconfig.json index c83c533..7a32f4a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,5 +16,5 @@ "sourceMap": true }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist", "**/*.test.ts"] }