clawdie-ai/setup/git.ts
Clawdie AI cdf2c8f296 Enable Tailscale jail auto-join (Sam & Codex)
Add a setup helper to enable tailscale inside jails when FEATURE_TAILSCALE

and an auth key are present, prefetch tailscale packages, and document

the installer shortcut.

---

Build: FAIL — not run

Tests: FAIL — not run
2026-04-04 15:40:47 +00:00

263 lines
8.6 KiB
TypeScript

/**
* 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<void> {
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;
}
}