Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/skillspector/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ def _scan_state(
"input_path": input_path,
"output_format": format.value,
"use_llm": not no_llm,
"llm_requested": not no_llm,
}
if yara_rules_dir is not None:
state["yara_rules_dir"] = yara_rules_dir
Expand Down
8 changes: 0 additions & 8 deletions src/skillspector/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
from langgraph.graph import END, START, StateGraph

from skillspector.inspection_ledger import guard_analyzer_node
from skillspector.llm_utils import is_llm_available
from skillspector.logging_config import get_logger
from skillspector.nodes.analyzers import ANALYZER_MODULES, ANALYZER_NODE_IDS, ANALYZER_NODES
from skillspector.nodes.build_context import build_context
Expand Down Expand Up @@ -60,13 +59,6 @@ def create_graph():
logger.warning("Skipping analyzer %s: is_available() returned False", analyzer_id)
continue

requires_api_key = getattr(mod, "requires_api_key", False)
if requires_api_key:
has_llm, _ = is_llm_available()
if not has_llm:
logger.warning("Skipping analyzer %s: required API key is missing", analyzer_id)
continue

workflow.add_node(
analyzer_id, guard_analyzer_node(analyzer_id, ANALYZER_NODES[analyzer_id])
)
Expand Down
46 changes: 39 additions & 7 deletions src/skillspector/mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

from __future__ import annotations

from collections.abc import Mapping
from pathlib import Path
from typing import TYPE_CHECKING, Any

Expand All @@ -36,6 +37,8 @@
from skillspector.graph import graph
from skillspector.llm_utils import is_llm_available
from skillspector.logging_config import get_logger
from skillspector.nodes.analyzers import ANALYZER_MODULES
from skillspector.semantic_runtime import llm_runtime_available, semantic_runtime_accounting
from skillspector.suppression import effective_findings

if TYPE_CHECKING:
Expand All @@ -46,6 +49,23 @@
VALID_FORMATS = ("json", "markdown", "sarif", "terminal")


def _llm_runtime_accounting(*, enabled: bool, result: Mapping[str, object]) -> tuple[bool, bool]:
"""Apply shared semantic runtime accounting to the discovered registry."""
return semantic_runtime_accounting(
enabled=enabled,
result=result,
discovered_modules=ANALYZER_MODULES,
)


def _llm_runtime_available(*, preflight_available: bool, result: Mapping[str, object]) -> bool:
"""Apply shared provider and meta-analysis runtime availability."""
return llm_runtime_available(
preflight_available=preflight_available,
result=result,
)


def _is_local_target(target: str) -> bool:
"""Return True when ``target`` names local filesystem content."""
stripped = target.strip()
Expand Down Expand Up @@ -105,22 +125,23 @@ async def run_scan(
if local_target or local_yara_rules:
raise ValueError("local targets are disabled for this MCP transport")

llm_available, _ = is_llm_available()
llm_used = use_llm and llm_available
llm_preflight_available, _ = is_llm_available()
llm_enabled = use_llm and llm_preflight_available

state: dict[str, Any] = {
"input_path": target,
"output_format": output_format,
"use_llm": llm_used,
"use_llm": llm_enabled,
"llm_requested": use_llm,
}
if yara_rules_dir:
state["yara_rules_dir"] = yara_rules_dir

logger.debug(
"MCP scan started: target=%s, format=%s, llm_used=%s",
"MCP scan started: target=%s, format=%s, llm_enabled=%s",
target,
output_format,
llm_used,
llm_enabled,
)

result: dict[str, Any] | None = None
Expand All @@ -132,28 +153,39 @@ async def run_scan(
"tags": ["skillspector", "mcp"],
"metadata": {
"input_path": target,
"use_llm": llm_used,
"use_llm": llm_enabled,
"llm_requested": use_llm,
"output_format": output_format,
"version": __version__,
},
},
)
findings = effective_findings(result)
risk_score = int(result.get("risk_score") or 0)
llm_used, llm_runtime_complete = _llm_runtime_accounting(enabled=llm_enabled, result=result)
llm_available = _llm_runtime_available(
preflight_available=llm_preflight_available,
result=result,
)
execution_successful = bool(result.get("execution_successful", True))
analysis_completeness = result.get("analysis_completeness") or {}
entirely_uninspected = int(analysis_completeness.get("entirely_uninspected_files", 0))
analysis_requirement_met = not use_llm or llm_runtime_complete
safe_to_install = (
risk_score <= RISK_THRESHOLD
and execution_successful
and entirely_uninspected == 0
and bool(analysis_completeness.get("is_complete", True))
and analysis_requirement_met
)
recommendation = result.get("risk_recommendation")
if not analysis_requirement_met and recommendation == "SAFE":
recommendation = "CAUTION"
return {
"target": target,
"risk_score": risk_score,
"severity": result.get("risk_severity"),
"recommendation": result.get("risk_recommendation"),
"recommendation": recommendation,
"safe_to_install": safe_to_install,
"execution_successful": execution_successful,
"analysis_completeness": analysis_completeness,
Expand Down
2 changes: 1 addition & 1 deletion src/skillspector/multi_skill.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ def detect_skills(directory: Path) -> MultiSkillDetectionResult:
continue
except OSError as exc:
raise _read_error("multi_skill_directory_entry") from exc
if entry.name in _SKIP_DIRS or entry.name.startswith("."):
if entry.name in _SKIP_DIRS:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Powered by Codex: [P0] Including a dot-prefixed child and then recursively re-rooting it erases the hidden path segment used by build_context to enforce local-only handling. A harmless marker was excluded from llm_file_cache as .review-helper/SKILL.md in the parent, but appeared in llm_file_cache as SKILL.md after child re-rooting. This contradicts the documented invariant that hidden content is never sent to an external LLM. Carry immutable root-relative hidden/local-only provenance into the child scan and add a mocked-provider test proving the marker never reaches an LLM batch.

continue

has_manifest = _has_skill_md(child, budget=budget)
Expand Down
Loading