@lowdefy/ai-utils 5.5.0 → 5.6.0

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.
@@ -16,7 +16,7 @@
16
16
  import { createMCPClient } from '@ai-sdk/mcp';
17
17
  import { Experimental_StdioMCPTransport } from '@ai-sdk/mcp/mcp-stdio';
18
18
  import { ConfigError } from '@lowdefy/errors';
19
- import { serializer, translate } from '@lowdefy/helpers';
19
+ import { getKey, isReserved, serializer, setKey, translate } from '@lowdefy/helpers';
20
20
  import listFiles from './fileSystem/listFiles.js';
21
21
  import readFile from './fileSystem/readFile.js';
22
22
  import searchFiles from './fileSystem/searchFiles.js';
@@ -30,7 +30,7 @@ import RESERVED_PLATFORM_TOOL_NAMES from './reservedToolNames.js';
30
30
  function cleanBuildArtifact(obj) {
31
31
  return JSON.parse(JSON.stringify(serializer.deserialize(obj)));
32
32
  }
33
- function assertNotReserved(name, kind, i18n) {
33
+ function assertNotPlatformToolName(name, kind, i18n) {
34
34
  if (RESERVED_PLATFORM_TOOL_NAMES.includes(name)) {
35
35
  throw new ConfigError(translate({
36
36
  key: 'agent.runtime.reservedToolName',
@@ -59,11 +59,20 @@ async function buildAgentTools({ agent, context, depth = 0 }) {
59
59
  // Build endpoint tools
60
60
  for (const toolConfig of agent.tools ?? []){
61
61
  const { endpointId, confirm } = toolConfig;
62
- assertNotReserved(endpointId, 'Endpoint tool', context.i18n);
62
+ assertNotPlatformToolName(endpointId, 'Endpoint tool', context.i18n);
63
+ // A second, disjoint list: RESERVED_PLATFORM_TOOL_NAMES are the tools Lowdefy itself registers,
64
+ // isReserved are the prototype-pollution keys setKey refuses. A name can pass one gate and fail
65
+ // the other, so both checks are needed. The build rejects reserved key names, so this only fires
66
+ // for a stale or hand-edited build artifact. Skip the tool rather than fail the whole agent, as
67
+ // the MCP name cases below do.
68
+ if (isReserved(endpointId)) {
69
+ console.warn(`Endpoint tool "${endpointId}" uses a reserved key name — skipped.`);
70
+ continue;
71
+ }
63
72
  const endpointConfig = await context.getEndpointConfig({
64
73
  endpointId
65
74
  });
66
- tools[endpointId] = tool({
75
+ setKey(tools, endpointId, tool({
67
76
  description: endpointConfig.description,
68
77
  inputSchema: jsonSchema(cleanBuildArtifact(endpointConfig.payloadSchema)),
69
78
  ...confirm ? {
@@ -83,7 +92,7 @@ async function buildAgentTools({ agent, context, depth = 0 }) {
83
92
  }
84
93
  return cleanBuildArtifact(result.response);
85
94
  }
86
- });
95
+ }));
87
96
  }
88
97
  // Build MCP clients and merge their tools
89
98
  for (const mcpSource of agent.mcp ?? []){
@@ -132,18 +141,20 @@ async function buildAgentTools({ agent, context, depth = 0 }) {
132
141
  console.warn(`MCP tool "${name}" from ${source.url ?? source.command} ` + `uses a reserved platform tool name — skipped.`);
133
142
  continue;
134
143
  }
135
- if (tools[name]) {
136
- console.warn(`MCP tool "${name}" from ${source.url ?? source.command} ` + `conflicts with endpoint tool — skipped.`);
144
+ // Same policy as the reserved-platform-name and collision cases above:
145
+ // MCP tool names are not under app control, so warn and skip.
146
+ if (isReserved(name)) {
147
+ console.warn(`MCP tool "${name}" from ${source.url ?? source.command} ` + `uses a reserved key name — skipped.`);
137
148
  continue;
138
149
  }
139
- if (source.confirm) {
140
- tools[name] = {
141
- ...mcpTool,
142
- needsApproval: true
143
- };
144
- } else {
145
- tools[name] = mcpTool;
150
+ if (getKey(tools, name)) {
151
+ console.warn(`MCP tool "${name}" from ${source.url ?? source.command} ` + `conflicts with endpoint tool — skipped.`);
152
+ continue;
146
153
  }
154
+ setKey(tools, name, source.confirm ? {
155
+ ...mcpTool,
156
+ needsApproval: true
157
+ } : mcpTool);
147
158
  }
148
159
  } catch (err) {
149
160
  const label = source.transport === 'stdio' ? source.command : source.url;
@@ -152,7 +163,13 @@ async function buildAgentTools({ agent, context, depth = 0 }) {
152
163
  }
153
164
  // Build sub-agent tools
154
165
  for (const subAgentRef of agent.agents ?? []){
155
- assertNotReserved(subAgentRef.agentId, 'Sub-agent', context.i18n);
166
+ assertNotPlatformToolName(subAgentRef.agentId, 'Sub-agent', context.i18n);
167
+ // Same policy as the endpoint tool name above: unreachable from valid config, so skip rather
168
+ // than fail the whole agent.
169
+ if (isReserved(subAgentRef.agentId)) {
170
+ console.warn(`Sub-agent tool "${subAgentRef.agentId}" uses a reserved key name — skipped.`);
171
+ continue;
172
+ }
156
173
  const subAgentConfig = await context.getAgentConfig({
157
174
  agentId: subAgentRef.agentId
158
175
  });
@@ -192,7 +209,7 @@ async function buildAgentTools({ agent, context, depth = 0 }) {
192
209
  'task'
193
210
  ]
194
211
  });
195
- tools[subAgentRef.agentId] = tool({
212
+ setKey(tools, subAgentRef.agentId, tool({
196
213
  description,
197
214
  inputSchema,
198
215
  execute: async (input, { abortSignal })=>{
@@ -213,7 +230,7 @@ async function buildAgentTools({ agent, context, depth = 0 }) {
213
230
  type: 'text',
214
231
  value: output.text ?? String(output)
215
232
  })
216
- });
233
+ }));
217
234
  }
218
235
  // Build fileSystem tools
219
236
  if (agent.properties?.fileSystem) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lowdefy/ai-utils",
3
- "version": "5.5.0",
3
+ "version": "5.6.0",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -17,8 +17,8 @@
17
17
  ],
18
18
  "dependencies": {
19
19
  "@ai-sdk/mcp": "1.0.41",
20
- "@lowdefy/errors": "5.5.0",
21
- "@lowdefy/helpers": "5.5.0",
20
+ "@lowdefy/errors": "5.6.0",
21
+ "@lowdefy/helpers": "5.6.0",
22
22
  "ai": "6.0.176",
23
23
  "glob": "13.0.6"
24
24
  },