Plugins keep their configuration in their own file (protocol 35) - #276
Merged
Merged
Conversation
A plugin's JS file is now the source of truth for what it does on error,
which requests it covers and its setting values (contract addendum 4).
- tw-plugin: a small tokenizer finds `export const manifest = { ... }` and
reads the literal as pure data with its exact byte span, skipping
strings, template literals, comments and regex literals elsewhere. A
writer prints a literal in one fixed style, and `literal::rewrite`
replaces only that span. At load the literal has to equal what the
sandbox evaluates, otherwise it is `gw.plugin.manifest_not_data(_at)`.
The manifest gains `on_error`; settings `value` replaces `default`.
- config: plugin entries hold only id, file, sha256 and enabled. The
`on_error`, `scope` and `settings` written by 0.58.0 are ignored and
dropped on the next write of the plugins section. Plugin settings no
longer need multi-line values in the config, so that exception is gone.
- control: one save (`PUT /plugins/{id}`) that core classifies as a
data-only change or a code change, and `POST /plugins/rewrite`, which
rewrites the data in a source without side effects. A native
confirmation is needed only for a plugin holding `reply_tool_calls`:
installing it, turning it on, changing its code or approving an on-disk
change go through the `confirmed` endpoints. Raw config writes (PUT and
PATCH /config, rollback) cannot do those things either.
- UpdatePlugin, UpdatePluginConfirmed and ReplacePluginSource are removed.
- The default plugins use the new shape. Seeding writes four-field entries
and carries what the user wrote in the file over to a new shipped
version.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A plugin's JS file is now the source of truth for what it does on error, which requests it covers, and its setting values (contract addendum 4).
CONTROL_API_VERSIONgoes from 34 to 35. The workspace version is not bumped here.Plugin file
tw_plugin::literalfindsexport const manifest = { … }and reads the literal as pure data, along with its exact byte span. The tokenizer skips strings, template literals (including the code inside${…}), comments and regex literals elsewhere in the file. A regex is told from a division by the previous token.gw.plugin.manifest_not_data_at(with a line and column) orgw.plugin.manifest_not_data.literal::rewritereplaces only the literal's span. Comments inside the literal are not kept.on_error: "reject" | "skip"is new. Settingsvaluereplacesdefault.reply-languageandwsl-pathsuse the new shape and are written in the writer's style, so saving a setting changes one line.manifests.jsonis regenerated.Config
id,file,sha256andenabled.on_error,scopeandsettings. These are ignored whatever they contain, so loading never fails because of them. They are removed the next time core writes thepluginssection (tw_config::plugins::drop_legacy).rewriteandconfirmedare reserved ids.Control plane
PluginRewritePOST /plugins/rewrite(PluginRewriteRequest→PluginSource)CreatePluginPOST /pluginsreply_tool_callsCreatePluginConfirmedPOST /plugins/confirmedSavePluginPUT /plugins/{id}(PluginSave { source, enabled, base_version })SavePluginConfirmedPUT /plugins/{id}/confirmedApprovePluginFilePOST /plugins/{id}/approveApprovePluginFileConfirmedPOST /plugins/{id}/approve/confirmedHow a save is classified. Core compares the new source with the approved one:
on_error,matchand settingsvalue.enabledchanges, and the files are not touched.How a save is written. The file, the approved copy and the config hash change together under the plugin write lock, with no "changed" state in between. The write is tied to the config version that was read.
When confirmation is needed (§3). The plain endpoints refuse with 403
control.plugin.needs_confirmationwhen the plugin holdsreply_tool_calls(in the old or new manifest, or when the old one cannot be read) and the request would:Raw config writes are guarded too.
PUT /config,PATCH /configand rollback can no longer turn on, add or re-approve such a plugin. They are webview-callable, and they used to bypass the rule.Removed:
UpdatePlugin,UpdatePluginConfirmed,ReplacePluginSource,PluginUpdate,PluginSourceReplace, andPluginView.settings(the values are insettings_schema[].value).Other type changes:
SettingSpecView.defaultis nowvalue, andManifestViewgainson_error.Default plugins. The record of a default plugin follows data-only saves. A new shipped version still replaces a default the user only configured, and what the user wrote in the file (on_error, match, settings with an explicit
value) is carried over.Message codes
gw.plugin.manifest_not_datagw.plugin.manifest_not_data_atconfig.plugin.blank_patternconfig.plugin.setting_typecontrol.plugin.needs_confirmationTests
*anywhere, case-insensitive, in clients, models and upstreams.🤖 Generated with Claude Code