@deepstrike/sdk 0.2.44 → 0.2.45

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.
@@ -124,9 +124,11 @@ export class LocalExecutionPlane {
124
124
  },
125
125
  };
126
126
  try {
127
- const args = JSON.parse(call.arguments || "{}");
128
- const originalArgsStr = JSON.stringify(args);
129
- const validation = validateToolArguments(registered.schema.parameters, args);
127
+ const rawArgs = JSON.parse(call.arguments || "{}");
128
+ const originalArgsStr = JSON.stringify(rawArgs);
129
+ // validation.args, not rawArgs, from here on: a oneOf/anyOf ROOT accepts a repaired probe
130
+ // CLONE — the original reference never sees those repairs (auto-casts, strips, defaults).
131
+ const validation = validateToolArguments(registered.schema.parameters, rawArgs);
130
132
  if (validation.error)
131
133
  return { callId: call.id, output: `invalid arguments: ${validation.error}`, isError: true };
132
134
  if (validation.repaired) {
@@ -135,13 +137,13 @@ export class LocalExecutionPlane {
135
137
  callId: call.id,
136
138
  name: call.name,
137
139
  originalArguments: originalArgsStr,
138
- repairedArguments: JSON.stringify(args),
140
+ repairedArguments: JSON.stringify(validation.args),
139
141
  };
140
142
  }
141
143
  // M3/G4: pass the run context (incl. `cwd`) so cwd-aware tools scope their work to the
142
144
  // sub-agent's worktree. `RunContext` is structurally assignable to the tool's `ToolExecContext`.
143
145
  // The per-call `audit` helper (above) layers best-effort side-effect handling on top.
144
- const output = await registered.execute(args, callCtx);
146
+ const output = await registered.execute(validation.args, callCtx);
145
147
  if (isAsyncIterable(output)) {
146
148
  let combined = "";
147
149
  const iterator = output[Symbol.asyncIterator]();
@@ -26,6 +26,7 @@ export declare function toolChunkText(chunk: ToolChunk): string;
26
26
  export declare function validateToolArguments(schemaJson: string, args: Record<string, unknown>): {
27
27
  error?: string;
28
28
  repaired: boolean;
29
+ args: Record<string, unknown>;
29
30
  };
30
31
  export declare function executeTools(calls: {
31
32
  id: string;
@@ -1,11 +1,24 @@
1
1
  import { formatToolError } from "./errors.js";
2
+ /** Fail at registration, not as a vendor 400 at call time: every major provider (OpenAI-compat,
3
+ * Anthropic, Gemini) rejects a tool whose parameters root is not `type: "object"` — the wire
4
+ * error ("schema must be a JSON Schema of 'type: \"object\"'") surfaces far from the tool that
5
+ * caused it. Union roots must be wrapped: object root + flattened properties + `oneOf` sibling. */
6
+ function assertObjectRootSchema(name, parameters) {
7
+ if (!parameters || typeof parameters !== "object" || Array.isArray(parameters) || parameters.type !== "object") {
8
+ throw new Error(`tool "${name}": parameters must be a JSON Schema with root type "object" `
9
+ + `(got type: ${JSON.stringify(parameters?.type ?? null)}); `
10
+ + `providers reject any other root — wrap union variants as an object root with a oneOf sibling`);
11
+ }
12
+ }
2
13
  export function tool(name, description, parameters, fn) {
14
+ assertObjectRootSchema(name, parameters);
3
15
  return {
4
16
  schema: { name, description, parameters: JSON.stringify(parameters) },
5
17
  async execute(args, ctx) { return fn(args, ctx); },
6
18
  };
7
19
  }
8
20
  export function streamingTool(name, description, parameters, fn) {
21
+ assertObjectRootSchema(name, parameters);
9
22
  return {
10
23
  schema: { name, description, parameters: JSON.stringify(parameters) },
11
24
  execute(args, ctx) { return fn(args, ctx); },
@@ -27,12 +40,14 @@ export function validateToolArguments(schemaJson, args) {
27
40
  schema = JSON.parse(schemaJson);
28
41
  }
29
42
  catch {
30
- return { error: "invalid tool schema", repaired: false };
43
+ return { error: "invalid tool schema", repaired: false, args };
31
44
  }
32
45
  const state = { repaired: false };
33
46
  const wrapper = { root: args };
34
47
  const error = validateValue(schema, wrapper, "root", "$", state);
35
- return { error, repaired: state.repaired };
48
+ // A oneOf/anyOf ROOT replaces the value with its accepted probe clone — in-place mutation of
49
+ // the caller's object only covers non-union roots. Callers must use the returned `args`.
50
+ return { error, repaired: state.repaired, args: wrapper.root };
36
51
  }
37
52
  function validateValue(schema, parent, key, path, state) {
38
53
  let value = parent[key];
@@ -188,12 +203,72 @@ function validateValue(schema, parent, key, path, state) {
188
203
  if (typeof value !== "boolean")
189
204
  return `${path} must be boolean`;
190
205
  }
206
+ else if (expectedType === "null") {
207
+ if (value !== null)
208
+ return `${path} must be null`;
209
+ }
191
210
  }
192
211
  else if (path === "$" && (!value || typeof value !== "object" || Array.isArray(value))) {
193
212
  return `${path} must be object`;
194
213
  }
195
214
  if (Array.isArray(schema.enum) && !schema.enum.includes(value))
196
215
  return `${path} must be one of enum values`;
216
+ // `const` is THE discriminator convention for oneOf variants (kind: {const: "edit"}). Without
217
+ // it, union branches match on required+type alone and the WRONG branch can win — then its
218
+ // allow-list strips keys the right branch declared.
219
+ if ("const" in schema) {
220
+ const want = schema.const;
221
+ const matches = want !== null && typeof want === "object"
222
+ ? JSON.stringify(value) === JSON.stringify(want)
223
+ : value === want;
224
+ if (!matches)
225
+ return `${path} must equal the const value ${JSON.stringify(want)}`;
226
+ }
227
+ // Constraint keywords, checked per the value's actual type (JSON Schema semantics: string
228
+ // constraints ignore non-strings, etc.). Keywords outside this set (allOf, multipleOf,
229
+ // uniqueItems, format, if/then/else, …) are ignored, not rejected.
230
+ if (typeof value === "string") {
231
+ if (typeof schema.minLength === "number" && value.length < schema.minLength) {
232
+ return `${path} must be at least ${schema.minLength} characters`;
233
+ }
234
+ if (typeof schema.maxLength === "number" && value.length > schema.maxLength) {
235
+ return `${path} must be at most ${schema.maxLength} characters`;
236
+ }
237
+ if (typeof schema.pattern === "string") {
238
+ let re;
239
+ try {
240
+ re = new RegExp(schema.pattern);
241
+ }
242
+ catch {
243
+ re = undefined;
244
+ } // author-side bad regex: skip, never fail the call
245
+ if (re && !re.test(value))
246
+ return `${path} must match pattern ${schema.pattern}`;
247
+ }
248
+ }
249
+ else if (typeof value === "number") {
250
+ if (typeof schema.minimum === "number" && value < schema.minimum)
251
+ return `${path} must be >= ${schema.minimum}`;
252
+ if (typeof schema.maximum === "number" && value > schema.maximum)
253
+ return `${path} must be <= ${schema.maximum}`;
254
+ if (typeof schema.exclusiveMinimum === "number" && value <= schema.exclusiveMinimum)
255
+ return `${path} must be > ${schema.exclusiveMinimum}`;
256
+ if (typeof schema.exclusiveMaximum === "number" && value >= schema.exclusiveMaximum)
257
+ return `${path} must be < ${schema.exclusiveMaximum}`;
258
+ }
259
+ else if (Array.isArray(value)) {
260
+ if (typeof schema.minItems === "number" && value.length < schema.minItems)
261
+ return `${path} must have at least ${schema.minItems} items`;
262
+ if (typeof schema.maxItems === "number" && value.length > schema.maxItems)
263
+ return `${path} must have at most ${schema.maxItems} items`;
264
+ }
265
+ // `not`: probe on a clone so a matching (= rejected) subschema's repairs never leak out.
266
+ if (schema.not && typeof schema.not === "object" && !Array.isArray(schema.not)) {
267
+ const probe = { v: structuredClone(value) };
268
+ if (!validateValue(schema.not, probe, "v", path, { repaired: false })) {
269
+ return `${path} must not match the disallowed shape`;
270
+ }
271
+ }
197
272
  return undefined;
198
273
  }
199
274
  export async function executeTools(calls, registry) {
@@ -206,7 +281,9 @@ export async function executeTools(calls, registry) {
206
281
  const validation = validateToolArguments(t.schema.parameters, args);
207
282
  if (validation.error)
208
283
  return { callId: c.id, output: `invalid arguments: ${validation.error}`, isError: true };
209
- const output = await t.execute(args);
284
+ // validation.args, not args: a oneOf/anyOf ROOT accepts a repaired probe CLONE — the
285
+ // original reference never sees those repairs (auto-casts, strips, defaults).
286
+ const output = await t.execute(validation.args);
210
287
  if (isAsyncIterable(output)) {
211
288
  let combined = "";
212
289
  for await (const chunk of output)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepstrike/sdk",
3
- "version": "0.2.44",
3
+ "version": "0.2.45",
4
4
  "description": "DeepStrike Node.js SDK",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -72,7 +72,7 @@
72
72
  },
73
73
  "dependencies": {
74
74
  "@anthropic-ai/sdk": "^0.99.0",
75
- "@deepstrike/core": "0.2.44",
75
+ "@deepstrike/core": "0.2.45",
76
76
  "@google/generative-ai": "^0.24.1",
77
77
  "openai": "^5.23.2"
78
78
  },