feat(ops): add portable strict War Room preflight (#56)

* feat(ops): add portable War Room preflight command

Add npm run ops:preflight that checks operational readiness before
launching a War Room mission: API health, active mission status,
pending action receipts, local-agent/provider configuration, and
arsenal tool availability. Follows same pattern as scripts/doctor.mjs.

Supports --json for machine-readable output and --strict to fail
on warnings.

Fixes #35

Signed-off-by: Shivam Singh <shivamsingh-007@users.noreply.github.com>

* chore: revert package-lock.json churn to reduce diff noise

Restores package-lock.json to upstream main version.
Removes unintended npm-version lockfile rewrite (peer flips, @emnapi/* entry shifts).

* fix(ops-preflight): correct response-shape reads for mission, approvals, and agent checks

Three bugs identified by jmagly in review:
1. Mission-active: API returns {active: boolean}, not {phase}
2. Pending-approvals: API returns {approvals: [...]}, not a flat array
3. Local-agent: API returns {connected: [...]} (array), not a boolean

All three verified against live endpoints per review feedback.

Signed-off-by: Shivam Singh <shivamsingh-007@users.noreply.github.com>

* fix(ops): harden portable preflight checks

Signed-off-by: Joseph Magly <1159087+jmagly@users.noreply.github.com>

---------

Signed-off-by: Shivam Singh <shivamsingh-007@users.noreply.github.com>
Signed-off-by: Joseph Magly <1159087+jmagly@users.noreply.github.com>
Co-authored-by: Shivam Singh <shivamsingh-007@users.noreply.github.com>
Co-authored-by: Joseph Magly <1159087+jmagly@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-07-15 21:59:27 -04:00
1 parent 916c260fd8
commit 3e19454bd4
3 files changed
+468 -1

No files matched your search

+3 -1
View File
@@ -54,7 +54,7 @@
"install:tools": "bash scripts/install-tools.sh",
"mcp": "tsx src/mcp-server.ts",
"mcp:prod": "node dist/mcp-server.js",
"test": "vitest run src",
"test": "vitest run src && node scripts/test-ops-preflight.mjs",
"test:watch": "vitest src",
"lint": "eslint src/**/*.ts",
"typecheck": "tsc --noEmit",
@@ -80,6 +80,8 @@
"capability": "node scripts/capability-smoke.mjs",
"gauntlet": "node scripts/capability-smoke.mjs --gauntlet",
"test:gate": "npm run build --silent && node scripts/test-gate.mjs",
"ops:preflight": "node scripts/ops-preflight.mjs",
"test:ops-preflight": "node scripts/test-ops-preflight.mjs",
"update": "node scripts/update.mjs",
"update:dry": "node scripts/update.mjs --dry-run",
"update:hard": "node scripts/update.mjs --hard",
+219
View File
@@ -0,0 +1,219 @@
#!/usr/bin/env node
import { access, constants } from 'node:fs/promises';
import { posix, resolve, win32 } from 'node:path';
import process from 'node:process';
import { pathToFileURL } from 'node:url';
const rawBaseUrl = process.env.T3MP3ST_API_URL || 'http://127.0.0.1:3333';
const jsonMode = process.argv.includes('--json');
const strictMode = process.argv.includes('--strict');
const checks = [];
function check(name, passed, detail = '', severity = 'block') {
checks.push({ name, passed: Boolean(passed), detail, severity });
}
export function validateBaseUrl(value) {
try {
const url = new URL(value);
const hostname = url.hostname.toLowerCase().replace(/^\[|\]$/g, '');
const loopbackIpv4 = /^127(?:\.\d{1,3}){3}$/.test(hostname)
&& hostname.split('.').every(part => Number(part) <= 255);
const loopback = hostname === 'localhost' || hostname === '::1' || loopbackIpv4;
const rootPath = url.pathname === '' || url.pathname === '/';
if (!['http:', 'https:'].includes(url.protocol)) throw new Error('protocol must be http or https');
if (!loopback) throw new Error('host must be loopback (localhost, 127.0.0.0/8, or ::1)');
if (url.username || url.password) throw new Error('credentials are not allowed in the API URL');
if (!rootPath || url.search || url.hash) throw new Error('API URL must be an origin without a path, query, or fragment');
return { ok: true, url: url.origin };
} catch (error) {
return { ok: false, url: value, error: error instanceof Error ? error.message : String(error) };
}
}
function commandCandidates(binary, platform, env) {
const windows = platform === 'win32';
const pathApi = windows ? win32 : posix;
const separator = windows ? ';' : ':';
const pathValue = env.PATH || env.Path || env.path || '';
const directories = pathValue.split(separator).map(item => item.trim().replace(/^"|"$/g, '')).filter(Boolean);
const hasExtension = windows && win32.extname(binary) !== '';
const extensions = windows && !hasExtension
? (env.PATHEXT || '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean)
: [''];
return directories.flatMap(directory => extensions.map(extension => pathApi.join(directory, `${binary}${extension}`)));
}
export async function commandExists(binary, options = {}) {
const platform = options.platform || process.platform;
const env = options.env || process.env;
const accessFn = options.accessFn || access;
const mode = platform === 'win32' ? constants.F_OK : constants.X_OK;
for (const candidate of commandCandidates(binary, platform, env)) {
try {
await accessFn(candidate, mode);
return true;
} catch {
// Keep searching PATH candidates.
}
}
return false;
}
async function apiGet(baseUrl, path) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 3000);
try {
const response = await fetch(`${baseUrl}${path}`, {
signal: controller.signal,
redirect: 'error',
});
const text = await response.text();
let data = {};
try {
data = text ? JSON.parse(text) : {};
} catch {
data = { raw: text };
}
return { ok: response.ok, status: response.status, data };
} catch (error) {
return {
ok: false,
status: 0,
data: {},
error: error instanceof Error ? error.message : String(error),
};
} finally {
clearTimeout(timer);
}
}
function apiFailure(result) {
if (result.status) return `HTTP ${result.status}`;
return result.error || 'request failed';
}
function finish(startedAt, baseUrl, forcedPass) {
const blockers = checks.filter(item => !item.passed && item.severity === 'block');
const warnings = checks.filter(item => !item.passed && item.severity === 'warn');
const passed = forcedPass ?? (blockers.length === 0 && (!strictMode || warnings.length === 0));
const result = {
tool: 't3mp3st-ops-preflight',
startedAt,
finishedAt: new Date().toISOString(),
baseUrl,
passed,
summary: {
checks: checks.length,
passed: checks.filter(item => item.passed).length,
warnings: warnings.length,
blockers: blockers.length,
strict: strictMode,
},
checks,
};
if (jsonMode) {
console.log(JSON.stringify(result, null, 2));
} else {
console.log(`T3MP3ST ops preflight: ${passed ? 'PASS' : 'FAIL'}`);
console.log(`${result.summary.passed}/${result.summary.checks} checks passed, ${warnings.length} warning(s), ${blockers.length} blocker(s)`);
for (const item of checks) {
const marker = item.passed ? 'ok' : item.severity === 'warn' ? 'warn' : 'block';
console.log(`- ${marker.padEnd(5)} ${item.name}${item.detail ? ` - ${item.detail}` : ''}`);
}
}
process.exitCode = passed ? 0 : 1;
return result;
}
export async function main() {
const startedAt = new Date().toISOString();
const baseUrlResult = validateBaseUrl(rawBaseUrl);
check('Local API URL', baseUrlResult.ok, baseUrlResult.ok ? baseUrlResult.url : baseUrlResult.error, 'block');
if (!baseUrlResult.ok) return finish(startedAt, rawBaseUrl, false);
const baseUrl = baseUrlResult.url;
const health = await apiGet(baseUrl, '/health');
const apiReachable = health.ok;
check('API health endpoint', apiReachable,
apiReachable ? `HTTP ${health.status}` : `${apiFailure(health)} - run npm run server`, 'block');
if (!apiReachable) {
check('Node runtime', true, `${process.execPath} (${process.version})`, 'warn');
const npmAvailable = Boolean(process.env.npm_execpath) || await commandExists('npm');
check('npm available', npmAvailable, npmAvailable ? 'available' : 'not found on PATH', 'warn');
return finish(startedAt, baseUrl, false);
}
check('API reports operational', health.data?.status === 'operational',
`status: ${health.data?.status || 'unknown'}`, 'block');
const mission = await apiGet(baseUrl, '/api/mission/status');
const missionShapeValid = mission.ok && typeof mission.data?.active === 'boolean';
check('Mission status available', missionShapeValid,
missionShapeValid ? 'verified' : mission.ok ? 'invalid response shape' : apiFailure(mission), 'block');
if (missionShapeValid) {
const missionActive = mission.data.active === true;
const activeMission = mission.data.mission || {};
const activeDetail = [
`mission ${activeMission.id || mission.data.name || 'unknown'}`,
activeMission.currentPhase ? `phase ${activeMission.currentPhase}` : '',
].filter(Boolean).join(', ');
check('No active mission in progress', !missionActive, missionActive ? activeDetail : 'idle', 'block');
}
const approvals = await apiGet(baseUrl, '/api/approvals?status=pending');
const approvalsShapeValid = approvals.ok && Array.isArray(approvals.data?.approvals);
check('Approval receipt status available', approvalsShapeValid,
approvalsShapeValid ? 'verified' : approvals.ok ? 'invalid response shape' : apiFailure(approvals), 'block');
if (approvalsShapeValid) {
const pendingCount = approvals.data.approvals.length;
check('No pending action receipts', pendingCount === 0,
pendingCount > 0 ? `${pendingCount} pending approval(s) - review before launch` : 'none pending', 'warn');
}
const llmStatus = health.data?.llm;
const serverBackendAvailable = llmStatus?.configured === true || llmStatus?.connected === true;
const localAgent = await apiGet(baseUrl, '/api/agents/local/status');
const localAgentShapeValid = localAgent.ok && Array.isArray(localAgent.data?.connected);
const connectedAgents = localAgentShapeValid ? localAgent.data.connected : [];
const backendAvailable = serverBackendAvailable || connectedAgents.length > 0;
let backendDetail;
if (serverBackendAvailable) {
backendDetail = `${llmStatus.provider || 'server'} backend ${llmStatus.connected ? 'connected' : 'configured'}`;
} else if (connectedAgents.length > 0) {
const firstAgent = connectedAgents[0];
backendDetail = `${firstAgent.name || firstAgent.id || 'local agent'} connected`;
} else if (!localAgentShapeValid) {
backendDetail = `server LLM unavailable; local-agent status ${localAgent.ok ? 'has invalid shape' : apiFailure(localAgent)}`;
} else {
backendDetail = 'no server LLM or connected local agent - configure War Room Settings';
}
check('LLM backend available', backendAvailable, backendDetail, 'warn');
const arsenal = await apiGet(baseUrl, '/api/arsenal/status');
const arsenalShapeValid = arsenal.ok
&& Number.isFinite(arsenal.data?.summary?.installedCommandReady)
&& Number.isFinite(arsenal.data?.summary?.commandReady);
check('Arsenal status available', arsenalShapeValid,
arsenalShapeValid ? 'verified' : arsenal.ok ? 'invalid response shape' : apiFailure(arsenal), 'warn');
if (arsenalShapeValid) {
const installed = arsenal.data.summary.installedCommandReady;
const total = arsenal.data.summary.commandReady;
check('Arsenal tools available', installed > 0,
`${installed}/${total} command-ready tools installed`, 'warn');
}
return finish(startedAt, baseUrl);
}
const invokedAsScript = process.argv[1]
&& import.meta.url === pathToFileURL(resolve(process.argv[1])).href;
if (invokedAsScript) {
main().catch(error => {
console.error(`ops-preflight failed: ${error?.stack || error?.message || error}`);
process.exitCode = 1;
});
}
+246
View File
@@ -0,0 +1,246 @@
#!/usr/bin/env node
import assert from 'node:assert/strict';
import { spawn } from 'node:child_process';
import { createServer } from 'node:http';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { commandExists, validateBaseUrl } from './ops-preflight.mjs';
const scriptDir = dirname(fileURLToPath(import.meta.url));
const preflightPath = join(scriptDir, 'ops-preflight.mjs');
let passed = 0;
let failed = 0;
function healthyRoutes(overrides = {}) {
return {
'/health': {
status: 'operational',
llm: { configured: true, connected: true, provider: 'mock' },
},
'/api/mission/status': { active: false, progress: [], tasks: [] },
'/api/approvals?status=pending': { approvals: [] },
'/api/agents/local/status': { connected: [] },
'/api/arsenal/status': {
summary: { installedCommandReady: 2, commandReady: 4 },
},
...overrides,
};
}
async function withServer(routes, fn) {
const server = createServer((req, res) => {
const route = routes[req.url];
if (!route) {
res.writeHead(404, { 'content-type': 'application/json' });
res.end(JSON.stringify({ error: 'not found' }));
return;
}
const status = route.httpStatus || 200;
const headers = { 'content-type': 'application/json', ...(route.headers || {}) };
res.writeHead(status, headers);
res.end(route.raw ?? JSON.stringify(route.body ?? route));
});
await new Promise((resolve, reject) => {
server.once('error', reject);
server.listen(0, '127.0.0.1', resolve);
});
const address = server.address();
const url = `http://127.0.0.1:${address.port}`;
try {
return await fn(url);
} finally {
await new Promise(resolve => server.close(resolve));
}
}
function runPreflight(baseUrl, args = []) {
return new Promise((resolve, reject) => {
const child = spawn(process.execPath, [preflightPath, '--json', ...args], {
env: { ...process.env, T3MP3ST_API_URL: baseUrl },
stdio: ['ignore', 'pipe', 'pipe'],
});
let stdout = '';
let stderr = '';
child.stdout.on('data', chunk => { stdout += chunk; });
child.stderr.on('data', chunk => { stderr += chunk; });
child.once('error', reject);
child.once('close', code => {
let result;
try {
result = JSON.parse(stdout);
} catch (error) {
reject(new Error(`invalid JSON output (exit ${code}): ${error.message}\nstdout=${stdout}\nstderr=${stderr}`));
return;
}
resolve({ code, result, stderr });
});
});
}
function findCheck(result, name) {
const item = result.checks.find(check => check.name === name);
assert(item, `missing check: ${name}`);
return item;
}
async function test(name, fn) {
try {
await fn();
passed++;
console.log(` OK ${name}`);
} catch (error) {
failed++;
console.error(` FAIL ${name}: ${error.stack || error.message || error}`);
}
}
console.log('\n======== ops preflight tests ========\n');
await test('accepts loopback origins and rejects non-local or credentialed URLs', async () => {
assert.equal(validateBaseUrl('http://127.0.0.1:3333').ok, true);
assert.equal(validateBaseUrl('https://localhost:3333/').ok, true);
assert.equal(validateBaseUrl('http://[::1]:3333').ok, true);
assert.equal(validateBaseUrl('https://example.com').ok, false);
assert.equal(validateBaseUrl('https://127.example.com').ok, false);
assert.equal(validateBaseUrl('http://user:secret@127.0.0.1:3333').ok, false);
assert.equal(validateBaseUrl('http://127.0.0.1:3333/api').ok, false);
});
await test('resolves command candidates portably without invoking a shell', async () => {
const windowsAttempts = [];
const windowsFound = await commandExists('npm', {
platform: 'win32',
env: { PATH: 'C:\\Tools;D:\\Node', PATHEXT: '.EXE;.CMD' },
accessFn: async candidate => {
windowsAttempts.push(candidate);
if (candidate.toLowerCase() === 'd:\\node\\npm.cmd') return;
throw new Error('missing');
},
});
assert.equal(windowsFound, true);
assert(windowsAttempts.some(candidate => candidate.toLowerCase() === 'd:\\node\\npm.cmd'));
const posixFound = await commandExists('npm', {
platform: 'linux',
env: { PATH: '/usr/bin:/opt/node/bin' },
accessFn: async candidate => {
if (candidate === '/opt/node/bin/npm') return;
throw new Error('missing');
},
});
assert.equal(posixFound, true);
});
await test('passes a healthy idle War Room using server-authoritative backend state', async () => {
await withServer(healthyRoutes(), async url => {
const { code, result, stderr } = await runPreflight(url);
assert.equal(stderr, '');
assert.equal(code, 0);
assert.equal(result.passed, true);
assert.equal(findCheck(result, 'LLM backend available').passed, true);
assert.equal(findCheck(result, 'No active mission in progress').detail, 'idle');
});
});
await test('blocks an active mission and reports current nested mission fields', async () => {
const routes = healthyRoutes({
'/api/mission/status': {
active: true,
mission: { id: 'mission-123', currentPhase: 'recon' },
},
});
await withServer(routes, async url => {
const { code, result } = await runPreflight(url);
assert.equal(code, 1);
const item = findCheck(result, 'No active mission in progress');
assert.equal(item.passed, false);
assert.match(item.detail, /mission-123/);
assert.match(item.detail, /recon/);
});
});
await test('warns for pending approvals and strict mode fails the warning', async () => {
const routes = healthyRoutes({
'/api/approvals?status=pending': { approvals: [{ id: 'approval-1' }] },
});
await withServer(routes, async url => {
const normal = await runPreflight(url);
assert.equal(normal.code, 0);
assert.equal(findCheck(normal.result, 'No pending action receipts').passed, false);
const strict = await runPreflight(url, ['--strict']);
assert.equal(strict.code, 1);
assert.equal(strict.result.passed, false);
});
});
await test('blocks when mission state cannot be verified', async () => {
const routes = healthyRoutes({
'/api/mission/status': { httpStatus: 503, body: { error: 'unavailable' } },
});
await withServer(routes, async url => {
const { code, result } = await runPreflight(url);
assert.equal(code, 1);
assert.equal(findCheck(result, 'Mission status available').passed, false);
});
});
await test('blocks when approval state is malformed instead of assuming zero', async () => {
const routes = healthyRoutes({
'/api/approvals?status=pending': { total: 0 },
});
await withServer(routes, async url => {
const { code, result } = await runPreflight(url);
assert.equal(code, 1);
assert.equal(findCheck(result, 'Approval receipt status available').passed, false);
});
});
await test('uses health LLM state when the local-agent endpoint is unavailable', async () => {
const routes = healthyRoutes({
'/api/agents/local/status': { httpStatus: 500, body: { error: 'unavailable' } },
});
await withServer(routes, async url => {
const { code, result } = await runPreflight(url);
assert.equal(code, 0);
assert.equal(findCheck(result, 'LLM backend available').passed, true);
});
});
await test('reports a missing backend and strict mode treats it as a failure', async () => {
const routes = healthyRoutes({
'/health': {
status: 'operational',
llm: { configured: false, connected: false, provider: 'openrouter' },
},
});
await withServer(routes, async url => {
const normal = await runPreflight(url);
assert.equal(normal.code, 0);
assert.equal(findCheck(normal.result, 'LLM backend available').passed, false);
const strict = await runPreflight(url, ['--strict']);
assert.equal(strict.code, 1);
});
});
await test('reports arsenal endpoint failure and strict mode treats it as a failure', async () => {
const routes = healthyRoutes({
'/api/arsenal/status': { httpStatus: 500, body: { error: 'unavailable' } },
});
await withServer(routes, async url => {
const normal = await runPreflight(url);
assert.equal(normal.code, 0);
assert.equal(findCheck(normal.result, 'Arsenal status available').passed, false);
const strict = await runPreflight(url, ['--strict']);
assert.equal(strict.code, 1);
});
});
await test('rejects a non-loopback API URL before making a request', async () => {
const { code, result } = await runPreflight('https://example.com');
assert.equal(code, 1);
assert.equal(result.checks.length, 1);
assert.equal(findCheck(result, 'Local API URL').passed, false);
});
console.log(`\n======== ${failed ? `${failed} FAILED` : 'ALL PASS'} (${passed} passed) ========\n`);
if (failed) process.exitCode = 1;