mirror of
https://github.com/morluto/rea.git
synced 2026-11-10 17:30:32 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2001a585ec | ||
|
|
4da394a52f |
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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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.`);
|
||||
@@ -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;
|
||||
@@ -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.`,
|
||||
);
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -5,6 +5,8 @@ import { expect, it, vi } from "vitest";
|
||||
import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
|
||||
import { apiContractSchema } from "../../domain/apiContract/apiContract.js";
|
||||
import { AnalysisResourceConstraintError } from "../../domain/analysisErrorCore.js";
|
||||
import { parseEvidence } from "../../domain/evidence.js";
|
||||
import { projectAnalysisView } from "../../domain/analysisView/analysisView.js";
|
||||
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
|
||||
import { inspectApiContractEvidence } from "./ApiContractService.js";
|
||||
import * as readOwner from "./ApiContractRead.js";
|
||||
@@ -317,3 +319,89 @@ it("cancels a cooperative document traversal after the actual verified file read
|
||||
await resources.close();
|
||||
}
|
||||
});
|
||||
|
||||
it.each(["between-paths", "after-projection"] as const)(
|
||||
"preserves authenticated OpenAPI facts on cancellation %s",
|
||||
async (stage) => {
|
||||
const bytes = JSON.stringify({
|
||||
openapi: "3.1.2",
|
||||
info: { title: "Cancellation", version: "1" },
|
||||
paths: { "/first": { get: {} }, "/second": { post: {} } },
|
||||
});
|
||||
const { path } = await inputFile(bytes);
|
||||
const resources = new ArtifactResourceScope();
|
||||
const controller = new AbortController();
|
||||
const realParse = parser.parseApiContractSteps;
|
||||
try {
|
||||
const baseline = await inspectApiContractEvidence({ path }, resources);
|
||||
if (!baseline.ok) throw baseline.error;
|
||||
const complete = apiContractSchema.parse(
|
||||
baseline.value.normalized_result,
|
||||
);
|
||||
const scheduled = vi
|
||||
.spyOn(parser, "parseApiContractSteps")
|
||||
.mockImplementationOnce(function* (...args) {
|
||||
const steps = realParse(...args);
|
||||
if (stage === "between-paths") {
|
||||
expect(steps.next().done).toBe(false);
|
||||
expect(steps.next().done).toBe(false);
|
||||
setImmediate(() => controller.abort(stage));
|
||||
return yield* steps;
|
||||
}
|
||||
const result = yield* steps;
|
||||
setImmediate(() => controller.abort(stage));
|
||||
return result;
|
||||
});
|
||||
try {
|
||||
const result = await inspectApiContractEvidence({ path }, resources, {
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (result.ok) throw new Error("Expected cancellation");
|
||||
expect(result.error._tag).toBe("AnalysisCancelledError");
|
||||
expect(result.error.cause).toBe(stage);
|
||||
const evidence = parseEvidence(result.error.partialObservation);
|
||||
const retained = apiContractSchema.parse(evidence.normalized_result);
|
||||
expect(evidence.subject).toEqual(baseline.value.subject);
|
||||
expect(evidence.parameters).toEqual(baseline.value.parameters);
|
||||
expect(retained.artifact).toEqual(complete.artifact);
|
||||
expect(retained.contract_id).toBe(complete.contract_id);
|
||||
if (stage === "between-paths") {
|
||||
expect(retained.operations).toEqual(complete.operations.slice(0, 1));
|
||||
expect(retained.source_coverage.status).toBe("partial");
|
||||
expect(evidence.evidence_id).not.toBe(baseline.value.evidence_id);
|
||||
const page = projectAnalysisView(
|
||||
{
|
||||
evidenceId: evidence.evidence_id,
|
||||
operation: evidence.operation,
|
||||
predicateType: evidence.predicate_type,
|
||||
normalizedResult: evidence.normalized_result,
|
||||
limitations: evidence.limitations,
|
||||
},
|
||||
{
|
||||
kind: "api-contract",
|
||||
collection: "operations",
|
||||
offset: 0,
|
||||
limit: 1,
|
||||
},
|
||||
);
|
||||
expect(page).toMatchObject({
|
||||
ok: true,
|
||||
value: {
|
||||
parent_evidence_id: evidence.evidence_id,
|
||||
items: [{ path: "/first", state: "declared" }],
|
||||
coverage: { exhausted: true },
|
||||
source_coverage: { status: "partial" },
|
||||
},
|
||||
});
|
||||
} else {
|
||||
expect(retained).toEqual(complete);
|
||||
expect(evidence.evidence_id).toBe(baseline.value.evidence_id);
|
||||
}
|
||||
} finally {
|
||||
scheduled.mockRestore();
|
||||
}
|
||||
} finally {
|
||||
await resources.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -11,6 +11,8 @@ import { err, ok, type Result } from "../../domain/result.js";
|
||||
import {
|
||||
API_CONTRACT_OPERATION as operation,
|
||||
API_CONTRACT_PROVIDER,
|
||||
apiContractSchema,
|
||||
type ApiContract,
|
||||
type ApiContractArtifact,
|
||||
} from "../../domain/apiContract/apiContract.js";
|
||||
import {
|
||||
@@ -70,8 +72,32 @@ function* documentSteps(document: unknown, artifact: ApiContractArtifact) {
|
||||
return yield* parseApiContractSteps(document, artifact);
|
||||
}
|
||||
|
||||
type OpenApiInput = Extract<
|
||||
InspectApiContractInput,
|
||||
{ format: "openapi-json" }
|
||||
>;
|
||||
|
||||
const documentEvidence = (input: OpenApiInput, result: ApiContract): Evidence =>
|
||||
createEvidence(
|
||||
{ path: input.path, format: "file", sha256: result.artifact.sha256 },
|
||||
API_CONTRACT_PROVIDER,
|
||||
{
|
||||
operation,
|
||||
predicateType: "rea.api-contract-inventory",
|
||||
parameters: {
|
||||
path: input.path,
|
||||
format: input.format,
|
||||
max_file_bytes: input.max_file_bytes,
|
||||
},
|
||||
result,
|
||||
confidence: "observed",
|
||||
limitations: result.limitations,
|
||||
locations: [{ kind: "artifact-path", path: input.path }],
|
||||
},
|
||||
);
|
||||
|
||||
const importDocument = async (
|
||||
input: Extract<InspectApiContractInput, { format: "openapi-json" }>,
|
||||
input: OpenApiInput,
|
||||
bytes: Buffer,
|
||||
sha256: string,
|
||||
signal?: AbortSignal,
|
||||
@@ -86,26 +112,16 @@ const importDocument = async (
|
||||
}),
|
||||
signal,
|
||||
);
|
||||
if (!parsed.ok) return parsed;
|
||||
const result = parsed.value;
|
||||
if (!parsed.ok) {
|
||||
const partial = apiContractSchema.safeParse(
|
||||
parsed.error.partialObservation,
|
||||
);
|
||||
if (partial.success)
|
||||
parsed.error.partialObservation = documentEvidence(input, partial.data);
|
||||
return parsed;
|
||||
}
|
||||
return completeProviderEvidence(
|
||||
createEvidence(
|
||||
{ path: input.path, format: "file", sha256 },
|
||||
API_CONTRACT_PROVIDER,
|
||||
{
|
||||
operation,
|
||||
predicateType: "rea.api-contract-inventory",
|
||||
parameters: {
|
||||
path: input.path,
|
||||
format: input.format,
|
||||
max_file_bytes: input.max_file_bytes,
|
||||
},
|
||||
result,
|
||||
confidence: "observed",
|
||||
limitations: result.limitations,
|
||||
locations: [{ kind: "artifact-path", path: input.path }],
|
||||
},
|
||||
),
|
||||
documentEvidence(input, parsed.value),
|
||||
signal,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]);
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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
@@ -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 = (
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 },
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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(" ");
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 : "";
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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(" ");
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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(" ");
|
||||
@@ -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";
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
@@ -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",
|
||||
};
|
||||
};
|
||||
@@ -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 = [
|
||||
{
|
||||
|
||||
@@ -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,6 +1,7 @@
|
||||
import type { PartialTaskMeasurement } from "./desktop/taskDifficulty.js";
|
||||
import type { AppleIpsPartialObservation } from "./native/appleIps.js";
|
||||
import type { PlaywrightTrace } from "./playwrightTrace/playwrightTrace.js";
|
||||
import type { ApiContract } from "./apiContract/apiContract.js";
|
||||
import type { GraphqlContract } from "./apiContract/graphqlContract.js";
|
||||
import type { SavedAnalysisSnapshotPartialObservation } from "./savedAnalysisSnapshot.js";
|
||||
import type { AnalysisSettingLimit } from "./analysisSettings.js";
|
||||
@@ -498,6 +499,7 @@ export type AnalysisPartialObservation =
|
||||
}
|
||||
| { readonly kind: "unretained-evidence"; readonly evidence: Evidence }
|
||||
| { readonly kind: "retained-evidence"; readonly evidence_id: string }
|
||||
| ApiContract
|
||||
| GraphqlContract
|
||||
| PlaywrightTrace
|
||||
| SavedAnalysisSnapshotPartialObservation
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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];
|
||||
@@ -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;
|
||||
@@ -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 }),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -185,7 +185,7 @@ export const projectApiContractView = (
|
||||
unknowns:
|
||||
context.source_coverage.status === "partial"
|
||||
? [
|
||||
"Unresolved or malformed declarations limit this inventory; query path-item-references and diagnostics for exact source pointers.",
|
||||
"This inventory is partial. Inspect limitations for coverage and query path-item-references and diagnostics for any retained source pointers.",
|
||||
]
|
||||
: [],
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import { readFileSync } from "node:fs";
|
||||
import { expect, it } from "vitest";
|
||||
import { apiContractSchema, type ApiContractArtifact } from "./apiContract.js";
|
||||
import { parseApiContractSteps } from "./parseApiContract.js";
|
||||
import { AnalysisCancelledError } from "../analysisErrorCore.js";
|
||||
|
||||
const artifact: ApiContractArtifact = {
|
||||
path: "/contract.json",
|
||||
@@ -305,9 +306,39 @@ it("handles 3.2 QUERY and case-sensitive additional methods, diagnosing invalid
|
||||
|
||||
it("bounds expanded identities and source pointers before building a giant row", () => {
|
||||
const result = parse(
|
||||
document({ [`/${"a".repeat(1_100_000)}`]: { get: {} } }),
|
||||
document({
|
||||
"/retained": { get: {} },
|
||||
[`/${"a".repeat(1_100_000)}`]: { get: {} },
|
||||
}),
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok)
|
||||
expect(result.error._tag).toBe("AnalysisResourceConstraintError");
|
||||
if (result.ok) throw new Error("Expected a retained-facts refusal");
|
||||
expect(result.error._tag).toBe("AnalysisResourceConstraintError");
|
||||
const partial = apiContractSchema.parse(result.error.partialObservation);
|
||||
expect(partial.operations.map((row) => row.path)).toEqual(["/retained"]);
|
||||
expect(partial.source_coverage.status).toBe("partial");
|
||||
});
|
||||
|
||||
it("retains an interpreted path when cancellation interrupts traversal", () => {
|
||||
const input = document({
|
||||
"/first": { get: { operationId: "first" } },
|
||||
"/second": { get: { operationId: "second" } },
|
||||
});
|
||||
const steps = parseApiContractSteps(input, artifact);
|
||||
for (let checkpoint = 0; checkpoint < 3; checkpoint++)
|
||||
expect(steps.next().done).toBe(false);
|
||||
const cancellation = new AnalysisCancelledError("inspect_api_contract");
|
||||
const stopped = steps.throw(cancellation);
|
||||
if (!stopped.done || stopped.value.ok)
|
||||
throw new Error("Expected cancellation");
|
||||
expect(stopped.value.error).toBe(cancellation);
|
||||
const partial = apiContractSchema.parse(cancellation.partialObservation);
|
||||
const complete = report(input);
|
||||
expect(partial.operations).toEqual(complete.operations.slice(0, 1));
|
||||
expect(partial.summary.operation_slots).toBe(1);
|
||||
expect(partial.source_coverage).toMatchObject({
|
||||
status: "partial",
|
||||
path_entries_examined: 1,
|
||||
});
|
||||
expect(complete.source_coverage.status).toBe("complete-within-scope");
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisInputError,
|
||||
AnalysisResourceConstraintError,
|
||||
AnalysisUnsupportedTargetError,
|
||||
@@ -316,14 +317,17 @@ function* pathItem(
|
||||
return ok(undefined);
|
||||
}
|
||||
|
||||
const completedInventory = (
|
||||
interface ContractReportContext {
|
||||
readonly openapi: string;
|
||||
readonly info: ApiContract["info"];
|
||||
readonly pathsState: ApiContract["source_coverage"]["paths_state"];
|
||||
examined: number;
|
||||
}
|
||||
|
||||
const contractReport = (
|
||||
inventory: ContractInventory,
|
||||
context: {
|
||||
openapi: string;
|
||||
info: ApiContract["info"];
|
||||
pathsState: ApiContract["source_coverage"]["paths_state"];
|
||||
examined: number;
|
||||
},
|
||||
context: ContractReportContext,
|
||||
incomplete = false,
|
||||
): ApiContract => {
|
||||
const artifact = inventory.artifact;
|
||||
let declared = 0;
|
||||
@@ -348,7 +352,9 @@ const completedInventory = (
|
||||
source_coverage: {
|
||||
scope: "inline-path-operations",
|
||||
status:
|
||||
inventory.references.length > 0 || inventory.diagnostics.length > 0
|
||||
incomplete ||
|
||||
inventory.references.length > 0 ||
|
||||
inventory.diagnostics.length > 0
|
||||
? "partial"
|
||||
: "complete-within-scope",
|
||||
paths_state: context.pathsState,
|
||||
@@ -358,6 +364,11 @@ const completedInventory = (
|
||||
duplicate_json_members: "last-value-wins",
|
||||
},
|
||||
limitations: [
|
||||
...(incomplete
|
||||
? [
|
||||
"Traversal stopped before the inventory was complete. Counts describe the retained rows.",
|
||||
]
|
||||
: []),
|
||||
"Operations are document declarations, not evidence of runtime implementation, reachability or authorization.",
|
||||
"Only inline operations under paths are inventoried; callbacks, webhooks, components, servers, parameters, schemas, request/response contracts and security are not analyzed.",
|
||||
"No reference is resolved, including local fragments. A Path Item reference makes its effective operations unknown; adjacent inline slots are preserved without merging referenced definitions.",
|
||||
@@ -368,6 +379,32 @@ const completedInventory = (
|
||||
};
|
||||
};
|
||||
|
||||
function* collectPaths(
|
||||
sourcePaths: Record<string, unknown>,
|
||||
inventory: ContractInventory,
|
||||
context: ContractReportContext,
|
||||
modern: boolean,
|
||||
): Generator<void, Result<void, AnalysisError>> {
|
||||
for (const path in sourcePaths) {
|
||||
if (!Object.hasOwn(sourcePaths, path) || path.startsWith("x-")) continue;
|
||||
yield;
|
||||
context.examined++;
|
||||
// Reserve expanded pointer/path strings before escaping or building a row.
|
||||
const admitted = inventory.reserve(512 + path.length * 16);
|
||||
if (!admitted.ok) return admitted;
|
||||
const pointer = `/paths/${pointerPart(path)}`;
|
||||
const read = path.startsWith("/")
|
||||
? yield* pathItem(sourcePaths[path], { inventory, path, pointer, modern })
|
||||
: inventory.diagnostic(
|
||||
"invalid-path-key",
|
||||
pointer,
|
||||
"A Paths entry must start with /; it was not interpreted as an endpoint.",
|
||||
);
|
||||
if (!read.ok) return read;
|
||||
}
|
||||
return ok(undefined);
|
||||
}
|
||||
|
||||
/** Enumerate declarations only; never resolve references, servers, callbacks or runtime expressions. */
|
||||
export function* parseApiContractSteps(
|
||||
value: unknown,
|
||||
@@ -417,36 +454,32 @@ export function* parseApiContractSteps(
|
||||
);
|
||||
if (!diagnosed.ok) return diagnosed;
|
||||
}
|
||||
let examined = 0;
|
||||
const sourcePaths = paths ?? {};
|
||||
for (const path in sourcePaths) {
|
||||
if (!Object.hasOwn(sourcePaths, path) || path.startsWith("x-")) continue;
|
||||
yield;
|
||||
examined++;
|
||||
// Reserve expanded pointer/path strings before escaping or building a row.
|
||||
const admitted = inventory.reserve(512 + path.length * 16);
|
||||
if (!admitted.ok) return admitted;
|
||||
const pointer = `/paths/${pointerPart(path)}`;
|
||||
const read = path.startsWith("/")
|
||||
? yield* pathItem(sourcePaths[path], {
|
||||
inventory,
|
||||
path,
|
||||
pointer,
|
||||
modern: version[1] === "2",
|
||||
})
|
||||
: inventory.diagnostic(
|
||||
"invalid-path-key",
|
||||
pointer,
|
||||
"A Paths entry must start with /; it was not interpreted as an endpoint.",
|
||||
);
|
||||
if (!read.ok) return read;
|
||||
const context: ContractReportContext = {
|
||||
openapi: root.openapi,
|
||||
info: info.value,
|
||||
pathsState,
|
||||
examined: 0,
|
||||
};
|
||||
const interrupted = (error: AnalysisError) => {
|
||||
const retainedRows =
|
||||
inventory.operations.length +
|
||||
inventory.references.length +
|
||||
inventory.diagnostics.length;
|
||||
if (retainedRows > 0)
|
||||
error.retainPartialObservation(contractReport(inventory, context, true));
|
||||
return err(error);
|
||||
};
|
||||
try {
|
||||
const read = yield* collectPaths(
|
||||
paths ?? {},
|
||||
inventory,
|
||||
context,
|
||||
version[1] === "2",
|
||||
);
|
||||
if (!read.ok) return interrupted(read.error);
|
||||
} catch (cause: unknown) {
|
||||
if (cause instanceof AnalysisCancelledError) return interrupted(cause);
|
||||
throw cause;
|
||||
}
|
||||
return ok(
|
||||
completedInventory(inventory, {
|
||||
openapi: root.openapi,
|
||||
info: info.value,
|
||||
pathsState,
|
||||
examined,
|
||||
}),
|
||||
);
|
||||
return ok(contractReport(inventory, context));
|
||||
}
|
||||
@@ -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. */
|
||||
|
||||
@@ -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 }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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[];
|
||||
@@ -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,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. */
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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
@@ -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
@@ -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,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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
@@ -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 });
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,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 },
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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.`,
|
||||
},
|
||||
);
|
||||
/**
|
||||
|
||||
@@ -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 }),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Loaded 100 of 121 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user