From 2d976b0b19b56619909661a8898f0ca97c3d9332 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 6 Nov 2026 04:50:52 +0100 Subject: [PATCH] fix(plugins): a plugin reporting settings is sent every key of an options patch --- docs/plugins.md | 2 +- .../src/server/agent/plugin-provider.test.ts | 6 ++++-- .../server/src/server/agent/plugin-provider.ts | 14 +++----------- 3 files changed, 8 insertions(+), 14 deletions(-) diff --git a/docs/plugins.md b/docs/plugins.md index a5670fcbdf..e38e5bd2b8 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -544,7 +544,7 @@ The provider draws some of those keys in the composer by reporting `controls` an built-in providers ([options flow](providers.md#options-flow)). `PluginAgentSession` in `packages/server/src/server/agent/plugin-provider.ts` is that adapter. A provider still reporting `settings` is read through `plugin-provider-options.ts`, which turns each setting into a control and -its value into an option and is sent both shapes in `session.configure` (`settings` holds only the keys it reports); delete it with +its value into an option and is sent both shapes in `session.configure`; delete it with `COMPAT(plugin-provider-settings)`. The ACP shim still reports `settings`. Providers add an optional veto to daemon baseline cleanup through the per-session diff --git a/packages/server/src/server/agent/plugin-provider.test.ts b/packages/server/src/server/agent/plugin-provider.test.ts index 465dcda931..610e9545aa 100644 --- a/packages/server/src/server/agent/plugin-provider.test.ts +++ b/packages/server/src/server/agent/plugin-provider.test.ts @@ -1481,8 +1481,10 @@ describe("PluginAgentClientRegistry", () => { changes: { providerOptions: object; settings?: object }; }; expect(configure.changes.providerOptions).toEqual({ loud: true, launch_key: "changed" }); - // A plugin reporting `settings` reads only the ones it reports. - expect(configure.changes.settings).toEqual(shape === "settings" ? { loud: true } : undefined); + // A plugin reporting `settings` is sent the same keys under that name. + expect(configure.changes.settings).toEqual( + shape === "settings" ? { loud: true, launch_key: "changed" } : undefined, + ); await session.close(); }); diff --git a/packages/server/src/server/agent/plugin-provider.ts b/packages/server/src/server/agent/plugin-provider.ts index 2c30d2c144..f5166016a2 100644 --- a/packages/server/src/server/agent/plugin-provider.ts +++ b/packages/server/src/server/agent/plugin-provider.ts @@ -1430,7 +1430,7 @@ class PluginAgentSession implements AgentSession { private async applyOptionsNow(options: ProviderOptions): Promise> { this.assertReplayComplete(); const before = this.currentOptions(); - const { controls, legacySettings } = readPluginOptionState(this.bridge.config); + const { legacySettings } = readPluginOptionState(this.bridge.config); // The plugin applies every key it is given or rejects the whole request. const settings: Record = {}; for (const [id, value] of Object.entries(options)) { @@ -1440,18 +1440,10 @@ class PluginAgentSession implements AgentSession { this.applyingOptions = true; try { // COMPAT(plugin-provider-settings): added in v0.15.0, remove after 2027-05-01. A plugin - // reporting `settings` reads only its own settings, so it is sent those. + // reporting `settings` is sent the same keys under that name. await this.bridge.configure({ providerOptions: settings, - ...(legacySettings - ? { - settings: Object.fromEntries( - Object.entries(settings).filter(([id]) => - controls.some((control) => control.key === id), - ), - ), - } - : {}), + ...(legacySettings ? { settings } : {}), }); } catch (error) { // Nothing is stored for a refused patch, so what the plugin moved is reported once the