fix(settings): parse runtime launch seeds at the operation boundary

Startup no longer rejects a runtime-owned REA_ seed, so update_analysis_settings can replace it. Debugger construction keeps only the restart-only caps, Kuna stores the launch baseline rather than a cached override, and the advertised operations include GDB close, Rizin close, Kuna list_procedures, and the stable-file ChangeTime callers.
This commit is contained in:
Cursor Agent authored and morluto committed 2026-11-11 20:16:24 +08:00
1 parent 6ebd033dff
commit b9be864801
17 files changed
+452 -298

No files matched your search

@@ -1,8 +1,13 @@
import { launchAnalysisSetting } from "../config/analysisSettings.js";
import {
parseEnvironment,
parseEnvironmentValues,
type Environment,
} from "../config/environment.js";
import {
DEFAULT_GHIDRA_STARTUP_TIMEOUT_MS,
DEFAULT_HOPPER_STARTUP_TIMEOUT_MS,
} from "../domain/analysisResourceLimits.js";
import { ConfigurationError } from "../domain/configurationErrors.js";
import { err, ok, type Result } from "../domain/result.js";
import {
@@ -318,6 +323,56 @@ const validatedDetails = (
return ok(environmentDetails(environment, validated.value, unresolved));
};
const admittedTimeout = (
id: Parameters<typeof launchAnalysisSetting>[0],
environment: Readonly<Record<string, string | undefined>>,
fallback: number,
): number => {
const parsed = launchAnalysisSetting(id, environment);
return parsed.ok && parsed.value.value !== null
? parsed.value.value
: fallback;
};
const registrationTimeoutInputs = (
environment: Readonly<Record<string, string>>,
): Pick<
McpToolTimeoutInputs,
| "ghidraStartupTimeoutMs"
| "hopperStartupTimeoutMs"
| "ghidraDecompileTimeoutMs"
| "jebMcpTimeoutMs"
> => {
const decompile = launchAnalysisSetting(
"ghidra.decompile_timeout_seconds",
environment,
);
const jeb = launchAnalysisSetting("jeb.mcp_timeout_ms", environment);
return {
ghidraStartupTimeoutMs: admittedTimeout(
"ghidra.startup_timeout_ms",
environment,
DEFAULT_GHIDRA_STARTUP_TIMEOUT_MS,
),
hopperStartupTimeoutMs: admittedTimeout(
"hopper.startup_timeout_ms",
environment,
DEFAULT_HOPPER_STARTUP_TIMEOUT_MS,
),
...(decompile.ok &&
decompile.value.source === "setting" &&
decompile.value.value !== null
? { ghidraDecompileTimeoutMs: decompile.value.value * 1000 }
: {}),
...(jeb.ok &&
jeb.value.source === "setting" &&
jeb.value.value !== null &&
environment.REA_JEB_MCP_TIMEOUT_MS !== undefined
? { jebMcpTimeoutMs: jeb.value.value }
: {}),
};
};
const environmentDetails = (
environment: Readonly<Record<string, string>>,
configuration: Environment,
@@ -335,17 +390,7 @@ const environmentDetails = (
? undefined
: {
analysisProvider: configuration.REA_ANALYSIS_PROVIDER,
ghidraStartupTimeoutMs: configuration.REA_GHIDRA_STARTUP_TIMEOUT_MS,
...(configuration.REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS === undefined
? {}
: {
ghidraDecompileTimeoutMs:
configuration.REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS * 1000,
}),
hopperStartupTimeoutMs: configuration.REA_HOPPER_STARTUP_TIMEOUT_MS,
...(configuration.REA_JEB_MCP_TIMEOUT_MS === undefined
? {}
: { jebMcpTimeoutMs: configuration.REA_JEB_MCP_TIMEOUT_MS }),
...registrationTimeoutInputs(environment),
},
});
+46 -50
View File
@@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest";
import { launchAnalysisSetting } from "./config/analysisSettings.js";
import {
defaultHopperLauncherPath,
inspectNativeAnalysisConfiguration,
@@ -21,17 +22,31 @@ it("still rejects a NUL in an unresolved expression", () => {
});
it.each([
["REA_ASAR_MAX_LISTING_BYTES", "9007199254740992"],
["REA_GHIDRA_VALUE_FLOW_MAX_OPERATIONS", "0"],
["REA_GHIDRA_VALUE_FLOW_MAX_INPUTS_PER_OPERATION", "2147483648"],
["REA_GHIDRA_VALUE_FLOW_MAX_EDGES", "1.5"],
["REA_GHIDRA_DECOMPILE_MAX_PAYLOAD_MIB", "2048"],
])(
"rejects invalid %s at launch with its exact setting identity",
(setting, value) => {
["asar.max_listing_bytes", "REA_ASAR_MAX_LISTING_BYTES", "9007199254740992"],
[
"ghidra.value_flow_max_operations",
"REA_GHIDRA_VALUE_FLOW_MAX_OPERATIONS",
"0",
],
[
"ghidra.value_flow_max_inputs_per_operation",
"REA_GHIDRA_VALUE_FLOW_MAX_INPUTS_PER_OPERATION",
"2147483648",
],
["ghidra.value_flow_max_edges", "REA_GHIDRA_VALUE_FLOW_MAX_EDGES", "1.5"],
[
"ghidra.decompile_max_payload_mib",
"REA_GHIDRA_DECOMPILE_MAX_PAYLOAD_MIB",
"2048",
],
] as const)(
"defers invalid %s until the operation reads it",
(id, setting, value) => {
const parsed = parseConfig({ [setting]: value });
if (parsed.ok) throw new Error("Invalid resource policy accepted");
expect(parsed.error.settings).toContainEqual(
if (!parsed.ok) throw new Error("runtime seed rejected startup");
const boundary = launchAnalysisSetting(id, { [setting]: value });
if (boundary.ok) throw new Error("invalid runtime seed was admitted");
expect(boundary.error.settings).toContainEqual(
expect.objectContaining({ setting }),
);
},
@@ -200,7 +215,6 @@ describe("runtime configuration", () => {
});
expect(rejected.issues.map(({ setting }) => setting)).toEqual([
"REA_ANALYSIS_PROVIDER",
"REA_GHIDRA_STARTUP_TIMEOUT_MS",
]);
expect(
parseConfig({
@@ -377,22 +391,16 @@ describe("configuration failure diagnostics", () => {
);
it.each(["", "0", "-1", "1.5", "1e3", "invalid", "2147483648"])(
"rejects malformed Hopper startup deadline %j with its setting",
"defers a malformed Hopper startup deadline %j until the operation reads it",
(value) => {
const result = parseConfig({ REA_HOPPER_STARTUP_TIMEOUT_MS: value });
if (result.ok) throw new Error("Expected invalid startup deadline");
expect(projectAnalysisError(result.error)).toMatchObject({
code: "configuration_invalid",
category: "invalid_input",
details: {
settings: [
{
setting: "REA_HOPPER_STARTUP_TIMEOUT_MS",
constraint: expect.any(String),
},
],
},
if (!result.ok) throw new Error("runtime seed rejected startup");
expect(result.value.hopperStartupTimeoutMs).toBe(120_000);
expect(result.value.hopperStartupTimeoutSource).toBe("default");
const boundary = launchAnalysisSetting("hopper.startup_timeout_ms", {
REA_HOPPER_STARTUP_TIMEOUT_MS: value,
});
if (boundary.ok) throw new Error("invalid Hopper deadline was admitted");
},
);
@@ -411,19 +419,12 @@ describe("configuration failure diagnostics", () => {
"rejects malformed Ghidra startup deadline %j with its setting and constraint",
(value) => {
const result = parseConfig({ REA_GHIDRA_STARTUP_TIMEOUT_MS: value });
if (result.ok) throw new Error("Expected invalid startup deadline");
expect(projectAnalysisError(result.error)).toMatchObject({
code: "configuration_invalid",
category: "invalid_input",
details: {
settings: [
{
setting: "REA_GHIDRA_STARTUP_TIMEOUT_MS",
constraint: expect.any(String),
},
],
},
if (!result.ok) throw new Error("runtime seed rejected startup");
expect(result.value.ghidraStartupTimeoutMs).toBe(330_000);
const boundary = launchAnalysisSetting("ghidra.startup_timeout_ms", {
REA_GHIDRA_STARTUP_TIMEOUT_MS: value,
});
if (boundary.ok) throw new Error("invalid Ghidra deadline was admitted");
},
);
@@ -524,8 +525,8 @@ describe("JEB MCP timeout configuration", () => {
value: 900_000,
});
expect(
parseEnvironmentValues({ REA_JEB_MCP_TIMEOUT_MS: "900000" }),
).toMatchObject({ ok: true, value: { REA_JEB_MCP_TIMEOUT_MS: 900_000 } });
parseEnvironmentValues({ REA_JEB_MCP_TIMEOUT_MS: "900000" }).ok,
).toBe(true);
});
it.each(["", "0", "-1", "1.5", "1e3", "invalid", "2147483648"])(
@@ -641,18 +642,13 @@ describe("Ghidra decompile limit configuration", () => {
const result = parseConfig({
REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS: value,
});
if (result.ok) throw new Error("Expected invalid decompile limit");
expect(projectAnalysisError(result.error)).toMatchObject({
code: "configuration_invalid",
details: {
settings: [
{
setting: "REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS",
constraint: expect.any(String),
},
],
},
});
if (!result.ok) throw new Error("runtime seed rejected startup");
expect(result.value.ghidraDecompileTimeoutSeconds).toBeUndefined();
const boundary = launchAnalysisSetting(
"ghidra.decompile_timeout_seconds",
{ REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS: value },
);
if (boundary.ok) throw new Error("invalid decompile limit was admitted");
},
);
});
+12 -14
View File
@@ -49,7 +49,7 @@ it("keeps the 16 MiB ceiling when the setting is unset", () => {
);
const environment = parseEnvironment({});
if (!environment.ok) throw new Error("doctor rejected an unset ceiling");
expect(environment.value.REA_BRIDGE_MAX_REQUEST_BYTES).toBeUndefined();
expect(environment.ok).toBe(true);
});
it("accepts a caller-selected ceiling inside the floor and maximum", () => {
@@ -62,7 +62,7 @@ it("accepts a caller-selected ceiling inside the floor and maximum", () => {
[BRIDGE_MAX_REQUEST_BYTES_SETTING]: "8192",
});
if (!environment.ok) throw new Error("doctor rejected a valid ceiling");
expect(environment.value.REA_BRIDGE_MAX_REQUEST_BYTES).toBe(8192);
expect(environment.ok).toBe(true);
});
it("sets the maximum to the serialized string bound minus the framing LF", () => {
@@ -153,16 +153,14 @@ it.each([
const doctor = parseEnvironment({
[BRIDGE_MAX_REQUEST_BYTES_SETTING]: value,
});
if (startup.ok) throw new Error(`Startup accepted ${JSON.stringify(value)}`);
expect(startup.error._tag).toBe("ConfigurationError");
expect(startup.error.settings).toContainEqual({
setting: BRIDGE_MAX_REQUEST_BYTES_SETTING,
constraint: parsed.error.message,
});
if (doctor.ok) throw new Error(`Doctor accepted ${JSON.stringify(value)}`);
expect(doctor.error._tag).toBe("ConfigurationError");
expect(doctor.error.settings).toContainEqual({
setting: BRIDGE_MAX_REQUEST_BYTES_SETTING,
constraint: parsed.error.message,
});
if (!startup.ok)
throw new Error(`Startup rejected a runtime seed ${JSON.stringify(value)}`);
expect(startup.value.bridgeMaxRequestBytes).toEqual(
DEFAULT_BRIDGE_MAX_REQUEST,
);
if (!doctor.ok)
throw new Error(`Doctor rejected a runtime seed ${JSON.stringify(value)}`);
expect(() => parseBridgeMaxRequestBytes(value)).not.toThrow();
if (parseBridgeMaxRequestBytes(value).ok)
throw new Error(`Bridge parser accepted ${JSON.stringify(value)}`);
});
+12 -11
View File
@@ -21,6 +21,7 @@ import {
CAPTURE_DECODER_OLD_GENERATION_SETTING,
DEFAULT_CAPTURE_DECODER_HEAP,
parseCaptureDecoderHeap,
resolveCaptureDecoderHeap,
} from "./captureDecoderHeap.js";
import { MAXIMUM_CAPTURE_DECODER_OLD_GENERATION_MIB } from "../domain/analysisResourceLimits.js";
@@ -258,20 +259,20 @@ it.each([
const startup = parseEnvironment({
REA_CAPTURE_DECODER_OLD_GENERATION_MIB: value,
});
if (startup.ok) throw new Error(`Startup accepted ${JSON.stringify(value)}`);
expect(startup.error.settings).toContainEqual({
setting: CAPTURE_DECODER_OLD_GENERATION_SETTING,
constraint: parsed.error.message,
});
if (!startup.ok)
throw new Error(`Startup rejected a runtime seed ${JSON.stringify(value)}`);
const config = parseConfig({
REA_CAPTURE_DECODER_OLD_GENERATION_MIB: value,
});
if (config.ok)
throw new Error(`parseConfig accepted ${JSON.stringify(value)}`);
expect(projectAnalysisError(config.error)).toMatchObject({
code: "configuration_invalid",
message: expect.stringContaining("REA_CAPTURE_DECODER_OLD_GENERATION_MIB"),
});
if (!config.ok)
throw new Error(
`parseConfig rejected a runtime seed ${JSON.stringify(value)}`,
);
expect(() =>
resolveCaptureDecoderHeap({
REA_CAPTURE_DECODER_OLD_GENERATION_MIB: value,
}),
).toThrow(ConfigurationError);
expect(
() =>
new NodeCaptureAdapter("har", {
+18 -20
View File
@@ -139,11 +139,18 @@ it.each([
);
expect(detail.constraint).toContain(`default is ${defaultValue} ${unit}`);
const startup = parseEnvironment({ [setting]: value });
if (startup.ok) throw new Error(`${setting} was accepted at startup`);
expect(projectAnalysisError(startup.error)).toMatchObject({
code: "configuration_invalid",
details: { settings: [detail] },
});
const runtimeSeed =
setting === DEBUGGER_COMMAND_DEADLINE_SETTING ||
setting === DEBUGGER_MAX_COMMAND_OUTPUT_BYTES_SETTING;
if (runtimeSeed) {
if (!startup.ok) throw new Error(`${setting} was rejected at startup`);
} else {
if (startup.ok) throw new Error(`${setting} was accepted at startup`);
expect(projectAnalysisError(startup.error)).toMatchObject({
code: "configuration_invalid",
details: { settings: [detail] },
});
}
},
);
@@ -280,30 +287,21 @@ it("does not let one Rizin manager consume another manager's session cap", async
}
});
it("refuses an invalid GDB deadline before a session starts", () => {
it("defers an invalid GDB deadline until a command reads it", () => {
expect(
() =>
new GdbSessionManager({
environment: { [DEBUGGER_COMMAND_DEADLINE_SETTING]: "0" },
}),
).toThrow(ConfigurationError);
).not.toThrow();
const parsed = parseDebuggerSessionLimits({
[DEBUGGER_COMMAND_DEADLINE_SETTING]: "0",
});
if (parsed.ok) throw new Error("invalid deadline was accepted");
try {
new GdbSessionManager({
environment: { [DEBUGGER_COMMAND_DEADLINE_SETTING]: "0" },
});
} catch (error) {
if (!(error instanceof ConfigurationError)) throw error;
expect(projectAnalysisError(error)).toMatchObject({
code: "configuration_invalid",
details: {
settings: parsed.error.settings,
},
});
}
expect(projectAnalysisError(parsed.error)).toMatchObject({
code: "configuration_invalid",
details: { settings: parsed.error.settings },
});
expect(
resolveDebuggerSessionLimits({
[DEBUGGER_COMMAND_DEADLINE_SETTING]: "1500",
+27 -4
View File
@@ -133,14 +133,11 @@ export const debuggerMaxCommandOutputBytesSchema = positiveDecimalIntegerSchema(
);
/** Schemas startup validation spreads. The setting names are not restated. */
/** Restart-only debugger caps. Runtime deadline and output are parsed per call. */
export const debuggerSessionLimitEnvironmentShape = {
[DEBUGGER_COMMAND_DEADLINE_SETTING]:
debuggerCommandDeadlineMsSchema.optional(),
[DEBUGGER_MAX_SESSIONS_SETTING]: debuggerMaxSessionsSchema.optional(),
[DEBUGGER_MAX_PENDING_COMMANDS_SETTING]:
debuggerMaxPendingCommandsSchema.optional(),
[DEBUGGER_MAX_COMMAND_OUTPUT_BYTES_SETTING]:
debuggerMaxCommandOutputBytesSchema.optional(),
};
export interface DebuggerSessionLimits {
@@ -236,6 +233,32 @@ export const parseDebuggerSessionLimits = (
});
};
const RUNTIME_DEBUGGER_SETTINGS = new Set<string>([
DEBUGGER_COMMAND_DEADLINE_SETTING,
DEBUGGER_MAX_COMMAND_OUTPUT_BYTES_SETTING,
]);
/**
* Session and pending caps still fail construction. An invalid runtime deadline
* or output cap does not: the next command reads the snapshot or the launch seed.
*/
export const debuggerConstructionLimits = (
environment: Readonly<Record<string, string | undefined>>,
): DebuggerSessionLimits => {
const parsed = parseDebuggerSessionLimits(environment);
if (parsed.ok) return parsed.value;
const blockers = parsed.error.settings.filter(
(setting) => !RUNTIME_DEBUGGER_SETTINGS.has(setting.setting),
);
if (blockers.length > 0) throw parsed.error;
const startup = { ...environment };
delete startup[DEBUGGER_COMMAND_DEADLINE_SETTING];
delete startup[DEBUGGER_MAX_COMMAND_OUTPUT_BYTES_SETTING];
const again = parseDebuggerSessionLimits(startup);
if (!again.ok) throw again.error;
return again.value;
};
/** Refuse an invalid launch cap instead of falling back to the default. */
export const resolveDebuggerSessionLimits = (
environment: Readonly<Record<string, string | undefined>>,
-50
View File
@@ -19,34 +19,13 @@ import {
import { ghidraDisabledAnalyzersSchema } from "./ghidraDisabledAnalyzers.js";
import {
DEFAULT_SWIFTC_COMPILE_TIMEOUT_MS,
ghidraDecompileTimeoutSchema,
providerStartupTimeoutSchema,
} from "./providerStartupTimeout.js";
import {
DEFAULT_GHIDRA_STARTUP_TIMEOUT_MS,
DEFAULT_HOPPER_CALLER_SCAN_BUDGET_MS,
DEFAULT_HOPPER_STARTUP_TIMEOUT_MS,
} from "../domain/analysisResourceLimits.js";
import { mcpInputSchemaProfileSchema } from "./mcpInputSchemaProfile.js";
import { mcpOutputSchemaProfileSchema } from "./mcpOutputSchemaProfile.js";
import { mcpToolCatalogProfileSchema } from "./mcpToolCatalogProfile.js";
import { mcpResponseBudgetSchema } from "./mcpResponseBudget.js";
import { asarMaxHeaderBytesSchema } from "./asarHeaderAdmission.js";
import { asarMaxListingBytesSchema } from "./asarListingAdmission.js";
import {
ghidraValueFlowCountSchema,
ghidraDecompilePayloadMiBSchema,
} from "./ghidraValueFlowLimits.js";
import { bridgeMaxRequestBytesSchema } from "./bridgeMaxRequestBytes.js";
import { captureDecoderOldGenerationMiBSchema } from "./captureDecoderHeap.js";
import { debuggerSessionLimitEnvironmentShape } from "./debuggerSessionLimits.js";
import { regexMatchingDeadlineMsSchema } from "./regexMatchingDeadline.js";
import { regexWorkerHeapRequestMiBSchema } from "./regexWorkerHeapRequest.js";
import {
windowsChangeTimeDriftMsSchema,
windowsChangeTimeSettleMaxPollsSchema,
windowsChangeTimeSettlePollMsSchema,
} from "./windowsChangeTimeSettle.js";
import {
mcpDoctorDeadlineMsSchema,
mcpFirstCatalogBudgetMsSchema,
@@ -61,26 +40,8 @@ import { parseJebMcpEndpoint } from "./jebMcpEndpoint.js";
const environmentSchema = z.object({
...debuggerSessionLimitEnvironmentShape,
REA_ANALYSIS_PROVIDER: analysisProviderSelectorSchema.default("auto"),
REA_BRIDGE_MAX_REQUEST_BYTES: bridgeMaxRequestBytesSchema.optional(),
REA_MCP_MAX_RESPONSE_BYTES: mcpResponseBudgetSchema.optional(),
REA_MCP_MAX_INPUT_ELEMENTS: mcpInputBudgetSchema.optional(),
REA_ASAR_MAX_HEADER_BYTES: asarMaxHeaderBytesSchema.optional(),
REA_ASAR_MAX_LISTING_BYTES: asarMaxListingBytesSchema.optional(),
REA_GHIDRA_VALUE_FLOW_MAX_OPERATIONS: ghidraValueFlowCountSchema.optional(),
REA_GHIDRA_VALUE_FLOW_MAX_INPUTS_PER_OPERATION:
ghidraValueFlowCountSchema.optional(),
REA_GHIDRA_VALUE_FLOW_MAX_EDGES: ghidraValueFlowCountSchema.optional(),
REA_GHIDRA_DECOMPILE_MAX_PAYLOAD_MIB:
ghidraDecompilePayloadMiBSchema.optional(),
REA_CAPTURE_DECODER_OLD_GENERATION_MIB:
captureDecoderOldGenerationMiBSchema.optional(),
REA_REGEX_MATCHING_DEADLINE_MS: regexMatchingDeadlineMsSchema.optional(),
REA_REGEX_WORKER_HEAP_REQUEST_MIB: regexWorkerHeapRequestMiBSchema.optional(),
REA_WINDOWS_CHANGE_TIME_DRIFT_MS: windowsChangeTimeDriftMsSchema.optional(),
REA_WINDOWS_CHANGE_TIME_SETTLE_POLL_MS:
windowsChangeTimeSettlePollMsSchema.optional(),
REA_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS:
windowsChangeTimeSettleMaxPollsSchema.optional(),
REA_MCP_INITIALIZE_BUDGET_MS: mcpInitializeBudgetMsSchema.optional(),
REA_MCP_TOOLS_LIST_BUDGET_MS: mcpToolsListBudgetMsSchema.optional(),
REA_MCP_FIRST_CATALOG_BUDGET_MS: mcpFirstCatalogBudgetMsSchema.optional(),
@@ -94,7 +55,6 @@ const environmentSchema = z.object({
.min(1)
.refine(isAbsolute, "REA_IDA_MCP_CONFIG must be absolute")
.optional(),
REA_JEB_MCP_TIMEOUT_MS: providerStartupTimeoutSchema.optional(),
REA_JEB_MCP_URL: z
.string()
.superRefine((value, context) => {
@@ -124,19 +84,9 @@ const environmentSchema = z.object({
.min(1)
.refine(isAbsolute, "REA_GHIDRA_NATIVEAOT_JAR must be absolute")
.optional(),
REA_GHIDRA_STARTUP_TIMEOUT_MS: providerStartupTimeoutSchema.default(
DEFAULT_GHIDRA_STARTUP_TIMEOUT_MS,
),
REA_HOPPER_STARTUP_TIMEOUT_MS: providerStartupTimeoutSchema.default(
DEFAULT_HOPPER_STARTUP_TIMEOUT_MS,
),
REA_HOPPER_CALLER_SCAN_BUDGET_MS: providerStartupTimeoutSchema.default(
DEFAULT_HOPPER_CALLER_SCAN_BUDGET_MS,
),
REA_SWIFTC_COMPILE_TIMEOUT_MS: providerStartupTimeoutSchema.default(
DEFAULT_SWIFTC_COMPILE_TIMEOUT_MS,
),
REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS: ghidraDecompileTimeoutSchema.optional(),
REA_GHIDRA_PROJECT_PATH: ghidraProjectPathSchema.optional(),
REA_GHIDRA_CHECKPOINT_DIR: z
.string()
+61 -37
View File
@@ -5,9 +5,16 @@ import {
type ConfigurationSettingIssue,
} from "../domain/configurationErrors.js";
import { err, ok, type Result } from "../domain/result.js";
import { launchAnalysisSetting } from "./analysisSettings.js";
import type { AnalysisSettingId } from "../domain/analysisSettings.js";
import { parseEnvironment } from "./environment.js";
import { providerStartupTimeoutSchema } from "./providerStartupTimeout.js";
import { homeDirectoryFromEnvironment } from "./homeDirectory.js";
import {
DEFAULT_GHIDRA_STARTUP_TIMEOUT_MS,
DEFAULT_HOPPER_CALLER_SCAN_BUDGET_MS,
DEFAULT_HOPPER_STARTUP_TIMEOUT_MS,
} from "../domain/analysisResourceLimits.js";
import { DEFAULT_BRIDGE_MAX_REQUEST } from "./bridgeMaxRequestBytes.js";
import { hopperCallerScanBudgetFromEnvironment } from "./hopperCallerScanBudget.js";
import { DEFAULT_REGEX_MATCHING_DEADLINE } from "./regexMatchingDeadline.js";
@@ -70,12 +77,14 @@ export const inspectNativeAnalysisConfiguration = (
const issues = parsed.ok ? [] : parsed.error.settings;
const configuredTimeout = environment.REA_HOPPER_STARTUP_TIMEOUT_MS;
if (configuredTimeout === undefined) return { issues };
if (parsed.ok)
const admitted = launchAnalysisSetting(
"hopper.startup_timeout_ms",
environment,
);
if (parsed.ok && admitted.ok && admitted.value.value !== null)
return {
issues,
configuredHopperStartupTimeout: ok(
parsed.value.REA_HOPPER_STARTUP_TIMEOUT_MS,
),
configuredHopperStartupTimeout: ok(admitted.value.value),
};
// Another setting may reject the aggregate before this supplied timeout's
// facts are available. Its dedicated diagnostic still uses the same schema.
@@ -103,6 +112,23 @@ export const parseConfig = (
const parsed = parseEnvironment(nativeAnalysisEnvironment(environment));
if (!parsed.ok) return parsed;
const env = parsed.value;
const admitted = (id: AnalysisSettingId): number | null | undefined => {
const result = launchAnalysisSetting(id, environment);
return result.ok ? result.value.value : undefined;
};
const ghidraStartup = admitted("ghidra.startup_timeout_ms");
const hopperStartup = admitted("hopper.startup_timeout_ms");
const decompileTimeout = admitted("ghidra.decompile_timeout_seconds");
const decompilePayload = admitted("ghidra.decompile_max_payload_mib");
const valueFlowOperations = admitted("ghidra.value_flow_max_operations");
const valueFlowInputs = admitted(
"ghidra.value_flow_max_inputs_per_operation",
);
const valueFlowEdges = admitted("ghidra.value_flow_max_edges");
const callerScan = admitted("hopper.caller_scan_budget_ms");
const bridgeBytes = admitted("bridge.max_request_bytes");
const regexDeadline = admitted("regex.matching_deadline_ms");
const regexHeap = admitted("regex.worker_heap_request_mib");
return ok({
...(env.REA_MCP_TOOL_FAMILIES === undefined
? {}
@@ -141,34 +167,22 @@ export const parseConfig = (
? {}
: { ghidraCheckpointDirectory: env.REA_GHIDRA_CHECKPOINT_DIR }),
ghidraJavaHome: env.JAVA_HOME,
ghidraStartupTimeoutMs: env.REA_GHIDRA_STARTUP_TIMEOUT_MS,
...(env.REA_GHIDRA_DECOMPILE_MAX_PAYLOAD_MIB === undefined
ghidraStartupTimeoutMs: ghidraStartup ?? DEFAULT_GHIDRA_STARTUP_TIMEOUT_MS,
...(decompilePayload === undefined || decompilePayload === null
? {}
: {
ghidraDecompileMaxPayloadMiB:
env.REA_GHIDRA_DECOMPILE_MAX_PAYLOAD_MIB,
}),
...(env.REA_GHIDRA_VALUE_FLOW_MAX_OPERATIONS === undefined
: { ghidraDecompileMaxPayloadMiB: decompilePayload }),
...(valueFlowOperations === undefined || valueFlowOperations === null
? {}
: {
ghidraValueFlowMaxOperations:
env.REA_GHIDRA_VALUE_FLOW_MAX_OPERATIONS,
}),
...(env.REA_GHIDRA_VALUE_FLOW_MAX_INPUTS_PER_OPERATION === undefined
: { ghidraValueFlowMaxOperations: valueFlowOperations }),
...(valueFlowInputs === undefined || valueFlowInputs === null
? {}
: {
ghidraValueFlowMaxInputsPerOperation:
env.REA_GHIDRA_VALUE_FLOW_MAX_INPUTS_PER_OPERATION,
}),
...(env.REA_GHIDRA_VALUE_FLOW_MAX_EDGES === undefined
: { ghidraValueFlowMaxInputsPerOperation: valueFlowInputs }),
...(valueFlowEdges === undefined || valueFlowEdges === null
? {}
: { ghidraValueFlowMaxEdges: env.REA_GHIDRA_VALUE_FLOW_MAX_EDGES }),
...(env.REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS === undefined
: { ghidraValueFlowMaxEdges: valueFlowEdges }),
...(decompileTimeout === undefined || decompileTimeout === null
? {}
: {
ghidraDecompileTimeoutSeconds:
env.REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS,
}),
: { ghidraDecompileTimeoutSeconds: decompileTimeout }),
...(env.REA_GHIDRA_LANGUAGE_ID === undefined
? {}
: {
@@ -195,14 +209,18 @@ export const parseConfig = (
process.platform,
homeDirectoryFromEnvironment(environment, process.platform),
),
hopperStartupTimeoutMs: env.REA_HOPPER_STARTUP_TIMEOUT_MS,
hopperStartupTimeoutMs: hopperStartup ?? DEFAULT_HOPPER_STARTUP_TIMEOUT_MS,
hopperStartupTimeoutSource:
environment.REA_HOPPER_STARTUP_TIMEOUT_MS === undefined
environment.REA_HOPPER_STARTUP_TIMEOUT_MS === undefined ||
hopperStartup === undefined
? "default"
: "setting",
hopperCallerScanBudget: hopperCallerScanBudgetFromEnvironment(
environment.REA_HOPPER_CALLER_SCAN_BUDGET_MS,
env.REA_HOPPER_CALLER_SCAN_BUDGET_MS,
callerScan === undefined ||
environment.REA_HOPPER_CALLER_SCAN_BUDGET_MS === undefined
? undefined
: environment.REA_HOPPER_CALLER_SCAN_BUDGET_MS,
callerScan ?? DEFAULT_HOPPER_CALLER_SCAN_BUDGET_MS,
),
hopperTargetPath: env.HOPPER_TARGET_PATH,
hopperTargetKind: env.HOPPER_TARGET_KIND,
@@ -212,24 +230,30 @@ export const parseConfig = (
secretPatterns: env.referenceSecretPatterns,
},
bridgeMaxRequestBytes:
env.REA_BRIDGE_MAX_REQUEST_BYTES === undefined
bridgeBytes === undefined ||
bridgeBytes === null ||
environment.REA_BRIDGE_MAX_REQUEST_BYTES === undefined
? DEFAULT_BRIDGE_MAX_REQUEST
: {
maximumBytes: env.REA_BRIDGE_MAX_REQUEST_BYTES,
maximumBytes: bridgeBytes,
source: "setting",
},
regexMatchingDeadline:
env.REA_REGEX_MATCHING_DEADLINE_MS === undefined
regexDeadline === undefined ||
regexDeadline === null ||
environment.REA_REGEX_MATCHING_DEADLINE_MS === undefined
? DEFAULT_REGEX_MATCHING_DEADLINE
: {
deadlineMs: env.REA_REGEX_MATCHING_DEADLINE_MS,
deadlineMs: regexDeadline,
source: "setting",
},
regexWorkerHeapRequest:
env.REA_REGEX_WORKER_HEAP_REQUEST_MIB === undefined
regexHeap === undefined ||
regexHeap === null ||
environment.REA_REGEX_WORKER_HEAP_REQUEST_MIB === undefined
? DEFAULT_REGEX_WORKER_HEAP_REQUEST
: {
heapMiB: env.REA_REGEX_WORKER_HEAP_REQUEST_MIB,
heapMiB: regexHeap,
source: "setting",
},
});
+12 -13
View File
@@ -11,6 +11,7 @@ import { parseConfig } from "./parseConfig.js";
import {
DEFAULT_REGEX_MATCHING_DEADLINE,
parseRegexMatchingDeadline,
resolveRegexMatchingDeadline,
REGEX_MATCHING_DEADLINE_SETTING,
regexMatchingDeadlineContinuingRecovery,
regexMatchingDeadlineRecovery,
@@ -153,19 +154,17 @@ it.each([
},
]);
const startup = parseEnvironment({ REA_REGEX_MATCHING_DEADLINE_MS: value });
if (startup.ok) throw new Error(`Startup accepted ${JSON.stringify(value)}`);
expect(startup.error.settings).toContainEqual({
setting: REGEX_MATCHING_DEADLINE_SETTING,
constraint: parsed.error.message,
});
if (!startup.ok)
throw new Error(`Startup rejected a runtime seed ${JSON.stringify(value)}`);
const config = parseConfig({ REA_REGEX_MATCHING_DEADLINE_MS: value });
if (config.ok)
throw new Error(`parseConfig accepted ${JSON.stringify(value)}`);
expect(projectAnalysisError(config.error)).toMatchObject({
code: "configuration_invalid",
message: expect.stringContaining("REA_REGEX_MATCHING_DEADLINE_MS"),
});
expect(
() => new KunaProvider({ REA_REGEX_MATCHING_DEADLINE_MS: value }),
if (!config.ok)
throw new Error(
`parseConfig rejected a runtime seed ${JSON.stringify(value)}`,
);
expect(config.value.regexMatchingDeadline).toEqual(
DEFAULT_REGEX_MATCHING_DEADLINE,
);
expect(() =>
resolveRegexMatchingDeadline({ REA_REGEX_MATCHING_DEADLINE_MS: value }),
).toThrow(ConfigurationError);
});
+15 -11
View File
@@ -11,6 +11,7 @@ import { parseConfig } from "./parseConfig.js";
import {
DEFAULT_REGEX_WORKER_HEAP_REQUEST,
parseRegexWorkerHeapRequest,
resolveRegexWorkerHeapRequest,
REGEX_WORKER_HEAP_REQUEST_SETTING,
regexWorkerHeapRequestContinuingRecovery,
regexWorkerHeapRequestRecovery,
@@ -168,18 +169,21 @@ it.each([
const startup = parseEnvironment({
REA_REGEX_WORKER_HEAP_REQUEST_MIB: value,
});
if (startup.ok) throw new Error(`Startup accepted ${JSON.stringify(value)}`);
expect(startup.error.settings).toContainEqual({
setting: REGEX_WORKER_HEAP_REQUEST_SETTING,
constraint: parsed.error.message,
});
if (!startup.ok)
throw new Error(`Startup rejected a runtime seed ${JSON.stringify(value)}`);
const config = parseConfig({ REA_REGEX_WORKER_HEAP_REQUEST_MIB: value });
if (config.ok)
throw new Error(`parseConfig accepted ${JSON.stringify(value)}`);
expect(projectAnalysisError(config.error)).toMatchObject({
code: "configuration_invalid",
message: expect.stringContaining("REA_REGEX_WORKER_HEAP_REQUEST_MIB"),
});
if (!config.ok)
throw new Error(
`parseConfig rejected a runtime seed ${JSON.stringify(value)}`,
);
expect(config.value.regexWorkerHeapRequest).toEqual(
DEFAULT_REGEX_WORKER_HEAP_REQUEST,
);
expect(() =>
resolveRegexWorkerHeapRequest({
REA_REGEX_WORKER_HEAP_REQUEST_MIB: value,
}),
).toThrow(ConfigurationError);
expect(
() => new KunaProvider({ REA_REGEX_WORKER_HEAP_REQUEST_MIB: value }),
).toThrow(ConfigurationError);
+4 -14
View File
@@ -46,11 +46,7 @@ it("keeps today's settle defaults when the settings are unset", () => {
});
const startup = parseEnvironment({});
if (!startup.ok) throw new Error("unset settle settings were rejected");
expect(startup.value.REA_WINDOWS_CHANGE_TIME_DRIFT_MS).toBeUndefined();
expect(startup.value.REA_WINDOWS_CHANGE_TIME_SETTLE_POLL_MS).toBeUndefined();
expect(
startup.value.REA_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS,
).toBeUndefined();
expect(startup.ok).toBe(true);
});
it("uses valid settle overrides", () => {
@@ -77,9 +73,7 @@ it("uses valid settle overrides", () => {
REA_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS: "1000",
});
if (!startup.ok) throw new Error("valid settle settings were rejected");
expect(startup.value.REA_WINDOWS_CHANGE_TIME_DRIFT_MS).toBe(60_000);
expect(startup.value.REA_WINDOWS_CHANGE_TIME_SETTLE_POLL_MS).toBe(1);
expect(startup.value.REA_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS).toBe(1_000);
expect(startup.ok).toBe(true);
});
it.each([
@@ -113,12 +107,8 @@ it.each([
details: { settings: [facts] },
});
const startup = parseEnvironment({ [bound.setting]: value });
if (startup.ok) throw new Error(`Startup accepted ${bound.setting}=${value}`);
expect(startup.error.settings).toContainEqual(facts);
expect(startup.error.settings[0]).not.toHaveProperty("pending_restart");
expect(projectAnalysisError(startup.error).details).toMatchObject({
settings: [facts],
});
if (!startup.ok)
throw new Error(`Startup rejected runtime seed ${bound.setting}`);
});
it("applies a Windows settle override before rejecting the launch value", () => {
@@ -0,0 +1,39 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { expect, it } from "vitest";
import {
analysisSettingDefinition,
runtimeAnalysisEnvironmentNames,
} from "./analysisSettingDefinitions.js";
const source = (path: string) =>
readFileSync(join(import.meta.dirname, path), "utf8");
it("does not validate runtime-owned launch seeds at startup", () => {
const startup = [
source("../config/environment.ts"),
source("../config/debuggerSessionLimits.ts"),
].join("\n");
const mentioned = [...runtimeAnalysisEnvironmentNames()].filter(
(name) => startup.includes(`${name}:`) || startup.includes(`[${name}]`),
);
expect(mentioned).toEqual([]);
});
it("advertises the operations that consume each migrated setting", () => {
const operations = (id: Parameters<typeof analysisSettingDefinition>[0]) =>
analysisSettingDefinition(id).operations;
expect(operations("debugger.command_deadline_ms")).toEqual(
expect.arrayContaining(["close_gdb_session", "close_rizin_debug_session"]),
);
expect(operations("kuna.function_timeout_ms")).toContain("list_procedures");
expect(operations("windows.change_time_drift_ms")).toEqual(
expect.arrayContaining(["inspect_binary_layout", "inspect_recorded_crash"]),
);
expect(
operations("pwntools.max_input_bytes").includes("inspect_binary_layout"),
).toBe(true);
expect(analysisSettingDefinition("pwntools.max_input_bytes").parameter).toBe(
"producer_limits.input_bytes",
);
});
+35 -4
View File
@@ -406,8 +406,10 @@ export const ANALYSIS_SETTINGS = {
operations: [
"start_gdb_session",
"gdb_console",
"close_gdb_session",
"start_rizin_debug_session",
"rizin_debug_command",
"close_rizin_debug_session",
],
},
"debugger.max_command_output_bytes": {
@@ -472,6 +474,7 @@ export const ANALYSIS_SETTINGS = {
"procedure_pseudo_code",
"procedure_assembly",
"analyze_function",
"list_procedures",
],
},
"pwntools.max_input_bytes": {
@@ -483,7 +486,7 @@ export const ANALYSIS_SETTINGS = {
appliesAt: "next_operation",
provider: "pwntools",
operations: ["inspect_binary_layout", "inspect_recorded_crash"],
parameter: "input_bytes",
parameter: "producer_limits.input_bytes",
},
"windows.change_time_drift_ms": {
environment: "REA_WINDOWS_CHANGE_TIME_DRIFT_MS",
@@ -493,7 +496,13 @@ export const ANALYSIS_SETTINGS = {
unit: "ms",
appliesAt: "next_operation",
provider: "rea-artifact-graph",
operations: ["inventory_artifact", "inspect_artifact", "extract_artifact"],
operations: [
"inventory_artifact",
"inspect_artifact",
"extract_artifact",
"inspect_binary_layout",
"inspect_recorded_crash",
],
},
"windows.change_time_settle_poll_ms": {
environment: "REA_WINDOWS_CHANGE_TIME_SETTLE_POLL_MS",
@@ -503,7 +512,13 @@ export const ANALYSIS_SETTINGS = {
unit: "ms",
appliesAt: "next_operation",
provider: "rea-artifact-graph",
operations: ["inventory_artifact", "inspect_artifact", "extract_artifact"],
operations: [
"inventory_artifact",
"inspect_artifact",
"extract_artifact",
"inspect_binary_layout",
"inspect_recorded_crash",
],
},
"windows.change_time_settle_max_polls": {
environment: "REA_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS",
@@ -513,9 +528,25 @@ export const ANALYSIS_SETTINGS = {
unit: "count",
appliesAt: "next_operation",
provider: "rea-artifact-graph",
operations: ["inventory_artifact", "inspect_artifact", "extract_artifact"],
operations: [
"inventory_artifact",
"inspect_artifact",
"extract_artifact",
"inspect_binary_layout",
"inspect_recorded_crash",
],
},
} as const satisfies Record<AnalysisSettingId, SettingDefinition>;
/** Launch seeds `update_analysis_settings` can replace. Startup must not reject them. */
export const runtimeAnalysisEnvironmentNames = (): ReadonlySet<string> =>
new Set(
Object.values(ANALYSIS_SETTINGS).flatMap((definition) =>
definition.updateMode === "runtime" && "environment" in definition
? [definition.environment]
: [],
),
);
export const analysisSettingIds = Object.keys(
ANALYSIS_SETTINGS,
) as AnalysisSettingId[];
+21 -8
View File
@@ -32,7 +32,8 @@ import {
type SpawnedOwnedProviderProcess,
} from "../process/ProviderProcess.js";
import {
resolveDebuggerSessionLimits,
debuggerConstructionLimits,
parseDebuggerSessionLimits,
type DebuggerAdmissionLimit,
} from "../config/debuggerSessionLimits.js";
import {
@@ -617,9 +618,15 @@ export class GdbSessionManager {
currentAnalysisOperationSnapshot()?.overrides[
"debugger.command_deadline_ms"
];
return override === undefined
? this.#launchCommandDeadline
: { ...this.#launchCommandDeadline, value: override, source: "runtime" };
if (override !== undefined)
return {
...this.#launchCommandDeadline,
value: override,
source: "runtime",
};
const parsed = parseDebuggerSessionLimits(this.#environment);
if (!parsed.ok) throw parsed.error;
return parsed.value.commandDeadline;
}
get #commandTimeoutMs(): number {
return this.#commandDeadline.value;
@@ -629,9 +636,15 @@ export class GdbSessionManager {
currentAnalysisOperationSnapshot()?.overrides[
"debugger.max_command_output_bytes"
];
return override === undefined
? this.#launchCommandOutput
: { ...this.#launchCommandOutput, value: override, source: "runtime" };
if (override !== undefined)
return {
...this.#launchCommandOutput,
value: override,
source: "runtime",
};
const parsed = parseDebuggerSessionLimits(this.#environment);
if (!parsed.ok) throw parsed.error;
return parsed.value.maxCommandOutputBytes;
}
get #maxCommandOutputBytes(): number {
return this.#commandOutput.value;
@@ -664,7 +677,7 @@ export class GdbSessionManager {
) {
this.#environment = { ...(options.environment ?? process.env) };
this.#command = this.#environment.REA_GDB_COMMAND ?? "gdb";
const limits = resolveDebuggerSessionLimits(this.#environment);
const limits = debuggerConstructionLimits(this.#environment);
this.#launchCommandDeadline = limits.commandDeadline;
this.#startupTimeoutMs = options.startupTimeoutMs ?? STARTUP_TIMEOUT_MS;
this.#platform = options.platform ?? process.platform;
+69 -37
View File
@@ -48,6 +48,9 @@ export interface KunaConfiguration {
/** Deadline for one single-function `kuna decompile` run. */
readonly functionTimeoutMs: number;
readonly functionTimeoutSource: "default" | "setting" | "runtime";
/** False when the launch seed was invalid and only a runtime override admitted the open. */
readonly projectTimeoutLaunchValid?: boolean;
readonly functionTimeoutLaunchValid?: boolean;
readonly jobs: number;
}
@@ -59,38 +62,49 @@ export interface KunaDeadline {
}
/** Single-function decompiles and the standalone inventory. */
export const kunaFunctionDeadline = (
const kunaDeadline = (
configuration: KunaConfiguration,
setting: KunaDeadline["setting"],
overrideSetting: "kuna.function_timeout_ms" | "kuna.project_timeout_ms",
ms: number,
source: KunaDeadline["source"],
launchValid: boolean | undefined,
): KunaDeadline => {
const override =
currentAnalysisOperationSnapshot()?.overrides["kuna.function_timeout_ms"];
return override === undefined
? {
setting: "REA_KUNA_TIMEOUT_MS",
ms: configuration.functionTimeoutMs,
source: configuration.functionTimeoutSource,
}
: { setting: "REA_KUNA_TIMEOUT_MS", ms: override, source: "runtime" };
currentAnalysisOperationSnapshot()?.overrides[overrideSetting];
if (override !== undefined)
return { setting, ms: override, source: "runtime" };
if (launchValid === false)
throw new Error(
`${setting} is not a valid launch seed and no runtime override is active.`,
);
return { setting, ms, source };
};
export const kunaFunctionDeadline = (
configuration: KunaConfiguration,
): KunaDeadline =>
kunaDeadline(
configuration,
"REA_KUNA_TIMEOUT_MS",
"kuna.function_timeout_ms",
configuration.functionTimeoutMs,
configuration.functionTimeoutSource,
configuration.functionTimeoutLaunchValid,
);
/** The project export and whole-program string, graph and xref scans. */
export const kunaProjectDeadline = (
configuration: KunaConfiguration,
): KunaDeadline => {
const override =
currentAnalysisOperationSnapshot()?.overrides["kuna.project_timeout_ms"];
return override === undefined
? {
setting: "REA_KUNA_PROJECT_TIMEOUT_MS",
ms: configuration.projectTimeoutMs,
source: configuration.projectTimeoutSource,
}
: {
setting: "REA_KUNA_PROJECT_TIMEOUT_MS",
ms: override,
source: "runtime",
};
};
): KunaDeadline =>
kunaDeadline(
configuration,
"REA_KUNA_PROJECT_TIMEOUT_MS",
"kuna.project_timeout_ms",
configuration.projectTimeoutMs,
configuration.projectTimeoutSource,
configuration.projectTimeoutLaunchValid,
);
/** Why the configured environment cannot select a Kuna installation. */
export interface KunaConfigurationFailure {
@@ -130,26 +144,28 @@ const configuredTimeout = (
raw: string | undefined,
setting: string,
fallback: number,
override: number | undefined,
): Result<
{
readonly ms: number;
readonly source: "default" | "setting" | "runtime";
readonly source: "default" | "setting";
readonly launchValid: boolean;
},
KunaConfigurationFailure
> => {
if (raw === undefined)
return ok({
ms: override ?? fallback,
source: override === undefined ? "default" : "runtime",
});
return ok({ ms: fallback, source: "default", launchValid: true });
const parsed = providerStartupTimeoutSchema.safeParse(raw);
if (parsed.success)
return ok({
ms: override ?? parsed.data,
source: override === undefined ? "setting" : "runtime",
});
if (override !== undefined) return ok({ ms: override, source: "runtime" });
return ok({ ms: parsed.data, source: "setting", launchValid: true });
return ok({ ms: fallback, source: "default", launchValid: false });
};
const refuseInvalidLaunch = (
raw: string | undefined,
setting: string,
parsed: { readonly launchValid: boolean },
): Result<void, KunaConfigurationFailure> => {
if (parsed.launchValid) return ok(undefined);
return failure(
"open_options_invalid",
`${setting} must be a decimal millisecond count from 1 to 2147483647; received ${String(raw)}.`,
@@ -199,16 +215,30 @@ export const readKunaConfiguration = (
environment.REA_KUNA_PROJECT_TIMEOUT_MS,
"REA_KUNA_PROJECT_TIMEOUT_MS",
DEFAULT_PROJECT_TIMEOUT_MS,
overrides?.["kuna.project_timeout_ms"],
);
if (!projectTimeout.ok) return projectTimeout;
if (overrides?.["kuna.project_timeout_ms"] === undefined) {
const refused = refuseInvalidLaunch(
environment.REA_KUNA_PROJECT_TIMEOUT_MS,
"REA_KUNA_PROJECT_TIMEOUT_MS",
projectTimeout.value,
);
if (!refused.ok) return refused;
}
const functionTimeout = configuredTimeout(
environment.REA_KUNA_TIMEOUT_MS,
"REA_KUNA_TIMEOUT_MS",
DEFAULT_FUNCTION_TIMEOUT_MS,
overrides?.["kuna.function_timeout_ms"],
);
if (!functionTimeout.ok) return functionTimeout;
if (overrides?.["kuna.function_timeout_ms"] === undefined) {
const refused = refuseInvalidLaunch(
environment.REA_KUNA_TIMEOUT_MS,
"REA_KUNA_TIMEOUT_MS",
functionTimeout.value,
);
if (!refused.ok) return refused;
}
const rawJobs = environment.REA_KUNA_JOBS;
const jobs = rawJobs === undefined ? DEFAULT_JOBS : positiveInteger(rawJobs);
@@ -231,8 +261,10 @@ export const readKunaConfiguration = (
cacheMaxBytes,
projectTimeoutMs: projectTimeout.value.ms,
projectTimeoutSource: projectTimeout.value.source,
projectTimeoutLaunchValid: projectTimeout.value.launchValid,
functionTimeoutMs: functionTimeout.value.ms,
functionTimeoutSource: functionTimeout.value.source,
functionTimeoutLaunchValid: functionTimeout.value.launchValid,
jobs,
});
};
+21 -8
View File
@@ -37,7 +37,8 @@ import {
} from "../process/ProviderProcess.js";
import {
debuggerControlReplyOutput,
resolveDebuggerSessionLimits,
debuggerConstructionLimits,
parseDebuggerSessionLimits,
type DebuggerAdmissionLimit,
} from "../config/debuggerSessionLimits.js";
import {
@@ -285,7 +286,7 @@ export class RizinDebugSessionManager {
) {
this.#environment = { ...environment };
this.#command = environment.REA_RIZIN_COMMAND ?? "rizin";
const limits = resolveDebuggerSessionLimits(this.#environment);
const limits = debuggerConstructionLimits(this.#environment);
this.#launchCommandDeadline = limits.commandDeadline;
this.#startupTimeoutMs = options.startupTimeoutMs ?? STARTUP_TIMEOUT_MS;
this.#maxSessions = limits.maxSessions;
@@ -301,9 +302,15 @@ export class RizinDebugSessionManager {
currentAnalysisOperationSnapshot()?.overrides[
"debugger.command_deadline_ms"
];
return override === undefined
? this.#launchCommandDeadline
: { ...this.#launchCommandDeadline, value: override, source: "runtime" };
if (override !== undefined)
return {
...this.#launchCommandDeadline,
value: override,
source: "runtime",
};
const parsed = parseDebuggerSessionLimits(this.#environment);
if (!parsed.ok) throw parsed.error;
return parsed.value.commandDeadline;
}
get #frameTimeoutMs(): number {
return this.#commandDeadline.value;
@@ -313,9 +320,15 @@ export class RizinDebugSessionManager {
currentAnalysisOperationSnapshot()?.overrides[
"debugger.max_command_output_bytes"
];
return override === undefined
? this.#launchCommandOutput
: { ...this.#launchCommandOutput, value: override, source: "runtime" };
if (override !== undefined)
return {
...this.#launchCommandOutput,
value: override,
source: "runtime",
};
const parsed = parseDebuggerSessionLimits(this.#environment);
if (!parsed.ok) throw parsed.error;
return parsed.value.maxCommandOutputBytes;
}
get #maxCommandOutputBytes(): number {
return this.#commandOutput.value;
@@ -363,12 +363,10 @@ it("refuses invalid settings at startup validation and at each owner", async ()
},
]);
const startup = parseEnvironment({ REA_ASAR_MAX_HEADER_BYTES: value });
if (startup.ok)
throw new Error(`Startup accepted ${JSON.stringify(value)}`);
expect(startup.error.settings).toContainEqual({
setting: "REA_ASAR_MAX_HEADER_BYTES",
constraint: parsed.error.message,
});
if (!startup.ok)
throw new Error(
`Startup rejected a runtime seed ${JSON.stringify(value)}`,
);
}
// Owners refuse rather than falling back to the default.