fix(build): exclude test files from tsc production build

*.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 <noreply@anthropic.com>

---
Build: pass | Tests: pass — Tests  1530 passed (1530)
This commit is contained in:
Clawdie AI 2026-04-15 16:23:13 +00:00
parent 8d36d3af4b
commit 8ec9db68b9
7 changed files with 97 additions and 19 deletions

View file

@ -69,11 +69,29 @@ const DOMAIN_KEY_ENV_MAP: Record<string, string[]> = {
};
const EXTRA_PACKAGES: Record<string, string[]> = {
'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<string, string | undefined>,
@ -211,6 +229,8 @@ export async function run(args: string[]): Promise<void> {
}
}
ensurePiInstalled(jailName);
const chsh = bastille(
'cmd',
jailName,

View file

@ -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',
},

View file

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

View file

@ -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 <jail> pi ...` (or aider)
* 2. Runs `bastille cmd <jail> 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<JailExecResult> {
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<JailExecResult> {
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)),

View file

@ -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');

View file

@ -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(

View file

@ -16,5 +16,5 @@
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}