Repository navigation
Conversation
Signed-off-by: Whj9283 <1621370123@qq.com>
rng1995
left a comment
There was a problem hiding this comment.
[SkillSpector Review]
Hi @agentsope, thank you for your contribution to SkillSpector — we really appreciate the time you put into this! A few items need attention before it can be merged; details below.
This PR makes _RP1_NPX_CMD match on a single line only ([ \t]+ instead of \s+) and adds \b before npx. That fixes the cross-line false positives in #639: name: npx followed by the next frontmatter line, and prose that wraps after "npx". I compared the old and new regexes, including the pin check, on shell, JSON, YAML and TOML MCP-config forms.
The single-line restriction is correct for shell commands. npx -y …, npx --yes …, unpinned pkg@latest and pinned @x.y.z behave exactly as on main, and CRLF is unaffected. However, the change also removes two things that main currently detects.
Findings
-
[Blocking]
src/skillspector/nodes/analyzers/mcp_rug_pull.py:125,tests/test_mcp_rug_pull.py:62-66: the\bstopspnpxfrom matching.pnpxis pnpm's npx-style runner (an alias ofpnpm dlx). Likenpx, it fetches and runs the latest version of an unpinned package.pnpx @scope/mcp-serverreports RP1 on main and nothing on this head, and the newtest_rp1_npx_requires_a_word_boundaryasserts that miss.Please keep matching the runner, e.g.
\bp?npx[ \t]+…or(?<![\w-])p?npx[ \t]+…, and change the test to expect RP1 forpnpx. If you want a word-boundary test, use a non-runner identifier. Optionally, as a follow-up:bunx,pnpm dlxandyarn dlxare not detected on main either. -
[Blocking]
src/skillspector/nodes/analyzers/mcp_rug_pull.py:125: YAML MCP configs lose RP1 coverage. On main,command: npx(or Goose'scmd: npx) on its own line, followed byargs:, matched across the newline, and the pin check then read theargsline. For example:mcpServers: fs: command: npx args: ["-y", "@scope/server"]
This reported RP1 on main, as did the block-list form used by Continue and Goose (
args:followed by- "-y"/- "@scope/server"). The pinned flow form ("@scope/server@1.2.3") was correctly skipped. On this head neither form produces RP1.Nothing else covers them. The manifest check runs the same regex over
str(manifest), where a quote followsnpx('command': 'npx'). The coverage on main was accidental (the matched text isnpx\n args), but it is the only RP1 coverage YAML MCP configs have. Before dropping cross-line matching, please add an explicit, bounded matcher: acommand/cmdkey whose value isnpx/pnpx, followed within a few lines by anargslist (flow or block) whose package token is checked with_VERSION_PIN_RE. Please include tests where an unpinned config fires and a pinned one produces no finding. -
[Non-blocking] JSON configs (
"command": "npx", "args": ["-y", "pkg@latest"], single-line or multi-line) are detected neither on main nor on this head, because the regex needs whitespace right afternpxand JSON has a closing quote there. This is not a regression. The matcher from (2) could also cover.mcp.json/claude_desktop_config.json, which are the most common MCP config format. -
[Non-blocking]
_RP1_UVX_CMD,_RP1_PIP_INSTALLand_RP1_DOCKER_CMD(L128–139) still use\s+and have the same cross-line false positive. For example,name: uvx\ndescription: reproproducesuvx\ndescription, and "…with docker run" followed by--rm exampleproducesdocker run\n--rm. Consider applying the same fix, together with the config-aware handling, here or in a follow-up. -
[Note] #641 also edits
mcp_rug_pull.py(_RugPullBudget.emit) andtests/test_mcp_rug_pull.py(test_rp1_npx_unpinned). Those hunks do not overlap with this PR's and merge cleanly.
Tests/CI
All checks pass. The new tests cover the #639 false positives and same-line flags. However, the word-boundary test locks in a false negative, and no test covers MCP config forms (YAML in particular) or tabs.
Decision: Changes Requested (reviewed head 0db07011e7db94302b6bb4f4120a4a8d33ce5cea)
Signed-off-by: Whj9283 <1621370123@qq.com>
rng1995
left a comment
There was a problem hiding this comment.
[SkillSpector Review]
Hi @agentsope, thank you for the follow-up commit that brings back pnpx and adds a dedicated YAML matcher!
Value and readiness: The single-line npx regex fixes the #639 false positives from frontmatter and wrapped prose, and pnpx is detected again. The new YAML matcher restores RP1 for the common layout where args comes right after command: npx. However, other YAML layouts that main reports are still lost. In two of them, a skill author can hide an unpinned server just by reordering or adding keys. One more change is needed before merge.
Previous findings:
-
pnpxdropped by\b: Resolved._RP1_NPX_CMDis now\bp?npx[ \t]+…(mcp_rug_pull.py:125).test_rp1_pnpx_unpinnedasserts the finding, and the word-boundary test now usesfoonpx.
-
- YAML MCP configs lose RP1: Partly resolved, still open.
_iter_config_npx_commandscoverscommand:/cmd:followed directly by a flow or blockargslist, with unpinned and pinned tests. The remaining gaps are in Material finding 1.
- YAML MCP configs lose RP1: Partly resolved, still open.
-
- JSON configs not detected (non-blocking): Still open. This is not a regression and is fine as a follow-up.
-
- Cross-line false positives in uvx/pip/docker (non-blocking): Still open.
_RP1_UVX_CMD,_RP1_PIP_INSTALLand_RP1_DOCKER_CMDstill use\s+. Fine as a follow-up.
- Cross-line false positives in uvx/pip/docker (non-blocking): Still open.
Material findings
-
[Blocker]
src/skillspector/nodes/analyzers/mcp_rug_pull.py:202,:128: the matcher only acceptsargsas the next sibling key. It also never matches whencommandis the first key of a list item. Both of these valid configs report RP1 onmainand nothing on this head:mcpServers: fs: command: npx env: FOO: bar args: ["-y", "@scope/server"]
servers: - command: npx args: ["-y", "@scope/server"]
The same happens with
type: stdio,cwd:ordescription:betweencommandandargs. At L202, the first key at the same indent that is notargsends the search._RP1_CONFIG_RUNNER(L128) does not allow a leading-. Two more layouts thatmainreports are also missed:argsplaced beforecommand, and a path-qualified runner (command: /usr/local/bin/npx). Addingenv: {}between the two keys is enough to hide an unpinned server.Please search all sibling keys of the same mapping, before and after the command line. Stop when indentation drops below the
commandkey, and keep the line bound. Treat- command:as a key at the column after-, and preferably accept a runner path ending in/npxor/pnpx. Please add tests for an intervening key, the list-item form andargsbeforecommand, each with a pinned version that produces no finding. -
[Non-blocking]
src/skillspector/nodes/analyzers/mcp_rug_pull.py:226-227: YAML comments on theargsline are tokenized as arguments. Takeargs: # "@scope/server@1.2.3"followed by- "@scope/server": the commented string becomes the package, the pin check passes, and the unpinned server is not reported.mainhas the same gap, so this is not a regression. The new matcher already handles a#comment on thecommandline. Dropping an unquoted#…tail from eachargsline before tokenizing would close this gap. -
[Non-blocking]
src/skillspector/nodes/analyzers/mcp_rug_pull.py:338: the YAML finding's message andmatched_textinclude the raw multi-line block, e.g.'command: npx\n args: [...]'. Consider naming the runner and package in the message (e.g.npx @scope/server) so terminal, Markdown and SARIF output stay on one line.
PIC tradeoffs: None identified.
Verification and gaps:
- Since the last review, the only new commit is
3f8e7d2. It is an author change, not a merge ofmain. The full diff against the merge base contains only the regex change, the YAML matcher and its tests. - I compared
main's and this head's npx regex literals in an isolated harness, and traced_iter_config_npx_commandsby hand on the YAML inputs above. Adjacent flow and blockargsfire, and the pinned adjacent forms are skipped. The interveningenv/type, list-item, args-first and path-qualified forms fire onmainand not here. - Shell forms behave as before:
npx -y …,pnpx …, CRLF. A YAML line with a full shell command (command: npx -y pkg) is reported once, because the config runner regex requires a bare runner value. - Overlap with #683: the two PRs change different hunks, and
git merge-treeof both heads is clean. #683's check, which only accepts a pin on the package token, is consistent with this matcher's check of the first package token. - CI: all 6 checks are green. Per policy, I did not run the tests locally.
Decision: Changes Requested (reviewed head 3f8e7d2331097ff3529a822ff9bbd25b1abe065a)
Signed-off-by: Whj9283 <1621370123@qq.com>
|
Thanks for the follow-up review. I've addressed the remaining YAML coverage blocker and the related comment-tokenization gap, and merged current Updated head: The bounded matcher now searches sibling keys on either side of Regression controls cover unpinned and pinned packages in ten layouts, separate mappings/list items, nested decoy args, fake pins in comments, CRLF, command-line locations and both sides of the line bound. The original frontmatter/wrapped-prose false-positive controls and pnpx behavior remain covered. An independent synthetic check also verified all 288 permutations of four sibling fields, three runners, mapping/list forms and pin states against parsed YAML semantics. Validation:
This follow-up does not expand JSON support, other runner regexes, or the optional multi-line report presentation change. Please re-check the YAML blocker on the updated head once hosted CI completes. |
Signed-off-by: Whj9283 <1621370123@qq.com>
|
Thanks for flagging the empty quoted argument case. I've reproduced and fixed the crash when an empty argument is encountered before the first package operand. Updated head: The tokenizer now chooses the quoted capture with an explicit Added 29 deterministic cases covering single/double quotes, flow/block YAML args, leading/flag-following/trailing empty arguments, pinned controls, and continued scanning of another server and file. The existing line bounds and runtime checks are unchanged; this is scoped to empty-argument handling. Validation:
Please re-check the empty-argument finding on the updated head once hosted CI completes. |
rng1995
left a comment
There was a problem hiding this comment.
[SkillSpector Review]
Hi @agentsope, thank you for the sibling-key rework, the YAML comment stripping and the thorough layout and empty-argument test matrices!
Value and readiness: This now fixes #639, plus a newer false positive on current main. There, cross-line matching combined with #683's operand-only pin check reads the next YAML key (args) as the package. As a result, every YAML command: npx config is reported, pinned or not.
With this head:
- pinned YAML configs are silent;
- every layout from the last review (intervening keys, list items,
argsbeforecommand, path-qualified runners) reports unpinned servers.
However, two ordinary YAML layouts that main reports are still missed, so a config can hide an unpinned server just by its formatting:
- block sequences written at the key's own indentation, which is PyYAML's default
yaml.dumpoutput and kubectl's; - an
argskey more than 8 physical lines fromcommand, for example after anenvblock with seven variables.
One more change is needed before merge.
Previous findings (review at 3f8e7d2). Since then:
7f3ddfdmerges main at4a55062, which includes #683, together with your matcher rework.ed2d3a6fixes the empty-argument handling.
The merge was conflict-free, and only the PR's two files differ from an automatic merge of the same parents.
-
[Blocker] The YAML matcher only accepted
argsas the next sibling and rejected- command:: Resolved for every layout I listed.- These now report unpinned servers and skip pinned ones: intervening
env/type/cwd/descriptionkeys,- command: npxlist items,argsbeforecommand, and/usr/local/bin/npxor./node_modules/.bin/pnpxrunners. test_rp1_yaml_sibling_layouts_preserve_pin_behaviorcovers 10 layouts, each pinned and unpinned.argsfrom another mapping or list item is not bound (test_rp1_yaml_does_not_bind_args_from_another_mapping).
A related layout gap remains; see Material finding 1.
- These now report unpinned servers and skip pinned ones: intervening
-
[Non-blocking] A
#comment on theargsline could supply a fake pin: Resolved._strip_yaml_comment(mcp_rug_pull.py:178) drops unquoted comments and keeps a quoted#, andtest_rp1_yaml_comments_cannot_supply_a_fake_package_pincovers it. -
[Non-blocking] Multi-line message and
matched_textfor YAML findings: Still open (mcp_rug_pull.py:410-419). Optional. -
[Non-blocking, first review] JSON MCP configs are not detected: Still open. Not a regression; main misses them too.
-
[Non-blocking, first review]
_RP1_UVX_CMD,_RP1_PIP_INSTALLand_RP1_DOCKER_CMDstill match across lines with\s+(:144-154): Still open. Fine as a follow-up. -
[Copilot] An empty quoted argument crashed the analyzer: Resolved in
ed2d3a6(:284-291), covered by a 28-case matrix.
Material findings
-
[Blocker]
src/skillspector/nodes/analyzers/mcp_rug_pull.py:277,:252,:227,:270: two ordinary YAML layouts that main reports produce no RP1 at this head.(a) Block sequences at the key's indentation. YAML allows a block sequence value to start at the same column as its key. Both PyYAML's default
yaml.dumpand kubectl write configs this way.- At L277 the
argscollector stops at the first line withindent <= args_indent, so it collects nothing. - At L252 the sibling search stops at a
- itemline in the command's column, because that line is not a key.
Each of these reports nothing here and RP1 on main:
mcpServers: fs: command: npx args: - -y - "@scope/server"
servers: - command: npx args: - -y - "@scope/server"
mcpServers: fs: command: npx autoApprove: - read_file args: ["-y", "@scope/server"]
(b) The 8-line window counts a sibling's nested lines. I asked you to keep a bound last round, and that is still right; the problem is what it counts. L227 and L270 count physical lines, so a sibling's value uses up the bound. Main reports all of the following, and this head reports none:
- an
envblock with seven variables betweencommand: npxandargs: ["-y", "@scope/server"], such as thePGHOST,PGPORT, ... settings for a Postgres server; - a 7-line
description: |in the same position; - an
argslist with eight flag lines before the package.
Expected fix:
- When collecting
args, also accept-lines at theargskey's column. - In the sibling search, treat a
-line at the command's column as part of the previous sibling's value and skip it. - Count only same-column sibling keys (and sequence items) toward
_RP1_CONFIG_MAX_LINES. Skip deeper-indented lines without counting them, under a larger hard cap plus the existing runtime checks.
Tests to add, each unpinned and pinned:
- the indentless mapping form;
- the indentless list-item form;
- an indentless sibling list between
commandandargs; - an
envblock longer than the window.
- At L277 the
-
[Non-blocking]
mcp_rug_pull.py:283-296, re Copilot's--packagecomment. Both the YAML path and the shell path treat the first non-flag token as the package, so option values are misread:["-p", "@scope/helper@1.2.3", "-p", "@scope/server", "server-bin"]reads as pinned, although both packages are installed.["--registry", "http://10.0.0.1:8080", "@scope/server"]also reads as pinned, because:8080matches the pin regex.
The shell path has applied the same first-operand rule since #683, and
npx -p … -p …is missed on main as well. Main reports the YAML forms only through the cross-line match that also flags pinned configs. A follow-up that parses-p/--package/--registryvalues for both paths fits better than this PR.Copilot's exact example is not an unpinned fetch: with
--package, npm runs the first positional as a command and does not install it. -
[Non-blocking] PR description: it still describes only the regex change. Please add the YAML matcher, which is now most of the diff.
PIC tradeoffs: None identified.
Verification and gaps:
- History:
7f3ddfdis a merge commit (parents3f8e7d2and main4a55062) that also carries author changes.- An automatic merge of the same parents is conflict-free, and
7f3ddfddiffers from it only inmcp_rug_pull.pyandtests/test_mcp_rug_pull.py. - The PR diff against its merge-base touches only those two files.
- Main has not changed
mcp_rug_pull.pysince4a55062, andgit merge-treeagainst current main (e9f7427) is clean.
- An automatic merge of the same parents is conflict-free, and
- #683: it is included through the merge, and the shell loop still calls
_operand_has_version_pinunchanged. The YAML matcher pin-checks only the first positional token, which is consistent with #683. - Harness: I transcribed the main and head regex literals (checked verbatim against the source) and wrote my own model of
_iter_config_npx_commandsfrom the diff.- The model reproduces all 71 expectations in the PR's new tests.
- I then ran 25 layouts, each pinned and unpinned, against main and this head; finding 1 comes from those results.
- PyYAML's
yaml.dumpof{"command": "npx", "args": [...]}emits the indentless form from finding 1(a).
- Shell forms: unchanged.
npx -y …andpnpx …are reported,foonpxdoes not match, CRLF is handled, andcommand: npx -y pkgis reported once. - Not run: contributor tests (policy).
- CI: all 6 checks green on
ed2d3a6. GitHub's mergeability is still UNKNOWN; the local merge-tree is clean. - Overlaps: no other open PR changes RP1. #234 only adds an unrelated test in a different file.
Decision: Changes Requested (reviewed head ed2d3a6777490dab526cdfba975cd776f6b1e69a)
| if not stripped or stripped.startswith("#"): | ||
| continue | ||
| indent = len(candidate_line) - len(candidate_line.lstrip(" \t")) | ||
| if indent <= args_indent: |
There was a problem hiding this comment.
[Blocker] A block sequence at the key's own indentation is valid YAML and is PyYAML's default yaml.dump output and kubectl's (args: followed by - -y / - "@scope/server" in the same column). This check stops at the first - line, so nothing is collected and command: npx plus that unpinned args list reports no RP1 (main reports it). The same happens for the list-item form (- command: npx / args: / - "@scope/server"). The sibling search has the same problem at L252: a - read_file line under a sibling such as autoApprove: ends the search, so a later args is never found. Please accept - lines at the args key's column here, skip them as part of the previous sibling's value at L252, and add unpinned/pinned tests for these forms.
| for direction in (-1, 1): | ||
| if direction == -1 and command.group("item"): | ||
| continue # This command is already the first key in its list item. | ||
| for distance in range(1, _RP1_CONFIG_MAX_LINES + 1): |
There was a problem hiding this comment.
[Blocker] The bound counts physical lines, including a sibling's nested value lines. A realistic env block with seven variables (e.g. PGHOST, PGPORT, ... for a Postgres server), or a 7-line description: |, between command: npx and args: ["-y", "@scope/server"] hides the unpinned server; main reports it. The args collector at L270 has the same limit, so eight flag lines before the package also hide it. Please count only same-column sibling keys (and sequence items) toward _RP1_CONFIG_MAX_LINES, skipping deeper-indented lines without counting them, under a larger hard cap plus the existing runtime checks. Please also add a test with an env block longer than the window.
Signed-off-by: Whj9283 <1621370123@qq.com>
Signed-off-by: Whj9283 <1621370123@qq.com>
Signed-off-by: Whj9283 <1621370123@qq.com>
|
Addressed the remaining YAML layout blocker and updated the PR description. New head:
Validation: the new layout selection reproduced 24 failures with 24 passing controls before the implementation change. Both MCP rug-pull test files now pass 163 tests. An independent check validates 1,152 parsed YAML layouts. The full non-integration/non-provider suite passed 8,763 tests (14 skipped, 134 deselected, 4 xfailed). Repository-wide Ruff lint/format, targeted mypy and whitespace checks pass. No sample commands or live provider calls were executed. The description now covers the YAML matcher and its bounds. Option-value parsing, JSON support, other runners' cross-line behavior and single-line presentation remain separate follow-ups as suggested. Please re-check the layout blocker once hosted CI completes. |

Summary
Closes #639.
Bounds and scope
The sibling search considers up to eight sibling keys in each direction, with a separate hard cap of 256 physical lines per direction. Argument collection has a 128-physical-line cap. Extremely large configurations beyond these bounds can remain unrecognized; this is a bounded text matcher, not full YAML semantic analysis. Tests cover both sides of each limit.
JSON MCP configuration support, npx option-value parsing (
-p/--package/--registry), cross-line handling for uvx/pip/docker and optional single-line report presentation are separate follow-ups, not addressed here.Validation