chore(deps): upgrade zod to 4.6.7 (#2405)

* chore(deps): upgrade zod to 4.6.7

* test: normalize tool schema fixture paths to forward slashes

* fix: omit AgentTool fields before cwd refinement and export settings schema via zodToJsonSchema
This commit is contained in:
JATMN
2026-10-24 15:26:20 -07:00
committed by GitHub
parent ef78478fc0
commit be3a71711c
38 changed files with 794 additions and 111 deletions
+9
View File
@@ -256,6 +256,15 @@ Keep all direct dependencies and devDependencies pinned to exact versions in the
root and web manifests. Regenerate the relevant Bun lockfile
when changing a pin.
Schema validation uses Zod `4.6.7`, pinned exactly in the root development
dependencies and in the root and website resolution overrides. Import
`zod/v4` for application schemas, use native conversion through the shared
`zodToJsonSchema` helper, and preserve validation messages and generated schema
contracts. Follow [Schema validation](docs/schema-validation.md) for object
strictness, defaults, error customization, dependency compatibility and SDK
peer handling. Regenerate both lockfiles with `bun install` after changing
the shared pin.
The static website in `web/` uses Astro `7.4.2`, `@astrojs/check` `0.9.12`,
and `@astrojs/sitemap` `3.7.4`, all pinned exactly. Install its dependencies
with `bun install --cwd web`, then run `bun run web:typecheck` and
+3 -4
View File
@@ -88,7 +88,7 @@
"ws": "8.22.0",
"xss": "1.0.16",
"yaml": "2.9.1",
"zod": "3.25.76",
"zod": "4.6.7",
},
"peerDependencies": {
"@anthropic-ai/sdk": "^0.137.0",
@@ -109,6 +109,7 @@
"ip-address": "10.7.4",
"lodash-es": "4.18.1",
"node-domexception": "file:vendor/node-domexception-shim",
"zod": "4.6.7",
},
"packages": {
"@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.3.1", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-cUD0PT0YX+rImRhfn7BfMRduP/tX/Zqt/N0dHlAyn2RlcJssCb5KsCJkhln61eh9vuJDkH8wKn3L8zJzzWXPFg=="],
@@ -1239,7 +1240,7 @@
"yoctocolors": ["yoctocolors@2.2.0", "", {}, "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg=="],
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"zod": ["zod@4.6.7", "", {}, "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg=="],
"zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
@@ -1475,8 +1476,6 @@
"http-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
"knip/zod": ["zod@4.6.7", "", {}, "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg=="],
"needle/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
"npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
+40
View File
@@ -0,0 +1,40 @@
# Schema validation
Use Zod 4 through `zod/v4`. The root development dependency and root and website
resolution overrides pin the package to exactly `4.6.7`. Regenerate both lockfiles
with Bun after changing the pin.
Use `z.strictObject` when unknown keys must fail, `z.looseObject` when they must
survive, and `z.object` when unknown keys should be stripped. Records require
explicit key and value schemas, for example `z.record(z.string(), z.unknown())`.
Prefer top-level format schemas such as `z.email`, `z.url`, and `z.iso.datetime`.
Use `.check` to apply a format schema after existing length checks. Customize
messages with the `error` option and read validation failures from `.issues`.
A default short-circuits parsing and must match the output type. Use `.prefault`
when a fallback must pass through preprocessing or transforms. Defaults inside
optional object fields are still applied. Preserve these choices when editing
schemas, especially around numeric and boolean input preprocessing.
Generate API schemas through the shared `zodToJsonSchema` helper. It retains
native conversion, caches by schema identity, and preserves the established
`anyOf` representation for primitive and discriminated unions. This keeps the
published JSON documents stable across the package upgrade. It visits schema
keywords only, leaving defaults and other user data intact.
The schema compatibility tests capture fingerprints of complete generated and
advertised JSON documents for every exported input and output schema. They run
the enumeration in a fresh process to avoid shared mocks hiding schemas. These
snapshots were captured before the dependency upgrade; do not regenerate them
to hide unintended changes. The fixture's `--full` option prints the documents
for investigating a changed fingerprint.
The sandbox dependency still uses `zod/v3`. The exact root override resolves it
to the v3 compatibility export shipped in the pinned v4 package; the dependency
is unmodified. Dependency compatibility tests exercise that configuration
parser, both protocol conversion branches, and structured-output parsing.
Zod is bundled into the distributed JavaScript. The published SDK declarations
do not refer to it, and it is not exposed as a peer dependency. If that type
contract changes, declare a compatible peer range and document the SDK breaking
change before publishing.
+3 -2
View File
@@ -168,7 +168,7 @@
"ws": "8.22.0",
"xss": "1.0.16",
"yaml": "2.9.1",
"zod": "3.25.76"
"zod": "4.6.7"
},
"peerDependencies": {
"@anthropic-ai/sdk": "^0.137.0",
@@ -215,7 +215,8 @@
"ip-address": "10.7.4",
"google-auth-library": "11.3.1",
"lodash-es": "4.18.1",
"node-domexception": "file:vendor/node-domexception-shim"
"node-domexception": "file:vendor/node-domexception-shim",
"zod": "4.6.7"
},
"allowScripts": {
"protobufjs@7.6.4": true
+3 -3
View File
@@ -7,7 +7,7 @@ process.env.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS ??= 'true'
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { ZodError } from 'zod'
import { ZodError } from 'zod/v4'
import {
CallToolRequestSchema,
type CallToolResult,
@@ -35,7 +35,7 @@ import { getMainLoopModel } from '../utils/model/model.js'
import { hasPermissionsToUseTool } from '../utils/permissions/permissions.js'
import { setCwd } from '../utils/Shell.js'
import { jsonStringify } from '../utils/slowOperations.js'
import { getErrorParts } from '../utils/toolErrors.js'
import { formatMcpValidationError, getErrorParts } from '../utils/toolErrors.js'
import { zodToJsonSchema } from '../utils/zodToJsonSchema.js'
type ToolInput = Tool['inputSchema']
@@ -233,7 +233,7 @@ export async function startMCPServer(
content: [
{
type: 'text',
text: `Tool ${name} input is invalid:\n${error.errors.map(e => `- ${e.path.join('.')}: ${e.message}`).join('\n')}`,
text: formatMcpValidationError(name, error),
},
],
}
+3 -4
View File
@@ -90,7 +90,7 @@ export const SandboxFilesystemConfigSchema = lazySchema(() =>
*/
export const SandboxSettingsSchema = lazySchema(() =>
z
.object({
.looseObject({
enabled: z.boolean().optional(),
failIfUnavailable: z
.boolean()
@@ -101,7 +101,7 @@ export const SandboxSettingsSchema = lazySchema(() =>
'When false (default), a warning is shown and commands run unsandboxed. ' +
'Intended for managed-settings deployments that require sandboxing as a hard gate.',
),
// Note: enabledPlatforms is an undocumented setting read via .passthrough()
// Note: enabledPlatforms is an undocumented setting read via z.looseObject()
// It restricts sandboxing to specific platforms (e.g., ["macos"]).
//
// Added to unblock NVIDIA enterprise rollout: they want to enable
@@ -139,8 +139,7 @@ export const SandboxSettingsSchema = lazySchema(() =>
})
.optional()
.describe('Custom ripgrep configuration for bundled ripgrep support'),
})
.passthrough(),
}),
)
// Inferred types from schemas
+1 -1
View File
@@ -10,7 +10,7 @@ const LogEventSchema = lazySchema(() =>
method: z.literal('log_event'),
params: z.object({
eventName: z.string(),
eventData: z.object({}).passthrough(),
eventData: z.looseObject({}),
}),
}),
)
+1 -1
View File
@@ -96,7 +96,7 @@ function buildHookSchemas() {
const HttpHookSchema = z.object({
type: z.literal('http').describe('HTTP hook type'),
url: z.string().url().describe('URL to POST the hook input JSON to'),
url: z.url().describe('URL to POST the hook input JSON to'),
if: IfConditionSchema(),
timeout: z
.number()
+2 -2
View File
@@ -21,7 +21,7 @@ import {
getClaudeAIOAuthTokens,
hasProfileScope,
} from 'src/utils/auth.js'
import { z } from 'zod'
import { z } from 'zod/v4'
import { getOauthConfig, OAUTH_BETA_HEADER } from '../../constants/oauth.js'
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
import { logForDebugging } from '../../utils/debug.js'
@@ -47,7 +47,7 @@ import {
const bootstrapResponseSchema = lazySchema(() =>
z.object({
client_data: z.record(z.unknown()).nullish(),
client_data: z.record(z.string(), z.unknown()).nullish(),
additional_model_options: z
.array(
z
+1 -3
View File
@@ -44,9 +44,7 @@ const McpOAuthConfigSchema = lazySchema(() =>
z.object({
clientId: z.string().optional(),
callbackPort: z.number().int().positive().optional(),
authServerMetadataUrl: z
.string()
.url()
authServerMetadataUrl: z.url()
.startsWith('https://', {
message: 'authServerMetadataUrl must use https://',
})
+1 -1
View File
@@ -24,7 +24,7 @@ export const LogEventNotificationSchema = lazySchema(() =>
method: z.literal('log_event'),
params: z.object({
eventName: z.string(),
eventData: z.object({}).passthrough(),
eventData: z.looseObject({}),
}),
}),
)
+43
View File
@@ -0,0 +1,43 @@
import { createHash } from 'node:crypto'
import { fileURLToPath } from 'node:url'
import type { Tool } from '../Tool.js'
import { zodToJsonSchema } from '../utils/zodToJsonSchema.js'
// A fresh process avoids shared mocks and registry filters hiding schemas.
const toolsDir = new URL('../tools/', import.meta.url)
const files = [...new Bun.Glob('**/*Tool.{ts,tsx}').scanSync(fileURLToPath(toolsDir))]
.map(file => file.replaceAll('\\', '/'))
.sort()
const schemas: { id: string; input: unknown; advertised: unknown; output: unknown }[] = []
for (const file of files) {
const exports = await import(new URL(file, toolsDir).href)
const seen = new Set<object>()
for (const [name, value] of Object.entries(exports).sort(([a], [b]) => a.localeCompare(b))) {
if (!value || typeof value !== 'object' || !('inputSchema' in value) || seen.has(value)) continue
seen.add(value)
const tool = value as Tool
const input = zodToJsonSchema(tool.inputSchema)
schemas.push({
id: `${file}:${name}`,
input,
advertised: tool.inputJSONSchema ?? input,
output: tool.outputSchema ? zodToJsonSchema(tool.outputSchema) : null,
})
}
}
const output = process.argv.includes('--full') ? schemas : schemas.map(schema => ({
id: fingerprint(schema.id),
input: fingerprint(schema.input),
advertised: fingerprint(schema.advertised),
output: schema.output ? fingerprint(schema.output) : null,
}))
await Bun.write(Bun.stdout, JSON.stringify(output))
function fingerprint(schema: unknown): string {
// Sort keys recursively so snapshots measure schema content, not key order.
return createHash('sha256').update(JSON.stringify(schema, (_key, value: unknown) =>
value && typeof value === 'object' && !Array.isArray(value)
? Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)))
: value,
)).digest('hex')
}
@@ -2,6 +2,7 @@ import { afterAll, describe, expect, test } from 'bun:test'
import { mkdtempSync, rmSync } from 'fs'
import { tmpdir } from 'os'
import { join } from 'path'
import { fileURLToPath } from 'node:url'
import {
AgentTool,
assertAgentToolCwdAllowed,
@@ -32,6 +33,54 @@ afterAll(() => {
}
})
describe('AgentTool input schema background gates', () => {
for (const { name, disableBackground, fork, interactive, backgroundField } of [
{ name: 'default', disableBackground: false, fork: false, interactive: true, backgroundField: true },
{ name: 'background tasks disabled', disableBackground: true, fork: false, interactive: true, backgroundField: false },
{ name: 'interactive fork', disableBackground: false, fork: true, interactive: true, backgroundField: false },
{ name: 'noninteractive fork', disableBackground: false, fork: true, interactive: false, backgroundField: true },
]) {
test(`${name} loads the module and preserves cwd validation`, () => {
// Set session state before importing: buildTool reads the gated schema
// during module initialization, and lazySchema memoizes that choice.
const child = Bun.spawnSync([
process.execPath,
...(fork ? ['--feature=FORK_SUBAGENT'] : []),
'--eval',
`
import assert from 'node:assert/strict'
import { setIsInteractive } from ${JSON.stringify(new URL('../../bootstrap/state.ts', import.meta.url).href)}
setIsInteractive(${interactive})
const { inputSchema, fullInputSchema } = await import(${JSON.stringify(new URL('./AgentTool.tsx', import.meta.url).href)})
assert.equal('run_in_background' in inputSchema().shape, ${backgroundField})
assert.equal('run_in_background' in fullInputSchema().shape, true)
const baseInput = ${JSON.stringify(baseInput)}
for (const schema of [inputSchema(), fullInputSchema()]) {
assert.equal(schema.safeParse(baseInput).success, true)
assert.equal(schema.safeParse({ ...baseInput, cwd: ${JSON.stringify(existingCwd)} }).success, true)
const invalid = schema.safeParse({ ...baseInput, cwd: 'relative/path' })
assert.equal(invalid.success, false)
assert.deepEqual(invalid.error.issues, [{
code: 'custom', path: ['cwd'], message: 'cwd must be an absolute path.'
}])
}
console.log('ok')
`,
], {
cwd: fileURLToPath(new URL('../../../', import.meta.url)),
env: {
...process.env,
CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: String(disableBackground),
},
timeout: 20_000,
})
expect(child.stderr.toString()).toBe('')
expect(child.exitCode).toBe(0)
expect(child.stdout.toString().trim()).toBe('ok')
}, 25_000)
}
})
describe('AgentTool input schema model override', () => {
test('accepts aliases and custom provider-supported model IDs', () => {
const acceptedModels = [
+17 -7
View File
@@ -100,22 +100,30 @@ const baseInputSchema = lazySchema(() => z.object({
}));
// Full schema combining base + multi-agent params + isolation
export const fullInputSchema = lazySchema(() => {
const fullInputObjectSchema = lazySchema(() => {
// Multi-agent parameters
const multiAgentInputSchema = z.object({
name: z.string().optional().describe('Name for the spawned agent. Makes it addressable via SendMessage({to: name}) while running.'),
team_name: z.string().optional().describe('Team name for spawning. Uses current team context if omitted.'),
mode: permissionModeSchema().optional().describe('Permission mode for spawned teammate (e.g., "plan" to require plan approval).')
});
return baseInputSchema().merge(multiAgentInputSchema).extend({
return baseInputSchema().extend(multiAgentInputSchema.shape).extend({
isolation: z.enum(['worktree']).optional().describe('Isolation mode. "worktree" creates a temporary git worktree so the agent works on an isolated copy of the repo. When the session is outside a git repository (for example a parent of multiple repos), pass cwd set to the target repository root so the worktree is created from that repo.'),
cwd: z.string().optional().describe('Absolute path to run the agent in. Overrides the working directory for all filesystem and shell operations within this agent. When isolation is "worktree", cwd selects which git repository to create the worktree from — use this when the session cwd is not itself a git repo (for example a folder parenting multiple repos).')
}).refine(input => input.cwd === undefined || isAbsolute(input.cwd), {
path: ['cwd'],
message: 'cwd must be an absolute path.',
});
});
const isAbsoluteCwd = (input: { cwd?: string }): boolean =>
input.cwd === undefined || isAbsolute(input.cwd);
const cwdRefinementParams = {
path: ['cwd'],
message: 'cwd must be an absolute path.',
};
export const fullInputSchema = lazySchema(() =>
fullInputObjectSchema().refine(isAbsoluteCwd, cwdRefinementParams)
);
// Strip optional fields from the schema when the backing feature is off so
// the model never sees them. Done via .omit() rather than conditional spread
// inside .extend() because the spread-ternary breaks Zod's type inference
@@ -125,7 +133,8 @@ export const fullInputSchema = lazySchema(() => {
export const inputSchema = lazySchema(() => {
// cwd stays available in the open build so multi-repo parent sessions can
// pin subagents (and worktree isolation) to a child repository (#2052).
const schema = fullInputSchema();
// Zod requires object omission before adding refinements.
const schema = fullInputObjectSchema();
// GrowthBook-in-lazySchema is acceptable here (unlike subagent_type, which
// was removed in 906da6c723): the divergence window is one-session-per-
@@ -134,9 +143,10 @@ export const inputSchema = lazySchema(() => {
// by forceAsync) or "schema hides a param that would've worked" (gate
// flips off mid-session: everything still runs async via memoized
// forceAsync). No Zod rejection, no crash — unlike required→optional.
return isBackgroundTasksDisabled || isForkSubagentEnabled() ? schema.omit({
const gatedSchema = isBackgroundTasksDisabled || isForkSubagentEnabled() ? schema.omit({
run_in_background: true
}) : schema;
return gatedSchema.refine(isAbsoluteCwd, cwdRefinementParams);
});
type InputSchema = ReturnType<typeof inputSchema>;
@@ -76,7 +76,7 @@ export type AllowedPrompt = z.infer<ReturnType<typeof allowedPromptSchema>>
const inputSchema = lazySchema(() =>
z
.strictObject({
.looseObject({
// Prompt-based permissions requested by the plan
allowedPrompts: z
.array(allowedPromptSchema())
@@ -84,8 +84,7 @@ const inputSchema = lazySchema(() =>
.describe(
'Prompt-based permissions needed to implement the plan. These describe categories of actions rather than specific commands.',
),
})
.passthrough(),
}),
)
type InputSchema = ReturnType<typeof inputSchema>
+1 -1
View File
@@ -15,7 +15,7 @@ import {
} from './UI.js'
// Allow any input object since MCP tools define their own schemas
export const inputSchema = lazySchema(() => z.object({}).passthrough())
export const inputSchema = lazySchema(() => z.looseObject({}))
type InputSchema = ReturnType<typeof inputSchema>
// MCP tools can return either a plain string or an array of content blocks
@@ -8,7 +8,7 @@ import type { PermissionResult } from '../../utils/permissions/PermissionResult.
import { jsonStringify } from '../../utils/slowOperations.js'
// Allow any input object since the schema is provided dynamically
const inputSchema = lazySchema(() => z.object({}).passthrough())
const inputSchema = lazySchema(() => z.looseObject({}))
type InputSchema = ReturnType<typeof inputSchema>
const outputSchema = lazySchema(() =>
+1 -1
View File
@@ -43,7 +43,7 @@ async function scrapeWithFirecrawl(
const inputSchema = lazySchema(() =>
z.strictObject({
url: z.string().url().describe('The URL to fetch content from'),
url: z.url().describe('The URL to fetch content from'),
prompt: z.string().describe('The prompt to run on the fetched content'),
}),
)
@@ -0,0 +1,320 @@
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots
exports[`schema compatibility preserves generated and advertised JSON schemas for every exported input and output 1`] = `
[
{
"advertised": "77cb6c6df24c59271386c9cdb7b195bf8576341937db2ea30547b91cb2d133be",
"id": "e820cd467e09d6441c73c7f9015c491a791c296011a0346d3e1203ba9128dc1b",
"input": "77cb6c6df24c59271386c9cdb7b195bf8576341937db2ea30547b91cb2d133be",
"output": "429a77dc977db873ada3590226fb0a714635c120cad07272ea5a814de8217a5f",
},
{
"advertised": "88c3daf70e465d9d0aa661d4c477cf74df4e5a66bde465cd260781008fb8b7bb",
"id": "7d6736a24c16fa68771624a7f0fbfa904444ab163be9f013b0b8925641cd5e2e",
"input": "88c3daf70e465d9d0aa661d4c477cf74df4e5a66bde465cd260781008fb8b7bb",
"output": "34ea57afac8716bcced861601882bc9f8996371108a58d8cb13bb13cd5dbaf4f",
},
{
"advertised": "8cb4c4c027f884e4d94e7c9756c32d57593634485e192863bf4f3dd78e358b55",
"id": "bfd20582e06fe2d19a8f7050b45f7da4ae2839fcf0bb6eea0a7c81f8f2a11fc5",
"input": "8cb4c4c027f884e4d94e7c9756c32d57593634485e192863bf4f3dd78e358b55",
"output": "c2aa1a4538272e346a8d3626afd15a2d4129ab231d839314625d87b0529ef3df",
},
{
"advertised": "d2d282ffcf734a07a94677047f55c6ec5aac60a30f9cfccb033dd64b350e75a5",
"id": "bf40d691734596f7f62ee20a339344e0d2e9384335f5681e5195dd7dd6032ede",
"input": "d2d282ffcf734a07a94677047f55c6ec5aac60a30f9cfccb033dd64b350e75a5",
"output": "538df22ac99f76031c2412bab4b2cf634ff2efdc8b91dbf24f99ad0acd7bdef0",
},
{
"advertised": "71fc596a20f27da3dec80050e5d1f553eba34aea24c2853b853488a811e6b892",
"id": "83d330fdbd8b299dc77fce2e982a2bf2fc6d9df93fed6439926ffc57fa48d5b7",
"input": "71fc596a20f27da3dec80050e5d1f553eba34aea24c2853b853488a811e6b892",
"output": null,
},
{
"advertised": "71fc596a20f27da3dec80050e5d1f553eba34aea24c2853b853488a811e6b892",
"id": "b70fee5c304dbdfe136c17c8882d3bb1dc22da1acde039b2964741442a7b42e1",
"input": "71fc596a20f27da3dec80050e5d1f553eba34aea24c2853b853488a811e6b892",
"output": "a3d61c43e17761158ad9a4fd03fd0e500e85d55d23b07674853db2ea712c1ea5",
},
{
"advertised": "f5b65ab7eb376c7a73506ba7a02406f0ca3a0f8ff76356aa9e0355770260ab69",
"id": "face872522c2c9910d38fb9490fc7cc6a41b4dd7ab0af1dae5d4e0b42fea29b8",
"input": "f5b65ab7eb376c7a73506ba7a02406f0ca3a0f8ff76356aa9e0355770260ab69",
"output": "4fa19d4e4123e3987fe1cb9bd61014df2e6a0801dd78430daa66cd2d63194847",
},
{
"advertised": "1cf52979f184328f1c84682d9dfca4db649f40f8716772de7c988c05b8dcfd1e",
"id": "5320f71c5cdd4a2dec84800720b773ba3d62ab47c9f992299209e89995eb83f5",
"input": "1cf52979f184328f1c84682d9dfca4db649f40f8716772de7c988c05b8dcfd1e",
"output": "08e4aaac64ced96a1c5cf9733faa24c1e1d8151b0ee51933000b5ef7d6d324e2",
},
{
"advertised": "f4c0cd1a6fc0f75d4dd2ce6f0cbf6836d48f8ab16660816c05515ac08d3c8b42",
"id": "8841d846ea3240c83355501006136736372dfc69ddef27cb238660010cd955a8",
"input": "f4c0cd1a6fc0f75d4dd2ce6f0cbf6836d48f8ab16660816c05515ac08d3c8b42",
"output": "fe4fec8544de6cc2f9676885c146f049dcedbd12b0ad714f3aa89236997b9650",
},
{
"advertised": "73c20da9a0cd63a1eb6f21ac29e3c9124ecd39c8a10076d94b4eb29c3a5018f0",
"id": "fe427124fd2b513a96bef2c48f1db0547b4fc2be6f1eb37a39c360304bdbadd3",
"input": "73c20da9a0cd63a1eb6f21ac29e3c9124ecd39c8a10076d94b4eb29c3a5018f0",
"output": "f0472e096d47094ac159aba319b8463a2bac616707d6562fefb8c52bc150d719",
},
{
"advertised": "384be111a755f235f20b1c632d0ed1a665e9f16d191f02108e80232c7fc4b488",
"id": "9a2998a581c2edf8caa1013469b66074fca7177181dd360b706462aee1d22321",
"input": "384be111a755f235f20b1c632d0ed1a665e9f16d191f02108e80232c7fc4b488",
"output": "b38e37bd6dcff1b2affe78817fe5dd356735531ab1c17943aa9618ab239269d4",
},
{
"advertised": "32dd0537e15f621781fa35bd4db0fce13449e8171eb17b97246a7ed13e6e5038",
"id": "d113dee074c6d58039be9d05735b6ec9df209d24a3d72761221575c66c2e6f7f",
"input": "32dd0537e15f621781fa35bd4db0fce13449e8171eb17b97246a7ed13e6e5038",
"output": "c1288e0d1217c09b910d1622e4afe03906d438f09f469899438c643cc502a004",
},
{
"advertised": "959396f9008ff9960fe843d9a516c0a5be31c0d6dfb25552c23c5fc203b1b9b6",
"id": "dadf27af7db20323f220b802038a61b2dd371f89d13ed6295fe2e666860df6fe",
"input": "959396f9008ff9960fe843d9a516c0a5be31c0d6dfb25552c23c5fc203b1b9b6",
"output": "0ea0ad25a0a042b1b51bed4f80a651339c8040513ad34ef137b2a1396a359ff4",
},
{
"advertised": "bda3f7833f5e09fd9f14c0ebb6b466396ecb0a95839343619bbc7daf05416022",
"id": "71172a6b4174685a2c976e1af8e5d53e9a9549803ee0d4ef8d93d9d53df62b52",
"input": "bda3f7833f5e09fd9f14c0ebb6b466396ecb0a95839343619bbc7daf05416022",
"output": "b57a7e2d220fb3f4374acd18f4472a2d8b4b1339982e5c5b3829165248c2588e",
},
{
"advertised": "4752208900ad2b325b619268e2a2ed6f54b624fb82cedf418312ad5f5c20bb9e",
"id": "335f8386c1541cc80455c046cf696aa8d18be234fa6f4cc8f3ac485af44a90ec",
"input": "4752208900ad2b325b619268e2a2ed6f54b624fb82cedf418312ad5f5c20bb9e",
"output": "0438983e0b3b594b538bcd1fb579777a3e762e5d02558c226b4aeadb42afc2bd",
},
{
"advertised": "e2b47278f25fd719ba266b7717c1e6bc8750ce85ae25498f2801150770292fc8",
"id": "b3c0caa2accc1faaaf00dbdded50ff5dcc5fdba307c0a9ac53ead64eb3ce3611",
"input": "e2b47278f25fd719ba266b7717c1e6bc8750ce85ae25498f2801150770292fc8",
"output": "b9ceee09d8fe9e41ee3c83d7caae3bd57817cd57878c8198874ca2f8508e183d",
},
{
"advertised": "8d7d5e23b454edcf308cf2d220347c39de4360c4d4a55c58f355d0009ad01da3",
"id": "8d2bfcc82de8581913683c3f1be0ddd062897f2a432f2dfc6797c92f1ed77d60",
"input": "8d7d5e23b454edcf308cf2d220347c39de4360c4d4a55c58f355d0009ad01da3",
"output": "e6c8312ea9d2622a776ccc2f891c8d844dad76839c9be627bb753135cf2ebf6f",
},
{
"advertised": "df250eb67ec8e567aa29535c5e53da579c592a553355cc0adb63a9782f939b83",
"id": "49d6ee37a6f7656a32975edbf5c1db6e1fd9de40a62e80d45ec7871d02ed80df",
"input": "df250eb67ec8e567aa29535c5e53da579c592a553355cc0adb63a9782f939b83",
"output": "d5cca1f184a8cd748ba8012ac6f44a484ebd2050bcd138956bc912e9cb6c136a",
},
{
"advertised": "353e011f2b233942c1e4f959ad0dea06510198acfbdcb7d15a6b6067ad4b8094",
"id": "6fca54b7d9c67db2781977d951874fb02e3b49e5e5496b27b4ce2bdb9d1b64a8",
"input": "353e011f2b233942c1e4f959ad0dea06510198acfbdcb7d15a6b6067ad4b8094",
"output": "1e680f4adf6d8f38f9769764328e3cace27113e6259b9290464bc29c8c463c5d",
},
{
"advertised": "71fc596a20f27da3dec80050e5d1f553eba34aea24c2853b853488a811e6b892",
"id": "6e03df47c8f45fa4cb9e1eed9c589909e9524047d6cccef2f4cf1c7480ee7a2b",
"input": "71fc596a20f27da3dec80050e5d1f553eba34aea24c2853b853488a811e6b892",
"output": null,
},
{
"advertised": "09206d3aeff46e21148fc6b0472c77b240480d3f0ab19ecbe1bffa3e5a048ed2",
"id": "c6277ab141dc080d2b4f391c3e93ffe709b919fad3d2a6f86817db86dca3a8cd",
"input": "09206d3aeff46e21148fc6b0472c77b240480d3f0ab19ecbe1bffa3e5a048ed2",
"output": "6f5c83fd72140fe13917468022b340f565a5b163ecd1005798935d62aeff3107",
},
{
"advertised": "1fc3ed2694b6385febea1cfefefbbf81d186cd3d3e6349bddc5704a7ada032b2",
"id": "c2f88f01ee5dcea9c4b9bd6d31af76b59fc9cfb8e04462468624f39b366bfa6d",
"input": "1fc3ed2694b6385febea1cfefefbbf81d186cd3d3e6349bddc5704a7ada032b2",
"output": "2b3370a0cc9e186a394903f49790a92303d4212ec1bc70d73095d6bfc35e0b67",
},
{
"advertised": "9e8261cfa302abc3bccac5d24e222abc59a82111addfeae97a3395003af671df",
"id": "a9ca4741547cfce1552acd0dc7b89ea8902e7b755d56b415c36ac99382c00096",
"input": "9e8261cfa302abc3bccac5d24e222abc59a82111addfeae97a3395003af671df",
"output": "ae33ad38219c68eb5fb6c657b1379df28f68f53f7d643c46dc745b404c246ca7",
},
{
"advertised": "995fa712490de724671fcf782e72486c21c3b569cf68b6441e85fd79dda304e3",
"id": "ad68fb8682719715dfd11fe84e425283304037ef022295cd4aa5a459a49e1f55",
"input": "995fa712490de724671fcf782e72486c21c3b569cf68b6441e85fd79dda304e3",
"output": "3728014d56349be8bb3366dadd2803f893d9a9018e7af4378ac32e6443c565e0",
},
{
"advertised": "71fc596a20f27da3dec80050e5d1f553eba34aea24c2853b853488a811e6b892",
"id": "4b55f14dd751df72e841281ec6533f233b0e45f9ff602bb6c8ce76095056d749",
"input": "71fc596a20f27da3dec80050e5d1f553eba34aea24c2853b853488a811e6b892",
"output": null,
},
{
"advertised": "60b9691b11b33bf093753c77eb35564f59c81712a6ae37803f2e09d6fe35bc10",
"id": "be341dfb1dc93e01ec0ccb3199fbb6b840ff8487aa7c8db01775120b5004bd30",
"input": "60b9691b11b33bf093753c77eb35564f59c81712a6ae37803f2e09d6fe35bc10",
"output": "3f9a882802dbd0821b08d3a9f52d7b8aa98e76edb5ef4d9649d7cbbf81815666",
},
{
"advertised": "865230a52e30149c5ebe16edda290f13f0ce72842bd49a8bef5afafd0e1199e3",
"id": "63599064a971606946e438380d225817818c540afc28b50d14320ea4a04f925e",
"input": "865230a52e30149c5ebe16edda290f13f0ce72842bd49a8bef5afafd0e1199e3",
"output": "85d708fc6f8d200041c3fae9d193483067fc559850e1977bd6ff3f690f8fe2cd",
},
{
"advertised": "71fc596a20f27da3dec80050e5d1f553eba34aea24c2853b853488a811e6b892",
"id": "7ab63dc4573b61911ae3b4932f2813d88121bea34a9ddacbed3b09eff162b9be",
"input": "71fc596a20f27da3dec80050e5d1f553eba34aea24c2853b853488a811e6b892",
"output": "bdc48d794483ba79b5a9202367a09c22a96628eabd48eb6261d999c51ee8d652",
},
{
"advertised": "62d9e8e53e4c49b332448b47da537b84b942550e3b345c6e2078f7612fb8b28d",
"id": "7295becc13cd3cf96335c842f889b7d4b65acebaae2816637c6a5c0693e9d527",
"input": "62d9e8e53e4c49b332448b47da537b84b942550e3b345c6e2078f7612fb8b28d",
"output": null,
},
{
"advertised": "c0754a94ca562253bc700026f2f23a9577a483e59139a94c036dfcc925998a59",
"id": "8f745fae6deccf4c5323fd7c8556d7d1ba4d846ae0da6bc2ed231df7ddebef44",
"input": "c0754a94ca562253bc700026f2f23a9577a483e59139a94c036dfcc925998a59",
"output": "9392a64293a631e738702c9facb92c636056bb50ee72f27d06046bd8019a11ea",
},
{
"advertised": "1ea46b7b91a096f0ebd76111c3337238bcb0b2d57d3dd23d8e4e2ea09f77a4bc",
"id": "6820d861ad3d1dd451aebf66a4e7a35b32205e6787fe7a1aa95e40381d41e25c",
"input": "1ea46b7b91a096f0ebd76111c3337238bcb0b2d57d3dd23d8e4e2ea09f77a4bc",
"output": null,
},
{
"advertised": "8d7d5e23b454edcf308cf2d220347c39de4360c4d4a55c58f355d0009ad01da3",
"id": "246794557465dc048346b3baa603db7001956f67b0160c9838b3135d25675a28",
"input": "8d7d5e23b454edcf308cf2d220347c39de4360c4d4a55c58f355d0009ad01da3",
"output": "189ef3f99d87aeb26b60950fcb672c021af8a6c204781259eb28e15537d9994e",
},
{
"advertised": "0ee69f567a9240a3aed35f267eb0219cf974839480944af4db84fdea0d3e11b1",
"id": "dde972b9cf8648eadf82e00ed1f0a380146436042e72d9192b8fbfa09c6356ea",
"input": "0ee69f567a9240a3aed35f267eb0219cf974839480944af4db84fdea0d3e11b1",
"output": "2ea86994447c0b6ba1e17ef83fd630a26469e000af367d98d6ffcb25ae39ae3c",
},
{
"advertised": "83868e55a4a803551f011494c8bc021113742db57989503d4391fa0dd9ec057a",
"id": "301bcb286d6a83bc86895c5f86ead159f682f332666ce5e2db977b31ca1f7eed",
"input": "83868e55a4a803551f011494c8bc021113742db57989503d4391fa0dd9ec057a",
"output": "0ac7d77d806635ebd074675db1edf0b2a43cf0844861b306a48515126219994d",
},
{
"advertised": "71fc596a20f27da3dec80050e5d1f553eba34aea24c2853b853488a811e6b892",
"id": "9b3d29a242f9840335a5b9ddc6eca0f2a202e67a85d80ca5da060c2f6869ca28",
"input": "71fc596a20f27da3dec80050e5d1f553eba34aea24c2853b853488a811e6b892",
"output": "91d8f23df7f0991623f783fcf90af1d1120092547eefecdb4f3b604a49be655e",
},
{
"advertised": "891cde9c7057d6e54554116564655318c862ae8b7019466aa00aab28e56fe4da",
"id": "87950de3ec70fd966719053c66dcf4f74f25609f6a320ce8adbdb93b76d176f9",
"input": "891cde9c7057d6e54554116564655318c862ae8b7019466aa00aab28e56fe4da",
"output": null,
},
{
"advertised": "9f76ff6acc16e7f316c68606df009097b011cec429eb170fb41adbc0f812b926",
"id": "2bfaeee71433859cfd6e4516ed4e064f33cfc558dd831c1ca170baa2e789d8f1",
"input": "9f76ff6acc16e7f316c68606df009097b011cec429eb170fb41adbc0f812b926",
"output": "7b98afccc859185afc6c77e78da460ea0da1356a66eeb9a307ecccd5f05bd61d",
},
{
"advertised": "518f1409411eb7ac540c47626e72521092c0d61874d590d3e85e39c2145207ac",
"id": "4fdcb1d2bac5cde4c017ced61657ca6b9fb7b91c9ff59a27eb66f580d3848fc1",
"input": "518f1409411eb7ac540c47626e72521092c0d61874d590d3e85e39c2145207ac",
"output": "cf865014979f3277899dc6cb835737242698c9cb2c537185cd41d352f649675b",
},
{
"advertised": "09f93a3aa0d8f4e1b81d74044a8aa554b2e4e47aeec12247e12e5e7426848e14",
"id": "d03e96478c61a5b67db9148f0bd5cb475d4047cf2732df3c0f7e6c602b1d6006",
"input": "09f93a3aa0d8f4e1b81d74044a8aa554b2e4e47aeec12247e12e5e7426848e14",
"output": null,
},
{
"advertised": "71fc596a20f27da3dec80050e5d1f553eba34aea24c2853b853488a811e6b892",
"id": "e961d9d2e8822ff47ea84e570a839f0797358177e51312afcb4ef380f12d6a91",
"input": "71fc596a20f27da3dec80050e5d1f553eba34aea24c2853b853488a811e6b892",
"output": null,
},
{
"advertised": "f29f42721d4d46a4ce50572a8fef3f8a3142fc0fcf544459d1804966424fed99",
"id": "79a0a14e693704b32c14d60fde371850ca489c5d29fa887e041645effcc0d507",
"input": "f29f42721d4d46a4ce50572a8fef3f8a3142fc0fcf544459d1804966424fed99",
"output": "ec82e6db1f5b340724db18cf269de5ea1325d044d68f054c732c96fa3e532c50",
},
{
"advertised": "5280abc39104a88d80ece779109a0c1b717c377c291b0df51f72f8406e6a687f",
"id": "8fbb0e7c76d52de420eca0be72143727622d7575bfd404832f5bb86e82583579",
"input": "5280abc39104a88d80ece779109a0c1b717c377c291b0df51f72f8406e6a687f",
"output": "c8a9da99d15b311fa999cdbbbeca21f39693b3bd22d242e126914e9d21ed1660",
},
{
"advertised": "29188b18ff3e3cc1a8326091768850706c9b5aa545717facb774eae4f6d7ed85",
"id": "28ac27c6b2b47389ee7879655bea10a97cd950b8bb23de458497cdc989c54df5",
"input": "29188b18ff3e3cc1a8326091768850706c9b5aa545717facb774eae4f6d7ed85",
"output": "dfab3d9383b6902c737345efee32272cff801c7f259245285b461cc5ad67ec0b",
},
{
"advertised": "ce035ada7312377e4219f1f4d74d325e1db5b9e3b9c4bf9b8e46e7e119b7cbbb",
"id": "b11dc8a983e218c22cb4c1a3d26e1771b027ef2da5ded3273dcc3ba481e8efb6",
"input": "ce035ada7312377e4219f1f4d74d325e1db5b9e3b9c4bf9b8e46e7e119b7cbbb",
"output": "c2384b75e26d17cdd6625de8ec2d3b507a77786f7fd59e0b5fd7ebfad8e9635d",
},
{
"advertised": "71fc596a20f27da3dec80050e5d1f553eba34aea24c2853b853488a811e6b892",
"id": "23063c391ff1101398b617433ce3225220200e2144b2dc988c7269ab51bd6379",
"input": "71fc596a20f27da3dec80050e5d1f553eba34aea24c2853b853488a811e6b892",
"output": null,
},
{
"advertised": "71fc596a20f27da3dec80050e5d1f553eba34aea24c2853b853488a811e6b892",
"id": "839ffbedaf5ba514f5176b8e7d169047e3280c43ea829306724a3461c47b99be",
"input": "71fc596a20f27da3dec80050e5d1f553eba34aea24c2853b853488a811e6b892",
"output": null,
},
]
`;
exports[`schema compatibility preserves defaults, optional and nullable fields, records and unknown-key policy 1`] = `
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"additionalProperties": false,
"properties": {
"count": {
"default": 3,
"type": "number",
},
"enabled": {
"type": "boolean",
},
"entries": {
"additionalProperties": {},
"propertyNames": {
"type": "string",
},
"type": "object",
},
"note": {
"anyOf": [
{
"type": "string",
},
{
"type": "null",
},
],
},
},
"required": [
"count",
"entries",
],
"type": "object",
}
`;
+3 -4
View File
@@ -18,7 +18,7 @@ import {
import { createServer, type Server, type Socket } from 'net'
import { homedir, platform } from 'os'
import { join } from 'path'
import { z } from 'zod'
import { z } from 'zod/v4'
import { lazySchema } from '../lazySchema.js'
import { jsonParse, jsonStringify } from '../slowOperations.js'
import { getSecureSocketPath, getSocketDir } from './common.js'
@@ -83,10 +83,9 @@ export async function runChromeNativeHost(): Promise<void> {
const messageSchema = lazySchema(() =>
z
.object({
.looseObject({
type: z.string(),
})
.passthrough(),
}),
)
type ToolRequest = {
+1 -1
View File
@@ -1,5 +1,5 @@
import { expect, test, describe } from 'bun:test'
import { z } from 'zod'
import { z } from 'zod/v4'
import {
DEFAULT_GLOBAL_CONFIG,
GLOBAL_CONFIG_KEYS,
+2 -2
View File
@@ -52,8 +52,8 @@ const EnvSchema = z.object({
ANTHROPIC_AUTH_TOKEN: optionalNonEmptyString,
OPENCLAUDE_CONFIG_DIR: optionalNonEmptyString,
CLAUDE_CONFIG_DIR: optionalNonEmptyString,
HTTP_PROXY: z.string().url().optional().or(z.literal('')),
HTTPS_PROXY: z.string().url().optional().or(z.literal('')),
HTTP_PROXY: z.url().optional().or(z.literal('')),
HTTPS_PROXY: z.url().optional().or(z.literal('')),
NODE_EXTRA_CA_CERTS: optionalNonEmptyString,
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: z.string().optional(),
CLAUDE_CODE_DISABLE_TERMINAL_TITLE: z.string().optional(),
@@ -0,0 +1,25 @@
import { describe, expect, test } from 'bun:test'
import { validateElicitationInput } from './elicitationValidation.js'
describe('formatted input validation', () => {
test.each([
['email', 'not-email', 'Must be a valid email address'],
['uri', 'not-uri', 'Must be a valid URI'],
['date', '2024-02-31', 'Must be a valid date'],
['date-time', 'not-time', 'Must be a valid date-time'],
] as const)('preserves the custom %s error', (format, input, message) => {
const result = validateElicitationInput(input, { type: 'string', format })
expect(result.isValid).toBe(false)
expect(result.error).toStartWith(message)
})
test('retains length checks alongside format checks', () => {
const schema = { type: 'string', format: 'uri', minLength: 20, maxLength: 25 } as const
expect(validateElicitationInput('urn:example:abc', schema).error)
.toBe('Must be at least 20 characters')
expect(validateElicitationInput('urn:example:abcdefghijklmnop', schema).error)
.toBe('Must be at most 25 characters')
expect(validateElicitationInput('urn:example:abcdefghij', schema))
.toEqual({ value: 'urn:example:abcdefghij', isValid: true })
})
})
+16 -16
View File
@@ -144,36 +144,36 @@ function getZodSchema(schema: PrimitiveSchemaDefinition): z.ZodTypeAny {
let stringSchema = z.string()
if (schema.minLength !== undefined) {
stringSchema = stringSchema.min(schema.minLength, {
message: `Must be at least ${schema.minLength} ${plural(schema.minLength, 'character')}`,
error: `Must be at least ${schema.minLength} ${plural(schema.minLength, 'character')}`,
})
}
if (schema.maxLength !== undefined) {
stringSchema = stringSchema.max(schema.maxLength, {
message: `Must be at most ${schema.maxLength} ${plural(schema.maxLength, 'character')}`,
error: `Must be at most ${schema.maxLength} ${plural(schema.maxLength, 'character')}`,
})
}
switch (schema.format) {
case 'email':
stringSchema = stringSchema.email({
message: 'Must be a valid email address, e.g. user@example.com',
})
stringSchema = stringSchema.check(z.email({
error: 'Must be a valid email address, e.g. user@example.com',
}))
break
case 'uri':
stringSchema = stringSchema.url({
message: 'Must be a valid URI, e.g. https://example.com',
})
stringSchema = stringSchema.check(z.url({
error: 'Must be a valid URI, e.g. https://example.com',
}))
break
case 'date':
stringSchema = stringSchema.date(
stringSchema = stringSchema.check(z.iso.date(
'Must be a valid date, e.g. 2024-03-15, today, next Monday',
)
))
break
case 'date-time':
stringSchema = stringSchema.datetime({
stringSchema = stringSchema.check(z.iso.datetime({
offset: true,
message:
error:
'Must be a valid date-time, e.g. 2024-03-15T14:30:00Z, tomorrow at 3pm',
})
}))
break
default:
// No specific format validation
@@ -201,16 +201,16 @@ function getZodSchema(schema: PrimitiveSchemaDefinition): z.ZodTypeAny {
error: rangeMsg,
})
if (schema.type === 'integer') {
numberSchema = numberSchema.int({ message: rangeMsg })
numberSchema = numberSchema.int({ error: rangeMsg })
}
if (schema.minimum !== undefined) {
numberSchema = numberSchema.min(schema.minimum, {
message: rangeMsg,
error: rangeMsg,
})
}
if (schema.maximum !== undefined) {
numberSchema = numberSchema.max(schema.maximum, {
message: rangeMsg,
error: rangeMsg,
})
}
return numberSchema
+1 -1
View File
@@ -259,7 +259,7 @@ const fileEditTool = createToolFixture(fileInputSchema, {
})
function mcpTool(name: string, readOnlyHint?: boolean) {
return createToolFixture(z.object({}).passthrough(), {
return createToolFixture(z.looseObject({}), {
name,
isMcp: true,
mcpInfo: { serverName: 'test', toolName: name.split('__').at(-1) ?? name },
+9 -19
View File
@@ -213,9 +213,7 @@ const McpbPath = lazySchema(() =>
message: 'MCPB file path must end with .mcpb or .dxt',
})
.describe('Path to MCPB file relative to plugin root'),
z
.string()
.url()
z.url()
.refine(url => url.endsWith('.mcpb') || url.endsWith('.dxt'), {
message: 'MCPB URL must end with .mcpb or .dxt',
})
@@ -332,9 +330,7 @@ const PluginManifestMetadataSchema = lazySchema(() =>
author: PluginAuthorSchema()
.optional()
.describe('Information about the plugin creator or maintainer'),
homepage: z
.string()
.url()
homepage: z.url()
.optional()
.describe('Plugin homepage or documentation URL'),
repository: z.string().optional().describe('Source code repository URL'),
@@ -622,7 +618,7 @@ const PluginManifestMcpServerSchema = lazySchema(() =>
*/
const PluginUserConfigOptionSchema = lazySchema(() =>
z
.object({
.strictObject({
type: z
.enum(['string', 'number', 'boolean', 'directory', 'file'])
.describe('Type of the configuration value'),
@@ -652,8 +648,7 @@ const PluginUserConfigOptionSchema = lazySchema(() =>
),
min: z.number().optional().describe('Minimum value (number type only)'),
max: z.number().optional().describe('Maximum value (number type only)'),
})
.strict(),
}),
)
/**
@@ -708,7 +703,7 @@ const PluginManifestChannelsSchema = lazySchema(() =>
channels: z
.array(
z
.object({
.strictObject({
server: z
.string()
.min(1)
@@ -728,8 +723,7 @@ const PluginManifestChannelsSchema = lazySchema(() =>
'Fields to prompt the user for when enabling this plugin in assistant mode. ' +
'Saved values are substituted into ${user_config.KEY} references in the mcpServers env.',
),
})
.strict(),
}),
)
.describe(
'Channels this plugin provides. Each entry declares an MCP server as a message channel ' +
@@ -943,7 +937,7 @@ export const MarketplaceSourceSchema = lazySchema(() =>
z.discriminatedUnion('source', [
z.object({
source: z.literal('url'),
url: z.string().url().describe('Direct URL to marketplace.json file'),
url: z.url().describe('Direct URL to marketplace.json file'),
headers: z
.record(z.string(), z.string())
.optional()
@@ -1116,9 +1110,7 @@ export const PluginSourceSchema = lazySchema(() =>
.string()
.optional()
.describe('Specific version or version range (e.g., ^1.0.0, ~2.1.0)'),
registry: z
.string()
.url()
registry: z.url()
.optional()
.describe(
'Custom NPM registry URL (defaults to using system default, likely npmjs.org)',
@@ -1135,9 +1127,7 @@ export const PluginSourceSchema = lazySchema(() =>
.string()
.optional()
.describe('Version specifier (e.g., ==1.0.0, >=2.0.0, <3.0.0)'),
registry: z
.string()
.url()
registry: z.url()
.optional()
.describe(
'Custom PyPI registry URL (defaults to using system default, likely pypi.org)',
+8 -9
View File
@@ -216,7 +216,7 @@ export async function validatePluginManifest(
// them. `claude plugin validate` is a developer tool — authors running it
// want to know these fields don't belong here. But it's a warning, not an
// error: the plugin loads fine at runtime (the base schema strips unknown
// keys). We strip them here so the .strict() call below doesn't double-
// keys). We strip them here so the z.strictObject() call below doesn't double-
// report them as unrecognized-key errors on top of the targeted warnings.
let toValidate = parsed
if (typeof parsed === 'object' && parsed !== null) {
@@ -241,10 +241,10 @@ export async function validatePluginManifest(
}
// Validate against schema (post-strip, so marketplace fields don't fail it).
// We call .strict() locally here even though the base schema is lenient —
// We call z.strictObject() locally here even though the base schema is lenient —
// the runtime load path silently strips unknown keys for resilience, but
// this is a developer tool and authors running it want typo feedback.
const result = PluginManifestSchema().strict().safeParse(toValidate)
const result = z.strictObject(PluginManifestSchema().shape).safeParse(toValidate)
if (!result.success) {
errors.push(...formatZodErrors(result.error))
@@ -397,14 +397,13 @@ export async function validateMarketplaceManifest(
// Validate against schema.
// The base schemas are lenient (strip unknown keys) for runtime resilience,
// but this is a developer tool — authors want typo feedback. We rebuild the
// schema with .strict() here. Note .strict() on the outer object does NOT
// schema with z.strictObject() here. Note z.strictObject() on the outer object does NOT
// propagate into z.array() elements, so we also override the plugins array
// with strict entries to catch typos inside individual plugin entries too.
const strictMarketplaceSchema = PluginMarketplaceSchema()
.extend({
plugins: z.array(PluginMarketplaceEntrySchema().strict()),
})
.strict()
const strictMarketplaceSchema = z.strictObject({
...PluginMarketplaceSchema().shape,
plugins: z.array(z.strictObject(PluginMarketplaceEntrySchema().shape)),
})
const result = strictMarketplaceSchema.safeParse(parsed)
if (!result.success) {
+12
View File
@@ -0,0 +1,12 @@
import { expect, test } from 'bun:test'
import { generateSettingsJSONSchema } from './schemaOutput.js'
test('settings JSON export preserves the anyOf union contract', () => {
const output = generateSettingsJSONSchema()
const schema = JSON.parse(output)
expect(schema.type).toBe('object')
expect(schema.properties).toHaveProperty('model')
expect(output).toContain('"anyOf"')
expect(output.match(/"oneOf"\s*:/g)).toBeNull()
})
+2 -2
View File
@@ -1,8 +1,8 @@
import { toJSONSchema } from 'zod/v4'
import { jsonStringify } from '../slowOperations.js'
import { zodToJsonSchema } from '../zodToJsonSchema.js'
import { SettingsSchema } from './types.js'
export function generateSettingsJSONSchema(): string {
const jsonSchema = toJSONSchema(SettingsSchema(), { unrepresentable: 'any' })
const jsonSchema = zodToJsonSchema(SettingsSchema())
return jsonStringify(jsonSchema, null, 2)
}
+10 -17
View File
@@ -42,7 +42,7 @@ const ModelTokenRateSchema = z
.max(MAX_MODEL_TOKEN_RATE_USD_PER_MILLION)
export const ModelPricingEntrySchema = z
.object({
.strictObject({
inputTokens: ModelTokenRateSchema.describe(
'Input-token price in USD per 1,000,000 tokens.',
),
@@ -65,7 +65,6 @@ export const ModelPricingEntrySchema = z
'Web-search price in USD per request. Defaults to $0.01 when omitted.',
),
})
.strict()
const MODEL_PRICING_KEY_PREFIX = '\0'
@@ -139,7 +138,7 @@ export const EnvironmentVariablesSchema = lazySchema(() =>
*/
export const PermissionsSchema = lazySchema(() =>
z
.object({
.looseObject({
allow: z
.array(PermissionRuleSchema())
.optional()
@@ -184,8 +183,7 @@ export const PermissionsSchema = lazySchema(() =>
.array(z.string())
.optional()
.describe('Additional directories to include in the permission scope'),
})
.passthrough(),
}),
)
/**
@@ -353,7 +351,7 @@ export const DeniedMcpServerEntrySchema = lazySchema(() =>
* The settings system handles backward compatibility automatically:
* - When updating settings, invalid fields are preserved in the file (see settings.ts lines 233-249)
* - Type coercion via z.coerce (e.g., env vars convert numbers to strings)
* - .passthrough() preserves unknown fields in permissions object
* - z.looseObject() preserves unknown fields in permissions object
* - Invalid settings are simply not used, but remain in the file to be fixed by the user
*/
@@ -371,7 +369,7 @@ export const CUSTOMIZATION_SURFACES = [
export const SettingsSchema = lazySchema(() =>
z
.object({
.looseObject({
$schema: z
.literal(CLAUDE_CODE_SETTINGS_SCHEMA_URL)
.optional()
@@ -402,15 +400,13 @@ export const SettingsSchema = lazySchema(() =>
),
// Gated so the SDK generator (which runs without CLAUDE_CODE_ENABLE_XAA)
// doesn't surface this in GlobalClaudeSettings. Read via getXaaIdpSettings().
// .passthrough() on the outer object keeps an existing settings.json key
// z.looseObject() on the outer object keeps an existing settings.json key
// alive across env-var-off sessions — it's just not schema-validated then.
...(isEnvTruthy(process.env.CLAUDE_CODE_ENABLE_XAA)
? {
xaaIdp: z
.object({
issuer: z
.string()
.url()
issuer: z.url()
.describe('IdP issuer URL for OIDC discovery'),
clientId: z
.string()
@@ -902,9 +898,7 @@ export const SettingsSchema = lazySchema(() =>
.string()
.optional()
.describe('Actual model name to send to the API. Defaults to the surrounding agentModels key.'),
base_url: z
.string()
.url()
base_url: z.url()
.optional()
.describe('OpenAI-compatible API endpoint (must be https:// or http://). Omit together with api_key to reuse the current provider (model-only route).'),
api_key: z
@@ -1178,7 +1172,7 @@ export const SettingsSchema = lazySchema(() =>
// works when explicitly enabled. Which servers can connect at all is
// still governed by allowedMcpServers/deniedMcpServers. Not
// feature-spread: KAIROS_CHANNELS is external:true, and the spread
// wrecks type inference for allowedChannelPlugins (the .passthrough()
// wrecks type inference for allowedChannelPlugins (the z.looseObject()
// catch-all gives {} instead of the array type).
channelsEnabled: z
.boolean()
@@ -1405,8 +1399,7 @@ export const SettingsSchema = lazySchema(() =>
'Useful for enterprise administrators to add organization-specific context ' +
'(e.g., "All plugins from our internal marketplace are vetted and approved.").',
),
})
.passthrough(),
}),
)
/**
+1 -1
View File
@@ -193,7 +193,7 @@ export function validateSettingsFileContent(content: string):
const jsonData = jsonParse(content)
// Validate against SettingsSchema in strict mode
const result = SettingsSchema().strict().safeParse(jsonData)
const result = z.strictObject(SettingsSchema().shape).safeParse(jsonData)
if (result.success) {
return { isValid: true }
@@ -1,4 +1,4 @@
import { z } from 'zod'
import { z } from 'zod/v4'
import type { SuggestionItem } from '../../components/PromptInput/PromptInputFooterSuggestions.js'
import type { MCPServerConnection } from '../../services/mcp/types.js'
import { logForDebugging } from '../debug.js'
+4
View File
@@ -136,3 +136,7 @@ export function formatZodValidationError(
return errorContent
}
export function formatMcpValidationError(name: string, error: ZodError): string {
return `Tool ${name} input is invalid:\n${error.issues.map(issue => `- ${issue.path.join('.')}: ${issue.message}`).join('\n')}`
}
+33
View File
@@ -0,0 +1,33 @@
import { describe, expect, test } from 'bun:test'
import { zodOutputFormat } from '@anthropic-ai/sdk/helpers/zod'
import { SandboxRuntimeConfigSchema } from '@anthropic-ai/sandbox-runtime'
import { parseWithCompat, toJsonSchemaCompat } from '@modelcontextprotocol/sdk/server/zod-json-schema-compat.js'
import { z as legacy } from 'zod/v3'
import { z } from 'zod/v4'
describe('dependency schema compatibility', () => {
test('the structured output helper converts and validates v4 schemas', () => {
const format = zodOutputFormat(z.strictObject({ value: z.string() }))
expect(format.schema.type).toBe('object')
expect(format.parse('{"value":"ok"}')).toEqual({ value: 'ok' })
expect(() => format.parse('{"value":1}')).toThrow()
})
test('the protocol converter and parser support both packaged schema generations', () => {
for (const schema of [z.strictObject({ value: z.string() }), legacy.object({ value: legacy.string() }).strict()]) {
expect(toJsonSchemaCompat(schema).type).toBe('object')
expect(parseWithCompat(schema, { value: 'ok' })).toEqual({ value: 'ok' })
expect(() => parseWithCompat(schema, { value: 1 })).toThrow()
}
})
test('the unmodified sandbox uses the v3 compatibility export supplied by the v4 package', () => {
const config = {
network: { allowedDomains: [], deniedDomains: [] },
filesystem: { denyRead: [], allowWrite: [], denyWrite: [] },
}
expect(SandboxRuntimeConfigSchema.safeParse(config).success).toBe(true)
expect(SandboxRuntimeConfigSchema.safeParse({}).success).toBe(false)
expect(SandboxRuntimeConfigSchema.safeParse({ ...config, network: { allowedDomains: 3 } }).success).toBe(false)
})
})
+119
View File
@@ -0,0 +1,119 @@
import { describe, expect, test } from 'bun:test'
import { fileURLToPath } from 'node:url'
import { z } from 'zod/v4'
import { semanticBoolean } from './semanticBoolean.js'
import { semanticNumber } from './semanticNumber.js'
import { formatMcpValidationError, formatZodValidationError } from './toolErrors.js'
import { zodToJsonSchema } from './zodToJsonSchema.js'
describe('schema compatibility', () => {
test('preserves generated and advertised JSON schemas for every exported input and output', () => {
// Hash entire JSON documents, including descriptions, without duplicating
// prompts and examples in a large snapshot. The fixture also includes
// conditional schemas that registry filters would otherwise hide.
const child = Bun.spawnSync([
process.execPath,
fileURLToPath(new URL('../test/toolSchemas.fixture.ts', import.meta.url)),
], { cwd: fileURLToPath(new URL('../../', import.meta.url)), timeout: 20_000 })
expect(child.stderr.toString()).toBe('')
expect(child.exitCode).toBe(0)
const schemas = JSON.parse(child.stdout.toString())
expect(schemas.length).toBeGreaterThan(40)
expect(schemas).toMatchSnapshot()
// Simulate Windows glob paths and reverse discovery order in a fresh child
// so separator normalization, sorting, imports and id hashes are all covered.
const windowsChild = Bun.spawnSync([
process.execPath,
'--eval',
`
const scanSync = Bun.Glob.prototype.scanSync
Bun.Glob.prototype.scanSync = function* (...args) {
yield* [...scanSync.apply(this, args)]
.map(file => file.replaceAll('/', '\\\\'))
.reverse()
}
await import(${JSON.stringify(new URL('../test/toolSchemas.fixture.ts', import.meta.url).href)})
`,
], { cwd: fileURLToPath(new URL('../../', import.meta.url)), timeout: 20_000 })
expect(windowsChild.stderr.toString()).toBe('')
expect(windowsChild.exitCode).toBe(0)
expect(JSON.parse(windowsChild.stdout.toString())).toEqual(schemas)
}, 45_000) // Allow both children their 20-second deadlines plus assertion time.
test('preserves defaults, optional and nullable fields, records and unknown-key policy', () => {
const schema = z.strictObject({
count: semanticNumber(z.number().default(3)),
enabled: semanticBoolean(z.boolean().optional()),
note: z.string().nullable().optional(),
entries: z.record(z.string(), z.unknown()),
})
expect(schema.parse({ entries: {} })).toEqual({ count: 3, entries: {} })
expect(schema.parse({ count: '2', enabled: 'false', note: null, entries: { extra: 1 } }))
.toEqual({ count: 2, enabled: false, note: null, entries: { extra: 1 } })
for (const invalid of [{ extra: 1 }, { count: null }, { enabled: null }]) {
expect(schema.safeParse({ entries: {}, ...invalid }).success).toBe(false)
}
expect(JSON.parse(JSON.stringify(zodToJsonSchema(schema)))).toMatchSnapshot()
expect(zodToJsonSchema(schema)).toBe(zodToJsonSchema(schema))
})
test('preserves strip and passthrough policies and optional inner defaults', () => {
expect(z.object({ value: z.string() }).parse({ value: 'ok', extra: 1 }))
.toEqual({ value: 'ok' })
expect(z.looseObject({ value: z.string() }).parse({ value: 'ok', extra: 1 }))
.toEqual({ value: 'ok', extra: 1 })
expect(z.object({ value: z.boolean().default(false).optional() }).parse({}))
.toEqual({ value: false })
})
test('preserves readable missing, unknown and wrong-type errors', () => {
const schema = z.strictObject({ items: z.array(z.object({ value: z.string() })) })
const result = schema.safeParse({ items: [{}, { value: 3 }], extra: true })
expect(result.success).toBe(false)
if (!result.success) {
expect(formatZodValidationError('Input', result.error)).toBe(
'Input failed due to the following issues:\n' +
'The required parameter `items[0].value` is missing\n' +
'An unexpected parameter `extra` was provided\n' +
'The parameter `items[1].value` type is expected as `string` but provided as `number`',
)
}
})
test('formats server errors through v4 issues, including custom messages', () => {
const schema = z.strictObject({ values: z.array(z.string().min(1, { error: 'Value cannot be empty' })) })
const result = schema.safeParse({ values: [''] })
expect(result.success).toBe(false)
if (!result.success) {
expect(result.error).toBeInstanceOf(z.ZodError)
expect(formatMcpValidationError('Input', result.error)).toBe(
'Tool Input input is invalid:\n- values.0: Value cannot be empty',
)
}
})
test('preserves discriminated unions and nullable wire schemas without rewriting default data', () => {
const data = { type: ['string', 'null'], anyOf: [{ type: ['number', 'null'] }] }
const schema = z.discriminatedUnion('kind', [
z.strictObject({ kind: z.literal('a'), value: z.string().nullable(), data: z.unknown().default(data) }),
z.strictObject({ kind: z.literal('b'), value: z.number() }),
])
expect(schema.parse({ kind: 'a', value: null })).toEqual({ kind: 'a', value: null, data })
expect(schema.safeParse({ kind: 'a', value: 1 }).success).toBe(false)
expect(schema.safeParse({ kind: 'c', value: null }).success).toBe(false)
const json = zodToJsonSchema(schema)
expect(json.oneOf).toBeUndefined()
const branches = json.anyOf as { properties: Record<string, Record<string, unknown>> }[]
expect(branches[0]!.properties.value).toEqual({ anyOf: [{ type: 'string' }, { type: 'null' }] })
expect(branches[0]!.properties.data!.default).toEqual(data)
expect(Reflect.ownKeys(json)).not.toContain('~standard')
})
test('distinguishes output defaults from pre-parse defaults around transforms', () => {
const schema = z.string().trim().transform(value => value.length)
expect(schema.default(9).parse(undefined)).toBe(9)
expect(schema.prefault(' ok ').parse(undefined)).toBe(2)
expect(schema.parse(' ok ')).toBe(2)
})
})
+39 -1
View File
@@ -17,7 +17,45 @@ const cache = new WeakMap<ZodTypeAny, JsonSchema7Type>()
export function zodToJsonSchema(schema: ZodTypeAny): JsonSchema7Type {
const hit = cache.get(schema)
if (hit) return hit
const result = toJSONSchema(schema) as JsonSchema7Type
const converted = toJSONSchema(schema, {
override: ({ zodSchema, jsonSchema }) => {
// Discriminator values are disjoint, so either keyword describes the
// same accepted values. Preserve the established wire representation.
if (zodSchema._zod.def.type === 'union' && 'discriminator' in zodSchema._zod.def && jsonSchema.oneOf) {
jsonSchema.anyOf = jsonSchema.oneOf
delete jsonSchema.oneOf
}
},
})
// Native conversion also attaches non-enumerable runtime metadata. Return
// plain JSON as before, and expand compact type unions to the existing form.
const result: JsonSchema7Type = structuredClone(converted)
expandTypeUnions(result)
cache.set(schema, result)
return result
}
function expandTypeUnions(value: unknown): void {
if (!value || typeof value !== 'object' || Array.isArray(value)) return
const schema = value as JsonSchema7Type
if (Array.isArray(schema.type)) {
schema.anyOf = schema.type.map(type => ({ type }))
delete schema.type
}
// Visit only schema-valued keywords: defaults, enum members and examples
// are user data, even when they look like JSON schemas themselves.
for (const keyword of ['properties', 'patternProperties', '$defs', 'definitions', 'dependentSchemas']) {
const children = schema[keyword]
if (children && typeof children === 'object') {
for (const child of Object.values(children)) expandTypeUnions(child)
}
}
for (const keyword of [
'anyOf', 'oneOf', 'allOf', 'prefixItems', 'items', 'additionalProperties',
'unevaluatedProperties', 'contains', 'not', 'if', 'then', 'else', 'propertyNames',
]) {
const children = schema[keyword]
if (Array.isArray(children)) children.forEach(expandTypeUnions)
else expandTypeUnions(children)
}
}
+4 -3
View File
@@ -13,6 +13,9 @@
},
},
},
"overrides": {
"zod": "4.6.7",
},
"packages": {
"@astrojs/astro2tsx": ["@astrojs/astro2tsx@0.1.3", "", { "dependencies": { "@emnapi/core": "1.11.3", "@napi-rs/wasm-runtime": "1.2.4" } }, "sha512-GMDC7Eo0myBUA/QLBl0u+OcegJ8LiBhccmNZR34KmprQNP8gQUJlWlOOZWUsxNA2rNS6GuPYcGDyLoPnjmn4/Q=="],
@@ -662,7 +665,7 @@
"yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="],
"zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
"zod": ["zod@4.6.7", "", {}, "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg=="],
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
@@ -688,8 +691,6 @@
"anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
"astro/zod": ["zod@4.6.7", "", {}, "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg=="],
"cliui/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
"csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="],
+3
View File
@@ -15,5 +15,8 @@
"@fontsource-variable/geist-mono": "5.3.0",
"astro": "7.4.2",
"typescript": "6.0.3"
},
"overrides": {
"zod": "4.6.7"
}
}