From b3dcddc115b32dfe95329297c5e73bb9fea8ff1b Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Wed, 11 Nov 2026 05:22:37 -0500 Subject: [PATCH] fix(providers): preserve bridge limits through error attribution --- src/cutter/CutterBridgeClient.ts | 5 ++- src/ghidra/GhidraProvider.behavior.test.ts | 31 +++++++++++++++++++ src/ghidra/GhidraProviderClient.ts | 6 +++- .../cutterBridgeRequestLimit.test.ts | 31 +++++++++++++++++++ 4 files changed, 71 insertions(+), 2 deletions(-) diff --git a/src/cutter/CutterBridgeClient.ts b/src/cutter/CutterBridgeClient.ts index e6eb6f013b..f9ff8f6d94 100644 --- a/src/cutter/CutterBridgeClient.ts +++ b/src/cutter/CutterBridgeClient.ts @@ -429,7 +429,10 @@ export class CutterBridgeClient { cause.limit.resource, cause.limit.reason, cause.limit.reportedLimits, - resourceConstraintRecovery(cause.limit), + { + ...resourceConstraintRecovery(cause.limit), + limit: cause.limit.limit, + }, ); } return undefined; diff --git a/src/ghidra/GhidraProvider.behavior.test.ts b/src/ghidra/GhidraProvider.behavior.test.ts index a751070f41..f25eca32cb 100644 --- a/src/ghidra/GhidraProvider.behavior.test.ts +++ b/src/ghidra/GhidraProvider.behavior.test.ts @@ -1,3 +1,4 @@ +import { bridgeRequestOversizeError } from "../config/bridgeMaxRequestBytes.js"; import { z } from "zod"; import { GHIDRA_LITERAL_CANDIDATES_LIMITATION } from "./GhidraLiteralSearch.js"; import { @@ -1886,6 +1887,36 @@ describe("Ghidra decompiler failure projection", () => { }); }); +it("preserves a bridge request limit when reattributing a Ghidra session failure", async () => { + const refusal = bridgeRequestOversizeError( + "procedure_pseudo_code", + { maximumBytes: 8192, source: "runtime" }, + 9000, + "Ghidra", + ); + const ghidra = provider(installationHost(), () => ({ + start: () => Promise.resolve(ok(sessionInfo())), + callTool: () => + Promise.resolve( + err( + new GhidraSessionError( + "protocol", + "Bridge request refused", + {}, + { cause: refusal }, + ), + ), + ), + close: () => Promise.resolve(ok(null)), + })); + const client = await createElfClient(ghidra); + const result = await client.execute("procedure_pseudo_code", { + procedure: "main", + }); + if (result.ok) throw new Error("Expected bridge limit refusal"); + expect(result.error.limit).toEqual(refusal.limit); +}); + describe("Ghidra decompile deadline for a finished invocation", () => { const decompileDeadline = async ( seconds: number, diff --git a/src/ghidra/GhidraProviderClient.ts b/src/ghidra/GhidraProviderClient.ts index 79c618be51..9aa4ef1d74 100644 --- a/src/ghidra/GhidraProviderClient.ts +++ b/src/ghidra/GhidraProviderClient.ts @@ -1319,7 +1319,11 @@ const projectPrimarySessionError = ( physicalCause.resource, physicalCause.reason, physicalCause.reportedLimits, - { ...context, ...resourceConstraintRecovery(physicalCause) }, + { + ...context, + ...resourceConstraintRecovery(physicalCause), + limit: physicalCause.limit, + }, ); if ( origin === "owned-or-classified" && diff --git a/tests/boundary/providers/cutterBridgeRequestLimit.test.ts b/tests/boundary/providers/cutterBridgeRequestLimit.test.ts index 945576b877..b7e7165311 100644 --- a/tests/boundary/providers/cutterBridgeRequestLimit.test.ts +++ b/tests/boundary/providers/cutterBridgeRequestLimit.test.ts @@ -87,6 +87,37 @@ it("uses a runtime bridge ceiling for a Cutter client built from the launch valu }); }); +it("retains the runtime limit when an authenticated status response is oversized", async () => { + const directory = await privateDirectory(); + const server = authenticatedServer( + TOKEN, + SESSION, + (_request, respond) => respond({ ok: true, padding: "x".repeat(8192) }), + true, + ); + const port = await listen(server); + await writeDescriptor(directory, SESSION, port, TOKEN, { + maxRequestBytes: 8192, + }); + const client = clientFor(directory, "8192"); + await expect( + runWithAnalysisOperationSnapshot( + { revision: 1, overrides: { "bridge.max_request_bytes": 8192 } }, + () => client.listSessions(), + ), + ).rejects.toSatisfy((error: unknown) => { + expect(error).toBeInstanceOf(AnalysisResourceConstraintError); + if (!(error instanceof AnalysisResourceConstraintError)) return false; + expect(error.operation).toBe("list_cutter_sessions"); + expect(error.limit).toMatchObject({ + setting: "bridge.max_request_bytes", + effective_value: 8192, + source: "runtime", + }); + return true; + }); +}); + it("rejects a bridge ceiling below the floor when the call has no override", async () => { const client = new CutterBridgeClient({ REA_BRIDGE_MAX_REQUEST_BYTES: "7463",