import { promises as fs } from 'node:fs' import { spawn } from 'node:child_process' import { join } from 'node:path' import { dirname, resolve } from 'node:path' import { randomUUID } from 'node:crypto' import { sandboxConfiguration } from './configuration' import { stageSandboxToolchains } from './toolchains' import { verifiedBinary } from './native-component' import { sealWorkspace, saveWorkspaceRecord, workspaceRecord } from './workspace' import { assertActive } from './policy' import { assertPolicyCurrent } from './policy-store' import type { SandboxEffectivePolicy } from '@shared/sandbox' export interface GitControl { root: string repository: string run(args: string[], options?: { input?: Buffer; allowFailure?: boolean; network?: boolean; config?: Record; environment?: Record; timeoutMs?: number }): Promise<{ code: number; stdout: Buffer; stderr: string }> close(): Promise } const activeControls = new Set<() => void>() export function disposeGitControls(): void { for (const cancel of [...activeControls]) cancel() } /** Fixed, verified Git in a parent-monitored native Job; never a model shell. */ export async function createGitControl(policy: SandboxEffectivePolicy, signal?: AbortSignal): Promise { assertActive(signal) const parent = join(sandboxConfiguration().stateRoot, 'git-control') await fs.mkdir(parent, { recursive: true }) const root = await fs.mkdtemp(join(parent, 'operation-')) await sealWorkspace(root) const owner = { version: 1, root, pid: process.pid, nonce: randomUUID(), createdAt: Date.now(), state: 'active' } await saveWorkspaceRecord(join(root, 'owner.json'), owner, false) for (const name of ['workspace', 'repository', 'home', 'tmp']) await fs.mkdir(join(root, name)) try { await stageSandboxToolchains(root, ['git'], signal) } catch (error) { await fs.rm(root, { recursive: true, force: true }).catch(() => undefined); throw error } const binary = await verifiedBinary(true, true, true), tool = join(root, 'tools/git') let running = false return { root, repository: join(root, 'repository'), async run(args, options = {}) { if (running) throw new Error('Git control operations must be sequential') if (args.length > 128 || args.some(arg => typeof arg !== 'string' || arg.includes('\0'))) throw new Error('Invalid Git control argv') assertActive(signal); await assertPolicyCurrent(policy) const config: Record = { 'core.hooksPath': join(tool, 'empty-hooks'), 'core.fsmonitor': 'false', 'credential.helper': '', 'commit.gpgSign': 'false', 'tag.gpgSign': 'false', 'maintenance.auto': 'false', 'gc.auto': '0', 'diff.autoRefreshIndex': 'false', 'core.attributesFile': 'NUL', 'core.excludesFile': 'NUL', 'protocol.allow': 'never', 'http.followRedirects': 'false', 'http.sslVerify': 'true', ...options.config } const win = process.env.SystemRoot ?? 'C:\\Windows' const environment: Record = { SYSTEMROOT: win, WINDIR: win, SYSTEMDRIVE: win.slice(0, 2), COMSPEC: join(win, 'System32/cmd.exe'), PATH: `${tool};${join(win, 'System32')}`, HOME: join(root, 'home'), USERPROFILE: join(root, 'home'), TEMP: join(root, 'tmp'), TMP: join(root, 'tmp'), LC_ALL: 'C.UTF-8', GIT_DIR: join(root, 'repository'), GIT_WORK_TREE: join(root, 'workspace'), GIT_CONFIG_NOSYSTEM: '1', GIT_CONFIG_GLOBAL: 'NUL', GIT_CONFIG_SYSTEM: 'NUL', GIT_TERMINAL_PROMPT: '0', GIT_OPTIONAL_LOCKS: '0', GIT_PAGER: '', GIT_EDITOR: '', GIT_SEQUENCE_EDITOR: '', GIT_EXEC_PATH: tool, GIT_TEMPLATE_DIR: join(tool, 'empty-template'), GIT_NO_LAZY_FETCH: '1', GIT_ALLOW_PROTOCOL: options.network ? 'https:http:ssh' : '', GIT_CONFIG_COUNT: String(Object.keys(config).length), ...options.environment } Object.entries(config).forEach(([key, value], index) => { environment[`GIT_CONFIG_KEY_${index}`] = key; environment[`GIT_CONFIG_VALUE_${index}`] = value }) const fields = (values: string[]): string => values.map(value => Buffer.from(value).toString('hex')).join('\n') + '\n' await fs.writeFile(join(root, 'arguments'), fields(args)) await fs.writeFile(join(root, 'environment'), fields(Object.entries(environment).map(([name, value]) => `${name}=${value}`))) await fs.writeFile(join(root, 'input'), options.input ?? Buffer.alloc(0)) await fs.unlink(join(root, 'native-lease.json')).catch(error => { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error }) const timeout = options.timeoutMs ?? 120000 running = true const child = spawn(binary, ['--git-control', root, String(timeout)], { windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] }) const chunks: Buffer[] = []; let length = 0, stderr = '', failure: Error | undefined, checking = false const fail = (error: Error): void => { failure ??= error; child.kill() } const revoke = (): void => fail(Object.assign(new Error('Git control cancelled or policy revoked'), { name: 'AbortError' })) activeControls.add(revoke) child.stdout.on('data', (bytes: Buffer) => { length += bytes.length; if (length > 32 * 1024 * 1024) fail(new Error('Git control output exceeds limit')); else chunks.push(bytes) }) child.stderr.on('data', (bytes: Buffer) => { stderr = (stderr + bytes.toString('utf8')).slice(-2 * 1024 * 1024) }) const abort = (): void => fail(Object.assign(new Error('Git operation cancelled'), { name: 'AbortError' })) signal?.addEventListener('abort', abort, { once: true }); if (signal?.aborted) abort() const timer = setTimeout(() => fail(new Error('Git control timeout')), timeout + 5000) const watch = setInterval(() => { if (checking) return checking = true void assertPolicyCurrent(policy).catch(error => fail(error as Error)).finally(() => { checking = false }) }, 250) try { const code = await new Promise((done, reject) => { child.once('error', reject); child.once('close', code => done(code ?? 130)) }) if (failure) throw failure if (code !== 0 && !options.allowFailure) throw new Error(`Git control failed (${code}): ${stderr.slice(-4096)}`) return { code, stdout: Buffer.concat(chunks), stderr } } finally { clearTimeout(timer); clearInterval(watch); signal?.removeEventListener('abort', abort); activeControls.delete(revoke); running = false } }, async close() { if (running) throw new Error('Cannot clean an active Git control Job') await saveWorkspaceRecord(join(root, 'owner.json'), { ...owner, state: 'closed' }).catch(() => undefined) await fs.rm(root, { recursive: true, force: true }).catch(async () => { await saveWorkspaceRecord(join(root, 'owner.json'), { ...owner, state: 'cleanup-required' }).catch(() => undefined) }) } } } export async function cleanupGitControls(): Promise> { const parent = resolve(sandboxConfiguration().stateRoot, 'git-control'), entries = await fs.readdir(parent, { withFileTypes: true }).catch(error => { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return []; throw error }) if (entries.length > 10000) throw new Error('Too many Git control records') const alive = (pid: number): boolean => { try { process.kill(pid, 0); return true } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ESRCH') return false; throw error } } const result: Array<{ id: string; state: 'removed' | 'active' | 'invalid'; reason?: string }> = [] for (const entry of entries) { if (!/^operation-[a-zA-Z0-9_-]{6,}$/.test(entry.name) || !entry.isDirectory() || entry.isSymbolicLink()) { result.push({ id: entry.name, state: 'invalid' }); continue } const root = resolve(parent, entry.name) try { if ((await fs.realpath(dirname(root))).toLowerCase() !== (await fs.realpath(parent)).toLowerCase() || (await fs.realpath(root)).toLowerCase() !== root.toLowerCase()) throw new Error('Git cleanup root changed or escaped') const owner = await workspaceRecord<{ version: number; root: string; pid: number; nonce: string; state: string }>(join(root, 'owner.json'), 4096) if (owner.version !== 1 || owner.root.toLowerCase() !== root.toLowerCase() || !Number.isSafeInteger(owner.pid) || owner.pid < 1 || !/^[a-f0-9-]{36}$/.test(owner.nonce)) throw new Error('Invalid Git control owner') if (owner.state === 'active' && alive(owner.pid)) { result.push({ id: entry.name, state: 'active' }); continue } const lease = await workspaceRecord<{ version: number; root: string; pid: number }>(join(root, 'native-lease.json'), 4096).catch(error => { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null; throw error }) if (lease && (lease.version !== 1 || lease.root.toLowerCase() !== root.toLowerCase() || !Number.isSafeInteger(lease.pid) || lease.pid < 1)) throw new Error('Invalid native Git control lease') if (lease && alive(lease.pid)) { result.push({ id: entry.name, state: 'active' }); continue } await fs.rm(root, { recursive: true, force: false }); result.push({ id: entry.name, state: 'removed' }) } catch (error) { result.push({ id: entry.name, state: 'invalid', reason: (error as Error).message }) } } return result }