mirror of
https://github.com/morluto/rea.git
synced 2026-10-29 19:05:45 +00:00
Restore source-position-aware ambient aliases, scalar env/argv reads, candidate endpoint worker data, Chrome DOMSnapshot empty strings, and accumulated Ghidra LaunchSupport diagnostics. Align regression fixtures with declared current contracts, while preserving identity, provenance, cancellation, and cleanup checks. Assert workflow caveats independently at both established locations. Validation: original five source failures reproduced on main; 307 focused tests passed with one platform skip, 22 fixture follow-up tests and 22 workflow/MCP tests passed; full static and docs checks passed. Real public http-server CLI analyses preserve identical artifact identity, statistics, and 581-token selected output. Real Chromium confirms restored DOM form destinations; isolated real service-worker verification passes on main and the change. The complete local browser run retained an unclassified later marker timeout; current-head CI provides complete real-browser and recovery lane results. Native IDA/Ghidra execution and complete local suite execution are not claimed. Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Co-authored-by: N0zoM1z0 <161784452+N0zoM1z0@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
318 lines
10 KiB
TypeScript
318 lines
10 KiB
TypeScript
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
|
import type { JsonValue } from "../domain/jsonValue.js";
|
|
import { DarwinProcessOwnershipInspectionError } from "../process/DarwinProcessRunTokenReader.js";
|
|
import { boundedUtf8Text } from "../process/boundedUtf8Text.js";
|
|
import { GhidraLaunchSupportError } from "./GhidraJavaLaunch.js";
|
|
import {
|
|
GhidraLaunchError,
|
|
GHIDRA_LAUNCH_COMMAND_OUTPUT_BYTES,
|
|
} from "./GhidraLauncher.js";
|
|
|
|
const MAX_CAUSE_DEPTH = 3;
|
|
const MAX_CAUSE_MESSAGE_BYTES = 2 * 1024;
|
|
const MAX_CAUSE_FIELD_BYTES = 1024;
|
|
const MAX_COMPILER_STDERR_BYTES = 8 * 1024;
|
|
const MAX_COMMAND_OUTPUT_BYTES = 8 * 1024;
|
|
|
|
/** Stable failure phase for one private Ghidra headless session. */
|
|
export type GhidraSessionFailureKind =
|
|
| "cancelled"
|
|
| "timeout"
|
|
| "start"
|
|
| "process"
|
|
| "protocol"
|
|
| "remote"
|
|
| "analysis_timeout";
|
|
|
|
/** Additional structured context for a Ghidra session failure. */
|
|
export interface GhidraSessionErrorOptions extends ErrorOptions {
|
|
readonly timeoutMs?: number;
|
|
readonly remoteCode?: string;
|
|
readonly cleanupFailure?: AnalysisError;
|
|
}
|
|
|
|
/** Provider-owned failure with bounded, token-redacted local diagnostics. */
|
|
export class GhidraSessionError extends Error {
|
|
readonly diagnostics: Readonly<Record<string, JsonValue>>;
|
|
readonly timeoutMs: number | undefined;
|
|
readonly remoteCode: string | undefined;
|
|
readonly cleanupFailure: AnalysisError | undefined;
|
|
|
|
constructor(
|
|
readonly kind: GhidraSessionFailureKind,
|
|
message: string,
|
|
diagnostics: Readonly<Record<string, JsonValue>> = {},
|
|
options: GhidraSessionErrorOptions = {},
|
|
) {
|
|
super(message, options);
|
|
this.name = "GhidraSessionError";
|
|
this.diagnostics = structuredClone(diagnostics);
|
|
this.timeoutMs = options.timeoutMs;
|
|
this.remoteCode = options.remoteCode;
|
|
this.cleanupFailure = options.cleanupFailure;
|
|
}
|
|
}
|
|
|
|
/** Build a session failure while preserving bounded remote failure metadata. */
|
|
const createGhidraSessionError = (failure: {
|
|
readonly kind: GhidraSessionFailureKind;
|
|
readonly message: string;
|
|
readonly diagnostics: Readonly<Record<string, JsonValue>>;
|
|
readonly cause?: unknown;
|
|
readonly timeoutMs?: number;
|
|
readonly remoteCode?: string;
|
|
readonly redact: (value: string, incomplete?: boolean) => string;
|
|
}): GhidraSessionError =>
|
|
new GhidraSessionError(
|
|
failure.kind,
|
|
failure.message,
|
|
{
|
|
...failure.diagnostics,
|
|
failure_kind: failure.kind,
|
|
failure_message: failure.message,
|
|
...(failure.remoteCode === undefined
|
|
? {}
|
|
: {
|
|
remote_code: failure.redact(failure.remoteCode),
|
|
remote_message: failure.message,
|
|
}),
|
|
},
|
|
{
|
|
...(failure.cause === undefined ? {} : { cause: failure.cause }),
|
|
...(failure.timeoutMs === undefined
|
|
? {}
|
|
: { timeoutMs: failure.timeoutMs }),
|
|
...(failure.remoteCode === undefined
|
|
? {}
|
|
: { remoteCode: failure.remoteCode }),
|
|
},
|
|
);
|
|
|
|
/** Bind session-failure construction to a live, token-redacted diagnostic view. */
|
|
export const bindGhidraSessionFailure =
|
|
(
|
|
diagnostics: () => Readonly<Record<string, JsonValue>>,
|
|
redact: (value: string, incomplete?: boolean) => string = (value) => value,
|
|
) =>
|
|
(
|
|
kind: GhidraSessionFailureKind,
|
|
message: string,
|
|
cause?: unknown,
|
|
options: Pick<GhidraSessionErrorOptions, "timeoutMs" | "remoteCode"> = {},
|
|
): GhidraSessionError =>
|
|
createGhidraSessionError({
|
|
kind,
|
|
message: redact(message),
|
|
diagnostics: {
|
|
...diagnostics(),
|
|
...(cause === undefined
|
|
? {}
|
|
: { failure_cause: describeCause(cause, redact) }),
|
|
},
|
|
redact,
|
|
...(cause === undefined ? {} : { cause }),
|
|
...options,
|
|
});
|
|
|
|
const describeCause = (
|
|
cause: unknown,
|
|
redact: (value: string, incomplete?: boolean) => string,
|
|
depth = 0,
|
|
): JsonValue => {
|
|
if (cause instanceof Error) {
|
|
return describeErrorCause(cause, redact, depth);
|
|
}
|
|
if (typeof cause === "string") {
|
|
const message = boundedUtf8Text(redact(cause), MAX_CAUSE_MESSAGE_BYTES);
|
|
return {
|
|
type: "string",
|
|
message: message.text,
|
|
...(message.truncated ? { message_truncated: true } : {}),
|
|
};
|
|
}
|
|
if (cause === null || typeof cause === "boolean")
|
|
return { type: cause === null ? "null" : "boolean", value: cause };
|
|
if (typeof cause === "number" && Number.isFinite(cause))
|
|
return { type: "number", value: cause };
|
|
return { type: typeof cause };
|
|
};
|
|
|
|
const describeErrorCause = (
|
|
cause: Error,
|
|
redact: (value: string, incomplete?: boolean) => string,
|
|
depth: number,
|
|
): JsonValue => {
|
|
const message = boundedUtf8Text(
|
|
redact(cause.message),
|
|
MAX_CAUSE_MESSAGE_BYTES,
|
|
);
|
|
const name = boundedUtf8Text(redact(cause.name), MAX_CAUSE_FIELD_BYTES);
|
|
return {
|
|
name: name.text,
|
|
...(name.truncated ? { name_truncated: true } : {}),
|
|
message: message.text,
|
|
...(message.truncated ? { message_truncated: true } : {}),
|
|
...causeCode(cause, redact),
|
|
...causeSignal(cause, redact),
|
|
...causeCommandOutcome(cause, redact),
|
|
...causeCompilerOutcome(cause, redact),
|
|
...causeLaunchOutcome(cause, redact),
|
|
...nestedErrorCause(cause, redact, depth),
|
|
};
|
|
};
|
|
|
|
const causeCommandOutcome = (
|
|
cause: Error,
|
|
redact: (value: string, incomplete?: boolean) => string,
|
|
): Record<string, JsonValue> => {
|
|
if (!(cause instanceof GhidraLaunchSupportError)) return {};
|
|
const outcome = cause.outcome;
|
|
if (outcome === undefined) return {};
|
|
const stdout = boundedUtf8Text(
|
|
redact(outcome.stdout, outcome.outputTruncated !== false),
|
|
MAX_COMMAND_OUTPUT_BYTES,
|
|
);
|
|
const stderr = boundedUtf8Text(
|
|
redact(outcome.stderr, outcome.outputTruncated !== false),
|
|
MAX_COMMAND_OUTPUT_BYTES,
|
|
);
|
|
return {
|
|
command_outcome: {
|
|
stdout: stdout.text,
|
|
stderr: stderr.text,
|
|
stdoutBytes: outcome.stdoutBytes ?? Buffer.byteLength(outcome.stdout),
|
|
stderrBytes: outcome.stderrBytes ?? Buffer.byteLength(outcome.stderr),
|
|
stdoutTruncated: stdout.truncated,
|
|
stderrTruncated: stderr.truncated,
|
|
outputTruncated: outcome.outputTruncated,
|
|
killed: outcome.killed,
|
|
},
|
|
};
|
|
};
|
|
|
|
const causeCode = (
|
|
cause: Error,
|
|
redact: (value: string, incomplete?: boolean) => string,
|
|
): Record<string, JsonValue> => {
|
|
const code = "code" in cause ? cause.code : undefined;
|
|
if (typeof code === "string") {
|
|
const bounded = boundedUtf8Text(redact(code), MAX_CAUSE_FIELD_BYTES);
|
|
return {
|
|
code: bounded.text,
|
|
...(bounded.truncated ? { code_truncated: true } : {}),
|
|
};
|
|
}
|
|
if (typeof code === "number" && Number.isFinite(code)) return { code };
|
|
return {};
|
|
};
|
|
|
|
const causeSignal = (
|
|
cause: Error,
|
|
redact: (value: string, incomplete?: boolean) => string,
|
|
): Record<string, JsonValue> => {
|
|
const signal = "signal" in cause ? cause.signal : undefined;
|
|
if (typeof signal === "string") {
|
|
const bounded = boundedUtf8Text(redact(signal), MAX_CAUSE_FIELD_BYTES);
|
|
return {
|
|
signal: bounded.text,
|
|
...(bounded.truncated ? { signal_truncated: true } : {}),
|
|
};
|
|
}
|
|
if (signal === null) return { signal: null };
|
|
return {};
|
|
};
|
|
|
|
const causeCompilerOutcome = (
|
|
cause: Error,
|
|
redact: (value: string, incomplete?: boolean) => string,
|
|
): Record<string, JsonValue> => {
|
|
if (!(cause instanceof DarwinProcessOwnershipInspectionError)) return {};
|
|
const outcome = cause.compilerOutcome;
|
|
if (outcome === undefined) return {};
|
|
const stderr = boundedUtf8Text(
|
|
redact(outcome.stderr),
|
|
MAX_COMPILER_STDERR_BYTES,
|
|
);
|
|
const code = boundedCompilerField(outcome.code, redact);
|
|
const signal = boundedCompilerField(outcome.signal, redact);
|
|
return {
|
|
compiler_outcome: {
|
|
...outcome,
|
|
code: code.value,
|
|
signal: signal.value,
|
|
stderr: stderr.text,
|
|
stderrTruncated: outcome.stderrTruncated || stderr.truncated,
|
|
...(code.truncated ? { codeTruncated: true } : {}),
|
|
...(signal.truncated ? { signalTruncated: true } : {}),
|
|
},
|
|
};
|
|
};
|
|
|
|
const boundedCompilerField = (
|
|
value: string | number | null,
|
|
redact: (value: string, incomplete?: boolean) => string,
|
|
): { readonly value: string | number | null; readonly truncated: boolean } => {
|
|
if (typeof value !== "string") return { value, truncated: false };
|
|
const bounded = boundedUtf8Text(redact(value), MAX_CAUSE_FIELD_BYTES);
|
|
return { value: bounded.text, truncated: bounded.truncated };
|
|
};
|
|
|
|
/** Retain a launch failure's launcher command outcome in its cause envelope. */
|
|
const causeLaunchOutcome = (
|
|
cause: Error,
|
|
redact: (value: string, incomplete?: boolean) => string,
|
|
): Record<string, JsonValue> => {
|
|
if (!(cause instanceof GhidraLaunchError)) return {};
|
|
const launchSupport =
|
|
cause.launchSupport.length === 0
|
|
? {}
|
|
: {
|
|
launch_support: cause.launchSupport.map((output) => ({
|
|
...output,
|
|
stderr: redact(output.stderr),
|
|
})),
|
|
};
|
|
// A failed LaunchSupport helper reports its own command outcome in the
|
|
// nested cause; earlier helper warnings still belong to this launch.
|
|
if (cause.cause instanceof GhidraLaunchSupportError) return launchSupport;
|
|
const output = cause.capturedOutput;
|
|
if (output === undefined) return launchSupport;
|
|
const stdout = boundedUtf8Text(
|
|
redact(output.stdout),
|
|
GHIDRA_LAUNCH_COMMAND_OUTPUT_BYTES,
|
|
);
|
|
const stderr = boundedUtf8Text(
|
|
redact(output.stderr),
|
|
GHIDRA_LAUNCH_COMMAND_OUTPUT_BYTES,
|
|
);
|
|
const truncated = output.truncated || stdout.truncated || stderr.truncated;
|
|
return {
|
|
...launchSupport,
|
|
launch_outcome: {
|
|
stdout: stdout.text,
|
|
stderr: stderr.text,
|
|
truncated,
|
|
...(truncated
|
|
? {
|
|
stdout_bytes: Buffer.byteLength(output.stdout),
|
|
stderr_bytes: Buffer.byteLength(output.stderr),
|
|
}
|
|
: {}),
|
|
...(output.exit_code === undefined
|
|
? {}
|
|
: { exit_code: output.exit_code }),
|
|
...(output.signal === undefined ? {} : { signal: output.signal }),
|
|
},
|
|
};
|
|
};
|
|
|
|
const nestedErrorCause = (
|
|
cause: Error,
|
|
redact: (value: string, incomplete?: boolean) => string,
|
|
depth: number,
|
|
): Record<string, JsonValue> => {
|
|
if (!("cause" in cause)) return {};
|
|
if (depth >= MAX_CAUSE_DEPTH) return { cause_truncated: true };
|
|
return { cause: describeCause(cause.cause, redact, depth + 1) };
|
|
};
|