From dda31538afd356038ea7e4ce4bb3c6c67ab572aa Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Mon, 2 Nov 2026 21:59:35 +0100 Subject: [PATCH] fix: hand the assistant's host flags to plugins in api.host.features The sandbox runtime copied only the three policy flags from the host's features, so a flag an assistant area adds (sidePanel, files, ...) never reached api.host.features and the plugin hid the feature. The runtime now copies every other flag the host names as true or false. The host bridge assertions compare against withAssistant(...) like the snapshot ones already did, because the init payload and host-change carry the assistant's flags on top of the three policy flags. --- lib/__tests__/plugin-host-features.test.ts | 15 +++++++++++---- lib/plugin-sandbox/runtime.tsx | 5 +++++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/lib/__tests__/plugin-host-features.test.ts b/lib/__tests__/plugin-host-features.test.ts index 9e0dc0abf..bea136c3a 100644 --- a/lib/__tests__/plugin-host-features.test.ts +++ b/lib/__tests__/plugin-host-features.test.ts @@ -85,21 +85,21 @@ describe('host bridge', () => { const plugin = { id: 'p', name: 'P', version: '1.0.0', permissions: [], settings: {}, settingsSchema: {} } as never; const instance = createBackgroundInstance({ plugin, tier: 'privileged', code: '', locale: 'en' }); const pending = (instance as unknown as { pendingInit: { hostFeatures?: unknown } | null }).pendingInit; - expect(pending?.hostFeatures).toEqual({ externalSenderLabel: true, externalRecipientWarning: false, noThirdPartyTraffic: false }); + expect(pending?.hostFeatures).toEqual(withAssistant({ externalSenderLabel: true, externalRecipientWarning: false, noThirdPartyTraffic: false })); // Before init has gone out a change lands in the payload ... setExternalMail({}); - expect(pending?.hostFeatures).toEqual({ externalSenderLabel: false, externalRecipientWarning: false, noThirdPartyTraffic: false }); + expect(pending?.hostFeatures).toEqual(withAssistant({ externalSenderLabel: false, externalRecipientWarning: false, noThirdPartyTraffic: false })); // ... after it, as a host-change message. const sent: unknown[] = []; (instance as unknown as { pendingInit: unknown }).pendingInit = null; (instance as unknown as { send: (m: unknown) => void }).send = (m) => sent.push(m); setExternalMail({ recipientWarning: true }); - expect(sent).toEqual([{ type: 'host-change', features: { externalSenderLabel: false, externalRecipientWarning: true, noThirdPartyTraffic: false } }]); + expect(sent).toEqual([{ type: 'host-change', features: withAssistant({ externalSenderLabel: false, externalRecipientWarning: true, noThirdPartyTraffic: false }) }]); // The admin turns third-party traffic off: running plugins hear it. setNoThirdPartyTraffic(true); - expect(sent[1]).toEqual({ type: 'host-change', features: { externalSenderLabel: false, externalRecipientWarning: true, noThirdPartyTraffic: true } }); + expect(sent[1]).toEqual({ type: 'host-change', features: withAssistant({ externalSenderLabel: false, externalRecipientWarning: true, noThirdPartyTraffic: true }) }); setNoThirdPartyTraffic(false); instance.destroy(); }, 30_000); @@ -165,5 +165,12 @@ describe('api.host in the sandbox runtime', () => { expect(kept.externalSenderLabel).toBe(true); expect((kept as { noThirdPartyTraffic?: boolean }).noThirdPartyTraffic).toBe(true); expect(changes).toEqual([{ externalSenderLabel: true, externalRecipientWarning: false, noThirdPartyTraffic: true }]); + + // The assistant's flags (lib/plugin-sandbox/assistant) reach the plugin too. + expect((kept as { sidePanel?: boolean }).sidePanel).toBeUndefined(); + post(hostWindow, HOST_ORIGIN, { type: 'host-change', features: { externalSenderLabel: true, externalRecipientWarning: false, noThirdPartyTraffic: true, sidePanel: true } }); + expect((kept as { sidePanel?: boolean }).sidePanel).toBe(true); + post(hostWindow, HOST_ORIGIN, { type: 'host-change', features: { externalSenderLabel: true, externalRecipientWarning: false, noThirdPartyTraffic: true, sidePanel: 'yes' } }); + expect((kept as { sidePanel?: boolean }).sidePanel).toBe(false); }, 30_000); }); diff --git a/lib/plugin-sandbox/runtime.tsx b/lib/plugin-sandbox/runtime.tsx index 63b0b361a..ab741c7a9 100644 --- a/lib/plugin-sandbox/runtime.tsx +++ b/lib/plugin-sandbox/runtime.tsx @@ -161,6 +161,11 @@ function applyHostFeatures(next: HostFeaturesView | undefined): void { hostFeaturesState.externalRecipientWarning = next.externalRecipientWarning === true; // A host older than 1.20 doesn't send it, and never let such a plugin run in the mode. hostFeaturesState.noThirdPartyTraffic = next.noThirdPartyTraffic === true; + // The AI assistant's flags (lib/plugin-sandbox/assistant): whatever the host names, true or false. + const flags = hostFeaturesState as unknown as Record; + for (const [key, value] of Object.entries(next)) { + if (key !== 'externalSenderLabel' && key !== 'externalRecipientWarning' && key !== 'noThirdPartyTraffic') flags[key] = value === true; + } for (const listener of hostFeatureListeners) { try { listener(hostFeaturesState); } catch (err) { console.error('[plugin] host feature listener failed', err); } }