mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 09:41:04 +01:00
Keep one VM per environment with independent lease cgroups, persistent workspaces, immutable runner images, and an explicit experimental gate. Add live compatibility, recovery, browser-preview, and stress campaigns. Co-Authored-By: Paperclip <noreply@paperclip.ing>
90 lines
3.7 KiB
JavaScript
90 lines
3.7 KiB
JavaScript
// Image-owned execution supervisor. One systemd cgroup per lease, independent
|
|
// sockets per command. Closing SSH is transport loss, never proof of termination.
|
|
import net from 'node:net';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { spawn } from 'node:child_process';
|
|
|
|
const [mode, root] = process.argv.slice(2);
|
|
if (!root || !/^\/var\/lib\/paperclip-exe\/[a-f0-9]{32}\/[a-f0-9]{32}$/.test(root)) {
|
|
throw new Error('Invalid lease root');
|
|
}
|
|
const socketPath = path.join(root, 'control.sock');
|
|
const MAX_FRAME = 16 * 1024 * 1024;
|
|
const send = (socket, value) => !socket.destroyed && socket.write(JSON.stringify(value) + '\n');
|
|
|
|
function frames(stream, onFrame) {
|
|
let pending = '';
|
|
stream.setEncoding('utf8');
|
|
stream.on('data', (data) => {
|
|
pending += data;
|
|
if (pending.length > MAX_FRAME) return stream.destroy(new Error('Frame too large'));
|
|
for (;;) {
|
|
const newline = pending.indexOf('\n');
|
|
if (newline < 0) break;
|
|
const line = pending.slice(0, newline);
|
|
pending = pending.slice(newline + 1);
|
|
try { onFrame(JSON.parse(line)); }
|
|
catch { stream.destroy(new Error('Invalid frame')); }
|
|
}
|
|
});
|
|
}
|
|
|
|
if (mode === 'serve') {
|
|
process.umask(0o077);
|
|
fs.rmSync(socketPath, { force: true });
|
|
const server = net.createServer((socket) => {
|
|
let child;
|
|
let timer;
|
|
let done = false;
|
|
let timedOut = false;
|
|
const terminate = () => {
|
|
if (!child?.pid || done) return;
|
|
try { process.kill(-child.pid, 'SIGKILL'); } catch {}
|
|
};
|
|
socket.on('error', () => {});
|
|
// Commands survive a disconnected SSH client. The lease's cgroup remains
|
|
// authoritative; cancellation and expiry explicitly stop it from outside.
|
|
frames(socket, (message) => {
|
|
if (message.type === 'stdin' && child) {
|
|
child.stdin.write(Buffer.from(message.data, 'base64'));
|
|
return;
|
|
}
|
|
if (message.type === 'end' && child) { child.stdin.end(); return; }
|
|
if (message.type === 'cancel') { terminate(); return; }
|
|
if (message.type !== 'start' || child) throw new Error('Expected start');
|
|
if (typeof message.command !== 'string' || !Array.isArray(message.args)) throw new Error('Invalid command');
|
|
const cwd = message.cwd ?? path.join(root, 'workspace');
|
|
child = spawn(message.command, message.args, {
|
|
cwd,
|
|
env: { ...process.env, HOME: path.join(root, 'home'), ...message.env },
|
|
detached: true,
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
});
|
|
child.stdin.on('error', () => {});
|
|
child.stdout.on('data', (data) => send(socket, { type: 'stdout', data: data.toString('base64') }));
|
|
child.stderr.on('data', (data) => send(socket, { type: 'stderr', data: data.toString('base64') }));
|
|
const finish = (code, signal, error) => {
|
|
if (done) return;
|
|
done = true;
|
|
clearTimeout(timer);
|
|
send(socket, { type: 'exit', code, signal, timedOut, error });
|
|
socket.end();
|
|
};
|
|
child.on('error', (error) => finish(null, null, error.message));
|
|
// Use exit, not close: a deliberately detached daemon can retain an fd.
|
|
child.on('exit', (code, signal) => finish(code, signal));
|
|
if (message.timeoutMs > 0) timer = setTimeout(() => { timedOut = true; terminate(); }, message.timeoutMs);
|
|
});
|
|
});
|
|
server.listen(socketPath);
|
|
} else if (mode === 'call') {
|
|
const socket = net.connect(socketPath);
|
|
socket.on('error', (error) => { console.error(error.message); process.exitCode = 1; });
|
|
socket.on('connect', () => process.stdin.pipe(socket, { end: false }));
|
|
socket.pipe(process.stdout);
|
|
socket.on('end', () => { process.stdin.pause(); process.stdin.unref?.(); });
|
|
} else {
|
|
throw new Error('Invalid supervisor mode');
|
|
}
|