// Deliberately executes a fixed fake program, never Pi or caller-supplied code. // PI_CODING_AGENT_DIR matches the statically verified 0.85.1 auth boundary. import { spawn } from 'node:child_process'; import { mkdtemp, mkdir, writeFile, open, rm } from 'node:fs/promises'; import { constants } from 'node:fs'; import { validId } from './records.mjs'; import { exact, refuse, validateFixtureCredential } from './execution.mjs'; const fake = ` import { readFile, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; const [command, subcommand, flag, provider] = process.argv.slice(1); if (command !== 'auth' || subcommand !== 'check' || flag !== '--provider') process.exit(2); if (Object.keys(process.env).some(k => !['HOME','PI_CODING_AGENT_DIR','FIXTURE_MODE'].includes(k))) process.exit(2); const path = join(process.env.PI_CODING_AGENT_DIR, 'auth.json'); const mode = process.env.FIXTURE_MODE; if (mode === 'timeout') await new Promise(() => setInterval(() => {}, 1000)); if (mode === 'failure') { process.stderr.write('FIXTURE_PRIVATE_DIAGNOSTIC'); process.exit(1); } if (mode === 'malformed') { await writeFile(path, '{', { mode: 0o600 }); process.exit(0); } const auth = JSON.parse(await readFile(path, 'utf8')); const c = auth[provider]; if (mode === 'rotate' && c.type === 'oauth') { c.access = 'FIXTURE_ROTATED_ACCESS'; c.refresh = 'FIXTURE_ROTATED_REFRESH'; c.expires = Date.now() + 3600000; } await writeFile(path, JSON.stringify(auth), { mode: 0o600 }); process.stdout.write('ready'); `; export function validateRefreshOptions(options = {}) { exact(options, [], ['mode', 'timeoutMs']); const mode = options.mode ?? 'rotate', timeoutMs = options.timeoutMs ?? 2000; if (!['rotate', 'unchanged', 'failure', 'timeout', 'malformed'].includes(mode) || !Number.isInteger(timeoutMs) || timeoutMs < 10 || timeoutMs > 10000) refuse('invalid-refresh-option'); return { mode, timeoutMs }; } export async function refreshFixtureCredential(provider, credential, options = {}) { if (!validId(provider)) refuse('invalid-provider'); const { mode, timeoutMs } = validateRefreshOptions(options); const input = validateFixtureCredential(credential, credential?.type); const root = await mkdtemp('/tmp/mosaic-refresh-fixture-'); try { const agent = `${root}/agent`, home = `${root}/home`, cwd = `${root}/cwd`; for (const dir of [agent, home, cwd]) await mkdir(dir, { mode: 0o700 }); const file = `${agent}/auth.json`; await writeFile(file, JSON.stringify({ [provider]: input }), { mode: 0o600, flag: 'wx' }); const outcome = await new Promise(resolve => { let timedOut = false, spawnFailed = false; const child = spawn(process.execPath, ['--input-type=module', '-e', fake, 'auth', 'check', '--provider', provider], { cwd, env: { HOME: home, PI_CODING_AGENT_DIR: agent, FIXTURE_MODE: mode }, // Child output is discarded, never buffered, parsed, logged or returned. stdio: 'ignore', shell: false, }); const timer = setTimeout(() => { timedOut = true; child.kill('SIGKILL'); }, timeoutMs); child.on('error', () => { spawnFailed = true; }); child.on('close', (code, signal) => { clearTimeout(timer); resolve({ code, signal, timedOut, spawnFailed }); }); }); if (outcome.timedOut) refuse('refresh-timeout'); if (outcome.spawnFailed || outcome.code !== 0 || outcome.signal) refuse('refresh-failed'); const h = await open(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK); let result; try { const s = await h.stat(); if (!s.isFile() || s.uid !== process.getuid() || (s.mode & 0o777) !== 0o600 || s.size > 4096) refuse('invalid-refresh-output'); const buf = Buffer.alloc(4097); let size = 0; while (size < buf.length) { const { bytesRead } = await h.read(buf, size, buf.length - size, null); if (!bytesRead) break; size += bytesRead; } if (size > 4096) refuse('invalid-refresh-output'); try { result = JSON.parse(buf.subarray(0, size).toString('utf8')); } catch { refuse('invalid-refresh-output'); } } finally { await h.close(); } exact(result, [provider]); const output = validateFixtureCredential(result[provider], input.type); if (output.type === 'oauth' && output.expires <= Date.now() + 300000) refuse('refresh-not-ready'); return output; } finally { await rm(root, { recursive: true, force: true }); } }