Skip to content

fix(2.13.x): parse-limit regression, gateway LLM output and OSV advisory cap - #1010

Open
mohgupta-ship-it wants to merge 4 commits into
yashraj/v2.13.0-basefrom
claude/nvcarps-ci-2.13-fixes
Open

mohgupta-ship-it wants to merge 4 commits into
yashraj/v2.13.0-basefrom
claude/nvcarps-ci-2.13-fixes

Conversation

@mohgupta-ship-it

Copy link
Copy Markdown
Member

Summary

Maintenance fixes for the 2.13.x line. A downstream CI pinned to 2.13.3 reported 193 incomplete scans in one day. The most common causes were static_parse_limit from the tool-misuse parser and llm_structured_response_invalid from semantic_developer_intent. This PR fixes the parser regression that 2.13.3 introduced, hardens LLM JSON output on gateway Claude routes, reports why each model answer was rejected, and raises the OSV per-package advisory cap. Each change is its own commit.

Commit Change
fix(parser) Backtick words in Python docstrings no longer cause static_parse_limit
fix(llm) Ask Claude models on OpenAI-compatible routes for exactly one JSON object when the schema is bound as json_schema
fix(llm) Add a content-free diagnostic to rejected-response ledger rows and logs
fix(sc4) Retain up to 64 OSV advisories per locked package (was 16)

Root causes

  • Parse limits: of 35 files that are partial only on 2.13.3, 18 were complete on 2.12.0 and on main. All 18 bisect to cd0e5791, the 2.13.3 backport of fix(static): preserve shell and browser template boundaries #948.
    • That backport stopped treating Python quotes as shell quotes, so a backtick word inside a string, such as else `tool` on PATH in a docstring, became its own command-word candidate.
    • Its operands are bounded by the string's value, but the 2.13-only string-value guard (0e96f345, not on main) keeps the whole-source fallback for many ordinary constructs, such as re.compile, os.makedirs, getattr and type(e).__name__. The scan therefore fails closed.
    • Fix: a docstring or other bare string statement is discarded, so its own body bounds the word. The exception is a module that reads docstrings back through __doc__ or getdoc.
    • The guard also treats non-executing os calls and the __name__, __qualname__, __module__ and __file__ attributes as inert. Shell sinks, reflection, dynamic imports and object escapes keep the fallback, and destructive operands inside a docstring still exhaust the check. All existing guard tests pass unchanged.
  • LLM validation: for the gateway route, the OpenAI provider binds strict json_schema, and the analyzer prompts carry no output-format instruction. A gateway can serve that format for Claude through a tool the model is not forced to call, because Claude 5.5 rejects forced tool calls. A prose answer then fails every bounded retry as "not a JSON object". Every failure in the CI sample is of that class. The instruction is limited to Claude model IDs on OpenAI-protocol clients bound with json_schema; validation is unchanged.
  • output_limit on poetry.lock: filelock==4.1.1 has 17 OSV advisories, one more than the per-package cap. The per-scan detail and result budgets are unchanged.

Validation

  • Replay of 59 production skills fetched at the commits from the failing jobs, static analysis only. Private inputs and reports are retained locally. reference_missing is treated as advisory, as in the downstream CI.

    2.13.3 This branch
    Skills with no blocking rows 19 26
    static_parse_limit rows 65 50
    output_limit rows 1 0

    No skill gained a blocking row. Mapped onto the 193 incomplete CI scans: 62 are in skills not available locally. Of the rest, 24 now clear on static grounds, against 4 on 2.13.3, and 10 more are left with only LLM rows.

  • Unit tests: 13,386 passed. 23 failures and 51 errors are identical on unmodified 2.13.3 in the local sandbox: the TP4 HTTP fixtures cannot bind a local port, the real-uv launcher tests cannot run, and three tests are timing-bound.

  • New tests: 24 parser tests, including docstrings read back through __doc__, getattr and getdoc, destructive docstrings, and assigned strings that reach a shell. Also 8 tests for the LLM output instruction, one OSV test, and the diagnostics tests.

  • ruff check and ruff format --check pass.

Not validated

  • The LLM change has not been run against the gateway; no gateway credential was available. With the new diagnostics, any remaining failure on that route will name the rejecting check in the CI report.

Out of scope

🤖 Generated with Claude Code

mohgupta-ship-it and others added 4 commits November 4, 2026 11:07
Ledger rows for a rejected structured response all said
ValidationError, which hid whether the model answered in prose,
dropped a required field, returned the wrong tool, or was cut short.

Tag every rejection with a fixed check code and add a content-free
diagnostic to the ledger row and log line: the check, the answer
channel (tool call or text), the provider's completion label, where
prose or code fences surrounded a JSON object, schema field paths
with their validation error types, and the check that rejected each
bounded attempt. Diagnostics are validated against a fixed grammar
and never carry model output, field values or undeclared keys.

Backport of the diagnostics change prepared for main; retries,
recovery and fail-closed outcomes are unchanged.

Signed-off-by: Mohit Gupta <mohgupta@nvidia.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A widely used package can carry more than sixteen advisories for one
locked version (filelock 4.1.1 has 17), and truncating them left the
dependency manifest partially inspected with output_limit. Raise the
per-package cap to 64; the per-scan detail-request and result budgets
still bound latency, and exceeding any cap still reports output_limit.

Signed-off-by: Mohit Gupta <mohgupta@nvidia.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The OpenAI provider binds a strict json_schema response format, and
the analyzer prompts carry no output-format instruction. An
OpenAI-compatible gateway can serve that format for Claude through a
tool the model is not forced to call (Claude 5.5 rejects forced tool
calls), so a prose answer leaves no JSON object and is rejected as
llm_structured_response_invalid after the bounded retries.

For Claude model IDs on OpenAI-protocol clients bound with
json_schema, append an instruction to return exactly one JSON object
matching the schema, without prose or code fences. Other models and
methods keep their prompts; validation is unchanged.

Signed-off-by: Mohit Gupta <mohgupta@nvidia.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 2.13.3 shell-ownership backport (cd0e579) stopped treating Python
quotes as shell quotes, so a backtick word inside a string, such as
"else `tool` on PATH" in a docstring, became its own command-word
candidate. Its operands are bounded by the string's value, but the
2.13 string-value guard keeps the whole-source fallback whenever the
module has any recognized shell path or an unlisted os/dunder use, so
ordinary helpers that were complete in 2.12 and on main became
static_parse_limit.

- A docstring or other bare string statement is evaluated and dropped,
  so its own body bounds such a word whatever else the module does,
  unless the module reads docstrings back (__doc__, getdoc).
- The guard treats os calls that run no program (makedirs, kill,
  readlink, ...) and name attributes (__name__, __qualname__,
  __module__, __file__) as inert.

Shell sinks, reflection, dynamic imports and object escapes still keep
the fallback, and a destructive operand inside a docstring still
exhausts the bounded check.

Signed-off-by: Mohit Gupta <mohgupta@nvidia.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@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.

[SkillSpector Review]

Decision: Changes Requested.

Value and readiness

The maintenance work usefully reduces unnecessary partial scans, makes rejected model answers diagnosable, and retains more advisories under the existing aggregate budget. The new discarded-string exception still treats reachable docstrings as inert in a supported readback form, so it needs one correction before approval.

Material findings

Blocker — src/skillspector/python_tokens.py:941-951 and src/skillspector/nodes/analyzers/static_patterns_tool_misuse.py:3661-3665: preserve the fallback for aliased or indirect docstring readback. The new helper recognizes a literal "__doc__", an attribute named __doc__/getdoc, or a name spelled getdoc. It does not recognize from inspect import getdoc as doc followed by run(doc(find) + ' -rf /'), although that retrieves and executes the same function docstring as the tested inspect.getdoc(find) case. The imported helper is an ast.alias, and the call name is doc; neither matches the guard. getattr(find, '__' + 'doc__') similarly escapes the literal-only check. With a backtick word in find's docstring, python_discarded_string_spans therefore returns its span even when the whole-module shell/reflection guard has correctly refused isolated bounds. The new _discarded(owner) override then admits that span, and the operand check at lines 2627-2635 can limit inspection to the docstring body while excluding the later destructive operands. This is an unsound ownership proof for a value that actually reaches a shell, rather than a harmless documentation false positive. Separate genuinely discarded bare-string statements from retrievable docstrings, retain the conservative path when introspection aliases/indirect attribute access cannot be excluded, and add regressions for the aliased getdoc and computed __doc__ forms. The existing tests cover only explicit attribute/name/literal spellings.

No separate material blocker found in the other three changes. The gateway instruction is restricted to OpenAI-protocol clients, final model-ID components naming Claude, and json_schema/its default; validation, refusal/completion checks and bounded retries remain in place. The diagnostics derive channel, completion and shape from fixed codes, redact undeclared schema keys to *, exclude validation inputs/context, cap schema entries, and carry the field through batch, TP4, meta and ledger projection paths. The OSV per-package cap rises to 64 while the 64-detail-request, 256-result, response-byte and runtime budgets remain enforced, with limitations retained when exhausted.

PIC tradeoffs

Raising the per-package cap can spend more of the unchanged shared detail budget on an advisory-heavy early package, leaving later details partial; the implementation reports the limitation. Live gateway behavior remains unvalidated, so the additional instruction is a compatibility measure, not evidence of model-quality acceptance. Neither tradeoff requires weakening the existing validation boundary.

Verification and gaps

Read the complete 17-file diff and pinned implementation/test context for discarded-string ownership, the global shell guard and bounded operand consumer, gateway binding, structured-response checks, diagnostic propagation and OSV budget enforcement. Read the complete empty review/comment/thread history. Fresh metadata confirms the head is unchanged, open and non-draft, and all six current checks report success in CI run 41781149578. The older local failures described in the PR body are not being presented as current failures. The readback finding is a static source trace; no end-to-end scan result or runtime reproduction is claimed. No contributor code, tests, snippets, imports, builds, setup, hooks, scanner invocations or model calls were executed.

Reviewed head: 09c81a42b285ee9c19711c408ad2eb637ec89fd1.

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.

2 participants