Files

106 lines
3.6 KiB
JavaScript

import { readFileSync, lstatSync } from 'node:fs';
import { basename, resolve } from 'node:path';
import { BusError } from './broker.mjs';
const MARKERS = [
'MOSAIC_BUS_CAP',
'MOSAIC_RUN_ID',
'MOSAIC_AGENT_RUN',
'CLAUDECODE',
'CLAUDE_CODE_ENTRYPOINT',
'CODEX_THREAD_ID',
'PI_AGENT_DIR',
];
// Row 252 (#1753, Webby N5): the Console wrapper's nonce and the subject its spawn bound. The
// verifier reads each from the wrapper's initial environment, never from its proof alone.
export const CONSOLE_ENV = Object.freeze(['MOSAIC_BUS_CONSOLE_NONCE', 'MOSAIC_BUS_CONSOLE_BUSINESS', 'MOSAIC_BUS_CONSOLE_PROVIDER', 'MOSAIC_BUS_CONSOLE_ISS', 'MOSAIC_BUS_CONSOLE_SUB']);
const NONCE_KEYS = ['MOSAIC_BUS_CLI_NONCE', 'MOSAIC_BUS_USER_NONCE', ...CONSOLE_ENV];
const refuse = () => {
throw new BusError('human-required');
};
export function readProcess(pid, { readFile = readFileSync, stat = lstatSync } = {}) {
if (!Number.isSafeInteger(pid) || pid < 1) refuse();
try {
const dir = `/proc/${pid}`,
raw = readFile(dir + '/stat', 'utf8'),
fields = raw.slice(raw.lastIndexOf(')') + 2).split(' ');
let env = {};
let environment;
try {
environment = readFile(dir + '/environ', 'utf8');
} catch (e) {
if (e.code !== 'EACCES') throw e;
env = null;
}
if (environment !== undefined)
for (const entry of environment.split('\0')) {
const i = entry.indexOf('=');
const key = entry.slice(0, i);
if (NONCE_KEYS.includes(key) || MARKERS.includes(key)) env[key] = entry.slice(i + 1);
}
return {
pid,
ppid: Number(fields[1]),
startTime: fields[19],
uid: stat(dir).uid,
argv: readFile(dir + '/cmdline', 'utf8')
.split('\0')
.filter(Boolean),
env,
};
} catch {
refuse();
}
}
// Cooperative same-UID process checks, not peer credentials or a hostile-process wall.
// A nonce in the CLI's launch environment binds the submitted PID to a live CLI.
export function verifyHuman(proof, { cliPath, launches = [], readProcess: read = readProcess, nonceKey = 'MOSAIC_BUS_CLI_NONCE' }) {
if (
!proof ||
typeof proof !== 'object' ||
Object.keys(proof).some((k) => !['pid', 'startTime', 'nonce', 'business'].includes(k)) ||
!Number.isSafeInteger(proof.pid) ||
proof.pid < 2 ||
typeof proof.business !== 'string' ||
typeof proof.nonce !== 'string' ||
!/^[a-f0-9]{64}$/.test(proof.nonce)
)
refuse();
try {
const p = read(proof.pid);
if (
p.uid !== process.getuid() ||
p.startTime !== proof.startTime ||
p.env?.[nonceKey] !== proof.nonce ||
p.argv[1] !== resolve(cliPath)
)
refuse();
const seen = new Set();
let current = p;
for (let depth = 0; depth < 128; depth++) {
if (seen.has(current.pid)) refuse();
seen.add(current.pid);
if (
(current.env !== null && MARKERS.some((k) => current.env[k])) ||
launches.some((r) => r.pid === current.pid && r.startTime === current.startTime)
)
refuse();
const command = basename(current.argv[0] ?? '');
if (
/^(pi|claude|claude-code|codex)(?:\.js)?$/.test(command) ||
current.argv.some((v) => /\/(?:pi-coding-agent|codex)\/(?:dist|bin)\//.test(v))
)
refuse();
if (current.ppid === 1) {
const again = read(proof.pid);
if (again.startTime !== proof.startTime || again.ppid !== p.ppid) refuse();
return { business: proof.business };
}
if (current.ppid < 2) refuse();
current = read(current.ppid);
}
} catch {
refuse();
}
refuse();
}