SkillSpector Architecture: Inside NVIDIA's LangGraph Security Analysis Pipeline
SkillSpector implements a deterministic, state-machine workflow using LangGraph to process skills through sequential nodes—input resolution, context building, static analysis, LLM meta-analysis, and reporting—ensuring consistent and extensible security scanning.
The SkillSpector architecture powers NVIDIA's open-source security scanner for AI skills (directory structures, zip archives, URLs, or standalone SKILL.md files). Built on LangGraph, the system orchestrates a shared state across specialized nodes, enabling both high-performance static analysis and optional semantic evaluation via LLM providers like OpenAI, Anthropic, and NVIDIA Build.
Core Architectural Components
The codebase organizes functionality into distinct layers, each handling a specific phase of the security analysis lifecycle.
Entry Point and Workflow Orchestration
The CLI entry point in src/skillspector/cli.py parses command-line arguments and constructs the workflow graph. It delegates execution to the compiled LangGraph instance defined in src/skillspector/graph.py, which declares the StateGraph using the SkillspectorState TypedDict schema from src/skillspector/state.py. This state object holds mutable shared data including the input path, file caches, AST caches, component metadata, findings, and final risk scores.
Input Handling and Context Building
The pipeline begins with two foundational nodes:
resolve_input(src/skillspector/nodes/resolve_input.py): Normalizes user input by handling git clones, zip extractions, local path validation, and URL downloads, storing the resolved path inskill_path.build_context(src/skillspector/nodes/build_context.py): traverses the skill tree to populatefile_cacheandast_cache, extracts manifest files, and records component metadata necessary for downstream analysis.
Static and LLM-Based Analysis Nodes
The analysis layer comprises 20+ pure-Python analyzers located in src/skillspector/nodes/analyzers/*.py. These modules detect specific patterns including prompt injection, supply-chain risks, YARA matches, AST behavioral analysis, and MCP protocol violations. Each analyzer returns an AnalyzerNodeResponse containing Finding instances defined in src/skillspector/models.py.
Following static analysis, the meta_analyzer node (src/skillspector/nodes/meta_analyzer.py) optionally invokes LLM providers to re-evaluate findings, filter false positives, and generate human-readable explanations. Provider implementations reside in src/skillspector/providers/, while src/skillspector/llm_analyzer_base.py supplies common prompt-building and response-parsing logic.
Reporting and Data Models
The final report node (src/skillspector/nodes/report.py) consumes the completed state—including risk_score, risk_severity, risk_recommendation, and SARIF-compatible structures from src/skillspector/sarif_models.py—to generate outputs in terminal, JSON, Markdown, or SARIF formats.
Execution Flow in the StateGraph
The workflow graph defined in src/skillspector/graph.py executes deterministically through six sequential stages:
- START → resolve_input: Ingest and normalize the skill source.
- resolve_input → build_context: Gather files, build AST caches, and extract component metadata.
- build_context → analyzers: Execute all static-pattern analyzers in parallel; each appends findings to the shared state.
- analyzers → meta_analyzer: Perform optional LLM semantic analysis on accumulated findings.
- meta_analyzer → report: Calculate risk scores and generate formatted output.
- report → END: Return the final
SkillspectorStateto the CLI or API caller.
All nodes function as pure functions accepting the current SkillspectorState and returning updated state fragments. The graph compiles once via create_graph() and reuses the compiled instance for every scan, ensuring thread safety and deterministic ordering.
Practical Implementation Examples
Using the Python API Directly
You can invoke the compiled workflow programmatically using the same graph the CLI runs:
from skillspector.graph import graph
result = graph.invoke(
{
"input_path": "/path/to/skill",
"output_format": "json",
"use_llm": True,
}
)
print("Risk score:", result["risk_score"])
print("Recommendation:", result["risk_recommendation"])
for finding in result["filtered_findings"]:
print(f"[{finding.severity}] {finding.id}: {finding.message}")
Command-Line Execution
Typical CLI usage supports multiple input formats and output options:
# Scan local directory with full analysis
skillspector scan ./my-skill/
# Scan git repository, output SARIF for CI integration
skillspector scan https://github.com/user/bad-skill.git \
--format sarif --output report.sarif
# Static-only scan without LLM costs
skillspector scan ./my-skill/ --no-llm
Extending with Custom Analyzers
Add a new security check by creating src/skillspector/nodes/analyzers/static_my_pattern.py:
from skillspector.models import Finding
from skillspector.state import SkillspectorState, AnalyzerNodeResponse
def node(state: SkillspectorState) -> AnalyzerNodeResponse:
findings = []
for path, code in state["file_cache"].items():
if "dangerous()" in code:
findings.append(
Finding(
id="MY01",
category="custom",
severity="HIGH",
message="Usage of dangerous() detected",
location={"file": path, "start_line": 1},
)
)
return {"findings": findings}
Register the analyzer in src/skillspector/nodes/analyzers/__init__.py by adding it to ANALYZER_NODE_IDS and ANALYZER_NODES. The graph automatically includes it in the parallel execution phase following build_context.
Key Source Files Reference
src/skillspector/graph.py: Compiles the LangGraphStateGraphand wires node dependencies.src/skillspector/state.py: Defines theSkillspectorStateTypedDict for shared mutable state.src/skillspector/nodes/resolve_input.py: Handles source normalization for directories, zips, and URLs.src/skillspector/nodes/build_context.py: Builds file and AST caches, extracts metadata.src/skillspector/nodes/analyzers/__init__.py: Registry for all analyzer nodes (static and LLM-based).src/skillspector/nodes/meta_analyzer.py: Implements LLM-based semantic re-evaluation of findings.src/skillspector/nodes/report.py: Generates final risk scores and formatted reports.src/skillspector/providers/: LLM backend implementations (OpenAI, Anthropic, NVIDIA Build).src/skillspector/models.py&sarif_models.py: Core data structures (Finding,Severity,Location) and SARIF output types.src/skillspector/cli.py: Command-line interface and workflow entry point.
Summary
- SkillSpector uses LangGraph to orchestrate a deterministic, stateful pipeline for security analysis of AI skills.
- The architecture separates concerns into input resolution, context building, parallel static analysis, LLM meta-analysis, and reporting nodes.
- All nodes in
src/skillspector/nodes/operate on a sharedSkillspectorStatedefined insrc/skillspector/state.py, ensuring type safety and consistency. - The system supports 20+ static analyzers in
src/skillspector/nodes/analyzers/that run in parallel, followed by optional LLM evaluation viasrc/skillspector/nodes/meta_analyzer.py. - Extending the tool requires only adding a pure function analyzer and registering it in the analyzer
__init__.pymodule.
Frequently Asked Questions
What makes LangGraph suitable for SkillSpector's security scanning workflow?
LangGraph provides deterministic state management and explicit node dependencies, which are critical for security auditing. According to the NVIDIA/SkillSpector source code, the graph compiles once via create_graph() in src/skillspector/graph.py and reuses the compiled instance, ensuring that every scan follows the exact same execution path (START → resolve_input → build_context → analyzers → meta_analyzer → report → END) regardless of input source.
Can SkillSpector analyze skills without LLM API access?
Yes. The architecture treats LLM analysis as an optional layer controlled by the use_llm state parameter. Setting --no-llm in the CLI or use_llm: False in the API bypasses the meta_analyzer node entirely, executing only the static analyzers in src/skillspector/nodes/analyzers/*.py. This enables fast, cost-free scans suitable for CI/CD pipelines.
How does SkillSpector handle different input formats like URLs and zip files?
The resolve_input node in src/skillspector/nodes/resolve_input.py normalizes all inputs—local directories, git repository URLs, zip archives, or single SKILL.md files—into a consistent directory structure stored in skill_path. This abstraction allows downstream nodes to operate uniformly on the file cache without concerning themselves with the original input format.
What is the performance impact of running 20+ analyzers in parallel?
The graph executes all static analyzers concurrently as parallel branches from the build_context node, merging results back into the shared state before the sequential meta_analyzer phase. Because each analyzer in src/skillspector/nodes/analyzers/ is a pure function operating on the immutable file cache, the parallel execution scales efficiently with available CPU cores without blocking the main thread.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →