port: apply 7 upstream security/robustness patches

Ports from isolated upstream-port branch (base b3c5a4b), verified in
isolation via baseline-vs-port failure-set diff and re-verified live
(195 pass / 0 fail on affected tests):
- redact Discord bot tokens in outbound (router.ts SECRET_PATTERNS)
- block SSRF to private hosts in MoA base URL (moa.ts)
- refuse public dashboard bind without auth token (web-dashboard-server.ts)
- merge upstream .gitignore rules for python/build/secret noise
- real CPU utilization from /proc/stat instead of load avg (unified-dashboard.ts)
- width-safe placeholder for missing usage window on mobile (unified-dashboard.ts)
- bump direct deps to patch known vulnerabilities (discord.js/yaml/cron-parser)

Risky upstream commits (d5a94af phantom reset-time, patch 6 Codex usage)
intentionally skipped to avoid touching the credential-isolation tree.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Codex
2026-08-24 19:35:59 +09:00
parent b3c5a4b41b
commit c016b9c2fa
11 changed files with 393 additions and 205 deletions

View File

@@ -488,6 +488,47 @@ function buildStatusContent(): string {
return `${header}\n\n${sections}`;
}
/** Previous /proc/stat sample for CPU utilization deltas. */
let lastCpuSample: { idle: number; total: number } | null = null;
/**
* Real CPU utilization percent from /proc/stat deltas between calls.
* First call (no previous sample) and non-Linux hosts return null.
* Exported for testing alongside resetCpuUtilizationSample.
*/
export function readCpuUtilizationPct(
readStat: () => string = () => fs.readFileSync('/proc/stat', 'utf-8'),
): number | null {
try {
const cpuLine = readStat()
.split('\n')
.find((line) => line.startsWith('cpu '));
if (!cpuLine) return null;
const fields = cpuLine.trim().split(/\s+/).slice(1).map(Number);
if (fields.length < 5 || fields.some((n) => !Number.isFinite(n))) {
return null;
}
// user nice system idle iowait irq softirq steal ...
const idle = fields[3] + (fields[4] ?? 0); // idle + iowait
const total = fields.reduce((a, b) => a + b, 0);
const prev = lastCpuSample;
lastCpuSample = { idle, total };
if (!prev || total <= prev.total) return null;
const totalDelta = total - prev.total;
const idleDelta = idle - prev.idle;
return Math.round(
Math.min(100, Math.max(0, ((totalDelta - idleDelta) / totalDelta) * 100)),
);
} catch {
return null;
}
}
/** Test-only: reset the CPU sample so the next read starts fresh. */
export function resetCpuUtilizationSample(): void {
lastCpuSample = null;
}
/**
* Render usage table lines from two row groups (Claude and Codex).
* Returns rendered lines including code block markers.
@@ -517,6 +558,10 @@ export function renderUsageTable(
const lines: string[] = [];
// Missing-window placeholder. Must mirror a real cell's char composition
// (5 ambiguous-width bar chars + 4 ASCII): mobile fonts render block/box
// chars double-width, so a plain '—' cell breaks column alignment.
const emptyCell = '─'.repeat(5) + ' ';
const renderRows = (rows: UsageRow[]) => {
for (const row of rows) {
if (row.error) {
@@ -529,11 +574,11 @@ export function renderUsageTable(
const h5 =
row.h5pct >= 0
? `${bar(row.h5pct)}${String(row.h5pct).padStart(3)}%`
: ' — ';
: emptyCell;
const d7 =
row.d7pct >= 0
? `${bar(row.d7pct)}${String(row.d7pct).padStart(3)}%`
: ' — ';
: emptyCell;
lines.push(`${padName(row.name)}${h5} ${d7}`);
const r5 = compactReset(row.h5reset);
const r7 = compactReset(row.d7reset);
@@ -626,7 +671,11 @@ async function buildUsageContent(): Promise<string> {
const loadAvg = os.loadavg();
const cpuCount = os.cpus().length;
const cpuPct = Math.round((loadAvg[1] / cpuCount) * 100);
// Real CPU utilization from /proc/stat deltas between renders.
// Load-average-based percent counts D-state (I/O-wait) processes, so a
// writeback storm once rendered as "CPU 3104%" despite idle CPUs.
const cpuPct = readCpuUtilizationPct() ?? 0;
const loadPerCore = loadAvg[0] / cpuCount;
const totalMem = os.totalmem();
// os.freemem() includes buffers/cache as "used" — misleading.
// Read MemAvailable from /proc/meminfo for actual available memory.
@@ -696,6 +745,12 @@ async function buildUsageContent(): Promise<string> {
lines.push('```');
lines.push(`${'CPU'.padEnd(8)}${bar(cpuPct)} ${String(cpuPct).padStart(3)}%`);
// Raw load average with core count: I/O storms (D-state pileups) show up
// here without masquerading as CPU usage. Flag when load exceeds cores.
const loadFlag = loadPerCore > 1 ? ' ▲' : '';
lines.push(
`${'Load'.padEnd(8)}${loadAvg[0] >= 100 ? loadAvg[0].toFixed(0) : loadAvg[0].toFixed(1)}/${cpuCount}cpu${loadFlag}`,
);
lines.push(
`${'Memory'.padEnd(8)}${bar(memPct)} ${String(memPct).padStart(3)}% ${memUsedGB}/${memTotalGB}GB`,
);