[GH-ISSUE #5986] [Bug] Hopper invalid regex errors omit the pattern field and input-contract recovery #3654

Closed
opened 2026-10-31 00:42:44 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @coygeek on GitHub (Oct 21, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/5986

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Native analysis or decompilation

Observed behavior

Hopper regex searches reject an invalid pattern with the correct invalid_request code and a useful syntax message, but omit details.issues identifying pattern and omit inspect_input_contract recovery. The remediation says to correct the reported address, document or arguments, without providing a structured rejected-field entry.

This reproduced for both search_strings and search_procedures through production CLI composition with the repository's simulated Hopper bridge. Correcting the pattern succeeded for both operations.

Reproduction

The CLI requests exercised were equivalent to:

rea search sample '(' --provider hopper --kind strings --mode regex --json
rea search sample '(' --provider hopper --kind procedures --mode regex --json

The following focused test executes those requests through the repository's serveCli harness. Save it as tests/process-global/hopperPatternDiagnostics.test.ts in the built checkout and run:

node node_modules/vitest/vitest.mjs run tests/process-global/hopperPatternDiagnostics.test.ts --maxWorkers 1

Only Hopper process admission is substituted. The production CLI, provider/client, authenticated socket protocol, regex worker and error projection run normally. The system executable supplies native target admission and is never executed. This does not test a real Hopper installation or MCP transport.

import { copyFile, mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, expect, it, vi } from "vitest";
import type { BridgeLauncher, BridgeSession } from "../../src/hopper/BridgeLauncher.js";
import { analysisCliErrorEnvelopeSchema } from "../../src/contracts/errorSchemas.js";
import { parseEvidence } from "../../src/domain/evidence.js";
import { waitForExit } from "../support/process/processFixture.js";
import { HopperFixtureLauncher } from "../boundary/providers/hopper/hopperClient.fixture.js";
import { serveCli } from "../boundary/cli/cliServeHarness.js";

const originalExitCode = process.exitCode;
afterEach(() => { process.exitCode = originalExitCode; });
const fixture = vi.hoisted(() => ({ launcher: undefined as BridgeLauncher | undefined }));
vi.mock("../../src/hopper/BridgeLauncher.js", async (importOriginal) => ({
  ...(await importOriginal<typeof import("../../src/hopper/BridgeLauncher.js")>()),
  HopperApplicationLauncher: class implements BridgeLauncher {
    launch(session: BridgeSession) {
      if (fixture.launcher === undefined) throw new Error("Missing fixture launcher");
      return fixture.launcher.launch(session);
    }
  },
}));

it.each(["strings", "procedures"] as const)(
  "%s regex reports an invalid pattern without correction metadata; valid control succeeds",
  async (kind) => {
    for (const pattern of ["(", "fixture"]) {
      process.exitCode = undefined;
      const home = await mkdtemp(join(process.env.TEST_WORK_ROOT ?? tmpdir(), "hopper-pattern-"));
      const target = join(home, "sample");
      const launcher = new HopperFixtureLauncher();
      fixture.launcher = launcher;
      try {
        await copyFile("/usr/bin/true", target);
        const run = await serveCli([
          "search", target, pattern, "--provider", "hopper", "--kind", kind,
          "--mode", "regex", "--json",
        ], { HOME: home, HOPPER_LAUNCHER_PATH: process.execPath, REA_LOG_LEVEL: "silent" });
        const output: unknown = JSON.parse(run.stdout);
        if (pattern === "(") {
          const error = analysisCliErrorEnvelopeSchema.parse(output);
          expect(process.exitCode ?? run.exitCode).toBe(1);
          expect(error.code).toBe("invalid_request");
          expect(error.category).toBe("invalid_input");
          expect(error.retryable).toBe(false);
          expect(error.message).toContain("Invalid regex pattern");
          expect(error.details?.operation).toBe(`search_${kind}`);
          expect(error.details?.issues).toBeUndefined();
          expect(error.recovery).toBeUndefined();
          expect(error.remediation.action).toBe("Correct the reported address, document, or arguments and retry.");
          console.log(JSON.stringify({ kind, pattern, exit: process.exitCode ?? run.exitCode,
            code: error.code, category: error.category, message: error.message,
            retryable: error.retryable, operation: error.details?.operation,
            issues: error.details?.issues ?? null, recovery: error.recovery ?? null,
            remediation: error.remediation }));
        } else {
          expect(process.exitCode ?? run.exitCode).toBe(0);
          const evidence = parseEvidence(output);
          expect(evidence.operation).toBe(`search_${kind}`);
          expect(evidence.normalized_result).toEqual([{ address: "0x1000", value: "fixture" }]);
          console.log(JSON.stringify({ kind, pattern, exit: process.exitCode ?? run.exitCode,
            operation: evidence.operation, result: evidence.normalized_result }));
        }
        expect(launcher.requests.map(({ method }) => method)).toEqual(["health", `list_${kind}`, "shutdown"]);
        expect(launcher.processes).toHaveLength(1);
        expect(launcher.processes[0]?.exitCode !== null || launcher.processes[0]?.signalCode !== null).toBe(true);
      } finally {
        fixture.launcher = undefined;
        for (const child of launcher.processes) {
          if (child.exitCode === null && child.signalCode === null) child.kill("SIGTERM");
          await waitForExit(child, 5_000);
        }
        await rm(home, { recursive: true, force: true });
      }
    }
  },
);

Executed once: both test cases passed, covering two invalid-pattern requests and two successful controls. The assertions above describe the current faulty diagnostic; restoration should require a pattern issue and input-contract recovery instead of their absence. Every invocation completed simulated provider shutdown.

Expected behavior

Keep the correct rejection, code, category, operation identity and nonzero exit status. Include a structured issue such as:

{"path":["pattern"],"reason":"invalid_format","message":"Supply a valid regular expression."}

Also provide recovery: {"kind":"inspect_input_contract","operation":"search_strings"} or the corresponding search_procedures operation, with schema/usage correction guidance.

The IDA input boundary already returns exactly this issue for the same search inputs. Executing that built helper and the shared projector for both operations confirmed the issue, recovery and schema/usage remediation without launching IDA. The documented recovery hint describes how callers use it; the hint is optional in the general error contract.

Impact and scope

P3: missing structured correction diagnostics for rejected inputs. The message already identifies invalid regex syntax, so callers can correct it manually. Replacing ( with fixture succeeded for both operations. No wrong successful analysis, target mutation, data loss or security boundary crossing was observed.

Observed scope is both CLI regex search kinds using a simulated Hopper inventory. The shared provider path also serves MCP, but MCP transport and real Hopper execution were not tested. Runtime occurrence frequency is unknown.

REA version

rea-agents 6.7.0; source revision 52415975942a4b9c53a817915b313ccc404f280e.

Runtime and platform

macOS 27.0.1 arm64; Node.js v26.12.0; Vitest 5.0.3. The repository's exact Node 24.18.0 contributor pin was not exercised. No real Hopper engine was launched.

Target and artifact context

A copy of the system native executable supplies target admission. The simulated Hopper bridge returns one inventory record: {"address":"0x1000","value":"fixture"}. Only the search pattern is malformed.

Interface

CLI

Earliest failure stage

Request parsing or validation

Relevant output or evidence

Observed diagnostic subset for the string search; JSON null below records an absent field:

{
  "exit": 1,
  "code": "invalid_request",
  "category": "invalid_input",
  "message": "Hopper search_strings failed (-32000, invalid_request): Invalid regex pattern: Invalid regular expression: /(/iu: Unterminated group",
  "retryable": false,
  "operation": "search_strings",
  "issues": null,
  "recovery": null,
  "remediation": {
    "action": "Correct the reported address, document, or arguments and retry."
  }
}

The procedure search returned the same fields with operation search_procedures. Both corrected-pattern controls returned exit 0 and normalized_result: [{"address":"0x1000","value":"fixture"}]. All four calls asserted health, the selected list_ inventory request and shutdown, with the owned provider process exited afterward.

Typed outcome or recovery expected

invalid_request / invalid_input, retryable: false, nonempty details.issues naming pattern, and inspect_input_contract recovery naming the accepted search operation. Corrected valid searches should retain their current results and cleanup behavior.

Analysis or suspected cause

Source-derived: the Hopper provider routes regex searches to its local matcher after the search schema accepts the string. HopperRegexSearch overrides the matcher's invalid-pattern handling with HopperRemoteError, even though this is a caller-input failure.

The shared projector supplies input-contract recovery and structured issues for AnalysisInputError. The Hopper provider-error class bypasses both branches. The matcher fallback is already an input error but has no issues, so simply removing the override would restore recovery without restoring the field path.

Additional context

Related reports #5950 and #5971 concern native matching Evidence and Android input validation, respectively; neither changes the Hopper invalid-pattern callback. Closed #4680 concerns attribution of failed Hopper inventory requests, while the inventory succeeds here. #2146 concerns incorrect argument names, not invalid regex syntax supplied under the correct pattern key.

Fetched upstream main 25e1a24a3ed3671a0148757555f18d16c29d936a leaves the relevant callback, search schema and issue/recovery logic unchanged. That newer revision was inspected, not executed.

Originally created by @coygeek on GitHub (Oct 21, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5986 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Native analysis or decompilation ### Observed behavior Hopper regex searches reject an invalid pattern with the correct `invalid_request` code and a useful syntax message, but omit `details.issues` identifying `pattern` and omit `inspect_input_contract` recovery. The remediation says to correct the reported address, document or arguments, without providing a structured rejected-field entry. This reproduced for both `search_strings` and `search_procedures` through production CLI composition with the repository's simulated Hopper bridge. Correcting the pattern succeeded for both operations. ### Reproduction The CLI requests exercised were equivalent to: ```sh rea search sample '(' --provider hopper --kind strings --mode regex --json rea search sample '(' --provider hopper --kind procedures --mode regex --json ``` The following focused test executes those requests through the repository's `serveCli` harness. Save it as `tests/process-global/hopperPatternDiagnostics.test.ts` in the built checkout and run: ```sh node node_modules/vitest/vitest.mjs run tests/process-global/hopperPatternDiagnostics.test.ts --maxWorkers 1 ``` Only Hopper process admission is substituted. The production CLI, provider/client, authenticated socket protocol, regex worker and error projection run normally. The system executable supplies native target admission and is never executed. This does not test a real Hopper installation or MCP transport. ```ts import { copyFile, mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, expect, it, vi } from "vitest"; import type { BridgeLauncher, BridgeSession } from "../../src/hopper/BridgeLauncher.js"; import { analysisCliErrorEnvelopeSchema } from "../../src/contracts/errorSchemas.js"; import { parseEvidence } from "../../src/domain/evidence.js"; import { waitForExit } from "../support/process/processFixture.js"; import { HopperFixtureLauncher } from "../boundary/providers/hopper/hopperClient.fixture.js"; import { serveCli } from "../boundary/cli/cliServeHarness.js"; const originalExitCode = process.exitCode; afterEach(() => { process.exitCode = originalExitCode; }); const fixture = vi.hoisted(() => ({ launcher: undefined as BridgeLauncher | undefined })); vi.mock("../../src/hopper/BridgeLauncher.js", async (importOriginal) => ({ ...(await importOriginal<typeof import("../../src/hopper/BridgeLauncher.js")>()), HopperApplicationLauncher: class implements BridgeLauncher { launch(session: BridgeSession) { if (fixture.launcher === undefined) throw new Error("Missing fixture launcher"); return fixture.launcher.launch(session); } }, })); it.each(["strings", "procedures"] as const)( "%s regex reports an invalid pattern without correction metadata; valid control succeeds", async (kind) => { for (const pattern of ["(", "fixture"]) { process.exitCode = undefined; const home = await mkdtemp(join(process.env.TEST_WORK_ROOT ?? tmpdir(), "hopper-pattern-")); const target = join(home, "sample"); const launcher = new HopperFixtureLauncher(); fixture.launcher = launcher; try { await copyFile("/usr/bin/true", target); const run = await serveCli([ "search", target, pattern, "--provider", "hopper", "--kind", kind, "--mode", "regex", "--json", ], { HOME: home, HOPPER_LAUNCHER_PATH: process.execPath, REA_LOG_LEVEL: "silent" }); const output: unknown = JSON.parse(run.stdout); if (pattern === "(") { const error = analysisCliErrorEnvelopeSchema.parse(output); expect(process.exitCode ?? run.exitCode).toBe(1); expect(error.code).toBe("invalid_request"); expect(error.category).toBe("invalid_input"); expect(error.retryable).toBe(false); expect(error.message).toContain("Invalid regex pattern"); expect(error.details?.operation).toBe(`search_${kind}`); expect(error.details?.issues).toBeUndefined(); expect(error.recovery).toBeUndefined(); expect(error.remediation.action).toBe("Correct the reported address, document, or arguments and retry."); console.log(JSON.stringify({ kind, pattern, exit: process.exitCode ?? run.exitCode, code: error.code, category: error.category, message: error.message, retryable: error.retryable, operation: error.details?.operation, issues: error.details?.issues ?? null, recovery: error.recovery ?? null, remediation: error.remediation })); } else { expect(process.exitCode ?? run.exitCode).toBe(0); const evidence = parseEvidence(output); expect(evidence.operation).toBe(`search_${kind}`); expect(evidence.normalized_result).toEqual([{ address: "0x1000", value: "fixture" }]); console.log(JSON.stringify({ kind, pattern, exit: process.exitCode ?? run.exitCode, operation: evidence.operation, result: evidence.normalized_result })); } expect(launcher.requests.map(({ method }) => method)).toEqual(["health", `list_${kind}`, "shutdown"]); expect(launcher.processes).toHaveLength(1); expect(launcher.processes[0]?.exitCode !== null || launcher.processes[0]?.signalCode !== null).toBe(true); } finally { fixture.launcher = undefined; for (const child of launcher.processes) { if (child.exitCode === null && child.signalCode === null) child.kill("SIGTERM"); await waitForExit(child, 5_000); } await rm(home, { recursive: true, force: true }); } } }, ); ``` Executed once: both test cases passed, covering two invalid-pattern requests and two successful controls. The assertions above describe the current faulty diagnostic; restoration should require a `pattern` issue and input-contract recovery instead of their absence. Every invocation completed simulated provider shutdown. ### Expected behavior Keep the correct rejection, code, category, operation identity and nonzero exit status. Include a structured issue such as: ```json {"path":["pattern"],"reason":"invalid_format","message":"Supply a valid regular expression."} ``` Also provide `recovery: {"kind":"inspect_input_contract","operation":"search_strings"}` or the corresponding `search_procedures` operation, with schema/usage correction guidance. The [IDA input boundary](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/ida/IdaInput.ts#L61-L77) already returns exactly this issue for the same search inputs. Executing that built helper and the shared projector for both operations confirmed the issue, recovery and schema/usage remediation without launching IDA. The [documented recovery hint](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/docs/mcp-contracts.md#L91-L101) describes how callers use it; the hint is optional in the general error contract. ### Impact and scope P3: missing structured correction diagnostics for rejected inputs. The message already identifies invalid regex syntax, so callers can correct it manually. Replacing `(` with `fixture` succeeded for both operations. No wrong successful analysis, target mutation, data loss or security boundary crossing was observed. Observed scope is both CLI regex search kinds using a simulated Hopper inventory. The shared provider path also serves MCP, but MCP transport and real Hopper execution were not tested. Runtime occurrence frequency is unknown. ### REA version rea-agents 6.7.0; source revision `52415975942a4b9c53a817915b313ccc404f280e`. ### Runtime and platform macOS 27.0.1 arm64; Node.js v26.12.0; Vitest 5.0.3. The repository's exact Node 24.18.0 contributor pin was not exercised. No real Hopper engine was launched. ### Target and artifact context A copy of the system native executable supplies target admission. The simulated Hopper bridge returns one inventory record: `{"address":"0x1000","value":"fixture"}`. Only the search pattern is malformed. ### Interface CLI ### Earliest failure stage Request parsing or validation ### Relevant output or evidence Observed diagnostic subset for the string search; JSON `null` below records an absent field: ```json { "exit": 1, "code": "invalid_request", "category": "invalid_input", "message": "Hopper search_strings failed (-32000, invalid_request): Invalid regex pattern: Invalid regular expression: /(/iu: Unterminated group", "retryable": false, "operation": "search_strings", "issues": null, "recovery": null, "remediation": { "action": "Correct the reported address, document, or arguments and retry." } } ``` The procedure search returned the same fields with operation `search_procedures`. Both corrected-pattern controls returned exit 0 and `normalized_result: [{"address":"0x1000","value":"fixture"}]`. All four calls asserted `health`, the selected `list_` inventory request and `shutdown`, with the owned provider process exited afterward. ### Typed outcome or recovery expected `invalid_request` / `invalid_input`, `retryable: false`, nonempty `details.issues` naming `pattern`, and `inspect_input_contract` recovery naming the accepted search operation. Corrected valid searches should retain their current results and cleanup behavior. ### Analysis or suspected cause Source-derived: the [Hopper provider](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/hopper/HopperProvider.ts#L349-L355) routes regex searches to its local matcher after the [search schema](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/contracts/analysisSearchContract.ts#L4-L12) accepts the string. [HopperRegexSearch](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/hopper/HopperRegexSearch.ts#L126-L138) overrides the matcher's invalid-pattern handling with `HopperRemoteError`, even though this is a caller-input failure. The shared projector supplies [input-contract recovery](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/domain/analysisErrorProjection.ts#L126-L141) and [structured issues](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/domain/analysisErrorProjection.ts#L333-L346) for `AnalysisInputError`. The Hopper provider-error class bypasses both branches. The [matcher fallback](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/process/RegexMatcher.ts#L117-L125) is already an input error but has no issues, so simply removing the override would restore recovery without restoring the field path. ### Additional context Related reports #5950 and #5971 concern native matching Evidence and Android input validation, respectively; neither changes the Hopper invalid-pattern callback. Closed #4680 concerns attribution of failed Hopper inventory requests, while the inventory succeeds here. #2146 concerns incorrect argument names, not invalid regex syntax supplied under the correct `pattern` key. Fetched upstream main `25e1a24a3ed3671a0148757555f18d16c29d936a` leaves the relevant callback, search schema and issue/recovery logic unchanged. That newer revision was inspected, not executed.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#3654
No description provided.