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)
This commit is contained in:
Ysopwnable 2026-10-09 17:18:17 +08:00 committed by GitHub
parent f4c99ab8a5
commit aa5750c263
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 33 additions and 20 deletions

View File

@ -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);

View File

@ -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)

View File

@ -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([]),

View File

@ -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 =>

View File

@ -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."),

View File

@ -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({

View File

@ -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,
);
});