Files
stack/agents/researcher/work/i1-human-proof/tools/gate.mjs
T

106 lines
5.0 KiB
JavaScript

// usage: node gate.mjs <bus-src> <listenSock> <forwardSock> (env: CLI_PATH, MODE, SOCK_MODE, PEER_PL)
// Spike H2's human-proof gate: a small relay that runs the repository's own verifyHuman
// against the HOST /proc and, if it passes, forwards the request to the broker's
// group-gated human socket. It holds no credential and no dataRoot, and it is not
// reachable by an agent container (its socket is not mounted anywhere else).
// CLI_PATH host path of human-cli.mjs; verifyHuman compares it with the claimed
// process's argv[1], so the gate must be told the host path, not its own.
// MODE claimed the claimed pid is trusted, exactly as today (parity)
// peer the claimed pid must also equal the pid SO_PEERCRED reports for
// the connecting socket (tools/peer.pl creds on the accepted fd)
// peer-noenv peer, and an unreadable environ (env=null) is replaced by the
// claimed nonce: the only way the check can run inside a container
// SOCK_MODE octal mode for the listening socket (default 600)
// The forwarded envelope carries only {business}: pid, start time and nonce stop here.
import { createServer } from 'node:net';
import { chmodSync, lstatSync, unlinkSync } from 'node:fs';
import { spawnSync } from 'node:child_process';
import { pathToFileURL } from 'node:url';
import { join } from 'node:path';
const [src, listen, forward] = process.argv.slice(2);
const { CLI_PATH, MODE = 'claimed', SOCK_MODE = '600', PEER_PL = '/t/peer.pl' } = process.env;
const mod = (f) => import(pathToFileURL(join(src, f)).href);
const { verifyHuman, readProcess } = await mod('human.mjs');
const { BusError } = await mod('broker.mjs');
const { Client } = await mod('client.mjs');
const LIMIT = 65536;
// The kernel's view of the connecting process. Node cannot ask for SO_PEERCRED, so the
// accepted socket's fd goes to perl as stdin. socket._handle.fd is undocumented.
function peerPid(socket) {
const fd = socket._handle?.fd;
if (!Number.isInteger(fd) || fd < 0) throw new BusError('human-required');
const r = spawnSync('perl', [PEER_PL, 'creds'], { stdio: [fd, 'pipe', 'pipe'], timeout: 2000 });
if (r.status !== 0) throw new BusError('human-required');
const pid = JSON.parse(r.stdout.toString()).pid;
if (!Number.isSafeInteger(pid) || pid < 2) throw new BusError('human-required');
return pid;
}
process.umask(0o077);
const server = createServer({ allowHalfOpen: true }, (socket) => {
socket.on('error', () => {});
socket.setTimeout(5000, () => socket.destroy());
let input = Buffer.alloc(0);
let done = false;
const finish = (result) => {
if (done) return;
done = true;
socket.end(JSON.stringify(result) + '\n');
};
socket.on('data', async (chunk) => {
if (done) return;
if (input.length + chunk.length > LIMIT) return finish({ ok: false, error: 'request-too-large' });
input = Buffer.concat([input, chunk]);
const end = input.indexOf(10);
if (end < 0) return;
let log = 'claimed=? peer=?';
try {
if (end !== input.length - 1) throw new BusError('invalid-envelope');
const r = JSON.parse(input.subarray(0, end).toString('utf8'));
if (
!r ||
typeof r !== 'object' ||
Array.isArray(r) ||
Object.keys(r).some((k) => !['human', 'verb', 'args'].includes(k)) ||
typeof r.verb !== 'string' ||
!r.human
)
throw new BusError('invalid-envelope');
const peer = MODE.startsWith('peer') ? peerPid(socket) : null;
log = `claimed=${r.human.pid} peer=${peer ?? 'unchecked'}`;
if (peer !== null && peer !== r.human.pid) throw new BusError('human-required');
// peer-noenv: a container cannot read /proc/<pid>/environ of a host process (EACCES,
// even for container root with CAP_SYS_PTRACE), so readProcess returns env=null. In
// this mode the kernel-attested peer pid replaces the nonce, and the environment
// markers on the CLI and its ancestors are NOT checked (that is the weakening).
const read =
MODE === 'peer-noenv'
? (pid) => {
const p = readProcess(pid);
return p.env === null ? { ...p, env: { MOSAIC_BUS_CLI_NONCE: r.human.nonce } } : p;
}
: readProcess;
const { business } = verifyHuman(r.human, { cliPath: CLI_PATH, readProcess: read });
const result = await new Client({ path: forward, human: { business } }).call(r.verb, r.args ?? {});
console.log(`gate ${MODE} accept ${log}`);
finish({ ok: true, result });
} catch (e) {
const code = e instanceof BusError ? e.code : 'invalid-envelope';
console.log(`gate ${MODE} refuse ${code} ${log}`);
finish({ ok: false, error: code });
}
});
});
try {
lstatSync(listen);
unlinkSync(listen);
} catch {}
await new Promise((res, rej) => {
server.once('error', rej);
server.listen(listen, res);
});
chmodSync(listen, parseInt(SOCK_MODE, 8));
console.log(`gate ready mode=${MODE} uid=${process.getuid()} listen=${listen} sock=${SOCK_MODE} cli=${CLI_PATH}`);