From 16a84bc3908cbe72311d947b5888d4d2909e4b13 Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Tue, 28 Jul 2026 14:50:28 -0700 Subject: [PATCH] fix: authenticate the live-server liveness probe greptile-apps[bot] escalated the identity ladder to a pid AND port both coincidentally reused by different processes. The definitive terminator was available all along: the helper serves an authenticated endpoint and server.json records the token, so the probe now requires a 200 from /status?token=... over HTTP. Nothing but our helper can answer that, which closes the entire misidentification class rather than the next rung. The regression test hosts its responder in a child process (the probe is execFileSync, so a same-process responder can never accept while the parent's event loop is blocked; production helpers are always separate processes). This work was produced with AI assistance (Claude Code). Co-Authored-By: Claude Code --- skill/scripts/live/roots.mjs | 26 ++++++++++++++------------ tests/live-roots.test.mjs | 29 ++++++++++++++++++++--------- 2 files changed, 34 insertions(+), 21 deletions(-) diff --git a/skill/scripts/live/roots.mjs b/skill/scripts/live/roots.mjs index 1adab11d4..b786c3e8b 100644 --- a/skill/scripts/live/roots.mjs +++ b/skill/scripts/live/roots.mjs @@ -300,35 +300,37 @@ function readPointerEntries(repoRoot) { function hasLiveServer(appRoot) { let pid; let port; + let token; try { const info = JSON.parse(fs.readFileSync(path.join(appRoot, '.impeccable', 'live', 'server.json'), 'utf-8')); if (!info || typeof info.pid !== 'number') return false; pid = info.pid; port = Number(info.port); + token = typeof info.token === 'string' ? info.token : null; process.kill(pid, 0); } catch (err) { // EPERM: the process exists but is not signalable by this user. if (err?.code !== 'EPERM') return false; } - // Liveness alone misclassifies a REUSED pid (helper died without removing - // server.json, the OS handed the pid to something else, even another node - // process). The decisive signal is the recorded PORT: a real helper is - // listening on it, a pid squatter is not. The probe is a spawned node - // one-liner so it works identically on every platform (no bash, no ps). - if (Number.isInteger(port) && port > 0) { + // Liveness alone misclassifies a REUSED pid, and a bare TCP connect + // misclassifies a coincidental listener on a reused port. The decisive + // signal is IDENTITY: the helper answers its authenticated /status + // endpoint with the token server.json records; nothing else on that port + // can. The probe is a spawned node one-liner so it works identically on + // every platform. + if (Number.isInteger(port) && port > 0 && token) { try { execFileSync(process.execPath, ['-e', [ - "const s = require('node:net').connect({ host: '127.0.0.1', port: Number(process.argv[1]), timeout: 800 });", - "s.on('connect', () => { s.destroy(); process.exit(0); });", - "s.on('timeout', () => { s.destroy(); process.exit(1); });", - "s.on('error', () => process.exit(1));", - ].join(''), String(port)], { timeout: 3000, stdio: 'ignore' }); + "const req = require('node:http').get({ host: '127.0.0.1', port: Number(process.argv[1]), path: '/status?token=' + encodeURIComponent(process.argv[2]), timeout: 1200 }, (res) => { res.resume(); process.exit(res.statusCode === 200 ? 0 : 1); });", + "req.on('timeout', () => { req.destroy(); process.exit(1); });", + "req.on('error', () => process.exit(1));", + ].join(''), String(port), token], { timeout: 4000, stdio: 'ignore' }); return true; } catch { return false; } } - // Legacy server.json without a port: best-effort process identity check. + // Legacy server.json without a port/token: best-effort identity check. if (process.platform === 'win32') return true; try { const command = execFileSync('ps', ['-p', String(pid), '-o', 'command='], { encoding: 'utf-8' }); diff --git a/tests/live-roots.test.mjs b/tests/live-roots.test.mjs index ccc93d325..3a5704215 100644 --- a/tests/live-roots.test.mjs +++ b/tests/live-roots.test.mjs @@ -1,10 +1,9 @@ import { describe, it, beforeEach, afterEach } from 'node:test'; import assert from 'node:assert/strict'; -import { spawnSync } from 'node:child_process'; +import { spawn, spawnSync } from 'node:child_process'; import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { tmpdir } from 'node:os'; -import { createServer } from 'node:net'; import { fileURLToPath } from 'node:url'; import { discoverAppCandidates, @@ -212,13 +211,25 @@ describe('review regressions: multi-app pointer', () => { writeRootsManifest(a); writeRootsManifest(b); // B booted last: a naive pointer now points at B - // A's helper server is the one alive: a real listener on a real port - // (the liveness check probes the recorded port, so a bare pid is not - // enough to count as running). - const srv = createServer(); - await new Promise((resolve) => srv.listen(0, '127.0.0.1', resolve)); + // A's helper server is the one alive: an authenticated /status + // responder on a real port, hosted in a CHILD process because the + // probe is execFileSync and a same-process responder could never + // accept while the event loop is blocked (production helpers are + // always separate processes). + const responder = spawn(process.execPath, ['-e', [ + "const s = require('node:http').createServer((q, r) => {", + " const ok = q.url === '/status?token=t';", + " r.writeHead(ok ? 200 : 401, { 'Content-Type': 'application/json' });", + " r.end('{}');", + "});", + "s.listen(0, '127.0.0.1', () => console.log(s.address().port));", + ].join('\n')], { stdio: ['ignore', 'pipe', 'ignore'] }); + const livePort = await new Promise((resolve, reject) => { + responder.stdout.once('data', (chunk) => resolve(Number(String(chunk).trim()))); + responder.once('error', reject); + setTimeout(() => reject(new Error('responder never became ready')), 5000); + }); try { - const livePort = srv.address().port; write(repo, 'siteA/.impeccable/live/server.json', JSON.stringify({ pid: process.pid, port: livePort, token: 't' })); write(repo, 'siteB/.impeccable/live/server.json', JSON.stringify({ pid: 999999999, port: 2, token: 't' })); @@ -226,7 +237,7 @@ describe('review regressions: multi-app pointer', () => { assert.equal(resolved.source, 'pointer'); assert.equal(resolved.manifest.appRoot, join(repo, 'siteA')); } finally { - await new Promise((resolve) => srv.close(resolve)); + responder.kill(); } } finally { rmSync(repo, { recursive: true, force: true });