mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
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:
1 parent
52bca6de41
commit
63b66fa498
7 files changed
+103
-17
No files matched your search
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -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"),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user