Repository navigation
Conversation
|
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. |
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — The requested range introduces a broad plugin execution platform spanning server processes, persistence, RPC authorization, MCP tools, npm distribution, client views, and orchestration behavior—not merely documentation or examples. It also changes product capability defaults and adds static-analysis suppression directives, making the security and integration surface unsuitable for automatic approval. You can add or adjust custom eligibility rules. Learn more. |
7fef3e6 to
d858456
Compare
|
Review requested
Logged so this PR shows when a maintainer was asked to review it. |
220a807 to
a4fc048
Compare
dd97af8 to
894560a
Compare
A view asset such as `..board.js` sits inside the plugin directory, but the containment check treated any `..` prefix as leaving it, so the whole installation's views failed to load. Only a whole `..` segment now counts, matching the entry check in the manifest loader. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A view's files were read before the directory was digested, so a file changed for the read and restored before the digest served bytes that were never approved. Each read file's hash must now match the hash the digest pass took of it, and a file that several views share must read the same bytes every time. A view file that fails to read also runs the digest, so an approved plugin whose view was edited into an invalid file is disabled instead of keeping its stale approval. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed updates Adds `plugins.npm.list/add/stageUpdate/applyUpdate/discardUpdate`, gated on the `pluginNpm` environment capability. Install and update download one exact version, require the registry's sha512 integrity to match, check the whole archive in memory before writing it, refuse install scripts and unbundled dependencies, and hand the unpacked directory to the catalogue, which still requires consent to its digest before anything runs. Applying an update consents to the staged digest and swaps the files in one catalogue step; interrupted swaps are finished or rolled back at startup. Listing needs orchestration:read; installing and updating need access:write. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The integrity check compares the tarball with the sha512 the same registry publishes. Over plain http a network attacker can replace both, so the check authenticated nothing. Registries must now use https, except a loopback registry for local testing. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…https rule Only the registry typed into add was checked. Metadata requests followed redirects anywhere, so one plain http hop let a network attacker supply both the integrity and the tarball, and an update of an installation saved from a plain http registry skipped the check entirely. Metadata requests now follow each redirect only to https or a loopback registry, and every metadata lookup refuses a saved registry that is not one. Tarball downloads still follow redirects: their integrity comes from that metadata. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Three header cases were misread. A GNU header's `ustar ` magic passed the POSIX check, so its access time became a path prefix and files landed under the wrong names. A directory entry's size is space to reserve, not data, so a nonzero one shifted every later header. A pax global header that sets `path` or `size` was skipped, so later entries kept names and sizes their writer did not mean. Only the full POSIX magic and version now read a prefix, directories carry no data, and a global path or size is refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A staged update's summary was built by its own copy of the manifest summarizer, which left out tools, settings, and actions, so an administrator reviewing an update could not see changes to them before applying it. The npm installer now uses the catalogue's summarizer, so the review shows exactly what the catalogue will list once the update is applied. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The inflate cap allowed 1.5 KiB of header and padding per file, but directories and extended headers are entries of their own, so an archive inside the file, byte, and entry limits could still be refused as too large. The cap now budgets every entry and one tar record of end padding. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pax reader dropped each record's last declared byte as its newline without checking it, so a malformed record such as `20 path=package/fooX` renamed the next file instead of being refused. A record whose last byte is not a newline now makes the archive unsafe. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PluginNpm now reaches the plugin catalogue through its module namespace and builds each PluginCatalogError where the failure happens instead of through a forwarding helper. A failed file step keeps the file system error as the storage error's cause, and every Node builtin import exemption in the npm install modules says why it is needed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A pax size waiting for the next file was applied to a GNU long-name record in between, so the reader misread the name and refused a valid archive. Extended headers now always use their declared size. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A pax path may carry a NUL, which passed the path checks and then failed the staging write after earlier files were already written. Such an archive is now refused before anything is staged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nabled again When enabling the plugin again failed after the consent to the new files was saved, applying the update reported a failure although the new version was installed, and a retry found no update to apply. It now returns the applied version and the installation as it is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Applying an update whose files match the installed ones could lose the package: after a restart between the two moves, recovery read the existing consent as a finished swap and deleted the only copy. Such an update is now discarded before anything moves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Plugins could only be added, approved, enabled or removed by sending raw RPCs from an administrative connection. Settings now has a Plugins page on web and desktop: add a directory, review its files and digest before approving, enable, disable, resume, check files again and remove. Event delivery is shown beside the process state, and retrying or stopped delivery offers Resume. Controls need access:write from the current session read and a live catalogue; a standard pairing sees the list read-only. Mobile shows the same catalogue, states and details read-only, with a notice to manage plugins from an administrative web or desktop connection: the mobile app always pairs with standard scopes, so it has no access:write. The client-runtime catalogue subscription and the shared presentation model keep web and mobile on the same states and copy. The five per-feature plugin pages are rewritten into one guide, docs/user/plugins.md. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…lugins Deselecting every environment on the Plugins screen said to update T3 Code, even when every connected server supports plugins. It now asks to select an environment, as the usage screens do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Plugins could declare settings and secrets, but the only way to save them was a raw plugins.settings.update request. Each installed plugin that declares settings now gets a form under Settings > Integrations on web and desktop. Secrets are write-only (the form shows only whether one is saved, with Clear), and values are checked against the field before they are sent. Saving needs access:write from the current session read. A standard pairing sees the values read-only, and every save path (submit, reset or clear, choices, switches) refuses at dispatch, not only through disabled controls. Mobile lists each plugin's saved values read-only on the environment's settings screen (secrets only as saved or not set), and says to edit them from an administrative web or desktop connection: the mobile app always pairs with standard scopes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Adds Install from npm next to Add plugin on web and desktop, a review of the downloaded package (name, version, registry, sha512 integrity, scripts policy) before approval, Discard for an unapproved download, and an Updates section that downloads, reviews and applies a new version bound to the digest the user acknowledged. Only servers that report the pluginNpm capability get the entry or any npm request, and every step needs administrative access, as on the server. The removal confirmation says what happens to the files for each origin, and that saved settings and storage are deleted. Mobile shows where a plugin came from, read-only: the npm package on its row, and Package and Integrity in its details. Installing and updating stay on web and desktop, because the mobile app pairs with standard scopes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…the list A staged npm download of the installed version still needs to be applied or discarded, but the plugin row hid it because only a new version counted. The row now says a download is ready to review whenever one is staged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s allow Plugin details and the consent review now list a plugin's declared contributions (actions with targets and placements, tools, settings, events, and view titles once enabled) from its manifest summary, without starting it, and give each capability a plain meaning. A plugin the environment's action limit left out says so in its details. A downloaded npm update lists what the new version contributes before it is applied. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A plugin with the `status` capability (proposed API) can set and clear short statuses on threads. They reach clients through the existing thread status channel and show beside provider statuses (such as a Pi extension's) in the web and desktop thread header and the mobile status row, attributed to the plugin by name. Plugin statuses use their own capacity pool (3 sources per thread, 32 threads, 64 items), so provider statuses keep exactly the room they had. Each plugin process is limited to 16 statuses and 10 updates at once, then 2 per second, and everything it set is cleared when that process stops. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The status store drops a new status when the thread already shows its most plugin sources or items. status.set still reported success, kept the key against the plugin's 16-status budget, and left the thread's source open with nothing shown. It now checks what the store admitted, frees the slot and returns an error. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A plugin with the `notifications` capability (proposed API) can send short notifications. Web and desktop show them as toasts and mobile as a banner, with an "Open thread" action when the notification names a thread. The server keeps the newest 20 for 2 minutes in memory and sends that whole set on subscribe and after every change, so a client that reconnects shows what it missed exactly once and closes what was withdrawn meanwhile. A plugin's notifications are withdrawn when its process stops. Each process may send 5 at once, then one every 5 seconds. Subscribing needs orchestration:read. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The banner disappears after a few seconds, so a screen reader user heard it only if focus happened to land on it. It is now a live region for TalkBack and announced on iOS when it appears. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ions PluginNotifications reaches the plugin supervisor through its module namespace and builds each PluginHostCallError where the call fails, without a forwarding helper or a shared stopped error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A plugin with the `transforms` capability and a declared `transforms.enrich` can now answer `t3.transform.enrich` with short titled context. Before the provider session opens, the turn-start effect records one `plugin_context` item per plugin (at most 4, plus one record counting any plugins not called), calls the plugins outside the thread lock, saves their answers or why none was added, and sends the provider the kept context ahead of the user's text. Only turns the history pager counts are enriched; steering, `/` commands and wakes are not. A retried start reuses the saved records and never calls a plugin twice; Stop closes a running record. Failures fail open. The records are existing `dynamic_tool` items, so every client decodes them. The web/desktop inspector and the mobile expanded detail show "From <plugin>" with the context, or "Not added" with the reason, instead of the input. The capability's consent line now says the plugin reads your messages. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A plugin that declares the approvals capability is offered the command, file and permission requests agents send, once the request's approval card is shown, and can approve or decline that one request through the orchestrator's ordinary respond path; the first recorded answer wins and anything else leaves the request with the user. Approval rows answered by a plugin are titled with the plugin, and expanding them shows its reason on web, desktop and mobile. Pi and OpenCode v1 no longer overwrite T3's recorded answer when they acknowledge it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Adds three example plugins (turn notifier, TODO count, machine label) with a test that loads each through the real catalogue and supervisor, and docs/internals/plugins.md with the cross-layer decisions: trust model, gating, reserved names and revocation. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
6986284 to
59faa73
Compare
Model: Claude Opus 5.5. Rebased onto current main with Claude Opus 5.5 on 2026-10-12.
Stacked on #16064 (and #15010). Review only the top commit: af081bc.
Problem
The plugin guide documents each capability on its own, but there is no complete plugin to start from. An author has to assemble a manifest, an entry module and the proposed API from separate sections, and nothing in the repository proves that a plugin written that way actually loads and runs.
Why this qualifies
This is the "who uses this?" answer for the plugin system proposed in #6837 / #6714, and no maintainer has agreed to that system yet. It is a small, optional PR on top of the plugin approvals PR and the plugin PRs below it, so it rides on the same plugin-system approval; if the plugin system is declined, this closes with it. Nothing depends on it. Previous PR in this stack: feat(server,web,mobile): let plugins answer approval requests (#16064).
Fix
examples/plugins/turn-notifier:events+notifications. Notifies when a turn ends ("Fix login finished", "Upgrade deps failed"), or when finishing it fails (names the failed step), and offers to open the thread.examples/plugins/todo-count:tools. Gives agents acount_todostool with a declared input schema; read-only, skips hidden entries andnode_modules, caps files and size, honours cancellation.examples/plugins/machine-label:events+settings+status. Shows a configurable label and color on threads that finish a turn, defaulting to the host name..mjs, a README with install steps) using only the documented proposed API. They are not workspace packages and nothing imports them.apps/server/src/plugins/examplePlugins.test.tsinstalls each copy through the real manifest loader, catalogue (add → approve → enable) and supervisor, running real plugin child processes, and asserts the notifications, tool results and statuses a user would see.docs/user/plugins.md"Writing a plugin" points to the examples.docs/internals/plugins.mdrecords the cross-PR decisions a maintainer could otherwise get wrong, now that every capability in this stack exists: plugins are trusted code and the child process is an availability boundary, not a sandbox; consent binds an inspected tree, not executed-artifact identity; contributions are static manifest declarations and new requirements are new capability names; per-session capability gating at send time; reservedt3.names and host-method prefixes; the generation as revocation epoch and disable ordering.Size: 12 files, +615; 320 lines are the test.
Evidence
Environment: macOS arm64; this PR on top of the plugin approvals PR.
Checks at this head (
f2955898a3), re-run 2026-10-05 (CI=true, all exit 0):vp test run src/plugins/examplePlugins.test.ts(apps/server): 1 file, 4 tests pass. Recorded during development: with the examples removed, all 4 fail (FileSystem.copy … examples/plugins/<name>not found).vp run --filter t3 typecheck;vp lint --report-unused-disable-directivesandvp fmt --checkon the touched files (no warnings);vp run knip:check;vp run --filter @t3tools/web build;vp run build:desktop;node scripts/release-smoke.ts.vp run lint:mobile. All pass.Dev-server run of each example (on an earlier revision of this PR that differs from this head only in an example README; isolated
vp run dev, fresh state, environment labelled "Proof server", Codex GPT-6.1-Sol turns, WebSocket frames recorded from the browser). Before this PR there is noexamplesdirectory, so there is nothing to add. With this PR each example was copied unchanged, then added in Settings > Plugins, reviewed, trusted and enabled (plugins.add→plugins.consentwith the digest →plugins.enable, generation 1):example.turn-notifier, " finished", tone success) and a toast with Open thread; disabled → no notification on the next turnplugin_tools_listthenplugin_tool_calland reported 3 markers (index.js 2, README.md 1); disabled → the next turn does not call itSurfaces
docs/user/plugins.md; each example has a README;docs/internals/plugins.md(decisions and constraints only).Not verified
Claude Opus 5.5 (build), GPT-6.1 Sol (review) and GPT-6 Astra (captures) via T3 Code
🤖 Generated with Claude Code