burtenshaw's picture
burtenshaw HF Staff
feat: publish beam pi study source
5741b22 verified
Raw History Blame Contribute Delete
5.46 kB
import { spawn } from 'node:child_process';
import { createWriteStream } from 'node:fs';
import { mkdir } from 'node:fs/promises';
import { dirname } from 'node:path';
export const quote = value => "'" + String(value).replaceAll("'", "'\\''") + "'";
export function command(argv, { signal, outputPath, maxBytes = 100000 } = {}) {
return new Promise((resolve, reject) => {
const child = spawn(argv[0], argv.slice(1), { stdio: ['ignore', 'pipe', 'pipe'] });
let output = '', bytes = 0, truncated = false, killTimer;
const file = outputPath ? createWriteStream(outputPath, { mode: 0o600 }) : null;
if (file) child.stdout.pipe(file);
const collect = chunk => {
const remaining = Math.max(0, maxBytes - bytes);
output += chunk.subarray(0, remaining).toString();
bytes += chunk.length;
if (bytes > maxBytes) truncated = true;
};
if (!file) child.stdout.on('data', collect);
child.stderr.on('data', collect);
const abort = () => { child.kill('SIGTERM'); killTimer = setTimeout(() => child.kill('SIGKILL'), 5000); };
signal?.addEventListener('abort', abort, { once: true });
if (signal?.aborted) abort();
child.on('error', reject);
file?.on('error', reject);
child.on('close', async (code, terminationSignal) => {
clearTimeout(killTimer);
signal?.removeEventListener('abort', abort);
if (file && !file.writableFinished) await new Promise(r => file.once('finish', r));
resolve({ output: output + (truncated ? '\n[output truncated]' : ''), exitCode: code ?? 128, signal: terminationSignal });
});
});
}
export class ContainerBackend {
constructor(config, event = () => {}, execute = command) {
this.config = config; this.event = event; this.execute = execute;
this.root = `${config.work_root ?? '/workspace'}/.pi-study`;
this.bare = `${this.root}/shared.git`;
this.active = new Map(); this.nextTool = 0;
}
argv(script) {
return [...(this.config.docker_prefix ?? ['docker']), 'exec', '--user', this.config.container_user ?? 'agent', this.config.container_name, 'bash', '-lc', script];
}
async checked(script, options) {
const result = await this.execute(this.argv(script), options);
if (result.exitCode) throw new Error(`Container command failed (${result.exitCode}): ${result.output}`);
return result;
}
agentPath(id) {
if (!/^agent-\d+$/.test(id)) throw new Error('Invalid agent ID');
return `${this.root}/agents/${id}`;
}
async prepare() {
const seed = this.config.seed_dir ?? `${this.config.work_root ?? '/workspace'}/solution`;
await this.checked(`set -euo pipefail\ntest ! -e ${quote(this.root)}\nmkdir -p ${quote(this.root + '/agents')} ${quote(this.root + '/jobs')}\ngit clone --bare --no-hardlinks ${quote(seed)} ${quote(this.bare)}\ngit --git-dir=${quote(this.bare)} rev-parse --verify main\ngit --git-dir=${quote(this.bare)} symbolic-ref HEAD refs/heads/main`);
}
async createAgent(id) {
const path = this.agentPath(id);
await this.checked(`set -euo pipefail\ngit clone ${quote(this.bare)} ${quote(path)}\ngit -C ${quote(path)} config user.name ${quote(id)}\ngit -C ${quote(path)} config user.email ${quote(id + '@study.invalid')}\ngit -C ${quote(path)} checkout -b ${quote(id)} origin/main`);
}
async bash(id, script, { signal, timeout = 120 } = {}) {
if (signal?.aborted) throw new Error('Tool cancelled');
const job = `${this.root}/jobs/${id}-${++this.nextTool}.pid`;
const cancelled = `${job}.cancelled`;
const seconds = Math.max(1, Math.min(Number(timeout) || 120, 600));
const wrapped = `set -uo pipefail\ncd ${quote(this.agentPath(id))}\ntest ! -e ${quote(cancelled)} || exit 130\nsetsid timeout --signal=TERM --kill-after=5s ${seconds}s bash -lc ${quote(script)} &\nchild=$!\nprintf '%s' "$child" > ${quote(job)}\nif test -e ${quote(cancelled)}; then kill -TERM -- "-$child" 2>/dev/null || true; sleep 1; kill -KILL -- "-$child" 2>/dev/null || true; fi\nwait "$child"\nstatus=$?\nrm -f ${quote(job)}\nexit "$status"`;
const kill = async () => {
await this.execute(this.argv(`touch ${quote(cancelled)}; if test -f ${quote(job)}; then pid=$(cat ${quote(job)}); case "$pid" in ''|*[!0-9]*) exit 1;; esac; kill -TERM -- "-$pid" 2>/dev/null || true; sleep 1; kill -KILL -- "-$pid" 2>/dev/null || true; rm -f ${quote(job)}; fi`));
};
this.active.set(job, kill);
const onAbort = () => { void kill(); };
signal?.addEventListener('abort', onAbort, { once: true });
try { return await this.execute(this.argv(wrapped), { signal }); }
finally { signal?.removeEventListener('abort', onAbort); this.active.delete(job); if (signal?.aborted) await kill(); }
}
async revision() {
const revision = (await this.checked(`git --git-dir=${quote(this.bare)} rev-parse refs/heads/main`)).output.trim();
if (!/^[0-9a-f]{40,64}$/.test(revision)) throw new Error('Invalid snapshot revision');
return revision;
}
async snapshot(outputPath, revision = null) {
await mkdir(dirname(outputPath), { recursive: true });
revision ??= await this.revision();
if (!/^[0-9a-f]{40,64}$/.test(revision)) throw new Error('Invalid snapshot revision');
await this.checked(`git --git-dir=${quote(this.bare)} archive --format=tar.gz ${quote(revision)}`, { outputPath });
return { path: outputPath, revision };
}
async stop() { await Promise.allSettled([...this.active.values()].map(kill => kill())); }
}