T3MP3ST v1 — hardened launch (AGPL, verify-claims 24/24, leak-scrubbed)
This commit is contained in:
1 parent
c7a356bde0
commit
6c57e09023
242 files changed
+1761
-51038
No files matched your search
+23
-23
@@ -101,7 +101,7 @@ async function smokeCoreSurfaces() {
|
||||
async function smokeArsenalCatalog() {
|
||||
const catalog = await get('/api/arsenal/catalog');
|
||||
const adapterIds = (catalog.data.adapters || []).map(adapter => adapter.id);
|
||||
record('Arsenal catalog exposes adapter spine', catalog.ok && catalog.data.schema_version === 't3mp3st.arsenal_catalog/v1' && adapterIds.includes('nuclei') && adapterIds.includes('semgrep') && adapterIds.includes('promptfoo'), `${adapterIds.length} adapters`);
|
||||
record('Arsenal catalog exposes adapter spine', catalog.ok && catalog.data.schema_version === 't3mp3st_arsenal_catalog/v1' && adapterIds.includes('nuclei') && adapterIds.includes('semgrep') && adapterIds.includes('promptfoo'), `${adapterIds.length} adapters`);
|
||||
record('Arsenal safe commands exclude catalog-only heavy tools', catalog.ok && (catalog.data.safeCommands || []).includes('nuclei') && !(catalog.data.safeCommands || []).includes('msfconsole'), `${countItems(catalog.data.safeCommands)} safe commands`);
|
||||
|
||||
const webCatalog = await get('/api/arsenal/catalog?family=web_api');
|
||||
@@ -113,7 +113,7 @@ async function smokeArsenalCatalog() {
|
||||
record('Supply-chain arsenal includes evidence scanners', supplyCatalog.ok && ['semgrep', 'gitleaks', 'trivy', 'syft', 'grype'].every(id => supplyIds.includes(id)), supplyIds.join(', '));
|
||||
|
||||
const status = await get('/api/arsenal/status?family=web_api');
|
||||
record('Arsenal status reports installed and missing adapters', status.ok && status.data.schema_version === 't3mp3st.arsenal_status/v1' && typeof status.data.summary?.readiness === 'number' && Array.isArray(status.data.missingCommandReady), `${status.data.summary?.installed || 0} installed / ${status.data.summary?.readiness ?? 'n/a'}%`);
|
||||
record('Arsenal status reports installed and missing adapters', status.ok && status.data.schema_version === 't3mp3st_arsenal_status/v1' && typeof status.data.summary?.readiness === 'number' && Array.isArray(status.data.missingCommandReady), `${status.data.summary?.installed || 0} installed / ${status.data.summary?.readiness ?? 'n/a'}%`);
|
||||
|
||||
const plan = await post('/api/arsenal/plan', {
|
||||
family: 'web_api',
|
||||
@@ -121,7 +121,7 @@ async function smokeArsenalCatalog() {
|
||||
objective: 'Plan a local-only web/API smoke toolchain with evidence flow.',
|
||||
});
|
||||
const planIds = (plan.data.steps || []).map(step => step.adapterId);
|
||||
record('Arsenal planner turns adapters into gated evidence steps', plan.ok && plan.data.schema_version === 't3mp3st.arsenal_plan/v1' && planIds.includes('nuclei') && (plan.data.steps || []).every(step => step.gate && step.nextEvidenceMove), `${countItems(plan.data.steps)} planned steps`);
|
||||
record('Arsenal planner turns adapters into gated evidence steps', plan.ok && plan.data.schema_version === 't3mp3st_arsenal_plan/v1' && planIds.includes('nuclei') && (plan.data.steps || []).every(step => step.gate && step.nextEvidenceMove), `${countItems(plan.data.steps)} planned steps`);
|
||||
}
|
||||
|
||||
async function smokeKnowledgeAtlas() {
|
||||
@@ -190,7 +190,7 @@ async function smokeContractsAndLedgers() {
|
||||
|
||||
const route = await post('/api/route-preview', { draftId: draft.data.id });
|
||||
const operationDraft = route.data.operationDraft || {};
|
||||
record('Route preview emits operation contract', route.ok && operationDraft.schema_version === 't3mp3st.operation/v1', operationDraft.schema_version || summarizeError(route));
|
||||
record('Route preview emits operation contract', route.ok && operationDraft.schema_version === 't3mp3st_operation/v1', operationDraft.schema_version || summarizeError(route));
|
||||
|
||||
const missionApprovalRequest = await post('/api/approvals/request', {
|
||||
action: 'mission_execution',
|
||||
@@ -263,7 +263,7 @@ async function smokeContractsAndLedgers() {
|
||||
record('Hypothesis ledger can be filtered by mission', hypothesisList.ok && (hypothesisList.data.hypotheses || []).some(item => item.id === hypothesis.data.id), `${countItems(hypothesisList.data.hypotheses)} hypotheses`);
|
||||
|
||||
const evidenceGraph = await get(`/api/evidence-graph?missionId=${encodeURIComponent(draft.data.id)}&operationId=${encodeURIComponent(operationDraft.operation_id)}&family=web_api`);
|
||||
record('Evidence graph links hypotheses and proof', evidenceGraph.ok && evidenceGraph.data.schema_version === 't3mp3st.evidence_graph/v1' && countItems(evidenceGraph.data.nodes) >= 2 && countItems(evidenceGraph.data.edges) >= 1, `${countItems(evidenceGraph.data.nodes)} nodes / ${countItems(evidenceGraph.data.edges)} edges`);
|
||||
record('Evidence graph links hypotheses and proof', evidenceGraph.ok && evidenceGraph.data.schema_version === 't3mp3st_evidence_graph/v1' && countItems(evidenceGraph.data.nodes) >= 2 && countItems(evidenceGraph.data.edges) >= 1, `${countItems(evidenceGraph.data.nodes)} nodes / ${countItems(evidenceGraph.data.edges)} edges`);
|
||||
|
||||
const crossFamilyHypothesis = await post('/api/hypotheses', {
|
||||
missionId: draft.data.id,
|
||||
@@ -290,7 +290,7 @@ async function smokeContractsAndLedgers() {
|
||||
const decomposition = hypothesis.data.id
|
||||
? await post(`/api/hypotheses/${hypothesis.data.id}/decompose`, {})
|
||||
: { status: 0, data: {} };
|
||||
record('Hypothesis decomposes into specialist work orders', decomposition.status === 201 && decomposition.data.schema_version === 't3mp3st.work_order_decomposition/v1' && countItems(decomposition.data.workOrders) >= 5, `${countItems(decomposition.data.workOrders)} work orders`);
|
||||
record('Hypothesis decomposes into specialist work orders', decomposition.status === 201 && decomposition.data.schema_version === 't3mp3st_work_order_decomposition/v1' && countItems(decomposition.data.workOrders) >= 5, `${countItems(decomposition.data.workOrders)} work orders`);
|
||||
|
||||
const workOrderList = await get(`/api/work-orders?missionId=${encodeURIComponent(draft.data.id)}&operationId=${encodeURIComponent(operationDraft.operation_id)}&family=web_api`);
|
||||
record('Work order queue can be filtered by mission', workOrderList.ok && countItems(workOrderList.data.workOrders) >= 5, `${countItems(workOrderList.data.workOrders)} queued`);
|
||||
@@ -329,7 +329,7 @@ async function smokeContractsAndLedgers() {
|
||||
target: 'local-lab',
|
||||
spawnWorkOrders: false,
|
||||
});
|
||||
record('Watch loop emits scoped signals', watchPulse.status === 201 && watchPulse.data.schema_version === 't3mp3st.watch_loop_cycle/v1' && countItems(watchPulse.data.signals) >= 1, `${watchPulse.data.summary?.signals || 0} signals / ${watchPulse.data.summary?.actions || 0} actions`);
|
||||
record('Watch loop emits scoped signals', watchPulse.status === 201 && watchPulse.data.schema_version === 't3mp3st_watch_loop_cycle/v1' && countItems(watchPulse.data.signals) >= 1, `${watchPulse.data.summary?.signals || 0} signals / ${watchPulse.data.summary?.actions || 0} actions`);
|
||||
|
||||
const watchNudge = await post('/api/watch-loop/run', {
|
||||
missionId: draft.data.id,
|
||||
@@ -338,11 +338,11 @@ async function smokeContractsAndLedgers() {
|
||||
target: 'local-lab',
|
||||
spawnWorkOrders: true,
|
||||
});
|
||||
record('Watch loop nudge spawns specialist work orders', watchNudge.status === 201 && watchNudge.data.schema_version === 't3mp3st.watch_loop_cycle/v1' && (watchNudge.data.summary?.spawnedWorkOrders || 0) >= 5, `${watchNudge.data.summary?.spawnedWorkOrders || 0} spawned`);
|
||||
record('Watch loop nudge spawns specialist work orders', watchNudge.status === 201 && watchNudge.data.schema_version === 't3mp3st_watch_loop_cycle/v1' && (watchNudge.data.summary?.spawnedWorkOrders || 0) >= 5, `${watchNudge.data.summary?.spawnedWorkOrders || 0} spawned`);
|
||||
|
||||
const watchStatus = await get(`/api/watch-loop/status?missionId=${encodeURIComponent(draft.data.id)}&operationId=${encodeURIComponent(operationDraft.operation_id)}&family=web_api&target=local-lab`);
|
||||
const watchCycleIds = (watchStatus.data.cycles || []).map(cycle => cycle.id);
|
||||
record('Watch loop status recalls latest pulse', watchStatus.ok && watchStatus.data.schema_version === 't3mp3st.watch_loop_status/v1' && watchCycleIds.includes(watchNudge.data.id), `${watchStatus.data.latestCycle?.id || 'none'} latest / ${watchCycleIds.length} retained`);
|
||||
record('Watch loop status recalls latest pulse', watchStatus.ok && watchStatus.data.schema_version === 't3mp3st_watch_loop_status/v1' && watchCycleIds.includes(watchNudge.data.id), `${watchStatus.data.latestCycle?.id || 'none'} latest / ${watchCycleIds.length} retained`);
|
||||
|
||||
const selfHeal = await post('/api/self-heal/run', {
|
||||
missionId: draft.data.id,
|
||||
@@ -352,7 +352,7 @@ async function smokeContractsAndLedgers() {
|
||||
operationDraft,
|
||||
apply: true,
|
||||
});
|
||||
record('Self-heal diagnoses and safely applies watch repair', selfHeal.status === 201 && selfHeal.data.schema_version === 't3mp3st.self_heal/v1' && countItems(selfHeal.data.actions) >= 1 && typeof selfHeal.data.summary?.applied === 'number', `${selfHeal.data.health || 'unknown'} / ${selfHeal.data.summary?.applied || 0} applied`);
|
||||
record('Self-heal diagnoses and safely applies watch repair', selfHeal.status === 201 && selfHeal.data.schema_version === 't3mp3st_self_heal/v1' && countItems(selfHeal.data.actions) >= 1 && typeof selfHeal.data.summary?.applied === 'number', `${selfHeal.data.health || 'unknown'} / ${selfHeal.data.summary?.applied || 0} applied`);
|
||||
|
||||
const supportedHypothesis = hypothesis.data.id
|
||||
? await patch(`/api/hypotheses/${hypothesis.data.id}`, { status: 'supported', confidence: 0.76 })
|
||||
@@ -400,7 +400,7 @@ async function smokeContractsAndLedgers() {
|
||||
record('Retest can be queued', retest.status === 201 && retest.data.id, retest.data.id || summarizeError(retest));
|
||||
|
||||
const gateWithQueuedRetest = await post('/api/mission-gate', { operationDraft });
|
||||
record('Mission gate holds unresolved retest', gateWithQueuedRetest.ok && gateWithQueuedRetest.data.schema_version === 't3mp3st.mission_gate/v1' && gateWithQueuedRetest.data.status !== 'ready', `${gateWithQueuedRetest.data.status || 'unknown'} ${gateWithQueuedRetest.data.score ?? 'n/a'}/100`);
|
||||
record('Mission gate holds unresolved retest', gateWithQueuedRetest.ok && gateWithQueuedRetest.data.schema_version === 't3mp3st_mission_gate/v1' && gateWithQueuedRetest.data.status !== 'ready', `${gateWithQueuedRetest.data.status || 'unknown'} ${gateWithQueuedRetest.data.score ?? 'n/a'}/100`);
|
||||
|
||||
const completedRetest = retest.data.id
|
||||
? await patch(`/api/retests/${retest.data.id}`, { status: 'passed', resultSummary: 'Arsenal smoke retest passed.' })
|
||||
@@ -409,12 +409,12 @@ async function smokeContractsAndLedgers() {
|
||||
|
||||
const reproPacks = await post('/api/repro-packs', { operationDraft });
|
||||
const readyReproPack = (reproPacks.data.packs || []).find(pack => pack.readiness === 'ready');
|
||||
record('Repro packs summarize replay readiness', reproPacks.ok && reproPacks.data.schema_version === 't3mp3st.repro_packs/v1' && (reproPacks.data.summary?.total || 0) >= 2 && (reproPacks.data.summary?.ready || 0) >= 1, `${reproPacks.data.summary?.ready || 0}/${reproPacks.data.summary?.total || 0} ready`);
|
||||
record('Repro packs summarize replay readiness', reproPacks.ok && reproPacks.data.schema_version === 't3mp3st_repro_packs/v1' && (reproPacks.data.summary?.total || 0) >= 2 && (reproPacks.data.summary?.ready || 0) >= 1, `${reproPacks.data.summary?.ready || 0}/${reproPacks.data.summary?.total || 0} ready`);
|
||||
record('Repro pack includes safe replay contract', Boolean(readyReproPack && countItems(readyReproPack.replaySteps) >= 4 && countItems(readyReproPack.falsifiers) >= 3 && readyReproPack.safeProbe), readyReproPack?.id || summarizeError(reproPacks));
|
||||
|
||||
const pressurePaths = await post('/api/pressure-paths', { operationDraft });
|
||||
const bestPressurePath = (pressurePaths.data.paths || [])[0];
|
||||
record('Pressure paths convert repro into gated offensive chain', pressurePaths.ok && pressurePaths.data.schema_version === 't3mp3st.pressure_paths/v1' && (pressurePaths.data.summary?.total || 0) >= 2 && (pressurePaths.data.summary?.maxOffensiveScore || 0) > 0, `${pressurePaths.data.summary?.armed || 0}/${pressurePaths.data.summary?.total || 0} armed, max ${pressurePaths.data.summary?.maxOffensiveScore || 0}`);
|
||||
record('Pressure paths convert repro into gated offensive chain', pressurePaths.ok && pressurePaths.data.schema_version === 't3mp3st_pressure_paths/v1' && (pressurePaths.data.summary?.total || 0) >= 2 && (pressurePaths.data.summary?.maxOffensiveScore || 0) > 0, `${pressurePaths.data.summary?.armed || 0}/${pressurePaths.data.summary?.total || 0} armed, max ${pressurePaths.data.summary?.maxOffensiveScore || 0}`);
|
||||
record('Pressure path includes simulator and no-go gates', Boolean(bestPressurePath && bestPressurePath.safeSimulator?.mode === 'local_canary' && countItems(bestPressurePath.chainStages) >= 5 && countItems(bestPressurePath.noGo) >= 2), bestPressurePath?.id || summarizeError(pressurePaths));
|
||||
|
||||
const pressureCanary = await post('/api/pressure-paths/canary', {
|
||||
@@ -422,7 +422,7 @@ async function smokeContractsAndLedgers() {
|
||||
pathId: bestPressurePath?.id,
|
||||
pressurePaths: pressurePaths.data,
|
||||
});
|
||||
record('Pressure canary rehearses top path locally', pressureCanary.ok && pressureCanary.data.schema_version === 't3mp3st.pressure_canary/v1' && pressureCanary.data.status === 'passed' && pressureCanary.data.canary?.mode === 'local_canary', pressureCanary.data.canary?.observedSignal || summarizeError(pressureCanary));
|
||||
record('Pressure canary rehearses top path locally', pressureCanary.ok && pressureCanary.data.schema_version === 't3mp3st_pressure_canary/v1' && pressureCanary.data.status === 'passed' && pressureCanary.data.canary?.mode === 'local_canary', pressureCanary.data.canary?.observedSignal || summarizeError(pressureCanary));
|
||||
// Honest by design: a local synthetic canary is CONTEXT evidence, never 'replayable'
|
||||
// proof against the real target (see buildPressureCanary — stamping it 'replayable'
|
||||
// would let a finding self-promote on fabricated evidence). Assert 'context'.
|
||||
@@ -434,7 +434,7 @@ async function smokeContractsAndLedgers() {
|
||||
pressurePaths: pressurePaths.data,
|
||||
pressureCanary: pressureCanary.data,
|
||||
});
|
||||
record('Pressure duel survives only evidence-backed canary route', pressureDuel.ok && pressureDuel.data.schema_version === 't3mp3st.pressure_duel/v1' && pressureDuel.data.status === 'survived' && (pressureDuel.data.survivabilityScore || 0) >= 86, `${pressureDuel.data.status || 'unknown'} ${pressureDuel.data.survivabilityScore || 0}/100`);
|
||||
record('Pressure duel survives only evidence-backed canary route', pressureDuel.ok && pressureDuel.data.schema_version === 't3mp3st_pressure_duel/v1' && pressureDuel.data.status === 'survived' && (pressureDuel.data.survivabilityScore || 0) >= 86, `${pressureDuel.data.status || 'unknown'} ${pressureDuel.data.survivabilityScore || 0}/100`);
|
||||
record('Pressure duel writes skeptic evidence and follow-up task', Boolean(pressureDuel.data.evidence?.id && pressureDuel.data.workOrder?.id && countItems(pressureDuel.data.duel?.rounds) >= 3), `${pressureDuel.data.evidence?.id || 'no evidence'} / ${pressureDuel.data.workOrder?.id || 'no work order'}`);
|
||||
|
||||
const pressureMutations = await post('/api/pressure-paths/mutate', {
|
||||
@@ -443,7 +443,7 @@ async function smokeContractsAndLedgers() {
|
||||
pressurePaths: pressurePaths.data,
|
||||
pressureDuel: pressureDuel.data,
|
||||
});
|
||||
record('Pressure mutation gauntlet forks survived route', pressureMutations.ok && pressureMutations.data.schema_version === 't3mp3st.pressure_mutations/v1' && pressureMutations.data.status === 'queued' && countItems(pressureMutations.data.mutations) >= 5 && (pressureMutations.data.summary?.maxFangScore || 0) >= 80, `${countItems(pressureMutations.data.mutations)} mutations / max ${pressureMutations.data.summary?.maxFangScore || 0}`);
|
||||
record('Pressure mutation gauntlet forks survived route', pressureMutations.ok && pressureMutations.data.schema_version === 't3mp3st_pressure_mutations/v1' && pressureMutations.data.status === 'queued' && countItems(pressureMutations.data.mutations) >= 5 && (pressureMutations.data.summary?.maxFangScore || 0) >= 80, `${countItems(pressureMutations.data.mutations)} mutations / max ${pressureMutations.data.summary?.maxFangScore || 0}`);
|
||||
record('Pressure mutation gauntlet queues specialist work orders', Boolean(pressureMutations.data.evidence?.id && countItems(pressureMutations.data.workOrders) >= 3 && pressureMutations.data.workOrders.every(order => order.requiresReceipt === false)), `${pressureMutations.data.evidence?.id || 'no evidence'} / ${countItems(pressureMutations.data.workOrders)} work orders`);
|
||||
|
||||
const pressureChains = await post('/api/pressure-paths/chains', {
|
||||
@@ -452,7 +452,7 @@ async function smokeContractsAndLedgers() {
|
||||
pressurePaths: pressurePaths.data,
|
||||
pressureMutations: pressureMutations.data,
|
||||
});
|
||||
record('Pressure fang chains compose local weird-machine routes', pressureChains.ok && pressureChains.data.schema_version === 't3mp3st.pressure_chains/v1' && pressureChains.data.status === 'queued' && countItems(pressureChains.data.chains) >= 3 && (pressureChains.data.summary?.maxChainScore || 0) >= 80, `${countItems(pressureChains.data.chains)} chains / max ${pressureChains.data.summary?.maxChainScore || 0}`);
|
||||
record('Pressure fang chains compose local weird-machine routes', pressureChains.ok && pressureChains.data.schema_version === 't3mp3st_pressure_chains/v1' && pressureChains.data.status === 'queued' && countItems(pressureChains.data.chains) >= 3 && (pressureChains.data.summary?.maxChainScore || 0) >= 80, `${countItems(pressureChains.data.chains)} chains / max ${pressureChains.data.summary?.maxChainScore || 0}`);
|
||||
record('Pressure fang chains queue staged specialist work orders', Boolean(pressureChains.data.evidence?.id && countItems(pressureChains.data.workOrders) >= 2 && pressureChains.data.workOrders.every(order => order.requiresReceipt === false)), `${pressureChains.data.evidence?.id || 'no evidence'} / ${countItems(pressureChains.data.workOrders)} work orders`);
|
||||
|
||||
const evidenceList = await get(`/api/evidence?missionId=${encodeURIComponent(draft.data.id)}`);
|
||||
@@ -469,15 +469,15 @@ async function smokeContractsAndLedgers() {
|
||||
record('Retest ledger can be listed', retestList.ok && (retestList.data.retests || []).some(item => item.id === retest.data.id), `${countItems(retestList.data.retests)} retests`);
|
||||
|
||||
const bundle = await post('/api/mission-bundles', { operationDraft });
|
||||
record('Mission bundle exports handoff', bundle.ok && bundle.data.schema_version === 't3mp3st.mission_bundle/v1', bundle.data.handoff?.humanSummary || summarizeError(bundle));
|
||||
record('Mission bundle carries repro packs', bundle.ok && bundle.data.reproPacks?.schema_version === 't3mp3st.repro_packs/v1' && (bundle.data.reproPacks.summary?.total || 0) >= 2, `${bundle.data.reproPacks?.summary?.ready || 0}/${bundle.data.reproPacks?.summary?.total || 0} ready`);
|
||||
record('Mission bundle carries pressure paths', bundle.ok && bundle.data.pressurePaths?.schema_version === 't3mp3st.pressure_paths/v1' && (bundle.data.pressurePaths.summary?.total || 0) >= 2, `${bundle.data.pressurePaths?.summary?.armed || 0}/${bundle.data.pressurePaths?.summary?.total || 0} armed`);
|
||||
record('Mission bundle exports handoff', bundle.ok && bundle.data.schema_version === 't3mp3st_mission_bundle/v1', bundle.data.handoff?.humanSummary || summarizeError(bundle));
|
||||
record('Mission bundle carries repro packs', bundle.ok && bundle.data.reproPacks?.schema_version === 't3mp3st_repro_packs/v1' && (bundle.data.reproPacks.summary?.total || 0) >= 2, `${bundle.data.reproPacks?.summary?.ready || 0}/${bundle.data.reproPacks?.summary?.total || 0} ready`);
|
||||
record('Mission bundle carries pressure paths', bundle.ok && bundle.data.pressurePaths?.schema_version === 't3mp3st_pressure_paths/v1' && (bundle.data.pressurePaths.summary?.total || 0) >= 2, `${bundle.data.pressurePaths?.summary?.armed || 0}/${bundle.data.pressurePaths?.summary?.total || 0} armed`);
|
||||
|
||||
const bundleByMission = await get(`/api/mission-bundles/${encodeURIComponent(draft.data.id)}`);
|
||||
record('Mission bundle can be rebuilt by mission id', bundleByMission.ok && bundleByMission.data.schema_version === 't3mp3st.mission_bundle/v1', bundleByMission.data.handoff?.humanSummary || summarizeError(bundleByMission));
|
||||
record('Mission bundle can be rebuilt by mission id', bundleByMission.ok && bundleByMission.data.schema_version === 't3mp3st_mission_bundle/v1', bundleByMission.data.handoff?.humanSummary || summarizeError(bundleByMission));
|
||||
|
||||
const gate = await post('/api/mission-gate', { operationDraft });
|
||||
record('Mission gate evaluates ledgers and receipts', gate.ok && gate.data.schema_version === 't3mp3st.mission_gate/v1' && ['hold', 'ready'].includes(gate.data.status), `${gate.data.status || 'unknown'} ${gate.data.score ?? 'n/a'}/100`);
|
||||
record('Mission gate evaluates ledgers and receipts', gate.ok && gate.data.schema_version === 't3mp3st_mission_gate/v1' && ['hold', 'ready'].includes(gate.data.status), `${gate.data.status || 'unknown'} ${gate.data.score ?? 'n/a'}/100`);
|
||||
|
||||
const tempDraft = await post('/api/mission-drafts', {
|
||||
title: 'Arsenal Smoke Delete Fixture',
|
||||
@@ -561,7 +561,7 @@ async function smokeImprovementAndLearning(context) {
|
||||
record('Repeated learning review reinforces, not duplicates', repeated.status === 201 && beforeCount === afterCount && reinforced, `${afterCount} proposals / reinforced=${reinforced}`);
|
||||
|
||||
const capsule = await get('/api/memory/capsule');
|
||||
record('Memory capsule exposes accepted entries only', capsule.ok && capsule.data.schema_version === 't3mp3st.memory_capsule/v1' && (capsule.data.entries || []).some(item => item.id === acceptedMemory.data.entry?.id), `${countItems(capsule.data.entries)} entries`);
|
||||
record('Memory capsule exposes accepted entries only', capsule.ok && capsule.data.schema_version === 't3mp3st_memory_capsule/v1' && (capsule.data.entries || []).some(item => item.id === acceptedMemory.data.entry?.id), `${countItems(capsule.data.entries)} entries`);
|
||||
}
|
||||
|
||||
// NOTE: The "Pliny Specials" backend routes (/api/pliny/leviathan/engage,
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
// classes.mjs — the SPECIALIST CLASS REGISTRY: the single source of truth for the swarm's
|
||||
// specialists. Each class is a CoD-style loadout: a tuned focus prompt + optional curated
|
||||
// toolkit + default reasoning-effort + the target families it owns. A run "squad" is a
|
||||
// composition of classes (which x how many x effort), built MANUALLY (--squad) or by the
|
||||
// Admiral (composeSquad). These are the HARDENED focus strings that took CVE-Zero 6->9 at full
|
||||
// power; the shared anti-fabrication DOCTRINE lives in cli-swarm's specialistPrompt.
|
||||
//
|
||||
// Honesty note: composeSquad routes on CWE family via the SAME CWE_FAMILY map the scorer uses
|
||||
// (imported, never duplicated) so a composed squad stays comparable to every other number.
|
||||
import { fam } from './cve-zero-hunt.mjs';
|
||||
|
||||
// ── the classes ─────────────────────────────────────────────────────────────
|
||||
// solo:true marks the generalist control (one agent, all classes) — excluded from the roster.
|
||||
export const CLASSES = {
|
||||
injection: { id: 'injection', name: 'Injection', targetFamilies: ['inject', 'cmdi', 'sqli'],
|
||||
focus: 'command / code / eval / template / SQL injection — any untrusted input reaching an interpreter, shell, child_process, eval/Function, or query.' },
|
||||
'path-traversal': { id: 'path-traversal', name: 'Path traversal', targetFamilies: ['path'],
|
||||
focus: 'arbitrary file read/write/include via user-influenced paths — sendFile/static, fs.* sinks, require(), archive extraction, and the classic startsWith/normalize prefix bugs.' },
|
||||
'prototype-pollution': { id: 'prototype-pollution', name: 'Prototype pollution', targetFamilies: ['inject', 'deser'],
|
||||
focus: 'recursive merge/extend/clone and query/body/cookie/header parsing that writes attacker keys (__proto__/constructor/prototype), and the gadget that turns it into impact.' },
|
||||
'redos-dos': { id: 'redos-dos', name: 'ReDoS / resource DoS', targetFamilies: ['dos'],
|
||||
focus: 'catastrophic-backtracking regex on untrusted input; unbounded loops, recursion, allocations, or quadratic parsing reachable from a request.' },
|
||||
'ssrf-redirect': { id: 'ssrf-redirect', name: 'SSRF / open-redirect', targetFamilies: ['ssrf'],
|
||||
focus: 'ENUMERATE every outbound request (http.NewRequest / client.Do / net.Dial / url.Parse / *.get / fetch) AND every struct/DTO/form field named url/host/webhook/target/callback; for each, answer: is the host attacker-controlled, and does the response OR error return to the caller? Flag io.ReadAll(resp.Body) or a response body reflected to the caller as full-read SSRF / info-disclosure. Also open-redirect + host-header trust + parser-differential confusion.' },
|
||||
'authz-bypass': { id: 'authz-bypass', name: 'Authz / access-control', targetFamilies: ['authz', 'csrf', 'info'],
|
||||
focus: 'Work ENUMERATIVELY: build the route/endpoint inventory and, per endpoint, determine which principal reaches it (anonymous / guest / public-share / authenticated / which role). IMMEDIATELY flag any AllowAny / empty permission_classes / authentication removed on a network-reachable view AT ITS OWN LINE as a CWE-306 candidate. Verify every permission/role/identity write is guarded BEFORE it runs, and that sentinel-identity branches (== "anonymous"/"guest"/"admin", isAnonymous, RoleMember) cover ALL sensitive assignments in the function, not just the one they wrap. A docstring or comment that merely SAYS a token is required is NOT a check.' },
|
||||
deserialization: { id: 'deserialization', name: 'Deserialization / parse', targetFamilies: ['deser'],
|
||||
focus: 'cookie / JSON / multipart / content-type / config parsing, unsafe deserialization, type confusion, and length/bounds handling on parsed data (including duplicate-parameter / interpretation-conflict smuggling).' },
|
||||
'crypto-secrets': { id: 'crypto-secrets', name: 'Crypto / secrets', targetFamilies: ['tls'],
|
||||
focus: 'weak or predictable randomness for security tokens, non-constant-time comparison of secrets, signature-verification flaws, hardcoded or leaked credentials (and credential fields omitted from config redaction).' },
|
||||
'memory-safety': { id: 'memory-safety', name: 'Memory safety', targetFamilies: ['mem'],
|
||||
focus: 'OOB read/write, heap/stack buffer overflow, integer overflow leading to undersized allocation, use-after-free, unchecked length/offset/count/size fields when parsing UNTRUSTED binary input; trace attacker-controlled record sizes/offsets to memcpy/resize/indexing sinks.' },
|
||||
'supply-chain': { id: 'supply-chain', name: 'Supply-chain / privileged-action', targetFamilies: ['supply'],
|
||||
focus: 'a privileged action performed WITHOUT a confirmation prompt, allowlist, or explicit opt-in gate: install / download / dynamic-import / require-by-caller-supplied-name of a package, spawn, or auto-update (CWE-829-style arbitrary inclusion). Flag the ABSENCE of the confirmation even when installing "the package the user asked for" looks like intended behavior — name the exact decision site.' },
|
||||
// solo control — one generalist, all classes, one pass. Not part of the specialist roster.
|
||||
generalist: { id: 'generalist', name: 'Generalist', solo: true, targetFamilies: [],
|
||||
focus: 'ALL vulnerability classes in a single pass: injection (command/code/eval/template/SQL), path traversal / arbitrary file access, prototype pollution, ReDoS / resource DoS, SSRF / open-redirect, authz / access-control (enumerate endpoints + which principal reaches each; flag AllowAny / missing-auth at its own line; a docstring is not a check), unsafe deserialization / parsing / type confusion, crypto / secrets, memory safety, and supply-chain / privileged-action-without-confirmation. Map the whole shipped attack surface and report every exploitable bug you can substantiate.' },
|
||||
};
|
||||
|
||||
// back-compat: some consumers (cli-swarm, pack/types) read `lens.key` — alias it to `id`.
|
||||
for (const _c of Object.values(CLASSES)) _c.key = _c.id;
|
||||
|
||||
// ── accessors (LENSES/GENERALIST kept for back-compat with cli-swarm + pack/types.ts) ──
|
||||
export const classList = () => Object.values(CLASSES).filter((c) => !c.solo);
|
||||
export const getClass = (id) => CLASSES[id];
|
||||
export const LENSES = classList(); // the 10 specialists
|
||||
export const GENERALIST = CLASSES.generalist;
|
||||
|
||||
// ── squad presets: a squad is [{ class:id, count, effort? }] ──
|
||||
export const SQUAD_PRESETS = {
|
||||
full: classList().map((c) => ({ class: c.id, count: 1 })),
|
||||
// a lean triage squad weighted to the classes that carry the most CVE-Zero families
|
||||
'cwe-triage': [
|
||||
{ class: 'authz-bypass', count: 2 }, { class: 'ssrf-redirect', count: 2 },
|
||||
{ class: 'injection', count: 1 }, { class: 'path-traversal', count: 1 },
|
||||
{ class: 'deserialization', count: 1 }, { class: 'memory-safety', count: 1 },
|
||||
],
|
||||
};
|
||||
|
||||
// expand a squad into a flat per-agent run list; per-agent effort overrides class default overrides codex default
|
||||
export function expandSquad(squad) {
|
||||
const out = [];
|
||||
for (const slot of (squad || [])) {
|
||||
const cls = getClass(slot.class);
|
||||
if (!cls || cls.solo) continue;
|
||||
const n = Math.max(1, slot.count || 1);
|
||||
for (let i = 0; i < n; i++) out.push({ class: cls, agentIndex: i, effort: slot.effort ?? cls.defaultEffort });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// parse a --squad arg: a preset name, or inline JSON [{class,count,effort}], else the full preset
|
||||
export function parseSquadArg(str) {
|
||||
if (!str || str === 'true') return SQUAD_PRESETS.full;
|
||||
if (SQUAD_PRESETS[str]) return SQUAD_PRESETS[str];
|
||||
try { const j = JSON.parse(str); if (Array.isArray(j)) return j; } catch {}
|
||||
return SQUAD_PRESETS.full;
|
||||
}
|
||||
|
||||
// ── the Admiral's rule-based auto-composer: target -> squad ──
|
||||
// Matching classes get 2 agents; a small FIXED breadth budget of off-target classes get 1 each
|
||||
// (so a composed squad can be CHEAPER-and-competitive vs full — otherwise ranking learns nothing).
|
||||
// Unknown/unmapped target -> full preset (documented fallback; e.g. CVE families with no class).
|
||||
const BREADTH_BUDGET = 3;
|
||||
export function composeSquad(target) {
|
||||
const cwes = (target && target.cwes) || [];
|
||||
const families = [...new Set(cwes.map((c) => fam(c)))];
|
||||
const roster = classList();
|
||||
const matching = roster.filter((c) => (c.targetFamilies || []).some((f) => families.includes(f)));
|
||||
if (!matching.length) return { squad: SQUAD_PRESETS.full, reason: 'unknown-target -> full fallback', families };
|
||||
const off = roster.filter((c) => !matching.includes(c)).slice(0, BREADTH_BUDGET);
|
||||
const squad = [
|
||||
...matching.map((c) => ({ class: c.id, count: 2 })),
|
||||
...off.map((c) => ({ class: c.id, count: 1 })),
|
||||
];
|
||||
return { squad, reason: `matched ${matching.map((c) => c.id).join('+')} on ${families.join(',')} (+${off.length} breadth)`, families };
|
||||
}
|
||||
|
||||
// ── offline self-test ──
|
||||
export function selfTest() {
|
||||
let pass = 0, fail = 0;
|
||||
const ok = (l, c) => (c ? (pass++, console.log(` ✅ ${l}`)) : (fail++, console.log(` ❌ ${l}`)));
|
||||
ok('10 specialist classes (roster excludes solo)', classList().length === 10);
|
||||
ok('every class has id + focus + targetFamilies', classList().every((c) => c.id && c.focus && Array.isArray(c.targetFamilies)));
|
||||
ok('generalist is solo + not in roster', getClass('generalist').solo === true && !classList().some((c) => c.id === 'generalist'));
|
||||
ok('LENSES back-compat = the 10 specialists', LENSES.length === 10 && LENSES[0].key === LENSES[0].id && !!LENSES[0].id);
|
||||
ok('supply-chain class present (recovers CWE-829)', !!getClass('supply-chain') && getClass('supply-chain').targetFamilies.includes('supply'));
|
||||
ok('presets expand to a run list', expandSquad(SQUAD_PRESETS.full).length === 10 && expandSquad(SQUAD_PRESETS['cwe-triage']).length === 8);
|
||||
ok('parseSquadArg: preset name', parseSquadArg('cwe-triage') === SQUAD_PRESETS['cwe-triage']);
|
||||
ok('parseSquadArg: inline JSON', parseSquadArg('[{"class":"ssrf-redirect","count":3}]')[0].class === 'ssrf-redirect');
|
||||
ok('expandSquad honors count + effort override', (() => { const e = expandSquad([{ class: 'authz-bypass', count: 2, effort: 'xhigh' }]); return e.length === 2 && e[0].effort === 'xhigh' && e[0].class.id === 'authz-bypass'; })());
|
||||
const ssrf = composeSquad({ cwes: ['CWE-918'] });
|
||||
ok('composeSquad: SSRF target -> ssrf-redirect x2 + breadth, cheaper than full', ssrf.squad.find((s) => s.class === 'ssrf-redirect')?.count === 2 && expandSquad(ssrf.squad).length < 10);
|
||||
const authz = composeSquad({ cwes: ['CWE-306', 'CWE-918'] });
|
||||
ok('composeSquad: authz+ssrf target matches both classes', authz.squad.some((s) => s.class === 'authz-bypass') && authz.squad.some((s) => s.class === 'ssrf-redirect'));
|
||||
const supply = composeSquad({ cwes: ['CWE-829'] });
|
||||
ok('composeSquad: CWE-829 -> supply-chain class', supply.squad.find((s) => s.class === 'supply-chain')?.count === 2);
|
||||
const info = composeSquad({ cwes: ['CWE-200'] });
|
||||
ok('composeSquad: CWE-200 (info) -> authz class', info.squad.find((s) => s.class === 'authz-bypass')?.count === 2);
|
||||
const unknown = composeSquad({ cwes: ['CWE-79'] }); // xss: no class -> full fallback
|
||||
ok('composeSquad: unmapped target -> full fallback', unknown.squad === SQUAD_PRESETS.full);
|
||||
console.log(`\n${fail === 0 ? '✅ ALL PASS' : `❌ ${fail} FAILED`} — ${pass}/${pass + fail}\n`);
|
||||
return fail === 0;
|
||||
}
|
||||
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import path from 'node:path';
|
||||
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
||||
if (isMain && process.argv.includes('--self-test')) { process.exitCode = selfTest() ? 0 : 1; }
|
||||
@@ -192,7 +192,7 @@ function main() {
|
||||
|
||||
if (args.verify) {
|
||||
console.log(`\n → adversarial refute pass (refute-finding.mjs)…`);
|
||||
const r = spawnSync('node', ['scripts/refute-finding.mjs', '--finding', path.relative(REPO_ROOT, outPath)], { cwd: REPO_ROOT, stdio: 'inherit' });
|
||||
const r = spawnSync('node', ['scripts/refute-finding.mjs', '--finding', path.relative(REPO_ROOT, outPath), '--repo', repo], { cwd: REPO_ROOT, stdio: 'inherit' });
|
||||
process.exit(r.status === 0 ? 0 : 3); // 0=SURVIVED, 3=REFUTED/needs-review
|
||||
}
|
||||
console.log(`\n next: node scripts/refute-finding.mjs --finding ${path.relative(REPO_ROOT, outPath)} (adversarial reachability)`);
|
||||
|
||||
+81
-52
@@ -33,23 +33,14 @@ import os from 'node:os';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { extractFindings, normalizeFinding, parseArgs } from './cli-hunt.mjs';
|
||||
// The specialist CLASSES registry (HARDENED focus strings + squad machinery) + the solo GENERALIST
|
||||
// control now live in classes.mjs — the single source of truth. LENSES/GENERALIST re-exported there
|
||||
// for back-compat. A run deploys a SQUAD (classes x count x effort): --squad <preset|json>, default full.
|
||||
import { classList, getClass, LENSES, GENERALIST, SQUAD_PRESETS, expandSquad, parseSquadArg, composeSquad } from './classes.mjs';
|
||||
|
||||
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const now = () => new Date().toISOString().slice(11, 19);
|
||||
|
||||
// ── the specialist lenses: a division of labor across vuln classes ──
|
||||
const LENSES = [
|
||||
{ key: 'injection', name: 'Injection', focus: 'command / code / eval / template / SQL injection — any untrusted input reaching an interpreter, shell, child_process, eval/Function, or query.' },
|
||||
{ key: 'path-traversal', name: 'Path traversal', focus: 'arbitrary file read/write/include via user-influenced paths — sendFile/static, fs.* sinks, require(), view/template resolution, archive extraction, the classic startsWith/normalize prefix bugs.' },
|
||||
{ key: 'prototype-pollution', name: 'Prototype pollution', focus: 'recursive merge/extend/clone/defaults and query/body/cookie/header parsing that writes attacker keys (__proto__, constructor, prototype) into objects, and the gadget that turns it into impact.' },
|
||||
{ key: 'redos-dos', name: 'ReDoS / resource DoS', focus: 'regexes with catastrophic backtracking run on untrusted input; unbounded loops, recursion, allocations, or quadratic parsing reachable from a request.' },
|
||||
{ key: 'ssrf-redirect', name: 'SSRF / open-redirect', focus: 'URL / host / Location / proxy / redirect parsing — server-side request forgery, open redirect, host-header trust, and parser-differential confusion.' },
|
||||
{ key: 'authz-bypass', name: 'Authz / auth bypass', focus: 'authentication, session, signature/HMAC verify, CSRF, and access-control checks — and the input that bypasses or skips them (timing, type confusion, missing-check paths).' },
|
||||
{ key: 'deserialization', name: 'Deserialization / parse', focus: 'cookie / JSON / multipart / content-type / config parsing, unsafe deserialization, type confusion, and length/bounds handling on parsed data.' },
|
||||
{ key: 'crypto-secrets', name: 'Crypto / secrets', focus: 'weak or predictable randomness for security tokens, non-constant-time comparison of secrets, signature-verification flaws, and hardcoded or leaked credentials.' },
|
||||
{ key: 'memory-safety', name: 'Memory safety', focus: 'out-of-bounds read/write, heap/stack buffer overflow, integer overflow leading to undersized allocation, use-after-free, unchecked length/offset/count/size fields, and missing bounds checks when parsing UNTRUSTED binary input (C/C++/native/unsafe) — the classic malicious-file -> memory-corruption -> RCE surface. Trace attacker-controlled record sizes/offsets to memcpy/resize/indexing sinks.' },
|
||||
];
|
||||
|
||||
// ── a rate-limited / quota-exhausted / errored backend must NEVER read as "clean" ──
|
||||
function isRateLimited(text) {
|
||||
return /you'?ve hit your usage limit|usage limit|rate.?limit|\b429\b|too many requests|quota exceeded|insufficient_quota/i.test(String(text || ''));
|
||||
@@ -123,8 +114,8 @@ Output a SINGLE JSON object on the LAST line, no prose after:
|
||||
{"clusters":[{"root_cause":"...","members":["file:line",...],"classification":"NOVEL_REAL|BY_DESIGN|KNOWN|LOW_VALUE|DUPLICATE","severity":"...","why":"..."}],"novel_real":N,"summary":"one honest sentence"}`;
|
||||
}
|
||||
|
||||
async function triagePass(repo, project, confirmed, commit) {
|
||||
const r = await runCodexAsync(repo, triagePrompt(project, confirmed, commit), { label: 'triage', timeoutMs: 12 * 60 * 1000 });
|
||||
async function triagePass(repo, project, confirmed, commit, model) {
|
||||
const r = await runCodexAsync(repo, triagePrompt(project, confirmed, commit), { label: 'triage', timeoutMs: 12 * 60 * 1000, model });
|
||||
if (!r.ok) return null;
|
||||
const objs = extractObjects(r.text);
|
||||
return objs.length ? objs[objs.length - 1] : null;
|
||||
@@ -132,11 +123,12 @@ async function triagePass(repo, project, confirmed, commit) {
|
||||
|
||||
// ── async codex runner (parallel-safe: unique out-file per call) ──
|
||||
let _runSeq = 0;
|
||||
function runCodexAsync(repo, prompt, { model, label, timeoutMs = 10 * 60 * 1000 } = {}) {
|
||||
function runCodexAsync(repo, prompt, { model, label, effort, timeoutMs = 10 * 60 * 1000 } = {}) {
|
||||
return new Promise((resolve) => {
|
||||
const outFile = path.join(os.tmpdir(), `cli-swarm-${process.pid}-${++_runSeq}.txt`);
|
||||
const args = ['exec', '--skip-git-repo-check', '-c', 'approval_policy="never"', '--sandbox', 'read-only', '--color', 'never', '--output-last-message', outFile];
|
||||
if (model) args.push('-m', model);
|
||||
if (effort) args.push('-c', `model_reasoning_effort="${effort}"`); // per-agent reasoning effort (class/squad loadout)
|
||||
args.push(prompt);
|
||||
// stdin:'ignore' is critical — an open stdin pipe makes `codex exec` wait on an
|
||||
// EOF that never arrives and hang to the timeout. spawnSync closes stdin for us;
|
||||
@@ -199,7 +191,9 @@ function renderBoard(state) {
|
||||
function selfTest() {
|
||||
let pass = 0, fail = 0;
|
||||
const ok = (l, c) => (c ? (pass++, console.log(` ✅ ${l}`)) : (fail++, console.log(` ❌ ${l}`)));
|
||||
ok('9 specialist lenses defined (incl. memory-safety)', LENSES.length === 9 && LENSES.every((l) => l.key && l.focus) && LENSES.some((l) => l.key === 'memory-safety'));
|
||||
ok('10 specialist classes (incl. memory-safety + supply-chain)', LENSES.length === 10 && LENSES.every((l) => l.key && l.focus) && LENSES.some((l) => l.key === 'memory-safety') && LENSES.some((l) => l.key === 'supply-chain'));
|
||||
ok('solo generalist covers all vuln classes', GENERALIST.key === 'generalist' && /memory safety/i.test(GENERALIST.focus) && /injection/i.test(GENERALIST.focus) && /prototype pollution/i.test(GENERALIST.focus) && /crypto/i.test(GENERALIST.focus));
|
||||
ok('squad machinery: full preset expands to 10 agents, --squad json parses', expandSquad(SQUAD_PRESETS.full).length === 10 && parseSquadArg('[{"class":"authz-bypass","count":2}]')[0].class === 'authz-bypass');
|
||||
ok('specialist prompt carries anti-fab doctrine', /VALID, respected result/.test(specialistPrompt(LENSES[0], '', 1)));
|
||||
ok('specialist prompt demands self-refutation', /REFUTE YOURSELF FIRST/.test(specialistPrompt(LENSES[0], '', 1)));
|
||||
ok('specialist prompt threads the swarm board', specialistPrompt(LENSES[0], 'OPEN LEADS: foo', 2).includes('OPEN LEADS: foo'));
|
||||
@@ -231,32 +225,58 @@ async function main() {
|
||||
if (args['self-test']) return selfTest();
|
||||
|
||||
if (!args.repo || args.repo === 'true') {
|
||||
console.error('usage: node scripts/cli-swarm.mjs --repo <path> [--rounds 3] [--concurrency 4] [--budget-min 240] [--lenses k1,k2] [--commit <sha>] [--model <m>]');
|
||||
console.error('usage: node scripts/cli-swarm.mjs --repo <path> [--squad <preset|json>] [--auto-squad --cwes CWE-x,..] [--effort <lvl>] [--rounds 3] [--concurrency 4] [--budget-min 240] [--commit <sha>] [--model <m>]');
|
||||
process.exit(2);
|
||||
}
|
||||
const repo = path.resolve(args.repo);
|
||||
if (!fs.existsSync(repo)) { console.error(`repo not found: ${repo}`); process.exit(2); }
|
||||
const project = path.basename(repo);
|
||||
const commit = args.commit && args.commit !== 'true' ? args.commit : null;
|
||||
const maxRounds = Math.max(1, parseInt(args.rounds, 10) || 3);
|
||||
const model = (args.model && args.model !== 'true') ? args.model : undefined;
|
||||
const solo = !!args.solo && args.solo !== 'false';
|
||||
// the adversarial skeptic is the swarm's refutation filter. Default ON for the swarm, OFF for
|
||||
// the solo control — but independently toggleable via --skeptic so a bake-off can run a
|
||||
// solo+skeptic control arm that isolates "having a refuter" from the rest of coordination.
|
||||
const useSkeptic = args.skeptic === undefined ? !solo : (args.skeptic !== 'false');
|
||||
const maxRounds = solo ? 1 : Math.max(1, parseInt(args.rounds, 10) || 3);
|
||||
const concurrency = Math.max(1, parseInt(args.concurrency, 10) || 4);
|
||||
const budgetMs = (parseFloat(args['budget-min']) || 240) * 60 * 1000;
|
||||
const lenses = args.lenses && args.lenses !== 'true'
|
||||
? LENSES.filter((l) => String(args.lenses).split(',').map((s) => s.trim()).includes(l.key))
|
||||
: LENSES;
|
||||
// ── resolve the SQUAD (classes x count x effort): manual --squad / Admiral --auto-squad / default full ──
|
||||
const globalEffort = (args.effort && args.effort !== 'true') ? args.effort : undefined; // squad-wide reasoning-effort override
|
||||
let squad, squadReason;
|
||||
if (solo) {
|
||||
squad = [{ class: 'generalist', count: 1, effort: globalEffort }];
|
||||
squadReason = 'solo control';
|
||||
} else if (args['auto-squad'] && args['auto-squad'] !== 'false') {
|
||||
const cwes = (args.cwes && args.cwes !== 'true') ? String(args.cwes).split(',').map((s) => s.trim()) : [];
|
||||
const c = composeSquad({ cwes });
|
||||
squad = c.squad.map((s) => ({ ...s, effort: s.effort ?? globalEffort }));
|
||||
squadReason = `auto-squad (${c.reason})`;
|
||||
} else {
|
||||
squad = parseSquadArg(args.squad).map((s) => ({ ...s, effort: s.effort ?? globalEffort }));
|
||||
squadReason = (args.squad && args.squad !== 'true') ? `--squad ${typeof args.squad === 'string' ? args.squad : 'json'}` : 'full preset';
|
||||
if (args.lenses && args.lenses !== 'true') { // legacy --lenses filters the squad by class id
|
||||
const keep = new Set(String(args.lenses).split(',').map((s) => s.trim()));
|
||||
squad = squad.filter((s) => keep.has(s.class));
|
||||
}
|
||||
}
|
||||
// flat per-agent run list (solo = the generalist control; expandSquad excludes solo classes)
|
||||
const runList = solo ? [{ class: GENERALIST, agentIndex: 0, effort: globalEffort }] : expandSquad(squad);
|
||||
const classNames = [...new Set(runList.map((u) => u.class.key))];
|
||||
const arm = solo ? (useSkeptic ? 'solo+skeptic' : 'solo') : 'swarm';
|
||||
const startedAt = Date.now();
|
||||
const overBudget = () => Date.now() - startedAt > budgetMs;
|
||||
|
||||
console.log(`\n${'═'.repeat(72)}`);
|
||||
console.log(` ⚡ CLI-SWARM — BYO-subscription collaborative zero-day swarm`);
|
||||
console.log(` target: ${repo}${commit ? ` @ ${commit}` : ''}`);
|
||||
console.log(` specialists: ${lenses.length} (${lenses.map((l) => l.key).join(', ')})`);
|
||||
console.log(` plan: up to ${maxRounds} collaborative rounds · ${concurrency} parallel · budget ${(budgetMs / 60000) | 0}min`);
|
||||
console.log(` mode: READ-ONLY · anti-fabrication · adversarially self-refuting · $0 marginal (codex sub)`);
|
||||
console.log(` squad: ${runList.length} agent(s) / ${classNames.length} class(es) [${squadReason}]${globalEffort ? ` · effort=${globalEffort}` : ''}`);
|
||||
console.log(` plan: ${solo ? 'ONE pass (single-agent baseline)' : `up to ${maxRounds} collaborative rounds`} · ${concurrency} parallel · budget ${(budgetMs / 60000) | 0}min`);
|
||||
console.log(` mode: ${arm === 'swarm' ? 'SWARM — specialists + shared board + adversarial refutation' : arm === 'solo+skeptic' ? 'SOLO+SKEPTIC control — one generalist, then the skeptic refutes (isolates the refuter)' : 'SOLO BASELINE — one generalist, one pass, NO refuter (the control arm)'}${model ? ` · model=${model}` : ' · model=codex-default'} · READ-ONLY · $0 marginal (codex sub)`);
|
||||
console.log(`${'═'.repeat(72)}\n`);
|
||||
|
||||
const state = { confirmed: [], leads: [], refuted: [], cleanLenses: [] };
|
||||
const transcript = { project, commit, started: new Date().toISOString(), rounds: [] };
|
||||
const transcript = { project, commit, mode: solo ? 'solo-baseline' : 'swarm', arm, skeptic: useSkeptic, model: model || 'codex-default', squad, squadReason, runList: runList.map((u) => ({ class: u.class.key, effort: u.effort || null })), started: new Date().toISOString(), rounds: [] };
|
||||
let dryRounds = 0;
|
||||
let backendCalls = 0, backendFails = 0, rateLimited = false; // distinguish a real clean from a degraded/rate-limited non-run
|
||||
|
||||
@@ -267,9 +287,10 @@ async function main() {
|
||||
const roundRec = { round, started: new Date().toISOString(), specialists: [], newConfirmed: [], newLeads: [], refuted: [], outOfScope: 0 };
|
||||
|
||||
// fan out the specialists in parallel
|
||||
const runs = await pool(lenses, concurrency, async (lens) => {
|
||||
const runs = await pool(runList, concurrency, async (unit) => {
|
||||
const lens = unit.class;
|
||||
const t0 = Date.now();
|
||||
const r = await runCodexAsync(repo, specialistPrompt(lens, board, round, commit), { label: lens.key });
|
||||
const r = await runCodexAsync(repo, specialistPrompt(lens, board, round, commit), { label: lens.key, model, effort: unit.effort });
|
||||
const secs = ((Date.now() - t0) / 1000).toFixed(0);
|
||||
backendCalls++;
|
||||
if (r.rateLimited) rateLimited = true;
|
||||
@@ -296,28 +317,36 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
// skeptic refutation for the high-confidence / confirmed fresh items (parallel)
|
||||
// split: confident findings get scored; leads are held (NEVER scored) — this split is
|
||||
// IDENTICAL across all three arms, so the ONLY thing that differs between solo (skeptic off)
|
||||
// and solo+skeptic (skeptic on) is whether the refuter runs. That isolates the refuter as a
|
||||
// clean variable, per the design review.
|
||||
const toVet = fresh.filter((f) => f.status === 'confirmed' || f.confidence === 'high');
|
||||
const leadsOnly = fresh.filter((f) => !(f.status === 'confirmed' || f.confidence === 'high'));
|
||||
if (toVet.length) console.log(` ⚔ skeptic refuting ${toVet.length} candidate finding(s)…`);
|
||||
const vetted = await pool(toVet, concurrency, async (f) => {
|
||||
const r = await runCodexAsync(repo, skepticPrompt(f, commit), { label: `skeptic:${f.lens}` });
|
||||
let verdict = { verdict: 'SURVIVES', reason: 'skeptic produced no parseable verdict (kept for human review)', guard_found: null };
|
||||
if (r.ok) { const objs = extractObjects(r.text); if (objs.length) verdict = objs[objs.length - 1]; }
|
||||
return { f, verdict };
|
||||
});
|
||||
|
||||
for (const { f, verdict } of vetted) {
|
||||
if (String(verdict.verdict).toUpperCase() === 'REFUTED') {
|
||||
f.reason = verdict.reason; f.guard_found = verdict.guard_found;
|
||||
state.refuted.push(f); roundRec.refuted.push(f);
|
||||
console.log(` ✗ REFUTED ${f.cwe} ${f.file}:${f.line ?? '?'} — ${verdict.guard_found || verdict.reason?.slice(0, 60) || ''}`);
|
||||
} else {
|
||||
state.confirmed.push(f); roundRec.newConfirmed.push(f);
|
||||
console.log(` ✓ SURVIVES ${f.cwe} ${f.file}:${f.line ?? '?'} — ${f.title}`);
|
||||
if (useSkeptic) {
|
||||
if (toVet.length) console.log(` ⚔ skeptic refuting ${toVet.length} candidate finding(s)…`);
|
||||
const vetted = await pool(toVet, concurrency, async (f) => {
|
||||
const r = await runCodexAsync(repo, skepticPrompt(f, commit), { label: `skeptic:${f.lens}`, model });
|
||||
let verdict = { verdict: 'SURVIVES', reason: 'skeptic produced no parseable verdict (kept for human review)', guard_found: null };
|
||||
if (r.ok) { const objs = extractObjects(r.text); if (objs.length) verdict = objs[objs.length - 1]; }
|
||||
return { f, verdict };
|
||||
});
|
||||
for (const { f, verdict } of vetted) {
|
||||
if (String(verdict.verdict).toUpperCase() === 'REFUTED') {
|
||||
f.reason = verdict.reason; f.guard_found = verdict.guard_found;
|
||||
state.refuted.push(f); roundRec.refuted.push(f);
|
||||
console.log(` ✗ REFUTED ${f.cwe} ${f.file}:${f.line ?? '?'} — ${verdict.guard_found || verdict.reason?.slice(0, 60) || ''}`);
|
||||
} else {
|
||||
state.confirmed.push(f); roundRec.newConfirmed.push(f);
|
||||
console.log(` ✓ SURVIVES ${f.cwe} ${f.file}:${f.line ?? '?'} — ${f.title}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// no-refuter arm (the solo control): the confident findings ARE the output, unfiltered.
|
||||
for (const f of toVet) { state.confirmed.push(f); roundRec.newConfirmed.push(f); }
|
||||
console.log(` ${toVet.length} confident finding(s) kept (no refuter — ${arm} arm)`);
|
||||
}
|
||||
// merge leads (dedupe vs existing leads too)
|
||||
// merge leads (dedupe vs existing leads too) — identical across arms; leads are NEVER scored
|
||||
for (const f of leadsOnly) { if (!titleSeen(state.leads, f)) { state.leads.push(f); roundRec.newLeads.push(f); } }
|
||||
// promote-resolved leads: drop leads that a confirmed/refuted now covers
|
||||
state.leads = state.leads.filter((l) => !titleSeen(state.confirmed, l) && !titleSeen(state.refuted, l));
|
||||
@@ -333,7 +362,7 @@ async function main() {
|
||||
let triage = null;
|
||||
if (state.confirmed.length && args.triage !== 'false') {
|
||||
console.log(`\n──────── TRIAGE (${now()}) — clustering ${state.confirmed.length} survivors by root cause + classifying ────────`);
|
||||
triage = await triagePass(repo, project, state.confirmed, commit);
|
||||
triage = await triagePass(repo, project, state.confirmed, commit, model);
|
||||
if (triage) {
|
||||
transcript.triage = triage;
|
||||
for (const c of (triage.clusters || [])) console.log(` [${c.classification}/${c.severity}] ${c.root_cause} (${(c.members || []).length} site(s)) — ${c.why}`);
|
||||
@@ -362,14 +391,14 @@ async function main() {
|
||||
fs.writeFileSync(outPath, JSON.stringify(transcript, null, 2));
|
||||
|
||||
console.log(`\n${'═'.repeat(72)}`);
|
||||
console.log(` 🐝 SWARM COMPLETE — ${transcript.elapsed_min}min · ${transcript.rounds.length} round(s)`);
|
||||
console.log(` ${arm === 'swarm' ? '🐝 SWARM' : `🔬 ${arm.toUpperCase()}`} COMPLETE — ${transcript.elapsed_min}min · ${transcript.rounds.length} round(s)`);
|
||||
console.log(`${'═'.repeat(72)}`);
|
||||
if (degraded) {
|
||||
console.log(` ⛔ INVALID RUN — backend ${rateLimited ? 'RATE-LIMITED (usage limit)' : 'errored'} on ${backendFails}/${backendCalls} specialist calls.`);
|
||||
console.log(` ⛔ INVALID RUN — backend ${rateLimited ? 'RATE-LIMITED (usage limit)' : 'errored'} on ${backendFails}/${backendCalls} call(s).`);
|
||||
console.log(` This is NOT a clean assessment — the hunt did not actually execute. Re-run when the backend is available (e.g. after the quota resets).`);
|
||||
} else if (state.confirmed.length === 0) {
|
||||
console.log(` ✅ CLEAN — ${lenses.length} specialists across ${transcript.rounds.length} round(s) found no surviving exploitable vulnerability.`);
|
||||
console.log(` A deep, adversarially-refuted clean is a STRONG result on a hardened target.`);
|
||||
console.log(` ✅ CLEAN — ${arm === 'swarm' ? `${runList.length} specialist agent(s) across ${transcript.rounds.length} round(s)` : `the ${arm} arm`} found no ${useSkeptic ? 'surviving ' : ''}exploitable vulnerability.`);
|
||||
console.log(` ${arm === 'swarm' ? 'A deep, adversarially-refuted clean is a STRONG result on a hardened target.' : 'Control-arm clean (single generalist pass).'}`);
|
||||
} else if (triage && triage.clusters) {
|
||||
const novel = triage.clusters.filter((c) => c.classification === 'NOVEL_REAL');
|
||||
const downgraded = triage.clusters.filter((c) => c.classification !== 'NOVEL_REAL');
|
||||
@@ -407,4 +436,4 @@ function extractObjects(text) {
|
||||
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
||||
if (isMain) main();
|
||||
|
||||
export { LENSES, specialistPrompt, skepticPrompt, triagePrompt, triagePass, renderBoard, dedupKey, extractObjects, inScope, isRateLimited };
|
||||
export { LENSES, GENERALIST, specialistPrompt, skepticPrompt, triagePrompt, triagePass, renderBoard, dedupKey, extractObjects, inScope, isRateLimited };
|
||||
@@ -0,0 +1,86 @@
|
||||
// config-history.mjs — append-only SCORED LEDGER of {squad config -> benchmark score}.
|
||||
// A thin index over the numbers the harnesses already compute (it RECOMPUTES nothing). Every run
|
||||
// appends a row; rankConfigs() aggregates repeat runs per config (mean strict + pooled Wilson CI +
|
||||
// run-count) so the Admiral / an operator can see which squads win — but ONLY on the split the
|
||||
// caller asks for, so a train-set winner is never mistaken for a proven generalization.
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import crypto from 'node:crypto';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const LEDGER = path.join(ROOT, 'bench', 'cve-zero', 'results', 'config-history.jsonl');
|
||||
|
||||
// stable hash of the squad SHAPE (class+count+effort, order-independent) + model + rounds
|
||||
export function configHash({ squad, model, rounds }) {
|
||||
const norm = [...(squad || [])]
|
||||
.map((s) => ({ class: s.class, count: s.count || 1, effort: s.effort || null }))
|
||||
.sort((a, b) => a.class.localeCompare(b.class));
|
||||
return crypto.createHash('sha1')
|
||||
.update(JSON.stringify({ squad: norm, model: model || 'codex-default', rounds: rounds ?? null }))
|
||||
.digest('hex').slice(0, 12);
|
||||
}
|
||||
|
||||
export function appendConfigRecord(rec) {
|
||||
const row = { ts: new Date().toISOString(), configHash: configHash(rec), ...rec };
|
||||
fs.mkdirSync(path.dirname(LEDGER), { recursive: true });
|
||||
fs.appendFileSync(LEDGER, JSON.stringify(row) + '\n');
|
||||
return row;
|
||||
}
|
||||
|
||||
function readAll() {
|
||||
if (!fs.existsSync(LEDGER)) return [];
|
||||
return fs.readFileSync(LEDGER, 'utf8').split('\n').filter(Boolean)
|
||||
.map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
||||
}
|
||||
|
||||
function wilson(k, n) {
|
||||
if (!n) return [0, 0];
|
||||
const z = 1.96, p = k / n, d = 1 + (z * z) / n;
|
||||
const c = p + (z * z) / (2 * n), m = z * Math.sqrt((p * (1 - p)) / n + (z * z) / (4 * n * n));
|
||||
return [Math.max(0, (c - m) / d), Math.min(1, (c + m) / d)];
|
||||
}
|
||||
|
||||
// rank distinct configs by mean strict (then narrower CI, then fewer calls). Filter by split + family.
|
||||
export function rankConfigs({ split, targetFamily } = {}) {
|
||||
let rows = readAll().filter((r) => r.valid !== false); // invalid/rate-limited non-runs never count toward a config's score
|
||||
if (split) rows = rows.filter((r) => r.split === split);
|
||||
if (targetFamily) rows = rows.filter((r) => r.targetFamily === targetFamily);
|
||||
const byHash = new Map();
|
||||
for (const r of rows) {
|
||||
const g = byHash.get(r.configHash) || { configHash: r.configHash, squad: r.squad, model: r.model, rounds: r.rounds, n: 0, strict: 0, lenient: 0, anyHit: 0, calls: [], precisionCaveat: r.precisionCaveat };
|
||||
g.n++; g.strict += r.strict ? 1 : 0; g.lenient += r.lenient ? 1 : 0; g.anyHit += r.anyHit ? 1 : 0;
|
||||
if (typeof r.calls === 'number') g.calls.push(r.calls);
|
||||
byHash.set(r.configHash, g);
|
||||
}
|
||||
const out = [...byHash.values()].map((g) => {
|
||||
const ci = wilson(g.strict, g.n);
|
||||
return { configHash: g.configHash, squad: g.squad, model: g.model, rounds: g.rounds, n: g.n,
|
||||
meanStrict: g.n ? g.strict / g.n : 0, ci: ci.map((x) => +x.toFixed(3)),
|
||||
meanCalls: g.calls.length ? +(g.calls.reduce((a, b) => a + b, 0) / g.calls.length).toFixed(1) : null,
|
||||
precisionCaveat: g.precisionCaveat };
|
||||
});
|
||||
out.sort((a, b) => (b.meanStrict - a.meanStrict) || ((a.ci[1] - a.ci[0]) - (b.ci[1] - b.ci[0])) || ((a.meanCalls ?? 1e9) - (b.meanCalls ?? 1e9)));
|
||||
return out;
|
||||
}
|
||||
|
||||
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
||||
if (isMain) {
|
||||
const argv = process.argv.slice(2);
|
||||
const gv = (k) => { const i = argv.indexOf('--' + k); return i >= 0 ? argv[i + 1] : undefined; };
|
||||
if (argv.includes('--self-test')) {
|
||||
let pass = 0, fail = 0; const ok = (l, c) => (c ? (pass++, console.log(' ✅ ' + l)) : (fail++, console.log(' ❌ ' + l)));
|
||||
const h1 = configHash({ squad: [{ class: 'a', count: 1 }, { class: 'b', count: 2 }], model: 'm', rounds: 2 });
|
||||
const h2 = configHash({ squad: [{ class: 'b', count: 2 }, { class: 'a', count: 1 }], model: 'm', rounds: 2 });
|
||||
ok('configHash is order-independent', h1 === h2);
|
||||
ok('configHash differs on count change', h1 !== configHash({ squad: [{ class: 'a', count: 1 }, { class: 'b', count: 1 }], model: 'm', rounds: 2 }));
|
||||
ok('configHash differs on model change', h1 !== configHash({ squad: [{ class: 'a', count: 1 }, { class: 'b', count: 2 }], model: 'n', rounds: 2 }));
|
||||
console.log(`\n${fail === 0 ? '✅ ALL PASS' : '❌ ' + fail + ' FAILED'} — ${pass}/${pass + fail}\n`);
|
||||
process.exitCode = fail === 0 ? 0 : 1;
|
||||
} else {
|
||||
const ranked = rankConfigs({ split: gv('split'), targetFamily: gv('family') });
|
||||
console.log(`\nconfig-history ranking${gv('split') ? ` (split=${gv('split')})` : ''}${gv('family') ? ` (family=${gv('family')})` : ''} — ${ranked.length} distinct config(s):\n`);
|
||||
for (const r of ranked) console.log(` ${r.configHash} strict ${(r.meanStrict * 100).toFixed(0)}% (n=${r.n}, CI ${(r.ci[0] * 100).toFixed(0)}-${(r.ci[1] * 100).toFixed(0)}%) calls~${r.meanCalls ?? '?'} [${(r.squad || []).map((s) => s.class + 'x' + (s.count || 1)).join(' ')}]`);
|
||||
console.log(ranked.length ? '' : ' (empty ledger — no runs recorded yet)\n');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env node
|
||||
// CVE-Zero SWARM-vs-SOLO bake-off — a CONFOUND-REDUCED, PRE-REGISTERED PILOT.
|
||||
//
|
||||
// The earlier swarm numbers were confounded (swarm on codex, baseline on OpenRouter, different
|
||||
// models). This runs every arm on the SAME backend (codex), SAME model, SAME deterministic
|
||||
// scorer. To keep "coordination" from being one big bundle, it runs THREE arms so the refuter
|
||||
// is isolated from the rest of the machinery (per the design review):
|
||||
// • solo = cli-swarm --solo : 1 generalist, 1 pass, NO refuter.
|
||||
// • solo+skeptic = cli-swarm --solo --skeptic true : 1 generalist, then the SAME refuter. ← isolates the refuter
|
||||
// • swarm = cli-swarm : 9 specialists × R rounds + board + refuter.
|
||||
// Decomposition: refuter effect = (solo+skeptic − solo) ; coordination effect = (swarm − solo+skeptic) ; total = (swarm − solo).
|
||||
//
|
||||
// HONEST CEILING — this is a PILOT, not a significance test:
|
||||
// • n=10, single run per arm, no repeated runs → the ~1-challenge noise floor is NOT measured.
|
||||
// • Wilson-95 on a 10-sample proportion is ≈ ±30 points; any small Δ is inside noise. Directional only.
|
||||
// • Compute is UNMATCHED: swarm ≈ 20-30 codex calls vs solo = 1. A swarm win is "a 20x-compute swarm
|
||||
// scores X," not "coordination beats a single agent." Per-arm elapsed/calls are recorded so the price is visible.
|
||||
// • The scorer (file+CWE+line, top-3 by severity) is precision-blind: it can't punish solo over-labeling
|
||||
// and can't credit the swarm's precision. Companion metrics (finding count, any-hit recall, rank-of-first-hit) are reported alongside.
|
||||
// • Both arms run --triage false, so the scored set is skeptic-survivors, NOT the triage-gated shipped pipeline.
|
||||
//
|
||||
// node scripts/cve-zero-bakeoff.mjs --model gpt-5.5 # all 10 samples, 3 arms
|
||||
// node scripts/cve-zero-bakeoff.mjs --model gpt-5.5 --rounds 2 GHSA-xxx # subset
|
||||
import { loadSamples, scoreSample, scoreFinding } from './cve-zero-hunt.mjs';
|
||||
import { spawn } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const SWARM_OUT = path.join(ROOT, 'bench', 'wild-hunt', 'swarm');
|
||||
const RESULTS = path.join(ROOT, 'bench', 'cve-zero', 'results');
|
||||
const SEV = { critical: 4, high: 3, medium: 2, low: 1, info: 0 };
|
||||
const INVALID = /INVALID_DEGRADED|NO_TRANSCRIPT|PARSE_ERR/;
|
||||
|
||||
const argv = process.argv.slice(2);
|
||||
function optval(name, def) {
|
||||
const i = argv.indexOf('--' + name);
|
||||
if (i >= 0 && argv[i + 1] && !argv[i + 1].startsWith('--')) { const v = argv[i + 1]; argv.splice(i, 2); return v; }
|
||||
const eq = argv.find((a) => a.startsWith(`--${name}=`));
|
||||
if (eq) { argv.splice(argv.indexOf(eq), 1); return eq.split('=').slice(1).join('='); }
|
||||
return def;
|
||||
}
|
||||
const MODEL = optval('model', null);
|
||||
let ROUNDS = parseInt(optval('rounds', '2'), 10);
|
||||
if (!Number.isFinite(ROUNDS) || ROUNDS < 1) { console.error('warning: --rounds clamped to 1'); ROUNDS = 1; }
|
||||
const ids = argv.filter((a) => !a.startsWith('--'));
|
||||
|
||||
// Wilson 95% CI for a binomial proportion — the honest uncertainty at n=10.
|
||||
function wilson(k, n) {
|
||||
if (!n) return [0, 0];
|
||||
const z = 1.96, p = k / n, d = 1 + (z * z) / n;
|
||||
const c = p + (z * z) / (2 * n), m = z * Math.sqrt((p * (1 - p)) / n + (z * z) / (4 * n * n));
|
||||
return [Math.max(0, (c - m) / d), Math.min(1, (c + m) / d)];
|
||||
}
|
||||
const pct = (x) => `${(x * 100).toFixed(0)}%`;
|
||||
|
||||
// Spawn one cli-swarm arm; read its fresh transcript. Snapshot BEFORE each spawn isolates its own output.
|
||||
function runArm(repo, arm) {
|
||||
return new Promise((resolve) => {
|
||||
const before = new Set(fs.existsSync(SWARM_OUT) ? fs.readdirSync(SWARM_OUT) : []);
|
||||
const args = ['scripts/cli-swarm.mjs', '--repo', repo, '--triage', 'false'];
|
||||
if (arm === 'solo') args.push('--solo', 'true');
|
||||
else if (arm === 'solo+skeptic') args.push('--solo', 'true', '--skeptic', 'true');
|
||||
else args.push('--rounds', String(ROUNDS)); // swarm
|
||||
if (MODEL) args.push('--model', MODEL);
|
||||
const child = spawn('node', args, { cwd: ROOT, stdio: ['ignore', 'inherit', 'inherit'] });
|
||||
child.on('exit', () => {
|
||||
const fresh = (fs.existsSync(SWARM_OUT) ? fs.readdirSync(SWARM_OUT) : [])
|
||||
.filter((f) => f.endsWith('.json') && !before.has(f)).sort();
|
||||
if (!fresh.length) return resolve({ confirmed: [], verdict: 'NO_TRANSCRIPT', elapsed: null, calls: null });
|
||||
try {
|
||||
const t = JSON.parse(fs.readFileSync(path.join(SWARM_OUT, fresh[fresh.length - 1]), 'utf8'));
|
||||
const confirmed = (t.rounds || []).flatMap((r) => r.newConfirmed || []);
|
||||
resolve({ confirmed, verdict: t.verdict || 'FINDINGS', model: t.model, elapsed: t.elapsed_min, calls: t.backend?.calls ?? null });
|
||||
} catch (e) { resolve({ confirmed: [], verdict: 'PARSE_ERR:' + e.message, elapsed: null, calls: null }); }
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function scoreArm(res, gt) {
|
||||
const confirmed = res.confirmed || [];
|
||||
const ranked = [...confirmed].sort((a, b) => (SEV[b.severity] || 0) - (SEV[a.severity] || 0));
|
||||
const s = scoreSample(ranked, gt); // strict = top-1 hit, lenient = top-3 hit
|
||||
// severity-INDEPENDENT recall + rank-of-first-hit over ALL confirmed (the precision-blind-scorer companion)
|
||||
let anyHit = false, rankOfFirstHit = null;
|
||||
for (let i = 0; i < ranked.length; i++) { if (scoreFinding(ranked[i], gt).hit) { anyHit = true; rankOfFirstHit = i + 1; break; } }
|
||||
return {
|
||||
valid: !INVALID.test(res.verdict || ''), verdict: res.verdict,
|
||||
strict: !!s.strict, lenient: !!s.lenient, anyHit, rankOfFirstHit,
|
||||
n: confirmed.length, elapsed: res.elapsed, calls: res.calls,
|
||||
};
|
||||
}
|
||||
|
||||
const samples = loadSamples(ids.length ? ids : null);
|
||||
if (!samples.length) { console.error('no samples matched'); process.exit(2); }
|
||||
|
||||
const ARMS = ['solo', 'solo+skeptic', 'swarm'];
|
||||
const agg = {
|
||||
schema: 't3mp3st.bench.cve-zero.bakeoff/v2', model: MODEL || 'codex-default', rounds: ROUNDS, backend: 'codex',
|
||||
framing: {
|
||||
kind: 'pre-registered PILOT — directional, NOT a significance test',
|
||||
confoundsRemoved: ['backend (both codex)', 'model (both same)', 'scorer (identical)'],
|
||||
confoundsRemaining: ['~20x compute unmatched (solo 1 call vs swarm ~20-30)', 'n=10 underpowered (Wilson-95 ≈ ±30pts)', 'single run/arm — noise floor assumed not measured', 'scorer precision-blind (companion metrics reported)', 'scored set is skeptic-survivors, not triage-gated shipped pipeline'],
|
||||
decisionRule: 'a delta counts as real ONLY if it is large and consistent across per-sample pairs; a small Δ is reported as noise-indistinguishable, not as uplift/regression',
|
||||
},
|
||||
attempted: 0, invalid: 0,
|
||||
tally: { solo: { strict: 0, lenient: 0, anyHit: 0 }, 'solo+skeptic': { strict: 0, lenient: 0, anyHit: 0 }, swarm: { strict: 0, lenient: 0, anyHit: 0 } },
|
||||
delta: { refuter_strict: 0, coordination_strict: 0, total_strict: 0 },
|
||||
discordant: { swarm_beats_solo: 0, solo_beats_swarm: 0 },
|
||||
per: [], invalidDetail: [],
|
||||
};
|
||||
|
||||
console.log(`\nCVE-Zero BAKE-OFF (confound-reduced PILOT) — ${samples.length} sample(s) · model=${MODEL || 'codex-default'} · 3 arms on codex · swarm rounds=${ROUNDS}`);
|
||||
console.log(` arms: solo | solo+skeptic (isolates refuter) | swarm · DIRECTIONAL pilot, not a significance test\n`);
|
||||
|
||||
for (const s of samples) {
|
||||
const repo = fs.existsSync(path.join(s.dir, 'source')) ? path.join(s.dir, 'source') : s.dir;
|
||||
console.log(`\n════════ ${s.id} (${s.gt.repo}) — gt ${s.gt.cwes.join(',')}@${s.gt.vuln_files.map((f) => f.split('/').pop()).join(',')} ════════`);
|
||||
const scored = {};
|
||||
for (const arm of ARMS) {
|
||||
console.log(` ▸ ${arm} arm…`);
|
||||
scored[arm] = scoreArm(await runArm(repo, arm), s.gt);
|
||||
}
|
||||
const anyInvalid = ARMS.some((a) => !scored[a].valid);
|
||||
const rec = { id: s.id, ...Object.fromEntries(ARMS.map((a) => [a, scored[a]])) };
|
||||
agg.per.push(rec);
|
||||
if (anyInvalid) {
|
||||
agg.invalid++;
|
||||
agg.invalidDetail.push({ id: s.id, arms: Object.fromEntries(ARMS.filter((a) => !scored[a].valid).map((a) => [a, scored[a].verdict])) });
|
||||
console.log(` ⚠ EXCLUDED from tally — invalid arm(s): ${ARMS.filter((a) => !scored[a].valid).map((a) => `${a}=${scored[a].verdict}`).join(', ')}`);
|
||||
continue;
|
||||
}
|
||||
agg.attempted++;
|
||||
for (const a of ARMS) { for (const k of ['strict', 'lenient', 'anyHit']) agg.tally[a][k] += scored[a][k] ? 1 : 0; }
|
||||
const d = (scored.swarm.strict ? 1 : 0) - (scored.solo.strict ? 1 : 0);
|
||||
if (d > 0) agg.discordant.swarm_beats_solo++; else if (d < 0) agg.discordant.solo_beats_swarm++;
|
||||
console.log(` → strict: solo=${+scored.solo.strict} solo+skeptic=${+scored['solo+skeptic'].strict} swarm=${+scored.swarm.strict}` +
|
||||
` | anyHit: ${+scored.solo.anyHit}/${+scored['solo+skeptic'].anyHit}/${+scored.swarm.anyHit}` +
|
||||
` | found: ${scored.solo.n}/${scored['solo+skeptic'].n}/${scored.swarm.n}` +
|
||||
` | calls: ${scored.solo.calls ?? '?'}/${scored['solo+skeptic'].calls ?? '?'}/${scored.swarm.calls ?? '?'}`);
|
||||
}
|
||||
|
||||
const N = agg.attempted;
|
||||
agg.delta.refuter_strict = agg.tally['solo+skeptic'].strict - agg.tally.solo.strict;
|
||||
agg.delta.coordination_strict = agg.tally.swarm.strict - agg.tally['solo+skeptic'].strict;
|
||||
agg.delta.total_strict = agg.tally.swarm.strict - agg.tally.solo.strict;
|
||||
agg.ci = Object.fromEntries(ARMS.map((a) => [a, wilson(agg.tally[a].strict, N).map((x) => +x.toFixed(3))]));
|
||||
|
||||
fs.mkdirSync(RESULTS, { recursive: true });
|
||||
const out = path.join(RESULTS, `bakeoff-${MODEL ? MODEL.replace(/[^a-z0-9]+/gi, '-') + '-' : ''}${Date.now()}.json`);
|
||||
fs.writeFileSync(out, JSON.stringify(agg, null, 2));
|
||||
|
||||
console.log(`\n${'═'.repeat(70)}`);
|
||||
console.log(` BAKE-OFF PILOT (${agg.model}, n=${N} scored${agg.invalid ? `, ${agg.invalid} excluded-invalid` : ''}, both/all arms on codex, swarm rounds=${ROUNDS}):`);
|
||||
for (const a of ARMS) {
|
||||
const t = agg.tally[a], ci = wilson(t.strict, N);
|
||||
console.log(` ${a.padEnd(13)} strict ${t.strict}/${N} (${pct(t.strict / (N || 1))}, 95% CI ${pct(ci[0])}–${pct(ci[1])}) lenient ${t.lenient}/${N} anyHit ${t.anyHit}/${N}`);
|
||||
}
|
||||
console.log(` ── Δstrict: refuter (solo+skeptic−solo) = ${agg.delta.refuter_strict >= 0 ? '+' : ''}${agg.delta.refuter_strict}` +
|
||||
` · coordination (swarm−solo+skeptic) = ${agg.delta.coordination_strict >= 0 ? '+' : ''}${agg.delta.coordination_strict}` +
|
||||
` · total (swarm−solo) = ${agg.delta.total_strict >= 0 ? '+' : ''}${agg.delta.total_strict}`);
|
||||
console.log(` ── paired swarm vs solo: ${agg.discordant.swarm_beats_solo} swarm-win / ${agg.discordant.solo_beats_swarm} solo-win (discordant pairs)`);
|
||||
console.log(` DIRECTIONAL PILOT — n=${N} is underpowered (CI ≈ ±30pts) and compute is unmatched; a small Δ is inside noise, not uplift.`);
|
||||
console.log(`${'═'.repeat(70)}`);
|
||||
console.log(`report: ${path.relative(ROOT, out)}\n`);
|
||||
@@ -281,7 +281,7 @@ export function parseFindings(text) {
|
||||
}
|
||||
|
||||
// ---- scoring against real patch ------------------------------------------
|
||||
const CWE_FAMILY = {
|
||||
export const CWE_FAMILY = {
|
||||
'CWE-22': 'path', 'CWE-23': 'path', 'CWE-36': 'path', 'CWE-35': 'path',
|
||||
'CWE-918': 'ssrf', 'CWE-441': 'ssrf',
|
||||
'CWE-78': 'cmdi', 'CWE-77': 'cmdi',
|
||||
@@ -290,16 +290,17 @@ const CWE_FAMILY = {
|
||||
'CWE-79': 'xss',
|
||||
'CWE-862': 'authz', 'CWE-863': 'authz', 'CWE-285': 'authz', 'CWE-639': 'authz', 'CWE-306': 'authz', 'CWE-287': 'authz',
|
||||
'CWE-200': 'info', 'CWE-209': 'info', 'CWE-538': 'info',
|
||||
'CWE-502': 'deser',
|
||||
'CWE-502': 'deser', 'CWE-436': 'deser',
|
||||
'CWE-829': 'supply',
|
||||
'CWE-611': 'xxe',
|
||||
'CWE-835': 'dos', 'CWE-400': 'dos', 'CWE-770': 'dos',
|
||||
'CWE-787': 'mem', 'CWE-125': 'mem', 'CWE-119': 'mem', 'CWE-122': 'mem', 'CWE-121': 'mem',
|
||||
'CWE-352': 'csrf',
|
||||
'CWE-295': 'tls', 'CWE-322': 'tls',
|
||||
};
|
||||
function fam(cwe) { return CWE_FAMILY[(cwe||'').toUpperCase().trim()] || (cwe||'').toUpperCase().trim(); }
|
||||
export function fam(cwe) { return CWE_FAMILY[(cwe||'').toUpperCase().trim()] || (cwe||'').toUpperCase().trim(); }
|
||||
|
||||
function scoreFinding(f, gt) {
|
||||
export function scoreFinding(f, gt) {
|
||||
const rptFile = (f.file || '').replace(/^\.?\//, '');
|
||||
const rptBase = path.basename(rptFile);
|
||||
// file_match: basename matches any vuln file (path prefixes vary by how agent refers to it)
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// cve-zero-split.mjs — the HONESTY RAIL: a deterministic, family-balanced train/test/holdout split.
|
||||
//
|
||||
// The recommender (composeSquad tuning + rankConfigs learning) may read ONLY the TRAIN split, and
|
||||
// any "config X beats config Y" comparison is reported on TEST. A generalization claim ("our tuned
|
||||
// squads win in general") may use ONLY the HOLDOUT split — fresh, never-seen CVEs (CVE-Zero-v2),
|
||||
// which is intentionally EMPTY until curated. This is what stops us from overfitting the eval and
|
||||
// then quoting the overfit number. The three families shared across the corpus (path / ssrf / authz)
|
||||
// each land >=1 in BOTH train and test so neither half is blind to them.
|
||||
export const SPLIT = {
|
||||
train: ['GHSA-hx53-77qj-8663', 'GHSA-fqvv-jvhr-g5jc', 'GHSA-3jmg-p96m-m328', 'GHSA-hm2w-vr2p-hq7w', 'GHSA-m6xr-fvfg-5g64'],
|
||||
test: ['GHSA-ph9p-34f9-6g65', 'GHSA-w4g9-mxgg-j532', 'GHSA-rg3m-cfq7-g6h6', 'GHSA-vv9j-gjw2-j8wp', 'GHSA-36hh-x5p5-jgc8'],
|
||||
// CVE-Zero-v2 — fresh 2026 CVEs the hardened prompts have NEVER seen (built 2026-07-04). The ONLY
|
||||
// split a generalization claim may use. Diverse: path/authz (gonic), authz (nezha), path (dulwich),
|
||||
// memory (oj), proto-pollution (adonisjs, piscina), redos (jsonata), ssrf (fluentd).
|
||||
holdout: ['GHSA-2fp4-5v5c-4448', 'GHSA-q6xx-5vr8-p898', 'GHSA-gfhv-vqv2-4544', 'GHSA-3m6q-jj5j-38c9', 'GHSA-qcm7-3vpr-hj5h', 'GHSA-x9g3-xrwr-cwfg', 'GHSA-86vw-mfpg-wwv9', 'GHSA-72f5-rr8c-r6gr', 'GHSA-wc3f-xc32-435f', 'GHSA-97vg-427p-8hx5'],
|
||||
};
|
||||
|
||||
export function splitOf(id) {
|
||||
for (const k of ['train', 'test', 'holdout']) if (SPLIT[k].includes(id)) return k;
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
// map a --split arg (train|test|holdout|all) to a set of ids to keep, or null for 'all' (back-compat)
|
||||
export function splitIds(which) {
|
||||
if (!which || which === 'all' || which === 'true') return null;
|
||||
return SPLIT[which] || null;
|
||||
}
|
||||
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import path from 'node:path';
|
||||
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
||||
if (isMain && process.argv.includes('--self-test')) {
|
||||
const run = async () => {
|
||||
const { loadSamples, fam } = await import('./cve-zero-hunt.mjs');
|
||||
const gt = Object.fromEntries(loadSamples().map((s) => [s.id, s.gt]));
|
||||
const famsIn = (ids) => new Set(ids.flatMap((id) => (gt[id]?.cwes || []).map(fam)));
|
||||
const trainF = famsIn(SPLIT.train), testF = famsIn(SPLIT.test);
|
||||
let pass = 0, fail = 0; const ok = (l, c) => (c ? (pass++, console.log(' ✅ ' + l)) : (fail++, console.log(' ❌ ' + l)));
|
||||
ok('train + test are 5 + 5, disjoint', SPLIT.train.length === 5 && SPLIT.test.length === 5 && !SPLIT.train.some((x) => SPLIT.test.includes(x)));
|
||||
ok('every split id is a real sample', [...SPLIT.train, ...SPLIT.test].every((id) => gt[id]));
|
||||
for (const f of ['path', 'ssrf', 'authz']) ok(`shared family "${f}" appears in BOTH train and test`, trainF.has(f) && testF.has(f));
|
||||
ok('holdout is empty until CVE-Zero-v2 is curated', SPLIT.holdout.length === 0);
|
||||
console.log(`\n${fail === 0 ? '✅ ALL PASS' : '❌ ' + fail + ' FAILED'} — ${pass}/${pass + fail}\n`);
|
||||
process.exitCode = fail === 0 ? 0 : 1;
|
||||
};
|
||||
run();
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env node
|
||||
// CVE-Zero SWARM harness — runs cli-swarm (a SQUAD of specialist CLASSES) on each sample's source,
|
||||
// scores confirmed survivors with the SAME deterministic scoreSample() as the single-agent baseline,
|
||||
// and appends every run to the config-history ledger so squads can be ranked. Honesty rail:
|
||||
// --split train|test|holdout filters the sample set AND stamps the ledger row (the recommender may
|
||||
// only ever learn from train; a generalization claim may use only a populated holdout).
|
||||
//
|
||||
// node scripts/cve-zero-swarm.mjs # all samples, full squad, codex default
|
||||
// node scripts/cve-zero-swarm.mjs --split test --auto-squad # test split, Admiral-composed squad per sample
|
||||
// node scripts/cve-zero-swarm.mjs --squad cwe-triage --effort xhigh --model gpt-5.5
|
||||
import { loadSamples, scoreSample, scoreFinding, fam } from './cve-zero-hunt.mjs';
|
||||
import { composeSquad } from './classes.mjs';
|
||||
import { splitIds } from './cve-zero-split.mjs';
|
||||
import { appendConfigRecord } from './config-history.mjs';
|
||||
import { spawn } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const SWARM_OUT = path.join(ROOT, 'bench', 'wild-hunt', 'swarm');
|
||||
const RESULTS = path.join(ROOT, 'bench', 'cve-zero', 'results');
|
||||
const SEV = { critical: 4, high: 3, medium: 2, low: 1, info: 0 };
|
||||
const INVALID = /INVALID_DEGRADED|NO_TRANSCRIPT|PARSE_ERR/;
|
||||
|
||||
const argv = process.argv.slice(2);
|
||||
function opt(name) {
|
||||
const i = argv.indexOf('--' + name);
|
||||
if (i >= 0 && argv[i + 1] && !argv[i + 1].startsWith('--')) { const v = argv[i + 1]; argv.splice(i, 2); return v; }
|
||||
const eq = argv.find((a) => a.startsWith(`--${name}=`));
|
||||
if (eq) { argv.splice(argv.indexOf(eq), 1); return eq.split('=').slice(1).join('='); }
|
||||
return undefined;
|
||||
}
|
||||
function flag(name) {
|
||||
const i = argv.indexOf('--' + name);
|
||||
if (i < 0) return undefined;
|
||||
const hasVal = argv[i + 1] && !argv[i + 1].startsWith('--');
|
||||
const v = hasVal ? argv[i + 1] : 'true';
|
||||
argv.splice(i, hasVal ? 2 : 1);
|
||||
return v;
|
||||
}
|
||||
const MODEL = opt('model');
|
||||
const SPLIT = opt('split');
|
||||
const SQUAD = opt('squad'); // preset name or inline json (manual)
|
||||
const EFFORT = opt('effort');
|
||||
const ROUNDS = opt('rounds') || '2';
|
||||
const AUTO = flag('auto-squad');
|
||||
const ids = argv.filter((a) => !a.startsWith('--'));
|
||||
|
||||
function runSwarm(repo, squadArg) {
|
||||
return new Promise((resolve) => {
|
||||
const before = new Set(fs.existsSync(SWARM_OUT) ? fs.readdirSync(SWARM_OUT) : []);
|
||||
const args = ['scripts/cli-swarm.mjs', '--repo', repo, '--rounds', String(ROUNDS), '--triage', 'false'];
|
||||
if (squadArg) args.push('--squad', squadArg); // per-sample auto-composed squad
|
||||
else if (SQUAD) args.push('--squad', SQUAD); // fixed manual squad
|
||||
if (EFFORT) args.push('--effort', EFFORT);
|
||||
if (MODEL) args.push('--model', MODEL);
|
||||
const child = spawn('node', args, { cwd: ROOT, stdio: ['ignore', 'inherit', 'inherit'] });
|
||||
child.on('exit', () => {
|
||||
const fresh = (fs.existsSync(SWARM_OUT) ? fs.readdirSync(SWARM_OUT) : []).filter((f) => f.endsWith('.json') && !before.has(f)).sort();
|
||||
if (!fresh.length) return resolve({ confirmed: [], verdict: 'NO_TRANSCRIPT', calls: null });
|
||||
try {
|
||||
const t = JSON.parse(fs.readFileSync(path.join(SWARM_OUT, fresh[fresh.length - 1]), 'utf8'));
|
||||
resolve({ confirmed: (t.rounds || []).flatMap((r) => r.newConfirmed || []), verdict: t.verdict || 'FINDINGS', calls: t.backend?.calls ?? null });
|
||||
} catch (e) { resolve({ confirmed: [], verdict: 'PARSE_ERR:' + e.message, calls: null }); }
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const keepIds = splitIds(SPLIT);
|
||||
const samples = loadSamples(ids.length ? ids : keepIds);
|
||||
if (!samples.length) { console.error('no samples matched'); process.exit(2); }
|
||||
|
||||
const agg = {
|
||||
schema: 't3mp3st.bench.cve-zero.swarm/v2', model: MODEL ? `codex-swarm:${MODEL}` : 'codex-swarm',
|
||||
split: SPLIT || 'all', squad: AUTO ? 'auto-compose' : (SQUAD || 'full'), effort: EFFORT || null,
|
||||
attempted: 0, invalid: 0, strict: 0, lenient: 0, anyHit: 0, per: [],
|
||||
};
|
||||
console.log(`\nCVE-Zero SWARM — ${samples.length} sample(s) · model=${MODEL || 'codex-default'} · split=${SPLIT || 'all'} · squad=${agg.squad}${EFFORT ? ` · effort=${EFFORT}` : ''}\n`);
|
||||
|
||||
for (const s of samples) {
|
||||
const repo = fs.existsSync(path.join(s.dir, 'source')) ? path.join(s.dir, 'source') : s.dir;
|
||||
const composed = AUTO ? composeSquad({ cwes: s.gt.cwes }) : null;
|
||||
console.log(`\n──── ${s.id} (${s.gt.repo})${composed ? ` · squad: ${composed.reason}` : ''} ────`);
|
||||
const { confirmed, verdict, calls } = await runSwarm(repo, composed ? JSON.stringify(composed.squad) : undefined);
|
||||
const ranked = [...confirmed].sort((a, b) => (SEV[b.severity] || 0) - (SEV[a.severity] || 0));
|
||||
const score = scoreSample(ranked, s.gt);
|
||||
const anyHit = ranked.some((f) => scoreFinding(f, s.gt).hit);
|
||||
const valid = !INVALID.test(verdict);
|
||||
// append to the scored config-history ledger — stamped with the split + precision caveat.
|
||||
appendConfigRecord({
|
||||
squad: composed ? composed.squad : (SQUAD ? [{ class: SQUAD }] : undefined), model: agg.model, rounds: +ROUNDS,
|
||||
split: SPLIT || 'all', sampleId: s.id, targetFamily: fam((s.gt.cwes || [])[0]),
|
||||
strict: score.strict, lenient: score.lenient, anyHit, calls, verdict, valid,
|
||||
precisionCaveat: 'skeptic-survivors, not triage-gated',
|
||||
});
|
||||
if (!valid) { agg.invalid++; agg.per.push({ id: s.id, valid: false, verdict }); console.log(` ⚠ INVALID (${verdict}) — excluded from tally`); continue; }
|
||||
agg.attempted++; agg.strict += score.strict ? 1 : 0; agg.lenient += score.lenient ? 1 : 0; agg.anyHit += anyHit ? 1 : 0;
|
||||
agg.per.push({ id: s.id, strict: score.strict, lenient: score.lenient, anyHit, confirmed: confirmed.length, calls, verdict });
|
||||
console.log(` → strict=${score.strict} lenient=${score.lenient} anyHit=${anyHit} (${confirmed.length} survivors, ${calls} calls, ${verdict})`);
|
||||
}
|
||||
|
||||
fs.mkdirSync(RESULTS, { recursive: true });
|
||||
const out = path.join(RESULTS, `swarm-aggregate-${MODEL ? MODEL.replace(/[^a-z0-9]+/gi, '-') + '-' : ''}${Date.now()}.json`);
|
||||
fs.writeFileSync(out, JSON.stringify(agg, null, 2));
|
||||
console.log(`\n══════ SWARM (${agg.model}, split=${agg.split}, squad=${agg.squad}): strict ${agg.strict}/${agg.attempted} · lenient ${agg.lenient}/${agg.attempted} · anyHit ${agg.anyHit}/${agg.attempted}${agg.invalid ? ` · ${agg.invalid} invalid-excluded` : ''} ══════`);
|
||||
console.log(`logged ${samples.length} run(s) to config-history.jsonl · report: ${path.relative(ROOT, out)}`);
|
||||
@@ -45,9 +45,9 @@ function selfTest() {
|
||||
|
||||
// test: innocuous query detection
|
||||
const innocuousQueries = [
|
||||
'Does function ParseStateInfo validate val_len before using it in memcpy?',
|
||||
'What is sizeof(DirectLidarStateInfo) and what are the sizes of each field?',
|
||||
'List all callers of ParseStateInfo. Does any caller check packet length?',
|
||||
'Does function parse_tlv_record validate rec_len before using it in memcpy?',
|
||||
'What is sizeof(TlvRecord) and what are the sizes of each field?',
|
||||
'List all callers of parse_tlv_record. Does any caller check packet length?',
|
||||
'Trace the data flow from recvfrom to memcpy in this code path.',
|
||||
];
|
||||
const offensiveQueries = [
|
||||
|
||||
+2
-2
@@ -120,10 +120,10 @@ async function main() {
|
||||
const preflight = await apiGet('/api/preflight');
|
||||
check('Capability preflight', preflight.ok && typeof preflight.data.score === 'number', `${preflight.data.score ?? 'n/a'}/100`);
|
||||
const arsenal = await apiGet('/api/arsenal/status');
|
||||
check('Arsenal status', arsenal.ok && arsenal.data.schema_version === 't3mp3st.arsenal_status/v1', `${arsenal.data.summary?.installedCommandReady ?? 0}/${arsenal.data.summary?.commandReady ?? 0} installed`);
|
||||
check('Arsenal status', arsenal.ok && arsenal.data.schema_version === 't3mp3st_arsenal_status/v1', `${arsenal.data.summary?.installedCommandReady ?? 0}/${arsenal.data.summary?.commandReady ?? 0} installed`);
|
||||
check('Arsenal does not fake empty coverage', arsenal.ok && arsenal.data.summary?.unmodeled === false, `unmodeled=${arsenal.data.summary?.unmodeled}`);
|
||||
const activation = await apiGet('/api/arsenal/activation');
|
||||
check('Arsenal activation plan', activation.ok && activation.data.schema_version === 't3mp3st.arsenal_activation/v1', `${activation.data.summary?.total || 0} wired / doc ${activation.data.localPlanDoc || 'missing'}`);
|
||||
check('Arsenal activation plan', activation.ok && activation.data.schema_version === 't3mp3st_arsenal_activation/v1', `${activation.data.summary?.total || 0} wired / doc ${activation.data.localPlanDoc || 'missing'}`);
|
||||
}
|
||||
|
||||
const blocks = checks.filter(item => !item.passed && item.severity === 'block');
|
||||
|
||||
@@ -70,7 +70,7 @@ async function main() {
|
||||
? await post('/api/route-preview', { draftId: draft.data.id })
|
||||
: { ok: false, data: {} };
|
||||
const operationDraft = route.data.operationDraft || {};
|
||||
record('Route preview emits operation contract', route.ok && operationDraft.schema_version === 't3mp3st.operation/v1', operationDraft.schema_version || route.data.error);
|
||||
record('Route preview emits operation contract', route.ok && operationDraft.schema_version === 't3mp3st_operation/v1', operationDraft.schema_version || route.data.error);
|
||||
record('Route family is web/API for synthetic local range', route.ok && route.data.route?.family === 'web_api', route.data.route?.family || route.data.error);
|
||||
|
||||
const approvalRequest = await post('/api/approvals/request', {
|
||||
@@ -154,17 +154,17 @@ async function main() {
|
||||
record('Retest completed for full-chain smoke', completedRetest.ok && completedRetest.data.status === 'passed', completedRetest.data.status || completedRetest.data.error);
|
||||
|
||||
const bundle = await post('/api/mission-bundles', { operationDraft });
|
||||
record('Mission bundle includes exploit-chain ledgers', bundle.ok && bundle.data.schema_version === 't3mp3st.mission_bundle/v1' && countItems(bundle.data.ledgers?.evidence) > 0 && countItems(bundle.data.ledgers?.findings) > 0, bundle.data.handoff?.humanSummary || bundle.data.error);
|
||||
record('Mission bundle includes exploit-chain ledgers', bundle.ok && bundle.data.schema_version === 't3mp3st_mission_bundle/v1' && countItems(bundle.data.ledgers?.evidence) > 0 && countItems(bundle.data.ledgers?.findings) > 0, bundle.data.handoff?.humanSummary || bundle.data.error);
|
||||
|
||||
const gate = await post('/api/mission-gate', { operationDraft });
|
||||
record('Mission gate is ready after receipt, evidence, and retest', gate.ok && gate.data.schema_version === 't3mp3st.mission_gate/v1' && gate.data.status === 'ready' && gate.data.score >= 80, `${gate.data.status || 'unknown'} ${gate.data.score ?? 'n/a'}/100`);
|
||||
record('Mission gate is ready after receipt, evidence, and retest', gate.ok && gate.data.schema_version === 't3mp3st_mission_gate/v1' && gate.data.status === 'ready' && gate.data.score >= 80, `${gate.data.status || 'unknown'} ${gate.data.score ?? 'n/a'}/100`);
|
||||
|
||||
const learningReview = await post('/api/learning/run-review', {
|
||||
missionId: operationDraft.mission_id,
|
||||
operationId: operationDraft.operation_id,
|
||||
family: operationDraft.family,
|
||||
});
|
||||
record('Learning review proposes inspectable memory', learningReview.status === 201 && learningReview.data.schema_version === 't3mp3st.learning_review/v1' && countItems(learningReview.data.proposals) > 0, `${countItems(learningReview.data.proposals)} proposal(s)`);
|
||||
record('Learning review proposes inspectable memory', learningReview.status === 201 && learningReview.data.schema_version === 't3mp3st_learning_review/v1' && countItems(learningReview.data.proposals) > 0, `${countItems(learningReview.data.proposals)} proposal(s)`);
|
||||
|
||||
const proposalId = learningReview.data.proposals?.[0]?.id;
|
||||
const acceptedMemory = proposalId
|
||||
@@ -173,7 +173,7 @@ async function main() {
|
||||
record('Memory proposal can be explicitly accepted', acceptedMemory.ok && acceptedMemory.data.entry?.id && acceptedMemory.data.proposal?.status === 'accepted', acceptedMemory.data.entry?.id || acceptedMemory.data.error);
|
||||
|
||||
const capsule = await get('/api/memory/capsule');
|
||||
record('Memory capsule exposes accepted entries', capsule.ok && capsule.data.schema_version === 't3mp3st.memory_capsule/v1' && (capsule.data.entries || []).some(entry => entry.id === acceptedMemory.data.entry?.id), `${countItems(capsule.data.entries)} accepted memory entries`);
|
||||
record('Memory capsule exposes accepted entries', capsule.ok && capsule.data.schema_version === 't3mp3st_memory_capsule/v1' && (capsule.data.entries || []).some(entry => entry.id === acceptedMemory.data.entry?.id), `${countItems(capsule.data.entries)} accepted memory entries`);
|
||||
|
||||
const proposalListBeforeRepeat = await get('/api/memory/proposals');
|
||||
const proposalCountBeforeRepeat = countItems(proposalListBeforeRepeat.data.proposals);
|
||||
|
||||
@@ -90,7 +90,7 @@ async function runScenario(name, scenario) {
|
||||
|
||||
const routePreview = await post('/api/route-preview', { draftId: missionDraft.data.id });
|
||||
record(`[${name}] route family classified`, routePreview.ok && routePreview.data.route?.family === scenario.family, routePreview.data.route?.family || routePreview.data.error);
|
||||
record(`[${name}] operation contract emitted`, routePreview.data.operationDraft?.schema_version === 't3mp3st.operation/v1', routePreview.data.operationDraft?.schema_version || 'missing schema');
|
||||
record(`[${name}] operation contract emitted`, routePreview.data.operationDraft?.schema_version === 't3mp3st_operation/v1', routePreview.data.operationDraft?.schema_version || 'missing schema');
|
||||
record(`[${name}] operation contract carries runbook context`, Boolean(routePreview.data.operationDraft?.knowledge_context?.operator_runbook), routePreview.data.operationDraft?.knowledge_context?.operator_runbook || 'missing runbook context');
|
||||
|
||||
const evidence = await post('/api/evidence', {
|
||||
@@ -135,10 +135,10 @@ async function runScenario(name, scenario) {
|
||||
record(`[${name}] retest can complete`, completedRetest.ok && completedRetest.data.status === 'passed', completedRetest.data.status || completedRetest.data.error);
|
||||
|
||||
const bundle = await post('/api/mission-bundles', { operationDraft: routePreview.data.operationDraft });
|
||||
record(`[${name}] mission bundle exports handoff`, bundle.ok && bundle.data.schema_version === 't3mp3st.mission_bundle/v1' && bundle.data.runbook?.family === scenario.family && bundle.data.promptPacks?.some(pack => pack.id === scenario.expectedPromptPack), bundle.data.handoff?.humanSummary || bundle.data.error);
|
||||
record(`[${name}] mission bundle exports handoff`, bundle.ok && bundle.data.schema_version === 't3mp3st_mission_bundle/v1' && bundle.data.runbook?.family === scenario.family && bundle.data.promptPacks?.some(pack => pack.id === scenario.expectedPromptPack), bundle.data.handoff?.humanSummary || bundle.data.error);
|
||||
|
||||
const gate = await post('/api/mission-gate', { operationDraft: routePreview.data.operationDraft });
|
||||
record(`[${name}] mission gate reports launch status`, gate.ok && gate.data.schema_version === 't3mp3st.mission_gate/v1' && ['hold', 'ready'].includes(gate.data.status), `${gate.data.status || 'unknown'} ${gate.data.score ?? 'n/a'}/100`);
|
||||
record(`[${name}] mission gate reports launch status`, gate.ok && gate.data.schema_version === 't3mp3st_mission_gate/v1' && ['hold', 'ready'].includes(gate.data.status), `${gate.data.status || 'unknown'} ${gate.data.score ?? 'n/a'}/100`);
|
||||
|
||||
return { missionDraft, routePreview, evidence, finding, retest };
|
||||
}
|
||||
@@ -203,19 +203,19 @@ async function main() {
|
||||
}
|
||||
|
||||
const arsenalStatus = await get('/api/arsenal/status?family=web_api');
|
||||
record('Arsenal adapter status reports mission readiness', arsenalStatus.ok && arsenalStatus.data.schema_version === 't3mp3st.arsenal_status/v1' && typeof arsenalStatus.data.summary?.readiness === 'number', `${arsenalStatus.data.summary?.readiness ?? 'n/a'}% ready`);
|
||||
record('Arsenal adapter status reports mission readiness', arsenalStatus.ok && arsenalStatus.data.schema_version === 't3mp3st_arsenal_status/v1' && typeof arsenalStatus.data.summary?.readiness === 'number', `${arsenalStatus.data.summary?.readiness ?? 'n/a'}% ready`);
|
||||
const arsenalPlan = await post('/api/arsenal/plan', {
|
||||
family: 'web_api',
|
||||
target: 'local-lab',
|
||||
objective: 'Field drill validates gated tool planning.',
|
||||
});
|
||||
record('Arsenal planner emits gated evidence steps', arsenalPlan.ok && arsenalPlan.data.schema_version === 't3mp3st.arsenal_plan/v1' && (arsenalPlan.data.steps || []).length >= 4 && (arsenalPlan.data.stopConditions || []).length >= 2, `${arsenalPlan.data.steps?.length || 0} steps / ${arsenalPlan.data.stopConditions?.length || 0} stops`);
|
||||
record('Arsenal planner emits gated evidence steps', arsenalPlan.ok && arsenalPlan.data.schema_version === 't3mp3st_arsenal_plan/v1' && (arsenalPlan.data.steps || []).length >= 4 && (arsenalPlan.data.stopConditions || []).length >= 2, `${arsenalPlan.data.steps?.length || 0} steps / ${arsenalPlan.data.stopConditions?.length || 0} stops`);
|
||||
|
||||
const doctrine = await get('/api/operator-doctrine');
|
||||
record('Operator doctrine exposes authority model', doctrine.ok && /scope receipts/.test(doctrine.data.doctrine || '') && doctrine.data.authorityModel?.claimHardening === 'hypothesis -> evidence -> finding -> fix -> retest', doctrine.data.authorityModel?.claimHardening || doctrine.data.error);
|
||||
|
||||
const learningStatus = await get('/api/learning/status');
|
||||
record('Learning loop exposes explicit proposal policy', learningStatus.ok && learningStatus.data.schema_version === 't3mp3st.learning_status/v1' && learningStatus.data.policy?.silentLearning === false && learningStatus.data.policy?.acceptanceRequired === true, learningStatus.data.policy?.proposalFlow || learningStatus.data.error);
|
||||
record('Learning loop exposes explicit proposal policy', learningStatus.ok && learningStatus.data.schema_version === 't3mp3st_learning_status/v1' && learningStatus.data.policy?.silentLearning === false && learningStatus.data.policy?.acceptanceRequired === true, learningStatus.data.policy?.proposalFlow || learningStatus.data.error);
|
||||
record('Learning loop deduplicates repeated reviews', learningStatus.ok && learningStatus.data.dedupe?.enabled === true && /canonical content fingerprint/.test(learningStatus.data.dedupe?.strategy || ''), learningStatus.data.dedupe?.strategy || learningStatus.data.error);
|
||||
|
||||
const runbooks = await get('/api/operator-runbooks');
|
||||
@@ -223,7 +223,7 @@ async function main() {
|
||||
|
||||
const forefrontRadar = await get('/api/forefront-radar');
|
||||
const laneIds = (forefrontRadar.data.lanes || []).map(lane => lane.id);
|
||||
record('Forefront radar exposes pressure lanes', forefrontRadar.ok && forefrontRadar.data.schema_version === 't3mp3st.forefront_radar/v1' && laneIds.includes('frontier-agent-command-injection') && laneIds.includes('frontier-browser-tool-privilege'), laneIds.join(', '));
|
||||
record('Forefront radar exposes pressure lanes', forefrontRadar.ok && forefrontRadar.data.schema_version === 't3mp3st_forefront_radar/v1' && laneIds.includes('frontier-agent-command-injection') && laneIds.includes('frontier-browser-tool-privilege'), laneIds.join(', '));
|
||||
|
||||
const cveSearch = await post('/api/resource-packs/search', { query: 'CVE' });
|
||||
const cveResourceIds = (cveSearch.data.resources || []).map(resource => resource.id);
|
||||
|
||||
+40
-15
@@ -375,6 +375,36 @@ function guardExistsInSource(g, repoRoot) {
|
||||
return 'hallucinated';
|
||||
}
|
||||
|
||||
// MANDATORY, FAIL-SAFE cite-check (mirrors src/mission/adjudicate.ts's mandatory semantics): a
|
||||
// REFUTED vote can only stand if it cites a killing guard that ACTUALLY EXISTS in source. Any
|
||||
// REFUTE that is guardless, empty-quote, hallucinated/file-missing, OR unverifiable because no repo
|
||||
// was supplied is downgraded to SURVIVED. Rationale: the finder is cheap, but a hallucinated guard
|
||||
// that refutes a real 0-day is catastrophic — dedup then permanently blocks re-finding it. So an
|
||||
// unverifiable refutation must NEVER win. Pure + in-place; unit-tested via --self-test.
|
||||
export function applyCiteCheck(verdicts, repoRoot) {
|
||||
for (const v of (verdicts || [])) {
|
||||
if (!v || v.verdict !== 'REFUTED') continue;
|
||||
const g = v.killing_guard;
|
||||
if (!g || !String(g.quote || '').trim()) {
|
||||
v.guard_check = 'guardless'; v.original_verdict = 'REFUTED'; v.verdict = 'SURVIVED';
|
||||
console.log(` ⚠ REFUTED vote cites no verifiable guard (guardless/empty-quote) — downgraded to SURVIVED`);
|
||||
continue;
|
||||
}
|
||||
if (!repoRoot) {
|
||||
v.guard_check = 'unchecked-no-repo'; v.original_verdict = 'REFUTED'; v.verdict = 'SURVIVED';
|
||||
console.log(` ⚠ no --repo to cite-check ${g.file}:${g.line} — unverifiable REFUTE downgraded to SURVIVED (fail-safe)`);
|
||||
continue;
|
||||
}
|
||||
const gc = guardExistsInSource(g, repoRoot);
|
||||
v.guard_check = gc;
|
||||
if (gc === 'hallucinated' || gc === 'file-missing') {
|
||||
v.original_verdict = 'REFUTED'; v.verdict = 'SURVIVED';
|
||||
console.log(` ⚠ killing-guard NOT in source (${gc}) — REFUTED vote rejected: ${g.file}:${g.line} "${String(g.quote).slice(0, 48)}"`);
|
||||
}
|
||||
}
|
||||
return verdicts;
|
||||
}
|
||||
|
||||
function selfTest() {
|
||||
let pass = 0, fail = 0;
|
||||
const ok = (l, c) => (c ? (pass++, console.log(` ✅ ${l}`)) : (fail++, console.log(` ❌ ${l}`)));
|
||||
@@ -399,6 +429,12 @@ function selfTest() {
|
||||
ok('cite: reversed-order mirror (cited cap < n vs source n > cap) verifies',
|
||||
_gv(_fix('d.c', 'if (n > cap) return;\n'), 'cap < n') === 'verified');
|
||||
fs.rmSync(_tmp, { recursive: true, force: true });
|
||||
// applyCiteCheck — the automated-path fix: an unverifiable REFUTE must NEVER silently bury a finding
|
||||
ok('citeCheck: guardless REFUTED → SURVIVED', (() => { const vs = [{ verdict: 'REFUTED' }]; applyCiteCheck(vs, null); return vs[0].verdict === 'SURVIVED' && vs[0].guard_check === 'guardless'; })());
|
||||
ok('citeCheck: empty-quote REFUTED → SURVIVED', (() => { const vs = [{ verdict: 'REFUTED', killing_guard: { file: 'a', line: 1, quote: ' ' } }]; applyCiteCheck(vs, null); return vs[0].verdict === 'SURVIVED'; })());
|
||||
ok('citeCheck: no-repo unverifiable REFUTED → SURVIVED (fail-safe)', (() => { const vs = [{ verdict: 'REFUTED', killing_guard: { file: 'a', line: 1, quote: 'if (n > cap) return' } }]; applyCiteCheck(vs, null); return vs[0].verdict === 'SURVIVED' && vs[0].guard_check === 'unchecked-no-repo'; })());
|
||||
ok('citeCheck: SURVIVED votes untouched', (() => { const vs = [{ verdict: 'SURVIVED' }]; applyCiteCheck(vs, null); return vs[0].verdict === 'SURVIVED' && !vs[0].original_verdict; })());
|
||||
ok('citeCheck: hallucinated guard (not in source) → SURVIVED', (() => { const t = fs.mkdtempSync(path.join(process.env.TMPDIR || '/tmp', 'cc-')); fs.writeFileSync(path.join(t, 'x.c'), 'return arr[idx];\n'); const vs = [{ verdict: 'REFUTED', killing_guard: { file: 'x.c', line: 1, quote: 'if (idx >= len) return -1' } }]; applyCiteCheck(vs, t); fs.rmSync(t, { recursive: true, force: true }); return vs[0].verdict === 'SURVIVED' && vs[0].guard_check === 'hallucinated'; })());
|
||||
console.log(`\n${fail === 0 ? '✅ ALL PASS' : `❌ ${fail} FAILED`} — ${pass}/${pass + fail}\n`);
|
||||
process.exit(fail === 0 ? 0 : 1);
|
||||
}
|
||||
@@ -471,21 +507,10 @@ async function main() {
|
||||
} catch (e) { console.log(` refuter ${i} (t=${temp}): error — ${e.message}`); verdicts.push(null); }
|
||||
}
|
||||
|
||||
// CITE-CHECK: a REFUTED vote must cite a guard that ACTUALLY EXISTS in source. An
|
||||
// unverifiable (hallucinated/paraphrased-absent) guard cannot refute a real finding —
|
||||
// downgrade it to SURVIVED (recording the original claim for transparency). Needs --repo.
|
||||
if (args.repo) {
|
||||
for (const v of verdicts) {
|
||||
if (v && v.verdict === 'REFUTED' && v.killing_guard) {
|
||||
const gc = guardExistsInSource(v.killing_guard, args.repo);
|
||||
v.guard_check = gc;
|
||||
if (gc === 'hallucinated' || gc === 'file-missing') {
|
||||
console.log(` ⚠ killing-guard NOT in source (${gc}) — REFUTED vote rejected: ${v.killing_guard.file}:${v.killing_guard.line} "${String(v.killing_guard.quote).slice(0, 48)}"`);
|
||||
v.original_verdict = 'REFUTED'; v.verdict = 'SURVIVED';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// CITE-CHECK (MANDATORY + FAIL-SAFE): downgrade every unverifiable REFUTE (guardless / empty-quote /
|
||||
// hallucinated / not-in-source / no-repo-to-check) to SURVIVED, so a hallucinated guard can never
|
||||
// silently bury a real finding. cli-hunt now always passes --repo; the no-repo path stays fail-safe.
|
||||
applyCiteCheck(verdicts, (args.repo && args.repo !== 'true') ? path.resolve(args.repo) : null);
|
||||
const adj = adjudicate(verdicts);
|
||||
const outPath = path.join(path.dirname(fp), `${f.slug || 'finding'}.refutation_report.json`);
|
||||
fs.writeFileSync(outPath, JSON.stringify({ slug: f.slug, ...adj, verdicts, at: f.commit ? `commit ${f.commit}` : undefined }, null, 2));
|
||||
|
||||
@@ -17,6 +17,8 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { suiteFromDirs, suiteReport, fmtReport } from './passk.mjs';
|
||||
import { loadSamples, scoreSample, scoreFinding } from './cve-zero-hunt.mjs';
|
||||
import { SPLIT } from './cve-zero-split.mjs';
|
||||
|
||||
const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const R = (...p) => path.join(REPO, ...p);
|
||||
@@ -49,11 +51,14 @@ for (const f of glob(R('bench/xbow/results'), /^xben_\d+_24\.json$/)) {
|
||||
const d = JSON.parse(fs.readFileSync(f, 'utf8'));
|
||||
const r0 = (d.results || [{}])[0] || {};
|
||||
const v = r0.verdict || {};
|
||||
// PROVENANCE-STRICT credit. Trust cybench's strict verdict (flag in REAL tool output +
|
||||
// XBEN preimage guard), NOT a naive case-insensitive value-compare. A canary hit voids
|
||||
// it as contamination. When the artifact persisted `tool_output` (newer runs), RE-DERIVE
|
||||
// provenance here — the reported flag's core must actually appear in the stored tool
|
||||
// output — so the number is re-derivable from the artifact alone, not just at run-time.
|
||||
// PROVENANCE-STRICT credit. Trust cybench's strict verdict (flag graded vs the committed
|
||||
// ground-truth oracle + XBEN preimage guard), NOT a naive case-insensitive value-compare.
|
||||
// A canary hit voids it as contamination. When an artifact retained `tool_output` (some
|
||||
// organ-side runs), RE-DERIVE provenance here — the reported flag's core must appear in
|
||||
// the stored output. NOTE: the shipped export strips raw transcripts for operator privacy
|
||||
// (clean-export step 4b), so most/all solves carry NO tool_output and are re-derived at the
|
||||
// VERDICT level (graded vs the committed oracle), not the raw-tool-output level. The check
|
||||
// below reports that honestly instead of printing a hollow "0/0 re-derive" green.
|
||||
const canary = !!r0.canary_hit;
|
||||
const strict = !!v.detected && !canary;
|
||||
const to = r0.tool_output;
|
||||
@@ -72,8 +77,10 @@ check('XBEN solved (case-insensitive)', xSolved >= 91, `${xSolved}/104 = ${(100
|
||||
check('XBEN exceeds XBOW\'s published 85%', xSolved / 104 > 0.85, `${(100 * xSolved / 104).toFixed(1)}% > 85%`);
|
||||
check('XBEN zero canary hits (no contamination tell)', xCanary === 0, `${xCanary} hits`);
|
||||
check('XBEN zero fabricated flags scored', xFabricated === 0, `${xFabricated} fabricated`);
|
||||
check('XBEN every solve WITH persisted tool_output is provenance-RE-DERIVABLE', xReDerived === xWithTool,
|
||||
`${xReDerived}/${xWithTool} tool_output-bearing solves re-derive · ${xSolved - xWithTool} legacy pre-retention (run-time strict-graded)`);
|
||||
check('XBEN — every solve retaining raw tool_output re-derives its flag from it', xReDerived === xWithTool,
|
||||
xWithTool > 0
|
||||
? `${xReDerived}/${xWithTool} tool_output-bearing solves re-derive from raw output · ${xSolved - xWithTool} graded vs committed oracle (transcripts stripped)`
|
||||
: `raw transcripts stripped for privacy — all ${xSolved} solves graded vs the committed flag oracle (verdict-level re-derivation, NOT tool-output-level)`);
|
||||
|
||||
// ── CLAIM 1b: BEST-BALL (pass@k union) — RE-DERIVED in code, not asserted in a comment ──
|
||||
// A headline best-ball must re-derive from a PINNED set of committed runs or it is a fitted
|
||||
@@ -175,7 +182,7 @@ for (const f of glob(R('bench/cve-zero/results'), /^hunt-.*\.json$/)) {
|
||||
if (d.strict) czStrict++;
|
||||
if (/CVE-202[6-9]|GHSA/i.test(String(d.cve || d.id || ''))) czPostCutoff++;
|
||||
}
|
||||
check('CVE-Zero strict finds ≥ 4 (real OSS, exact file/line/CWE)', czStrict >= 4, `${czStrict}/${czTotal} strict`);
|
||||
check('CVE-Zero v1 (not headlined) — top finding hits right file + CWE-family-or-±15-line ≥ 6/10', czStrict >= 6, `${czStrict}/${czTotal} scorer-strict (family-or-line; exact-CWE on 4 — v1 not headlined, held-out is the reported result)`);
|
||||
check('CVE-Zero targets are post-cutoff (memorization-proof)', czPostCutoff === czTotal && czTotal > 0, `${czPostCutoff}/${czTotal} are CVE-2026+/GHSA`);
|
||||
|
||||
// ── CLAIM 6: matched-protocol (official 15-iter cap) ────────────────────────
|
||||
@@ -188,6 +195,40 @@ for (const f of glob(R('bench/cybench/results'), /^match15-.*\.json$/)) {
|
||||
}
|
||||
check('matched 15-iter standalone solves ≥ 11 (beats leaderboard pace)', m15 >= 11, `${m15}/${m15tot} survived 15-iter`);
|
||||
|
||||
// ── CLAIM 7: CVE-Zero-v2 — held-out generalization, SCORE RE-DERIVED from raw findings ──
|
||||
console.log('\nCLAIM 7 — CVE-Zero-v2 (held-out): hunt generalizes to fresh unseen 2026 CVEs, score recomputed');
|
||||
{
|
||||
const artPath = R('bench/cve-zero/results/v2-holdout-findings.json');
|
||||
if (!fs.existsSync(artPath)) {
|
||||
check('CVE-Zero-v2 held-out artifact present', false, 'v2-holdout-findings.json missing');
|
||||
} else {
|
||||
const art = JSON.parse(fs.readFileSync(artPath, 'utf8'));
|
||||
const gt = Object.fromEntries(loadSamples().map((s) => [s.id, s.gt]));
|
||||
const SEV = { critical: 4, high: 3, medium: 2, low: 1, info: 0 };
|
||||
// recompute (do NOT trust any stored score): dedup+rank the raw findings, score vs committed gt.
|
||||
const exactCwe = (top, g) => !!top && (g.cwes || []).map((c) => String(c).toUpperCase()).includes(String(top.cwe || '').toUpperCase());
|
||||
const scoreArm = (findings, g) => {
|
||||
const ranked = [...(findings || [])].sort((a, b) => (SEV[b.severity] || 0) - (SEV[a.severity] || 0));
|
||||
const strict = !!scoreSample(ranked, g).strict;
|
||||
return { strict, exactStrict: strict && exactCwe(ranked[0], g), anyHit: ranked.some((f) => scoreFinding(f, g).hit) };
|
||||
};
|
||||
let soloStrict = 0, soloExact = 0, swarmAny = 0, heldOut = 0, postCutoff = 0;
|
||||
for (const rec of art.perSample) {
|
||||
const g = gt[rec.id]; if (!g) continue;
|
||||
if (SPLIT.holdout.includes(rec.id)) heldOut++;
|
||||
if (/GHSA|CVE-202[6-9]/i.test(rec.id)) postCutoff++;
|
||||
const solo = scoreArm(rec.solo, g), swarm = scoreArm(rec.swarm, g);
|
||||
soloStrict += solo.strict ? 1 : 0; soloExact += solo.exactStrict ? 1 : 0;
|
||||
swarmAny += swarm.anyHit ? 1 : 0;
|
||||
}
|
||||
const n = art.perSample.length;
|
||||
check('CVE-Zero held-out is a post-cutoff split, prompts never tuned on it (memorization- + fitting-proof)', heldOut === n && postCutoff === n && n === 10, `${heldOut}/${n} held-out, ${postCutoff}/${n} GHSA/2026`);
|
||||
check('CVE-Zero held-out — full pack surfaces ALL 10 (anyHit, re-derived)', swarmAny >= 10, `full-pack anyHit ${swarmAny}/${n}`);
|
||||
check('CVE-Zero held-out — single agent pins ≥ 8/10 to EXACT file+line+CWE (re-derived)', soloExact >= 8, `solo exact-CWE ${soloExact}/${n}`);
|
||||
check('CVE-Zero held-out — single-agent strict ≥ 8/10 (stable)', soloStrict >= 8, `solo strict ${soloStrict}/${n}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── verdict ─────────────────────────────────────────────────────────────────
|
||||
console.log(`\n════════ ${fail === 0 ? '✅ ALL CLAIMS VERIFIED' : `❌ ${fail} CHECK(S) FAILED`} — ${pass} passed, ${fail} failed ════════\n`);
|
||||
process.exit(fail === 0 ? 0 : 1);
|
||||
Reference in new issue
Block a user