Repository navigation
feat(plugin): Stage C — native loading, more manifests, static risk, runtime evidence - #171
Conversation
rng1995
left a comment
There was a problem hiding this comment.
Review of Stage C at 16ea9ba: correctness and security findings. Each inline comment was reproduced against a snapshot of this SHA or confirmed by reading the code, and every one has a severity, a failure scenario and a suggested fix.
Highest priority:
--probe-mcpexpands host environment variables into plugin-declared headers and sends them to the plugin's own URL.- The
hooks.allowed_urlsprefix match can be bypassed with a look-alike host.
Beyond those, most findings are detection bypasses in plugin_component_risk.py (caps and truncation that fail open, ReDoS), CVE-audit coverage gaps, and canary false negatives and false positives.
e1a0006..28e804a (native loading) was pushed after this SHA. I re-checked every finding against 28e804a, and all still apply. I dropped one docs finding (the hook census never being deployed) because native staging now wires hook_census.sh. The existing CodeQL alerts are not repeated here.
Live Harbor sample (non-blocking)A small Harbor sample ran on the equivalent internal code with Claude Code (native and wrapper loading) and Codex (native loading). It confirmed that native loading works (Claude Code reports the plugin, the skill and the MCP server as
🤖 Generated with Claude Code |
…atic risk checks Detect and validate .codex-plugin, .cursor-plugin and the Agent Plugins v1 root plugin.json next to the Claude manifest, with per-format component profiles and flagged name/version conflicts. Oversize or non-UTF-8 client manifests no longer hide their hooks or MCP servers. Add Tier 1 static risk checks: hook events, handlers, auto-approve, fetch-and-execute, context injection and hooks.allowed_urls; subagent and command privileges; skill frontmatter hooks and allowed-tools; monitor and LSP commands; permission-bypass flags in every config node. Unreadable or oversize component files fail closed, and whole-tree scans cover declared skill folders. Co-authored-by: Narendran Raghavan <nraghavan@nvidia.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Christopher Kevin <christopherk@nvidia.com>
Add npm and container image CVE audits for plugins. An audit that runs offline, hits its discovery cap or cannot find its scanner reports INCOMPLETE instead of passing. Add --resolve-endpoints: DNS, redirect and TLS checks against the endpoint policy for every resolved address, with WHATWG redirect parsing and time budgets that report unchecked endpoints. Add opt-in parity with claude plugin validate --strict; its child process gets a scrubbed environment and a throwaway HOME. Co-authored-by: Narendran Raghavan <nraghavan@nvidia.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Christopher Kevin <christopherk@nvidia.com>
Add --plugin-load wrapper|native|auto (default wrapper, which keeps the PR #28 behavior). Native Claude Code loads the plugin with --plugin-dir: skills, rules, MCP servers including ${CLAUDE_PLUGIN_ROOT} launches, hooks, agents, commands, output styles, LSP servers, settings and userConfig. Codex, OpenCode and Hermes get their own config files, and OpenCode subagents keep their tool limits. Only the with-plugin arm changes. A per-trial load census records what each harness listed or loaded. A failed MCP server never counts as loaded, and declared components that were not staged make the run INCOMPLETE. auto falls back to the wrapper per agent and records why. Hermes is experimental and refused with the OpenAI and OpenAI-compatible providers. Launch rewriting is linear-time. Co-authored-by: Narendran Raghavan <nraghavan@nvidia.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Christopher Kevin <christopherk@nvidia.com>
Add per-arm canary exfiltration checks (incl. Hermes, OpenCode and subagent tool calls), a hook execution census, and --probe-mcp proof that connects only to policy-checked addresses and never sends host secrets. Native skill and MCP tool names are matched for every harness, so native runs credit the components they used. Render load, census, canary, signals and coverage in JSON, Markdown, HTML, SARIF, the CLI summary and the BENCHMARK card, with untrusted text escaped. Reports no longer crash on a missing baseline score, and a failed arm no longer drops the with-plugin evidence. Co-authored-by: Narendran Raghavan <nraghavan@nvidia.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Christopher Kevin <christopherk@nvidia.com>
The accuracy, goal accuracy and behavior judges now see write-tool content under every harness's argument names (for example OpenCode patchText), instead of a silent 200-character cut. Long text keeps its start and end around a visible truncation marker, budgets are filled by priority so the final answer, the skill call, test results and the latest write to each path survive, and judge text is redacted. The configurable evidence budgets from #160 drive this logic. Host and verifier copies stay identical, and the verifier runs on Python 3.9 and 3.11 (CI smoke added). Also update the docs, the changelog and the Gitleaks allowlist for synthetic test fixtures. Co-authored-by: Narendran Raghavan <nraghavan@nvidia.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Christopher Kevin <christopherk@nvidia.com>
6f0e1a7 to
429aa26
Compare
38a4f64
into
naren/plugin-evaluation-all-tiers
Summary
Stage C and open-gap work for plugin evaluation, stacked on #28. The base is
naren/plugin-evaluation-all-tiers; it retargets tomainonce #28 merges. It closes the remaining gaps from the plugin-evaluation review: more manifest formats, deeper static risk checks, runtime evidence, and native plugin loading for each harness.What's included
.codex-plugin/plugin.json, Cursor.cursor-plugin/plugin.json, and Agent Plugins v1 (plugin.jsonwith an agent-plugins.org$schema), in every tier. Precedence when several are present:agent_plugin.yaml>.claude-plugin> Agent Plugins >.codex-plugin>.cursor-plugin. The other manifests are recorded undermanifest_declarations, and name/version conflicts are flagged. Components flow into the existing inventory, MCP policy and Tier 3 staging. Codexhttp_headersare checked together withheaders. A rootplugin.jsonthat is not UTF-8 or exceeds 1 MiB no longer fails discovery; an additional manifest like that is a MEDIUMplugin_manifest_additional_invalid. An Agent Pluginsplugin.jsonwith a JSON syntax error is still auto-detected as a plugin, so the error is reported.tools/allowed-tools/permissionMode. Flags unrestrictedBash, wildcard tools,bypassPermissions/acceptEdits, and subagents that inherit all tools while write-capable MCP servers are present. MCP denials are matched against Claude Code'smcp__plugin_<plugin>_<server>__*tool names,--read-only=falseis not treated as read-only, and tool lists are checked in full, so padding cannot hide unrestrictedBash.curl … | sh, including path-prefixed shells,env/sudo/xargswrappers, intermediate pipe stages, and download-then-run), http hook endpoint policy with ahooks.allowed_urlsallowlist compared as parsed URLs (scheme, host, port, and path on a segment boundary), and inline secrets, including credentials embedded in URLs. Matchers are classified without a backtracking regex engine and fail closed when too long or complex. Flat hooks in Cursor and Agent Plugins extensions get the same checks. A hook script that can't be analyzed is a HIGHplugin_hook_script_unanalyzed, and hitting a scan limit is a HIGHplugin_hook_scan_truncated. URL credentials are stripped from finding messages.dependencycheck audits npm exact pins and container image refs with OSV-Scanner, falling back tonpm audit --package-lock-only --ignore-scripts, then Grype or Trivy for images. Images from every manifest the plugin ships are covered. Plugin files never reach a scanner directly (synthesized lockfile). The plugin audit never passes silently: no scanner, a failed scanner run (pip-audit included), or a manifest, lockfile or Dockerfile that can't be read within bounds makes the check INCOMPLETE.validate --resolve-endpoints: resolves MCP (including servers that only an additional manifest declares) and http-hook hosts, flags private or metadata answers, and sends one HEAD request (TLS 1.2 minimum, no redirect following, no credentials). Non-public hosts are never contacted, and hosts with an unexpanded${VAR}are skipped.--checks claude-validaterunsclaude plugin validate --strict --jsonwhen the CLI is present and reports disagreements.hook_census.shwraps natively loaded hooks and records runs, failures and duration without changing stdin, stdout or the exit code. Natively staged hooks that ran becomeexercisedin coverage. Hooks that weren't staged or loaded are never promoted from census lines.sh -c/bash -lc/evalpayloads, heredocs and argv-list commands, with taint tracked across statements. Whole-environment dumps count; reading one variable by name does not. URLs count only for network, MCP and command tools.canary_exfiltration--probe-mcp: probes the author's URL MCP servers from the host (initialize+tools/list, endpoint policy first), then upgrades status from in-agent evidence:declared → reachable-host → reachable-in-agent → used-successfully. The probe sends only literal declared headers; host environment variables are never expanded unless named with--probe-mcp-env NAME, which can be repeated.loadedandexercised(precedence exercised > loaded > staged). Subagents and commands observed at runtime count as exercised. The headline counts every evaluated state (staged, loaded, exercised).--plugin-load {wrapper,native,auto}(defaultwrapper). The with-plugin arm loads the plugin the way each harness does: claude-code via--plugin-dir(skills, commands, agents, hooks wrapped by the census,.mcp.json, output styles; rules in$CLAUDE_CONFIG_DIR/rules); codex (skills,$CODEX_HOME/AGENTS.md,config.tomlmcp_servers); opencode (OPENCODE_CONFIGmcp and instructions, agents, commands); hermes (skills and MCP; rules stay in the wrapper). Codex, Cursor and Agent Plugins manifests are staged through their normalized Claude view. A load census inside the container marks componentsloaded. Member-skills and no-plugin arms are unchanged, so lift stays comparable. Hermes is added as a Tier 3 agent (containers only). No bypass flags are ever added, and configs carrying them fail closed.All new data is rendered in the JSON, Markdown, HTML and CLI plugin sections (plugin-controlled text is escaped in the CLI report), and documented in
docs/plugin-evaluation.mdx, including updated limitations.Review follow-ups
All 30 comments from the review and the 3 CodeQL alerts are addressed and resolved. The fixes are folded into the table above. The most important:
--probe-mcpcredential exfiltration (High): headers declared as${VAR}were filled from the host environment and sent to the plugin's own URL. Now only variables named with--probe-mcp-envare expanded.hooks.allowed_urlsbypass (High): matching was a string prefix, sohttps://hooks.example.com.evil.netpassed. Now parsed URLs are compared.Needs live validation (after review)
/logs/agentand passthrough of hook decisions.--probe-mcpagainst real streamable-HTTP/SSE servers and behind proxies.claude plugin validate --jsonoutput on current Claude Code builds.--plugin-dirin-pmode,$CLAUDE_CONFIG_DIR/rules, the Codexconfig.toml/AGENTS.mdappends, the OpenCode config merge), hooks firing under Harbor's--permission-mode=bypassPermissions, the Hermes install and provider routing, and/skillevaland/logs/agentpermissions for non-root agent users. The census proves files are present, not that they were loaded.Verification
63f9f93):ruff check .: passes.pytest -n auto): 10,729 passed, 26 skipped, 0 failed.--probe-mcp-env.Review fixes
A review of the Stage C code (same design as this PR) turned up correctness and security defects. This push ports the 14 fixes that apply here, and merges the updated
naren/plugin-evaluation-all-tiers(#28 with its own ported review fixes) to clear this PR's conflict with its base. Each fix was confirmed against this branch before porting, and each ported regression test fails on the previous commit and passes after.hooks.allowed_urlsentries with userinfo, a query or a fragment match nothing; hook scripts are read under each format's root placeholder (${PLUGIN_ROOT},${CURSOR_PLUGIN_ROOT}, relative Cursor scripts, Agent Plugins namespaces); every resolved address must be allowlisted, whatever the DNS answer orderExternalTool.runenv layering, MCP from additional manifests, the additionalagent_plugin.yml, and the unreadable rootplugin.jsonwere already handled--probe-mcpconnections are pinned to the policy-checked addresses (answers the DNS-rebinding review comment); grading data (results dirs, generated output, evals source) is kept out of the native Claude Code plugin copy; only the selected manifest's components are staged natively, and hook sources with no staged handlers are reportednot_loaded; only the Hermeschatlaunch gets the native setup prefix; docs--probe-mcp-env)--probe-mcpprivate-host allowlist applies🤖 Generated with Claude Code