@illuma-ai/agents 1.4.0-alpha.3 → 1.4.0-alpha.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/providers/tools-server/ToolsServerCapabilityProvider.cjs +8 -1
- package/dist/cjs/providers/tools-server/ToolsServerCapabilityProvider.cjs.map +1 -1
- package/dist/cjs/providers/types.cjs.map +1 -1
- package/dist/cjs/tools/artifacts/schema.cjs +30 -7
- package/dist/cjs/tools/artifacts/schema.cjs.map +1 -1
- package/dist/cjs/tools/artifacts/tool.cjs +8 -2
- package/dist/cjs/tools/artifacts/tool.cjs.map +1 -1
- package/dist/cjs/tools/proxyTool.cjs +7 -5
- package/dist/cjs/tools/proxyTool.cjs.map +1 -1
- package/dist/esm/providers/tools-server/ToolsServerCapabilityProvider.mjs +8 -1
- package/dist/esm/providers/tools-server/ToolsServerCapabilityProvider.mjs.map +1 -1
- package/dist/esm/providers/types.mjs.map +1 -1
- package/dist/esm/tools/artifacts/schema.mjs +30 -7
- package/dist/esm/tools/artifacts/schema.mjs.map +1 -1
- package/dist/esm/tools/artifacts/tool.mjs +8 -2
- package/dist/esm/tools/artifacts/tool.mjs.map +1 -1
- package/dist/esm/tools/proxyTool.mjs +7 -5
- package/dist/esm/tools/proxyTool.mjs.map +1 -1
- package/dist/types/providers/tools-server/ToolsServerCapabilityProvider.d.ts +14 -0
- package/dist/types/providers/types.d.ts +14 -0
- package/dist/types/tools/proxyTool.d.ts +7 -0
- package/package.json +1 -1
- package/src/providers/__tests__/ToolsServerCapabilityProvider.test.ts +63 -0
- package/src/providers/tools-server/ToolsServerCapabilityProvider.ts +28 -0
- package/src/providers/types.ts +17 -0
- package/src/tools/artifacts/__tests__/tool.test.ts +31 -15
- package/src/tools/artifacts/schema.ts +36 -13
- package/src/tools/artifacts/tool.ts +28 -16
- package/src/tools/artifacts/types.ts +18 -5
- package/src/tools/proxyTool.ts +25 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.mjs","sources":["../../../../src/tools/artifacts/schema.ts"],"sourcesContent":["import { z } from 'zod';\n\nexport const ARTIFACT_WRITE_ACTIONS = ['write'
|
|
1
|
+
{"version":3,"file":"schema.mjs","sources":["../../../../src/tools/artifacts/schema.ts"],"sourcesContent":["import { z } from 'zod';\n\nexport const ARTIFACT_WRITE_ACTIONS = [\n 'write',\n 'edit',\n 'verify',\n 'delete',\n] as const;\nexport const CONTENT_READ_ACTIONS = ['read', 'search', 'list', 'info'] as const;\n\nexport const artifactToolSchema = z.object({\n action: z\n .enum(ARTIFACT_WRITE_ACTIONS)\n .describe(\n 'Authoring action: write (create/overwrite), edit (str_replace), verify (syntax check), delete (remove).'\n ),\n content_id: z\n .string()\n .optional()\n .describe(\n 'ID of the artifact entry. Required for edit/verify/delete; optional for write (supply to overwrite an existing entry).'\n ),\n\n // write\n content: z\n .string()\n .optional()\n .describe('Full file content (required for write action).'),\n name: z\n .string()\n .optional()\n .describe(\n 'Filename for the new entry (required when creating). MUST include the correct file extension — the extension drives the preview template.'\n ),\n\n // edit (str_replace)\n old_str: z\n .string()\n .optional()\n .describe('Exact string to find and replace (required for edit).'),\n new_str: z\n .string()\n .optional()\n .describe('Replacement string (required for edit).'),\n replace_all: z\n .boolean()\n .optional()\n .describe(\n 'edit: when true, replaces every occurrence of old_str. When false (default) and old_str matches more than one location, the edit is refused.'\n ),\n});\n\nexport const contentReaderSchema = z.object({\n action: z\n .enum(CONTENT_READ_ACTIONS)\n .describe(\n 'Read-only action: read (lines), search (regex), list (all entries), info (metadata).'\n ),\n content_id: z\n .string()\n .optional()\n .describe(\n 'ID of the content entry. Required for read/search/info. Omit for list.'\n ),\n\n // read pagination\n start_line: z.number().optional().describe('1-based start line for reading.'),\n end_line: z.number().optional().describe('1-based end line (inclusive).'),\n\n // search\n pattern: z\n .string()\n .optional()\n .describe('Regex pattern (required for search).'),\n flags: z\n .string()\n .optional()\n .describe('Regex flags (e.g., \"i\" for case-insensitive).'),\n context: z\n .number()\n .optional()\n .describe('Lines of context around each match.'),\n\n // shared pagination\n offset: z\n .number()\n .optional()\n .describe('Offset for read or search pagination.'),\n limit: z\n .number()\n .optional()\n .describe('Max lines (read) or matches (search).'),\n});\n\nexport const ARTIFACT_TOOL_NAME = 'artifact_tool';\nexport const CONTENT_READER_NAME = 'content_reader';\n\nexport type ArtifactToolInput = z.infer<typeof artifactToolSchema>;\nexport type ContentReaderInput = z.infer<typeof contentReaderSchema>;\n"],"names":[],"mappings":";;AAEO,MAAM,sBAAsB,GAAG;IACpC,OAAO;IACP,MAAM;IACN,QAAQ;IACR,QAAQ;;AAEH,MAAM,oBAAoB,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM;AAE9D,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;AACzC,IAAA,MAAM,EAAE;SACL,IAAI,CAAC,sBAAsB;SAC3B,QAAQ,CACP,yGAAyG,CAC1G;AACH,IAAA,UAAU,EAAE;AACT,SAAA,MAAM;AACN,SAAA,QAAQ;SACR,QAAQ,CACP,wHAAwH,CACzH;;AAGH,IAAA,OAAO,EAAE;AACN,SAAA,MAAM;AACN,SAAA,QAAQ;SACR,QAAQ,CAAC,gDAAgD,CAAC;AAC7D,IAAA,IAAI,EAAE;AACH,SAAA,MAAM;AACN,SAAA,QAAQ;SACR,QAAQ,CACP,2IAA2I,CAC5I;;AAGH,IAAA,OAAO,EAAE;AACN,SAAA,MAAM;AACN,SAAA,QAAQ;SACR,QAAQ,CAAC,uDAAuD,CAAC;AACpE,IAAA,OAAO,EAAE;AACN,SAAA,MAAM;AACN,SAAA,QAAQ;SACR,QAAQ,CAAC,yCAAyC,CAAC;AACtD,IAAA,WAAW,EAAE;AACV,SAAA,OAAO;AACP,SAAA,QAAQ;SACR,QAAQ,CACP,8IAA8I,CAC/I;AACJ,CAAA;AAEM,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC;AAC1C,IAAA,MAAM,EAAE;SACL,IAAI,CAAC,oBAAoB;SACzB,QAAQ,CACP,sFAAsF,CACvF;AACH,IAAA,UAAU,EAAE;AACT,SAAA,MAAM;AACN,SAAA,QAAQ;SACR,QAAQ,CACP,wEAAwE,CACzE;;AAGH,IAAA,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,iCAAiC,CAAC;AAC7E,IAAA,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,QAAQ,CAAC,+BAA+B,CAAC;;AAGzE,IAAA,OAAO,EAAE;AACN,SAAA,MAAM;AACN,SAAA,QAAQ;SACR,QAAQ,CAAC,sCAAsC,CAAC;AACnD,IAAA,KAAK,EAAE;AACJ,SAAA,MAAM;AACN,SAAA,QAAQ;SACR,QAAQ,CAAC,+CAA+C,CAAC;AAC5D,IAAA,OAAO,EAAE;AACN,SAAA,MAAM;AACN,SAAA,QAAQ;SACR,QAAQ,CAAC,qCAAqC,CAAC;;AAGlD,IAAA,MAAM,EAAE;AACL,SAAA,MAAM;AACN,SAAA,QAAQ;SACR,QAAQ,CAAC,uCAAuC,CAAC;AACpD,IAAA,KAAK,EAAE;AACJ,SAAA,MAAM;AACN,SAAA,QAAQ;SACR,QAAQ,CAAC,uCAAuC,CAAC;AACrD,CAAA;AAEM,MAAM,kBAAkB,GAAG;AAC3B,MAAM,mBAAmB,GAAG;;;;"}
|
|
@@ -105,7 +105,10 @@ function createArtifactTool(config) {
|
|
|
105
105
|
return await handlers.delete(args, scope);
|
|
106
106
|
}
|
|
107
107
|
default:
|
|
108
|
-
return [
|
|
108
|
+
return [
|
|
109
|
+
`Unknown action: ${input.action}`,
|
|
110
|
+
{},
|
|
111
|
+
];
|
|
109
112
|
}
|
|
110
113
|
}
|
|
111
114
|
catch (err) {
|
|
@@ -182,7 +185,10 @@ function createContentReaderTool(config) {
|
|
|
182
185
|
return await handlers.info(args, scope);
|
|
183
186
|
}
|
|
184
187
|
default:
|
|
185
|
-
return [
|
|
188
|
+
return [
|
|
189
|
+
`Unknown action: ${input.action}`,
|
|
190
|
+
{},
|
|
191
|
+
];
|
|
186
192
|
}
|
|
187
193
|
}
|
|
188
194
|
catch (err) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool.mjs","sources":["../../../../src/tools/artifacts/tool.ts"],"sourcesContent":["/**\n * artifact_tool + content_reader library factories.\n *\n * The library owns the LangChain wiring (schema, description, response\n * shape) and the action dispatch; the runtime supplies a handler bundle\n * matching the `ArtifactWriteHandlers` / `ContentReadHandlers` interface.\n *\n * This keeps 800+ LOC of host-specific handler logic (S3 adapters, file\n * model CRUD, syntax checkers, line utils) out of the library while\n * still centralizing the tool surface every runtime shares.\n */\n\nimport { tool, DynamicStructuredTool } from '@langchain/core/tools';\nimport {\n artifactToolSchema,\n contentReaderSchema,\n ARTIFACT_TOOL_NAME,\n CONTENT_READER_NAME,\n} from './schema';\nimport type {\n ArtifactToolConfig,\n ContentReaderToolConfig,\n ArtifactToolScope,\n ArtifactToolResult,\n WriteArgs,\n EditArgs,\n VerifyArgs,\n DeleteArgs,\n ReadArgs,\n SearchArgs,\n ListArgs,\n InfoArgs,\n ContentIdResolver,\n} from './types';\n\nconst DEFAULT_ARTIFACT_DESCRIPTION = `Author content artifacts that render live in the host's preview panel — this tool does NOT produce downloadable files (use execute_code for those).\n\nActions:\n- write: Create a new artifact. Write a COMPLETE file in one call. The \\`name\\` field MUST include the file extension — it routes the preview (.tsx, .html, .mmd, .svg, .csv, .json, .dot, .md, .drawio).\n- verify: Check an artifact for syntax errors. REQUIRED as the next step after every write/edit on code files — do not render until verify passes.\n- edit: Surgical string replacement — provide old_str (exact match) and new_str. Works on all file types.\n- delete: Remove an artifact and its backing file.\n\nArtifacts are persisted by the host. No manual save needed.`;\n\nconst DEFAULT_CONTENT_READER_DESCRIPTION = `Read and navigate stored content — artifacts authored by artifact_tool, large tool results auto-cached by the host, uploaded file attachments, and code blocks.\n\nRead-only surface. Use write/edit tools (artifact_tool, execute_code) to mutate.\n\nActions:\n- read: Return line ranges from a specific content_id.\n- search: Regex search across a specific content_id with paginated matches.\n- list: Enumerate every content entry currently stored.\n- info: Metadata (size, kind, creation time) for a specific content_id.`;\n\n/**\n * Optional content_id self-healing — if the runtime supplies a resolver,\n * we pre-resolve the ID on every action that takes one so nicknames\n * (e.g., \"Dashboard\") map to canonical IDs.\n */\nasync function resolveContentIdIfPresent(\n resolver: ContentIdResolver | undefined,\n id: string | undefined,\n scope: ArtifactToolScope,\n logger?: { debug: (msg: string) => void },\n): Promise<string | undefined> {\n if (!resolver || !id) return id;\n const out = await resolver.resolve(id, scope);\n if (!out) return id;\n if (out.resolvedId !== id) {\n logger?.debug(\n `[artifact] resolved \"${id}\" → \"${out.resolvedId}\"${out.resolvedName ? ` (\"${out.resolvedName}\")` : ''}`,\n );\n }\n return out.resolvedId;\n}\n\n// ─── Writer tool (artifact_tool) ─────────────────────────────────────────\n\nexport function createArtifactTool(\n config: ArtifactToolConfig,\n): DynamicStructuredTool {\n const { handlers, getScope, resolver, logger, descriptionOverride } = config;\n\n return tool(\n async (rawInput, runnableConfig): Promise<ArtifactToolResult> => {\n const scope = getScope(runnableConfig);\n if (!scope) {\n logger?.warn('[artifact_tool] no scope resolved from runnableConfig');\n return ['Error: No conversation context available', {}];\n }\n\n const input = rawInput as {\n action: 'write' | 'edit' | 'verify' | 'delete';\n content_id?: string;\n content?: string;\n name?: string;\n old_str?: string;\n new_str?: string;\n replace_all?: boolean;\n };\n\n const resolvedContentId = await resolveContentIdIfPresent(\n resolver,\n input.content_id,\n scope,\n logger,\n );\n\n const started = Date.now();\n try {\n switch (input.action) {\n case 'write': {\n const args: WriteArgs = {\n action: 'write',\n content_id: resolvedContentId,\n content: input.content ?? '',\n name: input.name,\n };\n if (!args.content) return ['Error: write requires content', {}];\n return await handlers.write(args, scope);\n }\n case 'edit': {\n if (!resolvedContentId) return ['Error: edit requires content_id', {}];\n const args: EditArgs = {\n action: 'edit',\n content_id: resolvedContentId,\n old_str: input.old_str ?? '',\n new_str: input.new_str ?? '',\n replace_all: input.replace_all,\n };\n if (!args.old_str) return ['Error: edit requires old_str', {}];\n return await handlers.edit(args, scope);\n }\n case 'verify': {\n if (!resolvedContentId) return ['Error: verify requires content_id', {}];\n const args: VerifyArgs = {\n action: 'verify',\n content_id: resolvedContentId,\n };\n return await handlers.verify(args, scope);\n }\n case 'delete': {\n if (!resolvedContentId) return ['Error: delete requires content_id', {}];\n const args: DeleteArgs = {\n action: 'delete',\n content_id: resolvedContentId,\n };\n return await handlers.delete(args, scope);\n }\n default:\n return [`Unknown action: ${(input as { action: string }).action}`, {}];\n }\n } catch (err) {\n const e = err instanceof Error ? err : new Error(String(err));\n logger?.error('[artifact_tool] handler threw', {\n action: input.action,\n contentId: resolvedContentId,\n error: e.message,\n elapsed: `${Date.now() - started}ms`,\n });\n return [`Error: ${e.message}`, {}];\n }\n },\n {\n name: ARTIFACT_TOOL_NAME,\n responseFormat: 'content_and_artifact',\n description: descriptionOverride ?? DEFAULT_ARTIFACT_DESCRIPTION,\n schema: artifactToolSchema,\n },\n );\n}\n\n// ─── Reader tool (content_reader) ────────────────────────────────────────\n\nexport function createContentReaderTool(\n config: ContentReaderToolConfig,\n): DynamicStructuredTool {\n const { handlers, getScope, resolver, logger, descriptionOverride } = config;\n\n return tool(\n async (rawInput, runnableConfig): Promise<ArtifactToolResult> => {\n const scope = getScope(runnableConfig);\n if (!scope) {\n logger?.warn('[content_reader] no scope resolved from runnableConfig');\n return ['Error: No conversation context available', {}];\n }\n\n const input = rawInput as {\n action: 'read' | 'search' | 'list' | 'info';\n content_id?: string;\n start_line?: number;\n end_line?: number;\n pattern?: string;\n flags?: string;\n context?: number;\n offset?: number;\n limit?: number;\n };\n\n const resolvedContentId = await resolveContentIdIfPresent(\n resolver,\n input.content_id,\n scope,\n logger,\n );\n\n const started = Date.now();\n try {\n switch (input.action) {\n case 'read': {\n if (!resolvedContentId) return ['Error: read requires content_id', {}];\n const args: ReadArgs = {\n action: 'read',\n content_id: resolvedContentId,\n start_line: input.start_line,\n end_line: input.end_line,\n offset: input.offset,\n limit: input.limit,\n };\n return await handlers.read(args, scope);\n }\n case 'search': {\n if (!resolvedContentId) return ['Error: search requires content_id', {}];\n if (!input.pattern) return ['Error: search requires pattern', {}];\n const args: SearchArgs = {\n action: 'search',\n content_id: resolvedContentId,\n pattern: input.pattern,\n flags: input.flags,\n context: input.context,\n offset: input.offset,\n limit: input.limit,\n };\n return await handlers.search(args, scope);\n }\n case 'list': {\n const args: ListArgs = { action: 'list' };\n return await handlers.list(args, scope);\n }\n case 'info': {\n if (!resolvedContentId) return ['Error: info requires content_id', {}];\n const args: InfoArgs = {\n action: 'info',\n content_id: resolvedContentId,\n };\n return await handlers.info(args, scope);\n }\n default:\n return [`Unknown action: ${(input as { action: string }).action}`, {}];\n }\n } catch (err) {\n const e = err instanceof Error ? err : new Error(String(err));\n logger?.error('[content_reader] handler threw', {\n action: input.action,\n contentId: resolvedContentId,\n error: e.message,\n elapsed: `${Date.now() - started}ms`,\n });\n return [`Error: ${e.message}`, {}];\n }\n },\n {\n name: CONTENT_READER_NAME,\n responseFormat: 'content_and_artifact',\n description: descriptionOverride ?? DEFAULT_CONTENT_READER_DESCRIPTION,\n schema: contentReaderSchema,\n },\n );\n}\n\nexport {\n ARTIFACT_TOOL_NAME,\n CONTENT_READER_NAME,\n ARTIFACT_WRITE_ACTIONS,\n CONTENT_READ_ACTIONS,\n} from './schema';\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tool.mjs","sources":["../../../../src/tools/artifacts/tool.ts"],"sourcesContent":["/**\n * artifact_tool + content_reader library factories.\n *\n * The library owns the LangChain wiring (schema, description, response\n * shape) and the action dispatch; the runtime supplies a handler bundle\n * matching the `ArtifactWriteHandlers` / `ContentReadHandlers` interface.\n *\n * This keeps 800+ LOC of host-specific handler logic (S3 adapters, file\n * model CRUD, syntax checkers, line utils) out of the library while\n * still centralizing the tool surface every runtime shares.\n */\n\nimport { tool, DynamicStructuredTool } from '@langchain/core/tools';\nimport {\n artifactToolSchema,\n contentReaderSchema,\n ARTIFACT_TOOL_NAME,\n CONTENT_READER_NAME,\n} from './schema';\nimport type {\n ArtifactToolConfig,\n ContentReaderToolConfig,\n ArtifactToolScope,\n ArtifactToolResult,\n WriteArgs,\n EditArgs,\n VerifyArgs,\n DeleteArgs,\n ReadArgs,\n SearchArgs,\n ListArgs,\n InfoArgs,\n ContentIdResolver,\n} from './types';\n\nconst DEFAULT_ARTIFACT_DESCRIPTION = `Author content artifacts that render live in the host's preview panel — this tool does NOT produce downloadable files (use execute_code for those).\n\nActions:\n- write: Create a new artifact. Write a COMPLETE file in one call. The \\`name\\` field MUST include the file extension — it routes the preview (.tsx, .html, .mmd, .svg, .csv, .json, .dot, .md, .drawio).\n- verify: Check an artifact for syntax errors. REQUIRED as the next step after every write/edit on code files — do not render until verify passes.\n- edit: Surgical string replacement — provide old_str (exact match) and new_str. Works on all file types.\n- delete: Remove an artifact and its backing file.\n\nArtifacts are persisted by the host. No manual save needed.`;\n\nconst DEFAULT_CONTENT_READER_DESCRIPTION = `Read and navigate stored content — artifacts authored by artifact_tool, large tool results auto-cached by the host, uploaded file attachments, and code blocks.\n\nRead-only surface. Use write/edit tools (artifact_tool, execute_code) to mutate.\n\nActions:\n- read: Return line ranges from a specific content_id.\n- search: Regex search across a specific content_id with paginated matches.\n- list: Enumerate every content entry currently stored.\n- info: Metadata (size, kind, creation time) for a specific content_id.`;\n\n/**\n * Optional content_id self-healing — if the runtime supplies a resolver,\n * we pre-resolve the ID on every action that takes one so nicknames\n * (e.g., \"Dashboard\") map to canonical IDs.\n */\nasync function resolveContentIdIfPresent(\n resolver: ContentIdResolver | undefined,\n id: string | undefined,\n scope: ArtifactToolScope,\n logger?: { debug: (msg: string) => void }\n): Promise<string | undefined> {\n if (!resolver || !id) return id;\n const out = await resolver.resolve(id, scope);\n if (!out) return id;\n if (out.resolvedId !== id) {\n logger?.debug(\n `[artifact] resolved \"${id}\" → \"${out.resolvedId}\"${out.resolvedName ? ` (\"${out.resolvedName}\")` : ''}`\n );\n }\n return out.resolvedId;\n}\n\n// ─── Writer tool (artifact_tool) ─────────────────────────────────────────\n\nexport function createArtifactTool(\n config: ArtifactToolConfig\n): DynamicStructuredTool {\n const { handlers, getScope, resolver, logger, descriptionOverride } = config;\n\n return tool(\n async (rawInput, runnableConfig): Promise<ArtifactToolResult> => {\n const scope = getScope(runnableConfig);\n if (!scope) {\n logger?.warn('[artifact_tool] no scope resolved from runnableConfig');\n return ['Error: No conversation context available', {}];\n }\n\n const input = rawInput as {\n action: 'write' | 'edit' | 'verify' | 'delete';\n content_id?: string;\n content?: string;\n name?: string;\n old_str?: string;\n new_str?: string;\n replace_all?: boolean;\n };\n\n const resolvedContentId = await resolveContentIdIfPresent(\n resolver,\n input.content_id,\n scope,\n logger\n );\n\n const started = Date.now();\n try {\n switch (input.action) {\n case 'write': {\n const args: WriteArgs = {\n action: 'write',\n content_id: resolvedContentId,\n content: input.content ?? '',\n name: input.name,\n };\n if (!args.content) return ['Error: write requires content', {}];\n return await handlers.write(args, scope);\n }\n case 'edit': {\n if (!resolvedContentId)\n return ['Error: edit requires content_id', {}];\n const args: EditArgs = {\n action: 'edit',\n content_id: resolvedContentId,\n old_str: input.old_str ?? '',\n new_str: input.new_str ?? '',\n replace_all: input.replace_all,\n };\n if (!args.old_str) return ['Error: edit requires old_str', {}];\n return await handlers.edit(args, scope);\n }\n case 'verify': {\n if (!resolvedContentId)\n return ['Error: verify requires content_id', {}];\n const args: VerifyArgs = {\n action: 'verify',\n content_id: resolvedContentId,\n };\n return await handlers.verify(args, scope);\n }\n case 'delete': {\n if (!resolvedContentId)\n return ['Error: delete requires content_id', {}];\n const args: DeleteArgs = {\n action: 'delete',\n content_id: resolvedContentId,\n };\n return await handlers.delete(args, scope);\n }\n default:\n return [\n `Unknown action: ${(input as { action: string }).action}`,\n {},\n ];\n }\n } catch (err) {\n const e = err instanceof Error ? err : new Error(String(err));\n logger?.error('[artifact_tool] handler threw', {\n action: input.action,\n contentId: resolvedContentId,\n error: e.message,\n elapsed: `${Date.now() - started}ms`,\n });\n return [`Error: ${e.message}`, {}];\n }\n },\n {\n name: ARTIFACT_TOOL_NAME,\n responseFormat: 'content_and_artifact',\n description: descriptionOverride ?? DEFAULT_ARTIFACT_DESCRIPTION,\n schema: artifactToolSchema,\n }\n );\n}\n\n// ─── Reader tool (content_reader) ────────────────────────────────────────\n\nexport function createContentReaderTool(\n config: ContentReaderToolConfig\n): DynamicStructuredTool {\n const { handlers, getScope, resolver, logger, descriptionOverride } = config;\n\n return tool(\n async (rawInput, runnableConfig): Promise<ArtifactToolResult> => {\n const scope = getScope(runnableConfig);\n if (!scope) {\n logger?.warn('[content_reader] no scope resolved from runnableConfig');\n return ['Error: No conversation context available', {}];\n }\n\n const input = rawInput as {\n action: 'read' | 'search' | 'list' | 'info';\n content_id?: string;\n start_line?: number;\n end_line?: number;\n pattern?: string;\n flags?: string;\n context?: number;\n offset?: number;\n limit?: number;\n };\n\n const resolvedContentId = await resolveContentIdIfPresent(\n resolver,\n input.content_id,\n scope,\n logger\n );\n\n const started = Date.now();\n try {\n switch (input.action) {\n case 'read': {\n if (!resolvedContentId)\n return ['Error: read requires content_id', {}];\n const args: ReadArgs = {\n action: 'read',\n content_id: resolvedContentId,\n start_line: input.start_line,\n end_line: input.end_line,\n offset: input.offset,\n limit: input.limit,\n };\n return await handlers.read(args, scope);\n }\n case 'search': {\n if (!resolvedContentId)\n return ['Error: search requires content_id', {}];\n if (!input.pattern) return ['Error: search requires pattern', {}];\n const args: SearchArgs = {\n action: 'search',\n content_id: resolvedContentId,\n pattern: input.pattern,\n flags: input.flags,\n context: input.context,\n offset: input.offset,\n limit: input.limit,\n };\n return await handlers.search(args, scope);\n }\n case 'list': {\n const args: ListArgs = { action: 'list' };\n return await handlers.list(args, scope);\n }\n case 'info': {\n if (!resolvedContentId)\n return ['Error: info requires content_id', {}];\n const args: InfoArgs = {\n action: 'info',\n content_id: resolvedContentId,\n };\n return await handlers.info(args, scope);\n }\n default:\n return [\n `Unknown action: ${(input as { action: string }).action}`,\n {},\n ];\n }\n } catch (err) {\n const e = err instanceof Error ? err : new Error(String(err));\n logger?.error('[content_reader] handler threw', {\n action: input.action,\n contentId: resolvedContentId,\n error: e.message,\n elapsed: `${Date.now() - started}ms`,\n });\n return [`Error: ${e.message}`, {}];\n }\n },\n {\n name: CONTENT_READER_NAME,\n responseFormat: 'content_and_artifact',\n description: descriptionOverride ?? DEFAULT_CONTENT_READER_DESCRIPTION,\n schema: contentReaderSchema,\n }\n );\n}\n\nexport {\n ARTIFACT_TOOL_NAME,\n CONTENT_READER_NAME,\n ARTIFACT_WRITE_ACTIONS,\n CONTENT_READ_ACTIONS,\n} from './schema';\n"],"names":[],"mappings":"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"}
|
|
@@ -22,7 +22,7 @@ import { HttpError } from '../utils/httpClient.mjs';
|
|
|
22
22
|
* per-invocation credential rotation should rebuild the tool.
|
|
23
23
|
*/
|
|
24
24
|
function buildProxyTool(capability, credentials, options) {
|
|
25
|
-
const { client, executePath = '/execute/:name', onExecute } = options;
|
|
25
|
+
const { client, executePath = '/execute/:name', onExecute, getAuthHeaders, } = options;
|
|
26
26
|
const url = executePath.replace(':name', encodeURIComponent(capability.name));
|
|
27
27
|
return tool(async (input) => {
|
|
28
28
|
const startMs = Date.now();
|
|
@@ -31,10 +31,12 @@ function buildProxyTool(capability, credentials, options) {
|
|
|
31
31
|
// DEBUG: log (remove after POC stabilizes)
|
|
32
32
|
// eslint-disable-next-line no-console
|
|
33
33
|
console.debug(`${debugPrefix} invoking — inputKeys=${input && typeof input === 'object' ? Object.keys(input).length : 0}`);
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
})
|
|
34
|
+
const extraHeaders = getAuthHeaders
|
|
35
|
+
? await getAuthHeaders()
|
|
36
|
+
: undefined;
|
|
37
|
+
const res = await client.post(url, { input, credentials }, extraHeaders && Object.keys(extraHeaders).length > 0
|
|
38
|
+
? { headers: extraHeaders }
|
|
39
|
+
: undefined);
|
|
38
40
|
const durationMs = Date.now() - startMs;
|
|
39
41
|
if (res.status < 200 || res.status >= 300) {
|
|
40
42
|
throw new HttpError(res.status, url, res.data);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proxyTool.mjs","sources":["../../../src/tools/proxyTool.ts"],"sourcesContent":["/**\n * proxyTool — wraps a Capability into a LangChain StructuredTool that\n * dispatches execution to a remote backend (e.g., tools-server) over HTTP.\n *\n * The LLM sees this tool identically to a locally-implemented tool:\n * - same name\n * - same description\n * - same input schema (JSON Schema)\n * - same invoke(input) contract\n *\n * Under the hood, invoke() POSTs to the backend's execute endpoint with\n * the input and a caller-supplied credential map. The backend returns\n * { success, result, error, timing } which this wrapper unpacks.\n */\n\nimport type { AxiosInstance } from 'axios';\nimport { tool, type StructuredToolInterface } from '@langchain/core/tools';\nimport type { Capability, CredentialMap } from '@/providers/types';\nimport { HttpError } from '@/utils/httpClient';\n\n/** Shape of the backend's execute response. Matches tools-server. */\nexport interface ExecuteResponse {\n success: boolean;\n result?: unknown;\n error?: string;\n timing?: { durationMs: number };\n}\n\n/** Options passed to buildProxyTool. */\nexport interface ProxyToolOptions {\n /** HTTP client configured with backend base URL + auth. */\n client: AxiosInstance;\n /**\n * Path template for execution. The literal `:name` is replaced with the\n * capability's name. Defaults to `/execute/:name` (tools-server convention).\n */\n executePath?: string;\n /**\n * Optional callback invoked on every execution — used for metrics,\n * telemetry, debug logging. Errors in the hook are swallowed.\n */\n onExecute?: (ctx: ExecuteCallbackContext) => void;\n}\n\nexport interface ExecuteCallbackContext {\n capabilityName: string;\n input: unknown;\n response?: ExecuteResponse;\n error?: Error;\n durationMs: number;\n}\n\n/**\n * Build a StructuredTool that proxies to a remote backend.\n *\n * The credentialMap is baked into the closure — callers that need\n * per-invocation credential rotation should rebuild the tool.\n */\nexport function buildProxyTool(\n capability: Capability,\n credentials: CredentialMap,\n options: ProxyToolOptions\n): StructuredToolInterface {\n const {
|
|
1
|
+
{"version":3,"file":"proxyTool.mjs","sources":["../../../src/tools/proxyTool.ts"],"sourcesContent":["/**\n * proxyTool — wraps a Capability into a LangChain StructuredTool that\n * dispatches execution to a remote backend (e.g., tools-server) over HTTP.\n *\n * The LLM sees this tool identically to a locally-implemented tool:\n * - same name\n * - same description\n * - same input schema (JSON Schema)\n * - same invoke(input) contract\n *\n * Under the hood, invoke() POSTs to the backend's execute endpoint with\n * the input and a caller-supplied credential map. The backend returns\n * { success, result, error, timing } which this wrapper unpacks.\n */\n\nimport type { AxiosInstance } from 'axios';\nimport { tool, type StructuredToolInterface } from '@langchain/core/tools';\nimport type { Capability, CredentialMap } from '@/providers/types';\nimport { HttpError } from '@/utils/httpClient';\n\n/** Shape of the backend's execute response. Matches tools-server. */\nexport interface ExecuteResponse {\n success: boolean;\n result?: unknown;\n error?: string;\n timing?: { durationMs: number };\n}\n\n/** Options passed to buildProxyTool. */\nexport interface ProxyToolOptions {\n /** HTTP client configured with backend base URL + auth. */\n client: AxiosInstance;\n /**\n * Path template for execution. The literal `:name` is replaced with the\n * capability's name. Defaults to `/execute/:name` (tools-server convention).\n */\n executePath?: string;\n /**\n * Optional callback invoked on every execution — used for metrics,\n * telemetry, debug logging. Errors in the hook are swallowed.\n */\n onExecute?: (ctx: ExecuteCallbackContext) => void;\n /**\n * Optional per-invocation auth header builder. Called on every tool\n * invocation before POSTing; returned headers are merged into the\n * request alongside the base client's headers. Typical use: pass a\n * freshly minted per-user JWT for admin-gated tools.\n */\n getAuthHeaders?: () =>\n | Record<string, string>\n | Promise<Record<string, string>>;\n}\n\nexport interface ExecuteCallbackContext {\n capabilityName: string;\n input: unknown;\n response?: ExecuteResponse;\n error?: Error;\n durationMs: number;\n}\n\n/**\n * Build a StructuredTool that proxies to a remote backend.\n *\n * The credentialMap is baked into the closure — callers that need\n * per-invocation credential rotation should rebuild the tool.\n */\nexport function buildProxyTool(\n capability: Capability,\n credentials: CredentialMap,\n options: ProxyToolOptions\n): StructuredToolInterface {\n const {\n client,\n executePath = '/execute/:name',\n onExecute,\n getAuthHeaders,\n } = options;\n const url = executePath.replace(':name', encodeURIComponent(capability.name));\n\n return tool(\n async (input: unknown): Promise<string> => {\n const startMs = Date.now();\n const debugPrefix = `[proxyTool:${capability.name}]`;\n\n try {\n // DEBUG: log (remove after POC stabilizes)\n // eslint-disable-next-line no-console\n console.debug(\n `${debugPrefix} invoking — inputKeys=${input && typeof input === 'object' ? Object.keys(input as object).length : 0}`\n );\n\n const extraHeaders = getAuthHeaders\n ? await getAuthHeaders()\n : undefined;\n const res = await client.post<ExecuteResponse>(\n url,\n { input, credentials },\n extraHeaders && Object.keys(extraHeaders).length > 0\n ? { headers: extraHeaders }\n : undefined,\n );\n\n const durationMs = Date.now() - startMs;\n\n if (res.status < 200 || res.status >= 300) {\n throw new HttpError(res.status, url, res.data);\n }\n\n const body = res.data;\n if (onExecute) {\n try {\n onExecute({\n capabilityName: capability.name,\n input,\n response: body,\n durationMs,\n });\n } catch {\n // hook errors are non-fatal\n }\n }\n\n if (!body.success) {\n // DEBUG\n // eslint-disable-next-line no-console\n console.debug(\n `${debugPrefix} backend reported failure — ${body.error}`\n );\n throw new Error(body.error ?? 'Tool execution failed');\n }\n\n // LangChain tools typically return strings; stringify non-string results\n if (typeof body.result === 'string') {\n return body.result;\n }\n return JSON.stringify(body.result);\n } catch (err) {\n const durationMs = Date.now() - startMs;\n const error = err instanceof Error ? err : new Error(String(err));\n if (onExecute) {\n try {\n onExecute({\n capabilityName: capability.name,\n input,\n error,\n durationMs,\n });\n } catch {\n // hook errors are non-fatal\n }\n }\n throw error;\n }\n },\n {\n name: capability.name,\n description: capability.description,\n schema: (capability.schema as object) ?? {\n type: 'object',\n properties: {},\n },\n responseFormat: 'content',\n }\n );\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -29,6 +29,19 @@ export interface ToolsServerConfig {
|
|
|
29
29
|
client?: AxiosInstance;
|
|
30
30
|
/** Optional proxy override (defaults to process.env.PROXY). */
|
|
31
31
|
proxy?: string | null;
|
|
32
|
+
/**
|
|
33
|
+
* Optional per-request auth header builder — invoked on every tool
|
|
34
|
+
* invocation (NOT on the manifest fetch, which is service-to-service).
|
|
35
|
+
* When provided, the returned headers are merged into the `/execute`
|
|
36
|
+
* request so the host can pass user-scoped identity (e.g.,
|
|
37
|
+
* `Authorization: Bearer <jwt>`) that tools-server verifies for
|
|
38
|
+
* admin-gated tools.
|
|
39
|
+
*
|
|
40
|
+
* Typical host wiring: mint a short-lived JWT per call carrying the
|
|
41
|
+
* authenticated user's `{ userId, role }` claims; tools-server's
|
|
42
|
+
* `TOOLS_SERVER_JWT_SECRET` validates.
|
|
43
|
+
*/
|
|
44
|
+
getExecuteAuthHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
|
|
32
45
|
}
|
|
33
46
|
export declare class ToolsServerCapabilityProvider implements CapabilityProvider {
|
|
34
47
|
private readonly config;
|
|
@@ -37,6 +50,7 @@ export declare class ToolsServerCapabilityProvider implements CapabilityProvider
|
|
|
37
50
|
private readonly manifestPath;
|
|
38
51
|
private readonly executePath;
|
|
39
52
|
private readonly cache;
|
|
53
|
+
private readonly getExecuteAuthHeaders?;
|
|
40
54
|
constructor(config: ToolsServerConfig);
|
|
41
55
|
fetchManifest(filter?: CapabilityFilter): Promise<Capability[]>;
|
|
42
56
|
createRunnables(capabilities: Capability[], credentials: CredentialMap): Promise<StructuredToolInterface[]>;
|
|
@@ -82,6 +82,20 @@ export interface CapabilityMetadata {
|
|
|
82
82
|
category?: string;
|
|
83
83
|
/** Free-form tags for filtering / search. */
|
|
84
84
|
tags?: string[];
|
|
85
|
+
/**
|
|
86
|
+
* When true, hosts omit this capability from user-facing pickers
|
|
87
|
+
* (Agent Builder, tool dropdown). Still invokable when a saved agent
|
|
88
|
+
* references it by name.
|
|
89
|
+
*/
|
|
90
|
+
hidden?: boolean;
|
|
91
|
+
/** When true, only admin-role users can enable or invoke this capability. */
|
|
92
|
+
admin?: boolean;
|
|
93
|
+
/**
|
|
94
|
+
* Deployment-stage restriction. When set, hosts whose deployment stage
|
|
95
|
+
* isn't in the array should hide the capability (e.g., a prod host
|
|
96
|
+
* shouldn't surface a tool with `env: ['development']`).
|
|
97
|
+
*/
|
|
98
|
+
env?: Array<'development' | 'production' | 'test' | 'staging'>;
|
|
85
99
|
/** Reserved for upstream `fix/auth-events` — auth mode declared by tool. */
|
|
86
100
|
auth?: string;
|
|
87
101
|
/** Reserved for upstream `fix/auth-events` — Unix timestamp for token expiration. */
|
|
@@ -38,6 +38,13 @@ export interface ProxyToolOptions {
|
|
|
38
38
|
* telemetry, debug logging. Errors in the hook are swallowed.
|
|
39
39
|
*/
|
|
40
40
|
onExecute?: (ctx: ExecuteCallbackContext) => void;
|
|
41
|
+
/**
|
|
42
|
+
* Optional per-invocation auth header builder. Called on every tool
|
|
43
|
+
* invocation before POSTing; returned headers are merged into the
|
|
44
|
+
* request alongside the base client's headers. Typical use: pass a
|
|
45
|
+
* freshly minted per-user JWT for admin-gated tools.
|
|
46
|
+
*/
|
|
47
|
+
getAuthHeaders?: () => Record<string, string> | Promise<Record<string, string>>;
|
|
41
48
|
}
|
|
42
49
|
export interface ExecuteCallbackContext {
|
|
43
50
|
capabilityName: string;
|
package/package.json
CHANGED
|
@@ -203,4 +203,67 @@ describe('ToolsServerCapabilityProvider.createRunnables', () => {
|
|
|
203
203
|
/missing API key/
|
|
204
204
|
);
|
|
205
205
|
});
|
|
206
|
+
|
|
207
|
+
it('getExecuteAuthHeaders is invoked per call and forwarded as HTTP headers', async () => {
|
|
208
|
+
const client = {
|
|
209
|
+
get: jest.fn().mockResolvedValue({ status: 200, data: manifestFixture }),
|
|
210
|
+
post: jest.fn().mockResolvedValue({
|
|
211
|
+
status: 200,
|
|
212
|
+
data: {
|
|
213
|
+
success: true,
|
|
214
|
+
result: 'ok',
|
|
215
|
+
timing: { durationMs: 1 },
|
|
216
|
+
},
|
|
217
|
+
}),
|
|
218
|
+
defaults: { baseURL: 'http://stub', headers: {} },
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
let mintCount = 0;
|
|
222
|
+
const p = new ToolsServerCapabilityProvider({
|
|
223
|
+
baseUrl: 'http://x',
|
|
224
|
+
apiKey: 'k',
|
|
225
|
+
client: client as unknown as ReturnType<typeof axios.create>,
|
|
226
|
+
getExecuteAuthHeaders: async () => {
|
|
227
|
+
mintCount += 1;
|
|
228
|
+
return { Authorization: `Bearer TOKEN-${mintCount}` };
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
const caps = await p.fetchManifest();
|
|
232
|
+
const [wikipedia] = await p.createRunnables(
|
|
233
|
+
caps.filter((c) => c.name === 'wikipedia'),
|
|
234
|
+
{}
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
await wikipedia.invoke({ query: 'a' });
|
|
238
|
+
await wikipedia.invoke({ query: 'b' });
|
|
239
|
+
|
|
240
|
+
// Two invocations → two mints (fresh token each call)
|
|
241
|
+
expect(mintCount).toBe(2);
|
|
242
|
+
|
|
243
|
+
// Each POST includes the Authorization header from the mint
|
|
244
|
+
const firstCall = (client.post as jest.Mock).mock.calls[0];
|
|
245
|
+
const secondCall = (client.post as jest.Mock).mock.calls[1];
|
|
246
|
+
expect(firstCall[2]?.headers).toEqual({ Authorization: 'Bearer TOKEN-1' });
|
|
247
|
+
expect(secondCall[2]?.headers).toEqual({ Authorization: 'Bearer TOKEN-2' });
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('getExecuteAuthHeaders is NOT called during manifest fetch (service-to-service)', async () => {
|
|
251
|
+
const client = {
|
|
252
|
+
get: jest.fn().mockResolvedValue({ status: 200, data: manifestFixture }),
|
|
253
|
+
post: jest.fn(),
|
|
254
|
+
defaults: { baseURL: 'http://stub', headers: {} },
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const authBuilder = jest.fn().mockReturnValue({ Authorization: 'Bearer X' });
|
|
258
|
+
const p = new ToolsServerCapabilityProvider({
|
|
259
|
+
baseUrl: 'http://x',
|
|
260
|
+
apiKey: 'k',
|
|
261
|
+
client: client as unknown as ReturnType<typeof axios.create>,
|
|
262
|
+
getExecuteAuthHeaders: authBuilder,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
await p.fetchManifest();
|
|
266
|
+
// Manifest fetch is service-to-service — no per-user auth headers
|
|
267
|
+
expect(authBuilder).not.toHaveBeenCalled();
|
|
268
|
+
});
|
|
206
269
|
});
|
|
@@ -48,6 +48,21 @@ export interface ToolsServerConfig {
|
|
|
48
48
|
client?: AxiosInstance;
|
|
49
49
|
/** Optional proxy override (defaults to process.env.PROXY). */
|
|
50
50
|
proxy?: string | null;
|
|
51
|
+
/**
|
|
52
|
+
* Optional per-request auth header builder — invoked on every tool
|
|
53
|
+
* invocation (NOT on the manifest fetch, which is service-to-service).
|
|
54
|
+
* When provided, the returned headers are merged into the `/execute`
|
|
55
|
+
* request so the host can pass user-scoped identity (e.g.,
|
|
56
|
+
* `Authorization: Bearer <jwt>`) that tools-server verifies for
|
|
57
|
+
* admin-gated tools.
|
|
58
|
+
*
|
|
59
|
+
* Typical host wiring: mint a short-lived JWT per call carrying the
|
|
60
|
+
* authenticated user's `{ userId, role }` claims; tools-server's
|
|
61
|
+
* `TOOLS_SERVER_JWT_SECRET` validates.
|
|
62
|
+
*/
|
|
63
|
+
getExecuteAuthHeaders?: () =>
|
|
64
|
+
| Record<string, string>
|
|
65
|
+
| Promise<Record<string, string>>;
|
|
51
66
|
}
|
|
52
67
|
|
|
53
68
|
/**
|
|
@@ -79,6 +94,10 @@ interface ToolsServerManifestEntry {
|
|
|
79
94
|
tags?: string[];
|
|
80
95
|
toolkit?: string;
|
|
81
96
|
endpoint?: string;
|
|
97
|
+
/** Governance flags forwarded by tools-server getManifest(). */
|
|
98
|
+
hidden?: boolean;
|
|
99
|
+
admin?: boolean;
|
|
100
|
+
env?: Array<'development' | 'production' | 'test' | 'staging'>;
|
|
82
101
|
}
|
|
83
102
|
|
|
84
103
|
export class ToolsServerCapabilityProvider implements CapabilityProvider {
|
|
@@ -87,6 +106,8 @@ export class ToolsServerCapabilityProvider implements CapabilityProvider {
|
|
|
87
106
|
private readonly manifestPath: string;
|
|
88
107
|
private readonly executePath: string;
|
|
89
108
|
private readonly cache: ManifestCache;
|
|
109
|
+
private readonly getExecuteAuthHeaders?:
|
|
110
|
+
| (() => Record<string, string> | Promise<Record<string, string>>);
|
|
90
111
|
|
|
91
112
|
constructor(private readonly config: ToolsServerConfig) {
|
|
92
113
|
const {
|
|
@@ -98,6 +119,7 @@ export class ToolsServerCapabilityProvider implements CapabilityProvider {
|
|
|
98
119
|
manifestTtlMs = 60_000,
|
|
99
120
|
client,
|
|
100
121
|
proxy,
|
|
122
|
+
getExecuteAuthHeaders,
|
|
101
123
|
} = config;
|
|
102
124
|
|
|
103
125
|
if (!baseUrl) {
|
|
@@ -111,6 +133,7 @@ export class ToolsServerCapabilityProvider implements CapabilityProvider {
|
|
|
111
133
|
this.manifestPath = manifestPath;
|
|
112
134
|
this.executePath = executePath;
|
|
113
135
|
this.cache = new ManifestCache({ ttlMs: manifestTtlMs });
|
|
136
|
+
this.getExecuteAuthHeaders = getExecuteAuthHeaders;
|
|
114
137
|
|
|
115
138
|
if (client) {
|
|
116
139
|
this.client = client;
|
|
@@ -180,6 +203,7 @@ export class ToolsServerCapabilityProvider implements CapabilityProvider {
|
|
|
180
203
|
buildProxyTool(cap, credentials, {
|
|
181
204
|
client: this.client,
|
|
182
205
|
executePath: this.executePath,
|
|
206
|
+
getAuthHeaders: this.getExecuteAuthHeaders,
|
|
183
207
|
})
|
|
184
208
|
);
|
|
185
209
|
}
|
|
@@ -215,6 +239,10 @@ function normalizeEntry(entry: ToolsServerManifestEntry): Capability {
|
|
|
215
239
|
icon: entry.icon,
|
|
216
240
|
category: entry.category,
|
|
217
241
|
tags: entry.tags,
|
|
242
|
+
// Governance — hosts use these to gate UI/role/environment.
|
|
243
|
+
hidden: entry.hidden,
|
|
244
|
+
admin: entry.admin,
|
|
245
|
+
env: entry.env,
|
|
218
246
|
},
|
|
219
247
|
};
|
|
220
248
|
}
|
package/src/providers/types.ts
CHANGED
|
@@ -88,6 +88,23 @@ export interface CapabilityMetadata {
|
|
|
88
88
|
/** Free-form tags for filtering / search. */
|
|
89
89
|
tags?: string[];
|
|
90
90
|
|
|
91
|
+
// --- Governance flags ------------------------------------------------
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* When true, hosts omit this capability from user-facing pickers
|
|
95
|
+
* (Agent Builder, tool dropdown). Still invokable when a saved agent
|
|
96
|
+
* references it by name.
|
|
97
|
+
*/
|
|
98
|
+
hidden?: boolean;
|
|
99
|
+
/** When true, only admin-role users can enable or invoke this capability. */
|
|
100
|
+
admin?: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* Deployment-stage restriction. When set, hosts whose deployment stage
|
|
103
|
+
* isn't in the array should hide the capability (e.g., a prod host
|
|
104
|
+
* shouldn't surface a tool with `env: ['development']`).
|
|
105
|
+
*/
|
|
106
|
+
env?: Array<'development' | 'production' | 'test' | 'staging'>;
|
|
107
|
+
|
|
91
108
|
// --- Reserved for upstream patterns (not populated today) ---
|
|
92
109
|
|
|
93
110
|
/** Reserved for upstream `fix/auth-events` — auth mode declared by tool. */
|
|
@@ -10,10 +10,7 @@
|
|
|
10
10
|
* We verify each of those paths here with mock handlers.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {
|
|
14
|
-
createArtifactTool,
|
|
15
|
-
createContentReaderTool,
|
|
16
|
-
} from '../tool';
|
|
13
|
+
import { createArtifactTool, createContentReaderTool } from '../tool';
|
|
17
14
|
import type {
|
|
18
15
|
ArtifactWriteHandlers,
|
|
19
16
|
ContentReadHandlers,
|
|
@@ -105,10 +102,14 @@ describe('createArtifactTool', () => {
|
|
|
105
102
|
await t.invoke({ action: 'write', content: 'hello', name: 'doc.md' });
|
|
106
103
|
expect(handlers.write).toHaveBeenCalledTimes(1);
|
|
107
104
|
expect(handlers._calls[0].args).toEqual(
|
|
108
|
-
expect.objectContaining({
|
|
105
|
+
expect.objectContaining({
|
|
106
|
+
action: 'write',
|
|
107
|
+
content: 'hello',
|
|
108
|
+
name: 'doc.md',
|
|
109
|
+
})
|
|
109
110
|
);
|
|
110
111
|
expect(handlers._calls[0].scope).toEqual(
|
|
111
|
-
expect.objectContaining({ conversationId: 'conv-1' })
|
|
112
|
+
expect.objectContaining({ conversationId: 'conv-1' })
|
|
112
113
|
);
|
|
113
114
|
});
|
|
114
115
|
|
|
@@ -131,17 +132,26 @@ describe('createArtifactTool', () => {
|
|
|
131
132
|
getScope: makeScope,
|
|
132
133
|
});
|
|
133
134
|
const e = await t.invoke({ action: 'edit', old_str: 'a', new_str: 'b' });
|
|
134
|
-
expect(String(Array.isArray(e) ? e[0] : e)).toMatch(
|
|
135
|
+
expect(String(Array.isArray(e) ? e[0] : e)).toMatch(
|
|
136
|
+
/edit requires content_id/i
|
|
137
|
+
);
|
|
135
138
|
const v = await t.invoke({ action: 'verify' });
|
|
136
|
-
expect(String(Array.isArray(v) ? v[0] : v)).toMatch(
|
|
139
|
+
expect(String(Array.isArray(v) ? v[0] : v)).toMatch(
|
|
140
|
+
/verify requires content_id/i
|
|
141
|
+
);
|
|
137
142
|
const d = await t.invoke({ action: 'delete' });
|
|
138
|
-
expect(String(Array.isArray(d) ? d[0] : d)).toMatch(
|
|
143
|
+
expect(String(Array.isArray(d) ? d[0] : d)).toMatch(
|
|
144
|
+
/delete requires content_id/i
|
|
145
|
+
);
|
|
139
146
|
});
|
|
140
147
|
|
|
141
148
|
it('applies the resolver before dispatching to handler', async () => {
|
|
142
149
|
const handlers = makeWriteHandlers();
|
|
143
150
|
const resolver: ContentIdResolver = {
|
|
144
|
-
resolve: jest.fn(async (id) => ({
|
|
151
|
+
resolve: jest.fn(async (id) => ({
|
|
152
|
+
resolvedId: `canonical:${id}`,
|
|
153
|
+
resolvedName: 'X',
|
|
154
|
+
})),
|
|
145
155
|
};
|
|
146
156
|
const t = createArtifactTool({
|
|
147
157
|
handlers,
|
|
@@ -155,7 +165,7 @@ describe('createArtifactTool', () => {
|
|
|
155
165
|
new_str: 'b',
|
|
156
166
|
});
|
|
157
167
|
expect(handlers._calls[0].args).toEqual(
|
|
158
|
-
expect.objectContaining({ content_id: 'canonical:nickname' })
|
|
168
|
+
expect.objectContaining({ content_id: 'canonical:nickname' })
|
|
159
169
|
);
|
|
160
170
|
});
|
|
161
171
|
|
|
@@ -202,11 +212,17 @@ describe('createContentReaderTool', () => {
|
|
|
202
212
|
getScope: makeScope,
|
|
203
213
|
});
|
|
204
214
|
const r = await t.invoke({ action: 'read' });
|
|
205
|
-
expect(String(Array.isArray(r) ? r[0] : r)).toMatch(
|
|
215
|
+
expect(String(Array.isArray(r) ? r[0] : r)).toMatch(
|
|
216
|
+
/read requires content_id/i
|
|
217
|
+
);
|
|
206
218
|
const i = await t.invoke({ action: 'info' });
|
|
207
|
-
expect(String(Array.isArray(i) ? i[0] : i)).toMatch(
|
|
219
|
+
expect(String(Array.isArray(i) ? i[0] : i)).toMatch(
|
|
220
|
+
/info requires content_id/i
|
|
221
|
+
);
|
|
208
222
|
const s = await t.invoke({ action: 'search', pattern: 'p' });
|
|
209
|
-
expect(String(Array.isArray(s) ? s[0] : s)).toMatch(
|
|
223
|
+
expect(String(Array.isArray(s) ? s[0] : s)).toMatch(
|
|
224
|
+
/search requires content_id/i
|
|
225
|
+
);
|
|
210
226
|
});
|
|
211
227
|
|
|
212
228
|
it('rejects search without pattern even when content_id is given', async () => {
|
|
@@ -237,7 +253,7 @@ describe('createContentReaderTool', () => {
|
|
|
237
253
|
content_id: 'c-1',
|
|
238
254
|
start_line: 10,
|
|
239
255
|
end_line: 50,
|
|
240
|
-
})
|
|
256
|
+
})
|
|
241
257
|
);
|
|
242
258
|
});
|
|
243
259
|
});
|
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
|
|
3
|
-
export const ARTIFACT_WRITE_ACTIONS = [
|
|
3
|
+
export const ARTIFACT_WRITE_ACTIONS = [
|
|
4
|
+
'write',
|
|
5
|
+
'edit',
|
|
6
|
+
'verify',
|
|
7
|
+
'delete',
|
|
8
|
+
] as const;
|
|
4
9
|
export const CONTENT_READ_ACTIONS = ['read', 'search', 'list', 'info'] as const;
|
|
5
10
|
|
|
6
11
|
export const artifactToolSchema = z.object({
|
|
7
12
|
action: z
|
|
8
13
|
.enum(ARTIFACT_WRITE_ACTIONS)
|
|
9
14
|
.describe(
|
|
10
|
-
'Authoring action: write (create/overwrite), edit (str_replace), verify (syntax check), delete (remove).'
|
|
15
|
+
'Authoring action: write (create/overwrite), edit (str_replace), verify (syntax check), delete (remove).'
|
|
11
16
|
),
|
|
12
17
|
content_id: z
|
|
13
18
|
.string()
|
|
14
19
|
.optional()
|
|
15
20
|
.describe(
|
|
16
|
-
'ID of the artifact entry. Required for edit/verify/delete; optional for write (supply to overwrite an existing entry).'
|
|
21
|
+
'ID of the artifact entry. Required for edit/verify/delete; optional for write (supply to overwrite an existing entry).'
|
|
17
22
|
),
|
|
18
23
|
|
|
19
24
|
// write
|
|
@@ -25,7 +30,7 @@ export const artifactToolSchema = z.object({
|
|
|
25
30
|
.string()
|
|
26
31
|
.optional()
|
|
27
32
|
.describe(
|
|
28
|
-
'Filename for the new entry (required when creating). MUST include the correct file extension — the extension drives the preview template.'
|
|
33
|
+
'Filename for the new entry (required when creating). MUST include the correct file extension — the extension drives the preview template.'
|
|
29
34
|
),
|
|
30
35
|
|
|
31
36
|
// edit (str_replace)
|
|
@@ -33,12 +38,15 @@ export const artifactToolSchema = z.object({
|
|
|
33
38
|
.string()
|
|
34
39
|
.optional()
|
|
35
40
|
.describe('Exact string to find and replace (required for edit).'),
|
|
36
|
-
new_str: z
|
|
41
|
+
new_str: z
|
|
42
|
+
.string()
|
|
43
|
+
.optional()
|
|
44
|
+
.describe('Replacement string (required for edit).'),
|
|
37
45
|
replace_all: z
|
|
38
46
|
.boolean()
|
|
39
47
|
.optional()
|
|
40
48
|
.describe(
|
|
41
|
-
'edit: when true, replaces every occurrence of old_str. When false (default) and old_str matches more than one location, the edit is refused.'
|
|
49
|
+
'edit: when true, replaces every occurrence of old_str. When false (default) and old_str matches more than one location, the edit is refused.'
|
|
42
50
|
),
|
|
43
51
|
});
|
|
44
52
|
|
|
@@ -46,13 +54,13 @@ export const contentReaderSchema = z.object({
|
|
|
46
54
|
action: z
|
|
47
55
|
.enum(CONTENT_READ_ACTIONS)
|
|
48
56
|
.describe(
|
|
49
|
-
'Read-only action: read (lines), search (regex), list (all entries), info (metadata).'
|
|
57
|
+
'Read-only action: read (lines), search (regex), list (all entries), info (metadata).'
|
|
50
58
|
),
|
|
51
59
|
content_id: z
|
|
52
60
|
.string()
|
|
53
61
|
.optional()
|
|
54
62
|
.describe(
|
|
55
|
-
'ID of the content entry. Required for read/search/info. Omit for list.'
|
|
63
|
+
'ID of the content entry. Required for read/search/info. Omit for list.'
|
|
56
64
|
),
|
|
57
65
|
|
|
58
66
|
// read pagination
|
|
@@ -60,13 +68,28 @@ export const contentReaderSchema = z.object({
|
|
|
60
68
|
end_line: z.number().optional().describe('1-based end line (inclusive).'),
|
|
61
69
|
|
|
62
70
|
// search
|
|
63
|
-
pattern: z
|
|
64
|
-
|
|
65
|
-
|
|
71
|
+
pattern: z
|
|
72
|
+
.string()
|
|
73
|
+
.optional()
|
|
74
|
+
.describe('Regex pattern (required for search).'),
|
|
75
|
+
flags: z
|
|
76
|
+
.string()
|
|
77
|
+
.optional()
|
|
78
|
+
.describe('Regex flags (e.g., "i" for case-insensitive).'),
|
|
79
|
+
context: z
|
|
80
|
+
.number()
|
|
81
|
+
.optional()
|
|
82
|
+
.describe('Lines of context around each match.'),
|
|
66
83
|
|
|
67
84
|
// shared pagination
|
|
68
|
-
offset: z
|
|
69
|
-
|
|
85
|
+
offset: z
|
|
86
|
+
.number()
|
|
87
|
+
.optional()
|
|
88
|
+
.describe('Offset for read or search pagination.'),
|
|
89
|
+
limit: z
|
|
90
|
+
.number()
|
|
91
|
+
.optional()
|
|
92
|
+
.describe('Max lines (read) or matches (search).'),
|
|
70
93
|
});
|
|
71
94
|
|
|
72
95
|
export const ARTIFACT_TOOL_NAME = 'artifact_tool';
|