fix(search): clarify regex diagnostics and recovery (#8776)

* fix(ghidra): identify invalid regex patterns

* docs(native): clarify regex dialects in search contracts

* fix(ghidra): guide regex syntax recovery

* fix(search): guide regex worker recovery

* fix(search): preserve regex parser diagnostics

* test(search): update regex boundary expectations

* refactor(search): remove unused regex error hook

---------

Co-authored-by: Cursor Agent <[email protected]>
This commit is contained in:
morlutoandCursor Agent authored and GitHub committed 2026-11-05 00:20:01 +08:00
1 parent 52bca6de41
commit 63b66fa498
7 files changed
+103 -17

No files matched your search

+1 -1
View File
@@ -4929,7 +4929,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
// without retaining its cause. This producer reason is not bad syntax.
if (exception.getDescription().equals("Stack overflow during pattern compilation"))
throw regexStackExhausted(expression, "compiling");
throw new RequestFailure("invalid_request", "Invalid regex pattern " + expression + ": " + exception.getDescription() + " at index " + exception.getIndex());
throw new RequestFailure("invalid_regex_pattern", "Invalid regex pattern " + expression + ": " + exception.getDescription() + " at index " + exception.getIndex());
}
catch (StackOverflowError exhausted) {
throw regexStackExhausted(expression, "compiling");
+1 -1
View File
@@ -21,7 +21,7 @@ import { HOPPER_MEMORY_TOOL_DEFINITIONS } from "./hopperMemoryContracts.js";
import { nativeSearchInput } from "../domain/native/nativeSearch.js";
const hopperRegexDeadline =
"Hopper regex mode uses ECMAScript Unicode syntax in a cancellable worker. The matching deadline is REA_REGEX_MATCHING_DEADLINE_MS (a positive decimal integer of milliseconds from 1 to 2147483647, default 5000). Unset keeps the default. A deadline failure names the effective milliseconds, source (default or setting), the default, and the maximum, and leaves the API usable. The worker old-generation request is REA_REGEX_WORKER_HEAP_REQUEST_MIB (a decimal integer of mebibytes from 7 to 16384, default 128). Unset keeps the default. A positive Node --max-old-space-size, including NODE_OPTIONS, overrides that request and is not limited by the setting's floor or maximum. A heap failure names the effective request, its source (default or setting), the default, the floor, and the maximum.";
"Regex dialect depends on the provider: Ghidra uses Java Pattern; Kuna, IDA and Hopper use ECMAScript Unicode. See binary_session for selected-provider limits. Hopper regex mode uses a cancellable worker. The matching deadline is REA_REGEX_MATCHING_DEADLINE_MS (a positive decimal integer of milliseconds from 1 to 2147483647, default 5000). Unset keeps the default. A deadline failure names the effective milliseconds, source (default or setting), the default, and the maximum, and leaves the API usable. The worker old-generation request is REA_REGEX_WORKER_HEAP_REQUEST_MIB (a decimal integer of mebibytes from 7 to 16384, default 128). Unset keeps the default. A positive Node --max-old-space-size, including NODE_OPTIONS, overrides that request and is not limited by the setting's floor or maximum. A heap failure names the effective request, its source (default or setting), the default, the floor, and the maximum.";
const official = <
Name extends keyof typeof officialOutputSchemas,
+20
View File
@@ -1427,6 +1427,26 @@ const projectPrimarySessionError = (
},
);
}
if (
failure.kind === "remote" &&
failure.remoteCode === "invalid_regex_pattern"
)
return new AnalysisInputError(
operation,
{
...context,
userMessage: failure.message,
remediationAction:
"Correct the pattern using the reported Ghidra regex syntax error, or select mode literal to search for the text literally, then retry.",
},
[
{
path: ["pattern"],
reason: "invalid_format",
message: failure.message,
},
],
);
if (failure.kind === "remote" && isGhidraInputFailureCode(failure.remoteCode))
return new AnalysisInputError(operation, context, [
{ path: [], reason: "invalid_value", message: failure.message },
+1
View File
@@ -190,6 +190,7 @@ const createGhidraSessionError = (failure: {
/** Bridge input failures share classification with their domain error projection. */
export const isGhidraInputFailureCode = (code: string | undefined): boolean =>
code === "invalid_request" ||
code === "invalid_regex_pattern" ||
code === "not_found" ||
code === "procedure_not_found" ||
code === "file_offset_unmapped" ||
+13 -13
View File
@@ -552,7 +552,6 @@ export const matchRegexValues = async (input: {
readonly caseSensitive: boolean;
readonly values: readonly string[];
readonly signal?: AbortSignal | undefined;
readonly invalidPattern?: (reason: string) => AnalysisError;
/** Resolved deadline. Omitted means the 5000 ms default, not ambient `process.env`. */
readonly deadline?: RegexMatchingDeadline | undefined;
/** Resolved heap request. Omitted means the 128 MiB default, not ambient `process.env`. */
@@ -623,20 +622,21 @@ export const matchRegexValues = async (input: {
if (reply.data.status === "invalid_pattern")
return finish(
err(
input.invalidPattern?.(reply.data.reason) ??
new AnalysisInputError(
input.operation,
new AnalysisInputError(
input.operation,
{
userMessage: `Invalid regex pattern: ${reply.data.reason}`,
remediationAction:
"Correct the pattern using the reported regex syntax error, or select mode literal to search for the text literally, then retry.",
},
[
{
userMessage: `Invalid regex pattern: ${reply.data.reason}`,
path: ["pattern"],
reason: "invalid_format",
message: reply.data.reason,
},
[
{
path: ["pattern"],
reason: "invalid_format",
message: "Supply a valid regular expression.",
},
],
),
],
),
),
);
if (
@@ -0,0 +1,63 @@
import { expect, it } from "vitest";
import { err, ok } from "../../../src/domain/result.js";
import { GhidraSessionError } from "../../../src/ghidra/GhidraSessionError.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { connectGhidraMcp } from "./ghidraMcpHarness.js";
it.each(["compact", "full"] as const)(
"identifies the invalid Ghidra regex field and permits correction (%s)",
async (profile) => {
const message =
"Invalid regex pattern [: Unclosed character class at index 0";
const harness = await connectGhidraMcp(
"ghidra-regex-diagnostic",
(operation, input) =>
Promise.resolve(
input.pattern === "["
? err(
new GhidraSessionError(
"remote",
message,
{},
{ remoteCode: "invalid_regex_pattern" },
),
)
: ok([]),
),
{ environment: { REA_MCP_OUTPUT_SCHEMA_PROFILE: profile } },
);
try {
for (const operation of [
"search_strings",
"search_procedures",
] as const) {
const rejected = await harness.mcp.callTool({
name: operation,
arguments: { pattern: "[", mode: "regex" },
});
const error = parseMcpToolError(rejected).error;
expect(error).toMatchObject({
code: "invalid_request",
message,
remediation: {
action: expect.stringMatching(/pattern.*mode literal/u),
},
details: {
issues: [{ path: ["pattern"], reason: "invalid_format", message }],
},
});
expect(error.recovery).toMatchObject({
kind: "inspect_input_contract",
operation,
});
const corrected = await harness.mcp.callTool({
name: operation,
arguments: { pattern: "valid", mode: "regex" },
});
expect(corrected.isError).not.toBe(true);
}
} finally {
await harness.close();
}
},
);
+4 -2
View File
@@ -583,6 +583,7 @@ it("names the rejected pattern field for a default invalid-pattern refusal", asy
expect(projectAnalysisError(matched.error)).toMatchObject({
code: "invalid_request",
category: "invalid_input",
remediation: { action: expect.stringMatching(/pattern.*mode literal/u) },
recovery: {
kind: "inspect_input_contract",
operation: "search_procedures",
@@ -593,7 +594,7 @@ it("names the rejected pattern field for a default invalid-pattern refusal", asy
{
path: ["pattern"],
reason: "invalid_format",
message: "Supply a valid regular expression.",
message: expect.stringContaining("Invalid regular expression"),
},
],
},
@@ -620,6 +621,7 @@ it("keeps Hopper's result projection and invalid-pattern contract with the share
code: "invalid_request",
category: "invalid_input",
message: expect.stringContaining("Invalid regex pattern"),
remediation: { action: expect.stringMatching(/pattern.*mode literal/u) },
recovery: { kind: "inspect_input_contract", operation: "search_strings" },
details: {
operation: "search_strings",
@@ -627,7 +629,7 @@ it("keeps Hopper's result projection and invalid-pattern contract with the share
{
path: ["pattern"],
reason: "invalid_format",
message: "Supply a valid regular expression.",
message: expect.stringContaining("Invalid regular expression"),
},
],
},