Repository navigation
Conversation
Contributor
|
Macroscope has since reviewed this pull request. An earlier review was skipped by a cost limit; a review has now completed, so that notice no longer applies. |
Contributor
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — This PR introduces a full plugin platform with trusted server-side code execution, npm installation and updates, new persistent data, authorization surfaces, MCP/actions/views, and substantial web, desktop, mobile, and orchestration behavior. It also enables multiple capabilities by default and adds static-analysis suppressions, making the aggregate risk and scope unsuitable for automatic approval. You can add or adjust custom eligibility rules. Learn more. |
📝 Walkthrough
|
Stacked on #16056 (and #15010). Review only the top 2 commits: 1d7af3b.
Problem
The server can install plugins from npm (the npm install PR below this one), but no client offers it. A user who wants a published plugin has to send raw
plugins.npm.*requests from an administrative connection, and has no screen that shows the package, its registry checksum or the scripts policy before they approve it. Updating an npm plugin is the same: there is no way to download, review and apply a new version from the app.This PR adds installing and updating plugins from npm to the Plugins page on web and desktop. Mobile shows where a plugin came from, read-only.
Why this qualifies
This is the proposal route in CONTRIBUTING, and no maintainer has agreed to it yet. It needs the plugin-system approval on #6714 / #6837, like the plugin PRs below it.
It stacks on the plugin settings forms PR, and is the first client of the npm install PR's RPCs; we recommend reviewing the two together. It adds no server code, no RPC and no scope. If the answer is no, we close this with the plugin PRs around it. Previous PR in this stack: feat(web,mobile): fill in plugin settings from Settings (#16056).
Fix
Shared client state (
packages/client-runtime):state/pluginNpm: the npm list query and the four npm commands (add,stageUpdate,applyUpdate,discardUpdate), one step per environment at a time. Every request checks thepluginNpmcapability on the session it would use, so an older server never receives one.state/pluginNpmPresentation: the request gate for the install and update forms (decodes the package name and version with the contract schemas, so ranges are refused before sending; an empty version meanslatest; an empty registry is left to the server's default), the npm list state, and where each installation came from. After a step, the step's reply is trusted only until a list read that started after the step; after a failed step the screen says "Checking what is installed…" until that read, so it never claims a version after a rollback.settlePluginNpmStep(instate/pluginNpm) records the list the client holds when the step settles and reads it again in the same call; that refresh cancels a read already running, so a list that arrived while the step ran, or a read that started before it ended, cannot take the place of the reply. The list is read again after each npm step and when installations or their files change, never on a timer.Web and desktop (Settings > Plugins): Install from npm next to Add plugin, only on servers with
pluginNpm. The dialog takes a package, a version or tag, and an optional registry, and explains an invalid request inline. Download and review opens the existing review, which now also shows the package, its sha512 integrity and what it means, and the scripts policy. Discard removes a download nobody approved. Rows of npm plugins shownpm · name@version, and "Update X ready to review" when one is downloaded. In an npm plugin's details, Updates downloads a version next to the installed one, shows its version, integrity, digest and capabilities (new ones highlighted, removed ones listed), and applies it only after the user ticks trust for that exact download. The Remove confirmation says the plugin's saved settings and storage are deleted, and what happens to its files: an added directory stays, a downloaded copy is deleted, and while the origin is not yet known it says both.Mobile (read-only): a plugin's row shows
npm · name@version, and its screen shows Package and Integrity (with what the checksum means). Installing, updating and discarding are not on mobile: the mobile app always pairs with standard scopes (unchanged from main), so it never has theaccess:writethese steps need, and its Plugins screens already say plugins are managed from an administrative web or desktop connection.Access: every npm step needs
access:write, as on the server (plugins.npm.listisorchestration:read, the four mutations areaccess:write). A standard pairing sees npm provenance but the entry and every action are disabled, and each submit path refuses at dispatch, not only through disabled controls.Docs:
docs/user/plugins.md"Installing from npm" now describes the app flow. The raw-request section is replaced by a short "Publishing to npm" note for plugin authors, and Managing says removing an npm plugin deletes the server's copy.Size: 15 files, +2215 / −70. About 1.1k of the added lines are tests (the fake server these tests use comes from the plugin management PR).
Evidence
Environment: macOS arm64; this PR on top of the plugin settings forms PR.
How to exercise it: use an isolated
vp run devand a local registry that servesnpm pack --ignore-scriptstarballs of a fixture plugin at 1.0.0 and 1.1.0 with their sha512 integrity. Install from npm with that registry, approve, then download 1.1.0 under Updates and apply it. Then pair a second client with a standard link.Captures (isolated dev server on fresh state, web in Chromium, the built desktop app, and one
vp run dev --shareremote browser; the environment label is a neutral "Proof server"; the registry is a local test registry):Install, review, approve (web video, desktop video): a range is refused inline and nothing is sent; the review shows the package and registry, the sha512 integrity and what it does and does not prove, the digest, and the scripts policy; Approve stays disabled until trust is ticked; the row then reads "npm · name@1.0.0".
Update (video): downloading 1.1.0 shows its integrity, digest and capabilities with the new one highlighted ("New: tools"); the row says "Update 1.1.0 ready to review"; Apply needs trust for that download; then "Updated to 1.1.0". Discard update returns to the empty form.
Failed apply (video): with the staged files removed on disk, Apply shows the server's error, "Checking what is installed…", then the version the server reports.
plugins.npm.list. (That pairing was standard; on an npm-capable server a standard pairing still shows the entry, disabled, so the absence comes from the capability.)Remote (
vp run dev --share, its own pairing link, a fresh browser profile; video): install 1.0.0 → approve → download 1.1.0 → apply worked over HTTPS; a fresh standard pairing was view only. Relay and T3 Connect were not exercised.The captures above were taken at an earlier revision of this PR that differs from this head outside mobile only in the Remove confirmation's wording and in comments and tests, so every web and desktop state above renders the same here. The Remove confirmation and the Android screens below were captured at this head.
Remove (this head, web and built desktop, light and dark): both kinds of plugin say their saved settings and storage are deleted.
Android (this head, an emulator with a standard pairing; the plugins were installed from an administrative web session): read-only provenance, with no Install from npm, update, discard or remove control.
The row reads "npm · t3-proof-npm@1.0.0 from http://127.0.0.1:"; the plugin screen shows Package and the sha512 Integrity with what it does and does not prove; both end with "Plugins are view-only on mobile. Add, install, approve, enable, update, or remove them from an administrative web or desktop connection."
Checks at this head (
065649b7cd), re-run 2026-10-05 (vp test run,CI=true, all exit 0):state/pluginNpm,state/pluginNpmPresentation,state/pluginPresentation,state/plugins: 4 files, 73 tests pass. They cover the install and update request gates (exact versions and tags pass, ranges and bad names are refused, nothing is sent without management or while busy), provenance after successful and failed steps, the list key, the update presentation, and the apply gate (refused without acknowledgement, for a different download, or with nothing staged). Two run the real npm list atom against held list reads: a reply outlives a list that arrived during the step until a read after it, and a read already running when the step settles is cancelled.PluginsSettings.npm.test.tsxmounts the Plugins section in jsdom with the real atoms and registry; only the server is fake, holding each request until the test answers it. Clicking and typing like a user, it checks that the exact version typed is downloaded; that the review keeps the install's package and checksum when a list read finished during the download and another was running when it ended; consent then enable for the reviewed digest; that a range sends nothing; that an old server gets no entry and no list read; that Discard removes an unapproved download once; and that a failed update download shows "Checking what is installed…" even when a list arrived during it, then applies only the acknowledged download. WithPluginsSettings.test.tsxandPluginSettingsForm.test.tsx: 3 files, 13 tests pass.src/plugins/PluginSettings.test.tsandsrc/plugins/PluginNpm.test.ts: 2 files, 38 tests pass, including "keeps values across disable and re-enable, and deletes them on remove". Contractssrc/pluginNpm.test.ts: 3 pass; mobiledependency-graph: 3 pass.vp run --filtertypecheck for contracts, client-runtime, web and mobile;vp lint --report-unused-disable-directivesandvp fmt --checkon the added and modified files (no warnings);vp run knip:check;vp run lint:mobile.vp run --filter @t3tools/web build,vp run build:desktopandnode scripts/release-smoke.tspassed at the top of this stack (the example plugins PR), which contains this change.Surfaces
PluginNpmPackageNameandPluginNpmVersionRequest(and its type) are exported again so the forms can check input with the server's own schemas.docs/user/plugins.mdinstall, managing and authoring sections rewritten. No internals doc.Not verified
--share.https://registry.npmjs.org); that string is a display constant, the server keeps the actual default.Claude Opus 5.5 (build), GPT-6.1 Sol (review) and GPT-6 Astra (captures) via T3 Code
🤖 Generated with Claude Code