Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,24 @@ export type AgentCreateRequest = Omit<
*/
type?: string;
access_control?: Pick<AgentAccessControl, 'access_mode'>;
/**
* AB-004: when true the agent is created readonly (package-managed).
* Fleet package installs set this so UI edits warn/block, mirroring the
* managed workflow pattern. Carried on update so package upgrades keep it.
*/
readonly?: boolean;
};

export type AgentUpdateRequest = Partial<
Pick<AgentDefinition, 'name' | 'description' | 'labels' | 'avatar_color' | 'avatar_symbol'>
> & {
access_control?: Pick<AgentAccessControl, 'access_mode'>;
configuration?: Partial<AgentConfiguration>;
/**
* AB-004: package upgrades re-assert the managed flag; a reinstall must not
* silently downgrade a package agent to an editable user agent.
*/
readonly?: boolean;
};

export type AgentDeleteRequest = Pick<AgentDefinition, 'id'>;
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export interface AgentDefinition {
* Optional labels used to organize or filter agents
*/
labels?: string[];
/** True when the agent is managed by a package and must not be edited by users. */
/**
* Optional avatar eui icon for built-in agents
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@

import type { ZodObject } from '@kbn/zod/v4';
import type { KibanaRequest } from '@kbn/core-http-server';
import type { AgentCreateRequest, ConversationTemplate } from '@kbn/agent-builder-common';
import type {
AgentCreateRequest,
ConversationTemplate,
PersistedSkillCreateRequest,
} from '@kbn/agent-builder-common';
import type { ConversationPublicClient } from './conversations';
import type { StaticToolRegistration, ToolRegistry } from './tools';
import type { AttachmentTypeDefinition } from './attachments';
Expand Down Expand Up @@ -238,11 +242,36 @@ export interface TopSnippetsConfig {
/**
* Setup contract of the agentBuilder plugin.
*/

/**
* Internal management API for package-owned persisted agents and skills
* (e.g. Fleet package install/uninstall).
*/
export interface AgentBuilderManagementSetup {
/** Read a single agent, or null when it does not exist. Used to enrich Fleet asset listings. */
getAgent(agentId: string, request: KibanaRequest): Promise<unknown>;
createOrUpdateAgent(params: AgentCreateRequest, request: KibanaRequest): Promise<unknown>;
deletePackageManagedAgent(agentId: string, spaceId: string): Promise<boolean>;
createOrUpdateSkill(params: PersistedSkillCreateRequest, request: KibanaRequest): Promise<unknown>;
deletePackageManagedSkill(skillId: string, spaceId: string): Promise<boolean>;
/**
* List the skills a package owns, so install can reap the ones the current
* archive no longer produces. Package-managed skills are readonly, so an
* orphan left behind by an id-scheme change is otherwise undeletable.
*/
listPackageManagedSkills(
pluginId: string,
spaceId: string
): Promise<Array<{ id: string; plugin_id?: string }>>;
}

export interface AgentBuilderPluginSetup {
/**
* Agents setup contract, which can be used to register built-in agents.
*/
agents: AgentsSetup;
/** Internal management API for programmatic agent/skill CRUD (Fleet package install). */
management: AgentBuilderManagementSetup;
/**
* Tools setup contract, which can be used to register built-in tools.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ export const item: GetInfoResponse['item'] = {
],
assets: {
kibana: {
workflow: [],
agent: [],
alerting_rule_template: [],
slo_template: [],
dashboard: [
Expand Down Expand Up @@ -304,6 +306,7 @@ export const item: GetInfoResponse['item'] = {
ml_model: [],
knowledge_base: [],
esql_view: [],
index_alias: [],
},
},
policy_templates: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ export const item: GetInfoResponse['item'] = {
],
assets: {
kibana: {
workflow: [],
agent: [],
alerting_rule_template: [],
slo_template: [],
dashboard: [
Expand Down Expand Up @@ -133,6 +135,7 @@ export const item: GetInfoResponse['item'] = {
ml_model: [],
knowledge_base: [],
esql_view: [],
index_alias: [],
},
},
policy_templates: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const _allowedExperimentalValues = {
disableAgentlessLegacyAPI: true, // When enabled, the legacy agent/package policy APIs reject agentless create, update, upgrade, and copy. Forces enableAgentlessPoliciesUI on (see below).
enableAgentlessPoliciesUI: true, // When enabled, the UI reads/writes agentless integration policies through the managed integrations API. Disable as a kill switch to fall back to the legacy APIs — but disableAgentlessLegacyAPI overrides it (the fallback would 400).
enableEsqlViewInstall: false,
enableIndexAliasInstall: false,
enableSloTemplates: true,
newBrowseIntegrationUx: true, // When enabled integrations, browse integrations page will use the new UX.
enableVersionSpecificPolicies: true, // When enabled, version specific policies will be created when packages use agent version conditions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,13 @@ export { getOtelCollectorDisplayName, getOtelCollectorConfigName } from './otel_
export { isNamespaceAllowedByPrefixes } from './namespace_prefixes';

export { getAgentlessThroughputIndexPatterns } from './agentless_throughput_helper';
export {
isKibanaOnlyIntegration,
isConnectorVar,
getConnectorChecklist,
isConnectorSetupComplete,
} from './kibana_only_integration';
export type { ConnectorChecklistItem } from './kibana_only_integration';

export type { YamlModule } from './yaml_utils';
export { createYamlKeysSorter, toYaml } from './yaml_utils';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/*
* 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 type { PackageInfo, RegistryPolicyTemplate, RegistryVarsEntry } from '../types';
import {
isKibanaOnlyIntegration,
isConnectorVar,
getConnectorChecklist,
isConnectorSetupComplete,
} from './kibana_only_integration';

const template = (name: string, inputs: unknown[]): RegistryPolicyTemplate =>
({ name, title: name, description: name, inputs } as unknown as RegistryPolicyTemplate);

const pkg = (templates: RegistryPolicyTemplate[]): Pick<PackageInfo, 'policy_templates'> =>
({ policy_templates: templates } as Pick<PackageInfo, 'policy_templates'>);

const varDef = (
name: string,
extra: Partial<RegistryVarsEntry> = {}
): RegistryVarsEntry => ({ name, type: 'text', ...extra } as RegistryVarsEntry);

describe('FLEET-013 · Kibana-only integration helpers', () => {
describe('isKibanaOnlyIntegration', () => {
it('detects a package whose only policy template has empty inputs', () => {
expect(isKibanaOnlyIntegration(pkg([template('sdlc_intel', [])]))).toBe(true);
});

it('is false when the template declares inputs', () => {
expect(isKibanaOnlyIntegration(pkg([template('nginx', [{ type: 'logfile' }])]))).toBe(false);
});

it('is false when ANY template declares inputs (partial match must not hide the agent step)', () => {
expect(
isKibanaOnlyIntegration(
pkg([template('kibana_only', []), template('agent_based', [{ type: 'logfile' }])])
)
).toBe(false);
});

it('is true only when EVERY template has empty inputs', () => {
expect(isKibanaOnlyIntegration(pkg([template('a', []), template('b', [])]))).toBe(true);
});

it('is false when inputs is undefined rather than an empty array', () => {
expect(
isKibanaOnlyIntegration(pkg([{ name: 'x', title: 'x' } as RegistryPolicyTemplate]))
).toBe(false);
});

it('is false for a package with no policy templates', () => {
expect(isKibanaOnlyIntegration(pkg([]))).toBe(false);
});

it('is false for undefined package info', () => {
expect(isKibanaOnlyIntegration(undefined)).toBe(false);
});

it('scopes the check to a named template when given', () => {
const info = pkg([template('kibana_only', []), template('agent_based', [{ type: 'log' }])]);

expect(isKibanaOnlyIntegration(info, 'kibana_only')).toBe(true);
expect(isKibanaOnlyIntegration(info, 'agent_based')).toBe(false);
});

it('is false when the named template does not exist', () => {
expect(isKibanaOnlyIntegration(pkg([template('a', [])]), 'missing')).toBe(false);
});
});

describe('isConnectorVar', () => {
it.each([['github_connector_id'], ['slack_connector']])('recognises %s', (name) => {
expect(isConnectorVar(varDef(name))).toBe(true);
});

it.each([['analysis_window_days'], ['connector_timeout'], ['id']])(
'does not recognise %s',
(name) => {
expect(isConnectorVar(varDef(name))).toBe(false);
}
);
});

describe('getConnectorChecklist', () => {
const packageVars = {
vars: [
varDef('github_connector_id', { required: true, description: 'GitHub connector' }),
varDef('slack_connector', { required: false, title: 'Slack' }),
varDef('analysis_window_days', { required: true }),
],
} as Pick<PackageInfo, 'vars'>;

it('lists only connector vars, ignoring unrelated package vars', () => {
expect(getConnectorChecklist(packageVars).map((i) => i.name)).toEqual([
'github_connector_id',
'slack_connector',
]);
});

it('marks a connector as configured when a value is present', () => {
const checklist = getConnectorChecklist(packageVars, { github_connector_id: 'abc-123' });

expect(checklist.find((i) => i.name === 'github_connector_id')?.configured).toBe(true);
expect(checklist.find((i) => i.name === 'slack_connector')?.configured).toBe(false);
});

it('treats an empty or whitespace-only value as not configured', () => {
const checklist = getConnectorChecklist(packageVars, {
github_connector_id: ' ',
slack_connector: '',
});

expect(checklist.every((i) => !i.configured)).toBe(true);
});

it('carries required and title/description through for rendering', () => {
const [github, slack] = getConnectorChecklist(packageVars);

expect(github).toMatchObject({
name: 'github_connector_id',
title: 'github_connector_id',
description: 'GitHub connector',
required: true,
});
expect(slack).toMatchObject({ title: 'Slack', required: false });
});

it('returns an empty checklist when the package declares no vars', () => {
expect(getConnectorChecklist({ vars: [] } as Pick<PackageInfo, 'vars'>)).toEqual([]);
expect(getConnectorChecklist(undefined)).toEqual([]);
});
});

describe('isConnectorSetupComplete', () => {
it('is true when every required connector is configured', () => {
expect(
isConnectorSetupComplete([
{ name: 'a', title: 'a', required: true, configured: true },
{ name: 'b', title: 'b', required: false, configured: false },
])
).toBe(true);
});

it('is false when a required connector is missing', () => {
expect(
isConnectorSetupComplete([{ name: 'a', title: 'a', required: true, configured: false }])
).toBe(false);
});

it('is true for an empty checklist (nothing to configure)', () => {
expect(isConnectorSetupComplete([])).toBe(true);
});

it('ignores optional connectors when deciding completeness', () => {
expect(
isConnectorSetupComplete([{ name: 'b', title: 'b', required: false, configured: false }])
).toBe(true);
});
});

describe('sdlc_intel manifest shape (regression guard)', () => {
it('classifies the shipped sdlc_intel manifest as Kibana-only with a GitHub connector', () => {
const sdlcIntel = {
policy_templates: [template('sdlc_intel', [])],
vars: [
varDef('github_connector_id', { required: true }),
varDef('analysis_window_days'),
],
} as Pick<PackageInfo, 'policy_templates' | 'vars'>;

expect(isKibanaOnlyIntegration(sdlcIntel)).toBe(true);

const checklist = getConnectorChecklist(sdlcIntel);
expect(checklist).toHaveLength(1);
expect(checklist[0]).toMatchObject({ name: 'github_connector_id', required: true });
expect(isConnectorSetupComplete(checklist)).toBe(false);

const configured = getConnectorChecklist(sdlcIntel, { github_connector_id: 'gh-1' });
expect(isConnectorSetupComplete(configured)).toBe(true);
});
});
});
Loading