feat(plugins): add the plugin marketplace, manifest, CLI, and credential proxy
What & why
A plugin system: a marketplace format anyone can publish, a manifest per plugin, a CLI that installs them, and a server-side proxy that holds credentials so tokens never reach agent config files.
Format. A marketplace is a git repo with .agent-marketplace.json; the shape mirrors Claude Code's marketplace file so one repo can serve both ecosystems. Each plugin has a single plugin.json with Superset's fields under extensions.superset — interface, auth, bind, and one mcp server. Schemas are published at superset.sh/schemas/{plugin,marketplace}/1.0.0.json.
Auth. oauth2 or api_key, both sharing inputs that any URL can template as ${inputs.site} — which covers per-tenant providers like self-hosted Jira. Either type's credential lands in ${config.access_token}, so bind never branches on auth type. An optional identity block declares how to learn which account a connection belongs to; it proves the token works the moment auth completes rather than on the first tool call.
Storage. Two new tables. plugin_installs holds the resolved manifest, so a remote plugin's tool calls need nothing on disk. plugin_connections keys on (user, plugin, external_account) — multi-account is a wider index later, not a new subsystem. Tokens are encrypted with better-auth's symmetricEncrypt; integration_connections stores them in cleartext and that is deliberately not copied here.
These are separate from integration_connections because its provider is an enum — every new plugin would cost a migration, which defeats the marketplace — and because 28 call sites assume every row is an org-scoped integration with a non-null org. Integrations can migrate onto the new table one provider at a time.
Two plugins, covering both shapes: github (remote MCP, OAuth token that also authenticates api.githubcopilot.com/mcp, plus issue-triage and ci-triage skills) and linear (custom zero-dependency server, 8 KB bundle).
Agent-interactive CLI. install and connect never prompt. When credentials are missing, the error names each question to ask and the exact command to run with the answers, sourced from the manifest — so an author writes the questions once and every agent asks them correctly.
How I tested it
Against real endpoints, not mocks:
- GitHub MCP end to end —
tools/listreturned 44 tools andmcp call github get_mereturned a real profile, confirming one OAuth token serves both the REST API and the MCP URL. - Bundled server — linear's built module loads and answers
get-tools/call-tool; no-token and unknown-tool paths returnisError. - Install/sync/reap — skills materialize to
~/.superset/skillswithname:rewritten to match the directory; removing a plugin reaps its skills and leaves hand-written ones untouched. - Unit tests covering the traversal strings, the shared-prefix containment case, that
${env.SECRET}stays literal in manifest templates, the OAuth state token's forgery and expiry paths, and the publish/check drift detectors. - Full monorepo
typecheck(39 packages) andbun run lintpass;bun run check:pluginsandpackages/i18n checkclean. The 14 failures in a fullbun run testare pre-existing onmain(GitWatcher, Redirect, terminal-ws-transport, Shell Environment), verified by re-running against a clean tree.
Security fixes found by review and reproduced before fixing: two path traversals reachable from marketplace-controlled data (a crafted version escaped the cache root, which is then rmSync'd recursively), and an api_key credential travelling as a query parameter on a cookie-authenticated GET.
Review round
Findings from the CodeRabbit review and a pass against the repo's own standards, all with tests.
Credentials off URLs. plugins install --inputs put api_key values in a query string, which the route already rejected with a 405 for that reason, so the path was also broken. plugins connect built the OAuth URL from whatever inputs it was handed, secrets included, and the desktop form kept values across a method switch so an API key could ride into the oauth2 URL. Now: POST with a JSON body, a refusal when an oauth2 method declares a secret input, and only the selected method's non-secret inputs in the URL.
Credentials to the right host. The token exchange and identity probe send a client secret and bearer token to a manifest-supplied URL, and fetch follows redirects. credentialFetch requires https and treats a redirect as an error; dispatch's MCP call goes through it too.
Credentials to the right manifest. installedPlugin picked the alphabetically-first install when a name existed in two marketplaces. It refuses now and callers answer 409. Scoping connections to (user, marketplace, plugin) is the fuller fix and wants a migration.
Convergence. Overlapping install/remove CLI runs could leave a removed plugin's skills on disk; they queue. managed-skills reaped on read failures it should have tolerated, and preserved skills the user had disabled. The detail page skipped the local install when the account said installed, so a plugin added on another machine never got skills here.
Install path. host.ts resolved SUPERSET_HOME first, but that is the CLI installer's prefix, not the data home — so anyone who set it got a CLI writing installed_plugins.json where agent-setup does not read, and provisioning reaps. Both now resolve through one helper. @superset/agent-setup was imported but undeclared in the CLI's package.json.
A guard for the generated files. Everything a plugin ships is a build artifact of plugins/<name>/, and nothing regenerated it on the way into CI. plugins check now fails on a stale versions/<v> snapshot or a manifests.generated.ts a publish would rewrite, and bun run check:plugins runs in the Lint job.
Lint was red. The pinned biome (2.4.2, via scripts/lint.sh) fails on any diagnostic, and plugins/*/plugin.json is written by the CLI in a shape its formatter disagrees with, so every publish re-broke it. Excluded, as .agent-marketplace.json already was.
Linear skills mutated a shared backlog and reported it afterwards. Creating an issue, commenting on a duplicate, merging, and posting a status now show the content and wait. "Closed" no longer implies regression. Published as 1.4.0.
Docs. AGENTS.md and docs/plugins.md cover which files are generated and why install state is one file every provisioner reads. A bundled plugins skill documents the plugins/mcp/marketplace commands for agents.
Still open: per-workflow Linear tool allowlists (needs the live tool surface to scope safely), and account-side install of a non-first-party marketplace — POST /api/plugins/<name>/install still resolves first-party manifests only, so --marketplace steers local resolution and nothing else.
The marketplace column on plugin_connections is not the fix and is no longer planned. plugin_connections_account_active_unique is keyed on install_id instead of plugin_name, so the foreign key names the marketplace and nothing is duplicated. That closes the write path the install_id column left open: the same external account authorized against two marketplaces' copies of one name used to collide on the name and overwrite the first row's install_id and token.
Notes for review
- Migration
0104is generated but not applied — it goes through the normal deploy flow. It is one migration, regenerated from the schema after rebasing onto main (which took0103forstage_page_assets) rather than renumbered a second time. - Any database that applied an earlier
0103from this branch needs a reset, not a patch:scripts/local/fix-plugin-install-id.sqlis deleted, since it restamped the ledger for a migration that no longer exists and did not carry the corrected index. - The marketplace files exist only on this branch, so the GitHub fallback finds nothing until this merges. The CLI detects a marketplace at or above the working directory in the meantime.
- Token refresh is stored but not yet used —
packages/trpc/src/router/integration/token-refresh.tsshould be generalized rather than copied.
Checklist
- PR title follows conventional commits (
type(scope): subject) -
bun run lintandbun run typecheckpass (CI fails on lint warnings too) - "Allow edits from maintainers" is checked on fork PRs
https://claude.ai/code/session_015YytmsAiiZcTVrLwFSVpMF
Summary by cubic
Adds an account-backed plugin marketplace, manifest format, CLI, desktop management, and credential proxy. Plugins no longer put provider tokens in agent configuration or MCP commands; Superset encrypts credentials and routes tool calls through the API, so installs follow the signed-in user across machines.
Highlights
- Adds marketplace and manifest schemas, authoring/build/validate/publish commands, and first-party
githubandlinearplugins. - Moves the plugin endpoints onto tRPC; only the OAuth start and provider callback stay raw routes, and plugin OAuth state now rides the shared signer.
- Uses the account catalog as the source of truth and reconciles local skills safely across machines; same-named plugins from two marketplaces both materialize under unique source names.
- Treats an unreadable
installed_plugins.jsonas "skip sync" rather than "no plugins", and writes it atomically so a torn file refuses instead of being replaced with a reaping empty set. - Supports OAuth2 and API keys; a manifest names its OAuth client pair through
requires_env(restricted toPLUGIN_*_CLIENT_ID/_SECRETand checked byplugins validate), with explicit update confirmation, install-scoped connections, and HTTPS-only credential requests. - Keeps credentials off URLs and always encrypts them, even when the manifest omits the secret flag.
- Distributes releases as git tags;
plugins validatecompares the working tree against the published tag, and the generated bundle pinsref,path, andintegrity. - Hardens remote plugin handling by requiring a present, matching server digest, rejecting symlinks and unsafe URL inputs, retrying MCP initialization when required, and running the credential-identity probe only for first-party manifests.
- Installs marketplaces with a blob-filtered, sparse clone, and
plugins enable|disablenow write the flag provisioning reads, so disabling actually drops skills. - Reads stdin only when asked, so
install,connect, andcall-toolno longer block under agent harnesses holding the pipe open. - Adds generated-artifact and i18n checks to CI and documents plugin authoring, publishing, and provisioning.
- Disables Remove while a connect or disconnect is in flight, derives connected state from connection records, and keeps an update from re-enabling a plugin the user disabled.
- Sorts a plugin's skills before regenerating the bundle, so
check:pluginspasses on any OS. - Takes the install ledger path as a parameter, so callers and tests don't depend on the ambient environment.
Migration
- Apply generated migration
0106for plugin marketplaces, installs, and connections. - Set
PLUGIN_GITHUB_CLIENT_ID,PLUGIN_GITHUB_CLIENT_SECRET,PLUGIN_LINEAR_CLIENT_ID, andPLUGIN_LINEAR_CLIENT_SECRETfor OAuth. - Rotating
BETTER_AUTH_SECRETinvalidates stored plugin credentials, so users must reconnect. - Account-side installation resolves first-party manifests only and refuses a marketplace it cannot resolve; custom marketplaces use local CLI resolution, and refresh-token use remains pending.
Summary by CodeRabbit
-
New Features
- Added a plugin marketplace featuring GitHub and Linear integrations.
- Added plugin installation, updating, enabling, removal, and account connection flows.
- Added OAuth and API-key authentication support.
- Added bundled plugin tool execution and new triage, issue, cycle, and project skills.
- Added desktop plugin catalog, version updates, and connection management.
- Added CLI commands for marketplaces, plugins, connections, skills, and tool execution.
- Added marketplace and plugin manifest schemas.
-
Bug Fixes
- Improved validation and error handling for plugin installation, authentication, and tool execution.