@vellumai/assistant 0.12.4-staging.2 → 0.12.4-staging.4
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/package.json +1 -1
- package/src/__tests__/openai-responses-prompt-cache.test.ts +3 -1
- package/src/__tests__/pricing.test.ts +33 -0
- package/src/__tests__/skill-load-tool.test.ts +42 -11
- package/src/__tests__/skill-tool-factory.test.ts +50 -2
- package/src/__tests__/validate-input.test.ts +73 -0
- package/src/cli/commands/__tests__/plugins.test.ts +29 -1
- package/src/cli/commands/plugins.ts +16 -0
- package/src/cli/lib/__tests__/inspect-plugin.test.ts +29 -0
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +32 -2
- package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +15 -0
- package/src/cli/lib/__tests__/plugin-catalog-resolve.test.ts +21 -0
- package/src/cli/lib/__tests__/plugin-catalog-visibility.test.ts +65 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +43 -0
- package/src/cli/lib/__tests__/plugins-install-offline.test.ts +26 -0
- package/src/cli/lib/inspect-plugin.ts +5 -0
- package/src/cli/lib/plugin-catalog-cache.ts +24 -2
- package/src/cli/lib/plugin-catalog-local.ts +9 -0
- package/src/cli/lib/plugin-catalog-visibility.ts +41 -0
- package/src/cli/lib/plugin-details.ts +10 -7
- package/src/cli/lib/search-plugins.ts +2 -0
- package/src/config/feature-flag-registry.json +9 -1
- package/src/plugins/defaults/memory/substrate/skill-content.ts +1 -1
- package/src/plugins/defaults/memory/v3/__tests__/render-injection.test.ts +3 -0
- package/src/providers/__tests__/connection-model-compat.test.ts +2 -0
- package/src/providers/anthropic/client.ts +2 -2
- package/src/providers/model-catalog.ts +234 -0
- package/src/providers/openai/codex-models.ts +2 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +8 -0
- package/src/runtime/routes/plugins-routes.ts +5 -2
- package/src/skills/validate-input.ts +75 -11
- package/src/tools/skills/execute.ts +1 -1
- package/src/tools/skills/format-input-schema.ts +15 -0
- package/src/tools/skills/load.ts +2 -26
- package/src/tools/skills/skill-tool-factory.ts +2 -1
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Deliberate strict subset of JSON Schema: it covers only the keywords
|
|
5
5
|
* actually used by `assistant/src/config/bundled-skills/**\/TOOLS.json`
|
|
6
|
-
* (required / type / enum / items
|
|
7
|
-
* else (`$ref`, `oneOf`, `anyOf`,
|
|
8
|
-
* `
|
|
9
|
-
*
|
|
6
|
+
* (required / type / enum / nested properties and items / required-only oneOf)
|
|
7
|
+
* plus unknown-key detection. Anything else (`$ref`, richer `oneOf`, `anyOf`,
|
|
8
|
+
* `allOf`, `format`, `pattern`, `minimum`, `maximum`, object-shape
|
|
9
|
+
* `additionalProperties`, etc.) is skipped. Nested objects allow extra keys
|
|
10
|
+
* unless their schema explicitly closes them.
|
|
10
11
|
*
|
|
11
12
|
* Each error message is written to be agent-readable and self-correcting
|
|
12
13
|
* (e.g. `surface_id is required`, `mode must be one of "replace", "append"`).
|
|
@@ -313,6 +314,15 @@ export function validateInputAgainstSchema(
|
|
|
313
314
|
_toolName: string,
|
|
314
315
|
input: Record<string, unknown>,
|
|
315
316
|
schema: Record<string, unknown> | undefined,
|
|
317
|
+
): InputValidationResult {
|
|
318
|
+
return validateObjectInput(input, schema, "", true);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function validateObjectInput(
|
|
322
|
+
input: Record<string, unknown>,
|
|
323
|
+
schema: Record<string, unknown> | undefined,
|
|
324
|
+
path: string,
|
|
325
|
+
rejectUnknown: boolean,
|
|
316
326
|
): InputValidationResult {
|
|
317
327
|
// Skip when there's no schema or no properties block — matches today's
|
|
318
328
|
// lenient behaviour for tools that declare only `{ type: "object" }`.
|
|
@@ -338,17 +348,46 @@ export function validateInputAgainstSchema(
|
|
|
338
348
|
continue;
|
|
339
349
|
}
|
|
340
350
|
if (!(key in input)) {
|
|
341
|
-
errors.push(`${key} is required`);
|
|
351
|
+
errors.push(`${path}${key} is required`);
|
|
342
352
|
}
|
|
343
353
|
}
|
|
344
354
|
}
|
|
345
355
|
|
|
356
|
+
// Only enforce alternatives whose complete contract is field presence.
|
|
357
|
+
// Richer unions stay permissive because unsupported keywords can decide
|
|
358
|
+
// which branch matches.
|
|
359
|
+
const alternatives = schema.oneOf;
|
|
360
|
+
if (
|
|
361
|
+
Array.isArray(alternatives) &&
|
|
362
|
+
alternatives.length > 0 &&
|
|
363
|
+
alternatives.every(
|
|
364
|
+
(branch) =>
|
|
365
|
+
isPlainObject(branch) &&
|
|
366
|
+
Object.keys(branch).every(
|
|
367
|
+
(key) => key === "required" || key === "description",
|
|
368
|
+
) &&
|
|
369
|
+
Array.isArray(branch.required) &&
|
|
370
|
+
branch.required.every((key: unknown) => typeof key === "string"),
|
|
371
|
+
)
|
|
372
|
+
) {
|
|
373
|
+
const choices = alternatives as { required: string[] }[];
|
|
374
|
+
if (
|
|
375
|
+
choices.filter((branch) => branch.required.every((key) => key in input))
|
|
376
|
+
.length !== 1
|
|
377
|
+
) {
|
|
378
|
+
errors.push(
|
|
379
|
+
`${path ? path.slice(0, -1) : "input"} must match exactly one required field set: ${choices.map((branch) => quoteList(branch.required)).join(" or ")}`,
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
346
384
|
// 2. Per-property checks: type, enum, items.type.
|
|
347
|
-
for (const [
|
|
348
|
-
if (!(
|
|
385
|
+
for (const [property, rawSubSchema] of Object.entries(properties)) {
|
|
386
|
+
if (!(property in input)) {
|
|
349
387
|
continue;
|
|
350
388
|
}
|
|
351
|
-
const
|
|
389
|
+
const key = `${path}${property}`;
|
|
390
|
+
const value = input[property];
|
|
352
391
|
// Skip type-checking for absent values; presence is enforced by the
|
|
353
392
|
// `required` check above. Note: `null` IS a present value and is
|
|
354
393
|
// type-checked below — only schemas that explicitly opt in to null via
|
|
@@ -381,13 +420,25 @@ export function validateInputAgainstSchema(
|
|
|
381
420
|
if (!matchesType(value, type)) {
|
|
382
421
|
errors.push(
|
|
383
422
|
type === "boolean" && typeof value === "string"
|
|
384
|
-
? `${key} must be a boolean
|
|
423
|
+
? `${key} must be a boolean: pass true or false as a JSON boolean, not a string`
|
|
385
424
|
: `${key} must be ${typeArticle(type)} ${type}`,
|
|
386
425
|
);
|
|
387
426
|
// No point checking enum/items if the base type is wrong.
|
|
388
427
|
continue;
|
|
389
428
|
}
|
|
390
429
|
|
|
430
|
+
if (type === "object" && isPlainObject(value)) {
|
|
431
|
+
const nested = validateObjectInput(
|
|
432
|
+
value,
|
|
433
|
+
rawSubSchema,
|
|
434
|
+
`${key}.`,
|
|
435
|
+
rawSubSchema.additionalProperties === false,
|
|
436
|
+
);
|
|
437
|
+
if (!nested.ok) {
|
|
438
|
+
errors.push(...nested.errors);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
391
442
|
// 2a. Enum (string enums are the only shape used today).
|
|
392
443
|
const enumValues = rawSubSchema.enum;
|
|
393
444
|
if (Array.isArray(enumValues) && enumValues.length > 0) {
|
|
@@ -412,6 +463,17 @@ export function validateInputAgainstSchema(
|
|
|
412
463
|
errors.push(
|
|
413
464
|
`${key}[${index}] must be ${typeArticle(itemTypeName)} ${itemTypeName}`,
|
|
414
465
|
);
|
|
466
|
+
} else if (itemTypeName === "object" && isPlainObject(element)) {
|
|
467
|
+
const itemSchema = rawSubSchema.items as Record<string, unknown>;
|
|
468
|
+
const nested = validateObjectInput(
|
|
469
|
+
element,
|
|
470
|
+
itemSchema,
|
|
471
|
+
`${key}[${index}].`,
|
|
472
|
+
itemSchema.additionalProperties === false,
|
|
473
|
+
);
|
|
474
|
+
if (!nested.ok) {
|
|
475
|
+
errors.push(...nested.errors);
|
|
476
|
+
}
|
|
415
477
|
}
|
|
416
478
|
});
|
|
417
479
|
}
|
|
@@ -420,10 +482,12 @@ export function validateInputAgainstSchema(
|
|
|
420
482
|
}
|
|
421
483
|
|
|
422
484
|
// 3. Unknown keys — parity with the previous `validateNoUnknownParams`.
|
|
423
|
-
const unknownKeys =
|
|
485
|
+
const unknownKeys = rejectUnknown
|
|
486
|
+
? Object.keys(input).filter((k) => !knownKeySet.has(k))
|
|
487
|
+
: [];
|
|
424
488
|
for (const key of unknownKeys) {
|
|
425
489
|
errors.push(
|
|
426
|
-
`Unknown parameter "${key}". Supported: ${quoteList(knownKeys)}`,
|
|
490
|
+
`Unknown parameter "${path}${key}". Supported: ${quoteList(knownKeys)}`,
|
|
427
491
|
);
|
|
428
492
|
}
|
|
429
493
|
|
|
@@ -178,7 +178,7 @@ export function augmentSkillExecuteError(
|
|
|
178
178
|
export const skillExecuteTool = {
|
|
179
179
|
name: "skill_execute",
|
|
180
180
|
description:
|
|
181
|
-
"Execute a tool provided by a loaded skill.
|
|
181
|
+
"Execute a tool provided by a loaded skill. When skill_load is available, call it for the owning skill before first use, even if a capability card says it is available. Use the current instructions and input schema over remembered arguments. Use this instead of calling skill tools directly. For browser automation, use the `assistant browser` CLI commands instead.",
|
|
182
182
|
category: "skills",
|
|
183
183
|
executionTarget: "sandbox",
|
|
184
184
|
defaultRiskLevel: RiskLevel.Low,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { getWorkspaceDirDisplay } from "../../util/platform.js";
|
|
2
|
+
|
|
3
|
+
/** Preserve nested fields and alternatives when disclosing a skill's input. */
|
|
4
|
+
export function formatSkillInputSchema(schema: object): string {
|
|
5
|
+
const workspaceDir = getWorkspaceDirDisplay();
|
|
6
|
+
const json = JSON.stringify(
|
|
7
|
+
schema,
|
|
8
|
+
(_key, value: unknown) =>
|
|
9
|
+
typeof value === "string"
|
|
10
|
+
? value.replaceAll("{workspaceDir}", workspaceDir)
|
|
11
|
+
: value,
|
|
12
|
+
2,
|
|
13
|
+
);
|
|
14
|
+
return `Input schema for \`skill_execute.input\`:\n\`\`\`json\n${json}\n\`\`\``;
|
|
15
|
+
}
|
package/src/tools/skills/load.ts
CHANGED
|
@@ -35,6 +35,7 @@ import type {
|
|
|
35
35
|
ToolDefinition,
|
|
36
36
|
ToolExecutionResult,
|
|
37
37
|
} from "../types.js";
|
|
38
|
+
import { formatSkillInputSchema } from "./format-input-schema.js";
|
|
38
39
|
|
|
39
40
|
/** Skill sources eligible for inline command expansion in v1. */
|
|
40
41
|
const INLINE_COMMAND_ELIGIBLE_SOURCES = new Set([
|
|
@@ -123,32 +124,7 @@ function formatToolSchemas(
|
|
|
123
124
|
tool.description.replaceAll("{workspaceDir}", getWorkspaceDirDisplay()),
|
|
124
125
|
);
|
|
125
126
|
|
|
126
|
-
|
|
127
|
-
const properties = schema.properties as
|
|
128
|
-
| Record<string, Record<string, unknown>>
|
|
129
|
-
| undefined;
|
|
130
|
-
if (properties && Object.keys(properties).length > 0) {
|
|
131
|
-
const requiredSet = new Set<string>(
|
|
132
|
-
Array.isArray(schema.required) ? (schema.required as string[]) : [],
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
lines.push("Parameters:");
|
|
136
|
-
for (const [paramName, paramDef] of Object.entries(properties)) {
|
|
137
|
-
const paramType =
|
|
138
|
-
typeof paramDef.type === "string" ? paramDef.type : "any";
|
|
139
|
-
const requiredLabel = requiredSet.has(paramName)
|
|
140
|
-
? "required"
|
|
141
|
-
: "optional";
|
|
142
|
-
const descPart =
|
|
143
|
-
typeof paramDef.description === "string"
|
|
144
|
-
? `: ${paramDef.description.replaceAll("{workspaceDir}", getWorkspaceDirDisplay())}`
|
|
145
|
-
: "";
|
|
146
|
-
lines.push(
|
|
147
|
-
`- ${paramName} (${paramType}, ${requiredLabel})${descPart}`,
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
127
|
+
lines.push(formatSkillInputSchema(tool.input_schema));
|
|
152
128
|
lines.push("");
|
|
153
129
|
}
|
|
154
130
|
|
|
@@ -15,6 +15,7 @@ import { bundledToolInputMisuseMessage } from "../shared/input-misuse.js";
|
|
|
15
15
|
import { bundledToolInputRepairs } from "../shared/input-repairs.js";
|
|
16
16
|
import type { ExecutionTarget } from "../tool-types.js";
|
|
17
17
|
import type { Tool, ToolContext, ToolExecutionResult } from "../types.js";
|
|
18
|
+
import { formatSkillInputSchema } from "./format-input-schema.js";
|
|
18
19
|
import { runSkillToolScript } from "./skill-script-runner.js";
|
|
19
20
|
|
|
20
21
|
const riskMap: Record<SkillToolEntry["risk"], RiskLevel> = {
|
|
@@ -94,7 +95,7 @@ export function createSkillTool(
|
|
|
94
95
|
return {
|
|
95
96
|
content:
|
|
96
97
|
misuse ??
|
|
97
|
-
`Invalid input for tool "${entry.name}": ${validation.errors.join("; ")}.
|
|
98
|
+
`Invalid input for tool "${entry.name}": ${validation.errors.join("; ")}. If skill_load is available, load the owning skill for its current instructions. Retry with arguments matching this schema.\n\n${formatSkillInputSchema(entry.input_schema)}`,
|
|
98
99
|
isError: true,
|
|
99
100
|
};
|
|
100
101
|
}
|