/** * setup/git.ts — Provision a local Bastille git jail. * * Creates a thin jail with git + rsync, mirrors from REMOTE_GIT_URL * plus any GIT_MIRROR_URLS, or creates an empty bare repo as fallback. * Fully idempotent — safe to rerun. */ import { execSync, spawnSync } from 'child_process'; import path from 'path'; import { AGENT_INTERNAL_DOMAIN, AGENT_NAME, CODE_HOSTING_MODE, FEATURE_GIT, GIT_DEFAULT_REPO_NAME, GIT_JAIL_NAME, GIT_MIRROR_URLS, GIT_JAIL_IP, GIT_STORAGE_ROOT, REMOTE_GIT_URL, SUBNET_BASE, } from '../src/config.js'; import { logger } from '../src/logger.js'; import { loadPackageList, mountPkgCacheInJail } from './packages.js'; import { getPlatform } from './platform.js'; import { emitStatus } from './status.js'; import { maybeEnableTailscaleInJail } from './tailscale.js'; const LOG = 'logs/setup.log'; function bastille(...args: string[]): { ok: boolean; output: string } { const result = spawnSync('bastille', args, { encoding: 'utf-8', env: process.env, }); const output = [result.stdout || '', result.stderr || ''] .filter(Boolean) .join('\n') .trim(); return { ok: (result.status ?? 1) === 0, output }; } function jailExists(name: string): boolean { const { output } = bastille('list'); return output.split('\n').some((line) => { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('JID')) { return false; } const cols = trimmed.split(/\s+/u); return cols.length > 1 && cols[1] === name; }); } function detectFreeBSDRelease(): string { const output = execSync('freebsd-version -u', { encoding: 'utf-8' }).trim(); const match = output.match(/^(\d+\.\d+-\w+)/); return match?.[1] ?? output; } function repoNameFromRemote(remote: string): string | null { const trimmed = remote.trim(); if (!trimmed) return null; const hashIndex = trimmed.indexOf('#'); const clean = hashIndex >= 0 ? trimmed.slice(0, hashIndex) : trimmed; try { const url = new URL(clean); const base = path.posix.basename(url.pathname); return base || null; } catch { // Fall through to SCP-style parsing. } const colonIndex = clean.lastIndexOf(':'); if (colonIndex !== -1 && clean.slice(colonIndex + 1).includes('/')) { const base = path.posix.basename(clean.slice(colonIndex + 1)); return base || null; } const base = path.posix.basename(clean); return base || null; } function repoExistsInJail(jail: string, repoPath: string): boolean { const { ok } = bastille('cmd', jail, 'test', '-d', repoPath); return ok; } export async function run(_args: string[]): Promise { const jailName = GIT_JAIL_NAME; const hostname = `git.${AGENT_INTERNAL_DOMAIN}`; const repoPath = path.posix.join(GIT_STORAGE_ROOT, GIT_DEFAULT_REPO_NAME); const extraMirrorUrls = GIT_MIRROR_URLS.filter((url) => url && url !== REMOTE_GIT_URL); const runBastille = (args: string[]) => bastille(...args); // Feature gate — skip if git hosting is explicitly disabled if (!FEATURE_GIT || CODE_HOSTING_MODE === 'external') { emitStatus('SETUP_GIT', { STATUS: 'skipped', REASON: !FEATURE_GIT ? 'feature_disabled' : 'external_hosting', LOG, }); logger.info('Git jail skipped — FEATURE_GIT=%s, CODE_HOSTING_MODE=%s', FEATURE_GIT, CODE_HOSTING_MODE); return; } // Platform gate if (getPlatform() !== 'freebsd') { emitStatus('SETUP_GIT', { STATUS: 'failed', ERROR: 'unsupported_platform', LOG, }); process.exit(1); } try { const exists = jailExists(jailName); // ── Create jail if missing ────────────────────────────────────── if (!exists) { const release = detectFreeBSDRelease(); const gateway = process.env.WARDEN_GATEWAY || `${SUBNET_BASE}.1`; const bridge = process.env.WARDEN_BRIDGE || 'warden0'; logger.info({ jailName, ip: GIT_JAIL_IP, release }, 'Creating git jail'); const create = bastille( 'create', '-T', '-B', '-g', gateway, jailName, release, `${GIT_JAIL_IP}/24`, bridge, ); if (!create.ok) { throw new Error(`bastille create failed: ${create.output}`); } // Set hostname and restart so it takes effect bastille('config', jailName, 'set', 'host.hostname', hostname); bastille('restart', jailName); } else { logger.info({ jailName }, 'Git jail already exists, skipping creation'); } // ── Install packages ──────────────────────────────────────────── mountPkgCacheInJail(jailName); const packages = loadPackageList('git-jail.txt'); const pkg = bastille('pkg', jailName, 'install', '-y', ...packages); if (!pkg.ok) { logger.warn({ output: pkg.output }, 'Package install had warnings'); } maybeEnableTailscaleInJail(runBastille, jailName, jailName); // ── Create storage root ───────────────────────────────────────── bastille('cmd', jailName, 'install', '-d', '-m', '755', GIT_STORAGE_ROOT); // ── Create or mirror repository ───────────────────────────────── if (!repoExistsInJail(jailName, repoPath)) { let mirrored = false; if (REMOTE_GIT_URL) { logger.info({ url: REMOTE_GIT_URL }, 'Attempting mirror clone'); const clone = bastille( 'cmd', jailName, 'git', 'clone', '--mirror', REMOTE_GIT_URL, repoPath, ); if (clone.ok) { mirrored = true; } else { logger.warn( { output: clone.output }, 'Mirror clone failed — falling back to empty bare repo', ); } } if (!mirrored) { const init = bastille('cmd', jailName, 'git', 'init', '--bare', repoPath); if (!init.ok) { throw new Error(`git init --bare failed: ${init.output}`); } } } else { logger.info({ repoPath }, 'Bare repo already exists, skipping'); } if (extraMirrorUrls.length) { for (const mirrorUrl of extraMirrorUrls) { const repoName = repoNameFromRemote(mirrorUrl); if (!repoName) { logger.warn({ mirrorUrl }, 'Unable to derive repo name for mirror URL'); continue; } if (repoName === GIT_DEFAULT_REPO_NAME) { continue; } const mirrorRepoPath = path.posix.join(GIT_STORAGE_ROOT, repoName); if (repoExistsInJail(jailName, mirrorRepoPath)) { logger.info({ mirrorRepoPath }, 'Mirror repo already exists, skipping'); continue; } logger.info({ mirrorUrl, mirrorRepoPath }, 'Attempting mirror clone'); const clone = bastille( 'cmd', jailName, 'git', 'clone', '--mirror', mirrorUrl, mirrorRepoPath, ); if (!clone.ok) { logger.warn( { mirrorUrl, output: clone.output }, 'Mirror clone failed — skipping extra repo', ); } } } // ── Enable on boot ────────────────────────────────────────────── try { execSync(`sysrc jail_list+="${jailName}"`, { stdio: 'ignore' }); } catch { logger.warn('sysrc jail_list update failed — jail may not start on boot'); } // ── Validate ──────────────────────────────────────────────────── const gitVer = bastille('cmd', jailName, 'git', '--version'); if (!gitVer.ok) { throw new Error('Validation failed: git not installed in jail'); } const isBare = bastille( 'cmd', jailName, 'git', '--git-dir', repoPath, 'rev-parse', '--is-bare-repository', ); if (!isBare.ok || !isBare.output.includes('true')) { throw new Error(`Validation failed: ${repoPath} is not a valid bare repository`); } emitStatus('SETUP_GIT', { STATUS: 'success', JAIL_NAME: jailName, JAIL_IP: GIT_JAIL_IP, GIT_STORAGE_ROOT, GIT_REPO: GIT_DEFAULT_REPO_NAME, MIRROR_SOURCE: REMOTE_GIT_URL || 'none', MIRROR_EXTRAS: extraMirrorUrls.join(',') || 'none', LOG, }); logger.info({ jailName, ip: GIT_JAIL_IP, repoPath }, 'Git jail provisioned'); } catch (error) { const message = error instanceof Error ? error.message : String(error); emitStatus('SETUP_GIT', { STATUS: 'failed', ERROR: message, LOG, }); throw error; } }