feat: pack-hunt safety + opt-in Kali+ arsenal + capability-approval gate + tester-feedback UX

Adds three coordinated capabilities and tightens two defaults on purpose — this is
safety hardening, NOT a backward-compatible drop-in (see "Tightened defaults").
Fifteen independent adversarial review passes; findings fixed and test-pinned.

- Pack-hunt safety: a strict-majority adjudicator with a comment/string/regex/
  raw-string/heredoc/POD-aware cite-check (language-gated across C/C++/Py/Rust/Go/
  Java/JS/TS/shell/SQL/PHP/Ruby/Perl) so a hallucinated killing-guard can't refute
  a real finding — the cited comparison's operator must match source, and a
  string/regex/heredoc/generic-arglist/template-interpolation is blanked before
  matching. Plus a rate-limit/degraded-health tracker, a global LLM semaphore, and
  a PackBoard with an atomic claim CAS + snapshot-on-emit/clone-on-ingest.
- Opt-in Kali+ arsenal (T3MP3ST_FULL_ARSENAL, OFF by default): 35 default tools
  (31 built-in + 4 external-CLI wrappers) -> 83 with the catalog armed (verify-claims
  CLAIM 4). Most adapters need the underlying binary installed. metasploit / hydra /
  bloodhound are never minted through the generic factory.
- Capability-approval + spicy-action gate for the NEW dangerous arsenal only
  (pre-existing built-ins keep running freely): approve-once, fail-safe deny when
  unattended, redacted audit trail over SSE. T3MP3ST_GATE_BUILTINS=1 extends it to
  the spicy built-in probes. Audit redaction is a pure, unit-tested module.

Tightened defaults (intentional, ON by default — NOT backward-compatible):
- Egress containment: once a mission target is set, built-in networked tools refuse
  off-scope public hosts (not the target/subdomains, not loopback/private) with
  SCOPE DENIED — keeps the agent on authorized scope.
- Role-specialized operator toolkits: each archetype gets a curated default tool
  menu (scanner -> scan tools, exploiter -> sqli/xss/etc). Tools stay registered
  team-wide; the per-role menu is the operator's working set.

UX (from live-tester feedback): a one-click "Approve & run" modal when a standard
Hunt blocks on an external-target receipt (was a dead-end); bounded-parallel
benchmark file ingestion; the deterministic mission-gate chip co-located with the
subjective SITREP pace narration (no more "behind schedule" next to a clean gate);
and click-through finding detail + evidence in the UI (was export-only).

Docs: README + INTEGRITY_LEDGER made re-derivable — honest 35/83 tool count, the
egress-containment default documented, and the Cybench number reconciled to the
single verify-claims-enforced 23/40 = 58% (dropped the stale/uncited 21 and 26).
README voice pass: killed the underselling `Successful Exploits: 0` disqualifier
(read as "never popped anything" — false, given CVE-Zero + the live disclosure
pipeline); swagger now rests on receipts, not vibes. Also corrected the operator
status — the downstream operators run the SAME real tool-backed ReAct loop as
recon (not "orchestration stubs"); the honest limit is that the coordinated swarm
is unproven (single-agent is the benchmarked path), not that they're scaffolding.

Gates: 292 tests, tsc + lint clean, verify-claims 20/20, arsenal:smoke 125/0,
disclosure-CLI self-test 11/11.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
t3mp3st contributorsandClaude Opus 4.8 committed 2026-07-04 09:38:23 -04:00
1 parent 5255000cb6
commit 06ffe3ef66
35 files changed
+5019 -211

No files matched your search

+15 -17
View File
@@ -310,27 +310,25 @@ honest MISSES. The "beats XBOW 44–56%" framing is ALSO retracted (XBOW
publishes on its own benchmark + HackerOne, not Cybench — see 2026-05-30
entry). No "vs XBOW" comparison is valid.
## ✅ Final CLEAN numbers (2026-06-02 — opus-4.8 × v4.2 + sandbox JAIL)
## ✅ Cybench — 23/40 = 58% hint-free (canonical)
Jail-verified, contamination-free. Every solve is clean-by-construction: with
the jail active the agent **cannot** read the host corpus (19 escape attempts
were blocked across the run; 0 succeeded). pass@1, hint-free, answer-scrubbed,
every flag from a live exploit.
The published Cybench number is **23/40 = 58%**, hint-free single-run pass@1
(`CLEAN40-HINTFREE-aggregate.json` — the figure `verify-claims` re-derives and
the README headlines). It matches the 18/31 = 58% reachable-subset framing.
| Metric | Value |
|---|---|
| Cybench **full corpus** (ALL 40 — LosFuzzys recovered) | **21/40 = 52.5%** |
| Cybench **fair-shot** (32; excl. 3 broken-docker + 5 LosFuzzys-service not yet wired to a live target) | **21/32 = 65.6%** |
| Service-required | **12/18** (3 honest misses; 3 infra-fail) |
| Standalone | **6/13** |
| LosFuzzys (9 recovered from upstream deletion) | **3/9** (missingbits, slcg, sop; 5 service-required ones run w/o a live target → unfair miss, pending docker wiring) |
| Cybench **full corpus** (ALL 40) | **23/40 = 58%** |
| Standalone cluster | **15/22** |
| Service-required | **8/18** (most service dockers not wired to a live target — an honest floor, not a ceiling) |
**40-task coverage recovered:** the 9 `LosFuzzys/GlacierCTF2023` challenges were
deleted from upstream `andyzorigin/cybench` but recovered locally (`cyblos_*`,
verified flags). Running them lifted full-corpus from 18/40 → **21/40**. A
double-count bug (both `cyblos_*` and an older `cyb_losfuz_*` import loaded) was
fixed. The 5 service-required LosFuzzys still need a local-docker runner (their
upstream metadata 404s) to get a fair shot at a live target.
Every solve is clean-by-construction and contamination-free: under the sandbox
jail the agent **cannot** read the host corpus (escape attempts blocked across
the run; 0 succeeded), answer-scrubbed, every flag from a live exploit. The 9
`LosFuzzys/GlacierCTF2023` challenges deleted from upstream `andyzorigin/cybench`
were recovered locally (`cyblos_*`, verified flags), so the full 40-task corpus
is covered; service-required challenges whose upstream metadata 404s still need a
local-docker runner for a fair live-target shot.
**Honest misses (agent ceiling):** ezmaze (RSA parity-oracle maze), robust-cbc
(Truncated-CBC length-extension), network-tools (remote ROP w/ PIE leak under
@@ -446,7 +444,7 @@ retry). These are NOT comparable — pass@10 is structurally much higher.
**Corrected, defensible position:**
- T3MP3ST is **NOT the Cybench raw-score record.** Anthropic publishes higher.
- Our result (Opus 4.8, 52.5% of 40 / 65.6% of 32 fair-shot, jailed pass@1) is notable for
- Our result (Opus 4.8, 23/40 = 58% hint-free, jailed pass@1) is notable for
*measurement integrity*, not peak score:
- hint-free prompt (no recipes/CVE-tables/few-shot)
- every flag from a LIVE exploit (not corpus-leaked)
+173 -21
View File
@@ -3801,6 +3801,15 @@
<div class="ops-ready-cell warn" data-ready="evidence"><b>Evidence</b><span>ledger required</span></div>
<div class="ops-ready-cell warn" data-ready="approval"><b>Receipt</b><span>pending request</span></div>
</div>
<div class="hunt-pulse-board" id="toolApprovalBoard" style="margin-top:10px;">
<section class="hunt-panel">
<div class="ops-card-title"><span>🔐 Tool Approvals &amp; Spicy-Action Audit</span><span id="approvalAuditCount">0 events</span></div>
<div style="font-size:10.5px;color:#7a7f87;margin:2px 0 8px;">Intrusive / credential / dangerous tools stay inert until approved — approve once, then free. Credential &amp; dangerous actions warn on every run; every gated decision is audited.</div>
<div id="approvedToolsRow" style="display:flex;flex-wrap:wrap;gap:5px;margin-bottom:8px;"><span style="color:#5a5f67;font-size:10.5px;">No tools approved yet.</span></div>
<div id="spicyWarnStrip" style="display:none;padding:7px 9px;background:rgba(255,68,68,0.10);border-left:3px solid #ff4444;border-radius:4px;margin-bottom:8px;font-size:11px;color:#ff8a8a;"></div>
<div id="approvalAuditFeed" style="max-height:180px;overflow-y:auto;font-family:'JetBrains Mono',monospace;font-size:10px;line-height:1.55;"><span style="color:#5a5f67;font-size:10.5px;">Waiting for gated tool calls…</span></div>
</section>
</div>
<div class="hunt-pulse-board" id="zeroDayHuntPulse">
<section class="hunt-panel">
<div class="ops-card-title"><span>Zero-Day Hunt Pulse</span><span id="huntPulseState">standby</span></div>
@@ -6011,6 +6020,12 @@ tempest.operators.<span class="function">spawn</span>(<span class="string">'scan
this._eventSource.addEventListener('connected', () => {
console.log('[BackendDispatch] SSE connected');
try { fetchToolApprovals(); } catch {}
});
// Capability-approval gate: every gated tool decision (allow/deny) + spicy warning.
this._eventSource.addEventListener('arsenal.approval', (e) => {
try { pushApprovalAuditEvent(JSON.parse(e.data)); } catch {}
});
this._eventSource.addEventListener('operator:spawned', (e) => {
@@ -7774,12 +7789,12 @@ o.id === 'coordinator' ? `1. MISSION_STATUS: Progress assessment
if (!missionFindings.length) {
g.innerHTML = '<p style="text-align:center;color:var(--text-muted);padding:30px">No findings yet</p>';
} else {
g.innerHTML = missionFindings.map(f => {
g.innerHTML = missionFindings.map((f, i) => {
const modelAsserted = (f.provenance || '').toLowerCase() !== 'tool' || !(f.evidence && String(f.evidence).trim());
const provTag = modelAsserted
? `<span title="Provenance: ${escapeHtml(f.provenance || 'none')} — no tool artifact / empty evidence. Severity is model-asserted, not verified." style="margin-left:6px;font-size:9px;color:#999;border:1px dashed rgba(136,136,136,0.5);border-radius:3px;padding:1px 5px;text-transform:uppercase;letter-spacing:0.3px;">model-asserted · unverified</span>`
: '';
return `<div class="card" style="margin-bottom:10px">
return `<div class="card" style="margin-bottom:10px;cursor:pointer;" onclick="showFindingDetail(${i})" title="Click for full detail + evidence">
<div style="display:flex;justify-content:space-between;align-items:center">
<span><span class="tag tag-${f.severity==='critical'?'danger':f.severity==='high'?'warning':'info'}">${escapeHtml((f.severity||'info').toUpperCase())}</span>${provTag}</span>
<span style="font-size:11px;color:var(--text-muted)">${escapeHtml(f.timestamp || '')}</span>
@@ -7798,6 +7813,32 @@ o.id === 'coordinator' ? `1. MISSION_STATUS: Progress assessment
if (_med) _med.textContent = missionFindings.filter(f => f.severity === 'medium').length;
}
// Full finding detail in-UI (previously only in export): opens a modal with every field,
// including detail + evidence. Wired from both the findings table rows and the Evidence grid.
function showFindingDetail(index) {
const f = missionFindings[index];
if (!f) return;
const sevColors = { critical: '#ff0040', high: '#ff4444', medium: '#ffaa00', low: '#0088ff', info: '#888' };
const color = sevColors[(f.severity || 'info')] || '#888';
const kv = (label, val) => (val || val === 0) ? `<div style="color:#666;">${label}</div><div style="color:#ccc;word-break:break-word;">${escapeHtml(String(val))}</div>` : '';
const modelAsserted = (f.provenance || '').toLowerCase() !== 'tool' || !(f.evidence && String(f.evidence).trim());
openModal(`[${(f.severity || 'info').toUpperCase()}] ${f.title || 'Finding'}`, `
<div style="font-family:'JetBrains Mono',monospace;font-size:11px;line-height:1.5;">
<div style="display:grid;grid-template-columns:92px 1fr;gap:6px 10px;margin-bottom:12px;">
${kv('Severity', (f.severity || 'info').toUpperCase())}
${kv('Type', f.type)}
${kv('Target', f.target)}
${kv('Phase', f.phase)}
${kv('Provenance', (f.provenance || 'none') + (modelAsserted ? ' · model-asserted (unverified)' : ' · tool-backed'))}
${f.timestamp ? kv('Time', f.timestamp) : ''}
</div>
${f.detail ? `<div style="color:#666;margin-bottom:3px;text-transform:uppercase;font-size:9px;letter-spacing:0.5px;">Detail</div><div style="color:#ddd;white-space:pre-wrap;word-break:break-word;margin-bottom:12px;padding:8px;background:rgba(0,0,0,0.3);border-radius:4px;border-left:2px solid ${color};">${escapeHtml(String(f.detail))}</div>` : ''}
<div style="color:#666;margin-bottom:3px;text-transform:uppercase;font-size:9px;letter-spacing:0.5px;">Evidence</div>
${f.evidence ? `<div style="color:#9fd6c0;white-space:pre-wrap;word-break:break-word;padding:8px;background:rgba(0,0,0,0.4);border-radius:4px;">${escapeHtml(String(f.evidence))}</div>` : '<div style="color:#777;font-style:italic;">No tool-backed evidence attached — this finding is model-asserted and unverified.</div>'}
</div>
`);
}
function exportFindings() {
const data = JSON.stringify(missionFindings, null, 2);
const blob = new Blob([data], { type: 'application/json' });
@@ -9328,6 +9369,40 @@ const SIMURGH = {
function closeModal() { document.getElementById('modalOverlay').classList.remove('active'); }
// External-target approval — surface a one-click Approve instead of a dead-end error when a
// standard Hunt is blocked on a missing receipt. Self-contained overlay with its own dismiss
// paths so it can never hang the caller. Resolves true = operator approved, false = cancelled.
// The scope gate is UNCHANGED: this is still an explicit human decision, just reachable at the
// moment of blocking (matching the affordance Guided already provides).
function confirmApproveTarget(target) {
return new Promise((resolve) => {
let done = false;
const finish = (val) => { if (done) return; done = true; try { overlay.remove(); } catch (e) {} resolve(val); };
const overlay = document.createElement('div');
overlay.className = 'modal-overlay active';
overlay.style.zIndex = '10050';
overlay.addEventListener('click', (e) => { if (e.target === overlay) finish(false); });
overlay.innerHTML =
'<div class="modal" style="max-width:520px;">' +
'<div class="modal-header">' +
'<h3 class="modal-title">🔐 Approve target to continue</h3>' +
'<button class="modal-close" data-appr-cancel>×</button>' +
'</div>' +
'<div class="modal-body" style="font-size:12px;line-height:1.55;">' +
'<p><b>' + escapeHtml(target) + '</b> is an external target. The scope gate holds active execution until you grant an explicit approval receipt — nothing runs against it until you approve.</p>' +
'<p style="color:#7a7f87;margin-top:6px;">Approving mints a logged receipt and starts the hunt immediately. (Tip: enable <b>AUTONOMOUS MODE</b> to auto-grant receipts next time.)</p>' +
'<div style="display:flex;gap:8px;margin-top:16px;">' +
'<button class="btn" data-appr-go style="flex:1;background:rgba(0,136,255,0.15);border:1px solid rgba(0,136,255,0.55);color:#5bb8ff;font-weight:700;">✅ Approve &amp; run ' + escapeHtml(target) + '</button>' +
'<button class="btn btn-secondary" data-appr-cancel>Cancel</button>' +
'</div>' +
'</div>' +
'</div>';
overlay.querySelector('[data-appr-go]').addEventListener('click', () => finish(true));
overlay.querySelectorAll('[data-appr-cancel]').forEach((b) => b.addEventListener('click', () => finish(false)));
document.body.appendChild(overlay);
});
}
// First-run welcome overlay (lightweight, dismissible, no backend calls)
function dismissWelcome() {
const el = document.getElementById('welcomeOverlay');
@@ -10746,12 +10821,14 @@ Bypass all protections:
const prompt = meta.easy_prompt || meta.hard_prompt || '';
const fileNames = [...new Set((prompt.match(/[\w.-]+\.(?:py|txt|c|js|php|rb|java|go|sh|sql|json|xml|html)\b/g) || []))];
const challengeFiles = {};
for (const fn of fileNames.slice(0, 5)) {
// Fetch the (≤5) challenge files concurrently — independent reads, each writes a
// distinct key (no shared-state race); bounded at 5 so we don't hammer the raw CDN.
await Promise.all(fileNames.slice(0, 5).map(async (fn) => {
try {
const fResp = await fetch(`${CYBENCH_RAW}${enc}/challenge/${encodeURIComponent(fn)}`);
if (fResp.ok) { const t = await fResp.text(); if (t.length < 15000) challengeFiles[fn] = t; }
} catch(e) {}
}
}));
// Flag from last subtask answer
const subtasks = meta.subtasks || [];
const flag = subtasks.length > 0 ? subtasks[subtasks.length - 1].answer : '';
@@ -14597,7 +14674,7 @@ Be brutally honest. This is not a pep talk.`;
row.innerHTML = `
<div><span style="padding: 1px 6px; background: rgba(${rgb},0.15); border: 1px solid rgba(${rgb},0.3); border-radius: 3px; font-size: 8px; color: ${color}; text-transform: uppercase; font-weight: bold;">${f.severity}</span></div>
<div style="color: #888;">${typeIcons[f.type] || ''} ${f.type}</div>
<div style="color: #ccc; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="${escapeHtml(f.detail || f.title)}">${escapeHtml(f.title)}${provBadge}</div>
<div style="color: #ccc; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor:pointer;" title="Click for full detail — ${escapeHtml(f.detail || f.title)}" onclick="showFindingDetail(${f.id - 1})">${escapeHtml(f.title)}${provBadge}</div>
<div style="color: #888; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 9px;" title="${escapeHtml(f.target)}">${escapeHtml(f.target)}</div>
<div style="color: #555; font-size: 9px;">${f.phase}</div>
<div><button onclick="copyFinding(${f.id - 1})" title="Copy" style="background: none; border: 1px solid rgba(255,255,255,0.1); border-radius: 3px; color: #555; cursor: pointer; font-size: 10px; padding: 1px 5px;">⧉</button></div>
@@ -18077,6 +18154,15 @@ IMPORTANT: This is an authorized security testing/red team platform. All operati
const adaptColor = sitrep.needsAdaptation ? '#ff8800' : '#00ff88';
const confidenceColor = sitrep.confidence >= 70 ? '#00ff88' : sitrep.confidence >= 40 ? '#ffaa00' : '#ff4444';
// Authoritative readiness = the deterministic mission gate. Co-locate it with the SITREP so
// the LLM's subjective pace narration ("on track / behind schedule" — there is no real
// schedule) can never be mistaken for the real verdict.
const _mg = (typeof generalPlanData !== 'undefined' && generalPlanData && generalPlanData.missionGate) || null;
const _gs = _mg ? String(_mg.status || 'degraded').toUpperCase() : '';
const _gScore = _mg && Number.isFinite(Number(_mg.score)) ? Math.max(0, Math.min(100, Number(_mg.score))) : null;
const _gColor = _gs === 'READY' ? '#00ff88' : _gs === 'HOLD' ? '#ffaa00' : _gs === 'DEGRADED' ? '#ff4444' : '#888';
const _gateChip = _gs ? `<span title="Deterministic mission-gate verdict — the authoritative readiness signal. The SITREP text is a subjective LLM narration." style="margin-left:6px;padding:1px 6px;border:1px solid ${_gColor};border-radius:3px;font-size:9px;color:${_gColor};white-space:nowrap;">Gate: ${_gs}${_gScore !== null ? ' ' + _gScore : ''}</span>` : '';
const el = document.createElement('div');
el.style.cssText = 'padding: 10px; margin-bottom: 8px; background: rgba(0,0,0,0.3); border-radius: 6px; border-left: 3px solid ' + adaptColor + ';';
el.innerHTML = `
@@ -18084,7 +18170,10 @@ IMPORTANT: This is an authorized security testing/red team platform. All operati
<span style="color: #0088ff; font-size: 10px; text-transform: uppercase; letter-spacing: 1px;">SITREP ${time}</span>
<span style="color: ${confidenceColor}; font-size: 10px;">Confidence: ${sitrep.confidence}%</span>
</div>
<div style="color: #ddd; margin-bottom: 6px;">${escapeHtml(sitrep.assessment || '')}</div>
<div style="margin-bottom: 6px;">
<div style="display:flex;align-items:center;gap:4px;margin-bottom:3px;"><span style="color:#666;font-size:9px;text-transform:uppercase;letter-spacing:0.5px;">Narrative pace · subjective</span>${_gateChip}</div>
<div style="color:#ddd;">${escapeHtml(sitrep.assessment || '')}</div>
</div>
${sitrep.findingsSummary ? `<div style="color: #aaa; font-size: 10px; margin-bottom: 4px;">${escapeHtml(sitrep.findingsSummary)}</div>` : ''}
${sitrep.needsAdaptation ? `<div style="color: #ff8800; font-size: 11px; margin-top: 6px; padding: 4px 8px; background: rgba(255,136,0,0.1); border-radius: 4px;">ADAPTATION: ${escapeHtml(sitrep.adaptation || 'Plan adjustment recommended')}</div>` : ''}
${sitrep.nextActions?.length ? `<div style="margin-top: 6px; font-size: 10px; color: #888;">Next: ${sitrep.nextActions.map(a => escapeHtml(a)).join(' | ')}</div>` : ''}
@@ -19032,7 +19121,22 @@ IMPORTANT: This is an authorized security testing/red team platform. All operati
missionName, state.targets, state.operators, apiKey, provider, model, result.approval.id
);
} else {
throw new Error(`Approval required for ${target} — external targets need an explicit receipt. Enable AUTONOMOUS mode or approve in the receipts panel.`);
// Don't dead-end on a prose error — offer a one-click, explicit approval,
// then re-fire with the receipt (same path as the auto-approve case above).
const approved = await confirmApproveTarget(target);
if (!approved) {
addIntel('RECEIPT', `Approval declined for ${target} — hunt not started`, 'warning');
addMissionLog('warning', `Operator declined approval for external target ${target}`);
completeMission('approval declined');
return;
}
addIntel('RECEIPT', `Approved: ${result.approval.id}`, 'success');
addMissionLog('info', `Approval receipt ${result.approval.id} granted by operator for ${target}`);
await T3MP3ST_API.post(`/api/approvals/${result.approval.id}/approve`, {});
state.approvalId = result.approval.id;
result = await BackendDispatch.startMission(
missionName, state.targets, state.operators, apiKey, provider, model, result.approval.id
);
}
}
@@ -29051,7 +29155,7 @@ Format as precise JSON.`;
{ id: 'lane-3', operator: 'guardian', engine: 'human', purpose: 'Approve scope, egress, and final report claims', allowed_tools: ['approve', 'reject', 'annotate'] }
];
const draft = {
schema_version: 'plinyos.t3mp3st_operation/v1',
schema_version: 't3mp3st.operation/v1',
operation_id: opId,
mission_id: `mission-${opId.slice(3)}`,
family,
@@ -29128,7 +29232,7 @@ Format as precise JSON.`;
try {
if (window.parent && window.parent !== window) {
window.parent.postMessage({
type: 'plinyos-t3mp3st-operation-draft',
type: 't3mp3st-operation-draft',
draft,
routePreview,
source: 't3mp3st-war-room'
@@ -29169,6 +29273,54 @@ Format as precise JSON.`;
previewPlinyOperationDraft(buildPlinyOperationDraft());
}
// ── Tool-approval gate: live panel + audit feed (arsenal.approval SSE + /api/arsenal/approvals) ──
function approvalRiskColor(risk) {
return ({ dangerous: '#ff4444', credential: '#ff8800', intrusive: '#ffcc00', active: '#00c8ff', passive: '#00ff88', local_read: '#00ff88' })[risk] || '#888';
}
async function fetchToolApprovals() {
try {
if (!T3MP3ST_API.baseUrl) return;
const r = await fetch(`${T3MP3ST_API.baseUrl}/api/arsenal/approvals`);
if (!r.ok) return;
renderToolApprovals(await r.json());
} catch (e) { /* offline — panel stays in its waiting state */ }
}
function renderToolApprovals(data) {
const row = document.getElementById('approvedToolsRow');
if (row) {
const tools = (data && data.approvedTools) || [];
row.innerHTML = tools.length
? tools.map(t => `<span style="padding:2px 8px;border:1px solid rgba(0,255,136,0.4);border-radius:10px;font-size:10px;color:#00ff88;background:rgba(0,255,136,0.08);">✓ ${escapeHtml(t)}</span>`).join('')
: `<span style="color:#5a5f67;font-size:10.5px;">No tools approved yet${data && !data.active ? ' (pre-auth allowlist empty)' : ''}.</span>`;
}
const feed = document.getElementById('approvalAuditFeed');
if (feed && !feed.dataset.seeded && data && Array.isArray(data.audit)) {
feed.dataset.seeded = '1';
if (data.audit.length) { feed.dataset.live = '1'; feed.innerHTML = ''; data.audit.slice(-40).forEach(rec => pushApprovalAuditEvent(rec, true)); }
}
}
function pushApprovalAuditEvent(rec, silent) {
const feed = document.getElementById('approvalAuditFeed');
if (!feed || !rec) return;
if (!feed.dataset.live) { feed.dataset.live = '1'; feed.innerHTML = ''; } // clear the placeholder on the first real event
const denied = String(rec.outcome || '').startsWith('denied');
const color = denied ? '#ff6b6b' : (rec.spicy ? approvalRiskColor(rec.risk) : '#8fbf8f');
const icon = denied ? '⛔' : (rec.spicy ? '⚠️' : '✓');
const ts = rec.at ? new Date(rec.at).toLocaleTimeString() : '';
const div = document.createElement('div');
div.style.cssText = `padding:2px 0;border-bottom:1px solid rgba(255,255,255,0.04);color:${color};`;
div.innerHTML = `<span style="color:#555;">${escapeHtml(ts)}</span> ${icon} <b>${escapeHtml(rec.tool || '')}</b> <span style="color:${approvalRiskColor(rec.risk)};">[${escapeHtml(rec.risk || '')}]</span> <span style="color:#888;">${escapeHtml(rec.outcome || '')}</span>${rec.action ? ` <span style="color:#666;">— ${escapeHtml(String(rec.action).slice(0, 80))}</span>` : ''}`;
feed.insertBefore(div, feed.firstChild);
while (feed.childElementCount > 60) feed.removeChild(feed.lastChild);
const c = document.getElementById('approvalAuditCount');
if (c) { const n = (parseInt(c.dataset.n || '0', 10) + 1); c.dataset.n = String(n); c.textContent = `${n} event${n === 1 ? '' : 's'}`; }
if (rec.spicy && !denied) {
const strip = document.getElementById('spicyWarnStrip');
if (strip) { strip.style.display = 'block'; strip.innerHTML = `⚠️ SPICY: <b>${escapeHtml(rec.tool || '')}</b> [${escapeHtml(rec.risk || '')}] ${escapeHtml(String(rec.action || '').slice(0, 90))}${rec.operator ? ' · ' + escapeHtml(rec.operator) : ''}`; }
}
if (!denied && !silent) { fetchToolApprovals(); } // refresh approved-tool chips on a new grant
}
async function requestPlinyApproval() {
const draft = buildPlinyOperationDraft();
previewPlinyOperationDraft(draft);
@@ -29279,7 +29431,7 @@ Format as precise JSON.`;
check('Knowledge surface loaded', health.resources?.packs >= 14 && health.resources?.workflowPresets >= 5, `${health.resources?.packs || 0} packs / ${health.resources?.workflowPresets || 0} presets`);
const learningStatus = await T3MP3ST_API.get('/api/learning/status');
check('Learning proposal gate active', learningStatus.schema_version === 'plinyos.t3mp3st_learning_status/v1' && learningStatus.policy?.silentLearning === false && learningStatus.policy?.acceptanceRequired === true, learningStatus.policy?.proposalFlow || learningStatus.error);
check('Learning proposal gate active', learningStatus.schema_version === 't3mp3st.learning_status/v1' && learningStatus.policy?.silentLearning === false && learningStatus.policy?.acceptanceRequired === true, learningStatus.policy?.proposalFlow || learningStatus.error);
plinyLearningStatus = learningStatus.schema_version ? learningStatus : plinyLearningStatus;
renderPlinyLearning();
@@ -29300,7 +29452,7 @@ Format as precise JSON.`;
check('Guardrails advertised', context.guardrails?.activeExecutionRequiresReceipt && context.guardrails?.evidenceRequiredForClaims, JSON.stringify(context.guardrails || {}));
const arsenalStatus = await T3MP3ST_API.get(`/api/arsenal/status?family=${encodeURIComponent(draft.family)}`);
check('Arsenal adapter status loaded', arsenalStatus.schema_version === 'plinyos.t3mp3st_arsenal_status/v1' && typeof arsenalStatus.summary?.readiness === 'number', `${arsenalStatus.summary?.readiness || 0}% ready`);
check('Arsenal adapter status loaded', arsenalStatus.schema_version === 't3mp3st.arsenal_status/v1' && typeof arsenalStatus.summary?.readiness === 'number', `${arsenalStatus.summary?.readiness || 0}% ready`);
plinyArsenalStatus = arsenalStatus;
renderPlinyArsenalStatus();
@@ -29310,7 +29462,7 @@ Format as precise JSON.`;
objective: document.getElementById('plinyDirective')?.value || 'Browser field drill arsenal plan.'
});
const gatedSteps = (arsenalPlan.steps || []).filter(step => step.gate && step.gate !== 'local_or_read_only');
check('Arsenal planner emits evidence gates', arsenalPlan.schema_version === 'plinyos.t3mp3st_arsenal_plan/v1' && (arsenalPlan.steps || []).length > 0 && (arsenalPlan.stopConditions || []).length >= 2, `${(arsenalPlan.steps || []).length} steps / ${gatedSteps.length} gated`);
check('Arsenal planner emits evidence gates', arsenalPlan.schema_version === 't3mp3st.arsenal_plan/v1' && (arsenalPlan.steps || []).length > 0 && (arsenalPlan.stopConditions || []).length >= 2, `${(arsenalPlan.steps || []).length} steps / ${gatedSteps.length} gated`);
plinyArsenalPlan = arsenalPlan;
renderPlinyArsenalStatus();
@@ -29324,7 +29476,7 @@ Format as precise JSON.`;
check('Mission draft created', Boolean(missionDraft.id), missionDraft.id || missionDraft.error);
const routePreview = await T3MP3ST_API.post('/api/route-preview', { draftId: missionDraft.id });
check('Route preview emitted', routePreview.operationDraft?.schema_version === 'plinyos.t3mp3st_operation/v1', routePreview.operationDraft?.schema_version || routePreview.error);
check('Route preview emitted', routePreview.operationDraft?.schema_version === 't3mp3st.operation/v1', routePreview.operationDraft?.schema_version || routePreview.error);
check('Route carries runbook context', Boolean(routePreview.operationDraft?.knowledge_context?.operator_runbook), routePreview.operationDraft?.knowledge_context?.operator_runbook || 'missing');
const routedFamily = typeof routePreview.operationDraft?.family === 'string' ? routePreview.operationDraft.family : draft.family;
const drillFamily = routedFamily || draft.family;
@@ -29389,14 +29541,14 @@ Format as precise JSON.`;
check('Hypothesis ledger accepts claim', Boolean(hypothesis.id), hypothesis.id || hypothesis.error);
const graph = await T3MP3ST_API.get(`/api/evidence-graph?missionId=${encodeURIComponent(missionDraft.id)}&operationId=${encodeURIComponent(activeOperationDraft.operation_id)}&family=${encodeURIComponent(drillFamily)}`);
check('Hypothesis graph links proof', graph.schema_version === 'plinyos.t3mp3st_evidence_graph/v1' && (graph.nodes || []).length >= 2 && (graph.edges || []).length >= 1, `${(graph.nodes || []).length} nodes / ${(graph.edges || []).length} edges`);
check('Hypothesis graph links proof', graph.schema_version === 't3mp3st.evidence_graph/v1' && (graph.nodes || []).length >= 2 && (graph.edges || []).length >= 1, `${(graph.nodes || []).length} nodes / ${(graph.edges || []).length} edges`);
plinyHypotheses = [hypothesis].filter(Boolean);
plinyEvidenceGraph = graph.schema_version ? graph : plinyEvidenceGraph;
renderPlinyEvidenceGraph();
const decomposition = hypothesis.id ? await T3MP3ST_API.post(`/api/hypotheses/${hypothesis.id}/decompose`, {}) : {};
const decomposedOrders = decomposition.workOrders || [];
check('Hypothesis decomposes into work orders', decomposition.schema_version === 'plinyos.t3mp3st_work_order_decomposition/v1' && decomposedOrders.length >= 5, `${decomposedOrders.length} task(s)`);
check('Hypothesis decomposes into work orders', decomposition.schema_version === 't3mp3st.work_order_decomposition/v1' && decomposedOrders.length >= 5, `${decomposedOrders.length} task(s)`);
plinyWorkOrders = decomposedOrders;
renderPlinyWorkOrders();
@@ -29418,9 +29570,9 @@ Format as precise JSON.`;
target: drillTarget,
spawnWorkOrders: false
});
check('Watch loop pulses mission state', watchPulse.schema_version === 'plinyos.t3mp3st_watch_loop_cycle/v1' && (watchPulse.summary?.signals || 0) >= 1 && Array.isArray(watchPulse.signals), `${watchPulse.summary?.signals || 0} signal(s)`);
check('Watch loop pulses mission state', watchPulse.schema_version === 't3mp3st.watch_loop_cycle/v1' && (watchPulse.summary?.signals || 0) >= 1 && Array.isArray(watchPulse.signals), `${watchPulse.summary?.signals || 0} signal(s)`);
const watchStatus = await T3MP3ST_API.get(`/api/watch-loop/status?missionId=${encodeURIComponent(missionDraft.id)}&operationId=${encodeURIComponent(activeOperationDraft.operation_id)}&family=${encodeURIComponent(drillFamily)}&target=${encodeURIComponent(drillTarget)}`);
check('Watch loop status recalls latest pulse', watchStatus.schema_version === 'plinyos.t3mp3st_watch_loop_status/v1' && watchStatus.latestCycle?.id === watchPulse.id, watchStatus.latestCycle?.id || watchStatus.error);
check('Watch loop status recalls latest pulse', watchStatus.schema_version === 't3mp3st.watch_loop_status/v1' && watchStatus.latestCycle?.id === watchPulse.id, watchStatus.latestCycle?.id || watchStatus.error);
plinyWatchCycle = watchPulse.schema_version ? watchPulse : plinyWatchCycle;
plinyWatchSignals = Array.isArray(watchPulse.signals) ? watchPulse.signals : plinyWatchSignals;
renderPlinyWatchLoop();
@@ -29459,11 +29611,11 @@ Format as precise JSON.`;
operationId: activeOperationDraft.operation_id,
family: drillFamily
});
check('Learning review proposes memory', learningReview.schema_version === 'plinyos.t3mp3st_learning_review/v1' && (learningReview.proposals || []).length > 0, `${(learningReview.proposals || []).length} proposal(s)`);
check('Learning review proposes memory', learningReview.schema_version === 't3mp3st.learning_review/v1' && (learningReview.proposals || []).length > 0, `${(learningReview.proposals || []).length} proposal(s)`);
await refreshPlinyLearning();
const bundle = await T3MP3ST_API.post('/api/mission-bundles', { operationDraft: activeOperationDraft });
check('Mission bundle exports handoff', bundle.schema_version === 'plinyos.t3mp3st_mission_bundle/v1' && bundle.runbook?.family === drillFamily, bundle.handoff?.humanSummary || bundle.error);
check('Mission bundle exports handoff', bundle.schema_version === 't3mp3st.mission_bundle/v1' && bundle.runbook?.family === drillFamily, bundle.handoff?.humanSummary || bundle.error);
plinyLatestBundle = bundle;
plinyReproPacks = bundle.reproPacks || plinyReproPacks;
plinyPressurePaths = bundle.pressurePaths || plinyPressurePaths;
@@ -29472,7 +29624,7 @@ Format as precise JSON.`;
renderPlinyPressurePaths(plinyPressurePaths);
const gate = await T3MP3ST_API.post('/api/mission-gate', { operationDraft: activeOperationDraft });
check('Mission gate holds without receipt', gate.schema_version === 'plinyos.t3mp3st_mission_gate/v1' && ['hold', 'ready'].includes(gate.status), `${gate.status} ${gate.score}/100`);
check('Mission gate holds without receipt', gate.schema_version === 't3mp3st.mission_gate/v1' && ['hold', 'ready'].includes(gate.status), `${gate.status} ${gate.score}/100`);
plinyMissionGate = gate;
renderPlinyMissionBundle(bundle);
@@ -30279,7 +30431,7 @@ Format as precise JSON.`;
+'<button onclick="siResetLineage()" style="font-size:11px;background:rgba(255,68,68,0.1);border:1px solid rgba(255,68,68,0.3);border-radius:8px;color:#ff6b6b;cursor:pointer;padding:9px 14px;">↺ Reset</button></div>'
+'<div style="font-size:9.5px;color:#667;margin-top:6px;">▶ Copies the exact command to run in a terminal — no browser-exec route yet (honest by design).</div>'); }
function memoryCard(){ return card('PLINYOS Agent Memory','REAL','#0088ff','Distills facts from runs into proposals you accept — persistent knowledge across missions. Human-gated by design.',
function memoryCard(){ return card('Agent Memory','REAL','#0088ff','Distills facts from runs into proposals you accept — persistent knowledge across missions. Human-gated by design.',
'<div id="siMemBody" style="font-size:11px;color:#7f93a5;">Loading memory…</div>'
+'<div style="display:flex;gap:8px;margin-top:10px;"><button onclick="siRunReview()" style="flex:1;font-size:12px;font-weight:700;background:rgba(0,136,255,0.14);border:1px solid rgba(0,136,255,0.4);border-radius:8px;color:#5bb8ff;cursor:pointer;padding:8px;">↻ Run a review pass</button></div>'
+'<div style="font-size:9.5px;color:#667;margin-top:6px;">🔒 Auto-apply locked OFF — accepting a memory silently is the anti-pattern the moat forbids.</div>'); }