fix(web): REPL spawn passed run-only flags as globals, breaking every run

The REPL-backed path (run/whitebox/greybox) spawns `neurosploit` with NO
subcommand, so only global flags are valid in argv — but authArgs() emitted
run-subcommand flags there (--environment, --policy, --in-scope, --budget,
--compliance, --revalidate-poc, --token-limit, --deep-test-limit, --order,
--sample-per-route, --scope-file). clap aborted on the first one
("unexpected argument '--environment'"), so the engagement died at launch and
the live view sat empty. authArgs now emits only the real global flags with
their global names (--session-environment / --session-in-scope / --session-policy,
plus --capability-token/--transport/--oob-*/--sms/--typesafe/--decision-backend/
--intercept/--sandbox); run-only knobs ride the REPL script or defaults.

Also:
- sidebar: a disk run whose status says "running" but has no live job is shown
  as "interrupted", not "running" (no more stale RUNNING entries); brand-new
  in-memory jobs are injected so an engagement appears the moment it starts;
  new "Interrupted" group; clicking a running/interrupted row attaches the live
  stream or offers resume.
- stop: robust now — graceful /stop then SIGTERM/SIGKILL fallback, a second
  press escalates, a job whose child already exited is marked done so the UI
  stops showing it as running.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
CyberSecurityUPandClaude Opus 4.8 committed 2026-10-03 01:08:57 -03:00
1 parent 999d9c2209
commit e1350c0c4a
2 files changed
+114 -20

No files matched your search

+49 -1
View File
@@ -1625,7 +1625,8 @@ function renderSidebar() {
const match = (r) => !q || `${r.name} ${r.target} ${r.id}`.toLowerCase().includes(q);
const runs = state.runs.filter(match);
const running = runs.filter((r) => r.state === 'running');
const past = runs.filter((r) => r.state !== 'running');
const interrupted = runs.filter((r) => r.state === 'interrupted');
const past = runs.filter((r) => r.state !== 'running' && r.state !== 'interrupted');
if (running.length) {
const wrap = document.createElement('div');
@@ -1646,6 +1647,16 @@ function renderSidebar() {
root.appendChild(wrap);
}
if (interrupted.length) {
const wrap = document.createElement('div');
wrap.className = 'sb-group';
wrap.innerHTML = `<div class="sb-group-head"><span class="caret">▾</span><span>Interrupted</span><span class="count">${interrupted.length}</span></div><div class="sb-items"></div>`;
wrap.querySelector('.sb-group-head').addEventListener('click', () => wrap.classList.toggle('collapsed'));
const items = wrap.querySelector('.sb-items');
for (const r of interrupted) items.appendChild(runButton(r));
root.appendChild(wrap);
}
const folders = runFolders(past);
if (!folders.length) {
const empty = document.createElement('div');
@@ -1682,6 +1693,29 @@ function renderSidebar() {
}
}
// Resume an interrupted run from the sidebar: if a persisted job exists for it,
// relaunch it live; otherwise just open its detail (partial findings on disk).
async function resumeFromSidebar(run) {
let jobs = [];
try { jobs = await api('/api/exploit'); } catch { /* fall through to detail */ }
const job = jobs.find((j) => (j.interrupted && j.resumable) && (j.runId === run.id || j.id === run.id || j.id === run.jobId));
if (job) {
try {
await api(`/api/exploit/${job.id}/resume`, { method: 'POST' });
localStorage.setItem(ACTIVE_JOB_KEY, job.id);
attachLiveJob(job.id, run.target, run.name, job.pinnedAgents || []);
await refreshRuns();
return;
} catch (e) {
toast(`Couldn't resume: ${e.message}`, 'error', 8000);
}
}
// No resumable job — show what's on disk.
show($('#wizardView'), false); show($('#liveView'), false); show($('#dashView'), false); show($('#detailView'), true);
loadDetail(run.id);
renderSidebar();
}
function openRun(run) {
state.currentDetailId = run.id;
if (run.state === 'running' && state.currentJob && run.id === state.currentJob.runId) {
@@ -1689,6 +1723,20 @@ function openRun(run) {
renderSidebar();
return;
}
// A running job we're not already attached to (e.g. opened in another tab, or
// just started): reconnect its live stream instead of showing a static detail.
if (run.state === 'running' && (run.jobId || run.id)) {
const jid = run.jobId || run.id;
localStorage.setItem(ACTIVE_JOB_KEY, jid);
attachLiveJob(jid, run.target, run.name, []);
renderSidebar();
return;
}
if (run.state === 'interrupted') {
// Offer to resume where it left off; falls back to the static detail view.
resumeFromSidebar(run);
return;
}
show($('#wizardView'), false); show($('#liveView'), false); show($('#dashView'), false); show($('#detailView'), true);
loadDetail(run.id);
renderSidebar();