Compare commits

..
Author SHA1 Message Date
morluto 2eb48add14 fix(python): validate shared pyc header facts 2026-11-10 21:41:05 -05:00
118 changed files with 496 additions and 3003 deletions

No files matched your search

@@ -108,13 +108,11 @@ is not a recommendation to change it. Do not invent a setting. Host memory and
- Capacity, when the result is `resource_constraint` and `retryable` is true:
wait for the active admission to finish, then retry within the reported
limits. Do not restart REA to free the slot.
- `configuration_invalid`: when the error reports `update_mode: "runtime"`,
call `update_analysis_settings` for that setting. When it reports
`update_mode: "parameter"`, change that call's argument; the setter does not
accept it. Otherwise correct the named setting in the environment that starts
REA (the shell or the MCP registration) and start REA again. Run `rea doctor`
to see the `runtime-configuration:` check. Re-run `rea setup` for that client
when the registration must carry the new setting, then reconnect.
- `configuration_invalid`: correct the named setting in the environment that
starts REA (the shell or the MCP registration), then restart REA. Run
`rea doctor` to see the `runtime-configuration:` check. Re-run `rea setup`
for that client when the registration must carry the new setting, then
reconnect.
- `invalid_request`: correct the reported fields from the current schema. Do
not repeat the unchanged call.
@@ -536,7 +536,8 @@ the framing LF still fits. Cutter measures, signs, and writes a fitting
response in bounded chunks. The bridge searches only the newly read bytes
for the framing LF. The client unescapes each socket chunk into one response
string before parsing it, so this ceiling does not keep the paged wire frame
and another concatenated copy. Invalid values are configuration errors and do not fall back to 16777216. `bridge.max_request_bytes` can be changed at runtime with `update_analysis_settings`. The launch seed is `REA_BRIDGE_MAX_REQUEST_BYTES`. Start Hopper or Cutter from that same environment so the bridge keeps the ceiling it started with until that bridge starts again. An out-of-band bridge
and another concatenated copy. Invalid values are configuration errors and do not fall back to 16777216. Set the variable in the environment that starts REA, restart REA,
and start Hopper or Cutter from that same environment. An out-of-band bridge
without the setting stays at 16777216 bytes. The client reports that mismatch
during discovery only after the hello proves a live authenticated bridge, and
does not send a status request or the command. A stale descriptor is ignored.
-2
View File
@@ -1,7 +1,5 @@
# Changelog
The next release changes analysis-setting output (#9885). `update_analysis_settings`, `inspect_analysis_settings`, and limit errors no longer include `rea_restart_required`, `restart_required`, `restartRequired`, or `effect`. Read `update_mode` (`runtime`, `restart`, or `parameter`) and `pending_restart`. `runtime` means the setter can change the active value. `restart` means this process keeps the value it started with. `parameter` means this call supplied the value, so `update_analysis_settings` cannot change it; artifact reader limits, web-network capture budgets, and an explicit `analysis_timeout_ms`, `max_heap_mb`, or `command_timeout_ms` use it. There is no compatibility alias. Release Please should copy this migration into that release's breaking-changes section.
## [6.8.0](https://github.com/morluto/rea/compare/rea-agents-6.7.0...rea-agents-6.8.0) (2026-10-25)
### Highlights
-5
View File
@@ -358,11 +358,6 @@ export const documentationSections = [
source: "design/limits-and-recovery.md",
route: "/design/limits-and-recovery",
},
{
text: "REA limit inventory",
source: "design/rea-limit-inventory.md",
route: "/design/rea-limit-inventory",
},
{
text: "MCP tool design",
source: "development/tool-design.md",
-1
View File
@@ -111,7 +111,6 @@ product pages. The VitePress documentation source is this `docs/` tree.
## Development and verification
- [Settings and recovery](design/limits-and-recovery.md)
- [REA limit inventory](design/rea-limit-inventory.md)
- [Flutter budget calibration](verification/flutter-budget-calibration.md)
- [Source-map budget calibration](verification/source-map-budget-calibration.md)
- [Source-location delivery qualification](verification/source-location-delivery.md)
+13 -35
View File
@@ -5,9 +5,7 @@ who owns each value, when an update takes effect, how providers enforce it, and
how to extend the system. The public tool contract is in
[MCP contracts](../reference/mcp-contracts.md#editable-analysis-settings); recorded runtime
checks and their limitations are in the
[verification report](../verification/runtime-analysis-settings.md). The
[limit inventory](rea-limit-inventory.md) lists every numeric `REA_` limit, which
of them are runtime settings, and which stay restart-only.
[verification report](../verification/runtime-analysis-settings.md).
## Problem and design boundary
@@ -131,13 +129,12 @@ Windows ChangeTime admission waits out a fresh timestamp before recording a
baseline. `REA_WINDOWS_CHANGE_TIME_DRIFT_MS` (default 2000 ms, floor 1 ms,
maximum 60000 ms), `REA_WINDOWS_CHANGE_TIME_SETTLE_POLL_MS` (default 25 ms,
floor 1 ms, maximum 1000 ms), and `REA_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS`
(default 40, floor 1, maximum 1000) are runtime settings. Unset keeps those
defaults. An invalid launch value is a configuration error: it names the
setting, the rejected value, source `setting`, the default, the floor, the
maximum, `update_mode: "runtime"`, and `pending_restart: true`. Change the
active value with `update_analysis_settings`. The launch seed must be a
positive decimal integer in that range. Setup forwards an explicit selection.
When the poll budget runs out, admission keeps the last observation
(default 40, floor 1, maximum 1000) are launch settings. Unset keeps those
defaults. An invalid value is a configuration error: it names the setting, the
rejected value, source `setting`, the default, the floor, and the maximum, and
says to set a positive decimal integer in the environment that starts REA and
then restart REA. There is no runtime override. Setup forwards an explicit
selection. When the poll budget runs out, admission keeps the last observation
instead of failing the readable file. Raising a setting only changes how long
that wait can last. It does not change Windows ChangeTime itself.
@@ -258,8 +255,8 @@ are runtime-wide, not scoped to a target or an individual connection.
### Failure and retry
An ASAR admission error supplies `details.limit` with the exact setting,
effective value, units, bounds, scope, application point, `update_mode`,
`pending_restart` and, when known, required bytes and value source. The agent can immediately call:
effective value, units, bounds, scope, application point, restart requirement
and, when known, required bytes and value source. The agent can immediately call:
```json
{
@@ -276,27 +273,13 @@ that argument; raising a runtime default cannot override it.
The resource owner constructs `details.limit` from the registry plus the budget
it actually enforced. Metadata includes `agent_editable: true`,
`scope: "rea-runtime"`, `update_mode`, and `pending_restart`. `update_mode`
is `runtime` when `update_analysis_settings` can change the active value,
`restart` when this process keeps the value it started with, and `parameter`
when this call supplied the value. An explicit `analysis_timeout_ms`,
`max_heap_mb`, or `command_timeout_ms`, plus artifact reader limits and
web-network capture budgets, use `parameter`. `pending_restart` is false when
that reported value is the one this process is using. Optional
`scope: "rea-runtime"`, and `rea_restart_required: false`. Optional
`required_value`, `source`, and `parameter` fields are supplied only where known
or applicable. Do not infer a source because a value happens to equal a default.
Error copying, attribution, redaction, and delivery must preserve these facts.
Human-readable remediation explains the same failure; clients should not need
to parse that sentence to identify an editable setting.
This output is breaking. Results and errors no longer carry the previous
restart boolean or a single effect field, and there is no compatibility alias.
Read `update_mode` to see whether the setter can change the active value, and
`pending_restart` to see whether a configured value is waiting. A configuration
error for a Windows ChangeTime setting carries those two fields on
`details.settings`, not only in the constraint sentence. The removed field names
are listed in the changelog.
`scope` and `applies_at` describe a continuing caller, such as an MCP
connection. A one-shot invocation, such as a CLI command, has released its
provider session and exited its runtime before its caller reads the failure,
@@ -305,10 +288,8 @@ so its adapter projects with an explicit `"invocation"` caller lifetime.
`scope: "rea-invocation"`, `applies_at: "next_invocation"` and the launch
`environment` seed when one exists. `agent_editable: true` still holds, because
the caller chooses that environment value or per-call `parameter` for the new
invocation; `update_mode: "runtime"` and `pending_restart: false` mean the
reported value is active in this process. `update_mode: "restart"` means
`update_analysis_settings` does not change it. `update_mode: "parameter"` means
this call supplied the value. A producer keeps guidance that holds only while its caller retains the
invocation; `rea_restart_required: false` means no running REA process needs a
restart. A producer keeps guidance that holds only while its caller retains the
session, such as "the Ghidra session remains usable" or a same-session runtime
update, in `continuingCallerGuidance`; the shared presentation adds it only for
a continuing caller. The producer's own message and remediation must stay true
@@ -518,10 +499,7 @@ own `expanded_bytes` controls its independently enforced bundle expansion.
Failures preserve `details.reader_limit` with the exact parameter, effective
value, units, supported range, required value when known, next-call scope and
`can_increase`. `update_mode` is `parameter`: the caller selects the value on
the next call, and `update_analysis_settings` does not accept it.
`pending_restart` is false. Web-network capture budgets use the same mode.
When `can_increase` is true, raise that field and retry the same
`can_increase`. When `can_increase` is true, raise that field and retry the same
complete artifact. At the supported maximum, or when the required representation
exceeds it, the failure is not retryable by increasing that budget; inspect the
declared dimensions and input validity, or explicitly choose a suitable reader.
-230
View File
@@ -1,230 +0,0 @@
# REA limit inventory
Audit for [#9881](https://github.com/morluto/rea/issues/9881). Counts below are
from this revision of `src`. A numeric `REA_` limit is a name under `src` (tests
excluded) that carries a byte, time, count, heap, budget, deadline, or maximum.
`scripts/verify/rea-numeric-limits.mjs` fails the static checks when one of those
names is neither an environment seed of
[`ANALYSIS_SETTINGS`](https://github.com/morluto/rea/blob/main/src/domain/analysisSettingDefinitions.ts) nor an
entry in [`numericLimitRegistry.ts`](https://github.com/morluto/rea/blob/main/src/domain/numericLimitRegistry.ts)
with a reason.
`update_mode` is `runtime` when `update_analysis_settings` can change the active
value, `restart` when this process keeps the value it started with, and
`parameter` when this call supplied the value.
`pending_restart` is true only when a configured value is not the one this
process is using. A rejected Windows ChangeTime launch value is not active, so
that failure reports `pending_restart: true`. Every other value reported here
is the one in force, so `pending_restart` is false. Prose that only says to
restart REA is marked `prose`. `mcp_server_restart_required` is a
server-identity alignment state in `src/serverIdentity.ts`, not a setting field.
Three numeric limits are absent from this tree and are being moved into the
runtime system on their own PRs. This audit does not change them:
| Name | Tracking |
| -------------------------------------------- | -------- |
| `REA_NTFS_MFT_MAX_FILE_BYTES` | #9868 |
| `REA_REGF_HIVE_MAX_FILE_BYTES` | #9865 |
| `REA_WEB_BUNDLE_ENDPOINT_LITERAL_CANDIDATES` | #9836 |
When those names land in `src`, the static check requires them to be analysis
settings. They are not on the restart-only list.
## Counts
| Category | Count |
| --------------------------------------------------------------------- | ---------------------------------------------------------------------------: |
| Runtime analysis settings | 28 (25 have an `REA_` seed; 3 are per-call parameters only) |
| Per-call numeric `REA_` limits still read from the launch environment | 0 |
| Restart-only numeric `REA_` limits | 15 |
| Numeric `REA_` names discovered in `src` | 40 |
| Non-numeric `REA_` settings kept restart-only | 53 caller settings, plus process-protocol names that are not caller settings |
| Hardcoded ceilings left for follow-up | grouped below; none converted in the inventory change |
## Runtime analysis settings
Already adjustable with `update_analysis_settings`. Each call reads the snapshot
accepted for that operation. Launch source is `default` or `setting`; a runtime
override is `source: "runtime"`. The structured field is `update_mode: "runtime"` and `pending_restart: false`.
`applies_at` is the finer point
(`next_operation`, `next_worker`, or `next_startup`).
| Setting | Environment seed | Owner | Default | Floor | Maximum | Read |
| -------------------------------------------- | ------------------------------------------------ | ------------------------------------------------------------- | ----------------------------------------- | ---------------: | -------------------------------------: | --------------------- |
| `asar.max_header_bytes` | `REA_ASAR_MAX_HEADER_BYTES` | `src/domain/analysisResourceLimits.ts` | 16777216 | 1 | 268435456 | per operation |
| `asar.max_listing_bytes` | `REA_ASAR_MAX_LISTING_BYTES` | `src/domain/analysisResourceLimits.ts` | 67108864 | 1 | `Number.MAX_SAFE_INTEGER` | per operation |
| `ghidra.value_flow_max_operations` | `REA_GHIDRA_VALUE_FLOW_MAX_OPERATIONS` | `src/domain/analysisResourceLimits.ts` | 3000 | 1 | 2147483647 | per operation |
| `ghidra.value_flow_max_inputs_per_operation` | `REA_GHIDRA_VALUE_FLOW_MAX_INPUTS_PER_OPERATION` | `src/domain/analysisResourceLimits.ts` | 64 | 1 | 2147483647 | per operation |
| `ghidra.value_flow_max_edges` | `REA_GHIDRA_VALUE_FLOW_MAX_EDGES` | `src/domain/analysisResourceLimits.ts` | 12000 | 1 | 2147483647 | per operation |
| `ghidra.decompile_max_payload_mib` | `REA_GHIDRA_DECOMPILE_MAX_PAYLOAD_MIB` | `src/domain/analysisResourceLimits.ts` | unset (`null`, unbounded payload policy) | 1 | 2047 | per operation |
| `ghidra.startup_timeout_ms` | `REA_GHIDRA_STARTUP_TIMEOUT_MS` | `src/domain/analysisResourceLimits.ts` | 330000 | 1 | 2147483647 | next provider startup |
| `hopper.startup_timeout_ms` | `REA_HOPPER_STARTUP_TIMEOUT_MS` | `src/domain/analysisResourceLimits.ts` | 120000 | 1 | 2147483647 | next provider startup |
| `ghidra.decompile_timeout_seconds` | `REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS` | `src/domain/analysisResourceLimits.ts` | unset (`null`, decompile stays unbounded) | 1 | `MAX_GHIDRA_DECOMPILE_TIMEOUT_SECONDS` | per operation |
| `jadx.operation_timeout_ms` | `REA_JADX_OPERATION_TIMEOUT_MS` | `src/domain/analysisResourceLimits.ts` | 120000 | 1 | 2147483647 | per operation |
| `regex.matching_deadline_ms` | `REA_REGEX_MATCHING_DEADLINE_MS` | `src/domain/analysisResourceLimits.ts` | 5000 | 1 | 2147483647 | per operation |
| `regex.worker_heap_request_mib` | `REA_REGEX_WORKER_HEAP_REQUEST_MIB` | `src/domain/analysisResourceLimits.ts` | 128 | 7 | 16384 | next worker |
| `capture.decoder_old_generation_mib` | `REA_CAPTURE_DECODER_OLD_GENERATION_MIB` | `src/domain/analysisResourceLimits.ts` | 192 | semi-space floor | 16384 | next worker |
| `javascript.analysis_timeout_ms` | none (`analysis_timeout_ms`) | `src/domain/javascript/javascriptAnalysisResourceControls.ts` | owner default | 1 | 2147483647 | per operation |
| `javascript.max_heap_mb` | none (`max_heap_mb`) | `src/domain/javascript/javascriptAnalysisResourceControls.ts` | owner default | owner floor | owner maximum | next worker |
| `wasm.command_timeout_ms` | none (`command_timeout_ms`) | `src/domain/wasm/wasmArtifact.ts` | owner default | 1 | owner maximum | per operation |
| `native.matching_representation_bytes` | `REA_NATIVE_MATCHING_REPRESENTATION_BYTES` | `src/domain/analysisResourceLimits.ts` | 100663296 | 1 | 1073741824 | per operation |
An out-of-range `update_analysis_settings` value is rejected by the setting's
schema. Invalid launch seeds that the global environment parser accepts as
provider-local stay inspectable with `source: "invalid"`.
## Per-call limits in the runtime system
These are checked when the work runs. Each keeps its `REA_` seed, default,
floor, and maximum. The accepted operation snapshot supplies the value.
`update_mode` is `runtime` and `pending_restart` is false for the value in
force. An update outside the range is rejected. A hit names the setting and
reports the current value, source, and range. A rejected Windows ChangeTime
launch value is not active, so that failure reports `pending_restart: true`.
| Environment | Owner | Default | Floor | Maximum | Applies | Fields |
| ------------------------------------------ | ------------------------------------------- | ----------------------: | ----: | ---------: | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------ |
| `REA_BRIDGE_MAX_REQUEST_BYTES` | `src/config/bridgeMaxRequestBytes.ts` | 16777216 | 7464 | 536870887 | each bridge request, from the accepted operation snapshot | `update_mode: "runtime"`, `pending_restart: false` |
| `REA_DEBUGGER_COMMAND_DEADLINE_MS` | `src/domain/analysisResourceLimits.ts` | 120000 | 1 | 2147483647 | each command, from the accepted operation snapshot | `update_mode: "runtime"`, `pending_restart: false` |
| `REA_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES` | `src/domain/analysisResourceLimits.ts` | 16777216 | 1 | 89303718 | each command, from the accepted operation snapshot | `update_mode: "runtime"`, `pending_restart: false` |
| `REA_HOPPER_CALLER_SCAN_BUDGET_MS` | `src/domain/analysisResourceLimits.ts` | 2000 | 1 | 2147483647 | each caller scan, from the accepted operation snapshot | `update_mode: "runtime"`, `pending_restart: false` |
| `REA_JEB_MCP_TIMEOUT_MS` | `src/jeb/JebMcpConnection.ts` | 60000 (MCP SDK default) | 1 | 2147483647 | discovery and each tools/call, from the accepted operation snapshot | `update_mode: "runtime"`, `pending_restart: false` |
| `REA_KUNA_PROJECT_TIMEOUT_MS` | `src/kuna/KunaConfiguration.ts` | 1800000 | 1 | 2147483647 | each project export and whole-program scan, from the accepted operation snapshot | `update_mode: "runtime"`, `pending_restart: false` |
| `REA_KUNA_TIMEOUT_MS` | `src/kuna/KunaConfiguration.ts` | 120000 | 1 | 2147483647 | each function decompile, from the accepted operation snapshot | `update_mode: "runtime"`, `pending_restart: false` |
| `REA_PWNTOOLS_MAX_INPUT_BYTES` | `src/native/pwntools/PwntoolsInputLimit.ts` | 33554432 | 1 | 1073741824 | each pwntools decode, unless the call passes `producer_limits.input_bytes`; otherwise the operation snapshot | `update_mode: "runtime"`, `pending_restart: false` |
| `REA_WINDOWS_CHANGE_TIME_DRIFT_MS` | `src/config/windowsChangeTimeSettle.ts` | 2000 | 1 | 60000 | each ChangeTime observation, from the accepted operation snapshot | `update_mode: "runtime"`; invalid launch reports `pending_restart: true` |
| `REA_WINDOWS_CHANGE_TIME_SETTLE_POLL_MS` | `src/config/windowsChangeTimeSettle.ts` | 25 | 1 | 1000 | each ChangeTime observation, from the accepted operation snapshot | `update_mode: "runtime"`; invalid launch reports `pending_restart: true` |
| `REA_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS` | `src/config/windowsChangeTimeSettle.ts` | 40 | 1 | 1000 | each ChangeTime observation, from the accepted operation snapshot | `update_mode: "runtime"`; invalid launch reports `pending_restart: true` |
## Restart-only numeric limits
These stay out of `update_analysis_settings`.
| Environment | Owner | Default | Floor | Maximum | Read | Why restart-only |
| ----------------------------------- | -------------------------------------- | ------------------: | -------: | ------------------------: | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `REA_DEBUGGER_MAX_PENDING_COMMANDS` | `src/domain/analysisResourceLimits.ts` | 16 | 1 | 256 | debugger manager construction | Queue capacity of the manager created at startup. `update_mode: "restart"` and `pending_restart: false`. |
| `REA_DEBUGGER_MAX_SESSIONS` | `src/domain/analysisResourceLimits.ts` | 8 | 1 | 64 | debugger manager construction | Session-pool size of the manager created at startup. `update_mode: "restart"` and `pending_restart: false`. |
| `REA_HOPPER_DEMO_DIALOG_TIMEOUT_MS` | `src/domain/hopperStartupFailure.ts` | 10000 | 1 | 2147483647 | Linux private-display adapter start | The helper reads the process environment. Prose restart. |
| `REA_JADX_ACTIVE_PROCESSOR_COUNT` | `src/android/JadxConfiguration.ts` | unset (JVM default) | 1 | 2147483647 | JADX JVM launch | `-XX:ActiveProcessorCount` is fixed for that JVM. Prose on invalid values. |
| `REA_JADX_HEAP_MIB` | `src/android/JadxConfiguration.ts` | unset (JVM default) | 1 | `Number.MAX_SAFE_INTEGER` | JADX JVM launch | `-Xmx` is fixed for that JVM. Prose on invalid values. |
| `REA_KUNA_CACHE_MAX_BYTES` | `src/kuna/KunaConfiguration.ts` | 4294967296 | 1048576 | `Number.MAX_SAFE_INTEGER` | Kuna cache construction | The cache is sized once for the provider. Prose restart. |
| `REA_KUNA_JOBS` | `src/kuna/KunaConfiguration.ts` | 1 | 1 | 32 | Kuna run-slot construction | Worker-pool width for the provider. Prose restart. |
| `REA_MCP_DOCTOR_DEADLINE_MS` | `src/config/mcpStartupBudgets.ts` | 30000 | 1 | 2147483647 | MCP server startup | Startup phase of the MCP server itself. Prose. |
| `REA_MCP_FIRST_CATALOG_BUDGET_MS` | `src/config/mcpStartupBudgets.ts` | 20000 | 1 | 2147483647 | MCP server startup | Startup phase of the MCP server itself. Prose. |
| `REA_MCP_INITIALIZE_BUDGET_MS` | `src/config/mcpStartupBudgets.ts` | 15000 | 1 | 2147483647 | MCP server startup | Startup phase of the MCP server itself. Prose. |
| `REA_MCP_MAX_INPUT_ELEMENTS` | `src/config/mcpInputBudget.ts` | 1024 | 1 | `Number.MAX_SAFE_INTEGER` | MCP server creation | Argument admission for the server process. Prose restart. |
| `REA_MCP_MAX_RESPONSE_BYTES` | `src/config/mcpResponseBudget.ts` | 10485760 when unset | 10485760 | `Number.MAX_SAFE_INTEGER` | MCP server creation | Stdio receive buffer and the paired client buffer. Prose restart. |
| `REA_MCP_TOOLS_LIST_BUDGET_MS` | `src/config/mcpStartupBudgets.ts` | 5000 | 1 | 2147483647 | MCP server startup | Startup phase of the MCP server itself. Prose. |
| `REA_RECONNECT_TIMEOUT_SECONDS` | `src/hopper/BridgeLauncher.ts` | 30 | 30 | 30 | written into the Hopper bridge helper | Not a caller setting. It is `HOPPER_SHUTDOWN_TIMEOUT_MS / 1000`. |
| `REA_SWIFTC_COMPILE_TIMEOUT_MS` | `src/config/providerStartupTimeout.ts` | 120000 | 1 | 2147483647 | Darwin ownership-helper reader construction | Captured at process start for that helper compile. Prose restart. |
## Restart-only non-numeric settings
No floor or maximum. They are read from the launch environment (startup). None of them set `effect`. Recovery text tells the caller to change the launch
environment, which takes effect on the next process. They stay
restart-only because they choose a command, a binary, a filesystem root, the
browser sandbox, secret patterns, the provider, or the shape of the MCP server
or logger.
| Name | Owner | Why |
| ------------------------------------ | -------------------------------------------------- | ------------------------------------------- |
| `REA_ADB_PATH` | `src/adb/AdbConfiguration.ts` | Caller-selected executable path. |
| `REA_AGENT_ENV_COMMAND` | `src/application/DesktopBuildDoctor.ts` | Caller-selected command. |
| `REA_ANALYSIS_PROVIDER` | `src/config/environment.ts` | Provider choice for the process. |
| `REA_APKTOOL_COMMAND` | `src/apktool/ApktoolConfiguration.ts` | Caller-selected command. |
| `REA_BINWALK_COMMAND` | `src/application/SetupRegistrationEnvironment.ts` | Caller-selected command. |
| `REA_BROWSER_EXECUTABLE` | `src/application/CapabilityInventory.ts` | Caller-selected browser binary. |
| `REA_BROWSER_NO_SANDBOX` | `src/application/SetupRegistrationEnvironment.ts` | Browser sandbox policy. |
| `REA_CAPINFOS_COMMAND` | `src/pcap/PcapConfiguration.ts` | Caller-selected command. |
| `REA_CUTTER_BRIDGE_DIR` | `src/application/SetupRegistrationEnvironment.ts` | Filesystem root for the Cutter bridge. |
| `REA_DARWIN_MODULE_CACHE_PATH` | `src/application/SetupRegistrationEnvironment.ts` | Filesystem path for the Swift module cache. |
| `REA_DARWIN_SWIFTC_PATH` | `src/application/SetupRegistrationEnvironment.ts` | Caller-selected compiler path. |
| `REA_DCONF_COMMAND` | `src/desktop/DconfDatabaseReader.ts` | Caller-selected command. |
| `REA_DOCKER_COMMAND` | `src/application/DesktopBuildDoctor.ts` | Caller-selected command. |
| `REA_ELECTRON_CAPTURE_ARGV` | `src/browser/PlaywrightElectronActiveProvider.ts` | Caller-selected process arguments. |
| `REA_EVM_PRLIMIT_COMMAND` | `src/application/SetupRegistrationEnvironment.ts` | Caller-selected command. |
| `REA_FIRMWARE_PRLIMIT_COMMAND` | `src/application/SetupRegistrationEnvironment.ts` | Caller-selected command. |
| `REA_GDB_COMMAND` | `src/application/SetupRegistrationEnvironment.ts` | Caller-selected debugger binary. |
| `REA_GHIDRA_CHECKPOINT_DIR` | `src/config/environment.ts` | Filesystem root. |
| `REA_GHIDRA_COMPILER_SPEC_ID` | `src/config/environment.ts` | Provider analysis choice. |
| `REA_GHIDRA_DISABLED_ANALYZERS` | `src/config/environment.ts` | Provider analysis choice. |
| `REA_GHIDRA_LANGUAGE_ID` | `src/config/environment.ts` | Provider analysis choice. |
| `REA_GHIDRA_NATIVEAOT_JAR` | `src/config/environment.ts` | Caller-selected binary path. |
| `REA_GHIDRA_PROGRAM_NAME` | `src/config/environment.ts` | Provider project selection. |
| `REA_GHIDRA_PROJECT_PATH` | `src/config/environment.ts` | Filesystem root. |
| `REA_GHIDRA_SEED_FILE` | `src/config/environment.ts` | Filesystem path. |
| `REA_IDA_MCP_CONFIG` | `src/config/environment.ts` | Filesystem path of the IDA MCP config. |
| `REA_ILSPY_CMD_PATH` | `src/config/environment.ts` | Caller-selected executable path. |
| `REA_JADX_MCP_JAR` | `src/android/JadxConfiguration.ts` | Caller-selected binary path. |
| `REA_JAVASCRIPT_PRLIMIT_COMMAND` | `src/application/SetupRegistrationEnvironment.ts` | Caller-selected command. |
| `REA_JEB_MCP_URL` | `src/config/environment.ts` | Provider endpoint. |
| `REA_KUNA_CACHE_DIR` | `src/kuna/KunaConfiguration.ts` | Filesystem root. |
| `REA_KUNA_COMMAND` | `src/kuna/KunaConfiguration.ts` | Caller-selected executable path. |
| `REA_LOG_LEVEL` | `src/config/environment.ts` | Logger configuration for the process. |
| `REA_MANAGED_LINUX_HOPPER_LAUNCHER` | `src/application/HopperLauncherDiscovery.ts` | Caller-selected launcher path. |
| `REA_MCP_INPUT_SCHEMA_PROFILE` | `src/config/environment.ts` | MCP server schema shape. |
| `REA_MCP_OUTPUT_SCHEMA_PROFILE` | `src/config/environment.ts` | MCP server schema shape. |
| `REA_MCP_TOOL_CATALOG_PROFILE` | `src/config/environment.ts` | MCP server catalog shape. |
| `REA_MCP_TOOL_FAMILIES` | `src/config/environment.ts` | Which tools the MCP server registers. |
| `REA_MITMDUMP_COMMAND` | `src/application/SetupRegistrationEnvironment.ts` | Caller-selected command. |
| `REA_OBJDUMP_COMMAND` | `src/application/SetupRegistrationEnvironment.ts` | Caller-selected command. |
| `REA_PWNDBG_GDB` | `src/native/pwndbg/PwndbgCoreContext.ts` | Caller-selected debugger binary. |
| `REA_PWNDBG_GDBINIT` | `src/native/pwndbg/PwndbgCoreContext.ts` | Caller-selected file path. |
| `REA_PWNDBG_VENV_PATH` | `src/native/pwndbg/PwndbgCoreContext.ts` | Filesystem root. |
| `REA_PWNTOOLS_PYTHON` | `src/native/pwntools/PwntoolsDecoder.ts` | Caller-selected interpreter path. |
| `REA_REFERENCE_SECRET_PATTERNS_JSON` | `src/config/environment.ts` | Secret-pattern policy. |
| `REA_RIZIN_COMMAND` | `src/rizin/RizinDebugSessionManager.ts` | Caller-selected debugger binary. |
| `REA_SIGROK_COMMAND` | `src/signals/SigrokRecordingProvider.ts` | Caller-selected command. |
| `REA_SIGROK_PRLIMIT_COMMAND` | `src/signals/SigrokRecordingProvider.ts` | Caller-selected command. |
| `REA_TSHARK_COMMAND` | `src/pcap/PcapConfiguration.ts` | Caller-selected command. |
| `REA_TSHARK_PRLIMIT_COMMAND` | `src/pcap/PcapNativeBound.ts` | Caller-selected command. |
| `REA_UNBLOB_COMMAND` | `src/application/SetupRegistrationEnvironment.ts` | Caller-selected command. |
| `REA_WABT_BIN_DIRECTORY` | `src/wasm/WabtArtifactProvider.ts` | Filesystem root of the WABT tools. |
| `REA_WAKARU_COMMAND` | `src/application/SetupRegistrationEnvironment.ts` | Caller-selected command. |
| `REA_WORKFLOW_PROVIDER` | `src/application/DirectAnalysis.ts` | Provider choice. |
| `REA_XPROP_COMMAND` | `src/application/desktop/DesktopCaptureFailure.ts` | Caller-selected command. |
Names that look like `REA_` but are process protocol, not caller settings:
`REA_BUILD_COMMIT`, `REA_BUILD_COMPUTER_REA_ARCHIVE`, `REA_INSTALL_SCRIPT`,
`REA_OWNS_PROCESS_LIFETIME`, `REA_PAYLOAD`, `REA_PROCESS_RUN_ID`, `REA_RUN_ID`,
`REA_SOCKET`, `REA_STATE_FORMAT`, `REA_TARGET_PATH`, `REA_TOKEN`, bridge markers
such as `REA_COMMAND_COMPLETE_`, and the `ERR_REA_WINDOWS_NATIVE` code.
`GHIDRA_INSTALL_DIR`, `JAVA_HOME`, `HOPPER_LAUNCHER_PATH`, `HOPPER_TARGET_PATH`,
`HOPPER_TARGET_KIND`, and `HOPPER_LOADER_ARGS_JSON` are launch configuration
without the `REA_` prefix. Paths and the provider choice among them stay
restart-only for the same reason as the table above.
## Hardcoded ceilings left for follow-up
These are checked per call and tell the caller this release has no runtime
override. They are not `REA_` settings, so the numeric-limit check does not
list them. Each family has its own owner. Turning one into a setting needs that
owner's default, floor, maximum, and an error that carries the setting id,
value, source, and range. Format and protocol ceilings that the
[limits guide](limits-and-recovery.md) already calls fixed stay fixed. In
particular, JSON nesting stops at `MAX_JSON_DEPTH` (1000) and the debugger
control-reply budget stays separate from
`REA_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES`.
| Family | Examples | Follow-up |
| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------- |
| Native structural input ceilings | PE resources, imports, load config, certificates, exports, initializers, binary hardening, Windows shortcuts | One setting per ceiling, or a shared input budget only where the owners match. |
| Native plist | `MAX_NATIVE_PLIST_BYTES`, decode bytes, nesting depth | Separate from JSON depth. |
| SQLite inspection | concurrent inspections, input bytes, diagnostic bytes | Pool width stays restart-only if it sizes the worker set; byte budgets can become settings. |
| Producer command allowances | fixed command deadline and retained output where the text says there is no runtime override | Distinct from per-call `producer_limits`. |
| Hopper caller-scan retention and page allowances | `HOPPER_CALLER_SCAN_RETENTION_BYTES`, `HOPPER_CALLER_PAGE_ALLOWANCE_BYTES` | Explicitly not raised by the scan time budget. |
| Rizin frame protocol | `MAX_FRAME_BYTES`, queued frames | Protocol bounds, not caller budgets. |
| Debugger detach and control replies | `DETACH_TIMEOUT_MS`, `DEBUGGER_CONTROL_REPLY_BYTES` | Ownership deadlines, not the shared command deadline. |
| Web network capture nesting | `max_nesting_depth` already has a per-call parameter | The fixed depth maximum is the follow-up, not the parameter. |
## Static check
`npm run lint` runs `scripts/verify/rea-numeric-limits.mjs` after
dependencies are installed. `scripts/verify/architecture-guards.mjs` runs the
same check when `typescript6` is already installed; the pre-install CI step
cannot. The check scans `src` excluding tests,
keeps names that are numeric limits, and requires each name to be an
`environment` on an analysis setting or a registry entry with a reason.
The registry accepts only `restart-only`. A limit checked on each call has to
be an analysis setting. The check rejects a `per-call` registry entry.
+2 -4
View File
@@ -196,7 +196,5 @@ The lane compiles a small source-owned fixture with the host C compiler and veri
Failed runs also report process-lineage evidence and exit unsuccessfully. If provider or transport cleanup is unconfirmed, the report says `cleanup: "incomplete"` and records `retained_workspace`; that directory is preserved for investigation and cleanup retries.
Doctor reports an invalid Kuna cache, job count, or timeout setting with that
setting's correction. Cache size and job count stay with the process that
started them. The two timeouts can be changed at runtime with
`update_analysis_settings`. These configuration rejections do not require
replacing the selected Kuna executable.
setting's correction. Correct or unset the named setting and restart REA; these
configuration rejections do not require replacing the selected Kuna executable.
+1 -2
View File
@@ -65,8 +65,7 @@ escaping, before allocating the serialized request or sending it. The framing
newline is excluded. An oversized request returns `resource_constraint` with
the effective setting, its source, and the exact payload byte count.
`bridge.max_request_bytes` can be changed at runtime with
`update_analysis_settings`. The launch seed is `REA_BRIDGE_MAX_REQUEST_BYTES`.
Set the value in the environment that starts REA, then restart REA to apply it.
This setting is enforced by the REA client; the Java bridge has no corresponding
request ceiling.
+1 -1
View File
@@ -148,7 +148,7 @@
"build:termux:run": "npm run compile:termux && npm run artifacts:skills",
"format": "oxfmt .",
"format:check": "oxfmt --check .",
"lint": "node scripts/ci/static-check-report.mjs lint && npm run verify:module-boundaries && node scripts/verify/rea-numeric-limits.mjs",
"lint": "node scripts/ci/static-check-report.mjs lint && npm run verify:module-boundaries",
"lint:details": "oxlint && npm run verify:module-boundaries",
"lint:fix": "oxlint --fix && npm run verify:module-boundaries",
"knip": "knip --cache",
-21
View File
@@ -1,5 +1,3 @@
import { spawnSync } from "node:child_process";
import { existsSync } from "node:fs";
import { readFile, readdir } from "node:fs/promises";
import { join, relative, sep } from "node:path";
import { fileURLToPath } from "node:url";
@@ -195,23 +193,4 @@ if (failures.length > 0) {
console.error(`Architecture guards failed:\n\n${failures.join("\n\n")}\n`);
process.exit(1);
}
// CI runs this file before `npm ci`. The numeric-limit parser needs
// typescript6, so that check waits until dependencies exist. `npm run lint`
// runs it after install.
const typescript6 = new URL(
"../../node_modules/typescript6/package.json",
import.meta.url,
);
if (!existsSync(typescript6)) {
console.log(
"Numeric REA_ limit check waits until dependencies are installed.",
);
} else {
const numericLimits = spawnSync(
process.execPath,
[fileURLToPath(new URL("./rea-numeric-limits.mjs", import.meta.url))],
{ stdio: "inherit" },
);
if (numericLimits.status !== 0) process.exit(numericLimits.status ?? 1);
}
console.log(`Architecture guards passed across ${files.length} source files.`);
-23
View File
@@ -1,23 +0,0 @@
/** Relative path of the registry. Its text is a declaration, not a producer. */
export const NUMERIC_LIMIT_DECLARATION: "src/domain/numericLimitRegistry.ts";
/** Limits that must become analysis settings and cannot be registry exceptions. */
export const REQUIRED_RUNTIME_LIMITS: readonly [
"REA_NTFS_MFT_MAX_FILE_BYTES",
"REA_REGF_HIVE_MAX_FILE_BYTES",
"REA_WEB_BUNDLE_ENDPOINT_LITERAL_CANDIDATES",
];
/** True for a numeric `REA_` limit, including the documented endpoint-candidate count. */
export function isNumericLimit(name: string): boolean;
/** Producer paths for numeric limits. Registry text, comments, and allowlists are omitted. */
export function discoverNumericLimitUses(
files: readonly { readonly path: string; readonly source: string }[],
): Map<string, string>;
/** Why a tracked limit is not an analysis setting, when it is present. */
export function requiredRuntimeLimitFailure(
name: string,
presence: { readonly inRuntime: boolean; readonly inRegistry: boolean },
): string | undefined;
-401
View File
@@ -1,401 +0,0 @@
import { readFile, readdir } from "node:fs/promises";
import { join, relative, resolve, sep } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import ts from "typescript6";
// Every numeric REA_ limit under src must be an analysis setting or a
// restart-only exception with a reason and an update_mode. A per-call limit
// belongs in ANALYSIS_SETTINGS. The three limits moving on #9868, #9865, and
// #9836 are not in this tree; once their names appear here they must join
// ANALYSIS_SETTINGS. REA_WEB_BUNDLE_ENDPOINT_LITERAL_CANDIDATES is numeric
// even though its name has no byte or time suffix.
const root = fileURLToPath(new URL("../../", import.meta.url));
const definitionsPath = join(root, "src/domain/analysisSettingDefinitions.ts");
const registryPath = join(root, "src/domain/numericLimitRegistry.ts");
const listFiles = async (dir) => {
const output = [];
for (const entry of await readdir(dir, { withFileTypes: true })) {
const path = join(dir, entry.name);
if (entry.isDirectory()) {
if (entry.name === "node_modules" || entry.name === "dist") continue;
output.push(...(await listFiles(path)));
} else if (
entry.name.endsWith(".ts") &&
!entry.name.endsWith(".test.ts") &&
!entry.name.endsWith(".fixture.ts")
)
output.push(path);
}
return output;
};
/**
* Count-shaped limits whose names do not use a byte, time, or maximum suffix.
* `REA_WEB_BUNDLE_ENDPOINT_LITERAL_CANDIDATES` is the runtime setting on #9836.
*/
const DOCUMENTED_COUNT_LIMITS = new Set([
"REA_WEB_BUNDLE_ENDPOINT_LITERAL_CANDIDATES",
]);
/** Registry text is a declaration, not a producer that uses the limit. */
export const NUMERIC_LIMIT_DECLARATION = "src/domain/numericLimitRegistry.ts";
/**
* These limits are moving onto runtime analysis settings on their own PRs.
* A producer or a registry entry cannot satisfy that requirement.
*/
export const REQUIRED_RUNTIME_LIMITS = [
"REA_NTFS_MFT_MAX_FILE_BYTES",
"REA_REGF_HIVE_MAX_FILE_BYTES",
"REA_WEB_BUNDLE_ENDPOINT_LITERAL_CANDIDATES",
];
const unwrapExpression = (node) => {
let current = node;
while (
ts.isAsExpression(current) ||
ts.isSatisfiesExpression(current) ||
ts.isParenthesizedExpression(current) ||
ts.isTypeAssertionExpression(current) ||
ts.isNonNullExpression(current)
)
current = current.expression;
return current;
};
const literalText = (node) =>
ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)
? node.text
: undefined;
/** Same-file const strings and `setting` fields, so `environment[name]` still resolves. */
const stringBindings = (sourceFile) => {
const bindings = new Map();
const visit = (node) => {
if (
ts.isVariableDeclaration(node) &&
ts.isIdentifier(node.name) &&
node.initializer !== undefined
) {
const initializer = unwrapExpression(node.initializer);
const literal = literalText(initializer);
if (literal !== undefined) bindings.set(node.name.text, literal);
if (ts.isObjectLiteralExpression(initializer)) {
for (const property of initializer.properties) {
if (
!ts.isPropertyAssignment(property) ||
(!ts.isIdentifier(property.name) &&
!ts.isStringLiteral(property.name))
)
continue;
const value = literalText(unwrapExpression(property.initializer));
if (value !== undefined)
bindings.set(`${node.name.text}.${property.name.text}`, value);
}
}
}
ts.forEachChild(node, visit);
};
visit(sourceFile);
return bindings;
};
const boundLimitName = (node, bindings) => {
const expression = unwrapExpression(node);
const literal = literalText(expression);
if (literal !== undefined) return literal;
if (ts.isIdentifier(expression)) return bindings.get(expression.text);
if (
ts.isPropertyAccessExpression(expression) &&
ts.isIdentifier(expression.expression) &&
(ts.isIdentifier(expression.name) || ts.isStringLiteral(expression.name))
)
return bindings.get(
`${expression.expression.text}.${expression.name.text}`,
);
return undefined;
};
export const isNumericLimit = (name) =>
DOCUMENTED_COUNT_LIMITS.has(name) ||
/(?:_BYTES|_MS|_MIB|_SECONDS|_POLLS|_JOBS|_ELEMENTS|_COUNT|_SESSIONS|_COMMANDS)$/u.test(
name,
) ||
/_MAX_/u.test(name) ||
/_(?:TIMEOUT|DEADLINE|BUDGET)(?:_|$)/u.test(name) ||
/_HEAP(?:_|$)/u.test(name);
const limitName = (text) =>
text !== undefined && /^REA_[A-Z0-9_]+$/u.test(text) && isNumericLimit(text)
? text
: undefined;
const remember = (discovered, path, name) => {
const limit = limitName(name);
if (limit === undefined || discovered.has(limit)) return;
discovered.set(limit, path);
};
/**
* Producers are syntax, not text. A read is a property or element access,
* including a const string used as the element, a string argument to a call,
* an object-binding key (rename and default included), an `in` check, or an
* object key that a spread can copy. Comments and array literals are not reads.
*/
export const discoverNumericLimitUses = (files) => {
const discovered = new Map();
for (const file of files) {
const relativePath = file.path.split(sep).join("/");
if (relativePath === NUMERIC_LIMIT_DECLARATION) continue;
const sourceFile = ts.createSourceFile(
relativePath,
file.source,
ts.ScriptTarget.Latest,
true,
ts.ScriptKind.TS,
);
const bindings = stringBindings(sourceFile);
const visit = (node) => {
if (
ts.isPropertyAccessExpression(node) ||
ts.isPropertyAccessChain?.(node)
)
remember(discovered, relativePath, node.name.text);
if (ts.isElementAccessExpression(node))
remember(
discovered,
relativePath,
boundLimitName(node.argumentExpression, bindings),
);
if (ts.isCallExpression(node) || ts.isNewExpression(node)) {
for (const argument of node.arguments ?? [])
remember(
discovered,
relativePath,
boundLimitName(argument, bindings),
);
}
if (
ts.isBinaryExpression(node) &&
node.operatorToken.kind === ts.SyntaxKind.InKeyword
)
remember(discovered, relativePath, boundLimitName(node.left, bindings));
if (
ts.isBindingElement(node) &&
node.dotDotDotToken === undefined &&
ts.isObjectBindingPattern(node.parent)
) {
const key = node.propertyName ?? node.name;
if (
ts.isIdentifier(key) ||
ts.isStringLiteral(key) ||
ts.isNumericLiteral(key)
)
remember(discovered, relativePath, key.text);
}
if (
(ts.isPropertyAssignment(node) ||
ts.isShorthandPropertyAssignment(node)) &&
(ts.isIdentifier(node.name) ||
ts.isStringLiteral(node.name) ||
ts.isNumericLiteral(node.name))
)
remember(discovered, relativePath, node.name.text);
if (ts.isVariableDeclaration(node) && node.initializer !== undefined)
remember(
discovered,
relativePath,
literalText(unwrapExpression(node.initializer)),
);
if (ts.isPropertyAssignment(node))
remember(
discovered,
relativePath,
literalText(unwrapExpression(node.initializer)),
);
if (
ts.isTemplateExpression(node) ||
ts.isNoSubstitutionTemplateLiteral(node)
) {
const chunks = ts.isNoSubstitutionTemplateLiteral(node)
? [node.text]
: [
node.head.text,
...node.templateSpans.map((span) => span.literal.text),
];
for (const chunk of chunks)
for (const token of chunk.match(/REA_[A-Z0-9_]+/gu) ?? [])
remember(discovered, relativePath, token);
}
ts.forEachChild(node, visit);
};
visit(sourceFile);
}
return discovered;
};
/** Tracked limits join ANALYSIS_SETTINGS. The exception registry cannot classify them. */
export const requiredRuntimeLimitFailure = (
name,
{ inRuntime, inRegistry },
) => {
if (!REQUIRED_RUNTIME_LIMITS.includes(name)) return undefined;
if (inRuntime && !inRegistry) return undefined;
return `${name} must join ANALYSIS_SETTINGS; numericLimitRegistry cannot classify it`;
};
const namesIn = (source, pattern) =>
new Set([...source.matchAll(pattern)].map((match) => match[1]));
const direct =
process.argv[1] !== undefined &&
import.meta.url === pathToFileURL(resolve(process.argv[1])).href;
if (direct) {
const failures = [];
const definitions = await readFile(definitionsPath, "utf8");
const registry = await readFile(registryPath, "utf8");
const runtime = namesIn(definitions, /environment:\s*"(REA_[A-Z0-9_]+)"/gu);
if (registry.includes('policy: "per-call"'))
failures.push(
"per-call limits belong in ANALYSIS_SETTINGS, not numericLimitRegistry.ts",
);
const declaredNames = [
...registry.matchAll(/name:\s*"(REA_[A-Z0-9_]+)"/gu),
].map((match) => match[1]);
const entries = [
...registry.matchAll(
/name:\s*"(REA_[A-Z0-9_]+)"\s*,\s*update_mode:\s*"(runtime|restart)"\s*,\s*policy:\s*"(restart-only)"\s*,\s*read:\s*"(startup)"\s*,\s*reason:\s*"((?:\\.|[^"\\])*)"/gu,
),
];
const listed = new Map();
for (const match of entries) {
const [, name, updateMode, policy, read, reason] = match;
if (listed.has(name)) failures.push(`${name} is listed twice`);
listed.set(name, { updateMode, policy, read, reason });
if (reason.trim().length < 40)
failures.push(`${name} needs a reason that states why`);
if (updateMode !== "restart")
failures.push(
`${name} is outside ANALYSIS_SETTINGS, so its update_mode must be restart`,
);
if (runtime.has(name))
failures.push(
`${name} is both a runtime setting and on the exception list`,
);
if (read !== "startup")
failures.push(`${name} is restart-only but is not read at startup`);
}
for (const name of declaredNames) {
if (!listed.has(name))
failures.push(`${name} is registered without update_mode`);
}
const settingBodies = [
...definitions.matchAll(/\n {2}"[^"]+": \{([\s\S]*?)\n {2}\},/gu),
];
if (settingBodies.length === 0)
failures.push("ANALYSIS_SETTINGS did not parse into setting bodies");
for (const [, body] of settingBodies) {
if (!/updateMode:\s*"(?:runtime|restart)"/u.test(body))
failures.push("an analysis setting is missing updateMode");
}
const discovered = discoverNumericLimitUses(
await Promise.all(
(await listFiles(join(root, "src"))).map(async (path) => ({
path: relative(root, path),
source: await readFile(path, "utf8"),
})),
),
);
for (const [name, file] of [...discovered].sort(([left], [right]) =>
left < right ? -1 : left > right ? 1 : 0,
)) {
if (!runtime.has(name) && !listed.has(name))
failures.push(
`${name} in ${file} is neither an analysis setting nor docs/design/rea-limit-inventory.md via src/domain/numericLimitRegistry.ts`,
);
}
for (const name of listed.keys()) {
if (!discovered.has(name))
failures.push(`${name} is registered but no longer appears in src`);
}
const present = new Set([...discovered.keys(), ...listed.keys(), ...runtime]);
for (const name of REQUIRED_RUNTIME_LIMITS) {
if (!present.has(name)) continue;
const failure = requiredRuntimeLimitFailure(name, {
inRuntime: runtime.has(name),
inRegistry: listed.has(name),
});
if (failure !== undefined) failures.push(failure);
}
const parameterOwners = [
"src/artifacts/ArtifactReaderLimits.ts",
"src/domain/artifactReaderLimits.ts",
"src/domain/webNetworkCapture.ts",
];
for (const relativePath of parameterOwners) {
const source = await readFile(join(root, relativePath), "utf8");
if (/update_mode:\s*"runtime"/u.test(source))
failures.push(
`${relativePath} is a per-call parameter, so update_mode cannot be runtime`,
);
if (!/update_mode:\s*"parameter"/u.test(source))
failures.push(`${relativePath} must report update_mode parameter`);
}
const forbidden =
/(?<![A-Za-z0-9_])(?:rea_restart_required|restartRequired|restart_required)(?![A-Za-z0-9_])|effect:\s*"(?:immediate|next_process)"|SettingChangeEffect/gu;
// Docs and verification receipts keep the field names they recorded.
// Source, tests, skills, and scripts cannot reintroduce those names.
const scanRoots = ["src", "tests", ".agents", "scripts"].map((dir) =>
join(root, dir),
);
const skipDir = new Set([
"node_modules",
"dist",
".turbo",
".cache",
"rea-numeric-limits.mjs",
]);
const listText = async (dir) => {
const output = [];
let entries;
try {
entries = await readdir(dir, { withFileTypes: true });
} catch {
return output;
}
for (const entry of entries) {
if (skipDir.has(entry.name)) continue;
const path = join(dir, entry.name);
if (entry.isDirectory()) output.push(...(await listText(path)));
else if (/\.(?:ts|tsx|mjs|js|md|json|py|yml|yaml)$/u.test(entry.name))
output.push(path);
}
return output;
};
for (const dir of scanRoots) {
for (const path of await listText(dir)) {
if (path.endsWith(`${sep}rea-numeric-limits.mjs`)) continue;
const source = await readFile(path, "utf8");
const hit = source.match(forbidden);
if (hit)
failures.push(
`${relative(root, path).split(sep).join("/")} still uses ${[...new Set(hit)].join(", ")}`,
);
}
}
if (failures.length > 0) {
console.error(`Numeric REA_ limit check failed:\n${failures.join("\n")}`);
process.exit(1);
}
console.log(
`Numeric REA_ limits: ${runtime.size} runtime, ${listed.size} restart-only, ${discovered.size} discovered.`,
);
}
+1 -2
View File
@@ -36,8 +36,7 @@ it("names REA_JADX_OPERATION_TIMEOUT_MS when the operation deadline elapses", ()
applies_at: "next_operation",
agent_editable: true,
scope: "rea-runtime",
update_mode: "runtime",
pending_restart: false,
rea_restart_required: false,
},
});
expect(projected).toMatchObject({
+2 -176
View File
@@ -1,16 +1,5 @@
import { expect, it } from "vitest";
import { AnalysisSettings } from "./AnalysisSettings.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import {
applyWindowsChangeTimeSnapshot,
resolveWindowsChangeTimeSettle,
} from "../config/windowsChangeTimeSettle.js";
import { pwntoolsInputBytes } from "../native/pwntools/PwntoolsInputLimit.js";
import { resolveBridgeMaxRequestBytes } from "../config/bridgeMaxRequestBytes.js";
import { resolveCaptureDecoderHeap } from "../config/captureDecoderHeap.js";
import { resolveRegexMatchingDeadline } from "../config/regexMatchingDeadline.js";
import { resolveRegexWorkerHeapRequest } from "../config/regexWorkerHeapRequest.js";
import { runWithAnalysisOperationSnapshot } from "../domain/analysisOperationSnapshot.js";
import {
runtimeAnalysisConfig,
runtimeAsarHeaderAdmission,
@@ -145,13 +134,7 @@ it("exposes shared provider controls and preserves the explicit unbounded decomp
store
.inspect({ provider: "ghidra", operation: "search_strings" })
.settings.map(({ setting }) => setting),
).toEqual(["ghidra.startup_timeout_ms", "bridge.max_request_bytes"]);
for (const operation of ["list_cutter_sessions", "cutter_command"] as const)
expect(
store
.inspect({ provider: "cutter", operation })
.settings.map(({ setting }) => setting),
).toContain("bridge.max_request_bytes");
).toEqual(["ghidra.startup_timeout_ms"]);
expect(
store
.inspect({ provider: "ida", operation: "search_strings" })
@@ -161,23 +144,7 @@ it("exposes shared provider controls and preserves the explicit unbounded decomp
store
.inspect({ operation: "inventory_artifact" })
.settings.map(({ setting }) => setting),
).toEqual([
"asar.max_listing_bytes",
"asar.max_header_bytes",
"windows.change_time_drift_ms",
"windows.change_time_settle_poll_ms",
"windows.change_time_settle_max_polls",
]);
for (const operation of [
"procedure_pseudo_code",
"procedure_assembly",
"analyze_function",
] as const)
expect(
store
.inspect({ provider: "kuna", operation })
.settings.map(({ setting }) => setting),
).toContain("kuna.project_timeout_ms");
).toEqual(["asar.max_listing_bytes", "asar.max_header_bytes"]);
expect(
store.update({
changes: [
@@ -225,144 +192,3 @@ it("exposes shared provider controls and preserves the explicit unbounded decomp
).toBe(true);
expect(config.ghidraDecompileTimeoutSeconds).toBe(7);
});
it("rejects an out-of-range per-call limit and reads an accepted value from the operation snapshot", () => {
const store = new AnalysisSettings({});
const rejected = store.update({
changes: [
{ setting: "windows.change_time_drift_ms", action: "set", value: 0 },
],
});
expect(rejected.ok).toBe(false);
if (!rejected.ok)
expect(projectAnalysisError(rejected.error)).toMatchObject({
code: "invalid_request",
});
expect(
store.update({
changes: [
{ setting: "bridge.max_request_bytes", action: "set", value: 1 },
],
}).ok,
).toBe(false);
expect(
store.update({
changes: [
{
setting: "windows.change_time_settle_max_polls",
action: "set",
value: 1001,
},
],
}).ok,
).toBe(false);
const accepted = store.update({
changes: [
{ setting: "windows.change_time_drift_ms", action: "set", value: 5000 },
{ setting: "pwntools.max_input_bytes", action: "set", value: 65536 },
{ setting: "bridge.max_request_bytes", action: "set", value: 33554432 },
],
});
expect(accepted.ok).toBe(true);
if (!accepted.ok) return;
expect(accepted.value).toMatchObject({
pending_restart: false,
});
const parsed = parseConfig({});
if (!parsed.ok) throw parsed.error;
const live = runtimeAnalysisConfig(parsed.value, store.read);
store.run(() => {
const windows = resolveWindowsChangeTimeSettle({});
if (!windows.ok) throw windows.error;
expect(applyWindowsChangeTimeSnapshot(windows.value)).toMatchObject({
driftMs: 5000,
driftSource: "runtime",
pollSource: "default",
});
expect(pwntoolsInputBytes({})).toMatchObject({
configured: true,
bytes: 65536,
source: "runtime",
});
expect(live.bridgeMaxRequestBytes).toMatchObject({
maximumBytes: 33554432,
source: "runtime",
});
expect(live.hopperStartupTimeoutSource).toBe("default");
});
expect(pwntoolsInputBytes({})).toMatchObject({
bytes: 32 * 1024 * 1024,
source: "default",
});
});
it("reads an accepted override before an invalid launch seed", () => {
const resolved = runWithAnalysisOperationSnapshot(
{
revision: 1,
overrides: {
"bridge.max_request_bytes": 8192,
"capture.decoder_old_generation_mib": 64,
"regex.matching_deadline_ms": 250,
"regex.worker_heap_request_mib": 128,
"asar.max_header_bytes": 8192,
"asar.max_listing_bytes": 4096,
},
},
() => ({
bridge: resolveBridgeMaxRequestBytes({
REA_BRIDGE_MAX_REQUEST_BYTES: "nope",
}),
capture: resolveCaptureDecoderHeap({
REA_CAPTURE_DECODER_OLD_GENERATION_MIB: "0",
}),
deadline: resolveRegexMatchingDeadline({
REA_REGEX_MATCHING_DEADLINE_MS: "nope",
}),
heap: resolveRegexWorkerHeapRequest({
REA_REGEX_WORKER_HEAP_REQUEST_MIB: "1",
}),
asar: runtimeAsarHeaderAdmission({
REA_ASAR_MAX_HEADER_BYTES: "nope",
REA_ASAR_MAX_LISTING_BYTES: "0",
}),
}),
);
expect(resolved.bridge).toMatchObject({
maximumBytes: 8192,
source: "runtime",
});
expect(resolved.capture).toMatchObject({
oldGenerationMiB: 64,
source: "runtime",
});
expect(resolved.deadline).toMatchObject({
deadlineMs: 250,
source: "runtime",
});
expect(resolved.heap).toMatchObject({ heapMiB: 128, source: "runtime" });
expect(resolved.asar).toMatchObject({
maximumBytes: 8192,
source: "runtime",
listing: { maximumBytes: 4096, source: "runtime" },
});
});
it("keeps the Hopper startup deadline source through a runtime override", () => {
const parsed = parseConfig({ REA_HOPPER_STARTUP_TIMEOUT_MS: "45000" });
if (!parsed.ok) throw parsed.error;
const store = new AnalysisSettings({});
const live = runtimeAnalysisConfig(parsed.value, store.read);
expect(live.hopperStartupTimeoutSource).toBe("setting");
expect(
store.update({
changes: [
{ setting: "hopper.startup_timeout_ms", action: "set", value: 8000 },
],
}).ok,
).toBe(true);
store.run(() => {
expect(live.hopperStartupTimeoutMs).toBe(8000);
expect(live.hopperStartupTimeoutSource).toBe("runtime");
});
});
+2 -7
View File
@@ -1,4 +1,3 @@
import { runWithAnalysisOperationSnapshot } from "../domain/analysisOperationSnapshot.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { launchAnalysisSetting } from "../config/analysisSettings.js";
import { AsyncLocalStorage } from "node:async_hooks";
@@ -43,9 +42,7 @@ export class AnalysisSettings {
readonly read = (): AnalysisSettingsSnapshot =>
this.#accepted.getStore() ?? this.#current;
run<Value>(operation: () => Value): Value {
return this.#accepted.run(this.#current, () =>
runWithAnalysisOperationSnapshot(this.#current, operation),
);
return this.#accepted.run(this.#current, operation);
}
inspect(input: z.infer<typeof inspectAnalysisSettingsInputSchema> = {}) {
return {
@@ -137,7 +134,7 @@ export class AnalysisSettings {
};
}),
active_work_changed: false as const,
pending_restart: false as const,
rea_restart_required: false as const,
});
}
#describe(setting: AnalysisSettingId) {
@@ -159,8 +156,6 @@ export class AnalysisSettings {
minimum: definition.minimum,
maximum: definition.maximum,
applies_at: definition.appliesAt,
update_mode: definition.updateMode,
pending_restart: false as const,
provider: definition.provider,
operations: [...definition.operations],
...(definition.environment === undefined
@@ -1,7 +1,6 @@
import { discoverJavaScriptPreloadPaths } from "./JavaScriptPreloadDiscovery.js";
import { artifactNameForClassification } from "../../domain/artifactNameSelection.js";
import type { AnalysisSettingValues } from "../../domain/analysisSettings.js";
import { selectedAnalysisSetting } from "../../domain/analysisSettingDefinitions.js";
import { absoluteCallerPath } from "../../filesystem/callerPath.js";
import {
DEFAULT_ASAR_HEADER_ADMISSION,
@@ -182,24 +181,18 @@ export const reconstructJavaScriptArtifact = async (
? {}
: { fileNodes: input.max_semantic_nodes_per_file }),
});
const heap = selectedAnalysisSetting(
input.max_heap_mb,
overrides?.["javascript.max_heap_mb"],
JAVASCRIPT_ANALYSIS_RESOURCE_DEFAULTS.heapMb,
);
const timeout = selectedAnalysisSetting(
input.analysis_timeout_ms,
overrides?.["javascript.analysis_timeout_ms"],
JAVASCRIPT_ANALYSIS_RESOURCE_DEFAULTS.timeoutMs,
);
const sourceLimits = {
...(input.semantic_precision === undefined
? {}
: { semanticPrecision: input.semantic_precision }),
heapMb: heap.value,
heapSource: heap.source,
timeoutMs: timeout.value,
timeoutSource: timeout.source,
heapMb:
input.max_heap_mb ??
overrides?.["javascript.max_heap_mb"] ??
JAVASCRIPT_ANALYSIS_RESOURCE_DEFAULTS.heapMb,
timeoutMs:
input.analysis_timeout_ms ??
overrides?.["javascript.analysis_timeout_ms"] ??
JAVASCRIPT_ANALYSIS_RESOURCE_DEFAULTS.timeoutMs,
};
const preloads = await discoverJavaScriptPreloadPaths(
files,
@@ -66,6 +66,28 @@ const report: PythonPyc = {
],
};
it.each([
{
flags: 1,
hash_based: true,
source_hash_hex: "",
source_mtime: null,
source_size: null,
},
{ flags: 1 },
])("refuses inconsistent provider header facts %j", async (header) => {
const result = await new PythonPycService({
close: async () => {},
identity,
inspect: async () =>
ok({ ...report, header: { ...report.header, ...header } }),
}).inspect({ path: report.artifact.path });
expect(result).toMatchObject({
ok: false,
error: { _tag: "AnalysisOutputError" },
});
});
it("rejects a relative path and extra arguments before reading", async () => {
let calls = 0;
const service = new PythonPycService({
-2
View File
@@ -15,8 +15,6 @@ export type WasmArtifactExecution = Omit<AnalysisExecution, "subject"> & {
/** Effective operation budget, separate from caller-authored Evidence parameters. */
export interface WasmArtifactExecutionOptions extends ExecutionOptions {
readonly commandTimeoutMs?: number;
/** Selection owner for `commandTimeoutMs`. Absent when the caller did not record it. */
readonly commandTimeoutSource?: "default" | "runtime" | "request" | undefined;
}
/** Replaceable offline WASM inspection engine, independent of target execution and network clients. */
@@ -240,7 +240,6 @@ it("gives cancellation precedence over malformed raw input", async () => {
it("uses runtime defaults only for omitted budgets, preserving explicit input and reset", async () => {
const settings = new AnalysisSettings({});
const accepted: number[] = [];
const sources: (string | undefined)[] = [];
const authored: (number | undefined)[] = [];
const service = new WasmArtifactService(
{
@@ -249,7 +248,6 @@ it("uses runtime defaults only for omitted budgets, preserving explicit input an
const value = wasmArtifactFixture();
value.tool_profile.timeout_ms = options!.commandTimeoutMs!;
accepted.push(options!.commandTimeoutMs!);
sources.push(options!.commandTimeoutSource);
authored.push(input.command_timeout_ms);
return ok(wasmArtifactExecution(value));
},
@@ -277,7 +275,6 @@ it("uses runtime defaults only for omitted budgets, preserving explicit input an
});
expect((await settings.run(() => service.inspect({ path }))).ok).toBe(true);
expect(accepted).toEqual([123000, 42000, 30000]);
expect(sources).toEqual(["runtime", "request", "default"]);
expect(authored).toEqual([undefined, 42000, undefined]);
});
+6 -9
View File
@@ -1,5 +1,4 @@
import type { AnalysisSettingsReader } from "../../domain/analysisSettings.js";
import { selectedAnalysisSetting } from "../../domain/analysisSettingDefinitions.js";
import { constants as bufferConstants } from "node:buffer";
import type { ExecutionOptions } from "../AnalysisProvider.js";
import {
@@ -79,15 +78,13 @@ export class WasmArtifactService {
),
);
}
const timeout = selectedAnalysisSetting(
input.value.command_timeout_ms,
this.settings?.().overrides["wasm.command_timeout_ms"],
DEFAULT_WASM_COMMAND_TIMEOUT_MS,
);
const timeout =
input.value.command_timeout_ms ??
this.settings?.().overrides["wasm.command_timeout_ms"] ??
DEFAULT_WASM_COMMAND_TIMEOUT_MS;
const inspected = await this.provider.inspect(input.value, {
...options,
commandTimeoutMs: timeout.value,
commandTimeoutSource: timeout.source,
commandTimeoutMs: timeout,
});
if (!inspected.ok) return inspected;
const execution = inspected.value;
@@ -111,7 +108,7 @@ export class WasmArtifactService {
!report.success ||
report.data.artifact.path !== input.value.path ||
execution.subject.path !== input.value.path ||
report.data.tool_profile.timeout_ms !== timeout.value ||
report.data.tool_profile.timeout_ms !== timeout ||
report.data.tool_profile.output_budget_bytes !==
wasmResourceLimits(input.value).outputBytes ||
!wasmArtifactMatchesSubject(report.data.artifact, execution.subject) ||
@@ -54,8 +54,7 @@ it("preserves adjustable reader admission facts and gives cleanup priority", ()
required_value: 2,
unit: "count",
applies_at: "next_operation",
update_mode: "parameter",
pending_restart: false,
rea_restart_required: false,
},
},
});
+1 -2
View File
@@ -40,8 +40,7 @@ export const artifactReaderLimitFailure = (
applies_at: "next_operation",
agent_editable: true,
can_increase: canIncrease,
update_mode: "parameter" as const,
pending_restart: false as const,
rea_restart_required: false,
},
});
};
-24
View File
@@ -2,7 +2,6 @@ import type { Stats } from "node:fs";
import { expect, it } from "vitest";
import { runWithAnalysisOperationSnapshot } from "../domain/analysisOperationSnapshot.js";
import { settleChangeTimeBaseline } from "../filesystem/ChangeTimeBaseline.js";
const DRIFT_MS = 2_000;
@@ -230,26 +229,3 @@ it("throws the caller's typed cancellation rather than the raw reason", async ()
}),
).rejects.toBeInstanceOf(Cancelled);
});
it("uses a Windows settle override before rejecting an invalid launch seed", async () => {
const observed = await runWithAnalysisOperationSnapshot(
{
revision: 1,
overrides: {
"windows.change_time_drift_ms": 50,
"windows.change_time_settle_poll_ms": 10,
"windows.change_time_settle_max_polls": 4,
},
},
() =>
settleChangeTimeBaseline(async () => statsAt(1_000), {
environment: {
REA_WINDOWS_CHANGE_TIME_DRIFT_MS: "nope",
REA_WINDOWS_CHANGE_TIME_SETTLE_POLL_MS: "0",
REA_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS: "1001",
},
platform: "linux",
}),
);
expect(observed.ctimeMs).toBe(1_000);
});
@@ -89,8 +89,7 @@ it.each(["max_input_bytes", "max_reply_bytes", "max_nesting_depth"] as const)(
parameter: control,
effective_value: selected,
source: "request",
update_mode: "parameter",
pending_restart: false,
rea_restart_required: false,
},
},
});
+2 -2
View File
@@ -569,8 +569,8 @@ const modules = {
},
}),
),
cutter: service(({ providerEnvironment, settings }) =>
createCutterBridgeService(providerEnvironment, { read: settings }),
cutter: service(({ providerEnvironment }) =>
createCutterBridgeService(providerEnvironment),
),
browserObservation: capability(
({ options }) => options.browserObservation,
+24 -46
View File
@@ -3,7 +3,6 @@ import { parseJebMcpTimeoutMs } from "../config/environment.js";
import { ConfigurationError } from "../domain/configurationErrors.js";
import { AnalysisInputError } from "../domain/analysisErrorCore.js";
import { analysisErrorRemediationAction } from "../domain/analysisErrorPresentation.js";
import { currentAnalysisOperationSnapshot } from "../domain/analysisOperationSnapshot.js";
import { err } from "../domain/result.js";
import type { JebMcpConnectionFactory } from "../jeb/JebMcpConnection.js";
import { JebProvider } from "../jeb/JebProvider.js";
@@ -11,8 +10,7 @@ import { JebProvider } from "../jeb/JebProvider.js";
/**
* Construct one JEB provider bound to the caller-selected MCP endpoint. An
* unusable endpoint makes only JEB unavailable; every other CLI command and
* MCP tool is composed alongside it. An invalid launch timeout stays
* unavailable until the accepted operation supplies jeb.mcp_timeout_ms.
* MCP tool is composed alongside it.
*/
export const createJebAnalysisProvider = (
environment: Readonly<Record<string, string | undefined>> = process.env,
@@ -21,49 +19,29 @@ export const createJebAnalysisProvider = (
const requestTimeout = parseJebMcpTimeoutMs(
environment.REA_JEB_MCP_TIMEOUT_MS,
);
const build = (): JebAnalysisPort => {
try {
return new JebProvider({
environment,
...(requestTimeout.ok && requestTimeout.value !== undefined
? {
requestTimeoutMs: requestTimeout.value,
discoveryTimeoutMs: requestTimeout.value,
}
: {}),
...(connectionFactory === undefined ? {} : { connectionFactory }),
});
} catch (cause: unknown) {
if (!(cause instanceof AnalysisInputError)) throw cause;
const constraint =
typeof cause.cause === "string" ? cause.cause : cause.message;
return unconfiguredJebProvider(
new ConfigurationError("Invalid REA environment configuration", {
cause,
settings: [{ setting: "REA_JEB_MCP_URL", constraint }],
}),
);
}
};
if (requestTimeout.ok) return build();
const rejected = unconfiguredJebProvider(requestTimeout.error);
let provider: JebAnalysisPort | undefined;
const live = (): JebAnalysisPort => (provider ??= build());
const overrideActive = (): boolean =>
currentAnalysisOperationSnapshot()?.overrides["jeb.mcp_timeout_ms"] !==
undefined;
return {
inspectAvailability: (signal) =>
overrideActive()
? live().inspectAvailability(signal)
: rejected.inspectAvailability(signal),
close: () =>
provider === undefined ? Promise.resolve() : provider.close(),
execute: (request, options) =>
overrideActive()
? live().execute(request, options)
: rejected.execute(request, options),
};
if (!requestTimeout.ok) return unconfiguredJebProvider(requestTimeout.error);
try {
return new JebProvider({
environment,
...(requestTimeout.value === undefined
? {}
: {
requestTimeoutMs: requestTimeout.value,
discoveryTimeoutMs: requestTimeout.value,
}),
...(connectionFactory === undefined ? {} : { connectionFactory }),
});
} catch (cause: unknown) {
if (!(cause instanceof AnalysisInputError)) throw cause;
const constraint =
typeof cause.cause === "string" ? cause.cause : cause.message;
return unconfiguredJebProvider(
new ConfigurationError("Invalid REA environment configuration", {
cause,
settings: [{ setting: "REA_JEB_MCP_URL", constraint }],
}),
);
}
};
const unconfiguredJebProvider = (
-11
View File
@@ -141,18 +141,7 @@ describe("runtime configuration", () => {
expect(empty.value.hopperTargetPath).toBeUndefined();
expect(empty.value.analysisProvider).toBe("auto");
expect(empty.value.hopperStartupTimeoutMs).toBe(120_000);
expect(empty.value.hopperStartupTimeoutSource).toBe("default");
}
const hopperTimeout = parseConfig({
REA_HOPPER_STARTUP_TIMEOUT_MS: "45000",
});
expect(hopperTimeout).toMatchObject({
ok: true,
value: {
hopperStartupTimeoutMs: 45_000,
hopperStartupTimeoutSource: "setting",
},
});
const result = parseConfig({ HOPPER_TARGET_PATH: "/usr/bin/true" });
expect(result.ok).toBe(true);
if (result.ok) {
-23
View File
@@ -17,17 +17,6 @@ import { regexMatchingDeadlineMsSchema } from "./regexMatchingDeadline.js";
import { regexWorkerHeapRequestMiBSchema } from "./regexWorkerHeapRequest.js";
import { captureDecoderOldGenerationMiBSchema } from "./captureDecoderHeap.js";
import { parseNativeMatchingAllowance } from "./nativeMatchingAllowance.js";
import { bridgeMaxRequestBytesSchema } from "./bridgeMaxRequestBytes.js";
import {
debuggerCommandDeadlineMsSchema,
debuggerMaxCommandOutputBytesSchema,
} from "./debuggerSessionLimits.js";
import {
windowsChangeTimeDriftMsSchema,
windowsChangeTimeSettleMaxPollsSchema,
windowsChangeTimeSettlePollMsSchema,
} from "./windowsChangeTimeSettle.js";
import { pwntoolsInputBytesSchema } from "../native/pwntools/PwntoolsInputLimit.js";
const environmentSchemas: Readonly<Record<string, z.ZodType<number, string>>> =
{
REA_ASAR_MAX_HEADER_BYTES: asarMaxHeaderBytesSchema,
@@ -44,18 +33,6 @@ const environmentSchemas: Readonly<Record<string, z.ZodType<number, string>>> =
REA_REGEX_WORKER_HEAP_REQUEST_MIB: regexWorkerHeapRequestMiBSchema,
REA_CAPTURE_DECODER_OLD_GENERATION_MIB:
captureDecoderOldGenerationMiBSchema,
REA_BRIDGE_MAX_REQUEST_BYTES: bridgeMaxRequestBytesSchema,
REA_DEBUGGER_COMMAND_DEADLINE_MS: debuggerCommandDeadlineMsSchema,
REA_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES: debuggerMaxCommandOutputBytesSchema,
REA_HOPPER_CALLER_SCAN_BUDGET_MS: providerStartupTimeoutSchema,
REA_JEB_MCP_TIMEOUT_MS: providerStartupTimeoutSchema,
REA_KUNA_PROJECT_TIMEOUT_MS: providerStartupTimeoutSchema,
REA_KUNA_TIMEOUT_MS: providerStartupTimeoutSchema,
REA_PWNTOOLS_MAX_INPUT_BYTES: pwntoolsInputBytesSchema,
REA_WINDOWS_CHANGE_TIME_DRIFT_MS: windowsChangeTimeDriftMsSchema,
REA_WINDOWS_CHANGE_TIME_SETTLE_POLL_MS: windowsChangeTimeSettlePollMsSchema,
REA_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS:
windowsChangeTimeSettleMaxPollsSchema,
};
export const launchAnalysisSetting = (
id: AnalysisSettingId,
-18
View File
@@ -1,9 +1,6 @@
import { expect, it } from "vitest";
import { analysisSettingDefinition } from "../domain/analysisSettingDefinitions.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { GHIDRA_OPERATIONS } from "../ghidra/GhidraProviderMetadata.js";
import { HOPPER_OPERATIONS } from "../hopper/HopperProviderMetadata.js";
import { parseEnvironment } from "./environment.js";
import { parseConfig } from "./parseConfig.js";
import {
@@ -22,21 +19,6 @@ const symbolName = "\u0001".repeat(1024);
const session = "00000000-0000-4000-8000-000000000001";
const token = "a".repeat(64);
it("advertises the bridge ceiling for every bridge-backed operation", () => {
const advertised = new Set(
analysisSettingDefinition("bridge.max_request_bytes").operations,
);
const required = [
...HOPPER_OPERATIONS,
...GHIDRA_OPERATIONS,
"list_cutter_sessions",
"cutter_command",
];
expect(required.filter((operation) => !advertised.has(operation))).toEqual(
[],
);
});
it("keeps the 16 MiB ceiling when the setting is unset", () => {
expect(parseBridgeMaxRequestBytes(undefined)).toEqual({
ok: true,
+18 -43
View File
@@ -1,24 +1,13 @@
import { positiveDecimalIntegerSchema } from "./positiveDecimalInteger.js";
import { analysisSettingLimit } from "../domain/analysisSettingDefinitions.js";
import { currentAnalysisSettingOverride } from "../domain/analysisOperationSnapshot.js";
import { AnalysisResourceConstraintError } from "../domain/analysisErrorCore.js";
import { ConfigurationError } from "../domain/configurationErrors.js";
import { err, ok, type Result } from "../domain/result.js";
import {
DEFAULT_BRIDGE_MAX_REQUEST_BYTES,
MAXIMUM_BRIDGE_MAX_REQUEST_BYTES,
MINIMUM_BRIDGE_MAX_REQUEST_BYTES,
} from "../domain/analysisResourceLimits.js";
export const BRIDGE_MAX_REQUEST_BYTES_SETTING = "REA_BRIDGE_MAX_REQUEST_BYTES";
export {
DEFAULT_BRIDGE_MAX_REQUEST_BYTES,
MAXIMUM_BRIDGE_MAX_REQUEST_BYTES,
MINIMUM_BRIDGE_MAX_REQUEST_BYTES,
};
/** Request payload ceiling used when the setting is unset. Today's fixed constant. */
export const DEFAULT_BRIDGE_MAX_REQUEST_BYTES = 16 * 1024 * 1024;
/**
* Smallest ceiling that still admits the largest bounded bridge request.
@@ -33,7 +22,10 @@ export {
* Comments, search text, and Cutter command text are unbounded; this ceiling
* is what limits them. The floor stays below the 16 MiB default so a caller
* can lower the ceiling.
*
*/
export const MINIMUM_BRIDGE_MAX_REQUEST_BYTES = 7464;
/**
* Largest configurable request, in encoded payload bytes.
*
* The ceiling is the final JSON payload, not the raw command. Callers measure
@@ -47,11 +39,15 @@ export {
* then parses that string. A fitting response does not keep the paged wire
* frame, a second concatenated copy, and an outer JSON string.
*/
const NODE_MAX_STRING_LENGTH = 536_870_888;
const FRAMING_LF_CODE_UNITS = 1;
export const MAXIMUM_BRIDGE_MAX_REQUEST_BYTES =
NODE_MAX_STRING_LENGTH - FRAMING_LF_CODE_UNITS;
/** Effective bridge request ceiling and where its value came from. */
export interface BridgeMaxRequestBytes {
readonly maximumBytes: number;
readonly source: "default" | "setting" | "runtime";
readonly source: "default" | "setting";
}
export const DEFAULT_BRIDGE_MAX_REQUEST: BridgeMaxRequestBytes = {
@@ -103,9 +99,6 @@ export const parseBridgeMaxRequestBytes = (
export const resolveBridgeMaxRequestBytes = (
environment: Readonly<Record<string, string | undefined>>,
): BridgeMaxRequestBytes => {
const override = currentAnalysisSettingOverride("bridge.max_request_bytes");
if (override !== undefined)
return { maximumBytes: override, source: "runtime" };
const parsed = parseBridgeMaxRequestBytes(
environment[BRIDGE_MAX_REQUEST_BYTES_SETTING],
);
@@ -114,22 +107,16 @@ export const resolveBridgeMaxRequestBytes = (
};
/** How to read and change the effective bridge request ceiling. */
export const bridgeRequestLimit = (limit: BridgeMaxRequestBytes) =>
analysisSettingLimit(
"bridge.max_request_bytes",
limit.maximumBytes,
limit.source,
);
export const bridgeMaxRequestBytesRecovery = (
limit: BridgeMaxRequestBytes,
peer: BridgePeer = "Hopper",
): string =>
[
`The effective bridge.max_request_bytes value is ${String(limit.maximumBytes)} bytes (source ${limit.source}; default ${String(DEFAULT_BRIDGE_MAX_REQUEST_BYTES)}, floor ${String(MINIMUM_BRIDGE_MAX_REQUEST_BYTES)}, maximum ${String(MAXIMUM_BRIDGE_MAX_REQUEST_BYTES)}). It can be changed at runtime with update_analysis_settings. The launch seed is ${BRIDGE_MAX_REQUEST_BYTES_SETTING}.`,
`The effective ${BRIDGE_MAX_REQUEST_BYTES_SETTING} value is ${String(limit.maximumBytes)} bytes (source ${limit.source}; default ${String(DEFAULT_BRIDGE_MAX_REQUEST_BYTES)}, floor ${String(MINIMUM_BRIDGE_MAX_REQUEST_BYTES)}, maximum ${String(MAXIMUM_BRIDGE_MAX_REQUEST_BYTES)}).`,
`Set ${BRIDGE_MAX_REQUEST_BYTES_SETTING} in the environment that starts REA to a decimal integer from ${String(MINIMUM_BRIDGE_MAX_REQUEST_BYTES)} to ${String(MAXIMUM_BRIDGE_MAX_REQUEST_BYTES)}, then restart REA.`,
peer === "Ghidra"
? `The Ghidra bridge enforces no request ceiling of its own, so only the REA client applies bridge.max_request_bytes to Ghidra requests.`
: "The Hopper or Cutter bridge keeps the ceiling it started with until that bridge starts again.",
? `The Ghidra bridge enforces no request ceiling of its own, so only the REA client applies ${BRIDGE_MAX_REQUEST_BYTES_SETTING} to Ghidra requests.`
: "Restart Hopper or Cutter from that same environment so the bridge uses this ceiling.",
].join(" ");
const limitFacts = (
@@ -155,11 +142,7 @@ export const bridgeRequestOversizeError = (
"transport",
`${refusal} ${recovery}`,
{ ...limitFacts(limit), payload_bytes: payloadBytes },
{
remediationAction: recovery,
retryable: false,
limit: bridgeRequestLimit(limit),
},
{ remediationAction: recovery, retryable: false },
);
};
@@ -180,11 +163,7 @@ export const bridgeRequestLimitMismatchError = (
bridge_max_request_bytes: bridgeBytes,
client_max_request_bytes: limit.maximumBytes,
},
{
remediationAction: recovery,
retryable: false,
limit: bridgeRequestLimit(limit),
},
{ remediationAction: recovery, retryable: false },
);
};
@@ -198,10 +177,6 @@ export const bridgeResponseOversizeError = (
"transport",
`Cutter bridge response exceeded its framed transport limit of ${String(limit.maximumBytes)} bytes. ${recovery}`,
limitFacts(limit),
{
remediationAction: recovery,
retryable: false,
limit: bridgeRequestLimit(limit),
},
{ remediationAction: recovery, retryable: false },
);
};
+3 -3
View File
@@ -103,10 +103,11 @@ it("uses a valid REA_CAPTURE_DECODER_OLD_GENERATION_MIB override", async () => {
/floor 8 MiB.*maximum 16384 MiB/iu,
);
expect(projected.remediation.action).toContain("update_analysis_settings");
expect(projected.remediation.action).toContain("launch seed");
expect(projected.remediation.action).toMatch(/restart REA/iu);
expect(projected.remediation.action).toContain(
"capture.decoder_old_generation_mib",
);
expect(projected.remediation.action).toMatch(/without a REA restart/iu);
expect(projected.remediation.action).toMatch(
/removes inherited NODE_OPTIONS/iu,
);
@@ -124,8 +125,7 @@ it("uses a valid REA_CAPTURE_DECODER_OLD_GENERATION_MIB override", async () => {
unit: "MiB",
applies_at: "next_worker",
agent_editable: true,
update_mode: "runtime",
pending_restart: false,
rea_restart_required: false,
},
reported_limits: {
old_generation_heap_mib: 256,
+1 -7
View File
@@ -6,7 +6,6 @@ import {
import { positiveDecimalIntegerSchema } from "./positiveDecimalInteger.js";
import { ConfigurationError } from "../domain/configurationErrors.js";
import { currentAnalysisSettingOverride } from "../domain/analysisOperationSnapshot.js";
import { err, ok, type Result } from "../domain/result.js";
export const CAPTURE_DECODER_OLD_GENERATION_SETTING =
@@ -61,11 +60,6 @@ export const parseCaptureDecoderHeap = (
export const resolveCaptureDecoderHeap = (
environment: Readonly<Record<string, string | undefined>>,
): CaptureDecoderHeap => {
const override = currentAnalysisSettingOverride(
"capture.decoder_old_generation_mib",
);
if (override !== undefined)
return { oldGenerationMiB: override, source: "runtime" };
const parsed = parseCaptureDecoderHeap(
environment[CAPTURE_DECODER_OLD_GENERATION_SETTING],
);
@@ -77,7 +71,7 @@ export const resolveCaptureDecoderHeap = (
export const captureDecoderHeapRecovery = (heap: CaptureDecoderHeap): string =>
[
`The effective ${CAPTURE_DECODER_OLD_GENERATION_SETTING} value is ${String(heap.oldGenerationMiB)} MiB (source ${heap.source}; default ${String(DEFAULT_CAPTURE_DECODER_OLD_GENERATION_MIB)} MiB, floor ${String(CAPTURE_DECODER_SEMI_SPACE_MIB)} MiB, maximum ${String(MAXIMUM_CAPTURE_DECODER_OLD_GENERATION_MIB)} MiB).`,
`capture.decoder_old_generation_mib can be changed at runtime with update_analysis_settings. The launch seed is ${CAPTURE_DECODER_OLD_GENERATION_SETTING}, a decimal integer from ${String(CAPTURE_DECODER_SEMI_SPACE_MIB)} to ${String(MAXIMUM_CAPTURE_DECODER_OLD_GENERATION_MIB)}.`,
`Set ${CAPTURE_DECODER_OLD_GENERATION_SETTING} in the environment that starts REA to a decimal integer from ${String(CAPTURE_DECODER_SEMI_SPACE_MIB)} to ${String(MAXIMUM_CAPTURE_DECODER_OLD_GENERATION_MIB)}, then restart REA. In a running MCP server, use update_analysis_settings with setting capture.decoder_old_generation_mib and action set instead; the new value applies to new work without a REA restart.`,
`On Node.js the decoder passes --max-old-space-size=${String(heap.oldGenerationMiB)} and removes inherited NODE_OPTIONS, so additional host memory and NODE_OPTIONS do not raise this heap. The ${String(CAPTURE_DECODER_SEMI_SPACE_MIB)} MiB semi-space stays fixed.`,
"On Bun the V8 flags are ignored. Windows limits the decoder's committed memory to this value plus a 512 MiB reserve. Other Bun hosts enforce no decoder heap limit.",
].join(" ");
+1 -34
View File
@@ -15,7 +15,6 @@ import {
} from "../domain/analysisResourceLimits.js";
import { GdbSessionManager } from "../gdb/GdbSessionManager.js";
import { RizinDebugSessionManager } from "../rizin/RizinDebugSessionManager.js";
import { runWithAnalysisOperationSnapshot } from "../domain/analysisOperationSnapshot.js";
import { parseEnvironment } from "./environment.js";
import {
DEBUGGER_COMMAND_DEADLINE_SETTING,
@@ -147,37 +146,6 @@ it.each([
},
);
it("applies runtime debugger overrides before rejecting invalid launch values", () => {
const parsed = runWithAnalysisOperationSnapshot(
{
revision: 1,
overrides: {
"debugger.command_deadline_ms": 25,
"debugger.max_command_output_bytes": 4096,
},
},
() =>
parseDebuggerSessionLimits({
[DEBUGGER_COMMAND_DEADLINE_SETTING]: "nope",
[DEBUGGER_MAX_COMMAND_OUTPUT_BYTES_SETTING]: "0",
}),
);
if (!parsed.ok) throw parsed.error;
expect(parsed.value.commandDeadline.source).toBe("default");
expect(parsed.value.maxCommandOutputBytes.source).toBe("default");
const sessions = runWithAnalysisOperationSnapshot(
{
revision: 1,
overrides: { "debugger.command_deadline_ms": 25 },
},
() =>
parseDebuggerSessionLimits({
[DEBUGGER_MAX_SESSIONS_SETTING]: "nope",
}),
);
expect(sessions.ok).toBe(false);
});
it("names the session setting on a Rizin capacity refusal", async () => {
const manager = new RizinDebugSessionManager(
{ [DEBUGGER_MAX_SESSIONS_SETTING]: "1" },
@@ -203,8 +171,7 @@ it("names the session setting on a Rizin capacity refusal", async () => {
minimum: MINIMUM_DEBUGGER_MAX_SESSIONS,
maximum: MAXIMUM_DEBUGGER_MAX_SESSIONS,
unit: "sessions",
update_mode: "restart",
pending_restart: false,
restart_required: true,
max_sessions: 1,
active_or_reserved_sessions: 1,
},
+8 -25
View File
@@ -1,5 +1,3 @@
import { currentAnalysisOperationSnapshot } from "../domain/analysisOperationSnapshot.js";
import type { AnalysisSettingValues } from "../domain/analysisSettings.js";
import {
DEBUGGER_CONTROL_REPLY_BYTES,
DEFAULT_DEBUGGER_COMMAND_DEADLINE_MS,
@@ -36,7 +34,7 @@ export const DEBUGGER_MAX_COMMAND_OUTPUT_BYTES_SETTING =
export interface DebuggerAdmissionLimit {
readonly setting: string;
readonly value: number;
readonly source: "default" | "setting" | "runtime";
readonly source: "default" | "setting";
readonly defaultValue: number;
readonly minimum: number;
readonly maximum: number;
@@ -150,12 +148,6 @@ export interface DebuggerSessionLimits {
readonly maxCommandOutputBytes: DebuggerAdmissionLimit;
}
const RUNTIME_DEBUGGER_OVERRIDES = {
[DEBUGGER_COMMAND_DEADLINE_SETTING]: "debugger.command_deadline_ms",
[DEBUGGER_MAX_COMMAND_OUTPUT_BYTES_SETTING]:
"debugger.max_command_output_bytes",
} as const satisfies Partial<Record<string, keyof AnalysisSettingValues>>;
const parseOne = (
admission: DebuggerAdmissionLimit,
schema: ReturnType<typeof positiveDecimalIntegerSchema>,
@@ -164,22 +156,13 @@ const parseOne = (
): Result<DebuggerAdmissionLimit, ConfigurationError> => {
if (value === undefined) return ok(admission);
const parsed = schema.safeParse(value);
if (parsed.success)
return ok({ ...admission, value: parsed.data, source: "setting" });
const override =
RUNTIME_DEBUGGER_OVERRIDES[
admission.setting as keyof typeof RUNTIME_DEBUGGER_OVERRIDES
];
if (
override !== undefined &&
currentAnalysisOperationSnapshot()?.overrides[override] !== undefined
)
return ok(admission);
return err(
new ConfigurationError(constraint, {
settings: [{ setting: admission.setting, constraint }],
}),
);
return parsed.success
? ok({ ...admission, value: parsed.data, source: "setting" })
: err(
new ConfigurationError(constraint, {
settings: [{ setting: admission.setting, constraint }],
}),
);
};
/** Resolve every shared debugger cap from one environment. Invalid values are refused. */
+5 -33
View File
@@ -6,10 +6,7 @@ import { z } from "zod";
import { mcpInputBudgetSchema } from "./mcpInputBudget.js";
import { isAbsolute } from "node:path";
import {
ConfigurationError,
type ConfigurationSettingIssue,
} from "../domain/configurationErrors.js";
import { ConfigurationError } from "../domain/configurationErrors.js";
import { err, ok, type Result } from "../domain/result.js";
import { analysisProviderSelectorSchema } from "../domain/providerSelection.js";
import {
@@ -168,31 +165,6 @@ const environmentSchema = z.object({
REA_REFERENCE_SECRET_PATTERNS_JSON: z.string().default("[]"),
});
/** Copy producer-owned update facts from a Zod issue without inventing them. */
const configurationSettingFromIssue = (issue: {
readonly path: readonly PropertyKey[];
readonly message: string;
readonly code: string;
readonly params?: Readonly<Record<string, unknown>> | undefined;
}): ConfigurationSettingIssue => {
const setting = String(issue.path[0] ?? "environment");
const constraint = issue.message;
if (issue.code !== "custom" || issue.params === undefined)
return { setting, constraint };
const updateMode = issue.params.update_mode;
const pendingRestart = issue.params.pending_restart;
return {
setting,
constraint,
...(updateMode === "runtime" || updateMode === "restart"
? { update_mode: updateMode }
: {}),
...(typeof pendingRestart === "boolean"
? { pending_restart: pendingRestart }
: {}),
};
};
/** Parse the JEB-only timeout at its isolated provider-composition boundary. */
export const parseJebMcpTimeoutMs = (
value: string | undefined,
@@ -257,10 +229,10 @@ export const parseEnvironmentValues = (
const parsed = environmentSchema.safeParse(literals);
if (!parsed.success) {
// Zod messages name the constraint, never the rejected value.
// A producer may attach update_mode and pending_restart on a custom issue.
const settings = parsed.error.issues.map((issue) =>
configurationSettingFromIssue(issue),
);
const settings = parsed.error.issues.map((issue) => ({
setting: String(issue.path[0] ?? "environment"),
constraint: issue.message,
}));
return err(
new ConfigurationError("Invalid REA environment configuration", {
cause: parsed.error,
+18 -12
View File
@@ -13,7 +13,6 @@ import {
import { parseConfig } from "./parseConfig.js";
import {
HOPPER_CALLER_SCAN_BUDGET_SETTING,
hopperCallerScanLimit,
hopperCallerScanRecovery,
} from "./hopperCallerScanBudget.js";
@@ -58,16 +57,21 @@ it("explains the time budget and that fixed byte allowances have no override", (
const parsed = parseConfig({ [HOPPER_CALLER_SCAN_BUDGET_SETTING]: "250" });
if (!parsed.ok) throw parsed.error;
const budget = parsed.value.hopperCallerScanBudget;
expect(hopperCallerScanLimit(budget)).toMatchObject({
setting: "hopper.caller_scan_budget_ms",
effective_value: 250,
source: "setting",
minimum: MINIMUM_HOPPER_CALLER_SCAN_BUDGET_MS,
maximum: MAX_NODE_TIMER_DELAY_MS,
update_mode: "runtime",
pending_restart: false,
});
expect(DEFAULT_HOPPER_CALLER_SCAN_BUDGET_MS).toBe(2000);
const time = hopperCallerScanRecovery(
facts({ stop: { code: "work_budget", budget_ms: 250 } }),
budget,
);
expect(time).toContain(`${HOPPER_CALLER_SCAN_BUDGET_SETTING} is 250 ms`);
expect(time).toContain(
`The default is ${String(DEFAULT_HOPPER_CALLER_SCAN_BUDGET_MS)} ms`,
);
expect(time).toContain(
`from ${String(MINIMUM_HOPPER_CALLER_SCAN_BUDGET_MS)} to ${String(MAX_NODE_TIMER_DELAY_MS)} ms`,
);
expect(time).toContain("no runtime override");
expect(time).toContain(
`${HOPPER_CALLER_SCAN_BUDGET_SETTING} does not raise them`,
);
const page = hopperCallerScanRecovery(
facts({
stop: {
@@ -78,7 +82,9 @@ it("explains the time budget and that fixed byte allowances have no override", (
budget,
);
expect(page).toContain("no runtime override");
expect(page).toContain("hopper.caller_scan_budget_ms does not raise them");
expect(page).toContain(
`${HOPPER_CALLER_SCAN_BUDGET_SETTING} does not raise them`,
);
expect(page).not.toContain("is 250 ms");
expect(
hopperCallerScanRecovery(facts({ stop: { code: "complete" } }), budget),
+4 -15
View File
@@ -1,4 +1,3 @@
import { analysisSettingLimit } from "../domain/analysisSettingDefinitions.js";
import {
DEFAULT_HOPPER_CALLER_SCAN_BUDGET_MS,
MAX_NODE_TIMER_DELAY_MS,
@@ -17,7 +16,7 @@ export const HOPPER_CALLER_SCAN_BUDGET_SETTING =
export interface HopperCallerScanBudget {
readonly setting: typeof HOPPER_CALLER_SCAN_BUDGET_SETTING;
readonly milliseconds: number;
readonly source: "default" | "setting" | "runtime";
readonly source: "default" | "setting";
readonly defaultMilliseconds: number;
readonly floorMilliseconds: number;
readonly maximumMilliseconds: number;
@@ -53,15 +52,7 @@ export const hopperCallerScanBudgetForMilliseconds = (
milliseconds,
);
const fixedAllowanceRecovery = `This release has no runtime override for the ${String(HOPPER_CALLER_PAGE_ALLOWANCE_BYTES)} byte instruction, native-caller, and page allowances or the ${String(HOPPER_CALLER_SCAN_RETENTION_BYTES)} byte retention budget. hopper.caller_scan_budget_ms does not raise them.`;
/** Structured facts for the scan time budget. The sentence is display text. */
export const hopperCallerScanLimit = (budget: HopperCallerScanBudget) =>
analysisSettingLimit(
"hopper.caller_scan_budget_ms",
budget.milliseconds,
budget.source,
);
const fixedAllowanceRecovery = `This release has no runtime override for the ${String(HOPPER_CALLER_PAGE_ALLOWANCE_BYTES)} byte instruction, native-caller, and page allowances or the ${String(HOPPER_CALLER_SCAN_RETENTION_BYTES)} byte retention budget. ${HOPPER_CALLER_SCAN_BUDGET_SETTING} does not raise them.`;
/** Environment recovery for a caller-scan limit. Domain facts stay separate. */
export const hopperCallerScanRecovery = (
@@ -73,9 +64,7 @@ export const hopperCallerScanRecovery = (
facts.stop.code === "retention_budget" ||
!facts.instructions_complete ||
facts.native_callers_complete === false;
if (facts.stop.code === "work_budget") {
const limit = hopperCallerScanLimit(budget);
return `hopper.caller_scan_budget_ms is ${String(limit.effective_value)} ms (source ${limit.source ?? "unknown"}; default ${String(budget.defaultMilliseconds)} ms, floor ${String(limit.minimum)} ms, maximum ${String(limit.maximum)} ms). It can be changed at runtime with update_analysis_settings. The launch seed is ${budget.setting}. ${fixedAllowanceRecovery}`;
}
if (facts.stop.code === "work_budget")
return `${budget.setting} is ${String(budget.milliseconds)} ms (source: ${budget.source}). The default is ${String(budget.defaultMilliseconds)} ms. Allowed values are integers from ${String(budget.floorMilliseconds)} to ${String(budget.maximumMilliseconds)} ms. Set it in the environment that starts REA and restart REA. ${fixedAllowanceRecovery}`;
return fixedLimit ? fixedAllowanceRecovery : "";
};
-4
View File
@@ -196,10 +196,6 @@ export const parseConfig = (
homeDirectoryFromEnvironment(environment, process.platform),
),
hopperStartupTimeoutMs: env.REA_HOPPER_STARTUP_TIMEOUT_MS,
hopperStartupTimeoutSource:
environment.REA_HOPPER_STARTUP_TIMEOUT_MS === undefined
? "default"
: "setting",
hopperCallerScanBudget: hopperCallerScanBudgetFromEnvironment(
environment.REA_HOPPER_CALLER_SCAN_BUDGET_MS,
env.REA_HOPPER_CALLER_SCAN_BUDGET_MS,
+3 -3
View File
@@ -49,8 +49,7 @@ it("keeps the 5000 ms deadline when the setting is unset", () => {
applies_at: "next_operation",
agent_editable: true,
scope: "rea-runtime",
update_mode: "runtime",
pending_restart: false,
rea_restart_required: false,
},
reported_limits: {
matching_deadline_ms: DEFAULT_REGEX_MATCHING_DEADLINE.deadlineMs,
@@ -66,7 +65,8 @@ it("keeps the 5000 ms deadline when the setting is unset", () => {
expect(projected.remediation.action).toContain("source default");
expect(projected.remediation.action).toContain("update_analysis_settings");
expect(projected.remediation.action).toContain("regex.matching_deadline_ms");
expect(projected.remediation.action).toContain("launch seed");
expect(projected.remediation.action).toMatch(/restart REA/i);
expect(projected.remediation.action).toMatch(/without a REA restart/i);
expect(projected.remediation.action).toMatch(
/heap does not raise this deadline/i,
);
+1 -5
View File
@@ -5,7 +5,6 @@ import {
import { positiveDecimalIntegerSchema } from "./positiveDecimalInteger.js";
import { ConfigurationError } from "../domain/configurationErrors.js";
import { currentAnalysisSettingOverride } from "../domain/analysisOperationSnapshot.js";
import { err, ok, type Result } from "../domain/result.js";
export const REGEX_MATCHING_DEADLINE_SETTING = "REA_REGEX_MATCHING_DEADLINE_MS";
@@ -58,9 +57,6 @@ export const parseRegexMatchingDeadline = (
export const resolveRegexMatchingDeadline = (
environment: Readonly<Record<string, string | undefined>>,
): RegexMatchingDeadline => {
const override = currentAnalysisSettingOverride("regex.matching_deadline_ms");
if (override !== undefined)
return { deadlineMs: override, source: "runtime" };
const parsed = parseRegexMatchingDeadline(
environment[REGEX_MATCHING_DEADLINE_SETTING],
);
@@ -74,6 +70,6 @@ export const regexMatchingDeadlineRecovery = (
): string =>
[
`The effective ${REGEX_MATCHING_DEADLINE_SETTING} value is ${String(deadline.deadlineMs)} ms (source ${deadline.source}; default ${String(DEFAULT_REGEX_MATCHING_DEADLINE_MS)} ms, floor 1 ms, maximum ${String(MAXIMUM_REGEX_MATCHING_DEADLINE_MS)} ms).`,
`regex.matching_deadline_ms can be changed at runtime with update_analysis_settings. The launch seed is ${REGEX_MATCHING_DEADLINE_SETTING}, a positive decimal integer from 1 to ${String(MAXIMUM_REGEX_MATCHING_DEADLINE_MS)}.`,
`Set ${REGEX_MATCHING_DEADLINE_SETTING} in the environment that starts REA to a positive decimal integer from 1 to ${String(MAXIMUM_REGEX_MATCHING_DEADLINE_MS)}, then restart REA. In a running MCP server, use update_analysis_settings with setting regex.matching_deadline_ms and action set instead; the new value applies to new work without a REA restart.`,
"The worker old-generation heap does not raise this deadline.",
].join(" ");
+3 -3
View File
@@ -56,8 +56,7 @@ it("keeps the 128 MiB request when the setting is unset", () => {
applies_at: "next_worker",
agent_editable: true,
scope: "rea-runtime",
update_mode: "runtime",
pending_restart: false,
rea_restart_required: false,
},
reported_limits: {
requested_worker_heap_mib: DEFAULT_REGEX_WORKER_HEAP_REQUEST.heapMiB,
@@ -77,7 +76,8 @@ it("keeps the 128 MiB request when the setting is unset", () => {
);
expect(projected.remediation.action).toContain("--max-old-space-size");
expect(projected.remediation.action).toContain("NODE_OPTIONS");
expect(projected.remediation.action).toContain("launch seed");
expect(projected.remediation.action).toMatch(/restart REA/i);
expect(projected.remediation.action).toMatch(/without a REA restart/i);
expect(projected.remediation.action).toMatch(/overrides this request/i);
});
+1 -6
View File
@@ -6,7 +6,6 @@ import {
import { positiveDecimalIntegerSchema } from "./positiveDecimalInteger.js";
import { ConfigurationError } from "../domain/configurationErrors.js";
import { currentAnalysisSettingOverride } from "../domain/analysisOperationSnapshot.js";
import { err, ok, type Result } from "../domain/result.js";
export const REGEX_WORKER_HEAP_REQUEST_SETTING =
@@ -60,10 +59,6 @@ export const parseRegexWorkerHeapRequest = (
export const resolveRegexWorkerHeapRequest = (
environment: Readonly<Record<string, string | undefined>>,
): RegexWorkerHeapRequest => {
const override = currentAnalysisSettingOverride(
"regex.worker_heap_request_mib",
);
if (override !== undefined) return { heapMiB: override, source: "runtime" };
const parsed = parseRegexWorkerHeapRequest(
environment[REGEX_WORKER_HEAP_REQUEST_SETTING],
);
@@ -77,6 +72,6 @@ export const regexWorkerHeapRequestRecovery = (
): string =>
[
`The effective ${REGEX_WORKER_HEAP_REQUEST_SETTING} value is ${String(request.heapMiB)} MiB (source ${request.source}; default ${String(DEFAULT_REGEX_WORKER_HEAP_REQUEST_MIB)} MiB, floor ${String(MINIMUM_REGEX_WORKER_HEAP_REQUEST_MIB)} MiB, maximum ${String(MAXIMUM_REGEX_WORKER_HEAP_REQUEST_MIB)} MiB).`,
`regex.worker_heap_request_mib can be changed at runtime with update_analysis_settings. The launch seed is ${REGEX_WORKER_HEAP_REQUEST_SETTING}, a decimal integer from ${String(MINIMUM_REGEX_WORKER_HEAP_REQUEST_MIB)} to ${String(MAXIMUM_REGEX_WORKER_HEAP_REQUEST_MIB)}.`,
`Set ${REGEX_WORKER_HEAP_REQUEST_SETTING} in the environment that starts REA to a decimal integer from ${String(MINIMUM_REGEX_WORKER_HEAP_REQUEST_MIB)} to ${String(MAXIMUM_REGEX_WORKER_HEAP_REQUEST_MIB)}, then restart REA. In a running MCP server, use update_analysis_settings with setting regex.worker_heap_request_mib and action set instead; the new value applies to new work without a REA restart.`,
"A positive Node --max-old-space-size, including NODE_OPTIONS, overrides this request and sets the effective heap. That flag is not limited by this setting's floor or maximum. Raising host memory alone does not raise the request.",
].join(" ");
+7 -61
View File
@@ -1,5 +1,4 @@
import type { AnalysisSettingsReader } from "../domain/analysisSettings.js";
import { currentAnalysisSettingOverride } from "../domain/analysisOperationSnapshot.js";
import type { ConfigurationError } from "../domain/configurationErrors.js";
import { ok, type Result } from "../domain/result.js";
import type { AppConfig } from "./types.js";
@@ -8,12 +7,8 @@ import {
DEFAULT_GHIDRA_VALUE_FLOW_MAX_INPUTS_PER_OPERATION,
DEFAULT_GHIDRA_VALUE_FLOW_MAX_EDGES,
} from "../domain/analysisResourceLimits.js";
import { parseAsarListingAdmission } from "./asarListingAdmission.js";
import {
parseAsarListingAdmission,
DEFAULT_ASAR_LISTING_ADMISSION,
} from "./asarListingAdmission.js";
import {
DEFAULT_ASAR_HEADER_ADMISSION,
parseAsarHeaderAdmission,
type AsarHeaderAdmission,
} from "./asarHeaderAdmission.js";
@@ -34,8 +29,6 @@ export const runtimeAnalysisConfig = (
if (read === undefined) return config;
const deadline = config.regexMatchingDeadline;
const heap = config.regexWorkerHeapRequest;
const scan = config.hopperCallerScanBudget;
const bridge = config.bridgeMaxRequestBytes;
return {
...config,
get ghidraStartupTimeoutMs() {
@@ -50,11 +43,6 @@ export const runtimeAnalysisConfig = (
config.hopperStartupTimeoutMs
);
},
get hopperStartupTimeoutSource() {
return read().overrides["hopper.startup_timeout_ms"] === undefined
? config.hopperStartupTimeoutSource
: "runtime";
},
get ghidraDecompileTimeoutSeconds() {
const value = read().overrides["ghidra.decompile_timeout_seconds"];
return value === undefined
@@ -116,31 +104,6 @@ export const runtimeAnalysisConfig = (
: "runtime";
},
},
hopperCallerScanBudget: {
...scan,
get milliseconds() {
return (
read().overrides["hopper.caller_scan_budget_ms"] ?? scan.milliseconds
);
},
get source() {
return read().overrides["hopper.caller_scan_budget_ms"] === undefined
? scan.source
: "runtime";
},
},
bridgeMaxRequestBytes: {
get maximumBytes() {
return (
read().overrides["bridge.max_request_bytes"] ?? bridge.maximumBytes
);
},
get source() {
return read().overrides["bridge.max_request_bytes"] === undefined
? bridge.source
: "runtime";
},
},
};
};
export const runtimeAsarHeaderAdmission = (
@@ -150,42 +113,25 @@ export const runtimeAsarHeaderAdmission = (
const parsed = parseAsarHeaderAdmission(
environment.REA_ASAR_MAX_HEADER_BYTES,
);
const headerOverride =
read?.().overrides["asar.max_header_bytes"] ??
currentAnalysisSettingOverride("asar.max_header_bytes");
if (!parsed.ok && headerOverride === undefined) throw parsed.error;
if (!parsed.ok) throw parsed.error;
const listing = parseAsarListingAdmission(
environment.REA_ASAR_MAX_LISTING_BYTES,
);
const listingOverride =
read?.().overrides["asar.max_listing_bytes"] ??
currentAnalysisSettingOverride("asar.max_listing_bytes");
if (!listing.ok && listingOverride === undefined) throw listing.error;
const baseline = parsed.ok ? parsed.value : DEFAULT_ASAR_HEADER_ADMISSION;
const listingBaseline = listing.ok
? listing.value
: DEFAULT_ASAR_LISTING_ADMISSION;
if (!listing.ok) throw listing.error;
const baseline = parsed.value;
return read === undefined
? {
maximumBytes: headerOverride ?? baseline.maximumBytes,
source: headerOverride === undefined ? baseline.source : "runtime",
listing: {
maximumBytes: listingOverride ?? listingBaseline.maximumBytes,
source:
listingOverride === undefined ? listingBaseline.source : "runtime",
},
}
? { ...baseline, listing: listing.value }
: {
listing: {
get maximumBytes() {
return (
read().overrides["asar.max_listing_bytes"] ??
listingBaseline.maximumBytes
listing.value.maximumBytes
);
},
get source() {
return read().overrides["asar.max_listing_bytes"] === undefined
? listingBaseline.source
? listing.value.source
: "runtime";
},
},
-2
View File
@@ -41,8 +41,6 @@ export interface AppConfig {
readonly ilspyCmdPath: string | undefined;
readonly hopperLauncherPath: string;
readonly hopperStartupTimeoutMs: number;
/** Whether `hopperStartupTimeoutMs` came from the launch environment. */
readonly hopperStartupTimeoutSource: "default" | "setting" | "runtime";
/** Cooperative per-operation budget for forward caller reconciliation. */
readonly hopperCallerScanBudget: HopperCallerScanBudget;
readonly hopperTargetPath: string | undefined;
+8 -44
View File
@@ -2,13 +2,11 @@ import { expect, it } from "vitest";
import { ConfigurationError } from "../domain/configurationErrors.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { runWithAnalysisOperationSnapshot } from "../domain/analysisOperationSnapshot.js";
import { parseEnvironment } from "./environment.js";
import {
WINDOWS_CHANGE_TIME_DRIFT_MS,
WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS,
WINDOWS_CHANGE_TIME_SETTLE_POLL_MS,
applyWindowsChangeTimeSnapshot,
resolveWindowsChangeTimeSettle,
} from "./windowsChangeTimeSettle.js";
@@ -29,7 +27,7 @@ const requiredFacts = (
expect(message).toContain(String(fact.defaultValue));
expect(message).toContain(String(fact.floor));
expect(message).toContain(String(fact.maximum));
expect(message).toContain("positive decimal integer");
expect(message).toContain("restart REA");
};
it("keeps today's settle defaults when the settings are unset", () => {
@@ -100,51 +98,17 @@ it.each([
const message = resolved.error.settings[0]?.constraint;
if (message === undefined) throw new Error("Missing configuration guidance");
requiredFacts(message, { ...bound, currentValue: value, source: "setting" });
const facts = {
setting: bound.setting,
constraint: message,
update_mode: "runtime" as const,
pending_restart: true as const,
};
expect(resolved.error.settings).toEqual([facts]);
expect(resolved.error.settings).toEqual([
{ setting: bound.setting, constraint: message },
]);
expect(projectAnalysisError(resolved.error)).toMatchObject({
code: "configuration_invalid",
details: { settings: [facts] },
details: { settings: [{ setting: bound.setting, constraint: message }] },
});
const startup = parseEnvironment({ [bound.setting]: value });
if (startup.ok) throw new Error(`Startup accepted ${bound.setting}=${value}`);
expect(startup.error.settings).toContainEqual(facts);
expect(projectAnalysisError(startup.error).details).toMatchObject({
settings: [facts],
});
});
it("applies a Windows settle override before rejecting the launch value", () => {
const limits = runWithAnalysisOperationSnapshot(
{
revision: 1,
overrides: {
"windows.change_time_drift_ms": 50,
"windows.change_time_settle_poll_ms": 10,
"windows.change_time_settle_max_polls": 4,
},
},
() => {
const resolved = resolveWindowsChangeTimeSettle({
REA_WINDOWS_CHANGE_TIME_DRIFT_MS: "nope",
REA_WINDOWS_CHANGE_TIME_SETTLE_POLL_MS: "0",
REA_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS: "1001",
});
if (!resolved.ok) throw resolved.error;
return applyWindowsChangeTimeSnapshot(resolved.value);
},
);
expect(limits).toMatchObject({
driftMs: 50,
driftSource: "runtime",
pollMs: 10,
pollSource: "runtime",
maxPolls: 4,
maxPollsSource: "runtime",
expect(startup.error.settings).toContainEqual({
setting: bound.setting,
constraint: message,
});
});
+20 -68
View File
@@ -1,14 +1,5 @@
import { z } from "zod";
import { currentAnalysisOperationSnapshot } from "../domain/analysisOperationSnapshot.js";
import {
DEFAULT_WINDOWS_CHANGE_TIME_DRIFT_MS,
DEFAULT_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS,
DEFAULT_WINDOWS_CHANGE_TIME_SETTLE_POLL_MS,
MAXIMUM_WINDOWS_CHANGE_TIME_DRIFT_MS,
MAXIMUM_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS,
MAXIMUM_WINDOWS_CHANGE_TIME_SETTLE_POLL_MS,
} from "../domain/analysisResourceLimits.js";
import { ConfigurationError } from "../domain/configurationErrors.js";
import { err, ok, type Result } from "../domain/result.js";
@@ -28,18 +19,18 @@ interface WindowsChangeTimeBound {
*/
export const WINDOWS_CHANGE_TIME_DRIFT_MS = {
setting: "REA_WINDOWS_CHANGE_TIME_DRIFT_MS",
defaultValue: DEFAULT_WINDOWS_CHANGE_TIME_DRIFT_MS,
defaultValue: 2_000,
floor: 1,
maximum: MAXIMUM_WINDOWS_CHANGE_TIME_DRIFT_MS,
maximum: 60_000,
unit: "ms",
} as const satisfies WindowsChangeTimeBound;
/** Delay between ChangeTime observations. Unset keeps the 25 ms poll. */
export const WINDOWS_CHANGE_TIME_SETTLE_POLL_MS = {
setting: "REA_WINDOWS_CHANGE_TIME_SETTLE_POLL_MS",
defaultValue: DEFAULT_WINDOWS_CHANGE_TIME_SETTLE_POLL_MS,
defaultValue: 25,
floor: 1,
maximum: MAXIMUM_WINDOWS_CHANGE_TIME_SETTLE_POLL_MS,
maximum: 1_000,
unit: "ms",
} as const satisfies WindowsChangeTimeBound;
@@ -49,9 +40,9 @@ export const WINDOWS_CHANGE_TIME_SETTLE_POLL_MS = {
*/
export const WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS = {
setting: "REA_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS",
defaultValue: DEFAULT_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS,
defaultValue: 40,
floor: 1,
maximum: MAXIMUM_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS,
maximum: 1_000,
unit: "polls",
} as const satisfies WindowsChangeTimeBound;
@@ -64,19 +55,17 @@ export interface WindowsChangeTimeSettingFailure {
readonly floor: number;
readonly maximum: number;
readonly unit: "ms" | "polls";
readonly update_mode: "runtime";
/** The rejected environment value is not the value this process is using. */
readonly pending_restart: true;
readonly restartRequired: true;
}
/** Effective settle budget and whether each part came from the default or a setting. */
export interface WindowsChangeTimeSettleLimits {
readonly driftMs: number;
readonly driftSource: "default" | "setting" | "runtime";
readonly driftSource: "default" | "setting";
readonly pollMs: number;
readonly pollSource: "default" | "setting" | "runtime";
readonly pollSource: "default" | "setting";
readonly maxPolls: number;
readonly maxPollsSource: "default" | "setting" | "runtime";
readonly maxPollsSource: "default" | "setting";
}
const windowsChangeTimeSettingFailure = (
@@ -90,15 +79,14 @@ const windowsChangeTimeSettingFailure = (
floor: bound.floor,
maximum: bound.maximum,
unit: bound.unit,
update_mode: "runtime",
pending_restart: true,
restartRequired: true,
});
/** Name the rejected value, its source, and the allowed range. */
/** Name the rejected value, its source, the allowed range, and the required restart. */
const windowsChangeTimeSettingMessage = (
failure: WindowsChangeTimeSettingFailure,
): string =>
`The effective ${failure.setting} value is ${failure.currentValue} ${failure.unit} (source ${failure.source}; default ${String(failure.defaultValue)} ${failure.unit}, floor ${String(failure.floor)} ${failure.unit}, maximum ${String(failure.maximum)} ${failure.unit}). windows change-time settings can be changed at runtime with update_analysis_settings. The launch seed ${failure.setting} must be a positive decimal integer from ${String(failure.floor)} to ${String(failure.maximum)}. The rejected value is not active.`;
`The effective ${failure.setting} value is ${failure.currentValue} ${failure.unit} (source ${failure.source}; default ${String(failure.defaultValue)} ${failure.unit}, floor ${String(failure.floor)} ${failure.unit}, maximum ${String(failure.maximum)} ${failure.unit}). Set ${failure.setting} in the environment that starts REA to a positive decimal integer from ${String(failure.floor)} to ${String(failure.maximum)}, then restart REA.`;
const inRange = (bound: WindowsChangeTimeBound, value: string): boolean => {
if (!decimalInteger.test(value)) return false;
@@ -112,14 +100,11 @@ export const windowsChangeTimeSettingSchema = (bound: WindowsChangeTimeBound) =>
.string()
.superRefine((value, context) => {
if (inRange(bound, value)) return;
const failure = windowsChangeTimeSettingFailure(bound, value);
context.addIssue({
code: "custom",
message: windowsChangeTimeSettingMessage(failure),
params: {
update_mode: failure.update_mode,
pending_restart: failure.pending_restart,
},
message: windowsChangeTimeSettingMessage(
windowsChangeTimeSettingFailure(bound, value),
),
});
})
.transform(Number);
@@ -132,14 +117,6 @@ export const windowsChangeTimeSettlePollMsSchema =
export const windowsChangeTimeSettleMaxPollsSchema =
windowsChangeTimeSettingSchema(WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS);
const WINDOWS_CHANGE_TIME_OVERRIDES = {
[WINDOWS_CHANGE_TIME_DRIFT_MS.setting]: "windows.change_time_drift_ms",
[WINDOWS_CHANGE_TIME_SETTLE_POLL_MS.setting]:
"windows.change_time_settle_poll_ms",
[WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS.setting]:
"windows.change_time_settle_max_polls",
} as const;
const readBound = (
bound: WindowsChangeTimeBound,
environment: Readonly<Record<string, string | undefined>>,
@@ -148,15 +125,11 @@ const readBound = (
const value = environment[bound.setting];
if (value === undefined) return undefined;
const parsed = windowsChangeTimeSettingSchema(bound).safeParse(value);
if (parsed.success) return parsed.data;
const override =
WINDOWS_CHANGE_TIME_OVERRIDES[
bound.setting as keyof typeof WINDOWS_CHANGE_TIME_OVERRIDES
];
if (currentAnalysisOperationSnapshot()?.overrides[override] !== undefined)
if (!parsed.success) {
failures.push(windowsChangeTimeSettingFailure(bound, value));
return undefined;
failures.push(windowsChangeTimeSettingFailure(bound, value));
return undefined;
}
return parsed.data;
};
/** Resolve the settle budget from one environment. Invalid values are refused. */
@@ -187,8 +160,6 @@ export const resolveWindowsChangeTimeSettle = (
settings: failures.map((failure) => ({
setting: failure.setting,
constraint: windowsChangeTimeSettingMessage(failure),
update_mode: failure.update_mode,
pending_restart: failure.pending_restart,
})),
},
),
@@ -202,22 +173,3 @@ export const resolveWindowsChangeTimeSettle = (
maxPollsSource: maxPolls === undefined ? "default" : "setting",
});
};
/** Apply the accepted operation snapshot. An explicit caller budget is left alone. */
export const applyWindowsChangeTimeSnapshot = (
limits: WindowsChangeTimeSettleLimits,
): WindowsChangeTimeSettleLimits => {
const overrides = currentAnalysisOperationSnapshot()?.overrides;
if (overrides === undefined) return limits;
const driftMs = overrides["windows.change_time_drift_ms"];
const pollMs = overrides["windows.change_time_settle_poll_ms"];
const maxPolls = overrides["windows.change_time_settle_max_polls"];
return {
driftMs: driftMs ?? limits.driftMs,
driftSource: driftMs === undefined ? limits.driftSource : "runtime",
pollMs: pollMs ?? limits.pollMs,
pollSource: pollMs === undefined ? limits.pollSource : "runtime",
maxPolls: maxPolls ?? limits.maxPolls,
maxPollsSource: maxPolls === undefined ? limits.maxPollsSource : "runtime",
};
};
+1 -3
View File
@@ -48,8 +48,6 @@ const descriptor = z.strictObject({
minimum: z.number(),
maximum: z.number(),
applies_at: z.enum(["next_operation", "next_worker", "next_startup"]),
update_mode: z.enum(["runtime", "restart"]),
pending_restart: z.boolean(),
provider: z.string(),
operations: z.array(z.string()),
environment: z.string().optional(),
@@ -85,7 +83,7 @@ export const updateAnalysisSettingsResultSchema =
}),
),
active_work_changed: z.literal(false),
pending_restart: z.literal(false),
rea_restart_required: z.literal(false),
});
export const ANALYSIS_SETTINGS_TOOL_CONTRACTS = [
{
+13 -49
View File
@@ -46,16 +46,12 @@ import { readBoundedFileBytes } from "../process/BoundedFileBytes.js";
import { homeDirectoryFromEnvironment } from "../config/homeDirectory.js";
import { appendCallerPath } from "../filesystem/callerPath.js";
import {
BRIDGE_MAX_REQUEST_BYTES_SETTING,
bridgeRequestLimitMismatchError,
bridgeRequestOversizeError,
DEFAULT_BRIDGE_MAX_REQUEST_BYTES,
parseBridgeMaxRequestBytes,
resolveBridgeMaxRequestBytes,
type BridgeMaxRequestBytes,
} from "../config/bridgeMaxRequestBytes.js";
import { ConfigurationError } from "../domain/configurationErrors.js";
import { currentAnalysisOperationSnapshot } from "../domain/analysisOperationSnapshot.js";
import type { AnalysisSettingsReader } from "../domain/analysisSettings.js";
const descriptorSchema = z.object({
...cutterSessionSchema.shape,
@@ -258,15 +254,11 @@ export interface WindowsCutterDescriptorReader {
interface CutterBridgeClientOptions {
readonly platform?: NodeJS.Platform;
readonly windowsPrivateReader?: WindowsCutterDescriptorReader;
/** Accepted runtime overrides. The operation snapshot wins when one is bound. */
readonly read?: AnalysisSettingsReader;
}
/** Discovers and communicates with REA Python bridge plugins in live Cutter processes. */
export class CutterBridgeClient {
readonly #launchRequestBytes: BridgeMaxRequestBytes | undefined;
readonly #launchRequestError: ConfigurationError | undefined;
readonly #read: AnalysisSettingsReader | undefined;
readonly #maxRequestBytes: BridgeMaxRequestBytes;
readonly #directory: string;
readonly #descriptorResources = new ArtifactResourceScope();
readonly #platform: NodeJS.Platform;
@@ -277,16 +269,7 @@ export class CutterBridgeClient {
environment: Readonly<NodeJS.ProcessEnv> = process.env,
options: CutterBridgeClientOptions = {},
) {
const launchRequest = parseBridgeMaxRequestBytes(
environment[BRIDGE_MAX_REQUEST_BYTES_SETTING],
);
this.#launchRequestBytes = launchRequest.ok
? launchRequest.value
: undefined;
this.#launchRequestError = launchRequest.ok
? undefined
: launchRequest.error;
this.#read = options.read;
this.#maxRequestBytes = resolveBridgeMaxRequestBytes(environment);
const configured = environment.REA_CUTTER_BRIDGE_DIR;
const localAppData = environment.LOCALAPPDATA;
this.#platform = options.platform ?? process.platform;
@@ -322,23 +305,6 @@ export class CutterBridgeClient {
);
}
/** Ceiling for this call. A bound operation keeps the value it accepted. */
#requestLimit(): BridgeMaxRequestBytes {
const snapshot = currentAnalysisOperationSnapshot();
const override =
snapshot === undefined
? this.#read?.().overrides["bridge.max_request_bytes"]
: snapshot.overrides["bridge.max_request_bytes"];
if (override !== undefined)
return { maximumBytes: override, source: "runtime" };
if (this.#launchRequestBytes === undefined)
throw (
this.#launchRequestError ??
new ConfigurationError("Invalid REA environment configuration")
);
return this.#launchRequestBytes;
}
async retryDescriptorCleanup(): Promise<void> {
try {
await this.#descriptorResources.retryCleanup();
@@ -358,7 +324,6 @@ export class CutterBridgeClient {
async listSessions(
options: CutterDiscoveryOptions = {},
): Promise<CutterBridgeDiscovery> {
this.#requestLimit();
const { signal } = options;
const mismatches: number[] = [];
const loaded = await this.#readDescriptors("list_cutter_sessions", signal);
@@ -375,7 +340,7 @@ export class CutterBridgeClient {
assertBridgeActive(signal, "list_cutter_sessions");
const advertised =
descriptor.max_request_bytes ?? DEFAULT_BRIDGE_MAX_REQUEST_BYTES;
if (advertised !== this.#requestLimit().maximumBytes) {
if (advertised !== this.#maxRequestBytes.maximumBytes) {
// A crashed process can leave the previous ceiling on disk. Count
// the mismatch only after the hello proves a live authenticated
// bridge. The probe does not send a status request.
@@ -387,7 +352,7 @@ export class CutterBridgeClient {
const status = responseSchema.safeParse(
await this.#request(
descriptor,
serializeRequest(this.#requestLimit(), descriptor, {
serializeRequest(this.#maxRequestBytes, descriptor, {
kind: "status",
session_id: descriptor.session_id,
}),
@@ -444,7 +409,7 @@ export class CutterBridgeClient {
if (mismatches.length > 0)
throw bridgeRequestLimitMismatchError(
"list_cutter_sessions",
this.#requestLimit(),
this.#maxRequestBytes,
mismatches[0] ?? DEFAULT_BRIDGE_MAX_REQUEST_BYTES,
"Cutter",
"No status request was sent to the mismatched session.",
@@ -465,23 +430,22 @@ export class CutterBridgeClient {
readonly json: boolean;
readonly signal?: AbortSignal;
}): Promise<CutterBridgeExecution> {
this.#requestLimit();
try {
assertBridgeActive(input.signal, "cutter_command");
const descriptor = await this.#find(input.sessionId, input.signal);
assertBridgeActive(input.signal, "cutter_command");
const advertised =
descriptor.max_request_bytes ?? DEFAULT_BRIDGE_MAX_REQUEST_BYTES;
if (advertised !== this.#requestLimit().maximumBytes)
if (advertised !== this.#maxRequestBytes.maximumBytes)
throw bridgeRequestLimitMismatchError(
"cutter_command",
this.#requestLimit(),
this.#maxRequestBytes,
advertised,
"Cutter",
);
const requestId = randomUUID();
const commandRequest = serializeRequest(
this.#requestLimit(),
this.#maxRequestBytes,
descriptor,
{
kind: "command",
@@ -668,7 +632,7 @@ export class CutterBridgeClient {
): Promise<z.infer<typeof responseSchema>> {
const raw = await this.#request(
descriptor,
serializeRequest(this.#requestLimit(), descriptor, {
serializeRequest(this.#maxRequestBytes, descriptor, {
kind: "status",
session_id: descriptor.session_id,
}),
@@ -717,7 +681,7 @@ export class CutterBridgeClient {
try {
const raw = await this.#request(
descriptor,
serializeRequest(this.#requestLimit(), descriptor, {
serializeRequest(this.#maxRequestBytes, descriptor, {
kind: "ack",
request_id: requestId,
}),
@@ -998,7 +962,7 @@ export class CutterBridgeClient {
host: descriptor.host,
port: descriptor.port,
});
const frames = new CutterBridgeResponseFrames(this.#requestLimit());
const frames = new CutterBridgeResponseFrames(this.#maxRequestBytes);
let settled = false;
const settle = (
outcome:
@@ -1073,7 +1037,7 @@ export class CutterBridgeClient {
host: descriptor.host,
port: descriptor.port,
});
const frames = new CutterBridgeResponseFrames(this.#requestLimit());
const frames = new CutterBridgeResponseFrames(this.#maxRequestBytes);
let completion: (() => void) | undefined;
let requestSent = false;
let authenticated = false;
+1 -16
View File
@@ -1512,8 +1512,7 @@ describe("analysis error projection: caller lifetime", () => {
minimum: limit.minimum,
maximum: limit.maximum,
agent_editable: true,
update_mode: "runtime",
pending_restart: false,
rea_restart_required: false,
applies_at: "next_invocation",
scope: "rea-invocation",
environment: "REA_REGEX_MATCHING_DEADLINE_MS",
@@ -1530,25 +1529,11 @@ describe("analysis error projection: caller lifetime", () => {
expect.objectContaining({
setting: "wasm.command_timeout_ms",
source: "request",
update_mode: "parameter",
parameter: "command_timeout_ms",
applies_at: "next_invocation",
scope: "rea-invocation",
}),
);
expect(
analysisSettingLimit("wasm.command_timeout_ms", 100, "runtime"),
).toMatchObject({ source: "runtime", update_mode: "runtime" });
expect(
analysisSettingLimit("javascript.max_heap_mb", 128, "request"),
).toMatchObject({
source: "request",
update_mode: "parameter",
parameter: "max_heap_mb",
});
expect(
analysisSettingLimit("javascript.analysis_timeout_ms", 1, "default"),
).toMatchObject({ source: "default", update_mode: "runtime" });
expect(
projectAnalysisError(error, "invocation").details?.limit,
).not.toHaveProperty("environment");
+3 -9
View File
@@ -1044,15 +1044,9 @@ const lifecycleErrorDetails = (
}
if (error instanceof ConfigurationError && error.settings.length > 0)
return {
settings: error.settings.map((issue) => ({
setting: issue.setting,
constraint: issue.constraint,
...(issue.update_mode === undefined
? {}
: { update_mode: issue.update_mode }),
...(issue.pending_restart === undefined
? {}
: { pending_restart: issue.pending_restart }),
settings: error.settings.map(({ setting, constraint }) => ({
setting,
constraint,
})),
};
if (error instanceof BinaryTargetError)
-30
View File
@@ -1,30 +0,0 @@
import { AsyncLocalStorage } from "node:async_hooks";
import type {
AnalysisSettingValues,
AnalysisSettingsSnapshot,
} from "./analysisSettings.js";
const snapshots = new AsyncLocalStorage<AnalysisSettingsSnapshot>();
/**
* Bind the snapshot accepted for one operation. Providers and filesystem
* helpers read it at the point they enforce a limit. A later update does not
* change work already inside this call.
*/
export const runWithAnalysisOperationSnapshot = <Value>(
snapshot: AnalysisSettingsSnapshot,
operation: () => Value,
): Value => snapshots.run(snapshot, operation);
/** The snapshot for the operation on this async stack, when one was accepted. */
export const currentAnalysisOperationSnapshot = ():
| AnalysisSettingsSnapshot
| undefined => snapshots.getStore();
/** Accepted override for one setting, when this operation has a snapshot. */
export const currentAnalysisSettingOverride = <
Setting extends keyof AnalysisSettingValues,
>(
setting: Setting,
): AnalysisSettingValues[Setting] | undefined =>
currentAnalysisOperationSnapshot()?.overrides[setting];
-25
View File
@@ -191,28 +191,3 @@ export const MAXIMUM_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES = Math.floor(
* a caller cap below this budget must not truncate those required replies.
*/
export const DEBUGGER_CONTROL_REPLY_BYTES = 16 * 1024 * 1024;
/** Hopper, Ghidra, and Cutter request payload ceiling. Same bound as the bridge owner. */
export const DEFAULT_BRIDGE_MAX_REQUEST_BYTES = 16 * 1024 * 1024;
export const MINIMUM_BRIDGE_MAX_REQUEST_BYTES = 7464;
export const MAXIMUM_BRIDGE_MAX_REQUEST_BYTES = 536_870_888 - 1;
/** MCP client SDK default request timeout. JEB uses it when the setting is unset. */
export const DEFAULT_JEB_MCP_TIMEOUT_MS = 60_000;
/** Kuna project export and whole-program scans. */
export const DEFAULT_KUNA_PROJECT_TIMEOUT_MS = 30 * 60 * 1000;
/** One Kuna function decompile. */
export const DEFAULT_KUNA_FUNCTION_TIMEOUT_MS = 120_000;
/** Pwntools artifact admission when producer_limits.input_bytes is omitted. */
export const DEFAULT_PWNTOOLS_MAX_INPUT_BYTES = 32 * 1024 * 1024;
export const MAXIMUM_PWNTOOLS_MAX_INPUT_BYTES = 1024 * 1024 * 1024;
/** Windows ChangeTime settle window, poll, and poll count. */
export const DEFAULT_WINDOWS_CHANGE_TIME_DRIFT_MS = 2_000;
export const MAXIMUM_WINDOWS_CHANGE_TIME_DRIFT_MS = 60_000;
export const DEFAULT_WINDOWS_CHANGE_TIME_SETTLE_POLL_MS = 25;
export const MAXIMUM_WINDOWS_CHANGE_TIME_SETTLE_POLL_MS = 1_000;
export const DEFAULT_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS = 40;
export const MAXIMUM_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS = 1_000;
+1 -250
View File
@@ -29,28 +29,6 @@ import {
DEFAULT_JADX_OPERATION_TIMEOUT_MS,
DEFAULT_NATIVE_MATCHING_REPRESENTATION_BYTES,
MAXIMUM_NATIVE_MATCHING_REPRESENTATION_BYTES,
DEFAULT_BRIDGE_MAX_REQUEST_BYTES,
MINIMUM_BRIDGE_MAX_REQUEST_BYTES,
MAXIMUM_BRIDGE_MAX_REQUEST_BYTES,
DEFAULT_DEBUGGER_COMMAND_DEADLINE_MS,
MINIMUM_DEBUGGER_COMMAND_DEADLINE_MS,
MAXIMUM_DEBUGGER_COMMAND_DEADLINE_MS,
DEFAULT_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES,
MINIMUM_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES,
MAXIMUM_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES,
DEFAULT_HOPPER_CALLER_SCAN_BUDGET_MS,
MINIMUM_HOPPER_CALLER_SCAN_BUDGET_MS,
DEFAULT_JEB_MCP_TIMEOUT_MS,
DEFAULT_KUNA_FUNCTION_TIMEOUT_MS,
DEFAULT_KUNA_PROJECT_TIMEOUT_MS,
DEFAULT_PWNTOOLS_MAX_INPUT_BYTES,
MAXIMUM_PWNTOOLS_MAX_INPUT_BYTES,
DEFAULT_WINDOWS_CHANGE_TIME_DRIFT_MS,
MAXIMUM_WINDOWS_CHANGE_TIME_DRIFT_MS,
DEFAULT_WINDOWS_CHANGE_TIME_SETTLE_POLL_MS,
MAXIMUM_WINDOWS_CHANGE_TIME_SETTLE_POLL_MS,
DEFAULT_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS,
MAXIMUM_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS,
} from "./analysisResourceLimits.js";
import {
JAVASCRIPT_ANALYSIS_RESOURCE_DEFAULTS,
@@ -68,7 +46,6 @@ interface SettingDefinition {
readonly maximum: number;
readonly unit: "bytes" | "ms" | "seconds" | "MiB" | "count";
readonly appliesAt: "next_operation" | "next_worker" | "next_startup";
readonly updateMode: "runtime" | "restart";
readonly provider: string;
readonly relatedProviders?: readonly string[];
readonly operations: readonly string[];
@@ -83,7 +60,6 @@ const timer = {
/** Metadata aggregates existing owner definitions; no environment mutation or engine discovery. */
export const ANALYSIS_SETTINGS = {
"ghidra.decompile_max_payload_mib": {
updateMode: "runtime",
environment: "REA_GHIDRA_DECOMPILE_MAX_PAYLOAD_MIB",
defaultValue: null,
minimum: 1,
@@ -101,7 +77,6 @@ export const ANALYSIS_SETTINGS = {
],
},
"asar.max_listing_bytes": {
updateMode: "runtime",
environment: "REA_ASAR_MAX_LISTING_BYTES",
defaultValue: DEFAULT_ASAR_MAX_LISTING_BYTES,
minimum: 1,
@@ -125,7 +100,6 @@ export const ANALYSIS_SETTINGS = {
],
},
"ghidra.value_flow_max_operations": {
updateMode: "runtime",
environment: "REA_GHIDRA_VALUE_FLOW_MAX_OPERATIONS",
defaultValue: DEFAULT_GHIDRA_VALUE_FLOW_MAX_OPERATIONS,
minimum: 1,
@@ -140,7 +114,6 @@ export const ANALYSIS_SETTINGS = {
],
},
"ghidra.value_flow_max_inputs_per_operation": {
updateMode: "runtime",
environment: "REA_GHIDRA_VALUE_FLOW_MAX_INPUTS_PER_OPERATION",
defaultValue: DEFAULT_GHIDRA_VALUE_FLOW_MAX_INPUTS_PER_OPERATION,
minimum: 1,
@@ -155,7 +128,6 @@ export const ANALYSIS_SETTINGS = {
],
},
"ghidra.value_flow_max_edges": {
updateMode: "runtime",
environment: "REA_GHIDRA_VALUE_FLOW_MAX_EDGES",
defaultValue: DEFAULT_GHIDRA_VALUE_FLOW_MAX_EDGES,
minimum: 1,
@@ -170,7 +142,6 @@ export const ANALYSIS_SETTINGS = {
],
},
"asar.max_header_bytes": {
updateMode: "runtime",
environment: "REA_ASAR_MAX_HEADER_BYTES",
defaultValue: DEFAULT_ASAR_MAX_HEADER_BYTES,
minimum: 1,
@@ -194,7 +165,6 @@ export const ANALYSIS_SETTINGS = {
],
},
"ghidra.startup_timeout_ms": {
updateMode: "runtime",
...timer,
environment: "REA_GHIDRA_STARTUP_TIMEOUT_MS",
defaultValue: DEFAULT_GHIDRA_STARTUP_TIMEOUT_MS,
@@ -215,7 +185,6 @@ export const ANALYSIS_SETTINGS = {
],
},
"hopper.startup_timeout_ms": {
updateMode: "runtime",
...timer,
environment: "REA_HOPPER_STARTUP_TIMEOUT_MS",
defaultValue: DEFAULT_HOPPER_STARTUP_TIMEOUT_MS,
@@ -236,7 +205,6 @@ export const ANALYSIS_SETTINGS = {
],
},
"ghidra.decompile_timeout_seconds": {
updateMode: "runtime",
environment: "REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS",
defaultValue: null,
minimum: 1,
@@ -252,7 +220,6 @@ export const ANALYSIS_SETTINGS = {
],
},
"jadx.operation_timeout_ms": {
updateMode: "runtime",
...timer,
environment: "REA_JADX_OPERATION_TIMEOUT_MS",
defaultValue: DEFAULT_JADX_OPERATION_TIMEOUT_MS,
@@ -266,7 +233,6 @@ export const ANALYSIS_SETTINGS = {
],
},
"regex.matching_deadline_ms": {
updateMode: "runtime",
...timer,
environment: "REA_REGEX_MATCHING_DEADLINE_MS",
defaultValue: DEFAULT_REGEX_MATCHING_DEADLINE_MS,
@@ -276,7 +242,6 @@ export const ANALYSIS_SETTINGS = {
operations: ["search_strings", "search_procedures"],
},
"regex.worker_heap_request_mib": {
updateMode: "runtime",
environment: "REA_REGEX_WORKER_HEAP_REQUEST_MIB",
defaultValue: DEFAULT_REGEX_WORKER_HEAP_REQUEST_MIB,
minimum: MINIMUM_REGEX_WORKER_HEAP_REQUEST_MIB,
@@ -288,7 +253,6 @@ export const ANALYSIS_SETTINGS = {
operations: ["search_strings", "search_procedures"],
},
"capture.decoder_old_generation_mib": {
updateMode: "runtime",
environment: "REA_CAPTURE_DECODER_OLD_GENERATION_MIB",
defaultValue: DEFAULT_CAPTURE_DECODER_OLD_GENERATION_MIB,
minimum: CAPTURE_DECODER_SEMI_SPACE_MIB,
@@ -299,7 +263,6 @@ export const ANALYSIS_SETTINGS = {
operations: ["inspect_web_network_capture"],
},
"javascript.analysis_timeout_ms": {
updateMode: "runtime",
...timer,
defaultValue: JAVASCRIPT_ANALYSIS_RESOURCE_DEFAULTS.timeoutMs,
provider: "rea-javascript-application",
@@ -307,7 +270,6 @@ export const ANALYSIS_SETTINGS = {
parameter: "analysis_timeout_ms",
},
"javascript.max_heap_mb": {
updateMode: "runtime",
defaultValue: JAVASCRIPT_ANALYSIS_RESOURCE_DEFAULTS.heapMb,
minimum: JAVASCRIPT_WORKER_HEAP_MIB_MINIMUM,
maximum: JAVASCRIPT_WORKER_HEAP_MIB_MAXIMUM,
@@ -318,7 +280,6 @@ export const ANALYSIS_SETTINGS = {
parameter: "max_heap_mb",
},
"wasm.command_timeout_ms": {
updateMode: "runtime",
...timer,
defaultValue: DEFAULT_WASM_COMMAND_TIMEOUT_MS,
maximum: MAX_WASM_COMMAND_TIMEOUT_MS,
@@ -327,7 +288,6 @@ export const ANALYSIS_SETTINGS = {
parameter: "command_timeout_ms",
},
"native.matching_representation_bytes": {
updateMode: "runtime",
environment: "REA_NATIVE_MATCHING_REPRESENTATION_BYTES",
defaultValue: DEFAULT_NATIVE_MATCHING_REPRESENTATION_BYTES,
minimum: 1,
@@ -337,197 +297,6 @@ export const ANALYSIS_SETTINGS = {
provider: "rea-function-comparison",
operations: ["match_native_functions"],
},
"bridge.max_request_bytes": {
updateMode: "runtime",
environment: "REA_BRIDGE_MAX_REQUEST_BYTES",
defaultValue: DEFAULT_BRIDGE_MAX_REQUEST_BYTES,
minimum: MINIMUM_BRIDGE_MAX_REQUEST_BYTES,
maximum: MAXIMUM_BRIDGE_MAX_REQUEST_BYTES,
unit: "bytes",
appliesAt: "next_operation",
provider: "hopper",
relatedProviders: ["ghidra", "cutter"],
// Every Hopper bridge operation, every Ghidra bridge operation, and the
// Cutter bridge calls. bridgeMaxRequestBytes.test.ts fails when a registry
// gains an operation that is missing here.
operations: [
"address_name",
"comment",
"current_address",
"current_procedure",
"current_document",
"goto_address",
"inline_comment",
"list_bookmarks",
"list_documents",
"list_names",
"list_procedures",
"list_segments",
"list_strings",
"next_address",
"prev_address",
"procedure_address",
"procedure_assembly",
"procedure_callees",
"procedure_callers",
"procedure_info",
"read_function_instructions",
"read_bytes",
"address_to_file_offset",
"procedure_references",
"procedure_pseudo_code",
"resolve_containing_procedure",
"search_procedures",
"search_strings",
"set_address_name",
"set_addresses_names",
"set_bookmark",
"set_comment",
"set_inline_comment",
"unset_bookmark",
"xrefs",
"analyze_function",
"binary_overview",
"inspect_native_load_image",
"search_bytes",
"annotate_native_function",
"inspect_native_data_type",
"inspect_native_instruction",
"resolve_native_call_targets",
"list_cutter_sessions",
"cutter_command",
],
},
"debugger.command_deadline_ms": {
updateMode: "runtime",
...timer,
environment: "REA_DEBUGGER_COMMAND_DEADLINE_MS",
defaultValue: DEFAULT_DEBUGGER_COMMAND_DEADLINE_MS,
minimum: MINIMUM_DEBUGGER_COMMAND_DEADLINE_MS,
maximum: MAXIMUM_DEBUGGER_COMMAND_DEADLINE_MS,
provider: "gdb",
relatedProviders: ["rizin"],
operations: [
"start_gdb_session",
"gdb_console",
"start_rizin_debug_session",
"rizin_debug_command",
],
},
"debugger.max_command_output_bytes": {
updateMode: "runtime",
environment: "REA_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES",
defaultValue: DEFAULT_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES,
minimum: MINIMUM_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES,
maximum: MAXIMUM_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES,
unit: "bytes",
appliesAt: "next_operation",
provider: "gdb",
relatedProviders: ["rizin"],
operations: ["gdb_console", "rizin_debug_command"],
},
"hopper.caller_scan_budget_ms": {
updateMode: "runtime",
...timer,
environment: "REA_HOPPER_CALLER_SCAN_BUDGET_MS",
defaultValue: DEFAULT_HOPPER_CALLER_SCAN_BUDGET_MS,
minimum: MINIMUM_HOPPER_CALLER_SCAN_BUDGET_MS,
provider: "hopper",
operations: [
"procedure_callers",
"analyze_function",
"procedure_references",
],
},
"jeb.mcp_timeout_ms": {
updateMode: "runtime",
...timer,
environment: "REA_JEB_MCP_TIMEOUT_MS",
defaultValue: DEFAULT_JEB_MCP_TIMEOUT_MS,
provider: "jeb",
operations: [
"inspect_jeb_client",
"open_jeb_project",
"list_jeb_units",
"decompile_jeb_item",
],
},
"kuna.project_timeout_ms": {
updateMode: "runtime",
...timer,
environment: "REA_KUNA_PROJECT_TIMEOUT_MS",
defaultValue: DEFAULT_KUNA_PROJECT_TIMEOUT_MS,
provider: "kuna",
operations: [
"list_strings",
"search_strings",
"list_procedures",
"search_procedures",
"xrefs",
"procedure_callers",
"procedure_callees",
"procedure_pseudo_code",
"procedure_assembly",
"analyze_function",
],
},
"kuna.function_timeout_ms": {
updateMode: "runtime",
...timer,
environment: "REA_KUNA_TIMEOUT_MS",
defaultValue: DEFAULT_KUNA_FUNCTION_TIMEOUT_MS,
provider: "kuna",
operations: [
"procedure_pseudo_code",
"procedure_assembly",
"analyze_function",
],
},
"pwntools.max_input_bytes": {
updateMode: "runtime",
environment: "REA_PWNTOOLS_MAX_INPUT_BYTES",
defaultValue: DEFAULT_PWNTOOLS_MAX_INPUT_BYTES,
minimum: 1,
maximum: MAXIMUM_PWNTOOLS_MAX_INPUT_BYTES,
unit: "bytes",
appliesAt: "next_operation",
provider: "pwntools",
operations: ["inspect_binary_layout", "inspect_recorded_crash"],
parameter: "input_bytes",
},
"windows.change_time_drift_ms": {
updateMode: "runtime",
environment: "REA_WINDOWS_CHANGE_TIME_DRIFT_MS",
defaultValue: DEFAULT_WINDOWS_CHANGE_TIME_DRIFT_MS,
minimum: 1,
maximum: MAXIMUM_WINDOWS_CHANGE_TIME_DRIFT_MS,
unit: "ms",
appliesAt: "next_operation",
provider: "rea-artifact-graph",
operations: ["inventory_artifact", "inspect_artifact", "extract_artifact"],
},
"windows.change_time_settle_poll_ms": {
updateMode: "runtime",
environment: "REA_WINDOWS_CHANGE_TIME_SETTLE_POLL_MS",
defaultValue: DEFAULT_WINDOWS_CHANGE_TIME_SETTLE_POLL_MS,
minimum: 1,
maximum: MAXIMUM_WINDOWS_CHANGE_TIME_SETTLE_POLL_MS,
unit: "ms",
appliesAt: "next_operation",
provider: "rea-artifact-graph",
operations: ["inventory_artifact", "inspect_artifact", "extract_artifact"],
},
"windows.change_time_settle_max_polls": {
updateMode: "runtime",
environment: "REA_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS",
defaultValue: DEFAULT_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS,
minimum: 1,
maximum: MAXIMUM_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS,
unit: "count",
appliesAt: "next_operation",
provider: "rea-artifact-graph",
operations: ["inventory_artifact", "inspect_artifact", "extract_artifact"],
},
} as const satisfies Record<AnalysisSettingId, SettingDefinition>;
export const analysisSettingIds = Object.keys(
ANALYSIS_SETTINGS,
@@ -548,23 +317,6 @@ export const analysisSettingValueSchema = (id: AnalysisSettingId) => {
? number.nullable()
: number;
};
/**
* Caller argument, then the accepted runtime override, then the built-in
* default. The third value is that default, not a launch-environment seed.
*/
export const selectedAnalysisSetting = (
request: number | undefined,
override: number | undefined,
fallback: number,
): {
readonly value: number;
readonly source: "default" | "runtime" | "request";
} => {
if (request !== undefined) return { value: request, source: "request" };
if (override !== undefined) return { value: override, source: "runtime" };
return { value: fallback, source: "default" };
};
/** Project one owner's accepted budget, including the explicit per-call precedence. */
export const analysisSettingLimit = (
setting: AnalysisSettingId,
@@ -582,8 +334,7 @@ export const analysisSettingLimit = (
applies_at: definition.appliesAt,
agent_editable: true,
scope: "rea-runtime",
update_mode: source === "request" ? "parameter" : definition.updateMode,
pending_restart: false,
rea_restart_required: false,
...(definition.parameter === undefined
? {}
: { parameter: definition.parameter }),
+1 -25
View File
@@ -17,17 +17,6 @@ export interface AnalysisSettingValues {
readonly "javascript.max_heap_mb": number;
readonly "wasm.command_timeout_ms": number;
readonly "native.matching_representation_bytes": number;
readonly "bridge.max_request_bytes": number;
readonly "debugger.command_deadline_ms": number;
readonly "debugger.max_command_output_bytes": number;
readonly "hopper.caller_scan_budget_ms": number;
readonly "jeb.mcp_timeout_ms": number;
readonly "kuna.project_timeout_ms": number;
readonly "kuna.function_timeout_ms": number;
readonly "pwntools.max_input_bytes": number;
readonly "windows.change_time_drift_ms": number;
readonly "windows.change_time_settle_poll_ms": number;
readonly "windows.change_time_settle_max_polls": number;
}
export type AnalysisSettingId = keyof AnalysisSettingValues;
export interface AnalysisSettingsSnapshot {
@@ -37,14 +26,6 @@ export interface AnalysisSettingsSnapshot {
/** Injected by the runtime owner; reads the accepted operation's immutable snapshot. */
export type AnalysisSettingsReader = () => AnalysisSettingsSnapshot;
/**
* Whether `update_analysis_settings` can change the active value.
* `runtime` applies inside this process. `restart` stays at the value this
* process already started with. A limit fact uses `parameter` when this call
* supplied the value; the setting definition itself stays `runtime`.
*/
export type AnalysisSettingUpdateMode = "runtime" | "restart";
/** Limit facts supplied by the failing owner, never inferred from error prose. */
export interface AnalysisSettingLimit {
readonly setting: AnalysisSettingId;
@@ -56,12 +37,7 @@ export interface AnalysisSettingLimit {
readonly applies_at: "next_operation" | "next_worker" | "next_startup";
readonly agent_editable: true;
readonly scope: "rea-runtime";
readonly update_mode: AnalysisSettingUpdateMode | "parameter";
/**
* True only when a configured value is not the one this process is using.
* The reported effective value is active, so limit errors leave this false.
*/
readonly pending_restart: false;
readonly rea_restart_required: false;
readonly required_value?: number;
readonly parameter?: string;
}
+1 -2
View File
@@ -191,8 +191,7 @@ export interface ArtifactReaderLimitFact {
readonly applies_at: "next_operation";
readonly agent_editable: true;
readonly can_increase: boolean;
readonly update_mode: "parameter";
readonly pending_restart: false;
readonly rea_restart_required: false;
}
/** Runtime representation ceilings differ from aggregate plaintext-work budgets. */
+3 -14
View File
@@ -7,10 +7,6 @@ import {
export interface ConfigurationSettingIssue {
readonly setting: string;
readonly constraint: string;
/** Whether `update_analysis_settings` can change this setting, when the producer owns that fact. */
readonly update_mode?: "runtime" | "restart";
/** True when the rejected value is configured but not active in this process. */
readonly pending_restart?: boolean;
}
/** Rejected settings retained for diagnostics; their values are not. */
@@ -28,16 +24,9 @@ export class ConfigurationError extends AnalysisError {
constructor(message: string, options?: ConfigurationErrorOptions) {
super(message, options);
this.remediationAction = options?.remediationAction;
this.settings = (options?.settings ?? []).map((issue) => ({
setting: issue.setting,
constraint: issue.constraint,
...(issue.update_mode === undefined
? {}
: { update_mode: issue.update_mode }),
...(issue.pending_restart === undefined
? {}
: { pending_restart: issue.pending_restart }),
}));
this.settings = (options?.settings ?? []).map(
({ setting, constraint }) => ({ setting, constraint }),
);
}
}
-1
View File
@@ -233,7 +233,6 @@ const redactFailure = (
...(error.remediationAction === undefined
? {}
: { remediationAction: text(error.remediationAction) }),
...(error.limit === undefined ? {} : { limit: error.limit }),
});
if (error instanceof AnalysisAccessDeniedError)
return new AnalysisAccessDeniedError(
@@ -41,21 +41,7 @@ export interface JavaScriptSourceAnalysisFailure {
export interface JavaScriptSourceAnalysisLimits {
readonly semanticPrecision?: JavaScriptSemanticPrecisionInput;
readonly heapMb: number;
/** Where `heapMb` was selected. Absent when the caller did not record it. */
readonly heapSource?:
| "default"
| "setting"
| "runtime"
| "request"
| undefined;
readonly timeoutMs: number;
/** Where `timeoutMs` was selected. Absent when the caller did not record it. */
readonly timeoutSource?:
| "default"
| "setting"
| "runtime"
| "request"
| undefined;
}
/** Sequential source-analysis authority with independently verified cleanup. */
-31
View File
@@ -1,31 +0,0 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { expect, it } from "vitest";
import { ANALYSIS_SETTINGS } from "./analysisSettingDefinitions.js";
import { NUMERIC_LIMIT_REGISTRY } from "./numericLimitRegistry.js";
const inventory = readFileSync(
join(import.meta.dirname, "../../docs/design/rea-limit-inventory.md"),
"utf8",
);
it("gives every numeric exception a reason and a matching inventory row", () => {
const names = NUMERIC_LIMIT_REGISTRY.map((entry) => entry.name);
expect(new Set(names).size).toBe(names.length);
const runtimeEnvironments = new Set<string>(
Object.values(ANALYSIS_SETTINGS).flatMap((definition) =>
"environment" in definition && definition.environment !== undefined
? [definition.environment]
: [],
),
);
for (const entry of NUMERIC_LIMIT_REGISTRY) {
expect(entry.update_mode).toBe("restart");
expect(entry.reason.trim().length).toBeGreaterThanOrEqual(40);
expect(entry.policy).toBe("restart-only");
expect(entry.read).toBe("startup");
expect(runtimeEnvironments.has(entry.name)).toBe(false);
expect(inventory).toContain(`\`${entry.name}\``);
}
expect(NUMERIC_LIMIT_REGISTRY).toHaveLength(15);
});
-143
View File
@@ -1,143 +0,0 @@
/**
* Numeric `REA_` limits that are not runtime analysis settings.
*
* The static check in `scripts/verify/rea-numeric-limits.mjs` fails when a
* numeric `REA_` limit in `src` is absent from both this list and
* `ANALYSIS_SETTINGS`. Every remaining entry is restart-only: it shapes the
* MCP server, a worker or pool created at startup, or a helper that reads the
* launch environment. A limit checked on each call belongs in
* `ANALYSIS_SETTINGS` instead of this list.
*
* Narrative bounds, owners, and the non-numeric settings live in
* `docs/design/rea-limit-inventory.md`.
*/
export interface NumericLimitRegistryEntry {
readonly name: string;
readonly update_mode: "restart";
readonly policy: "restart-only";
readonly read: "startup";
readonly reason: string;
}
export const NUMERIC_LIMIT_REGISTRY = [
{
name: "REA_DEBUGGER_MAX_PENDING_COMMANDS",
update_mode: "restart",
policy: "restart-only",
read: "startup",
reason:
"Caps the pending-command queue created with the debugger session manager. It is pool capacity for that manager, not a budget checked independently on each command.",
},
{
name: "REA_DEBUGGER_MAX_SESSIONS",
update_mode: "restart",
policy: "restart-only",
read: "startup",
reason:
"Caps how many debugger sessions the session manager created at startup will admit. Changing the pool size of a manager that already exists requires a new REA process.",
},
{
name: "REA_HOPPER_DEMO_DIALOG_TIMEOUT_MS",
update_mode: "restart",
policy: "restart-only",
read: "startup",
reason:
"The Linux private-display adapter reads this watch from the process environment when that helper starts. A running adapter does not observe a value stored only in the REA process.",
},
{
name: "REA_JADX_ACTIVE_PROCESSOR_COUNT",
update_mode: "restart",
policy: "restart-only",
read: "startup",
reason:
"Passed to the JVM as -XX:ActiveProcessorCount when the JADX worker process starts. A running JVM does not adopt a new processor count.",
},
{
name: "REA_JADX_HEAP_MIB",
update_mode: "restart",
policy: "restart-only",
read: "startup",
reason:
"Passed to the JVM as -Xmx when the JADX worker process starts. A running JVM does not adopt a new heap size.",
},
{
name: "REA_KUNA_CACHE_MAX_BYTES",
update_mode: "restart",
policy: "restart-only",
read: "startup",
reason:
"Sizes the Kuna project cache when that cache is constructed for the provider. An existing cache does not resize from a later value.",
},
{
name: "REA_KUNA_JOBS",
update_mode: "restart",
policy: "restart-only",
read: "startup",
reason:
"Selects the Kuna worker-pool width when the provider builds its run slots. The pool is not resized for a process that is already running.",
},
{
name: "REA_MCP_DOCTOR_DEADLINE_MS",
update_mode: "restart",
policy: "restart-only",
read: "startup",
reason:
"Bounds the MCP doctor phase while the server is starting. It shapes server startup, not a later analysis call.",
},
{
name: "REA_MCP_FIRST_CATALOG_BUDGET_MS",
update_mode: "restart",
policy: "restart-only",
read: "startup",
reason:
"Bounds the first MCP catalog phase while the server is starting. It shapes server startup, not a later analysis call.",
},
{
name: "REA_MCP_INITIALIZE_BUDGET_MS",
update_mode: "restart",
policy: "restart-only",
read: "startup",
reason:
"Bounds the MCP initialize phase while the server is starting. It shapes server startup, not a later analysis call.",
},
{
name: "REA_MCP_MAX_INPUT_ELEMENTS",
update_mode: "restart",
policy: "restart-only",
read: "startup",
reason:
"Sizes argument admission for the MCP server created at process start. A running server does not rebuild that parser for a new allowance.",
},
{
name: "REA_MCP_MAX_RESPONSE_BYTES",
update_mode: "restart",
policy: "restart-only",
read: "startup",
reason:
"Sizes the MCP stdio receive buffer when the server process starts. A running transport and the paired client buffer do not adopt a new ceiling.",
},
{
name: "REA_MCP_TOOLS_LIST_BUDGET_MS",
update_mode: "restart",
policy: "restart-only",
read: "startup",
reason:
"Bounds the MCP tools/list phase while the server is starting. It shapes server startup, not a later analysis call.",
},
{
name: "REA_RECONNECT_TIMEOUT_SECONDS",
update_mode: "restart",
policy: "restart-only",
read: "startup",
reason:
"Fixed 30-second value written into the Hopper bridge helper from HOPPER_SHUTDOWN_TIMEOUT_MS. It is not a caller-selected launch setting.",
},
{
name: "REA_SWIFTC_COMPILE_TIMEOUT_MS",
update_mode: "restart",
policy: "restart-only",
read: "startup",
reason:
"Captured when the Darwin ownership-helper reader is constructed at process start. That helper compile uses the captured deadline, so a later value needs a new REA process.",
},
] as const satisfies readonly NumericLimitRegistryEntry[];
+40
View File
@@ -3,9 +3,49 @@ import {
inspectPythonPycInputSchema,
pythonPycMarshalProfile,
PYTHON_PYC_MARSHAL_PROFILES,
pythonPycHeaderSchema,
} from "./pythonPyc.js";
import { pythonBytecodeIdentity } from "./pythonBytecodeIdentity.js";
const hashHeader = {
magic: 3531,
python_version: "3.12",
code_layout: "py311",
flags: 3,
hash_based: true,
check_source: true,
source_hash_hex: "0123456789abcdef",
source_mtime: null,
source_size: null,
};
it.each(["", "ab", "g".repeat(16)])(
"rejects an invalid source hash %j",
(source_hash_hex) => {
expect(
pythonPycHeaderSchema.safeParse({ ...hashHeader, source_hash_hex })
.success,
).toBe(false);
},
);
it.each([
{ hash_based: false },
{ check_source: false },
{ source_hash_hex: null },
{ source_mtime: 0 },
{ source_size: 0 },
])("keeps header facts consistent with the recorded flags: %j", (changed) => {
expect(
pythonPycHeaderSchema.safeParse({ ...hashHeader, ...changed }).success,
).toBe(false);
});
it("preserves unknown raw header flags and valid hash bytes", () => {
const header = { ...hashHeader, flags: 0x80000003 };
expect(pythonPycHeaderSchema.parse(header)).toEqual(header);
});
it("keeps pinned CPython identity separate from standalone marshal coverage", () => {
expect(pythonPycMarshalProfile(3394)).toEqual({
code_layout: "py37",
+47 -34
View File
@@ -205,17 +205,53 @@ const pythonCodeObjectSchema = z.strictObject({
});
export type PythonPycCodeObject = z.infer<typeof pythonCodeObjectSchema>;
export const pythonPycHeaderSchema = z.strictObject({
magic: z.number().int().min(0).max(0xffff),
python_version: z.string().min(1),
code_layout: z.enum(PYTHON_PYC_CODE_LAYOUTS),
flags: uint,
hash_based: z.boolean(),
check_source: z.boolean(),
source_hash_hex: z.string().nullable(),
source_mtime: uint.nullable(),
source_size: uint.nullable(),
});
export const pythonPycHeaderSchema = z
.strictObject({
magic: z.number().int().min(0).max(0xffff),
python_version: z.string().min(1),
code_layout: z.enum(PYTHON_PYC_CODE_LAYOUTS),
flags: uint,
hash_based: z.boolean(),
check_source: z.boolean(),
source_hash_hex: z
.string()
.regex(/^[0-9a-f]{16}$/u)
.nullable(),
source_mtime: uint.nullable(),
source_size: uint.nullable(),
})
.superRefine((header, context) => {
for (const [field, flag] of [
["hash_based", PYTHON_PYC_HASH_BASED],
["check_source", PYTHON_PYC_CHECK_SOURCE],
] as const)
if (header[field] !== ((header.flags & flag) !== 0))
context.addIssue({
code: "custom",
path: [field],
message: `${field} disagrees with its recorded header flag.`,
});
if (header.hash_based === (header.source_hash_hex === null))
context.addIssue({
code: "custom",
path: ["source_hash_hex"],
message:
"Hash-based pycs require an 8-byte source hash; others must omit it.",
});
if (header.hash_based === (header.source_mtime !== null))
context.addIssue({
code: "custom",
path: ["source_mtime"],
message: "Timestamp pycs require mtime; hash-based pycs must omit it.",
});
if (header.hash_based === (header.source_size !== null))
context.addIssue({
code: "custom",
path: ["source_size"],
message:
"Timestamp pycs require source_size; hash-based pycs must omit it.",
});
});
/** Bounded CPython 3.7–3.13 pyc observation; marshal is walked, bytecode is not decompiled. */
export const pythonPycSchema = z
@@ -253,29 +289,6 @@ export const pythonPycSchema = z
message: "Marshal byte count disagrees with the header-stripped file.",
});
}
if (report.header.hash_based === (report.header.source_hash_hex === null)) {
context.addIssue({
code: "custom",
path: ["header", "source_hash_hex"],
message:
"Hash-based pycs require an 8-byte source hash; others must omit it.",
});
}
if (report.header.hash_based === (report.header.source_mtime !== null)) {
context.addIssue({
code: "custom",
path: ["header", "source_mtime"],
message: "Timestamp pycs require mtime; hash-based pycs must omit it.",
});
}
if (report.header.hash_based === (report.header.source_size !== null)) {
context.addIssue({
code: "custom",
path: ["header", "source_size"],
message:
"Timestamp pycs require source_size; hash-based pycs must omit it.",
});
}
if (report.coverage.code_objects !== report.code_objects.length) {
context.addIssue({
code: "custom",
+1 -2
View File
@@ -79,8 +79,7 @@ export const webNetworkCaptureBudgetFacts = (
? MAX_WEB_NETWORK_CAPTURE_NESTING_DEPTH
: Number.MAX_SAFE_INTEGER,
applies_at: "next_operation",
update_mode: "parameter" as const,
pending_restart: false as const,
rea_restart_required: false,
};
};
+1 -2
View File
@@ -1,7 +1,6 @@
import type { Stats } from "node:fs";
import {
applyWindowsChangeTimeSnapshot,
resolveWindowsChangeTimeSettle,
type WindowsChangeTimeSettleLimits,
} from "../config/windowsChangeTimeSettle.js";
@@ -50,7 +49,7 @@ const limitsFrom = (
seams.environment ?? process.env,
);
if (!resolved.ok) throw resolved.error;
return applyWindowsChangeTimeSnapshot(resolved.value);
return resolved.value;
};
/** Stop settling as soon as the owning operation is cancelled. */
+11 -31
View File
@@ -23,7 +23,6 @@ import type {
GdbOutputLimit,
} from "../domain/commandObservation.js";
import { DEBUGGER_CONTROL_REPLY_BYTES } from "../domain/analysisResourceLimits.js";
import { currentAnalysisOperationSnapshot } from "../domain/analysisOperationSnapshot.js";
import { err, ok, type Result } from "../domain/result.js";
import {
ProviderProcessSupervisor,
@@ -68,7 +67,6 @@ const copiedTimeoutGuidance = (
...(cause.diagnostics === undefined
? {}
: { diagnostics: cause.diagnostics }),
...(cause.limit === undefined ? {} : { limit: cause.limit }),
});
/** A host executable refusal cannot be repaired by changing MI dialect. */
class GdbExecutableRefusal extends AnalysisCapabilityUnavailableError {
@@ -539,6 +537,7 @@ interface GdbSession {
readonly verifyInferiorsEndedAfterExit: () => Promise<boolean>;
readonly knownInferiorIdentities: Map<string, OwnedInferiorIdentity>;
readonly inferiorTty: string | undefined;
readonly maxCommandOutputBytes: number;
/** Startup and detach MI. Separate from the caller command-output cap. */
readonly controlReplyBytes: number;
readonly stdoutSettled: Promise<void>;
@@ -597,7 +596,8 @@ export class GdbSessionManager {
>();
readonly #command: string;
readonly #environment: NodeJS.ProcessEnv;
readonly #launchCommandDeadline: DebuggerAdmissionLimit;
readonly #commandDeadline: DebuggerAdmissionLimit;
readonly #commandTimeoutMs: number;
readonly #startupTimeoutMs: number;
readonly #platform: NodeJS.Platform;
readonly #stopProcess: typeof stopOwnedDebuggerProcess;
@@ -610,32 +610,9 @@ export class GdbSessionManager {
readonly #readInferiorProcessStartTime: ProcessStartTimeReader | undefined;
readonly #maxSessions: DebuggerAdmissionLimit;
readonly #maxPendingCommands: DebuggerAdmissionLimit;
readonly #launchCommandOutput: DebuggerAdmissionLimit;
readonly #commandOutput: DebuggerAdmissionLimit;
readonly #maxCommandOutputBytes: number;
readonly #controlReplyBytes: number;
get #commandDeadline(): DebuggerAdmissionLimit {
const override =
currentAnalysisOperationSnapshot()?.overrides[
"debugger.command_deadline_ms"
];
return override === undefined
? this.#launchCommandDeadline
: { ...this.#launchCommandDeadline, value: override, source: "runtime" };
}
get #commandTimeoutMs(): number {
return this.#commandDeadline.value;
}
get #commandOutput(): DebuggerAdmissionLimit {
const override =
currentAnalysisOperationSnapshot()?.overrides[
"debugger.max_command_output_bytes"
];
return override === undefined
? this.#launchCommandOutput
: { ...this.#launchCommandOutput, value: override, source: "runtime" };
}
get #maxCommandOutputBytes(): number {
return this.#commandOutput.value;
}
#sessionReservations = 0;
#closing = false;
#closePromise: Promise<void> | undefined;
@@ -665,7 +642,8 @@ export class GdbSessionManager {
this.#environment = { ...(options.environment ?? process.env) };
this.#command = this.#environment.REA_GDB_COMMAND ?? "gdb";
const limits = resolveDebuggerSessionLimits(this.#environment);
this.#launchCommandDeadline = limits.commandDeadline;
this.#commandDeadline = limits.commandDeadline;
this.#commandTimeoutMs = this.#commandDeadline.value;
this.#startupTimeoutMs = options.startupTimeoutMs ?? STARTUP_TIMEOUT_MS;
this.#platform = options.platform ?? process.platform;
this.#stopProcess = options.stopProcess ?? stopOwnedDebuggerProcess;
@@ -675,7 +653,8 @@ export class GdbSessionManager {
this.#readInferiorProcessStartTime = options.readInferiorProcessStartTime;
this.#maxSessions = limits.maxSessions;
this.#maxPendingCommands = limits.maxPendingCommands;
this.#launchCommandOutput = limits.maxCommandOutputBytes;
this.#commandOutput = limits.maxCommandOutputBytes;
this.#maxCommandOutputBytes = this.#commandOutput.value;
this.#controlReplyBytes =
options.controlReplyBytes ?? DEBUGGER_CONTROL_REPLY_BYTES;
}
@@ -900,6 +879,7 @@ export class GdbSessionManager {
},
knownInferiorIdentities: new Map(),
inferiorTty,
maxCommandOutputBytes: this.#maxCommandOutputBytes,
controlReplyBytes: this.#controlReplyBytes,
stdoutSettled,
settleStdout,
@@ -2258,7 +2238,7 @@ export class GdbSessionManager {
timer,
outputBytes: 0,
outputBudgetBytes: command.startsWith("-interpreter-exec console ")
? this.#maxCommandOutputBytes
? session.maxCommandOutputBytes
: session.controlReplyBytes,
outputLimits: new Set(),
outputInvalidUtf8: false,
-20
View File
@@ -1,4 +1,3 @@
import { readFile } from "node:fs/promises";
import { describe, expect, it } from "vitest";
import type { JsonValue } from "../domain/jsonValue.js";
@@ -1120,25 +1119,6 @@ describe("Ghidra native instruction decoder context for ARM TMode and MIPS ISA_M
});
});
// Kept on the file rather than in its own suite. The full source run collected
// that suite with no tests and failed the file before this assertion could run.
it("binds TMode to ARM and ISA_MODE to MIPS in the Ghidra bridge script", async () => {
const bridgePath = new URL(
"../../bridge/ghidra/ReaGhidraBridge.java",
import.meta.url,
);
const content = await readFile(bridgePath, "utf8");
expect(content).toContain(
'currentProgram.getLanguageID().getIdAsString().startsWith("ARM:")',
);
expect(content).toContain(
'currentProgram.getLanguageID().getIdAsString().startsWith("MIPS:")',
);
expect(content).toContain('"TMode"');
expect(content).toContain('"ISA_MODE"');
expect(content).toContain('"ghidra-program-context"');
});
describe("Ghidra dossier nested boundary composition", () => {
const identityLocations = [
"caller",
+3 -6
View File
@@ -1868,8 +1868,7 @@ describe("Ghidra decompiler failure projection", () => {
applies_at: "next_operation",
agent_editable: true,
scope: "rea-runtime",
update_mode: "runtime",
pending_restart: false,
rea_restart_required: false,
},
},
});
@@ -1984,8 +1983,7 @@ describe("Ghidra decompile deadline for a finished invocation", () => {
minimum: 1,
maximum: 2_147_483,
agent_editable: true,
update_mode: "runtime",
pending_restart: false,
rea_restart_required: false,
};
expect(invocation.details?.limit).toEqual({
...enforced,
@@ -4367,8 +4365,7 @@ describe("Ghidra startup deadline for a finished invocation", () => {
setting: "ghidra.startup_timeout_ms",
effective_value: 250,
agent_editable: true,
update_mode: "runtime",
pending_restart: false,
rea_restart_required: false,
};
const invocationLimit = {
...startupLimit,
+1 -7
View File
@@ -103,12 +103,6 @@ export interface HopperClientOptions {
readonly runId?: string;
readonly startupTimeoutMs?: number;
readonly readStartupTimeoutMs?: () => number;
/** Selection owner for the startup deadline. Absent when the caller did not record it. */
readonly readStartupTimeoutSource?: () =>
| "default"
| "setting"
| "runtime"
| undefined;
/** Effective request ceiling. Omitted uses the 16 MiB default. */
readonly bridgeMaxRequestBytes?: BridgeMaxRequestBytes;
readonly onDiagnostic?: (event: HopperDiagnostic) => void;
@@ -972,7 +966,7 @@ export class HopperClient {
limit: analysisSettingLimit(
"hopper.startup_timeout_ms",
deadline.timeoutMs,
this.#options.readStartupTimeoutSource?.(),
undefined,
),
},
);
-1
View File
@@ -328,7 +328,6 @@ export class HopperProvider implements AnalysisProviderCandidate {
...(context === undefined ? {} : { runId: context.runId }),
startupTimeoutMs: this.config.hopperStartupTimeoutMs,
readStartupTimeoutMs: () => this.config.hopperStartupTimeoutMs,
readStartupTimeoutSource: () => this.config.hopperStartupTimeoutSource,
bridgeMaxRequestBytes: this.config.bridgeMaxRequestBytes,
logger: this.logger.child({ layer: "bridge" }),
onDiagnostic: (diagnostic) =>
+1 -2
View File
@@ -187,8 +187,7 @@ it.each(["initialize", "transport"])(
applies_at: "next_operation",
agent_editable: true,
scope: "rea-runtime",
update_mode: "runtime",
pending_restart: false,
rea_restart_required: false,
},
diagnostics: { producer_fact: "retained" },
});
@@ -948,7 +948,7 @@ export class JavaScriptAnalysisWorker {
limit: analysisSettingLimit(
"javascript.max_heap_mb",
this.limits.heapMb,
this.limits.heapSource,
undefined,
),
remediationAction: javaScriptWorkerHeapRemediation(this.limits.heapMb),
},
@@ -1147,7 +1147,7 @@ export class JavaScriptAnalysisWorker {
limit: analysisSettingLimit(
"javascript.analysis_timeout_ms",
this.limits.timeoutMs,
this.limits.timeoutSource,
undefined,
),
capturedOutput: this.#captured(),
userMessage: `JavaScript source analysis reached its ${String(this.limits.timeoutMs)}ms per-source server/provider deadline (analysis_timeout_ms), not the MCP client deadline.`,
@@ -1182,7 +1182,7 @@ export class JavaScriptAnalysisWorker {
limit: analysisSettingLimit(
"javascript.max_heap_mb",
this.limits.heapMb,
this.limits.heapSource,
undefined,
),
}
: {}),
+12 -19
View File
@@ -1,4 +1,7 @@
import type { Client } from "@modelcontextprotocol/client";
import {
type Client,
DEFAULT_REQUEST_TIMEOUT_MSEC,
} from "@modelcontextprotocol/client";
import {
mcpToolResultSchema,
mcpTextBlocks,
@@ -19,7 +22,7 @@ import { ProviderCleanupError } from "../domain/providerCleanupError.js";
import { AnalysisError } from "../domain/analysisErrorBase.js";
import { SharedStartupAttempt } from "../process/SharedStartupAttempt.js";
import { err, ok, type Result } from "../domain/result.js";
import { JebMcpDeadline, selectedJebMcpTimeout } from "./JebMcpDeadline.js";
import { JebMcpDeadline } from "./JebMcpDeadline.js";
/** Upstream protocol names for the operations REA exposes. */
export const JEB_ENGINE_TOOLS = {
@@ -96,10 +99,10 @@ export const createStreamableHttpJebMcpConnection: JebMcpConnectionFactory = (
endpoint,
timeouts,
) => {
const discoveryTimeout = () =>
selectedJebMcpTimeout(timeouts?.discoveryTimeoutMs);
const requestTimeout = () =>
selectedJebMcpTimeout(timeouts?.requestTimeoutMs);
const discoveryTimeoutMs =
timeouts?.discoveryTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MSEC;
const requestTimeoutMs =
timeouts?.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MSEC;
let state: JebMcpConnectionOwner | undefined;
let sessionGeneration = 0;
let closing: Promise<void> | undefined;
@@ -207,12 +210,10 @@ export const createStreamableHttpJebMcpConnection: JebMcpConnectionFactory = (
const acquisition = new SharedStartupAttempt<
Result<readonly string[], unknown>
>(async (startupSignal) => {
const selected = discoveryTimeout();
const deadline = new JebMcpDeadline(
selected.ms,
discoveryTimeoutMs,
startupSignal,
"discovery",
selected.source,
);
try {
await owner.client.connect(owner.transport, {
@@ -275,12 +276,10 @@ export const createStreamableHttpJebMcpConnection: JebMcpConnectionFactory = (
signal === undefined
? owner.signal.signal
: AbortSignal.any([signal, owner.signal.signal]);
const selected = discoveryTimeout();
const deadline = new JebMcpDeadline(
selected.ms,
discoveryTimeoutMs,
discoverySignal,
"discovery",
selected.source,
);
try {
const refreshed = await readToolNames(owner.client, deadline);
@@ -308,13 +307,7 @@ export const createStreamableHttpJebMcpConnection: JebMcpConnectionFactory = (
signal === undefined
? owner.signal.signal
: AbortSignal.any([signal, owner.signal.signal]);
const selected = requestTimeout();
const deadline = new JebMcpDeadline(
selected.ms,
requestSignal,
"tools/call",
selected.source,
);
const deadline = new JebMcpDeadline(requestTimeoutMs, requestSignal);
let result: unknown;
try {
result = await owner.client.callTool(
+13 -35
View File
@@ -1,47 +1,26 @@
import { SdkError, SdkErrorCode } from "@modelcontextprotocol/client";
import {
DEFAULT_REQUEST_TIMEOUT_MSEC,
SdkError,
SdkErrorCode,
} from "@modelcontextprotocol/client";
import {
AnalysisCancelledError,
AnalysisTimeoutError,
} from "../domain/analysisErrorCore.js";
import { analysisSettingLimit } from "../domain/analysisSettingDefinitions.js";
import { currentAnalysisOperationSnapshot } from "../domain/analysisOperationSnapshot.js";
import { DEFAULT_JEB_MCP_TIMEOUT_MS } from "../domain/analysisResourceLimits.js";
import { MAX_NODE_TIMER_DELAY_MS } from "../domain/analysisResourceLimits.js";
import { ProviderStartupDeadline } from "../process/ProviderDeadline.js";
/** Which JEB MCP phase consumed `REA_JEB_MCP_TIMEOUT_MS`. */
export type JebMcpDeadlinePhase = "discovery" | "tools/call";
/** Where the JEB deadline in force was selected. */
export type JebMcpTimeoutSource = "default" | "setting" | "runtime";
/**
* Runtime override, then the launch setting, then the built-in default.
* An invalid launch value is omitted by the caller so this can still select
* the override.
*/
export const selectedJebMcpTimeout = (
launchMs: number | undefined,
): { readonly ms: number; readonly source: JebMcpTimeoutSource } => {
const override =
currentAnalysisOperationSnapshot()?.overrides["jeb.mcp_timeout_ms"];
if (override !== undefined) return { ms: override, source: "runtime" };
if (launchMs !== undefined) return { ms: launchMs, source: "setting" };
return { ms: DEFAULT_JEB_MCP_TIMEOUT_MS, source: "default" };
};
/** Name the deadline that both JEB phases share. */
/** Name the launch-environment deadline that both JEB phases share. */
export const jebMcpTimeoutGuidance = (
phase: JebMcpDeadlinePhase,
timeoutMs: number,
source: JebMcpTimeoutSource,
) => {
const limit = analysisSettingLimit("jeb.mcp_timeout_ms", timeoutMs, source);
return {
userMessage: `This request reached REA's configured JEB ${phase === "discovery" ? "catalog discovery" : phase} deadline of ${String(timeoutMs)} ms (deadlineExpired).`,
remediationAction: `jeb.mcp_timeout_ms is ${String(timeoutMs)} ms (source ${source}; default ${String(DEFAULT_JEB_MCP_TIMEOUT_MS)} ms, floor ${String(limit.minimum)} ms, maximum ${String(limit.maximum)} ms). It can be changed at runtime with update_analysis_settings. The launch seed is REA_JEB_MCP_TIMEOUT_MS. The same setting raises catalog discovery and each tools/call. Allow execution, cleanup and delivery headroom in the outer MCP client deadline. Confirm outstanding work and cleanup before retrying.`,
limit,
};
};
): { readonly userMessage: string; readonly remediationAction: string } => ({
userMessage: `This request reached REA's configured JEB ${phase === "discovery" ? "catalog discovery" : phase} deadline of ${String(timeoutMs)} ms (deadlineExpired).`,
remediationAction: `Increase REA_JEB_MCP_TIMEOUT_MS in REA's launch environment to an integer from 1 to ${String(MAX_NODE_TIMER_DELAY_MS)} milliseconds (omitted means ${String(DEFAULT_REQUEST_TIMEOUT_MSEC)} ms) and restart and reconnect REA to apply it. The same setting raises catalog discovery and each tools/call. Allow execution, cleanup and delivery headroom in the outer MCP client deadline. Confirm outstanding work and cleanup before retrying.`,
});
/** SDK 2.3.1 preserves an SdkError abort reason by identity. */
export class JebMcpDeadline {
@@ -57,7 +36,6 @@ export class JebMcpDeadline {
readonly timeoutMs: number,
signal?: AbortSignal,
readonly phase: JebMcpDeadlinePhase = "tools/call",
readonly source: JebMcpTimeoutSource = "default",
) {
this.#deadline = new ProviderStartupDeadline(timeoutMs, signal);
if (this.#deadline.signal.aborted) this.#onAbort();
@@ -80,7 +58,7 @@ export class JebMcpDeadline {
return this.#deadline.interruption === "timeout"
? new AnalysisTimeoutError(operation, this.timeoutMs, {
cause: this.#deadline.signal.reason,
...jebMcpTimeoutGuidance(this.phase, this.timeoutMs, this.source),
...jebMcpTimeoutGuidance(this.phase, this.timeoutMs),
})
: new AnalysisCancelledError(operation, {
cause: this.#deadline.signal.reason,
@@ -89,7 +67,7 @@ export class JebMcpDeadline {
if (cause instanceof SdkError && cause.code === SdkErrorCode.RequestTimeout)
return new AnalysisTimeoutError(operation, this.timeoutMs, {
cause,
...jebMcpTimeoutGuidance(this.phase, this.timeoutMs, this.source),
...jebMcpTimeoutGuidance(this.phase, this.timeoutMs),
});
return cause;
}
+1 -47
View File
@@ -1,11 +1,6 @@
import { describe, expect, it, vi } from "vitest";
import { createJebAnalysisProvider } from "../composition/jeb.js";
import { runWithAnalysisOperationSnapshot } from "../domain/analysisOperationSnapshot.js";
import {
jebMcpTimeoutGuidance,
selectedJebMcpTimeout,
} from "./JebMcpDeadline.js";
import { JebProvider } from "./JebProvider.js";
import { decodeJebToolResult } from "./JebMcpConnection.js";
import type {
@@ -109,44 +104,13 @@ describe("JebProvider live availability", () => {
await provider.close();
});
it("keeps the selected JEB deadline source", () => {
expect(selectedJebMcpTimeout(undefined)).toEqual({
ms: 60_000,
source: "default",
});
expect(selectedJebMcpTimeout(900_000)).toEqual({
ms: 900_000,
source: "setting",
});
expect(
runWithAnalysisOperationSnapshot(
{ revision: 1, overrides: { "jeb.mcp_timeout_ms": 1500 } },
() => selectedJebMcpTimeout(900_000),
),
).toEqual({ ms: 1500, source: "runtime" });
expect(
jebMcpTimeoutGuidance("tools/call", 1500, "runtime").limit,
).toMatchObject({
setting: "jeb.mcp_timeout_ms",
effective_value: 1500,
source: "runtime",
update_mode: "runtime",
pending_restart: false,
});
expect(
jebMcpTimeoutGuidance("discovery", 60_000, "default").limit,
).toMatchObject({ source: "default", update_mode: "runtime" });
});
it("rejects an invalid JEB timeout before constructing its MCP connection", async () => {
let constructed = false;
const provider = createJebAnalysisProvider(
{ REA_JEB_MCP_TIMEOUT_MS: "0" },
() => {
constructed = true;
return scriptedConnection({
get_client_information: { success: true, version: "5.48.0" },
});
return scriptedConnection({});
},
);
expect(constructed).toBe(false);
@@ -154,16 +118,6 @@ describe("JebProvider live availability", () => {
status: "unavailable",
diagnostics: { setting: "REA_JEB_MCP_TIMEOUT_MS" },
});
const accepted = await runWithAnalysisOperationSnapshot(
{ revision: 1, overrides: { "jeb.mcp_timeout_ms": 1500 } },
() =>
provider.execute({
operation: "inspect_jeb_client",
input: {},
}),
);
expect(constructed).toBe(true);
expect(accepted.ok).toBe(true);
await provider.close();
});
-1
View File
@@ -574,7 +574,6 @@ export class JebProvider implements JebAnalysisPort {
...(cause.remediationAction === undefined
? {}
: { remediationAction: cause.remediationAction }),
...(cause.limit === undefined ? {} : { limit: cause.limit }),
})
: new AnalysisCancelledError(operation, options);
return failure
+2 -8
View File
@@ -4,7 +4,6 @@ import { stat } from "node:fs/promises";
import { Transform, type Writable } from "node:stream";
import { pipeline } from "node:stream/promises";
import { analysisSettingLimit } from "../domain/analysisSettingDefinitions.js";
import { AnalysisError } from "../domain/analysisErrorBase.js";
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
import {
@@ -389,15 +388,10 @@ const commandFailure = (
});
else if (cause.reason === "timeout") {
const { setting, ms } = request.deadline;
const analysisSetting =
setting === "REA_KUNA_TIMEOUT_MS"
? "kuna.function_timeout_ms"
: "kuna.project_timeout_ms";
primary = new AnalysisTimeoutError(operation, ms, {
...details,
limit: analysisSettingLimit(analysisSetting, ms, request.deadline.source),
userMessage: `kuna ${subcommand} did not finish within ${analysisSetting} (${String(ms)} ms).`,
remediationAction: `${analysisSetting} is ${String(ms)} ms. It can be changed at runtime with update_analysis_settings. The launch seed is ${setting}.`,
userMessage: `kuna ${subcommand} did not finish within ${setting} (${String(ms)} ms).`,
remediationAction: `Raise ${setting} in the environment that starts REA, then restart REA; it reads Kuna settings when it starts.`,
});
}
// The same input overflows the same fixed buffer; waiting cannot help.
+33 -84
View File
@@ -1,6 +1,5 @@
import { isAbsolute } from "node:path";
import { currentAnalysisOperationSnapshot } from "../domain/analysisOperationSnapshot.js";
import { defaultKunaCacheDirectory } from "../config/kunaCacheDirectory.js";
import { homeDirectoryFromEnvironment } from "../config/homeDirectory.js";
import { providerStartupTimeoutSchema } from "../config/providerStartupTimeout.js";
@@ -44,10 +43,8 @@ export interface KunaConfiguration {
readonly cacheRoot: string;
readonly cacheMaxBytes: number;
readonly projectTimeoutMs: number;
readonly projectTimeoutSource: "default" | "setting" | "runtime";
/** Deadline for one single-function `kuna decompile` run. */
readonly functionTimeoutMs: number;
readonly functionTimeoutSource: "default" | "setting" | "runtime";
readonly jobs: number;
}
@@ -55,42 +52,23 @@ export interface KunaConfiguration {
export interface KunaDeadline {
readonly setting: "REA_KUNA_TIMEOUT_MS" | "REA_KUNA_PROJECT_TIMEOUT_MS";
readonly ms: number;
readonly source: "default" | "setting" | "runtime";
}
/** Single-function decompiles and the standalone inventory. */
export const kunaFunctionDeadline = (
configuration: KunaConfiguration,
): 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" };
};
): KunaDeadline => ({
setting: "REA_KUNA_TIMEOUT_MS",
ms: configuration.functionTimeoutMs,
});
/** 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 => ({
setting: "REA_KUNA_PROJECT_TIMEOUT_MS",
ms: configuration.projectTimeoutMs,
});
/** Why the configured environment cannot select a Kuna installation. */
export interface KunaConfigurationFailure {
@@ -122,41 +100,6 @@ const positiveInteger = (value: string): number | undefined =>
? Number(value)
: undefined;
/**
* A runtime override replaces an invalid launch string. Cache size and jobs
* stay launch-only and still fail closed.
*/
const configuredTimeout = (
raw: string | undefined,
setting: string,
fallback: number,
override: number | undefined,
): Result<
{
readonly ms: number;
readonly source: "default" | "setting" | "runtime";
},
KunaConfigurationFailure
> => {
if (raw === undefined)
return ok({
ms: override ?? fallback,
source: override === undefined ? "default" : "runtime",
});
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 failure(
"open_options_invalid",
`${setting} must be a decimal millisecond count from 1 to 2147483647; received ${String(raw)}.`,
setting,
);
};
/** Parse the Kuna settings without touching the filesystem or the executable. */
export const readKunaConfiguration = (
environment: Readonly<NodeJS.ProcessEnv>,
@@ -194,21 +137,29 @@ export const readKunaConfiguration = (
"REA_KUNA_CACHE_MAX_BYTES",
);
const overrides = currentAnalysisOperationSnapshot()?.overrides;
const projectTimeout = configuredTimeout(
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;
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;
const rawTimeout = environment.REA_KUNA_PROJECT_TIMEOUT_MS;
const timeout =
rawTimeout === undefined
? undefined
: providerStartupTimeoutSchema.safeParse(rawTimeout);
if (timeout !== undefined && !timeout.success)
return failure(
"open_options_invalid",
`REA_KUNA_PROJECT_TIMEOUT_MS must be a decimal millisecond count from 1 to 2147483647; received ${String(rawTimeout)}.`,
"REA_KUNA_PROJECT_TIMEOUT_MS",
);
const rawFunctionTimeout = environment.REA_KUNA_TIMEOUT_MS;
const functionTimeout =
rawFunctionTimeout === undefined
? undefined
: providerStartupTimeoutSchema.safeParse(rawFunctionTimeout);
if (functionTimeout !== undefined && !functionTimeout.success)
return failure(
"open_options_invalid",
`REA_KUNA_TIMEOUT_MS must be a decimal millisecond count from 1 to 2147483647; received ${String(rawFunctionTimeout)}.`,
"REA_KUNA_TIMEOUT_MS",
);
const rawJobs = environment.REA_KUNA_JOBS;
const jobs = rawJobs === undefined ? DEFAULT_JOBS : positiveInteger(rawJobs);
@@ -229,10 +180,8 @@ export const readKunaConfiguration = (
platform,
),
cacheMaxBytes,
projectTimeoutMs: projectTimeout.value.ms,
projectTimeoutSource: projectTimeout.value.source,
functionTimeoutMs: functionTimeout.value.ms,
functionTimeoutSource: functionTimeout.value.source,
projectTimeoutMs: timeout?.data ?? DEFAULT_PROJECT_TIMEOUT_MS,
functionTimeoutMs: functionTimeout?.data ?? DEFAULT_FUNCTION_TIMEOUT_MS,
jobs,
});
};
+1 -1
View File
@@ -215,7 +215,7 @@ export const exportKunaProject = async (
): Promise<KunaExportOutcome> => {
const { installation, signal } = request;
const { configuration } = installation;
const deadline = Date.now() + kunaProjectDeadline(configuration).ms;
const deadline = Date.now() + configuration.projectTimeoutMs;
const input = join(request.workspace, "input", basename(request.targetPath));
await mkdir(join(request.workspace, "input"), { mode: 0o700 });
await mkdir(request.output, { recursive: true, mode: 0o700 });
+6 -44
View File
@@ -39,7 +39,6 @@ import { combineFunctionRelationshipCoverage } from "../domain/functionCallCover
import { functionDossierSchema } from "../domain/native/functionDossier.js";
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
import type { JsonValue } from "../domain/jsonValue.js";
import { analysisSettingLimit } from "../domain/analysisSettingDefinitions.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import type { RelationshipCoverage } from "../domain/relationshipCoverage.js";
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
@@ -53,12 +52,7 @@ import {
kunaRetainedResourceCleanupError,
type KunaProcessLauncher,
} from "./KunaCommand.js";
import {
kunaFunctionDeadline,
kunaProjectDeadline,
type KunaConfiguration,
type KunaDeadline,
} from "./KunaConfiguration.js";
import { kunaFunctionDeadline } from "./KunaConfiguration.js";
import {
createKunaResourceScope,
inspectKunaInstallation,
@@ -284,15 +278,13 @@ const exportUnavailableMessage = (
/**
* How a request that needs the export reports the export's failure. A session
* never rebuilds a failed export, so repeating the request cannot succeed; the
* error says what to change before reopening the target. A failed export is
* not rebuilt in the same session. The project deadline can change at runtime
* for the next export. Cancellation and
* error says what to change before reopening the target. Kuna settings are read
* when REA starts, so changing one also needs a restart. Cancellation and
* unproven cleanup keep their own errors, which callers must still observe.
*/
const exportUnavailable = (
operation: string,
failure: AnalysisError,
deadline?: KunaDeadline,
): AnalysisError => {
if (failure instanceof AnalysisCancelledError || failure.cleanupIncomplete)
return failure;
@@ -300,23 +292,9 @@ const exportUnavailable = (
const exported = projectAnalysisError(failure);
const restart =
"REA reads Kuna settings when it starts, so restart REA, then reopen the target.";
if (
failure instanceof AnalysisTimeoutError &&
failure.limit === undefined &&
deadline !== undefined &&
deadline.setting === "REA_KUNA_PROJECT_TIMEOUT_MS" &&
deadline.ms === failure.timeoutMs
)
failure.bindLimit(
analysisSettingLimit(
"kuna.project_timeout_ms",
deadline.ms,
deadline.source,
),
);
const next =
failure instanceof AnalysisTimeoutError
? `To retry, set kuna.project_timeout_ms above ${String(failure.timeoutMs)} ms with update_analysis_settings. It can be changed at runtime. The launch seed is REA_KUNA_PROJECT_TIMEOUT_MS. Then reopen the target so the export runs again.`
? `To retry, set REA_KUNA_PROJECT_TIMEOUT_MS above ${String(failure.timeoutMs)} ms in the environment that starts REA. ${restart}`
: failure instanceof AnalysisResourceConstraintError &&
failure.remediationAction !== undefined
? // A fixed buffer (`memory`) is no setting, so a restart cannot help.
@@ -416,8 +394,6 @@ export class KunaSessionClient implements AnalysisClient {
#installation:
| SharedStartupAttempt<Result<KunaInstallation, AnalysisError>>
| undefined;
/** Launch configuration captured when the export starts, for its deadline source. */
#kunaConfiguration: KunaConfiguration | undefined;
#inventory:
| Promise<Result<readonly KunaInventoryFunction[], AnalysisError>>
| undefined;
@@ -2060,14 +2036,7 @@ export class KunaSessionClient implements AnalysisClient {
return this.#starting;
}
#projectDeadline(): KunaDeadline | undefined {
return this.#kunaConfiguration === undefined
? undefined
: kunaProjectDeadline(this.#kunaConfiguration);
}
async #lookupOrBuild(installation: KunaInstallation): Promise<void> {
this.#kunaConfiguration = installation.configuration;
const key = kunaProjectKey({
targetSha256: this.#target.sha256,
targetBasename: basename(this.#target.path),
@@ -2305,11 +2274,7 @@ export class KunaSessionClient implements AnalysisClient {
settled.error instanceof KunaPublishedProjectCleanupError
)
return undefined;
const unavailable = exportUnavailable(
operation,
settled.error,
this.#projectDeadline(),
);
const unavailable = exportUnavailable(operation, settled.error);
return unavailable instanceof AnalysisCapabilityUnavailableError
? unavailable
: undefined;
@@ -2343,10 +2308,7 @@ export class KunaSessionClient implements AnalysisClient {
}, WAIT_PROGRESS_INTERVAL_MS);
try {
const built = await waitFor(project, options.signal, operation);
if (!built.ok)
return err(
exportUnavailable(operation, built.error, this.#projectDeadline()),
);
if (!built.ok) return err(exportUnavailable(operation, built.error));
return ok(this.#readerFor(built.value));
} finally {
clearInterval(timer);
-11
View File
@@ -278,12 +278,6 @@ export class PwntoolsDecoder<
...resolvePwntoolsLimits(selectedLimits.data),
command_timeout_ms: timeoutMs,
};
let inputBytesSource:
| "default"
| "setting"
| "runtime"
| "request"
| undefined;
const context = {
operation: OPERATION,
providerId: this.identity.id,
@@ -291,9 +285,6 @@ export class PwntoolsDecoder<
get producerLimits() {
return producerLimits;
},
get inputBytesSource() {
return inputBytesSource;
},
};
const replySchema = z.discriminatedUnion("ok", [
z.strictObject({
@@ -374,7 +365,6 @@ export class PwntoolsDecoder<
: {
configured: true as const,
bytes: selectedLimits.data.input_bytes,
source: "request" as const,
};
if (!inputLimit.configured)
throw pwntoolsUnavailable(
@@ -387,7 +377,6 @@ export class PwntoolsDecoder<
configurationKey: "REA_PWNTOOLS_MAX_INPUT_BYTES",
},
);
inputBytesSource = inputLimit.source;
phase = "artifact-read";
producerLimits = {
...resolvePwntoolsLimits(selectedLimits.data, inputLimit.bytes),
@@ -7,7 +7,6 @@ import {
pwntoolsHostSupported,
} from "./PwntoolsDecoder.js";
import { pwntoolsDecoderFailure } from "./PwntoolsFailures.js";
import { resolvePwntoolsLimits } from "../../domain/native/pwntoolsResourceLimits.js";
it.each([undefined, null, false, 0, "stop", { stop: true }, new Error("stop")])(
"preserves opaque cancellation from command and artifact owners: %s",
@@ -30,35 +29,6 @@ it.each([undefined, null, false, 0, "stop", { stop: true }, new Error("stop")])(
},
);
it("keeps the caller-selected pwntools input-limit source", () => {
const projected = projectAnalysisError(
pwntoolsDecoderFailure(
new ArtifactReaderFailure(
"limit",
"artifact exceeds the input allowance",
{},
),
"artifact-read",
"/selected",
"/python",
{},
{
operation: "inspect_binary_layout",
providerId: "pwntools",
producerLimits: resolvePwntoolsLimits(undefined, 4096),
inputBytesSource: "request",
},
),
);
expect(projected.details?.limit).toMatchObject({
setting: "pwntools.max_input_bytes",
effective_value: 4096,
source: "request",
update_mode: "parameter",
pending_restart: false,
});
});
it("distinguishes the wall deadline from caller-selected producer resource budgets", () => {
const projected = projectAnalysisError(
pwntoolsDecoderFailure(
+1 -13
View File
@@ -1,4 +1,3 @@
import { analysisSettingLimit } from "../../domain/analysisSettingDefinitions.js";
import {
PWNTOOLS_LIMITS,
resolvePwntoolsLimits,
@@ -45,12 +44,6 @@ export interface PwntoolsFailureContext {
readonly configurationKey?: string;
readonly timeoutMs?: number;
readonly producerLimits?: PwntoolsProducerLimits;
readonly inputBytesSource?:
| "default"
| "setting"
| "runtime"
| "request"
| undefined;
}
/** Recovery identifies the selected controls rather than unrelated wall deadlines. */
@@ -303,12 +296,7 @@ const classifyPwntoolsDecoderFailure = (
...(cause.reason === "limit"
? {
userMessage: `${cause.message} Selected producer_limits.input_bytes is ${String(limits.input_bytes)} bytes. ${budgetRecovery}`,
remediationAction: `pwntools.max_input_bytes is the launch default of ${String(PWNTOOLS_LIMITS.input_bytes)} bytes and can be changed at runtime with update_analysis_settings. The launch seed is REA_PWNTOOLS_MAX_INPUT_BYTES. producer_limits.input_bytes overrides that default on the next call (CLI --producer-limits JSON). The effective input_bytes for this call is ${String(limits.input_bytes)}. CPU, memory, output and transport allowances remain independent.`,
limit: analysisSettingLimit(
"pwntools.max_input_bytes",
limits.input_bytes,
context.inputBytesSource,
),
remediationAction: `Increase producer_limits.input_bytes above ${String(limits.input_bytes)} on the next call (CLI --producer-limits JSON). No restart is required. Changing REA_PWNTOOLS_MAX_INPUT_BYTES affects the launch default instead. CPU, memory, output and transport allowances remain independent.`,
}
: {}),
},
@@ -14,7 +14,6 @@ describe("pwntools input bound", () => {
expect(pwntoolsInputBytes({})).toEqual({
configured: true,
bytes: PWNTOOLS_LIMITS.input_bytes,
source: "default",
});
expect(PWNTOOLS_LIMITS.input_bytes).toBe(32 * 1024 * 1024);
});
@@ -26,7 +25,7 @@ describe("pwntools input bound", () => {
["1073741824", 1024 * 1024 * 1024],
] as const)("accepts %s as %s bytes", (value, expected) => {
expect(pwntoolsInputBytes({ REA_PWNTOOLS_MAX_INPUT_BYTES: value })).toEqual(
{ configured: true, bytes: expected, source: "setting" },
{ configured: true, bytes: expected },
);
});
+3 -16
View File
@@ -1,6 +1,5 @@
import { z } from "zod";
import { currentAnalysisOperationSnapshot } from "../../domain/analysisOperationSnapshot.js";
import { PWNTOOLS_LIMITS } from "../../domain/native/pwntoolsResourceLimits.js";
// The artifact is read into one in-memory buffer and staged again as a private
@@ -21,11 +20,7 @@ const CONSTRAINT =
/** Resolved input bound, or the reason the caller's override cannot be used. */
export type PwntoolsInputBytes =
| {
readonly configured: true;
readonly bytes: number;
readonly source: "default" | "setting" | "runtime" | "request";
}
| { readonly configured: true; readonly bytes: number }
| { readonly configured: false; readonly error: string };
/**
@@ -35,19 +30,11 @@ export type PwntoolsInputBytes =
export const pwntoolsInputBytes = (
environment: Readonly<NodeJS.ProcessEnv>,
): PwntoolsInputBytes => {
const override =
currentAnalysisOperationSnapshot()?.overrides["pwntools.max_input_bytes"];
if (override !== undefined)
return { configured: true, bytes: override, source: "runtime" };
const value = environment.REA_PWNTOOLS_MAX_INPUT_BYTES;
if (value === undefined)
return {
configured: true,
bytes: PWNTOOLS_LIMITS.input_bytes,
source: "default",
};
return { configured: true, bytes: PWNTOOLS_LIMITS.input_bytes };
const parsed = pwntoolsInputBytesSchema.safeParse(value);
return parsed.success
? { configured: true, bytes: parsed.data, source: "setting" }
? { configured: true, bytes: parsed.data }
: { configured: false, error: CONSTRAINT };
};
+1 -1
View File
@@ -515,7 +515,7 @@ export const regexWorkerHeapError = (
),
}
: {}),
remediationAction: `${heapBound(heap, request)} Use update_analysis_settings with setting regex.worker_heap_request_mib to change the request for the next worker. The launch seed is ${REGEX_WORKER_HEAP_REQUEST_SETTING}. Restart REA after changing --max-old-space-size; launch heap flags can override the request. A launch --max-old-space-size-percentage takes precedence over that flag in either order. Simplify the pattern or use literal mode.`,
remediationAction: `${heapBound(heap, request)} Use update_analysis_settings with setting regex.worker_heap_request_mib to change the request for the next worker; launch heap flags can override it. Restart REA after changing --max-old-space-size or ${REGEX_WORKER_HEAP_REQUEST_SETTING} in the launch environment. A launch --max-old-space-size-percentage takes precedence over that flag in either order. Simplify the pattern or use literal mode.`,
},
);
/**
+23 -63
View File
@@ -1,12 +1,6 @@
import { readFile } from "node:fs/promises";
import {
DEBUGGER_COMMAND_DEADLINE_SETTING,
DEBUGGER_MAX_COMMAND_OUTPUT_BYTES_SETTING,
type DebuggerAdmissionLimit,
} from "../config/debuggerSessionLimits.js";
import { analysisSettingLimit } from "../domain/analysisSettingDefinitions.js";
import type { AnalysisSettingId } from "../domain/analysisSettings.js";
import type { DebuggerAdmissionLimit } from "../config/debuggerSessionLimits.js";
import { AnalysisResourceConstraintError } from "../domain/analysisErrorCore.js";
import type { JsonValue } from "../domain/jsonValue.js";
import { parseLinuxProcessStat } from "./LinuxProcessStat.js";
@@ -17,64 +11,34 @@ export type OwnedDebuggerStopResult =
| { readonly status: "stopped" | "already_exited" }
| { readonly status: "incomplete"; readonly reason: string };
const RUNTIME_DEBUGGER_SETTINGS = {
[DEBUGGER_COMMAND_DEADLINE_SETTING]: "debugger.command_deadline_ms",
[DEBUGGER_MAX_COMMAND_OUTPUT_BYTES_SETTING]:
"debugger.max_command_output_bytes",
} as const satisfies Partial<Record<string, AnalysisSettingId>>;
const runtimeDebuggerSetting = (
admission: DebuggerAdmissionLimit,
): AnalysisSettingId | undefined =>
RUNTIME_DEBUGGER_SETTINGS[
admission.setting as keyof typeof RUNTIME_DEBUGGER_SETTINGS
];
/** Structured limit for a debugger budget that update_analysis_settings can change. */
export const debuggerAnalysisLimit = (admission: DebuggerAdmissionLimit) => {
const setting = runtimeDebuggerSetting(admission);
return setting === undefined
? undefined
: analysisSettingLimit(setting, admission.value, admission.source);
};
/** Structured facts a limit error exposes. The sentence is display text. */
export const debuggerAdmissionDiagnostics = (
admission: DebuggerAdmissionLimit,
) => {
const analysisSetting = runtimeDebuggerSetting(admission);
const update_mode = analysisSetting === undefined ? "restart" : "runtime";
return {
setting: admission.setting,
effective_value: admission.value,
source: admission.source,
default_value: admission.defaultValue,
minimum: admission.minimum,
maximum: admission.maximum,
unit: admission.unit,
update_mode,
pending_restart: false as const,
...(analysisSetting === undefined
? {}
: { analysis_setting: analysisSetting }),
};
};
): {
readonly setting: string;
readonly effective_value: number;
readonly source: DebuggerAdmissionLimit["source"];
readonly default_value: number;
readonly minimum: number;
readonly maximum: number;
readonly unit: DebuggerAdmissionLimit["unit"];
readonly restart_required: true;
} => ({
setting: admission.setting,
effective_value: admission.value,
source: admission.source,
default_value: admission.defaultValue,
minimum: admission.minimum,
maximum: admission.maximum,
unit: admission.unit,
restart_required: true,
});
/** Name the setting, the effective value, its source, and the range. */
/** Name the setting, the effective value, its source, the range, and a restart. */
export const debuggerAdmissionRecovery = (
admission: DebuggerAdmissionLimit,
): string => {
const facts = debuggerAdmissionDiagnostics(admission);
const identity =
facts.analysis_setting === undefined
? admission.setting
: `${facts.analysis_setting} (launch seed ${admission.setting})`;
const change =
facts.update_mode === "runtime"
? `It can be changed at runtime with update_analysis_settings.`
: `Set ${admission.setting} in the environment that starts REA to a positive decimal integer from ${String(admission.minimum)} to ${String(admission.maximum)}, then restart REA.`;
return `The effective ${identity} value is ${String(admission.value)} ${admission.unit} (source ${admission.source}; default ${String(admission.defaultValue)} ${admission.unit}, floor ${String(admission.minimum)} ${admission.unit}, maximum ${String(admission.maximum)} ${admission.unit}). ${change}`;
};
): string =>
`The effective ${admission.setting} value is ${String(admission.value)} ${admission.unit} (source ${admission.source}; default ${String(admission.defaultValue)} ${admission.unit}, floor ${String(admission.minimum)} ${admission.unit}, maximum ${String(admission.maximum)} ${admission.unit}). Set ${admission.setting} in the environment that starts REA to a positive decimal integer from ${String(admission.minimum)} to ${String(admission.maximum)}, then restart REA.`;
/** Fixed startup deadline. Distinct from the shared command deadline setting. */
export const fixedDeadlineGuidance = (
@@ -95,12 +59,10 @@ export const configurableDeadlineGuidance = (
readonly diagnostics: ReturnType<typeof debuggerAdmissionDiagnostics>;
} => {
const recovery = debuggerAdmissionRecovery(admission);
const limit = debuggerAnalysisLimit(admission);
return {
userMessage: `${what} reached its ${String(admission.value)} ms deadline (deadlineExpired). ${recovery}`,
remediationAction: recovery,
diagnostics: debuggerAdmissionDiagnostics(admission),
...(limit === undefined ? {} : { limit }),
};
};
@@ -115,7 +77,6 @@ export const debuggerResourceConstraintError = (
): AnalysisResourceConstraintError => {
const facts = debuggerAdmissionDiagnostics(admission);
const recovery = debuggerAdmissionRecovery(admission);
const limit = debuggerAnalysisLimit(admission);
return new AnalysisResourceConstraintError(
operation,
resource,
@@ -124,7 +85,6 @@ export const debuggerResourceConstraintError = (
{
remediationAction: `${remediation} ${recovery}`,
diagnostics: facts,
...(limit === undefined ? {} : { limit }),
},
);
};
-1
View File
@@ -201,7 +201,6 @@ export const analysisClientFailureForOperation = (
...(cause.remediationAction === undefined
? {}
: { remediationAction: cause.remediationAction }),
...(cause.limit === undefined ? {} : { limit: cause.limit }),
});
else if (cause instanceof AnalysisOutputError)
result = new AnalysisOutputError(operation, cause.reason, {
+8 -30
View File
@@ -28,7 +28,6 @@ import type {
} from "../domain/analysisErrorBase.js";
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
import { createEvidence, type Evidence } from "../domain/evidence.js";
import { currentAnalysisOperationSnapshot } from "../domain/analysisOperationSnapshot.js";
import { err, ok, type Result } from "../domain/result.js";
import {
ProviderProcessSupervisor,
@@ -264,11 +263,13 @@ export class RizinDebugSessionManager {
#closingAll = false;
readonly #startDrainWaiters: Array<() => void> = [];
readonly #command: string;
readonly #launchCommandDeadline: DebuggerAdmissionLimit;
readonly #commandDeadline: DebuggerAdmissionLimit;
readonly #frameTimeoutMs: number;
readonly #startupTimeoutMs: number;
readonly #maxSessions: DebuggerAdmissionLimit;
readonly #maxPendingCommands: DebuggerAdmissionLimit;
readonly #launchCommandOutput: DebuggerAdmissionLimit;
readonly #commandOutput: DebuggerAdmissionLimit;
readonly #maxCommandOutputBytes: number;
readonly #platform: NodeJS.Platform;
readonly #stopProcess: typeof stopOwnedDebuggerProcess;
readonly #spawnProcess: typeof spawnOwnedProviderProcess;
@@ -286,41 +287,18 @@ export class RizinDebugSessionManager {
this.#environment = { ...environment };
this.#command = environment.REA_RIZIN_COMMAND ?? "rizin";
const limits = resolveDebuggerSessionLimits(this.#environment);
this.#launchCommandDeadline = limits.commandDeadline;
this.#commandDeadline = limits.commandDeadline;
this.#frameTimeoutMs = this.#commandDeadline.value;
this.#startupTimeoutMs = options.startupTimeoutMs ?? STARTUP_TIMEOUT_MS;
this.#maxSessions = limits.maxSessions;
this.#maxPendingCommands = limits.maxPendingCommands;
this.#launchCommandOutput = limits.maxCommandOutputBytes;
this.#commandOutput = limits.maxCommandOutputBytes;
this.#maxCommandOutputBytes = this.#commandOutput.value;
this.#platform = options.platform ?? process.platform;
this.#stopProcess = options.stopProcess ?? stopOwnedDebuggerProcess;
this.#spawnProcess = options.spawnProcess ?? spawnOwnedProviderProcess;
}
get #commandDeadline(): DebuggerAdmissionLimit {
const override =
currentAnalysisOperationSnapshot()?.overrides[
"debugger.command_deadline_ms"
];
return override === undefined
? this.#launchCommandDeadline
: { ...this.#launchCommandDeadline, value: override, source: "runtime" };
}
get #frameTimeoutMs(): number {
return this.#commandDeadline.value;
}
get #commandOutput(): DebuggerAdmissionLimit {
const override =
currentAnalysisOperationSnapshot()?.overrides[
"debugger.max_command_output_bytes"
];
return override === undefined
? this.#launchCommandOutput
: { ...this.#launchCommandOutput, value: override, source: "runtime" };
}
get #maxCommandOutputBytes(): number {
return this.#commandOutput.value;
}
async start(
input: {
readonly path: string;
+1 -9
View File
@@ -1066,15 +1066,7 @@ it("enforces a selected command deadline and completes a corrected retry after c
if (timed.ok) throw new Error("Selected 20 ms deadline was not enforced");
expect(projectAnalysisError(timed.error)).toMatchObject({
code: "provider_timeout",
details: {
timeout_ms: 20,
limit: {
setting: "wasm.command_timeout_ms",
effective_value: 20,
source: "request",
update_mode: "parameter",
},
},
details: { timeout_ms: 20 },
remediation: { action: expect.stringContaining("command_timeout_ms") },
});
expect(timed.error.cleanupIncomplete).toBe(false);
-7
View File
@@ -229,10 +229,6 @@ export class WabtArtifactProvider implements WasmArtifactPort {
input.command_timeout_ms ??
options?.commandTimeoutMs ??
DEFAULT_WASM_COMMAND_TIMEOUT_MS;
const commandTimeoutSource =
input.command_timeout_ms !== undefined
? "request"
: options?.commandTimeoutSource;
try {
if (options?.signal?.aborted)
throw new AnalysisCancelledError(OPERATION, {
@@ -699,9 +695,6 @@ export class WabtArtifactProvider implements WasmArtifactPort {
phase === "artifact" ? artifactInput.path : input.path,
{
commandTimeoutMs,
...(commandTimeoutSource === undefined
? {}
: { commandTimeoutSource }),
limits,
inputField: artifactInput.field,
observedOutput: capturedOutput,
+2 -22
View File
@@ -52,8 +52,7 @@ it.each(["configuration", "producer"] as const)(
limit: {
setting: "wasm.command_timeout_ms",
effective_value: 1234,
update_mode: "runtime",
pending_restart: false,
rea_restart_required: false,
},
},
recovery: { kind: "inspect_timeout_state", timeout_ms: 1234 },
@@ -116,30 +115,11 @@ it.each([1234, 2_147_483_647])(
parameter: "command_timeout_ms",
applies_at: "next_operation",
agent_editable: true,
update_mode: "runtime",
pending_restart: false,
rea_restart_required: false,
},
},
recovery: { kind: "inspect_timeout_state", timeout_ms: commandTimeoutMs },
});
expect(
projectAnalysisError(
wabtFailure(
new OwnedCommandFailure("timeout", "deadline"),
"producer",
"/module.wasm",
{ commandTimeoutMs, commandTimeoutSource: "request" },
),
),
).toMatchObject({
details: {
limit: {
source: "request",
update_mode: "parameter",
effective_value: commandTimeoutMs,
},
},
});
},
);
+1 -11
View File
@@ -63,12 +63,6 @@ export type WabtVersionProbe = {
type WabtFailureContext = {
readonly commandTimeoutMs: number;
readonly commandTimeoutSource?:
| "default"
| "setting"
| "runtime"
| "request"
| undefined;
readonly limits?: WasmResourceLimits;
readonly versionProbe?: WabtVersionProbe;
readonly inputField?: readonly (string | number)[];
@@ -188,17 +182,13 @@ const classifyWabtFailure = (
// Classify once before cleanup takes precedence, so the primary remains actionable.
const timeoutMs =
execution?.commandTimeoutMs ?? DEFAULT_WASM_COMMAND_TIMEOUT_MS;
const timeoutSource =
execution?.commandTimeoutMs === undefined
? "default"
: execution.commandTimeoutSource;
const timeout =
cause.reason === "timeout"
? new AnalysisTimeoutError("inspect_wasm_artifact", timeoutMs, {
limit: analysisSettingLimit(
"wasm.command_timeout_ms",
timeoutMs,
timeoutSource,
undefined,
),
...options,
// The cleanup wrapper retains output once; its nested primary needs only recovery facts.
+1 -1
View File
@@ -148,7 +148,7 @@ it("advertises exact contracts and recovers a full large-index archive over one
expect(updated.structuredContent).toMatchObject({
revision: 1,
active_work_changed: false,
pending_restart: false,
rea_restart_required: false,
});
const recovered = await invoke("inspect_artifact", {});
expect(recovered.isError).not.toBe(true);
@@ -186,8 +186,7 @@ for (const reader of readers)
effective_value: reader.value,
applies_at: "next_operation",
agent_editable: true,
update_mode: "parameter",
pending_restart: false,
rea_restart_required: false,
},
},
});
@@ -87,8 +87,7 @@ it.skipIf(process.versions.bun !== undefined && jobLimit === null)(
setting: "capture.decoder_old_generation_mib",
effective_value: testHeap.oldGenerationMiB,
source: testHeap.source,
update_mode: "runtime",
pending_restart: false,
rea_restart_required: false,
},
reported_limits:
jobLimit === null
@@ -148,8 +147,7 @@ it.skipIf(process.versions.bun !== undefined && jobLimit === null)(
setting: "capture.decoder_old_generation_mib",
effective_value: testHeap.oldGenerationMiB,
source: testHeap.source,
update_mode: "runtime",
pending_restart: false,
rea_restart_required: false,
},
},
remediation: {
@@ -216,8 +216,7 @@ it("names the default header admission budget and its setting when source select
applies_at: "next_operation",
agent_editable: true,
scope: "rea-runtime",
update_mode: "runtime",
pending_restart: false,
rea_restart_required: false,
},
},
});
@@ -66,8 +66,7 @@ it.each([
required_value: needed,
applies_at: "next_operation",
agent_editable: true,
update_mode: "parameter",
pending_restart: false,
rea_restart_required: false,
},
});
const recovered = await readDotnetBundle(
@@ -74,7 +74,7 @@ it("names the JEB discovery deadline setting instead of repeating the expired de
reason: "provider_unavailable",
});
expect(entry?.remediation).toMatch(
/jeb\.mcp_timeout_ms.*update_analysis_settings.*REA_JEB_MCP_TIMEOUT_MS/iu,
/increase REA_JEB_MCP_TIMEOUT_MS.*restart and reconnect REA/iu,
);
expect(entry?.remediation).toMatch(
/catalog discovery.*tools\/call.*cleanup.*headroom/iu,
@@ -36,9 +36,7 @@ it("preserves full advertised schemas and handler meaning when presentation orde
REA_MCP_INPUT_SCHEMA_PROFILE: "full",
REA_MCP_OUTPUT_SCHEMA_PROFILE: "full",
},
// The child advertises the full catalog and calls every browser, Electron,
// and JavaScript example. A busy runner exceeded 20s and was killed.
timeout: 60_000,
timeout: 20_000,
maxBuffer: 16 * 1024 * 1024,
});
const result = resultSchema.parse(JSON.parse(stdout));
Loaded 100 of 118 files, more files were not shown because too many files have changed in this diff. Show more