Add basic SkillService implementation and API handlers#3804
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 #3804 +/- ##
==========================================
+ Coverage 66.89% 66.99% +0.09%
==========================================
Files 439 441 +2
Lines 43561 43689 +128
==========================================
+ Hits 29142 29270 +128
+ Misses 12166 12163 -3
- Partials 2253 2256 +3 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
I brought up some of the issues below in Slack but I'll summarise here for the wider audience.
This PR builds on top of an API contract that may need some tweaks. Currently, as it is, is a mix between REST and RPC like conventions which is inconsistent with the rest of ToolHives API.
I believe how other ToolHive resources do it:
- Groups:
POST / (create),DELETE /{name} (delete),GET / (list),GET /{name} (get) - Workloads:
POST / (create),DELETE /{name} (delete),GET / (list),GET /{name} (get)
How skills does it:
POST /api/v1beta/skills/install (create)- should bePOST /api/v1beta/skills/(although it was mentioned the issue on long running requests)POST /api/v1beta/skills/uninstall (delete)- should beDELETE /api/v1beta/skills/{name}orDELETE /api/v1beta/skills/{name}?scope=XGET /api/v1beta/skills/ (list)- fineGET /api/v1beta/skills/{name} (get)- fine
Install and uninstall seems like CREATE and DELETE with verb-named endpoints. validate, build and push aren't really REST (not CRUD on the skills resources), however given its probably more complexity and hassle to support RPC for those, maybe they can be done as sub-resources on the skills resources? Something like /skills/{id}/builds etc? Am aware we won't be able to make it perfect
There is also a route conflict risk between the GET /api/v1beta/skills{name} and GET /api/v1beta/skills/install if someone names a skill "install", "uninstall", "validate", "build" or "push". Doing some googling, Chi may resolve this by preferring statics routes over parametersed ones, but its a collision none the less in standard REST patterns.
Apologies for the late response on the skills stuff in general, I've not taken a look at any of it until now due to other priorities.
ce097c2 to
f5d56d2
Compare
f5d56d2 to
b01f878
Compare
aponcedeleonch
left a comment
There was a problem hiding this comment.
A few nits after re-reviewing with the latest commit applied.
Implement the SkillService with CRUD operations and wire it into the API server, replacing the stub handlers for list, install, uninstall, and getSkillInfo endpoints. The service lives in pkg/skills/skillsvc/ (sub-package) to avoid an import cycle between pkg/skills and pkg/storage. It delegates to the existing SkillStore for persistence and validates all inputs via ValidateSkillName before any store operations. Server wiring creates a default SkillStore in createDefaultManagers() and closes it on shutdown via the cleanup() method. The validate, build, and push endpoints remain as 501 stubs for future work. Closes #3741 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Update skill handlers and tests to align with the RESTful routes merged in #3818: - Install: POST /skills with Location header - Uninstall: DELETE /skills/{name}?scope= (path param + query param) - Add ValidateScope calls to list, info, and uninstall handlers - Update tests for new routes and validation - Regenerate swagger docs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Address PR review feedback:
- Info() now returns storage.ErrNotFound (404) instead of
200 with {installed: false}, consistent with how groups
and workloads handle missing resources.
- Info() now uses the scope from InfoOptions instead of
hardcoding ScopeUser.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
5080794 to
536486c
Compare
Remove duplicate name validation from installSkill handler since the service layer already validates via ValidateSkillName. Make error handling consistent across all handlers by returning bare errors instead of wrapping some with fmt.Errorf. Remove the vestigial Installed bool field from SkillInfo since Info() now returns 404 for missing skills, making the field always true. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Juan Antonio Osorio <ozz@stacklok.com>
ca86de8 to
9e08f09
Compare
Summary
This PR delivers the service layer and API wiring needed to make the skills
endpoints functional, building on the SQLite SkillStore merged in #3801.
Why a sub-package?
pkg/skillsandpkg/storagehave a circular import(storage uses skills types, skills needs the store interface). The service
implementation lives in
pkg/skills/skillsvc/to break this cycle cleanly.What each endpoint does now
GET /api/v1beta/skills?scope=filterPOST /api/v1beta/skills/installPOST /api/v1beta/skills/uninstallGET /api/v1beta/skills/{name}{installed: false}POST /api/v1beta/skills/validatePOST /api/v1beta/skills/buildPOST /api/v1beta/skills/pushServer lifecycle changes
createDefaultManagers()now opens aSkillStoreviasqlite.NewDefaultSkillStore()and passes it to
skillsvc.New(). The store reference is tracked as anio.Closeron the
Serverstruct and closed incleanup()— before unix socket removal, evenon crash paths. When callers inject their own service via
WithSkillManager(), theyown the store lifecycle (documented in the method comment).
Service design decisions
{installed: false}for missing skills, not 404comes when
InfoOptionsgains aScopefieldValidateSkillNamebefore touching the storeScopeUservia extracteddefaultScope()helperHandler patterns
Handlers follow the same pattern as
groups.go: decode JSON → validate requiredfields → call service → encode response. Error mapping uses
httperr.WithCode()for 4xx and lets
ErrorHandlersanitize 5xx to generic messages.Closes #3741
Large PR Justification
The PR is ~980 lines total but only ~200 lines are production code (across 3 modified
files and 1 new file). The remaining ~780 lines are test files:
pkg/skills/skillsvc/skillsvc_test.go(367 lines) — service unit testspkg/api/v1/skills_test.go(380 lines) — handler testsdocs/server/— auto-generated swagger docsThe production code cannot be split further — the service implementation, API handlers,
and server wiring are tightly coupled (handlers call the service, server creates the
service). Shipping them separately would leave broken 501 stubs between PRs.
Test plan
validation errors, error propagation, scope defaulting, and version passthrough
covering JSON decoding, empty/malformed input, status code mapping (400/404/409/501),
scope+version passthrough, and response format validation
task lint-fixclean,task buildcompiles, race detector passes🤖 Generated with Claude Code