mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-04 00:14:12 +00:00
refactor: give the AI assistant's host APIs one set of files per area
The assistant's new host calls come in four areas (context, actions, Files, the panel in every view), built side by side. Each area gets its own files under lib/plugin-sandbox/assistant for its permissions, method table, dispatcher, host feature flags and sandbox methods; the shared files only read the merged result. No area adds anything yet. The host features test now compares against the core flags plus the areas' flags.
This commit is contained in:
@@ -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<unknown>) => ({ 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']]]);
|
||||
});
|
||||
});
|
||||
@@ -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<string, boolean>) => ({ ...core, ...assistantFeatures() });
|
||||
|
||||
function setExternalMail(patch: Partial<typeof DEFAULT_ORGANISATION_POLICY.externalMail>) {
|
||||
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
|
||||
|
||||
@@ -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:
|
||||
|
||||
- `<area>.types.ts` (pure, no imports from the app): `PERMISSIONS`, the
|
||||
permissions the area adds (they join `ALL_PERMISSIONS`), and
|
||||
`<Area>HostFeatures`, the flags it adds to `api.host.features`.
|
||||
- `<area>.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).
|
||||
- `<area>.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.<key>`, and docs.
|
||||
@@ -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 {};
|
||||
}
|
||||
@@ -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<Record<string, Permission | null>> = {};
|
||||
|
||||
/** Runs a method of this area; NOT_MINE for any other. */
|
||||
export async function dispatch(_plugin: InstalledPlugin, _method: string, _args: unknown[]): Promise<unknown> {
|
||||
return NOT_MINE;
|
||||
}
|
||||
|
||||
/** This area's flags in api.host.features, right now. */
|
||||
export function features(): AreaHostFeaturesB {
|
||||
return {};
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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 {};
|
||||
}
|
||||
@@ -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<Record<string, Permission | null>> = {};
|
||||
|
||||
/** Runs a method of this area; NOT_MINE for any other. */
|
||||
export async function dispatch(_plugin: InstalledPlugin, _method: string, _args: unknown[]): Promise<unknown> {
|
||||
return NOT_MINE;
|
||||
}
|
||||
|
||||
/** This area's flags in api.host.features, right now. */
|
||||
export function features(): AreaHostFeaturesC {
|
||||
return {};
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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 {};
|
||||
}
|
||||
@@ -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<Record<string, Permission | null>> = {};
|
||||
|
||||
/** Runs a method of this area; NOT_MINE for any other. */
|
||||
export async function dispatch(_plugin: InstalledPlugin, _method: string, _args: unknown[]): Promise<unknown> {
|
||||
return NOT_MINE;
|
||||
}
|
||||
|
||||
/** This area's flags in api.host.features, right now. */
|
||||
export function features(): AreaHostFeaturesD {
|
||||
return {};
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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 {};
|
||||
}
|
||||
@@ -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<Record<string, Permission | null>> = {};
|
||||
|
||||
/** Runs a method of this area; NOT_MINE for any other. */
|
||||
export async function dispatch(_plugin: InstalledPlugin, _method: string, _args: unknown[]): Promise<unknown> {
|
||||
return NOT_MINE;
|
||||
}
|
||||
|
||||
/** This area's flags in api.host.features, right now. */
|
||||
export function features(): AreaHostFeaturesF {
|
||||
return {};
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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<Record<string, Permission | null>> = 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<unknown> {
|
||||
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()));
|
||||
}
|
||||
@@ -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');
|
||||
@@ -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<unknown>;
|
||||
/** Methods by namespace: { email: { getThread: (...) => ... } }. */
|
||||
export type RuntimeAdditions = Record<string, Record<string, (...args: never[]) => unknown>>;
|
||||
|
||||
/** Adds every area's methods to the plugin's api, namespace by namespace. */
|
||||
export function withAssistantRuntime<T extends object>(api: T, callApi: CallApi): T {
|
||||
const target = api as unknown as Record<string, Record<string, unknown>>;
|
||||
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;
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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<string>([
|
||||
]);
|
||||
|
||||
export const PERM_PER_METHOD: Readonly<Record<string, Permission | null>> = {
|
||||
// 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;
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+4
-1
@@ -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];
|
||||
|
||||
Reference in New Issue
Block a user