diff --git a/lib/__tests__/plugin-assistant-areas.test.ts b/lib/__tests__/plugin-assistant-areas.test.ts new file mode 100644 index 000000000..2574cd39c --- /dev/null +++ b/lib/__tests__/plugin-assistant-areas.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it, vi } from 'vitest'; + +// The AI assistant's host APIs live in one set of files per area +// (lib/plugin-sandbox/assistant). These tests check that whatever an area +// adds reaches the permission table, the dispatcher, api.host.features and +// the plugin's api, without the shared files naming it. + +vi.mock('@/stores/toast-store', () => ({ + toast: { success: vi.fn(), error: vi.fn(), info: vi.fn(), warning: vi.fn() }, +})); + +const area = vi.hoisted(() => ({ + calls: [] as unknown[][], +})); + +vi.mock('@/lib/plugin-sandbox/assistant/b', () => ({ + PERMS: { 'email.probeArea': 'email:read' }, + dispatch: async (_plugin: unknown, method: string, args: unknown[]) => { area.calls.push([method, ...args]); return 'from area b'; }, + features: () => ({ probeArea: true }), +})); + +vi.mock('@/lib/plugin-sandbox/assistant/b.runtime', () => ({ + runtime: (callApi: (m: string, a: unknown[]) => Promise) => ({ email: { probeArea: (x: string) => callApi('email.probeArea', [x]) } }), +})); + +const { PERM_PER_METHOD, dispatchApiCall } = await import('@/lib/plugin-sandbox/host-api'); +const { snapshotHostFeatures } = await import('@/lib/plugin-sandbox/host-features'); +const { withAssistantRuntime } = await import('@/lib/plugin-sandbox/assistant/runtime'); +const { NOT_MINE, dispatchAssistant } = await import('@/lib/plugin-sandbox/assistant'); + +const plugin = { id: 'p', name: 'P', permissions: ['email:read'], managed: true } as never; + +describe('the assistant areas', () => { + it('put their methods behind their permissions and run them', async () => { + expect(PERM_PER_METHOD['email.probeArea']).toBe('email:read'); + expect(await dispatchApiCall(plugin, 'email.probeArea', ['x'])).toBe('from area b'); + expect(area.calls).toEqual([['email.probeArea', 'x']]); + // Without the permission the area never sees the call. + const without = { ...(plugin as object), permissions: [] } as never; + await expect(dispatchApiCall(without, 'email.probeArea', [])).rejects.toThrow(/lacks permission "email:read"/); + expect(area.calls).toHaveLength(1); + }); + + it('leave other methods alone', async () => { + expect(await dispatchAssistant(plugin, 'email.get', [])).toBe(NOT_MINE); + }); + + it('add their flags to api.host.features', () => { + expect(snapshotHostFeatures()).toMatchObject({ probeArea: true, externalSenderLabel: false }); + }); + + it('add their methods to the plugin api next to the existing ones', () => { + const calls: unknown[][] = []; + const api = withAssistantRuntime({ email: { get: () => 'core' } }, async (method, args) => { calls.push([method, args]); return null; }); + expect(api.email.get()).toBe('core'); + (api.email as unknown as { probeArea: (x: string) => unknown }).probeArea('y'); + expect(calls).toEqual([['email.probeArea', ['y']]]); + }); +}); diff --git a/lib/__tests__/plugin-host-features.test.ts b/lib/__tests__/plugin-host-features.test.ts index 5c76a65d5..9e0dc0abf 100644 --- a/lib/__tests__/plugin-host-features.test.ts +++ b/lib/__tests__/plugin-host-features.test.ts @@ -17,6 +17,10 @@ import { isSlotHandedOver, snapshotHostFeatures, } from '@/lib/plugin-sandbox/host-features'; +import { assistantFeatures } from '@/lib/plugin-sandbox/assistant'; + +/** The AI assistant's flags (lib/plugin-sandbox/assistant) come on top; their own tests check them. */ +const withAssistant = (core: Record) => ({ ...core, ...assistantFeatures() }); function setExternalMail(patch: Partial) { usePolicyStore.setState({ @@ -31,11 +35,11 @@ beforeEach(() => setExternalMail({})); describe('snapshotHostFeatures', () => { it('has one flag per switch', () => { - expect(snapshotHostFeatures()).toEqual({ externalSenderLabel: false, externalRecipientWarning: false, noThirdPartyTraffic: false }); + expect(snapshotHostFeatures()).toEqual(withAssistant({ externalSenderLabel: false, externalRecipientWarning: false, noThirdPartyTraffic: false })); setExternalMail({ senderLabel: true }); - expect(snapshotHostFeatures()).toEqual({ externalSenderLabel: true, externalRecipientWarning: false, noThirdPartyTraffic: false }); + expect(snapshotHostFeatures()).toEqual(withAssistant({ externalSenderLabel: true, externalRecipientWarning: false, noThirdPartyTraffic: false })); setExternalMail({ recipientWarning: true }); - expect(snapshotHostFeatures()).toEqual({ externalSenderLabel: false, externalRecipientWarning: true, noThirdPartyTraffic: false }); + expect(snapshotHostFeatures()).toEqual(withAssistant({ externalSenderLabel: false, externalRecipientWarning: true, noThirdPartyTraffic: false })); }); // A link check switches Google Safe Browsing off in the mode instead of diff --git a/lib/plugin-sandbox/assistant/README.md b/lib/plugin-sandbox/assistant/README.md new file mode 100644 index 000000000..cf0d413aa --- /dev/null +++ b/lib/plugin-sandbox/assistant/README.md @@ -0,0 +1,27 @@ +# Host APIs for the AI assistant (2.2) + +Host calls the assistant plugin needs, one set of files per area, so the +areas can be built side by side without editing the same lines: + +| Area | What | Files | +| --- | --- | --- | +| b | More context: attachment text and images, threads, the list selection | `b.types.ts`, `b.ts`, `b.runtime.ts` | +| c | More actions: invitations, filters, batches, out-of-office, settings | `c.types.ts`, `c.ts`, `c.runtime.ts` | +| d | Files: find, read, attach, save, create | `d.types.ts`, `d.ts`, `d.runtime.ts` | +| f | The panel in every view | `f.types.ts`, `f.ts`, `f.runtime.ts` | + +Each area fills its own three files: + +- `.types.ts` (pure, no imports from the app): `PERMISSIONS`, the + permissions the area adds (they join `ALL_PERMISSIONS`), and + `HostFeatures`, the flags it adds to `api.host.features`. +- `.ts` (host side): `PERMS` (method → permission, joins + `PERM_PER_METHOD`), `dispatch(plugin, method, args)` (returns `NOT_MINE` + for methods it doesn't own) and `features()` (its flags right now). +- `.runtime.ts` (inside the sandbox): `runtime(callApi)`, the methods + it adds to the plugin's `api`, by namespace (`{ email: { getThread } }`). + +`index.ts`, `runtime.ts` and `types.ts` merge the areas; host-api.ts, +host-features.ts, runtime.tsx and plugin-types.ts read only those. A new +permission needs a check (a method in `PERMS`, a slot or a hook), its texts +in every locale under `plugins.permissions.`, and docs. diff --git a/lib/plugin-sandbox/assistant/b.runtime.ts b/lib/plugin-sandbox/assistant/b.runtime.ts new file mode 100644 index 000000000..b9e5f1850 --- /dev/null +++ b/lib/plugin-sandbox/assistant/b.runtime.ts @@ -0,0 +1,8 @@ +// Area b of the assistant's host APIs, inside the plugin sandbox (see README.md). + +import type { CallApi, RuntimeAdditions } from './runtime'; + +/** The methods this area adds to the plugin's api, by namespace. */ +export function runtime(_callApi: CallApi): RuntimeAdditions { + return {}; +} diff --git a/lib/plugin-sandbox/assistant/b.ts b/lib/plugin-sandbox/assistant/b.ts new file mode 100644 index 000000000..ea6ac7e71 --- /dev/null +++ b/lib/plugin-sandbox/assistant/b.ts @@ -0,0 +1,18 @@ +// Area b of the assistant's host APIs, host side (see README.md). + +import type { InstalledPlugin, Permission } from '../../plugin-types'; +import { NOT_MINE } from './not-mine'; +import type { AreaHostFeaturesB } from './b.types'; + +/** Method → permission, joined into PERM_PER_METHOD. */ +export const PERMS: Readonly> = {}; + +/** Runs a method of this area; NOT_MINE for any other. */ +export async function dispatch(_plugin: InstalledPlugin, _method: string, _args: unknown[]): Promise { + return NOT_MINE; +} + +/** This area's flags in api.host.features, right now. */ +export function features(): AreaHostFeaturesB { + return {}; +} diff --git a/lib/plugin-sandbox/assistant/b.types.ts b/lib/plugin-sandbox/assistant/b.types.ts new file mode 100644 index 000000000..41e079eb8 --- /dev/null +++ b/lib/plugin-sandbox/assistant/b.types.ts @@ -0,0 +1,9 @@ +// Area b of the assistant's host APIs: its permissions and its flags in +// api.host.features (see README.md). Pure: no imports from the app. + +/** Permissions this area adds to ALL_PERMISSIONS. */ +export const PERMISSIONS = [] as const; + +/** Flags this area adds to api.host.features. */ +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface AreaHostFeaturesB {} diff --git a/lib/plugin-sandbox/assistant/c.runtime.ts b/lib/plugin-sandbox/assistant/c.runtime.ts new file mode 100644 index 000000000..5e63b9136 --- /dev/null +++ b/lib/plugin-sandbox/assistant/c.runtime.ts @@ -0,0 +1,8 @@ +// Area c of the assistant's host APIs, inside the plugin sandbox (see README.md). + +import type { CallApi, RuntimeAdditions } from './runtime'; + +/** The methods this area adds to the plugin's api, by namespace. */ +export function runtime(_callApi: CallApi): RuntimeAdditions { + return {}; +} diff --git a/lib/plugin-sandbox/assistant/c.ts b/lib/plugin-sandbox/assistant/c.ts new file mode 100644 index 000000000..f0fc6c41e --- /dev/null +++ b/lib/plugin-sandbox/assistant/c.ts @@ -0,0 +1,18 @@ +// Area c of the assistant's host APIs, host side (see README.md). + +import type { InstalledPlugin, Permission } from '../../plugin-types'; +import { NOT_MINE } from './not-mine'; +import type { AreaHostFeaturesC } from './c.types'; + +/** Method → permission, joined into PERM_PER_METHOD. */ +export const PERMS: Readonly> = {}; + +/** Runs a method of this area; NOT_MINE for any other. */ +export async function dispatch(_plugin: InstalledPlugin, _method: string, _args: unknown[]): Promise { + return NOT_MINE; +} + +/** This area's flags in api.host.features, right now. */ +export function features(): AreaHostFeaturesC { + return {}; +} diff --git a/lib/plugin-sandbox/assistant/c.types.ts b/lib/plugin-sandbox/assistant/c.types.ts new file mode 100644 index 000000000..cebdb0f88 --- /dev/null +++ b/lib/plugin-sandbox/assistant/c.types.ts @@ -0,0 +1,9 @@ +// Area c of the assistant's host APIs: its permissions and its flags in +// api.host.features (see README.md). Pure: no imports from the app. + +/** Permissions this area adds to ALL_PERMISSIONS. */ +export const PERMISSIONS = [] as const; + +/** Flags this area adds to api.host.features. */ +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface AreaHostFeaturesC {} diff --git a/lib/plugin-sandbox/assistant/d.runtime.ts b/lib/plugin-sandbox/assistant/d.runtime.ts new file mode 100644 index 000000000..0ce8066a4 --- /dev/null +++ b/lib/plugin-sandbox/assistant/d.runtime.ts @@ -0,0 +1,8 @@ +// Area d of the assistant's host APIs, inside the plugin sandbox (see README.md). + +import type { CallApi, RuntimeAdditions } from './runtime'; + +/** The methods this area adds to the plugin's api, by namespace. */ +export function runtime(_callApi: CallApi): RuntimeAdditions { + return {}; +} diff --git a/lib/plugin-sandbox/assistant/d.ts b/lib/plugin-sandbox/assistant/d.ts new file mode 100644 index 000000000..d6e69eba4 --- /dev/null +++ b/lib/plugin-sandbox/assistant/d.ts @@ -0,0 +1,18 @@ +// Area d of the assistant's host APIs, host side (see README.md). + +import type { InstalledPlugin, Permission } from '../../plugin-types'; +import { NOT_MINE } from './not-mine'; +import type { AreaHostFeaturesD } from './d.types'; + +/** Method → permission, joined into PERM_PER_METHOD. */ +export const PERMS: Readonly> = {}; + +/** Runs a method of this area; NOT_MINE for any other. */ +export async function dispatch(_plugin: InstalledPlugin, _method: string, _args: unknown[]): Promise { + return NOT_MINE; +} + +/** This area's flags in api.host.features, right now. */ +export function features(): AreaHostFeaturesD { + return {}; +} diff --git a/lib/plugin-sandbox/assistant/d.types.ts b/lib/plugin-sandbox/assistant/d.types.ts new file mode 100644 index 000000000..2433c44fc --- /dev/null +++ b/lib/plugin-sandbox/assistant/d.types.ts @@ -0,0 +1,9 @@ +// Area d of the assistant's host APIs: its permissions and its flags in +// api.host.features (see README.md). Pure: no imports from the app. + +/** Permissions this area adds to ALL_PERMISSIONS. */ +export const PERMISSIONS = [] as const; + +/** Flags this area adds to api.host.features. */ +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface AreaHostFeaturesD {} diff --git a/lib/plugin-sandbox/assistant/f.runtime.ts b/lib/plugin-sandbox/assistant/f.runtime.ts new file mode 100644 index 000000000..f682649eb --- /dev/null +++ b/lib/plugin-sandbox/assistant/f.runtime.ts @@ -0,0 +1,8 @@ +// Area f of the assistant's host APIs, inside the plugin sandbox (see README.md). + +import type { CallApi, RuntimeAdditions } from './runtime'; + +/** The methods this area adds to the plugin's api, by namespace. */ +export function runtime(_callApi: CallApi): RuntimeAdditions { + return {}; +} diff --git a/lib/plugin-sandbox/assistant/f.ts b/lib/plugin-sandbox/assistant/f.ts new file mode 100644 index 000000000..6565ac62b --- /dev/null +++ b/lib/plugin-sandbox/assistant/f.ts @@ -0,0 +1,18 @@ +// Area f of the assistant's host APIs, host side (see README.md). + +import type { InstalledPlugin, Permission } from '../../plugin-types'; +import { NOT_MINE } from './not-mine'; +import type { AreaHostFeaturesF } from './f.types'; + +/** Method → permission, joined into PERM_PER_METHOD. */ +export const PERMS: Readonly> = {}; + +/** Runs a method of this area; NOT_MINE for any other. */ +export async function dispatch(_plugin: InstalledPlugin, _method: string, _args: unknown[]): Promise { + return NOT_MINE; +} + +/** This area's flags in api.host.features, right now. */ +export function features(): AreaHostFeaturesF { + return {}; +} diff --git a/lib/plugin-sandbox/assistant/f.types.ts b/lib/plugin-sandbox/assistant/f.types.ts new file mode 100644 index 000000000..82361b441 --- /dev/null +++ b/lib/plugin-sandbox/assistant/f.types.ts @@ -0,0 +1,9 @@ +// Area f of the assistant's host APIs: its permissions and its flags in +// api.host.features (see README.md). Pure: no imports from the app. + +/** Permissions this area adds to ALL_PERMISSIONS. */ +export const PERMISSIONS = [] as const; + +/** Flags this area adds to api.host.features. */ +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface AreaHostFeaturesF {} diff --git a/lib/plugin-sandbox/assistant/index.ts b/lib/plugin-sandbox/assistant/index.ts new file mode 100644 index 000000000..9d73e4a8d --- /dev/null +++ b/lib/plugin-sandbox/assistant/index.ts @@ -0,0 +1,29 @@ +// The assistant's host API areas, merged, host side (see README.md). + +import type { InstalledPlugin, Permission } from '../../plugin-types'; +import { NOT_MINE } from './not-mine'; +import * as b from './b'; +import * as c from './c'; +import * as d from './d'; +import * as f from './f'; +import type { AssistantHostFeatures } from './types'; + +export { NOT_MINE }; +const AREAS = [b, c, d, f]; + +/** Method → permission of every area, joined into PERM_PER_METHOD. */ +export const ASSISTANT_PERMS: Readonly> = Object.assign({}, ...AREAS.map((a) => a.PERMS)); + +/** Runs an area's method; NOT_MINE when no area owns it. The permission is checked before. */ +export async function dispatchAssistant(plugin: InstalledPlugin, method: string, args: unknown[]): Promise { + for (const area of AREAS) { + if (!(method in area.PERMS)) continue; + return area.dispatch(plugin, method, args); + } + return NOT_MINE; +} + +/** Every area's api.host.features flags, right now. */ +export function assistantFeatures(): AssistantHostFeatures { + return Object.assign({}, ...AREAS.map((a) => a.features())); +} diff --git a/lib/plugin-sandbox/assistant/not-mine.ts b/lib/plugin-sandbox/assistant/not-mine.ts new file mode 100644 index 000000000..3dd29ae6c --- /dev/null +++ b/lib/plugin-sandbox/assistant/not-mine.ts @@ -0,0 +1,2 @@ +/** What an area's dispatch returns for a method it doesn't own. */ +export const NOT_MINE: unique symbol = Symbol('not mine'); diff --git a/lib/plugin-sandbox/assistant/runtime.ts b/lib/plugin-sandbox/assistant/runtime.ts new file mode 100644 index 000000000..8d0c42800 --- /dev/null +++ b/lib/plugin-sandbox/assistant/runtime.ts @@ -0,0 +1,21 @@ +// The assistant's host API areas, merged, inside the plugin sandbox (see README.md). + +import { runtime as b } from './b.runtime'; +import { runtime as c } from './c.runtime'; +import { runtime as d } from './d.runtime'; +import { runtime as f } from './f.runtime'; + +export type CallApi = (method: string, args: unknown[], timeoutMs?: number) => Promise; +/** Methods by namespace: { email: { getThread: (...) => ... } }. */ +export type RuntimeAdditions = Record unknown>>; + +/** Adds every area's methods to the plugin's api, namespace by namespace. */ +export function withAssistantRuntime(api: T, callApi: CallApi): T { + const target = api as unknown as Record>; + for (const area of [b, c, d, f]) { + for (const [ns, methods] of Object.entries(area(callApi))) { + target[ns] = Object.assign(target[ns] ?? {}, methods); + } + } + return api; +} diff --git a/lib/plugin-sandbox/assistant/types.ts b/lib/plugin-sandbox/assistant/types.ts new file mode 100644 index 000000000..a334c8131 --- /dev/null +++ b/lib/plugin-sandbox/assistant/types.ts @@ -0,0 +1,15 @@ +// The assistant's host API areas, merged: permissions and api.host.features +// flags (see README.md). Pure: plugin-types.ts imports this. + +import { PERMISSIONS as B } from './b.types'; +import { PERMISSIONS as C } from './c.types'; +import { PERMISSIONS as D } from './d.types'; +import { PERMISSIONS as F } from './f.types'; +import type { AreaHostFeaturesB } from './b.types'; +import type { AreaHostFeaturesC } from './c.types'; +import type { AreaHostFeaturesD } from './d.types'; +import type { AreaHostFeaturesF } from './f.types'; + +export const ASSISTANT_PERMISSIONS = [...B, ...C, ...D, ...F] as const; + +export interface AssistantHostFeatures extends AreaHostFeaturesB, AreaHostFeaturesC, AreaHostFeaturesD, AreaHostFeaturesF {} diff --git a/lib/plugin-sandbox/host-api.ts b/lib/plugin-sandbox/host-api.ts index 76e9a50e7..d12b847f9 100644 --- a/lib/plugin-sandbox/host-api.ts +++ b/lib/plugin-sandbox/host-api.ts @@ -74,6 +74,7 @@ import { doComposerAttach, doComposerGetBody, doComposerGetSelection, doComposer import { wkdAllowed } from '../outgoing-hosts'; import { noThirdPartyTraffic } from '../third-party-mode'; import { openExternalLink } from '../plugin-hook-result'; +import { ASSISTANT_PERMS, NOT_MINE, dispatchAssistant } from './assistant'; /** * Methods only callable from the privileged (same-origin) tier. These expose @@ -112,6 +113,8 @@ const PRIVILEGED_ONLY_METHODS = new Set([ ]); export const PERM_PER_METHOD: Readonly> = { + // The AI assistant's host APIs, one table per area (lib/plugin-sandbox/assistant). + ...ASSISTANT_PERMS, // storage is unscoped by the manifest - implicit. 'storage.get': null, 'storage.set': null, @@ -2016,6 +2019,9 @@ export async function dispatchApiCall( throw new Error(`Unknown API method "${method}"`); } + const assistant = await dispatchAssistant(plugin, method, args); + if (assistant !== NOT_MINE) return assistant; + switch (method) { case 'proxy.fetch': return proxyFetch(plugin, args, opts?.sink ?? null); case 'proxy.cancel': proxyCancel(plugin, args[0]); return undefined; diff --git a/lib/plugin-sandbox/host-features.ts b/lib/plugin-sandbox/host-features.ts index 63edc8cb3..0d82ac618 100644 --- a/lib/plugin-sandbox/host-features.ts +++ b/lib/plugin-sandbox/host-features.ts @@ -12,6 +12,7 @@ import { usePolicyStore } from '@/stores/policy-store'; import { noThirdPartyTraffic, subscribeThirdPartyMode } from '@/lib/third-party-mode'; import type { HostFeaturesView } from '../plugin-types'; import type { OrganisationPolicy } from '@/lib/admin/organisation-policy'; +import { assistantFeatures } from './assistant'; /** The external-mail-warning plugin, retired in favour of organisation.externalMail. */ export const EXTERNAL_MAIL_WARNING_PLUGIN_ID = 'external-mail-warning'; @@ -26,7 +27,11 @@ export function hostFeaturesFor(externalMail: OrganisationPolicy['externalMail'] } export function snapshotHostFeatures(): HostFeaturesView { - return hostFeaturesFor(usePolicyStore.getState().getOrganisation().externalMail, noThirdPartyTraffic()); + return { + ...hostFeaturesFor(usePolicyStore.getState().getOrganisation().externalMail, noThirdPartyTraffic()), + // The AI assistant's host APIs this webmail offers (lib/plugin-sandbox/assistant). + ...assistantFeatures(), + }; } /** diff --git a/lib/plugin-sandbox/runtime.tsx b/lib/plugin-sandbox/runtime.tsx index daf0ee0db..63b0b361a 100644 --- a/lib/plugin-sandbox/runtime.tsx +++ b/lib/plugin-sandbox/runtime.tsx @@ -38,6 +38,7 @@ import type { TaskView, TemplateInput, TemplateView, VacationInput, VacationView, ComposerSnapshot, ComposerAttachmentData, } from '../plugin-types'; import { AddressBook, ContactCard } from '../jmap/types'; +import { withAssistantRuntime } from './assistant/runtime'; import { EncryptionAtRestConfig, PublicKeyInput } from '@/stores/account-security-store'; // ─── Module-scope state ────────────────────────────────────── @@ -424,7 +425,12 @@ interface PluginKeywordCounts { unread: number; } +/** The plugin's api, with the AI assistant's host APIs (lib/plugin-sandbox/assistant) added. */ function buildPluginApi(manifest: PluginManifest) { + return withAssistantRuntime(buildCoreApi(manifest), callApi); +} + +function buildCoreApi(manifest: PluginManifest) { applySettings({ ...manifest.settings }); localeTables = JSON.parse(JSON.stringify(manifest.locales ?? {})); return { diff --git a/lib/plugin-types.ts b/lib/plugin-types.ts index fa1a74b5d..a122af6c6 100644 --- a/lib/plugin-types.ts +++ b/lib/plugin-types.ts @@ -1,5 +1,6 @@ import type { CredentialSpec } from './plugin-secrets/credentials'; import type { CatalogSlotName } from './plugin-slot-catalog'; +import { ASSISTANT_PERMISSIONS, type AssistantHostFeatures } from './plugin-sandbox/assistant/types'; // Plugin & Theme system types @@ -956,7 +957,7 @@ export interface AppInfoView { * that does the same job steps back while one is true. Missing on hosts * older than 1.19.0, so read it as `api.host?.features?.x === true`. */ -export interface HostFeaturesView { +export interface HostFeaturesView extends AssistantHostFeatures { /** The admin turned on the "External" label on mail from outside the organisation (organisation.externalMail.senderLabel). */ externalSenderLabel: boolean; /** The admin turned on the confirm before sending outside the organisation (organisation.externalMail.recipientWarning). */ @@ -1591,6 +1592,8 @@ export const ALL_PERMISSIONS = [ 'http:proxy', 'admin:config', 'app:lifecycle', + // The AI assistant's host APIs, one list per area (lib/plugin-sandbox/assistant). + ...ASSISTANT_PERMISSIONS, ] as const; export type Permission = (typeof ALL_PERMISSIONS)[number];