Compare commits

..
Author SHA1 Message Date
Cursor Agent 95c8c9d157 feat(environment): render more read tools from the state and describe responses
Keep main's environment serving and fold in coverage for non-entity reads,
response summaries, and MCP tool annotations.

Squashed rebase of #5352 onto main.
2026-10-21 21:27:14 +00:00
525 changed files with 5373 additions and 26749 deletions

No files matched your search

@@ -22,11 +22,6 @@ requires complete application analysis; `source_paths: []` is invalid, not an
unfiltered request. `detail: "summary"` reduces delivery unfiltered request. `detail: "summary"` reduces delivery
and does not by itself reduce analysis work. and does not by itself reduce analysis work.
ASAR header/index admission is fixed at 16 MiB / 16777216 bytes for this
release and has no runtime override. `source_paths`, `detail`, and
`max_heap_mb` do not bypass or raise it. Analyze an extracted directory, or
supply an archive whose header fits.
Exact `#alias` specifiers resolve through the importer's Exact `#alias` specifiers resolve through the importer's
enclosing package `imports` map to package-local files; nested `node_modules` enclosing package `imports` map to package-local files; nested `node_modules`
packages use their own map. Wildcard, recursive, and external-package import packages use their own map. Wildcard, recursive, and external-package import
+1 -3
View File
@@ -431,9 +431,7 @@ naming the entity, collection or document path, or `{"source": "recording",
"reason": …}` with the view's reason, `no-view` or `unrecorded-request`. A "reason": …}` with the view's reason, `no-view` or `unrecorded-request`. A
state answer carries the base record's status, content type and `replay` state answer carries the base record's status, content type and `replay`
provenance with `truncated: false`; one whose JSON would exceed 256 KiB is provenance with `truncated: false`; one whose JSON would exceed 256 KiB is
refused as a `resource_constraint` tool error rather than cut. Extraction does refused as a `resource_constraint` tool error rather than cut. Serving is
not state-back a document whose rendered JSON exceeds 256 KiB; that read stays
recorded (`rendered-oversize`) and replays the captured body. Serving is
read-only: `POST`, `PUT`, `PATCH` and `DELETE` tools, other than query `POST`s, read-only: `POST`, `PUT`, `PATCH` and `DELETE` tools, other than query `POST`s,
are not served, so nothing a call does changes the state. Query `POST`s stay are not served, so nothing a call does changes the state. Query `POST`s stay
served and answer from their recordings with the reason `query-body`. served and answer from their recordings with the reason `query-body`.
+19 -40
View File
@@ -385,8 +385,7 @@ section explicitly truncated.
Disallowed-origin entries are filtered before retention. Disallowed-origin entries are filtered before retention.
Selected script-source collection shares a 32 MiB aggregate UTF-8 retention Selected script-source collection shares a 32 MiB aggregate UTF-8 retention
budget and the `command_timeout_ms` deadline (default 20000 ms) across the budget and a 20-second deadline across the capture. An oversized source is
capture. The byte budget has no runtime override. An oversized source is
omitted while later smaller sources may still fit. When the deadline expires, omitted while later smaller sources may still fit. When the deadline expires,
remaining source reads are skipped. Script metadata, per-item omission reasons, remaining source reads are skipped. Script metadata, per-item omission reasons,
and `script_sources` completeness remain available. A single CDP response can and `script_sources` completeness remain available. A single CDP response can
@@ -394,25 +393,19 @@ still be materialized up to the separate 64 MiB transport limit before source
admission; the retention budget does not bound that transient allocation. admission; the retention budget does not bound that transient allocation.
Bundle parsing and semantic inspection run sequentially in an owned child, Bundle parsing and semantic inspection run sequentially in an owned child,
without executing captured code. These are separate phases. `max_heap_mb` without executing captured code. `max_heap_mb` defaults to 1024 MiB V8
defaults to 1024 MiB V8 old-space (range 128..16384). `analysis_timeout_ms` old-space; `analysis_timeout_ms` defaults to 30000 per script, including startup
defaults to 30000 per script, including startup and transfer. and transfer. A failed or timed-out script becomes an explicit unknown while
`total_analysis_timeout_ms` defaults to 300000 across script analysis. A completed findings and later scripts remain available. `total_analysis_timeout_ms`
per-script deadline above the effective total is rejected. A failed or defaults to 300000 across script analysis; expiry returns a typed timeout with
timed-out script becomes an explicit unknown while completed findings and later partial Evidence and identifies unstarted scripts. These controls are also
scripts remain available. Total-deadline expiry returns a typed timeout with available as CLI flags with hyphenated names. Capture and optional source-map
partial Evidence and identifies unstarted scripts. `source_map_fetch_timeout_ms` fetching have separate budgets.
defaults to 30000 and is an item limitation, not that analysis timeout. These
controls are also available as CLI flags with hyphenated names. Worker protocol
and result byte budgets have no runtime override.
One web-bundle capture/analysis is admitted per REA process at a time. Completed One web-bundle capture/analysis is admitted per REA process at a time. Completed
script results share an 8 MiB serialized transfer budget, with host heap script results share an 8 MiB serialized transfer budget, with host heap
reservation checked before decoding and validating another result. reservation checked before decoding and validating another result.
When the host reservation is what reaches zero, the failure names that heap Exhausted result capacity stops further script launches and returns a typed
headroom and the remaining fixed budget. When the 8 MiB allowance itself is
exhausted, the failure names that fixed budget and says it has no runtime
override. Either stop ends further script launches and returns a typed
resource failure with completed facts and explicit unstarted-script unknowns. resource failure with completed facts and explicit unstarted-script unknowns.
Worker control frames are limited to 64 KiB. Cleanup must confirm child termination Worker control frames are limited to 64 KiB. Cleanup must confirm child termination
before releasing its private files; failed cleanup retains its exact owner and before releasing its private files; failed cleanup retains its exact owner and
@@ -422,10 +415,8 @@ facts, including cancellation during final cleanup.
The worker heap setting is not a process-family RSS limit. Node enforces V8 The worker heap setting is not a process-family RSS limit. Node enforces V8
old-space flags; Bun ignores them. Windows Bun instead uses an owned process old-space flags; Bun ignores them. Windows Bun instead uses an owned process
commit limit with a 512 MiB runtime reserve; other Bun hosts have no enforced commit limit with a 512 MiB runtime reserve; other Bun hosts have no enforced
memory limit. An allocation failure before that Windows limit is reported as memory limit. These bounds do not establish an aggregate host-memory limit
reached still names the configured limit and says it was not reached. These across other REA operations or external processes.
bounds do not establish an aggregate host-memory limit across other REA
operations or external processes.
Resource-tree inventories distinguish a valid empty `resources` array from a Resource-tree inventories distinguish a valid empty `resources` array from a
missing or malformed collection. Invalid resource entries are omitted with missing or malformed collection. Invalid resource entries are omitted with
@@ -439,24 +430,12 @@ an invalid supplied size remains unknown with incomplete resource coverage while
the resource's other facts are retained. Zero and nonnegative fractional sizes the resource's other facts are retained. Zero and nonnegative fractional sizes
are preserved. are preserved.
CDP discovery has a 10-second response deadline. HTTP discovery documents CDP discovery has a 10-second deadline. Each WebSocket handshake and command
use an 8 MiB budget. Browser-socket discovery (`Browser.getVersion`, response has its own 20-second deadline, separate from the caller-selected
`Target.getTargets`) uses the 64 MiB protocol message budget. Neither budget observation window. A stalled handshake or command reports `provider_timeout`;
nor the discovery deadline has a runtime override. `list_browser_targets` accepts no caller cancellation still reports cancellation. WebSocket messages remain subject
`command_timeout_ms`: HTTP discovery and browser-socket discovery to the [64 MiB limit](#request-boundary). REA does not impose a message-count or
(`Browser.getVersion`, `Target.getTargets`) stay on that fixed deadline, and pending-command quota. On cancellation, it closes only its own WebSocket,
the socket closes without a later command. Other CDP tools use
`command_timeout_ms` (default 20000) for each WebSocket handshake and command
response, separate from the caller-selected observation window, discovery, and
the 5000 ms cleanup grace. A browser-level socket's handshake and the following
`Target.attachToTarget` each receive that full deadline; they do not share one
budget. A stalled handshake or command reports
`provider_timeout` and names `command_timeout_ms` only when that operation
accepts it; caller cancellation still reports cancellation. WebSocket messages
remain subject to the [64 MiB limit](#request-boundary), which has no runtime
override. REA does
not impose a message-count or pending-command quota. On cancellation, it closes
only its own WebSocket,
waiting up to one second before terminating it. Malformed unsolicited events waiting up to one second before terminating it. Malformed unsolicited events
and malformed replies to pending commands poison the connection so later and malformed replies to pending commands poison the connection so later
commands fail closed. A correlated reply's session ID must match the command's commands fail closed. A correlated reply's session ID must match the command's
@@ -465,7 +444,7 @@ rejects it as unknown; other command rejections retain their reported failure.
Network and console coverage starts only after REA attaches and enables the relevant CDP domains. `prior_activity_available` is always `false`; absence from these arrays is not evidence that an event never occurred. Scripts are different: scripts the page parsed before REA attached are still reported under `scripts`. A page that loaded before attach can therefore list its scripts while `network.requests` lacks the requests that fetched them. If CDP reuses a request ID without supplying redirect metadata, REA retains the predecessor observation and marks network coverage incomplete. Network and console coverage starts only after REA attaches and enables the relevant CDP domains. `prior_activity_available` is always `false`; absence from these arrays is not evidence that an event never occurred. Scripts are different: scripts the page parsed before REA attached are still reported under `scripts`. A page that loaded before attach can therefore list its scripts while `network.requests` lacks the requests that fetched them. If CDP reuses a request ID without supplying redirect metadata, REA retains the predecessor observation and marks network coverage incomplete.
Source maps are not fetched by inspection. Bundle analysis fetches them only when `fetch_source_maps` is selected, with exact-origin redirect checks and no credentials, cookies, or referrer. Each fetch operation uses `source_map_fetch_timeout_ms` (default 30000) and a 64 MiB aggregate streamed-response budget across redirects. The byte budget has no runtime override. A fetch overrun is an item limitation, not a thrown analysis timeout. A deadline or byte-limit failure remains explicitly unavailable and makes bundle completeness partial. Source-map decoding also bounds raw resolved identities to 64 MiB and aggregate inline evidence to 32 MiB. Failed maps preserve their failure context when it fits; if another context cannot fit, collection stops with the full requested count, the returned processed count, and an explicit unknown-results limitation. Retained mappings and original sources are returned inline. See [source-location limits](web-source-location.md) for the structural decoder bounds. Original source artifacts with `.ts`, `.tsx`, `.mts`, or `.cts` suffixes carry `text/typescript`; other source names retain `text/javascript`. Source maps are not fetched by inspection. Bundle analysis fetches them only when `fetch_source_maps` is selected, with exact-origin redirect checks and no credentials, cookies, or referrer. Each fetch operation has a 30-second deadline and a 64 MiB aggregate streamed-response budget across redirects. A deadline or byte-limit failure remains explicitly unavailable and makes bundle completeness partial. Source-map decoding also bounds raw resolved identities to 64 MiB and aggregate inline evidence to 32 MiB. Failed maps preserve their failure context when it fits; if another context cannot fit, collection stops with the full requested count, the returned processed count, and an explicit unknown-results limitation. Retained mappings and original sources are returned inline. See [source-location limits](web-source-location.md) for the structural decoder bounds. Original source artifacts with `.ts`, `.tsx`, `.mts`, or `.cts` suffixes carry `text/typescript`; other source names retain `text/javascript`.
source-map items retain the script's `declared_url` separately from the observed source-map items retain the script's `declared_url` separately from the observed
`effective_url`. successful maps use the latter as their source-resolution base. `effective_url`. successful maps use the latter as their source-resolution base.
+3 -15
View File
@@ -23,21 +23,9 @@ that rejection does not roll back browser effects. REA removes its owned
future-document scripts, restores live frame methods, then disconnects. future-document scripts, restores live frame methods, then disconnects.
Add secret declarations, storage seeds, and additional artifact or event Add secret declarations, storage seeds, and additional artifact or event
capture only when needed. Action, secret, and storage counts are not capped. capture only when needed. Action, secret, and storage counts are not capped.
Duration, action, and navigation timeouts remain separate from CDP command Duration, action, and navigation timeouts remain fixed provider-owned liveness
deadlines. `command_timeout_ms` (default 20000; omit it to keep that default limits. `capture.limits` controls retained event and network-content budgets;
and leave it out of Evidence) bounds launch startup, the connect-over-CDP it does not change those liveness deadlines.
handshake, target lookup, environment commands, and storage CDP commands,
including replies from attached child targets. Set it in the scenario JSON.
`capture-browser-scenario` has no `--command-timeout-ms` option.
It does not change action `timeout_ms`, `wait_for_timeout.duration_ms`, or
navigation. Service-worker restoration keeps a fixed 5000 ms grace and does
not inherit a raised `command_timeout_ms`. CDP discovery stays on its own
fixed deadline. When service workers are blocked, the registration check
before discovery has its own fixed 10000 ms deadline; `command_timeout_ms`
does not apply because discovery has not started. The discovery deadline
covers endpoint discovery only. The following WebSocket handshake and
`Target.attachToTarget` each use `command_timeout_ms`. `capture.limits` controls retained event and network-content
budgets; it does not change those liveness deadlines.
## Minimal launch example ## Minimal launch example
+1 -24
View File
@@ -63,16 +63,6 @@ or repeat it to select several types.
Selection limits decoding to the selected types; member references remain an Selection limits decoding to the selected types; member references remain an
assembly-wide inventory, so the output can still be large. assembly-wide inventory, so the output can still be large.
Select `--detail summary --evidence-bundle /absolute/path/members.json` to
receive identity, selection, coverage, limitations and collection counts while
saving the complete original Evidence in a new bundle. Summary requires that
explicit destination; existing files are preserved. Later
`inspect-analysis-view` calls can page the exact parent from the saved bundle
without reopening the assembly. See
[CLI summary and saved member rows](managed-code-analysis.md#cli-summary-and-saved-member-rows)
for the complete command sequence. Omitted or `--detail complete` retains the
full inline response.
Choose the workflow from the artifact's contents rather than its `.exe` or Choose the workflow from the artifact's contents rather than its `.exe` or
`.dll` extension. The managed parser requires PE/CLI metadata; it does not `.dll` extension. The managed parser requires PE/CLI metadata; it does not
unpack .NET single-file hosts or decode IL2CPP. For the image, type and method unpack .NET single-file hosts or decode IL2CPP. For the image, type and method
@@ -289,12 +279,7 @@ Exports preserve an existing destination unless `--overwrite` is explicit.
For application traces and version comparisons, see For application traces and version comparisons, see
[JavaScript application workflows](javascript-application-workflows.md). [JavaScript application workflows](javascript-application-workflows.md).
MCP [retained Evidence references](mcp-contracts.md#retained-application-evidence-inputs) MCP [retained Evidence references](mcp-contracts.md#retained-application-evidence-inputs)
belong to one connection. Separate CLI calls can consume full saved records. belong to one connection. Separate CLI calls consume full saved records.
`inspect-analysis-view REQUEST.json --evidence-bundle BUNDLE.json` resolves
the request's `retained-evidence` source ID from that authenticated bundle,
including bundles exported from an MCP session. The same view JSON works for
every supported parent kind; no target is reopened and no record is selected
by file order. Omit `--evidence-bundle` when the request supplies inline Evidence.
## Import historical source ## Import historical source
@@ -551,14 +536,6 @@ command, objdump, Rizin and GDB, and the agent environment server
signal-derived status. This describes the current source routing; it is not a signal-derived status. This describes the current source routing; it is not a
claim that every command or provider runtime has been independently exercised. claim that every command or provider runtime has been independently exercised.
Commands with `--evidence_output` select their delivered view, then publish the
complete Evidence bundle atomically. Cancellation before publication leaves no
new file. A publication that has committed can win a later cancellation; the
`cancelled` result then keeps the written path and byte count under
`details.diagnostics.evidence_bundle`. If staging cleanup stays incomplete
after commit, the `cleanup_incomplete` error reports the same file as
`details.published_path` and `details.bytes`.
When the reader of piped output closes early, as `rea ... | head` does, the When the reader of piped output closes early, as `rea ... | head` does, the
CLI stops writing, prints no error and exits `141` (`128 + 13`, the status of a CLI stops writing, prints no error and exits `141` (`128 + 13`, the status of a
SIGPIPE-terminated writer). The operation itself may have completed; only its SIGPIPE-terminated writer). The operation itself may have completed; only its
+1 -7
View File
@@ -245,10 +245,4 @@ application path and capture outcome to the static graph, while frames, scripts,
workers, and execution claims remain unavailable until a separate passive runtime workers, and execution claims remain unavailable until a separate passive runtime
capture provides them. capture provides them.
ASAR observation reads the header through one open file handle and validates its ASAR observation reads the header through one open file handle, validates its declared lengths against the archive size, and limits encoded JSON headers to 16 MiB before parsing. The header cache also retains at most 16 MiB of encoded headers across eight archives. Oversized headers produce a scope refusal.
declared lengths against the archive size. Header/index admission is the same
fixed 16 MiB / 16777216 byte budget used by static ASAR reads. This release has
no runtime override. An oversized header is not parsed; selecting a member path
does not bypass admission, and the page stays unauthorized. The header cache
separately retains at most 16 MiB of already-admitted encoded headers across
eight archives. Evicting that cache does not raise admission.
+1 -11
View File
@@ -106,15 +106,6 @@ only reduces delivery; source selection reduces text acquisition and analysis.
Explicit native provider or snapshot routing rejects `--source-path` rather Explicit native provider or snapshot routing rejects `--source-path` rather
than ignoring it. than ignoring it.
ASAR header/index admission is a fixed 16 MiB / 16777216 bytes for this
release. There is no runtime override. The archive index is read before any
member can be selected, so `source_paths`, `--source-path`, `detail:
"summary"`, `max_heap_mb`, and available host memory do not bypass or raise
it. When the header exceeds that allowance, the failure names this budget.
Analyze an extracted application directory, which does not admit an ASAR
header, or supply an archive whose header fits. Repeating the unchanged
request fails the same way.
## Read the analysis ## Read the analysis
`analyze_javascript_application` returns the complete analysis Evidence by `analyze_javascript_application` returns the complete analysis Evidence by
@@ -181,8 +172,7 @@ for the tool-result contract.
Use MCP `detail: "summary"` for a large application, then read selected views of Use MCP `detail: "summary"` for a large application, then read selected views of
the retained analysis. Summary changes delivery only. To reduce source parsing, the retained analysis. Summary changes delivery only. To reduce source parsing,
select exact [source paths](#select-sources-before-analysis). Source selection select exact [source paths](#select-sources-before-analysis).
does not bypass ASAR header/index admission.
If a complete response exceeds the transport budget, use its retained Evidence If a complete response exceeds the transport budget, use its retained Evidence
ID for a summary, module page, or item, or export the bundle. See ID for a summary, module page, or item, or export the bundle. See
+1 -5
View File
@@ -106,11 +106,7 @@ nodes. REA evaluates compatible static candidates conservatively:
that disagreement. If a mapped candidate's digest is unavailable and no that disagreement. If a mapped candidate's digest is unavailable and no
mapped content match is known, the comparison remains `unknown`. mapped content match is known, the comparison remains `unknown`.
6. Missing authorization mappings and incomplete or truncated input coverage 6. Missing authorization mappings and incomplete or truncated input coverage
remain unknown or truncated rather than becoming absence claims. A runtime remain unknown or truncated rather than becoming absence claims.
location with no automatic artifact-root or caller-declared mapping into
any layer, and no unique content match, is `unknown` with
`no-authorized-location-mapping`, with or without captured bytes. Only a
mapped location can be `unmatched` with `no-static-content-match`.
Every successful cross-layer `observed_as` edge has Every successful cross-layer `observed_as` edge has
`authority: cross-layer-reconciliation` and `state: inferred`. The underlying `authority: cross-layer-reconciliation` and `state: inferred`. The underlying
+1 -1
View File
@@ -68,7 +68,7 @@ The background export belongs to the session. `close_binary`, or the end of a CL
The assembly label index is scanned once per export reader. Cancelling a request stops its wait without interrupting other readers; closing the session aborts and drains the shared scan before releasing its files. The assembly label index is scanned once per export reader. Cancelling a request stops its wait without interrupting other readers; closing the session aborts and drains the shared scan before releasing its files.
Regex procedure and string search uses ECMAScript Unicode matching in a cancellable worker with a five-second matching deadline and a worker old-generation heap. Literal matching remains the default. Cancellation or a matching deadline leaves the session available. This release has no runtime override for that deadline. The worker requests 128 MiB. A positive Node.js `--max-old-space-size` on the process that starts REA, including `NODE_OPTIONS`, overrides that request and sets the effective heap. `--max-old-space-size-percentage` takes precedence over that flag in either order. REA does not turn the percentage into a MiB, so the effective heap stays unestablished instead of the 128 MiB request or the absolute flag. The reported `NODE_OPTIONS` value is the process initial environment when that block can be read. A later edit to the environment is not the value Node applied, and an unreadable initial value leaves the effective heap unestablished. The launch Node options are the initial command line on Linux when that buffer still has a program entry, not a later edit to `process.execArgv`. A preload can clear that array after V8 has applied the flags. Setting `process.title` or `--title` overwrites the command line, so a buffer without a program entry leaves the effective heap unestablished. `--env-file` or `--env-file-if-exists` can supply `NODE_OPTIONS` when the initial environment has none. That file is not part of the initial environment block, so the effective heap stays unestablished instead of the 128 MiB request. An initial `NODE_OPTIONS` is still used, because Node does not let the file replace it. A positive `--max-heap-size`, including the underscore form and launch `NODE_OPTIONS` on Node versions that allow it, sets the total heap rather than the old generation and can be lower than the 128 MiB request. The effective old generation stays unestablished while that value is in effect. A zero or empty value clears it. `--experimental-config-file` or `--experimental-default-config-file` can set `nodeOptions`, including a heap percentage, from a file that is not read back. The effective heap stays unestablished when either flag is present, including when an absolute `--max-old-space-size` is also on the command line or in the recovered `NODE_OPTIONS`. Node accepts the config path as `=` or the next argument. Regex procedure and string search uses ECMAScript Unicode matching in a cancellable worker with a five-second matching deadline and a 128 MiB worker heap. Literal matching remains the default. Cancellation or a matching deadline leaves the session available. This release has no runtime override for that deadline or heap.
Single-function subprocess output and each cached source facet share a 16 MiB buffered-text capacity. Cached spans are checked before allocation; larger text remains in the local export and the error identifies the file and range. `analyze_function` initially returns the complete dossier, which can still be large; later retained views do not remove that first transfer. Prefer the individual text tools for a focused question. Single-function subprocess output and each cached source facet share a 16 MiB buffered-text capacity. Cached spans are checked before allocation; larger text remains in the local export and the error identifies the file and range. `analyze_function` initially returns the complete dossier, which can still be large; later retained views do not remove that first transfer. Prefer the individual text tools for a focused question.
+4 -50
View File
@@ -85,7 +85,7 @@ rejects selected summary before analysis; complete delivery remains available.
Complete `inspect_managed_members` delivery always includes the assembly-wide Complete `inspect_managed_members` delivery always includes the assembly-wide
`member_refs` and `exported_types`, which `type_names` does not narrow, so even `member_refs` and `exported_types`, which `type_names` does not narrow, so even
one selected or unmatched type can return megabytes. In MCP, select one selected or unmatched type can return megabytes. Select
`{"path":"/absolute/path/Assembly.dll","type_names":["Namespace.Type"],"detail":"summary"}` `{"path":"/absolute/path/Assembly.dll","type_names":["Namespace.Type"],"detail":"summary"}`
to keep the complete source in the session and receive identity, metadata to keep the complete source in the session and receive identity, metadata
status, the selection with its unmatched names, coverage/issues, the separate status, the selection with its unmatched names, coverage/issues, the separate
@@ -115,49 +115,6 @@ literal pool's own coverage. Older Evidence that never examined exported types
or user strings reports a null count and refuses those pages rather than or user strings reports a null count and refuses those pages rather than
returning an empty one. returning an empty one.
#### CLI summary and saved member rows
The CLI accepts the same `complete` and `summary` detail choices. Omitted or
`--detail complete` returns complete Evidence. To receive a summary, choose a
new Evidence bundle destination so its original complete parent remains
available after the command exits:
```bash
rea inspect-managed-members /absolute/path/Assembly.dll --type Namespace.Type \
--detail summary --evidence-bundle /absolute/path/evidence/members.json --json
```
`--evidence-bundle` writes the complete original Evidence in REA's canonical
bundle format. Its parent directory must exist, and it never replaces an
existing path. Relative paths resolve against the command's working directory.
The option also works with complete delivery when a saved bundle is useful.
Summary without a destination is rejected before analysis. A publication failure
returns a typed error with the completed Evidence under
`details.partial_observation`; it does not report successful summary delivery.
Cancellation before publication prevents the new output. Once atomic
publication has started, it can complete despite cancellation; a cancelled
delivery then retains the committed path and byte count under
`details.diagnostics.evidence_bundle`.
Copy `normalized_result.parent_evidence_id` from the summary into the
`source.evidence_id` field of the `managed-members` request above, then save that
small request as `members-page.json` and run:
```bash
rea inspect-analysis-view /absolute/path/evidence/members-page.json \
--evidence-bundle /absolute/path/evidence/members.json --json
```
The view command authenticates the selected bundle and resolves that exact
parent ID. It does not reopen the assembly, so the original target may have
been moved or removed. Subsequent calls can select another collection or use
the returned `coverage.next_offset` to continue the same collection. An inline
source already includes its parent and cannot also select `--evidence-bundle`.
Summary changes delivery, while type selection still controls decoding and the
same inspection resource limits apply. Each separate CLI page reads and
authenticates the saved bundle; the full source is retained on disk.
## Shipped scope ## Shipped scope
The canonical parser admits PE/CLI bytes and their metadata/CIL without loading The canonical parser admits PE/CLI bytes and their metadata/CIL without loading
@@ -665,9 +622,6 @@ which managed methods executed or how the CLR resolved them.
Managed member comparisons report a `metadata` dimension when an observed Managed member comparisons report a `metadata` dimension when an observed
MethodDef flags or implementation flags value differs, including accessibility MethodDef flags or implementation flags value differs, including accessibility
or synchronization changes. Observed method-body `init_locals` and `max_stack` or synchronization changes. This is separate from the CIL/signature matching
differences also contribute to `metadata`, even when CIL decoding is incomplete. tiers: identical instructions do not establish unchanged method metadata. The
An unavailable header value keeps body coverage unknown rather than establishing reported difference is static metadata evidence, not proof of runtime behavior.
equality or a difference. These comparisons are separate from the CIL/signature
matching tiers: identical instructions do not establish unchanged method metadata.
The reported difference is static metadata evidence, not proof of runtime behavior.
+5 -8
View File
@@ -234,14 +234,11 @@ The destination changes only when a complete export is atomically published.
Clients that explicitly configure a larger receive buffer can set the REA Clients that explicitly configure a larger receive buffer can set the REA
server's `REA_MCP_MAX_RESPONSE_BYTES` environment variable to the same byte server's `REA_MCP_MAX_RESPONSE_BYTES` environment variable to the same byte
count. This setting must be a safe decimal integer at least 10485760 (10 MiB); count. This setting must be a safe decimal integer at least 10485760; REA
REA reserves 1024 bytes for the envelope. The value is read when the REA reserves 1024 bytes for the envelope. Raising it restores complete inline
process starts, so a change requires a restart and a matching client receive delivery for responses that fit that buffer and Node's single-string limit.
buffer. Raising it restores complete inline delivery for responses that fit It does not change the client's buffer, analysis coverage, or retained content.
that buffer. It does not raise REA's reserved single-string limit, which is Ordinary responses keep their existing complete result contract.
1024 UTF-16 code units below Node's single-string ceiling, and it does not
raise that Node ceiling. It does not change analysis coverage or retained
content. Ordinary responses keep their existing complete result contract.
### Error recovery and deadline ownership ### Error recovery and deadline ownership
+3 -31
View File
@@ -202,38 +202,10 @@ Function dossiers retain this same string evidence. Native call edges retain
Hopper's partial `CallReference` classification and exact endpoints; detailed Hopper's partial `CallReference` classification and exact endpoints; detailed
reference flags remain unavailable rather than being invented. reference flags remain unavailable rather than being invented.
Regex searches use ECMAScript Unicode syntax in a cancellable worker with a Regex searches use ECMAScript Unicode syntax in a cancellable worker with a
five-second matching deadline and a worker old-generation heap. Deadline or five-second matching deadline and a 128 MiB worker heap. Deadline or
cancellation stops matching while leaving the Hopper API available. This cancellation stops matching while leaving the Hopper API available. This
release has no runtime override for that deadline. The worker requests 128 release has no runtime override for that deadline or heap. Literal mode
MiB. A positive Node.js `--max-old-space-size` on the process that starts retains Unicode casefold matching.
REA, including `NODE_OPTIONS`, overrides that request and sets the effective
heap. `--max-old-space-size-percentage` takes precedence over that flag in
either order. REA does not turn the percentage into a MiB, so the effective
heap stays unestablished instead of the 128 MiB request or the absolute flag.
The reported `NODE_OPTIONS` value is the process initial environment when that
block can be read. A later edit to the environment is not the value Node
applied, and an unreadable initial value leaves the effective heap
unestablished. The launch Node options are the initial command line on Linux
when that buffer still has a program entry, not a later edit to
`process.execArgv`. A preload can clear that array after V8 has applied the
flags. Setting `process.title` or `--title` overwrites the command line, so a
buffer without a program entry leaves the effective heap unestablished.
`--env-file` or `--env-file-if-exists` can supply `NODE_OPTIONS`
when the initial environment has none. That file is not part of the initial
environment block, so the effective heap stays unestablished instead of the
128 MiB request. An initial `NODE_OPTIONS` is still used, because Node does
not let the file replace it. A positive `--max-heap-size`, including the
underscore form and launch `NODE_OPTIONS` on Node versions that allow it,
sets the total heap rather than the old generation and can be lower than the
128 MiB request. The effective old generation stays unestablished while that
value is in effect. A zero or empty value clears it.
`--experimental-config-file` or `--experimental-default-config-file` can set
`nodeOptions`, including a heap percentage, from a file that is not read
back. The effective heap stays unestablished when either flag is present,
including when an absolute `--max-old-space-size` is also on the command
line or in the recovered `NODE_OPTIONS`. Node accepts the config path as
`=` or the next argument.
Literal mode retains Unicode casefold matching.
## Bounded static traces ## Bounded static traces
@@ -18,13 +18,10 @@ tool, view kind, provider, network activity or write effect.
item or list plan holds its latest initial value as a `document` when its item or list plan holds its latest initial value as a `document` when its
initial reads have one request line, whether or not the capture writes its initial reads have one request line, whether or not the capture writes its
path, unless its value changed between two of those reads across any path, unless its value changed between two of those reads across any
recorded write (even under another root), which leaves it `fields-differ`. A recorded write (even under another root), which leaves it `fields-differ`. More request lines give `path-varies` when they differ by path (a
value whose rendered JSON exceeds 256 KiB stays `rendered-oversize` and is template with a parameter before its end) and `query-varies` when they share
replayed from the recorded body, which itself fit. More request lines give one path. With every such read now a document, `path-varies`, `query-varies`
`path-varies` when they differ by path (a template with a parameter before or `body-unusable`, the unreachable `not-entity` reason leaves the result
its end) and `query-varies` when they share one path. With every such read
now a document, `path-varies`, `query-varies`, `body-unusable` or
`rendered-oversize`, the unreachable `not-entity` reason leaves the result
schema, and `no-identity` names item tools only. A document holds the value schema, and `no-identity` names item tools only. A document holds the value
of its one `path`. of its one `path`.
- When no path parameter proves an identity and no identity name identifies - When no path parameter proves an identity and no identity name identifies
@@ -83,10 +80,8 @@ Grafana-shaped fixture covers documents for its read-only paths (its one-member
team list among them) and object, array and enveloped-list descriptions; team list among them) and object, array and enveloped-list descriptions;
synthetic sources cover the `2xx` preference over an earlier error body, the synthetic sources cover the `2xx` preference over an earlier error body, the
first of several JSON records, the labelled fallback, the manifest's statuses, first of several JSON records, the labelled fallback, the manifest's statuses,
empty, scalar and fieldless bodies, a truncated member name, a name whose empty, scalar and fieldless bodies, a truncated member name, non-JSON bodies
JSON quoting exceeds the sample, a recorded body under 256 KiB whose rendered and the catalog budget; the in-process and compiled MCP servers advertise the
JSON exceeds it, non-JSON bodies and the catalog budget; the in-process and
compiled MCP servers advertise the
hints. A filtered list may still answer with one element whose identity name hints. A filtered list may still answer with one element whose identity name
repeats the requested value; list filters, pagination, lossless big integers repeats the requested value; list filters, pagination, lossless big integers
and cross-endpoint entity linking remain out of scope. Documents inline whole and cross-endpoint entity linking remain out of scope. Documents inline whole
@@ -110,14 +110,3 @@ and `fields-differ` is 9 to 10.
Not covered here: serving from the state, the AgentEnv data plane and write Not covered here: serving from the state, the AgentEnv data plane and write
effects, which are later increments. effects, which are later increments.
## After the squash onto main
The branch was squashed onto main once the state-serve branch merged (166fb082). Against a merge of the previous head
into that main, the squash keeps both sides of every conflict and drops nothing of this branch. Two tests then failed
on it and were refitted with the oversized-document fix in 2a926a0a: the bundle test of a state catalog over the
discovery budget uses 300 read tools, inside the window where the state's catalog is refused and replay's, with its
response summaries, still fits (280–320 by a probe), and the rea-state validation test names the pushed document by
the count of documents the state holds. The replay-only card test pinned main's 11,131-byte card; it now pins this
branch's 12,818-byte card, whose descriptions name each tool's recorded responses. The branch then merged main at
d7ca93cc without conflicts.
-12
View File
@@ -65,18 +65,6 @@ API dependency, and independent fixture compiler oracles keep their existing
TypeScript versions. TypeScript versions.
`npm run build:cached` compiles the CLI/MCP runtime and its packaged skill. `npm run build:cached` compiles the CLI/MCP runtime and its packaged skill.
The build also records `dist/mcp-catalog.json` (compact inputs) and
`dist/mcp-catalog-full.json` from the canonical SDK registrations and binds
each to the runtime with its own SHA-256 digest. These are
runtime artifacts, separate from the test catalog below. Default target-free
MCP startup serves its schemas without constructing analysis validators;
the first tool or guided-workflow request materializes the canonical runtime
on that same SDK server. Explicit compact/full input selections preserve
their advertised schemas throughout materialization. Initial-target,
HTTP and output-schema-off startup keep the eager path; source runs without
recorded artifacts also use eager composition. Compare memory both after discovery and after a completed
request; report diagnostic GC separately from normal resident memory.
Turbo's strict task environment preserves caller-selected `GOMAXPROCS`, Turbo's strict task environment preserves caller-selected `GOMAXPROCS`,
`GOMEMLIMIT`, and `UV_THREADPOOL_SIZE` for resource-constrained builds and `GOMEMLIMIT`, and `UV_THREADPOOL_SIZE` for resource-constrained builds and
checks. These execution controls do not change artifact content, so changing checks. These execution controls do not change artifact content, so changing
+6 -8
View File
@@ -610,9 +610,8 @@ with reason `envelope-varies`, `fields-differ` or `list-differs`; the other
reasons are `query-body` (a query `POST`), `no-init-recording`, reasons are `query-body` (a query `POST`), `no-init-recording`,
`body-unusable`, `no-identity` (an item read without a proved identity), `body-unusable`, `no-identity` (an item read without a proved identity),
`path-varies` (another read under more than one path, as a template with a `path-varies` (another read under more than one path, as a template with a
parameter before its end), `query-varies` (another read of one path under parameter before its end) and `query-varies` (another read of one path under
more than one query) and `rendered-oversize` (a document value whose JSON more than one query). A document holds the value of its one `path`, so for such
renders to more than 256 KiB). A document holds the value of its one `path`, so for such
a template it says nothing about the template's other paths. A body is usable a template it says nothing about the template's other paths. A body is usable
when its content type is when its content type is
JSON-like, it is UTF-8 text of at most 256 KiB, every number survives native JSON-like, it is UTF-8 text of at most 256 KiB, every number survives native
@@ -627,8 +626,8 @@ computes responses or derived values, and no effects of writes. The state
accumulator charges the manifest's admission and grouping, every record line, accumulator charges the manifest's admission and grouping, every record line,
each body as temporary bytes before it is decoded and parsed, and every value each body as temporary bytes before it is decoded and parsed, and every value
it keeps, within the same 96 MiB budget. Documents hold whole recorded response it keeps, within the same 96 MiB budget. Documents hold whole recorded response
values whose recorded text and rendered JSON are each at most 256 KiB, so a values of up to 256 KiB each, so a capture with many read-only paths yields a
capture with many read-only paths yields a correspondingly larger state result. The view performs no network activity correspondingly larger state result. The view performs no network activity
and serves nothing; `rea environment-serve` derives it from the capture at and serves nothing; `rea environment-serve` derives it from the capture at
startup, serves read tools from it and loads replacement states through the startup, serves read tools from it and loads replacement states through the
AgentEnv data plane, as AgentEnv data plane, as
@@ -637,14 +636,13 @@ AgentEnv data plane, as
## Resource and lifecycle boundaries ## Resource and lifecycle boundaries
Complete evidence has a 32 MiB input, 96 MiB decoder reply and 64-level nesting Complete evidence has a 32 MiB input, 96 MiB decoder reply and 64-level nesting
budget. Those budgets have no runtime override. `capture_timeout_ms` does not budget. Exceeding a budget returns no partial success.
raise them. Exceeding a budget returns no partial success.
Oversized selected captures return an input error identifying `capture_path` Oversized selected captures return an input error identifying `capture_path`
and the byte budget; owned reply/output failures remain output or provider errors. and the byte budget; owned reply/output failures remain output or provider errors.
HAR, SAZ and qlog run in an owned HAR, SAZ and qlog run in an owned
192 MiB Node old-generation heap with one V8 worker. Mitmproxy decoding applies 192 MiB Node old-generation heap with one V8 worker. Mitmproxy decoding applies
Linux limits of 768 MiB address space, 30 seconds CPU and 96 MiB file output; Linux limits of 768 MiB address space, 30 seconds CPU and 96 MiB file output;
both commands use `capture_timeout_ms` (default 30000) and independently supervised both commands have a 30-second wall deadline and independently supervised
process cleanup. Owned decoder diagnostics retain at most 1 MiB across stdout process cleanup. Owned decoder diagnostics retain at most 1 MiB across stdout
and stderr; the collector drops overflow as it arrives, reports the limit and stderr; the collector drops overflow as it arrives, reports the limit
failure and verifies cleanup. Heap limits alone do not establish aggregate RSS limits. failure and verifies cleanup. Heap limits alone do not establish aggregate RSS limits.
+1 -1
View File
@@ -52,7 +52,7 @@ The result contains directly registered listener types/flags, callback coordinat
## Bounds and verification ## Bounds and verification
Each protocol message has a 64 MiB budget; script/context metadata and request metadata each have an 8 MiB retained budget. Complete retained source text has a 32 MiB budget. Oversized or malformed evidence returns no partial successful result. Each command and the complete inspection/source-join phase use `command_timeout_ms` (default 20000). Cleanup uses a separate 5000 ms grace; `command_timeout_ms` does not raise or shorten it, and this release has no runtime override for that grace. Transport closure follows the grace. The selected observation duration must fit the host timer's positive 32-bit millisecond range. REA does not control the externally owned browser's CPU or memory. Each protocol message has a 64 MiB budget; script/context metadata and request metadata each have an 8 MiB retained budget. Complete retained source text has a 32 MiB budget. Oversized or malformed evidence returns no partial successful result. Each command and the complete inspection/source-join phase have a 20-second deadline; cleanup has a separate 5-second command deadline plus transport closure. The selected observation duration must fit the host timer's positive 32-bit millisecond range. REA does not control the externally owned browser's CPU or memory.
`npm run verify:browser:runtime` uses caller-supplied `REA_BROWSER_EXECUTABLE` and the native `ps` command on Linux/macOS against an owned synthetic fixture, through real public CLI and stdio MCP. It checks exact Unicode/CRLF text/digests, same-URL identities, same-document navigation, inert declared names, executed and zero-count branches, function-only unknowns on repeated observation, listener selection, initiator association, actual arming and preservation of the external page. A separate fixture page closes during an armed MCP window to verify prompt termination and inline cleanup diagnostics. The optional positional entrypoint to `scripts/verify-browser-runtime.mjs` permits the same checks against an isolated installed tarball. `npm run verify:browser:runtime` uses caller-supplied `REA_BROWSER_EXECUTABLE` and the native `ps` command on Linux/macOS against an owned synthetic fixture, through real public CLI and stdio MCP. It checks exact Unicode/CRLF text/digests, same-URL identities, same-document navigation, inert declared names, executed and zero-count branches, function-only unknowns on repeated observation, listener selection, initiator association, actual arming and preservation of the external page. A separate fixture page closes during an armed MCP window to verify prompt termination and inline cleanup diagnostics. The optional positional entrypoint to `scripts/verify-browser-runtime.mjs` permits the same checks against an isolated installed tarball.
+2 -3
View File
@@ -71,7 +71,7 @@ implement every current source-map extension.
One owned Node subprocess decodes the map. Its private request directory is One owned Node subprocess decodes the map. Its private request directory is
removed before success is returned. Only this child's inherited `NODE_OPTIONS` removed before success is returned. Only this child's inherited `NODE_OPTIONS`
is removed; explicit V8 flags set a 192 MiB old-generation and 8 MiB semi-space is removed; explicit V8 flags set a 192 MiB old-generation and 8 MiB semi-space
budget, one V8 worker and `decode_timeout_ms` (default 20000). The codec heap, map budget, and reply budget have no runtime override. Reported actual V8 heap limits budget, one V8 worker and a 20-second deadline. Reported actual V8 heap limits
are checked. These are heap limits, not an aggregate process RSS limit. When are checked. These are heap limits, not an aggregate process RSS limit. When
REA runs on Bun, the subprocess is Bun, which ignores these V8 flags: `runtime` REA runs on Bun, the subprocess is Bun, which ignores these V8 flags: `runtime`
records `id: "bun"`, the Bun version, its Node.js compatibility version and a records `id: "bun"`, the Bun version, its Node.js compatibility version and a
@@ -86,8 +86,7 @@ cleanup overrides success with `cleanup_incomplete` and the owning
operation/resources. operation/resources.
Complete evidence is bounded by a 4 MiB map, 32 MiB codec reply, 262144 decoded Complete evidence is bounded by a 4 MiB map, 32 MiB codec reply, 262144 decoded
rows/segments and 64 nested section levels. Those structural budgets and the rows/segments and 64 nested section levels. Exceeding a budget returns an explicit
192 MiB codec heap have no runtime override. Exceeding a budget returns an explicit
failure, without partial results. The existing 32 MiB manifest and 16 MiB selected failure, without partial results. The existing 32 MiB manifest and 16 MiB selected
script boundaries also apply. No global toolchain or browser configuration changes script boundaries also apply. No global toolchain or browser configuration changes
are made. are made.
@@ -1,84 +0,0 @@
"""Summarize completed CLI trials; token counts are estimates, not model usage."""
import argparse
import hashlib
import json
import statistics
from pathlib import Path
import tiktoken
parser = argparse.ArgumentParser()
parser.add_argument('root', type=Path)
parser.add_argument('--output', type=Path, required=True)
args = parser.parse_args()
root = args.root
inputs = json.loads((root / 'measurement-inputs.json').read_text())
expected = inputs['expected_runtime_bindings']
assert expected['complete']['git_revision'] == inputs['baseline_revision']
assert expected['selected']['git_revision'] == inputs['feature_parent_revision']
assert expected['complete']['root'] != expected['selected']['root']
assert expected['complete']['compiled_cli_sha256'] != expected['selected']['compiled_cli_sha256']
assert expected['complete']['source_diff_sha256'] == hashlib.sha256(b'').hexdigest()
assert expected['complete']['untracked_source_sha256'] == hashlib.sha256(b'[]').hexdigest()
enc = tiktoken.get_encoding('cl100k_base')
count = lambda text: len(enc.encode(text, disallowed_special=()))
compact = lambda obj: json.dumps(obj, ensure_ascii=False, separators=(',', ':'))
runs = []
for case in sorted(root.glob('pair-*')):
if not case.is_dir():
continue
report = json.loads((case / 'report.json').read_text())
assert report['status'] == 'passed' and report['cleanup_verified']
assert report['runtime_binding_verified']
assert report['runtime_binding'] == expected[report['mode']], case.name
assert str(Path(report['runtime']).resolve()) == report['runtime_binding']['root']
receipt = next(json.loads(line) for line in reversed(case.with_name(case.name + '.log').read_text().splitlines()) if line.startswith('{"resource_scope":'))
obs = receipt['payload_resource_observation']
assert receipt['abort'] is None and receipt['payload_resource_observation_verified']
assert obs['payload_exit_code'] == 0 and obs['payload_cleanup_verified']
assert obs['resource_events']['memory.events']['oom_kill'] == 0
tokens = {'question_and_recipe': count(report['question'] + '\n' + report['recipe']),
'arguments': sum(count(compact(call['arguments'])) for call in report['calls']),
'help_and_replies': sum(count((case / call['stdout_file']).read_text()) for call in report['calls'])}
tokens['whole_task'] = sum(tokens.values())
runs.append({'case': case.name, 'mode': report['mode'], 'cache': report['cache'], 'runtime': report['runtime'], 'runtime_binding': report['runtime_binding'], 'runtime_binding_audit_ms': report['runtime_binding_audit_ms'],
'wall_seconds': receipt['elapsed_seconds'], 'cpu_seconds': obs['cpu_stat']['usage_usec'] / 1e6,
'peak_process_family_bytes_including_page_cache': obs['memory_peak_bytes'],
'tokens_estimated': tokens, 'calls': report['calls'],
'parent_evidence_id': report['parent_evidence_id'], 'parent_semantic_sha256': report['parent_semantic_sha256'],
'answer_sha256': report['answer_sha256'], 'oracle_verified': report['oracle_verified'],
'cleanup_verified': True, 'resource_events': obs['resource_events']})
assert len(runs) == 12
for key in ['answer_sha256', 'parent_semantic_sha256', 'parent_evidence_id']:
assert len({row[key] for row in runs}) == 1, key
summary = {}
for cache in ['cold', 'warm']:
summary[cache] = {}
for mode in ['complete', 'selected']:
rows = [r for r in runs if r['mode'] == mode and r['cache'] == cache]
assert len(rows) == 3
summary[cache][mode] = {key: {'median': statistics.median(r[key] for r in rows), 'min': min(r[key] for r in rows), 'max': max(r[key] for r in rows)} for key in ['wall_seconds', 'cpu_seconds', 'peak_process_family_bytes_including_page_cache']}
summary[cache][mode]['whole_task_tokens_estimated'] = [r['tokens_estimated']['whole_task'] for r in rows]
result = {'status': 'passed', 'tokenizer': 'tiktoken==0.12.0/cl100k_base; estimates, not model usage',
'context_scope': 'Question, supplied command recipe, actual selected-command help, all logical CLI argv and all stdout. No model invoked; reasoning, framing, irrelevant tool discovery and actual native-agent usage unmeasured.',
'cache_scope': 'cold disables Node compile cache; warm primes the identical workflow once per runtime/mode before timing. Fresh processes and isolated HOME each trial; OS page cache uncontrolled.',
'resource_scope': 'Whole task driver plus all CLI children; includes pre/post source-and-compiled-runtime binding verification, bundle publication/authentication, help, extra calls and local answer/oracle checking. Kernel cgroup peak includes file cache; not sampled RSS. Download/build/oracle preparation and priming excluded.',
'acquisition': json.loads((root / 'acquisition.json').read_text()), 'measurement_inputs': inputs,
'summary': summary, 'runs': runs}
native = root / 'native-control'
if native.exists():
binding = json.loads((native / 'runtime-binding.json').read_text())
assert binding['runtime_binding_verified'] and binding['runtime_binding'] == expected['complete']
assert str(Path(binding['runtime']).resolve()) == binding['runtime_binding']['root']
receipt = next(json.loads(line) for line in reversed((root / 'native-control.log').read_text().splitlines()) if line.startswith('{"resource_scope":'))
obs = receipt['payload_resource_observation']
assert receipt['abort'] is None and receipt['payload_resource_observation_verified'] and obs['payload_exit_code'] == 0 and obs['payload_cleanup_verified']
baseline_case = root / next(run['case'] for run in runs if run['mode'] == 'complete')
assert json.loads((native / 'answer.json').read_text()) == json.loads((baseline_case / 'answer.json').read_text())
control_source = Path(__file__).with_name('managed-cli-local-control.py').read_text()
result['existing_native_control'] = {'runtime': binding['runtime'], 'runtime_binding': binding['runtime_binding'], 'runtime_binding_audit_ms': binding['runtime_binding_audit_ms'], 'repetitions': 1,
'tokens_estimated_script_plus_help_plus_answer': count(control_source) + count((native / 'help.txt').read_text()) + count((native / 'answer.json').read_text()),
'token_scope': 'Projection source, original CLI help and returned answer; excludes shared question and launcher argv. No actual model usage measured.',
'wall_seconds': receipt['elapsed_seconds'], 'cpu_seconds': obs['cpu_stat']['usage_usec'] / 1e6,
'peak_process_family_bytes_including_page_cache': obs['memory_peak_bytes'], 'answer_matches': True, 'cleanup_verified': True}
args.output.write_text(json.dumps(result, indent=2) + '\n')
print(json.dumps(summary, indent=2))
@@ -1,44 +0,0 @@
"""Existing native code-composition control: save complete stdout, return selected facts."""
import argparse
import json
import subprocess
import time
from pathlib import Path
parser = argparse.ArgumentParser()
parser.add_argument('runtime', type=Path)
parser.add_argument('target', type=Path)
parser.add_argument('output', type=Path)
parser.add_argument('--measurement-inputs', type=Path, required=True)
args = parser.parse_args()
args.output.mkdir()
node = 'node'
inputs = json.loads(args.measurement_inputs.read_text())
expected = inputs['expected_runtime_bindings']['complete']
assert expected['git_revision'] == inputs['baseline_revision']
helper = Path(__file__).with_name('managed-cli-runtime.mjs')
def capture_binding():
return json.loads(subprocess.check_output([node, str(helper), str(args.runtime)], text=True))
binding_started = time.monotonic()
binding = capture_binding()
binding_audit_ms = {'before': (time.monotonic() - binding_started) * 1000}
assert binding == expected
entry = str(args.runtime / 'scripts/rea.mjs')
help_result = subprocess.run([node, entry, 'inspect-managed-members', '--help'], check=True, capture_output=True, text=True)
(args.output / 'help.txt').write_text(help_result.stdout)
with (args.output / 'parent.json').open('w') as file:
subprocess.run([node, entry, 'inspect-managed-members', str(args.target), '--type', 'Newtonsoft.Json.JsonConvert', '--json'], check=True, stdout=file)
parent = json.loads((args.output / 'parent.json').read_text())
data = parent['normalized_result']
answer = {'parent_evidence_id': parent['evidence_id'], 'artifact_sha256': data['artifact']['sha256'],
'selection': data['selection'], 'source_coverage': data['coverage'],
'total_methods': len(data['methods']), 'total_member_refs': len(data['member_refs']),
'methods': data['methods'][:8], 'member_refs': data['member_refs'][:16],
'limitation': 'MemberRef rows are assembly-wide; methods are selected-type rows. Page exhaustion is not an absence claim.'}
(args.output / 'answer.json').write_text(json.dumps(answer))
binding_finished = time.monotonic()
assert capture_binding() == binding
binding_audit_ms['after'] = (time.monotonic() - binding_finished) * 1000
(args.output / 'runtime-binding.json').write_text(json.dumps({'runtime': str(args.runtime), 'runtime_binding': binding, 'runtime_binding_verified': True, 'runtime_binding_audit_ms': binding_audit_ms}))
print(json.dumps(answer))
@@ -1,18 +0,0 @@
import json,hashlib,argparse
from pathlib import Path
import dnfile
parser=argparse.ArgumentParser();parser.add_argument('root',type=Path);root=parser.parse_args().root;path=root/'Newtonsoft.Json.dll'
pe=dnfile.dnPE(str(path));assert pe.net is not None
counts={name:len(getattr(pe.net.mdtables,name).rows) if getattr(pe.net.mdtables,name,None) else 0 for name in ['TypeDef','MethodDef','MemberRef','Field','Property','Event','Param','StandAloneSig','TypeSpec','ExportedType']}
selected=[]
for index,row in enumerate(pe.net.mdtables.TypeDef.rows,1):
name=(str(row.TypeNamespace)+'.' if str(row.TypeNamespace) else '')+str(row.TypeName)
if name!='Newtonsoft.Json.JsonConvert':continue
selected.append({'token':f'0x{0x02000000|index:08x}','name':name,'methods':[{'token':f'0x{0x06000000|m.row_index:08x}','name':str(m.row.Name),'rva':m.row.Rva,'signature_hex':m.row.Signature.value.hex()} for m in row.MethodList]})
assert len(selected)==1
refs=[{'token':f'0x{0x0a000000|i:08x}','name':str(row.Name),'signature_hex':row.Signature.value.hex()} for i,row in enumerate(pe.net.mdtables.MemberRef.rows[:16],1)]
assembly=pe.net.mdtables.Assembly.rows[0]
oracle={'decoder':'dnfile==0.18.0 (independent Python PE/CLI parser)','assembly_sha256':hashlib.sha256(path.read_bytes()).hexdigest(),'assembly_name':str(assembly.Name),'assembly_version':'.'.join(str(getattr(assembly,k)) for k in ['MajorVersion','MinorVersion','BuildNumber','RevisionNumber']),'row_counts':counts,'selected_types':selected,'member_refs_first_16':refs,'target_execution':False}
(root/'oracle.json').write_text(json.dumps(oracle,indent=2)+'\n')
print(json.dumps({'assembly':oracle['assembly_name'],'version':oracle['assembly_version'],'row_counts':counts,'selected_type':selected[0]['name'],'selected_methods':len(selected[0]['methods']),'target_execution':False}))
pe.close()
@@ -1,106 +0,0 @@
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
import { readFile, readdir, realpath } from "node:fs/promises";
import { join, relative } from "node:path";
import { fileURLToPath } from "node:url";
const sha = (value) => createHash("sha256").update(value).digest("hex");
/** Bind a trial to its source checkout and compiled runtime, including deferred assets. */
export const captureRuntimeBinding = async (runtime, kind = "cli") => {
assert.ok(kind === "cli" || kind === "mcp", "Unknown runtime entry");
const root = await realpath(runtime);
const git = (args) =>
execFileSync("git", args, { cwd: root, maxBuffer: 16 * 1024 * 1024 });
const gitRevision = git(["rev-parse", "HEAD"]).toString().trim();
const sourcePaths = ["src", "scripts", "package.json", "package-lock.json"];
const sourceDiff = git([
"diff",
"--no-ext-diff",
"--no-textconv",
"--binary",
"HEAD",
"--",
...sourcePaths,
]);
const untracked = git([
"ls-files",
"--others",
"--exclude-standard",
"-z",
"--",
...sourcePaths,
])
.toString()
.split("\0")
.filter(Boolean)
.sort();
const untrackedEntries = [];
for (const name of untracked)
untrackedEntries.push([name, sha(await readFile(join(root, name)))]);
const files = ["scripts/rea.mjs", "package.json", "package-lock.json"];
const visit = async (directory) => {
const entries = await readdir(directory, { withFileTypes: true });
for (const entry of entries.sort((a, b) =>
a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
)) {
const path = join(directory, entry.name);
assert.ok(!entry.isSymbolicLink(), `Unbound compiled CLI link: ${path}`);
if (entry.isDirectory()) await visit(path);
else if (entry.isFile() && /\.(?:[cm]?js|json)$/.test(entry.name))
files.push(relative(root, path));
}
};
// MCP materializes additional workers and reads build-owned JSON after
// discovery. Bind every compiled JS/JSON asset, rather than only its entry.
await visit(join(root, kind === "mcp" ? "dist" : "dist/cli-runtime"));
assert.ok(
files.includes(
kind === "mcp"
? join("dist", "mcp", "main.js")
: join("dist", "cli-runtime", "cli.js"),
),
"Missing compiled runtime entry",
);
const hash = createHash("sha256");
let bytes = 0;
for (const name of files.sort()) {
const value = await readFile(join(root, name));
bytes += value.length;
hash.update(JSON.stringify([name, value.length, sha(value)]) + "\n");
}
return {
binding_version: kind === "mcp" ? 2 : 1,
root,
node: process.version,
git_revision: gitRevision,
source_diff_sha256: sha(sourceDiff),
untracked_source_sha256: sha(JSON.stringify(untrackedEntries)),
...(kind === "mcp"
? {
compiled_runtime_sha256: hash.digest("hex"),
compiled_runtime_files: files.length,
compiled_runtime_bytes: bytes,
}
: {
compiled_cli_sha256: hash.digest("hex"),
compiled_cli_files: files.length,
compiled_cli_bytes: bytes,
}),
};
};
export const assertRuntimeBinding = (actual, expected, mode) => {
assert.ok(expected, `Missing expected runtime binding for ${mode}`);
assert.deepEqual(actual, expected, `Runtime binding differs for ${mode}`);
};
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
console.log(
JSON.stringify(
await captureRuntimeBinding(process.argv[2], process.argv[3]),
null,
2,
),
);
}
@@ -1,72 +0,0 @@
{
"status": "passed",
"scope": "Single completed SDK regression after shared JSON reader/admission changes; not a new paired source-to-bundle performance comparison. Existing semantic-node cap one answers static correspondence only; no equal semantic-graph coverage claim. Corpus/reference preparation performed earlier and excluded.",
"artifact": {
"name": "Redux Toolkit 2.2.7",
"tarball_sha256": "f2517c466b50424b661a27e48039f15db7783e5822f46b5c7d39f20104ac67a9"
},
"runtime_parent_revision": "4fab2dfda16236dc407aa179920780d6a970dda3",
"production_manifest": "managed-cli.json#measurement_inputs.production_sources_sha256",
"input_schema_profile": "default compact",
"output_schema_profile": "full",
"tokens_estimated": {
"input_catalog_once": 49054,
"arguments": 307,
"text_replies": 7017,
"whole_task": 56378
},
"token_scope": "tiktoken==0.12.0/cl100k_base; one name/description/input-schema catalog plus actual arguments and MCP text replies; excludes output schemas, framing, model prompts/reasoning and actual model usage. Not actual native-agent token usage.",
"actual_model_usage": null,
"wall_seconds": 21.38,
"cpu_seconds": 21.022683,
"peak_process_family_bytes_including_page_cache": 974041088,
"calls": [
{
"name": "analyze_javascript_application",
"elapsed_ms": 9837.265845000002,
"is_error": false,
"reply_bytes": 25941
},
{
"name": "compare_source_to_bundle",
"elapsed_ms": 256.3669919999993,
"is_error": false,
"reply_bytes": 13199
},
{
"name": "inspect_analysis_view",
"elapsed_ms": 3012.057904000001,
"is_error": false,
"reply_bytes": 19393
},
{
"name": "export_evidence_bundle",
"elapsed_ms": 148.9011160000009,
"is_error": false,
"reply_bytes": 364
}
],
"independent_proof": {
"status": "passed",
"method": "Independent Python JSON/SHA-256/POSIX path and basename joins; no REA parser or matcher.",
"source_files": 180,
"embedded_nodes": 99,
"candidate_relations": 210,
"strong_location_rows": 33,
"limitation": "Static multiple-location projections; no semantic-equivalence or removal claim."
},
"resource_events": {
"memory.events": {
"low": 0,
"high": 0,
"max": 0,
"oom": 0,
"oom_kill": 0,
"oom_group_kill": 0
},
"pids.events": {
"max": 0
}
},
"cleanup_verified": true
}
@@ -1,266 +0,0 @@
import {
captureRuntimeBinding,
assertRuntimeBinding,
} from "./managed-cli-runtime.mjs";
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { createHash } from "node:crypto";
import { readFile, writeFile, mkdir } from "node:fs/promises";
import { join, resolve } from "node:path";
const execute = promisify(execFile);
const [runtime, root, output, mode, cache = "cold"] = process.argv.slice(2);
for (const value of [runtime, root, output])
assert.equal(resolve(value), value);
assert.ok(["complete", "selected"].includes(mode));
assert.ok(["cold", "warm"].includes(cache));
await mkdir(output);
await mkdir(join(output, "tmp"));
const oracle = JSON.parse(await readFile(join(root, "oracle.json"), "utf8"));
const target = join(root, "Newtonsoft.Json.dll");
const hash = (value) => createHash("sha256").update(value).digest("hex");
assert.equal(hash(await readFile(target)), oracle.assembly_sha256);
const stable = (value) =>
Array.isArray(value)
? value.map(stable)
: value !== null && typeof value === "object"
? Object.fromEntries(
Object.keys(value)
.sort()
.map((key) => [key, stable(value[key])]),
)
: value;
const report = {
status: "incomplete",
runtime,
mode,
cache,
node: process.version,
question:
"Report identity/selection/source coverage for JsonConvert, its total method count and the first eight complete method records, plus the first sixteen assembly-wide MemberRef records and continuation coverage.",
recipe:
mode === "selected"
? "Use --detail summary --evidence-bundle FILE; pass the exact parent_evidence_id to inspect-analysis-view --evidence-bundle FILE with source kind retained-evidence and view {kind:managed-members,collection:methods|member_refs,offset:0,limit:8|16}."
: "Use inspect-managed-members --type Newtonsoft.Json.JsonConvert; the completed full Evidence supplies the requested rows and remains accessible.",
calls: [],
cleanup_verified: false,
};
const began = performance.now();
const environment = {
PATH: process.env.PATH,
HOME: output,
TMPDIR: join(output, "tmp"),
LANG: "C.UTF-8",
REA_LOG_LEVEL: "silent",
UV_THREADPOOL_SIZE: "1",
NODE_OPTIONS: "--max-old-space-size=2048",
...(cache === "cold"
? { NODE_DISABLE_COMPILE_CACHE: "1" }
: { NODE_COMPILE_CACHE: join(root, `compile-cache-${mode}`) }),
};
const call = async (label, args, json = true) => {
const started = performance.now();
const result = await execute(
process.execPath,
[join(runtime, "scripts/rea.mjs"), ...args],
{
cwd: output,
env: environment,
maxBuffer: 16 * 1024 * 1024,
timeout: 120000,
},
);
await writeFile(join(output, label + ".stdout"), result.stdout);
await writeFile(join(output, label + ".stderr"), result.stderr);
report.calls.push({
label,
arguments: ["rea", ...args],
elapsed_ms: performance.now() - started,
stdout_bytes: Buffer.byteLength(result.stdout),
stdout_file: label + ".stdout",
});
return json ? JSON.parse(result.stdout) : result.stdout;
};
let parent, summary, methods, refs;
try {
const inputs = JSON.parse(
await readFile(join(root, "measurement-inputs.json"), "utf8"),
);
const expected = inputs.expected_runtime_bindings?.[mode];
assert.ok(expected, `Missing expected runtime for ${mode}`);
assert.equal(
expected.git_revision,
mode === "complete"
? inputs.baseline_revision
: inputs.feature_parent_revision,
);
assert.notEqual(
inputs.expected_runtime_bindings.complete.root,
inputs.expected_runtime_bindings.selected.root,
);
assert.notEqual(
inputs.expected_runtime_bindings.complete.compiled_cli_sha256,
inputs.expected_runtime_bindings.selected.compiled_cli_sha256,
);
const bindingStarted = performance.now();
report.runtime_binding = await captureRuntimeBinding(runtime);
report.runtime_binding_audit_ms = {
before: performance.now() - bindingStarted,
};
assertRuntimeBinding(report.runtime_binding, expected, mode);
await call("help-members", ["inspect-managed-members", "--help"], false);
if (mode === "complete") {
parent = await call("complete", [
"inspect-managed-members",
target,
"--type",
"Newtonsoft.Json.JsonConvert",
"--json",
]);
methods = parent.normalized_result.methods.slice(0, 8);
refs = parent.normalized_result.member_refs.slice(0, 16);
} else {
await call("help-view", ["inspect-analysis-view", "--help"], false);
const bundlePath = join(output, "parent.json");
summary = await call("summary", [
"inspect-managed-members",
target,
"--type",
"Newtonsoft.Json.JsonConvert",
"--detail",
"summary",
"--evidence-bundle",
bundlePath,
"--json",
]);
const id = summary.normalized_result.parent_evidence_id;
assert.ok(id);
const bundle = JSON.parse(await readFile(bundlePath, "utf8"));
assert.equal(bundle.records.length, 1);
parent = bundle.records[0];
assert.equal(parent.evidence_id, id);
for (const [collection, limit] of [
["methods", 8],
["member_refs", 16],
]) {
const value = await call(collection, [
"inspect-analysis-view",
JSON.stringify({
source: { kind: "retained-evidence", evidence_id: id },
view: { kind: "managed-members", collection, offset: 0, limit },
}),
"--evidence-bundle",
bundlePath,
"--json",
]);
const page = value.normalized_result;
assert.equal(page.parent_evidence_id, id);
assert.deepEqual(page.source_coverage, parent.normalized_result.coverage);
assert.deepEqual(
page.page.rows,
parent.normalized_result[collection].slice(0, limit),
);
assert.equal(
page.coverage.total,
parent.normalized_result[collection].length,
);
assert.equal(page.coverage.next_offset, limit);
assert.equal(page.coverage.exhausted, false);
if (collection === "methods") methods = page.page.rows;
else refs = page.page.rows;
}
}
const data = parent.normalized_result;
assert.equal(data.artifact.sha256, oracle.assembly_sha256);
assert.equal(data.methods.length, 72);
assert.equal(data.member_refs.length, 2149);
assert.equal(data.types.length, 1);
assert.equal(data.types[0].token, oracle.selected_types[0].token);
for (let i = 0; i < data.methods.length; i++) {
const row = data.methods[i],
wanted = oracle.selected_types[0].methods[i];
assert.equal(row.token, wanted.token);
assert.equal(row.name, wanted.name);
assert.equal(row.rva, wanted.rva);
assert.equal(
row.signature.raw_sha256,
hash(Buffer.from(wanted.signature_hex, "hex")),
);
}
for (let i = 0; i < refs.length; i++) {
const row = refs[i],
wanted = oracle.member_refs_first_16[i];
assert.equal(row.token, wanted.token);
assert.equal(row.name, wanted.name);
assert.equal(
row.signature.raw_sha256,
hash(Buffer.from(wanted.signature_hex, "hex")),
);
}
assert.equal(methods.length, 8);
assert.equal(refs.length, 16);
if (summary) {
assert.equal(summary.normalized_result.summary.reported_counts.methods, 72);
assert.equal(
summary.normalized_result.summary.reported_counts.member_refs,
2149,
);
assert.deepEqual(summary.normalized_result.summary.coverage, data.coverage);
assert.deepEqual(
summary.normalized_result.summary.selection,
data.selection,
);
}
const answer = {
parent_evidence_id: parent.evidence_id,
artifact_sha256: oracle.assembly_sha256,
selection: data.selection,
source_coverage: data.coverage,
total_methods: data.methods.length,
total_member_refs: data.member_refs.length,
methods,
member_refs: refs,
limitation:
"MemberRef rows are assembly-wide; methods are selected-type rows. Page exhaustion is not an absence claim.",
};
report.parent_evidence_id = parent.evidence_id;
report.parent_semantic_sha256 = hash(JSON.stringify(stable(parent)));
report.answer_sha256 = hash(JSON.stringify(stable(answer)));
report.oracle_verified = {
all_selected_method_metadata: 72,
returned_methods: 8,
returned_member_refs: 16,
};
const bindingFinished = performance.now();
assertRuntimeBinding(
await captureRuntimeBinding(runtime),
report.runtime_binding,
mode,
);
report.runtime_binding_audit_ms.after = performance.now() - bindingFinished;
report.runtime_binding_verified = true;
report.status = "passed";
await writeFile(join(output, "parent-evidence.json"), JSON.stringify(parent));
await writeFile(join(output, "answer.json"), JSON.stringify(answer));
} finally {
report.elapsed_ms = performance.now() - began;
report.cleanup_verified = true;
await writeFile(
join(output, "report.json"),
JSON.stringify(report, null, 2) + "\n",
);
}
console.log(
JSON.stringify({
status: report.status,
mode,
cache,
parent: report.parent_evidence_id,
answer: report.answer_sha256,
calls: report.calls.map((c) => ({
label: c.label,
elapsed_ms: c.elapsed_ms,
bytes: c.stdout_bytes,
})),
}),
);
File diff suppressed because it is too large. Load diff
@@ -1,219 +0,0 @@
# Portable managed CLI member views
Issue #5148 / PR #5167. Baseline `main@3dbe314ada9f6dcc994ca4655dc0faf082503847`;
feature integrates the original `7b3ed4d025edd24d062cde0327a53bd311e174fc`.
The measured production-file SHA-256 manifest is in [managed-cli.json](managed-cli.json).
These are working-tree measurements. Each trial verifies both source and the
complete compiled CLI graph before and after execution against its explicitly
selected build. The two roles have distinct checkout roots and compiled digests.
Identity-audit time is included in whole-task costs and recorded separately.
The audit reads compiled files before timing individual CLI calls, warming those
filesystem pages; cold means disabled Node compile cache, not disk-cold startup.
## Question and independent checks
Report identity, selected type, source coverage, the total method count, the first
eight complete method records, and the first sixteen assembly-wide MemberRef
records, including continuation coverage. Keep complete original Evidence available.
The question deliberately does not ask for every method or reference.
Public artifact: [Newtonsoft.Json 13.0.3](https://www.nuget.org/packages/Newtonsoft.Json/13.0.3),
from the NuGet flat-container archive. The 2,441,966-byte archive SHA-256 is
`872fc189e638ab1056555b03aaa38f68bcb54286e221aa646eb1129babf63c77`.
Selected member `lib/net6.0/Newtonsoft.Json.dll`, 712,464 bytes, SHA-256
`22c649f75fce5be7c7ccda8880473b634ef69ecf33f5d1ab8ad892caf47d5a07`.
Package version 13.0.3 is distinct from metadata assembly version 13.0.0.0.
No library code is executed; REA parses PE/CLI statically.
An independent Python oracle uses `dnfile==0.18.0` and `pefile==2024.8.26`.
It finds 494 TypeDefs, 4,208 MethodDefs and 2,149 MemberRefs. The selected
`Newtonsoft.Json.JsonConvert` has 72 methods. Every trial checks all 72 method
tokens, names, RVAs and original signature hashes, plus the sixteen returned
MemberRef tokens, names and signature hashes, against this oracle. This does
not independently establish complete CIL semantics or opcode correctness.
The full parent Evidence and requested answers are identical across all twelve
trials; returned rows equal the exact parent slices. Selection and source
coverage remain distinct from page exhaustion. MemberRefs remain assembly-wide.
## Completed-task cost
Linux x64, Node 24.18.0/npm 11.16.0. Three alternating pairs per cache condition,
serial, one CPU; 4 GiB hard / 3 GiB early-stop family guard, no swap, 192-task
ceiling. Every task includes actual selected-command help, all CLI calls,
provider parsing, bundle publication/authentication, local answer extraction
and oracle comparisons, plus both runtime-identity audits. Build, download, oracle preparation and warm priming
are excluded. OS page cache is uncontrolled. No build/test overlapped timing.
Complete mode starts two CLI processes (help plus analysis). Selected mode starts
five (two help commands, summary/publication, methods page, MemberRef page).
Cold disables the Node compile cache; warm primes the exact workflow once per
runtime/mode, then still starts fresh processes with isolated HOME each trial.
All trials complete with verified process cleanup and no OOM/resource-stop event.
| Whole task, median | Complete | Summary + two pages |
| ----------------------------------------------- | -------: | ------------------: |
| Estimated context tokens, both cache conditions | 451,373 | 13,078 |
| Cold wall seconds | 9.969 | 14.801 |
| Cold family CPU seconds | 9.743 | 14.555 |
| Cold kernel family peak MiB | 393.281 | 400.098 |
| Warm wall seconds | 10.339 | 24.141 |
| Warm family CPU seconds | 10.097 | 23.842 |
| Warm kernel family peak MiB | 401.809 | 407.164 |
Context estimates include the question, supplied command recipe, all logical
argv, actual help and stdout. Tokenizer `tiktoken==0.12.0`, `cl100k_base`.
The reduction is 97.1% for this fixed task. There is no model invocation;
actual billed/native-agent usage, reasoning and transport framing are unmeasured.
The memory counter is the kernel cgroup process-family peak including file
cache, not sampled RSS. Ranges and individual receipts are in the JSON.
This is an opt-in context/portability improvement with extra latency and CPU,
not a speed or memory optimization. Complete provider parsing and full authentication of file-supplied bundles
remain. Fresh managed Evidence is sealed at its producing factory and reused
without another payload clone, including low-headroom error projection. Complete output remains the default and is preferable
for small outputs or questions requiring every row. No cache or parser cap
was introduced.
## Existing native code composition
The unchanged baseline can already redirect full stdout to a file and select
rows in code. [The measured control](managed-cli-local-control.py) returns the
identical answer and retains the complete original Evidence. One cold run:
11.249 s wall, 11.000 s family CPU,
375.3 MiB kernel peak. Projection source,
original help and returned answer together estimate 10,555 tokens; the shared
question and launcher argv are excluded. This single-run control does not
establish stable performance or actual native-agent usage. It does demonstrate
that raw CLI reply savings are not universal savings over code composition.
The additive interface supplies authenticated reusable projections through the
same application/domain owner as MCP, without caller-maintained JSON slicing.
## Correctness and resource boundaries
The public CLI regression pages all eight managed collections after process
exit, target deletion and a working-directory change. Complete parent identity,
original row order, signatures, source/page coverage and unknowns survive.
Existing outputs are not replaced; failed publication retains completed facts.
The Evidence factory parses mutable caller data into independent trees, computes
identity, then seals its fresh owned snapshot with active ancestry/iterators.
It does not retain a set/array of every completed object. Existing sealed roots
can be shared; mutable input aliases are cloned independently. General graph
sealing keeps its separate cycle, sharing, hidden-field and atomic-failure rules.
Managed and derived-view factories seal their own completed Evidence, so CLI
error projection can borrow it without reauthentication or another payload copy.
The view owner retains strict JSON normalization for optional-field contracts;
the CLI no longer copies its already validated completed projection again.
Shared heap admission traverses by depth rather than queueing every report row
or allocating an entries array. It reserves parsed working copies and actual
escaping expansion before cloning/hashing, reporting UTF-16 storage explicitly.
Canonical bundle equality streams existing ordinary JSON encoder parts, preserving
native key order, omission, negative zero and Unicode semantics without two
whole-document strings. All production bundle readers explicitly pass their
operation: CLI import/export/compare/inspection, retained views, MCP import/
comparison and both native-matching read passes. Errors retain the original
cause, cleanup primary and lease through retry.
One process primitive dispatches queued callbacks and then their OS signal
notifications. The managed producer checks it before accepting complete Evidence.
Publication/read stages use it before their final checks. Inline, file and bundle
CLI views share the existing command finalizer and actual resource scope; the
finalizer checks interruption after close while preserving any primary/cleanup
failure. Existing inline error labels remain intact. Synchronous analysis is
cooperatively cancellable between stages, not preemptible.
Four compiled publication cases (SIGINT/SIGTERM × event/actual POSIX signal)
fail without their checkpoint and pass restored. Eight additional compiled
completion cases cover default complete delivery and inline projection; all fail
with only their new completion checkpoints removed and pass restored. Fixture
hooks match the selected target path or exact parent ID. Two real Newtonsoft
publication runs and four real default/inline completion runs retain the identical
parent or independently checked selected summary, exit 130/143 and preserve
committed receipts where applicable. No target library code is executed.
All 556 focused tests across 36 files pass, including direct
finalizer/cleanup ownership, WASM/Android cancellation and native matching.
All six `npm run check` tasks plus architecture guards, and all six
`npm run docs:check` tasks plus five README tests pass. The expanded domain
project passes 5,617 tests in 287 files with
3 skipped cases; this overlaps the focused gate. Its first
batched shard hit two existing 30-second timeouts in unchanged large synthetic
tests. Both passed isolated and in the fresh shard rerun under the same timeout;
the cause of the batch variation is unverified. No production/timeout change
was made for those cases. An earlier unsplit expansion was manually stopped
before its heap ceiling; that incomplete attempt is not counted as a pass.
Eight producing/operation negative controls also catch graph-sized sealing/
admission scratch and wrong operation attribution, including native reload.
Native built-in instrumentation is confined to the isolated process-global lane.
Initial test-only spy/probe/schema-import issues were corrected before these
passing gates. No actual heap exhaustion is induced. Simulated providers do not
qualify foreign engines; Windows OS signals remain unverified locally.
## Shared-reader real-artifact regression
The existing Redux Toolkit 2.2.7 source/bundle task also completes through the
updated reader with default full output schemas and compact input schemas.
An independent Python SHA-256/path oracle verifies 180 historical files,
99 embedded source nodes and 210 candidate relations (33 strong-location rows,
147 unknown). Original signals and partial source coverage remain visible.
This uses the existing explicit semantic-node cap of one for static correspondence;
it does not establish equivalent semantic-graph coverage or behavioral equivalence.
One completed task: 21.380 s wall,
21.023 s family CPU,
928.9 MiB kernel family peak,
56,378 estimated context tokens (one input catalog,
all arguments and MCP text replies; excludes output schemas, framing and model
reasoning). Corpus/reference preparation was performed earlier and excluded.
This is a regression verification with resource observation, not a new paired
performance comparison. Actual model usage remains unmeasured. Details:
[managed-cli-shared-read.json](managed-cli-shared-read.json).
## Reproduction
Extract the selected DLL to a private lab as `Newtonsoft.Json.dll`. Create an
`acquisition.json` containing the URL, hashes, sizes and selected ZIP member
shown above; never commit the DLL. Prepare the independent oracle:
```bash
uv run --no-project --with dnfile==0.18.0 --with pefile==2024.8.26 python \
experiments/analysis-efficiency/managed-cli-oracle.py /absolute/lab
```
Build baseline and feature separately with the pinned toolchain. Run each under
the shared resource guard, alternating complete/selected, selected/complete,
complete/selected for cold and warm. Use a new output directory for every run:
```bash
python3 experiments/analysis-efficiency/resource-run.py --observe-payload-exit -- \
node experiments/analysis-efficiency/managed-cli-task.mjs \
/absolute/runtime /absolute/lab /absolute/lab/pair-cold-1-selected selected cold
```
Save each guard output to the adjacent `pair-*.log`. Warm mode uses the lab's
mode-specific compile-cache directory; prime each identical workflow once first.
Supply a `measurement-inputs.json` with exact revisions and production hashes,
plus `expected_runtime_bindings.complete` and `.selected`. Capture each with
`node experiments/analysis-efficiency/managed-cli-runtime.mjs /absolute/runtime`
after building. Bind complete to the clean baseline revision, selected to the
feature parent revision plus tracked/untracked source digests, with different
roots and compiled-graph digests. The collector rejects a wrong build before any
CLI call, rechecks it afterward, and aggregation independently enforces identity.
A real wrong-runtime attempt is rejected before analysis. A copied trial with a
tampered revision fails aggregation; removing only that consumer guard accepts it.
The twelve-trial aggregation reproduces without private pilots. The native
control also requires `--measurement-inputs /absolute/lab/measurement-inputs.json`
and performs the same before/after identity checks.
Then aggregate with:
```bash
uv run --no-project --with tiktoken==0.12.0 python \
experiments/analysis-efficiency/managed-cli-costs.py /absolute/lab \
--output /absolute/results.json
```
The original issue reporter's executable, foreign operating systems and actual
model sessions remain unverified. Setup pilots and earlier unbound cohorts, including one failed setup run
while compiled artifacts were absent after a typecheck failure, are excluded.
@@ -226,8 +226,3 @@ keeps default valid calls and typed refusals while checking full producer
constraints separately. Five isolated suites passed 14 tests across call paths, constraints separately. Five isolated suites passed 14 tests across call paths,
ADB start, browser scenarios, source selection and managed analysis. The first ADB start, browser scenarios, source selection and managed analysis. The first
browser attempt failed and remains recorded; no performance cohort changed. browser attempt failed and remains recorded; no performance cohort changed.
The subsequent [startup audit](mcp-startup.md) extends this harness with
source/bundle bindings and a post-materialization directory check. Its new
measurements are a separate cohort; reproduce this report at its recorded
revision when comparing the original timing and model-session receipts.
@@ -7,10 +7,6 @@ import { parseArgs } from "node:util";
import { Client } from "@modelcontextprotocol/client"; import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"; import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { Ajv2020 } from "ajv/dist/2020.js"; import { Ajv2020 } from "ajv/dist/2020.js";
import {
captureRuntimeBinding,
assertRuntimeBinding,
} from "./managed-cli-runtime.mjs";
const { values } = parseArgs({ const { values } = parseArgs({
options: { options: {
@@ -19,68 +15,26 @@ const { values } = parseArgs({
oracle: { type: "string" }, oracle: { type: "string" },
output: { type: "string" }, output: { type: "string" },
profile: { type: "string", default: "default" }, profile: { type: "string", default: "default" },
phase: { type: "string", default: "analysis" },
cache: { type: "string", default: "cold" },
"cache-root": { type: "string" },
bindings: { type: "string" },
role: { type: "string" },
}, },
}); });
assert.ok(["analysis", "discovery"].includes(values.phase)); for (const key of ["runtime", "artifact", "oracle", "output"])
assert.ok(["cold", "warm"].includes(values.cache));
for (const key of values.phase === "analysis"
? ["runtime", "artifact", "oracle", "output"]
: ["runtime", "output"])
assert.ok( assert.ok(
values[key] && resolve(values[key]) === values[key], values[key] && resolve(values[key]) === values[key],
`${key} must be absolute`, `${key} must be absolute`,
); );
assert.ok(["default", "full", "compact"].includes(values.profile)); assert.ok(["default", "full", "compact"].includes(values.profile));
if (values.cache === "warm")
assert.ok(
values["cache-root"] &&
resolve(values["cache-root"]) === values["cache-root"],
);
await mkdir(values.output, { recursive: false }); await mkdir(values.output, { recursive: false });
const hash = (value) => createHash("sha256").update(value).digest("hex"); const hash = (value) => createHash("sha256").update(value).digest("hex");
let oracle; const oracle = JSON.parse(await readFile(values.oracle, "utf8"));
let artifactSha; const artifactSha = hash(await readFile(values.artifact));
if (values.phase === "analysis") { assert.equal(artifactSha, oracle.artifact_sha256);
oracle = JSON.parse(await readFile(values.oracle, "utf8"));
artifactSha = hash(await readFile(values.artifact));
assert.equal(artifactSha, oracle.artifact_sha256);
}
let expectedBinding;
let bindingBefore;
let auditBeforeMs;
let measurementInputsSha256;
const producerSources = {};
if (values.bindings !== undefined) {
assert.ok(["baseline", "candidate"].includes(values.role));
const inputBytes = await readFile(values.bindings);
measurementInputsSha256 = hash(inputBytes);
const inputs = JSON.parse(inputBytes.toString("utf8"));
for (const name of ["mcp-input-catalog.mjs", "managed-cli-runtime.mjs"]) {
producerSources[name] = hash(
await readFile(new URL(name, import.meta.url)),
);
assert.equal(producerSources[name], inputs.harness_sources_sha256[name]);
}
expectedBinding = inputs.runtime_bindings[values.role];
const start = performance.now();
bindingBefore = await captureRuntimeBinding(values.runtime, "mcp");
auditBeforeMs = performance.now() - start;
assertRuntimeBinding(bindingBefore, expectedBinding, values.role);
}
const environment = { const environment = {
PATH: process.env.PATH, PATH: process.env.PATH,
HOME: values.output, HOME: values.output,
REA_LOG_LEVEL: "silent", REA_LOG_LEVEL: "silent",
UV_THREADPOOL_SIZE: "1", UV_THREADPOOL_SIZE: "1",
NODE_OPTIONS: "--max-old-space-size=2048", NODE_OPTIONS: "--max-old-space-size=2048",
...(values.cache === "cold" NODE_DISABLE_COMPILE_CACHE: "1",
? { NODE_DISABLE_COMPILE_CACHE: "1" }
: { NODE_COMPILE_CACHE: values["cache-root"] }),
...(values.profile === "default" ...(values.profile === "default"
? {} ? {}
: { REA_MCP_INPUT_SCHEMA_PROFILE: values.profile }), : { REA_MCP_INPUT_SCHEMA_PROFILE: values.profile }),
@@ -92,29 +46,14 @@ const transport = new StdioClientTransport({
env: environment, env: environment,
stderr: "pipe", stderr: "pipe",
}); });
const protocolRequests = {};
const send = transport.send.bind(transport);
transport.send = (...args) => {
const method = args[0].method;
if (typeof method === "string")
protocolRequests[method] = (protocolRequests[method] ?? 0) + 1;
return send(...args);
};
const client = new Client({ name: "rea-catalog-task", version: "1" }); const client = new Client({ name: "rea-catalog-task", version: "1" });
const report = { const report = {
profile: values.profile, profile: values.profile,
phase: values.phase,
cache: values.cache,
cache_root: values["cache-root"] ?? null,
role: values.role,
measurement_inputs_sha256: measurementInputsSha256,
producer_sources_sha256: producerSources,
artifact_sha256: artifactSha, artifact_sha256: artifactSha,
calls: [], calls: [],
checks: [], checks: [],
status: "incomplete", status: "incomplete",
cleanup_verified: false, cleanup_verified: false,
protocol_requests: protocolRequests,
}; };
const transcript = []; const transcript = [];
let stderr = ""; let stderr = "";
@@ -124,15 +63,6 @@ transport.stderr?.on("data", (value) => {
const began = performance.now(); const began = performance.now();
try { try {
await client.connect(transport); await client.connect(transport);
report.protocol_era = client.getProtocolEra();
await writeFile(
join(values.output, "server.json"),
JSON.stringify({
info: client.getServerVersion(),
capabilities: client.getServerCapabilities(),
instructions: client.getInstructions(),
}),
);
const catalog = await client.listTools(); const catalog = await client.listTools();
assert.equal(catalog.nextCursor, undefined); assert.equal(catalog.nextCursor, undefined);
const definitions = new Map(catalog.tools.map((tool) => [tool.name, tool])); const definitions = new Map(catalog.tools.map((tool) => [tool.name, tool]));
@@ -143,145 +73,97 @@ try {
ajv.validateSchema(tool.inputSchema), ajv.validateSchema(tool.inputSchema),
`Invalid schema: ${tool.name}`, `Invalid schema: ${tool.name}`,
); );
{ await writeFile(join(values.output, "catalog.json"), JSON.stringify(catalog));
const encoded = JSON.stringify(catalog);
await writeFile(join(values.output, "catalog.json"), encoded);
report.catalog_sha256 = hash(encoded);
}
report.tool_names = [...definitions.keys()]; report.tool_names = [...definitions.keys()];
if (values.phase === "discovery") { const call = async (name, args) => {
const prompts = await client.listPrompts(); const definition = definitions.get(name);
assert.equal(prompts.nextCursor, undefined); assert.ok(definition, `Missing named tool ${name}`);
await writeFile( const start = performance.now();
join(values.output, "prompts.json"), const reply = await client.callTool(
JSON.stringify(prompts), { name, arguments: args },
{ toolDefinition: definition },
); );
report.prompts_sha256 = hash(JSON.stringify(prompts)); report.calls.push({
report.answer = { name,
tools: report.tool_names, elapsed_ms: performance.now() - start,
prompts: prompts.prompts.map((prompt) => prompt.name), is_error: reply.isError === true,
}; });
report.status = "passed"; transcript.push({ name, arguments: args, reply });
} else { return reply;
const call = async (name, args) => { };
const definition = definitions.get(name); const input = {
assert.ok(definition, `Missing named tool ${name}`); path: values.artifact,
const start = performance.now(); view: {
const reply = await client.callTool( collection: "packages",
{ name, arguments: args }, names: ["serde"],
{ toolDefinition: definition }, fields: ["name", "version", "kind"],
); },
report.calls.push({ };
name, const first = await call("inspect_rust_binary", input);
elapsed_ms: performance.now() - start, assert.notEqual(first.isError, true);
is_error: reply.isError === true, const selected = first.structuredContent;
}); assert.ok(selected?.normalized_result?.page);
transcript.push({ name, arguments: args, reply }); assert.deepEqual(selected.normalized_result.page.rows, oracle.serde_rows);
return reply; assert.equal(selected.subject.digest.sha256, artifactSha);
}; const parentId = selected.normalized_result.parent_evidence_id;
const input = { for (let index = 0; index < 2; index++) {
path: values.artifact, const reply = await call("inspect_analysis_view", {
source: { kind: "retained-evidence", evidence_id: parentId },
view: { view: {
kind: "rust",
collection: "packages", collection: "packages",
names: ["serde"], names: ["serde"],
fields: ["name", "version", "kind"], fields: ["name", "version", "kind"],
}, },
};
const first = await call("inspect_rust_binary", input);
assert.notEqual(first.isError, true);
const selected = first.structuredContent;
assert.ok(selected?.normalized_result?.page);
assert.deepEqual(selected.normalized_result.page.rows, oracle.serde_rows);
assert.equal(selected.subject.digest.sha256, artifactSha);
const parentId = selected.normalized_result.parent_evidence_id;
for (let index = 0; index < 2; index++) {
const reply = await call("inspect_analysis_view", {
source: { kind: "retained-evidence", evidence_id: parentId },
view: {
kind: "rust",
collection: "packages",
names: ["serde"],
fields: ["name", "version", "kind"],
},
});
assert.notEqual(reply.isError, true);
assert.deepEqual(
reply.structuredContent.normalized_result.page.rows,
oracle.serde_rows,
);
assert.equal(
reply.structuredContent.normalized_result.parent_evidence_id,
parentId,
);
}
const malformed = await call("inspect_rust_binary", {
path: values.artifact,
view: { collection: "packages", offset: -1 },
}); });
assert.equal(malformed.isError, true); assert.notEqual(reply.isError, true);
report.checks.push(
"Independent package rows and digest; repeated retained identity; canonical negative-offset refusal",
);
const exported = await call("export_evidence_bundle", {
path: join(values.output, "evidence.json"),
});
assert.notEqual(exported.isError, true);
const bundle = JSON.parse(
await readFile(join(values.output, "evidence.json"), "utf8"),
);
const parent = bundle.records.find(
(record) => record.evidence_id === parentId,
);
assert.ok(parent, "Complete original Evidence is retained");
assert.equal(parent.subject.digest.sha256, artifactSha);
assert.deepEqual( assert.deepEqual(
parent.normalized_result.auditable_metadata.packages, reply.structuredContent.normalized_result.page.rows,
oracle.packages, oracle.serde_rows,
); );
assert.deepEqual( assert.equal(
parent.normalized_result.auditable_metadata.dependency_edges, reply.structuredContent.normalized_result.parent_evidence_id,
oracle.edges, parentId,
); );
report.parent_evidence_id = parentId;
report.parent_semantic_sha256 = hash(JSON.stringify(parent));
report.answer = oracle.serde_rows;
assert.deepEqual(
await client.listTools(),
catalog,
"Advertisement changed after canonical materialization",
);
report.checks.push(
"Catalog remains identical after canonical materialization",
);
report.status = "passed";
} }
const malformed = await call("inspect_rust_binary", {
path: values.artifact,
view: { collection: "packages", offset: -1 },
});
assert.equal(malformed.isError, true);
report.checks.push(
"Independent package rows and digest; repeated retained identity; canonical negative-offset refusal",
);
const exported = await call("export_evidence_bundle", {
path: join(values.output, "evidence.json"),
});
assert.notEqual(exported.isError, true);
const bundle = JSON.parse(
await readFile(join(values.output, "evidence.json"), "utf8"),
);
const parent = bundle.records.find(
(record) => record.evidence_id === parentId,
);
assert.ok(parent, "Complete original Evidence is retained");
assert.equal(parent.subject.digest.sha256, artifactSha);
assert.deepEqual(
parent.normalized_result.auditable_metadata.packages,
oracle.packages,
);
assert.deepEqual(
parent.normalized_result.auditable_metadata.dependency_edges,
oracle.edges,
);
report.parent_evidence_id = parentId;
report.parent_semantic_sha256 = hash(JSON.stringify(parent));
report.answer = oracle.serde_rows;
report.status = "passed";
} finally { } finally {
await client.close(); await client.close();
await transport.close(); await transport.close();
report.cleanup_verified = transport.pid === null; report.cleanup_verified = transport.pid === null;
assert.ok(report.cleanup_verified); assert.ok(report.cleanup_verified);
report.elapsed_ms = performance.now() - began; report.elapsed_ms = performance.now() - began;
if (expectedBinding !== undefined) {
const start = performance.now();
const after = await captureRuntimeBinding(values.runtime, "mcp");
report.runtime_audit_ms = {
before: auditBeforeMs,
after: performance.now() - start,
};
assertRuntimeBinding(after, expectedBinding, values.role);
assert.deepEqual(after, bindingBefore);
assert.equal(
hash(await readFile(values.bindings)),
measurementInputsSha256,
);
for (const [name, digest] of Object.entries(producerSources))
assert.equal(
hash(await readFile(new URL(name, import.meta.url))),
digest,
);
report.runtime_binding = after;
report.runtime_binding_verified = true;
}
await writeFile( await writeFile(
join(values.output, "report.json"), join(values.output, "report.json"),
JSON.stringify(report, null, 2) + "\n", JSON.stringify(report, null, 2) + "\n",
@@ -1,118 +0,0 @@
"""Verify paired SDK startup/analysis receipts and account for the whole task."""
import argparse
import hashlib
import importlib.util
import json
import statistics
from pathlib import Path
import tiktoken
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('root', type=Path)
parser.add_argument('--output', type=Path, required=True)
args = parser.parse_args()
root = args.root
input_bytes = (root / 'measurement-inputs.json').read_bytes()
inputs = json.loads(input_bytes)
expected = inputs['runtime_bindings']
sha = lambda value: hashlib.sha256(value).hexdigest()
compact = lambda value: json.dumps(value, ensure_ascii=False, separators=(',', ':'), allow_nan=False)
encoding = tiktoken.get_encoding('cl100k_base')
count = lambda value: len(encoding.encode(value, disallowed_special=()))
for role in ['baseline', 'candidate']:
assert expected[role]['git_revision'] == inputs[f'{role}_revision']
assert expected[role]['source_diff_sha256'] == sha(b'')
assert expected[role]['untracked_source_sha256'] == sha(b'[]')
assert expected['baseline']['root'] != expected['candidate']['root']
assert expected['baseline']['compiled_runtime_sha256'] != expected['candidate']['compiled_runtime_sha256']
spec = importlib.util.spec_from_file_location('catalog_costs', Path(__file__).with_name('mcp-input-catalog-costs.py'))
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
catalogs = {}
identities = {}
rows = []
for case in inputs['cases']:
directory = root / case['case']
report = json.loads((directory / 'report.json').read_text())
assert report['status'] == 'passed' and report['cleanup_verified']
assert report['runtime_binding_verified']
assert report['measurement_inputs_sha256'] == sha(input_bytes)
for name, producer_digest in report['producer_sources_sha256'].items():
assert producer_digest == inputs['harness_sources_sha256'][name]
assert report['runtime_binding'] == expected[case['role']]
for key in ['role', 'cache', 'profile', 'phase']:
assert report[key] == case[key]
raw = (directory / 'catalog.json').read_bytes()
catalog_digest = sha(raw)
assert catalog_digest == report['catalog_sha256']
profile = case['profile']
if profile not in catalogs:
catalog = json.loads(raw)
definitions = [{key: tool[key] for key in ['name', 'description', 'inputSchema']} for tool in catalog['tools']]
catalogs[profile] = {'sha256': catalog_digest, 'tool_count': len(catalog['tools']),
'input_catalog_tokens': count(compact(definitions)),
'raw_catalog_tokens': count(raw.decode()), 'raw_catalog_bytes': len(raw)}
assert catalog_digest == catalogs[profile]['sha256'], 'Directory differs across roles or phases'
transcript = json.loads((directory / 'transcript.json').read_text())
server = json.loads((directory / 'server.json').read_text())
tokens = {'question_and_recipe': count(inputs['questions'][case['phase']] + '\n' + inputs['recipes'][case['phase']]),
'server_instructions': count(server.get('instructions') or ''),
'input_catalog_once': catalogs[profile]['input_catalog_tokens'],
'arguments': sum(count(compact({'name': call['name'], 'arguments': call['arguments']})) for call in transcript),
'text_replies': sum(count(block['text']) for call in transcript for block in call['reply']['content'] if block['type'] == 'text')}
requests = report['protocol_requests']
assert requests['initialize'] == 1
if case['phase'] == 'analysis':
assert len(transcript) == 5 and requests['tools/call'] == 5 and requests['tools/list'] == 2
assert report['artifact_sha256'] == inputs['artifacts'][case['artifact']]['sha256']
identity = (report['parent_evidence_id'], report['parent_semantic_sha256'], sha(compact(report['answer']).encode()))
assert identities.setdefault(case['artifact'], identity) == identity
assert sum(call['is_error'] for call in report['calls']) == 1
else:
assert not transcript and requests.get('tools/call', 0) == 0
assert requests['tools/list'] == 1 and requests['prompts/list'] == 1
prompts = (directory / 'prompts.json').read_bytes()
assert sha(prompts) == report['prompts_sha256']
assert identities.setdefault('prompts', report['prompts_sha256']) == report['prompts_sha256']
tokens['prompt_directory'] = count(prompts.decode())
tokens['whole_task'] = sum(tokens.values())
resource = module.resource(directory)
for name, guard_digest in resource['guard_sources_sha256'].items():
assert guard_digest == inputs['harness_sources_sha256'][name]
del resource['guard_sources_sha256']
row = {**case, 'runtime_binding_sha256': sha(compact(report['runtime_binding']).encode()), 'runtime_audit_ms': report['runtime_audit_ms'],
'catalog_sha256': catalog_digest, 'parent_evidence_id': report.get('parent_evidence_id'),
'parent_semantic_sha256': report.get('parent_semantic_sha256'),
'answer_sha256': sha(compact(report['answer']).encode()), 'tokens_estimated': tokens,
'raw_catalog_result_tokens_all_lists': catalogs[profile]['raw_catalog_tokens'] * requests['tools/list'],
'raw_catalog_result_bytes_all_lists': catalogs[profile]['raw_catalog_bytes'] * requests['tools/list'],
'actual_model_usage': None, 'protocol_era': report['protocol_era'], 'protocol_requests': requests, 'calls': report['calls'],
**resource,
'receipt_sha256': {name: sha((directory / name).read_bytes()) for name in ['report.json', 'catalog.json', 'server.json', 'transcript.json']}}
assert row['catalog_sha256'] == row['receipt_sha256']['catalog.json'] == catalogs[profile]['sha256']
rows.append(row)
assert len(rows) == 72
summary = []
for phase in ['discovery', 'analysis']:
for cache in ['cold', 'warm']:
for profile in ['default', 'full']:
for artifact in ([None] if phase == 'discovery' else ['ripgrep', 'zellij']):
item = {'phase': phase, 'cache': cache, 'profile': profile, 'artifact': artifact}
for role in ['baseline', 'candidate']:
selected = [row for row in rows if all(row[key] == item[key] for key in ['phase', 'cache', 'profile', 'artifact']) and row['role'] == role]
assert len(selected) == 3
item[role] = {metric: {'median': statistics.median(row[metric] for row in selected),
'min': min(row[metric] for row in selected), 'max': max(row[metric] for row in selected)}
for metric in ['wall_seconds', 'cpu_seconds', 'kernel_family_peak_bytes_including_page_cache']}
item[role]['tokens_estimated'] = [row['tokens_estimated']['whole_task'] for row in selected]
summary.append(item)
result = {'status': 'passed', 'tokenizer': f'tiktoken=={tiktoken.__version__}/cl100k_base; estimates, not model usage',
'aggregation_sources_sha256': {path.name: sha(path.read_bytes()) for path in [Path(__file__), Path(spec.origin)]},
'context_scope': 'Question, explicit recipe, server instructions, one complete name/description/input-schema directory, every tool argument and text reply, plus prompt directory for discovery. This projection assumes the client retains a single unchanged directory. Raw catalog result costs include every observed tools/list result separately, with output schemas and without JSON-RPC envelopes. SDK initialization/framing, reasoning and actual model usage are unmeasured. The analysis task includes retained-read, rejection, export/oracle and post-materialization directory checks.',
'cache_scope': 'Cold disables server Node compile cache. Warm enables a prepopulated role/profile-specific cache after one complete analysis priming run. Fresh server/client processes and HOME each trial. Driver compile cache disabled in both cohorts. Cache hit rate and OS page cache are unmeasured; source/runtime audits warm compiled-file pages before startup.',
'resource_scope': 'Whole SDK driver, resource observer and owned REA process family. Includes before/after source-and-all-compiled-JS/JSON binding, SDK schema checks, extra validation calls, bundle export and independent oracle checking. Kernel cgroup peak includes file cache. Build/download/oracle preparation and cache priming excluded. Retained V8 heap and forced-GC diagnostics were not separately measured.',
'measurement_inputs': inputs, 'catalogs': catalogs, 'summary': summary, 'records': rows,
'warmup_receipts': {name: sha((root / name / 'report.json').read_bytes()) for name in [f'warmup-{profile}-{role}' for profile in ['default', 'full'] for role in ['baseline', 'candidate']]}}
args.output.write_text(json.dumps(result, indent=2) + '\n')
print(json.dumps(summary, indent=2))
File diff suppressed because it is too large. Load diff
@@ -1,61 +0,0 @@
# MCP discovery and complete Rust tasks
Target-free stdio discovery previously constructed the canonical analysis runtime before the client could inspect its directory. The candidate records SDK advertisements at build time for default/explicit compact and explicit full inputs. Its first tool, prompt or completion request materializes canonical validators and handlers on the same SDK server. HTTP, output-schema-off, initial-target and unrecorded source runs retain eager composition.
Pinned baseline: [`main@c465ff5a8`](https://github.com/morluto/rea/commit/c465ff5a814b8e1a386fc547cbcd94df5c35f086). Measured candidate: [`04900abbc`](https://github.com/leeguooooo/rea/commit/04900abbcd3c3438fce4af932ff28400fe744a7d). Both use the same main base and locked dependencies. Before/after source and all compiled JS/JSON bindings identify the exact runtime used in every trial. The input manifest and producer scripts are also hashed and checked.
## Real tasks and independent facts
Official Alpine v3.21 packages supply ripgrep 14.1.1-r0 (3,416,168-byte executable) and Zellij 0.40.0-r0 (17,151,864 bytes). Their download URLs, archive/executable digests and aports revisions are retained in [the measurement data](mcp-startup.json). The independent Python ELF/section/zlib/JSON oracle checks 42 packages and 88 dependency edges for ripgrep, and 422 packages and 1,063 edges for Zellij. Serde versions are 1.0.210 and 1.0.137 respectively; the selected package kind remains explicit unknown (`null`). Verification covers embedded static build metadata; execution and runtime reachability remain unverified.
Each analysis task initializes the actual SDK client, lists and validates the input directory, selects serde name/version/kind, reads the exact retained parent twice, verifies canonical rejection of a negative offset, exports and independently checks the complete package/edge inventory, confirms unchanged advertisements after materialization, then closes. All five tool calls and both directory reads are included in the measured task. Discovery tasks initialize, list tools/prompts, validate the input directory and close without an analysis call.
All 72 paired trials and four priming runs passed. Each artifact has identical answers, parent Evidence IDs and complete semantic hashes across roles, profiles and cache modes. Both profiles retain byte-identical directories across baseline/candidate and before/after canonical materialization. The directory contains 213 tools. Complete Evidence is retained and exported through the existing shared workflow.
## Measured cost
Three alternating pairs per condition; table values are medians. Min/max ranges and individual receipts are in [mcp-startup.json](mcp-startup.json). Each cell is baseline → candidate. Kernel process-family peak includes file cache, SDK driver, observer and REA children.
| Task | Cache | Inputs | Wall seconds | Family CPU seconds | Family peak MiB |
| --------- | ----- | ----------------- | ------------: | -----------------: | --------------: |
| Directory | cold | compact (default) | 5.661 → 2.236 | 5.403 → 2.050 | 559.3 → 248.6 |
| Directory | cold | full | 5.256 → 2.337 | 5.040 → 2.175 | 650.9 → 349.1 |
| Directory | warm | compact (default) | 5.056 → 2.236 | 4.800 → 2.015 | 572.8 → 251.4 |
| Directory | warm | full | 4.957 → 2.338 | 4.754 → 2.139 | 672.7 → 349.6 |
| ripgrep | cold | compact (default) | 8.177 → 8.078 | 7.945 → 7.899 | 849.7 → 792.5 |
| zellij | cold | compact (default) | 8.276 → 7.276 | 8.041 → 7.105 | 846.2 → 819.0 |
| ripgrep | cold | full | 8.180 → 7.483 | 8.000 → 7.312 | 846.5 → 794.4 |
| zellij | cold | full | 8.484 → 7.776 | 8.284 → 7.514 | 850.4 → 813.1 |
| ripgrep | warm | compact (default) | 7.976 → 6.879 | 7.739 → 6.678 | 826.8 → 802.6 |
| zellij | warm | compact (default) | 8.285 → 7.472 | 8.019 → 7.254 | 882.1 → 811.4 |
| ripgrep | warm | full | 8.074 → 7.476 | 7.890 → 7.265 | 852.1 → 803.6 |
| zellij | warm | full | 8.382 → 7.479 | 8.151 → 7.293 | 854.3 → 830.0 |
Discovery improves clearly in this cohort. Completed-task changes depend on the workload: default cold ripgrep is approximately unchanged. Initialization moves into the first actual analysis request; its latency must be assessed with the full workflow.
| Default ripgrep first RPC | Baseline ms | Candidate ms |
| ------------------------- | ----------: | -----------: |
| cold | 231.5 | 2532.1 |
| warm | 227.1 | 2118.2 |
Within each profile, advertised context remains identical. Input-directory estimates are 49,670 tokens for default compact and 205,744 for full. Whole-task estimates include the supplied question/recipe, server instructions, directory once, every tool argument and text reply, and the prompt directory in discovery tasks. This projection assumes the client retains one unchanged directory; raw catalog result costs count every observed list result separately, including output schemas. These use tiktoken 0.14.0/cl100k_base; actual model usage, reasoning and SDK initialization/framing are unmeasured.
Linux x64 / AMD EPYC 7B12 / kernel 6.1.0-54-cloud-amd64 / Node 24.18.0, npm 11.16.0. Timed SDK tasks negotiated the legacy protocol era; auto/modern correctness is covered separately by compiled boundary tests. Trials run serially under a 1-CPU quota pinned to CPU 15, 4 GiB hard memory / 3 GiB early stop, no swap and 192 tasks. No resource stop, OOM or remaining payload process was observed. Cold disables server Node compile cache; warm uses a role/profile-specific cache populated by one complete task. Fresh processes and HOME are used each trial. Cache hit rate and OS page cache are unmeasured. Runtime audits warm compiled-file pages and are included in whole-task costs. Builds, downloads, oracle generation and cache priming are preparation outside those costs. Retained V8 heap and forced-GC measurements were not separately collected.
## Ownership and validation
`OwnedMcpServers` seals publication before taking its close snapshot, keeps failed exact close capabilities for retry and serves both eager and deferred paths. Deferred startup still joins servers published before an asynchronous serving handle settles. Post-snapshot and post-close factory calls are refused before allocation. Both production-boundary regressions fail with only the shared guard removed and pass restored.
The moved transport/delivery implementation preserves current complete-error retention, escaped-preview fitting, response/string-limit recovery, target-content provenance and native Evidence continuations. Canonical parsers remain authoritative after lazy binding. Both protocol eras, input/output profile choices, invalid input/output, completion cancellation, partial startup and cleanup retry are exercised.
Aggregation keeps catalog, producer-script and guard-script digests in distinct variables. Before publishing a trial it verifies that the catalog digest matches both its catalog-file receipt and the shared profile identity. Deliberately selecting the guard digest for that field is rejected. Regeneration preserves all measured timing, CPU, memory, context and parent-Evidence values.
Executed local gates: 125 focused tests across 14 files; a repeated 22-case schema/profile subset after the pinned main merge; all six static-check tasks plus architecture guards; builds for both pinned runtimes; six documentation checks and five README consistency tests. HTTP coverage includes the real compiled CLI session/replacement/Host tests. Runtime-binding admission rejects a mismatched compiled graph before MCP launch; aggregation rejects a tampered trial binding. The raw receipts remain in `/tmp/rea-perf-mcp-startup-20261022`, identified by hashes in the data. Real initial-target providers, foreign OS runtime behavior and other providers remain outside this measurement.
## Reproduction
Use the pinned Node/npm toolchain and separate clean baseline/candidate checkouts. Build each with `npm ci` and `npm run build:cached` (the measured workspace shared one locked dependency installation). Acquire and verify the official packages listed in the data, extract the named executables and generate independent oracles with `mcp-input-catalog-oracle.py`.
Capture each checkout with `node experiments/analysis-efficiency/managed-cli-runtime.mjs CHECKOUT mcp`. Construct `measurement-inputs.json` using the recorded schema, updating absolute checkout/artifact/oracle paths and their independently verified bindings. Keep it immutable through a cohort; producer source digests cover `mcp-input-catalog.mjs`, `managed-cli-runtime.mjs`, `resource-run.py` and `resource_payload.py`.
Run `mcp-input-catalog.mjs` through `resource-run.py --observe-payload-exit` with `--runtime`, `--output`, `--bindings`, `--role baseline|candidate`, `--profile default|full`, `--phase discovery|analysis` and `--cache cold|warm`. Analysis also takes `--artifact` and `--oracle`; warm requires a role/profile-specific `--cache-root`. Disable the driver compile cache for both cohorts. Prime each warm role/profile once with the complete ripgrep task. Follow the 72-case order in the data, reversing role order in every second pair. Aggregate with `python mcp-startup-costs.py LAB --output mcp-startup.json` using tiktoken 0.14.0. The aggregator verifies every input/runtime/guard binding and completed receipt before producing summaries.
+2 -2
View File
@@ -305,8 +305,8 @@
"verify:solidity:metadata": "npm run build:cached && node scripts/verify-solidity-metadata.mjs", "verify:solidity:metadata": "npm run build:cached && node scripts/verify-solidity-metadata.mjs",
"verify:recorded:crash": "npm run build:cached && node scripts/verify-recorded-crash.mjs", "verify:recorded:crash": "npm run build:cached && node scripts/verify-recorded-crash.mjs",
"mcp-catalog:check:built": "node scripts/generate-mcp-tool-catalog.mjs --check", "mcp-catalog:check:built": "node scripts/generate-mcp-tool-catalog.mjs --check",
"compile": "node scripts/clean-build-output.mjs && node scripts/run-typescript.mjs -p tsconfig.build.json && node scripts/record-catalog-digests.mjs && node scripts/record-mcp-catalog.mjs && node scripts/bundle-runtime.mjs", "compile": "node scripts/clean-build-output.mjs && node scripts/run-typescript.mjs -p tsconfig.build.json && node scripts/record-catalog-digests.mjs && node scripts/bundle-runtime.mjs",
"compile:termux": "node scripts/clean-build-output.mjs && node --max-old-space-size=3072 node_modules/typescript-termux/lib/tsc.js -p tsconfig.build.json && node scripts/record-catalog-digests.mjs && node scripts/record-mcp-catalog.mjs && node scripts/bundle-runtime.mjs", "compile:termux": "node scripts/clean-build-output.mjs && node --max-old-space-size=3072 node_modules/typescript-termux/lib/tsc.js -p tsconfig.build.json && node scripts/record-catalog-digests.mjs && node scripts/bundle-runtime.mjs",
"artifacts:mcp-catalog": "node scripts/generate-mcp-tool-catalog.mjs", "artifacts:mcp-catalog": "node scripts/generate-mcp-tool-catalog.mjs",
"artifacts:skills": "node scripts/generate-skill-metadata.mjs", "artifacts:skills": "node scripts/generate-skill-metadata.mjs",
"artifacts:product-catalog": "node scripts/generate-product-catalog.mjs", "artifacts:product-catalog": "node scripts/generate-product-catalog.mjs",
+14 -40
View File
@@ -1,31 +1,6 @@
import type { NormalizedWorkflowRun } from "./workflow-run.mjs"; import type { NormalizedWorkflowRun } from "./workflow-run.mjs";
import type { StaticTaskContext } from "./static-task-context.mjs"; import type { StaticTaskContext } from "./static-task-context.mjs";
export type { StaticTaskContext } from "./static-task-context.mjs";
export interface CiRun {
readonly id?: string | number;
readonly run_attempt?: number;
readonly status?: string;
readonly conclusion?: string | null;
readonly event?: string;
readonly path?: string;
readonly head_sha?: string;
readonly head_branch?: string;
readonly repository?: { readonly full_name?: string };
readonly pull_requests?: readonly {
readonly base?: {
readonly sha?: string;
readonly repo?: { readonly full_name?: string };
};
}[];
}
export interface CiJob {
readonly name?: string;
readonly status?: string;
readonly conclusion?: string | null;
readonly run_id?: number;
readonly run_attempt?: number;
}
export interface StaticDiagnostic { export interface StaticDiagnostic {
readonly kind: "lint" | "jscpd"; readonly kind: "lint" | "jscpd";
@@ -36,20 +11,15 @@ export interface StaticDiagnostic {
readonly severity?: string; readonly severity?: string;
} }
export interface StaticEvidence {
readonly context: StaticTaskContext;
readonly diagnostics: readonly StaticDiagnostic[];
}
export function classifyFailure(input: { export function classifyFailure(input: {
repository: string; repository: string;
defaultBranch: string; defaultBranch: string;
run: CiRun & NormalizedWorkflowRun; run: Record<string, unknown> & NormalizedWorkflowRun;
job: CiJob; job: Record<string, unknown>;
diagnostics: readonly StaticDiagnostic[]; diagnostics: readonly StaticDiagnostic[];
context: StaticTaskContext; context: StaticTaskContext;
mainRun: CiRun & NormalizedWorkflowRun; mainRun: Record<string, unknown> & NormalizedWorkflowRun;
mainJob: CiJob; mainJob: Record<string, unknown>;
mainDiagnostics: readonly StaticDiagnostic[]; mainDiagnostics: readonly StaticDiagnostic[];
mainContext: StaticTaskContext; mainContext: StaticTaskContext;
executionSourcesVerified: boolean; executionSourcesVerified: boolean;
@@ -77,7 +47,7 @@ export function safeSha(value: unknown): string | null;
export function validRepoPath(value: unknown): value is string; export function validRepoPath(value: unknown): value is string;
export function selectStaticCheckArtifact( export function selectStaticCheckArtifact(
page: unknown, page: unknown,
run: Pick<CiRun, "id" | "head_sha">, run: { id?: string | number; head_sha?: string },
runAttempt: number, runAttempt: number,
name?: string, name?: string,
): unknown | null; ): unknown | null;
@@ -87,8 +57,12 @@ export function readStaticCheckArtifact(options: {
artifactId: number | string; artifactId: number | string;
token: string; token: string;
fetchImpl?: typeof fetch; fetchImpl?: typeof fetch;
}): Promise<StaticEvidence>; }): Promise<{
context: StaticTaskContext;
diagnostics: readonly StaticDiagnostic[];
}>;
export function parseStaticArchive( export function parseStaticArchive(archivePath: string): Promise<{
archivePath: string, context: StaticTaskContext;
): Promise<StaticEvidence>; diagnostics: readonly StaticDiagnostic[];
}>;
+53 -1
View File
@@ -1,5 +1,38 @@
import type { CiRun, CiJob, StaticEvidence } from "./artifacts.mjs"; import type { NormalizedWorkflowRun } from "./workflow-run.mjs";
import type { StaticDiagnostic } from "./artifacts.mjs";
export type { StaticDiagnostic } from "./artifacts.mjs";
import type { StaticTaskContext } from "./static-task-context.mjs"; import type { StaticTaskContext } from "./static-task-context.mjs";
export type { StaticTaskContext } from "./static-task-context.mjs";
export interface CiRun {
readonly id?: string | number;
readonly run_attempt?: number;
readonly status?: string;
readonly conclusion?: string | null;
readonly event?: string;
readonly path?: string;
readonly head_sha?: string;
readonly head_branch?: string;
readonly repository?: { readonly full_name?: string };
readonly pull_requests?: readonly {
readonly base?: {
readonly sha?: string;
readonly repo?: { readonly full_name?: string };
};
}[];
}
export interface CiJob {
readonly name?: string;
readonly status?: string;
readonly conclusion?: string | null;
readonly run_id?: number;
readonly run_attempt?: number;
}
export interface StaticEvidence {
readonly context: StaticTaskContext;
readonly diagnostics: readonly StaticDiagnostic[];
}
export interface CiFailureReport { export interface CiFailureReport {
readonly version: 1; readonly version: 1;
@@ -66,6 +99,25 @@ export interface FailureReportApi {
): Promise<Readonly<Record<string, boolean>>>; ): Promise<Readonly<Record<string, boolean>>>;
} }
export function classifyFailure(input: {
repository: string;
defaultBranch: string;
run: CiRun & NormalizedWorkflowRun;
job: CiJob;
diagnostics: readonly StaticDiagnostic[];
context: StaticTaskContext;
mainRun: CiRun & NormalizedWorkflowRun;
mainJob: CiJob;
mainDiagnostics: readonly StaticDiagnostic[];
mainContext: StaticTaskContext;
executionSourcesVerified: boolean;
ancestry: { status: string; mergeBaseSha: string | null };
blobs: Readonly<Record<string, boolean>>;
}): {
classification: "matching-main-diagnostics" | "unknown";
reason: string;
};
export function reportCiFailures(options: { export function reportCiFailures(options: {
repository: string; repository: string;
defaultBranch: string; defaultBranch: string;
+2
View File
@@ -26,6 +26,8 @@ const MAX_MAIN_RUN_LOOKUPS = 100;
const MAX_PATHS_PER_COMPARISON = 50; const MAX_PATHS_PER_COMPARISON = 50;
const MAX_API_REQUESTS = 250; const MAX_API_REQUESTS = 250;
export { classifyFailure } from "./artifacts.mjs";
/** Uses only failed CI candidates present in the supplied bounded timing manifest. */ /** Uses only failed CI candidates present in the supplied bounded timing manifest. */
export async function reportCiFailures({ export async function reportCiFailures({
repository, repository,
@@ -142,9 +142,6 @@ export const verifyScenarioPolicySetup = async (
page, page,
) => { ) => {
const result = {}; const result = {};
// Exercise a caller-selected handshake deadline within the existing teardown
// guard; the default command deadline may legitimately outlast that guard.
const setupCommandTimeoutMs = 2_000;
for (const mode of ["http-websocket", "http-discovery", "deadline"]) { for (const mode of ["http-websocket", "http-discovery", "deadline"]) {
const proxy = await startProxy(endpoint, mode); const proxy = await startProxy(endpoint, mode);
const provider = createBrowserScenarioProvider(process.env); const provider = createBrowserScenarioProvider(process.env);
@@ -155,9 +152,6 @@ export const verifyScenarioPolicySetup = async (
target_id: targetId, target_id: targetId,
}, },
start_url: { url: origin }, start_url: { url: origin },
...(mode === "deadline"
? { command_timeout_ms: setupCommandTimeoutMs }
: {}),
environment: { service_workers: "block" }, environment: { service_workers: "block" },
actions: [ actions: [
{ step_id: "settle", action: "wait_for_timeout", duration_ms: 1 }, { step_id: "settle", action: "wait_for_timeout", duration_ms: 1 },
@@ -195,9 +189,7 @@ export const verifyScenarioPolicySetup = async (
if (!next.ok) throw next.error; if (!next.ok) throw next.error;
assert.equal(next.value.browser.cleanup, "disconnected-external"); assert.equal(next.value.browser.cleanup, "disconnected-external");
result[mode] = { result[mode] = {
...(mode === "deadline" ...(mode === "deadline" ? { timed_out: true } : { cancelled: true }),
? { timed_out: true, command_timeout_ms: setupCommandTimeoutMs }
: { cancelled: true }),
socket_closed: true, socket_closed: true,
target_reused: true, target_reused: true,
}; };
-1
View File
@@ -39,7 +39,6 @@ export const parseDevelopmentTestRequest = (mode, arguments_) => {
// runtime or typecheck prerequisites. Add a consumer here when it reads one. // runtime or typecheck prerequisites. Add a consumer here when it reads one.
const artifactConsumers = { const artifactConsumers = {
"artifacts:mcp-catalog": [ "artifacts:mcp-catalog": [
"tests/boundary/mcp/deferredRuntime.test.ts",
"tests/boundary/mcp/contractPresentation.test.ts", "tests/boundary/mcp/contractPresentation.test.ts",
"tests/boundary/mcp/toolSchemaValidity.test.ts", "tests/boundary/mcp/toolSchemaValidity.test.ts",
], ],
-7
View File
@@ -47,13 +47,6 @@ export function profileMcpModuleLoading(
/** Parse Linux procfs VmRSS text into bytes. */ /** Parse Linux procfs VmRSS text into bytes. */
export function parseLinuxVmRssBytes(status: string): number | null; export function parseLinuxVmRssBytes(status: string): number | null;
/** Name a fixed MCP startup phase budget. Mirrors `fixedMcpPhaseBudgetRecovery`. */
export function fixedMcpStartupPhaseBudgetRecovery(
phase: string,
value: number,
unit: "ms" | "bytes",
): string;
/** Collect unique file-backed ESM module URLs from Node diagnostics. */ /** Collect unique file-backed ESM module URLs from Node diagnostics. */
export function createEsmModuleProfileCollector(packageName?: string): { export function createEsmModuleProfileCollector(packageName?: string): {
append(chunk: string): void; append(chunk: string): void;
+14 -71
View File
@@ -35,22 +35,11 @@ export async function measureMcpStartup({
const startedAt = performance.now(); const startedAt = performance.now();
rss.start(); rss.start();
try { try {
await withFixedPhaseBudget( await client.connect(transport, { timeout: policy.initializeBudgetMs });
"MCP initialize phase budget",
policy.initializeBudgetMs,
"ms",
() => client.connect(transport, { timeout: policy.initializeBudgetMs }),
);
const initializedAt = performance.now(); const initializedAt = performance.now();
const tools = await withFixedPhaseBudget( const tools = await client.listTools(undefined, {
"MCP tools/list phase budget", timeout: policy.toolsListBudgetMs,
policy.toolsListBudgetMs, });
"ms",
() =>
client.listTools(undefined, {
timeout: policy.toolsListBudgetMs,
}),
);
const listedAt = performance.now(); const listedAt = performance.now();
rss.stop(); rss.stop();
const measurement = { const measurement = {
@@ -63,11 +52,7 @@ export async function measureMcpStartup({
assertStartupBudgets(measurement, policy); assertStartupBudgets(measurement, policy);
return measurement; return measurement;
} catch (cause) { } catch (cause) {
const detail = cause instanceof Error ? cause.message : String(cause); throw new Error(`packaged MCP startup probe failed: ${stderr}`, { cause });
throw new Error(
`packaged MCP startup probe failed: ${detail}${stderr.length === 0 ? "" : `\n${stderr}`}`,
{ cause },
);
} finally { } finally {
rss.stop(); rss.stop();
await Promise.allSettled([client.close(), transport.close()]); await Promise.allSettled([client.close(), transport.close()]);
@@ -97,24 +82,12 @@ export async function profileMcpModuleLoading({
version: "1.0.0", version: "1.0.0",
}); });
try { try {
await withFixedPhaseBudget( await client.connect(transport, {
"MCP doctor startup deadline", timeout: policy.doctorDeadlineMs,
policy.doctorDeadlineMs, });
"ms", await client.listTools(undefined, {
() => timeout: policy.toolsListBudgetMs,
client.connect(transport, { });
timeout: policy.doctorDeadlineMs,
}),
);
await withFixedPhaseBudget(
"MCP tools/list phase budget",
policy.toolsListBudgetMs,
"ms",
() =>
client.listTools(undefined, {
timeout: policy.toolsListBudgetMs,
}),
);
return profile.result(); return profile.result();
} finally { } finally {
await Promise.allSettled([client.close(), transport.close()]); await Promise.allSettled([client.close(), transport.close()]);
@@ -224,46 +197,16 @@ const createLinuxRssSampler = (pid) => {
}; };
}; };
/** Same sentence as `fixedMcpPhaseBudgetRecovery`; this script cannot import that TypeScript module. */
export const fixedMcpStartupPhaseBudgetRecovery = (phase, value, unit) =>
`REA's fixed ${phase} is ${String(value)} ${unit} and this release has no runtime override.`;
const phaseTimeout = (cause) => {
const message = cause instanceof Error ? cause.message : String(cause);
return (
message.includes("Request timed out") ||
message.includes("Maximum total timeout exceeded") ||
message.includes("timed out")
);
};
const withFixedPhaseBudget = async (phase, value, unit, operation) => {
try {
return await operation();
} catch (cause) {
if (!phaseTimeout(cause)) throw cause;
const detail = cause instanceof Error ? cause.message : String(cause);
throw new Error(
`${detail} ${fixedMcpStartupPhaseBudgetRecovery(phase, value, unit)}`,
{ cause },
);
}
};
const assertStartupBudgets = (measurement, policy) => { const assertStartupBudgets = (measurement, policy) => {
if (measurement.process_started_to_initialized_ms > policy.initializeBudgetMs) if (measurement.process_started_to_initialized_ms > policy.initializeBudgetMs)
throw new Error( throw new Error("packaged MCP exceeded the initialize regression budget");
`packaged MCP exceeded the initialize phase budget. ${fixedMcpStartupPhaseBudgetRecovery("MCP initialize phase budget", policy.initializeBudgetMs, "ms")}`,
);
if (measurement.initialized_to_tools_list_ms > policy.toolsListBudgetMs) if (measurement.initialized_to_tools_list_ms > policy.toolsListBudgetMs)
throw new Error( throw new Error("packaged MCP exceeded the tools/list regression budget");
`packaged MCP exceeded the tools/list phase budget. ${fixedMcpStartupPhaseBudgetRecovery("MCP tools/list phase budget", policy.toolsListBudgetMs, "ms")}`,
);
if ( if (
measurement.process_started_to_tools_list_ms > policy.firstCatalogBudgetMs measurement.process_started_to_tools_list_ms > policy.firstCatalogBudgetMs
) )
throw new Error( throw new Error(
`packaged MCP exceeded the first-catalog phase budget. ${fixedMcpStartupPhaseBudgetRecovery("MCP first-catalog phase budget", policy.firstCatalogBudgetMs, "ms")}`, "packaged MCP exceeded the first-catalog regression budget",
); );
}; };
-119
View File
@@ -1,119 +0,0 @@
import { createHash } from "node:crypto";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
// Runtime discovery is generated from the same canonical registrations, after
// default identity has been recorded and before bundling. Test catalogs stay separate.
const compiled = resolve(dirname(fileURLToPath(import.meta.url)), "../dist");
const read = (name) => import(pathToFileURL(join(compiled, name)).href);
const [
{ createServer },
{ createBinarySession },
{ parseConfig },
{ silentLogger },
{ PROMPT_CONTRACTS },
] = await Promise.all([
read("server/createServer.js"),
read("composition/binary.js"),
read("config/parseConfig.js"),
read("logger.js"),
read("contracts/promptContracts.js"),
]);
const recordCatalog = async (profile) => {
// Catalog generation is independent of the host account and user configuration.
const home = await mkdtemp(join(tmpdir(), "rea-mcp-catalog-"));
const environment = {
REA_MCP_INPUT_SCHEMA_PROFILE: profile,
HOME: home,
USERPROFILE: home,
XDG_CONFIG_HOME: join(home, "config"),
XDG_CACHE_HOME: join(home, "cache"),
XDG_STATE_HOME: join(home, "state"),
};
const cleanup = [];
let producerFailure;
try {
const config = parseConfig(environment);
if (!config.ok) throw config.error;
const session = createBinarySession(
config.value,
silentLogger,
environment,
);
cleanup.push(async () => {
const closed = await session.close({ retainProviderDocuments: true });
if (!closed.ok) throw closed.error;
});
const server = createServer({ kind: "session", session }, { environment });
cleanup.push(() => server.close());
const client = new Client({ name: "rea-catalog-recorder", version: "1" });
cleanup.push(() => client.close());
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
await server.connect(serverTransport);
await client.connect(clientTransport);
const tools = (await client.listTools()).tools;
const prompts = (await client.listPrompts()).prompts.map((prompt) => {
const contract = PROMPT_CONTRACTS.find(
(candidate) => candidate.name === prompt.name,
);
if (contract === undefined)
throw new Error(`Missing prompt ${prompt.name}`);
return { ...prompt, parameters: contract.arguments };
});
const catalog = {
version: 1,
info: client.getServerVersion(),
capabilities: client.getServerCapabilities(),
instructions: client.getInstructions(),
identity: server.catalogIdentity,
tools,
prompts,
};
const encoded = JSON.stringify(catalog);
const digest = createHash("sha256").update(encoded).digest("hex");
await writeFile(
join(
compiled,
profile === "full" ? "mcp-catalog-full.json" : "mcp-catalog.json",
),
encoded,
);
return digest;
} catch (cause) {
producerFailure = { cause };
throw cause;
} finally {
const results = await Promise.allSettled(
cleanup.reverse().map((close) => Promise.resolve().then(close)),
);
const failures = results.flatMap((result) =>
result.status === "rejected" ? [result.reason] : [],
);
if (failures.length === 0) {
try {
await rm(home, { recursive: true, force: true });
} catch (cause) {
failures.push(cause);
}
}
if (failures.length > 0)
throw new AggregateError(
producerFailure === undefined
? failures
: [producerFailure.cause, ...failures],
`Catalog cleanup failed; temporary home retained at ${home}`,
);
}
};
const digests = {};
for (const profile of ["compact", "full"])
digests[profile] = await recordCatalog(profile);
await writeFile(
join(compiled, "recordedMcpCatalog.js"),
`/** Recorded from the compiled runtime; do not edit. */\nexport const RECORDED_MCP_CATALOG_SHA256 = ${JSON.stringify(digests)};\n`,
);
-27
View File
@@ -784,33 +784,6 @@ it("projects package details from dumpsys package output", async () => {
}); });
}); });
// Package Manager prints an absent installer as the text "null"; the
// normalized result must report the absence, not a package named "null"
// (morluto/rea#3943).
it("reports a null installer declaration as an absent installer", async () => {
const { binary } = await writeAdbStub(
shellStub({
"dumpsys:package": `Packages:
Package [com.example] (abcdef):
versionName=1.2.3
installerPackageName=null
requested permissions:
android.permission.INTERNET
`,
}),
);
const result = await evidenceResult(providerOver(binary), {
operation: "inspect_adb_package",
input: { serial: "emulator-5554", package: "com.example" },
});
expect(result).toMatchObject({
version_name: "1.2.3",
installer_package_name: null,
requested_permissions_count: 1,
coverage: "complete",
});
});
// PackageInfo.getLongVersionCode() combines versionCodeMajor and versionCode // PackageInfo.getLongVersionCode() combines versionCodeMajor and versionCode
// into a signed 64-bit long, which JSON numbers cannot hold exactly. // into a signed 64-bit long, which JSON numbers cannot hold exactly.
it.each(["9007199254740993", "-4294967296"])( it.each(["9007199254740993", "-4294967296"])(
-46
View File
@@ -7,52 +7,6 @@ import {
parseWindowFocusOutput, parseWindowFocusOutput,
} from "./AdbShellOutput.js"; } from "./AdbShellOutput.js";
const packageDump = (installerLine: string | undefined): string =>
[
"Packages:",
" Package [com.example] (abcdef):",
" versionName=1.2.3",
...(installerLine === undefined ? [] : [` ${installerLine}`]),
" pkgFlags=[ HAS_CODE ]",
"",
].join("\n");
describe("parseDumpsysPackageOutput", () => {
it("keeps a reported installer package name", () => {
expect(
parseDumpsysPackageOutput(
packageDump("installerPackageName=com.android.vending"),
).installerPackageName,
).toBe("com.android.vending");
});
// Package Manager prints an absent String field as the text "null"; a
// missing or empty declaration means the same absence (morluto/rea#3943).
it.each([
["the literal null sentinel", "installerPackageName=null"],
["an empty value", "installerPackageName="],
["a missing line", undefined],
])("reports an absent installer as null for %s", (_case, line) => {
expect(parseDumpsysPackageOutput(packageDump(line))).toMatchObject({
versionName: "1.2.3",
installerPackageName: null,
});
});
// A manifest may declare the literal string "null" as its version name and
// dumpsys prints it the same way as an absent one, so the text is kept.
it("keeps a version name printed as the text null", () => {
expect(
parseDumpsysPackageOutput(
packageDump("installerPackageName=null").replace(
"versionName=1.2.3",
"versionName=null",
),
).versionName,
).toBe("null");
});
});
describe("parseWindowFocusOutput display contexts", () => { describe("parseWindowFocusOutput display contexts", () => {
it.each([false, true])( it.each([false, true])(
"retains both display owners independent of block order: %s", "retains both display owners independent of block order: %s",
+1 -12
View File
@@ -275,17 +275,6 @@ export interface AdbPackageDetails {
const DETAIL_LINE = /^\s+([A-Za-z][A-Za-z0-9_]*)=(.*)$/u; const DETAIL_LINE = /^\s+([A-Za-z][A-Za-z0-9_]*)=(.*)$/u;
/**
* Package Manager prints its `String` fields by concatenation, so an absent
* `installerPackageName` appears as the literal text `null`
* (morluto/rea#3943). No installer can be named "null", so an empty value or
* that sentinel is the absence of the fact and becomes the nullable result's
* null. `versionName` keeps the printed text: a manifest may declare the
* string "null" on purpose, and the parser cannot tell the two apart.
*/
const absentInstallerOrName = (value: string): string | null =>
value === "" || value === "null" ? null : value;
/** /**
* Project `dumpsys package PACKAGE` output. Only the listed fields are * Project `dumpsys package PACKAGE` output. Only the listed fields are
* extracted; the input is version-dependent and everything else is ignored * extracted; the input is version-dependent and everything else is ignored
@@ -351,7 +340,7 @@ export const parseDumpsysPackageOutput = (
key === "installerPackageName" && key === "installerPackageName" &&
details.installerPackageName === null details.installerPackageName === null
) )
details.installerPackageName = absentInstallerOrName(value); details.installerPackageName = value === "" ? null : value;
else if (key === "userId" && details.userId === null) { else if (key === "userId" && details.userId === null) {
const parsed = /^(\d+)/u.exec(value); const parsed = /^(\d+)/u.exec(value);
if (parsed !== null) details.userId = Number(parsed[1]!); if (parsed !== null) details.userId = Number(parsed[1]!);
-89
View File
@@ -1,89 +0,0 @@
import { EventEmitter } from "node:events";
import { win32 } from "node:path";
import { PassThrough } from "node:stream";
import { afterEach, expect, it, vi } from "vitest";
const { spawnMock, tmpdirMock, lstatMock } = vi.hoisted(() => ({
spawnMock: vi.fn(),
tmpdirMock: vi.fn(),
lstatMock: vi.fn(),
}));
vi.mock("node:child_process", async (importOriginal) => ({
...(await importOriginal<typeof import("node:child_process")>()),
spawn: spawnMock,
}));
vi.mock("node:os", async (importOriginal) => ({
...(await importOriginal<typeof import("node:os")>()),
tmpdir: tmpdirMock,
}));
vi.mock("node:fs/promises", async (importOriginal) => ({
...(await importOriginal<typeof import("node:fs/promises")>()),
lstat: lstatMock,
}));
import { createJadxWorkspaceRoot } from "./JadxWorkspaceRoot.js";
afterEach(() => {
vi.restoreAllMocks();
vi.clearAllMocks();
});
it("decodes a Windows guardian path across split UTF-8 stdout writes", async () => {
const previousPlatform = process.platform;
Object.defineProperty(process, "platform", {
configurable: true,
value: "win32",
});
const parent = "C:\\Users\\研究者\\AppData\\Local\\Temp";
const path = win32.join(parent, `rea-android-${"a".repeat(32)}`);
const child = Object.assign(new EventEmitter(), {
stdin: new PassThrough(),
stdout: new PassThrough(),
stderr: new PassThrough(),
unref: vi.fn(),
});
child.stdin.once("finish", () => {
child.stdout.end();
child.stderr.end();
child.emit("close", 0, null);
});
tmpdirMock.mockReturnValue(parent);
const missingPath = Object.assign(new Error("missing after cleanup"), {
code: "ENOENT",
});
lstatMock.mockRejectedValue(missingPath);
spawnMock.mockReturnValue(child);
const ready = Buffer.from(
`${JSON.stringify({
path,
volumeSerial: "0123456789abcdef",
fileId: "0123456789abcdef0123456789abcdef",
privateDacl: true,
})}\n`,
);
const split = ready.indexOf(Buffer.from("研")) + 1;
try {
const start = createJadxWorkspaceRoot();
setImmediate(() => {
child.stdout.write(ready.subarray(0, split), () => {
setImmediate(() => child.stdout.write(ready.subarray(split)));
});
});
const root = await start;
try {
expect(root.path).toBe(path);
} finally {
await expect(root.close()).resolves.toBeUndefined();
}
expect(lstatMock).toHaveBeenCalledWith(path);
} finally {
Object.defineProperty(process, "platform", {
configurable: true,
value: previousPlatform,
});
}
});
+4 -6
View File
@@ -105,9 +105,8 @@ class WindowsJadxWorkspaceRoot implements JadxWorkspaceRoot {
}, },
}); });
let diagnostics = ""; let diagnostics = "";
child.stderr.setEncoding("utf8"); child.stderr.on("data", (chunk: Buffer) => {
child.stderr.on("data", (chunk: string) => { diagnostics = (diagnostics + chunk.toString()).slice(-8192);
diagnostics = (diagnostics + chunk).slice(-8192);
}); });
const exit = new Promise<GuardianExit>((resolve) => { const exit = new Promise<GuardianExit>((resolve) => {
child.once("error", (error: Error) => child.once("error", (error: Error) =>
@@ -117,9 +116,8 @@ class WindowsJadxWorkspaceRoot implements JadxWorkspaceRoot {
}); });
const ready = new Promise<string>((resolve, reject) => { const ready = new Promise<string>((resolve, reject) => {
let frame = ""; let frame = "";
child.stdout.setEncoding("utf8"); child.stdout.on("data", (chunk: Buffer) => {
child.stdout.on("data", (chunk: string) => { frame += chunk.toString();
frame += chunk;
if (frame.length > 4096) { if (frame.length > 4096) {
reject( reject(
new Error("Android workspace guardian readiness exceeded 4 KiB"), new Error("Android workspace guardian readiness exceeded 4 KiB"),
-3
View File
@@ -1,4 +1,3 @@
import type { EvidenceCreationOptions } from "../domain/evidence.js";
import type { ProviderInspectionScope } from "../providers/ProviderInspectionScope.js"; import type { ProviderInspectionScope } from "../providers/ProviderInspectionScope.js";
import type { BinaryTarget } from "../domain/binaryTargetTypes.js"; import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import type { OfficialToolName } from "../contracts/officialToolContracts.js"; import type { OfficialToolName } from "../contracts/officialToolContracts.js";
@@ -147,7 +146,6 @@ export const createAnalysisExecutionEvidence = (
input: unknown, input: unknown,
execution: AnalysisExecution, execution: AnalysisExecution,
confidence: "observed" | "derived", confidence: "observed" | "derived",
options?: EvidenceCreationOptions,
): Evidence => ): Evidence =>
createEvidence( createEvidence(
execution.subject === undefined execution.subject === undefined
@@ -169,7 +167,6 @@ export const createAnalysisExecutionEvidence = (
locations: execution.locations, locations: execution.locations,
confidence, confidence,
}, },
options,
); );
/** Provider-neutral application capability for one analysis operation. */ /** Provider-neutral application capability for one analysis operation. */
-8
View File
@@ -138,9 +138,6 @@ const browserParameters = (
: "target_id" in input && observedOrigin !== undefined : "target_id" in input && observedOrigin !== undefined
? [observedOrigin] ? [observedOrigin]
: [], : [],
...("command_timeout_ms" in input && input.command_timeout_ms !== undefined
? { command_timeout_ms: input.command_timeout_ms }
: {}),
}; };
if (!("target_id" in input)) return scope; if (!("target_id" in input)) return scope;
return { return {
@@ -174,11 +171,6 @@ const browserParameters = (
...(input.total_analysis_timeout_ms === undefined ...(input.total_analysis_timeout_ms === undefined
? {} ? {}
: { total_analysis_timeout_ms: input.total_analysis_timeout_ms }), : { total_analysis_timeout_ms: input.total_analysis_timeout_ms }),
...(input.source_map_fetch_timeout_ms === undefined
? {}
: {
source_map_fetch_timeout_ms: input.source_map_fetch_timeout_ms,
}),
} }
: {}), : {}),
}; };
+23 -90
View File
@@ -1,5 +1,4 @@
import type { ExecutableFormatHint } from "../domain/dosCom.js"; import type { ExecutableFormatHint } from "../domain/dosCom.js";
import { dispatchQueuedInterrupts } from "../process/queuedInterrupts.js";
import type { BinaryArchitecture } from "../domain/binaryTargetTypes.js"; import type { BinaryArchitecture } from "../domain/binaryTargetTypes.js";
import type { JsonValue } from "../domain/jsonValue.js"; import type { JsonValue } from "../domain/jsonValue.js";
import type { DirectAnalysisDependencies } from "./DirectAnalysisDependencies.js"; import type { DirectAnalysisDependencies } from "./DirectAnalysisDependencies.js";
@@ -60,11 +59,7 @@ import {
import type { AnalysisProviderSelector } from "../contracts/providerSelection.js"; import type { AnalysisProviderSelector } from "../contracts/providerSelection.js";
import { artifactInspectionResultSchema } from "../domain/artifactInspection.js"; import { artifactInspectionResultSchema } from "../domain/artifactInspection.js";
import { resolveXrefsAddress } from "./XrefsAddressResolution.js"; import { resolveXrefsAddress } from "./XrefsAddressResolution.js";
import { import { AnalysisInputError } from "../domain/analysisErrorCore.js";
AnalysisCancelledError,
AnalysisInputError,
} from "../domain/analysisErrorCore.js";
import { deliverAnalysisEvidence } from "./EvidenceBundleCommands.js";
import type { BinaryTargetError } from "../domain/configurationErrors.js"; import type { BinaryTargetError } from "../domain/configurationErrors.js";
import { import {
directAnalysisCleanupFailure, directAnalysisCleanupFailure,
@@ -151,81 +146,24 @@ export const runDirectAnalysis = async (
/** Execute one provider-native semantic operation with atomic provenance. */ /** Execute one provider-native semantic operation with atomic provenance. */
export const runProviderAnalysis = async ( export const runProviderAnalysis = async (
dependencies: DirectAnalysisDependencies, dependencies: DirectAnalysisDependencies,
...[path, tool, arguments_, logger = silentLogger, signal, delivery]: ...[path, tool, arguments_, logger = silentLogger, signal]: readonly [
| readonly [ path: string,
path: string, tool: NativeToolName | ArtifactAnalysisOperation | ManagedToolName,
tool: NativeToolName | ArtifactAnalysisOperation | ManagedToolName, arguments_: Readonly<Record<string, JsonValue>>,
arguments_: Readonly<Record<string, JsonValue>>, logger?: Logger,
logger?: Logger | undefined, signal?: AbortSignal,
signal?: AbortSignal | undefined, ]
]
| readonly [
path: string,
tool: "inspect_managed_members",
arguments_: Readonly<Record<string, JsonValue>>,
logger: Logger | undefined,
signal: AbortSignal | undefined,
delivery: {
readonly detail?: "complete" | "summary";
readonly evidenceBundlePath?: string;
},
]
): Promise<JsonValue> => { ): Promise<JsonValue> => {
if (delivery !== undefined && tool !== "inspect_managed_members") if (isManagedToolName(tool))
return cliError( return runManagedProviderAnalysis(
new AnalysisInputError(tool, undefined, [ dependencies,
{ path,
path: ["delivery"], tool,
reason: "invalid_value", arguments_,
message: signal,
"Evidence delivery options are only supported by inspect_managed_members",
},
]),
); );
if ( return withProcessCancellation(signal, (operationSignal) =>
delivery?.detail === "summary" && runAnalysis(dependencies, path, tool, arguments_, {
delivery.evidenceBundlePath === undefined
)
return cliError(
new AnalysisInputError(tool, undefined, [
{
path: ["evidence_bundle"],
reason: "missing_argument",
message:
"summary detail requires --evidence-bundle so complete Evidence remains available to later CLI commands",
},
]),
);
return withProcessCancellation(signal, async (operationSignal) => {
if (isManagedToolName(tool)) {
const original = await runManagedProviderAnalysis(
dependencies,
path,
tool,
arguments_,
operationSignal,
);
await dispatchQueuedInterrupts();
if (!original.ok) return cliError(original.error);
if (operationSignal.aborted)
return cliError(
new AnalysisCancelledError(tool, {
cause: operationSignal.reason,
partialObservation: original.value,
}),
);
const result =
delivery?.evidenceBundlePath === undefined
? original
: await deliverAnalysisEvidence(
original.value,
delivery.evidenceBundlePath,
delivery.detail,
operationSignal,
);
return result.ok ? result.value : cliError(result.error);
}
return runAnalysis(dependencies, path, tool, arguments_, {
logger, logger,
snapshotPath: undefined, snapshotPath: undefined,
signal: operationSignal, signal: operationSignal,
@@ -234,8 +172,8 @@ export const runProviderAnalysis = async (
: NATIVE_INSPECTION_OPERATIONS.has(tool) : NATIVE_INSPECTION_OPERATIONS.has(tool)
? { createSession: dependencies.createNativeBinarySession } ? { createSession: dependencies.createNativeBinarySession }
: {}), : {}),
}); }),
}); );
}; };
/** Execute managed metadata inspection in an isolated managed-only session. */ /** Execute managed metadata inspection in an isolated managed-only session. */
@@ -261,14 +199,13 @@ export const runManagedProviderExecution = async (
); );
}); });
/** Preserve typed managed Evidence before selecting its CLI delivery. */
const runManagedProviderAnalysis = async ( const runManagedProviderAnalysis = async (
dependencies: Pick<DirectAnalysisDependencies, "createManagedBinarySession">, dependencies: DirectAnalysisDependencies,
path: string, path: string,
tool: ManagedToolName, tool: ManagedToolName,
arguments_: Readonly<Record<string, JsonValue>>, arguments_: Readonly<Record<string, JsonValue>>,
signal?: AbortSignal, signal?: AbortSignal,
): Promise<Result<Evidence, AnalysisError>> => { ): Promise<JsonValue> => {
const execution = await runManagedProviderExecution( const execution = await runManagedProviderExecution(
dependencies, dependencies,
path, path,
@@ -276,13 +213,9 @@ const runManagedProviderAnalysis = async (
signal, signal,
arguments_, arguments_,
); );
if (!execution.ok) return execution; if (!execution.ok) return cliError(execution.error);
const value = execution.value; const value = execution.value;
return ok( return createAnalysisExecutionEvidence(tool, arguments_, value, "observed");
createAnalysisExecutionEvidence(tool, arguments_, value, "observed", {
immutable: true,
}),
);
}; };
const runAnalysis = async ( const runAnalysis = async (
+12 -193
View File
@@ -1,23 +1,9 @@
import { dispatchQueuedInterrupts } from "../process/queuedInterrupts.js";
import { absoluteCallerPath } from "../filesystem/callerPath.js";
import { requireAuthenticationHeadroom } from "./JsonAuthenticationHeadroom.js";
import { import {
EvidenceFileError, EvidenceFileError,
EvidenceIntegrityError, EvidenceIntegrityError,
type PublishedEvidenceFile,
} from "../domain/evidenceErrors.js"; } from "../domain/evidenceErrors.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js"; import type { AnalysisError } from "../domain/analysisErrorBase.js";
import { import type { EvidenceBundle } from "../domain/evidenceBundle.js";
AnalysisCancelledError,
AnalysisInputError,
} from "../domain/analysisErrorCore.js";
import type { Evidence } from "../domain/evidence.js";
import {
createImmutableEvidenceBundle,
type EvidenceBundle,
} from "../domain/evidenceBundle.js";
import type { InspectAnalysisViewInput } from "../domain/analysisView/analysisView.js";
import type { JsonValue } from "../domain/jsonValue.js"; import type { JsonValue } from "../domain/jsonValue.js";
import { jsonValueSchema } from "../domain/jsonValue.js"; import { jsonValueSchema } from "../domain/jsonValue.js";
import { err, ok, type Result } from "../domain/result.js"; import { err, ok, type Result } from "../domain/result.js";
@@ -28,18 +14,12 @@ import {
readImmutableEvidenceBundle, readImmutableEvidenceBundle,
writeEvidenceBundle, writeEvidenceBundle,
} from "./EvidenceBundleFiles.js"; } from "./EvidenceBundleFiles.js";
import { inspectAnalysisViewValidated } from "./analysisView/AnalysisViewService.js";
/** Validate a standalone bundle and report its complete record/revision counts. */ /** Validate a standalone bundle and report its complete record/revision counts. */
export const importEvidenceBundleCommand = async ( export const importEvidenceBundleCommand = async (
path: string, path: string,
): Promise<Result<JsonValue, AnalysisError>> => { ): Promise<Result<JsonValue, AnalysisError>> => {
const loaded = await readEvidenceBundle( const loaded = await readEvidenceBundle(path);
path,
undefined,
undefined,
"import_evidence_bundle",
);
if (!loaded.ok) return loaded; if (!loaded.ok) return loaded;
return ok({ return ok({
imported: loaded.value.records.length, imported: loaded.value.records.length,
@@ -55,170 +35,16 @@ export const exportEvidenceBundleCommand = async (
overwrite: boolean, overwrite: boolean,
): Promise<Result<JsonValue, AnalysisError>> => { ): Promise<Result<JsonValue, AnalysisError>> => {
// The sealed snapshot lets the writer reuse this authentication. // The sealed snapshot lets the writer reuse this authentication.
const loaded = await readImmutableEvidenceBundle( const loaded = await readImmutableEvidenceBundle(sourcePath);
sourcePath,
undefined,
undefined,
"export_evidence_bundle",
);
if (!loaded.ok) return loaded; if (!loaded.ok) return loaded;
return projectWrite(
loaded.value,
await persistEvidenceBundleCommand(loaded.value, outputPath, overwrite),
);
};
/** Publish complete parent Evidence before delivering it or its linked summary. */
export const deliverAnalysisEvidence = async (
evidence: Evidence,
outputPath: string,
detail: "complete" | "summary" = "complete",
signal?: AbortSignal,
): Promise<Result<Evidence, AnalysisError>> => {
const headroom = requireAuthenticationHeadroom(evidence, {
operation: evidence.operation,
path: absoluteCallerPath(outputPath),
description:
"Insufficient heap headroom to authenticate the complete parent Evidence",
});
if (!headroom.ok)
return err(headroom.error.retainPartialObservation(evidence));
const bundle = createImmutableEvidenceBundle([evidence]);
const delivered =
detail === "summary"
? inspectAnalysisViewValidated(
{
source: {
kind: "retained-evidence",
evidence_id: evidence.evidence_id,
},
view: { kind: "summary" },
},
(id) => bundle.records.find((record) => record.evidence_id === id),
)
: ok(evidence);
if (!delivered.ok) return delivered;
const published = await publishEvidenceDelivery({
bundle,
complete: evidence,
delivered: delivered.value,
outputPath,
operation: evidence.operation,
signal,
});
return published.ok ? ok(published.value.delivered) : published;
};
/** Selected Evidence delivered after its complete bundle was committed. */
export interface PublishedEvidenceDelivery {
readonly delivered: Evidence;
readonly evidenceBundle: PublishedEvidenceFile;
}
/**
* Publish a complete bundle, then deliver Evidence already projected from it.
* Cancellation observed before commit leaves no output and retains the
* complete Evidence. A committed publication wins a later cancellation race;
* its receipt stays in the typed cancellation as diagnostics.evidence_bundle,
* and a cleanup failure after commit keeps its own published receipt.
*/
export const publishEvidenceDelivery = async (input: {
readonly bundle: EvidenceBundle;
readonly complete: Evidence;
readonly delivered: Evidence;
readonly outputPath: string;
readonly operation: string;
readonly signal?: AbortSignal | undefined;
}): Promise<Result<PublishedEvidenceDelivery, AnalysisError>> => {
const { signal, operation } = input;
const written = await persistEvidenceBundleCommand(
input.bundle,
input.outputPath,
false,
signal,
operation,
);
// Deliver interrupts queued during final publication cleanup before the
// command's signal owner accepts completion and removes its listeners.
await dispatchQueuedInterrupts();
if (!written.ok)
return err(written.error.retainPartialObservation(input.complete));
if (signal?.aborted)
return err(
new AnalysisCancelledError(operation, {
cause: signal.reason,
partialObservation: input.delivered,
diagnostics: { evidence_bundle: { ...written.value } },
}),
);
return ok({ delivered: input.delivered, evidenceBundle: written.value });
};
/** Resolve the caller's exact retained parent from an authenticated portable bundle. */
export const inspectAnalysisViewFromBundle = async (
input: InspectAnalysisViewInput,
bundlePath: string,
resources: ArtifactResourceScope,
signal?: AbortSignal,
): Promise<Result<Evidence, AnalysisError>> => {
if (input.source.kind !== "retained-evidence")
return err(
new AnalysisInputError("inspect_analysis_view", undefined, [
{
path: ["source", "kind"],
reason: "invalid_value",
message:
"--evidence-bundle requires a retained-evidence source with the exact parent evidence_id; inline Evidence already supplies its source",
},
]),
);
const loaded = await readImmutableEvidenceBundle(
bundlePath,
signal,
resources,
"inspect_analysis_view",
);
// Authentication and projection are synchronous; dispatch queued interrupts before accepting either result.
await dispatchQueuedInterrupts();
if (!loaded.ok) return loaded;
if (signal?.aborted)
return err(
new AnalysisCancelledError("inspect_analysis_view", {
cause: signal.reason,
}),
);
const projected = inspectAnalysisViewValidated(input, (id) =>
loaded.value.records.find((record) => record.evidence_id === id),
);
await dispatchQueuedInterrupts();
if (projected.ok && signal?.aborted)
return err(
new AnalysisCancelledError("inspect_analysis_view", {
cause: signal.reason,
partialObservation: projected.value,
}),
);
return projected;
};
/** Publish a completed bundle and retry its exact staged-output cleanup owner. */
const persistEvidenceBundleCommand = async (
bundle: EvidenceBundle,
outputPath: string,
overwrite: boolean,
signal?: AbortSignal,
operation?: string,
): Promise<Result<PublishedEvidenceFile, AnalysisError>> => {
const resources = new ArtifactResourceScope(); const resources = new ArtifactResourceScope();
const written = await writeEvidenceBundle( const written = await writeEvidenceBundle(
bundle, loaded.value,
outputPath, outputPath,
overwrite, overwrite,
resources, resources,
signal,
operation,
); );
if (written.ok) return written; if (written.ok) return projectWrite(loaded.value, written);
// Confirmed release resolves the bound error to its primary outcome; a // Confirmed release resolves the bound error to its primary outcome; a
// failed retry leaves that error incomplete with its exact retained owner. // failed retry leaves that error incomplete with its exact retained owner.
@@ -228,7 +54,10 @@ const persistEvidenceBundleCommand = async (
written.error instanceof EvidenceFileError && written.error instanceof EvidenceFileError &&
written.error.published written.error.published
) { ) {
return ok(written.error.published); return projectWrite(loaded.value, {
ok: true,
value: written.error.published,
});
} }
return written; return written;
}; };
@@ -241,18 +70,8 @@ export const compareEvidenceBundlesCommand = async (input: {
// Sealed snapshots let comparison and its bundle digests reuse this // Sealed snapshots let comparison and its bundle digests reuse this
// authentication instead of re-validating every record twice more. // authentication instead of re-validating every record twice more.
const [left, right] = await Promise.all([ const [left, right] = await Promise.all([
readImmutableEvidenceBundle( readImmutableEvidenceBundle(input.leftPath),
input.leftPath, readImmutableEvidenceBundle(input.rightPath),
undefined,
undefined,
"compare_bundles",
),
readImmutableEvidenceBundle(
input.rightPath,
undefined,
undefined,
"compare_bundles",
),
]); ]);
if (!left.ok) return left; if (!left.ok) return left;
if (!right.ok) return right; if (!right.ok) return right;
@@ -269,7 +88,7 @@ export const compareEvidenceBundlesCommand = async (input: {
const projectWrite = ( const projectWrite = (
bundle: EvidenceBundle, bundle: EvidenceBundle,
written: Result<PublishedEvidenceFile, AnalysisError>, written: Awaited<ReturnType<typeof writeEvidenceBundle>>,
): Result<JsonValue, AnalysisError> => ): Result<JsonValue, AnalysisError> =>
written.ok written.ok
? ok({ ? ok({
+18 -24
View File
@@ -1,4 +1,3 @@
import { requireAuthenticationHeadroom } from "./JsonAuthenticationHeadroom.js";
import { absoluteCallerPath } from "../filesystem/callerPath.js"; import { absoluteCallerPath } from "../filesystem/callerPath.js";
import { import {
@@ -40,47 +39,45 @@ export const readEvidenceBundle = (
path: string, path: string,
signal?: AbortSignal, signal?: AbortSignal,
resources?: ArtifactResourceScope, resources?: ArtifactResourceScope,
operation = "import_evidence_bundle",
): Promise<Result<EvidenceBundle, EvidenceReadFailure>> => ): Promise<Result<EvidenceBundle, EvidenceReadFailure>> =>
readBundle(path, parseEvidenceBundle, signal, resources, operation); readBundle(path, parseEvidenceBundle, signal, resources);
/** Read an authenticated immutable snapshot for a subsequent session import. */ /** Read an authenticated immutable snapshot for a subsequent session import. */
export const readImmutableEvidenceBundle = ( export const readImmutableEvidenceBundle = (
path: string, path: string,
signal?: AbortSignal, signal?: AbortSignal,
resources?: ArtifactResourceScope, resources?: ArtifactResourceScope,
operation = "import_evidence_bundle",
): Promise<Result<EvidenceBundle, EvidenceReadFailure>> => ): Promise<Result<EvidenceBundle, EvidenceReadFailure>> =>
readBundle(path, parseImmutableEvidenceBundle, signal, resources, operation); readBundle(path, parseImmutableEvidenceBundle, signal, resources);
const readBundle = async ( const readBundle = async (
path: string, path: string,
parseBundle: (input: unknown) => EvidenceBundle, parseBundle: (input: unknown) => EvidenceBundle,
signal?: AbortSignal, signal?: AbortSignal,
resources?: ArtifactResourceScope, resources?: ArtifactResourceScope,
operation = "import_evidence_bundle",
): Promise<Result<EvidenceBundle, EvidenceReadFailure>> => { ): Promise<Result<EvidenceBundle, EvidenceReadFailure>> => {
if (signal?.aborted) if (signal?.aborted)
return err( return err(
new AnalysisCancelledError(operation, { new AnalysisCancelledError("import_evidence_bundle", {
cause: signal.reason, cause: signal.reason,
}), }),
); );
const loaded = await readJsonFile(path, signal, resources, operation); const loaded = await readJsonFile(path, signal, resources);
if (!loaded.ok) return loaded; if (!loaded.ok) {
if (loaded.error instanceof AnalysisCancelledError)
return err(
new AnalysisCancelledError("import_evidence_bundle", {
cause: loaded.error.cause,
}),
);
return loaded;
}
if (signal?.aborted) if (signal?.aborted)
return err( return err(
new AnalysisCancelledError(operation, { new AnalysisCancelledError("import_evidence_bundle", {
cause: signal.reason, cause: signal.reason,
}), }),
); );
const headroom = requireAuthenticationHeadroom(loaded.value, {
operation,
path: absoluteCallerPath(path),
description:
"Insufficient heap headroom to authenticate the Evidence bundle",
});
if (!headroom.ok) return headroom;
let bundle: EvidenceBundle; let bundle: EvidenceBundle;
try { try {
bundle = parseBundle(loaded.value); bundle = parseBundle(loaded.value);
@@ -124,7 +121,6 @@ export const writeEvidenceBundle = async (
overwrite: boolean, overwrite: boolean,
resources: ArtifactResourceScope, resources: ArtifactResourceScope,
signal?: AbortSignal, signal?: AbortSignal,
operation = "export_evidence_bundle",
): Promise< ): Promise<
Result< Result<
{ readonly path: string; readonly bytes: number }, { readonly path: string; readonly bytes: number },
@@ -132,11 +128,7 @@ export const writeEvidenceBundle = async (
> >
> => { > => {
if (signal?.aborted === true) if (signal?.aborted === true)
return err( return err(new AnalysisCancelledError("export_evidence_bundle"));
new AnalysisCancelledError(operation, {
cause: signal.reason,
}),
);
let checked: EvidenceBundle; let checked: EvidenceBundle;
try { try {
checked = parseEvidenceBundle(bundle); checked = parseEvidenceBundle(bundle);
@@ -152,6 +144,8 @@ export const writeEvidenceBundle = async (
path, path,
overwrite, overwrite,
resources, resources,
signal === undefined ? undefined : { signal, operation }, signal === undefined
? undefined
: { signal, operation: "export_evidence_bundle" },
); );
}; };
@@ -22,9 +22,7 @@ describe("exact Evidence input resolution", () => {
actual: null, actual: null,
}, },
}); });
expect(projected.message).toContain( expect(projected.message).toContain("not retained in this session");
"not available from the selected source",
);
expect(projected.remediation.action).toContain( expect(projected.remediation.action).toContain(
"complete inline Evidence", "complete inline Evidence",
); );
+2 -2
View File
@@ -1,4 +1,4 @@
import type { EvidenceInput } from "../domain/evidenceReference.js"; import type { EvidenceInput } from "../contracts/evidenceInputContracts.js";
import type { Evidence } from "../domain/evidence.js"; import type { Evidence } from "../domain/evidence.js";
import { EvidenceReferenceError } from "../domain/evidenceErrors.js"; import { EvidenceReferenceError } from "../domain/evidenceErrors.js";
import { err, ok, type Result } from "../domain/result.js"; import { err, ok, type Result } from "../domain/result.js";
@@ -18,7 +18,7 @@ export const resolveEvidenceInput = (
new EvidenceReferenceError( new EvidenceReferenceError(
input.evidence_id, input.evidence_id,
"missing", "missing",
"Evidence available to the selected source", "Evidence retained in this session",
null, null,
), ),
); );
+7 -17
View File
@@ -9,7 +9,6 @@ import {
type EvidenceSubjectTarget, type EvidenceSubjectTarget,
} from "../domain/evidence.js"; } from "../domain/evidence.js";
import { jsonValueSchema } from "../domain/jsonValue.js"; import { jsonValueSchema } from "../domain/jsonValue.js";
import type { MobileApplicationProjectionWithSources } from "../domain/mobileApplicationGraphProjection.js";
import { err, ok, type Result } from "../domain/result.js"; import { err, ok, type Result } from "../domain/result.js";
import { requestInputError, workflowInputError } from "./workflowInputError.js"; import { requestInputError, workflowInputError } from "./workflowInputError.js";
@@ -24,20 +23,13 @@ interface InventoryProjectionResult {
readonly limitations: readonly string[]; readonly limitations: readonly string[];
} }
export interface InventoryProjectionEvidenceResult {
readonly evidence: Evidence;
readonly sources: readonly Evidence[];
}
interface InventoryProjectionOptions< interface InventoryProjectionOptions<
Input extends InventoryProjectionInput, Input extends InventoryProjectionInput,
Output extends InventoryProjectionResult, Output extends InventoryProjectionResult,
> { > {
readonly rawInput: unknown; readonly rawInput: unknown;
readonly schema: z.ZodType<Input>; readonly schema: z.ZodType<Input>;
readonly project: ( readonly project: (input: Input) => Output;
input: Input,
) => MobileApplicationProjectionWithSources<Output>;
readonly operation: string; readonly operation: string;
readonly predicateType: string; readonly predicateType: string;
readonly provider: EvidenceProvider; readonly provider: EvidenceProvider;
@@ -51,18 +43,17 @@ export const projectInventoryEvidence = <
Output extends InventoryProjectionResult, Output extends InventoryProjectionResult,
>( >(
options: InventoryProjectionOptions<Input, Output>, options: InventoryProjectionOptions<Input, Output>,
): Result<InventoryProjectionEvidenceResult, AnalysisError> => { ): Result<Evidence, AnalysisError> => {
const parsed = options.schema.safeParse(options.rawInput); const parsed = options.schema.safeParse(options.rawInput);
if (!parsed.success) if (!parsed.success)
return err( return err(
requestInputError(options.operation, parsed.error, options.rawInput), requestInputError(options.operation, parsed.error, options.rawInput),
); );
try { try {
const projected = options.project(parsed.data); const result = options.project(parsed.data);
const result = projected.projection; const first = parsed.data.inventory_evidence[0];
const first = projected.sources[0]; return ok(
return ok({ createEvidence(
evidence: createEvidence(
first?.subject === null || first?.subject === undefined first?.subject === null || first?.subject === undefined
? undefined ? undefined
: { : {
@@ -89,8 +80,7 @@ export const projectInventoryEvidence = <
evidenceLinks: result.source_evidence_ids, evidenceLinks: result.source_evidence_ids,
}, },
), ),
sources: projected.sources, );
});
} catch (cause: unknown) { } catch (cause: unknown) {
return err( return err(
cause instanceof TypeError || cause instanceof z.ZodError cause instanceof TypeError || cause instanceof z.ZodError
+15
View File
@@ -3,6 +3,7 @@ import {
createAnalysisProfile, createAnalysisProfile,
type AnalysisProfileCommitment, type AnalysisProfileCommitment,
} from "../domain/analysisProfile.js"; } from "../domain/analysisProfile.js";
/** Provider identity for deterministic composed analysis workflows. */ /** Provider identity for deterministic composed analysis workflows. */
export const REA_WORKFLOW_PROVIDER = { export const REA_WORKFLOW_PROVIDER = {
id: "rea-workflow", id: "rea-workflow",
@@ -111,6 +112,20 @@ export const JAVASCRIPT_APPLICATION_WORKFLOW_PROVIDER = {
version: "1", version: "1",
} as const; } as const;
/** Provider identity for deterministic cross-artifact comparisons. */
export const ARTIFACT_COMPARISON_PROVIDER = {
id: "rea-artifact-comparison",
name: "REA artifact comparison",
version: "1",
} as const;
/** Provider identity for deterministic cross-function comparisons. */
export const FUNCTION_COMPARISON_PROVIDER = {
id: "rea-function-comparison",
name: "REA function comparison",
version: "1",
} as const;
/** Provider identity for deterministic Evidence bundle comparisons. */ /** Provider identity for deterministic Evidence bundle comparisons. */
export const BUNDLE_COMPARISON_PROVIDER = { export const BUNDLE_COMPARISON_PROVIDER = {
id: "rea-bundle-comparison", id: "rea-bundle-comparison",
@@ -4,50 +4,6 @@ import {
requireAuthenticationHeadroom, requireAuthenticationHeadroom,
} from "./JsonAuthenticationHeadroom.js"; } from "./JsonAuthenticationHeadroom.js";
describe("JSON authentication headroom", () => { describe("JSON authentication headroom", () => {
it("does not queue every row while deciding whether a wide report can fit", () => {
let pending = 0;
let peak = 0;
const rows: { admission_probe: boolean }[] = [];
for (let index = 0; index < 512; index++) {
const row = {
get admission_probe() {
pending--;
return true;
},
};
// Observe reading a row separately from examining its scalar. A width
// queue reads every row first; an iterator finishes each row before advancing.
Object.defineProperty(rows, index, {
enumerable: true,
get: () => {
peak = Math.max(peak, ++pending);
if (pending > 32)
throw new Error(
"Admission queued report rows before refusing their allocation",
);
return row;
},
});
}
const value = { rows };
const result = requireAuthenticationHeadroom(value, {
operation: "inspect_managed_members",
path: "/tmp/parent.json",
description: "Cannot authenticate",
availableBytes: 0,
});
expect(pending).toBe(0);
expect(peak).toBe(1);
expect(result).toMatchObject({
ok: false,
error: {
_tag: "AnalysisResourceConstraintError",
reportedLimits: {
serialized_json_utf16_bytes: JSON.stringify(value).length * 2,
},
},
});
});
it("estimates parsed JSON at no less than its UTF-16 text", () => { it("estimates parsed JSON at no less than its UTF-16 text", () => {
const text = "x".repeat(1000); const text = "x".repeat(1000);
expect(estimateJsonValueBytes(text)).toBeGreaterThanOrEqual(2000); expect(estimateJsonValueBytes(text)).toBeGreaterThanOrEqual(2000);
@@ -86,28 +42,3 @@ describe("JSON authentication headroom", () => {
).toEqual({ ok: true, value: undefined }); ).toEqual({ ok: true, value: undefined });
}); });
}); });
it.each([
["control characters", { text: "\u0000\u0001\b\t\n\f\r".repeat(2000) }],
[
"escaped keys",
{ ['\u0000\\"'.repeat(2000)]: [true, false, null, -0, 1e30] },
],
["surrogate boundaries", { text: "😀\ud800x\udfff".repeat(2000) }],
])("reserves actual escaped JSON storage for %s", (_label, value) => {
const result = requireAuthenticationHeadroom(value, {
operation: "inspect_analysis_view",
path: "/tmp/bundle.json",
description: "Cannot authenticate bundle",
availableBytes: estimateJsonValueBytes(value) * 2 + 1,
});
expect(result).toMatchObject({
ok: false,
error: {
_tag: "AnalysisResourceConstraintError",
reportedLimits: {
serialized_json_utf16_bytes: JSON.stringify(value).length * 2,
},
},
});
});
+20 -58
View File
@@ -1,66 +1,32 @@
import { jsonStringCodeUnits } from "../domain/jsonSerialization.js";
import { getHeapStatistics } from "node:v8"; import { getHeapStatistics } from "node:v8";
import { AnalysisResourceConstraintError } from "../domain/analysisErrorCore.js"; import { AnalysisResourceConstraintError } from "../domain/analysisErrorCore.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import { err, ok, type Result } from "../domain/result.js"; import { err, ok, type Result } from "../domain/result.js";
/** Authentication clones parsed JSON and hashes its canonical commitment. */ /** Authentication clones parsed JSON and hashes its canonical commitment. */
const AUTHENTICATION_COPIES = 2; const AUTHENTICATION_COPIES = 2;
/** Parsed working copies and conservative UTF-16 storage for escaped JSON. */ /** Conservative bytes a parsed JSON value occupies: UTF-16 text plus container and slot overhead. */
const estimateJsonStorage = ( export const estimateJsonValueBytes = (value: unknown): number => {
value: unknown, let bytes = 0;
): { valueBytes: number; serializedUtf16Bytes: number } => { const stack: unknown[] = [value];
let valueBytes = 0;
let serializedCharacters = 0;
const stack: Iterator<unknown>[] = [[value][Symbol.iterator]()];
while (stack.length > 0) { while (stack.length > 0) {
const next = stack[stack.length - 1]!.next(); const item = stack.pop();
if (next.done) { if (typeof item === "string") bytes += 64 + item.length * 2;
stack.pop(); else if (Array.isArray(item)) {
continue; bytes += 128 + item.length * 16;
} for (const entry of item) stack.push(entry);
const item = next.value;
if (typeof item === "string") {
valueBytes += 64 + item.length * 2;
serializedCharacters += jsonStringCodeUnits(item);
} else if (Array.isArray(item)) {
valueBytes += 128 + item.length * 16;
serializedCharacters += 2 + Math.max(0, item.length - 1);
stack.push(item.values());
} else if (typeof item === "object" && item !== null) { } else if (typeof item === "object" && item !== null) {
valueBytes += 128; bytes += 128;
serializedCharacters += 2; for (const [key, entry] of Object.entries(item)) {
stack.push( bytes += 64 + key.length * 2;
(function* () { stack.push(entry);
let count = 0; }
for (const key in item) { } else bytes += 16;
if (!Object.hasOwn(item, key)) continue;
valueBytes += 64 + key.length * 2;
serializedCharacters +=
jsonStringCodeUnits(key) + 1 + (count++ === 0 ? 0 : 1);
yield (item as Record<string, unknown>)[key];
}
})(),
);
} else {
valueBytes += 16;
serializedCharacters +=
typeof item === "number"
? String(item).length
: item === true
? 4
: item === false
? 5
: 4;
}
} }
return { valueBytes, serializedUtf16Bytes: serializedCharacters * 2 }; return bytes;
}; };
/** Conservative parsed JSON bytes, independent of its escaped representation. */
export const estimateJsonValueBytes = (value: unknown): number =>
estimateJsonStorage(value).valueBytes;
/** Refuse before authentication when live heap headroom cannot hold its validation and commitment working copies. */ /** Refuse before authentication when live heap headroom cannot hold its validation and commitment working copies. */
export const requireAuthenticationHeadroom = ( export const requireAuthenticationHeadroom = (
value: unknown, value: unknown,
@@ -70,14 +36,11 @@ export const requireAuthenticationHeadroom = (
readonly description: string; readonly description: string;
readonly availableBytes?: number; readonly availableBytes?: number;
}, },
): Result<void, AnalysisResourceConstraintError> => { ): Result<void, AnalysisError> => {
const availableBytes = const availableBytes =
options.availableBytes ?? getHeapStatistics().total_available_size; options.availableBytes ?? getHeapStatistics().total_available_size;
const { valueBytes, serializedUtf16Bytes } = estimateJsonStorage(value); const valueBytes = estimateJsonValueBytes(value);
// Canonical hashing and manifest normalization can still encode scalars or const requiredBytes = valueBytes * AUTHENTICATION_COPIES;
// metadata; account for their escaped representations before allocating them.
const requiredBytes =
(valueBytes + serializedUtf16Bytes) * AUTHENTICATION_COPIES;
if (requiredBytes <= availableBytes) return ok(undefined); if (requiredBytes <= availableBytes) return ok(undefined);
return err( return err(
new AnalysisResourceConstraintError( new AnalysisResourceConstraintError(
@@ -87,7 +50,6 @@ export const requireAuthenticationHeadroom = (
{ {
input_path: options.path, input_path: options.path,
projected_value_bytes: valueBytes, projected_value_bytes: valueBytes,
serialized_json_utf16_bytes: serializedUtf16Bytes,
authentication_bytes: requiredBytes, authentication_bytes: requiredBytes,
available_heap_bytes: availableBytes, available_heap_bytes: availableBytes,
}, },
+4 -8
View File
@@ -52,14 +52,13 @@ export const readJsonFile = async (
path: string, path: string,
signal?: AbortSignal, signal?: AbortSignal,
resources?: ArtifactResourceScope, resources?: ArtifactResourceScope,
operation = "read_evidence_file",
): Promise< ): Promise<
Result< Result<
unknown, unknown,
EvidenceFileError | AnalysisResourceConstraintError | AnalysisCancelledError EvidenceFileError | AnalysisResourceConstraintError | AnalysisCancelledError
> >
> => { > => {
const read = await readJsonFileWithSource(path, signal, resources, operation); const read = await readJsonFileWithSource(path, signal, resources);
return read.ok ? ok(read.value.data) : read; return read.ok ? ok(read.value.data) : read;
}; };
@@ -68,7 +67,6 @@ export const readJsonFileWithSource = (
path: string, path: string,
signal?: AbortSignal, signal?: AbortSignal,
resources?: ArtifactResourceScope, resources?: ArtifactResourceScope,
operation = "read_evidence_file",
): Promise< ): Promise<
Result< Result<
{ readonly data: unknown; readonly path: string }, { readonly data: unknown; readonly path: string },
@@ -80,14 +78,13 @@ export const readJsonFileWithSource = (
async (selected) => { async (selected) => {
const read = await readJsonInputFile( const read = await readJsonInputFile(
selected, selected,
operation, "read_evidence_file",
signal, signal,
resources, resources,
); );
return read.ok ? ok({ data: read.value, path: selected }) : read; return read.ok ? ok({ data: read.value, path: selected }) : read;
}, },
signal, signal,
operation,
); );
/** Read selected JSON members through the same regular-file error boundary. */ /** Read selected JSON members through the same regular-file error boundary. */
@@ -127,7 +124,6 @@ const readJsonFileInput = async <Value>(
> >
>, >,
signal?: AbortSignal, signal?: AbortSignal,
operation = "read_evidence_file",
): Promise< ): Promise<
Result< Result<
Value, Value,
@@ -157,7 +153,7 @@ const readJsonFileInput = async <Value>(
if (cause instanceof AnalysisResourceConstraintError) return err(cause); if (cause instanceof AnalysisResourceConstraintError) return err(cause);
if (cause instanceof JsonInputCancellationFailure) if (cause instanceof JsonInputCancellationFailure)
return err( return err(
new AnalysisCancelledError(operation, { new AnalysisCancelledError("read_evidence_file", {
cause: cause.reason, cause: cause.reason,
}), }),
); );
@@ -170,7 +166,7 @@ const readJsonFileInput = async <Value>(
); );
if (isJsonReadCancellation(cause, signal)) if (isJsonReadCancellation(cause, signal))
return err( return err(
new AnalysisCancelledError(operation, { new AnalysisCancelledError("read_evidence_file", {
cause: signal?.reason, cause: signal?.reason,
}), }),
); );
@@ -35,13 +35,7 @@ import { MANAGED_WORKFLOW_PROVIDER } from "./InvestigationProviders.js";
import { createProcessCaptureEvidence } from "./process/ProcessEvidence.js"; import { createProcessCaptureEvidence } from "./process/ProcessEvidence.js";
import { deriveReconstructionObligationCandidates } from "./ReconstructionObligationCandidates.js"; import { deriveReconstructionObligationCandidates } from "./ReconstructionObligationCandidates.js";
const applicationWithUnknowns = ( const applicationWithUnknowns = (keys: readonly string[]) => {
keys: readonly string[],
{
kind = "unknown",
label = "Unclassified application boundary",
}: { kind?: "unknown" | "package"; label?: string } = {},
) => {
const source = createEvidence( const source = createEvidence(
undefined, undefined,
{ id: "fixture-source", name: "Fixture source", version: "1" }, { id: "fixture-source", name: "Fixture source", version: "1" },
@@ -56,7 +50,7 @@ const applicationWithUnknowns = (
const base = buildSyntheticJavaScriptApplicationGraph(); const base = buildSyntheticJavaScriptApplicationGraph();
const unknownNodes = keys.map((key) => const unknownNodes = keys.map((key) =>
createJavaScriptApplicationNode({ createJavaScriptApplicationNode({
kind, kind: "unknown",
identity: { identity: {
strategy: "artifact-local-key", strategy: "artifact-local-key",
stability: "artifact-version", stability: "artifact-version",
@@ -66,7 +60,7 @@ const applicationWithUnknowns = (
}, },
observations: [ observations: [
{ {
label, label: "Unclassified application boundary",
properties: {}, properties: {},
evidence: artifactEvidence( evidence: artifactEvidence(
APPLICATION_GRAPH_DIGESTS.asar, APPLICATION_GRAPH_DIGESTS.asar,
@@ -150,22 +144,6 @@ describe("reconstruction obligation candidates", () => {
).toBe(false); ).toBe(false);
}); });
it("falls back to a kind-based title when an application node label is blank", () => {
const {
unknownNodes: [node],
generated,
} = applicationWithUnknowns(["blank-label"], {
kind: "package",
label: " ",
});
expect(
generated.candidates.find(
({ target }) => target.application_node_id === node?.node_id,
)?.title,
).toBe("package boundary");
});
it("does not bind an unknown process image to a packaged artifact obligation", () => { it("does not bind an unknown process image to a packaged artifact obligation", () => {
const scenario = parseProcessScenario({ const scenario = parseProcessScenario({
executable: "/path/whose-launch-identity-is-unknown", executable: "/path/whose-launch-identity-is-unknown",
@@ -349,87 +327,3 @@ it("keeps each semantic candidate bound to its own file and authority context",
}); });
} }
}); });
it("falls back to a kind-based title when a semantic node label is blank", () => {
const contexts = new JavaScriptSemanticEvidenceContextRegistry();
const base = javascriptApplicationAnalysisResultSchema.parse(
JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE.normalized_result,
);
const source = proofEvidence("blank-label-file");
const module = createJavaScriptSemanticGraphNode(
{
kind: "module",
identity: {
artifact_sha256: base.root_artifact_sha256,
module_path: "app.js",
source_range: null,
role_key: "module",
},
function_node_id: null,
application_node_ids: [],
label: "app.js",
properties: {},
evidence: artifactEvidence(base.root_artifact_sha256, "app.js"),
},
contexts,
);
const sha256 = "4".repeat(64);
const request = createJavaScriptSemanticGraphNode(
{
kind: "request",
identity: {
artifact_sha256: sha256,
module_path: "file.js",
source_range: {
start: { line: 1, column: 0 },
end: { line: 1, column: 8 },
},
role_key: "request-1",
},
function_node_id: null,
application_node_ids: [],
label: " ",
properties: {},
evidence: {
...artifactEvidence(sha256, "file.js", "ast-static-analysis"),
evidence_ids: [source.evidence_id],
},
},
contexts,
);
const { graph_id: _graphId, ...semantic } = base.semantic_graph;
const semanticGraph = createJavaScriptSemanticGraph({
...semantic,
root_node_ids: [module.node_id],
nodes: [module, request],
evidence_contexts: contexts.contexts,
});
const example = JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE;
const application = createEvidence(
{
path: base.input_path,
sha256: base.root_artifact_sha256,
format: base.format,
},
example.provider,
{
predicateType: example.predicate_type,
operation: example.operation,
parameters: example.parameters,
result: jsonValueSchema.parse({ ...base, semantic_graph: semanticGraph }),
confidence: "derived",
authority: "shipped-artifact",
evidenceLinks: [source.evidence_id],
},
);
const generated = deriveReconstructionObligationCandidates(
createEvidenceBundle([source, application]),
[],
);
expect(
generated.candidates.find(
({ target }) => target.semantic_node_id === request.node_id,
)?.title,
).toBe("request semantic boundary");
});
@@ -114,7 +114,7 @@ const deriveApplicationCandidates = (
candidates, candidates,
generatedCandidate({ generatedCandidate({
key: `application:${node.node_id}:${candidatePolicy.family}`, key: `application:${node.node_id}:${candidatePolicy.family}`,
title: node.observations[0]?.label?.trim() || `${node.kind} boundary`, title: node.observations[0]?.label ?? `${node.kind} boundary`,
policy: candidatePolicy, policy: candidatePolicy,
artifactSha256: source.rootArtifactSha256, artifactSha256: source.rootArtifactSha256,
applicationNodeId: node.node_id, applicationNodeId: node.node_id,
@@ -143,7 +143,7 @@ const deriveApplicationCandidates = (
candidates, candidates,
generatedCandidate({ generatedCandidate({
key: `semantic:${node.node_id}:${candidatePolicy.family}`, key: `semantic:${node.node_id}:${candidatePolicy.family}`,
title: node.label?.trim() || `${node.kind} semantic boundary`, title: node.label ?? `${node.kind} semantic boundary`,
policy: candidatePolicy, policy: candidatePolicy,
artifactSha256: node.identity.artifact_sha256, artifactSha256: node.identity.artifact_sha256,
applicationNodeId: node.application_node_ids[0] ?? null, applicationNodeId: node.application_node_ids[0] ?? null,
+40 -11
View File
@@ -1,11 +1,9 @@
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js"; import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
import { import {
createHistoricalSourceGraph, createHistoricalSourceGraph,
historicalSourceInventoryHasPartialFacts,
historicalSourceLanguages, historicalSourceLanguages,
historicalSourceManifests, historicalSourceManifests,
historicalSourceParseFailureKey, historicalSourceParseFailureKey,
historicalSourceRelationshipKey,
type HistoricalSourceGraph, type HistoricalSourceGraph,
type HistoricalSourceGraphInput, type HistoricalSourceGraphInput,
} from "../domain/referenceSourceGraph.js"; } from "../domain/referenceSourceGraph.js";
@@ -47,13 +45,18 @@ const cancelled = (
const isAborted = (signal?: AbortSignal): boolean => signal?.aborted === true; const isAborted = (signal?: AbortSignal): boolean => signal?.aborted === true;
const relationshipKey = (
relationship: HistoricalSourceGraphInput["relationships"][number],
): string =>
`${relationship.from_path}\u0000${relationship.to}\u0000${relationship.kind}\u0000${relationship.resolution}\u0000${relationship.parse_state}`;
const deduplicateRelationships = ( const deduplicateRelationships = (
relationships: HistoricalSourceGraphInput["relationships"], relationships: HistoricalSourceGraphInput["relationships"],
): HistoricalSourceGraphInput["relationships"] => { ): HistoricalSourceGraphInput["relationships"] => {
const seen = new Set<string>(); const seen = new Set<string>();
const result: HistoricalSourceGraphInput["relationships"] = []; const result: HistoricalSourceGraphInput["relationships"] = [];
for (const relationship of relationships) { for (const relationship of relationships) {
const key = historicalSourceRelationshipKey(relationship); const key = relationshipKey(relationship);
if (seen.has(key)) continue; if (seen.has(key)) continue;
seen.add(key); seen.add(key);
result.push(relationship); result.push(relationship);
@@ -90,6 +93,37 @@ const buildProvenance = (
caller: options.caller, caller: options.caller,
}); });
const deriveInventoryState = (
input: Pick<
HistoricalSourceGraphInput,
| "entries"
| "relationships"
| "parse_failures"
| "exclusions"
| "limitations"
>,
): "complete" | "partial" | "unknown" => {
if (input.limitations.length > 0) return "partial";
if (input.exclusions.length > 0) return "partial";
if (input.parse_failures.length > 0) return "partial";
const partialEntry = input.entries.some(
(entry) =>
entry.limitations.length > 0 ||
(entry.kind === "file" && entry.content_state !== "hashed") ||
(entry.kind === "directory" && entry.tree_state !== "enumerated") ||
(entry.kind === "symlink" && entry.target_state !== "internal") ||
entry.kind === "unknown",
);
if (partialEntry) return "partial";
const partialRelationship = input.relationships.some(
({ parse_state, resolution }) =>
parse_state !== "parsed" ||
["unresolved", "unknown"].includes(resolution),
);
if (partialRelationship) return "partial";
return "complete";
};
const sortExclusions = ( const sortExclusions = (
exclusions: HistoricalSourceGraphInput["exclusions"], exclusions: HistoricalSourceGraphInput["exclusions"],
): HistoricalSourceGraphInput["exclusions"] => ): HistoricalSourceGraphInput["exclusions"] =>
@@ -227,21 +261,16 @@ export const importReferenceSource = async (
schema: "HistoricalSourceGraph", schema: "HistoricalSourceGraph",
authority: "historical-reference", authority: "historical-reference",
root_alias: "$REFERENCE_ROOT", root_alias: "$REFERENCE_ROOT",
inventory_state: historicalSourceInventoryHasPartialFacts({ inventory_state: deriveInventoryState({
entries: sortedEntries, entries: sortedEntries,
relationships: uniqueRelationships, relationships: uniqueRelationships,
parse_failures: uniqueFailures, parse_failures: uniqueFailures,
exclusions: sortedExclusions, exclusions: sortedExclusions,
limitations: sortedLimitations, limitations: sortedLimitations,
}) }),
? "partial"
: "complete",
entries: sortedEntries, entries: sortedEntries,
relationships: uniqueRelationships.sort((left, right) => relationships: uniqueRelationships.sort((left, right) =>
compareUnicodeCodePoints( compareUnicodeCodePoints(relationshipKey(left), relationshipKey(right)),
historicalSourceRelationshipKey(left),
historicalSourceRelationshipKey(right),
),
), ),
parse_failures: uniqueFailures, parse_failures: uniqueFailures,
exclusions: sortedExclusions, exclusions: sortedExclusions,
+7 -2
View File
@@ -9,7 +9,6 @@ import {
RegularFileCleanupFailure, RegularFileCleanupFailure,
retryRegularFileCleanup, retryRegularFileCleanup,
} from "./RegularFileRead.js"; } from "./RegularFileRead.js";
import { referenceSourceResourceFailure } from "../reference/ReferenceSourceReaderErrors.js";
import type { ReferenceSourceReaderError } from "../reference/ReferenceSourceReaderTypes.js"; import type { ReferenceSourceReaderError } from "../reference/ReferenceSourceReaderTypes.js";
export type ReferenceSourceVcsInfo = export type ReferenceSourceVcsInfo =
@@ -60,7 +59,13 @@ export const readReferenceSourceVcs = async (
}); });
} catch (cause: unknown) { } catch (cause: unknown) {
if (!(cause instanceof ArtifactReaderFailure)) throw cause; if (!(cause instanceof ArtifactReaderFailure)) throw cause;
return err(referenceSourceResourceFailure(cause)); return err({
tag: "reference-source-reader",
code: cause.reason === "cancelled" ? "cancelled" : "io",
message: cause.message,
...(cause.cleanup === undefined ? {} : { cleanup: cause.cleanup }),
cause,
});
} }
}; };
+40 -13
View File
@@ -405,20 +405,36 @@ export const clientConfigurationAligned = async (
} }
const original = await readRegularFileText(client.configPath); const original = await readRegularFileText(client.configPath);
const parsed = parseClientConfiguration(original, client.format); const parsed = parseClientConfiguration(original, client.format);
const desired = await clientConfigurationDesired( const policy = await readClientPolicyBlock(
client, client,
providerEnvironment,
command,
parsed, parsed,
files.value, await clientConfigurationDesired(
resolveProviderRequestBudget, client,
providerEnvironment,
command,
parsed,
files.value,
resolveProviderRequestBudget,
),
); );
const policy = await readClientPolicyBlock(client, parsed, desired);
if (!policy.ok) { if (!policy.ok) {
throwCleanupFailure(policy.error); throwCleanupFailure(policy.error);
return false; return false;
} }
return policy.value === undefined && registrationCurrent(parsed, desired); return (
policy.value === undefined &&
registrationCurrent(
parsed,
await clientConfigurationDesired(
client,
providerEnvironment,
command,
parsed,
files.value,
resolveProviderRequestBudget,
),
)
);
} catch (cause: unknown) { } catch (cause: unknown) {
if (cause instanceof RegularFileCleanupFailure) throw cause; if (cause instanceof RegularFileCleanupFailure) throw cause;
// Unreadable configuration is treated as not aligned so setup repairs it. // Unreadable configuration is treated as not aligned so setup repairs it.
@@ -528,15 +544,18 @@ const inspectClientDocument = async (
} }
try { try {
const parsed = parseClientConfiguration(original, client.format); const parsed = parseClientConfiguration(original, client.format);
const desired = await clientConfigurationDesired( const policy = await readClientPolicyBlock(
client, client,
providerEnvironment,
command,
parsed, parsed,
files.value, await clientConfigurationDesired(
resolveProviderRequestBudget, client,
providerEnvironment,
command,
parsed,
files.value,
resolveProviderRequestBudget,
),
); );
const policy = await readClientPolicyBlock(client, parsed, desired);
if (!policy.ok) { if (!policy.ok) {
throwCleanupFailure(policy.error); throwCleanupFailure(policy.error);
return { return {
@@ -546,6 +565,14 @@ const inspectClientDocument = async (
} }
if (policy.value !== undefined) if (policy.value !== undefined)
return { status: "invalid", remediation: policy.value }; return { status: "invalid", remediation: policy.value };
const desired = await clientConfigurationDesired(
client,
providerEnvironment,
command,
parsed,
files.value,
resolveProviderRequestBudget,
);
if (registrationCurrent(parsed, desired)) if (registrationCurrent(parsed, desired))
return { status: "already_current" }; return { status: "already_current" };
const fallback = await readZcodeFallbackServers(client, parsed); const fallback = await readZcodeFallbackServers(client, parsed);
+1
View File
@@ -53,6 +53,7 @@ import {
redirectedWriteBlocker, redirectedWriteBlocker,
type MsixPackageView, type MsixPackageView,
} from "./MsixPackageStorage.js"; } from "./MsixPackageStorage.js";
export { isUninstallFailure } from "./UninstallFailure.js";
interface ManagedPathStats { interface ManagedPathStats {
readonly uid?: number; readonly uid?: number;
+1
View File
@@ -3,6 +3,7 @@ import { gt, valid } from "semver";
import { PRODUCT_IDENTITY } from "../identity.js"; import { PRODUCT_IDENTITY } from "../identity.js";
import type { Result } from "../domain/result.js"; import type { Result } from "../domain/result.js";
import type { IntegrationMaintenance } from "./UpdateMaintenance.js"; import type { IntegrationMaintenance } from "./UpdateMaintenance.js";
export { isUpdateFailure } from "./UpdateFailure.js";
/** Global npm installation that owns the running package and its entry point. */ /** Global npm installation that owns the running package and its entry point. */
export interface NpmInstallation { export interface NpmInstallation {
+10
View File
@@ -1,5 +1,6 @@
import { closeOwnedWebResources } from "./closeOwnedWebResources.js"; import { closeOwnedWebResources } from "./closeOwnedWebResources.js";
import { isDeepStrictEqual } from "node:util"; import { isDeepStrictEqual } from "node:util";
import { isAbsolute } from "node:path";
import { z } from "zod"; import { z } from "zod";
import type { AnalysisError } from "../domain/analysisErrorBase.js"; import type { AnalysisError } from "../domain/analysisErrorBase.js";
import { import {
@@ -93,6 +94,15 @@ export class WebModuleTraceService {
}), }),
); );
const input = parsed.data; const input = parsed.data;
for (const [path, value] of [
["manifest_path", input.manifest_path],
["import_map.path", input.import_map?.path],
] as const)
if (value !== undefined && !isAbsolute(value))
return invalid(
path.split("."),
"Expected an absolute filesystem path on this host.",
);
for (const [path, value] of [ for (const [path, value] of [
["importer_url", input.importer_url], ["importer_url", input.importer_url],
["import_map.base_url", input.import_map?.base_url], ["import_map.base_url", input.import_map?.base_url],
@@ -43,30 +43,6 @@ it.each(["REDACTED", "[", "]", "/capture.har"])(
}, },
); );
it("records an explicit capture deadline and omits the default", async () => {
const seen: Array<number | undefined> = [];
const service = new WebNetworkCaptureService({
inspect: (input) => {
seen.push(input.capture_timeout_ms);
return Promise.resolve(ok(fixture()));
},
});
const explicit = await service.inspect({
capture_path: "/capture.har",
format: "har",
capture_timeout_ms: 250,
});
if (!explicit.ok) throw explicit.error;
expect(explicit.value.parameters.capture_timeout_ms).toBe(250);
const omitted = await service.inspect({
capture_path: "/capture.har",
format: "har",
});
if (!omitted.ok) throw omitted.error;
expect(omitted.value.parameters).not.toHaveProperty("capture_timeout_ms");
expect(seen).toEqual([250, undefined]);
});
it("keeps observed artifact identity when the entire sensitive path is excluded", async () => { it("keeps observed artifact identity when the entire sensitive path is excluded", async () => {
const result = await new WebNetworkCaptureService({ const result = await new WebNetworkCaptureService({
inspect: () => Promise.resolve(ok(fixture())), inspect: () => Promise.resolve(ok(fixture())),
+24 -3
View File
@@ -1,6 +1,7 @@
import { redactExplicitFailure } from "../domain/explicitSensitiveFailure.js"; import { redactExplicitFailure } from "../domain/explicitSensitiveFailure.js";
import { excludeCaptureCoordinates } from "../domain/webNetworkCaptureCoordinates.js"; import { excludeCaptureCoordinates } from "../domain/webNetworkCaptureCoordinates.js";
import { redactExplicitText } from "../domain/explicitSensitiveValues.js"; import { redactExplicitText } from "../domain/explicitSensitiveValues.js";
import { isAbsolute } from "node:path";
import type { ExecutionOptions } from "./AnalysisProvider.js"; import type { ExecutionOptions } from "./AnalysisProvider.js";
import type { WebNetworkCapturePort } from "./WebNetworkCapturePort.js"; import type { WebNetworkCapturePort } from "./WebNetworkCapturePort.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js"; import type { AnalysisError } from "../domain/analysisErrorBase.js";
@@ -56,6 +57,16 @@ export class WebNetworkCaptureService {
}), }),
); );
const input = parsed.data; const input = parsed.data;
if (!isAbsolute(input.capture_path))
return failure(
new AnalysisInputError(OPERATION, undefined, [
{
path: ["capture_path"],
reason: "invalid_format",
message: "Expected an absolute filesystem path on this host.",
},
]),
);
if ( if (
input.record_ordinals !== undefined && input.record_ordinals !== undefined &&
new Set(input.record_ordinals).size !== input.record_ordinals.length new Set(input.record_ordinals).size !== input.record_ordinals.length
@@ -80,6 +91,19 @@ export class WebNetworkCaptureService {
}, },
]), ]),
); );
if (
input.protobuf?.schema !== undefined &&
!isAbsolute(input.protobuf.schema.descriptor_set_path)
)
return failure(
new AnalysisInputError(OPERATION, undefined, [
{
path: ["protobuf", "schema", "descriptor_set_path"],
reason: "invalid_format",
message: "Expected an absolute descriptor-set path on this host.",
},
]),
);
const loaded = await this.port.inspect(input, options); const loaded = await this.port.inspect(input, options);
if (!loaded.ok) { if (!loaded.ok) {
const partial = webNetworkCaptureSchema.safeParse( const partial = webNetworkCaptureSchema.safeParse(
@@ -226,9 +250,6 @@ export class WebNetworkCaptureService {
}, },
}), }),
sensitive_value_count: input.sensitive_values.length, sensitive_value_count: input.sensitive_values.length,
...(input.capture_timeout_ms === undefined
? {}
: { capture_timeout_ms: input.capture_timeout_ms }),
}); });
return ok( return ok(
createEvidence( createEvidence(
@@ -36,7 +36,6 @@ export interface WebSourceMapPort {
readonly path: string; readonly path: string;
readonly url: string; readonly url: string;
readonly position: WebSourceLocationInput["generated_position"]; readonly position: WebSourceLocationInput["generated_position"];
readonly decodeTimeoutMs?: number;
}, },
options?: ExecutionOptions, options?: ExecutionOptions,
): Promise<Result<WebSourceMapReport, AnalysisError>>; ): Promise<Result<WebSourceMapReport, AnalysisError>>;
@@ -11,55 +11,12 @@ import {
AnalysisOutputError, AnalysisOutputError,
} from "../domain/analysisErrorCore.js"; } from "../domain/analysisErrorCore.js";
import { ok } from "../domain/result.js"; import { ok } from "../domain/result.js";
import { import { webSourceLocationResultSchema } from "../domain/webSourceLocation.js";
DEFAULT_WEB_SOURCE_MAP_DECODE_TIMEOUT_MS,
webSourceLocationResultSchema,
} from "../domain/webSourceLocation.js";
import { WebSourceLocationService } from "./WebSourceLocationService.js"; import { WebSourceLocationService } from "./WebSourceLocationService.js";
import type { WebSourceMapPort } from "./WebSourceLocationPorts.js"; import type { WebSourceMapPort } from "./WebSourceLocationPorts.js";
const service = (data = webSourceLocationFixture(), port = decoder) => const service = (data = webSourceLocationFixture(), port = decoder) =>
new WebSourceLocationService({ load: () => Promise.resolve(ok(data)) }, port); new WebSourceLocationService({ load: () => Promise.resolve(ok(data)) }, port);
const expectCodecDeadline = (
evidence: {
readonly limitations: readonly string[];
readonly normalized_result: unknown;
},
timeoutMs: number,
): void => {
const phrase = `${String(timeoutMs)} ms decode_timeout_ms deadline`;
const result = webSourceLocationResultSchema.parse(
evidence.normalized_result,
);
expect(
evidence.limitations.filter((text) => text.includes(phrase)),
).toHaveLength(1);
expect(
result.limitations.filter((text) => text.includes(phrase)),
).toHaveLength(1);
};
it("passes an explicit decode deadline and omits the default from Evidence", async () => {
const seen: Array<number | undefined> = [];
const port: WebSourceMapPort = {
trace: (input) => {
seen.push(input.decodeTimeoutMs);
return decoder.trace(input);
},
};
const explicit = await service(undefined, port).trace({
...args,
decode_timeout_ms: 250,
});
if (!explicit.ok) throw explicit.error;
expect(explicit.value.parameters.decode_timeout_ms).toBe(250);
expectCodecDeadline(explicit.value, 250);
const omitted = await service(undefined, port).trace(args);
if (!omitted.ok) throw omitted.error;
expect(omitted.value.parameters).not.toHaveProperty("decode_timeout_ms");
expectCodecDeadline(omitted.value, DEFAULT_WEB_SOURCE_MAP_DECODE_TIMEOUT_MS);
expect(seen).toEqual([250, undefined]);
});
it.each([ it.each([
{ ...args, manifest_path: "relative.json" }, { ...args, manifest_path: "relative.json" },
{ ...args, script_index: -1 }, { ...args, script_index: -1 },
+16 -14
View File
@@ -1,5 +1,6 @@
import { closeOwnedWebResources } from "./closeOwnedWebResources.js"; import { closeOwnedWebResources } from "./closeOwnedWebResources.js";
import { createHash } from "node:crypto"; import { createHash } from "node:crypto";
import { isAbsolute } from "node:path";
import { z } from "zod"; import { z } from "zod";
import type { AnalysisError } from "../domain/analysisErrorBase.js"; import type { AnalysisError } from "../domain/analysisErrorBase.js";
import { import {
@@ -16,7 +17,6 @@ import {
webArtifactFileSchema, webArtifactFileSchema,
} from "../domain/webScriptArtifacts.js"; } from "../domain/webScriptArtifacts.js";
import { import {
DEFAULT_WEB_SOURCE_MAP_DECODE_TIMEOUT_MS,
WEB_SOURCE_MAP_LIMITS, WEB_SOURCE_MAP_LIMITS,
webSourceLocationInputSchema, webSourceLocationInputSchema,
webSourceLocationResultSchema, webSourceLocationResultSchema,
@@ -52,19 +52,17 @@ const LIMITATIONS = [
"Lookup returns all segments at the greatest generated position not greater than the selected point, including earlier lines and generated-only segments. A mapped point does not establish an executed range or UI causality.", "Lookup returns all segments at the greatest generated position not greater than the selected point, including earlier lines and generated-only segments. A mapped point does not establish an executed range or UI causality.",
"Regular maps and inline indexed sections are supported. External section URLs are unsupported; unknown extensions are retained in raw map text and are not interpreted.", "Regular maps and inline indexed sections are supported. External section URLs are unsupported; unknown extensions are retained in raw map text and are not interpreted.",
]; ];
const codecBudget = (timeoutMs: number): string => const CODEC_BUDGET =
`Complete evidence has a 4 MiB map, 32 MiB reply, 262144 decoded rows/segments and 64-level section budget. The owned codec has a ${String(timeoutMs)} ms decode_timeout_ms deadline`; "Complete evidence has a 4 MiB map, 32 MiB reply, 262144 decoded rows/segments and 64-level section budget. The owned codec has a 20-second deadline";
/** Bun accepts but ignores the codec's V8 heap flags; Windows enforces a commit limit instead. */ /** Bun accepts but ignores the codec's V8 heap flags; Windows enforces a commit limit instead. */
const codecBudgetLimitation = ( const codecBudgetLimitation = (
runtime: WebSourceMapReport["runtime"], runtime: WebSourceMapReport["runtime"],
timeoutMs: number,
): string => { ): string => {
const budget = codecBudget(timeoutMs);
if (runtime.id === "node-v8") if (runtime.id === "node-v8")
return `${budget}, ${String(WEB_SOURCE_MAP_LIMITS.codecHeapMiB)} MiB old-generation heap and one V8 worker; cleanup may extend the deadline. V8 heap limits do not establish aggregate RSS limits.`; return `${CODEC_BUDGET}, ${String(WEB_SOURCE_MAP_LIMITS.codecHeapMiB)} MiB old-generation heap and one V8 worker; cleanup may extend the deadline. V8 heap limits do not establish aggregate RSS limits.`;
return runtime.process_memory_limit_bytes === null return runtime.process_memory_limit_bytes === null
? `${budget}; cleanup may extend the deadline. The codec ran on Bun, which does not enforce REA's V8 heap flags, so no codec heap limit is established.` ? `${CODEC_BUDGET}; cleanup may extend the deadline. The codec ran on Bun, which does not enforce REA's V8 heap flags, so no codec heap limit is established.`
: `${budget}; cleanup may extend the deadline. The codec ran on Bun, which does not enforce REA's V8 heap flags; a Windows Job Object limited its committed memory to ${String(runtime.process_memory_limit_bytes)} bytes, which is not a heap or RSS limit.`; : `${CODEC_BUDGET}; cleanup may extend the deadline. The codec ran on Bun, which does not enforce REA's V8 heap flags; a Windows Job Object limited its committed memory to ${String(runtime.process_memory_limit_bytes)} bytes, which is not a heap or RSS limit.`;
}; };
/** Join verified captured bytes with an explicitly selected source-map point. */ /** Join verified captured bytes with an explicitly selected source-map point. */
@@ -127,20 +125,15 @@ export class WebSourceLocationService {
path: map.file.path, path: map.file.path,
url: map.url, url: map.url,
position: input.generated_position, position: input.generated_position,
...(input.decode_timeout_ms === undefined
? {}
: { decodeTimeoutMs: input.decode_timeout_ms }),
}, },
options, options,
); );
if (!traced.ok) return traced; if (!traced.ok) return traced;
const report = verifyReport(input, map.file.sha256, traced.value); const report = verifyReport(input, map.file.sha256, traced.value);
if (!report.ok) return report; if (!report.ok) return report;
const decodeTimeoutMs =
input.decode_timeout_ms ?? DEFAULT_WEB_SOURCE_MAP_DECODE_TIMEOUT_MS;
const limitations = [ const limitations = [
...LIMITATIONS, ...LIMITATIONS,
codecBudgetLimitation(report.value.runtime, decodeTimeoutMs), codecBudgetLimitation(report.value.runtime),
]; ];
const normalized = webSourceLocationResultSchema.safeParse({ const normalized = webSourceLocationResultSchema.safeParse({
...report.value, ...report.value,
@@ -208,6 +201,15 @@ const invalid = (
const admitContext = ( const admitContext = (
input: WebSourceLocationInput, input: WebSourceLocationInput,
): Result<null, AnalysisError> => { ): Result<null, AnalysisError> => {
for (const [path, value] of [
["manifest_path", input.manifest_path],
["source_map.path", input.source_map.path],
] as const)
if (!isAbsolute(value))
return invalid(
path.split("."),
"Expected an absolute filesystem path on this host.",
);
if (!URL.canParse(input.source_map.url)) if (!URL.canParse(input.source_map.url))
return invalid( return invalid(
["source_map", "url"], ["source_map", "url"],
@@ -1,6 +1,5 @@
import { setImmediate } from "node:timers/promises"; import { setImmediate } from "node:timers/promises";
import { INSPECT_ANALYSIS_VIEW_OPERATION } from "../../domain/analysisView/analysisViewErrors.js"; import { INSPECT_ANALYSIS_VIEW_OPERATION } from "../../domain/analysisView/analysisViewErrors.js";
import { jsonValueSchema } from "../../domain/jsonValue.js";
import { z } from "zod"; import { z } from "zod";
import { ANALYSIS_VIEW_PROVIDER } from "../InvestigationProviders.js"; import { ANALYSIS_VIEW_PROVIDER } from "../InvestigationProviders.js";
@@ -26,6 +25,7 @@ import {
type AnalysisViewRequest, type AnalysisViewRequest,
type InspectAnalysisViewInput, type InspectAnalysisViewInput,
} from "../../domain/analysisView/analysisView.js"; } from "../../domain/analysisView/analysisView.js";
import { jsonValueSchema } from "../../domain/jsonValue.js";
import { import {
immutableJsonSnapshotMember, immutableJsonSnapshotMember,
isImmutableJsonSnapshot, isImmutableJsonSnapshot,
@@ -332,44 +332,39 @@ const createAnalysisViewEvidence = (
? {} ? {}
: { architecture: parent.subject.architecture }), : { architecture: parent.subject.architecture }),
}; };
return createEvidence( return createEvidence(target, ANALYSIS_VIEW_PROVIDER, {
target, predicateType: "rea.analysis-view",
ANALYSIS_VIEW_PROVIDER, operation: INSPECT_ANALYSIS_VIEW_OPERATION,
{ parameters: {
predicateType: "rea.analysis-view", parent_evidence_id: parent.evidence_id,
operation: INSPECT_ANALYSIS_VIEW_OPERATION, view: jsonValueSchema.parse(input.view),
parameters: {
parent_evidence_id: parent.evidence_id,
view: jsonValueSchema.parse(input.view),
},
result: jsonValueSchema.parse(result),
rawResult: null,
confidence:
// Grouping is whole-capture, so any templated group or parameter makes the result inferred;
// entity types and identity in an extracted state always are.
result.kind === "environment-state" ||
((result.kind === "api-surface" ||
result.kind === "environment-manifest") &&
result.templated_groups > 0)
? "inferred"
: "derived",
authority: parent.authority,
limitations: result.limitations,
...(result.kind === "native" && parent.relationship_coverage !== null
? { relationshipCoverage: parent.relationship_coverage }
: {}),
locations: [
...(result.artifact != null && result.artifact.path.length > 0
? [{ kind: "artifact-path" as const, path: result.artifact.path }]
: []),
...(result.kind === "native" &&
result.procedure_address !== null &&
result.procedure_address.length > 0
? [{ kind: "address" as const, address: result.procedure_address }]
: []),
],
evidenceLinks: [parent.evidence_id],
}, },
{ immutable: true }, result: jsonValueSchema.parse(result),
); rawResult: null,
confidence:
// Grouping is whole-capture, so any templated group or parameter makes the result inferred;
// entity types and identity in an extracted state always are.
result.kind === "environment-state" ||
((result.kind === "api-surface" ||
result.kind === "environment-manifest") &&
result.templated_groups > 0)
? "inferred"
: "derived",
authority: parent.authority,
limitations: result.limitations,
...(result.kind === "native" && parent.relationship_coverage !== null
? { relationshipCoverage: parent.relationship_coverage }
: {}),
locations: [
...(result.artifact != null && result.artifact.path.length > 0
? [{ kind: "artifact-path" as const, path: result.artifact.path }]
: []),
...(result.kind === "native" &&
result.procedure_address !== null &&
result.procedure_address.length > 0
? [{ kind: "address" as const, address: result.procedure_address }]
: []),
],
evidenceLinks: [parent.evidence_id],
});
}; };
@@ -32,13 +32,16 @@ export const readEvidenceFileSource = async (
cause: signal.reason, cause: signal.reason,
}), }),
); );
const loaded = await readJsonFile( const loaded = await readJsonFile(path, signal, resources);
path, if (!loaded.ok) {
signal, if (loaded.error instanceof AnalysisCancelledError)
resources, return err(
INSPECT_ANALYSIS_VIEW_OPERATION, new AnalysisCancelledError(INSPECT_ANALYSIS_VIEW_OPERATION, {
); cause: loaded.error.cause,
if (!loaded.ok) return loaded; }),
);
return loaded;
}
if (signal?.aborted) if (signal?.aborted)
return err( return err(
new AnalysisCancelledError(INSPECT_ANALYSIS_VIEW_OPERATION, { new AnalysisCancelledError(INSPECT_ANALYSIS_VIEW_OPERATION, {
@@ -1,21 +1,19 @@
import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import type { Evidence } from "../../domain/evidence.js";
import { import {
androidApplicationProjectionInputSchema, androidApplicationProjectionInputSchema,
projectAndroidApplication, projectAndroidApplication,
} from "../../domain/android/androidApplication.js"; } from "../../domain/android/androidApplication.js";
import type { Result } from "../../domain/result.js"; import type { Result } from "../../domain/result.js";
import { ANDROID_APPLICATION_PROVIDER } from "../InvestigationProviders.js"; import { ANDROID_APPLICATION_PROVIDER } from "../InvestigationProviders.js";
import { import { projectInventoryEvidence } from "../InventoryProjectionEvidence.js";
projectInventoryEvidence,
type InventoryProjectionEvidenceResult,
} from "../InventoryProjectionEvidence.js";
const OPERATION = "project_android_application_graph" as const; const OPERATION = "project_android_application_graph" as const;
/** Project authenticated APK inventory Evidence into Android application Evidence. */ /** Project authenticated APK inventory Evidence into Android application Evidence. */
export const projectAndroidApplicationEvidence = ( export const projectAndroidApplicationEvidence = (
rawInput: unknown, rawInput: unknown,
): Result<InventoryProjectionEvidenceResult, AnalysisError> => { ): Result<Evidence, AnalysisError> => {
return projectInventoryEvidence({ return projectInventoryEvidence({
rawInput, rawInput,
schema: androidApplicationProjectionInputSchema, schema: androidApplicationProjectionInputSchema,
@@ -1,21 +1,19 @@
import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import type { Evidence } from "../../domain/evidence.js";
import { import {
appleApplicationProjectionInputSchema, appleApplicationProjectionInputSchema,
projectAppleApplication, projectAppleApplication,
} from "../../domain/apple/appleApplication.js"; } from "../../domain/apple/appleApplication.js";
import type { Result } from "../../domain/result.js"; import type { Result } from "../../domain/result.js";
import { APPLE_APPLICATION_PROVIDER } from "../InvestigationProviders.js"; import { APPLE_APPLICATION_PROVIDER } from "../InvestigationProviders.js";
import { import { projectInventoryEvidence } from "../InventoryProjectionEvidence.js";
projectInventoryEvidence,
type InventoryProjectionEvidenceResult,
} from "../InventoryProjectionEvidence.js";
const OPERATION = "project_apple_application_graph" as const; const OPERATION = "project_apple_application_graph" as const;
/** Project authenticated IPA or macOS app inventory Evidence into Apple application evidence. */ /** Project authenticated IPA or macOS app inventory Evidence into Apple application evidence. */
export const projectAppleApplicationEvidence = ( export const projectAppleApplicationEvidence = (
rawInput: unknown, rawInput: unknown,
): Result<InventoryProjectionEvidenceResult, AnalysisError> => { ): Result<Evidence, AnalysisError> => {
return projectInventoryEvidence({ return projectInventoryEvidence({
rawInput, rawInput,
schema: appleApplicationProjectionInputSchema, schema: appleApplicationProjectionInputSchema,
@@ -1,3 +1,4 @@
import { isAbsolute } from "node:path";
import type { ExecutionOptions } from "../AnalysisProvider.js"; import type { ExecutionOptions } from "../AnalysisProvider.js";
import type { DesktopApplicationPort } from "./DesktopApplicationPort.js"; import type { DesktopApplicationPort } from "./DesktopApplicationPort.js";
import { ARTIFACT_GRAPH_PROVIDER } from "../InvestigationProviders.js"; import { ARTIFACT_GRAPH_PROVIDER } from "../InvestigationProviders.js";
@@ -40,6 +41,16 @@ export class DesktopApplicationService {
cause: parsed.error, cause: parsed.error,
}), }),
); );
if (!isAbsolute(parsed.data.path))
return err(
new AnalysisInputError(operation, undefined, [
{
path: ["path"],
reason: "invalid_format",
message: "Expected an absolute directory path on this host.",
},
]),
);
const result = await this.reader.inspect(parsed.data, options); const result = await this.reader.inspect(parsed.data, options);
if (!result.ok) return result; if (!result.ok) return result;
const report = desktopApplicationSchema.safeParse(result.value); const report = desktopApplicationSchema.safeParse(result.value);
@@ -25,18 +25,10 @@ it.each([
throw new Error("must not run"); throw new Error("must not run");
}, },
}); });
const result = await service.inspect(input); expect(await service.inspect(input)).toMatchObject({
expect(result).toMatchObject({
ok: false, ok: false,
error: { _tag: "AnalysisInputError" }, error: { _tag: "AnalysisInputError" },
}); });
if (input.path === "relative.elf")
expect(result).toMatchObject({
ok: false,
error: {
issues: [{ path: ["path"], reason: "invalid_format" }],
},
});
}, },
); );
@@ -1,8 +1,10 @@
import { isAbsolute } from "node:path";
import type { ExecutionOptions } from "../AnalysisProvider.js"; import type { ExecutionOptions } from "../AnalysisProvider.js";
import type { BinaryLayoutPort } from "./BinaryLayoutPort.js"; import type { BinaryLayoutPort } from "./BinaryLayoutPort.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import { import {
AnalysisCancelledError, AnalysisCancelledError,
AnalysisInputError,
AnalysisOutputError, AnalysisOutputError,
} from "../../domain/analysisErrorCore.js"; } from "../../domain/analysisErrorCore.js";
import { createEvidence, type Evidence } from "../../domain/evidence.js"; import { createEvidence, type Evidence } from "../../domain/evidence.js";
@@ -64,6 +66,16 @@ export class BinaryLayoutService {
cause: input.error, cause: input.error,
}), }),
); );
if (!isAbsolute(input.data.path))
return err(
new AnalysisInputError(OPERATION, undefined, [
{
path: ["path"],
reason: "invalid_format",
message: "Expected an absolute filesystem path on this host.",
},
]),
);
const inspected = await inspect(input.data); const inspected = await inspect(input.data);
if (!inspected.ok) return inspected; if (!inspected.ok) return inspected;
const diagnostics = binaryLayoutSchema.shape.diagnostics.safeParse( const diagnostics = binaryLayoutSchema.shape.diagnostics.safeParse(
@@ -1,8 +1,10 @@
import { isAbsolute } from "node:path";
import type { ExecutionOptions } from "../AnalysisProvider.js"; import type { ExecutionOptions } from "../AnalysisProvider.js";
import type { PeResourcesPort } from "./PeResourcesPort.js"; import type { PeResourcesPort } from "./PeResourcesPort.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import { import {
AnalysisCancelledError, AnalysisCancelledError,
AnalysisInputError,
AnalysisOutputError, AnalysisOutputError,
} from "../../domain/analysisErrorCore.js"; } from "../../domain/analysisErrorCore.js";
import { createEvidence, type Evidence } from "../../domain/evidence.js"; import { createEvidence, type Evidence } from "../../domain/evidence.js";
@@ -45,6 +47,16 @@ export class PeResourcesService {
cause: input.error, cause: input.error,
}), }),
); );
if (!isAbsolute(input.data.path))
return err(
new AnalysisInputError(operation, undefined, [
{
path: ["path"],
reason: "invalid_format",
message: "Expected an absolute filesystem path on this host.",
},
]),
);
const result = await this.provider.inspect(input.data, options); const result = await this.provider.inspect(input.data, options);
if (!result.ok) return result; if (!result.ok) return result;
const report = peResourcesSchema.safeParse(result.value); const report = peResourcesSchema.safeParse(result.value);
@@ -70,18 +70,10 @@ it.each([
throw new Error("must not run"); throw new Error("must not run");
}, },
}); });
const result = await service.inspect(input); expect(await service.inspect(input)).toMatchObject({
expect(result).toMatchObject({
ok: false, ok: false,
error: { _tag: "AnalysisInputError" }, error: { _tag: "AnalysisInputError" },
}); });
if (input.path === "relative.elf")
expect(result).toMatchObject({
ok: false,
error: {
issues: [{ path: ["path"], reason: "invalid_format" }],
},
});
}, },
); );
@@ -1,8 +1,10 @@
import { isAbsolute } from "node:path";
import type { ExecutionOptions } from "../AnalysisProvider.js"; import type { ExecutionOptions } from "../AnalysisProvider.js";
import type { RecordedCrashPort } from "./RecordedCrashPort.js"; import type { RecordedCrashPort } from "./RecordedCrashPort.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import { import {
AnalysisCancelledError, AnalysisCancelledError,
AnalysisInputError,
AnalysisOutputError, AnalysisOutputError,
} from "../../domain/analysisErrorCore.js"; } from "../../domain/analysisErrorCore.js";
import { createEvidence, type Evidence } from "../../domain/evidence.js"; import { createEvidence, type Evidence } from "../../domain/evidence.js";
@@ -65,6 +67,16 @@ export class RecordedCrashService {
cause: input.error, cause: input.error,
}), }),
); );
if (!isAbsolute(input.data.path))
return err(
new AnalysisInputError(OPERATION, undefined, [
{
path: ["path"],
reason: "invalid_format",
message: "Expected an absolute filesystem path on this host.",
},
]),
);
const inspected = await inspect(input.data); const inspected = await inspect(input.data);
if (!inspected.ok) return inspected; if (!inspected.ok) return inspected;
const diagnostics = recordedCrashSchema.shape.diagnostics.safeParse( const diagnostics = recordedCrashSchema.shape.diagnostics.safeParse(
@@ -1,102 +0,0 @@
import { expect, it } from "vitest";
import {
createAnalysisExecution,
type ExecutionOptions,
} from "../AnalysisProvider.js";
import { PacketCaptureService } from "./PacketCaptureService.js";
import { createAnalysisProfile } from "../../domain/analysisProfile.js";
import { inspectPacketCaptureInputSchema } from "../../domain/captures/packetCapture.js";
import { ok } from "../../domain/result.js";
it("retains provider metadata in a late-cancellation observation", async () => {
const input = inspectPacketCaptureInputSchema.parse({
path: process.platform === "win32" ? "C:\\capture.pcap" : "/capture.pcap",
fields: ["ip.addr"],
});
const provider = {
id: "fixture-capture",
name: "Fixture capture provider",
version: "1",
} as const;
const subject = {
path: input.path,
sha256: "a".repeat(64),
format: "file",
} as const;
const report = {
artifact: { path: input.path, sha256: subject.sha256, bytes: 12 },
selection: input,
packets: [],
output_window: {
matching_packets: 0,
returned_packets: 0,
has_more: false,
},
read_scope: "to-eof",
diagnostics: "",
tool: {
configured_path: "/fixture/tshark",
path: "/fixture/tshark",
sha256: "b".repeat(64),
version_banner: "TShark fixture",
version_diagnostics: "",
arguments: [],
},
configuration: "private-empty-personal-profile",
resource_limiter: {
configured_path: "/fixture/prlimit",
path: "/fixture/prlimit",
sha256: "c".repeat(64),
arguments: [],
},
limits: {
input_bytes: 1,
output_bytes: 1,
command_timeout_ms: 1,
address_space_bytes: 0,
file_bytes: 0,
cpu_seconds: 0,
},
hardware_acquisition: "not-performed",
} as const;
const rawResult = { producer: "fixture", bytes: 12 };
const profile = createAnalysisProfile(provider, { filter: null });
const relationshipCoverage = {
status: "unknown",
reason: "Packet relationships are not reported.",
} as const;
const execution = createAnalysisExecution(report, provider, {
subject,
rawResult,
analysisProfile: profile,
relationshipCoverage,
locations: [{ kind: "artifact-path", path: input.path }],
});
const controller = new AbortController();
const reason = Object.freeze({ requested: "cancel after capture result" });
const service = new PacketCaptureService({
inspect: async (_input, options?: ExecutionOptions) => {
controller.abort(reason);
expect(options?.signal).toBe(controller.signal);
return ok(execution);
},
inspectFields: async () => {
throw new Error("Unexpected field discovery call");
},
close: async () => {},
});
const result = await service.inspect(input, { signal: controller.signal });
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected late cancellation");
expect(result.error).toMatchObject({ _tag: "AnalysisCancelledError" });
expect(result.error.cause).toBe(reason);
expect(result.error.partialObservation).toMatchObject({
operation: "inspect_packet_capture",
raw_result: rawResult,
analysis_profile: profile,
relationship_coverage: relationshipCoverage,
});
});
@@ -3,25 +3,26 @@ import {
packetFieldsCatalogSchema, packetFieldsCatalogSchema,
type InspectPacketFieldsInput, type InspectPacketFieldsInput,
} from "../../domain/captures/packetFields.js"; } from "../../domain/captures/packetFields.js";
import { import type { JsonValue } from "../../domain/jsonValue.js";
createAnalysisExecutionEvidence, import { isAbsolute } from "node:path";
type AnalysisExecution, import type {
type ExecutionOptions, AnalysisExecution,
ExecutionOptions,
} from "../AnalysisProvider.js"; } from "../AnalysisProvider.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import { import {
AnalysisCancelledError, AnalysisCancelledError,
AnalysisInputError,
AnalysisOutputError, AnalysisOutputError,
} from "../../domain/analysisErrorCore.js"; } from "../../domain/analysisErrorCore.js";
import type { Evidence } from "../../domain/evidence.js"; import { createEvidence, type Evidence } from "../../domain/evidence.js";
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js"; import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
import { import {
inspectPacketCaptureInputSchema, inspectPacketCaptureInputSchema,
packetCaptureSchema, packetCaptureSchema,
type InspectPacketCaptureInput, type InspectPacketCaptureInput,
} from "../../domain/captures/packetCapture.js"; } from "../../domain/captures/packetCapture.js";
import { err, type Result } from "../../domain/result.js"; import { err, ok, type Result } from "../../domain/result.js";
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
export interface PacketCapturePort { export interface PacketCapturePort {
/** Release per-call resources before returning, or retain failed cleanup for close/retry. */ /** Release per-call resources before returning, or retain failed cleanup for close/retry. */
@@ -58,6 +59,16 @@ export class PacketCaptureService {
cause: input.error, cause: input.error,
}), }),
); );
if (!isAbsolute(input.data.path))
return err(
new AnalysisInputError(operation, undefined, [
{
path: ["path"],
reason: "invalid_format",
message: "Expected an absolute local path.",
},
]),
);
const decoded = await this.provider.inspect(input.data, options); const decoded = await this.provider.inspect(input.data, options);
if (!decoded.ok) return decoded; if (!decoded.ok) return decoded;
const execution = decoded.value; const execution = decoded.value;
@@ -75,15 +86,7 @@ export class PacketCaptureService {
"Capture analyzer returned malformed data or changed the selected artifact identity/options.", "Capture analyzer returned malformed data or changed the selected artifact identity/options.",
), ),
); );
return completeProviderEvidence( return this.#evidence(operation, input.data, execution, options);
createAnalysisExecutionEvidence(
operation,
input.data,
execution,
"observed",
),
options?.signal,
);
} }
async inspectFields( async inspectFields(
raw: unknown, raw: unknown,
@@ -117,14 +120,33 @@ export class PacketCaptureService {
"Field discovery returned malformed data or changed the selected installation/options.", "Field discovery returned malformed data or changed the selected installation/options.",
), ),
); );
return completeProviderEvidence( return this.#evidence(operation, input.data, execution, options);
createAnalysisExecutionEvidence( }
#evidence(
operation: string,
parameters: Readonly<Record<string, JsonValue>>,
execution: AnalysisExecution,
options?: ExecutionOptions,
): Result<Evidence, AnalysisError> {
const evidence = createEvidence(
execution.subject ?? undefined,
execution.provider,
{
operation, operation,
input.data, parameters,
execution, result: execution.result,
"observed", confidence: "observed",
), locations: execution.locations,
options?.signal, limitations: execution.limitations,
},
); );
return options?.signal?.aborted
? err(
new AnalysisCancelledError(operation, {
cause: options.signal.reason,
partialObservation: evidence,
}),
)
: ok(evidence);
} }
} }
@@ -1,8 +1,11 @@
import { isAbsolute } from "node:path";
import type { ExecutionOptions } from "../AnalysisProvider.js"; import type { ExecutionOptions } from "../AnalysisProvider.js";
import type { ElectronFusePort } from "./ElectronFusePort.js"; import type { ElectronFusePort } from "./ElectronFusePort.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import { import {
AnalysisCancelledError, AnalysisCancelledError,
AnalysisInputError,
AnalysisOutputError, AnalysisOutputError,
} from "../../domain/analysisErrorCore.js"; } from "../../domain/analysisErrorCore.js";
import { import {
@@ -42,6 +45,16 @@ export class ElectronFuseService {
cause: input.error, cause: input.error,
}), }),
); );
if (!isAbsolute(input.data.path))
return err(
new AnalysisInputError(OPERATION, undefined, [
{
path: ["path"],
reason: "invalid_format",
message: "Expected an absolute filesystem path on this host.",
},
]),
);
const inspected = await this.provider.inspect(input.data, options); const inspected = await this.provider.inspect(input.data, options);
if (!inspected.ok) return inspected; if (!inspected.ok) return inspected;
const report = electronFuseReportSchema.safeParse(inspected.value); const report = electronFuseReportSchema.safeParse(inspected.value);
@@ -387,11 +387,10 @@ describe("planEnvironmentBundle without a state", () => {
it("serves and predicts replay when the state's catalog exceeds the discovery budget that replay's fits", () => { it("serves and predicts replay when the state's catalog exceeds the discovery budget that replay's fits", () => {
// Read tools only, so leaving out write tools saves nothing while each state description is longer. // Read tools only, so leaving out write tools saves nothing while each state description is longer.
// 300 sits between the two catalogs on this fixture: replay still fits and the state catalog does not.
const reads = servedSource.manifest.tools.filter( const reads = servedSource.manifest.tools.filter(
({ method }) => method === "GET", ({ method }) => method === "GET",
); );
const tools = Array.from({ length: 300 }, (_, index) => { const tools = Array.from({ length: 350 }, (_, index) => {
const tool = reads[index % reads.length]!; const tool = reads[index % reads.length]!;
return index < reads.length return index < reads.length
? tool ? tool
@@ -417,7 +416,7 @@ describe("planEnvironmentBundle without a state", () => {
served: false, served: false,
code: "resource_constraint", code: "resource_constraint",
}); });
expect(planned.value.toolCount).toBe(300); expect(planned.value.toolCount).toBe(350);
expect(planned.value.check).toEqual({ expect(planned.value.check).toEqual({
tool: "get_annotations", tool: "get_annotations",
recordOrdinal: 25, recordOrdinal: 25,
@@ -13,13 +13,13 @@ import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
import { inspectAnalysisView } from "../analysisView/AnalysisViewService.js"; import { inspectAnalysisView } from "../analysisView/AnalysisViewService.js";
import { import {
REPLAY_BODY_LIMIT_BYTES, REPLAY_BODY_LIMIT_BYTES,
type EnvironmentSource,
type RecordedExchange,
} from "./environmentSource.js";
import {
replayEnvironmentTool, replayEnvironmentTool,
selectReplayExchange, selectReplayExchange,
} from "./environmentReplay.js"; } from "./environmentReplay.js";
import type {
EnvironmentSource,
RecordedExchange,
} from "./environmentSource.js";
const source = environmentServeSource(); const source = environmentServeSource();
@@ -16,6 +16,8 @@ import {
type RecordedExchange, type RecordedExchange,
} from "./environmentSource.js"; } from "./environmentSource.js";
export { REPLAY_BODY_LIMIT_BYTES } from "./environmentSource.js";
/** Caller arguments as the MCP tool receives them. */ /** Caller arguments as the MCP tool receives them. */
export type ReplayArguments = Readonly<Record<string, unknown>>; export type ReplayArguments = Readonly<Record<string, unknown>>;
@@ -418,30 +418,6 @@ describe("EnvironmentStateStore", () => {
}); });
}); });
describe("caller-owned state replacement", () => {
it("does not retain mutable data from a caller-owned replacement", () => {
const store = new EnvironmentStateStore(state);
const document = documentWith((value) => {
const entity = value.rea_state.entities.find((item) => item.id === "2");
if (!entity) throw new Error("Expected the user entity");
entity.fields = { ...(entity.fields as object), login: "renamed" };
});
expect(store.replace(document).ok).toBe(true);
const callerEntity = document.rea_state.entities.find(
(entity) => entity.id === "2",
);
if (!callerEntity) throw new Error("Expected the caller's user entity");
(callerEntity.fields as Record<string, unknown>).login = "later edit";
expect(store.snapshot().rea_state.entities).toContainEqual(
expect.objectContaining({
id: "2",
fields: expect.objectContaining({ login: "renamed" }),
}),
);
});
});
describe("rea-state/v1 validation", () => { describe("rea-state/v1 validation", () => {
it.each([ it.each([
[ [
@@ -485,12 +461,12 @@ describe("rea-state/v1 validation", () => {
"a document for a tool that is not a document view", "a document for a tool that is not a document view",
(document: Editable) => { (document: Editable) => {
document.rea_state.documents.push({ document.rea_state.documents.push({
tool: "get_folders", tool: "get_org",
path: "/api/folders", path: "/api/org",
value: {}, value: {},
}); });
}, },
`rea_state/documents/${String(state.documents.length)}/tool`, "rea_state/documents/0/tool",
], ],
[ [
"a member the format does not define", "a member the format does not define",
@@ -594,36 +570,3 @@ describe("rea-state/v1 validation", () => {
).toMatchObject({ path: "rea_state/version" }); ).toMatchObject({ path: "rea_state/version" });
}); });
}); });
describe("wide state value validation", () => {
it("preserves a wide caller value through the state replacement boundary", () => {
const wide = Array.from({ length: 200_000 }, (_, index) => index % 10);
const wideObject = Object.fromEntries(
Array.from({ length: 20_000 }, (_, index) => [
`field_${String(index)}`,
index,
]),
);
const document = documentWith((value) => {
const fields = value.rea_state.entities[0]!.fields as Record<
string,
unknown
>;
value.rea_state.entities[0]!.fields = {
...fields,
caller_wide: wide,
caller_wide_object: wideObject,
};
});
const store = new EnvironmentStateStore(state);
expect(store.replace(document).ok).toBe(true);
const retained = store.snapshot().rea_state.entities[0]?.fields.caller_wide;
if (!Array.isArray(retained)) throw new Error("Expected the wide value");
expect(retained).toHaveLength(wide.length);
expect(retained.at(-1)).toBe(wide.at(-1));
const retainedObject =
store.snapshot().rea_state.entities[0]?.fields.caller_wide_object;
expect(retainedObject).toEqual(wideObject);
});
});
@@ -9,10 +9,8 @@ import {
environmentStateServeInputs, environmentStateServeInputs,
} from "../../../tests/fixtures/environmentServe.js"; } from "../../../tests/fixtures/environmentServe.js";
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js"; import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
import { import { REPLAY_BODY_LIMIT_BYTES } from "./environmentReplay.js";
REPLAY_BODY_LIMIT_BYTES, import type { EnvironmentSource } from "./environmentSource.js";
type EnvironmentSource,
} from "./environmentSource.js";
import { import {
answerFromState, answerFromState,
type StateServing, type StateServing,
@@ -313,17 +311,6 @@ describe("recorded and document answers", () => {
}); });
}); });
it("replays a read whose rendered document exceeds the body limit", () => {
const values = Array.from({ length: 60_000 }, () => "1e3").join(",");
const body = `{"values":[${values}]}`;
const serving = servingFrom([["GET", "/api/config", body]]);
expect(serving.store.state.documents).toEqual([]);
expect(answer(serving, "get_config", {})).toMatchObject({
replay: { record_ordinal: 0 },
state: { source: "recording", reason: "rendered-oversize" },
});
});
it("answers a document's recorded line with the state's value for it", () => { it("answers a document's recorded line with the state's value for it", () => {
const capture = timedCaptureEvidence([ const capture = timedCaptureEvidence([
["GET", "/api/settings", { theme: "dark", density: "compact" }], ["GET", "/api/settings", { theme: "dark", density: "compact" }],
@@ -24,6 +24,7 @@ import { err, ok, type Result } from "../../domain/result.js";
import { import {
decodedPathSegment, decodedPathSegment,
pathValueMatches, pathValueMatches,
REPLAY_BODY_LIMIT_BYTES,
replayAnswer, replayAnswer,
replayEnvironmentTool, replayEnvironmentTool,
selectReplayExchange, selectReplayExchange,
@@ -33,7 +34,6 @@ import {
} from "./environmentReplay.js"; } from "./environmentReplay.js";
import { import {
ENVIRONMENT_SERVE_OPERATION, ENVIRONMENT_SERVE_OPERATION,
REPLAY_BODY_LIMIT_BYTES,
type EnvironmentManifestTool, type EnvironmentManifestTool,
type EnvironmentSource, type EnvironmentSource,
type RecordedExchange, type RecordedExchange,
@@ -1,4 +1,3 @@
import { types } from "node:util";
import { z } from "zod"; import { z } from "zod";
import { import {
@@ -17,11 +16,7 @@ import {
} from "../../domain/analysisErrorCore.js"; } from "../../domain/analysisErrorCore.js";
import { canonicalJson } from "../../domain/comparisonSemantics.js"; import { canonicalJson } from "../../domain/comparisonSemantics.js";
import { projectInputIssues } from "../../domain/inputIssueProjection.js"; import { projectInputIssues } from "../../domain/inputIssueProjection.js";
import { import { jsonValueSchema } from "../../domain/jsonValue.js";
jsonValueSchema,
ownedJsonValueSchema,
type JsonValue,
} from "../../domain/jsonValue.js";
import { err, ok, type Result } from "../../domain/result.js"; import { err, ok, type Result } from "../../domain/result.js";
import { compositeKey } from "../../domain/unicodeCodePointOrder.js"; import { compositeKey } from "../../domain/unicodeCodePointOrder.js";
import type { EnvironmentState } from "./environmentState.js"; import type { EnvironmentState } from "./environmentState.js";
@@ -42,42 +37,37 @@ export interface StateContent {
readonly documents: readonly HeldDocument[]; readonly documents: readonly HeldDocument[];
} }
const stateDocumentSchemaFor = (jsonValue: z.ZodType<JsonValue, unknown>) => { const fieldsSchema = z.record(z.string(), jsonValueSchema);
const fieldsSchema = z.record(z.string(), jsonValue); const ordinalSchema = z.number().int().nonnegative();
const ordinalSchema = z.number().int().nonnegative();
return z.strictObject({ const stateDocumentSchema = z.strictObject({
rea_state: z.strictObject({ rea_state: z.strictObject({
version: z.literal(1), version: z.literal(1),
// `data/get` reports these; accepting them back lets its output be loaded unchanged. // `data/get` reports these; accepting them back lets its output be loaded unchanged.
state_evidence_id: z.string().optional(), state_evidence_id: z.string().optional(),
types: z.array(jsonValue).optional(), types: z.array(jsonValueSchema).optional(),
entities: z.array( entities: z.array(
z.strictObject({ z.strictObject({
type: z.string(), type: z.string(),
collection: z.string(), collection: z.string(),
id: z.string(), id: z.string(),
fields: fieldsSchema, fields: fieldsSchema,
view_fields: z view_fields: z
.array(z.strictObject({ tool: z.string(), fields: fieldsSchema })) .array(z.strictObject({ tool: z.string(), fields: fieldsSchema }))
.optional(), .optional(),
record_ordinals: z.array(ordinalSchema).optional(), record_ordinals: z.array(ordinalSchema).optional(),
}), }),
), ),
documents: z.array( documents: z.array(
z.strictObject({ z.strictObject({
tool: z.string(), tool: z.string(),
path: z.string(), path: z.string(),
value: jsonValue, value: jsonValueSchema,
record_ordinal: ordinalSchema.optional(), record_ordinal: ordinalSchema.optional(),
}), }),
), ),
}), }),
}); });
};
const stateDocumentSchema = stateDocumentSchemaFor(jsonValueSchema);
const ownedStateDocumentSchema = stateDocumentSchemaFor(ownedJsonValueSchema);
type ParsedDocument = z.output<typeof stateDocumentSchema>["rea_state"]; type ParsedDocument = z.output<typeof stateDocumentSchema>["rea_state"];
type ParsedEntity = ParsedDocument["entities"][number]; type ParsedEntity = ParsedDocument["entities"][number];
@@ -115,82 +105,31 @@ const memberProblem = (path: Path): AnalysisInputIssue => {
/** The path of the first `__proto__` member in document order; schema parsing would drop such a member rather than refuse it. */ /** The path of the first `__proto__` member in document order; schema parsing would drop such a member rather than refuse it. */
const prototypeMemberPath = (root: unknown): Path | undefined => { const prototypeMemberPath = (root: unknown): Path | undefined => {
interface Cursor { interface Node {
readonly value: unknown; readonly value: unknown;
readonly parent: Cursor | undefined; readonly parent: Node | undefined;
readonly key: string | number; readonly key: string | number;
readonly entries?: Generator<string>;
index: number;
} }
const pathOf = (node: Cursor): (string | number)[] => { const pathOf = (node: Node): (string | number)[] => {
const path: (string | number)[] = []; const path: (string | number)[] = [];
for (let at: Cursor = node; at.parent !== undefined; at = at.parent) for (let at: Node = node; at.parent !== undefined; at = at.parent)
path.push(at.key); path.push(at.key);
return path.reverse(); return path.reverse();
}; };
function* ownKeys(value: Record<string, unknown>): Generator<string> { const pending: Node[] = [{ value: root, parent: undefined, key: "" }];
// `for...in` is lazy. The ownership check keeps inherited enumerable for (let node = pending.pop(); node !== undefined; node = pending.pop()) {
// properties out, matching Object.entries without allocating all entries.
for (const key in value) if (Object.hasOwn(value, key)) yield key;
}
const cursor = (
value: unknown,
parent: Cursor | undefined,
key: string | number,
): Cursor => ({
value,
parent,
key,
index: 0,
...(!Array.isArray(value) && typeof value === "object" && value !== null
? { entries: ownKeys(value as Record<string, unknown>) }
: {}),
});
const pending: Cursor[] = [cursor(root, undefined, "")];
while (pending.length > 0) {
const node = pending[pending.length - 1]!;
const { value } = node; const { value } = node;
if (typeof value !== "object" || value === null) { if (typeof value !== "object" || value === null) continue;
pending.pop();
continue;
}
// The canonical JSON validator reports proxies without invoking their traps.
if (types.isProxy(value)) {
pending.pop();
continue;
}
const array = Array.isArray(value); const array = Array.isArray(value);
if (!array && Object.hasOwn(value, "__proto__")) if (!array && Object.hasOwn(value, "__proto__"))
return [...pathOf(node), "__proto__"]; return [...pathOf(node), "__proto__"];
if (array) { const entries = Object.entries(value);
if (node.index >= value.length) { for (let index = entries.length - 1; index >= 0; index -= 1)
pending.pop(); pending.push({
continue; value: entries[index]![1],
} parent: node,
const index = node.index++; key: array ? index : entries[index]![0],
const descriptor = Object.getOwnPropertyDescriptor(value, index); });
if (
descriptor?.enumerable !== true ||
!Object.hasOwn(descriptor, "value")
)
continue;
const child: unknown = descriptor.value;
if (typeof child === "object" && child !== null)
pending.push(cursor(child, node, index));
continue;
}
const next = node.entries?.next();
if (next === undefined || next.done) {
pending.pop();
continue;
}
const key = next.value;
const descriptor = Object.getOwnPropertyDescriptor(value, key);
if (descriptor?.enumerable !== true || !Object.hasOwn(descriptor, "value"))
continue;
const child: unknown = descriptor.value;
if (typeof child === "object" && child !== null)
pending.push(cursor(child, node, key));
} }
return undefined; return undefined;
}; };
@@ -426,12 +365,9 @@ const heldContent = (
export const parseStateDocument = ( export const parseStateDocument = (
state: EnvironmentState, state: EnvironmentState,
value: unknown, value: unknown,
owned = false,
): Result<StateContent, AnalysisInputError> => { ): Result<StateContent, AnalysisInputError> => {
const prototype = prototypeMemberPath(value); const prototype = prototypeMemberPath(value);
const parsed = ( const parsed = stateDocumentSchema.safeParse(value);
owned ? ownedStateDocumentSchema : stateDocumentSchema
).safeParse(value);
const initial = new Map( const initial = new Map(
state.entities.map((entity) => [entityKey(entity), entity]), state.entities.map((entity) => [entityKey(entity), entity]),
); );
@@ -58,21 +58,9 @@ export class EnvironmentStateStore {
this.#install(this.state); this.#install(this.state);
} }
/** Validate a caller-owned document and copy its JSON values before retaining them. */ /** Validate a `rea-state/v1` document and, only when it is valid, make it the whole state. */
replace(document: unknown): Result<void, AnalysisInputError> { replace(document: unknown): Result<void, AnalysisInputError> {
return this.#replace(document, false); const content = parseStateDocument(this.state, document);
}
/** Validate and retain a JSON document created exclusively by the serving boundary. */
replaceOwnedDocument(document: unknown): Result<void, AnalysisInputError> {
return this.#replace(document, true);
}
#replace(
document: unknown,
owned: boolean,
): Result<void, AnalysisInputError> {
const content = parseStateDocument(this.state, document, owned);
if (!content.ok) return content; if (!content.ok) return content;
// data/get writes members a document may omit, so the state it reports must still be one data/add loads. // data/get writes members a document may omit, so the state it reports must still be one data/add loads.
if (jsonBytes(this.#snapshotOf(content.value)) > STATE_INTAKE_LIMIT_BYTES) if (jsonBytes(this.#snapshotOf(content.value)) > STATE_INTAKE_LIMIT_BYTES)
@@ -190,7 +190,7 @@ describe("environment tool response shapes", () => {
{ bodyText: JSON.stringify({ [member]: [{ id: 1 }, { id: 2 }] }) }, { bodyText: JSON.stringify({ [member]: [{ id: 1 }, { id: 2 }] }) },
]), ]),
).toContain( ).toContain(
`{numeric}; returns a JSON object whose "${"m".repeat(1022)}"… (member display truncated; full value in capture Evidence) lists objects with fields "id"; recorded statuses 200;`, `{numeric}; returns a JSON object whose "${"m".repeat(1024)}"… (member display truncated; full value in capture Evidence) lists objects with fields "id"; recorded statuses 200;`,
); );
}); });
@@ -219,19 +219,6 @@ describe("environment tool response shapes", () => {
expect(shown).toContain('whose "items; recorded statuses 500\\nmmm'); expect(shown).toContain('whose "items; recorded statuses 500\\nmmm');
expect(shown).not.toContain("items; recorded statuses 500\n"); expect(shown).not.toContain("items; recorded statuses 500\n");
}); });
it("applies the sample budget after JSON escaping", () => {
const member = "\u0000".repeat(1024);
const description = usersAnswering([
{ bodyText: JSON.stringify({ [member]: [{ id: 1 }] }) },
]);
const shown = /whose (?<name>"(?:\\u0000)*")/u.exec(description ?? "")
?.groups?.name;
expect(shown).toBeDefined();
expect(Buffer.byteLength(shown ?? "")).toBeLessThanOrEqual(1024);
expect(description).toContain("member display truncated");
expect(description).not.toContain("\u0000");
});
}); });
describe("environment tool response summaries", () => { describe("environment tool response summaries", () => {
@@ -83,16 +83,7 @@ const environmentToolAnnotations = ({
/** Agent-context budget for each captured display sample, excluding its provenance notice. */ /** Agent-context budget for each captured display sample, excluding its provenance notice. */
const DESCRIPTION_SAMPLE_BYTES = 1024; const DESCRIPTION_SAMPLE_BYTES = 1024;
/** Whether a rendered sample fits the display budget. Callers' `show` never shortens its input. */ /** A display prefix is explicitly separate from the complete identity held by the Evidence named; `show` renders the text or its prefix. */
const sampleFits = (shown: string): boolean =>
shown.length <= DESCRIPTION_SAMPLE_BYTES &&
Buffer.byteLength(shown, "utf8") <= DESCRIPTION_SAMPLE_BYTES;
/**
* A display prefix is explicitly separate from the complete identity held by the Evidence named.
* `show` renders the text or its prefix. The budget applies to that rendering, so JSON quoting of a
* name full of NULs or newlines cannot outgrow the sample.
*/
const describeText = ( const describeText = (
text: string, text: string,
label: string, label: string,
@@ -102,26 +93,13 @@ const describeText = (
if ( if (
text.length <= DESCRIPTION_SAMPLE_BYTES && text.length <= DESCRIPTION_SAMPLE_BYTES &&
Buffer.byteLength(text, "utf8") <= DESCRIPTION_SAMPLE_BYTES Buffer.byteLength(text, "utf8") <= DESCRIPTION_SAMPLE_BYTES
) { )
const shown = show(text); return show(text);
if (sampleFits(shown)) return shown; const { read } = new TextEncoder().encodeInto(
} text,
let low = 0; new Uint8Array(DESCRIPTION_SAMPLE_BYTES),
let high = Math.min(text.length, DESCRIPTION_SAMPLE_BYTES); );
let best = 0; return `${show(text.slice(0, read))}… (${label} display truncated; full value in ${holder})`;
while (low <= high) {
const mid = (low + high) >> 1;
if (sampleFits(show(text.slice(0, mid)))) {
best = mid;
low = mid + 1;
} else high = mid - 1;
}
// A lone surrogate is not a character. Step off a high surrogate so the prefix stays whole.
if (best > 0 && best < text.length) {
const unit = text.charCodeAt(best - 1);
if (unit >= 0xd8_00 && unit <= 0xdb_ff) best -= 1;
}
return `${show(text.slice(0, best))}… (${label} display truncated; full value in ${holder})`;
}; };
/** Sample whole values before joining; keep full lists in the Evidence that owns them. */ /** Sample whole values before joining; keep full lists in the Evidence that owns them. */
@@ -1,9 +1,11 @@
import { isAbsolute } from "node:path";
import { z } from "zod"; import { z } from "zod";
import type { ExecutionOptions } from "../AnalysisProvider.js"; import type { ExecutionOptions } from "../AnalysisProvider.js";
import type { EvmInterfacePort } from "./EvmInterfacePort.js"; import type { EvmInterfacePort } from "./EvmInterfacePort.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import { import {
AnalysisCancelledError, AnalysisCancelledError,
AnalysisInputError,
AnalysisOutputError, AnalysisOutputError,
} from "../../domain/analysisErrorCore.js"; } from "../../domain/analysisErrorCore.js";
import type { Evidence } from "../../domain/evidence.js"; import type { Evidence } from "../../domain/evidence.js";
@@ -45,6 +47,16 @@ export class EvmInterfaceService {
cause: input.error, cause: input.error,
}), }),
); );
if (!isAbsolute(input.data.path))
return err(
new AnalysisInputError(OPERATION, undefined, [
{
path: ["path"],
reason: "invalid_format",
message: "Expected an absolute filesystem path on this host.",
},
]),
);
const inspected = await this.provider.inspect(input.data, options); const inspected = await this.provider.inspect(input.data, options);
if (!inspected.ok) return inspected; if (!inspected.ok) return inspected;
const diagnostics = diagnosticsSchema.safeParse(inspected.value.result); const diagnostics = diagnosticsSchema.safeParse(inspected.value.result);
@@ -8,7 +8,6 @@ import {
} from "../../domain/firmware/firmwareAnalysis.js"; } from "../../domain/firmware/firmwareAnalysis.js";
import { AnalysisOutputError } from "../../domain/analysisErrorCore.js"; import { AnalysisOutputError } from "../../domain/analysisErrorCore.js";
import { err, ok } from "../../domain/result.js"; import { err, ok } from "../../domain/result.js";
import { createAnalysisProfile } from "../../domain/analysisProfile.js";
const fixture = ( const fixture = (
operation: FirmwareOperation, operation: FirmwareOperation,
@@ -69,35 +68,15 @@ it.each(["inspect_firmware_regions", "extract_firmware"] as const)(
"retains %s Evidence after late cancellation and preserves provider failure precedence", "retains %s Evidence after late cancellation and preserves provider failure precedence",
async (operation) => { async (operation) => {
const { input, execution } = fixture(operation); const { input, execution } = fixture(operation);
const profile = createAnalysisProfile(
{ ...execution.provider, version: "1" },
{
signature_database: "caller-selected snapshot",
},
);
const coverage = {
status: "unknown",
reason: "No call graph examined",
} as const;
const observation = {
...execution,
analysisProfile: profile,
relationshipCoverage: coverage,
};
const control = await new FirmwareAnalysisService({ const control = await new FirmwareAnalysisService({
execute: async () => ok(observation), execute: async () => ok(execution),
}).execute(operation, input); }).execute(operation, input);
if (!control.ok) throw control.error; if (!control.ok) throw control.error;
expect(control.value).toMatchObject({
raw_result: { source: "controlled producer" },
analysis_profile: profile,
relationship_coverage: coverage,
});
const controller = new AbortController(); const controller = new AbortController();
const result = await new FirmwareAnalysisService({ const result = await new FirmwareAnalysisService({
execute: async () => { execute: async () => {
controller.abort(); controller.abort();
return ok(observation); return ok(execution);
}, },
}).execute(operation, input, { signal: controller.signal }); }).execute(operation, input, { signal: controller.signal });
expect(result).toMatchObject({ expect(result).toMatchObject({
@@ -1,7 +1,4 @@
import { import type { ExecutionOptions } from "../AnalysisProvider.js";
createAnalysisExecutionEvidence,
type ExecutionOptions,
} from "../AnalysisProvider.js";
import type { FirmwareAnalysisPort } from "./FirmwareAnalysisPort.js"; import type { FirmwareAnalysisPort } from "./FirmwareAnalysisPort.js";
import { import {
firmwareRequestSchema, firmwareRequestSchema,
@@ -15,7 +12,8 @@ import {
AnalysisOutputError, AnalysisOutputError,
} from "../../domain/analysisErrorCore.js"; } from "../../domain/analysisErrorCore.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import type { Evidence } from "../../domain/evidence.js"; import { createEvidence, type Evidence } from "../../domain/evidence.js";
import { jsonObjectSchema } from "../../domain/jsonValue.js";
import { projectInputIssues } from "../../domain/inputIssueProjection.js"; import { projectInputIssues } from "../../domain/inputIssueProjection.js";
import { err, type Result } from "../../domain/result.js"; import { err, type Result } from "../../domain/result.js";
import { completeProviderEvidence } from "../ProviderOperationExecution.js"; import { completeProviderEvidence } from "../ProviderOperationExecution.js";
@@ -73,12 +71,15 @@ export class FirmwareAnalysisService {
), ),
); );
return completeProviderEvidence( return completeProviderEvidence(
createAnalysisExecutionEvidence( createEvidence(execution.subject, execution.provider, {
operation, operation,
parsed.data.input, parameters: jsonObjectSchema.parse(parsed.data.input),
execution, result: execution.result,
"observed", rawResult: execution.rawResult,
), locations: execution.locations,
limitations: execution.limitations,
confidence: "observed",
}),
options?.signal, options?.signal,
); );
} }
+12
View File
@@ -1,8 +1,10 @@
import { isAbsolute } from "node:path";
import type { ExecutionOptions } from "../AnalysisProvider.js"; import type { ExecutionOptions } from "../AnalysisProvider.js";
import type { GoBinaryPort } from "./GoBinaryPort.js"; import type { GoBinaryPort } from "./GoBinaryPort.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import { import {
AnalysisCancelledError, AnalysisCancelledError,
AnalysisInputError,
AnalysisOutputError, AnalysisOutputError,
} from "../../domain/analysisErrorCore.js"; } from "../../domain/analysisErrorCore.js";
import { createEvidence, type Evidence } from "../../domain/evidence.js"; import { createEvidence, type Evidence } from "../../domain/evidence.js";
@@ -51,6 +53,16 @@ export class GoBinaryService {
cause: input.error, cause: input.error,
}), }),
); );
if (!isAbsolute(input.data.path))
return err(
new AnalysisInputError(OPERATION, undefined, [
{
path: ["path"],
reason: "invalid_format",
message: "Expected an absolute filesystem path on this host.",
},
]),
);
const { detail, ...providerInput } = input.data; const { detail, ...providerInput } = input.data;
if (detail === "summary") { if (detail === "summary") {
const ready = requireAnalysisViewRetention(OPERATION, retention); const ready = requireAnalysisViewRetention(OPERATION, retention);
@@ -1,21 +1,19 @@
import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import type { Evidence } from "../../domain/evidence.js";
import { import {
harmonyApplicationProjectionInputSchema, harmonyApplicationProjectionInputSchema,
projectHarmonyApplication, projectHarmonyApplication,
} from "../../domain/harmony/harmonyApplication.js"; } from "../../domain/harmony/harmonyApplication.js";
import type { Result } from "../../domain/result.js"; import type { Result } from "../../domain/result.js";
import { HARMONY_APPLICATION_PROVIDER } from "../InvestigationProviders.js"; import { HARMONY_APPLICATION_PROVIDER } from "../InvestigationProviders.js";
import { import { projectInventoryEvidence } from "../InventoryProjectionEvidence.js";
projectInventoryEvidence,
type InventoryProjectionEvidenceResult,
} from "../InventoryProjectionEvidence.js";
const OPERATION = "project_harmony_application_graph" as const; const OPERATION = "project_harmony_application_graph" as const;
/** Project authenticated HarmonyOS package inventory Evidence into application evidence. */ /** Project authenticated HarmonyOS package inventory Evidence into application evidence. */
export const projectHarmonyApplicationEvidence = ( export const projectHarmonyApplicationEvidence = (
rawInput: unknown, rawInput: unknown,
): Result<InventoryProjectionEvidenceResult, AnalysisError> => { ): Result<Evidence, AnalysisError> => {
return projectInventoryEvidence({ return projectInventoryEvidence({
rawInput, rawInput,
schema: harmonyApplicationProjectionInputSchema, schema: harmonyApplicationProjectionInputSchema,
@@ -5,6 +5,7 @@ import type {
Il2cppSeedWriterPort, Il2cppSeedWriterPort,
Il2cppSeedReceipt, Il2cppSeedReceipt,
} from "./Il2cppSeedWriterPort.js"; } from "./Il2cppSeedWriterPort.js";
import { isAbsolute } from "node:path";
import type { ExecutionOptions } from "../AnalysisProvider.js"; import type { ExecutionOptions } from "../AnalysisProvider.js";
import { import {
requireAnalysisViewRetention, requireAnalysisViewRetention,
@@ -110,6 +111,19 @@ export class Il2cppMetadataService {
cause: input.error, cause: input.error,
}), }),
); );
for (const field of ["path", "binary_path"] as const) {
const value = input.data[field];
if (value !== undefined && !isAbsolute(value))
return err(
new AnalysisInputError(OPERATION, undefined, [
{
path: [field],
reason: "invalid_format",
message: "Expected an absolute filesystem path on this host.",
},
]),
);
}
if ( if (
input.data.binary_architecture !== undefined && input.data.binary_architecture !== undefined &&
input.data.binary_path === undefined input.data.binary_path === undefined
@@ -1,4 +1,3 @@
import { javascriptSourceRangeKey } from "../../domain/javascript/javascriptSourceCoordinates.js";
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraphSchemas.js"; import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraphSchemas.js";
import type { ElectronCallPath } from "../../domain/javascript/electronStaticAnalysisTypes.js"; import type { ElectronCallPath } from "../../domain/javascript/electronStaticAnalysisTypes.js";
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js"; import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
@@ -11,6 +10,7 @@ import {
import { import {
addElectronInferenceEdge, addElectronInferenceEdge,
electronFindingSourceNode, electronFindingSourceNode,
electronRangeKey,
} from "./ElectronBoundaryGraphContext.js"; } from "./ElectronBoundaryGraphContext.js";
import { import {
artifactLocalIdentity, artifactLocalIdentity,
@@ -191,15 +191,13 @@ const matches = (
path.steps.some( path.steps.some(
(step) => (step) =>
step.kind === sourceStepKind && step.kind === sourceStepKind &&
javascriptSourceRangeKey(step.location) === electronRangeKey(step.location) === electronRangeKey(finding.location),
javascriptSourceRangeKey(finding.location),
) && ) &&
(invocation === undefined || (invocation === undefined ||
path.steps.some( path.steps.some(
(step) => (step) =>
(step.kind === "call" || step.kind === "exposure") && (step.kind === "call" || step.kind === "exposure") &&
javascriptSourceRangeKey(step.location) === electronRangeKey(step.location) === electronRangeKey(invocation),
javascriptSourceRangeKey(invocation),
)) ))
); );
}; };
Loaded 100 of 525 files, more files were not shown because too many files have changed in this diff. Show more