Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 19 additions & 19 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,15 @@
"typescript": "^5.6.0",
"undici": "^8.10.0",
"yaml": "^2.5.1",
"zod": "^3.25.76"
"zod": "^4.6.6"
},
"devDependencies": {
"@types/node": "^22.10.0",
"@types/pg": "^8.23.1",
"c8": "^12.0.0"
},
"overrides": {
"body-parser": "^2.4.0",
"fast-uri": "^3.1.9"
}
}
12 changes: 12 additions & 0 deletions src/vendor/tests/smoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,18 @@ async function main(): Promise<void> {
throw new Error(`customModesSettingsSchema rejected a valid config: ${JSON.stringify(parsed.error)}`)
}

const oversizedGroups = customModesSettingsSchema.safeParse({
customModes: [{
slug: "oversized",
name: "Oversized",
roleDefinition: "Oversized mode",
groups: Array.from({ length: 33 }, () => ({})),
}],
})
if (oversizedGroups.success || oversizedGroups.error.issues.length !== 1 || oversizedGroups.error.issues[0]?.code !== "too_big") {
throw new Error("oversized groups should be rejected before their entries are parsed")
}

console.log(
`[smoke] system prompt built (${prompt.length} chars); native tools: ${tools.length}; anthropic tools: ${anthropicTools.length}; roomodes schema: ok`,
)
Expand Down
15 changes: 15 additions & 0 deletions src/vendor/zoo-code/VENDOR-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,3 +221,18 @@ creating Git subprocesses. This keeps inherited editor commands out of shadow
checkpoint operations and matches the dependency's unsafe-operation checks.
These are local adaptations to `ShadowCheckpointService.ts`; upstream
attribution and license notices remain in place.

## 7. Zod 4 compatibility and array bounds (2026-10-23)

The project now pins the Zod 4 release line (`zod` 4.6.6 or newer) to receive
upstream fixes and keep the MCP SDK on its supported Zod 4 path. Vendored Zoo
Code schemas use Zod 4 APIs; `ZodError.issues` replaces the removed `.errors`
alias, and the mode schema's type assertion uses Zod 4's two-parameter
`ZodType` form.

Input-controlled arrays parsed from workspace mode configuration and message
schemas have explicit upper bounds. This lets Zod 4 reject over-limit arrays
before validating each element. The deprecated-group preprocessor also leaves
oversized arrays intact for that early rejection rather than allocating a
filtered copy. The bounds are 32 tool groups, 128 allowed MCP servers, 64
custom modes, and 128 image entries per message.
8 changes: 4 additions & 4 deletions src/vendor/zoo-code/src/core/config/CustomModesManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -406,11 +406,11 @@ export class CustomModesManager {
// Validate the mode configuration before saving
const validationResult = modeConfigSchema.safeParse(config)
if (!validationResult.success) {
const errorMessages = validationResult.error.errors
const errorMessages = validationResult.error.issues
.map((err) => `${err.path.join(".")}: ${err.message}`)
.join(", ")
const errorMessage = `Invalid mode configuration: ${errorMessages}`
logger.error("Mode validation failed", { slug, errors: validationResult.error.errors })
logger.error("Mode validation failed", { slug, errors: validationResult.error.issues })
vscode.window.showErrorMessage(t("common:customModes.errors.updateFailed", { error: errorMessage }))
throw new Error(errorMessage)
}
Expand Down Expand Up @@ -963,11 +963,11 @@ export class CustomModesManager {
const validationResult = modeConfigSchema.safeParse(modeConfig)
if (!validationResult.success) {
logger.error(`Invalid mode configuration for ${modeConfig.slug}`, {
errors: validationResult.error.errors,
errors: validationResult.error.issues,
})
return {
success: false,
error: `Invalid mode configuration for ${modeConfig.slug}: ${validationResult.error.errors.map((e) => e.message).join(", ")}`,
error: `Invalid mode configuration for ${modeConfig.slug}: ${validationResult.error.issues.map((e) => e.message).join(", ")}`,
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/vendor/zoo-code/types/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ export const clineMessageSchema = z.object({
ask: clineAskSchema.optional(),
say: clineSaySchema.optional(),
text: z.string().optional(),
images: z.array(z.string()).optional(),
images: z.array(z.string()).max(128).optional(),
partial: z.boolean().optional(),
reasoning: z.string().optional(),
conversationHistoryIndex: z.number().optional(),
Expand Down Expand Up @@ -369,7 +369,7 @@ export const queuedMessageSchema = z.object({
timestamp: z.number(),
id: z.string(),
text: z.string(),
images: z.array(z.string()).optional(),
images: z.array(z.string()).max(128).optional(),
})

export type QueuedMessage = z.infer<typeof queuedMessageSchema>
13 changes: 9 additions & 4 deletions src/vendor/zoo-code/types/mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,11 @@ function isDeprecatedGroupEntry(entry: unknown): boolean {
/**
* Raw schema for validating group entries after deprecated groups are stripped.
*/
const rawGroupEntryArraySchema = z.array(groupEntrySchema).refine(
const MAX_GROUP_ENTRIES = 32
const MAX_ALLOWED_MCP_SERVERS = 128
const MAX_CUSTOM_MODES = 64

const rawGroupEntryArraySchema = z.array(groupEntrySchema).max(MAX_GROUP_ENTRIES).refine(
(groups) => {
const seen = new Set()

Expand Down Expand Up @@ -89,9 +93,9 @@ const rawGroupEntryArraySchema = z.array(groupEntrySchema).refine(
* and breaks `zodResolver` generic inference in downstream consumers.
*/
export const groupEntryArraySchema = z.preprocess((val) => {
if (!Array.isArray(val)) return val
if (!Array.isArray(val) || val.length > MAX_GROUP_ENTRIES) return val
return val.filter((entry) => !isDeprecatedGroupEntry(entry))
}, rawGroupEntryArraySchema) as z.ZodType<GroupEntry[], z.ZodTypeDef, GroupEntry[]>
}, rawGroupEntryArraySchema) as unknown as z.ZodType<GroupEntry[]>

export const modeConfigSchema = z.object({
slug: z.string().regex(/^[a-zA-Z0-9-]+$/, "Slug must contain only letters numbers and dashes"),
Expand All @@ -104,6 +108,7 @@ export const modeConfigSchema = z.object({
source: z.enum(["global", "project"]).optional(),
allowedMcpServers: z
.array(z.string())
.max(MAX_ALLOWED_MCP_SERVERS)
.describe(
"Optional list of MCP server names to include. When omitted, all servers are available. When set, only the listed servers are injected.",
)
Expand All @@ -117,7 +122,7 @@ export type ModeConfig = z.infer<typeof modeConfigSchema>
*/

export const customModesSettingsSchema = z.object({
customModes: z.array(modeConfigSchema).refine(
customModes: z.array(modeConfigSchema).max(MAX_CUSTOM_MODES).refine(
(modes) => {
const slugs = new Set()

Expand Down
Loading