diff --git a/package-lock.json b/package-lock.json index a1480fd..f546680 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ "typescript": "^5.6.0", "undici": "^8.10.0", "yaml": "^2.5.1", - "zod": "^3.25.76" + "zod": "^4.6.6" }, "bin": { "headlesscode": "bin/headlesscode.mjs", @@ -1329,18 +1329,18 @@ } }, "node_modules/body-parser": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", - "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.4.0.tgz", + "integrity": "sha512-Ifk1KcLrxaGt1occQagQNFNNOqRt1LFuehEnHHzB8Z6cRgIxRU1TBaiIsUiCdA+gy5PL7Y1IX31DNKkRmxtXjQ==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", - "content-type": "^2.0.0", + "content-type": "^2.1.0", "debug": "^4.4.3", "http-errors": "^2.0.1", - "iconv-lite": "^0.7.2", + "iconv-lite": "^0.7.3", "on-finished": "^2.4.1", - "qs": "^6.15.2", + "qs": "^6.16.1", "raw-body": "^3.0.2", "type-is": "^2.1.0" }, @@ -1490,9 +1490,9 @@ } }, "node_modules/content-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", - "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", "license": "MIT", "engines": { "node": ">=18" @@ -1815,9 +1815,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", - "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz", + "integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==", "funding": [ { "type": "github", @@ -2710,9 +2710,9 @@ } }, "node_modules/qs": { - "version": "6.16.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", - "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.1.tgz", + "integrity": "sha512-5Px6J7rfHBAGUhNQ3es4YzcOdBBw0SDcVtThJml4RdnKw4tKs2epA0kPZ+K8X+WiHktT93RHVcQ/3ym5aTCgtg==", "license": "BSD-3-Clause", "dependencies": { "es-define-property": "^1.0.1", @@ -3327,9 +3327,9 @@ } }, "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", + "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index ef82fa4..458f08d 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/src/vendor/tests/smoke.ts b/src/vendor/tests/smoke.ts index fc2fbad..22782d3 100644 --- a/src/vendor/tests/smoke.ts +++ b/src/vendor/tests/smoke.ts @@ -91,6 +91,18 @@ async function main(): Promise { 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`, ) diff --git a/src/vendor/zoo-code/VENDOR-NOTES.md b/src/vendor/zoo-code/VENDOR-NOTES.md index 59aae07..4cf21ae 100644 --- a/src/vendor/zoo-code/VENDOR-NOTES.md +++ b/src/vendor/zoo-code/VENDOR-NOTES.md @@ -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. diff --git a/src/vendor/zoo-code/src/core/config/CustomModesManager.ts b/src/vendor/zoo-code/src/core/config/CustomModesManager.ts index af6b379..e0eef73 100644 --- a/src/vendor/zoo-code/src/core/config/CustomModesManager.ts +++ b/src/vendor/zoo-code/src/core/config/CustomModesManager.ts @@ -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) } @@ -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(", ")}`, } } diff --git a/src/vendor/zoo-code/types/message.ts b/src/vendor/zoo-code/types/message.ts index dc0e3df..40493f4 100644 --- a/src/vendor/zoo-code/types/message.ts +++ b/src/vendor/zoo-code/types/message.ts @@ -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(), @@ -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 diff --git a/src/vendor/zoo-code/types/mode.ts b/src/vendor/zoo-code/types/mode.ts index 50a1259..f39f4df 100644 --- a/src/vendor/zoo-code/types/mode.ts +++ b/src/vendor/zoo-code/types/mode.ts @@ -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() @@ -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 +}, rawGroupEntryArraySchema) as unknown as z.ZodType export const modeConfigSchema = z.object({ slug: z.string().regex(/^[a-zA-Z0-9-]+$/, "Slug must contain only letters numbers and dashes"), @@ -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.", ) @@ -117,7 +122,7 @@ export type ModeConfig = z.infer */ export const customModesSettingsSchema = z.object({ - customModes: z.array(modeConfigSchema).refine( + customModes: z.array(modeConfigSchema).max(MAX_CUSTOM_MODES).refine( (modes) => { const slugs = new Set()