mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 11:13:21 +00:00
feat(analysis): make resource budgets recoverable across CLI and MCP (#9706)
* refactor(analysis): centralize resource budgets and recovery facts * feat(analysis): expose ASAR and Ghidra runtime admission controls * feat(artifacts): expose reader admission and preserve limited recognition * feat(javascript): expose recoverable semantic precision budgets * feat(web): add recoverable source-map budgets and compact retained views * feat(web): make historical capture decoding budgets recoverable * fix(desktop): enforce selected view policies across capture and export * feat(runtime): make Frida and LLDB retention budgets selectable * feat(native): expose diagnostic budgets and preserve exhaustion causes * feat(managed): expose recoverable snapshot and representation budgets * feat(metadata): make Go and Rust inspection budgets recoverable * feat(wasm): expose representation and producer admissions * feat(evm): expose per-call worker resource budgets * feat(android): make resource and string-pool omissions recoverable * feat(producers): expose command and recorded-capture budgets * docs(analysis): record recovery contracts and integrated validation * fix(docs): link calibration oracle from published verification pages * fix(docs): restore desktop task anchors and receipt links * test(analysis): align fixtures with selectable budget contracts * fix(analysis): remove source-location projector dependency cycle * test(analysis): refresh budget recovery and verifier regressions * test(analysis): preserve format and publication recovery expectations * test(windows): align conformance with owned runtime admission * fix(desktop): preserve selected alias kinds in exported state * test(analysis): align provider fixtures with recoverable budgets * test(desktop): validate emitted alias metadata structurally * fix(ghidra): classify bounded Windows endpoint refusals * test(cli): assert selectable artifact admission recovery * fix(cli): preserve capture provenance in environment builds
This commit is contained in:
1 parent
55b6d2ca20
commit
cb4e5cfbff
515 files changed
+21066
-3637
No files matched your search
@@ -28,6 +28,8 @@ import { jsonValueSchema, type JsonValue } from "../../domain/jsonValue.js";
|
||||
import { err, ok, type Result } from "../../domain/result.js";
|
||||
import {
|
||||
DEFAULT_REVERSE_COMMAND_TIMEOUT_MS,
|
||||
DEFAULT_REVERSE_COMMAND_OUTPUT_BYTES,
|
||||
MAX_REVERSE_COMMAND_OUTPUT_BYTES,
|
||||
MAX_REVERSE_COMMAND_TIMEOUT_MS,
|
||||
objdumpInputSchema,
|
||||
rizinInputSchema,
|
||||
@@ -88,10 +90,16 @@ interface CommandLaunchOutcome {
|
||||
succeeded: number;
|
||||
}
|
||||
|
||||
const MAX_COMBINED_OUTPUT_BYTES = 16 * 1024 * 1024;
|
||||
export const REVERSE_COMMAND_OUTPUT_LIMITATION = `REA stopped collecting output at its ${String(MAX_COMBINED_OUTPUT_BYTES)}-byte cap and requested termination of the provider process; the captured stdout and stderr are a prefix and command completion is unknown. Narrow the command or page its output.`;
|
||||
// Eight concurrent captures bound retained provider output to 128 MiB.
|
||||
const reverseCommandOutputLimitation = (
|
||||
bytes: number,
|
||||
source: "default" | "request",
|
||||
): string =>
|
||||
`REA stopped collecting output at max_output_bytes=${String(bytes)} bytes (source ${source}, default ${String(DEFAULT_REVERSE_COMMAND_OUTPUT_BYTES)}, range 1..${String(MAX_REVERSE_COMMAND_OUTPUT_BYTES)}) and requested termination of the provider process; captured stdout and stderr are a prefix and command completion is unknown. Narrow or page the provider command, or increase max_output_bytes (--max-output-bytes on the CLI) for the next call; no restart is required. Confirm command effects and cleanup before deciding whether it is safe to repeat. Client receive limits are separate.`;
|
||||
// Bound simultaneous provider process families independently of output admission.
|
||||
const MAX_CONCURRENT_REVERSE_ENGINEERING_OPERATIONS = 8;
|
||||
const MAX_ACTIVE_CAPTURE_BYTES =
|
||||
MAX_CONCURRENT_REVERSE_ENGINEERING_OPERATIONS *
|
||||
DEFAULT_REVERSE_COMMAND_OUTPUT_BYTES;
|
||||
|
||||
export interface ReverseEngineeringServiceOptions {
|
||||
readonly environment?: Readonly<NodeJS.ProcessEnv>;
|
||||
@@ -140,6 +148,17 @@ export const resolveRizinCommandRequest = (
|
||||
);
|
||||
};
|
||||
|
||||
/** One accepted command, including the controls retained through admission and cleanup. */
|
||||
interface ReverseCommandObservation {
|
||||
readonly operation: ReverseEngineeringOperation;
|
||||
readonly profile: ReverseEngineeringProviderCommand;
|
||||
readonly path: string | undefined;
|
||||
readonly parameters: Readonly<Record<string, JsonValue>>;
|
||||
readonly signal: AbortSignal | undefined;
|
||||
readonly timeoutMs: number;
|
||||
readonly outputBudgetBytes: number;
|
||||
}
|
||||
|
||||
/** Shared application workflow for caller-selected local analysis commands. */
|
||||
export class ReverseEngineeringService {
|
||||
readonly #environment: Readonly<NodeJS.ProcessEnv>;
|
||||
@@ -159,6 +178,7 @@ export class ReverseEngineeringService {
|
||||
AbortController,
|
||||
Promise<Result<Evidence, AnalysisError>>
|
||||
>();
|
||||
#reservedOutputBytes = 0;
|
||||
#closing = false;
|
||||
#closePromise: Promise<void> | undefined;
|
||||
#dispatched = 0;
|
||||
@@ -297,6 +317,7 @@ export class ReverseEngineeringService {
|
||||
const {
|
||||
detail,
|
||||
command_timeout_ms: commandTimeoutMs,
|
||||
max_output_bytes: maxOutputBytes,
|
||||
...input
|
||||
} = parsed.value;
|
||||
if (detail === "summary") {
|
||||
@@ -314,14 +335,17 @@ export class ReverseEngineeringService {
|
||||
} else parameters = input;
|
||||
if (commandTimeoutMs !== undefined)
|
||||
parameters = { ...parameters, command_timeout_ms: commandTimeoutMs };
|
||||
const observed = await this.#observe(
|
||||
"inspect_with_objdump",
|
||||
command,
|
||||
input.operation === "architectures" ? undefined : input.path,
|
||||
if (maxOutputBytes !== undefined)
|
||||
parameters = { ...parameters, max_output_bytes: maxOutputBytes };
|
||||
const observed = await this.#observe({
|
||||
operation: "inspect_with_objdump",
|
||||
profile: command,
|
||||
path: input.operation === "architectures" ? undefined : input.path,
|
||||
parameters,
|
||||
options.signal,
|
||||
commandTimeoutMs ?? DEFAULT_REVERSE_COMMAND_TIMEOUT_MS,
|
||||
);
|
||||
signal: options.signal,
|
||||
timeoutMs: commandTimeoutMs ?? DEFAULT_REVERSE_COMMAND_TIMEOUT_MS,
|
||||
outputBudgetBytes: maxOutputBytes ?? DEFAULT_REVERSE_COMMAND_OUTPUT_BYTES,
|
||||
});
|
||||
return observed.ok && detail === "summary"
|
||||
? summarizeRetainedAnalysis(observed.value, retention)
|
||||
: observed;
|
||||
@@ -342,26 +366,27 @@ export class ReverseEngineeringService {
|
||||
request: RizinInput,
|
||||
options: { readonly signal?: AbortSignal } = {},
|
||||
): Promise<Result<Evidence, AnalysisError>> {
|
||||
const { command_timeout_ms: commandTimeoutMs, ...input } = request;
|
||||
const {
|
||||
command_timeout_ms: commandTimeoutMs,
|
||||
max_output_bytes: maxOutputBytes,
|
||||
...input
|
||||
} = request;
|
||||
const command = this.#commands.rizin(input);
|
||||
return this.#observe(
|
||||
"execute_rizin_command",
|
||||
command,
|
||||
input.path,
|
||||
commandTimeoutMs === undefined ? input : request,
|
||||
options.signal,
|
||||
commandTimeoutMs ?? DEFAULT_REVERSE_COMMAND_TIMEOUT_MS,
|
||||
);
|
||||
return this.#observe({
|
||||
operation: "execute_rizin_command",
|
||||
profile: command,
|
||||
path: input.path,
|
||||
parameters: request,
|
||||
signal: options.signal,
|
||||
timeoutMs: commandTimeoutMs ?? DEFAULT_REVERSE_COMMAND_TIMEOUT_MS,
|
||||
outputBudgetBytes: maxOutputBytes ?? DEFAULT_REVERSE_COMMAND_OUTPUT_BYTES,
|
||||
});
|
||||
}
|
||||
|
||||
#observe(
|
||||
operation: ReverseEngineeringOperation,
|
||||
profile: ReverseEngineeringProviderCommand,
|
||||
path: string | undefined,
|
||||
parameters: Readonly<Record<string, JsonValue>>,
|
||||
signal: AbortSignal | undefined,
|
||||
timeoutMs: number,
|
||||
request: ReverseCommandObservation,
|
||||
): Promise<Result<Evidence, AnalysisError>> {
|
||||
const { operation, signal, outputBudgetBytes } = request;
|
||||
if (signal?.aborted)
|
||||
return Promise.resolve(
|
||||
err(new AnalysisCancelledError(operation, { cause: signal.reason })),
|
||||
@@ -377,21 +402,26 @@ export class ReverseEngineeringService {
|
||||
);
|
||||
if (
|
||||
this.#activeOperations.size >=
|
||||
MAX_CONCURRENT_REVERSE_ENGINEERING_OPERATIONS
|
||||
MAX_CONCURRENT_REVERSE_ENGINEERING_OPERATIONS ||
|
||||
this.#reservedOutputBytes + outputBudgetBytes > MAX_ACTIVE_CAPTURE_BYTES
|
||||
)
|
||||
return Promise.resolve(
|
||||
err(
|
||||
new AnalysisResourceConstraintError(
|
||||
operation,
|
||||
"transport",
|
||||
`Concurrent reverse-engineering operation capacity reached (${String(MAX_CONCURRENT_REVERSE_ENGINEERING_OPERATIONS)} operations). This release has no runtime override.`,
|
||||
`Reverse-engineering capture capacity reached (${String(MAX_CONCURRENT_REVERSE_ENGINEERING_OPERATIONS)} operations or ${String(MAX_ACTIVE_CAPTURE_BYTES)} reserved output bytes).`,
|
||||
{
|
||||
max_concurrent_operations:
|
||||
MAX_CONCURRENT_REVERSE_ENGINEERING_OPERATIONS,
|
||||
active_operations: this.#activeOperations.size,
|
||||
max_active_capture_bytes: MAX_ACTIVE_CAPTURE_BYTES,
|
||||
reserved_output_bytes: this.#reservedOutputBytes,
|
||||
requested_output_bytes: outputBudgetBytes,
|
||||
},
|
||||
{
|
||||
remediationAction: `Wait for an active objdump or rizin command to finish, then retry. This capacity of ${String(MAX_CONCURRENT_REVERSE_ENGINEERING_OPERATIONS)} operations has no runtime override, and command_timeout_ms does not raise it.`,
|
||||
retryable: true,
|
||||
remediationAction: `Wait for active objdump or rizin work to finish, then retry, or lower max_output_bytes to fit the ${String(MAX_ACTIVE_CAPTURE_BYTES)}-byte aggregate capture admission. This live capacity has no runtime override; command_timeout_ms does not raise it. No restart is required.`,
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -406,30 +436,32 @@ export class ReverseEngineeringService {
|
||||
signal === undefined || signal instanceof AbortSignal
|
||||
? controller.signal
|
||||
: signal;
|
||||
const running = this.#observeProvider(
|
||||
operation,
|
||||
profile,
|
||||
path,
|
||||
parameters,
|
||||
operationSignal,
|
||||
timeoutMs,
|
||||
);
|
||||
this.#reservedOutputBytes += outputBudgetBytes;
|
||||
const running = this.#observeProvider({
|
||||
...request,
|
||||
signal: operationSignal,
|
||||
});
|
||||
const tracked = running.finally(() => {
|
||||
signal?.removeEventListener("abort", relayAbort);
|
||||
this.#activeOperations.delete(controller);
|
||||
this.#reservedOutputBytes -= outputBudgetBytes;
|
||||
});
|
||||
this.#activeOperations.set(controller, tracked);
|
||||
return tracked;
|
||||
}
|
||||
|
||||
async #observeProvider(
|
||||
operation: ReverseEngineeringOperation,
|
||||
profile: ReverseEngineeringProviderCommand,
|
||||
path: string | undefined,
|
||||
parameters: Readonly<Record<string, JsonValue>>,
|
||||
signal: AbortSignal | undefined,
|
||||
timeoutMs: number,
|
||||
request: ReverseCommandObservation,
|
||||
): Promise<Result<Evidence, AnalysisError>> {
|
||||
const {
|
||||
operation,
|
||||
profile,
|
||||
path,
|
||||
parameters,
|
||||
signal,
|
||||
timeoutMs,
|
||||
outputBudgetBytes,
|
||||
} = request;
|
||||
const { executable: command, arguments: args } = profile;
|
||||
if (signal?.aborted)
|
||||
return err(
|
||||
@@ -540,7 +572,7 @@ export class ReverseEngineeringService {
|
||||
signal,
|
||||
timeout: timeoutMs,
|
||||
stopSignal: "SIGTERM",
|
||||
maxBuffer: MAX_COMBINED_OUTPUT_BYTES,
|
||||
maxBuffer: outputBudgetBytes,
|
||||
});
|
||||
stdout = output.stdout;
|
||||
stderr = output.stderr;
|
||||
@@ -718,8 +750,6 @@ export class ReverseEngineeringService {
|
||||
: { cleanupOwner: darwinFailure.cleanupOwner }),
|
||||
});
|
||||
} else {
|
||||
const provider =
|
||||
operation === "inspect_with_objdump" ? "objdump" : "rizin";
|
||||
const setting =
|
||||
operation === "inspect_with_objdump"
|
||||
? "REA_OBJDUMP_COMMAND"
|
||||
@@ -994,11 +1024,15 @@ export class ReverseEngineeringService {
|
||||
: digestAfter === null || digest === undefined
|
||||
? null
|
||||
: digestAfter !== digest;
|
||||
const outputBudgetSource =
|
||||
parameters.max_output_bytes === undefined ? "default" : "request";
|
||||
const result = {
|
||||
stdout,
|
||||
stderr,
|
||||
exit_code: exitCode,
|
||||
signal: signalName,
|
||||
output_budget_bytes: outputBudgetBytes,
|
||||
output_budget_source: outputBudgetSource,
|
||||
output_truncated: outputTruncated,
|
||||
completion_status: completionStatus,
|
||||
artifact_sha256_after: absolutePath === undefined ? null : digestAfter,
|
||||
@@ -1043,7 +1077,12 @@ export class ReverseEngineeringService {
|
||||
]
|
||||
: []),
|
||||
...(outputLimitReached
|
||||
? [REVERSE_COMMAND_OUTPUT_LIMITATION]
|
||||
? [
|
||||
reverseCommandOutputLimitation(
|
||||
outputBudgetBytes,
|
||||
outputBudgetSource,
|
||||
),
|
||||
]
|
||||
: outputTruncated
|
||||
? [
|
||||
"Captured provider output is partial because bytes were omitted; process completion is reported separately.",
|
||||
@@ -1092,6 +1131,9 @@ export class ReverseEngineeringService {
|
||||
...(absolutePath === undefined
|
||||
? {
|
||||
operation: "architectures",
|
||||
...(typeof parameters.max_output_bytes === "number"
|
||||
? { max_output_bytes: parameters.max_output_bytes }
|
||||
: {}),
|
||||
...(typeof parameters.command_timeout_ms === "number"
|
||||
? { command_timeout_ms: parameters.command_timeout_ms }
|
||||
: {}),
|
||||
@@ -1212,7 +1254,8 @@ const runOwnedCommandOutput =
|
||||
},
|
||||
{
|
||||
timeoutMs: options.timeout ?? DEFAULT_REVERSE_COMMAND_TIMEOUT_MS,
|
||||
diagnosticBytes: MAX_COMBINED_OUTPUT_BYTES,
|
||||
diagnosticBytes:
|
||||
options.maxBuffer ?? DEFAULT_REVERSE_COMMAND_OUTPUT_BYTES,
|
||||
acceptNonZeroExit: true,
|
||||
},
|
||||
{ signal: options.signal },
|
||||
@@ -1349,7 +1392,7 @@ const reverseCommandTimeoutGuidance = (
|
||||
timeoutMs < MAX_REVERSE_COMMAND_TIMEOUT_MS
|
||||
? `Increase command_timeout_ms above ${String(timeoutMs)} (integer milliseconds from 1 to ${String(MAX_REVERSE_COMMAND_TIMEOUT_MS)}; omitted means ${String(DEFAULT_REVERSE_COMMAND_TIMEOUT_MS)}), or use --command-timeout-ms on the CLI.`
|
||||
: `command_timeout_ms is already at the maximum ${String(MAX_REVERSE_COMMAND_TIMEOUT_MS)} ms; no higher deadline is supported.`
|
||||
} The new value applies on the next call; no restart is required. The ${String(MAX_COMBINED_OUTPUT_BYTES)} byte (16 MiB) combined output cap has no runtime override. Allow execution, cleanup and delivery headroom in the outer MCP client deadline. Confirm outstanding work and cleanup before retrying.`,
|
||||
} The new value applies on the next call; no restart is required. The combined output allowance is separately selected by max_output_bytes (default ${String(DEFAULT_REVERSE_COMMAND_OUTPUT_BYTES)} bytes). Allow execution, cleanup and delivery headroom in the outer MCP client deadline. Confirm outstanding work and cleanup before retrying.`,
|
||||
});
|
||||
|
||||
export const digestFile = async (
|
||||
|
||||
Reference in new issue
Block a user