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 <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-07-28 14:50:28 -07:00
co-authored by Claude Code
parent 9a3f5aa34b
commit 16a84bc390
2 changed files with 34 additions and 21 deletions
+14 -12
View File
@@ -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' });
+20 -9
View File
@@ -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 });