Skip to content

feat(llm): configurable analyzer fan-out concurrency via SKILLSPECTOR_MAX_LLM_CONCURRENCY (part of #303) - #305

Merged
keshprad merged 2 commits into
NVIDIA:mainfrom
Mark2Mac:feat/configurable-llm-concurrency
Jul 30, 2026
Merged

keshprad merged 2 commits into
NVIDIA:mainfrom
Mark2Mac:feat/configurable-llm-concurrency

Conversation

@Mark2Mac

Copy link
Copy Markdown
Contributor

What

arun_batches fans out up to a hardcoded max_concurrency=10. On a rate-limited provider (free tiers with a low RPM) that burst guarantees 429s, and — per the method's own contract — 429'd batches are logged and omitted from the result. The scan silently loses analyzer coverage, including the security-critical semantic_security_discovery batch, while still emitting a full risk_assessment. A genuinely malicious skill can then read as clean.

This adds an env knob so users on such tiers can serialize the fan-out and stay under the limit:

SKILLSPECTOR_MAX_LLM_CONCURRENCY=1 skillspector scan ./skill/
  • Default unchanged (10). An explicit max_concurrency= argument still wins.
  • Invalid values fall back to the default; values < 1 clamp to 1.

Test

TestResolveMaxConcurrency: unset→default, valid→value, blank→default, invalid→default, 0→1. Full test_llm_analyzer_base.py suite green (116 passed); ruff clean.

Scope

This is the first of the three asks in #303 (configurable concurrency). The retry/backoff on 429 and surfacing a partial LLM pass in the verdict are larger changes left for separate PRs / discussion.

Refs #303

`arun_batches` bursts up to a hardcoded `max_concurrency=10`. On a rate-limited
provider (free tiers with a low RPM) that burst guarantees 429s, and 429'd
batches are dropped from the result — silently losing analyzer coverage,
including the security-critical `semantic_security_discovery` batch, while the
report still renders a full risk_assessment.

Add `SKILLSPECTOR_MAX_LLM_CONCURRENCY` so users can serialize the fan-out
(set it to 1) and stay under the provider's rate limit. The default stays 10;
an explicit `max_concurrency=` argument still wins. Invalid values fall back to
the default, values < 1 clamp to 1.

Part of NVIDIA#303 (the retry/backoff and partial-pass surfacing are separate). Adds
unit tests for the env resolution.

Signed-off-by: Mark2Mac <[email protected]>
rng1995
rng1995 previously approved these changes Jul 24, 2026

@rng1995 rng1995 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Automated SkillSpector Review]

Approved. The default remains 10, explicit arguments retain precedence, environment parsing handles invalid and blank values safely, and values below one are clamped. The focused resolver tests cover the new configuration behavior; no schema or security regression found.

@Mark2Mac

Copy link
Copy Markdown
Contributor Author

Gentle ping. This one touches the async core, so if you would rather solve #303 your own way I am happy to close it — the part I care about is that a rate-limited provider currently loses analyzer coverage silently.

For context on why the knob matters in practice: on the Gemini AI Studio free tier the per-model allowance is 20 requests/day and 5 RPM, so the hardcoded max_concurrency=10 guarantees 429s on every unit, and the dropped batches make the scan read clean while a security analyzer never ran. With SKILLSPECTOR_MAX_LLM_CONCURRENCY=1 the same scan completes honestly, just slower.

Also filed #318 (SC4 and version ranges) from the same audit, in case it is useful signal on the supply-chain analyzer.

@keshprad

Copy link
Copy Markdown
Contributor

Can you resolve conflicts? @Mark2Mac

@keshprad
keshprad force-pushed the feat/configurable-llm-concurrency branch from d58aeb0 to 85a4e7d Compare July 30, 2026 19:46
@keshprad
keshprad merged commit da5cb13 into NVIDIA:main Jul 30, 2026
5 checks passed
AmirF194 added a commit to AmirF194/SkillSpector that referenced this pull request Aug 21, 2026
… one

_llm_runtime_status() only set degraded when every LLM call failed
(succeeded == 0). A rate-limited provider that drops a single batch
(e.g. semantic_security_discovery hits a 429) still has succeeded > 0,
so the scan reported a normal risk_assessment even though the
security-critical analyzer never ran. Widen the condition to
succeeded < attempted, so any dropped batch degrades the scan and
the existing fail-closed floor (CAUTION instead of SAFE) applies to a
partial pass too. Updated the two degraded-scan messages to say how
many of the calls failed instead of assuming all of them did.

Covers request 3 of NVIDIA#303 (surface incompleteness in the verdict).
Request 1 (configurable concurrency) shipped in NVIDIA#305; request 2
(retry with backoff) is left to the already-open NVIDIA#29.

Refs NVIDIA#303

Signed-off-by: Amir Fathi <[email protected]>
rng1995 pushed a commit that referenced this pull request Aug 21, 2026
… one (#362)

* fix(report): flag a partial LLM failure as degraded, not only a total one

_llm_runtime_status() only set degraded when every LLM call failed
(succeeded == 0). A rate-limited provider that drops a single batch
(e.g. semantic_security_discovery hits a 429) still has succeeded > 0,
so the scan reported a normal risk_assessment even though the
security-critical analyzer never ran. Widen the condition to
succeeded < attempted, so any dropped batch degrades the scan and
the existing fail-closed floor (CAUTION instead of SAFE) applies to a
partial pass too. Updated the two degraded-scan messages to say how
many of the calls failed instead of assuming all of them did.

Covers request 3 of #303 (surface incompleteness in the verdict).
Request 1 (configurable concurrency) shipped in #305; request 2
(retry with backoff) is left to the already-open #29.

Refs #303

Signed-off-by: Amir Fathi <[email protected]>

* fix(report): mark a batch record failed on any dropped batch, and stop meta-analysis fields inheriting other analyzers' failures

Two gaps from review on #362:

1. llm_call_log records were built with
   ok=bool(outcome.successful) or not outcome.failures, so an analyzer with
   one succeeded batch and one dropped/429'd batch still recorded ok=True.
   In that exact case succeeded == attempted at the report layer and the
   scan stayed SAFE, defeating the partial-coverage fix. Now the record is
   ok=not outcome.failures: any dropped batch marks the whole record failed.
   Applied identically in the three semantic analyzers and meta_analyzer,
   the four call sites that build this record.

2. meta_analysis_applied and the llm_available field were derived from the
   aggregate `degraded` flag, which pools every LLM-backed node together.
   That let a different analyzer's dropped batch force
   meta_analysis_applied=False, filtering_mode="heuristic" and
   llm_available=False even when meta_analyzer's own call fully succeeded,
   misstating two independent contracts (meta-analysis ran vs. some
   coverage was lost) as one boolean. Both fields now derive from
   is_llm_available() plus meta_analyzer's own llm_call_log record only;
   the coverage loss from other analyzers still surfaces through
   llm_degraded / llm_calls_attempted / llm_calls_succeeded, unchanged.

Verified: test_partial_batch_failure_records_llm_failure (renamed from
..._records_llm_success, now pins ok=False) and three new report-level
tests, run red against the pre-fix code (3 of 4 failed) and green after.
tests/nodes/test_report.py: 66 passed. Full suite in Docker
(python:3.12-slim): 1947 passed, 13 skipped, 4 xfailed, 0 failed. ruff
lint and format-check both pass.

Signed-off-by: Amir Fathi <[email protected]>

* fix(report): require an actual meta_analyzer record for meta_analysis_applied

all([]) is True on an empty list, so an empty meta_analyzer_records left
meta_analysis_applied True even when meta_analyzer made no call at all (the
no-findings path, where it short-circuits to not_applicable). That still
violated the "did meta-analysis actually run" contract from the prior
review.

meta_analysis_applied now requires at least one meta_analyzer record and
all of them ok. llm_available is unchanged: provider availability is a
separate contract from whether meta_analyzer had anything to do, and it
stays vacuously true when meta_analyzer never ran.

Adds a regression covering the no-findings/no-record case, asserting
meta_analysis_applied is False while llm_available stays True.

Signed-off-by: Amir Fathi <[email protected]>

---------

Signed-off-by: Amir Fathi <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants