Compare commits

..
Author SHA1 Message Date
Cursor Agent 6d9618b467 Merge branch 'cursor/rea-limit-inventory-01f4' into cursor/setting-effect-field-01f4 2026-11-11 02:28:56 +00:00
Cursor Agent 0e7a140007 merge: bring main into the limit inventory
Keep the file-level Ghidra bridge context assertion. Main removed the empty suite.
2026-11-11 02:28:55 +00:00
Cursor Agent 9935794a5a Merge branch 'cursor/rea-limit-inventory-01f4' into cursor/setting-effect-field-01f4 2026-11-11 02:26:28 +00:00
Cursor Agent 3ebd9292d0 test(mcp): give the catalog presentation child a 60s bound
A loaded shard killed the full-catalog child at its 20s limit.
2026-11-11 02:26:28 +00:00
Cursor Agent 58bd9a6f02 Merge branch 'cursor/rea-limit-inventory-01f4' into cursor/setting-effect-field-01f4 2026-11-11 01:04:39 +00:00
Cursor Agent 02d8fd878d test(ghidra): restore the bridge script read for the context contract
The main merge dropped the fs import that the file-level assertion uses.
2026-11-11 01:04:39 +00:00
Cursor Agent 7816142cd1 Merge branch 'cursor/rea-limit-inventory-01f4' into cursor/setting-effect-field-01f4 2026-11-11 01:04:04 +00:00
Cursor Agent b29909dbb8 merge: bring main into the limit inventory
Keep the Ghidra bridge context assertion on the file. Main's empty suite is the collection failure.
2026-11-11 01:03:59 +00:00
Cursor Agent fd9e91d11b merge: note why the Ghidra bridge context contract is file-level 2026-11-11 00:54:39 +00:00
Cursor Agent 6fedd53ebb test(ghidra): note why the bridge context contract is file-level 2026-11-11 00:54:17 +00:00
Cursor Agent 5d79a3c395 merge: keep the Ghidra bridge context contract as a file-level test 2026-11-11 00:18:01 +00:00
Cursor Agent 0e1389858e test(ghidra): keep the bridge context contract as a file-level test
A one-test suite named for the bridge script was collected with no tests in the full source run, which failed the file. The same assertion now sits directly on the file.
2026-11-11 00:18:01 +00:00
Cursor Agent 6d58a5fbd4 docs(settings): list parameter as an update_mode
The changelog and the limit migration now name runtime, restart, and parameter. parameter is the mode for a value this call supplied.
2026-11-10 23:44:47 +00:00
Cursor Agent 944e203f2f merge: read destructured environment keys in numeric-limit discovery 2026-11-10 23:44:18 +00:00
Cursor Agent d957d00db4 fix(limits): read destructured environment keys and keep the pre-install guard dependency-free
Object bindings, in-checks, and object keys that a spread can copy are producers. The architecture guard skips the TypeScript parse until typescript6 is installed, and the inventory links stay inside the published site.
2026-11-10 23:44:12 +00:00
Cursor Agent 70f91ac602 fix(settings): classify caller-selected limits as parameter updates
analysis_timeout_ms, max_heap_mb, and command_timeout_ms keep the source chosen by the caller, the runtime override, or the built-in default. A request value reports update_mode parameter because update_analysis_settings cannot change that call.
2026-11-10 23:24:09 +00:00
Cursor Agent f3dfe55eca merge: discover numeric limit reads from the syntax tree 2026-11-10 23:19:57 +00:00
Cursor Agent e603982ddd fix(limits): discover numeric limit reads from the syntax tree
Property access, element access, and helper-call arguments are producers. Comments and array literals are excluded by the tree, so a later argument such as read(environment, "REA_NEW_MAX_BYTES") stays classified.
2026-11-10 23:19:31 +00:00
Cursor Agent e4801e357a merge: keep bracket-access producers in numeric-limit discovery 2026-11-10 23:10:32 +00:00
Cursor Agent 8a7fbfdd21 fix(limits): keep bracket-access reads in numeric-limit discovery
A quoted name after an identifier, call, or optional chain is a producer. Only declaration arrays and comments stay out of discovery.
2026-11-10 23:10:30 +00:00
Cursor Agent bb598a23cf fix(settings): mark per-call parameters and keep historical receipts
artifact_limits and web-network budgets report update_mode parameter because update_analysis_settings cannot change them. The legacy-name scan no longer reads docs, and the 2026-11-09 Ghidra receipts are restored to the fields they recorded.
2026-11-10 23:05:50 +00:00
Cursor Agent 659fb1c7e6 merge: bring allowlist discovery and tracked-limit rules into the setting fields 2026-11-10 23:04:22 +00:00
Cursor Agent 42f42c1ffc fix(limits): ignore allowlists and require tracked limits to be settings
Discovery no longer treats comments or registration-list entries as producers. The three limits moving on their own PRs must join ANALYSIS_SETTINGS; the exception registry cannot classify them.
2026-11-10 23:04:06 +00:00
Cursor Agent 58b4b17fd3 fix(settings): keep Windows update mode on configuration errors
Invalid ChangeTime settings now carry update_mode and pending_restart through the configuration error and its projection, including startup parsing. Runtime recoveries name update_analysis_settings instead of telling the caller to restart REA.
2026-11-10 22:57:11 +00:00
Cursor Agent 3bc85783b9 merge: bring numeric-limit classification fixes into the setting fields 2026-11-10 22:54:28 +00:00
Cursor Agent 972d652483 fix(limits): classify endpoint candidates and ignore registry declarations
The numeric-limit check now treats REA_WEB_BUNDLE_ENDPOINT_LITERAL_CANDIDATES as a count limit and does not count registry text as a producer.
2026-11-10 22:53:54 +00:00
Cursor Agent f2b7c3bb92 feat(settings)!: replace effect with update_mode and pending_restart
Callers read update_mode to see whether update_analysis_settings can change a limit, and pending_restart only when a configured value is not active in this process.
2026-11-10 22:40:49 +00:00
Cursor Agent f4f927c94e feat(settings)!: report when a limit change takes effect
Part of #9881. Add effect immediate or next_process and keep rea_restart_required, restart_required, and restartRequired as compatibility aliases.
2026-11-10 22:16:39 +00:00
Cursor Agent 11839a2ef8 ci(limits): inventory numeric REA_ limits and check them
Part of #9881. Record every numeric REA_ limit and fail static checks when one is neither a runtime analysis setting nor a documented exception with a reason.
2026-11-10 22:12:59 +00:00
65 changed files with 1515 additions and 328 deletions

No files matched your search

+2
View File
@@ -1,5 +1,7 @@
# Changelog
The next release changes analysis-setting output (#9885). `update_analysis_settings`, `inspect_analysis_settings`, and limit errors no longer include `rea_restart_required`, `restart_required`, `restartRequired`, or `effect`. Read `update_mode` (`runtime`, `restart`, or `parameter`) and `pending_restart`. `runtime` means the setter can change the active value. `restart` means this process keeps the value it started with. `parameter` means this call supplied the value, so `update_analysis_settings` cannot change it; artifact reader limits, web-network capture budgets, and an explicit `analysis_timeout_ms`, `max_heap_mb`, or `command_timeout_ms` use it. There is no compatibility alias. Release Please should copy this migration into that release's breaking-changes section.
## [6.8.0](https://github.com/morluto/rea/compare/rea-agents-6.7.0...rea-agents-6.8.0) (2026-10-25)
### Highlights
+5
View File
@@ -358,6 +358,11 @@ export const documentationSections = [
source: "design/limits-and-recovery.md",
route: "/design/limits-and-recovery",
},
{
text: "REA limit inventory",
source: "design/rea-limit-inventory.md",
route: "/design/rea-limit-inventory",
},
{
text: "MCP tool design",
source: "development/tool-design.md",
+1
View File
@@ -111,6 +111,7 @@ 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)
+28 -7
View File
@@ -5,7 +5,9 @@ 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).
[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.
## Problem and design boundary
@@ -255,8 +257,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, restart requirement
and, when known, required bytes and value source. The agent can immediately call:
effective value, units, bounds, scope, application point, `update_mode`,
`pending_restart` and, when known, required bytes and value source. The agent can immediately call:
```json
{
@@ -273,13 +275,27 @@ 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"`, and `rea_restart_required: false`. Optional
`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
`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,
@@ -288,8 +304,10 @@ 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; `rea_restart_required: false` means no running REA process needs a
restart. A producer keeps guidance that holds only while its caller retains the
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
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
@@ -499,7 +517,10 @@ 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`. When `can_increase` is true, raise that field and retry the same
`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
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.
+229
View File
@@ -0,0 +1,229 @@
# 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 | 17 (14 have an `REA_` seed; 3 are per-call parameters only) |
| Per-call numeric `REA_` limits still read from the launch environment | 11 |
| 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 still outside the runtime system
These are checked when the work runs. They keep the `REA_` variable, default,
floor, and maximum, and the migration reads the accepted snapshot instead of a
value copied at process start. Until that lands they are `policy: "per-call"` in
the registry so the static check still requires a reason.
| Environment | Owner | Default | Floor | Maximum | Read today | Restart field |
| ------------------------------------------ | ------------------------------------------- | ----------------------: | ----: | ---------: | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| `REA_BRIDGE_MAX_REQUEST_BYTES` | `src/config/bridgeMaxRequestBytes.ts` | 16777216 | 7464 | 536870887 | each bridge request, from the launch value | prose |
| `REA_DEBUGGER_COMMAND_DEADLINE_MS` | `src/domain/analysisResourceLimits.ts` | 120000 | 1 | 2147483647 | copied when the debugger manager is constructed; applied to each command | `update_mode: "restart"` and `pending_restart: false` |
| `REA_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES` | `src/domain/analysisResourceLimits.ts` | 16777216 | 1 | 89303718 | copied when the debugger manager is constructed; applied to each command | `update_mode: "restart"` and `pending_restart: false` |
| `REA_HOPPER_CALLER_SCAN_BUDGET_MS` | `src/domain/analysisResourceLimits.ts` | 2000 | 1 | 2147483647 | each caller-scan operation, from config resolved at launch | prose |
| `REA_JEB_MCP_TIMEOUT_MS` | `src/jeb/JebMcpConnection.ts` | 60000 (MCP SDK default) | 1 | 2147483647 | captured when the JEB connection is created; applied to discovery and each tools/call | prose |
| `REA_KUNA_PROJECT_TIMEOUT_MS` | `src/kuna/KunaConfiguration.ts` | 1800000 | 1 | 2147483647 | project export and whole-program scans, from the launch configuration | prose |
| `REA_KUNA_TIMEOUT_MS` | `src/kuna/KunaConfiguration.ts` | 120000 | 1 | 2147483647 | each function decompile, from the launch configuration | prose |
| `REA_PWNTOOLS_MAX_INPUT_BYTES` | `src/native/pwntools/PwntoolsInputLimit.ts` | 33554432 | 1 | 1073741824 | each pwntools decode, unless the call passes `producer_limits.input_bytes` | prose |
| `REA_WINDOWS_CHANGE_TIME_DRIFT_MS` | `src/config/windowsChangeTimeSettle.ts` | 2000 | 1 | 60000 | each ChangeTime observation | `update_mode: "restart"` and `pending_restart: true` on an invalid launch value |
| `REA_WINDOWS_CHANGE_TIME_SETTLE_POLL_MS` | `src/config/windowsChangeTimeSettle.ts` | 25 | 1 | 1000 | each ChangeTime observation | `update_mode: "restart"` and `pending_restart: true` on an invalid launch value |
| `REA_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS` | `src/config/windowsChangeTimeSettle.ts` | 40 | 1 | 1000 | each ChangeTime observation | `update_mode: "restart"` and `pending_restart: true` on an invalid launch value |
## 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.
`policy: "per-call"` is temporary. The migration that moves the limit deletes
the entry and rejects any new `per-call` policy so the durable exceptions are
only `restart-only`.
+1 -1
View File
@@ -148,7 +148,7 @@
"build:termux:run": "npm run compile:termux && npm run artifacts:skills",
"format": "oxfmt .",
"format:check": "oxfmt --check .",
"lint": "node scripts/ci/static-check-report.mjs lint && npm run verify:module-boundaries",
"lint": "node scripts/ci/static-check-report.mjs lint && npm run verify:module-boundaries && node scripts/verify/rea-numeric-limits.mjs",
"lint:details": "oxlint && npm run verify:module-boundaries",
"lint:fix": "oxlint --fix && npm run verify:module-boundaries",
"knip": "knip --cache",
+21
View File
@@ -1,3 +1,5 @@
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";
@@ -193,4 +195,23 @@ if (failures.length > 0) {
console.error(`Architecture guards failed:\n\n${failures.join("\n\n")}\n`);
process.exit(1);
}
// CI runs this file before `npm ci`. The numeric-limit parser needs
// typescript6, so that check waits until dependencies exist. `npm run lint`
// runs it after install.
const typescript6 = new URL(
"../../node_modules/typescript6/package.json",
import.meta.url,
);
if (!existsSync(typescript6)) {
console.log(
"Numeric REA_ limit check waits until dependencies are installed.",
);
} else {
const numericLimits = spawnSync(
process.execPath,
[fileURLToPath(new URL("./rea-numeric-limits.mjs", import.meta.url))],
{ stdio: "inherit" },
);
if (numericLimits.status !== 0) process.exit(numericLimits.status ?? 1);
}
console.log(`Architecture guards passed across ${files.length} source files.`);
+23
View File
@@ -0,0 +1,23 @@
/** 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;
+399
View File
@@ -0,0 +1,399 @@
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);
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|per-call)"\s*,\s*read:\s*"(startup|per-call)"\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 (policy === "per-call" && read !== "per-call")
failures.push(`${name} is per-call policy but is not read per call`);
if (policy === "restart-only" && 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.values()].filter((entry) => entry.policy === "restart-only").length} restart-only, ${[...listed.values()].filter((entry) => entry.policy === "per-call").length} per-call pending migration, ${discovered.size} discovered.`,
);
}
+2 -1
View File
@@ -36,7 +36,8 @@ it("names REA_JADX_OPERATION_TIMEOUT_MS when the operation deadline elapses", ()
applies_at: "next_operation",
agent_editable: true,
scope: "rea-runtime",
rea_restart_required: false,
update_mode: "runtime",
pending_restart: false,
},
});
expect(projected).toMatchObject({
+3 -1
View File
@@ -134,7 +134,7 @@ export class AnalysisSettings {
};
}),
active_work_changed: false as const,
rea_restart_required: false as const,
pending_restart: false as const,
});
}
#describe(setting: AnalysisSettingId) {
@@ -156,6 +156,8 @@ 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,8 +5,6 @@ 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";
@@ -319,89 +317,3 @@ 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,8 +11,6 @@ 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 {
@@ -72,32 +70,8 @@ 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: OpenApiInput,
input: Extract<InspectApiContractInput, { format: "openapi-json" }>,
bytes: Buffer,
sha256: string,
signal?: AbortSignal,
@@ -112,16 +86,26 @@ const importDocument = async (
}),
signal,
);
if (!parsed.ok) {
const partial = apiContractSchema.safeParse(
parsed.error.partialObservation,
);
if (partial.success)
parsed.error.partialObservation = documentEvidence(input, partial.data);
return parsed;
}
if (!parsed.ok) return parsed;
const result = parsed.value;
return completeProviderEvidence(
documentEvidence(input, parsed.value),
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 }],
},
),
signal,
);
};
@@ -1,6 +1,7 @@
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,
@@ -181,18 +182,24 @@ 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:
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,
heapMb: heap.value,
heapSource: heap.source,
timeoutMs: timeout.value,
timeoutSource: timeout.source,
};
const preloads = await discoverJavaScriptPreloadPaths(
files,
+2
View File
@@ -15,6 +15,8 @@ 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,6 +240,7 @@ 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(
{
@@ -248,6 +249,7 @@ 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));
},
@@ -275,6 +277,7 @@ 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]);
});
+9 -6
View File
@@ -1,4 +1,5 @@
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 {
@@ -78,13 +79,15 @@ export class WasmArtifactService {
),
);
}
const timeout =
input.value.command_timeout_ms ??
this.settings?.().overrides["wasm.command_timeout_ms"] ??
DEFAULT_WASM_COMMAND_TIMEOUT_MS;
const timeout = selectedAnalysisSetting(
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,
commandTimeoutMs: timeout.value,
commandTimeoutSource: timeout.source,
});
if (!inspected.ok) return inspected;
const execution = inspected.value;
@@ -108,7 +111,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 ||
report.data.tool_profile.timeout_ms !== timeout.value ||
report.data.tool_profile.output_budget_bytes !==
wasmResourceLimits(input.value).outputBytes ||
!wasmArtifactMatchesSubject(report.data.artifact, execution.subject) ||
@@ -54,7 +54,8 @@ it("preserves adjustable reader admission facts and gives cleanup priority", ()
required_value: 2,
unit: "count",
applies_at: "next_operation",
rea_restart_required: false,
update_mode: "parameter",
pending_restart: false,
},
},
});
+2 -1
View File
@@ -40,7 +40,8 @@ export const artifactReaderLimitFailure = (
applies_at: "next_operation",
agent_editable: true,
can_increase: canIncrease,
rea_restart_required: false,
update_mode: "parameter" as const,
pending_restart: false as const,
},
});
};
@@ -89,7 +89,8 @@ it.each(["max_input_bytes", "max_reply_bytes", "max_nesting_depth"] as const)(
parameter: control,
effective_value: selected,
source: "request",
rea_restart_required: false,
update_mode: "parameter",
pending_restart: false,
},
},
});
+3 -3
View File
@@ -103,11 +103,10 @@ 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).toMatch(/restart REA/iu);
expect(projected.remediation.action).toContain("launch seed");
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,
);
@@ -125,7 +124,8 @@ it("uses a valid REA_CAPTURE_DECODER_OLD_GENERATION_MIB override", async () => {
unit: "MiB",
applies_at: "next_worker",
agent_editable: true,
rea_restart_required: false,
update_mode: "runtime",
pending_restart: false,
},
reported_limits: {
old_generation_heap_mib: 256,
+1 -1
View File
@@ -71,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).`,
`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.`,
`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)}.`,
`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(" ");
+2 -1
View File
@@ -171,7 +171,8 @@ it("names the session setting on a Rizin capacity refusal", async () => {
minimum: MINIMUM_DEBUGGER_MAX_SESSIONS,
maximum: MAXIMUM_DEBUGGER_MAX_SESSIONS,
unit: "sessions",
restart_required: true,
update_mode: "restart",
pending_restart: false,
max_sessions: 1,
active_or_reserved_sessions: 1,
},
+33 -5
View File
@@ -6,7 +6,10 @@ import { z } from "zod";
import { mcpInputBudgetSchema } from "./mcpInputBudget.js";
import { isAbsolute } from "node:path";
import { ConfigurationError } from "../domain/configurationErrors.js";
import {
ConfigurationError,
type ConfigurationSettingIssue,
} from "../domain/configurationErrors.js";
import { err, ok, type Result } from "../domain/result.js";
import { analysisProviderSelectorSchema } from "../domain/providerSelection.js";
import {
@@ -165,6 +168,31 @@ 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,
@@ -229,10 +257,10 @@ export const parseEnvironmentValues = (
const parsed = environmentSchema.safeParse(literals);
if (!parsed.success) {
// Zod messages name the constraint, never the rejected value.
const settings = parsed.error.issues.map((issue) => ({
setting: String(issue.path[0] ?? "environment"),
constraint: issue.message,
}));
// A producer may attach update_mode and pending_restart on a custom issue.
const settings = parsed.error.issues.map((issue) =>
configurationSettingFromIssue(issue),
);
return err(
new ConfigurationError("Invalid REA environment configuration", {
cause: parsed.error,
+3 -3
View File
@@ -49,7 +49,8 @@ it("keeps the 5000 ms deadline when the setting is unset", () => {
applies_at: "next_operation",
agent_editable: true,
scope: "rea-runtime",
rea_restart_required: false,
update_mode: "runtime",
pending_restart: false,
},
reported_limits: {
matching_deadline_ms: DEFAULT_REGEX_MATCHING_DEADLINE.deadlineMs,
@@ -65,8 +66,7 @@ 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).toMatch(/restart REA/i);
expect(projected.remediation.action).toMatch(/without a REA restart/i);
expect(projected.remediation.action).toContain("launch seed");
expect(projected.remediation.action).toMatch(
/heap does not raise this deadline/i,
);
+1 -1
View File
@@ -70,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).`,
`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.`,
`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)}.`,
"The worker old-generation heap does not raise this deadline.",
].join(" ");
+3 -3
View File
@@ -56,7 +56,8 @@ it("keeps the 128 MiB request when the setting is unset", () => {
applies_at: "next_worker",
agent_editable: true,
scope: "rea-runtime",
rea_restart_required: false,
update_mode: "runtime",
pending_restart: false,
},
reported_limits: {
requested_worker_heap_mib: DEFAULT_REGEX_WORKER_HEAP_REQUEST.heapMiB,
@@ -76,8 +77,7 @@ 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).toMatch(/restart REA/i);
expect(projected.remediation.action).toMatch(/without a REA restart/i);
expect(projected.remediation.action).toContain("launch seed");
expect(projected.remediation.action).toMatch(/overrides this request/i);
});
+1 -1
View File
@@ -72,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).`,
`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.`,
`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)}.`,
"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(" ");
+12 -8
View File
@@ -27,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("restart REA");
expect(message).toContain("positive decimal integer");
};
it("keeps today's settle defaults when the settings are unset", () => {
@@ -98,17 +98,21 @@ 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" });
expect(resolved.error.settings).toEqual([
{ setting: bound.setting, constraint: message },
]);
const facts = {
setting: bound.setting,
constraint: message,
update_mode: "restart" as const,
pending_restart: true as const,
};
expect(resolved.error.settings).toEqual([facts]);
expect(projectAnalysisError(resolved.error)).toMatchObject({
code: "configuration_invalid",
details: { settings: [{ setting: bound.setting, constraint: message }] },
details: { settings: [facts] },
});
const startup = parseEnvironment({ [bound.setting]: value });
if (startup.ok) throw new Error(`Startup accepted ${bound.setting}=${value}`);
expect(startup.error.settings).toContainEqual({
setting: bound.setting,
constraint: message,
expect(startup.error.settings).toContainEqual(facts);
expect(projectAnalysisError(startup.error).details).toMatchObject({
settings: [facts],
});
});
+14 -6
View File
@@ -55,7 +55,9 @@ export interface WindowsChangeTimeSettingFailure {
readonly floor: number;
readonly maximum: number;
readonly unit: "ms" | "polls";
readonly restartRequired: true;
readonly update_mode: "restart";
/** The rejected environment value is not the value this process is using. */
readonly pending_restart: true;
}
/** Effective settle budget and whether each part came from the default or a setting. */
@@ -79,10 +81,11 @@ const windowsChangeTimeSettingFailure = (
floor: bound.floor,
maximum: bound.maximum,
unit: bound.unit,
restartRequired: true,
update_mode: "restart",
pending_restart: true,
});
/** Name the rejected value, its source, the allowed range, and the required restart. */
/** Name the rejected value, its source, and the allowed range. */
const windowsChangeTimeSettingMessage = (
failure: WindowsChangeTimeSettingFailure,
): string =>
@@ -100,11 +103,14 @@ 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(
windowsChangeTimeSettingFailure(bound, value),
),
message: windowsChangeTimeSettingMessage(failure),
params: {
update_mode: failure.update_mode,
pending_restart: failure.pending_restart,
},
});
})
.transform(Number);
@@ -160,6 +166,8 @@ export const resolveWindowsChangeTimeSettle = (
settings: failures.map((failure) => ({
setting: failure.setting,
constraint: windowsChangeTimeSettingMessage(failure),
update_mode: failure.update_mode,
pending_restart: failure.pending_restart,
})),
},
),
+3 -1
View File
@@ -48,6 +48,8 @@ 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(),
@@ -83,7 +85,7 @@ export const updateAnalysisSettingsResultSchema =
}),
),
active_work_changed: z.literal(false),
rea_restart_required: z.literal(false),
pending_restart: z.literal(false),
});
export const ANALYSIS_SETTINGS_TOOL_CONTRACTS = [
{
-2
View File
@@ -1,7 +1,6 @@
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";
@@ -499,7 +498,6 @@ export type AnalysisPartialObservation =
}
| { readonly kind: "unretained-evidence"; readonly evidence: Evidence }
| { readonly kind: "retained-evidence"; readonly evidence_id: string }
| ApiContract
| GraphqlContract
| PlaywrightTrace
| SavedAnalysisSnapshotPartialObservation
+16 -1
View File
@@ -1512,7 +1512,8 @@ describe("analysis error projection: caller lifetime", () => {
minimum: limit.minimum,
maximum: limit.maximum,
agent_editable: true,
rea_restart_required: false,
update_mode: "runtime",
pending_restart: false,
applies_at: "next_invocation",
scope: "rea-invocation",
environment: "REA_REGEX_MATCHING_DEADLINE_MS",
@@ -1529,11 +1530,25 @@ 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");
+9 -3
View File
@@ -1044,9 +1044,15 @@ const lifecycleErrorDetails = (
}
if (error instanceof ConfigurationError && error.settings.length > 0)
return {
settings: error.settings.map(({ setting, constraint }) => ({
setting,
constraint,
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 }),
})),
};
if (error instanceof BinaryTargetError)
+37 -1
View File
@@ -46,6 +46,7 @@ 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[];
@@ -60,6 +61,7 @@ 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,
@@ -77,6 +79,7 @@ export const ANALYSIS_SETTINGS = {
],
},
"asar.max_listing_bytes": {
updateMode: "runtime",
environment: "REA_ASAR_MAX_LISTING_BYTES",
defaultValue: DEFAULT_ASAR_MAX_LISTING_BYTES,
minimum: 1,
@@ -100,6 +103,7 @@ 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,
@@ -114,6 +118,7 @@ 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,
@@ -128,6 +133,7 @@ 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,
@@ -142,6 +148,7 @@ export const ANALYSIS_SETTINGS = {
],
},
"asar.max_header_bytes": {
updateMode: "runtime",
environment: "REA_ASAR_MAX_HEADER_BYTES",
defaultValue: DEFAULT_ASAR_MAX_HEADER_BYTES,
minimum: 1,
@@ -165,6 +172,7 @@ export const ANALYSIS_SETTINGS = {
],
},
"ghidra.startup_timeout_ms": {
updateMode: "runtime",
...timer,
environment: "REA_GHIDRA_STARTUP_TIMEOUT_MS",
defaultValue: DEFAULT_GHIDRA_STARTUP_TIMEOUT_MS,
@@ -185,6 +193,7 @@ export const ANALYSIS_SETTINGS = {
],
},
"hopper.startup_timeout_ms": {
updateMode: "runtime",
...timer,
environment: "REA_HOPPER_STARTUP_TIMEOUT_MS",
defaultValue: DEFAULT_HOPPER_STARTUP_TIMEOUT_MS,
@@ -205,6 +214,7 @@ export const ANALYSIS_SETTINGS = {
],
},
"ghidra.decompile_timeout_seconds": {
updateMode: "runtime",
environment: "REA_GHIDRA_DECOMPILE_TIMEOUT_SECONDS",
defaultValue: null,
minimum: 1,
@@ -220,6 +230,7 @@ export const ANALYSIS_SETTINGS = {
],
},
"jadx.operation_timeout_ms": {
updateMode: "runtime",
...timer,
environment: "REA_JADX_OPERATION_TIMEOUT_MS",
defaultValue: DEFAULT_JADX_OPERATION_TIMEOUT_MS,
@@ -233,6 +244,7 @@ export const ANALYSIS_SETTINGS = {
],
},
"regex.matching_deadline_ms": {
updateMode: "runtime",
...timer,
environment: "REA_REGEX_MATCHING_DEADLINE_MS",
defaultValue: DEFAULT_REGEX_MATCHING_DEADLINE_MS,
@@ -242,6 +254,7 @@ 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,
@@ -253,6 +266,7 @@ 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,
@@ -263,6 +277,7 @@ 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",
@@ -270,6 +285,7 @@ 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,
@@ -280,6 +296,7 @@ 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,
@@ -288,6 +305,7 @@ 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,
@@ -317,6 +335,23 @@ 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,
@@ -334,7 +369,8 @@ export const analysisSettingLimit = (
applies_at: definition.appliesAt,
agent_editable: true,
scope: "rea-runtime",
rea_restart_required: false,
update_mode: source === "request" ? "parameter" : definition.updateMode,
pending_restart: false,
...(definition.parameter === undefined
? {}
: { parameter: definition.parameter }),
+14 -1
View File
@@ -26,6 +26,14 @@ 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;
@@ -37,7 +45,12 @@ export interface AnalysisSettingLimit {
readonly applies_at: "next_operation" | "next_worker" | "next_startup";
readonly agent_editable: true;
readonly scope: "rea-runtime";
readonly rea_restart_required: false;
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 required_value?: number;
readonly parameter?: string;
}
+1 -1
View File
@@ -185,7 +185,7 @@ export const projectApiContractView = (
unknowns:
context.source_coverage.status === "partial"
? [
"This inventory is partial. Inspect limitations for coverage and query path-item-references and diagnostics for any retained source pointers.",
"Unresolved or malformed declarations limit this inventory; query path-item-references and diagnostics for exact source pointers.",
]
: [],
};
@@ -3,7 +3,6 @@ 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",
@@ -306,39 +305,9 @@ 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({
"/retained": { get: {} },
[`/${"a".repeat(1_100_000)}`]: { get: {} },
}),
document({ [`/${"a".repeat(1_100_000)}`]: { get: {} } }),
);
expect(result.ok).toBe(false);
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");
if (!result.ok)
expect(result.error._tag).toBe("AnalysisResourceConstraintError");
});
+39 -72
View File
@@ -1,5 +1,4 @@
import {
AnalysisCancelledError,
AnalysisInputError,
AnalysisResourceConstraintError,
AnalysisUnsupportedTargetError,
@@ -317,17 +316,14 @@ function* pathItem(
return ok(undefined);
}
interface ContractReportContext {
readonly openapi: string;
readonly info: ApiContract["info"];
readonly pathsState: ApiContract["source_coverage"]["paths_state"];
examined: number;
}
const contractReport = (
const completedInventory = (
inventory: ContractInventory,
context: ContractReportContext,
incomplete = false,
context: {
openapi: string;
info: ApiContract["info"];
pathsState: ApiContract["source_coverage"]["paths_state"];
examined: number;
},
): ApiContract => {
const artifact = inventory.artifact;
let declared = 0;
@@ -352,9 +348,7 @@ const contractReport = (
source_coverage: {
scope: "inline-path-operations",
status:
incomplete ||
inventory.references.length > 0 ||
inventory.diagnostics.length > 0
inventory.references.length > 0 || inventory.diagnostics.length > 0
? "partial"
: "complete-within-scope",
paths_state: context.pathsState,
@@ -364,11 +358,6 @@ const contractReport = (
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.",
@@ -379,32 +368,6 @@ const contractReport = (
};
};
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,
@@ -454,32 +417,36 @@ export function* parseApiContractSteps(
);
if (!diagnosed.ok) return diagnosed;
}
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;
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;
}
return ok(contractReport(inventory, context));
return ok(
completedInventory(inventory, {
openapi: root.openapi,
info: info.value,
pathsState,
examined,
}),
);
}
+2 -1
View File
@@ -191,7 +191,8 @@ export interface ArtifactReaderLimitFact {
readonly applies_at: "next_operation";
readonly agent_editable: true;
readonly can_increase: boolean;
readonly rea_restart_required: false;
readonly update_mode: "parameter";
readonly pending_restart: false;
}
/** Runtime representation ceilings differ from aggregate plaintext-work budgets. */
+14 -3
View File
@@ -7,6 +7,10 @@ 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. */
@@ -24,9 +28,16 @@ export class ConfigurationError extends AnalysisError {
constructor(message: string, options?: ConfigurationErrorOptions) {
super(message, options);
this.remediationAction = options?.remediationAction;
this.settings = (options?.settings ?? []).map(
({ setting, constraint }) => ({ setting, constraint }),
);
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 }),
}));
}
}
@@ -41,7 +41,21 @@ 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. */
+37
View File
@@ -0,0 +1,37 @@
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.read).toBe(
entry.policy === "per-call" ? "per-call" : "startup",
);
expect(runtimeEnvironments.has(entry.name)).toBe(false);
expect(inventory).toContain(`\`${entry.name}\``);
}
expect(
NUMERIC_LIMIT_REGISTRY.filter((entry) => entry.policy === "restart-only"),
).toHaveLength(15);
expect(
NUMERIC_LIMIT_REGISTRY.filter((entry) => entry.policy === "per-call"),
).toHaveLength(11);
});
+230
View File
@@ -0,0 +1,230 @@
/**
* 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`. `restart-only` is the durable exception. `per-call`
* names a limit still read from the launch environment on each use; the
* runtime migration deletes that entry instead of leaving it here.
*
* 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" | "per-call";
readonly read: "startup" | "per-call";
readonly reason: string;
}
export const NUMERIC_LIMIT_REGISTRY = [
{
name: "REA_BRIDGE_MAX_REQUEST_BYTES",
update_mode: "restart",
policy: "per-call",
read: "per-call",
reason:
"The Hopper, Ghidra, and Cutter clients measure each request against this ceiling. The value is still taken from the launch environment, so a running process does not see a new value until the runtime migration removes this entry.",
},
{
name: "REA_DEBUGGER_COMMAND_DEADLINE_MS",
update_mode: "restart",
policy: "per-call",
read: "per-call",
reason:
"GDB and Rizin apply this deadline to each debugger command. The session manager currently copies it at construction from the launch environment; the runtime migration reads the accepted operation snapshot instead.",
},
{
name: "REA_DEBUGGER_MAX_COMMAND_OUTPUT_BYTES",
update_mode: "restart",
policy: "per-call",
read: "per-call",
reason:
"Each debugger command retains output up to this ceiling. The session manager currently copies it at construction from the launch environment; the runtime migration reads the accepted operation snapshot instead.",
},
{
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_CALLER_SCAN_BUDGET_MS",
update_mode: "restart",
policy: "per-call",
read: "per-call",
reason:
"Forward caller reconciliation reads this budget for each scan. It is still resolved from the launch environment; the runtime migration reads the accepted operation snapshot.",
},
{
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_JEB_MCP_TIMEOUT_MS",
update_mode: "restart",
policy: "per-call",
read: "per-call",
reason:
"Bounds each JEB tools/call and catalog discovery. The connection currently captures the launch value when it is created; the runtime migration reads the accepted snapshot for each request.",
},
{
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_KUNA_PROJECT_TIMEOUT_MS",
update_mode: "restart",
policy: "per-call",
read: "per-call",
reason:
"Bounds a Kuna project export and the whole-program scans that use that deadline. The configuration object still comes from the launch environment; the runtime migration reads the accepted snapshot at each use.",
},
{
name: "REA_KUNA_TIMEOUT_MS",
update_mode: "restart",
policy: "per-call",
read: "per-call",
reason:
"Bounds one Kuna function decompile. The configuration object still comes from the launch environment; the runtime migration reads the accepted snapshot at each use.",
},
{
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_PWNTOOLS_MAX_INPUT_BYTES",
update_mode: "restart",
policy: "per-call",
read: "per-call",
reason:
"Default artifact-size admission for a pwntools decode. A per-call producer_limits.input_bytes override already exists; the launch default is still read from the environment and the runtime migration moves that default into the snapshot.",
},
{
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.",
},
{
name: "REA_WINDOWS_CHANGE_TIME_DRIFT_MS",
update_mode: "restart",
policy: "per-call",
read: "per-call",
reason:
"Each Windows ChangeTime observation compares the file against this drift window. The observation still parses the launch environment; the runtime migration reads the accepted operation snapshot.",
},
{
name: "REA_WINDOWS_CHANGE_TIME_SETTLE_MAX_POLLS",
update_mode: "restart",
policy: "per-call",
read: "per-call",
reason:
"Each Windows ChangeTime settle loop stops after this many polls. The observation still parses the launch environment; the runtime migration reads the accepted operation snapshot.",
},
{
name: "REA_WINDOWS_CHANGE_TIME_SETTLE_POLL_MS",
update_mode: "restart",
policy: "per-call",
read: "per-call",
reason:
"Each Windows ChangeTime settle loop waits this long between reads. The observation still parses the launch environment; the runtime migration reads the accepted operation snapshot.",
},
] as const satisfies readonly NumericLimitRegistryEntry[];
+2 -1
View File
@@ -79,7 +79,8 @@ export const webNetworkCaptureBudgetFacts = (
? MAX_WEB_NETWORK_CAPTURE_NESTING_DEPTH
: Number.MAX_SAFE_INTEGER,
applies_at: "next_operation",
rea_restart_required: false,
update_mode: "parameter" as const,
pending_restart: false as const,
};
};
+20
View File
@@ -1,3 +1,4 @@
import { readFile } from "node:fs/promises";
import { describe, expect, it } from "vitest";
import type { JsonValue } from "../domain/jsonValue.js";
@@ -1119,6 +1120,25 @@ 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",
+6 -3
View File
@@ -1868,7 +1868,8 @@ describe("Ghidra decompiler failure projection", () => {
applies_at: "next_operation",
agent_editable: true,
scope: "rea-runtime",
rea_restart_required: false,
update_mode: "runtime",
pending_restart: false,
},
},
});
@@ -1983,7 +1984,8 @@ describe("Ghidra decompile deadline for a finished invocation", () => {
minimum: 1,
maximum: 2_147_483,
agent_editable: true,
rea_restart_required: false,
update_mode: "runtime",
pending_restart: false,
};
expect(invocation.details?.limit).toEqual({
...enforced,
@@ -4365,7 +4367,8 @@ describe("Ghidra startup deadline for a finished invocation", () => {
setting: "ghidra.startup_timeout_ms",
effective_value: 250,
agent_editable: true,
rea_restart_required: false,
update_mode: "runtime",
pending_restart: false,
};
const invocationLimit = {
...startupLimit,
+2 -1
View File
@@ -187,7 +187,8 @@ it.each(["initialize", "transport"])(
applies_at: "next_operation",
agent_editable: true,
scope: "rea-runtime",
rea_restart_required: false,
update_mode: "runtime",
pending_restart: false,
},
diagnostics: { producer_fact: "retained" },
});
@@ -948,7 +948,7 @@ export class JavaScriptAnalysisWorker {
limit: analysisSettingLimit(
"javascript.max_heap_mb",
this.limits.heapMb,
undefined,
this.limits.heapSource,
),
remediationAction: javaScriptWorkerHeapRemediation(this.limits.heapMb),
},
@@ -1147,7 +1147,7 @@ export class JavaScriptAnalysisWorker {
limit: analysisSettingLimit(
"javascript.analysis_timeout_ms",
this.limits.timeoutMs,
undefined,
this.limits.timeoutSource,
),
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,
undefined,
this.limits.heapSource,
),
}
: {}),
+4 -2
View File
@@ -22,7 +22,8 @@ export const debuggerAdmissionDiagnostics = (
readonly minimum: number;
readonly maximum: number;
readonly unit: DebuggerAdmissionLimit["unit"];
readonly restart_required: true;
readonly update_mode: "restart";
readonly pending_restart: false;
} => ({
setting: admission.setting,
effective_value: admission.value,
@@ -31,7 +32,8 @@ export const debuggerAdmissionDiagnostics = (
minimum: admission.minimum,
maximum: admission.maximum,
unit: admission.unit,
restart_required: true,
update_mode: "restart",
pending_restart: false,
});
/** Name the setting, the effective value, its source, the range, and a restart. */
+9 -1
View File
@@ -1066,7 +1066,15 @@ 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 },
details: {
timeout_ms: 20,
limit: {
setting: "wasm.command_timeout_ms",
effective_value: 20,
source: "request",
update_mode: "parameter",
},
},
remediation: { action: expect.stringContaining("command_timeout_ms") },
});
expect(timed.error.cleanupIncomplete).toBe(false);
+7
View File
@@ -229,6 +229,10 @@ 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, {
@@ -695,6 +699,9 @@ export class WabtArtifactProvider implements WasmArtifactPort {
phase === "artifact" ? artifactInput.path : input.path,
{
commandTimeoutMs,
...(commandTimeoutSource === undefined
? {}
: { commandTimeoutSource }),
limits,
inputField: artifactInput.field,
observedOutput: capturedOutput,
+22 -2
View File
@@ -52,7 +52,8 @@ it.each(["configuration", "producer"] as const)(
limit: {
setting: "wasm.command_timeout_ms",
effective_value: 1234,
rea_restart_required: false,
update_mode: "runtime",
pending_restart: false,
},
},
recovery: { kind: "inspect_timeout_state", timeout_ms: 1234 },
@@ -115,11 +116,30 @@ it.each([1234, 2_147_483_647])(
parameter: "command_timeout_ms",
applies_at: "next_operation",
agent_editable: true,
rea_restart_required: false,
update_mode: "runtime",
pending_restart: 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,
},
},
});
},
);
+11 -1
View File
@@ -63,6 +63,12 @@ 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)[];
@@ -182,13 +188,17 @@ 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,
undefined,
timeoutSource,
),
...options,
// The cleanup wrapper retains output once; its nested primary needs only recovery facts.
+1 -1
View File
@@ -148,7 +148,7 @@ it("advertises exact contracts and recovers a full large-index archive over one
expect(updated.structuredContent).toMatchObject({
revision: 1,
active_work_changed: false,
rea_restart_required: false,
pending_restart: false,
});
const recovered = await invoke("inspect_artifact", {});
expect(recovered.isError).not.toBe(true);
@@ -186,7 +186,8 @@ for (const reader of readers)
effective_value: reader.value,
applies_at: "next_operation",
agent_editable: true,
rea_restart_required: false,
update_mode: "parameter",
pending_restart: false,
},
},
});
@@ -87,7 +87,8 @@ it.skipIf(process.versions.bun !== undefined && jobLimit === null)(
setting: "capture.decoder_old_generation_mib",
effective_value: testHeap.oldGenerationMiB,
source: testHeap.source,
rea_restart_required: false,
update_mode: "runtime",
pending_restart: false,
},
reported_limits:
jobLimit === null
@@ -147,7 +148,8 @@ it.skipIf(process.versions.bun !== undefined && jobLimit === null)(
setting: "capture.decoder_old_generation_mib",
effective_value: testHeap.oldGenerationMiB,
source: testHeap.source,
rea_restart_required: false,
update_mode: "runtime",
pending_restart: false,
},
},
remediation: {
@@ -216,7 +216,8 @@ it("names the default header admission budget and its setting when source select
applies_at: "next_operation",
agent_editable: true,
scope: "rea-runtime",
rea_restart_required: false,
update_mode: "runtime",
pending_restart: false,
},
},
});
@@ -66,7 +66,8 @@ it.each([
required_value: needed,
applies_at: "next_operation",
agent_editable: true,
rea_restart_required: false,
update_mode: "parameter",
pending_restart: false,
},
});
const recovered = await readDotnetBundle(
@@ -36,7 +36,9 @@ it("preserves full advertised schemas and handler meaning when presentation orde
REA_MCP_INPUT_SCHEMA_PROFILE: "full",
REA_MCP_OUTPUT_SCHEMA_PROFILE: "full",
},
timeout: 20_000,
// The child advertises the full catalog and calls every browser, Electron,
// and JavaScript example. A busy runner exceeded 20s and was killed.
timeout: 60_000,
maxBuffer: 16 * 1024 * 1024,
});
const result = resultSchema.parse(JSON.parse(stdout));
@@ -72,7 +72,8 @@ const enforcedLimit = {
minimum: 1,
maximum: 2_147_483,
agent_editable: true,
rea_restart_required: false,
update_mode: "runtime",
pending_restart: false,
};
/** A finished invocation can act only through a new invocation's launch value. */
const invocationLimit = {
@@ -250,13 +250,15 @@ it("returns a directly usable partial analysis reference for an oversized timeou
limit: {
setting: "javascript.analysis_timeout_ms",
effective_value: 1,
source: "request",
parameter: "analysis_timeout_ms",
unit: "ms",
minimum: 1,
maximum: 2_147_483_647,
applies_at: "next_operation",
agent_editable: true,
rea_restart_required: false,
update_mode: "parameter",
pending_restart: false,
},
});
expect(error.recovery).toEqual({
+2 -1
View File
@@ -472,7 +472,8 @@ it("stops a slow match at the configured deadline and names the setting", async
effective_value: 100,
source: "setting",
agent_editable: true,
rea_restart_required: false,
update_mode: "runtime",
pending_restart: false,
});
expect(projected.remediation.action).toContain(
regexMatchingDeadlineRecovery({ deadlineMs: 100, source: "setting" }),
@@ -1922,7 +1922,8 @@ it("preserves a timeout while startup waits for the automatic-loading response",
effective_value: 25,
source: "setting",
unit: "ms",
restart_required: true,
update_mode: "restart",
pending_restart: false,
},
},
});
+145
View File
@@ -0,0 +1,145 @@
import { expect, it } from "vitest";
import {
NUMERIC_LIMIT_DECLARATION,
discoverNumericLimitUses,
isNumericLimit,
requiredRuntimeLimitFailure,
} from "../../scripts/verify/rea-numeric-limits.mjs";
const ENDPOINT_CANDIDATES = "REA_WEB_BUNDLE_ENDPOINT_LITERAL_CANDIDATES";
it("recognizes the documented endpoint-candidate limit as numeric", () => {
expect(isNumericLimit(ENDPOINT_CANDIDATES)).toBe(true);
const discovered = discoverNumericLimitUses([
{
path: "src/domain/webBundleAnalysis.ts",
source: `const candidates = process.env.${ENDPOINT_CANDIDATES};`,
},
]);
expect(discovered.get(ENDPOINT_CANDIDATES)).toBe(
"src/domain/webBundleAnalysis.ts",
);
});
it("does not treat a registry declaration as a producer", () => {
const name = "REA_REMOVED_MAX_BYTES";
const discovered = discoverNumericLimitUses([
{
path: NUMERIC_LIMIT_DECLARATION,
source: `name: "${name}",`,
},
{
path: "src/config/removedLimit.ts",
source: `const value = process.env.${name};`,
},
]);
expect(discovered.get(name)).toBe("src/config/removedLimit.ts");
expect(
discoverNumericLimitUses([
{ path: NUMERIC_LIMIT_DECLARATION, source: `name: "${name}",` },
]).has(name),
).toBe(false);
});
it("keeps a helper-call argument that is not the first argument", () => {
const name = "REA_NEW_MAX_BYTES";
const discovered = discoverNumericLimitUses([
{
path: "src/config/newLimit.ts",
source: `const setting = "${name}";\nconst value = read(environment, setting);`,
},
{
path: "src/config/literalLimit.ts",
source: `const value = read(environment, "${name}");`,
},
]);
expect(discovered.get(name)).toBe("src/config/newLimit.ts");
});
it("does not treat prose that mentions a limit name as a producer", () => {
const name = "REA_NEW_MAX_BYTES";
const discovered = discoverNumericLimitUses([
{
path: "src/domain/native/nativeDeadlines.ts",
source: `const guidance = "input_bytes overrides ${name}; otherwise that launch default applies.";`,
},
]);
expect(discovered.has(name)).toBe(false);
});
it("keeps a destructured environment key, including a rename and a default", () => {
const name = "REA_NEW_MAX_BYTES";
const discovered = discoverNumericLimitUses([
{
path: "src/config/newLimit.ts",
source: `const { ${name}: renamed = 1 } = process.env;\nconst present = "${name}" in environment;\nconst owned = Object.hasOwn(environment, "${name}");\nconst source = { ${name}: renamed };\nconst copy = { ...source };`,
},
]);
expect(discovered.get(name)).toBe("src/config/newLimit.ts");
});
it("does not invent a limit from a spread of the environment object", () => {
const discovered = discoverNumericLimitUses([
{
path: "src/config/newLimit.ts",
source: "const copy = { ...process.env, ...environment };",
},
]);
expect(discovered.size).toBe(0);
});
it("keeps a bracket-access read as a producer", () => {
const name = "REA_NEW_MAX_BYTES";
const discovered = discoverNumericLimitUses([
{
path: "src/config/newLimit.ts",
source: `const selected = environment["${name}"] ?? environment?.[ "${name}" ];`,
},
]);
expect(discovered.get(name)).toBe("src/config/newLimit.ts");
});
it("does not treat a registration allowlist or a comment as a producer", () => {
const discovered = discoverNumericLimitUses([
{
path: NUMERIC_LIMIT_DECLARATION,
source: `name: "${ENDPOINT_CANDIDATES}",`,
},
{
path: "src/application/SetupRegistrationEnvironment.ts",
source: `const SELECTED_REGISTRATION_SETTINGS = ["${ENDPOINT_CANDIDATES}"] as const;`,
},
{
path: "src/config/removedLimit.ts",
source: `// ${ENDPOINT_CANDIDATES} used to be read here\nconst unrelated = 1;\n`,
},
]);
expect(discovered.has(ENDPOINT_CANDIDATES)).toBe(false);
});
it("requires the tracked endpoint-candidate limit to be an analysis setting", () => {
expect(
requiredRuntimeLimitFailure(ENDPOINT_CANDIDATES, {
inRuntime: false,
inRegistry: true,
}),
).toMatch(/ANALYSIS_SETTINGS/);
expect(
requiredRuntimeLimitFailure(ENDPOINT_CANDIDATES, {
inRuntime: true,
inRegistry: false,
}),
).toBeUndefined();
expect(
requiredRuntimeLimitFailure("REA_NTFS_MFT_MAX_FILE_BYTES", {
inRuntime: false,
inRegistry: false,
}),
).toMatch(/ANALYSIS_SETTINGS/);
expect(
requiredRuntimeLimitFailure("REA_REGF_HIVE_MAX_FILE_BYTES", {
inRuntime: true,
inRegistry: true,
}),
).toMatch(/numericLimitRegistry/);
});