Skip to content

fix(plugins): review fixes for the web runtime, run hook and installs - #23

Draft
ashx-j wants to merge 13 commits into
mainfrom
feat/plugins-review-fixes
Draft

ashx-j wants to merge 13 commits into
mainfrom
feat/plugins-review-fixes

Conversation

@ashx-j

@ashx-j ashx-j commented Oct 30, 2026 •

Copy link
Copy Markdown
Owner

Fixes from an adversarial review of the plugin stack #18 to #22 (#3). It builds on #22, so until #14 to #22 merge this diff also shows their commits.

A second Claude Opus 5.5 at xhigh effort reviewed the 5 PRs from their code alone and found 10 issues. All 10 are fixed here, in one PR on top of the stack, because folding each fix into its own PR would mean rebasing 5 stacked branches through overlapping edits to the same files.

Data loss.

Run-completed hook (#21):

  • An interrupted or cancelled run reached afterRunCompleted twice, because a checkpoint later updates the ended run again. The hook now remembers the last 256 runs it handled, saved with its position, so a repeat is skipped, after a restart too.
  • Catching up after a long time off could skip runs: the event stream drops the oldest events when a plugin falls behind, and the saved position then moved past them. The hook now reads through a stream that waits instead of dropping. The public events.stored still drops, so a slow plugin can't hold the server back.

Installs (#22):

  • A cancelled install request could run the staging cleanup while the old and new versions were being swapped, and a cancelled git clone left its staging folder behind. The whole install now runs in the host's scope, like enabling and reloading, so a caller that goes away only stops waiting.
  • A crash between the 2 renames of the swap left the plugin missing. At startup the host now puts a stranded previous version back when nothing replaced it, then removes leftover staging folders.

Web runtime (#18, #19, #20):

  • Stopping the loader while a plugin's activate was still running left whatever it registered afterwards (panels, commands, styles, shortcuts) in place. Anything a plugin registers after it unloads is now undone at once.
  • One web plugin whose bundle never loaded, or whose activate never settled, blocked every later load and unload. Both now fail after 30 seconds, the same deadline the server gives activate, and the plugin's row shows why.
  • Reloading a plugin whose version didn't change kept the old web code until the page was refreshed. Plugin state now carries a revision that changes on every start, and the loader loads the bundle again when it changes. It is an optional field, so older clients ignore it.
  • The loader added its keyboard shortcut handler as soon as any web plugin loaded, so every key press did one extra lookup. It now adds it with the first plugin command.
  • Plugin tool renderers only worked on Codex. Claude reports MCP tools as mcp__<server>__plg_… and ACP agents as <server>.plg_…, so the renderer lookup now finds the plg_ name inside the reported one.

Docs (#22):

  • The user guide said an agent could install the first plugin, but agents only see the plugin tools once a plugin is installed. It now says the first plugin comes from Settings.

Not changed: the review also confirmed that auth on the new RPCs and agent tools, path handling for assets and installs, and the git clone arguments are sound, and found nothing that breaks older clients or mobile.

Verification:

  • New or extended tests, one per bug. I didn't run them against the old code:
    • api/turns.test.ts: a repeated update of an ended run is delivered once, also across a restart.
    • PluginHost.test.ts: removing a symlinked plugin keeps its source folder, and a version stranded in staging is restored at startup.
    • pluginLoader.browser.test.ts: a restart at the same version loads the bundle again, and a panel registered after the loader stopped is removed.
    • PluginToolBody.browser.test.tsx: a Claude-style tool name draws with the plugin's renderer.
  • vp run check:changed passed.
  • Not covered by a test: the 30-second web deadline, the lazy shortcut handler, and the install running in the host's scope.

Review: a second adversarial review of #23 to #25 found issues in this PR; the fixes are in #26.


PR filed by Claude Opus 5.5 in Claude Code through T3 Code.

ashx-j and others added 13 commits October 31, 2026 21:22
…bility flag

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PluginHost discovers installed and dev plugins, loads enabled CommonJS
entries after the server is listening, and hands each a Promise-based API
for logging, settings, storage, RPC methods and channels. Failures mark the
plugin failed and never stop the server. Settings live beside T3's state
with secrets in the server secret store; storage is a lazily opened SQLite
file. Eight plugins.* WebSocket methods manage plugins, and the environment
advertises plugins: true.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An adversarial review found that reloading one plugin left plugins whose
folders vanished running, that a caller disconnecting mid-disable left a
plugin half torn down but still routed, that a misspelled access level
made a write method callable by read-only sessions, and that reload kept
old code for symlinked plugin folders. Lifecycle changes now run in the
host's scope, vanished plugins are stopped, access must be read or write,
and discovery resolves real paths. Activation and cleanups get deadlines,
calls stop routing before teardown, and plugins.json keeps unknown keys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Settings gains a Plugins page for the selected environment: each plugin
with its status, failure, enable switch and reload, plus a settings form
generated from the manifest's JSON Schema with write-only secret fields
and a JSON fallback. The page follows plugins.subscribeStates only while
open; the form model lives in client-runtime for the later mobile screen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ng and send hooks

Plugins can now follow stored orchestration events by type, read and act
on threads and projects, run scheduled work, see and extend outgoing
messages before they are sent, offer MCP tools to agents and serve HTTP
routes under /api/plugins/<id>, and read the server's version, descriptor
and redacted settings. The send hook runs in ThreadManagementService.dispatch
with a 2 s deadline and fails open; with no hooks it is one Ref read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… writes

An adversarial review found that a send hook appending to /goal clear
turned it into setting a goal, that the SDK promised a veto nothing
honoured, that several event types merged out of sequence order, that a
schedule could outlive its plugin, and that plugin write tools skipped the
caller's mode limits. Hooks now skip slash commands, veto is gone from the
SDK, multi-type event streams stay ordered, schedules and hooks stop on
the deactivation signal, API calls end after cleanups, plugin write tools
need full access like environment writes, and tool schemas must be objects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Plugins could only run on the server. The server now serves each active
plugin's web bundle, and the web app loads it once an active plugin has a
web part: a starter follows plugin state after idle, and the loader fetches
bundles with the connection's credentials and imports them from blob URLs.
Bundles share the app's React, atom hooks and UI kit through a host object.

Plugins can add right panels, listed in the add menu and the empty panel,
and replace their generated settings form. An unloaded plugin's panel shows
"Plugin not active" with a close button. The desktop CSP allows blob scripts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Web plugins could add panels but nothing users could run from the
keyboard. Plugins can now register command palette commands that users
bind as plugin.<pluginId>.<commandId>, and slash commands in the composer
that run plugin code. They can also navigate and load files from their
assets folder, which the server serves under /api/plugins/<id>/assets/*.

The SDK gains a Vite preset that builds web.js against T3's React and UI
kit, keeping only the names a plugin uses, and typed UI components.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…om plugins

Web plugins could add panels and commands but not change how T3 looks or
how conversations read. A plugin's themes/ folder is now published through
the environment theme service, so its themes reach every client. Web
plugins can show composer banners, draw their own MCP tool calls in the
timeline, draw fenced code of a language in chat messages, and inject
styles. Everything is removed when the plugin unloads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tools

Plugins could see a message before it was sent but not what happened
after, and agents could list plugins but not use them.

turns.afterRunCompleted delivers every ended run in order, saving its
position in the plugin's storage so runs that end while the plugin is off
arrive when it starts. turns.beforeDispatch now gets the message id,
attachment summaries and the requested model. Agents get
t3_plugins_invoke and t3_plugins_set_enabled, gated as writes and only
advertised once a plugin is installed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Plugins had to be copied into the data folder by hand or listed as dev
folders. plugins.install now takes a folder on the server or a git
repository, stages it, checks the manifest and swaps it into place with a
rename, keeping the previous version if anything fails. plugins.uninstall
stops and deletes a plugin, keeping its data unless asked otherwise.

Settings → Plugins gets an Add plugin form and a remove action, agents get
t3_plugins_install and t3_plugins_uninstall, a user guide explains it, and
the SDK gets a README for plugin authors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An adversarial review of the plugin stack found 10 issues. Uninstalling a
symlinked plugin deleted the folder behind the link. afterRunCompleted
delivered interrupted runs twice and could skip runs while catching up.
A cancelled install could race its own swap, and a crash mid-swap lost
the plugin. The web loader leaked registrations made after unload, let
one stuck plugin block the rest, kept stale code after a same-version
reload, and added a shortcut handler before any plugin command existed.
Plugin tool renderers only matched Codex tool names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ashx-j
ashx-j force-pushed the feat/plugins-review-fixes branch from 2dc155f to a25648b Compare October 31, 2026 21:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant