@clear-capabilities/agentic-security-scanner 0.150.1 → 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.
Files changed (86) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/bin/agentic-security.js +73 -10
  3. package/dist/1122.index.js +16 -16
  4. package/dist/1208.index.js +23 -0
  5. package/dist/1301.index.js +3 -3
  6. package/dist/1379.index.js +3 -3
  7. package/dist/1444.index.js +3 -3
  8. package/dist/1660.index.js +1 -1
  9. package/dist/1700.index.js +5 -5
  10. package/dist/1905.index.js +12 -12
  11. package/dist/1920.index.js +10 -10
  12. package/dist/2238.index.js +4 -4
  13. package/dist/2271.index.js +3 -3
  14. package/dist/2432.index.js +8 -8
  15. package/dist/2659.index.js +1 -1
  16. package/dist/2826.index.js +2 -2
  17. package/dist/2830.index.js +3 -3
  18. package/dist/2923.index.js +1 -1
  19. package/dist/3001.index.js +4 -4
  20. package/dist/3117.index.js +4 -4
  21. package/dist/3180.index.js +9 -9
  22. package/dist/3276.index.js +1 -1
  23. package/dist/3415.index.js +1 -1
  24. package/dist/3499.index.js +1 -1
  25. package/dist/3518.index.js +4 -4
  26. package/dist/3736.index.js +7 -7
  27. package/dist/3839.index.js +4 -4
  28. package/dist/4113.index.js +441 -418
  29. package/dist/4265.index.js +4 -4
  30. package/dist/4384.index.js +8 -3
  31. package/dist/4399.index.js +29 -3
  32. package/dist/4547.index.js +2 -2
  33. package/dist/4863.index.js +6 -6
  34. package/dist/4970.index.js +2 -2
  35. package/dist/5051.index.js +15 -15
  36. package/dist/5144.index.js +4 -4
  37. package/dist/5333.index.js +8 -8
  38. package/dist/5343.index.js +2 -2
  39. package/dist/5350.index.js +6 -6
  40. package/dist/5561.index.js +1 -1
  41. package/dist/5637.index.js +10 -5
  42. package/dist/5724.index.js +21642 -0
  43. package/dist/5756.index.js +99 -489
  44. package/dist/5830.index.js +3 -3
  45. package/dist/6257.index.js +21 -8
  46. package/dist/6626.index.js +5 -5
  47. package/dist/6662.index.js +1 -1
  48. package/dist/6675.index.js +4 -4
  49. package/dist/6730.index.js +10 -10
  50. package/dist/6829.index.js +4 -4
  51. package/dist/6944.index.js +2 -2
  52. package/dist/6994.index.js +5 -5
  53. package/dist/7039.index.js +83 -19
  54. package/dist/7178.index.js +11 -6
  55. package/dist/7227.index.js +3 -3
  56. package/dist/7552.index.js +2 -2
  57. package/dist/7709.index.js +2 -2
  58. package/dist/7838.index.js +1037 -0
  59. package/dist/8218.index.js +4 -4
  60. package/dist/8476.index.js +4 -4
  61. package/dist/8513.index.js +8 -8
  62. package/dist/8520.index.js +1 -1
  63. package/dist/8752.index.js +4 -4
  64. package/dist/9207.index.js +2 -2
  65. package/dist/9220.index.js +5 -5
  66. package/dist/9390.index.js +6 -6
  67. package/dist/9503.index.js +2 -2
  68. package/dist/9560.index.js +3221 -0
  69. package/dist/957.index.js +4 -4
  70. package/dist/9801.index.js +1 -1
  71. package/dist/9824.index.js +5 -5
  72. package/dist/agentic-security.mjs +23 -23
  73. package/dist/agentic-security.mjs.sha256 +1 -1
  74. package/package.json +3 -2
  75. package/src/egress/redact.js +1 -1
  76. package/src/engine.js +13 -2
  77. package/src/llm-validator/agent-loop.js +85 -5
  78. package/src/llm-validator/fix-proposal.js +16 -3
  79. package/src/llm-validator/model-capabilities.js +26 -1
  80. package/src/llm-validator/model-probe.js +46 -8
  81. package/src/llm-validator/ollama-provider.js +26 -1
  82. package/src/llm-validator/oom-feedback.js +69 -0
  83. package/src/llm-validator/providers.js +50 -0
  84. package/src/llm-validator/redact.js +177 -11
  85. package/src/report/index.js +14 -3
  86. package/src/sast/java-ast-folding.js +18 -2
@@ -1,8 +1,8 @@
1
1
  export const id = 5756;
2
- export const ids = [5756,4399,7039];
2
+ export const ids = [5756];
3
3
  export const modules = {
4
4
 
5
- /***/ 5756:
5
+ /***/ 95756:
6
6
  /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
7
7
 
8
8
 
@@ -14,19 +14,19 @@ __webpack_require__.d(__webpack_exports__, {
14
14
  });
15
15
 
16
16
  // EXTERNAL MODULE: ./src/llm-validator/ollama-provider.js
17
- var ollama_provider = __webpack_require__(3837);
17
+ var ollama_provider = __webpack_require__(23837);
18
18
  // EXTERNAL MODULE: ./src/llm-validator/providers.js
19
- var providers = __webpack_require__(8947);
19
+ var providers = __webpack_require__(38947);
20
20
  // EXTERNAL MODULE: ./src/egress/policy.js
21
- var policy = __webpack_require__(5712);
21
+ var policy = __webpack_require__(45712);
22
22
  // EXTERNAL MODULE: external "node:fs"
23
- var external_node_fs_ = __webpack_require__(3024);
23
+ var external_node_fs_ = __webpack_require__(73024);
24
24
  // EXTERNAL MODULE: external "node:path"
25
- var external_node_path_ = __webpack_require__(6760);
25
+ var external_node_path_ = __webpack_require__(76760);
26
26
  // EXTERNAL MODULE: ./src/mcp/validate.js
27
- var validate = __webpack_require__(1211);
27
+ var validate = __webpack_require__(61211);
28
28
  // EXTERNAL MODULE: ./src/egress/redact.js + 1 modules
29
- var redact = __webpack_require__(4831);
29
+ var redact = __webpack_require__(74831);
30
30
  ;// CONCATENATED MODULE: ./src/llm-validator/agent-tools.js
31
31
  // PRD §18.2/§18.3 — the bounded local agent loop's tool registry.
32
32
  //
@@ -301,9 +301,11 @@ async function runTool(name, rawArgs, { scanRoot, statePath }) {
301
301
  }
302
302
 
303
303
  // EXTERNAL MODULE: ./src/llm-validator/model-probe.js
304
- var model_probe = __webpack_require__(7039);
304
+ var model_probe = __webpack_require__(27039);
305
305
  // EXTERNAL MODULE: ./src/posture/state-dir.js
306
- var state_dir = __webpack_require__(1174);
306
+ var state_dir = __webpack_require__(31174);
307
+ // EXTERNAL MODULE: ./src/llm-validator/oom-feedback.js
308
+ var oom_feedback = __webpack_require__(6782);
307
309
  ;// CONCATENATED MODULE: ./src/llm-validator/agent-loop.js
308
310
  // PRD §18.2/§18.4 — the bounded local Ollama tool-calling agent loop.
309
311
  //
@@ -334,6 +336,7 @@ var state_dir = __webpack_require__(1174);
334
336
 
335
337
 
336
338
 
339
+
337
340
  const AGENT_LOOP_ERROR = Object.freeze({
338
341
  NOT_CONFIGURED: 'agent-loop-not-configured',
339
342
  POLICY_BLOCKED: 'agent-loop-policy-blocked',
@@ -344,6 +347,35 @@ const AGENT_LOOP_ERROR = Object.freeze({
344
347
  const DEFAULT_MAX_TOOL_ITERATIONS = 12;
345
348
  const DEFAULT_WALL_CLOCK_TIMEOUT_MS = 5 * 60 * 1000;
346
349
 
350
+ // Adversarial-review finding (2026-09), confirmed against a real, slow
351
+ // (CPU-only) Ollama server: the wall-clock deadline used to be checked ONLY
352
+ // at the top of each iteration, never around the in-flight callOllamaChat
353
+ // itself. `docs/guides/ollama.md` tells users to raise
354
+ // AGENTIC_SECURITY_LLM_TIMEOUT_MS for a cold-loading model — that value
355
+ // flows into the PER-CALL requestTimeoutMs, which used to be entirely
356
+ // independent of this loop's own wall-clock budget. A single call already
357
+ // running when the wall clock expires would complete anyway (its own
358
+ // timeout could be minutes longer), and only the NEXT iteration's top-of-
359
+ // loop check would notice — reporting `wall-clock-timeout` after exactly
360
+ // one useful call, no matter how high the per-call timeout was raised,
361
+ // which made the documented remedy for slow models actively useless.
362
+ //
363
+ // Fix: cap the PER-CALL requestTimeoutMs at whatever wall-clock budget
364
+ // actually remains, every iteration. A slow call now gets cut off by ITS
365
+ // OWN timeout mechanism (producing the same clean `{ok:false,
366
+ // code:'ollama-timeout'}` every other caller already handles) at exactly
367
+ // the moment the wall clock would have run out anyway — never later. This
368
+ // also makes the two settings coherent for the first time: raising
369
+ // AGENTIC_SECURITY_LLM_TIMEOUT_MS now genuinely helps, as long as the loop's
370
+ // OWN budget (wallClockTimeoutMs / AGENTIC_SECURITY_LLM_AGENT_TIMEOUT_MS)
371
+ // is raised enough to give it room.
372
+ function _cappedTimeouts(baseTimeouts, remainingMs) {
373
+ if (!baseTimeouts) return { requestTimeoutMs: Math.max(1, remainingMs) };
374
+ const base = Number(baseTimeouts.requestTimeoutMs);
375
+ const capped = Number.isFinite(base) ? Math.min(base, remainingMs) : remainingMs;
376
+ return { ...baseTimeouts, requestTimeoutMs: Math.max(1, capped) };
377
+ }
378
+
347
379
  function systemPrompt(scanRoot) {
348
380
  return [
349
381
  'You are a security-scan assistant with READ-ONLY access to the scanned',
@@ -361,14 +393,49 @@ function systemPrompt(scanRoot) {
361
393
  * maxToolIterations?:number, wallClockTimeoutMs?:number}} opts
362
394
  * `statePath` defaults to posture/state-dir.js's real implementation;
363
395
  * overridable only for tests that need a fixture-scoped state dir.
396
+ * `wallClockTimeoutMs`, when not passed explicitly, falls back to
397
+ * `AGENTIC_SECURITY_LLM_AGENT_TIMEOUT_MS` — a SEPARATE setting from
398
+ * `AGENTIC_SECURITY_LLM_TIMEOUT_MS` (the per-call timeout) on purpose: the
399
+ * two used to be incoherent (raising the per-call setting alone did nothing
400
+ * for a loop that could still time out after one call), so a caller who
401
+ * genuinely needs a longer overall budget for a slow model must raise BOTH.
364
402
  * @returns {{ok:true, finalText, iterations, toolCalls, stopReason} |
365
403
  * {ok:false, code, reason}}
366
404
  */
367
- async function runAgentLoop({
405
+ async function runAgentLoop(opts = {}) {
406
+ const result = await _runAgentLoopCore(opts);
407
+ // Adversarial-review fix (2026-09, second pass): Round 1's original OOM-
408
+ // feedback fix only surfaced `priorOOMWarning` in `models doctor`'s
409
+ // advisory output — a user who never happens to run `doctor` would OOM
410
+ // again on the exact same model via `ask` with no warning at all, since
411
+ // `recommendAdmission` (where the warning lives) is never consulted on
412
+ // this real call path. Surface it here too, on any outcome where a real
413
+ // call was actually attempted (a pure config/capability refusal before
414
+ // any call has nothing useful to warn about).
415
+ const attemptedARealCall = result.ok || result.code === AGENT_LOOP_ERROR.FAILED;
416
+ if (attemptedARealCall) {
417
+ const resolved = (0,providers.resolveProvider)({ role: 'hunt', env: opts.env || process.env });
418
+ const prior = resolved.ok ? (0,oom_feedback/* priorOOMFor */.NL)(resolved.config.model) : null;
419
+ if (prior) {
420
+ return {
421
+ ...result,
422
+ priorOOMWarning: `'${resolved.config.model}' has previously failed with an out-of-memory error on this machine ` +
423
+ `(${prior.count} time${prior.count === 1 ? '' : 's'}, most recently ${new Date(prior.lastAt).toISOString()}).`,
424
+ };
425
+ }
426
+ }
427
+ return result;
428
+ }
429
+
430
+ async function _runAgentLoopCore({
368
431
  goal, scanRoot, env = process.env, statePath = state_dir.statePath,
369
- maxToolIterations = DEFAULT_MAX_TOOL_ITERATIONS, wallClockTimeoutMs = DEFAULT_WALL_CLOCK_TIMEOUT_MS,
432
+ maxToolIterations = DEFAULT_MAX_TOOL_ITERATIONS, wallClockTimeoutMs,
370
433
  } = {}) {
371
434
  const boundedIterations = Math.max(1, Math.min(maxToolIterations, DEFAULT_MAX_TOOL_ITERATIONS));
435
+ if (wallClockTimeoutMs === undefined) {
436
+ const fromEnv = Number(env.AGENTIC_SECURITY_LLM_AGENT_TIMEOUT_MS);
437
+ wallClockTimeoutMs = Number.isFinite(fromEnv) && fromEnv > 0 ? fromEnv : DEFAULT_WALL_CLOCK_TIMEOUT_MS;
438
+ }
372
439
 
373
440
  const resolved = (0,providers.resolveProvider)({ role: 'hunt', env });
374
441
  if (!resolved.ok || resolved.config.provider !== 'ollama') {
@@ -404,15 +471,30 @@ async function runAgentLoop({
404
471
  const deadline = Date.now() + boundedTimeoutMs;
405
472
 
406
473
  for (let iteration = 0; iteration < boundedIterations; iteration++) {
407
- if (Date.now() >= deadline) {
474
+ const remainingMs = deadline - Date.now();
475
+ if (remainingMs <= 0) {
408
476
  return { ok: true, finalText: null, iterations: iteration, toolCalls: toolCallLog, stopReason: 'wall-clock-timeout' };
409
477
  }
410
478
 
479
+ // Cap this call's own timeout at whatever wall-clock budget remains, so
480
+ // a slow call can never silently outlive the loop's overall bound (see
481
+ // the header comment above _cappedTimeouts for the incident this fixes).
482
+ const callTimeouts = _cappedTimeouts(timeouts, remainingMs);
483
+ const deadlineWasBinding = timeouts && Number(timeouts.requestTimeoutMs) > remainingMs;
411
484
  const r = await (0,ollama_provider/* callOllamaChat */.L5)({
412
485
  host: resolved.config.endpoint, model: resolved.config.model, messages,
413
- tools: TOOL_DEFINITIONS, keepAlive: oc?.keepAlive, timeouts,
486
+ tools: TOOL_DEFINITIONS, keepAlive: oc?.keepAlive, timeouts: callTimeouts,
414
487
  });
415
- if (!r.ok) return { ok: false, code: AGENT_LOOP_ERROR.FAILED, reason: r.reason || r.code };
488
+ if (!r.ok) {
489
+ // A timeout caused by the WALL CLOCK (not the operator's own per-call
490
+ // setting) is this loop doing exactly what it's supposed to, not an
491
+ // unexpected error — report it the same way the pre-flight check
492
+ // above does, rather than as a hard failure.
493
+ if (r.code === 'ollama-timeout' && deadlineWasBinding) {
494
+ return { ok: true, finalText: null, iterations: iteration, toolCalls: toolCallLog, stopReason: 'wall-clock-timeout' };
495
+ }
496
+ return { ok: false, code: AGENT_LOOP_ERROR.FAILED, reason: r.reason || r.code };
497
+ }
416
498
 
417
499
  const toolCalls = r.result.toolCalls || [];
418
500
  if (toolCalls.length === 0) {
@@ -444,479 +526,7 @@ async function runAgentLoop({
444
526
 
445
527
  /***/ }),
446
528
 
447
- /***/ 4399:
448
- /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
449
-
450
- /* harmony export */ __webpack_require__.d(__webpack_exports__, {
451
- /* harmony export */ MEMORY_PROFILES: () => (/* binding */ MEMORY_PROFILES),
452
- /* harmony export */ capabilitiesFromFamilyHint: () => (/* binding */ capabilitiesFromFamilyHint),
453
- /* harmony export */ classifyModelFamily: () => (/* binding */ classifyModelFamily),
454
- /* harmony export */ detectMemoryTier: () => (/* binding */ detectMemoryTier),
455
- /* harmony export */ detectSystemMemory: () => (/* binding */ detectSystemMemory),
456
- /* harmony export */ recommendAdmission: () => (/* binding */ recommendAdmission)
457
- /* harmony export */ });
458
- /* unused harmony exports KNOWN_MODEL_SIZE_GB, evaluateMemoryAdmission */
459
- /* harmony import */ var node_os__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(8161);
460
- // Model family hints, RAM-aware memory profiles, and the memory-admission
461
- // check for the Ollama provider (agentic-security-ollama-offline-prd.md
462
- // §13, §14, §15, §22.3, §30).
463
- //
464
- // FAMILY HINTS ARE DEFAULTS, NEVER AUTHORITY (PRD §12/§13). A name like
465
- // `gemma4:e2b` tells us nothing Ollama itself won't confirm — it only lets the
466
- // harness suggest a sane default before any network call. If a model actually
467
- // installed under a family-hinted name lacks a capability the hint implied,
468
- // the runtime probe (model-probe.js, added when tool-calling/structured-output
469
- // probing lands) always wins. This module only classifies and estimates; it
470
- // never asserts a capability is present.
471
- //
472
- // MEMORY NUMBERS ARE ESTIMATES, NOT PROMISES (PRD §22.3, §14.1). Ollama
473
- // artifact sizes and this module's headroom reserves are best-effort figures
474
- // sourced from what Ollama currently publishes; they exist so the harness can
475
- // fail BEFORE an OS-level OOM, not so it can claim an exact answer. Every
476
- // admission decision leaves a stated safety margin rather than trying to pack
477
- // memory to the byte.
478
-
479
-
480
-
481
- // PRD §12/§13 FR-1203 — non-authoritative family hint from a model name.
482
- // Longest/most-specific pattern first so `qwen3.5:4b` doesn't fall through to
483
- // the bare `qwen` bucket.
484
- const FAMILY_PATTERNS = [
485
- [/^qwen3\.5/i, 'qwen3.5'],
486
- [/^qwen3-coder-next/i, 'qwen3-coder-next'],
487
- [/^qwen3-coder/i, 'qwen3-coder'],
488
- [/^qwen2\.5-coder/i, 'qwen2.5-coder'],
489
- [/^qwen3/i, 'qwen3'],
490
- [/^qwen/i, 'qwen'],
491
- [/^gemma4/i, 'gemma4'],
492
- [/^functiongemma/i, 'functiongemma'],
493
- [/^gemma3/i, 'gemma3'],
494
- [/^gemma/i, 'gemma'],
495
- ];
496
-
497
- /** Non-authoritative family classification for defaults/messaging only. */
498
- function classifyModelFamily(modelName) {
499
- const name = String(modelName || '').trim();
500
- for (const [re, family] of FAMILY_PATTERNS) if (re.test(name)) return family;
501
- return 'unknown';
502
- }
503
-
504
- // PRD §13.1 — non-authoritative defaults per family, overridden by any real
505
- // runtime probe result (model-probe.js). `tools`/`structuredJson`/`thinking`
506
- // are 'unknown' where Ollama's own behavior varies by specific tag/quant
507
- // rather than by family alone.
508
- const FAMILY_CAPABILITY_HINTS = {
509
- 'qwen3.5': { chat: true, structuredJson: true, tools: true, thinking: 'unknown' },
510
- qwen3: { chat: true, structuredJson: true, tools: true, thinking: 'unknown' },
511
- 'qwen3-coder': { chat: true, structuredJson: true, tools: true, thinking: false },
512
- 'qwen3-coder-next': { chat: true, structuredJson: true, tools: true, thinking: false },
513
- 'qwen2.5-coder': { chat: true, structuredJson: true, tools: 'unknown', thinking: false },
514
- qwen: { chat: true, structuredJson: 'unknown', tools: 'unknown', thinking: 'unknown' },
515
- gemma4: { chat: true, structuredJson: true, tools: true, thinking: 'unknown' },
516
- functiongemma: { chat: true, structuredJson: 'unknown', tools: true, thinking: false },
517
- gemma3: { chat: true, structuredJson: true, tools: false, thinking: false },
518
- gemma: { chat: true, structuredJson: 'unknown', tools: 'unknown', thinking: 'unknown' },
519
- unknown: { chat: true, structuredJson: 'unknown', tools: 'unknown', thinking: 'unknown' },
520
- };
521
-
522
- /**
523
- * Build the PRD §13.1 ModelCapabilities object from a family hint alone
524
- * (Layer B). Layer A (Ollama's own /api/show metadata) and Layer C (runtime
525
- * probes) are applied by the caller and override these fields — this
526
- * function only ever sets `source.familyHint: true`.
527
- */
528
- function capabilitiesFromFamilyHint(modelName) {
529
- const family = classifyModelFamily(modelName);
530
- const hint = FAMILY_CAPABILITY_HINTS[family] || FAMILY_CAPABILITY_HINTS.unknown;
531
- return {
532
- chat: hint.chat,
533
- structuredJson: hint.structuredJson,
534
- tools: hint.tools,
535
- thinking: hint.thinking,
536
- vision: false,
537
- contextTokens: undefined,
538
- source: { metadata: false, familyHint: true, runtimeProbe: false },
539
- };
540
- }
541
-
542
- // ── RAM-aware memory profiles (PRD §14.4, §15.2, §22.3, §30) ───────────────
543
-
544
- const MB = 1024 * 1024;
545
- const GB = 1024 * MB;
546
-
547
- // Best-effort artifact sizes as currently distributed by Ollama, used only to
548
- // pick a SENSIBLE STARTING recommendation — the real admission decision below
549
- // uses actually-free memory, not this table. Keep in sync with the PRD's own
550
- // cited figures; a stale entry only affects the suggested default, never the
551
- // admission math (which reads real os.freemem()).
552
- const KNOWN_MODEL_SIZE_GB = Object.freeze({
553
- 'qwen3.5:2b': 1.7,
554
- 'qwen3.5:4b': 3.4,
555
- 'qwen3.5:9b': 6.6,
556
- 'gemma4:e2b': 7.2,
557
- 'gemma4:12b': 7.6,
558
- 'gemma4:latest': 9.6,
559
- });
560
-
561
- /** PRD §30 profile presets. `auto` picks between these by detected RAM. */
562
- const MEMORY_PROFILES = Object.freeze({
563
- '8gb': {
564
- label: '8gb',
565
- preferredModel: 'qwen3.5:4b',
566
- fallbackModel: 'qwen3.5:2b',
567
- initialContextTokens: 4096,
568
- targetContextTokens: 8192,
569
- maxConcurrency: 1,
570
- minFreeRamMb: 1536,
571
- },
572
- '16gb-qwen': {
573
- label: '16gb-qwen',
574
- preferredModel: 'qwen3.5:9b',
575
- fallbackModel: 'qwen3.5:4b',
576
- initialContextTokens: 16384,
577
- targetContextTokens: 32768,
578
- maxConcurrency: 1,
579
- minFreeRamMb: 2048,
580
- },
581
- '16gb-gemma': {
582
- label: '16gb-gemma',
583
- preferredModel: 'gemma4:e2b',
584
- fallbackModel: 'qwen3.5:4b',
585
- initialContextTokens: 8192,
586
- targetContextTokens: 16384,
587
- maxConcurrency: 1,
588
- minFreeRamMb: 2048,
589
- },
590
- });
591
-
592
- /**
593
- * PRD §22.3 — detect total/available system RAM. Thin wrapper over `os` so
594
- * tests can inject fake values without mocking the `os` module globally.
595
- */
596
- function detectSystemMemory({ totalBytes, freeBytes } = {}) {
597
- return {
598
- totalBytes: Number.isFinite(totalBytes) ? totalBytes : node_os__WEBPACK_IMPORTED_MODULE_0__.totalmem(),
599
- freeBytes: Number.isFinite(freeBytes) ? freeBytes : node_os__WEBPACK_IMPORTED_MODULE_0__.freemem(),
600
- };
601
- }
602
-
603
- /**
604
- * Pick the RAM tier ('8gb' | '16gb') a machine falls into. Anything under
605
- * ~9 GB total is treated as the 8 GB tier — real "8 GB" machines report
606
- * slightly less than 8*1024^3 bytes to userspace (firmware/GPU reservations),
607
- * so a hard `< 8*GB` cutoff would misclassify real 8 GB hardware as unknown.
608
- */
609
- function detectMemoryTier(totalBytes) {
610
- if (!Number.isFinite(totalBytes) || totalBytes <= 0) return 'unknown';
611
- if (totalBytes < 9 * GB) return '8gb';
612
- return '16gb';
613
- }
614
-
615
- /**
616
- * PRD §22.3 admission algorithm: does `contextTokens` at `model` fit in
617
- * currently-free memory with the configured reserve intact?
618
- *
619
- * This is deliberately conservative and coarse (PRD "avoid pretending memory
620
- * estimates are exact"): model residency is estimated from KNOWN_MODEL_SIZE_GB
621
- * when available (falling back to a pessimistic 8 GB assumption for an
622
- * unrecognized tag so an unknown model never LOOKS safer than a known large
623
- * one), and KV-cache growth is approximated as a fixed per-1K-token cost
624
- * rather than modeled per-architecture — real KV cache size depends on layer
625
- * count/head count/quantization the harness cannot know without Ollama's own
626
- * runtime numbers.
627
- */
628
- const ESTIMATED_KV_CACHE_MB_PER_1K_TOKENS = 32; // conservative, model-independent approximation
629
- const RUNTIME_OVERHEAD_MB = 512; // Ollama server + OS scheduler slack, independent of model size
630
-
631
- function evaluateMemoryAdmission({
632
- modelName,
633
- contextTokens,
634
- freeBytes,
635
- minFreeRamMb,
636
- modelSizeGb,
637
- } = {}) {
638
- const sizeGb = Number.isFinite(modelSizeGb) ? modelSizeGb : (KNOWN_MODEL_SIZE_GB[modelName] ?? 8);
639
- const modelMb = sizeGb * 1024;
640
- const kvCacheMb = (Number(contextTokens) || 0) / 1000 * ESTIMATED_KV_CACHE_MB_PER_1K_TOKENS;
641
- const requiredMb = modelMb + kvCacheMb + RUNTIME_OVERHEAD_MB + (Number(minFreeRamMb) || 0);
642
- const freeMb = (Number(freeBytes) || 0) / MB;
643
- const admitted = freeMb >= requiredMb;
644
- return {
645
- admitted,
646
- freeMb: Math.round(freeMb),
647
- requiredMb: Math.round(requiredMb),
648
- modelEstimateMb: Math.round(modelMb),
649
- kvCacheEstimateMb: Math.round(kvCacheMb),
650
- reserveMb: Number(minFreeRamMb) || 0,
651
- };
652
- }
653
-
654
- /**
655
- * Full recommendation flow (PRD §22 "Memory admission algorithm"):
656
- * try the profile's preferred context, shrink it, then fall back to the
657
- * profile's smaller model, before ever declaring the profile unusable.
658
- * Never recommends cloud — the worst outcome this function can return is
659
- * `{admitted:false}` with a human-readable explanation, which callers treat
660
- * as "run deterministic-only" (PRD §23.4).
661
- */
662
- function recommendAdmission({ profile, freeBytes, requestedContextTokens, requestedModel } = {}) {
663
- const p = MEMORY_PROFILES[profile];
664
- if (!p) return { admitted: false, reason: `unknown memory profile '${profile}'` };
665
-
666
- const model = requestedModel || p.preferredModel;
667
- const attempts = [];
668
-
669
- // 1. Requested (or target) context at the requested/preferred model.
670
- const primaryContext = Number.isFinite(requestedContextTokens) ? requestedContextTokens : p.targetContextTokens;
671
- let check = evaluateMemoryAdmission({ modelName: model, contextTokens: primaryContext, freeBytes, minFreeRamMb: p.minFreeRamMb });
672
- attempts.push({ model, contextTokens: primaryContext, ...check });
673
- if (check.admitted) return { admitted: true, model, contextTokens: primaryContext, attempts };
674
-
675
- // 2. Reduce context to the profile's conservative initial value first —
676
- // PRD FR-2104: "shrink context before declaring an otherwise compatible
677
- // model unusable."
678
- if (primaryContext !== p.initialContextTokens) {
679
- check = evaluateMemoryAdmission({ modelName: model, contextTokens: p.initialContextTokens, freeBytes, minFreeRamMb: p.minFreeRamMb });
680
- attempts.push({ model, contextTokens: p.initialContextTokens, ...check });
681
- if (check.admitted) return { admitted: true, model, contextTokens: p.initialContextTokens, attempts, reducedContext: true };
682
- }
683
-
684
- // 3. Fall back to the profile's smaller model at its initial context.
685
- if (p.fallbackModel && p.fallbackModel !== model) {
686
- check = evaluateMemoryAdmission({ modelName: p.fallbackModel, contextTokens: p.initialContextTokens, freeBytes, minFreeRamMb: p.minFreeRamMb });
687
- attempts.push({ model: p.fallbackModel, contextTokens: p.initialContextTokens, ...check });
688
- if (check.admitted) {
689
- return {
690
- admitted: true, model: p.fallbackModel, contextTokens: p.initialContextTokens, attempts,
691
- reducedContext: true, fellBackToSmallerModel: true,
692
- };
693
- }
694
- }
695
-
696
- // 4. Nothing fits — deterministic-only, never cloud.
697
- return {
698
- admitted: false,
699
- attempts,
700
- reason: `No local model/context combination fit in available memory with the configured reserve. ` +
701
- `Recommend deterministic-only scanning, or free memory before retrying.`,
702
- };
703
- }
704
-
705
-
706
- /***/ }),
707
-
708
- /***/ 7039:
709
- /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
710
-
711
- /* harmony export */ __webpack_require__.d(__webpack_exports__, {
712
- /* harmony export */ getModelCapabilities: () => (/* binding */ getModelCapabilities)
713
- /* harmony export */ });
714
- /* unused harmony exports capabilitiesFromShowMetadata, probeStructuredOutput, probeToolCalling, _internals */
715
- /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3024);
716
- /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(6760);
717
- /* harmony import */ var node_os__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(8161);
718
- /* harmony import */ var node_crypto__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(7598);
719
- /* harmony import */ var _ollama_provider_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(3837);
720
- /* harmony import */ var _model_capabilities_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(4399);
721
- // PRD §13.2 — the three-layer model capability detection strategy.
722
- //
723
- // LAYER A (metadata) is the cheapest and most authoritative: Ollama's own
724
- // `/api/show` response, when it reports a `capabilities` array, is not a
725
- // guess. LAYER B (model-capabilities.js's family hint) is a non-authoritative
726
- // default used only where Layer A is silent. LAYER C (this module's
727
- // `probeStructuredOutput`/`probeToolCalling`) is the most expensive — it
728
- // consumes real inference time — so it is OPT-IN (the caller decides when
729
- // "necessary" per the PRD's own wording), never run implicitly on every
730
- // `models doctor`/`models inspect` invocation.
731
- //
732
- // PRECEDENCE: Layer C overrides Layer A overrides Layer B, field by field. A
733
- // field only ever gets overridden by a MORE authoritative layer that actually
734
- // has an opinion — a probe that couldn't run (offline/timeout) leaves the
735
- // field exactly as the layer below it set it, it never downgrades to
736
- // 'unknown'.
737
- //
738
- // CACHE KEY = Ollama version + model digest + model name (PRD §13.2 exactly).
739
- // Digest is load-bearing: `ollama pull` replacing a tag's underlying weights
740
- // must invalidate the cache even though the name/tag string is unchanged.
741
- // Persisted forever (no TTL) because the key itself is what expires the
742
- // entry — a version/digest bump makes a new key, not a stale hit on the old
743
- // one. Same disk-cache directory convention as sca/sigstore-verify.js and
744
- // engine.js's OSV cache (`~/.claude/agentic-security/<name>/`).
745
-
746
-
747
-
748
-
749
-
750
-
751
-
752
-
753
- const CACHE_DIR = node_path__WEBPACK_IMPORTED_MODULE_1__.join(node_os__WEBPACK_IMPORTED_MODULE_2__.homedir(), '.claude', 'agentic-security', 'ollama-capability-cache');
754
-
755
- function _ensureCacheDir() { try { node_fs__WEBPACK_IMPORTED_MODULE_0__.mkdirSync(CACHE_DIR, { recursive: true }); } catch {} }
756
- function _cacheKey(ollamaVersion, modelDigest, modelName) {
757
- return node_crypto__WEBPACK_IMPORTED_MODULE_3__.createHash('sha256').update(`${ollamaVersion}::${modelDigest}::${modelName}`).digest('hex');
758
- }
759
- function _cachePath(key) { return node_path__WEBPACK_IMPORTED_MODULE_1__.join(CACHE_DIR, key + '.json'); }
760
-
761
- function _readProbeCache(key) {
762
- try { return JSON.parse(node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(_cachePath(key), 'utf8')); } catch { return null; }
763
- }
764
- function _writeProbeCache(key, value) {
765
- _ensureCacheDir();
766
- try { node_fs__WEBPACK_IMPORTED_MODULE_0__.writeFileSync(_cachePath(key), JSON.stringify(value)); } catch {}
767
- }
768
-
769
- /**
770
- * PRD §13.2 Layer A — parse `/api/show`'s response into the subset of
771
- * ModelCapabilities it can actually speak to. A field this layer has no
772
- * opinion on is omitted (not set to `false`) so the caller's merge never
773
- * mistakes silence for a negative.
774
- */
775
- function capabilitiesFromShowMetadata(show) {
776
- const out = { source: { metadata: true } };
777
- if (Array.isArray(show?.capabilities) && show.capabilities.length > 0) {
778
- const caps = show.capabilities;
779
- out.chat = caps.includes('completion') || caps.includes('chat');
780
- out.tools = caps.includes('tools');
781
- out.vision = caps.includes('vision');
782
- out.thinking = caps.includes('thinking');
783
- }
784
- const modelInfo = show?.modelInfo;
785
- if (modelInfo && typeof modelInfo === 'object') {
786
- const ctxKey = Object.keys(modelInfo).find((k) => k.endsWith('.context_length'));
787
- if (ctxKey && Number.isFinite(modelInfo[ctxKey])) out.contextTokens = modelInfo[ctxKey];
788
- }
789
- return out;
790
- }
791
-
792
- /**
793
- * PRD §13.2 Layer C — structured-output probe. A tiny schema, a request for
794
- * `{"ok": true}`, verified end to end through the SAME
795
- * callOllamaStructured() bounded-retry path every real structured call uses
796
- * (not a bespoke lighter-weight check that could disagree with production
797
- * behavior).
798
- */
799
- const PROBE_SCHEMA = { type: 'object', required: ['ok'], properties: { ok: { type: 'boolean' } } };
800
-
801
- async function probeStructuredOutput({ host, model, timeouts, keepAlive } = {}) {
802
- const r = await (0,_ollama_provider_js__WEBPACK_IMPORTED_MODULE_4__/* .callOllamaStructured */ .uM)({
803
- host, model,
804
- messages: [{ role: 'user', content: 'Reply with ONLY a JSON object: {"ok": true}' }],
805
- schema: PROBE_SCHEMA,
806
- validateFn: (obj) => (obj && obj.ok === true ? { ok: true, value: obj } : { ok: false }),
807
- keepAlive, timeouts,
808
- });
809
- if (r.ok) return { supported: true };
810
- // A transport-level failure (server unreachable, timed out) tells us
811
- // nothing about the MODEL's capability — leave it 'unknown' rather than
812
- // reporting a false negative for an offline/slow server.
813
- if (['ollama-unreachable', 'ollama-not-running', 'ollama-timeout', 'ollama-model-not-installed'].includes(r.code)) {
814
- return { supported: 'unknown', reason: r.reason || r.code };
815
- }
816
- return { supported: false, reason: r.reason || r.code };
817
- }
818
-
819
- /**
820
- * PRD §13.2 Layer C — tool-calling probe. One harmless `echo_capability_probe`
821
- * function; success is Ollama returning a structured `tool_calls` entry
822
- * naming it, not a check on what the model chose to reply with in prose.
823
- */
824
- const PROBE_TOOL = {
825
- type: 'function',
826
- function: {
827
- name: 'echo_capability_probe',
828
- description: 'Echo back the given value. Used only to test whether this model supports tool calling.',
829
- parameters: { type: 'object', required: ['value'], properties: { value: { type: 'string' } } },
830
- },
831
- };
832
-
833
- async function probeToolCalling({ host, model, timeouts, keepAlive } = {}) {
834
- const r = await (0,_ollama_provider_js__WEBPACK_IMPORTED_MODULE_4__/* .callOllamaChat */ .L5)({
835
- host, model,
836
- messages: [{ role: 'user', content: 'Call the echo_capability_probe function with value set to "probe-ok". Reply with nothing else.' }],
837
- tools: [PROBE_TOOL],
838
- keepAlive, timeouts,
839
- });
840
- if (!r.ok) {
841
- if (['ollama-unreachable', 'ollama-not-running', 'ollama-timeout', 'ollama-model-not-installed'].includes(r.code)) {
842
- return { supported: 'unknown', reason: r.reason || r.code };
843
- }
844
- return { supported: false, reason: r.reason || r.code };
845
- }
846
- const calls = r.result.toolCalls || [];
847
- const called = calls.some((c) => c?.function?.name === 'echo_capability_probe');
848
- return called ? { supported: true } : { supported: false, reason: 'model did not emit a tool_calls entry for the probe function' };
849
- }
850
-
851
- function _mergeLayer(base, overlay, sourceFlag) {
852
- const merged = { ...base };
853
- let touched = false;
854
- for (const field of ['chat', 'structuredJson', 'tools', 'thinking', 'vision', 'contextTokens']) {
855
- if (overlay[field] !== undefined) { merged[field] = overlay[field]; touched = true; }
856
- }
857
- if (touched) merged.source = { ...merged.source, [sourceFlag]: true };
858
- return merged;
859
- }
860
-
861
- /**
862
- * Orchestrates all three layers (PRD §13.2) with caching (PRD: "so startup
863
- * does not repeatedly consume inference time"). `probe: true` opts into
864
- * Layer C — omitted or false, this returns Layer A+B only, which is what
865
- * every non-probing caller (models list/inspect/doctor's default path)
866
- * should use, since Layer C spends real inference time on the user's
867
- * machine.
868
- *
869
- * @returns {{ok:true, capabilities:object, cached:boolean} | {ok:false, code, reason}}
870
- */
871
- async function getModelCapabilities({ host, model, env = process.env, probe = false, timeouts, keepAlive } = {}) {
872
- let capabilities = (0,_model_capabilities_js__WEBPACK_IMPORTED_MODULE_5__.capabilitiesFromFamilyHint)(model);
873
-
874
- const show = await (0,_ollama_provider_js__WEBPACK_IMPORTED_MODULE_4__/* .showOllamaModel */ .$G)({ host, model, timeouts });
875
- if (show.ok) {
876
- capabilities = _mergeLayer(capabilities, capabilitiesFromShowMetadata(show), 'metadata');
877
- }
878
-
879
- if (!probe) {
880
- return { ok: true, capabilities, cached: false };
881
- }
882
-
883
- const versionResult = await (0,_ollama_provider_js__WEBPACK_IMPORTED_MODULE_4__/* .getOllamaVersion */ .zm)({ host, timeouts });
884
- const ollamaVersion = versionResult.ok ? versionResult.version : 'unknown-version';
885
- // The digest is whatever Layer A's /api/show reported under `details`
886
- // (Ollama does not expose it on /api/show consistently across versions —
887
- // fall back to the model name alone, which still invalidates on a tag
888
- // change, just not on a same-tag re-pull).
889
- const modelDigest = show.ok && show.details?.digest ? show.details.digest : 'unknown-digest';
890
- const cacheKey = _cacheKey(ollamaVersion, modelDigest, model);
891
-
892
- const cached = _readProbeCache(cacheKey);
893
- if (cached) {
894
- return { ok: true, capabilities: _mergeLayer(capabilities, cached, 'runtimeProbe'), cached: true };
895
- }
896
-
897
- const [structured, tools] = await Promise.all([
898
- probeStructuredOutput({ host, model, timeouts, keepAlive }),
899
- probeToolCalling({ host, model, timeouts, keepAlive }),
900
- ]);
901
-
902
- const probeResult = {};
903
- if (structured.supported !== 'unknown') probeResult.structuredJson = structured.supported;
904
- if (tools.supported !== 'unknown') probeResult.tools = tools.supported;
905
-
906
- // Only cache a probe that actually resolved something — an all-'unknown'
907
- // result (server unreachable mid-probe) would otherwise poison the cache
908
- // with a permanent non-answer.
909
- if (Object.keys(probeResult).length > 0) _writeProbeCache(cacheKey, probeResult);
910
-
911
- return { ok: true, capabilities: _mergeLayer(capabilities, probeResult, 'runtimeProbe'), cached: false };
912
- }
913
-
914
- const _internals = { CACHE_DIR, _cacheKey, _cachePath };
915
-
916
-
917
- /***/ }),
918
-
919
- /***/ 1211:
529
+ /***/ 61211:
920
530
  /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
921
531
 
922
532
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {