From aa5750c263632d98d626c5ca3b985bb1519d2395 Mon Sep 17 00:00:00 2001 From: Ysopwnable <161784452+N0zoM1z0@users.noreply.github.com> Date: Fri, 9 Oct 2026 17:18:17 +0800 Subject: [PATCH] fix(mcp): emit portable Unicode NUL patterns (#1230) Preserve NUL rejection while avoiding the short escape rejected by strict schema consumers. Guard the entire advertised input/output catalog and all three JavaScript regexp modes. Assisted-by: Kogasa (gensokyo-skills:kogasa-surprise-testing) --- src/contracts/native/nativeToolContracts.ts | 2 +- src/domain/apple/dylibResolution.ts | 2 +- .../javascript/electronActiveObservation.ts | 4 ++- src/domain/localPath.ts | 2 +- src/domain/native/nativeCallObservation.ts | 8 +++-- src/domain/process/processScenario.ts | 4 +-- .../mcp/advertisedPatternsVMode.test.ts | 31 ++++++++++++------- 7 files changed, 33 insertions(+), 20 deletions(-) diff --git a/src/contracts/native/nativeToolContracts.ts b/src/contracts/native/nativeToolContracts.ts index 89b7b8c1e..f2c441703 100644 --- a/src/contracts/native/nativeToolContracts.ts +++ b/src/contracts/native/nativeToolContracts.ts @@ -71,7 +71,7 @@ export const swiftSymbolsSchema = z .string() .min(1) .regex(/^[^\n]*$/u, "Each Swift symbol must be one line.") - .regex(/^[^\0]*$/u, "Swift symbols cannot contain NUL."), + .regex(/^[^\u0000]*$/u, "Swift symbols cannot contain NUL."), ) .min(1); diff --git a/src/domain/apple/dylibResolution.ts b/src/domain/apple/dylibResolution.ts index 9fd98a309..7fcdc7ff4 100644 --- a/src/domain/apple/dylibResolution.ts +++ b/src/domain/apple/dylibResolution.ts @@ -21,7 +21,7 @@ import { /** Normalized path below the analyzed root: no `.`/`..` or empty segments. */ const ROOT_RELATIVE_PATH = - /^(?!\/)(?!(?:.*\/)?\.{1,2}(?:\/|$))(?!.*\/\/)(?!.*\/$)[^\\\0]+$/u; + /^(?!\/)(?!(?:.*\/)?\.{1,2}(?:\/|$))(?!.*\/\/)(?!.*\/$)[^\\\u0000]+$/u; const rootRelativePathSchema = z .string() .min(1) diff --git a/src/domain/javascript/electronActiveObservation.ts b/src/domain/javascript/electronActiveObservation.ts index be5092065..298803ec0 100644 --- a/src/domain/javascript/electronActiveObservation.ts +++ b/src/domain/javascript/electronActiveObservation.ts @@ -78,7 +78,9 @@ export const electronActiveObservationInputSchema = z.strictObject({ ), args: z .array( - z.string().regex(/^[^\0]*$/u, "Electron arguments must not contain NUL"), + z + .string() + .regex(/^[^\u0000]*$/u, "Electron arguments must not contain NUL"), ) .default([]), actions: z.array(actionSchema).default([]), diff --git a/src/domain/localPath.ts b/src/domain/localPath.ts index d1dd98fce..6de151e5d 100644 --- a/src/domain/localPath.ts +++ b/src/domain/localPath.ts @@ -5,7 +5,7 @@ import { z } from "zod"; export const localPathStringSchema = z .string() .min(1) - .regex(/^[^\0]*$/u, "Local filesystem paths cannot contain NUL"); + .regex(/^[^\u0000]*$/u, "Local filesystem paths cannot contain NUL"); /** True when the value is an absolute filesystem path on the host platform. */ export const isAbsoluteLocalPath = (value: string): boolean => diff --git a/src/domain/native/nativeCallObservation.ts b/src/domain/native/nativeCallObservation.ts index 8e4e0dbd2..c43a97195 100644 --- a/src/domain/native/nativeCallObservation.ts +++ b/src/domain/native/nativeCallObservation.ts @@ -5,7 +5,7 @@ const hexSchema = z.string().regex(/^0x[0-9a-f]+$/u); const nativeString = z .string() .regex( - /^[^\0]*$/u, + /^[^\u0000]*$/u, "Native launch and breakpoint strings cannot contain NUL", ); /** Objective-C class names and selectors never contain spaces or brackets. */ @@ -61,8 +61,10 @@ export const nativeCallObservationInputSchema = z.strictObject({ z .string() .min(1) - .regex(/^[^=\0]+$/u, "Environment names cannot contain '=' or NUL"), - z.string().regex(/^[^\0]*$/u, "Environment values cannot contain NUL"), + .regex(/^[^=\u0000]+$/u, "Environment names cannot contain '=' or NUL"), + z + .string() + .regex(/^[^\u0000]*$/u, "Environment values cannot contain NUL"), ) .default({}) .describe("Overrides on top of the environment REA runs with."), diff --git a/src/domain/process/processScenario.ts b/src/domain/process/processScenario.ts index f62c947d7..586775032 100644 --- a/src/domain/process/processScenario.ts +++ b/src/domain/process/processScenario.ts @@ -8,7 +8,7 @@ const reservedRunIdEnvironmentName = "REA_PROCESS_RUN_ID"; const environmentName = z .string() .min(1, "Environment names must not be empty") - .regex(/^(?!REA_PROCESS_RUN_ID(?![\s\S]))[^=\0]+$/u, { + .regex(/^(?!REA_PROCESS_RUN_ID(?![\s\S]))[^=\u0000]+$/u, { error: (issue) => issue.input === reservedRunIdEnvironmentName ? `${reservedRunIdEnvironmentName} is reserved by the process adapter` @@ -17,7 +17,7 @@ const environmentName = z const childProcessString = z .string() .regex( - /^[^\0]*$/u, + /^[^\u0000]*$/u, "Values passed to operating-system APIs cannot contain NUL", ); export const normalizationSchema = z.object({ diff --git a/tests/boundary/mcp/advertisedPatternsVMode.test.ts b/tests/boundary/mcp/advertisedPatternsVMode.test.ts index abd1ba4ba..1791ce753 100644 --- a/tests/boundary/mcp/advertisedPatternsVMode.test.ts +++ b/tests/boundary/mcp/advertisedPatternsVMode.test.ts @@ -30,7 +30,7 @@ const collectPatterns = ( } }; -it("advertises patterns that compile under every engine a client may use", async () => { +it("advertises portable NUL escapes and patterns that compile in all JS modes", async () => { const session = createTestBinarySession(() => { throw new Error("No deep provider may start for a schema projection"); }); @@ -63,15 +63,24 @@ it("advertises patterns that compile under every engine a client may use", async // class, so REA's own schema validation cannot observe the defect. `v` mode // rejects them, and a client that compiles advertised patterns rejects the // complete tools request rather than the affected call. - const uncompilable = patterns.flatMap((entry) => { - try { - new RegExp(entry.pattern, "v"); - return []; - } catch (cause: unknown) { - return [ - `${entry.tool} ${entry.where} ${entry.pattern}: ${cause instanceof Error ? cause.message : String(cause)}`, - ]; - } - }); + const uncompilable = patterns.flatMap((entry) => + ["", "u", "v"].flatMap((flags) => { + try { + new RegExp(entry.pattern, flags); + return []; + } catch (cause: unknown) { + return [ + `${entry.tool} ${entry.where} /${entry.pattern}/${flags}: ${cause instanceof Error ? cause.message : String(cause)}`, + ]; + } + }), + ); expect(uncompilable).toEqual([]); + + // DeepSeek rejects the short NUL escape even though V8 accepts it in all + // three modes. Schema compilation must not hide that interchange failure. + expect(patterns.filter(({ pattern }) => pattern.includes("\\0"))).toEqual([]); + expect(patterns.some(({ pattern }) => pattern.includes("\\u0000"))).toBe( + true, + ); });