Skip to content

[Agent Builder] Agent plugins: initial installation support#256478

Merged
pgayvallet merged 39 commits intoelastic:mainfrom
pgayvallet:ab-xxx-plugin-support
Mar 11, 2026
Merged

[Agent Builder] Agent plugins: initial installation support#256478
pgayvallet merged 39 commits intoelastic:mainfrom
pgayvallet:ab-xxx-plugin-support

Conversation

@pgayvallet
Copy link
Copy Markdown
Contributor

@pgayvallet pgayvallet commented Mar 6, 2026

Summary

Fix https://github.com/elastic/search-team/issues/13213

Add initial support for installing Claude agent skill plugins (and Agent Builder plugins - which will be a superset of that spec)

We currently support installing from three different sources:

  • GH Claude plugin url
    • e.g. https://github.com/anthropics/financial-services-plugins/tree/main/financial-analysis or https://github.com/anthropics/financial-services-plugins/blob/main/financial-analysis/.claude-plugin/plugin.json
  • Remote zip url
    • e.g. https://my-server/my-plugin.zip
  • Uploading the zip file directly

What do we support from the Claude plugin spec?

This initial implementation supports the following asset types:

  • skills (and associated file assets).

Support planned in follow ups:

  • commands
  • MCP servers (if we can)

Support not planned:

  • agents
  • hooks
  • everything else

Plugins and managed assets

Plugin managed assets (so skills only atm) are considered read-only, and can only be deleted by deleting the plugin.

They also won't be able to be "directly" associated with an agent - instead, you add the plugin to the agent, which will expose all its assets (note: this is out of scope of the current PR)

How to test

Installing a plugin from url

Install the plugin:

POST kbn://api/agent_builder/plugins/install
{
    "url": "https://github.com/anthropics/financial-services-plugins/tree/main/financial-analysis",
}

List the plugins:

GET kbn://api/agent_builder/plugins

Check that the plugin's skills are available:

GET kbn://api/agent_builder/skills

Note: the skills can't be added to an agent atm, it will be done in a follow-up

Summary by CodeRabbit

Release Notes

  • New Features
    • Added plugin management system with new API endpoints for listing, retrieving, deleting, and installing plugins
    • Plugins can be installed from GitHub repositories, direct ZIP URLs, or uploaded files
    • Plugin-managed skills are marked as read-only to ensure integrity
    • Support for custom GitHub server configuration for plugin sources

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 6, 2026

📝 Walkthrough

Walkthrough

This pull request introduces comprehensive plugin management functionality for the agent builder. It adds OpenAPI-documented endpoints for listing, retrieving, deleting, and installing plugins; implements a full PluginsService with storage, archive parsing, and URL resolution; and extends skill management to support plugin-managed skills with read-only protection.

Changes

Cohort / File(s) Summary
OpenAPI Documentation
oas_docs/output/kibana.serverless.yaml, oas_docs/output/kibana.yaml
Added four new agent-builder plugin endpoints (GET /plugins, GET /plugins/{id}, DELETE /plugins/{id}, POST /plugins/install) with complete metadata, examples, and code samples.
Error Types & Type Definitions
x-pack/platform/packages/shared/agent-builder/agent-builder-common/base/errors.ts, x-pack/platform/packages/shared/agent-builder/agent-builder-common/index.ts, x-pack/platform/packages/shared/agent-builder/agent-builder-common/plugins/*
Added pluginNotFound error, error factory/checker functions; introduced plugin type definitions (PluginManifest, ParsedPluginArchive, PluginDefinition) and re-export modules.
HTTP API & Configuration
x-pack/platform/plugins/shared/agent_builder/common/http_api/plugins.ts, x-pack/platform/plugins/shared/agent_builder/server/config.ts, x-pack/platform/plugins/shared/agent_builder/moon.yml, x-pack/platform/plugins/shared/agent_builder/tsconfig.json
Added plugin response types (ListPluginsResponse, GetPluginResponse, InstallPluginResponse, DeletePluginResponse), githubBaseUrl config property, added fs dependency, and updated project references.
Plugin Service & Client Layer
x-pack/platform/plugins/shared/agent_builder/server/services/plugins/*, x-pack/platform/plugins/shared/agent_builder/server/services/create_services.ts, x-pack/platform/plugins/shared/agent_builder/server/services/types.ts
Implemented PluginsService with lifecycle (setup/start), PluginClient for ES-backed storage (CRUD operations), storage schema, type definitions, and converters; integrated into ServiceManager and internal service contracts.
Archive & Parsing Utilities
x-pack/platform/plugins/shared/agent_builder/server/services/plugins/utils/archive/*, x-pack/platform/plugins/shared/agent_builder/server/services/plugins/utils/parsing/*
Added ZIP archive handling (openZipArchive, createScopedArchive, detectArchiveRootPrefix), plugin manifest parsing with validation, skill file frontmatter parsing, and unmanaged asset detection.
URL Resolution & Sourcing
x-pack/platform/plugins/shared/agent_builder/server/services/plugins/utils/sourcing/*
Implemented GitHub URL parsing, plugin URL resolution (supporting both ZIP and GitHub sources), plugin downloading from URLs/files, and file upload handling with temporary storage.
API Routes & Handlers
x-pack/platform/plugins/shared/agent_builder/server/routes/plugins.ts, x-pack/platform/plugins/shared/agent_builder/server/routes/index.ts, x-pack/platform/plugins/shared/agent_builder/server/routes/examples/*.yaml
Added comprehensive plugin route handlers (list, get, delete, install) with validation, versioning, security scopes, and OpenAPI examples for all operations.
Skill Service Integration
x-pack/platform/plugins/shared/agent_builder/server/services/skills/persisted/client/*.ts, x-pack/platform/plugins/shared/agent_builder/server/services/skills/persisted/converter.ts, x-pack/platform/plugins/shared/agent_builder/server/services/skills/utils.ts
Added plugin_id field to skill definitions, bulkCreate and deleteByPluginId methods to SkillClient, read-only enforcement for plugin-managed skills, and plugin_id propagation through converters.
Plugin Bootstrap & Configuration
x-pack/platform/plugins/shared/agent_builder/server/plugin.ts
Updated constructor initialization to pass config to ServiceManager for plugin service configuration.
Integration Tests
x-pack/platform/test/agent_builder_api_integration/apis/plugins/*, x-pack/platform/test/agent_builder_api_integration/utils/plugins_server/*, x-pack/platform/test/agent_builder_api_integration/configs/config.stateful.ts
Added comprehensive plugin API integration tests with test server for serving plugin archives, setup/teardown logic, multiple installation paths (URL, GitHub, file upload), skill validation, and read-only enforcement; updated test config for dynamic port allocation.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Router
    participant PluginsService
    participant PluginClient
    participant Elasticsearch as Elasticsearch<br/>(Storage)
    participant Archive as Archive<br/>(Zip)
    participant SkillService

    Client->>Router: POST /plugins/install<br/>(url, pluginName?)
    Router->>PluginsService: installPlugin(request, source)
    
    alt URL Source
        PluginsService->>Archive: parsePluginFromUrl(url)
        Archive->>Archive: downloadAndOpenArchive()
        Archive->>Archive: parsePluginZipFile()
        Archive-->>PluginsService: ParsedPluginArchive
    else File Source
        PluginsService->>Archive: parsePluginFromFile(filePath)
        Archive-->>PluginsService: ParsedPluginArchive
    end
    
    PluginsService->>PluginClient: findByName(name)
    PluginClient->>Elasticsearch: query by name
    Elasticsearch-->>PluginClient: result or undefined
    
    alt Duplicate Check Passes
        PluginsService->>SkillService: bulkCreate(skills)
        SkillService->>Elasticsearch: bulk index skills
        Elasticsearch-->>SkillService: skillIds
        
        PluginsService->>PluginClient: create(pluginRequest)
        PluginClient->>Elasticsearch: index plugin doc
        Elasticsearch-->>PluginClient: PersistedPluginDefinition
        PluginClient-->>PluginsService: plugin
        
        PluginsService-->>Router: PluginDefinition
        Router-->>Client: 200 OK
    else Duplicate Found
        PluginsService-->>Router: 400 Bad Request
        Router-->>Client: Error
    end
Loading
sequenceDiagram
    participant Client
    participant Router
    participant PluginsService
    participant PluginClient
    participant SkillClient
    participant Elasticsearch as Elasticsearch

    Client->>Router: DELETE /plugins/{pluginId}
    Router->>PluginsService: deletePlugin(request, pluginId)
    
    PluginsService->>PluginClient: get(pluginId)
    PluginClient->>Elasticsearch: fetch plugin doc
    Elasticsearch-->>PluginClient: plugin
    
    PluginsService->>SkillClient: deleteByPluginId(pluginName)
    SkillClient->>Elasticsearch: deleteByQuery(plugin_id=...)
    Elasticsearch-->>SkillClient: acknowledged
    
    PluginsService->>PluginClient: delete(pluginId)
    PluginClient->>Elasticsearch: delete plugin doc
    Elasticsearch-->>PluginClient: success
    
    PluginsService-->>Router: void
    Router-->>Client: 200 OK {success: true}
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • kowalczyk-krzysztof
  • ElenaStoeva
  • gsoldevila
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title '[Agent Builder] Agent plugins: initial installation support' clearly and directly summarizes the main change: adding initial plugin installation functionality to the Agent Builder feature.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Comment @coderabbitai help to get the list of available commands and usage tips.

@pgayvallet pgayvallet added release_note:skip Skip the PR/issue when compiling release notes backport:skip This PR does not require backporting v9.4.0 feature:agent-builder Identify agent builder functionalities to be grouped together for release notes labels Mar 9, 2026
@pgayvallet pgayvallet changed the title [Agent Builder] Agent plugins: initial server implementation [Agent Builder] Agent plugins: initial installation support Mar 9, 2026
@pgayvallet pgayvallet added the reviewer:coderabbit PR review with CodeRabbit label Mar 9, 2026
@pgayvallet pgayvallet requested a review from a team as a code owner March 9, 2026 20:50
coderabbitai[bot]

This comment was marked as outdated.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

🧹 Nitpick comments (14)
x-pack/platform/packages/shared/agent-builder/agent-builder-server/skills/internal.ts (1)

62-65: Consider using camelCase for the property name.

The property plugin_id uses snake_case, but the coding guidelines specify camelCase for object keys. If this naming is intentional for consistency with persistence or API contracts across the codebase, please disregard.

♻️ Suggested naming change
   /**
-   * If this skill was installed from a plugin, the plugin name.
+   * If this skill was installed from a plugin, the plugin ID.
    */
-  plugin_id?: string;
+  pluginId?: string;

As per coding guidelines: "Use camelCase for functions, variables, and object keys."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@x-pack/platform/packages/shared/agent-builder/agent-builder-server/skills/internal.ts`
around lines 62 - 65, The property plugin_id should be renamed to camelCase
pluginId across the code to follow coding guidelines; update the declaration in
internal.ts (replace plugin_id with pluginId), then update all references/usages
(type annotations, object literals, destructuring, assignments) to use pluginId
and adjust any serialization/deserialization or API mapping code that expects
snake_case (add mapping from plugin_id → pluginId or update serializers) and
update affected tests/types to preserve backward compatibility where needed.
x-pack/platform/packages/shared/agent-builder/agent-builder-common/plugins/plugin_definition.ts (1)

21-57: Make the plugin DTOs readonly.

These interfaces represent persisted, read-only plugin state, but all of the arrays are mutable today. Mark the exported fields/arrays as readonly/ReadonlyArray so consumers cannot accidentally mutate skill_ids or unmanaged_assets in memory.

As per coding guidelines, "Prefer readonly and as const for immutable structures in TypeScript."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@x-pack/platform/packages/shared/agent-builder/agent-builder-common/plugins/plugin_definition.ts`
around lines 21 - 57, The DTOs are mutable; make UnmanagedPluginAssets,
PluginManifestMetadata, and PluginDefinition use readonly fields and
ReadonlyArray for all arrays so consumers cannot mutate them: mark each property
in UnmanagedPluginAssets (commands, agents, hooks, mcp_servers, output_styles,
lsp_servers) as readonly ReadonlyArray<string>, mark
PluginManifestMetadata.keywords as readonly ReadonlyArray<string> (and other
optional fields readonly), and update PluginDefinition to use readonly for all
top-level properties (id, name, version, description, manifest, source_url,
skill_ids as ReadonlyArray<string>, unmanaged_assets, created_at, updated_at) so
the DTOs are fully immutable in memory.
x-pack/platform/plugins/shared/agent_builder/server/services/skills/utils.ts (1)

15-30: Omit plugin_id when it is unset.

Right now this helper always creates the plugin_id key, so plain-object consumers still see it as present with undefined. If the public contract is “only include this for plugin-managed skills,” build it conditionally instead.

♻️ Proposed change
 export const internalToPublicDefinition = async (
   skill: InternalSkillDefinition
 ): Promise<PublicSkillDefinition> => ({
   id: skill.id,
   name: skill.name,
   description: skill.description,
   content: skill.content,
   referenced_content: skill.referencedContent?.map((rc) => ({
     name: rc.name,
     relativePath: rc.relativePath,
     content: rc.content,
   })),
   tool_ids: await skill.getRegistryTools(),
   readonly: skill.readonly,
-  plugin_id: skill.plugin_id,
+  ...(skill.plugin_id !== undefined ? { plugin_id: skill.plugin_id } : {}),
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@x-pack/platform/plugins/shared/agent_builder/server/services/skills/utils.ts`
around lines 15 - 30, internalToPublicDefinition always includes the plugin_id
key even when undefined; change it to only add plugin_id when skill.plugin_id is
set (e.g., check for null/undefined) so the returned plain object omits the key
for non-plugin-managed skills—update the internalToPublicDefinition return
construction to conditionally include plugin_id (keeping the rest of the fields
and tool_ids await logic intact).
x-pack/platform/test/agent_builder_api_integration/apis/plugins/index.ts (1)

10-14: Add an explicit return type and prefer a typed const export here.

This entrypoint is new exported TS code, so I’d annotate it with : void and switch to the preferred const-arrow form.

♻️ Proposed change
-export default function ({ loadTestFile }: FtrProviderContext) {
+const registerPluginsApiTests = ({ loadTestFile }: FtrProviderContext): void => {
   describe('Plugins API', function () {
     loadTestFile(require.resolve('./installation'));
   });
-}
+};
+
+export default registerPluginsApiTests;

As per coding guidelines, "Prefer explicit return types for public APIs and exported functions in TypeScript" and "Prefer const arrow functions in TypeScript."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@x-pack/platform/test/agent_builder_api_integration/apis/plugins/index.ts`
around lines 10 - 14, The exported unnamed function lacks an explicit return
type and should be a typed const-arrow export; change the default export to a
named const arrow like e.g. const pluginsApi = ({ loadTestFile }:
FtrProviderContext): void => { describe('Plugins API', function () {
loadTestFile(require.resolve('./installation')); }); }; and then export default
pluginsApi; ensuring the parameter is typed with FtrProviderContext and the
function return type is explicitly void.
x-pack/platform/plugins/shared/agent_builder/server/services/skills/skill_service.test.ts (1)

30-33: Give the new persisted-client mocks async defaults.

bulkCreate and deleteByPluginId are async on the real client. Leaving them as bare jest.fn() makes the default mock return undefined, which can hide contract mismatches or produce misleading failures once these paths are exercised.

♻️ Proposed fix
-    bulkCreate: jest.fn(),
+    bulkCreate: jest.fn().mockResolvedValue([]),
...
-    deleteByPluginId: jest.fn(),
+    deleteByPluginId: jest.fn().mockResolvedValue(undefined),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@x-pack/platform/plugins/shared/agent_builder/server/services/skills/skill_service.test.ts`
around lines 30 - 33, The mocks for the persisted-client need async defaults:
change the plain jest.fn() for bulkCreate and deleteByPluginId to resolved mocks
so they return a Promise (e.g., jest.fn().mockResolvedValue(undefined) or an
appropriate resolved value) instead of undefined synchronously; update the mock
declarations for bulkCreate and deleteByPluginId in the test file so they use
mockResolvedValue(...) while leaving update and delete as-is.
x-pack/platform/plugins/shared/agent_builder/server/services/plugins/utils/sourcing/download_plugin.test.ts (1)

11-19: Replace the unknown[] passthrough mocks with typed signatures.

These mocks work, but they lose the openZipArchive / parsePluginZipFile contract and introduce new unknown usage in TS test code. Typing the forwarded parameters directly keeps the mock surface aligned with the real functions.

♻️ Proposed fix
 jest.mock('../archive', () => ({
-  openZipArchive: (...args: unknown[]) => mockOpenZipArchive(...args),
+  openZipArchive: (filePath: string) => mockOpenZipArchive(filePath),
 }));
 
 const mockParsePluginZipFile = jest.fn();
 jest.mock('../parsing', () => ({
-  parsePluginZipFile: (...args: unknown[]) => mockParsePluginZipFile(...args),
+  parsePluginZipFile: (archive: ZipArchive) => mockParsePluginZipFile(archive),
   PluginArchiveError: class extends Error {},
 }));
As per coding guidelines, `**/*.{ts,tsx}`: Use TypeScript for all new code; avoid `any` and `unknown`.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@x-pack/platform/plugins/shared/agent_builder/server/services/plugins/utils/sourcing/download_plugin.test.ts`
around lines 11 - 19, The test mocks currently forward parameters as (...args:
unknown[]) which loses the real function signatures for openZipArchive and
parsePluginZipFile; replace the unknown[] passthroughs with the actual parameter
types by using Parameters<typeof openZipArchive> and Parameters<typeof
parsePluginZipFile> (or import the functions and use their parameter types) when
defining the mock factories so the mockOpenZipArchive and mockParsePluginZipFile
preserve the real contracts; keep the PluginArchiveError class export as-is.
x-pack/platform/plugins/shared/agent_builder/server/services/plugins/utils/archive/create_scoped_archive.test.ts (1)

11-23: Consider extracting createMockArchive into a shared test helper.

The same ZipArchive test double is duplicated in x-pack/platform/plugins/shared/agent_builder/server/services/plugins/utils/parsing/parse_plugin_zip_file.test.ts at Line 11-Line 23. Pulling it into one helper would make future interface changes less error-prone.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@x-pack/platform/plugins/shared/agent_builder/server/services/plugins/utils/archive/create_scoped_archive.test.ts`
around lines 11 - 23, Extract the duplicated ZipArchive test double into a
single test helper by moving the createMockArchive function (which returns an
object implementing hasEntry, getEntryPaths, getEntryContent and close) into a
shared test utilities module and replace the local definitions in both tests
with imports of that helper; ensure the helper exports createMockArchive and
uses the same function signature (files: Record<string,string>) so existing
tests referencing createMockArchive, ZipArchive, getEntryContent, hasEntry and
close continue to work without further changes.
x-pack/platform/plugins/shared/agent_builder/server/services/plugins/utils/archive/open_zip_archive.test.ts (2)

14-18: Let the test helper generate raw-byte fixtures.

ZipArchive.getEntryContent() returns Buffer, but this helper only builds UTF-8 text entries. That means the “byte-for-byte” case still passes if the implementation accidentally round-trips through strings.

♻️ Suggested change
-const createTestZip = async (files: Record<string, string>, destPath: string): Promise<void> => {
+const createTestZip = async (
+  files: Record<string, string | Buffer>,
+  destPath: string
+): Promise<void> => {
   return new Promise((resolve, reject) => {
     const zipFile = new yazl.ZipFile();
     for (const [filePath, content] of Object.entries(files)) {
-      zipFile.addBuffer(Buffer.from(content, 'utf-8'), filePath);
+      zipFile.addBuffer(
+        typeof content === 'string' ? Buffer.from(content, 'utf-8') : content,
+        filePath
+      );
     }

Then use one binary fixture in the byte-preservation test, e.g. Buffer.from([0x00, 0xff, 0x0a]).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@x-pack/platform/plugins/shared/agent_builder/server/services/plugins/utils/archive/open_zip_archive.test.ts`
around lines 14 - 18, The helper createTestZip currently encodes every entry as
UTF-8 text which prevents true byte-for-byte tests; modify createTestZip so it
accepts file contents as either strings or Buffers and when adding entries to
the yazl.ZipFile call zipFile.addBuffer(...) with the raw Buffer when the value
is a Buffer, otherwise convert strings with Buffer.from(value, 'utf-8');
reference createTestZip and ZipArchive.getEntryContent in the test and then add
a binary fixture like Buffer.from([0x00, 0xff, 0x0a]) to the byte-preservation
test so the test verifies exact bytes are preserved.

98-104: Assert the missing-entry path as a rejected promise.

getEntryContent() is a Promise<Buffer> API, but toThrow() only covers synchronous exceptions. This test currently locks in a sync-throw implementation detail instead of the async contract.

✅ Suggested change
-    archive = await openZipArchive(zipPath);
-
-    expect(() => archive!.getEntryContent('does-not-exist.txt')).toThrow(/not found in archive/);
+    const zipArchive = await openZipArchive(zipPath);
+    archive = zipArchive;
+
+    await expect(
+      Promise.resolve().then(() => zipArchive.getEntryContent('does-not-exist.txt'))
+    ).rejects.toThrow(/not found in archive/);

Run this to confirm the contract and current implementation shape:

#!/bin/bash
set -euo pipefail

file="$(fd 'open_zip_archive\.ts$' x-pack/platform/plugins/shared/agent_builder/server/services/plugins/utils/archive | head -n1)"
rg -n -C4 '\binterface ZipArchive\b|\bgetEntryContent\s*\(' "$file"

Expected: the contract shows Promise<Buffer>; if the current implementation throws before returning a promise, the wrapper above still keeps the test aligned with the public async API.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@x-pack/platform/plugins/shared/agent_builder/server/services/plugins/utils/archive/open_zip_archive.test.ts`
around lines 98 - 104, The test is asserting a synchronous throw but
getEntryContent() returns a Promise; change the assertion to expect the promise
to reject: call archive!.getEntryContent('does-not-exist.txt') and use Jest's
.rejects.toThrow(/not found in archive/) (keeping the same regex) so the test
verifies the async contract of getEntryContent from openZipArchive; update the
test invocation accordingly to await or return the expect(...).rejects chain.
oas_docs/output/kibana.yaml (2)

3236-3245: Code sample placeholders should match the parameter name.

The path parameter is named pluginId, but the code samples use {id} as the placeholder. For clarity and consistency, consider using {pluginId} in the examples.

📝 Suggested fix
       x-codeSamples:
         - lang: curl
           source: |
             curl \
-              -X DELETE "${KIBANA_URL}/api/agent_builder/plugins/{id}" \
+              -X DELETE "${KIBANA_URL}/api/agent_builder/plugins/{pluginId}" \
               -H "Authorization: ApiKey ${API_KEY}" \
               -H "kbn-xsrf: true"
         - lang: Console
           source: |
-            DELETE kbn://api/agent_builder/plugins/{id}
+            DELETE kbn://api/agent_builder/plugins/{pluginId}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@oas_docs/output/kibana.yaml` around lines 3236 - 3245, The code samples under
x-codeSamples use the placeholder {id} but the OpenAPI path parameter is named
pluginId; update both examples (the curl and Console entries) to use {pluginId}
so the placeholder matches the parameter name (look for x-codeSamples, the curl
"-X DELETE" sample and the Console "DELETE kbn://api/agent_builder/plugins/{id}"
sample and replace {id} with {pluginId}).

3303-3311: Code sample placeholders should match the parameter name.

Same issue as the DELETE endpoint - the code samples use {id} but the path parameter is pluginId.

📝 Suggested fix
       x-codeSamples:
         - lang: curl
           source: |
             curl \
-              -X GET "${KIBANA_URL}/api/agent_builder/plugins/{id}" \
+              -X GET "${KIBANA_URL}/api/agent_builder/plugins/{pluginId}" \
               -H "Authorization: ApiKey ${API_KEY}"
         - lang: Console
           source: |
-            GET kbn://api/agent_builder/plugins/{id}
+            GET kbn://api/agent_builder/plugins/{pluginId}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@oas_docs/output/kibana.yaml` around lines 3303 - 3311, The code samples under
x-codeSamples for the GET operation use the placeholder "{id}" but the path
parameter is named "pluginId"; update the sample sources in the GET
x-codeSamples (both the curl sample and the Console sample) to use "{pluginId}"
so they match the operation parameter name (e.g., curl -X GET
"${KIBANA_URL}/api/agent_builder/plugins/{pluginId}" and GET
kbn://api/agent_builder/plugins/{pluginId}).
x-pack/platform/plugins/shared/agent_builder/server/services/plugins/client/converters.test.ts (1)

84-87: Avoid any in the fixture mutation.

This edge case can be modeled with the helper input instead of dropping type safety. If createPluginDocument({ skill_ids: undefined }) does not type-check under the current compiler flags, widen the helper input rather than casting here.

💡 Suggested fix
-      const doc = createPluginDocument();
-      (doc._source as any).skill_ids = undefined;
+      const doc = createPluginDocument({ skill_ids: undefined });
As per coding guidelines, "Use TypeScript for all new code; avoid `any` and `unknown`."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@x-pack/platform/plugins/shared/agent_builder/server/services/plugins/client/converters.test.ts`
around lines 84 - 87, The test is mutating the fixture with an any cast; instead
update the fixture helper to accept an optional skill_ids so you can call
createPluginDocument({ skill_ids: undefined }) instead of casting, or widen the
helper's input type to include skill_ids?: string[] | undefined; modify the
createPluginDocument signature (and its input type/interface) used by the test
so the edge-case can be modeled directly and remove the (doc._source as any)
mutation in the test.
x-pack/platform/plugins/shared/agent_builder/server/services/plugins/plugin_service.ts (1)

22-23: Remove the eslint-disable comment and address the empty interface.

Per coding guidelines, linting errors should not be suppressed. Consider using a type alias or adding a placeholder comment property if the interface will be extended later.

♻️ Suggested fix
-// eslint-disable-next-line `@typescript-eslint/no-empty-interface`
-export interface PluginsServiceSetup {}
+export type PluginsServiceSetup = Record<string, never>;

Alternatively, if the interface will gain members soon, add a TODO comment explaining the planned additions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@x-pack/platform/plugins/shared/agent_builder/server/services/plugins/plugin_service.ts`
around lines 22 - 23, Remove the eslint-disable comment and address the empty
interface PluginsServiceSetup: either convert the empty interface to an
equivalent type alias (e.g., type PluginsServiceSetup = {}) or keep the
interface but add a self-documenting placeholder (a TODO comment plus a
non-exported placeholder property or JSDoc indicating future members) so linting
is satisfied without suppressing the rule; update the declaration for
PluginsServiceSetup accordingly and ensure no eslint-disable comment remains.
x-pack/platform/plugins/shared/agent_builder/server/services/plugins/client/client.ts (1)

150-153: Avoid non-null assertion on _source.

Per coding guidelines, non-null assertions (!) should be avoided unless locally justified. The _source field could theoretically be undefined. Add a guard to handle this case.

♻️ Suggested fix
   async update(pluginId: string, update: PluginUpdateRequest): Promise<PersistedPluginDefinition> {
     const document = await this._getById(pluginId);
     if (!document) {
       throw createPluginNotFoundError({ pluginId });
     }
+    if (!document._source) {
+      throw createPluginNotFoundError({ pluginId });
+    }

     const updatedAttributes = updateRequestToEs({
-      current: document._source!,
+      current: document._source,
       update,
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@x-pack/platform/plugins/shared/agent_builder/server/services/plugins/client/client.ts`
around lines 150 - 153, The code calls updateRequestToEs with document._source
using a non-null assertion; instead, guard against a missing source by checking
document._source before calling updateRequestToEs (e.g., if (!document._source)
{ /* handle: throw a descriptive Error or return a failed Result/log and exit
early */ }), then only call updateRequestToEs({ current: document._source,
update }) when present; update references to updatedAttributes and any
downstream logic to handle the early-failure path accordingly so you don't rely
on the non-null assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@oas_docs/output/kibana.serverless.yaml`:
- Around line 3165-3174: Update the x-codeSamples entries so the path parameter
matches the documented operation parameter name: replace the occurrences of
"{id}" with "{pluginId}" in the curl and Console samples under the x-codeSamples
block (the DELETE sample for agent_builder/plugins) and apply the same
replacement for the other duplicate sample block later; ensure both the curl "-X
DELETE" URL and the Console "DELETE kbn://..." sample use "{pluginId}" so the
sample route shape matches the operation definition.
- Around line 3247-3345: The OpenAPI entry for operationId
post-agent-builder-plugins-install only documents JSON URL installs; add an
alternative multipart/form-data requestBody variant to support direct ZIP
uploads by including a file field (e.g., name: file, type: string, format:
binary) and the optional plugin_name field, update the content examples to
include a zipUploadExample (showing multipart form with a .zip file and optional
plugin_name), and ensure the schema allows either application/json (url +
optional plugin_name) or multipart/form-data (file + optional plugin_name) so
generated docs/clients cover both install modes.

In
`@x-pack/platform/packages/shared/agent-builder/agent-builder-common/skills/definition.ts`:
- Around line 51-54: The comment and field name are inconsistent: the field
plugin_id currently says "the plugin name"—decide whether this property should
be an identifier or a human-readable name; then update the TypeScript definition
and JSDoc accordingly (either rename plugin_id to plugin_name everywhere in this
file or change the comment to state "the plugin id/unique identifier") and apply
the same change to the other occurrence around lines 85-88 so consumers see a
consistent property name and description; reference the property symbol
plugin_id (or plugin_name if renaming) when making the edits.

In
`@x-pack/platform/plugins/shared/agent_builder/server/routes/examples/plugins_install.yaml`:
- Around line 1-18: Add a new example documenting the direct-upload (ZIP)
install flow in the application/json examples block by creating an
installPluginFromUploadExample entry that shows the expected payload for uploads
(e.g., a form-data file field named "file" containing the ZIP or, if the API
accepts base64 JSON, a "file_base64" field) and include the optional
"plugin_name" override; update the corresponding examples section referenced in
lines 51-67 as well so the direct-upload example appears alongside
installPluginFromGithubExample, installPluginFromZipExample, and
installPluginWithNameOverrideExample.

In
`@x-pack/platform/plugins/shared/agent_builder/server/services/plugins/plugin_service.ts`:
- Around line 140-143: The delete flow is non-atomic: calling
skillClient.deleteByPluginId(plugin.name) before pluginClient.delete(pluginId)
can leave skills removed but the plugin present if the latter fails; update the
deletion to remove the plugin first using pluginClient.delete(pluginId) and then
delete associated skills, or wrap both calls in a try/catch to rollback or retry
on failure to keep state consistent; also rename the confusing
skillClient.deleteByPluginId to deleteByPluginName (and update its usages) since
it accepts plugin.name, and ensure getScopedClients callers and references
(pluginClient, skillClient, deleteByPluginId, pluginId, plugin.name) are updated
accordingly.
- Around line 116-130: Wrap the skill creation + plugin creation sequence in a
try/catch: call skillClient.bulkCreate(createRequests) as before, then in the
try block build createRequest via parsedArchiveToCreateRequest({ parsedArchive,
sourceUrl, skillIds, nameOverride: pluginNameOverride }) and call await
pluginClient.create(createRequest); in the catch block, delete the previously
created skills using the skillIds (e.g., call skillClient.delete / bulkDelete
with skillIds) to clean up orphaned skills, then rethrow the original error so
callers see the failure; ensure this touches the createRequests/skillIds,
skillClient.bulkCreate, and pluginClient.create sites only.

In
`@x-pack/platform/plugins/shared/agent_builder/server/services/plugins/utils/archive/create_scoped_archive.ts`:
- Around line 23-25: The normalization in createScopedArchive makes an empty
prefix become '/' which breaks flat archives; change createScopedArchive so that
if the provided prefix is an empty string (as returned by
detectArchiveRootPrefix) it returns the original archive unchanged, otherwise
normalize non-empty prefixes to ensure they end with '/' and construct the
ScopedZipArchive; reference createScopedArchive, ScopedZipArchive and
detectArchiveRootPrefix when making this conditional return.

In
`@x-pack/platform/plugins/shared/agent_builder/server/services/plugins/utils/archive/open_zip_archive.ts`:
- Around line 20-35: The zip enumeration lacks handling for zip-level errors: in
the yauzl.open callback (where yauzl.open is called and ZipArchiveImpl is
constructed from entries), add a zipFile.on('error', ...) listener that closes
the zipFile (call zipFile.close()) and rejects the surrounding Promise with the
received error (or a typed wrapper) so readEntry() failures or archive
corruption won't leave the handle open or leave the promise pending; ensure this
handler is registered before calling zipFile.readEntry() and removed/ignored on
normal resolution.

In
`@x-pack/platform/plugins/shared/agent_builder/server/services/plugins/utils/parsing/parse_skill_file.ts`:
- Around line 16-28: The frontmatter parsing fails on Windows CRLF endings
because frontmatterRegex uses LF-only "\n"; update parsing in parseSkillFile to
handle CRLF by either normalizing rawContent (replace \r\n with \n) before
matching or changing frontmatterRegex to use "\r?\n" (e.g.,
/^---\s*\r?\n([\s\S]*?)---\s*\r?\n?([\s\S]*)$/) so ParsedSkillFileResult
correctly extracts meta and content for files with CRLF line endings.

In
`@x-pack/platform/plugins/shared/agent_builder/server/services/plugins/utils/sourcing/download_plugin.ts`:
- Around line 111-127: downloadToFile currently calls fetch(url, { redirect:
'follow' }) allowing unvalidated redirects and making the endpoint SSRFable;
update downloadToFile to validate the initial URL and every redirect hop against
a public-address policy before following. Implement or call a helper like
isPublicAddress(hostOrIp) to reject loopback, link-local, and private addresses,
change fetch to redirect: 'manual' and perform a controlled redirect loop (with
a max hop limit), validate each Location header URL with isPublicAddress before
continuing, and only stream the final response body after all hops pass
validation (keep getSafePath/createWriteStream/pipeline usage unchanged once URL
is approved).
- Around line 102-108: parsePluginFromFile currently opens the zip and calls
parsePluginZipFile on the raw root, which fails for GitHub-style archives
wrapped under a single top-level folder (e.g., repo-ref/); update
parsePluginFromFile to inspect the archive entries from openZipArchive(…) and if
all entries share a common single-directory prefix (like "repo-ref/"), set that
as the root scope and pass the scoped view into parsePluginZipFile (or call
parsePluginZipFile with that prefix) so plugin.json can be found; ensure you
still close archive in the finally block and preserve the
Promise<ParsedPluginArchive> return.

In
`@x-pack/platform/plugins/shared/agent_builder/server/services/plugins/utils/sourcing/parse_github_url.test.ts`:
- Around line 34-48: The tests and parseGithubUrl implementation incorrectly
hard-code ref: 'main' for bare GitHub URLs; update the parseGithubUrl function
so it does not assume 'main'—either return ref as undefined (or null) for URLs
like 'https://github.com/owner/repo' or make parseGithubUrl require an explicit
ref, and move default-branch resolution to a separate step that queries the
GitHub API when building archive URLs; update the tests
(parse_github_url.test.ts) to expect no default ref (or an error) instead of
'main' and add new tests for the separate default-branch resolution logic if you
implement the API lookup.

In
`@x-pack/platform/plugins/shared/agent_builder/server/services/plugins/utils/sourcing/parse_github_url.ts`:
- Around line 42-58: parseGithubUrl is incorrectly defaulting ref to 'main' for
bare repo URLs, which breaks repos with other default branches; modify
parseGithubUrl so it does not silently assume 'main'—either require callers to
supply /tree/{ref} for bare repo URLs (throw an error when ref is missing) or
defer defaulting until download time by returning ref as undefined and letting
the caller or a new helper (e.g., resolveDefaultBranch) query the GitHub API to
determine the repository's default branch before building the archive URL;
update the function signature and any callers of parseGithubUrl accordingly to
handle a possibly undefined ref.

In
`@x-pack/platform/plugins/shared/agent_builder/server/services/plugins/utils/sourcing/save_uploaded_file.ts`:
- Around line 21-30: The saveUploadedFile function currently awaits
pipeline(stream, writeStream) and if pipeline rejects the partially written
tmp/<uuid>.zip is never removed; wrap the pipeline call in try/catch so that on
any error you call deleteFile(fileName, { volume: VOLUME }) (or the cleanup
logic used in the success path) before rethrowing the error, ensuring you still
reference the same fileName/fullPath from getSafePath/createWriteStream;
optionally also ensure writeStream is properly closed/destroyed in the
catch/finally block to avoid resource leaks.

In
`@x-pack/platform/plugins/shared/agent_builder/server/services/skills/persisted/client/client.ts`:
- Around line 138-153: bulkCreate is incorrectly promoting the business key
attributes.id to the Elasticsearch _id (in bulkCreate -->
storage.getClient().bulk), making the id global across spaces; change bulkCreate
to use the same storage-id strategy as create() by not hard-coding _id (let ES
assign an opaque id) or by constructing a space-qualified/storage-generated id
via the same helper used by create(), keeping createAttributes and space-scoping
intact so duplicates fail instead of clobbering other-space documents.

---

Nitpick comments:
In `@oas_docs/output/kibana.yaml`:
- Around line 3236-3245: The code samples under x-codeSamples use the
placeholder {id} but the OpenAPI path parameter is named pluginId; update both
examples (the curl and Console entries) to use {pluginId} so the placeholder
matches the parameter name (look for x-codeSamples, the curl "-X DELETE" sample
and the Console "DELETE kbn://api/agent_builder/plugins/{id}" sample and replace
{id} with {pluginId}).
- Around line 3303-3311: The code samples under x-codeSamples for the GET
operation use the placeholder "{id}" but the path parameter is named "pluginId";
update the sample sources in the GET x-codeSamples (both the curl sample and the
Console sample) to use "{pluginId}" so they match the operation parameter name
(e.g., curl -X GET "${KIBANA_URL}/api/agent_builder/plugins/{pluginId}" and GET
kbn://api/agent_builder/plugins/{pluginId}).

In
`@x-pack/platform/packages/shared/agent-builder/agent-builder-common/plugins/plugin_definition.ts`:
- Around line 21-57: The DTOs are mutable; make UnmanagedPluginAssets,
PluginManifestMetadata, and PluginDefinition use readonly fields and
ReadonlyArray for all arrays so consumers cannot mutate them: mark each property
in UnmanagedPluginAssets (commands, agents, hooks, mcp_servers, output_styles,
lsp_servers) as readonly ReadonlyArray<string>, mark
PluginManifestMetadata.keywords as readonly ReadonlyArray<string> (and other
optional fields readonly), and update PluginDefinition to use readonly for all
top-level properties (id, name, version, description, manifest, source_url,
skill_ids as ReadonlyArray<string>, unmanaged_assets, created_at, updated_at) so
the DTOs are fully immutable in memory.

In
`@x-pack/platform/packages/shared/agent-builder/agent-builder-server/skills/internal.ts`:
- Around line 62-65: The property plugin_id should be renamed to camelCase
pluginId across the code to follow coding guidelines; update the declaration in
internal.ts (replace plugin_id with pluginId), then update all references/usages
(type annotations, object literals, destructuring, assignments) to use pluginId
and adjust any serialization/deserialization or API mapping code that expects
snake_case (add mapping from plugin_id → pluginId or update serializers) and
update affected tests/types to preserve backward compatibility where needed.

In
`@x-pack/platform/plugins/shared/agent_builder/server/services/plugins/client/client.ts`:
- Around line 150-153: The code calls updateRequestToEs with document._source
using a non-null assertion; instead, guard against a missing source by checking
document._source before calling updateRequestToEs (e.g., if (!document._source)
{ /* handle: throw a descriptive Error or return a failed Result/log and exit
early */ }), then only call updateRequestToEs({ current: document._source,
update }) when present; update references to updatedAttributes and any
downstream logic to handle the early-failure path accordingly so you don't rely
on the non-null assertion.

In
`@x-pack/platform/plugins/shared/agent_builder/server/services/plugins/client/converters.test.ts`:
- Around line 84-87: The test is mutating the fixture with an any cast; instead
update the fixture helper to accept an optional skill_ids so you can call
createPluginDocument({ skill_ids: undefined }) instead of casting, or widen the
helper's input type to include skill_ids?: string[] | undefined; modify the
createPluginDocument signature (and its input type/interface) used by the test
so the edge-case can be modeled directly and remove the (doc._source as any)
mutation in the test.

In
`@x-pack/platform/plugins/shared/agent_builder/server/services/plugins/plugin_service.ts`:
- Around line 22-23: Remove the eslint-disable comment and address the empty
interface PluginsServiceSetup: either convert the empty interface to an
equivalent type alias (e.g., type PluginsServiceSetup = {}) or keep the
interface but add a self-documenting placeholder (a TODO comment plus a
non-exported placeholder property or JSDoc indicating future members) so linting
is satisfied without suppressing the rule; update the declaration for
PluginsServiceSetup accordingly and ensure no eslint-disable comment remains.

In
`@x-pack/platform/plugins/shared/agent_builder/server/services/plugins/utils/archive/create_scoped_archive.test.ts`:
- Around line 11-23: Extract the duplicated ZipArchive test double into a single
test helper by moving the createMockArchive function (which returns an object
implementing hasEntry, getEntryPaths, getEntryContent and close) into a shared
test utilities module and replace the local definitions in both tests with
imports of that helper; ensure the helper exports createMockArchive and uses the
same function signature (files: Record<string,string>) so existing tests
referencing createMockArchive, ZipArchive, getEntryContent, hasEntry and close
continue to work without further changes.

In
`@x-pack/platform/plugins/shared/agent_builder/server/services/plugins/utils/archive/open_zip_archive.test.ts`:
- Around line 14-18: The helper createTestZip currently encodes every entry as
UTF-8 text which prevents true byte-for-byte tests; modify createTestZip so it
accepts file contents as either strings or Buffers and when adding entries to
the yazl.ZipFile call zipFile.addBuffer(...) with the raw Buffer when the value
is a Buffer, otherwise convert strings with Buffer.from(value, 'utf-8');
reference createTestZip and ZipArchive.getEntryContent in the test and then add
a binary fixture like Buffer.from([0x00, 0xff, 0x0a]) to the byte-preservation
test so the test verifies exact bytes are preserved.
- Around line 98-104: The test is asserting a synchronous throw but
getEntryContent() returns a Promise; change the assertion to expect the promise
to reject: call archive!.getEntryContent('does-not-exist.txt') and use Jest's
.rejects.toThrow(/not found in archive/) (keeping the same regex) so the test
verifies the async contract of getEntryContent from openZipArchive; update the
test invocation accordingly to await or return the expect(...).rejects chain.

In
`@x-pack/platform/plugins/shared/agent_builder/server/services/plugins/utils/sourcing/download_plugin.test.ts`:
- Around line 11-19: The test mocks currently forward parameters as (...args:
unknown[]) which loses the real function signatures for openZipArchive and
parsePluginZipFile; replace the unknown[] passthroughs with the actual parameter
types by using Parameters<typeof openZipArchive> and Parameters<typeof
parsePluginZipFile> (or import the functions and use their parameter types) when
defining the mock factories so the mockOpenZipArchive and mockParsePluginZipFile
preserve the real contracts; keep the PluginArchiveError class export as-is.

In
`@x-pack/platform/plugins/shared/agent_builder/server/services/skills/skill_service.test.ts`:
- Around line 30-33: The mocks for the persisted-client need async defaults:
change the plain jest.fn() for bulkCreate and deleteByPluginId to resolved mocks
so they return a Promise (e.g., jest.fn().mockResolvedValue(undefined) or an
appropriate resolved value) instead of undefined synchronously; update the mock
declarations for bulkCreate and deleteByPluginId in the test file so they use
mockResolvedValue(...) while leaving update and delete as-is.

In
`@x-pack/platform/plugins/shared/agent_builder/server/services/skills/utils.ts`:
- Around line 15-30: internalToPublicDefinition always includes the plugin_id
key even when undefined; change it to only add plugin_id when skill.plugin_id is
set (e.g., check for null/undefined) so the returned plain object omits the key
for non-plugin-managed skills—update the internalToPublicDefinition return
construction to conditionally include plugin_id (keeping the rest of the fields
and tool_ids await logic intact).

In `@x-pack/platform/test/agent_builder_api_integration/apis/plugins/index.ts`:
- Around line 10-14: The exported unnamed function lacks an explicit return type
and should be a typed const-arrow export; change the default export to a named
const arrow like e.g. const pluginsApi = ({ loadTestFile }: FtrProviderContext):
void => { describe('Plugins API', function () {
loadTestFile(require.resolve('./installation')); }); }; and then export default
pluginsApi; ensuring the parameter is typed with FtrProviderContext and the
function return type is explicitly void.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: cdf55c63-36e9-4027-bd0f-a00671637b39

📥 Commits

Reviewing files that changed from the base of the PR and between 059dbb5 and a1282ca.

📒 Files selected for processing (71)
  • oas_docs/output/kibana.serverless.yaml
  • oas_docs/output/kibana.yaml
  • x-pack/platform/packages/shared/agent-builder/agent-builder-common/base/errors.ts
  • x-pack/platform/packages/shared/agent-builder/agent-builder-common/index.ts
  • x-pack/platform/packages/shared/agent-builder/agent-builder-common/plugins/index.ts
  • x-pack/platform/packages/shared/agent-builder/agent-builder-common/plugins/parsing.ts
  • x-pack/platform/packages/shared/agent-builder/agent-builder-common/plugins/plugin_definition.ts
  • x-pack/platform/packages/shared/agent-builder/agent-builder-common/skills/definition.ts
  • x-pack/platform/packages/shared/agent-builder/agent-builder-server/skills/internal.ts
  • x-pack/platform/plugins/shared/agent_builder/common/http_api/plugins.ts
  • x-pack/platform/plugins/shared/agent_builder/moon.yml
  • x-pack/platform/plugins/shared/agent_builder/server/config.ts
  • x-pack/platform/plugins/shared/agent_builder/server/plugin.ts
  • x-pack/platform/plugins/shared/agent_builder/server/routes/examples/plugins_delete.yaml
  • x-pack/platform/plugins/shared/agent_builder/server/routes/examples/plugins_get_by_id.yaml
  • x-pack/platform/plugins/shared/agent_builder/server/routes/examples/plugins_install.yaml
  • x-pack/platform/plugins/shared/agent_builder/server/routes/examples/plugins_list.yaml
  • x-pack/platform/plugins/shared/agent_builder/server/routes/index.ts
  • x-pack/platform/plugins/shared/agent_builder/server/routes/plugins.ts
  • x-pack/platform/plugins/shared/agent_builder/server/services/create_services.ts
  • x-pack/platform/plugins/shared/agent_builder/server/services/plugins/client/client.test.ts
  • x-pack/platform/plugins/shared/agent_builder/server/services/plugins/client/client.ts
  • x-pack/platform/plugins/shared/agent_builder/server/services/plugins/client/converters.test.ts
  • x-pack/platform/plugins/shared/agent_builder/server/services/plugins/client/converters.ts
  • x-pack/platform/plugins/shared/agent_builder/server/services/plugins/client/index.ts
  • x-pack/platform/plugins/shared/agent_builder/server/services/plugins/client/storage.ts
  • x-pack/platform/plugins/shared/agent_builder/server/services/plugins/client/types.ts
  • x-pack/platform/plugins/shared/agent_builder/server/services/plugins/index.ts
  • x-pack/platform/plugins/shared/agent_builder/server/services/plugins/plugin_service.test.ts
  • x-pack/platform/plugins/shared/agent_builder/server/services/plugins/plugin_service.ts
  • x-pack/platform/plugins/shared/agent_builder/server/services/plugins/utils/archive/create_scoped_archive.test.ts
  • x-pack/platform/plugins/shared/agent_builder/server/services/plugins/utils/archive/create_scoped_archive.ts
  • x-pack/platform/plugins/shared/agent_builder/server/services/plugins/utils/archive/index.ts
  • x-pack/platform/plugins/shared/agent_builder/server/services/plugins/utils/archive/open_zip_archive.test.ts
  • x-pack/platform/plugins/shared/agent_builder/server/services/plugins/utils/archive/open_zip_archive.ts
  • x-pack/platform/plugins/shared/agent_builder/server/services/plugins/utils/index.ts
  • x-pack/platform/plugins/shared/agent_builder/server/services/plugins/utils/parsing/index.ts
  • x-pack/platform/plugins/shared/agent_builder/server/services/plugins/utils/parsing/parse_plugin_zip_file.test.ts
  • x-pack/platform/plugins/shared/agent_builder/server/services/plugins/utils/parsing/parse_plugin_zip_file.ts
  • x-pack/platform/plugins/shared/agent_builder/server/services/plugins/utils/parsing/parse_skill_file.test.ts
  • x-pack/platform/plugins/shared/agent_builder/server/services/plugins/utils/parsing/parse_skill_file.ts
  • x-pack/platform/plugins/shared/agent_builder/server/services/plugins/utils/sourcing/download_plugin.test.ts
  • x-pack/platform/plugins/shared/agent_builder/server/services/plugins/utils/sourcing/download_plugin.ts
  • x-pack/platform/plugins/shared/agent_builder/server/services/plugins/utils/sourcing/index.ts
  • x-pack/platform/plugins/shared/agent_builder/server/services/plugins/utils/sourcing/parse_github_url.test.ts
  • x-pack/platform/plugins/shared/agent_builder/server/services/plugins/utils/sourcing/parse_github_url.ts
  • x-pack/platform/plugins/shared/agent_builder/server/services/plugins/utils/sourcing/resolve_plugin_url.test.ts
  • x-pack/platform/plugins/shared/agent_builder/server/services/plugins/utils/sourcing/resolve_plugin_url.ts
  • x-pack/platform/plugins/shared/agent_builder/server/services/plugins/utils/sourcing/save_uploaded_file.test.ts
  • x-pack/platform/plugins/shared/agent_builder/server/services/plugins/utils/sourcing/save_uploaded_file.ts
  • x-pack/platform/plugins/shared/agent_builder/server/services/skills/persisted/client/client.ts
  • x-pack/platform/plugins/shared/agent_builder/server/services/skills/persisted/client/converters.ts
  • x-pack/platform/plugins/shared/agent_builder/server/services/skills/persisted/client/storage.ts
  • x-pack/platform/plugins/shared/agent_builder/server/services/skills/persisted/client/types.ts
  • x-pack/platform/plugins/shared/agent_builder/server/services/skills/persisted/converter.test.ts
  • x-pack/platform/plugins/shared/agent_builder/server/services/skills/persisted/converter.ts
  • x-pack/platform/plugins/shared/agent_builder/server/services/skills/persisted/provider.test.ts
  • x-pack/platform/plugins/shared/agent_builder/server/services/skills/skill_service.test.ts
  • x-pack/platform/plugins/shared/agent_builder/server/services/skills/utils.test.ts
  • x-pack/platform/plugins/shared/agent_builder/server/services/skills/utils.ts
  • x-pack/platform/plugins/shared/agent_builder/server/services/types.ts
  • x-pack/platform/plugins/shared/agent_builder/tsconfig.json
  • x-pack/platform/test/agent_builder_api_integration/apis/index.ts
  • x-pack/platform/test/agent_builder_api_integration/apis/plugins/assets/test-plugin/plugin.json
  • x-pack/platform/test/agent_builder_api_integration/apis/plugins/assets/test-plugin/skills/test-skill/SKILL.md
  • x-pack/platform/test/agent_builder_api_integration/apis/plugins/assets/test-plugin/skills/test-skill/reference.txt
  • x-pack/platform/test/agent_builder_api_integration/apis/plugins/index.ts
  • x-pack/platform/test/agent_builder_api_integration/apis/plugins/installation.ts
  • x-pack/platform/test/agent_builder_api_integration/configs/config.stateful.ts
  • x-pack/platform/test/agent_builder_api_integration/utils/plugins_server/index.ts
  • x-pack/platform/test/agent_builder_api_integration/utils/plugins_server/plugins_server.ts

Comment on lines +51 to +54
/**
* If this skill was installed from a plugin, the plugin name.
*/
plugin_id?: string;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Minor documentation inconsistency: field name vs. comment description.

The field is named plugin_id but the comments state it holds "the plugin name". Clarify whether this should be:

  • plugin_id holding a unique plugin identifier, or
  • plugin_name holding the human-readable plugin name

This affects API consumers' understanding of the expected value.

Suggested documentation fix (if plugin name is correct)
   /**
-   * If this skill was installed from a plugin, the plugin name.
+   * If this skill was installed from a plugin, the plugin identifier.
    */
   plugin_id?: string;

Or rename to plugin_name if it truly stores the name rather than an ID.

Also applies to: 85-88

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@x-pack/platform/packages/shared/agent-builder/agent-builder-common/skills/definition.ts`
around lines 51 - 54, The comment and field name are inconsistent: the field
plugin_id currently says "the plugin name"—decide whether this property should
be an identifier or a human-readable name; then update the TypeScript definition
and JSDoc accordingly (either rename plugin_id to plugin_name everywhere in this
file or change the comment to state "the plugin id/unique identifier") and apply
the same change to the other occurrence around lines 85-88 so consumers see a
consistent property name and description; reference the property symbol
plugin_id (or plugin_name if renaming) when making the edits.

Copy link
Copy Markdown
Contributor

@ppisljar ppisljar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pre-approving, some of the reviewer bot comments look relevant

Copy link
Copy Markdown
Member

@leemthompo leemthompo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just couple ideas for adding a bit more info to the List and Install API descriptions for user context

But otherwise OAS LGTM

security: AGENT_BUILDER_READ_SECURITY,
access: 'public',
summary: 'List plugins',
description: 'List all installed plugins.',
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
description: 'List all installed plugins.',
description: 'List all installed plugins and their managed assets. Plugins are installable packages that bundle agent capabilities such as skills, following the [Claude agent plugin specification](https://code.claude.com/docs/en/plugins).',

can we do something like this (caveat not sure if formatting is correct here, maybe need to escape the link syntax or something?)

Right now if I land on these docs I have no idea what plugins are:
Image

Once we get around to publishing narrative docs about this, we can add links, but think a bit of context here is helpful.

security: AGENT_BUILDER_WRITE_SECURITY,
access: 'public',
summary: 'Install a plugin',
description: 'Install a plugin from a GitHub URL or a direct zip URL.',
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
description: 'Install a plugin from a GitHub URL or a direct zip URL.',
description: 'Install a plugin from a [GitHub Claude plugin URL](https://code.claude.com/docs/en/plugins) or a direct ZIP URL. Plugins bundle agent capabilities such as skills. Installed assets are read-only and can only be removed by deleting the plugin.',

Again caveat the formatting, but same motivation as first suggestion :)

Copy link
Copy Markdown
Member

@florent-leborgne florent-leborgne left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM for docs

@pgayvallet
Copy link
Copy Markdown
Contributor Author

@elasticmachine merge upstream

@elasticmachine
Copy link
Copy Markdown
Contributor

⏳ Build in-progress, with failures

Failed CI Steps

Test Failures

  • [job] [logs] Scout: [ security / entity_store ] plugin / local-stateful-classic - Entity Store CCS logs extraction (test against local instance) - Should run CCS extraction for generic and write to updates then latest index
  • [job] [logs] Scout: [ security / entity_store ] plugin / local-stateful-classic - Entity Store CCS logs extraction (test against local instance) - Should run CCS extraction for generic and write to updates then latest index
  • [job] [logs] Scout: [ security / entity_store ] plugin / local-stateful-classic - Entity Store CCS logs extraction (test against local instance) - Should run CCS extraction for host and write to updates then latest index
  • [job] [logs] Scout: [ security / entity_store ] plugin / local-stateful-classic - Entity Store CCS logs extraction (test against local instance) - Should run CCS extraction for host and write to updates then latest index
  • [job] [logs] Scout: [ security / entity_store ] plugin / local-stateful-classic - Entity Store CCS logs extraction (test against local instance) - Should run CCS extraction for service and write to updates then latest index
  • [job] [logs] Scout: [ security / entity_store ] plugin / local-stateful-classic - Entity Store CCS logs extraction (test against local instance) - Should run CCS extraction for service and write to updates then latest index
  • [job] [logs] Scout: [ security / entity_store ] plugin / local-stateful-classic - Entity Store CCS logs extraction (test against local instance) - Should run CCS extraction for user and write to updates then latest index
  • [job] [logs] Scout: [ security / entity_store ] plugin / local-stateful-classic - Entity Store CCS logs extraction (test against local instance) - Should run CCS extraction for user and write to updates then latest index

History

@pgayvallet
Copy link
Copy Markdown
Contributor Author

@elasticmachine merge upstream

@pgayvallet pgayvallet enabled auto-merge (squash) March 11, 2026 06:37
@pgayvallet pgayvallet merged commit 1194cbd into elastic:main Mar 11, 2026
18 checks passed
mbondyra added a commit to mbondyra/kibana that referenced this pull request Mar 11, 2026
…e_fix

* commit '565f7545c422192218b803874fbdf93e8d8f08ee': (27 commits)
  [Lens API] ESQL schema for XY separately for Agent and some small token optimizations (elastic#256885)
  Fix "Accessing resource attributes before async attributes settled" telemetry error (elastic#256880)
  [Security Solution][Attacks/Alerts][Attacks page][Table section] Preserver "Sort by" state on Attacks page (elastic#256717) (elastic#256795)
  [APM] Improve redirect with default date range guard (elastic#256887)
  [Security Solution][Attacks/Alerts][Attacks page][Table section] Add assignees avatars to the group component (elastic#250126) (elastic#256901)
  [Docs] add xpack.alerting.rules.maxScheduledPerMinute setting description (elastic#257041)
  [SO] Fix non-deterministic ordering in nested find API integration tests (elastic#256447)
  [Write-restricted dashboards] Update user profile retrieval for getShouldAddAccessControl (elastic#255065)
  [One Workflow] Add Scout API test scaffold and execution tests (elastic#256300)
  [Fleet] add use_apm if dynamic_signal_types are enabled (elastic#256429)
  [Fleet] ignore data streams starting with `.` in Fleet API (elastic#256625)
  [ES|QL] METRICS_INFO support: columns_after & summary (elastic#256758)
  [Agent Builder] Agent plugins: initial installation support (elastic#256478)
  [Streams] Add field descriptions and documentation-only field overrides (elastic#255136)
  [api-docs] 2026-03-11 Daily api_docs build (elastic#257023)
  [Security Solution] fix alerts page infinite loading state due to data view error (elastic#256983)
  [Logging] Add `service.*` global fields (elastic#256878)
  [Canvas] Apply embeddable transforms to embeddable elements (elastic#252191)
  [table_list_view_table] stabilize jest test (elastic#254991)
  [Obs AI] get_index_info: add unit tests (elastic#256802)
  ...
sorenlouv pushed a commit that referenced this pull request Mar 17, 2026
## Summary

Fix https://github.com/elastic/search-team/issues/13213

Add initial support for installing [Claude agent skill
plugins](https://code.claude.com/docs/en/plugins) (and Agent Builder
plugins - which will be a superset of that spec)

We currently support installing from three different sources:
- GH Claude plugin url
- e.g.
`https://github.com/anthropics/financial-services-plugins/tree/main/financial-analysis`
or
`https://github.com/anthropics/financial-services-plugins/blob/main/financial-analysis/.claude-plugin/plugin.json`
- Remote zip url 
  - e.g. `https://my-server/my-plugin.zip`
- Uploading the zip file directly 

### What do we support from the Claude plugin spec?

This initial implementation supports the following asset types:
- skills (and associated file assets).

Support planned in follow ups:
- commands 
- MCP servers (if we can)

Support not planned:
- agents
- hooks
- everything else

### Plugins and managed assets

Plugin managed assets (so skills only atm) are considered read-only, and
can only be deleted by deleting the plugin.

They also won't be able to be "directly" associated with an agent -
instead, you add the plugin to the agent, which will expose all its
assets (note: this is out of scope of the current PR)

### How to test

#### Installing a plugin from url

Install the plugin:

```
POST kbn://api/agent_builder/plugins/install
{
    "url": "https://github.com/anthropics/financial-services-plugins/tree/main/financial-analysis",
}
```

List the plugins:

```
GET kbn://api/agent_builder/plugins
```

Check that the plugin's skills are available:

```
GET kbn://api/agent_builder/skills
```

**Note:** the skills can't be added to an agent atm, it will be done in
a follow-up


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

# Release Notes

* **New Features**
* Added plugin management system with new API endpoints for listing,
retrieving, deleting, and installing plugins
* Plugins can be installed from GitHub repositories, direct ZIP URLs, or
uploaded files
  * Plugin-managed skills are marked as read-only to ensure integrity
  * Support for custom GitHub server configuration for plugin sources
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Liam Thompson <leemthompo@gmail.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent-builder:skip-smoke-tests backport:skip This PR does not require backporting feature:agent-builder Identify agent builder functionalities to be grouped together for release notes release_note:skip Skip the PR/issue when compiling release notes reviewer:coderabbit PR review with CodeRabbit v9.4.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants