Skip to content

Commit 83b7728

Browse files
[onechat] implement tool namespaces (#233854)
## Summary Implement the tool namespacing strategy as defined in the corresponding RFC - move/rename our tools to the `platform.core.*` namespace - add a `readonly` attribute on all tool definitions (built-in tools are readonly, users tools are not) - change tool validation logic to handle protected namespaces (and get rid of the old `.prefix` format we were using) - remove labels from our tools ## Screenshots **listing** <img width="1251" height="813" alt="Screenshot 2025-09-03 at 17 04 14" src="https://github.com/user-attachments/assets/d47e7489-8487-4306-8c20-0d9b080db16c" /> **validation** <img width="1242" height="201" alt="Screenshot 2025-09-03 at 17 05 27" src="https://github.com/user-attachments/assets/01720d35-327b-4193-96b8-676c7b679bf0" /> --------- Co-authored-by: kibanamachine <[email protected]>
1 parent 9dc817d commit 83b7728

File tree

36 files changed

+370
-158
lines changed

36 files changed

+370
-158
lines changed

x-pack/platform/packages/shared/onechat/onechat-common/index.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,10 @@ export {
1010
ToolType,
1111
type ToolDefinition,
1212
type ToolDefinitionWithSchema,
13-
builtInToolIdPrefix,
14-
builtinToolIds,
15-
builtinTags,
13+
platformCoreTools,
1614
defaultAgentToolIds,
1715
editableToolTypes,
1816
isReservedToolId,
19-
isBuiltInToolId,
2017
type ByIdsToolSelection,
2118
type ToolSelection,
2219
isByIdsToolSelection,
@@ -31,15 +28,20 @@ export {
3128
type EsqlToolDefinition,
3229
type EsqlToolDefinitionWithSchema,
3330
EsqlToolFieldType,
34-
idRegexp,
31+
toolIdRegexp,
32+
toolIdMaxLength,
3533
activeToolsCountWarningThreshold,
34+
validateToolId,
3635
ToolResultType,
3736
type ToolResult,
3837
type ErrorResult,
3938
type QueryResult,
4039
type ResourceResult,
4140
type TabularDataResult,
4241
type OtherResult,
42+
internalNamespaces as toolNamespaces,
43+
protectedNamespaces as toolReservedNamespaces,
44+
isInProtectedNamespace,
4345
} from './tools';
4446
export {
4547
OnechatErrorCode,

x-pack/platform/packages/shared/onechat/onechat-common/tools/constants.ts

Lines changed: 17 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,23 @@
66
*/
77

88
import { ToolType } from './definition';
9+
import { internalNamespaces } from './namespaces';
10+
11+
const platformCoreTool = (toolName: string) => {
12+
return `${internalNamespaces.platformCore}.${toolName}`;
13+
};
914

1015
/**
1116
* Ids of built-in onechat tools
1217
*/
13-
export const builtinToolIds = {
14-
indexExplorer: '.index_explorer',
15-
search: '.search',
16-
listIndices: '.list_indices',
17-
getIndexMapping: '.get_index_mapping',
18-
getDocumentById: '.get_document_by_id',
19-
generateEsql: '.generate_esql',
20-
executeEsql: '.execute_esql',
18+
export const platformCoreTools = {
19+
indexExplorer: platformCoreTool('index_explorer'),
20+
search: platformCoreTool('search'),
21+
listIndices: platformCoreTool('list_indices'),
22+
getIndexMapping: platformCoreTool('get_index_mapping'),
23+
getDocumentById: platformCoreTool('get_document_by_id'),
24+
generateEsql: platformCoreTool('generate_esql'),
25+
executeEsql: platformCoreTool('execute_esql'),
2126
} as const;
2227

2328
/**
@@ -26,25 +31,12 @@ export const builtinToolIds = {
2631
export const editableToolTypes: ToolType[] = [ToolType.esql, ToolType.index_search];
2732

2833
export const defaultAgentToolIds = [
29-
builtinToolIds.search,
30-
builtinToolIds.listIndices,
31-
builtinToolIds.getIndexMapping,
32-
builtinToolIds.getDocumentById,
34+
platformCoreTools.search,
35+
platformCoreTools.listIndices,
36+
platformCoreTools.getIndexMapping,
37+
platformCoreTools.getDocumentById,
3338
];
3439

35-
export const builtInToolIdPrefix = '.';
36-
export const reservedKeywords = ['new'];
37-
38-
/**
39-
* Common set of tags used for platform tools.
40-
*/
41-
export const builtinTags = {
42-
/**
43-
* Tag associated to tools related to data retrieval
44-
*/
45-
retrieval: 'retrieval',
46-
} as const;
47-
4840
/**
4941
* The number of active tools that will trigger a warning in the UI.
5042
* Agent will perform poorly if it has too many tools.

x-pack/platform/packages/shared/onechat/onechat-common/tools/definition.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,12 @@ export interface ToolDefinition<TConfig extends object = Record<string, unknown>
4343
* The description for this tool, which will be exposed to the LLM.
4444
*/
4545
description: string;
46+
/**
47+
* Indicate whether this tool is editable by users or not.
48+
*/
49+
readonly: boolean;
4650
/**
4751
* Optional list of tags attached to this tool.
48-
* For built-in tools, this is specified during registration.
4952
*/
5053
tags: string[];
5154
/**

x-pack/platform/packages/shared/onechat/onechat-common/tools/index.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,9 @@
66
*/
77

88
export { ToolType, type ToolDefinition, type ToolDefinitionWithSchema } from './definition';
9-
export { isReservedToolId, isBuiltInToolId, idRegexp } from './tool_ids';
9+
export { isReservedToolId, validateToolId, toolIdRegexp, toolIdMaxLength } from './tool_ids';
1010
export {
11-
builtinToolIds,
12-
builtinTags,
13-
builtInToolIdPrefix,
11+
platformCoreTools,
1412
activeToolsCountWarningThreshold,
1513
defaultAgentToolIds,
1614
editableToolTypes,
@@ -49,3 +47,9 @@ export {
4947
type TabularDataResult,
5048
type OtherResult,
5149
} from './tool_result';
50+
export {
51+
internalNamespaces,
52+
protectedNamespaces,
53+
isInProtectedNamespace,
54+
hasProtectedNamespaceName,
55+
} from './namespaces';
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { isInProtectedNamespace } from './namespaces';
9+
10+
describe('isInProtectedNamespace', () => {
11+
it('returns true when the tool id is inside a protected namespace', () => {
12+
expect(isInProtectedNamespace('platform.core.some_tool')).toBe(true);
13+
});
14+
15+
it('returns true when the tool id is nested in a protected namespace', () => {
16+
expect(isInProtectedNamespace('platform.core.nested.some_tool')).toBe(true);
17+
});
18+
19+
it('returns false when the tool id is inside a part of a protected namespace', () => {
20+
expect(isInProtectedNamespace('platform.some_tool')).toBe(false);
21+
});
22+
});
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
/**
9+
* List of internally used namespaces
10+
* Note: those are not necessarily all protected.
11+
*/
12+
export const internalNamespaces = {
13+
platformCore: 'platform.core',
14+
} as const;
15+
16+
/**
17+
* List of protected namespaces which can only be used by internal tools.
18+
*/
19+
export const protectedNamespaces: string[] = [internalNamespaces.platformCore];
20+
21+
/**
22+
* Checks if the provided tool name belongs to a protected namespace.
23+
*/
24+
export const isInProtectedNamespace = (toolName: string) => {
25+
for (const namespace of protectedNamespaces) {
26+
if (toolName.startsWith(`${namespace}.`)) {
27+
return true;
28+
}
29+
}
30+
return false;
31+
};
32+
33+
/**
34+
* Checks if the provided tool name belongs to a protected namespace.
35+
*/
36+
export const hasProtectedNamespaceName = (toolName: string) => {
37+
return protectedNamespaces.includes(toolName);
38+
};
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { toolIdRegexp, validateToolId, toolIdMaxLength } from './tool_ids';
9+
import { protectedNamespaces } from './namespaces';
10+
11+
describe('validateToolId', () => {
12+
test('returns undefined for valid id (non built-in)', () => {
13+
expect(validateToolId({ toolId: 'mytool', builtIn: false })).toBeUndefined();
14+
15+
expect(validateToolId({ toolId: 'foo_bar-baz.qux_123', builtIn: false })).toBeUndefined();
16+
});
17+
18+
test('fails on invalid format (regexp)', () => {
19+
const invalids = [
20+
'',
21+
'.mytool',
22+
'mytool.',
23+
'core..mytool',
24+
'MyTool',
25+
'core.MyTool',
26+
'-tool',
27+
'tool-',
28+
'_tool',
29+
'tool_',
30+
'tool..id',
31+
'tool..',
32+
'tool.',
33+
'.tool',
34+
'tool#id',
35+
'tool/id',
36+
];
37+
38+
for (const toolId of invalids) {
39+
const error = validateToolId({ toolId, builtIn: false });
40+
expect(error).toBe(
41+
'Tool ids must start and end with a letter or number, and can only contain lowercase letters, numbers, dots, hyphens and underscores'
42+
);
43+
}
44+
});
45+
46+
test('fails on toolId exceeding max length', () => {
47+
const overMax = 'a'.repeat(toolIdMaxLength + 1);
48+
const error = validateToolId({ toolId: overMax, builtIn: false });
49+
expect(error).toBe(`Tool ids are limited to ${toolIdMaxLength} characters.`);
50+
});
51+
52+
test('fails when toolId equals a protected namespace name', () => {
53+
const protectedNamespaceName = protectedNamespaces[0];
54+
const error = validateToolId({ toolId: protectedNamespaceName, builtIn: false });
55+
expect(error).toBe('Tool id cannot have the same name as a reserved namespaces');
56+
});
57+
58+
test('fails when non built-in tool uses a protected namespace', () => {
59+
const protectedNamespaceName = protectedNamespaces[0];
60+
const toolId = `${protectedNamespaceName}.mytool`;
61+
const error = validateToolId({ toolId, builtIn: false });
62+
expect(error).toBe('Tool id is using a protected namespaces.');
63+
});
64+
65+
test('allows built-in tool to use a protected namespace', () => {
66+
const protectedNamespaceName = protectedNamespaces[0];
67+
const toolId = `${protectedNamespaceName}.internal_tool`;
68+
const error = validateToolId({ toolId, builtIn: true });
69+
expect(error).toBeUndefined();
70+
});
71+
});
72+
73+
describe('toolId regexp', () => {
74+
const validToolIds = [
75+
'mytool',
76+
'core.mytool',
77+
'core.foo.mytool',
78+
'a',
79+
'a.b',
80+
'tool_1',
81+
'tool-1',
82+
'foo_bar-baz.qux_123',
83+
'a1.b2.c3',
84+
'abc.def_ghi-jkl.mno',
85+
];
86+
87+
const invalidToolIds = [
88+
'', // empty string
89+
'.mytool', // starts with dot
90+
'mytool.', // ends with dot
91+
'core..mytool', // double dot
92+
'MyTool', // uppercase
93+
'core.MyTool', // uppercase segment
94+
'-tool', // starts with hyphen
95+
'tool-', // ends with hyphen
96+
'_tool', // starts with underscore
97+
'tool_', // ends with underscore
98+
'tool..id', // consecutive dots
99+
'tool..', // ends with dot
100+
'tool.', // ends with dot
101+
'.tool', // starts with dot
102+
'tool#id', // illegal char
103+
'tool/id', // illegal char
104+
];
105+
106+
test.each(validToolIds)('valid: %s', (toolId) => {
107+
expect(toolIdRegexp.test(toolId)).toBe(true);
108+
});
109+
110+
test.each(invalidToolIds)('invalid: %s', (toolId) => {
111+
expect(toolIdRegexp.test(toolId)).toBe(false);
112+
});
113+
});

x-pack/platform/packages/shared/onechat/onechat-common/tools/tool_ids.ts

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,49 @@
55
* 2.0.
66
*/
77

8-
import { builtInToolIdPrefix, reservedKeywords } from './constants';
8+
import { hasProtectedNamespaceName, isInProtectedNamespace } from './namespaces';
99

10-
export const idRegexp = /^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?$/;
10+
export const toolIdRegexp =
11+
/^(?:[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?)(?:\.(?:[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?))*$/;
12+
export const toolIdMaxLength = 64;
13+
14+
const reservedKeywords = ['new'];
1115

1216
/**
13-
* Check if the given ID is a built-in ID (starting with `.`)
17+
* Check if the given ID is a reserved ID
18+
* Atm this only checks for `new` because that's a value we're using for url paths on the UI.
1419
*/
15-
export const isBuiltInToolId = (id: string) => {
16-
return id.startsWith(builtInToolIdPrefix);
20+
export const isReservedToolId = (id: string) => {
21+
return reservedKeywords.includes(id);
1722
};
1823

1924
/**
20-
* Check if the given ID is a reserved ID
21-
* Atm this only checks for built-in IDs, but it will check for MCP and such later.
25+
* Validate that a tool id has the right format,
26+
* returning an error message if it fails the validation,
27+
* and undefined otherwise.
28+
*
29+
* @param toolId: the toolId to validate
30+
* @param builtIn: set to true if we're validating a built-in (internal) tool id.
2231
*/
23-
export const isReservedToolId = (id: string) => {
24-
return isBuiltInToolId(id) || reservedKeywords.includes(id);
32+
export const validateToolId = ({
33+
toolId,
34+
builtIn,
35+
}: {
36+
toolId: string;
37+
builtIn: boolean;
38+
}): string | undefined => {
39+
if (!toolIdRegexp.test(toolId)) {
40+
return `Tool ids must start and end with a letter or number, and can only contain lowercase letters, numbers, dots, hyphens and underscores`;
41+
}
42+
if (toolId.length > toolIdMaxLength) {
43+
return `Tool ids are limited to ${toolIdMaxLength} characters.`;
44+
}
45+
if (hasProtectedNamespaceName(toolId)) {
46+
return `Tool id cannot have the same name as a reserved namespaces`;
47+
}
48+
if (!builtIn) {
49+
if (isInProtectedNamespace(toolId)) {
50+
return `Tool id is using a protected namespaces.`;
51+
}
52+
}
2553
};

x-pack/platform/packages/shared/onechat/onechat-genai-utils/langchain/tools.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const createTool = (
2121
type: ToolType.builtin,
2222
description: '',
2323
configuration: {},
24+
readonly: false,
2425
tags: [],
2526
schema: z.object({}),
2627
execute: jest.fn(),

0 commit comments

Comments
 (0)