Add plugin REST API, thv ai-plugin CLI, and HTTP client (Phase 4) - #5782
Conversation
There was a problem hiding this comment.
Large PR Detected
This PR exceeds 1000 lines of changes and requires justification before it can be reviewed.
How to unblock this PR:
Add a section to your PR description with the following format:
## Large PR Justification
[Explain why this PR must be large, such as:]
- Generated code that cannot be split
- Large refactoring that must be atomic
- Multiple related changes that would break if separated
- Migration or data transformationAlternative:
Consider splitting this PR into smaller, focused changes (< 1000 lines each) for easier review and reduced risk.
See our Contributing Guidelines for more details.
This review will be automatically dismissed once you add the justification section.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #5782 +/- ##
==========================================
+ Coverage 72.48% 72.51% +0.03%
==========================================
Files 733 735 +2
Lines 75842 76255 +413
==========================================
+ Hits 54971 55294 +323
- Misses 16960 17052 +92
+ Partials 3911 3909 -2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Large PR justification has been provided. Thank you!
|
✅ Large PR justification has been provided. The size review has been dismissed and this PR can now proceed with normal review. |
CodeQL false positivesThe CodeQL check reports 20 "Uncontrolled data used in path expression" ( Why they're false positivesThe taint flow is: REST API request → CodeQL can't see through the validation barrier, but the input is sanitized at multiple layers:
What's affectedAll 20 alerts are on pre-existing files from Phases 2-3 (not files changed by this PR):
This PR adds new REST API call paths that reach these existing functions, which is why CodeQL reports them as "new." ResolutionFollowing the established repo pattern (49 previously dismissed |
a45b64d to
6b25884
Compare
CodeQL
|
samuv
left a comment
There was a problem hiding this comment.
Thanks for putting this together — the skills-system mirroring is executed faithfully, and the security follow-through is genuinely good: validateOCIRegistryHost closes the OCI-pull SSRF gap before Pull is reached, with tests asserting Pull is never invoked for rejected hosts, and the DNS-rebinding limitation honestly documented in the comment. The exposure layer (REST + client + CLI) is consistent with the skills precedent, test coverage is thorough and table-driven, generated docs were regenerated in the same PR, and docs/arch/14-plugins-system.md was updated.
Overall this reads as close to merge-ready — nothing blocking from my side. I've left four inline comments (two small should-fixes, two questions), plus a few PR-level notes below.
PR-level findings
- [SHOULD-FIX] PR body hygiene — the template's two required sections are absent: "Type of change" (exactly one checkbox) and "Test plan" (at least one checkbox). The Testing section covers the substance, so this is just re-adding the two sections to keep the template contract intact.
- [NIT] DCO — none of the 7 commits carry a
Signed-off-bytrailer, which CONTRIBUTING.md asks for on all commits. CI doesn't gate on it and recent main history suggests it's loosely applied, so noting only for awareness. - [NIT] follow-up tracking — commit c2c98db notes the skills OCI pull paths have the same SSRF gap, intentionally left out of this PR. A tracking issue would make sure that follow-up doesn't get lost.
What's working well
- The nil-slice guards on
listPlugins/listBuildsimprove on the skills original, and the client has a compile-timePluginServicecheck plus httptest-driven tests for all 10 methods, including path escaping. - Swagger and CLI docs regenerated in-PR, arch doc updated, and commit messages are clear what-and-why — nice hygiene throughout.
CI note: everything is green except E2E Test Lifecycle (kindest/node:v1.33.7), which failed on both recent runs while v1.34.3 and v1.35.1 passed. This PR touches no operator code, so it looks unrelated/flaky — probably worth a re-run before merge.
Verdict: approve with nits — the two should-fix items are one-liners, and everything else is optional.
Expose the frozen PluginService interface over REST at /api/v1beta/plugins, add the thv plugin cobra command tree mirroring thv skill, and implement an HTTP client in pkg/plugins/client. Wire the plugin service into the API server with Claude Code and Codex materialization adapters, SQLite plugin store, and OCI registry client. The plugin info text output surfaces the executable surface inventory (hooks, commands, agents, skills) separately from declared-but-unmanaged components (mcpServers, lspServers) per the issue exit gate. Closes #5528
The plugin store opens the same SQLite DB file as the skill store. TestNewServer_ReadTimeoutConfigured already injected a mock skill manager to avoid SQLITE_BUSY; it now also injects a mock plugin manager for the same reason.
- Fix 'Declared (NOT managed):' to 'Declared (NOT managed by ToolHive):' per issue #5528 exit gate requirement - Delete parsePluginInstallClients (byte-identical to parseSkillInstallClients), call the existing parseSkillInstallClients instead - Replace custom sortedKeys generic with slices.Sorted(maps.Keys(m)) from Go stdlib
The plugin command group manages plugins for AI tools (e.g. Claude Code, OpenCode), not plugins for ToolHive itself. Rename the subcommand to 'thv ai-plugin' so users are not confused about what is being plugged into what. Supersedes RFC THV-0077, which specified 'thv plugin'. Only the CLI surface changes: command files are renamed plugin_*.go -> ai_plugin_*.go, the parent command Use field and the informational-command registration in commands.go are updated, and CLI docs are regenerated. The REST API (/api/v1beta/plugins), pkg/plugins, and swagger docs are unchanged.
The plugin install and content endpoints accept an OCI reference whose registry host is fully caller-controlled, then pull from it — an SSRF vector (e.g. 127.0.0.1:5000/... or 10.0.0.5/...). The git install path already guards this via validateHost in pkg/skills/gitresolver; the OCI paths had no equivalent. Add validateOCIRegistryHost in pluginsvc, mirroring the gitresolver logic (strip port, networking.IsLocalhost, net.ParseIP + networking.IsPrivateIP, TOOLHIVE_DEV=true bypass), and enforce it before Pull in both installFromOCI and getContentFromOCI. Rejections return 400 with an explicit SSRF-prevention message. Registry-name installs funnel through installFromOCI, so they are covered too. The skills OCI pull paths have the same gap; they are intentionally left untouched here.
- Name Claude Code and Codex (not OpenCode) in ai-plugin help; only those two materialization adapters are wired in the server. - Rename jargon labels in ai-plugin info text output: "Unmaterialized Components" -> "Not activated on this client" with an explanation, "Project-scope Degraded Clients" -> a plain-language user-scope fallback note, and drop the shouting capitals from "Declared (not managed by ToolHive)". JSON output is unchanged. - Add the AI-tool qualifier to every ai-plugin leaf command Short. - Drop the success print from ai-plugin builds remove to comply with the silent-success CLI rule. - Drain the response body before returning a JSON decode error in the plugins API client so connections can be reused. - Regenerate CLI docs.
Fix swagger annotation for GET /plugins/builds to reference pluginBuildListResponse instead of the skills buildListResponse, so the generated spec names the right schema. Update the install --clients help example to name the actually-supported clients (claude-code, codex) instead of opencode, and clarify the client maxResponseSize comment to describe it as a defensive response cap rather than referencing the server-side request-body limit.
6b25884 to
0035a8b
Compare
Summary
Exposes the frozen
PluginServiceinterface (Phases 2–3) over REST, CLI, and HTTP client — Phase 4 of the plugin lifecycle management effort (RFC THV-0077).This is a "mirror the skills system" change: the
PluginServiceinterface was already complete from Phases 2–3; this phase adds the exposure layer.Naming note: the CLI subcommand is
thv ai-plugin(notthv pluginas the RFC/issue say) — a deliberate UX decision so users aren't confused whether these are plugins for ToolHive or plugins for AI tools. They are AI-tool plugins.Changes
REST API (
pkg/api/v1/plugins.go+plugins_types.go)/api/v1beta/plugins: list, install, uninstall, info, validate, build, push, builds, builds/remove, contentValidatePluginNameon path-param endpoints (uninstall, info)/marketplaceroute deferred to Phase 5c ([plugins] Phase 5c: marketplace generate + signing (THV-0077) #5530)HTTP client (
pkg/plugins/client/)Clientimplementingplugins.PluginServiceover HTTPNewDefaultClient(env var → server discovery → default URL)ErrServerUnreachablesentinel errorvar _ plugins.PluginService = (*Client)(nil)CLI (
cmd/thv/app/ai_plugin*.go)skill*.go: install, uninstall, list, info, validate, build, push, builds, builds/removeai-plugin infotext output surfaces the executable surface inventory (commands/agents/skills/hooks) separately from declared-but-unmanaged components (mcpServers/lspServers) with aDeclared (not managed by ToolHive):annotation — per the issue exit gateServer wiring (
pkg/api/server.go)pluginManagerfield onServerBuilderandServercreateDefaultPluginManagerblock: SQLite plugin store, OCI store, packager, registry client, Claude Code + Codex materialization adapters, group managerdevModePluginRegistryClientmirrors the skills dev-mode OCI registry dispatch/api/v1beta/plugins+ plugin store cleanupSecurity hardening (panel review)
install,content) reject localhost/private-IP registry hosts beforePull— mirrors the git path'svalidateHostSSRF guard, with aTOOLHIVE_DEVbypass and tests assertingPullis never called for rejected hostsTests
pkg/api/v1/plugins_test.go— table-driven, all 10 handlers + JSON key assertions + edge cases (empty fields, group filter, nil-slice)pkg/plugins/client/client_test.go— all 10 methods viahttptest.Server+ connection error + URL encodingpkg/plugins/pluginsvc/oci_ssrf_test.go— OCI registry host validation + dispatch-path coverageLarge PR Justification
This PR is large because it is a templated exposure layer — it mirrors the existing skills system (API + CLI + client) for plugins in a single atomic change. The lines break down as:
docs/server/docs.go,swagger.json,swagger.yaml) — auto-generated bytask docs, cannot be split or hand-edited.docs/cli/thv_ai-plugin*.md) — auto-generated bytask docs.plugins_test.go+client_test.go+oci_ssrf_test.go) — test-only, exempt per contributing guidelines.The change is purely additive — no existing files are modified in their logic (only
server.goandcommands.goget new wiring). Per.claude/rules/pr-creation.md: "Large PRs acceptable for: generated code, dependency updates, docs-only, test-only changes."Related Issues
Part of #5525 (Epic: Plugin lifecycle management)
Closes #5528 (Phase 4: REST API + thv ai-plugin CLI + content preview)
Refs RFC PR stacklok/toolhive-rfcs#77
Testing
task build✅task test✅ (all unit tests pass)task lint-fix✅ (0 issues)task docs✅ (CLI docs + swagger regenerated)