feat: add experimental Rust local skill inspector - #450
Conversation
PR Summary by QodoAdd experimental Rust local skill inspector
AI Description
Diagram
High-Level Assessment
Files changed (6)
|
Code Review by Qodo
1. Non-UTF-8 paths collide
|
| if let Err(error) = run(Cli::parse()) { | ||
| eprintln!("snyk-agent-scan-rust: {error}"); |
There was a problem hiding this comment.
1. main exposes internal errors 📘 Rule violation ☼ Reliability
The CLI prints the full ScanError display text, including absolute scanned paths and underlying OS I/O error details from ScanError::Read. User-facing failures should remain generic so local filesystem internals are not exposed.
Agent Prompt
## Issue description
The CLI renders raw `ScanError` details to stderr, which can expose absolute paths and underlying OS error text.
## Issue Context
Compliance rule 6 requires generic user-facing errors rather than exception or internal-system details. Preserve detailed diagnostics only in an explicitly safe debug channel that does not include scanned content or sensitive paths.
## Fix Focus Areas
- rust/src/main.rs[78-106]
- rust/src/main.rs[538-542]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| for name in SUBSET: | ||
| (source / name).symlink_to(SKILLS / name, target_is_directory=True) | ||
| aliases = [] | ||
| for number in range(1, 9): |
There was a problem hiding this comment.
2. Alias count is duplicated 📘 Rule violation ☼ Reliability
The alias corpus size is encoded separately as the range(1, 9) boundary and two literal 8 values in benchmark metadata. Changing only one occurrence would produce inconsistent benchmark setup and reported results.
Agent Prompt
## Issue description
The benchmark alias count is repeated through a range boundary and metadata literals.
## Issue Context
Use one named constant to drive alias creation, the reported alias count, and expected record calculation.
## Fix Focus Areas
- rust/bench_local_inspect.py[21-24]
- rust/bench_local_inspect.py[94-120]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| fn display_path(path: &Path) -> String { | ||
| path.to_string_lossy().into_owned() | ||
| } |
There was a problem hiding this comment.
5. Non-utf-8 paths collide 🐞 Bug ≡ Correctness
display_path and relative_path replace invalid Unix filename bytes with the same Unicode replacement character, so distinct valid paths can be emitted identically. Because these lossy strings are also used as JSON map keys, a later explicit path can silently overwrite an earlier result, while distinct files can receive duplicate incorrect files[].path values.
Agent Prompt
## Issue description
Unix paths are arbitrary byte sequences, but the Rust inspector converts them with `to_string_lossy()`. Distinct paths can therefore collapse to the same displayed path or JSON key and silently overwrite results.
## Issue Context
The Python implementation traverses filesystem strings using surrogate-escape semantics, whereas the Rust conversion inserts `�`. Use a reversible representation compatible with the expected JSON output, or reject non-UTF-8 paths explicitly before producing any output; do not silently replace bytes.
## Fix Focus Areas
- rust/src/main.rs[230-232]
- rust/src/main.rs[309-314]
- rust/src/main.rs[525-528]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| path: path.display().to_string(), | ||
| source, | ||
| }) | ||
| } |
There was a problem hiding this comment.
Tilde paths never expanded
Medium Severity
absolute_lexical treats ~/… as a relative path and joins it to the current working directory instead of expanding the home directory. The Python inspect path uses os.path.expanduser and expand_path, so the same explicit argument can succeed in Python and fail or scan the wrong location in the Rust binary.
Reviewed by Cursor Bugbot for commit b015942. Configure here.
| path: path.display().to_string(), | ||
| source, | ||
| }) | ||
| } |
There was a problem hiding this comment.
Relative paths forced absolute
Medium Severity
Relative explicit paths are always converted to absolute paths before they appear in JSON top-level keys and in client / path fields. The Python CLI keeps the caller’s relative path strings for those fields when a relative path is provided, so equivalent invocations can produce different JSON.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit b015942. Configure here.
| skills: vec![skill], | ||
| error: None, | ||
| }, | ||
| )); |
There was a problem hiding this comment.
Case insensitive skill root gate
Low Severity
When the input directory is a single skill, Rust treats any case variant of SKILL.md as a skill root via find_skill_md. Python’s explicit-path branch only accepts a case-sensitive SKILL.md filename before taking the single-skill path, so on Linux the two implementations can classify the same directory differently.
Reviewed by Cursor Bugbot for commit b015942. Configure here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
There are 4 total unresolved issues (including 3 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit b52966d. Configure here.
| && find_skill_md(&path)?.is_some() | ||
| { | ||
| skills.push(path); | ||
| } |
There was a problem hiding this comment.
Broken symlink aborts inspect
Low Severity
When listing skill candidates under a container directory, a broken child symlink makes fs::metadata fail and the whole inspect command errors out. Python’s inspect_skills_dir skips entries where os.path.isdir is false, so inspect can still succeed with the remaining skills.
Reviewed by Cursor Bugbot for commit b52966d. Configure here.


Adds an additive Rust
inspect --skills --jsonlocal vertical slice without changing the released Python CLI. It delegates frontmatter and redaction to the exact Python implementation, fails closed, and leaves discovery, MCP, network, packaging, and Windows deferred.On an M5 Pro: help 367.2→5.1 ms; full corpus 8.815→9.023 s; aliased skills 5.694→1.099 s. The docs define the supported boundary and reproducible benchmark.
Note
Medium Risk
New inspect path touches scanned skill content and depends on Python for redaction; equivalence tests and fail-closed behavior mitigate leakage, but behavior must stay aligned with
agent_scan.redactand frontmatter parsing.Overview
Introduces an additive experimental binary
snyk-agent-scan-rustthat implements onlyinspect --skills --json PATH...for explicitly supplied local skill paths. It does not replace the released Pythonsnyk-agent-scanCLI; unsupported modes error instead of falling back.The Rust layer handles filesystem discovery (case-insensitive
SKILL.md, symlink traversal with cycle detection, binary hashing for allowed extensions) and JSON shaped like Python inspect output. Secret redaction and SKILL frontmatter parsing stay on the exact Python implementation via a single long-lived worker (AGENT_SCAN_PYTHON), with stderr suppressed and no stdout JSON if the worker fails. A per-invocation target-identity cache deduplicates redaction when the same files appear under multiple lexical paths (e.g. symlink aliases).CI adds Rust local inspector workflow:
cargo fmt/test/clippy, release build, and pytest equivalence tests. Documentation andrust/bench_local_inspect.pydescribe the supported boundary and reproducible benchmarks..gitignoreignorestarget/and local Rust toolchain dirs.Reviewed by Cursor Bugbot for commit a0c6540. Bugbot is set up for automated code reviews on this repo. Configure here.