From 7a3d91d4f9039039a3a79f5aa8d0e5ddcb10c38f Mon Sep 17 00:00:00 2001 From: Kenneth Kreindler <42113355+KDKHD@users.noreply.github.com> Date: Mon, 14 Jul 2025 10:23:41 +0100 Subject: [PATCH 1/3] [Security Solution] [Ai Assistant] Enable the Global Security AI assistant (#226556) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Summarize your PR. If it involves visual changes include a screenshot or gif. Modify AI assistant visibility settings to allow for the Security AI assistant to be global. This fixes an issue in the product that several users have raised. It is very unintuitive for them that when in the Security Solution view, they can not open the AI assistant when in the Discover page. > Users couldn’t add the AI Assistant to the Discover flyout and hadn’t been able to resolve it for weeks Changes: - Add "Security" as an option in the `aiAssistant:preferredAIAssistantType` setting (this is the setting used in stateful Kibana to determine assistant visibility). - Modify setting labels and options inside of serverless oblt and serverless security to be relevant to the project type. ### Screenshots
Stateful The same setting is used across all solution views in stateful Kibana. This is not a perfect solution, as when a user is in the Security solution, they can technically still select the Oblt assistant (in this case no assistant will be shown). In the next minor release, this will be fixed with a new AI assistant settings page. ### Classic image ### Oblt image ### Security image image ### ES image
Serverless Security ![image](https://github.com/user-attachments/assets/45111617-ddcc-4b77-93f5-47131175c530) ![image](https://github.com/user-attachments/assets/85dcc3dd-30d2-4c3f-b624-f57032f532ae)
Serverless Oblt image
Serverless ES In serverless ES there is no ability to change the visibility of the AI assistant. This is the current behaviour and this has not changed in this PR. ![image](https://github.com/user-attachments/assets/6f79a349-9530-4278-821d-38240c001b2a)
### How to test: #### Stateful - Start Kibana (in classic/traditional flavour) ``` // Start ES yarn es snapshot --license trial -E xpack.security.authc.api_key.enabled=true // Start Kibana yarn start --no-base-path ``` - Create spaces for Classic, Security, Oblt and Search - In Classic, go to Stack Management > Advanced Settings and search for AI Assistant visibility - Verify that when selecting: - Only in their solutions -> only displays the Search/Oblt and Security Assistants in their respective solutions. - Observability and Search AI Assistants in other apps -> Shows the Oblt and Search assistant on the Discover page. - Security AI Assistants in other apps -> Shows the Security assistant on the Discover page. - Hide all assistants -> Does not show assistants anywhere #### Stateless - Start Kibana (in serverless) ``` // Start ES node scripts/es serverless --projectType security // Start Kibana node --no-experimental-require-module scripts/kibana --dev --serverless=security --no-base-path ``` (please try try different project types too. Instead of security you can use `oblt` or `es` ) - Go to stack management > advanced settings and search for AI Assistant visibility - Check that the options you have available look like this: ![image](https://github.com/user-attachments/assets/85dcc3dd-30d2-4c3f-b624-f57032f532ae) - Verify that when you select security, the Security assistant appears everywhere (including Discover) ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [X] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/src/platform/packages/shared/kbn-i18n/README.md) - [X] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [X] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [X] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [X] This was checked for breaking HTTP API changes, and any breaking changes have been approved by the breaking-change committee. The `release_note:breaking` label should be applied in these situations. - [X] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed - [X] The PR description includes the appropriate Release Notes section, and the correct `release_note:*` label is applied per the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) - [X] Review the [backport guidelines](https://docs.google.com/document/d/1VyN5k91e5OVumlc0Gb9RPa3h1ewuPE705nRtioPiTvY/edit?usp=sharing) and apply applicable `backport:*` labels. ### Identify risks Does this PR introduce any risks? For example, consider risks like hard to test bugs, performance regression, potential of data loss. Describe the risk, its severity, and mitigation for each identified risk. Invite stakeholders and evaluate how to proceed before merging. - [ ] [See some risk examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) - [ ] ... --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine Co-authored-by: Ievgen Sorokopud Co-authored-by: Dario Gieselaar (cherry picked from commit 6536c766d7470b1f754803a303fdca9a068e2b7b) # Conflicts: # src/platform/plugins/shared/ai_assistant_management/selection/server/plugin.ts --- config/serverless.security.yml | 3 + .../settings/security_project/index.ts | 1 + .../selection/common/ai_assistant_type.ts | 1 + .../selection/kibana.jsonc | 5 +- .../selection/public/plugin.test.ts | 46 ++++ .../selection/server/config.ts | 1 + .../selection/server/plugin.test.ts | 189 ++++++++++++++ .../selection/server/plugin.ts | 91 ++++--- .../server/src/settings/classic_setting.ts | 56 +++++ .../src/settings/observability_setting.ts | 42 ++++ .../server/src/settings/security_setting.ts | 42 ++++ .../server/src/settings/translations.ts | 49 ++++ .../selection/server/types.ts | 2 + .../selection/tsconfig.json | 4 +- .../test_suites/core_plugins/rendering.ts | 2 +- .../hooks/is_nav_control_visible.test.tsx | 237 ++++++++++++++++++ .../public/hooks/is_nav_control_visible.tsx | 6 +- .../assistant_context/assistant_provider.tsx | 3 +- .../use_assistant_availability.test.ts | 64 +++++ .../use_assistant_availability.ts | 4 +- .../use_is_nav_control_visible.test.ts | 118 +++++++++ .../use_is_nav_control_visible.ts | 12 +- ...migrate_conversation_from_local_storage.ts | 5 + .../public/assistant/provider.tsx | 10 +- .../use_assistant_availability/index.tsx | 35 +-- .../rule_status_failed_callout.test.tsx | 21 +- .../rules_table/rules_table_toolbar.tsx | 2 +- .../rule_upgrade_test_providers.tsx | 9 +- .../right/hooks/use_assistant.test.tsx | 16 ++ .../right/hooks/use_assistant.ts | 2 +- 30 files changed, 962 insertions(+), 116 deletions(-) create mode 100644 src/platform/plugins/shared/ai_assistant_management/selection/public/plugin.test.ts create mode 100644 src/platform/plugins/shared/ai_assistant_management/selection/server/plugin.test.ts create mode 100644 src/platform/plugins/shared/ai_assistant_management/selection/server/src/settings/classic_setting.ts create mode 100644 src/platform/plugins/shared/ai_assistant_management/selection/server/src/settings/observability_setting.ts create mode 100644 src/platform/plugins/shared/ai_assistant_management/selection/server/src/settings/security_setting.ts create mode 100644 src/platform/plugins/shared/ai_assistant_management/selection/server/src/settings/translations.ts create mode 100644 x-pack/solutions/observability/plugins/observability_ai_assistant_app/public/hooks/is_nav_control_visible.test.tsx create mode 100644 x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/is_nav_control_visible/use_is_nav_control_visible.test.ts diff --git a/config/serverless.security.yml b/config/serverless.security.yml index 3e3a1eb782a33..a607ace5d629d 100644 --- a/config/serverless.security.yml +++ b/config/serverless.security.yml @@ -211,3 +211,6 @@ xpack.alerting.rules.run.ruleTypeOverrides: # These features are disabled in Serverless until fully tested xpack.securitySolution.enableExperimental: - privilegedUserMonitoringDisabled + +# AI Assistant config +aiAssistantManagementSelection.preferredAIAssistantType: 'security' \ No newline at end of file diff --git a/src/platform/packages/shared/serverless/settings/security_project/index.ts b/src/platform/packages/shared/serverless/settings/security_project/index.ts index 6dacb7940a3aa..2ccbd97dac10e 100644 --- a/src/platform/packages/shared/serverless/settings/security_project/index.ts +++ b/src/platform/packages/shared/serverless/settings/security_project/index.ts @@ -26,4 +26,5 @@ export const SECURITY_PROJECT_SETTINGS = [ settings.SECURITY_SOLUTION_ENABLE_GRAPH_VISUALIZATION_SETTING, settings.SECURITY_SOLUTION_ENABLE_ASSET_INVENTORY_SETTING, settings.SECURITY_SOLUTION_ENABLE_CLOUD_CONNECTOR_SETTING, + settings.AI_ASSISTANT_PREFERRED_AI_ASSISTANT_TYPE, ]; diff --git a/src/platform/plugins/shared/ai_assistant_management/selection/common/ai_assistant_type.ts b/src/platform/plugins/shared/ai_assistant_management/selection/common/ai_assistant_type.ts index 7f50f99277a75..d0947535299bb 100644 --- a/src/platform/plugins/shared/ai_assistant_management/selection/common/ai_assistant_type.ts +++ b/src/platform/plugins/shared/ai_assistant_management/selection/common/ai_assistant_type.ts @@ -9,6 +9,7 @@ export enum AIAssistantType { Observability = 'observability', + Security = 'security', Default = 'default', Never = 'never', } diff --git a/src/platform/plugins/shared/ai_assistant_management/selection/kibana.jsonc b/src/platform/plugins/shared/ai_assistant_management/selection/kibana.jsonc index d1509c7d34262..5508eb3d5e1c3 100644 --- a/src/platform/plugins/shared/ai_assistant_management/selection/kibana.jsonc +++ b/src/platform/plugins/shared/ai_assistant_management/selection/kibana.jsonc @@ -13,12 +13,13 @@ "aiAssistantManagementSelection" ], "requiredPlugins": [ - "management" + "management", ], "optionalPlugins": [ "home", "serverless", - "features" + "features", + "cloud" ], "requiredBundles": [ "kibanaReact" diff --git a/src/platform/plugins/shared/ai_assistant_management/selection/public/plugin.test.ts b/src/platform/plugins/shared/ai_assistant_management/selection/public/plugin.test.ts new file mode 100644 index 0000000000000..c288feb8cbde0 --- /dev/null +++ b/src/platform/plugins/shared/ai_assistant_management/selection/public/plugin.test.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { CoreStart, PluginInitializerContext } from '@kbn/core/public'; +import { AIAssistantManagementPlugin } from './plugin'; +import { AIAssistantType } from '../common/ai_assistant_type'; +import { PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY } from '../common/ui_setting_keys'; + +describe('AI Assistant Management Selection Plugin', () => { + it('uses the correct setting key to get the correct value from uiSettings', async () => { + const plugin = new AIAssistantManagementPlugin({ + config: { + get: jest.fn(), + }, + env: { packageInfo: { buildFlavor: 'traditional', branch: 'main' } }, + } as unknown as PluginInitializerContext); + + const coreStart = { + uiSettings: { + get: jest.fn((key: string) => { + if (key === PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY) { + return AIAssistantType.Default; + } + }), + }, + } as unknown as CoreStart; + + const result = plugin.start(coreStart); + + const collected: any[] = []; + const subscription = result.aiAssistantType$.subscribe((value) => { + collected.push(value); + }); + subscription.unsubscribe(); + + const allCalls = (coreStart.uiSettings.get as jest.Mock).mock.calls; + expect(allCalls).toEqual([['aiAssistant:preferredAIAssistantType']]); + expect(collected).toEqual([AIAssistantType.Default]); + }); +}); diff --git a/src/platform/plugins/shared/ai_assistant_management/selection/server/config.ts b/src/platform/plugins/shared/ai_assistant_management/selection/server/config.ts index c8aaee1a80626..9e6b64c2492c2 100644 --- a/src/platform/plugins/shared/ai_assistant_management/selection/server/config.ts +++ b/src/platform/plugins/shared/ai_assistant_management/selection/server/config.ts @@ -18,6 +18,7 @@ const configSchema = schema.object({ schema.literal(AIAssistantType.Default), schema.literal(AIAssistantType.Never), schema.literal(AIAssistantType.Observability), + schema.literal(AIAssistantType.Security), ], { defaultValue: AIAssistantType.Default } ), diff --git a/src/platform/plugins/shared/ai_assistant_management/selection/server/plugin.test.ts b/src/platform/plugins/shared/ai_assistant_management/selection/server/plugin.test.ts new file mode 100644 index 0000000000000..5d00c575cb7e9 --- /dev/null +++ b/src/platform/plugins/shared/ai_assistant_management/selection/server/plugin.test.ts @@ -0,0 +1,189 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { PluginInitializerContext, CoreSetup } from '@kbn/core/server'; +import type { AIAssistantManagementSelectionPluginServerDependenciesSetup } from './types'; +import { AIAssistantType } from '../common/ai_assistant_type'; +import { PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY } from '../common/ui_setting_keys'; +import { classicSetting } from './src/settings/classic_setting'; +import { observabilitySolutionSetting } from './src/settings/observability_setting'; +import { securitySolutionSetting } from './src/settings/security_setting'; +import { AIAssistantManagementSelectionPlugin } from './plugin'; + +describe('plugin', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + describe('stateless', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + const initializerContext = { + env: { + packageInfo: { + buildFlavor: 'serverless', + }, + }, + config: { + get: jest.fn(), + }, + } as unknown as PluginInitializerContext; + + const coreSetup = { + uiSettings: { + register: jest.fn(), + }, + capabilities: { + registerProvider: jest.fn(), + }, + } as unknown as CoreSetup; + + const setupDeps = { + management: { + sections: { + getSection: jest.fn(), + }, + }, + serverless: { + uiSettings: { + register: jest.fn(), + }, + }, + }; + + it('registers correct uiSettings for serverless oblt', () => { + (initializerContext.config.get as jest.Mock).mockReturnValue({ + preferredAIAssistantType: AIAssistantType.Observability, + }); + const aiAssistantManagementSelectionPlugin = new AIAssistantManagementSelectionPlugin( + initializerContext + ); + aiAssistantManagementSelectionPlugin.setup(coreSetup, { + ...setupDeps, + cloud: { + serverless: { + projectType: 'observability', + }, + }, + } as unknown as AIAssistantManagementSelectionPluginServerDependenciesSetup); + + expect(coreSetup.uiSettings.register).toHaveBeenCalledTimes(1); + + expect(coreSetup.uiSettings.register).toHaveBeenCalledWith({ + [PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY]: { + ...observabilitySolutionSetting, + value: AIAssistantType.Observability, + }, + }); + }); + + it('registers correct uiSettings for serverless security', () => { + (initializerContext.config.get as jest.Mock).mockReturnValue({ + preferredAIAssistantType: AIAssistantType.Security, + }); + const aiAssistantManagementSelectionPlugin = new AIAssistantManagementSelectionPlugin( + initializerContext + ); + aiAssistantManagementSelectionPlugin.setup(coreSetup, { + ...setupDeps, + cloud: { + serverless: { + projectType: 'security', + }, + }, + } as unknown as AIAssistantManagementSelectionPluginServerDependenciesSetup); + + expect(coreSetup.uiSettings.register).toHaveBeenCalledTimes(1); + + expect(coreSetup.uiSettings.register).toHaveBeenCalledWith({ + [PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY]: { + ...securitySolutionSetting, + value: AIAssistantType.Security, + }, + }); + }); + + it('registers correct uiSettings for serverless search', () => { + (initializerContext.config.get as jest.Mock).mockReturnValue({ + preferredAIAssistantType: undefined, + }); + const aiAssistantManagementSelectionPlugin = new AIAssistantManagementSelectionPlugin( + initializerContext + ); + aiAssistantManagementSelectionPlugin.setup(coreSetup, { + ...setupDeps, + cloud: { + serverless: { + projectType: 'search', + }, + }, + } as unknown as AIAssistantManagementSelectionPluginServerDependenciesSetup); + + expect(coreSetup.uiSettings.register).toHaveBeenCalledTimes(1); + expect(coreSetup.uiSettings.register).toHaveBeenCalledWith({ + [PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY]: { + ...classicSetting, + value: AIAssistantType.Default, + }, + }); + }); + }); + + describe('stateful', () => { + it('uses the correct setting key to get the correct value from uiSettings', async () => { + const initializerContext = { + env: { + packageInfo: { + buildFlavor: 'classic', + }, + }, + config: { + get: jest.fn().mockReturnValue({ + preferredAIAssistantType: AIAssistantType.Observability, + }), + }, + } as unknown as PluginInitializerContext; + const aiAssistantManagementSelectionPlugin = new AIAssistantManagementSelectionPlugin( + initializerContext + ); + + const coreSetup = { + uiSettings: { + register: jest.fn(), + }, + capabilities: { + registerProvider: jest.fn(), + }, + } as unknown as CoreSetup; + + const setupDeps = { + management: { + sections: { + getSection: jest.fn(), + }, + }, + serverless: { + uiSettings: { + register: jest.fn(), + }, + }, + } as unknown as AIAssistantManagementSelectionPluginServerDependenciesSetup; + + aiAssistantManagementSelectionPlugin.setup(coreSetup, setupDeps); + + expect(coreSetup.uiSettings.register).toHaveBeenCalledTimes(1); + expect(coreSetup.uiSettings.register).toHaveBeenCalledWith({ + [PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY]: { + ...classicSetting, + value: AIAssistantType.Observability, + }, + }); + }); + }); +}); diff --git a/src/platform/plugins/shared/ai_assistant_management/selection/server/plugin.ts b/src/platform/plugins/shared/ai_assistant_management/selection/server/plugin.ts index 67a4e000ed78d..742584ceebafe 100644 --- a/src/platform/plugins/shared/ai_assistant_management/selection/server/plugin.ts +++ b/src/platform/plugins/shared/ai_assistant_management/selection/server/plugin.ts @@ -16,7 +16,6 @@ import { Plugin, DEFAULT_APP_CATEGORIES, } from '@kbn/core/server'; -import { schema } from '@kbn/config-schema'; import { KibanaFeatureScope } from '@kbn/features-plugin/common'; import type { AIAssistantManagementSelectionConfig } from './config'; import type { @@ -25,8 +24,11 @@ import type { AIAssistantManagementSelectionPluginServerSetup, AIAssistantManagementSelectionPluginServerStart, } from './types'; -import { AIAssistantType } from '../common/ai_assistant_type'; import { PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY } from '../common/ui_setting_keys'; +import { classicSetting } from './src/settings/classic_setting'; +import { observabilitySolutionSetting } from './src/settings/observability_setting'; +import { securitySolutionSetting } from './src/settings/security_setting'; +import { AIAssistantType } from '../common/ai_assistant_type'; export class AIAssistantManagementSelectionPlugin implements @@ -47,52 +49,6 @@ export class AIAssistantManagementSelectionPlugin core: CoreSetup, plugins: AIAssistantManagementSelectionPluginServerDependenciesSetup ) { - core.uiSettings.register({ - [PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY]: { - name: i18n.translate('aiAssistantManagementSelection.preferredAIAssistantTypeSettingName', { - defaultMessage: 'AI Assistant for Observability and Search visibility', - }), - category: [DEFAULT_APP_CATEGORIES.observability.id], - value: this.config.preferredAIAssistantType, - description: i18n.translate( - 'aiAssistantManagementSelection.preferredAIAssistantTypeSettingDescription', - { - defaultMessage: - '[technical preview] Whether to show the AI Assistant menu item in Observability and Search, everywhere, or nowhere.', - values: { - em: (chunks) => `${chunks}`, - }, - } - ), - schema: schema.oneOf( - [ - schema.literal(AIAssistantType.Default), - schema.literal(AIAssistantType.Observability), - schema.literal(AIAssistantType.Never), - ], - { defaultValue: this.config.preferredAIAssistantType } - ), - options: [AIAssistantType.Default, AIAssistantType.Observability, AIAssistantType.Never], - type: 'select', - optionLabels: { - [AIAssistantType.Default]: i18n.translate( - 'aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueDefault', - { defaultMessage: 'Observability and Search only (default)' } - ), - [AIAssistantType.Observability]: i18n.translate( - 'aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueObservability', - { defaultMessage: 'Everywhere' } - ), - [AIAssistantType.Never]: i18n.translate( - 'aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueNever', - { defaultMessage: 'Nowhere' } - ), - }, - requiresPageReload: true, - solution: 'oblt', - }, - }); - core.capabilities.registerProvider(() => { return { management: { @@ -154,9 +110,48 @@ export class AIAssistantManagementSelectionPlugin }, }); + this.registerUiSettings(core, plugins); + return {}; } + private registerUiSettings( + core: CoreSetup, + plugins: AIAssistantManagementSelectionPluginServerDependenciesSetup + ) { + const { cloud } = plugins; + const serverlessProjectType = cloud?.serverless.projectType; + + switch (serverlessProjectType) { + case 'observability': + core.uiSettings.register({ + [PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY]: { + ...observabilitySolutionSetting, + value: this.config.preferredAIAssistantType, + }, + }); + return; + case 'security': + core.uiSettings.register({ + [PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY]: { + ...securitySolutionSetting, + value: this.config.preferredAIAssistantType, + }, + }); + return; + // TODO: Add another case for search with the correct copy of the setting. + // see: https://github.com/elastic/kibana/issues/227695 + default: + // This case is hit when in stateful Kibana + return core.uiSettings.register({ + [PREFERRED_AI_ASSISTANT_TYPE_SETTING_KEY]: { + ...classicSetting, + value: this.config.preferredAIAssistantType ?? AIAssistantType.Default, + }, + }); + } + } + public start(core: CoreStart) { return {}; } diff --git a/src/platform/plugins/shared/ai_assistant_management/selection/server/src/settings/classic_setting.ts b/src/platform/plugins/shared/ai_assistant_management/selection/server/src/settings/classic_setting.ts new file mode 100644 index 0000000000000..bf5f9105102b0 --- /dev/null +++ b/src/platform/plugins/shared/ai_assistant_management/selection/server/src/settings/classic_setting.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { i18n } from '@kbn/i18n'; + +import { schema } from '@kbn/config-schema'; +import { UiSettingsParams } from '@kbn/core-ui-settings-common'; +import { AIAssistantType } from '../../../common/ai_assistant_type'; +import { + ONLY_IN_THEIR_SOLUTIONS, + OBSERVABILITY_IN_OTHER_APPS, + SECURITY_IN_OTHER_APPS, + HIDE_ALL_ASSISTANTS, + TITLE, +} from './translations'; + +// Define the classicSetting with proper typing +export const classicSetting: Omit, 'value'> = { + name: TITLE, + description: i18n.translate( + 'aiAssistantManagementSelection.preferredAIAssistantTypeSettingDescription', + { + defaultMessage: + 'Choose where and which AI Assistants are available. You can limit the AI Assistants to their own solutions, show either the Observability and Search AI Assistants or the Security AI Assistant in other Kibana apps, or hide AI Assistants entirely.', + } + ), + schema: schema.oneOf( + [ + schema.literal(AIAssistantType.Default), + schema.literal(AIAssistantType.Observability), + schema.literal(AIAssistantType.Security), + schema.literal(AIAssistantType.Never), + ], + { defaultValue: AIAssistantType.Default } + ), + options: [ + AIAssistantType.Default, + AIAssistantType.Observability, + AIAssistantType.Security, + AIAssistantType.Never, + ], + type: 'select' as const, + optionLabels: { + [AIAssistantType.Default]: ONLY_IN_THEIR_SOLUTIONS, + [AIAssistantType.Observability]: OBSERVABILITY_IN_OTHER_APPS, + [AIAssistantType.Security]: SECURITY_IN_OTHER_APPS, + [AIAssistantType.Never]: HIDE_ALL_ASSISTANTS, + }, + requiresPageReload: true, +}; diff --git a/src/platform/plugins/shared/ai_assistant_management/selection/server/src/settings/observability_setting.ts b/src/platform/plugins/shared/ai_assistant_management/selection/server/src/settings/observability_setting.ts new file mode 100644 index 0000000000000..caecf5d3099a5 --- /dev/null +++ b/src/platform/plugins/shared/ai_assistant_management/selection/server/src/settings/observability_setting.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { i18n } from '@kbn/i18n'; + +import { schema } from '@kbn/config-schema'; +import { UiSettingsParams } from '@kbn/core-ui-settings-common'; +import { AIAssistantType } from '../../../common/ai_assistant_type'; +import { SHOW_OBSERVABILITY, HIDE_ASSISTANT, TITLE } from './translations'; + +// Define the classicSetting with proper typing +export const observabilitySolutionSetting: Omit< + UiSettingsParams, + 'value' +> = { + name: TITLE, + description: i18n.translate( + 'aiAssistantManagementSelection.observabilitySolutionSetting.preferredAIAssistantTypeSettingDescription', + { + defaultMessage: + 'Choose if the Observability AI Assistant is available. Show the Observability AI Assistant, or hide the Assistant entirely.', + } + ), + schema: schema.oneOf( + [schema.literal(AIAssistantType.Observability), schema.literal(AIAssistantType.Never)], + { defaultValue: AIAssistantType.Observability } + ), + options: [AIAssistantType.Observability, AIAssistantType.Never], + type: 'select' as const, + optionLabels: { + [AIAssistantType.Observability]: SHOW_OBSERVABILITY, + [AIAssistantType.Never]: HIDE_ASSISTANT, + }, + requiresPageReload: true, + solution: 'oblt', +}; diff --git a/src/platform/plugins/shared/ai_assistant_management/selection/server/src/settings/security_setting.ts b/src/platform/plugins/shared/ai_assistant_management/selection/server/src/settings/security_setting.ts new file mode 100644 index 0000000000000..03b4553b02860 --- /dev/null +++ b/src/platform/plugins/shared/ai_assistant_management/selection/server/src/settings/security_setting.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { i18n } from '@kbn/i18n'; + +import { schema } from '@kbn/config-schema'; +import { UiSettingsParams } from '@kbn/core-ui-settings-common'; +import { AIAssistantType } from '../../../common/ai_assistant_type'; +import { SHOW_SECURITY, HIDE_ASSISTANT, TITLE } from './translations'; + +// Define the securitySolutionSetting with proper typing +export const securitySolutionSetting: Omit< + UiSettingsParams, + 'value' +> = { + name: TITLE, + description: i18n.translate( + 'aiAssistantManagementSelection.securitySolutionSetting.preferredAIAssistantTypeSettingDescription', + { + defaultMessage: + 'Choose if the Security AI Assistant is available. Show the Security AI Assistant, or hide the Assistant entirely.', + } + ), + schema: schema.oneOf( + [schema.literal(AIAssistantType.Security), schema.literal(AIAssistantType.Never)], + { defaultValue: AIAssistantType.Security } + ), + options: [AIAssistantType.Security, AIAssistantType.Never], + type: 'select' as const, + optionLabels: { + [AIAssistantType.Security]: SHOW_SECURITY, + [AIAssistantType.Never]: HIDE_ASSISTANT, + }, + requiresPageReload: true, + solution: 'security', +}; diff --git a/src/platform/plugins/shared/ai_assistant_management/selection/server/src/settings/translations.ts b/src/platform/plugins/shared/ai_assistant_management/selection/server/src/settings/translations.ts new file mode 100644 index 0000000000000..26634464ea1e4 --- /dev/null +++ b/src/platform/plugins/shared/ai_assistant_management/selection/server/src/settings/translations.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { i18n } from '@kbn/i18n'; + +export const TITLE = i18n.translate( + 'aiAssistantManagementSelection.preferredAIAssistantTypeSettingName', + { + defaultMessage: 'AI Assistant visibility', + } +); + +export const ONLY_IN_THEIR_SOLUTIONS = i18n.translate( + 'aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueDefault', + { defaultMessage: 'Only in their solutions' } +); +export const OBSERVABILITY_IN_OTHER_APPS = i18n.translate( + 'aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueObservability', + { defaultMessage: 'Observability and Search AI Assistants in other apps' } +); +export const SECURITY_IN_OTHER_APPS = i18n.translate( + 'aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueSecurity', + { defaultMessage: 'Security AI Assistant in other apps' } +); +export const HIDE_ALL_ASSISTANTS = i18n.translate( + 'aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueNever', + { defaultMessage: 'Hide all assistants' } +); + +export const SHOW_SECURITY = i18n.translate( + 'aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueNever', + { defaultMessage: 'Show Security AI Assistant' } +); + +export const SHOW_OBSERVABILITY = i18n.translate( + 'aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueNever', + { defaultMessage: 'Show Observability AI Assistant' } +); + +export const HIDE_ASSISTANT = i18n.translate( + 'aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueNever', + { defaultMessage: 'Hide AI Assistant' } +); diff --git a/src/platform/plugins/shared/ai_assistant_management/selection/server/types.ts b/src/platform/plugins/shared/ai_assistant_management/selection/server/types.ts index 4667ad9ab4fbc..d581b8be26540 100644 --- a/src/platform/plugins/shared/ai_assistant_management/selection/server/types.ts +++ b/src/platform/plugins/shared/ai_assistant_management/selection/server/types.ts @@ -7,6 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import { CloudSetup } from '@kbn/cloud-plugin/server'; import type { FeaturesPluginSetup } from '@kbn/features-plugin/server'; // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -14,6 +15,7 @@ export interface AIAssistantManagementSelectionPluginServerDependenciesStart {} export interface AIAssistantManagementSelectionPluginServerDependenciesSetup { features?: FeaturesPluginSetup; + cloud?: CloudSetup; } // eslint-disable-next-line @typescript-eslint/no-empty-interface diff --git a/src/platform/plugins/shared/ai_assistant_management/selection/tsconfig.json b/src/platform/plugins/shared/ai_assistant_management/selection/tsconfig.json index 0fc6cec454817..7d7827387699d 100644 --- a/src/platform/plugins/shared/ai_assistant_management/selection/tsconfig.json +++ b/src/platform/plugins/shared/ai_assistant_management/selection/tsconfig.json @@ -18,7 +18,9 @@ "@kbn/core-plugins-server", "@kbn/features-plugin", "@kbn/config", - "@kbn/doc-links" + "@kbn/doc-links", + "@kbn/core-ui-settings-common", + "@kbn/cloud-plugin" ], "exclude": ["target/**/*"] } diff --git a/src/platform/test/plugin_functional/test_suites/core_plugins/rendering.ts b/src/platform/test/plugin_functional/test_suites/core_plugins/rendering.ts index 746b7f7e8c110..272a87501f8cb 100644 --- a/src/platform/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/src/platform/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -380,7 +380,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) { 'xpack.observabilityAiAssistantManagement.spacesEnabled (boolean?)', 'xpack.observabilityAiAssistantManagement.visibilityEnabled (boolean?)', 'share.new_version.enabled (boolean?)', - 'aiAssistantManagementSelection.preferredAIAssistantType (default?|never?|observability?)', + 'aiAssistantManagementSelection.preferredAIAssistantType (default?|never?|observability?|security?)', /** * Rule form V2 feature flags */ diff --git a/x-pack/solutions/observability/plugins/observability_ai_assistant_app/public/hooks/is_nav_control_visible.test.tsx b/x-pack/solutions/observability/plugins/observability_ai_assistant_app/public/hooks/is_nav_control_visible.test.tsx new file mode 100644 index 0000000000000..fb4a5e1a7b685 --- /dev/null +++ b/x-pack/solutions/observability/plugins/observability_ai_assistant_app/public/hooks/is_nav_control_visible.test.tsx @@ -0,0 +1,237 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react'; +import { useIsNavControlVisible } from './is_nav_control_visible'; +import { CoreStart } from '@kbn/core/public'; +import { ObservabilityAIAssistantAppPluginStartDependencies } from '../types'; +import { of } from 'rxjs'; +import { AIAssistantType } from '@kbn/ai-assistant-management-plugin/public'; + +describe('isNavControlVisible', () => { + describe('with solution:oblt', () => { + it('returns true when the current app is discover and the ai assistant type is observability', () => { + const coreStart = { + application: { + currentAppId$: of('discover'), + applications$: of( + new Map([['discover', { id: 'discover', category: { id: 'kibana' } }]]) + ), + }, + } as unknown as CoreStart; + + const pluginsStart = { + aiAssistantManagementSelection: { + aiAssistantType$: of(AIAssistantType.Observability), + }, + spaces: { + getActiveSpace$: () => of({ solution: 'oblt' }), + }, + } as unknown as ObservabilityAIAssistantAppPluginStartDependencies; + + const { result } = renderHook(() => useIsNavControlVisible({ coreStart, pluginsStart })); + + expect(result.current.isVisible).toBe(true); + }); + + it('returns true when the current app is discover and the ai assistant type is default', () => { + const coreStart = { + application: { + currentAppId$: of('discover'), + applications$: of( + new Map([['discover', { id: 'discover', category: { id: 'kibana' } }]]) + ), + }, + } as unknown as CoreStart; + + const pluginsStart = { + aiAssistantManagementSelection: { + aiAssistantType$: of(AIAssistantType.Default), + }, + spaces: { + getActiveSpace$: () => of({ solution: 'oblt' }), + }, + } as unknown as ObservabilityAIAssistantAppPluginStartDependencies; + + const { result } = renderHook(() => useIsNavControlVisible({ coreStart, pluginsStart })); + + expect(result.current.isVisible).toBe(true); + }); + + it('returns true when the current app is observability and the ai assistant type is default', () => { + const coreStart = { + application: { + currentAppId$: of('observability'), + applications$: of( + new Map([['observability', { id: 'observability', category: { id: 'observability' } }]]) + ), + }, + } as unknown as CoreStart; + + const pluginsStart = { + aiAssistantManagementSelection: { + aiAssistantType$: of(AIAssistantType.Default), + }, + spaces: { + getActiveSpace$: () => of({ solution: 'oblt' }), + }, + } as unknown as ObservabilityAIAssistantAppPluginStartDependencies; + + const { result } = renderHook(() => useIsNavControlVisible({ coreStart, pluginsStart })); + + expect(result.current.isVisible).toBe(true); + }); + + it('returns true when the current app is search and the ai assistant type is default', () => { + const coreStart = { + application: { + currentAppId$: of('search'), + applications$: of( + new Map([['search', { id: 'search', category: { id: 'enterpriseSearch' } }]]) + ), + }, + } as unknown as CoreStart; + + const pluginsStart = { + aiAssistantManagementSelection: { + aiAssistantType$: of(AIAssistantType.Default), + }, + spaces: { + getActiveSpace$: () => of({ solution: 'oblt' }), + }, + } as unknown as ObservabilityAIAssistantAppPluginStartDependencies; + + const { result } = renderHook(() => useIsNavControlVisible({ coreStart, pluginsStart })); + + expect(result.current.isVisible).toBe(true); + }); + + it('returns false when the current app is security and the ai assistant type is observability', () => { + const coreStart = { + application: { + currentAppId$: of('security'), + applications$: of( + new Map([['security', { id: 'security', category: { id: 'securitySolution' } }]]) + ), + }, + } as unknown as CoreStart; + + const pluginsStart = { + aiAssistantManagementSelection: { + aiAssistantType$: of(AIAssistantType.Observability), + }, + spaces: { + getActiveSpace$: () => of({ solution: 'oblt' }), + }, + } as unknown as ObservabilityAIAssistantAppPluginStartDependencies; + + const { result } = renderHook(() => useIsNavControlVisible({ coreStart, pluginsStart })); + + expect(result.current.isVisible).toBe(false); + }); + + it('returns false when the ai assistant type is never', () => { + const coreStart = { + application: { + currentAppId$: of('observability'), + applications$: of( + new Map([['observability', { id: 'observability', category: { id: 'observability' } }]]) + ), + }, + } as unknown as CoreStart; + + const pluginsStart = { + aiAssistantManagementSelection: { + aiAssistantType$: of(AIAssistantType.Never), + }, + spaces: { + getActiveSpace$: () => of({ solution: 'oblt' }), + }, + } as unknown as ObservabilityAIAssistantAppPluginStartDependencies; + + const { result } = renderHook(() => useIsNavControlVisible({ coreStart, pluginsStart })); + + expect(result.current.isVisible).toBe(false); + }); + }); + + describe('with solution:es', () => { + it('returns true when the current space is es and the ai assistant type is observability', () => { + const coreStart = { + application: { + currentAppId$: of('discover'), + applications$: of( + new Map([['discover', { id: 'discover', category: { id: 'kibana' } }]]) + ), + }, + } as unknown as CoreStart; + + const pluginsStart = { + aiAssistantManagementSelection: { + aiAssistantType$: of(AIAssistantType.Observability), + }, + spaces: { + getActiveSpace$: () => of({ solution: 'es' }), + }, + } as unknown as ObservabilityAIAssistantAppPluginStartDependencies; + + const { result } = renderHook(() => useIsNavControlVisible({ coreStart, pluginsStart })); + + expect(result.current.isVisible).toBe(true); + }); + + it('returns false when the current space is es and the ai assistant type is never', () => { + const coreStart = { + application: { + currentAppId$: of('discover'), + applications$: of( + new Map([['discover', { id: 'discover', category: { id: 'kibana' } }]]) + ), + }, + } as unknown as CoreStart; + + const pluginsStart = { + aiAssistantManagementSelection: { + aiAssistantType$: of(AIAssistantType.Never), + }, + spaces: { + getActiveSpace$: () => of({ solution: 'es' }), + }, + } as unknown as ObservabilityAIAssistantAppPluginStartDependencies; + + const { result } = renderHook(() => useIsNavControlVisible({ coreStart, pluginsStart })); + + expect(result.current.isVisible).toBe(false); + }); + }); + + describe('with classic', () => { + it('returns false when the ai assistant type is default', () => { + const coreStart = { + application: { + currentAppId$: of('discover'), + applications$: of( + new Map([['discover', { id: 'discover', category: { id: 'kibana' } }]]) + ), + }, + } as unknown as CoreStart; + + const pluginsStart = { + aiAssistantManagementSelection: { + aiAssistantType$: of(AIAssistantType.Default), + }, + spaces: { + getActiveSpace$: () => of({ solution: 'classic' }), + }, + } as unknown as ObservabilityAIAssistantAppPluginStartDependencies; + + const { result } = renderHook(() => useIsNavControlVisible({ coreStart, pluginsStart })); + + expect(result.current.isVisible).toBe(false); + }); + }); +}); diff --git a/x-pack/solutions/observability/plugins/observability_ai_assistant_app/public/hooks/is_nav_control_visible.tsx b/x-pack/solutions/observability/plugins/observability_ai_assistant_app/public/hooks/is_nav_control_visible.tsx index ce295737315d9..e8fb82fb22ce0 100644 --- a/x-pack/solutions/observability/plugins/observability_ai_assistant_app/public/hooks/is_nav_control_visible.tsx +++ b/x-pack/solutions/observability/plugins/observability_ai_assistant_app/public/hooks/is_nav_control_visible.tsx @@ -30,7 +30,11 @@ function getVisibility( const categoryId = (appId && applications.get(appId)?.category?.id) || DEFAULT_APP_CATEGORIES.kibana.id; - if (preferredAssistantType === AIAssistantType.Observability || space.solution === 'es') { + if ( + preferredAssistantType === AIAssistantType.Observability || + space.solution === 'es' || + space.solution === 'oblt' + ) { return categoryId !== DEFAULT_APP_CATEGORIES.security.id; } diff --git a/x-pack/solutions/security/plugins/elastic_assistant/public/src/context/assistant_context/assistant_provider.tsx b/x-pack/solutions/security/plugins/elastic_assistant/public/src/context/assistant_context/assistant_provider.tsx index 3f9ca479e2915..dabff2d8e0d63 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/public/src/context/assistant_context/assistant_provider.tsx +++ b/x-pack/solutions/security/plugins/elastic_assistant/public/src/context/assistant_context/assistant_provider.tsx @@ -46,6 +46,7 @@ export function AssistantProvider({ children }: { children: React.ReactElement } const inferenceEnabled = useInferenceEnabled(); const basePath = useBasePath(); + const { isVisible } = useIsNavControlVisible(); const assistantAvailability = useAssistantAvailability(); const assistantTelemetry = useAssistantTelemetry(); @@ -113,8 +114,6 @@ export function AssistantProvider({ children }: { children: React.ReactElement } ); }, [assistantContextValue, elasticAssistantSharedState.assistantContextValue]); - const { isVisible } = useIsNavControlVisible(); - if (!isVisible) { return null; } diff --git a/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/assistant_availability/use_assistant_availability.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/assistant_availability/use_assistant_availability.test.ts index a5b22556720f8..0be2ab01e3784 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/assistant_availability/use_assistant_availability.test.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/assistant_availability/use_assistant_availability.test.ts @@ -12,15 +12,24 @@ import { LicenseService } from '../licence/license_service'; import { renderHook } from '@testing-library/react'; import { SECURITY_FEATURE_ID } from '../../../../common/constants'; import { ASSISTANT_FEATURE_ID } from '@kbn/security-solution-features/constants'; +import { useIsNavControlVisible } from '../is_nav_control_visible/use_is_nav_control_visible'; + jest.mock('../licence/use_licence'); jest.mock('../../context/typed_kibana_context/typed_kibana_context'); +jest.mock('../is_nav_control_visible/use_is_nav_control_visible'); const mockUseLicense = useLicense as jest.MockedFunction; const mockUseKibana = useKibana as jest.MockedFunction; +const mockUseIsNavControlVisible = useIsNavControlVisible as jest.MockedFunction< + typeof useIsNavControlVisible +>; describe('useAssistantAvailability', () => { beforeEach(() => { jest.resetAllMocks(); + mockUseIsNavControlVisible.mockReturnValue({ + isVisible: true, + }); }); it('returns correct values when all privileges are available', () => { @@ -48,6 +57,9 @@ describe('useAssistantAvailability', () => { }, }, }, + aiAssistantManagementSelection: { + aiAssistantManagementSelection$: jest.fn(), + }, featureFlags: { getBooleanValue: jest.fn().mockReturnValue(true), }, @@ -68,6 +80,58 @@ describe('useAssistantAvailability', () => { }); }); + it('returns correct values when all privileges are available but assistant his hidden', () => { + mockUseLicense.mockReturnValue({ + isEnterprise: jest.fn().mockReturnValue(true), + } as unknown as LicenseService); + + mockUseIsNavControlVisible.mockReturnValue({ + isVisible: false, + }); + + mockUseKibana.mockReturnValue({ + services: { + application: { + capabilities: { + [ASSISTANT_FEATURE_ID]: { + 'ai-assistant': true, + updateAIAssistantAnonymization: true, + manageGlobalKnowledgeBaseAIAssistant: true, + }, + [SECURITY_FEATURE_ID]: { + configurations: true, + }, + actions: { + show: true, + execute: true, + save: true, + delete: true, + }, + }, + }, + aiAssistantManagementSelection: { + aiAssistantManagementSelection$: jest.fn(), + }, + featureFlags: { + getBooleanValue: jest.fn().mockReturnValue(true), + }, + }, + } as unknown as ReturnType); + + const { result } = renderHook(() => useAssistantAvailability()); + + expect(result.current).toEqual({ + hasSearchAILakeConfigurations: true, + hasAssistantPrivilege: true, + hasConnectorsAllPrivilege: true, + hasConnectorsReadPrivilege: true, + isAssistantEnabled: false, + isStarterPromptsEnabled: true, + hasUpdateAIAssistantAnonymization: true, + hasManageGlobalKnowledgeBase: true, + }); + }); + it('returns correct values when no privileges are available', () => { mockUseLicense.mockReturnValue({ isEnterprise: jest.fn().mockReturnValue(false), diff --git a/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/assistant_availability/use_assistant_availability.ts b/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/assistant_availability/use_assistant_availability.ts index ced9f5101f8a7..f20598af543f1 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/assistant_availability/use_assistant_availability.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/assistant_availability/use_assistant_availability.ts @@ -11,9 +11,11 @@ import { SECURITY_FEATURE_ID } from '../../../../common/constants'; import { useKibana } from '../../context/typed_kibana_context/typed_kibana_context'; import { useLicense } from '../licence/use_licence'; +import { useIsNavControlVisible } from '../is_nav_control_visible/use_is_nav_control_visible'; export const STARTER_PROMPTS_FEATURE_FLAG = 'elasticAssistant.starterPromptsEnabled' as const; export const useAssistantAvailability = (): UseAssistantAvailability => { + const { isVisible } = useIsNavControlVisible(); const isEnterprise = useLicense().isEnterprise(); const { application: { capabilities }, @@ -45,7 +47,7 @@ export const useAssistantAvailability = (): UseAssistantAvailability => { hasConnectorsAllPrivilege, hasConnectorsReadPrivilege, isStarterPromptsEnabled, - isAssistantEnabled: isEnterprise, + isAssistantEnabled: isEnterprise && isVisible, hasUpdateAIAssistantAnonymization, hasManageGlobalKnowledgeBase, }; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/is_nav_control_visible/use_is_nav_control_visible.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/is_nav_control_visible/use_is_nav_control_visible.test.ts new file mode 100644 index 0000000000000..5858d8ac45323 --- /dev/null +++ b/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/is_nav_control_visible/use_is_nav_control_visible.test.ts @@ -0,0 +1,118 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { renderHook } from '@testing-library/react'; +import { useIsNavControlVisible } from './use_is_nav_control_visible'; +import { of } from 'rxjs'; +import { AIAssistantType } from '@kbn/ai-assistant-management-plugin/public'; +import { useKibana } from '../../context/typed_kibana_context/typed_kibana_context'; + +jest.mock('../../context/typed_kibana_context/typed_kibana_context', () => { + return { + useKibana: jest.fn(), + }; +}); + +describe('isNavControlVisible', () => { + it('returns true when the current app is security and the ai assistant type is default', () => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + application: { + currentAppId$: of('security'), + applications$: of( + new Map([ + ['security', { id: 'security', category: { id: 'securitySolution' } }], + ['observability', { id: 'observability', category: { id: 'observability' } }], + ]) + ), + }, + aiAssistantManagementSelection: { + aiAssistantType$: of(AIAssistantType.Default), + }, + }, + }); + + const { result } = renderHook(() => useIsNavControlVisible()); + expect(result.current.isVisible).toEqual(true); + }); + + it('returns false when the current app is observability and the ai assistant type is default', () => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + application: { + currentAppId$: of('observability'), + applications$: of( + new Map([['observability', { id: 'observability', category: { id: 'observability' } }]]) + ), + }, + aiAssistantManagementSelection: { + aiAssistantType$: of(AIAssistantType.Default), + }, + }, + }); + + const { result } = renderHook(() => useIsNavControlVisible()); + expect(result.current.isVisible).toEqual(false); + }); + + it('returns false when the current app is search and the ai assistant type is default', () => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + application: { + currentAppId$: of('search'), + applications$: of( + new Map([['search', { id: 'search', category: { id: 'enterpriseSearch' } }]]) + ), + }, + aiAssistantManagementSelection: { + aiAssistantType$: of(AIAssistantType.Security), + }, + }, + }); + + const { result } = renderHook(() => useIsNavControlVisible()); + expect(result.current.isVisible).toEqual(false); + }); + + it('returns false when the current app is discover and the ai assistant type is security', () => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + application: { + currentAppId$: of('discover'), + applications$: of( + new Map([['discover', { id: 'discover', category: { id: 'kibana' } }]]) + ), + }, + aiAssistantManagementSelection: { + aiAssistantType$: of(AIAssistantType.Security), + }, + }, + }); + + const { result } = renderHook(() => useIsNavControlVisible()); + expect(result.current.isVisible).toEqual(true); + }); + + it('returns false when the current app is discover and the ai assistant type is observability', () => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + application: { + currentAppId$: of('discover'), + applications$: of( + new Map([['discover', { id: 'discover', category: { id: 'kibana' } }]]) + ), + }, + aiAssistantManagementSelection: { + aiAssistantType$: of(AIAssistantType.Observability), + }, + }, + }); + + const { result } = renderHook(() => useIsNavControlVisible()); + expect(result.current.isVisible).toEqual(false); + }); +}); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/is_nav_control_visible/use_is_nav_control_visible.ts b/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/is_nav_control_visible/use_is_nav_control_visible.ts index b21becbefed76..42164fecff600 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/is_nav_control_visible/use_is_nav_control_visible.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/public/src/hooks/is_nav_control_visible/use_is_nav_control_visible.ts @@ -16,12 +16,20 @@ function getVisibility( applications: ReadonlyMap, preferredAssistantType: AIAssistantType ) { - // The "Global assistant" stack management setting for the security assistant still needs to be developed. - // In the meantime, while testing, show the Security assistant everywhere except in Observability. + if (preferredAssistantType === AIAssistantType.Never) { + return false; + } const categoryId = (appId && applications.get(appId)?.category?.id) || DEFAULT_APP_CATEGORIES.kibana.id; + if (preferredAssistantType === AIAssistantType.Security) { + return ( + DEFAULT_APP_CATEGORIES.observability.id !== categoryId && + DEFAULT_APP_CATEGORIES.enterpriseSearch.id !== categoryId + ); + } + return DEFAULT_APP_CATEGORIES.security.id === categoryId; } diff --git a/x-pack/solutions/security/plugins/security_solution/public/assistant/migrate_conversations_from_local_storage/use_migrate_conversation_from_local_storage.ts b/x-pack/solutions/security/plugins/security_solution/public/assistant/migrate_conversations_from_local_storage/use_migrate_conversation_from_local_storage.ts index faaaa3dafe979..2308f6168e671 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/assistant/migrate_conversations_from_local_storage/use_migrate_conversation_from_local_storage.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/assistant/migrate_conversations_from_local_storage/use_migrate_conversation_from_local_storage.ts @@ -43,3 +43,8 @@ export const useMigrateConversationsFromLocalStorage = () => { storage, ]); }; + +export const MigrateConversationsFromLocalStorage = () => { + useMigrateConversationsFromLocalStorage(); + return null; +}; diff --git a/x-pack/solutions/security/plugins/security_solution/public/assistant/provider.tsx b/x-pack/solutions/security/plugins/security_solution/public/assistant/provider.tsx index 7b4414dde23cc..b48fb45852081 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/assistant/provider.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/assistant/provider.tsx @@ -11,13 +11,12 @@ import { AssistantProvider as ElasticAssistantProvider } from '@kbn/elastic-assi import { isEmpty } from 'lodash/fp'; import useObservable from 'react-use/lib/useObservable'; import { useKibana } from '../common/lib/kibana'; -import { useAssistantAvailability } from './use_assistant_availability'; import { licenseService } from '../common/hooks/use_license'; import { useFindPromptContexts } from './content/prompt_contexts/use_find_prompt_contexts'; import { CommentActionsPortal } from './comment_actions/comment_actions_portal'; import { AugmentMessageCodeBlocksPortal } from './use_augment_message_code_blocks/augment_message_code_blocks_portal'; import { useElasticAssistantSharedStateSignalIndex } from './use_elastic_assistant_shared_state_signal_index/use_elastic_assistant_shared_state_signal_index'; -import { useMigrateConversationsFromLocalStorage } from './migrate_conversations_from_local_storage/use_migrate_conversation_from_local_storage'; +import { MigrateConversationsFromLocalStorage } from './migrate_conversations_from_local_storage/use_migrate_conversation_from_local_storage'; /** * This component configures the Elastic AI Assistant context provider for the Security Solution app. @@ -29,17 +28,15 @@ export const AssistantProvider: FC> = ({ children }) elasticAssistantSharedState.assistantContextValue.getAssistantContextValue$() ); - const assistantAvailability = useAssistantAvailability(); const hasEnterpriseLicence = licenseService.isEnterprise(); - useMigrateConversationsFromLocalStorage(); useElasticAssistantSharedStateSignalIndex(); const promptContexts = useFindPromptContexts({ context: { isAssistantEnabled: hasEnterpriseLicence && - assistantAvailability.isAssistantEnabled && - assistantAvailability.hasAssistantPrivilege, + (assistantContextValue?.assistantAvailability.isAssistantEnabled ?? false) && + (assistantContextValue?.assistantAvailability.hasAssistantPrivilege ?? false), httpFetch: http.fetch, toasts: notifications.toasts, }, @@ -66,6 +63,7 @@ export const AssistantProvider: FC> = ({ children }) + {children} ); diff --git a/x-pack/solutions/security/plugins/security_solution/public/assistant/use_assistant_availability/index.tsx b/x-pack/solutions/security/plugins/security_solution/public/assistant/use_assistant_availability/index.tsx index e08aa8579c2ae..00118f18473c6 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/assistant/use_assistant_availability/index.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/assistant/use_assistant_availability/index.tsx @@ -4,10 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import { useLicense } from '../../common/hooks/use_license'; -import { useKibana } from '../../common/lib/kibana'; -import { ASSISTANT_FEATURE_ID, SECURITY_FEATURE_ID } from '../../../common/constants'; +import { useAssistantContext } from '@kbn/elastic-assistant'; export interface UseAssistantAvailability { // True when searchAiLake configurations is available @@ -27,32 +24,6 @@ export interface UseAssistantAvailability { } export const useAssistantAvailability = (): UseAssistantAvailability => { - const isEnterprise = useLicense().isEnterprise(); - const capabilities = useKibana().services.application.capabilities; - const hasAssistantPrivilege = capabilities[ASSISTANT_FEATURE_ID]?.['ai-assistant'] === true; - const hasUpdateAIAssistantAnonymization = - capabilities[ASSISTANT_FEATURE_ID]?.updateAIAssistantAnonymization === true; - const hasManageGlobalKnowledgeBase = - capabilities[ASSISTANT_FEATURE_ID]?.manageGlobalKnowledgeBaseAIAssistant === true; - const hasSearchAILakeConfigurations = capabilities[SECURITY_FEATURE_ID]?.configurations === true; - - // Connectors & Actions capabilities as defined in x-pack/plugins/actions/server/feature.ts - // `READ` ui capabilities defined as: { ui: ['show', 'execute'] } - const hasConnectorsReadPrivilege = - capabilities.actions?.show === true && capabilities.actions?.execute === true; - // `ALL` ui capabilities defined as: { ui: ['show', 'execute', 'save', 'delete'] } - const hasConnectorsAllPrivilege = - hasConnectorsReadPrivilege && - capabilities.actions?.delete === true && - capabilities.actions?.save === true; - - return { - hasSearchAILakeConfigurations, - hasAssistantPrivilege, - hasConnectorsAllPrivilege, - hasConnectorsReadPrivilege, - isAssistantEnabled: isEnterprise, - hasUpdateAIAssistantAnonymization, - hasManageGlobalKnowledgeBase, - }; + const { assistantAvailability } = useAssistantContext(); + return assistantAvailability; }; diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/common/components/rule_execution_status/rule_status_failed_callout.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/common/components/rule_execution_status/rule_status_failed_callout.test.tsx index c57ad6dd4d2cb..3b17180d713b5 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/common/components/rule_execution_status/rule_status_failed_callout.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/common/components/rule_execution_status/rule_status_failed_callout.test.tsx @@ -47,15 +47,6 @@ const ContextWrapper: FC> = ({ children }) => { }; describe('RuleStatusFailedCallOut', () => { - const renderWith = (status: RuleExecutionStatus | null | undefined) => - render( - - ); const renderWithAssistant = (status: RuleExecutionStatus | null | undefined) => render( @@ -64,31 +55,31 @@ describe('RuleStatusFailedCallOut', () => { date={DATE} message={MESSAGE} ruleNameForChat="ruleNameForChat" - />{' '} + /> ); it('is hidden if status is undefined', () => { - const result = renderWith(undefined); + const result = renderWithAssistant(undefined); expect(result.queryByTestId(TEST_ID)).toBe(null); }); it('is hidden if status is null', () => { - const result = renderWith(null); + const result = renderWithAssistant(null); expect(result.queryByTestId(TEST_ID)).toBe(null); }); it('is hidden if status is "going to run"', () => { - const result = renderWith(RuleExecutionStatusEnum['going to run']); + const result = renderWithAssistant(RuleExecutionStatusEnum['going to run']); expect(result.queryByTestId(TEST_ID)).toBe(null); }); it('is hidden if status is "running"', () => { - const result = renderWith(RuleExecutionStatusEnum.running); + const result = renderWithAssistant(RuleExecutionStatusEnum.running); expect(result.queryByTestId(TEST_ID)).toBe(null); }); it('is hidden if status is "succeeded"', () => { - const result = renderWith(RuleExecutionStatusEnum.succeeded); + const result = renderWithAssistant(RuleExecutionStatusEnum.succeeded); expect(result.queryByTestId(TEST_ID)).toBe(null); }); diff --git a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_toolbar.tsx b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_toolbar.tsx index d16ca060a4e0f..0e2fa2c3f1c06 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_toolbar.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/detection_engine/rule_management_ui/components/rules_table/rules_table_toolbar.tsx @@ -103,7 +103,7 @@ export const RulesTableToolbar = React.memo(() => { - {hasAssistantPrivilege && selectedRules.length > 0 && ( + {hasAssistantPrivilege && selectedRules.length > 0 && isAssistantEnabled && ( ): J ({ eui: euiDarkVars, darkMode: true })}> - - {children} - + + + {children} + + diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.test.tsx b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.test.tsx index 66f16c9280923..3180f40aaf6da 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.test.tsx +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.test.tsx @@ -79,6 +79,22 @@ describe('useAssistant', () => { expect(hookResult.result.current.promptContextId).toEqual('123'); }); + it(`should return showAssistant false if isAssistantEnabled is false`, () => { + jest.mocked(useAssistantAvailability).mockReturnValue({ + hasSearchAILakeConfigurations: false, + hasAssistantPrivilege: true, + hasConnectorsAllPrivilege: true, + hasConnectorsReadPrivilege: true, + hasUpdateAIAssistantAnonymization: true, + hasManageGlobalKnowledgeBase: true, + isAssistantEnabled: false, + }); + + hookResult = renderUseAssistant(); + + expect(hookResult.result.current.showAssistant).toEqual(false); + }); + it(`should return showAssistant false if hasAssistantPrivilege is false`, () => { jest.mocked(useAssistantAvailability).mockReturnValue({ hasSearchAILakeConfigurations: false, diff --git a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.ts b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.ts index 7a3c137a396f9..0671aeff35c5e 100644 --- a/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.ts +++ b/x-pack/solutions/security/plugins/security_solution/public/flyout/document_details/right/hooks/use_assistant.ts @@ -107,7 +107,7 @@ export const useAssistant = ({ ); return { - showAssistant: hasAssistantPrivilege && promptContextId !== null, + showAssistant: isAssistantEnabled && hasAssistantPrivilege && promptContextId !== null, showAssistantOverlay, promptContextId: promptContextId || '', }; From 669eed196acea180e3a828e0d66b41131fdd5e78 Mon Sep 17 00:00:00 2001 From: Kenneth Kreindler Date: Mon, 14 Jul 2025 11:59:12 +0100 Subject: [PATCH 2/3] i18n --- .../packages/shared/kbn-management/settings/setting_ids/index.ts | 1 + .../plugins/private/translations/translations/fr-FR.json | 1 - .../plugins/private/translations/translations/ja-JP.json | 1 - .../plugins/private/translations/translations/zh-CN.json | 1 - 4 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/platform/packages/shared/kbn-management/settings/setting_ids/index.ts b/src/platform/packages/shared/kbn-management/settings/setting_ids/index.ts index 4769049a09c8f..0b5ff35a3422c 100644 --- a/src/platform/packages/shared/kbn-management/settings/setting_ids/index.ts +++ b/src/platform/packages/shared/kbn-management/settings/setting_ids/index.ts @@ -169,6 +169,7 @@ export const SECURITY_SOLUTION_ENABLE_ASSET_INVENTORY_SETTING = 'securitySolution:enableAssetInventory' as const; export const SECURITY_SOLUTION_ENABLE_CLOUD_CONNECTOR_SETTING = 'securitySolution:enableCloudConnector' as const; +export const AI_ASSISTANT_PREFERRED_AI_ASSISTANT_TYPE = 'aiAssistant:preferredAIAssistantType'; // Timelion settings export const TIMELION_ES_DEFAULT_INDEX_ID = 'timelion:es.default_index'; diff --git a/x-pack/platform/plugins/private/translations/translations/fr-FR.json b/x-pack/platform/plugins/private/translations/translations/fr-FR.json index 9f5ff85500a7c..0d36a376b57b6 100644 --- a/x-pack/platform/plugins/private/translations/translations/fr-FR.json +++ b/x-pack/platform/plugins/private/translations/translations/fr-FR.json @@ -96,7 +96,6 @@ "aiAssistantManagementSelection.breadcrumb.index": "Assistant d'IA", "aiAssistantManagementSelection.featureRegistry.featureName": "Assistant d'IA", "aiAssistantManagementSelection.managementSectionLabel": "Assistants d'IA", - "aiAssistantManagementSelection.preferredAIAssistantTypeSettingDescription": "[version d'évaluation technique] Afficher ou non l'élément de menu Assistant d’IA dans Observability et Search, partout ou nulle part.", "aiAssistantManagementSelection.preferredAIAssistantTypeSettingName": "Assistant d’IA de visibilité pour Observability et Search", "aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueDefault": "Observability et Search uniquement (par défaut)", "aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueNever": "Nulle part", diff --git a/x-pack/platform/plugins/private/translations/translations/ja-JP.json b/x-pack/platform/plugins/private/translations/translations/ja-JP.json index 3fb2ba5d518f6..c6136c351e5ad 100644 --- a/x-pack/platform/plugins/private/translations/translations/ja-JP.json +++ b/x-pack/platform/plugins/private/translations/translations/ja-JP.json @@ -96,7 +96,6 @@ "aiAssistantManagementSelection.breadcrumb.index": "AI Assistant", "aiAssistantManagementSelection.featureRegistry.featureName": "AI Assistant", "aiAssistantManagementSelection.managementSectionLabel": "AI Assistant", - "aiAssistantManagementSelection.preferredAIAssistantTypeSettingDescription": "[テクニカルプレビュー]オブザーバビリティとSearchのAI Assistantメニュー項目を、どの場所でも表示するか、どの場所でも表示しないか。", "aiAssistantManagementSelection.preferredAIAssistantTypeSettingName": "AI Assistant for Observability and Searchの表示", "aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueDefault": "Observability and Searchのみ(デフォルト)", "aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueNever": "なし", diff --git a/x-pack/platform/plugins/private/translations/translations/zh-CN.json b/x-pack/platform/plugins/private/translations/translations/zh-CN.json index 9e095727fd7e9..b13c338b97763 100644 --- a/x-pack/platform/plugins/private/translations/translations/zh-CN.json +++ b/x-pack/platform/plugins/private/translations/translations/zh-CN.json @@ -96,7 +96,6 @@ "aiAssistantManagementSelection.breadcrumb.index": "AI 助手", "aiAssistantManagementSelection.featureRegistry.featureName": "AI 助手", "aiAssistantManagementSelection.managementSectionLabel": "AI 助手", - "aiAssistantManagementSelection.preferredAIAssistantTypeSettingDescription": "[技术预览] Observability 和 Search 中是否显示 AI 助手菜单项:随处显示或者从不显示。", "aiAssistantManagementSelection.preferredAIAssistantTypeSettingName": "适用于 Observability 和 Search 的 AI 助手可见性", "aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueDefault": "仅限 Observability 和 Search(默认)", "aiAssistantManagementSelection.preferredAIAssistantTypeSettingValueNever": "从不显示", From 2372cf53ba1f21b9998e030f5aab386b9a048c6a Mon Sep 17 00:00:00 2001 From: Kenneth Kreindler Date: Mon, 14 Jul 2025 14:23:24 +0100 Subject: [PATCH 3/3] allow list AI_ASSISTANT_PREFERRED_AI_ASSISTANT_TYPE --- .../shared/serverless/settings/observability_project/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/platform/packages/shared/serverless/settings/observability_project/index.ts b/src/platform/packages/shared/serverless/settings/observability_project/index.ts index e12d28de1c81b..297aeb3d87a53 100644 --- a/src/platform/packages/shared/serverless/settings/observability_project/index.ts +++ b/src/platform/packages/shared/serverless/settings/observability_project/index.ts @@ -29,4 +29,5 @@ export const OBSERVABILITY_PROJECT_SETTINGS = [ export const OBSERVABILITY_AI_ASSISTANT_PROJECT_SETTINGS = [ settings.OBSERVABILITY_AI_ASSISTANT_SIMULATED_FUNCTION_CALLING, settings.OBSERVABILITY_AI_ASSISTANT_SEARCH_CONNECTOR_INDEX_PATTERN, + settings.AI_ASSISTANT_PREFERRED_AI_ASSISTANT_TYPE, ];