fix(ghidra): scope rejected-input diagnostics to their request (#7140)
Scope expected Ghidra input failures to the active request after the authenticated startup handshake. Preserve operation output, identity and log coordinates while retaining cumulative output with its session owner; lifecycle and unexpected failures keep full diagnostics. Share remote input-code classification with domain error projection. Verify startup rejection, healthy continuation, cleanup, unknown errors, OOM diagnostics and real CLI/MCP workflows. The measured unknown-symbol reply falls from 13,170 to 2,209 bytes with unchanged successful facts and Evidence IDs.
This commit is contained in:
1 parent
f69c52a1cf
commit
6381aa0b1d
10 files changed
+260
-52
No files matched your search
@@ -30,6 +30,17 @@ signal. Confirm resource causes from process output or host/container records;
|
|||||||
operation-scoped JVM `OutOfMemoryError` observations retain their separate resource
|
operation-scoped JVM `OutOfMemoryError` observations retain their separate resource
|
||||||
classification. Automatic kernel or cgroup OOM attribution is not implemented.
|
classification. Automatic kernel or cgroup OOM attribution is not implemented.
|
||||||
|
|
||||||
|
After the authenticated startup handshake, rejected inputs (`invalid_request`,
|
||||||
|
`not_found`, `ambiguous`, or an invalid function name) use
|
||||||
|
`diagnostics.diagnostic_scope: "request"` when their active output checkpoint is
|
||||||
|
available. They preserve the active request's
|
||||||
|
`failure_output`, target/profile identity and runtime/log coordinates, while
|
||||||
|
omitting cumulative launcher stdout/stderr and launch-support output. Startup,
|
||||||
|
process, protocol and other remote failures retain full session diagnostics.
|
||||||
|
The Ghidra client still owns its bounded cumulative output; this projection does
|
||||||
|
not clear it or alter session readiness. Log paths refer to the owned ephemeral
|
||||||
|
runtime and are removed by normal cleanup.
|
||||||
|
|
||||||
Provider selection and profile identity follow
|
Provider selection and profile identity follow
|
||||||
[MCP contracts](mcp-contracts.md#identity-and-discovery). One deep
|
[MCP contracts](mcp-contracts.md#identity-and-discovery). One deep
|
||||||
provider stays bound to the target; runtime failure never selects another
|
provider stays bound to the target; runtime failure never selects another
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ import {
|
|||||||
ghidraFailureOutput,
|
ghidraFailureOutput,
|
||||||
GHIDRA_JVM_OOME_MARKER,
|
GHIDRA_JVM_OOME_MARKER,
|
||||||
GHIDRA_PROCESS_DIAGNOSTIC_BYTES,
|
GHIDRA_PROCESS_DIAGNOSTIC_BYTES,
|
||||||
|
type GhidraDiagnosticsScope,
|
||||||
} from "./GhidraDiagnostics.js";
|
} from "./GhidraDiagnostics.js";
|
||||||
import { GhidraLaunchError, type GhidraLaunch } from "./GhidraLauncher.js";
|
import { GhidraLaunchError, type GhidraLaunch } from "./GhidraLauncher.js";
|
||||||
import { GhidraResponseRouter } from "./GhidraResponseRouter.js";
|
import { GhidraResponseRouter } from "./GhidraResponseRouter.js";
|
||||||
@@ -158,7 +159,7 @@ export class GhidraClient {
|
|||||||
#startupAttempt: GhidraStartupAttempt | undefined;
|
#startupAttempt: GhidraStartupAttempt | undefined;
|
||||||
#closePromise: Promise<void> | undefined;
|
#closePromise: Promise<void> | undefined;
|
||||||
readonly #failure = bindGhidraSessionFailure(
|
readonly #failure = bindGhidraSessionFailure(
|
||||||
() => this.#diagnostics(),
|
(scope) => this.#diagnostics(scope),
|
||||||
(value, incomplete) => this.#redactAuthentication(value, incomplete),
|
(value, incomplete) => this.#redactAuthentication(value, incomplete),
|
||||||
);
|
);
|
||||||
readonly #wire: GhidraWire;
|
readonly #wire: GhidraWire;
|
||||||
@@ -1339,7 +1340,17 @@ export class GhidraClient {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#diagnostics(): Readonly<Record<string, JsonValue>> {
|
#diagnostics(
|
||||||
|
requestedScope: GhidraDiagnosticsScope = "session",
|
||||||
|
): Readonly<Record<string, JsonValue>> {
|
||||||
|
// Startup errors still need launcher evidence. A request projection must
|
||||||
|
// have both a completed handshake and its own active output checkpoint.
|
||||||
|
const scope =
|
||||||
|
requestedScope === "request" &&
|
||||||
|
this.#startupSucceeded() &&
|
||||||
|
this.#activeOutputCheckpoint !== undefined
|
||||||
|
? "request"
|
||||||
|
: "session";
|
||||||
const currentSnapshot = this.#process?.snapshot() ?? this.#processSnapshot;
|
const currentSnapshot = this.#process?.snapshot() ?? this.#processSnapshot;
|
||||||
const snapshot: ProviderProcessSnapshot | undefined =
|
const snapshot: ProviderProcessSnapshot | undefined =
|
||||||
this.#outputDrainIncomplete && currentSnapshot !== undefined
|
this.#outputDrainIncomplete && currentSnapshot !== undefined
|
||||||
@@ -1350,7 +1361,9 @@ export class GhidraClient {
|
|||||||
}
|
}
|
||||||
: currentSnapshot;
|
: currentSnapshot;
|
||||||
const checkpoint =
|
const checkpoint =
|
||||||
this.#activeOutputCheckpoint ?? this.#startupOutputCheckpoint;
|
scope === "request"
|
||||||
|
? this.#activeOutputCheckpoint
|
||||||
|
: (this.#activeOutputCheckpoint ?? this.#startupOutputCheckpoint);
|
||||||
const scoped =
|
const scoped =
|
||||||
checkpoint === undefined
|
checkpoint === undefined
|
||||||
? undefined
|
? undefined
|
||||||
@@ -1360,6 +1373,7 @@ export class GhidraClient {
|
|||||||
? undefined
|
? undefined
|
||||||
: ghidraFailureOutput(scoped, this.#token, this.#outputDrainIncomplete);
|
: ghidraFailureOutput(scoped, this.#token, this.#outputDrainIncomplete);
|
||||||
return createGhidraDiagnostics({
|
return createGhidraDiagnostics({
|
||||||
|
scope,
|
||||||
targetPath: this.#options.targetPath,
|
targetPath: this.#options.targetPath,
|
||||||
targetSha256: this.#options.targetSha256,
|
targetSha256: this.#options.targetSha256,
|
||||||
...(this.#targetAdmission === undefined
|
...(this.#targetAdmission === undefined
|
||||||
|
|||||||
@@ -5,6 +5,64 @@ import {
|
|||||||
} from "./GhidraDiagnostics.js";
|
} from "./GhidraDiagnostics.js";
|
||||||
import { expect, it } from "vitest";
|
import { expect, it } from "vitest";
|
||||||
|
|
||||||
|
it("keeps request output and provenance without projecting launcher history", () => {
|
||||||
|
const output = { text: "launcher output", bytes: 15, observedBytes: 15 };
|
||||||
|
const options = {
|
||||||
|
targetPath: "./caller-selected/../target",
|
||||||
|
targetSha256: "a".repeat(64),
|
||||||
|
transport: "unix-socket" as const,
|
||||||
|
providerVersion: "12.1.4",
|
||||||
|
profileDigest: "b".repeat(64),
|
||||||
|
runtimeRoot: "/owned/runtime",
|
||||||
|
snapshot: { stdout: output, stderr: output, exitCode: null, signal: null },
|
||||||
|
previous: {},
|
||||||
|
failure_output: { stdout: { text: "query output" }, complete: false },
|
||||||
|
};
|
||||||
|
const request = createGhidraDiagnostics({ ...options, scope: "request" });
|
||||||
|
expect(request).toMatchObject({
|
||||||
|
diagnostic_scope: "request",
|
||||||
|
target_path: options.targetPath,
|
||||||
|
target_sha256: options.targetSha256,
|
||||||
|
profile_digest: options.profileDigest,
|
||||||
|
runtime_root: options.runtimeRoot,
|
||||||
|
failure_output: options.failure_output,
|
||||||
|
});
|
||||||
|
expect(request).not.toHaveProperty("stdout");
|
||||||
|
expect(request).not.toHaveProperty("stderr");
|
||||||
|
expect(createGhidraDiagnostics(options)).toMatchObject({
|
||||||
|
stdout: { text: output.text },
|
||||||
|
stderr: { text: output.text },
|
||||||
|
failure_output: options.failure_output,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps cached session output owned while projecting historical request coordinates", () => {
|
||||||
|
const previous = {
|
||||||
|
target_path: "/selected/target",
|
||||||
|
ghidra_log_path: "/owned/project/ghidra.log",
|
||||||
|
stdout: { text: "old launcher output" },
|
||||||
|
stderr: { text: "old launcher stderr" },
|
||||||
|
launch_support: [{ stdout: "JDK version" }],
|
||||||
|
failure_output: { stdout: { text: "old request" }, complete: true },
|
||||||
|
};
|
||||||
|
const request = createGhidraDiagnostics({
|
||||||
|
scope: "request",
|
||||||
|
targetPath: "/selected/target",
|
||||||
|
targetSha256: "a".repeat(64),
|
||||||
|
transport: "unix-socket",
|
||||||
|
providerVersion: "12.1.4",
|
||||||
|
profileDigest: "b".repeat(64),
|
||||||
|
previous,
|
||||||
|
});
|
||||||
|
expect(request).toEqual({
|
||||||
|
diagnostic_scope: "request",
|
||||||
|
target_path: previous.target_path,
|
||||||
|
ghidra_log_path: previous.ghidra_log_path,
|
||||||
|
});
|
||||||
|
expect(previous.stdout.text).toBe("old launcher output");
|
||||||
|
expect(previous.failure_output.stdout.text).toBe("old request");
|
||||||
|
});
|
||||||
|
|
||||||
it("does not reuse historical operation-scoped failure output without a current owner", () => {
|
it("does not reuse historical operation-scoped failure output without a current owner", () => {
|
||||||
const diagnostics = createGhidraDiagnostics({
|
const diagnostics = createGhidraDiagnostics({
|
||||||
targetPath: "/tmp/target",
|
targetPath: "/tmp/target",
|
||||||
|
|||||||
@@ -13,8 +13,12 @@ export const GHIDRA_PROCESS_DIAGNOSTIC_BYTES = 8 * 1024 * 1024;
|
|||||||
/** JVM error class located in all launcher output, beyond the retention budget. */
|
/** JVM error class located in all launcher output, beyond the retention budget. */
|
||||||
export const GHIDRA_JVM_OOME_MARKER = "java.lang.OutOfMemoryError";
|
export const GHIDRA_JVM_OOME_MARKER = "java.lang.OutOfMemoryError";
|
||||||
|
|
||||||
|
/** A rejected caller input owns request output, not the launcher's history. */
|
||||||
|
export type GhidraDiagnosticsScope = "session" | "request";
|
||||||
|
|
||||||
/** Inputs needed to project bounded diagnostics for one Ghidra runtime. */
|
/** Inputs needed to project bounded diagnostics for one Ghidra runtime. */
|
||||||
export interface GhidraDiagnosticsOptions {
|
export interface GhidraDiagnosticsOptions {
|
||||||
|
readonly scope?: GhidraDiagnosticsScope;
|
||||||
readonly targetPath: string;
|
readonly targetPath: string;
|
||||||
readonly targetSha256: string;
|
readonly targetSha256: string;
|
||||||
readonly targetAdmission?: JsonValue;
|
readonly targetAdmission?: JsonValue;
|
||||||
@@ -43,9 +47,16 @@ export const createGhidraDiagnostics = (
|
|||||||
) {
|
) {
|
||||||
const history: Record<string, JsonValue> = { ...options.previous };
|
const history: Record<string, JsonValue> = { ...options.previous };
|
||||||
delete history.failure_output;
|
delete history.failure_output;
|
||||||
|
if (options.scope === "request") {
|
||||||
|
delete history.stdout;
|
||||||
|
delete history.stderr;
|
||||||
|
delete history.launch_support;
|
||||||
|
history.diagnostic_scope = "request";
|
||||||
|
}
|
||||||
return history;
|
return history;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
...(options.scope === "request" ? { diagnostic_scope: "request" } : {}),
|
||||||
target_path: options.targetPath,
|
target_path: options.targetPath,
|
||||||
target_sha256: options.targetSha256,
|
target_sha256: options.targetSha256,
|
||||||
...(options.targetAdmission === undefined
|
...(options.targetAdmission === undefined
|
||||||
@@ -71,7 +82,8 @@ export const createGhidraDiagnostics = (
|
|||||||
ghidra_log_path: options.launch.ghidraLogPath,
|
ghidra_log_path: options.launch.ghidraLogPath,
|
||||||
script_log_path: options.launch.scriptLogPath,
|
script_log_path: options.launch.scriptLogPath,
|
||||||
process_id: options.launch.process.pid ?? null,
|
process_id: options.launch.process.pid ?? null,
|
||||||
...(options.launch.launchSupport === undefined
|
...(options.scope === "request" ||
|
||||||
|
options.launch.launchSupport === undefined
|
||||||
? {}
|
? {}
|
||||||
: {
|
: {
|
||||||
launch_support: options.launch.launchSupport.map((output) => ({
|
launch_support: options.launch.launchSupport.map((output) => ({
|
||||||
@@ -90,14 +102,18 @@ export const createGhidraDiagnostics = (
|
|||||||
stop_requested: options.snapshot.stopRequested === true,
|
stop_requested: options.snapshot.stopRequested === true,
|
||||||
diagnostic_truncated: options.snapshot.diagnosticTruncated === true,
|
diagnostic_truncated: options.snapshot.diagnosticTruncated === true,
|
||||||
output_incomplete: options.snapshot.outputIncomplete === true,
|
output_incomplete: options.snapshot.outputIncomplete === true,
|
||||||
stdout: ghidraDiagnosticStream(
|
...(options.scope === "request"
|
||||||
options.snapshot.stdout,
|
? {}
|
||||||
options.token,
|
: {
|
||||||
),
|
stdout: ghidraDiagnosticStream(
|
||||||
stderr: ghidraDiagnosticStream(
|
options.snapshot.stdout,
|
||||||
options.snapshot.stderr,
|
options.token,
|
||||||
options.token,
|
),
|
||||||
),
|
stderr: ghidraDiagnosticStream(
|
||||||
|
options.snapshot.stderr,
|
||||||
|
options.token,
|
||||||
|
),
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
...(options.failure_output === undefined
|
...(options.failure_output === undefined
|
||||||
? {}
|
? {}
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ import {
|
|||||||
} from "./GhidraProviderCapabilities.js";
|
} from "./GhidraProviderCapabilities.js";
|
||||||
import {
|
import {
|
||||||
GHIDRA_STARTUP_DEADLINE_ELAPSED,
|
GHIDRA_STARTUP_DEADLINE_ELAPSED,
|
||||||
|
isGhidraInputFailureCode,
|
||||||
type GhidraSessionError,
|
type GhidraSessionError,
|
||||||
} from "./GhidraSessionError.js";
|
} from "./GhidraSessionError.js";
|
||||||
import type { GhidraSessionInfo } from "./GhidraSessionValues.js";
|
import type { GhidraSessionInfo } from "./GhidraSessionValues.js";
|
||||||
@@ -1046,12 +1047,7 @@ const projectPrimarySessionError = (
|
|||||||
"Retry this search in literal mode or simplify the regex.",
|
"Retry this search in literal mode or simplify the regex.",
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (
|
if (failure.kind === "remote" && isGhidraInputFailureCode(failure.remoteCode))
|
||||||
failure.kind === "remote" &&
|
|
||||||
["invalid_request", "not_found", "ambiguous"].includes(
|
|
||||||
failure.remoteCode ?? "",
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return new AnalysisInputError(operation, context, [
|
return new AnalysisInputError(operation, context, [
|
||||||
{ path: [], reason: "invalid_value", message: failure.message },
|
{ path: [], reason: "invalid_value", message: failure.message },
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -7,6 +7,39 @@ import {
|
|||||||
bindGhidraSessionFailure,
|
bindGhidraSessionFailure,
|
||||||
} from "./GhidraSessionError.js";
|
} from "./GhidraSessionError.js";
|
||||||
import { GhidraLaunchError } from "./GhidraLauncher.js";
|
import { GhidraLaunchError } from "./GhidraLauncher.js";
|
||||||
|
import { createGhidraDiagnostics } from "./GhidraDiagnostics.js";
|
||||||
|
|
||||||
|
it.each(["decompile_failed", "future_remote_failure"])(
|
||||||
|
"retains complete session diagnostics for non-input remote code %s",
|
||||||
|
(remoteCode) => {
|
||||||
|
const factory = bindGhidraSessionFailure((scope) =>
|
||||||
|
createGhidraDiagnostics({
|
||||||
|
scope,
|
||||||
|
targetPath: "/selected/target",
|
||||||
|
targetSha256: "a".repeat(64),
|
||||||
|
transport: "unix-socket",
|
||||||
|
providerVersion: "12.1.4",
|
||||||
|
profileDigest: "b".repeat(64),
|
||||||
|
runtimeRoot: "/owned/runtime",
|
||||||
|
snapshot: {
|
||||||
|
stdout: { text: "engine diagnostic", bytes: 17, observedBytes: 17 },
|
||||||
|
stderr: { text: "", bytes: 0, observedBytes: 0 },
|
||||||
|
exitCode: null,
|
||||||
|
signal: null,
|
||||||
|
},
|
||||||
|
previous: {},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const failure = factory("remote", "Engine failed", undefined, {
|
||||||
|
remoteCode,
|
||||||
|
});
|
||||||
|
expect(failure.diagnostics).toMatchObject({
|
||||||
|
stdout: { text: "engine diagnostic" },
|
||||||
|
remote_code: remoteCode,
|
||||||
|
});
|
||||||
|
expect(failure.diagnostics).not.toHaveProperty("diagnostic_scope");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
it("reports truncated cyclic causes in serializable diagnostics", () => {
|
it("reports truncated cyclic causes in serializable diagnostics", () => {
|
||||||
const cause = new Error("cyclic launcher failure");
|
const cause = new Error("cyclic launcher failure");
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
import type { JsonValue } from "../domain/jsonValue.js";
|
import type { JsonValue } from "../domain/jsonValue.js";
|
||||||
import { DarwinProcessOwnershipInspectionError } from "../process/DarwinProcessRunTokenReader.js";
|
import { DarwinProcessOwnershipInspectionError } from "../process/DarwinProcessRunTokenReader.js";
|
||||||
import { boundedUtf8Text } from "../process/boundedUtf8Text.js";
|
import { boundedUtf8Text } from "../process/boundedUtf8Text.js";
|
||||||
|
import type { GhidraDiagnosticsScope } from "./GhidraDiagnostics.js";
|
||||||
import { GhidraLaunchSupportError } from "./GhidraJavaLaunch.js";
|
import { GhidraLaunchSupportError } from "./GhidraJavaLaunch.js";
|
||||||
import {
|
import {
|
||||||
GhidraLaunchError,
|
GhidraLaunchError,
|
||||||
@@ -184,10 +185,16 @@ const createGhidraSessionError = (failure: {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
/** Bind session-failure construction to a live, token-redacted diagnostic view. */
|
/** Bridge input failures share classification with their domain error projection. */
|
||||||
|
export const isGhidraInputFailureCode = (code: string | undefined): boolean =>
|
||||||
|
code === "invalid_request" || code === "not_found" || code === "ambiguous";
|
||||||
|
|
||||||
|
/** Bind session-failure construction to its causal diagnostic scope. */
|
||||||
export const bindGhidraSessionFailure =
|
export const bindGhidraSessionFailure =
|
||||||
(
|
(
|
||||||
diagnostics: () => Readonly<Record<string, JsonValue>>,
|
diagnostics: (
|
||||||
|
scope: GhidraDiagnosticsScope,
|
||||||
|
) => Readonly<Record<string, JsonValue>>,
|
||||||
redact: (value: string, incomplete?: boolean) => string = (value) => value,
|
redact: (value: string, incomplete?: boolean) => string = (value) => value,
|
||||||
) =>
|
) =>
|
||||||
(
|
(
|
||||||
@@ -203,7 +210,13 @@ export const bindGhidraSessionFailure =
|
|||||||
kind,
|
kind,
|
||||||
message: redact(message),
|
message: redact(message),
|
||||||
diagnostics: {
|
diagnostics: {
|
||||||
...diagnostics(),
|
...diagnostics(
|
||||||
|
kind === "remote" &&
|
||||||
|
(isGhidraInputFailureCode(options.remoteCode) ||
|
||||||
|
options.remoteCode === "invalid_function_name")
|
||||||
|
? "request"
|
||||||
|
: "session",
|
||||||
|
),
|
||||||
...(cause === undefined
|
...(cause === undefined
|
||||||
? {}
|
? {}
|
||||||
: { failure_cause: describeCause(cause, redact) }),
|
: { failure_cause: describeCause(cause, redact) }),
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ type FixtureMode =
|
|||||||
| "future_id"
|
| "future_id"
|
||||||
| "analysis_timeout"
|
| "analysis_timeout"
|
||||||
| "remote_error"
|
| "remote_error"
|
||||||
|
| "remote_startup_error"
|
||||||
| "shutdown_error"
|
| "shutdown_error"
|
||||||
| "invalid_shutdown_ack"
|
| "invalid_shutdown_ack"
|
||||||
| "hang_after_start"
|
| "hang_after_start"
|
||||||
|
|||||||
@@ -950,6 +950,71 @@ describe("GhidraClient captured source snapshot", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("scopes authenticated input-failure diagnostics to the request while retaining session output", async () => {
|
||||||
|
const client = clientFor(new FixtureLauncher("remote_error"));
|
||||||
|
|
||||||
|
const result = await client.callTool("list_procedures", {
|
||||||
|
document: null,
|
||||||
|
offset: 0,
|
||||||
|
limit: 500,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
ok: false,
|
||||||
|
error: {
|
||||||
|
kind: "remote",
|
||||||
|
remoteCode: "not_found",
|
||||||
|
diagnostics: {
|
||||||
|
diagnostic_scope: "request",
|
||||||
|
remote_code: "not_found",
|
||||||
|
remote_message: "Unknown Ghidra procedure name",
|
||||||
|
failure_output: {
|
||||||
|
stdout: { text: "Fixture request rejected: missing procedure\n" },
|
||||||
|
complete: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (result.ok) throw new Error("Expected remote lookup failure");
|
||||||
|
expect(result.error.diagnostics).not.toHaveProperty("stdout");
|
||||||
|
expect(result.error.diagnostics).not.toHaveProperty("stderr");
|
||||||
|
expect(result.error.diagnostics).not.toHaveProperty("launch_support");
|
||||||
|
expect(client.diagnostics()).toMatchObject({
|
||||||
|
stdout: {
|
||||||
|
text: "Fixture launcher startup output\nFixture request rejected: missing procedure\n",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(client.isReady()).toBe(true);
|
||||||
|
await expect(
|
||||||
|
client.callTool("list_strings", { document: null }),
|
||||||
|
).resolves.toMatchObject({ ok: true });
|
||||||
|
await client.close();
|
||||||
|
expect(client.diagnostics()).toMatchObject({
|
||||||
|
stdout: {
|
||||||
|
text: expect.stringContaining("Fixture launcher startup output"),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("retains launcher diagnostics when an input error rejects the startup handshake", async () => {
|
||||||
|
const client = clientFor(new FixtureLauncher("remote_startup_error"));
|
||||||
|
const result = await client.start();
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
ok: false,
|
||||||
|
error: {
|
||||||
|
kind: "remote",
|
||||||
|
remoteCode: "invalid_request",
|
||||||
|
diagnostics: {
|
||||||
|
stdout: {
|
||||||
|
text: expect.stringContaining("Fixture launcher startup output"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (result.ok) throw new Error("Expected startup failure");
|
||||||
|
expect(result.error.diagnostics).not.toHaveProperty("diagnostic_scope");
|
||||||
|
});
|
||||||
|
|
||||||
describe("GhidraClient", () => {
|
describe("GhidraClient", () => {
|
||||||
it("completes an exact, fragmented post-analysis handshake", async () => {
|
it("completes an exact, fragmented post-analysis handshake", async () => {
|
||||||
const launcher = new FixtureLauncher("fragmented");
|
const launcher = new FixtureLauncher("fragmented");
|
||||||
@@ -1051,28 +1116,6 @@ describe("GhidraClient", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("preserves an authenticated operation failure code and diagnostics", async () => {
|
|
||||||
const client = clientFor(new FixtureLauncher("remote_error"));
|
|
||||||
|
|
||||||
const result = await client.callTool("list_procedures", {
|
|
||||||
document: null,
|
|
||||||
offset: 0,
|
|
||||||
limit: 500,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result).toMatchObject({
|
|
||||||
ok: false,
|
|
||||||
error: {
|
|
||||||
kind: "remote",
|
|
||||||
remoteCode: "not_found",
|
|
||||||
diagnostics: {
|
|
||||||
remote_code: "not_found",
|
|
||||||
remote_message: "Unknown Ghidra procedure name",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
["wrong_identity", "protocol"],
|
["wrong_identity", "protocol"],
|
||||||
["malformed", "protocol"],
|
["malformed", "protocol"],
|
||||||
|
|||||||
Vendored
+32
-9
@@ -62,6 +62,11 @@ const cutIdentifierAtBudget = async () => {
|
|||||||
const writeThen = (stream, text, then) =>
|
const writeThen = (stream, text, then) =>
|
||||||
stream.write(text, () => setTimeout(then, 10));
|
stream.write(text, () => setTimeout(then, 10));
|
||||||
|
|
||||||
|
if (mode === "remote_error" || mode === "remote_startup_error")
|
||||||
|
await new Promise((resolve) =>
|
||||||
|
process.stdout.write("Fixture launcher startup output\n", resolve),
|
||||||
|
);
|
||||||
|
|
||||||
let endpointPublication;
|
let endpointPublication;
|
||||||
if (mode === "exit") process.exit(73);
|
if (mode === "exit") process.exit(73);
|
||||||
if (mode === "flood_silent") {
|
if (mode === "flood_silent") {
|
||||||
@@ -199,15 +204,20 @@ const handleRequest = (socket, server, request, state) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (state.mode === "remote_error" && request.method === "list_procedures") {
|
if (state.mode === "remote_error" && request.method === "list_procedures") {
|
||||||
socket.write(
|
writeThen(
|
||||||
`${JSON.stringify({
|
process.stdout,
|
||||||
id: request.id,
|
"Fixture request rejected: missing procedure\n",
|
||||||
ok: false,
|
() =>
|
||||||
error: {
|
socket.write(
|
||||||
code: "not_found",
|
`${JSON.stringify({
|
||||||
message: "Unknown Ghidra procedure name",
|
id: request.id,
|
||||||
},
|
ok: false,
|
||||||
})}\n`,
|
error: {
|
||||||
|
code: "not_found",
|
||||||
|
message: "Unknown Ghidra procedure name",
|
||||||
|
},
|
||||||
|
})}\n`,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -266,6 +276,19 @@ const handleRequest = (socket, server, request, state) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handlePing = (socket, request, state) => {
|
const handlePing = (socket, request, state) => {
|
||||||
|
if (state.mode === "remote_startup_error") {
|
||||||
|
socket.write(
|
||||||
|
`${JSON.stringify({
|
||||||
|
id: request.id,
|
||||||
|
ok: false,
|
||||||
|
error: {
|
||||||
|
code: "invalid_request",
|
||||||
|
message: "Fixture rejected startup handshake",
|
||||||
|
},
|
||||||
|
})}\n`,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
state.pings.value += 1;
|
state.pings.value += 1;
|
||||||
if (state.mode === "diagnostic_output" && state.pings.value === 1) {
|
if (state.mode === "diagnostic_output" && state.pings.value === 1) {
|
||||||
const retainedTokenPrefixBytes = Math.floor(state.token.length / 2);
|
const retainedTokenPrefixBytes = Math.floor(state.token.length / 2);
|
||||||
|
|||||||
Reference in new issue
Block a user