Skip to content

Plugins keep their configuration in their own file (protocol 35) - #276

Merged
fylorn merged 1 commit into
mainfrom
feat/plugin-source-of-truth
Oct 3, 2026
Merged

fylorn merged 1 commit into
mainfrom
feat/plugin-source-of-truth

Conversation

@fylorn

@fylorn fylorn commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

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_VERSION goes from 34 to 35. The workspace version is not bumped here.

Plugin file

  • Pure-data manifest. tw_plugin::literal finds export 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.
  • Load check. At load, the parsed literal has to equal what the sandbox evaluates. Otherwise loading fails with gw.plugin.manifest_not_data_at (with a line and column) or gw.plugin.manifest_not_data.
  • Writer. It prints a literal in one fixed style:
    • two-space indent, unquoted identifier keys, double-quoted strings;
    • the manifest's own fields one per line;
    • nested objects and arrays inline when they fit in 80 columns, otherwise one item per line with trailing commas;
    • numbers as JavaScript prints them.
  • Rewrite. literal::rewrite replaces only the literal's span. Comments inside the literal are not kept.
  • Manifest fields. on_error: "reject" | "skip" is new. Settings value replaces default.
  • Default plugins. reply-language and wsl-paths use the new shape and are written in the writer's style, so saving a setting changes one line. manifests.json is regenerated.

Config

  • Plugin entries hold only id, file, sha256 and enabled.
  • Entries written by 0.58.0 may still have on_error, scope and settings. These are ignored whatever they contain, so loading never fails because of them. They are removed the next time core writes the plugins section (tw_config::plugins::drop_legacy).
  • Plugin settings no longer live in the config, so the multi-line exception for them in config edits is gone.
  • rewrite and confirmed are reserved ids.

Control plane

Endpoint Webview may call it
PluginRewrite POST /plugins/rewrite (PluginRewriteRequest → PluginSource) yes. Pure, no side effects
CreatePlugin POST /plugins yes. Refuses a plugin holding reply_tool_calls
CreatePluginConfirmed POST /plugins/confirmed no
SavePlugin PUT /plugins/{id} (PluginSave { source, enabled, base_version }) yes
SavePluginConfirmed PUT /plugins/{id}/confirmed no
ApprovePluginFile POST /plugins/{id}/approve yes
ApprovePluginFileConfirmed POST /plugins/{id}/approve/confirmed no

How a save is classified. Core compares the new source with the approved one:

  • Data-only: the bytes outside the literal are identical, and the manifest differs only in on_error, match and settings value.
  • Code change: anything else.
  • Same bytes: only enabled changes, 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_confirmation when the plugin holds reply_tool_calls (in the old or new manifest, or when the old one cannot be read) and the request would:

  • install it,
  • turn it on,
  • change its code,
  • or approve an on-disk change.

Raw config writes are guarded too. PUT /config, PATCH /config and 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, and PluginView.settings (the values are in settings_schema[].value).

Other type changes: SettingSpecView.default is now value, and ManifestView gains on_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

  • New:
    • gw.plugin.manifest_not_data
    • gw.plugin.manifest_not_data_at
  • Removed (the fields are gone):
    • config.plugin.blank_pattern
    • config.plugin.setting_type
  • Same code, new sentence:
    • control.plugin.needs_confirmation

Tests

  • Literal parser and writer: value and escape cases, errors with positions, look-alikes in comments, strings, templates and regexes, and division versus regex.
  • Property and fuzz tests:
    • random manifests in random surrounding code are found byte-exactly;
    • random allowed rewrites leave every other byte identical, read back to the new values and are idempotent;
    • garbage and mutated sources never panic.
  • Sandbox property test: rewritten plugins reload in the real sandbox with exactly the new values.
  • Every row of the confirmation table, plus:
    • disable, delete and reorder;
    • a tampered manifest cache;
    • unreadable permissions;
    • raw config writes;
    • a 0.58.0 config and a 0.58.0-seeded default plugin.
  • Glob coverage for * anywhere, case-insensitive, in clients, models and upstreams.

🤖 Generated with Claude Code

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>
@fylorn
fylorn merged commit 550e6fd into main Oct 3, 2026
4 checks passed
@fylorn
fylorn deleted the feat/plugin-source-of-truth branch October 3, 2026 06:55
@fylorn fylorn mentioned this pull request Oct 3, 2026
fylorn added a commit that referenced this pull request Oct 3, 2026
Release for #276 and #277.

- Bump the workspace version to 0.59.0
- release-notes/0.59.0.md

The control-plane protocol (35) and the request store schema (25) are
already at their release values on main and do not change here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fylorn added a commit that referenced this pull request Oct 3, 2026
Release for #276 and #277.

- Bump the workspace version to 0.59.0
- release-notes/0.59.0.md

The control-plane protocol (35) and the request store schema (25) are
already at their release values on main and do not change here.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant