Repository navigation
feat(claude-code): emit installationPreference in marketplace.json entries - #41430
devin-ai-integration[bot] wants to merge 11 commits into
Conversation
…tries Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
|
|
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…nstallation_preference
…values Accept only available, auto_install, or required so a typo returns 422 instead of being silently dropped, and cover the PUT full-replace clearing behavior with a test
|
bugbot run |
|
bugbot run |
…ip check and cover hand-edited rows
|
bugbot run |
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 1ec6486. Configure here.
TLDR
Problem this solves:
installationPreferenceHow it solves it:
PluginSpecgains an optionalinstallation_preferencefieldavailable,auto_installorrequiredare accepted, anything else is a 422installationPreferenceper pluginUser Flow
Before: a proxy admin registers an internal plugin and Claude Desktop will not auto-install it
{"name": "my-plugin", "source": {"source": "archive", "url": "https://files.example.com/my-plugin.zip", "sha256": "..."}}and get back 200 with"action": "created"name,source,versionbut nothing about installationAfter: the same admin marks the plugin for auto install and Claude Desktop picks it up
"installation_preference": "auto_install"and get back 200 with"action": "created""installationPreference": "auto_install"A value outside
available,auto_installorrequiredis rejected with 422 rather than silently dropped. The same field is accepted by PUT https://litellm-domain/claude-code/plugins/{name} and echoed back asinstallation_preferenceby GET https://litellm-domain/claude-code/plugins and GET https://litellm-domain/claude-code/plugins/{name}. Entries without it are unchangedRelevant issues
Feature request from a customer (Pylon #8704)
Linear ticket
Resolves LIT-7868
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*,make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more@greptileaito re-request a review after pushing changes)Delays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Screenshots / Proof of Fix
Last updated: 1ec6486. QA and /live-pr-risk ran on 19a6c00, the last commit with a product diff
Two proxies booted from this rig, each with
--num_workers 2on a random port and its own fresh Postgres database: the before leg at the merge base 118ce3c on port 31361 and the after leg at this PR's tip 19a6c00 on port 50017. The proxy admin's calls are the curl commands below, run as typed with the leg's master key in$LITELLM_MASTER_KEY. The end-user client is Claude Code v2.1.286 driven interactively under tmux against each proxy's marketplace.json. Claude Desktop is the client that acts oninstallationPreference, and only under managed configuration, so it was not driven here; what it reads is the marketplace.json shown in each leg. Docs: BerriAI/litellm-docs#1497Before (118ce3c)
Register one plugin with
installation_preferenceand one without, read every surface that serves them back, then send a value outside the allowed setClaude Code v2.1.286 under tmux:
/plugin marketplace add http://127.0.0.1:31361/claude-code/marketplace.json, then/plugins,internaltyped into the Discover search, Enter oninternal-auto-pluginReplace the plugin with a body that carries only
sourceAfter (19a6c00)
Register one plugin with
installation_preferenceand one without, read every surface that serves them back, then send a value outside the allowed setClaude Code v2.1.286 under tmux:
/plugin marketplace add http://127.0.0.1:50017/claude-code/marketplace.json, then/plugins,internaltyped into the Discover search, Enter oninternal-auto-pluginReplace the plugin with a body that carries only
sourceDependents driven live (/live-pr-risk)
The bundled Admin UI, logged in as the admin on each leg, at
/ui/skillsand theinternal-auto-plugindetail page. Both legs render the same rows and fields; the page reads name, version, description, source and id, never the preference. The before leg listsbad-pluginbecause that proxy accepted the invalid valueMerge-ref check: this PR merged onto origin/main 6fd9334 (a throwaway merge commit, tree e1eabc8962) booted the same way on port 49052, and the whole command sequence above produced the same output as the after leg, the generated plugin id aside
Observations from the legs
/audit: integration cells, base 118ce3c vs head 1ec6486
One checked-in test per inventory row in
tests/integration/management/test_claude_code_plugin_installation_preference.py(groupmanagement), run against a real proxy rig: owned Redis, a fresh Postgres database per leg, the scripted upstream fromtests/integration/_support/upstream.py, and per leg two proxy processes sharing that database (primary and peer) booted from the leg's commit, no mocks inside the proxy. The chaos rows boot their own two-worker proxies throughowned_proxy_process. Command per run:python tests/integration/run.py management --seed 4106601 --order-seed 0 <file>managementgroupEvery cell asserts the new field, so every cell is red on the base by construction; the base column names where the base first diverged
test_registered_preference_is_served_on_the_marketplace[available|auto_install|required]installationPreferencetest_preference_echoes_on_list_detail_and_skill_hubtest_peer_process_serves_the_stored_preferencetest_omitted_preference_is_unsettest_explicit_null_preference_is_unsettest_invalid_preference_is_refused_on_register[6 ids]test_invalid_preference_is_refused_on_update[6 ids]test_duplicate_json_key_keeps_the_last_valuetest_repeated_identical_update_is_idempotenttest_duplicate_name_is_refused_and_keeps_the_first_preferencetest_update_changes_and_clears_the_preferencetest_update_replaces_the_whole_manifesttest_unauthenticated_requests_are_refusedtest_non_admin_key_cannot_modify_the_catalogtest_disabled_plugin_keeps_its_preference_for_granted_keystest_non_granted_key_cannot_read_a_disabled_plugintest_hand_edited_unknown_preference_is_not_servedtest_openapi_declares_the_preference_enum[3 schemas]test_burst_reads_across_processes_while_the_preference_flipstest_repeated_identical_reads_are_stabletest_registration_burst_survives_a_worker_killtest_registrations_survive_a_proxy_restartAudit verdict: blocked on row 23, and row 21 is short of its second green at this tip. Its own two-worker proxy took 76s to 97s to boot on a box at 21 to 27 GB of swap, past the suite's 70s readiness budget, in three of four file runs and in the group run, where the same class took 12 other cells across four suite files; the cell is green here in run 1 and four times at the two earlier tips on the same product code, and the file gets rerun on a calm box before any PASS. Claude Desktop honors
installationPreferenceonly for a marketplace served from a managed deployment'sinferenceGatewayBaseUrlorigin, so that row needs a managed Claude Desktop deployment against an HTTPS LiteLLM origin, which this rig cannot provide; rows 1-22 are verified. Pre-existing on both legs: a non-admin key gets 401 on/claude-code/plugins/{name}/enableand/disablebut 403 on POST, PUT, and DELETE, and concurrent PUTs on one plugin are last-commit-winsType
🆕 New Feature
Caveats (if any)
Medium
marketplace.json. The field name, its three values, the archive plussha256rule, and the same-origin rule were checked against Anthropic's Claude Desktop extensions doc on 2026-09-30. Closing it needs a managed Claude Desktop deployment whoseinferenceGatewayBaseUrlis an HTTPS LiteLLM origin that also serves the plugin archiveauto_installandrequiredare accepted on any source type, while Claude Desktop honors them only on anarchivesource with asha256served from its gateway originauto_installis served as marked and silently stays manual install in Claude Desktop. The alternative is a 422 on the mark for a non-archive source. Not done here because Anthropic's doc pins per-plugin marks to archives by example only, so it is left open for the reviewerLow
installation_preferenceoutside the three values is not servedmanifest_jsonby hand, since POST and PUT reject it with a 422.marketplace.json, the list, get, and skill hub responses treat it as unset instead of failing the requestproxy-endpoints / Run testsis red on main too, not from this PRbatches_endpoints/test_endpoints.py::test_create__non_object_metadata_is_400(both params) fails the same way on main run 36785087678misc / Run testsis red on main too, not from this PRtests/unit/interactions/test_openapi_compliance.pycases fail on main run 36785087678; the test reads Google's live Interactions specbudget-ratchetis non-gating and red from main, not from this PRreportUnknownArgumentTypemoved from 44358 to 44802 on main with fix(cost_calculator): bill ultrafast prompts above 272k at the ultrafast long-context rates #43764osv-scanis red on two lockfile pins main has already bumped, not from this PRuv.lock, which this PR does not touch; main carries 6.5.10 and 3.1.62 since c168199e33, so merging main into the branch clears it, and main's own latest scan run 36762246733 is red on its newer pins the same wayproxy-behavioris red on its coverage upload step, not on a test, and main fails the same stepUpload Lens database coverage, the same step that fails on main run 36805100205Final Attestation
Link to Devin session: https://app.devin.ai/sessions/ee4e29a1f283440e865ceea49877f0fb
Open in Devin Desktop: https://app.devin.ai/desktop/session/ee4e29a1f283440e865ceea49877f0fb?variant=devin
Note
Low Risk
Additive marketplace metadata on admin plugin CRUD and public skill hub listing; behavior is unchanged when the field is not set.
Overview
Adds optional
installation_preference(available,auto_install,required) on Claude Code marketplace plugin register/update APIs and related types. Invalid values are rejected at validation time (422) instead of being ignored.When set,
marketplace.jsonincludesinstallationPreferenceon each plugin entry so Claude Desktop can honor auto-install for same-origin archive plugins. The field is omitted when unset. GET plugin, list, and/public/skill_hubechoinstallation_preference; a full PUT without the field clears it per existing replace semantics.OpenAPI snapshot and dashboard
schema.d.tsare updated; unit tests cover marketplace emission, validation, propagation, and clearing.Reviewed by Cursor Bugbot for commit 4caf394. Bugbot is set up for automated code reviews on this repo. Configure here.