@clear-capabilities/agentic-security-scanner 0.149.4 → 0.150.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +138 -0
- package/bin/agentic-security.js +533 -2
- package/dist/1122.index.js +16 -16
- package/dist/1208.index.js +23 -0
- package/dist/1301.index.js +3 -3
- package/dist/1310.index.js +3161 -0
- package/dist/1379.index.js +3 -3
- package/dist/1444.index.js +3 -3
- package/dist/1660.index.js +1 -1
- package/dist/1700.index.js +5 -5
- package/dist/1905.index.js +107 -12
- package/dist/1920.index.js +10 -10
- package/dist/2238.index.js +4 -4
- package/dist/2271.index.js +3 -3
- package/dist/2432.index.js +8 -8
- package/dist/2659.index.js +1 -1
- package/dist/2826.index.js +2 -2
- package/dist/2830.index.js +3 -3
- package/dist/2923.index.js +1 -1
- package/dist/3001.index.js +4 -4
- package/dist/3117.index.js +4 -4
- package/dist/3180.index.js +9 -9
- package/dist/3276.index.js +1 -1
- package/dist/3415.index.js +1 -1
- package/dist/3499.index.js +1 -1
- package/dist/3518.index.js +4 -4
- package/dist/3736.index.js +7 -7
- package/dist/3839.index.js +4 -4
- package/dist/4113.index.js +441 -418
- package/dist/4265.index.js +4 -4
- package/dist/4384.index.js +8 -3
- package/dist/4399.index.js +292 -0
- package/dist/4547.index.js +2 -2
- package/dist/4863.index.js +6 -6
- package/dist/4970.index.js +2 -2
- package/dist/5051.index.js +15 -15
- package/dist/5144.index.js +4 -4
- package/dist/5333.index.js +8 -8
- package/dist/5343.index.js +2 -2
- package/dist/5350.index.js +6 -6
- package/dist/5561.index.js +1 -1
- package/dist/5637.index.js +10 -5
- package/dist/5724.index.js +21642 -0
- package/dist/5756.index.js +588 -0
- package/dist/5830.index.js +3 -3
- package/dist/6257.index.js +170 -0
- package/dist/6626.index.js +5 -5
- package/dist/6662.index.js +1 -1
- package/dist/6675.index.js +4 -4
- package/dist/6730.index.js +10 -10
- package/dist/6829.index.js +4 -4
- package/dist/6944.index.js +2 -2
- package/dist/6994.index.js +143 -0
- package/dist/7039.index.js +541 -0
- package/dist/7178.index.js +11 -6
- package/dist/7227.index.js +3 -3
- package/dist/7552.index.js +2 -2
- package/dist/7709.index.js +2 -2
- package/dist/7838.index.js +1037 -0
- package/dist/8218.index.js +4 -4
- package/dist/8476.index.js +4 -4
- package/dist/8513.index.js +8 -8
- package/dist/8520.index.js +1 -1
- package/dist/8752.index.js +4 -4
- package/dist/9207.index.js +2 -2
- package/dist/9220.index.js +5 -5
- package/dist/9390.index.js +6 -6
- package/dist/9503.index.js +2 -2
- package/dist/9560.index.js +3221 -0
- package/dist/957.index.js +127 -0
- package/dist/9801.index.js +1 -1
- package/dist/9824.index.js +5 -5
- package/dist/agentic-security.mjs +23 -23
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +4 -3
- package/src/discovery/disprove.js +6 -1
- package/src/discovery/hunter.js +10 -1
- package/src/discovery/llm-invoke.js +77 -0
- package/src/egress/policy.js +11 -1
- package/src/egress/redact.js +1 -1
- package/src/engine.js +37 -1
- package/src/llm-validator/agent-loop.js +215 -0
- package/src/llm-validator/agent-tools.js +271 -0
- package/src/llm-validator/explain-proposal.js +106 -0
- package/src/llm-validator/fix-proposal.js +149 -0
- package/src/llm-validator/index.js +51 -3
- package/src/llm-validator/model-capabilities.js +269 -0
- package/src/llm-validator/model-probe.js +232 -0
- package/src/llm-validator/model-status.js +27 -0
- package/src/llm-validator/ollama-provider.js +382 -0
- package/src/llm-validator/oom-feedback.js +69 -0
- package/src/llm-validator/poc-proposal.js +122 -0
- package/src/llm-validator/providers.js +75 -0
- package/src/llm-validator/redact.js +177 -11
- package/src/report/index.js +33 -0
- package/src/sast/java-ast-folding.js +18 -2
package/bin/agentic-security.js
CHANGED
|
@@ -127,6 +127,7 @@ Commands:
|
|
|
127
127
|
mcp Start the MCP stdio server (scan_diff, query_taint, explain_finding, apply_fix)
|
|
128
128
|
validator-cache stats|gc Inspect / prune .agentic-security/llm-cache/ (use --older-than <days> --dry-run)
|
|
129
129
|
verify [--finding <id>] Re-run the verifier loop on last-scan findings (use --live --target <url> to execute PoCs)
|
|
130
|
+
ask "<question>" [target] Bounded local-model Q&A with read-only tool access (requires AGENTIC_SECURITY_LLM_PRESET=ollama)
|
|
130
131
|
reset [--yes] [--keep ...] Right-to-delete: wipe accumulated learned state under .agentic-security/ (preserves operator-authored config)
|
|
131
132
|
--expired only remove artifacts past their retention-class TTL
|
|
132
133
|
Every run writes a deletion-report.json proving what was planned/deleted/preserved/failed.
|
|
@@ -1452,6 +1453,77 @@ async function cmdProfile(args) {
|
|
|
1452
1453
|
// /triage list | assign | transition | trend
|
|
1453
1454
|
async function cmdTriage(args) {
|
|
1454
1455
|
const target = path.resolve(args._[args._.length - 1] && !args._[args._.length - 1].startsWith('--') ? args._[args._.length - 1] : '.');
|
|
1456
|
+
// ollama-offline-prd.md §34 — `triage --explain <finding-id>` is read-only
|
|
1457
|
+
// narrative generation, not triage's own assign/transition/lock state
|
|
1458
|
+
// machine, so it doesn't need the pro-tier gate below (and is checked
|
|
1459
|
+
// before it for exactly that reason).
|
|
1460
|
+
if (args.flags.explain) {
|
|
1461
|
+
const id = String(args.flags.explain);
|
|
1462
|
+
const lastScanPath = statePath(target, 'last-scan.json');
|
|
1463
|
+
if (!fs.existsSync(lastScanPath)) { console.error('No prior scan found. Run `agentic-security scan` first.'); return 4; }
|
|
1464
|
+
const last = JSON.parse(await fsp.readFile(lastScanPath, 'utf8'));
|
|
1465
|
+
const f = (last.findings || []).find(x => x.id === id) || (last.secrets || []).find(x => x.id === id);
|
|
1466
|
+
if (!f) { console.error(`Finding ${id} not found in last scan.`); return 4; }
|
|
1467
|
+
console.log('Deterministic evidence');
|
|
1468
|
+
console.log('-----------------------');
|
|
1469
|
+
console.log(` ${f.vuln || f.title || 'finding'} [${f.severity || 'unknown'}] ${f.cwe || ''}`.trim());
|
|
1470
|
+
console.log(` ${f.file}:${f.line || '?'}`);
|
|
1471
|
+
if (f.description) console.log(` ${f.description}`);
|
|
1472
|
+
const { proposeOllamaExplanation, EXPLAIN_ERROR } = await import('../src/llm-validator/explain-proposal.js');
|
|
1473
|
+
const proposal = await proposeOllamaExplanation({
|
|
1474
|
+
finding: { file: f.file, line: f.line, vuln: f.vuln, cwe: f.cwe, severity: f.severity, confidence: f.confidence ?? f.llm_confidence },
|
|
1475
|
+
contextSnippet: f.snippet || '',
|
|
1476
|
+
scanRoot: target,
|
|
1477
|
+
});
|
|
1478
|
+
console.log('');
|
|
1479
|
+
if (proposal.ok) {
|
|
1480
|
+
console.log(`Model-generated explanation (${proposal.model}) — not deterministic evidence`);
|
|
1481
|
+
console.log('-----------------------------------------------------------------------');
|
|
1482
|
+
console.log(` ${proposal.modelExplanation}`);
|
|
1483
|
+
if (proposal.confidenceNote) console.log(` (${proposal.confidenceNote})`);
|
|
1484
|
+
} else if (proposal.code === EXPLAIN_ERROR.NOT_CONFIGURED) {
|
|
1485
|
+
console.log('(No AI-assisted explanation — configure AGENTIC_SECURITY_LLM_PRESET=ollama for one.)');
|
|
1486
|
+
} else {
|
|
1487
|
+
console.log(`(AI-assisted explanation unavailable: ${proposal.code} — ${proposal.reason})`);
|
|
1488
|
+
}
|
|
1489
|
+
return 0;
|
|
1490
|
+
}
|
|
1491
|
+
// ollama-offline-prd.md §18.1 — `triage --poc <finding-id>` is a headless,
|
|
1492
|
+
// Ollama-backed PoC SKETCH (narrative only — never executed, never written
|
|
1493
|
+
// to disk) for when Claude Code's own security-poc-generator agent isn't
|
|
1494
|
+
// available. Same read-only exemption from the pro-tier gate as --explain.
|
|
1495
|
+
if (args.flags.poc) {
|
|
1496
|
+
const id = String(args.flags.poc);
|
|
1497
|
+
const lastScanPath = statePath(target, 'last-scan.json');
|
|
1498
|
+
if (!fs.existsSync(lastScanPath)) { console.error('No prior scan found. Run `agentic-security scan` first.'); return 4; }
|
|
1499
|
+
const last = JSON.parse(await fsp.readFile(lastScanPath, 'utf8'));
|
|
1500
|
+
const f = (last.findings || []).find(x => x.id === id) || (last.secrets || []).find(x => x.id === id);
|
|
1501
|
+
if (!f) { console.error(`Finding ${id} not found in last scan.`); return 4; }
|
|
1502
|
+
console.log('Deterministic evidence');
|
|
1503
|
+
console.log('-----------------------');
|
|
1504
|
+
console.log(` ${f.vuln || f.title || 'finding'} [${f.severity || 'unknown'}] ${f.cwe || ''}`.trim());
|
|
1505
|
+
console.log(` ${f.file}:${f.line || '?'}`);
|
|
1506
|
+
if (f.description) console.log(` ${f.description}`);
|
|
1507
|
+
const { proposeOllamaPoc, POC_PROPOSAL_ERROR } = await import('../src/llm-validator/poc-proposal.js');
|
|
1508
|
+
const proposal = await proposeOllamaPoc({
|
|
1509
|
+
finding: { file: f.file, line: f.line, vuln: f.vuln, cwe: f.cwe, severity: f.severity },
|
|
1510
|
+
contextSnippet: f.snippet || '',
|
|
1511
|
+
scanRoot: target,
|
|
1512
|
+
});
|
|
1513
|
+
console.log('');
|
|
1514
|
+
if (proposal.ok) {
|
|
1515
|
+
console.log(`Model-generated PoC sketch (${proposal.model}) — unverified, not executed`);
|
|
1516
|
+
console.log('-----------------------------------------------------------------------');
|
|
1517
|
+
console.log(` ${proposal.pocNarrative}`);
|
|
1518
|
+
if (proposal.exampleInput) console.log(` Example input: ${proposal.exampleInput}`);
|
|
1519
|
+
if (proposal.expectedResult) console.log(` Expected result: ${proposal.expectedResult}`);
|
|
1520
|
+
} else if (proposal.code === POC_PROPOSAL_ERROR.NOT_CONFIGURED) {
|
|
1521
|
+
console.log('(No AI-assisted PoC sketch — configure AGENTIC_SECURITY_LLM_PRESET=ollama for one.)');
|
|
1522
|
+
} else {
|
|
1523
|
+
console.log(`(AI-assisted PoC sketch unavailable: ${proposal.code} — ${proposal.reason})`);
|
|
1524
|
+
}
|
|
1525
|
+
return 0;
|
|
1526
|
+
}
|
|
1455
1527
|
const profile = loadProfile(target);
|
|
1456
1528
|
if (profile.profile !== 'pro') {
|
|
1457
1529
|
console.error('Triage is a pro-mode feature. Run `agentic-security profile set pro` to enable.');
|
|
@@ -1848,6 +1920,283 @@ async function cmdValidatorCache(args) {
|
|
|
1848
1920
|
return 4;
|
|
1849
1921
|
}
|
|
1850
1922
|
|
|
1923
|
+
// `agentic-security models list|status|doctor|inspect <model>`
|
|
1924
|
+
//
|
|
1925
|
+
// agentic-security-ollama-offline-prd.md §11.2/§31 — the local-AI UX. Every
|
|
1926
|
+
// subcommand is read-only and never itself triggers a model pull or a chat
|
|
1927
|
+
// call (models test/benchmark, which DO run inference, are P1/P2 scope not
|
|
1928
|
+
// implemented here — see the PRD coverage note in docs/guides/ollama.md).
|
|
1929
|
+
async function cmdModels(args) {
|
|
1930
|
+
const sub = args._[1] || 'status';
|
|
1931
|
+
const {
|
|
1932
|
+
ollamaEndpointConfig, listOllamaModels, DEFAULT_OLLAMA_HOST,
|
|
1933
|
+
} = await import('../src/llm-validator/ollama-provider.js');
|
|
1934
|
+
const {
|
|
1935
|
+
classifyModelFamily, capabilitiesFromFamilyHint, detectSystemMemory,
|
|
1936
|
+
detectMemoryTier, recommendAdmission, MEMORY_PROFILES,
|
|
1937
|
+
} = await import('../src/llm-validator/model-capabilities.js');
|
|
1938
|
+
const { getModelCapabilities } = await import('../src/llm-validator/model-probe.js');
|
|
1939
|
+
const { resolveProvider } = await import('../src/llm-validator/providers.js');
|
|
1940
|
+
const wantsProbe = !!args.flags.probe;
|
|
1941
|
+
const wantsForceProbe = !!args.flags.force;
|
|
1942
|
+
|
|
1943
|
+
const envOverride = {};
|
|
1944
|
+
if (args.flags.host) envOverride.AGENTIC_SECURITY_OLLAMA_HOST = String(args.flags.host);
|
|
1945
|
+
if (args.flags['allow-remote-ollama']) envOverride.AGENTIC_SECURITY_OLLAMA_ALLOW_REMOTE = '1';
|
|
1946
|
+
const env = { ...process.env, ...envOverride };
|
|
1947
|
+
|
|
1948
|
+
if (sub === 'list' || sub === 'status' || sub === 'doctor') {
|
|
1949
|
+
const cfg = ollamaEndpointConfig(env);
|
|
1950
|
+
if (!cfg.ok) {
|
|
1951
|
+
console.log(`✗ ${cfg.reason}`);
|
|
1952
|
+
console.log('\nThe deterministic scanner will still run.\nNo cloud provider will be used automatically.');
|
|
1953
|
+
return args.flags.json ? (writeStdout(JSON.stringify({ ok: false, code: cfg.code, reason: cfg.reason }, null, 2) + '\n'), 0) : 1;
|
|
1954
|
+
}
|
|
1955
|
+
const modelsResult = await listOllamaModels({ host: cfg.config.host });
|
|
1956
|
+
if (!modelsResult.ok) {
|
|
1957
|
+
console.log(`✗ Ollama server is not reachable at ${cfg.config.host}`);
|
|
1958
|
+
console.log(` (${modelsResult.code}: ${modelsResult.reason})`);
|
|
1959
|
+
console.log('\nThe deterministic scanner will still run.\nNo cloud provider will be used automatically.');
|
|
1960
|
+
if (args.flags.json) writeStdout(JSON.stringify({ ok: false, code: modelsResult.code, reason: modelsResult.reason }, null, 2) + '\n');
|
|
1961
|
+
return 1;
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
if (sub === 'list') {
|
|
1965
|
+
if (args.flags.json) {
|
|
1966
|
+
writeStdout(JSON.stringify({ ok: true, host: cfg.config.host, models: modelsResult.models }, null, 2) + '\n');
|
|
1967
|
+
return 0;
|
|
1968
|
+
}
|
|
1969
|
+
console.log(`Installed models (${cfg.config.host}):`);
|
|
1970
|
+
if (modelsResult.models.length === 0) console.log(' (none installed)');
|
|
1971
|
+
for (const m of modelsResult.models) {
|
|
1972
|
+
const family = classifyModelFamily(m.name);
|
|
1973
|
+
const caps = capabilitiesFromFamilyHint(m.name);
|
|
1974
|
+
const sizeGb = Number.isFinite(m.sizeBytes) ? (m.sizeBytes / (1024 ** 3)).toFixed(1) + 'GB' : '?';
|
|
1975
|
+
console.log(` ${m.name.padEnd(24)} family=${family.padEnd(14)} size=${sizeGb.padEnd(8)} tools=${caps.tools === true ? 'yes' : caps.tools === false ? 'no' : 'not-detected'}`);
|
|
1976
|
+
}
|
|
1977
|
+
return 0;
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
// status / doctor
|
|
1981
|
+
const mem = detectSystemMemory();
|
|
1982
|
+
const tier = detectMemoryTier(mem.totalBytes);
|
|
1983
|
+
const resolved = resolveProvider({ role: 'validate', env });
|
|
1984
|
+
const model = resolved.ok ? resolved.config.model : null;
|
|
1985
|
+
const totalGb = (mem.totalBytes / (1024 ** 3)).toFixed(1);
|
|
1986
|
+
const freeGb = (mem.freeBytes / (1024 ** 3)).toFixed(1);
|
|
1987
|
+
|
|
1988
|
+
const lines = [];
|
|
1989
|
+
lines.push(sub === 'doctor' ? 'agentic-security local AI doctor' : 'agentic-security models status');
|
|
1990
|
+
lines.push('');
|
|
1991
|
+
lines.push('✓ Ollama server reachable');
|
|
1992
|
+
lines.push(cfg.config.offline ? '✓ Endpoint is loopback-only' : `↗ Endpoint is REMOTE (${cfg.config.host}) — offline guarantee does not apply`);
|
|
1993
|
+
lines.push(`✓ ${modelsResult.models.length} model(s) installed`);
|
|
1994
|
+
lines.push('');
|
|
1995
|
+
lines.push(`System RAM: ${totalGb} GB (free: ${freeGb} GB)`);
|
|
1996
|
+
lines.push(`Memory tier: ${tier}`);
|
|
1997
|
+
if (model) {
|
|
1998
|
+
const installed = modelsResult.models.some((m) => m.name === model);
|
|
1999
|
+
lines.push(`Default model: ${model} ${installed ? '' : '(NOT currently installed)'}`);
|
|
2000
|
+
const profileKey = tier === '8gb' ? '8gb' : (classifyModelFamily(model) === 'gemma4' ? '16gb-gemma' : '16gb-qwen');
|
|
2001
|
+
const admission = recommendAdmission({ profile: profileKey, freeBytes: mem.freeBytes, requestedModel: model });
|
|
2002
|
+
if (admission.admitted) {
|
|
2003
|
+
lines.push(` ✓ memory admission passed — context ${admission.contextTokens} tokens` +
|
|
2004
|
+
(admission.fellBackToSmallerModel ? ` (fell back to ${admission.model})` : admission.reducedContext ? ' (context reduced)' : ''));
|
|
2005
|
+
} else {
|
|
2006
|
+
lines.push(` ✗ memory admission FAILED — ${admission.reason}`);
|
|
2007
|
+
}
|
|
2008
|
+
// Adversarial-review fix (2026-09): a memory estimate that has already
|
|
2009
|
+
// been proven wrong by a real OOM on this machine no longer gets
|
|
2010
|
+
// presented with the same unqualified confidence.
|
|
2011
|
+
if (admission.priorOOMWarning) lines.push(` ⚠ ${admission.priorOOMWarning}`);
|
|
2012
|
+
const capsResult = await getModelCapabilities({ host: cfg.config.host, model, probe: wantsProbe, force: wantsForceProbe });
|
|
2013
|
+
const caps = capsResult.capabilities;
|
|
2014
|
+
const capSourceLabel = caps.source?.runtimeProbe ? (capsResult.cached ? 'runtime-probed, cached' : 'runtime-probed')
|
|
2015
|
+
: caps.source?.metadata ? 'Ollama metadata' : 'family hint — not runtime-probed';
|
|
2016
|
+
lines.push(` chat=${caps.chat ? 'yes' : 'no'} structuredJson=${caps.structuredJson} tools=${caps.tools} (${capSourceLabel})`);
|
|
2017
|
+
if (wantsProbe) lines.push(capsResult.cached ? ' ✓ capability probe cached (pass --force to re-probe)' : ' ✓ capability probe ran (now cached)');
|
|
2018
|
+
else lines.push(' ↗ run with --probe to runtime-verify structured output / tool calling (consumes inference time)');
|
|
2019
|
+
} else {
|
|
2020
|
+
lines.push('Default model: (none resolved)');
|
|
2021
|
+
}
|
|
2022
|
+
lines.push('');
|
|
2023
|
+
lines.push('Cloud fallback: disabled');
|
|
2024
|
+
lines.push('Deterministic scanner: enabled');
|
|
2025
|
+
|
|
2026
|
+
if (args.flags.json) {
|
|
2027
|
+
writeStdout(JSON.stringify({
|
|
2028
|
+
ok: true, host: cfg.config.host, offline: cfg.config.offline, egress: cfg.config.egress,
|
|
2029
|
+
installedModels: modelsResult.models.map((m) => m.name), systemRamGb: Number(totalGb), freeRamGb: Number(freeGb),
|
|
2030
|
+
memoryTier: tier, defaultModel: model, cloudFallback: false,
|
|
2031
|
+
}, null, 2) + '\n');
|
|
2032
|
+
} else {
|
|
2033
|
+
console.log(lines.join('\n'));
|
|
2034
|
+
}
|
|
2035
|
+
return 0;
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
if (sub === 'inspect') {
|
|
2039
|
+
const name = args._[2];
|
|
2040
|
+
if (!name) { console.error('Usage: agentic-security models inspect <model>'); return 4; }
|
|
2041
|
+
const cfg = ollamaEndpointConfig(env);
|
|
2042
|
+
if (!cfg.ok) { console.log(`✗ ${cfg.reason}`); return 1; }
|
|
2043
|
+
const modelsResult = await listOllamaModels({ host: cfg.config.host });
|
|
2044
|
+
if (!modelsResult.ok) { console.log(`✗ Ollama server is not reachable at ${cfg.config.host} (${modelsResult.code})`); return 1; }
|
|
2045
|
+
const info = modelsResult.models.find((m) => m.name === name);
|
|
2046
|
+
const family = classifyModelFamily(name);
|
|
2047
|
+
const capsResult = await getModelCapabilities({ host: cfg.config.host, model: name, probe: wantsProbe, force: wantsForceProbe });
|
|
2048
|
+
const caps = capsResult.capabilities;
|
|
2049
|
+
const out = { name, installed: !!info, family, capabilities: caps, cached: capsResult.cached, metadata: info || null };
|
|
2050
|
+
if (args.flags.json) { writeStdout(JSON.stringify(out, null, 2) + '\n'); return 0; }
|
|
2051
|
+
const capSourceLabel = caps.source?.runtimeProbe ? (capsResult.cached ? 'runtime-probed, cached' : 'runtime-probed')
|
|
2052
|
+
: caps.source?.metadata ? 'Ollama metadata' : 'hint only';
|
|
2053
|
+
console.log(`${name}`);
|
|
2054
|
+
console.log(` installed: ${out.installed ? 'yes' : 'no'}`);
|
|
2055
|
+
console.log(` family (hint): ${family}`);
|
|
2056
|
+
console.log(` capabilities (${capSourceLabel}): chat=${caps.chat} structuredJson=${caps.structuredJson} tools=${caps.tools} thinking=${caps.thinking}`);
|
|
2057
|
+
if (!wantsProbe) console.log(' (run with --probe to runtime-verify structured output / tool calling)');
|
|
2058
|
+
if (info) {
|
|
2059
|
+
console.log(` size: ${info.sizeBytes ? (info.sizeBytes / (1024 ** 3)).toFixed(1) + 'GB' : '?'}`);
|
|
2060
|
+
console.log(` parameters: ${info.parameterSize || '?'} quantization: ${info.quantization || '?'}`);
|
|
2061
|
+
}
|
|
2062
|
+
return 0;
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
if (sub === 'test') {
|
|
2066
|
+
// ollama-offline-prd.md §11.2 — `models test <model>` always runs the
|
|
2067
|
+
// real Layer C runtime probes (structured output + tool calling), unlike
|
|
2068
|
+
// doctor/inspect where --probe is opt-in: the whole point of `test` is
|
|
2069
|
+
// to spend the inference time and get a definitive answer, cached
|
|
2070
|
+
// afterward the same as any other probe.
|
|
2071
|
+
const name = args._[2];
|
|
2072
|
+
if (!name) { console.error('Usage: agentic-security models test <model>'); return 4; }
|
|
2073
|
+
const cfg = ollamaEndpointConfig(env);
|
|
2074
|
+
if (!cfg.ok) { console.log(`✗ ${cfg.reason}`); return 1; }
|
|
2075
|
+
const modelsResult = await listOllamaModels({ host: cfg.config.host });
|
|
2076
|
+
if (!modelsResult.ok) { console.log(`✗ Ollama server is not reachable at ${cfg.config.host} (${modelsResult.code})`); return 1; }
|
|
2077
|
+
if (!modelsResult.models.some((m) => m.name === name)) {
|
|
2078
|
+
console.log(`✗ Model '${name}' is not installed. Run \`ollama pull ${name}\` first.`);
|
|
2079
|
+
return 1;
|
|
2080
|
+
}
|
|
2081
|
+
// ollama-offline-prd.md premortem fix (2026-09): the capability cache has
|
|
2082
|
+
// no expiry beyond its version+digest+name key, which doesn't always
|
|
2083
|
+
// change on a same-tag re-pull — `--force` is the documented way to get
|
|
2084
|
+
// a definitively fresh answer right now, mirroring the existing
|
|
2085
|
+
// `validator-cache stats|gc --older-than <days>` convention rather than
|
|
2086
|
+
// leaving `test` unable to ever overrule its own past result.
|
|
2087
|
+
const capsResult = await getModelCapabilities({ host: cfg.config.host, model: name, probe: true, force: !!args.flags.force });
|
|
2088
|
+
const caps = capsResult.capabilities;
|
|
2089
|
+
if (args.flags.json) {
|
|
2090
|
+
writeStdout(JSON.stringify({ ok: true, name, capabilities: caps, cached: capsResult.cached }, null, 2) + '\n');
|
|
2091
|
+
return 0;
|
|
2092
|
+
}
|
|
2093
|
+
console.log(`agentic-security models test ${name}${args.flags.force ? ' --force' : ''}`);
|
|
2094
|
+
console.log('');
|
|
2095
|
+
console.log(` chat: ${caps.chat ? '✓ yes' : '✗ no'}`);
|
|
2096
|
+
console.log(` structured JSON: ${caps.structuredJson === true ? '✓ yes' : caps.structuredJson === false ? '✗ no' : '? unknown (probe inconclusive)'}`);
|
|
2097
|
+
console.log(` tool calling: ${caps.tools === true ? '✓ yes' : caps.tools === false ? '✗ no' : '? unknown (probe inconclusive)'}`);
|
|
2098
|
+
if (caps.contextTokens) console.log(` context window: ${caps.contextTokens} tokens`);
|
|
2099
|
+
console.log(` ${capsResult.cached ? '✓ capability probe cached (already ran before — pass --force to re-probe)' : '✓ capability probe ran (now cached)'}`);
|
|
2100
|
+
return 0;
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
if (sub === 'pull') {
|
|
2104
|
+
// ollama-offline-prd.md §11.2/§24: "models pull must be REFUSED when
|
|
2105
|
+
// strict offline mode is active because pulling a model requires network
|
|
2106
|
+
// access." This CLI treats loopback-enforced Ollama as the default,
|
|
2107
|
+
// always-on safety posture (see ollama-provider.js's header) rather than
|
|
2108
|
+
// a separately-toggled "strict mode", so `models pull` refuses
|
|
2109
|
+
// unconditionally and points at the real `ollama pull` instead of
|
|
2110
|
+
// silently shelling out to it — downloading model weights is
|
|
2111
|
+
// deliberately not something this CLI does on a user's behalf.
|
|
2112
|
+
const name = args._[2] || '<model>';
|
|
2113
|
+
console.log(
|
|
2114
|
+
`Model '${name}' is not installed, or you asked to pull it.\n\n` +
|
|
2115
|
+
'agentic-security never downloads model weights on your behalf — that keeps ' +
|
|
2116
|
+
'"offline mode" honest (no surprise egress the moment you run a scan).\n\n' +
|
|
2117
|
+
'Install it yourself, before disconnecting:\n' +
|
|
2118
|
+
` ollama pull ${name === '<model>' ? '<model>' : name}`,
|
|
2119
|
+
);
|
|
2120
|
+
return 1;
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
console.error('Usage: agentic-security models <list|status|doctor|inspect <model>|test <model>|pull <model>> [--host <url>] [--json] [--probe] [--force]');
|
|
2124
|
+
console.error(`Default Ollama host: ${DEFAULT_OLLAMA_HOST}`);
|
|
2125
|
+
return 4;
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
// `agentic-security ask "<question>" [target] [--max-iterations N]`
|
|
2129
|
+
//
|
|
2130
|
+
// ollama-offline-prd.md §18.2 — the one CLI surface for the bounded local
|
|
2131
|
+
// tool-calling agent loop (src/llm-validator/agent-loop.js). Deliberately a
|
|
2132
|
+
// separate command from `hunt` (structured candidate discovery, its own
|
|
2133
|
+
// propose/confirm/refute/judge pipeline) and `triage --explain/--poc`
|
|
2134
|
+
// (single-finding narrative, no tool access at all) — this is free-form Q&A
|
|
2135
|
+
// over the scanned project with READ-ONLY tool access, for questions that
|
|
2136
|
+
// don't map to either of those shapes ("which files touch this env var",
|
|
2137
|
+
// "does this project have a rate limiter").
|
|
2138
|
+
async function cmdAsk(args) {
|
|
2139
|
+
const goal = args._[1];
|
|
2140
|
+
if (!goal) { console.error('Usage: agentic-security ask "<question>" [target] [--max-iterations N] [--timeout-ms N]'); return 4; }
|
|
2141
|
+
const target = path.resolve(args._[2] && !args._[2].startsWith('--') ? args._[2] : '.');
|
|
2142
|
+
const { runAgentLoop, AGENT_LOOP_ERROR, DEFAULT_MAX_TOOL_ITERATIONS } = await import('../src/llm-validator/agent-loop.js');
|
|
2143
|
+
|
|
2144
|
+
const envOverride = {};
|
|
2145
|
+
if (args.flags.host) envOverride.AGENTIC_SECURITY_OLLAMA_HOST = String(args.flags.host);
|
|
2146
|
+
if (args.flags['allow-remote-ollama']) envOverride.AGENTIC_SECURITY_OLLAMA_ALLOW_REMOTE = '1';
|
|
2147
|
+
// ollama-offline-prd.md premortem fix (2026-09): --timeout-ms sets the
|
|
2148
|
+
// LOOP's own wall-clock budget (AGENTIC_SECURITY_LLM_AGENT_TIMEOUT_MS),
|
|
2149
|
+
// which used to be a fixed 5 minutes no matter how high
|
|
2150
|
+
// AGENTIC_SECURITY_LLM_TIMEOUT_MS (the PER-CALL timeout, still the right
|
|
2151
|
+
// knob for "this one model reply is slow") was raised — raising only the
|
|
2152
|
+
// per-call setting could not extend the loop, since a single slow call
|
|
2153
|
+
// could already exceed the whole fixed budget.
|
|
2154
|
+
if (args.flags['timeout-ms']) envOverride.AGENTIC_SECURITY_LLM_AGENT_TIMEOUT_MS = String(args.flags['timeout-ms']);
|
|
2155
|
+
const env = { ...process.env, ...envOverride };
|
|
2156
|
+
const maxToolIterations = args.flags['max-iterations'] ? parseInt(args.flags['max-iterations'], 10) : DEFAULT_MAX_TOOL_ITERATIONS;
|
|
2157
|
+
|
|
2158
|
+
const r = await runAgentLoop({ goal, scanRoot: target, env, maxToolIterations });
|
|
2159
|
+
// Adversarial-review fix (2026-09): surfaced here, not just in `models
|
|
2160
|
+
// doctor`, so a user who never runs `doctor` still sees it before/after
|
|
2161
|
+
// the exact command that would otherwise repeat a known OOM.
|
|
2162
|
+
if (r.priorOOMWarning) console.log(`⚠ ${r.priorOOMWarning}`);
|
|
2163
|
+
|
|
2164
|
+
if (!r.ok) {
|
|
2165
|
+
if (r.code === AGENT_LOOP_ERROR.NOT_CONFIGURED) {
|
|
2166
|
+
console.log('No local model configured for tool use — set AGENTIC_SECURITY_LLM_PRESET=ollama (see `agentic-security models doctor`).');
|
|
2167
|
+
} else if (r.code === AGENT_LOOP_ERROR.TOOLS_UNSUPPORTED) {
|
|
2168
|
+
console.log(`✗ ${r.reason}`);
|
|
2169
|
+
} else {
|
|
2170
|
+
console.log(`✗ ${r.code}: ${r.reason}`);
|
|
2171
|
+
}
|
|
2172
|
+
return 1;
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
if (args.flags.json) {
|
|
2176
|
+
writeStdout(JSON.stringify(r, null, 2) + '\n');
|
|
2177
|
+
return r.stopReason === 'complete' ? 0 : 1;
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
for (const call of r.toolCalls) {
|
|
2181
|
+
console.log(` → ${call.name}(${JSON.stringify(call.args)}) ${call.ok ? 'ok' : `failed: ${call.code}`}`);
|
|
2182
|
+
}
|
|
2183
|
+
console.log('');
|
|
2184
|
+
if (r.stopReason === 'complete') {
|
|
2185
|
+
console.log(r.finalText || '(no answer)');
|
|
2186
|
+
return 0;
|
|
2187
|
+
}
|
|
2188
|
+
if (r.stopReason === 'policy-violation') {
|
|
2189
|
+
console.log('✗ Stopped: the model requested a tool it was never offered.');
|
|
2190
|
+
return 1;
|
|
2191
|
+
}
|
|
2192
|
+
if (r.stopReason === 'max-iterations') {
|
|
2193
|
+
console.log(`✗ Stopped after the ${DEFAULT_MAX_TOOL_ITERATIONS}-iteration bound without a final answer. Try narrowing the question.`);
|
|
2194
|
+
return 1;
|
|
2195
|
+
}
|
|
2196
|
+
console.log('✗ Stopped: wall-clock timeout reached without a final answer. Try --timeout-ms <N> for a slow/cold-loading model (raising AGENTIC_SECURITY_LLM_TIMEOUT_MS alone does not extend this budget).');
|
|
2197
|
+
return 1;
|
|
2198
|
+
}
|
|
2199
|
+
|
|
1851
2200
|
// `agentic-security verify [--finding <id>] [--target <url>] [--live]`
|
|
1852
2201
|
//
|
|
1853
2202
|
// Re-runs the verifier loop over the most-recent scan. Without --live, it
|
|
@@ -3080,11 +3429,44 @@ async function cmdFix(args) {
|
|
|
3080
3429
|
}
|
|
3081
3430
|
}
|
|
3082
3431
|
|
|
3432
|
+
let ollamaFixMeta = null;
|
|
3083
3433
|
if (newContent === null) {
|
|
3084
|
-
|
|
3434
|
+
// ollama-offline-prd.md §33 — when no stored/deterministic patch exists,
|
|
3435
|
+
// AND an Ollama provider is configured for the fix role, ask it for a
|
|
3436
|
+
// proposal. Whatever comes back goes through the EXACT SAME
|
|
3437
|
+
// applyVerifiedFix() rescan/lint/test gate below as a deterministic
|
|
3438
|
+
// patch — this branch only ever changes where `newContent` came from,
|
|
3439
|
+
// never how it's verified.
|
|
3440
|
+
const { proposeOllamaFix, FIX_PROPOSAL_ERROR } = await import('../src/llm-validator/fix-proposal.js');
|
|
3441
|
+
const proposal = await proposeOllamaFix({
|
|
3442
|
+
finding: { file: f.file, line: f.line, vuln: f.vuln, cwe: f.cwe, severity: f.severity },
|
|
3443
|
+
fileContent: originalContent,
|
|
3444
|
+
scanRoot,
|
|
3445
|
+
});
|
|
3446
|
+
if (proposal.ok) {
|
|
3447
|
+
newContent = proposal.replacement;
|
|
3448
|
+
ollamaFixMeta = { model: proposal.model, rationale: proposal.rationale, expectedSecurityEffect: proposal.expectedSecurityEffect };
|
|
3449
|
+
} else if (proposal.code !== FIX_PROPOSAL_ERROR.NOT_CONFIGURED) {
|
|
3450
|
+
// A configured Ollama fix attempt that failed is worth surfacing
|
|
3451
|
+
// distinctly from "nothing was configured" — the operator asked for
|
|
3452
|
+
// AI-assisted fix and it didn't work, which is different from never
|
|
3453
|
+
// having asked.
|
|
3454
|
+
console.error(`Ollama fix proposal unavailable (${proposal.code}): ${proposal.reason}`);
|
|
3455
|
+
}
|
|
3456
|
+
}
|
|
3457
|
+
|
|
3458
|
+
if (newContent === null) {
|
|
3459
|
+
console.error('No mechanical fix is available for this finding. Use the security-fixer subagent (default `fix` mode), or configure AGENTIC_SECURITY_LLM_PRESET=ollama for an AI-assisted proposal, then apply with `--apply`.');
|
|
3085
3460
|
return 4;
|
|
3086
3461
|
}
|
|
3087
3462
|
|
|
3463
|
+
if (ollamaFixMeta) {
|
|
3464
|
+
console.log(`AI-assisted proposal (model: ${ollamaFixMeta.model}) — not yet mechanically verified:`);
|
|
3465
|
+
if (ollamaFixMeta.rationale) console.log(` rationale: ${ollamaFixMeta.rationale}`);
|
|
3466
|
+
if (ollamaFixMeta.expectedSecurityEffect) console.log(` expected effect: ${ollamaFixMeta.expectedSecurityEffect}`);
|
|
3467
|
+
console.log('');
|
|
3468
|
+
}
|
|
3469
|
+
|
|
3088
3470
|
if (isPreview) {
|
|
3089
3471
|
console.log(previewDiff(originalContent, newContent, f.file));
|
|
3090
3472
|
console.log('\nRun with --apply to write this change. Use `agentic-security undo` to revert.');
|
|
@@ -3116,12 +3498,51 @@ async function cmdFix(args) {
|
|
|
3116
3498
|
const fixMeta = (approvedBy || approvalReason || patchAuthor)
|
|
3117
3499
|
? { approval: { approvedBy: approvedBy || '', reason: approvalReason || '' }, ...(patchAuthor ? { author: patchAuthor } : {}) }
|
|
3118
3500
|
: null;
|
|
3119
|
-
|
|
3501
|
+
let result = await applyVerifiedFix({
|
|
3120
3502
|
scanRoot,
|
|
3121
3503
|
finding: { file: f.file, id: f.id, stableId: f.stableId || null, ruleId: f.cwe || f.title, vuln: f.vuln || f.title },
|
|
3122
3504
|
files: { [f.file]: newContent },
|
|
3123
3505
|
fixMeta,
|
|
3124
3506
|
});
|
|
3507
|
+
// Adversarial-review fix (2026-09): an Ollama-sourced patch that gets
|
|
3508
|
+
// rejected used to just fail outright — at temperature 0, a bare re-run
|
|
3509
|
+
// of `fix` would almost certainly ask the identical question and get the
|
|
3510
|
+
// identical bad patch back, burning the finite retry budget
|
|
3511
|
+
// (applyVerifiedFix's own maxAttempts) with no chance of a better outcome.
|
|
3512
|
+
// Orchestration test coverage: test/cli/fix-retry.test.js (real spawned
|
|
3513
|
+
// CLI + fake Ollama server; statically skipped in this sandbox's
|
|
3514
|
+
// documented child-process-loopback limitation, same as
|
|
3515
|
+
// test/cli/models.test.js — flip to `test` where a spawned child can
|
|
3516
|
+
// reach the parent's loopback server).
|
|
3517
|
+
// ONE bounded retry, feeding the gate's own rejection reason back into the
|
|
3518
|
+
// prompt so the model has an actual reason to propose something different
|
|
3519
|
+
// — never more than one, matching this codebase's existing "exactly one
|
|
3520
|
+
// retry" philosophy for model output elsewhere (callOllamaStructured's
|
|
3521
|
+
// schema retry). A deterministic/stored patch (no ollamaFixMeta) is never
|
|
3522
|
+
// retried — asking the same static template again cannot produce a
|
|
3523
|
+
// different answer.
|
|
3524
|
+
if (!result.ok && ollamaFixMeta && !result.budgetExceeded) {
|
|
3525
|
+
console.log(`AI-assisted proposal was rejected (${result.reason}) — asking for one revised attempt...`);
|
|
3526
|
+
const { proposeOllamaFix } = await import('../src/llm-validator/fix-proposal.js');
|
|
3527
|
+
const retryProposal = await proposeOllamaFix({
|
|
3528
|
+
finding: { file: f.file, line: f.line, vuln: f.vuln, cwe: f.cwe, severity: f.severity },
|
|
3529
|
+
fileContent: originalContent,
|
|
3530
|
+
scanRoot,
|
|
3531
|
+
rejectionFeedback: result.reason,
|
|
3532
|
+
});
|
|
3533
|
+
if (retryProposal.ok) {
|
|
3534
|
+
const retryResult = await applyVerifiedFix({
|
|
3535
|
+
scanRoot,
|
|
3536
|
+
finding: { file: f.file, id: f.id, stableId: f.stableId || null, ruleId: f.cwe || f.title, vuln: f.vuln || f.title },
|
|
3537
|
+
files: { [f.file]: retryProposal.replacement },
|
|
3538
|
+
fixMeta,
|
|
3539
|
+
});
|
|
3540
|
+
result = retryResult;
|
|
3541
|
+
if (retryResult.ok) {
|
|
3542
|
+
console.log(` revised proposal accepted (rationale: ${retryProposal.rationale || 'n/a'})`);
|
|
3543
|
+
}
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3125
3546
|
if (!result.ok) {
|
|
3126
3547
|
console.error(`Refusing to apply: ${result.reason}`);
|
|
3127
3548
|
if (result.budgetExceeded) console.error(` (${result.attempts}/${result.maxAttempts} attempts already made for this finding)`);
|
|
@@ -3180,7 +3601,105 @@ async function cmdUndo(args) {
|
|
|
3180
3601
|
return 0;
|
|
3181
3602
|
}
|
|
3182
3603
|
|
|
3604
|
+
// ollama-offline-prd.md §11.1's example transcript is an INTERACTIVE numbered
|
|
3605
|
+
// menu. This codebase has no interactive-prompt precedent anywhere else —
|
|
3606
|
+
// every other command here is flag-driven and scriptable, deliberately (CI
|
|
3607
|
+
// use is the primary case). Rather than introduce the first readline-based
|
|
3608
|
+
// prompt in the CLI, `setup --llm ollama` follows the PRD's own explicitly-
|
|
3609
|
+
// supported NONINTERACTIVE shape as the only shape: discover, pick a sane
|
|
3610
|
+
// memory-tier default when `--model` is omitted (recommend-and-proceed, not
|
|
3611
|
+
// block-and-ask), confirm the offline guarantee, print exactly what to
|
|
3612
|
+
// export. `--offline` is accepted for parity with the PRD's example command
|
|
3613
|
+
// line but is confirmatory only — Ollama's loopback enforcement is already
|
|
3614
|
+
// always-on by default (ollama-provider.js), not a mode this flag toggles.
|
|
3615
|
+
async function cmdSetupLlmOllama(args) {
|
|
3616
|
+
const {
|
|
3617
|
+
ollamaEndpointConfig, listOllamaModels, DEFAULT_OLLAMA_HOST,
|
|
3618
|
+
} = await import('../src/llm-validator/ollama-provider.js');
|
|
3619
|
+
const { detectSystemMemory, detectMemoryTier, recommendAdmission, classifyModelFamily } = await import('../src/llm-validator/model-capabilities.js');
|
|
3620
|
+
|
|
3621
|
+
const envOverride = {};
|
|
3622
|
+
if (args.flags.host) envOverride.AGENTIC_SECURITY_OLLAMA_HOST = String(args.flags.host);
|
|
3623
|
+
const env = { ...process.env, ...envOverride };
|
|
3624
|
+
const cfg = ollamaEndpointConfig(env);
|
|
3625
|
+
console.log('Local LLM provider: Ollama');
|
|
3626
|
+
if (!cfg.ok) {
|
|
3627
|
+
console.log(`✗ ${cfg.reason}`);
|
|
3628
|
+
return 1;
|
|
3629
|
+
}
|
|
3630
|
+
console.log(`Server: ${cfg.config.host}`);
|
|
3631
|
+
|
|
3632
|
+
const modelsResult = await listOllamaModels({ host: cfg.config.host });
|
|
3633
|
+
if (!modelsResult.ok) {
|
|
3634
|
+
console.log(`✗ Ollama server is not reachable at ${cfg.config.host} (${modelsResult.code}: ${modelsResult.reason})`);
|
|
3635
|
+
console.log('\nInstall Ollama and pull a model first — see docs/guides/ollama.md.');
|
|
3636
|
+
console.log('The deterministic scanner will still run. No cloud provider will be used automatically.');
|
|
3637
|
+
return 1;
|
|
3638
|
+
}
|
|
3639
|
+
console.log('Status: reachable');
|
|
3640
|
+
console.log('');
|
|
3641
|
+
if (modelsResult.models.length === 0) {
|
|
3642
|
+
console.log('No models installed. Pull one first, e.g.:');
|
|
3643
|
+
console.log(' ollama pull qwen3.5:4b');
|
|
3644
|
+
return 1;
|
|
3645
|
+
}
|
|
3646
|
+
console.log('Installed models:');
|
|
3647
|
+
for (const m of modelsResult.models) {
|
|
3648
|
+
const sizeGb = Number.isFinite(m.sizeBytes) ? (m.sizeBytes / (1024 ** 3)).toFixed(1) + 'GB' : '?';
|
|
3649
|
+
console.log(` ${m.name.padEnd(24)} ${sizeGb}`);
|
|
3650
|
+
}
|
|
3651
|
+
console.log('');
|
|
3652
|
+
|
|
3653
|
+
let chosen = args.flags.model ? String(args.flags.model) : null;
|
|
3654
|
+
if (chosen && !modelsResult.models.some((m) => m.name === chosen)) {
|
|
3655
|
+
console.log(`↗ '${chosen}' is not currently installed — pull it first with \`ollama pull ${chosen}\`. Continuing with this choice anyway.`);
|
|
3656
|
+
}
|
|
3657
|
+
if (!chosen) {
|
|
3658
|
+
// Recommend-and-proceed: pick the memory-tier's preferred model if it's
|
|
3659
|
+
// installed, else the first installed model, rather than blocking on an
|
|
3660
|
+
// interactive prompt this CLI has no precedent for.
|
|
3661
|
+
const mem = detectSystemMemory();
|
|
3662
|
+
const tier = detectMemoryTier(mem.totalBytes);
|
|
3663
|
+
const installedNames = new Set(modelsResult.models.map((m) => m.name));
|
|
3664
|
+
const preferredByTier = tier === '8gb' ? 'qwen3.5:4b'
|
|
3665
|
+
: modelsResult.models.some((m) => classifyModelFamily(m.name) === 'gemma4') ? null : 'qwen3.5:9b';
|
|
3666
|
+
chosen = (preferredByTier && installedNames.has(preferredByTier)) ? preferredByTier : modelsResult.models[0].name;
|
|
3667
|
+
console.log(`Choose default model: ${chosen} (auto-selected for your ${tier === 'unknown' ? 'detected' : tier} memory tier — pass --model to choose a different installed model)`);
|
|
3668
|
+
} else {
|
|
3669
|
+
console.log(`Choose default model: ${chosen}`);
|
|
3670
|
+
}
|
|
3671
|
+
console.log('');
|
|
3672
|
+
|
|
3673
|
+
const mem = detectSystemMemory();
|
|
3674
|
+
const tier = detectMemoryTier(mem.totalBytes);
|
|
3675
|
+
const profileKey = tier === '8gb' ? '8gb' : (classifyModelFamily(chosen) === 'gemma4' ? '16gb-gemma' : '16gb-qwen');
|
|
3676
|
+
const admission = recommendAdmission({ profile: profileKey, freeBytes: mem.freeBytes, requestedModel: chosen });
|
|
3677
|
+
|
|
3678
|
+
console.log('Offline LLM mode:');
|
|
3679
|
+
console.log(' ✓ Only loopback model requests are allowed');
|
|
3680
|
+
console.log(' ✓ No cloud fallback');
|
|
3681
|
+
console.log(' ✓ Missing local model falls back to deterministic-only scanning');
|
|
3682
|
+
if (!admission.admitted) {
|
|
3683
|
+
console.log(` ↗ Memory admission check: ${admission.reason}`);
|
|
3684
|
+
}
|
|
3685
|
+
if (admission.priorOOMWarning) console.log(` ⚠ ${admission.priorOOMWarning}`);
|
|
3686
|
+
console.log('');
|
|
3687
|
+
console.log('To use this configuration, export:');
|
|
3688
|
+
console.log(' export AGENTIC_SECURITY_LLM_PRESET=ollama');
|
|
3689
|
+
console.log(` export AGENTIC_SECURITY_LLM_MODEL=${chosen}`);
|
|
3690
|
+
if (cfg.config.host !== DEFAULT_OLLAMA_HOST) console.log(` export AGENTIC_SECURITY_OLLAMA_HOST=${cfg.config.host}`);
|
|
3691
|
+
console.log('');
|
|
3692
|
+
console.log('Then verify with: agentic-security models doctor');
|
|
3693
|
+
return 0;
|
|
3694
|
+
}
|
|
3695
|
+
|
|
3183
3696
|
async function cmdSetup(args) {
|
|
3697
|
+
if (args.flags.llm) {
|
|
3698
|
+
const provider = String(args.flags.llm).toLowerCase();
|
|
3699
|
+
if (provider === 'ollama') return cmdSetupLlmOllama(args);
|
|
3700
|
+
console.error(`agentic-security setup --llm: unsupported provider "${provider}" (only "ollama" runs a discovery/config flow here — other providers (anthropic/openai/gemini/local) configure via their AGENTIC_SECURITY_LLM_* env vars directly, no setup step needed).`);
|
|
3701
|
+
return 4;
|
|
3702
|
+
}
|
|
3184
3703
|
const projectDir = path.resolve(args._[1] || '.');
|
|
3185
3704
|
const commandsDir = path.join(projectDir, '.claude', 'commands');
|
|
3186
3705
|
await fsp.mkdir(commandsDir, { recursive: true });
|
|
@@ -6353,6 +6872,16 @@ async function main() {
|
|
|
6353
6872
|
checkNodeVersionOrExit();
|
|
6354
6873
|
const args = parseArgs(process.argv.slice(2));
|
|
6355
6874
|
const cmd = args._[0];
|
|
6875
|
+
// ollama-offline-prd.md §11.3/§10.2 — CLI flags win over every other
|
|
6876
|
+
// configuration layer. A generic bridge here (rather than per-command
|
|
6877
|
+
// flag parsing in cmdHunt/cmdSecure/etc.) means `--llm`/`--model` work
|
|
6878
|
+
// the same way on any command without duplicating the same six lines —
|
|
6879
|
+
// commands that don't consult an LLM provider simply never read the env
|
|
6880
|
+
// vars this sets, so this is a no-op for them.
|
|
6881
|
+
if (args.flags.llm) process.env.AGENTIC_SECURITY_LLM_PRESET = String(args.flags.llm);
|
|
6882
|
+
if (args.flags.model) process.env.AGENTIC_SECURITY_LLM_MODEL = String(args.flags.model);
|
|
6883
|
+
if (args.flags['allow-remote-ollama']) process.env.AGENTIC_SECURITY_OLLAMA_ALLOW_REMOTE = '1';
|
|
6884
|
+
if (args.flags['ollama-host']) process.env.AGENTIC_SECURITY_OLLAMA_HOST = String(args.flags['ollama-host']);
|
|
6356
6885
|
try {
|
|
6357
6886
|
switch (cmd) {
|
|
6358
6887
|
case 'scan': process.exit(await cmdScan(args));
|
|
@@ -6370,6 +6899,8 @@ async function main() {
|
|
|
6370
6899
|
case 'secure': process.exit(await cmdSecure(args));
|
|
6371
6900
|
case 'packs': process.exit(await cmdPacks(args));
|
|
6372
6901
|
case 'validator-cache': process.exit(await cmdValidatorCache(args));
|
|
6902
|
+
case 'models': process.exit(await cmdModels(args));
|
|
6903
|
+
case 'ask': process.exit(await cmdAsk(args));
|
|
6373
6904
|
case 'verify': process.exit(await cmdVerify(args));
|
|
6374
6905
|
case 'reset': process.exit(await cmdReset(args));
|
|
6375
6906
|
case 'export': process.exit(await cmdExport(args));
|