Compare commits

...
Author SHA1 Message Date
Kaoru0822andClaude Opus 5.5 3e1f48dd68 fix(wasm): resolve WABT runtime cleanup after a successful retry
WabtArtifactProvider retained a failed rea-wabt-* runtime root in its
ArtifactResourceScope but returned an unbound ProviderCleanupError. A
later successful close() released the root without updating that error,
so CLI/MCP output kept reporting cleanup_incomplete and the removed root
as a live resource. A primary failure such as WABT rejecting the module
was also flattened into the cleanup error's previous_error diagnostic.

Retain the runtime root through an AnalysisCleanupLease and bind the
returned cleanup error to it with AnalysisCleanupLeaseGroup. Compose a
primary failure with analysisErrorWithCleanupFailure and
bindCleanupDependencies so the typed primary (for example
invalid_request or cancelled) becomes the resolved error once the lease
confirms release. Retry errors from admission and close() now bind to the
same exact leases through bindArtifactFileCleanup.

Fixes #4787

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 12:59:38 +00:00
morluto 7aa4b98c98 fix(plist): own XML values and numeric facts in one decoder
Interpret the validated, depth-bounded DOM directly. Collect source number
facts during the same traversal and share them with keyed archives, Interface
Builder and native plist projections. Remove the second XML parse, key aliases,
source-location rewriting and both duplicate XML numeric scanners. Preserve
character references, container/null behavior, safe own-property construction
and prototype omission accounting.

Decode signed hexadecimal integers and combine real text/CDATA. Retain exact
integer identity when a finite integer exceeds even Number's finite range,
using the shared scalar projector. Keep unsupported mixed integer syntax
conservative and report incomplete metadata.

Adapt @apitoagents's hexadecimal regression and literal-parsing proposal from
PR #3475. Native plutil controls establish the supported hex/real cases and
confirm that the synthetic 401-digit overflow case exceeds native bounds;
retaining accepted input does not claim native-format support.

Validation: 274 focused checks in 30 files pass, one skipped; build,
check:fast and docs:check pass. Built CLI/actual stdio MCP, native plist and
Interface Builder controls cover kinds, references, exact digits and digests.
Eight baseline checks fail; native binary conversion independently verifies
updated CDATA real expectations. 48 tiny completed CLI cost comparisons
preserve ordinary XML/binary and all three native replies byte for byte.
Two read-only reviews clear. No statistical speed claim, heavy profiler,
investigated-target execution or real deep-provider verification.

Fixes #3373
Fixes #4042
Fixes #4789
2026-10-17 20:40:55 +08:00
morluto 78cf6100f3 fix(plist): preserve complete XML dictionary key identities
Use the validated DOM's character-data key names for both identity and
prototype omission. Replace first-child reinterpretation with canonical,
collision-free aliases inside the pinned decoder boundary; restore names
through the existing dictionary projection. Alias every spelling of the
same name to preserve duplicate assignment order. Handle empty key spellings
and keep scalar key elements outside aliasing. Preserve value text and bytes.

Adapt @thangldw's tests and proposal from PR #3426. Avoid the proposal's
closing-content scanner and re-escaping source names. Add duplicate, empty,
alias-collision, scalar-key, native fallback and Interface Builder controls.

Validation: 180 focused checks across 20 files pass, including built CLI and
actual stdio MCP, build, check:fast and docs:check. Baseline public inspection
truncates main to ma. 24 tiny completed CLI cost controls preserve ordinary
XML/binary full replies and Evidence IDs byte for byte. No statistical speed
claim, heavy profiling, investigated-target or real deep-provider execution.

Fixes #3371
2026-10-17 20:28:01 +08:00
morluto 2cf9c28f27 fix(apple): preserve decoded keyed archive scalar identity
Move plist projection into the graph owner and identify generated scalar
wrappers by producer identity through every dictionary/UID consumer. Remove
the intermediate JSON clone that erased those facts. Literal wrapper-shaped
dictionaries remain dictionaries; scalar $top values fail as malformed.
Keep numeric projection after graph interpretation, preserve unknown counts,
and retain binary prototype-key partial facts with an explicit limitation.

Adapt @Jax367's relevant scalar proposal and public regression from PR #4317.
Add scalar-root, literal-root and binary prototype controls. Correct only
the captured Foundation data node expectation and its manifest digest;
original producer bytes and source are unchanged. Unrelated PR changes excluded.

Validation: 175 focused checks across 23 files pass, one skipped; build,
check:fast and docs:check pass. Public CLI and stdio MCP cover both encodings.
Baseline scalar controls fail; 24 tiny completed CLI cost comparisons retain
ordinary full replies and Evidence IDs byte for byte. No statistical speed
claim, heavy profiling, target execution or real deep-provider verification.

Fixes #4295
Fixes #4785
2026-10-17 20:20:53 +08:00
morluto 5d6029ea62 fix(apple): derive launchd domains from owning directories
Use the matched Contents/Library/LaunchAgents or LaunchDaemons suffix to
select a plist's domain. Remove the independent scan of every ancestor.
Keep regular-file, application-root, sidecar and suffix admission rules.

Adapt @bnkaiteam's proposal and ZIP boundary regression from PR #3504,
adding public CLI/stdio MCP controls for nested bundles, case variants,
misleading ancestors and excluded paths.

Validation: 53 focused checks, build, check:fast and docs:check pass.
Two new controls fail before the fix. 12 completed actual built CLI cost
controls preserve the ordinary reply and projection ID byte for byte.
No heavy profiling, target or real deep-provider execution; npm release
and whole-agent costs remain unverified.

Fixes #3353
2026-10-17 20:10:12 +08:00
morluto fc935d0df3 fix(mobile): derive runtime hints from classified components
Use the owning DEX/JVM and Ark/JavaScript component lists for runtime
summaries. Remove the independent filename checks and Harmony path copy.
Preserve native format checks and framework path conventions.

Adapt @eco-null's proposal and regressions from PR #3627. Cover all eight
supported source extensions and unrelated content, plus the Android
inverse mismatch discovered during consumer review: .dex/.class files
had no bytecode components but produced runtime hints.

Validation: 44 focused checks, build, check:fast and docs:check pass.
Six Harmony assertions and one Android built CLI assertion fail before
this fix. 36 actual built CLI cost controls preserve four ordinary
complete replies and projection IDs byte for byte. No heavy profiling,
target or real deep-provider execution; npm release unverified.

Fixes #3350
Fixes #4782
2026-10-17 20:02:26 +08:00
morluto a425b3e528 fix(mobile): classify authenticated file occurrences as components
Select file and slice identity once before Android, Apple and Harmony
classification. Keep directory/link facts in source inventory and Apple
anatomy rather than deriving code roles, runtime hints or bridges from them.
Preserve occurrence order, multiplicity and existing unavailable facts.

Adapt @flyl1u's proposal and public CLI/stdio MCP controls from PR #3830,
extending the controls and structural fix to Apple and slice/link semantics.

Validation: 80 focused checks, build, check:fast and docs:check pass.
Four package controls fail before the fix. 72 tiny completed source cost
tasks preserve all regular-file control replies and projection IDs byte
for byte. No heavy profiling, target or real deep-provider execution;
full agent/public-task costs and published npm package unverified.

Fixes #3349
2026-10-17 19:58:18 +08:00
morluto 149a0fc06c fix(apple): derive bundle anatomy from directory facts
Build separate directory and authenticated-file indexes before applying
anatomy name preferences. Share those facts across application roots,
nested bundles, layout, metadata, executable candidates and signing paths.
Remove mixed child-name interpretation and downstream membership repairs.

A leaf Contents/Versions resource no longer changes a shallow layout, and
leaf files/symlinks cannot manufacture application roots. Real explicit
and implied directories retain their existing semantics and spelling.

Adapt the directory-identity proposal and source regressions from PR #3505
by @bnkaiteam, extending it to metadata file selection and root admission.

Validation: 57 focused checks, build, check:fast and docs:check pass.
30 tiny completed source projection cost controls preserve ordinary deep
and versioned replies byte for byte. No real-provider/target execution,
heavy profiling or published npm verification.

Fixes #3359
Fixes #4779
2026-10-17 19:53:45 +08:00
morluto c192c58815 fix(apple): retain unavailable application components
Preserve unread file and slice facts before Apple attribution. Report partial
component coverage while keeping manifest count completeness independent.
Directories and symlinks retain their existing representation.

Consolidate occurrence identity interpretation and component schemas across
Apple, Android and Harmony; remove superseded producers. Android/Harmony
completed source replies and projection IDs remain byte-identical in controls.
Apple's additive unavailable_components field changes its projection IDs.

Adapt the unavailable-component proposal and real ZIP regressions from
PR #4692 by @rodricksz4h5, adding directory, CLI, MCP and non-encrypted controls.

Validation: 77 focused checks; check:fast; docs:check (5 README checks).
18 tiny completed source cost controls; no real-provider/target execution
or heavy profiling. Published npm package and large-input costs unverified.

Fixes #4688
2026-10-17 19:46:20 +08:00
e5ee3ac658 fix(apktool): preserve decoded APK Evidence subject (#4771)
Bind shared CLI/MCP Evidence to the digest already computed from the owned
APK snapshot. Exercise source mutation during decoding through the shared
application workflow and verify subject identity in the real Apktool lane.

Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-17 11:36:21 +00:00
morluto 969aafcf28 fix(ghidra): compare final DOS relocation windows
Adapt the correction and authored overlap control proposed by @berelels in #4156 to the bounded prepared-image owner. Retain each fixup's application facts, then read every memory window after all writes. Carry the producer's load segment into comparison and advance only the MZ evidence revision.

Validation: 129 focused parser/comparison/MCP checks, 38 shared MCP harness checks and 41 selected provider checks passed, with 15 checks overlapping those runs. Fourteen lifecycle checks also passed, plus static/build/docs checks. Two baseline controls failed before the correction. Twelve completed normal source comparisons retained byte-identical replies; no real Ghidra or maximal-relocation performance qualification is claimed.

Fixes #3981
2026-10-17 19:30:11 +08:00
Kaoru0822andClaude Opus 5.5 34239d445b fix(flutter): keep completed inspection facts when cancelled during snapshot cleanup (#4766)
FlutterBuildProvider.execute checks the caller signal after its awaited
snapshot-root removal. When the signal was aborted during a successful
first removal, that check replaced the completed AnalysisExecution with a
bare AnalysisCancelledError, so identify-flutter-build and
inspect-dart-aot reported a typed cancellation without
details.partial_observation and the shared CLI/MCP Evidence assembly
never saw the facts.

The post-cleanup check now keeps the caller's abort reason and retains a
completed result as the cancellation's partial observation. The Evidence
composition from executeProviderOperation moves into
createAnalysisExecutionEvidence beside the provider contract, so the
success path and the retained cancellation use one composer and carry
identical Evidence. Failed results keep their existing classification.

Fixes #4717

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 04:27:55 -07:00
Kaoru0822andClaude Opus 5.5 83d2780008 fix(ida): refuse universal Mach-O targets in attached profiles (#4765)
Attached IDA binds the open database to the target only through the
recorded whole-input SHA-256, path and image base. Every slice of a
universal Mach-O shares that digest, so an attached database of another
slice passed the identity check and its results were attributed to the
host-selected architecture. Attached admission now reports the target
format as unsupported, with lipo -thin and headless recovery, before any
upstream contact. Headless mode keeps thinning the selected slice.

Fixes #3871

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 04:27:46 -07:00
Kaoru0822andClaude Opus 5.5 8589b7e778 fix(evidence): verify staged output identity after its final write (#4764)
* fix(evidence): verify staged output identity after its final write

On macOS FAT32 and exFAT volumes an empty file reports a placeholder file
ID, and the first write assigns its cluster-derived ID. The staged JSON
writer sampled the descriptor identity right after exclusive creation, so
publication and cleanup both rejected REA's own staged file as replaced:
every export failed with cleanup_incomplete and retained a .rea-write-*
directory.

The staged output owner now observes the descriptor identity immediately
before closing it, after the last write, and uses that identity for the
publication and cleanup pathname checks. A failed observation leaves the
identity unknown, so cleanup still refuses pathname removal.

Fixes #4613

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(evidence): cover failed final staged identity reads

Reject the staged descriptor's final stat and verify that the writer
still closes the descriptor, refuses publication, and retains the
unverified staged content with incomplete cleanup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 04:27:38 -07:00
Kaoru0822andClaude Opus 5.5 63c530998e fix(flutter): report cancellation during snapshot cleanup with completed observations (#4762)
The one-shot Flutter CLI commands passed their command signal to the
inspection but not to finishAnalysisCommand, whose post-close cancellation
guard only runs when a signal is supplied. When the provider's first
snapshot-root removal failed, close() retried it after the inspection had
returned; SIGINT during that retry set exit status 130 while the command
still printed ordinary successful Evidence.

Pass the command signal to the finisher so an abort observed after the
awaited cleanup retry yields typed cancellation that retains the completed
Evidence as partial_observation. A failed retry still reports
cleanup_incomplete ahead of the later signal.

Fixes #4640

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 04:27:23 -07:00
Kaoru0822andClaude Opus 5.5 f03a55909d fix(mcp): accept authentic native Evidence for managed boundary verification (#4761)
The MCP verify_managed_native_boundaries handler resolved native
observations through an inspect_macho/analyze_function allowlist before
calling the shared verification service, and reported any other authentic
operation as evidence_integrity_mismatch. The domain already owns which
native operations it can read symbols from and counts the rest as
unsupported, which is what the CLI returns. The MCP resolver now only
authenticates native observations and leaves operation support to the
shared workflow, so both adapters return the same partial verification.

Fixes #4639

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 04:27:06 -07:00
Kaoru0822andClaude Opus 5.5 f1ab5959bd fix(javascript): bind response endpoints only from writes that reach the read (#4759)
collectResponseConsumers copied the linked request's endpoint onto a
response consumer whenever the receiver binding had exactly one
initializer (added in 0323ce0a2, #3822). A single initializer is a source
fact, not proof that the write reaches the read: an incoming parameter
that is overwritten only conditionally, or only after the read, still
lets the consumer read a caller-supplied response, yet it received the
request's exact endpoint.

The sole write now also has to cover the read, using the same rule the
call resolver applies to incoming values: either it is the binding's
stable unconditional declaration initializer, or the shared read-position
owner (semanticBindingAtRead) reports a definite preceding initializer.
Otherwise the endpoint stays null.

Fixes #4606

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 04:26:55 -07:00
Kaoru0822andClaude Opus 5.5 31c87b40ee fix(javascript): report oversized recovery inputs as file-size limits (#4757)
scanRecoveryFile raised a plain AnalysisOutputError when a selected file
exceeded the remaining operation-wide input allowance, and
snapshotRecoveryInput folded that, read failures and UTF-8 decode
failures into one invalid_value request issue. A 64 MiB + 1 input was
therefore reported as a retryable invalid argument whose only record of
the 67108864-byte budget was message text that also called it an
invalid analysis output, and malformed UTF-8 received the same reason.

The scanner now marks its byte-bound refusals with the bound it
enforced. snapshotRecoveryInput, which owns the meaning of that bound
as the remaining total input allowance, reports it as a non-retryable
file-size AnalysisResourceConstraintError with maximum_input_bytes and
remaining_input_bytes, and decodes UTF-8 separately as an invalid_format
issue. Other scanner consumers still receive an AnalysisOutputError, and
the limit stays enforced.

Fixes #4629

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 04:26:39 -07:00
Kaoru0822andClaude Opus 5.5 6a407619fe fix(javascript): report malformed runtime context events as excluded (#4755)
V8 Inspector capture counted an execution-context created or destroyed
event whose context ID was missing or not a safe integer as retained,
but recorded nothing about it. The capture counters claimed the event
was kept while the context list silently omitted it, and no unknown
identified the loss.

The producer that parses the event and owns the capture counters now
records an invalid-context count, mirroring the existing invalid-script
accounting. The projection reports a non-zero count as an explicit
unknown, so the retained count and the reported omission agree. No
context is invented from a malformed ID, and all-valid captures are
unchanged.

Fixes #4685

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 04:26:25 -07:00
Kaoru0822andClaude Opus 5.5 95b561f2e9 fix(native): retain Apple dispatch metadata descriptor cleanup ownership (#4745)
* fix(native): retain Apple dispatch metadata descriptor cleanup ownership

inspect_native_dispatch_metadata opened the target with raw
openRegularFile and closed it in a finally block, so a rejected close
replaced integrity, capacity or cancellation failures and left no owner
to retry. The read now uses the shared owned regular-file operation in a
client resource scope; the physical failure stays the cleanup primary,
client close retries the exact descriptor, and a confirmed release
clears the original error's cleanup uncertainty. Decoding runs after the
descriptor is released.

Fixes #4743

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(verify): give the Apple dispatch verifier an owned file scope

Pass an ArtifactResourceScope to inspectAppleDispatchMetadata and close
it after inspection so a failed descriptor close is retained and retried
rather than reaching an undefined scope.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 04:20:40 -07:00
morluto 2f9b9c48b8 fix(ghidra): stream DOS MZ overlay attestation
Keep initialized image facts and full source identity separate at the snapshot owner. Stream overlay bytes into original and modified digests, retain format-bounded image bytes, and consume prepared relocation/mapping facts. Preserve completed comparisons, exact cancellation and cleanup retry through descriptor finalization. Keep explicit source refusals as typed primary failures.

Validation: 119 focused parser/comparison/MCP checks, 40 selected provider checks and 14 lifecycle checks passed; static checks, runtime build, discovery and docs checks passed. Twelve small completed source comparisons retained byte-identical full replies. Real Ghidra, large input and full public CLI/MCP performance remain unverified. The separate overlap timing issue #3981 remains open.

Fixes #4770
2026-10-17 19:18:34 +08:00
morluto 3fc007534b fix(native): validate NativeAOT PE virtual ownership
Reject contradictory headers, overlapping virtual sections, DOS header reuse and image intervals outside the 64-bit address domain before metadata interpretation. Preserve empty and adjacent ranges, source section order, and raw padding semantics. Advance the metadata profile revision and retain provider snapshot refusal facts.

Validation: 37 parser checks and 35 selected provider checks passed; check:fast and build:cached passed. Six tiny authored-fixture summary/detail tasks returned byte-identical 791-token replies. No real-engine or Windows loader qualification.

Fixes #4763
2026-10-17 19:02:29 +08:00
morluto 9c56d09c05 fix(ghidra): preserve load-image completion through cancellation
Consume the named inventory observation once and compare typed facts through one final cancellation check. Retain measured or completed load-image facts and exact reasons, preserve source partials and cleanup, and remove duplicate interpretation and format-specific abort guards.

Validation: 131 contract/projection checks, 40 selected provider checks, and 24 selected private-file/socket lifecycle checks pass; typecheck, lint and builds pass. Includes the original confirmed descriptor-close boundary and late result consumption. Tiny authored fixtures and simulated installation/engine facts; no real Ghidra, investigated target or heavy profiling.

Equivalent completed DOS/raw/NativeAOT tasks retain byte-identical full replies and one source read. First processes and two alternating warm repeats; no statistically established performance improvement.

Fixes #3977
2026-10-17 18:52:42 +08:00
morluto a43ce8a98f fix(ghidra): retain measured facts through NativeAOT failures
Attach each caller’s measured observations before source failure and cleanup projection. Preserve those facts through admission, peer and draining cancellation, cached results, cleanup fences and later lease resolution.

Validation: selected NativeAOT provider controls and typecheck/lint passed in the load-image audit tree. Related consumer validation follows in the next commit.

Refs #3977, #4753
2026-10-17 18:52:42 +08:00
ae029fe2f8 perf(ghidra): aggregate binary overview at the provider boundary (#4754)
Compute overview counts at the Ghidra inventory owner, preserve complete inventory access and coverage, and share CLI/MCP projection. Keep native aggregate Evidence separate from workflow snapshot identities.

Fixes #4746

Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-17 10:44:24 +00:00
morluto 5120eb4412 fix(ghidra): join shared NativeAOT derivation and retain outcomes
Replace the private promise race with a named snapshot owner using SharedStartupAttempt. Keep peers independent, stop and join final departure, preserve source failures and completed facts through draining waits, and fence replacement behind retained cleanup. Close joins derivation before releasing the underlying client.

Validation: 31 selected simulated-provider lifecycle and NativeAOT checks, 80 shared-contract/schema/projection checks, typecheck, lint and builds. Completed tiny summary/detail/close comparison preserves full replies and one snapshot read. No real Ghidra, investigated target or heavy profiler execution.

Refs #4753
2026-10-17 18:37:57 +08:00
Ysopwnableandabdooo2235 f0d175dace fix(verifiers): read packed bridge without extraction
Parse only the exact regular packed Python bridge member without extracting into the caller-owned private runtime root. Preserve required Ghidra admission, typed callers, native lease ownership and cleanup on success or failure.

Validation: 263 focused real tar/Python/ownership and CI selection checks, 42 official WABT packaged CLI/MCP cases, full check:ci/check:fast/actionlint, real-package alternating measurements and exact-head hosted CI passed. The required Windows lane exercises the retained native lease without skipping missing authority.

Fixes #4134.

Co-authored-by: abdooo2235 <abdalrhmantheiv15@gmail.com>
2026-10-17 10:35:31 +00:00
morluto 73965f9431 fix(ghidra): retain snapshot outcomes and join reader cleanup
Use the shared artifact resource scope for snapshot reads. Preserve completed facts, exact cancellation reasons and descriptor cleanup leases through session and public error projection; keep parent cleanup pending until all retained resources release.

Validation: 22 selected snapshot lifecycle checks and 85 contract, projection and bounded-reader checks; typecheck, lint and runtime builds. Uses small authored Node/socket fixtures, not real Ghidra or investigated target execution.

Refs #4752, #3977
2026-10-17 18:25:59 +08:00
morluto 5e13e0a876 refactor(files): allow reader-owned cancellation failures 2026-10-17 18:25:52 +08:00
morluto 4a076f368c fix(native): bound NativeAOT reads to declared PE headers
Validate the consumed optional-header extent before interpreting mapping fields. Require complete header-backed read intervals and prevent failed header reads from falling through to section mappings. Advance NativeAOT profile interpretation identity to v4.

Refs #4750
2026-10-17 18:07:29 +08:00
morluto d9099190ab fix(native)!: retain UI outcomes through finalization
Carry caller and runtime deadline cancellation through helper cleanup, preserve command-owner reasons and physical failures, and retain collected scenario reports with typed terminal errors.

BREAKING CHANGE: failed or cancelled native UI scenarios now return typed errors with the collected report in partial_observation instead of successful reports containing failed or cancelled steps.

Refs #4227
2026-10-17 17:57:50 +08:00
morluto 04221e5f01 fix(native): bind UI runtime cleanup to exact owners
Join admitted helper work before runtime removal, retain one preparation outcome and root, and bind originating and retry errors to exact runtime release leases with live resources.

Refs #4227
2026-10-17 17:57:50 +08:00
Ysopwnable 2092f90b7e perf(ci): upload only current portable build cache entries
Stage only the current portable Turbo task archives, metadata and available manifests for same-run artifact consumers. Share task identity/hash validation with cross-run keys and fail publication on incomplete preparation, while retaining existing cache authority and consumer build/prepack verification.

Validation: 252 focused tests, full check:ci, docs:check, check:fast, actionlint, real Turbo output equivalence and invalidation, restored CLI/MCP scenarios and full Linux package verification passed. Fault injection detected historical-entry inclusion. Exact-head hosted CI and available review passed; measurement limits are recorded in the PR.

Refs #4744.
2026-10-17 09:44:11 +00:00
morluto 0f7fd005db fix(native): retain LLDB readback ownership through cleanup
Own journal, capture, result and PID descriptors through confirmed release. Preserve parsed facts and exact cancellation reasons while retaining unresolved children and gating runtime removal on their completion.

Refs #4742
2026-10-17 17:40:49 +08:00
gongfppandgongfpp 5a7e8a166a fix(mcp): normalize browser scenario input errors (#4661)
Co-authored-by: gongfpp <22683417+gongfpp@users.noreply.github.com>
2026-10-17 17:36:32 +08:00
gongfppandgongfpp 9534e42ec8 fix(adb): mark unknown permission coverage partial (#4663)
fix(adb): mark unknown package coverage partial

Co-authored-by: gongfpp <22683417+gongfpp@users.noreply.github.com>
2026-10-17 17:36:19 +08:00
6b27d8f4e3 fix(electron): retain fuse descriptor ownership through failed final close (#4668)
* fix(electron): retain fuse descriptor ownership through failed final close

Static Electron fuse inspection admitted a file descriptor and closed it
in a bare finally: a rejecting final close() replaced the in-flight read
error, was classified by errno (e.g. EACCES) as a read permission failure,
and silently dropped ownership of the still-live descriptor.

Route the read through withOwnedRegularFile so a failed close keeps its
exact owner, preserves the primary error in the outcome, and surfaces as
RegularFileCleanupFailure. The provider now maps that to a primary error
with its own classification plus a ProviderCleanupError reporting
incomplete cleanup, instead of an access-denied read failure.

Regression tests fault close() (EACCES) with and without a primary read
error (EIO): both fail on the pre-fix revision and pass with the fix.

Fixes #4658

* fix(electron): keep cleanup report and partial evidence through cancel and successful reads

Address automated review on #4668:

- Handle RegularFileCleanupFailure before the cancellation check in
  failureFor: a cancelled operation whose final close rejects now reports a
  typed cancellation composed with the incomplete-cleanup error, instead of a
  bare AnalysisCancelledError that silently dropped the descriptor owner.
- Attach the completed fuse image as an explicitly partial Evidence
  observation when only the final close fails, instead of discarding the
  successful read. The abort-after-completion path keeps the same evidence on
  the composed cancellation error.
- Add provider-level regression tests: combined read+close faults, abort
  synchronized with the faulted close boundary, and partial-observation
  retention. Abort-during-close and partial-evidence tests fail pre-fix.

29/29 electron tests green; npm run check:fast clean.

---------

Co-authored-by: nosmile99 <nosmile99@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-17 17:36:06 +08:00
Oke Iyanuoluwa EnochandClaude Opus 5.5 f85a932d71 test(paths): pin Windows separator normalization before native admission (#4733)
rea-agents 6.5.0 passed an absolute caller path to Windows native
snapshot admission verbatim, so a spelling with forward slashes or
repeated separators reached the native component walker, which rejects
empty components as "Ambiguous Windows path component is unsupported"
(Win32 50). Current main already resolves every Windows caller path with
win32.resolve (fc791bb60), which collapses those spellings.

Pin that boundary: absoluteCallerPath normalizes four separator spellings
of one absolute Windows path, and on Windows parseBinaryTarget returns the
same canonical sourcePath for each of them. The Windows test fails when
the 6.5.0 verbatim-absolute-path rule is restored.

Fixes #4707

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 17:14:16 +08:00
morluto 69e670d268 fix(native): own call preflight hashing through client close
Reuse stable-file hashing and exact cleanup leases for target verification, join admitted call workflows before tracer close, and block replacement work while descriptor cleanup remains unresolved. Preserve physical failures, exact cancellation reasons and completed call facts; remove the duplicate reader and AbortError-name heuristic.

Validate real inert-file cleanup controls and controlled tracers through provider and MCP boundaries. No LLDB execution, real OS close-failure or Windows qualification.

Refs #4740
2026-10-17 17:12:39 +08:00
Ysopwnable 9d5b931875 perf(tests): reuse MCP connections within source matrices
Reuse test-owned stdio MCP connections within two stateless source-analysis matrices while retaining every original CLI/MCP scenario, schema/Evidence checks and test-finished cleanup. Add cross-input identity controls.

Validation: 23 focused public-workflow tests and complete check:ci passed; stale-response fault detected. Exact-head hosted CI and available review passed. Pinned workload measurements and their limits are recorded in the PR.

Refs #4737.
2026-10-17 09:11:22 +00:00
morluto 715c9c8a84 fix(native): retain signature facts through finalization cancellation
Bind signature inspection to its named domain result and retain completed facts in typed cancellation. Check cancellation after descriptor and root finalization, preserve exact caller reasons, and keep physical failures primary. Use explicit cancellation checkpoints instead of signal-state guesses.

Validate root and descriptor cleanup, retries, late root handoff, opaque reasons, provider projections, and existing real codesign/CLI controls. No real OS close-failure or Windows qualification.

Refs #4735
2026-10-17 17:01:21 +08:00
morluto 4f3b573b09 fix(native): bind signature snapshot cleanup to exact leases
Retain roots and descriptors through shared cleanup leases and bind composed errors to their actual pending owners. Successful retries shrink live attribution and reveal the original typed failure.

Validate mixed root and descriptor failures, partial/final release, existing target binding, and real codesign and CLI controls. No real OS close-failure or Windows qualification.

Refs #4734
2026-10-17 16:52:28 +08:00
morluto e09a503f44 fix(native): retain plist descriptor cleanup ownership
Use shared owned file operations for plist admission and source probes. Join admitted work during close, retain exact descriptor leases through retries, and preserve physical failures, cancellation reasons and completed converter output.

Validate with controlled cleanup faults and native plist CLI workflows on inert data. Real OS close-failure incidence and Windows behavior remain unverified.

Refs #4723
2026-10-17 16:43:14 +08:00
morluto 26bb85c007 fix(core): keep released cleanup dependencies empty
Distinguish unbound resource attribution from a completed dependency set so final release cannot restore historical resource names.

Refs #4578
2026-10-17 16:43:14 +08:00
9744d07211 docs(skills): capture measured analysis optimization workflow
Add a reusable contributor workflow for locating analysis bottlenecks, preserving Evidence and cache ownership, and comparing equivalent real tasks. Keep pinned Kuna results and experimental limits in an optional case study.

Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-17 08:33:18 +00:00
ShuaixinHuang a040533470 fix(ghidra): reject byte matches outside searched coverage (#4531)
* fix(ghidra): reject byte matches outside searched coverage

* test(ghidra): cover inclusive and overlay byte match bounds

* test(firmware): assert composed cleanup error evidence

* test(ghidra): assert cancellation during profile shutdown

* test(jeb): retain Linux ownership prerequisite in isolation
2026-10-17 16:18:18 +08:00
ShuaixinHuang abe49808b2 fix(plist): ignore inherited XML entity lookup properties (#4530)
* fix(plist): ignore inherited XML entity lookup properties

* test(firmware): assert composed cleanup error evidence

* test(ghidra): assert cancellation during profile shutdown

* test(jeb): retain Linux ownership prerequisite in isolation
2026-10-17 16:17:53 +08:00
morluto 70de702109 fix(browser): share DOM snapshot admission
Own decoded node columns, attributes and parent validity once for web, Electron and SVG interpretation. Reject dangling, self and cyclic parent facts; preserve source positions and independently unknown fields. Remove fallback conversions and duplicated string/attribute interpretation.

Validation: baseline public CLI controls failed for four malformed cases with valid control passing; final 103 focused checks, 5 final built CLI controls, 1 late-detach cancellation control (20 skipped), check:fast and docs:check passed. Fake CDP producers and generated local files; no real browser/Electron or Windows qualification.

Refs #4709
2026-10-17 15:59:57 +08:00
4e14452ddb fix(native): report unidentified text plists as unknown (#4320)
* fix(native): report unidentified text plists as unknown

Require explicit XML identification in file output instead of treating all
text as XML. Preserve decoded plist values and source-format provenance
when plutil falls back to XML conversion.

Add provider-boundary regressions and real macOS CLI controls for OpenStep,
data-bearing OpenStep, XML, UTF-16 XML and binary property lists.

Fixes #4294

* fix(native): keep XML plists without a declaration labeled xml

`file -b` names XML only when a document declaration is present, so the
narrowed `\bXML\b` check reported a declaration-less XML plist as unknown and
broke the real-plutil `decodes xml plists that plutil cannot express as JSON`
CLI test. When `file` reports only text, read the source head once and label it
XML only when its first non-whitespace character (after a UTF-8 BOM) is `<`;
OpenStep and other text stay unknown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(native): distinguish OpenStep data from XML source markup

Recognize XML markup instead of treating every leading angle bracket as XML.
Keep data-only OpenStep sources unknown, honor explicit JSON identification,
and decode BOM-marked UTF-16 prefixes before detecting declarationless XML.
Add source-format regressions and macOS CLI cases for data-only sources.

---------

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 15:52:25 +08:00
星闌 de9371b85d fix(javascript): preserve effective child-process stdio options (#4336)
* fix(javascript): preserve effective child-process stdio options

* fix(javascript): retain proven callbacks and primitive stdio keys

* fix(javascript): preserve argv shapes and nullish spreads

* fix(javascript): bound callable proofs and retain exposed binding uncertainty

* test(javascript): keep callback acceptance within transport budget

* test(javascript): inspect application coverage for callback proof limits

* fix(javascript): keep spread command argument slots uncertain

* fix(javascript): bound mapped argument content proofs

* fix(javascript): distinguish erased declarations from runtime shadows

* fix(javascript): prove runtime module context before binding isolation

* fix(javascript): share erased declaration and opaque scope proofs

* fix(javascript): keep lowered enum scopes opaque

* fix(javascript): narrow stdio proofs and consolidate regressions

* perf(javascript): reuse lexical references for child shapes

* fix(javascript): retain CommonJS child argument context

* fix(javascript): keep Annex B affected child arguments unknown

* fix(javascript): recognize value import-equals runtime modules

* fix(javascript): retain exact stdio through module and primitive syntax

* fix(javascript): bound inline spread property resolution

* fix(javascript): distinguish copied methods from getters
2026-10-17 15:51:55 +08:00
郭立 Guo Li 0cd4eafd61 perf: reduce MCP launcher and browser capture overhead (#4350)
* perf(browser): 为单次捕获复用脚本资源索引

* docs(setup): 说明多会话直接启动以减少 npm 驻留
2026-10-17 15:51:23 +08:00
Tapraj-PathakandCopilot 509d4c8f56 Fix permissions count issue for pkg=null in ADB analysis and add test cases (#4665)
* Fix permissions count issue for `pkg=null` in ADB analysis and add test cases

* fix(adb): handle null package permission counts

Recognize pkg=null as unknown permission data and keep the provider projection aligned with the parser. Add regression coverage for the parser and provider.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-17 15:45:15 +08:00
morluto 79d2b71896 fix(electron): preserve unknown resource sizes
Use one typed CDP resource-size owner in web and Electron capture. Supplied invalid sizes retain null plus completeness diagnostics; omitted sizes remain unknown without inventing malformed metadata. Preserve other resource facts and valid zero/positive values.

Validation: public CLI regression failed against baseline clamping; 36 focused controls passed after the fix, check:fast and docs:check passed. Fake CDP producer with real CLI/transport and tiny local files; no real Electron/browser or heavy profiling qualification.

Refs #4708
2026-10-17 15:41:07 +08:00
Kaoru0822andClaude Opus 5.5 9be493880c fix(kuna): name the setting that owns a timed-out Kuna run (#4343)
* fix(kuna): name the setting that owns a timed-out Kuna run

A Kuna run past its deadline surfaced the generic "Analysis took too
long. Try a smaller request" message. The request is already one
function or one whole-program scan, and the same run takes as long
again, so the advice was wrong and REA_KUNA_TIMEOUT_MS or
REA_KUNA_PROJECT_TIMEOUT_MS was never mentioned.

Each Kuna command now carries a KunaDeadline: the configured
milliseconds and the setting that owns them, from KunaConfiguration.
A timeout reports, for example, "kuna decompile did not finish within
REA_KUNA_TIMEOUT_MS (120000 ms)." Its remediation says to raise that
setting and restart REA, and for a single-function decompile that the
session's project export can still answer. AnalysisTimeoutError now
accepts a producer message and remediation, which projection uses and
per-waiter scan errors keep.

Refs #1957

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(kuna): scope the provider_timeout statement to direct Kuna runs

A timed-out project export is reported through the export failure as
capability_unavailable, not provider_timeout.

Refs #1957

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(kuna): verify native xrefs under the project deadline

The provider's xrefs scan runs under REA_KUNA_PROJECT_TIMEOUT_MS, but the
native oracle in verify:kuna aborted at REA_KUNA_TIMEOUT_MS. A scan that
fit the project deadline but not the function deadline failed
verification although the provider answered it. Use the same deadline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 00:38:16 -07:00
morluto 748ac7f0df fix(sqlite): preserve column identity before row selection
Read raw column metadata through the built-in PRAGMA in the private empty temp schema. Validate bytes before names become SQL identifiers, and select inventory subsets by exact encoded identity so unrelated malformed names cannot alias valid columns.

Validation: 5 focused identity controls; 15 selected producer/provider controls (33 skipped); public CLI baseline collision, fixed format refusal and valid-subset success; check:fast and docs:check. Generated inert SQLite fixtures on macOS arm64 with Node 24.18.0; no Windows qualification or large-file/stress workload.

Refs #4710
2026-10-17 15:33:59 +08:00
Saim ShafiqueandSaim Shafique 77c8a1ba32 fix(javascript): mark resource-skipped files parse_status skipped (#3852)
* fix(javascript): mark resource-skipped files parse_status skipped

Files above ~5 MB exhaust the worker heap during static analysis (or exceed
the 8 MB static-checkpoint budget with a larger heap). They were inventoried
with text included but never parsed, leaving parse_status null and no
counter, so the summary read as 'nothing there' instead of 'not analyzed'.

Report them as skipped: parse_status 'skipped' on the file observation and a
skipped_javascript_files statistics counter. The analysis-failure reason keeps
surfacing as a limitation entry.

Fixes #3670

* fix(javascript): default skipped_javascript_files when reading old saved evidence

Saved analysis evidence written before skipped_javascript_files existed failed validation against the strict reconstruction statistics schema, so users could not reuse old results for comparisons or runtime reconciliation. Accept the field's absence on read (defaulting to 0) while continuing to write it for new results.

* fix(javascript): preserve unknown skipped count for legacy evidence

The .default(0) gate fix fabricated a measured zero for saved
pre-change evidence, so comparisons would receive a definitive
"no files skipped" fact the producer never established. Use the
codebase's exactOptional idiom instead: legacy reads accept absence
and keep omission (unknown), while the producer still always writes
the field (required by JavaScriptArtifactReconstructionResult).

Tests: legacy payload now asserts the key stays absent/undefined;
current-output test keeps asserting the producer-written counter.

* fix(javascript): add required failure scope and status to graph fixture

---------

Co-authored-by: Saim Shafique <sx4im@users.noreply.github.com>
2026-10-17 16:33:41 +09:00
Mohamed Ayoub Eleuch 8a2a1a2b8b fix(cutter): preserve mixed Unicode output within the frame limit
Count real UTF-8 bytes in bounded chunks and retain authenticated omission for malformed Unicode. Preserve existing wire/resource limits and completion semantics.

Validation: producer/client regressions, fresh real Linux Cutter CLI/MCP output and Evidence recovery, and complete exact-head CI passed. Hosted unrelated CLI timeout and unchanged rerun results remain recorded in the PR.

Fixes #3330.
2026-10-17 07:30:30 +00:00
morluto c7e9e096d3 fix(plist): reject malformed XML before value decoding
Reuse the shared validated XML document for depth and prototype-key preflight, preventing recovery-only values from being projected as observations. Preserve OpenStep and valid XML source semantics.

Validation: 46 focused checks; 5 final syntax controls; 10 encoding and public CLI/MCP overlap checks; check:fast. Public malformed-entity CLI changed from success to typed format failure. Generated inert artifacts only; no real Apple application or native engine execution.

Refs #4714
2026-10-17 15:24:57 +08:00
Aditya kharmale 8c62a4aa36 fix(javascript): preserve case-sensitive Unicode seed identity
Use NFC for case-sensitive feature seeds while preserving the existing explicit insensitive compatibility fold.

Validation: exact/substring/absent controls, fresh public CLI/MCP workflows and complete exact-head CI passed. Hosted CLI-worker failure and unchanged rerun results remain recorded in the PR.

Fixes #3314.
2026-10-17 07:20:34 +00:00
morluto 3cfa00a29c fix(ghidra): preserve valid Unicode language metadata
Validate XML bytes before shared syntax parsing so literal U+FFFD remains valid while malformed bytes and declaration mismatches fail at their producing boundary.

Validation: 92 focused checks, 3 selected fake Apktool boundary checks, 6 final encoding controls, check:fast and docs:check. Ghidra coverage uses tiny real files and descriptors, without a real engine or Windows qualification.

Refs #4712
2026-10-17 15:17:03 +08:00
morluto 623339a7ab refactor(xml): centralize strict document parsing
Own syntax validation and source-located DOM projection together. Preserve explicit consumer policies and remove duplicated warning-message filters from Apktool and SAZ.

Validation: 92 focused checks, 3 selected fake Apktool boundary checks, check:fast and docs:check. No real Apktool or Ghidra execution.

Refs #4694
2026-10-17 15:16:24 +08:00
Kshitiz Jain c0b3cd14b1 fix(javascript): exclude class member keys from lexical reads
Classify noncomputed class member keys and private names at the shared reference owner, avoiding false reads and closure captures. Preserve computed keys and initializer/body reads.

Validation: regression controls, fresh public CLI/MCP workflows and complete exact-head CI passed.

Fixes #3310.
2026-10-17 07:11:19 +00:00
siegfly 195cf18818 docs(native): distinguish analysis memory from source bytes
Document initialized Ghidra analysis-memory bytes, source mappings and incomplete coverage. Retain the portable test-owned alternate ELF for liveness cancellation.

Validation: fresh docs:check and complete exact-head CI passed. Historical real Ghidra checks remain explicitly attributed to their recorded revision/platform in the PR.

Refs #3075.
2026-10-17 07:11:10 +00:00
keeponlight e26bb49a4b fix(managed): avoid duplicate decode budget charging on cached metadata strings
Charge each immutable metadata heap string decode once; keep unique-string and per-occurrence projection accounting bounded.

Validation: focused capacity regressions, fresh real .NET DLL CLI/MCP workflows and complete exact-head CI passed. Before/after measurements and their limits are recorded in the PR.

Fixes #3449.
2026-10-17 07:10:58 +00:00
Sua Choi 326c073880 fix(apktool): preserve valid replacement characters in decoded XML (#4697) 2026-10-17 15:09:14 +08:00
Ysopwnable c6b62754b0 perf(kuna): reuse sorted string inventory for exact lookups
Reuse the validated string inventory's numeric address order for O(log n + matches) exact reads, preserving every encoding at the selected address without a second lookup index. Keep streaming validation, session cache lifetime, full listings, provenance and caller choices.

Validation: complete latest-head CI passed. Original real Kuna 1.833 Linux correctness and warm-lookup measurements remain pinned to their recorded feature revisions in the PR. The latest local 300 ms export-deadline observation and unchanged 31-test provider recheck are explicitly documented there; no assertion or timeout was weakened.

Refs #4695.
2026-10-17 07:04:16 +00:00
morluto 264799b0ff fix(kuna): retain workspace rollback failures
Compose failed rollback with the original target or read failure, bind it to the exact workspace lease, and retain the failed attempt while cleanup remains unresolved. Remove the silent release wrapper and duplicate exception wrapping; keep clean failures retryable and unknown causes opaque.

Refs: #4704
2026-10-17 14:56:01 +08:00
Ysopwnable 5b24399387 test(kuna): preserve caller and producer graph failure context
Align malformed call-graph retry assertions with caller-attributed errors while retaining the parser operation in diagnostics and the cause. Keep all malformed shapes, corrected-response retry, exact native-command count and owned-client cleanup coverage.

Validation: clean main reproduces 3 failed / 14 passed; corrected complete Kuna boundary selection passes 266 tests across 26 files; full check:ci and complete current-head CI passed. Greptile 5/5; human owner reviewed the diff/evidence and explicitly declared readiness. Codex review remains unavailable due repository usage limits. Test-only; no production/schema/authority change or real-engine qualification.

Fixes #4701.
2026-10-17 06:47:06 +00:00
morluto 5927937179 fix(kuna): preserve cancellation and cleanup ownership
Reuse shared startup ownership for query waits and preserve opaque cancellation reasons, captured command facts and typed primary failures. Bind retained command errors to exact process and workspace leases; share close attempts and update earlier caller and aggregate errors after cleanup retry.

Refs: #4700, #4702
2026-10-17 14:38:50 +08:00
morluto 988494abdb fix(kuna): own cancellable installation startup
Reuse the shared startup owner for independent caller waits, final departure cleanup, and session closure. Carry the producer signal through version probing and fingerprint finalization. Retry cleanly cancelled acquisition after settlement while retaining authored failures and exact descriptor cleanup dependencies.

Refs: #4698
2026-10-17 14:23:17 +08:00
Ysopwnable 3c9b5d6d02 test(ci): align fixtures with runtime and evidence contracts
Restore test fixture runtime prerequisites and align assertions with current Evidence and uncertainty contracts. Separate actual CLI publisher refusal from explicitly admitted Hopper socket/CLI fixtures, retaining exact wire identity and cleanup coverage. Validate retained EVM Evidence against exact unchanged source-carrier bytes.

Validation: 88 targeted local test passes across three completed selections, two platform skips, full check:ci; hosted Windows probes and complete current-head CI. No production authority, authentication or schema change. Controlled fixtures do not qualify actual disassemblers or workers.

Fixes #4690.
2026-10-17 06:20:23 +00:00
morluto 54c71313ae fix(kuna): attribute inventory failures to callers
Normalize shared installation and inventory failures at the session boundary, retaining producer operation metadata. Preserve exact cancellation reasons before admission and while waiting without adopting foreign cleanup authority.

Refs: #4689
2026-10-17 14:13:25 +08:00
morluto 6eb55be4e8 fix(kuna): preserve live cleanup in caller failures
Reuse canonical typed failure attribution instead of copying cleanup markers. Preserve captured facts, typed primary outcomes and opaque cancellation reasons; bind caller projections to the exact producer cleanup owner.

Refs: #4693
2026-10-17 14:13:16 +08:00
morluto 5717ad24aa fix(kuna): attribute installation failures to caller operations
Project the immutable shared installation failure through the existing authored-failure normalizer for each requesting operation. Preserve direct health identity, diagnostics, retryability and live cleanup dependencies.

Validation: ten baseline/candidate public CLI cases each; nineteen focused boundary checks plus four overlapping final cleanup controls, check:fast and docs:check passed. Concurrent callers share one acquisition failure without mutating its producer. No real Kuna, Windows, MCP-specific failure replay or performance measurement. Refs #4631.
2026-10-17 13:49:27 +08:00
morluto d09be9cfa2 docs(browser): clarify scenario cleanup after termination
Document retained profile data, missing-executable Playwright artifacts, platform cancellation differences and the absence of later reclamation. Keep recovery work tracked in #4659.

Confirmed installed Playwright 1.58.2 preparation order and one public CLI missing-executable reproduction in a private fixture; an empty artifacts directory remained and was removed only with the owned experiment root. Forced termination was not rerun. docs:check passed.
2026-10-17 13:49:20 +08:00
morluto 7086768a27 fix(adb): preserve display-scoped window focus declarations
Retain every focus declaration with its display header and identity, including explicit absence, unknown identities and duplicates. Derive singular summaries only for one unambiguous context; preserve scope and ambiguity through named CLI/MCP contracts.

Validation: original public CLI replay before/after, four failing baseline parser controls, 23 focused checks plus 11 overlapping provider/parser controls, converted MCP schemas, check:fast and docs:check. Bounded completed synthetic CLI warm reply tokens 282/513, elapsed 0.391/0.403s and CPU 0.622/0.622s; no efficiency claim. No live Android, vendor or large-output qualification. Refs #4660.
2026-10-17 13:40:09 +08:00
morluto be0b2d138c fix(adb): scope package uninstall requests to user zero
Send shell pm uninstall --user 0 through the shared quoted command boundary. Align tool guidance and Evidence limitations with the selected request scope while preserving device refusal diagnostics.

Validation: two provider controls failed before the fix and pass afterward; three public CLI/MCP scenarios capture exact arguments and equivalent success facts; nineteen command/SDK contract checks, check:fast and docs:check pass. No real device, multi-user data preservation, vendor/APEX behavior or performance measurement. Refs #4673.
2026-10-17 13:30:29 +08:00
morluto c360cfcd6d fix(browser): parse complete COOP and COEP structured items
Validate token items and their parameters before projecting exact declared policy tokens. Keep malformed and non-token values unknown; retain separate grammar for other policy headers.

Validation: real pinned Chrome public CLI baseline/candidate, 101 focused checks and 90 overlapping final checks, 834 upstream item-vector adaptations with two permitted refusals, check:fast and docs:check. Other browser/platform and policy-specific MCP delivery unverified; no performance measurement. Refs #4677.
2026-10-17 13:26:13 +08:00
morluto df2fc251e3 fix(browser): preserve repeated document Link fields
Parse CDP repeated field lines independently before applying URI and quoted-parameter delimiters. Preserve declaration order and derive agent hints from retained links.

Validation: real pinned Chrome public CLI baseline and candidate each captured three responses per fixture; repeated Document links/hints restored and comma/single/Fetch controls unchanged. Three source controls failed before the fix. Sixty-one focused checks plus fifteen overlapping final network checks, check:fast and docs:check passed. No performance measurement or other browser/platform claim. Refs #4679.
2026-10-17 13:21:50 +08:00
morluto 0ec4826633 fix(evm): preserve command owner cancellation reasons
Copy the exact opaque reason already recorded by OwnedCommand rather than discarding it at EVM failure conversion. Keep physical, timeout and incomplete cleanup outcomes distinct.

Validation: direct compiled probe and eight source controls; four reason controls failed before the fix. Twenty-one overlapping service/adapter checks, check:fast and docs:check passed. No real Linux x64 worker cancellation or public delivery exercised. Refs #4686.
2026-10-17 13:18:05 +08:00
morluto d694aef227 fix(evm): retain completed evidence through cleanup cancellation
Share validated interface Evidence assembly across success, cleanup failure and late cancellation. Preserve exact admission and completion abort reasons.

Validation: 26 service, MCP and host-refusal checks passed; check:fast passed. Thirty Linux x64 provider checks skipped on Darwin arm64, including expanded root-removal controls. Linux real-worker and public SIGINT reproduction remain unverified; no performance measurement. Refs #4683.
2026-10-17 13:16:40 +08:00
morluto 5853089885 fix(hopper): retain search failure context and cleanup ownership
Attribute regex inventory failures to the accepted search operation while retaining wire identity, typed outcomes, captured facts and live cleanup dependencies. Preserve exact cancellation reasons at inventory and progress completion.

Validated with focused source, socket-boundary and public CLI fixtures, existing MCP contract checks, check:fast and docs:check. Simulated Hopper only; real-engine behavior and performance costs remain unmeasured. Refs #4680.
2026-10-17 13:12:43 +08:00
morluto 4d980d0176 fix(javascript): preserve unresolved version relationships
Keep positive topology differences separate from unpaired neighbor identity. Preserve exact content pairing and source observations, report unresolved relationships without inventing incomplete inventory, and omit comparison edges when no substantive difference was observed.

Validated with 45 focused domain/CLI/MCP tests, nine expanded domain controls, check:fast, and docs:check. Clean baseline fails seven controls and passes two positive controls. Controlled static fixtures only; no target execution or large-bundle profiling.

Refs #4681
2026-10-17 12:51:04 +08:00
morluto 556504edcb fix(javascript): distinguish relocation from application changes
Keep location-only matched pairs unchanged under complete coverage while retaining both coordinates and location dimensions. Preserve incomplete coverage as unknown and retain definite content, property, and relationship changes.

Validated with focused domain, compiled CLI and MCP workflows, static checks and docs checks. A tiny public CLI bundle fixture reproduces the issue; the reported Bitwarden releases were not rerun.

Refs #4520
2026-10-17 12:36:21 +08:00
morluto 1e36e707b1 fix(ghidra): preserve launch outcomes through cancellation and rollback
Carry producer cancellation separately from physical launch errors and diagnostic causes. Retain exact opaque reasons, LaunchSupport warnings, and rollback owners; consume the authored outcome without reclassifying failed launches from a later deadline.

Validation: focused client/provider/rollback and helper/MCP tests, check:fast, docs:check. Controlled native process ownership and Node bridge only; real Ghidra/JVM and Windows/Linux fault behavior unverified.

Refs #4517
2026-10-17 12:27:08 +08:00
morluto e31aa72c9d fix(ghidra): retain executable header read and cleanup ownership
Read bounded stable prefixes through the installation file owner, preserve unknown header diagnostics, and propagate cancellation or incomplete cleanup. Share extent and identity validation with full artifact reads.

Validated with 269 focused tests, check:fast, and docs:check. Controlled Darwin file faults reproduce the original lost owner; real Ghidra and native Windows/Linux verification remain unperformed.

Refs #4676
2026-10-17 12:17:44 +08:00
morluto 5112fe07ae fix(ghidra): admit installation metadata through owned bounded reads
Reject special files without blocking, cap the properties payload at 1 MiB before allocation, and verify descriptor extent and the selected name. Preserve Latin-1 data, selected symlinks, physical filesystem facts, completed observations and exact failed-close ownership.

Reuse the stable bounded reader for profile artifacts and consolidate Ghidra file failure projection. Distinguish missing-at-open from disappearance or read failure, and attribute typed prerequisite failures at the consuming operation.

Validation: 384 focused tests across 14 files, 42 seed/extension controls, 32 final metadata/profile/CLI controls, check:fast and docs:check. Controlled macOS file/process fixtures; no real Ghidra/JVM or Windows/Linux lifecycle verification.

Bounded FIFO reproduction: baseline direct and public doctor required external cleanup around 2.2s; candidate returned independently around 0.4/0.57s, all fixture groups verified empty. Warm normal doctor baseline/candidate: elapsed 1.211/1.230s, CPU 1.239/1.247s, sampled family RSS 524/524MB, reply tokens 3116/3116. No performance improvement claimed; true cold and whole-agent costs unmeasured.

Refs #4672
2026-10-17 12:02:26 +08:00
morluto c72786f289 fix(ghidra): attribute returned profile cancellation at its owner
Apply the existing cancellation projection to returned Results as well as thrown failures. Preserve the exact owner-close reason and keep shutdown joining the active resolution before later admission.

Strengthen the held-path regression to require typed cancellation instead of success. The original assertion and the stronger attribution control failed on unchanged cc2e18a26; focused validation and check:fast pass after repair. Controlled private files and recording provider ports; no native Ghidra claim.

Refs #4674, #4561
2026-10-17 11:53:59 +08:00
morluto cc2e18a265 fix(ghidra): own Java prerequisite probes through final cleanup
Replace synchronous Java execution with bounded owned commands. Keep task-scoped readiness observations fresh across later tasks, defer target client prerequisites behind a closeable owner, and join doctor cancellation and cleanup.

Preserve completed physical failures across late cancellation, exact cleanup dependencies and partial observations. Admit target/profile inputs before probing and return typed factory failures instead of unavailable client wrappers.

Validation: 405 focused tests across 24 files, 2 explicit skips; 109 provider/doctor controls rerun after final admission refinement; check:fast and docs:check. Controlled macOS Java-shaped commands and public CLI/MCP workflows; no new JVM/Ghidra or Windows/Linux lifecycle verification.

Measured warm public doctor baseline/candidate: elapsed 0.447/1.223s, CPU 0.624/1.237s, sampled family RSS 379/529MB, reply tokens 3133/3133. Ownership adds cost; no performance improvement claimed. True cold, whole-agent and MCP costs unmeasured.

Refs #4561. Remaining installation file admission tracked in #4672.
2026-10-17 11:29:03 +08:00
morluto fb146bf40b fix(providers): own deferred client acquisition through final cleanup
Return a closeable owner before prerequisite work begins. Share creation across independent callers, join the final departure and late handoffs, retain exact failed resource owners, and gate operation dispatch again after acquisition.

Keep reuse admission separate from native health. Move shared provider ownership below application workflows and consolidate failure attribution; remove the superseded lazy client.

Validation: 34 focused owner, lazy, composite and session controls; check:fast. Compiled baseline/candidate recording-provider workflow reproduces then fences dispatch after close. No native provider claim.

Refs #4561, #4667
2026-10-17 11:25:23 +08:00
morluto 870dcb1fda test(binary): await and validate session status projections
Replace the cast from an asynchronous owner result with an awaited schema-validated projection. Give the producer a narrow typed router port and construct complete recording inputs.

Validation: all three controls failed on unchanged e567033a7 and pass after repair; check:fast passed. No provider runtime behavior claim.

Refs #4666
2026-10-17 11:23:00 +08:00
morluto e567033a71 fix(process): preserve completed observations and cleanup authority
Retain physical stream errors and late caller cancellation independently. Expose an exact failed Darwin preparation root for cleanup retry without closing the shared reader.

Validation: 24 owned-command tests, 6 cancellation controls, 2 targeted Darwin cleanup retry controls, and check:fast. Controlled macOS processes only; no new real JVM or Ghidra verification.

Refs #4561, #4638
2026-10-17 11:10:02 +08:00
morluto 150e996a86 refactor(providers)!: await owned readiness observations
Carry asynchronous readiness through provider selection, session status, CLI and MCP. Join observations during cancellation and shutdown, keep status readable during finalization, and retain mixed provider errors and exact cleanup dependencies through the shared inspection failure owner.

Validation: 255 focused tests across 18 files; 21 registry tests rerun with primary restoration assertion; check:fast; docs:check and 5 README conformance tests. Registered MCP and SDK schema checks use controlled providers; no real Ghidra/JVM verification.

This is a prerequisite for #4561. The synchronous Ghidra Java producer and its doctor/client ownership migration remain unfinished.

BREAKING CHANGE: binary_session candidate availability can be unknown during owner finalization. Consumers must handle this state; the internal session status API is now asynchronous.

Refs: #4561
2026-10-17 09:57:43 +08:00
morluto 4087048270 fix(process): retain owned command cancellation provenance
Preserve the owner-observed opaque caller reason through preparation, collection and cleanup failure wrapping without assigning cleanup authority or serializing the raw reason.

Validation: 68 focused process/deadline tests; check:fast; docs:check (5 conformance tests). Controlled local process fixtures only; no native provider verification.

Refs: #4638
2026-10-17 09:41:13 +08:00
morluto 71929ed37c fix(cutter): normalize command checkpoint cancellation
Produce typed cancellation at each command checkpoint while retaining the exact opaque reason as cause. Preserve independent typed physical failures without downstream equality guesses.

Validated 72 focused tests across nine files, static checks, docs and five conformance controls, including real client/service admission races and matching physical-error controls. No native Cutter engine or new public-transport reproduction claimed.

Refs: #4635
2026-10-17 09:24:43 +08:00
morluto 199396f61c fix(cutter): retain command attribution through descriptor cleanup
Pass the requesting operation into the descriptor failure owner. Discovery retains its own identity; command execution and completion confirmation retain cutter_command with exact cleanup dependencies.

Validated 61 focused tests across five files, including denied retry and successful native descriptor release. Controlled close faults; no native Cutter engine verified.

Refs: #4634
2026-10-17 09:22:54 +08:00
morluto 5c0472c571 fix(cutter): attribute status proof failures to command
Parse command status and validate session identity at one bridge boundary. Keep the preflight watermark requirement separate from exact completion receipt proof, and retain status as a compact auxiliary diagnostic.

Reuse authenticated protocol and descriptor fixtures. Validate 66 focused tests across eight files, static checks, docs and five conformance controls. CLI refusal controls use an authenticated loopback fixture; native Cutter was not verified.

Refs: #4607
2026-10-17 09:19:02 +08:00
morluto 8f65d7f256 fix(ida): attribute startup failures and retain cleanup ownership
Attribute shared startup failures separately for each accepted operation. Preserve the original stage as a compact diagnostic, with typed timeout budgets, limits, output, recovery and exact cleanup dependencies.

Link cancellation wrappers to the originating live cleanup owner, including release before and after wrapping. Remove the redundant typed-error branch.

Validated 130 focused tests, static checks, docs and five conformance controls; four source controls rerun after fixture type correction. Public CLI uses a loopback supervisor; no native IDA engine verified.

Refs: #4614, #4633
2026-10-17 09:12:57 +08:00
morluto 3e8236639a fix(errors): restore typed primary after confirmed cleanup
Retain the primary error through cleanup composition and resolve its presentation only after live ownership confirms release. Preserve historical diagnostics separately from producer facts.

Validate domain primary types, nested owners, denied retry and late observations; exercise public CLI and MCP projection plus firmware descriptor retry. Focused tests, source controls, static checks and docs pass. Native providers were not verified.

Refs: #4628
2026-10-17 09:04:09 +08:00
morluto 0a51b246c6 fix(firmware): preserve snapshot input failures and descriptor ownership
Reuse canonical artifact chunk reads across hash, snapshot, range and publication, leaving cleanup to the exact owner. Let the reader own cancellation, preserve same-read physical failures, and classify growth past the input allowance through the existing typed admission failure.

Validation: 46 focused tests across four files, docs checks and five conformance tests. Native file IO with controlled faults; no Linux firmware engines verified. Shared post-cleanup error projection remains tracked in #4628. Refs #4619, #4475
2026-10-17 08:53:56 +08:00
morluto 8451d28392 fix(artifacts): preserve input size refusals and recovery
Classify the WABT reader file-size boundary as a deterministic resource constraint with its input allowance. Give Go input-size refusals selected-input recovery rather than evidence-write advice. Preserve configuration and cleanup semantics.

Validation: 66 focused tests across four files including public CLI refusals, plus docs checks and five conformance tests. Sparse admission fixtures and version-only WABT stubs; no real WABT execution. Refs #4621, #4622
2026-10-17 08:45:15 +08:00
morluto 3d028dbc74 fix(adb): distinguish absent focus from missing declarations
Parse window focus into declaration records, normalize explicit null references once, and preserve original named lines in Evidence. Keep fallback based on declaration presence and retain unknown focus when both sections omit fields.

Validation: 86 focused tests including public CLI/MCP with advertised schemas, plus docs checks and five conformance tests. Fake ADB only; no physical device verified. Refs #4608
2026-10-17 08:38:33 +08:00
morluto bfb7ed0bd4 fix(javascript): retain assignment and nested-default destructuring
Share source identity and one iterative pattern walker across declarations and assignments. Traverse nested default targets without losing dependency paths, preserving existing scalar-default occurrence identities and conservative unresolved slots.

Validation: 405 focused tests across five files, including public CLI dependency Evidence. Refs #4609, #4604
2026-10-17 08:32:26 +08:00
morluto c3e42385e7 fix(javascript): derive loop cardinality from exact literal values
Use the canonical cooked literal value for for-in and for-of string cardinality, preserving possible assignments and prototype uncertainty across quoted and template spellings.

Validation: 473 focused tests across six files, including public CLI assignment Evidence and exported return shapes. Refs #4617
2026-10-17 08:27:39 +08:00
morluto 746a044e42 fix(filesystem): preserve opaque caller reasons through cleanup
Do not interpret exact caller cancellation reasons as descriptor ownership records. Preserve checkpoint-shaped reasons through successful close retry and supply cancellation context in direct signal-aware cleanup consumers.

Validation: 156 focused tests across 14 files passed; no native provider execution. Refs #4610
2026-10-17 08:25:01 +08:00
morluto 3bd9aa3484 test(filesystem): assert admission cancellation provenance
Keep the explicit checkpoint wrapper and verify its exact caller reason alongside the retained cleanup owner.

Refs #4610
2026-10-17 08:23:36 +08:00
morluto 0c7b6d834a fix(javascript): bound local getter-result expansion
Charge inner alias and branch work to the existing getter query budget before allocating queued states. Widen local suffixes during fallback so a single getter cannot reintroduce path permutations.

Validation: two new class/object controls fail on the baseline; 411 tests across eight affected files pass. Public CLI baseline/candidate controls preserve unrelated observations and expose intended partial resource coverage. Typecheck/lint, docs and formatting pass. Diagnostic costs retained; no speed or token reduction claim. This fixes a separate getter gap and does not resolve the getter-free Qodo input. Refs #3839.
2026-10-17 06:58:19 +08:00
d5be402fb3 fix(objc): isolate empty protocol selectors and category class names (#4549)
* fix(objc): isolate empty Objective-C protocol method selectors

A protocol method_list_t entry whose selector reached an empty string was
appended to objc_protocols and objc_protocol_records unchecked. The final
metadata schema then rejected the whole result, discarding every valid
class, protocol and category.

The protocol method decoder now skips only that entry and reports it as
a protocol failure. The protocol keeps its other method declarations, and
its record is marked partial with protocol_method_selector_empty.

Fixes #4540

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(objc): treat empty local category class names as unresolved

A category whose class reference pointed at a mapped local class with an
empty class_ro_t name took that empty string as class_name. The final
metadata schema then rejected the whole result, discarding every valid
class, protocol and category.

An empty local class name now fails the class reference the same way an
unmapped one does: the category is kept with class_name null,
class_source unresolved and a category_class_unresolved issue, and its
name, methods and protocols survive.

Fixes #4541

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-17 06:43:12 +08:00
VADLACHARYandmorluto 8267edc0ef fix(ghidra): resolve keg-only Homebrew openjdk on macOS (#4556) (#4557)
* fix(ghidra): resolve keg-only Homebrew openjdk on macOS (#4556)

Fixes an issue where the `ghidra-java` check fails when OpenJDK is installed via Homebrew on macOS (where it is keg-only and not added to `PATH` or resolved by `java_home`).

- **Problem:** When `options.javaHome` is undefined, the provider defaulted to a bare `"java"` command, causing valid Homebrew OpenJDK installations to be reported as missing dependencies.
- **Solution:** Replaced the single fallback with an absolute candidate list (`javaCandidates`) that probes standard Homebrew OpenJDK paths (`/opt/homebrew/opt/openjdk/...` and `/usr/local/opt/openjdk/...`) before falling back to `"java"`. All candidates are absolute, preserving existing constraints and current behavior.

* fix(ghidra): fix javaHome type narrowing in java candidate list (#4556)

Fixes a TypeScript compilation error (`TS2322` and `TS2345`) in `src/ghidra/GhidraInstallation.ts` where `options.javaHome` was inferred as `string | undefined` inside `path.join()`.

- **Problem:** The ternary operator structure prevented TypeScript from narrowing `options.javaHome` to `string` in the `path.join()` call when `options.javaHome` was defined, causing CI compile checks to fail.
- **Solution:** Refactored the `javaCandidates` assignment using an immediately invoked function expression (IIFE) with an explicit `if (options.javaHome === undefined)` check. This properly narrows `options.javaHome` to `string` in the non-undefined branch while keeping the same logic and Homebrew OpenJDK paths intact.

* fix(ghidra): order homebrew java candidates by host architecture (#4556)

Updates the Homebrew OpenJDK candidate ordering in `src/ghidra/GhidraInstallation.ts` to be architecture-aware, addressing review feedback regarding cross-architecture runs (e.g., x64 under Rosetta on Apple Silicon).

- **Problem:** Previously, `/opt/homebrew` was always checked before `/usr/local`. When running REA as an x64 process under Rosetta on Apple Silicon, this could incorrectly select an ARM64 JDK over an Intel/x64 one, triggering a `nativeDecompilerCheck` failure and missing the valid fallback.
- **Solution:** Dynamically orders `javaCandidates` based on the runtime `architecture`. On `arm64`, `/opt/homebrew` is prioritized; on `x64`, `/usr/local` is prioritized. Bare `"java"` remains first in both cases to preserve standard PATH resolution.

---------

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-17 06:41:38 +08:00
360626434d fix(electron): delegate once dispatch to the active runtime (#4605)
* fix(electron): delegate once dispatch to the native runtime

Preserve the selected runtime callback dispatch and metadata semantics, recording actual entry or explicit observation gaps. Add paired cross-runtime controls and verify the real application hook source identity.

Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>

* refactor(electron): remove superseded once dispatch branches

The native once runtime owns callable-method selection. Both remaining observation-helper callers pass actual invocation frames, so remove the obsolete fifth argument and its unreachable call/apply dispatch paths.

Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>

* fix(electron): retain once gaps when registration bypasses on

Keep the unobserved synchronous registration scope with its owning channel and restore the previous scope after errors or reentry. Report the once limitation even when an application getter selects a native alias instead of patched on, while avoiding duplicate same-channel gaps.

Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>

* docs(electron): preserve native once verification cohorts

Record paired runtime controls, actual Joplin verification, and the unresolved startup, cleanup, and observation limits.

Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>

* fix(electron): retain once observation gaps on failed operations

Finalize unobserved registration scopes for both success and failure, preserving native errors and recording failed non-callable dispatch metadata.

Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>

* docs(electron): record failed once operation regressions

Preserve review findings, red/green controls, final runtime comparisons and real application limits without relabeling earlier cohorts.

Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>

---------

Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-17 06:38:13 +08:00
morluto d74e72406a fix(jeb): attribute transport timeouts to requested operations
Project canonical timeout and cancellation facts at the provider boundary while preserving causes, partial observations, live cleanup dependencies and provider-authored deadline guidance.

Validation: four real SDK/HTTP catalog and tool timeout controls fail before the fix. After rebasing onto the Ghidra timeout guidance change, 189 tests across ten files pass, including public CLI/MCP workflows and affected Ghidra tests. Typecheck/lint, docs, format and discovery pass. No native JEB engine or target verification. Refs #4597.
2026-10-17 06:31:16 +08:00
morluto a6e8f5269f fix(jeb): retain cleanup ownership and acquisition outcomes
Bind pending SDK resources to exact cleanup leases, coalesce disposal, and retry only unresolved children. Shared startup callers retain final cleanup while independent peers can cancel their waits.

Attribute SDK aborts by exact owned rejection identity, preserve physical failures under late cancellation, and retain completed Evidence at the application boundary.

Validation: 83 focused tests across eight files; typecheck/lint, docs check, formatting and discovery. Controlled real HTTP/SDK faults only; no native JEB engine or target verification. Refs #4603.
2026-10-17 06:30:02 +08:00
Syed Rishad 1b56237cae fix(ghidra): name the startup deadline setting when startup times out (#4590)
Fixes #4241
2026-10-17 06:10:15 +08:00
morluto 67384b23e5 fix(jeb): cache parsed metadata independently of optional version
Own the parsed engine facts separately from identity projections. Reuse successful unknown-version metadata, retain explicit refresh, and bind each execution identity to its own selected metadata under concurrent inspection.

Refs #4600 and #4597. Six affected files pass 58 tests; static, documentation and discovery checks passed. Real JEB and reconnect behavior remain unverified; prepared reconnect PR3754 stays separate pending cleanup ownership work.
2026-10-17 06:03:51 +08:00
morluto ece277b30b fix(jeb): retain requested operation during metadata probes
Pass the caller operation into the shared client-information producer and consume its parsed facts during inspection. Remove duplicate metadata parsing and the forwarding wrapper.

Refs #4597. Baseline real CLI/MCP controls failed for three requested operations; all 51 affected tests and static checks now pass. The endpoint is simulated; real JEB refusal behavior is unverified.
2026-10-17 05:54:18 +08:00
morluto cd9252bc63 test(filesystem): exercise digest cancellation and admission ownership
Cover ADB pull, package and bugreport physical failures and completed cancellation across successful and failed closes. Verify artifact retention callback timing and Pcap failed-admission owner recovery.

Refs #4598. Controlled real host files and simulated ADB output; native close faults and Android device behavior remain unverified.
2026-10-17 05:47:35 +08:00
morluto e65c3955f9 fix(filesystem): preserve digest failures through cleanup cancellation
Record cancellation at file checkpoints and before owner handoff. Keep physical hash failures, exact reasons, completed facts and failed-close owners distinct through artifact and provider projections and cleanup retries.

Refs #4598. Validated with 566 passing affected tests and 22 platform skips; final Kuna cleanup reason rerun passed 43 tests. No Android device or real JADX/Kuna execution claimed.
2026-10-17 05:47:19 +08:00
morluto 0d22474280 fix(adb): preserve version probe failures during admission
Classify producer-recorded timeout and capture overflow before configuration fallback, and pass typed failures through provider admission. Preserve exact version-probe cancellation reasons and parsed output facts. Clarify command lifecycle diagnostics.

Related: #3886, #4594. Controlled Node subprocess, provider/application and actual CLI regressions cover late abort ordering, active and preadmission cancellation, validated partial Evidence, physical failures, capture overflow and the unchanged ten-second deadline. The affected suite passed 218 tests across 14 files; static/docs/format/discovery checks passed. Native adb/device and Windows execution remain unverified.

Remaining related work: shared file-close cancellation provenance for digest consumers requires a separate producer and consumer migration.
2026-10-17 05:17:16 +08:00
morluto c4475d4a67 fix(adb): preserve command owner cancellation outcomes
Use the subprocess owner cancellation marker instead of the later signal state or an arbitrary ABORT_ERR code. Preserve the exact caller reason and captured output, and share parsed output projection with existence probes.

Related: #4594. Controlled process and source regressions fail on 9a95db681; the affected 14-file suite passes 218 tests. Actual Node subprocesses and simulated adb clients were exercised on macOS; no Android device or native Windows run.
2026-10-17 05:17:16 +08:00
709b5cbdc1 fix(electron): block synchronous shell effects during capture
Block showItemInFolder and writeShortcutLink using the shared active shell boundary, preserving undefined/false synchronous refusal contracts and shape-only timeline evidence.

Retain Nitin13-git implementation and add base-red/shared-contract coverage plus a reproducible unchanged Joplin welcome-attachment reveal. Local87cases, static and docs pass. The explicit test argv shim verifies the operation phase; standard startup and provider cleanup remain unsuccessful and are recorded without a full-workflow performance claim.

Fixes #4393

Co-authored-by: Nitin13-git <internshipsvc01@gmail.com>
Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-16 21:03:12 +00:00
morluto 9a95db6819 test(uninstall): verify current recovery snapshots through the CLI
Plan and approve Cursor setup in an isolated home, edit an unrelated server, then uninstall and verify both the historical backup and exact pre-uninstall recovery bytes. Related: #4581, PR #4580.

The new real CLI control fails on f9ccacca2 with a missing recovery file and passes with the recovery owner fix. Verified on macOS; native Windows/Linux runs remain unverified.
2026-10-17 05:01:57 +08:00
morluto eba64e9386 fix(setup): retain skill recovery backup cleanup ownership
Route skill backup selection and verification through the shared recovery owner with the installer resource scope. Reuse verified snapshots without copying and remove duplicate selection and EEXIST handling.

Cover failed closes at first backup, recovery backup, and post-copy verification; block new descriptors while cleanup is pending and retain the exact owner through disposal retries. Related: #4592.

Validation: 158 focused tests and 511 adjacent setup tests passed; check:fast, docs:check and formatting passed. Close failures were injected on real file handles on macOS; native Windows/Linux faults were not reproduced.
2026-10-17 05:01:46 +08:00
Adolanium 967a8bc279 test(uninstall): cover backups that fail verification after copying
Add cases where the backup copy reports success but leaves the recovery
file missing or holding other bytes; uninstall must fail before writing.
Narrow the installation guide so it promises a backup path only for the
failures that report one.

(cherry picked from commit 70bff72403)
2026-10-17 04:57:06 +08:00
Adolanium c15c2062ad fix(uninstall): preserve the configuration as it was before uninstall edits it
(cherry picked from commit 2dfc2d0f7c)
2026-10-17 04:57:06 +08:00
morluto f9ccacca2c fix(ghidra): own configured profile artifact lifetimes
Keep bounded stable reads, format interpretation, snapshot writes and final descriptor cleanup under exact owners. Join complete provider profile work during shutdown and retain primary failures through late cancellation and cleanup retry.

Related: #4584, #4586
2026-10-17 04:50:13 +08:00
morluto 61e688070f fix(adb): refuse transfers when destination existence is unknown
Own ADB command failure facts once and interpret only the clean empty test false result as absence. Preserve unknown probe output and reuse parsed facts for package refusals.

Related: #4574
2026-10-17 04:30:50 +08:00
morluto f26973a529 fix(providers): own profile selection through finalization
Join pending profile producers on cancellation and shutdown. Preserve failed results before later signals, retain every automatic-selection cleanup owner, and move profile file ownership into the registry.

Related: #4569
2026-10-17 04:21:21 +08:00
morluto bf7fad390c fix(cleanup): project resources from unresolved owners
Retain historical cleanup facts while deriving live resource attribution from exact error dependencies and labelled leases. Unlabelled owners preserve uncertainty.

Related: #4578
2026-10-17 04:21:21 +08:00
morluto 50dbd875cf fix(process): preserve owning operation cancellation 2026-10-17 04:21:21 +08:00
bf3475867f fix(electron): preserve native IPC listener behavior (#4389)
Preserve original-function removal, native IPC dispatch and argument identity, with explicit metadata ownership and Evidence verification. Runtime cleanup limitations remain documented.

Fixes #4390.

Co-authored-by: Adolanium <94890352+Adolanium@users.noreply.github.com>
Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-17 04:17:11 +08:00
morluto f22e8e7268 test(ghidra): align Java probe fixtures with raw output
Model spawnSync results as typed Buffer output, retain Unicode path assertions, and verify ENOBUFS remains runtime_unavailable with captured diagnostics.

Fixes four stale fixture failures observed in Linux portable and Windows curated CI at ae07d1517. Validation: 131 tests passed across five Java admission files; check:fast passed. Native Windows rerun remains CI verification.
2026-10-17 03:52:49 +08:00
morluto b0ddb58e27 fix(ghidra): own PSP inspection finalization
Retain exact file and directory cleanup owners through profile admission and launch reinspection. Preserve typed primary failures and exact cancellation; keep fingerprint framing and bounds stable.

Validation: 137 tests passed, 3 platform skips across six filesystem/provider/MCP files; check:fast passed. Controlled real-handle fixtures do not establish actual Allegrex execution or native Windows faults.

Refs: #4521, #4517. Shared profile evaluation completion remains tracked in #4569.
2026-10-17 03:52:33 +08:00
morluto ae07d15170 fix(ghidra): explain keg-only JDK admission remediation
Explain how to select an existing Homebrew JDK with JAVA_HOME when macOS
Java discovery cannot find a keg-only installation. Preserve caller-selected
runtime inputs and the Ghidra installation's declared Java bounds. Document
the installation paths and retain a regression showing only the requested
Java command is probed; no automatic JDK discovery or installation added.

Validation: 4 focused files, 122 passed; npm run check and docs:check passed.
Public CLI prerequisite fixtures ran on macOS; Homebrew keg-only guidance
is source/fixture verified against the primary formula documentation, not
a fresh Homebrew installation or a Windows runtime test.

Refs #4556
2026-10-17 03:34:11 +08:00
morluto c75cf827e7 fix(ghidra): retain typed Java prerequisite outcomes
Replace ambiguous absent observations with an owning Result producer for
Java prerequisite probes. Preserve missing-command, runtime-failure and
unresolved-property outcomes, process error codes, both bounded streams,
raw byte counts and partial properties through provider and doctor adapters.
Reject malformed numeric-prefix versions while retaining their source facts.
Migrate all injected hosts to the named probe contract and remove the old
parser/observation ownership from installation discovery.

Validation: 17 focused files, 350 passed; npm run check passed. Public doctor
fixtures distinguish absent/nonzero/malformed commands. Real installed JDK
and Ghidra readiness facts matched across 20 baseline/candidate CLI tasks.
Warm repeat raw replies6392tokens each, elapsed .448/.444s, CPU .662/.660s;
sampled family memory varied and reversed direction on repeat. No causal
performance improvement claim, true-cold or full-agent/fault-path measurement.

Synchronous probe deadline/cancellation/family cleanup remains #4561 and needs
coordinated discovery/admission supervision; no claim that it is fixed here.
No real failing JVM or Windows runtime verification performed.

Refs #4560, #4561
2026-10-17 03:33:16 +08:00
morluto 8116dc0651 fix(native)!: preserve unknown provider symbol addresses
Normalize empty addresses once at the symbol-inventory interpretation
boundary. Keep Swift and Objective-C records with null locations, preserve
the original empty value in evidence.reported_address, and retain partial
coverage instead of failing the entire metadata result. Nonempty provider
address syntax remains unchanged.

Validation: domain and production-registration MCP regressions failed before
the fix. Ten focused files passed 181 tests with 2 skipped, including SDK
converted JSON Schema/catalog validation and graph consumers. npm run check
and docs:check passed (5 documentation tests). Fixture provider only; no
native-provider malformed-symbol or Windows runtime reproduction claimed.

BREAKING CHANGE: Swift symbol address can now be null when the provider has
no location. Clients consuming swift_symbols must handle an unknown address.
The original provider value is retained in optional evidence.reported_address.

Refs #4542
2026-10-17 03:14:30 +08:00
morluto 060587b456 fix(cli): prefer native byte inspection for automatic dispatch metadata
Allow the composed one-shot dispatch workflow to open a covered Mach-O
without selecting a deep provider. The registry still owns selector
precedence; explicit request/environment choices remain binding, while
non-Mach-O and unsupported hosts retain deep symbol fallback. Requested
snapshots keep ordinary binding and profile semantics.

Share runtime composition and target admission with the native byte reader.
Keep target parsing, resource ownership and Evidence on the original session.

Validation: 13 focused files, 192 passed and 2 skipped; npm run check passed.
The six previously blocked native property CLI controls now pass. Ten measured
CLI workflows preserved exact metadata facts; warm raw reply token medians
1344/927, elapsed .475/.547s, CPU .691/.745s, sampled family RSS351.2/359.3MiB
baseline/candidate. Baseline used explicit Ghidra workaround; true cold and
full-agent costs unmeasured. No speed/memory improvement or Windows claim.

Refs #4555
2026-10-17 03:11:32 +08:00
morluto a70c42e03d fix(native): recover malformed Objective-C names at decode boundaries
Validate protocol and implementation selectors, ivar names and category
names before publishing records. Share validated local class identity
between class, superclass and category consumers, retaining unresolved
addresses and the decoder's failure reason.

Byte fixtures reproduced the reported validation failures. Final focused
4-file run: 118 passed; npm run check passed. Six existing CLI property
controls remain blocked before decoding by deep-provider auto-selection
ambiguity, as exposed by improved assertion diagnostics (#4555).
No Windows runtime or real malformed-application verification performed.

Refs #4540, #4541, #4533, #4534, #4115, #4555
2026-10-17 03:02:26 +08:00
morluto f31b231a92 fix(cli): isolate static native inspection from deep provider selection
Compose native utility sessions for the five existing static CLI questions.
Keep ordinary session routing for native runtime observations, and retain
caller-selected environments, target admission, Evidence and cleanup.

Validation: 17 focused files, 97 passed and 2 skipped; npm run check passed.
The three thin Mach-O CLI regressions now pass with macOS native utilities.
No Windows runtime verification or performance measurements performed.

Refs #4555
2026-10-17 02:56:35 +08:00
morluto 5eaef92e5b test(firmware): assert cleanup provenance through composed failures
Refs #4475.

Check public cleanup projection and retained primary diagnostics instead of a
superseded error subclass and flat diagnostic location. Require the caller's
retained error to resolve after the exact cleanup owner succeeds on retry.

Validation: 59 tests passed in the cached Linux audit container with Node
24.18.0 and npm 11.16.0, including both failing CI regressions. Producers are
controlled Node fixtures, not real unblob/binwalk engines. The temporary
container was removed through its recorded owner. Static checks passed.
2026-10-17 02:47:42 +08:00
morluto 0f61261e01 fix(native): retain owned preparation failures through command adapters
Refs #4522, #4526, #4555.

Retain producer-owned compiler output and cleanup markers through xcrun
resolution and native failure projection. Keep physical preparation failures
primary and preserve separate command deadline classification for interrupted
preparation. Omitted output facts stay unknown; client close does not resolve
foreign helper cleanup. Include CLI failure output in native regression reports.

Validation: 87 focused tests passed. Broader 140-file run passed 1483 tests,
skipped 109, and exposed three pre-existing deep-provider selection failures
tracked in #4555. Static checks passed. Compiler faults use controlled Node
fixtures; native Swift compiler faults and Windows execution were not verified.
Other command consumers still need preparation-fact propagation.
2026-10-17 02:47:42 +08:00
SAKURA 4838f44493 docs(i18n): explain translation contribution entry points
Document the translated README and website PO contribution entry points. Resolve README filenames through the existing locale inventory, preserving canonical Korean README links.

Validated on Linux with docs:check, five README consistency tests, all 20 inventory README paths, and current-head CI/automated reviews.
2026-10-17 01:49:55 +08:00
星闌 60bf102459 docs(browser): correct cache fingerprint completeness limits
Document complete decoded cache-body fingerprints, the existing CDP message bound, and incomplete coverage for missing/undecodable bodies.

Validated with docs:check, eight storage/cache tests, and current-head automated review/CI.
2026-10-17 01:27:45 +08:00
559 changed files with 55576 additions and 6146 deletions

No files matched your search

@@ -0,0 +1,113 @@
---
name: rea-analysis-optimization
description: Optimize REA providers and shared analysis workflows for agent context, latency, CPU, and memory while preserving correctness and Evidence. Use for analysis bottlenecks and cache or query design; CI/test-suite optimization belongs to rea-ci-tests-audit.
---
# REA analysis optimization
Find a concrete source of unnecessary work, change it at its owner, and verify
equivalent completed investigations. A useful outcome can also be a measured
decision to leave an implementation alone.
Follow the checkout's [performance priorities](../../../AGENTS.md#performance),
[tool design](../../../docs/tool-design.md), and
[testing guide](../../../docs/testing.md). This skill supplies an optimization
workflow, not additional permission to execute targets, publish, or merge.
Use the current contracts, provider prerequisites, and user authorization.
Read [the Kuna case study](references/kuna-case-study.md) when selecting a
reuse strategy or interpreting a benchmark. Its pinned observations illustrate
tradeoffs; they are not current-provider guarantees or universal speedups.
## Pin the question and the correctness oracle
Describe the analyst's completed task, including the facts and relationships
needed to answer it, requested detail, source access, and meaningful unknowns.
Choose a representative real artifact and a small or empty case. Record the
baseline revision, target digest, engine identity, options, host, and entrypoint.
Use a separate checkout for changes without disturbing other work.
Establish an oracle independent of the changed implementation: source-built
fixture expectations, independently mapped file bytes, or a native producer
paired with checks of its interpretation. Comparing two wrappers around the
same faulty decoder does not establish correctness. Preserve address widths,
same-address alternatives, unresolved endpoints, coverage, and source linkage.
An empty result proves absence only within the actually examined scope.
## Locate the earliest unnecessary work
Trace one public CLI or MCP task through shared application code to its producer.
Measure relevant phases: admission and identity checks, native analysis,
decoding, lookup, projection, serialization, Evidence retention, and cleanup.
Include catalog discovery and extra calls when they affect the completed task.
Use the evidence to choose the smallest structural correction:
| Observed cost | Candidate correction | Condition to establish |
| --------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| Repeated scans of validated facts | Reuse existing order or lookup structure | The owner establishes ordering and identity; every matching alternative survives |
| Repeated equivalent engine work | Share a validated result or in-flight producer | Inputs, freshness, lifetime, and per-caller outcomes remain explicit |
| Full reports for selected facts | Push selection or projection to the earliest capable owner | Required facts and evidence remain reachable; examined coverage is reported |
| Large temporary transport buffers | Decode incrementally or use bounded source reads | Validate the whole admitted document; retained facts and copies still consume memory |
| Many repeated homogeneous reads | Share admission, decoding, or batch work | Preserve item identity, order where required, and individual failures |
Do not introduce an index, generic cache, batch endpoint, or orchestration layer
without identifying the work it removes and its maintenance/resource cost.
Projection after materializing a full report may reduce output while leaving
analysis time and peak memory unchanged. Fix missing or incorrect facts before
benchmarking an alternative composition against a better-covered workflow.
## Keep one interpretation and a defined reuse lifetime
Let the producer boundary validate and normalize facts once. Consumers query
that representation through the shared CLI/MCP implementation; adapters should
not rebuild relationships or independently reinterpret raw reports. Keep lookup
identity separate from display formatting. Reuse contracts when their intent
matches, honoring requested detail and complete Evidence access.
For every cache or shared producer, state its key, owner, invalidation, retained
size, and lifetime. Account for result-affecting target bytes, engine contents,
analysis options, architecture/load mapping, and parser/schema versions where
they affect reuse. Validate live state before calling a cached observation
current. Cache a refusal only when it is deterministic for that validated key;
malformed, partial, transient, or cancelled attempts need explicit retry policy.
One cancelled waiter must not invalidate a successful peer. Preserve producer
failure context and each caller's cancellation reason. Retain ownership and
capacity charges until shared work and cleanup settle; preserve completed
observations when cleanup fails. Exercise invalidation, concurrent consumers,
last-waiter cancellation, close, and cleanup retry when those paths change.
Bound the process family and retained state, not just a parent heap or worker
count. Account for decoding and serialization expansion before allocation.
Choose CPU, memory, concurrency, and disk limits from the host and workload;
stop before OOM and verify owned cleanup. A guard-stopped task is incomplete.
Do not replace a transport buffer cap with an unbounded retained inventory or
hide partial results behind a success status.
## Compare completed tasks and limit the claim
Run the affected real public workflow before and after with equivalent inputs,
results, coverage, and Evidence semantics. Include small inputs so setup costs
and regressions are visible. Distinguish fresh sessions, warm runtime caches,
and OS file caches. Alternate or repeat runs when variance affects the decision.
Choose meaningful boundary regressions for cases the actual engine cannot
reliably reproduce; a fake engine does not qualify a real provider.
Report task outcome, relevant phase timings, total elapsed and CPU time, peak
process-family memory and its accounting method, resource stops, and cleanup.
Check the payload exit status as well as the profiler's. Keep failed attempts
and identify unmeasured costs; a passing retry does not explain a failure.
For token claims, count schemas/discovery, arguments, replies, retries, and
continuations needed to finish the task. Label tokenizer estimates separately
from actual model usage and record the client/catalog behavior. Smaller JSON
alone establishes neither fewer model tokens nor better investigation quality.
Do not claim a complex-software advantage without completed representative
investigations and comparable correctness/coverage.
Finish with the bottleneck, owning change, preserved semantics, executed
checks, before/after measurements, and remaining limits. Keep any integration
record current with its tested revision and distinguish merged production work
from experiment-only code. Stop when the demonstrated bottleneck is resolved
or further benefit remains speculative.
@@ -0,0 +1,4 @@
interface:
display_name: "REA analysis optimization"
short_description: "Optimize analysis with preserved Evidence and measured costs"
default_prompt: "Use $rea-analysis-optimization to find and verify a concrete analysis bottleneck while preserving correctness and Evidence."
@@ -0,0 +1,112 @@
# Kuna: reuse before adding machinery
Read this reference for concrete examples of the skill's decisions. The records
below concern static Linux analysis with caller-supplied Kuna. They do not
establish parity with another provider or a general advantage for Code Mode,
atomic tools, or any particular orchestration design.
## Exact-address lookup: a measured, narrow improvement
[PR #4695](https://github.com/morluto/rea/pull/4695) reused the validated string
inventory's numeric address order. Previously, each `list_strings(address)`
filtered all cached facts. The owner now performs a binary lower-bound lookup
and returns every encoding at that address: O(log n + matches), without a
second retained lookup index.
The owning implementation is
[`KunaStringInventory.ts`](../../../../src/kuna/KunaStringInventory.ts), consumed
by [`KunaStringQueries.ts`](../../../../src/kuna/KunaStringQueries.ts). Inspect
their current versions before adapting the example. Numeric u64 addresses,
same-address encodings, duplicate rejection, source digests, caller selection,
and the existing session lifetime were part of the contract. Full listings and
literal/regex searches kept their semantics. An attempted removal of the
duplicate-detection set was dropped: no stable memory benefit was established,
and it delayed malformed-input rejection.
### Recorded workload and results
The benchmark used unmodified Kuna 1.833 and LLVM 16 on Linux x86-64, with
602,544 string facts. Baseline was `be0b2d138`; the final feature was
`5f564962da0e1b17f00a68a74a312539f0209a01`, subsequently integrated in
`c6b62754b095a134d6d67877508fa34311a2eda2`. These are historical observations,
not a benchmark of every subsequent main revision.
The same production stdio MCP task discovered the catalog, opened the binary,
made one cold selected read and 28 warm exact reads, checked retained Evidence,
and closed the session. Five selected addresses and two absent selectors were
read four times. A native filtered scan selected the strings; an independent
ELF load-header mapping checked the source bytes and terminators. Returned
facts, source linkage, target digests, and cleanup were checked.
Three alternating fresh sessions per implementation used one CPU, a 4 GiB
process-family hard limit, a 3 GiB early stop, and no swap. OS file caches were
warm. Whole-harness costs include the independent native oracle, hashing,
catalog/client work, and correctness checks.
| Median | Before | After |
| ---------------------------------------- | ---------: | ---------: |
| 28 warm exact MCP reads | 495.2 ms | 90.8 ms |
| Cold selected MCP read | 11.30 s | 11.01 s |
| Whole harness elapsed | 28.24 s | 27.43 s |
| Whole process-family CPU | 27.71 s | 26.89 s |
| Kernel family peak, including file cache | 2237.6 MiB | 2240.6 MiB |
Warm totals ranged from 491.7–544.3 ms before to 84.8–99.5 ms after: about
5.5 times faster at this phase. Cold/whole-task timing and memory overlapped;
no stable improvement in those costs was established. One small SQLite pair
(7,985 facts) measured 20 warm reads at 88.6 → 80.5 ms and the whole harness at
13.68 → 13.94 s. The large-inventory lookup benefit does not generalize to a
whole-task speedup on small inputs.
Catalog hashes, call counts, and normalized facts were unchanged. A
`cl100k_base` estimate over one pass of raw catalog JSON, arguments, and replies
was about 846,000 tokens in both lanes, with about 802,562 from the catalog.
This was transport serialization, not rendered model context or actual usage;
client discovery, prompts, replay, and protocol framing were unmeasured.
**No token reduction or improved agent investigation outcome was established.**
Pinned target SHA-256:
`f62d254b7f2bf42df8c8b07d46ee3bb4c2cafeca436b2e6bc6ccbe4581f58f40`.
Pinned engine SHA-256:
`d858918b0766435cd96b999a77f2d5a31a0cc4f07256421c883b5b9dc4dd58e4`.
All six normalized-result sequence digests were
`4d2f90269da8568984090b20274163c4a7c2a0538aab63530bd2fe44840a0bba`.
Selected byte agreement does not prove native discovery completeness; missing
strings and relationships can remain unknown.
## Other transferable changes and their limits
- [Cached string inventory and search, #4095](https://github.com/morluto/rea/pull/4095):
share validated facts across queries in a defined session. Audit key inputs
and lifetime before extending reuse across targets or engine revisions.
- [Shared single-function work, #4045](https://github.com/morluto/rea/pull/4045)
and [repeat-request reuse, #4169](https://github.com/morluto/rea/pull/4169):
avoid duplicate native work while retaining per-caller cancellation and
cleanup ownership. A query count alone cannot reveal duplicate subprocesses.
- [Streamed strings, #4318](https://github.com/morluto/rea/pull/4318)
and [streamed call graph, #4329](https://github.com/morluto/rea/pull/4329):
remove fixed aggregate transport-buffer failures through incremental parsing.
Streaming transport does not bound every retained object or final reply.
These links establish changes; no numerical speedup is asserted here.
Typed reference facts, native string-query projection, and shared native byte
reads remained experimental at the 2026-10-17 checkpoint
`65f2f9a22bc892f61d2b00f9a4611124f57d4bff` on
`experiment/atomic-codemode-20261016`. Some experiments used a patched native
engine. Check current integration and bring-your-own engine capabilities before
proposing them; a working experiment is not a stock-provider guarantee.
## Applying the reasoning elsewhere
For another provider or format, inspect its actual representation and native
capabilities before borrowing the implementation. Existing numeric ordering
may support range reads; an on-disk native query may avoid materializing an
inventory; an immutable parse may support several views. Measure the relevant
alternative rather than requiring every provider to use Kuna's cached array.
For live observations, immutable artifact caching rules are insufficient:
validate the process/session identity and freshness required by the caller.
For compact replies, preserve access to source Evidence and report examined
coverage; projection must not silently turn an unresolved edge into no edge.
For batching, test mixed successes and failures and measure the entire task,
including discovery, extra calls, native work, and serialization.
@@ -124,6 +124,26 @@ operation's full ordered list. Candidate capabilities describe possible
providers. Top-level session capabilities describe the current route, and
`analysis_provider_binding` identifies the selected provider.
## Static memory and original file bytes
For Ghidra, `read_bytes` reads initialized analysis memory, including loader
fixups; it does not read a running process. Check `returned_bytes` and `complete`
before decoding a table. An incomplete read is missing coverage, not a zero-filled
tail. Use `list_segments` to inspect the selected block's initialization and address-space
facts, and retain unavailable facts as unknown.
Use `address_to_file_offset` to anchor one observed address to original file
bytes. A mapping for the first address does not establish a contiguous mapping
for an entire table, and loader fixups can make analysis bytes differ from source
bytes. Keep source ranges and analysis-memory observations separate.
A pointer table can refer to writable slots populated by a constructor rather
than directly to immutable resources. An uninitialized block or an observed zero
does not establish the slot's later runtime value. Follow `xrefs` and the relevant
writer's assembly or pseudocode; distinguish recorded assignments from inferred
execution order. Do not replace unreadable bytes with zeros or conclude that a
resource is absent without the necessary initialization evidence.
## DOS MZ with Ghidra
For an admitted DOS MZ target, use `inspect_native_load_image` to check measured
@@ -133,10 +153,8 @@ reported static image, not runtime DOS or PC-98 hardware behavior.
For Ghidra decompilation, `procedure_pseudo_code` also accepts `procedure: "file-offset:0x..."`; CLI `rea decompile <path> --file-offset=0x... --provider ghidra` resolves the source byte offset internally. An unresolved 0x-prefixed numeric procedure selector also falls back to a unique file mapping. Existing symbols and analysis addresses win; multiple mappings fail as ambiguous. Preserve the returned file-offset and address locations and raw selection observations. ELF virtual addresses are distinct from file offsets; never assume an arbitrary offset plus image base is an analysis address.
Use `read_bytes` for initialized analysis memory and `address_to_file_offset` to
anchor one observed address to original file bytes. Loader fixups can change a
word; a source offset does not imply byte equality. Complete function body ranges
use inclusive ends and can contain gaps. Do not treat the enclosing span as code.
Complete function body ranges use inclusive ends and can contain gaps. Do not
treat the enclosing span as code.
For packed targets, retain the original and separately derived artifact identities;
decompiling an unpacking stub does not recover the unpacked program.
@@ -24,19 +24,7 @@ runs:
BUILD_CACHE_PREFIX: rea-portable-build-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.npmrc', '.nvmrc', 'package-lock.json') }}
run: |
npm run --silent test:prepare -- --dry=json |
node --input-type=module -e '
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
const { tasks } = JSON.parse(readFileSync(0, "utf8"));
const hashes = tasks.map(({ taskId, hash }) => {
if (typeof hash !== "string")
throw new Error(`Build task ${taskId} must have an eager Turbo hash`);
return `${taskId}:${hash}`;
}).sort();
const hash = createHash("sha256").update(hashes.join("\n")).digest("hex");
console.log(`key=${process.env.BUILD_CACHE_PREFIX}-${hash}`);
console.log(`prefix=${process.env.BUILD_CACHE_PREFIX}-`);
' >> "$GITHUB_OUTPUT"
node scripts/ci/portable-build-cache.mjs key >> "$GITHUB_OUTPUT"
- name: Restore portable build outputs
if: inputs.build_cache_artifact == '' && inputs.save != 'true'
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
+7 -1
View File
@@ -357,10 +357,15 @@ jobs:
# test:prepare caches only portable JS, skill, and catalog outputs.
# Native package jobs still run their build and prepack checks; Turbo
# restores matching outputs or rebuilds when their inputs differ.
- name: Stage current portable build task cache
shell: bash
run: |
npm run --silent test:prepare -- --dry=json |
node scripts/ci/portable-build-cache.mjs snapshot
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: rea-build-cache
path: .turbo/cache/
path: .cache/ci-build-cache/
include-hidden-files: true
if-no-files-found: error
retention-days: 7
@@ -748,6 +753,7 @@ jobs:
tests/boundary/filesystem/webScriptManifestPaths.test.ts
tests/boundary/process/ProcessEvidence.host.test.ts
tests/boundary/process/packageNpmInvocation.test.ts
tests/boundary/process/packedBridgeVerification.test.ts
tests/boundary/process/windowsOutputCapture.test.ts
tests/acceptance/webModuleCli.test.ts
tests/acceptance/webSourceLocationCli.test.ts
+20
View File
@@ -21,6 +21,9 @@ For analysis workflows and agent interfaces, follow the
[performance priorities](AGENTS.md#performance). Explain the selected output
scope, reuse/cache decisions, and relevant before/after measurements when these
costs change.
Use the [analysis optimization skill](.agents/skills/rea-analysis-optimization/SKILL.md)
to locate provider and shared-workflow bottlenecks, preserve Evidence, and
compare equivalent real tasks before claiming an improvement.
When adding or changing an MCP tool, follow the [tool design guide](docs/tool-design.md) and preserve the canonical contracts and generated catalog.
@@ -258,6 +261,23 @@ The separate GitHub Pages host uses only the manual
**Settings → Pages → Build and deployment → Source** must be set to
**GitHub Actions** before deploying there.
### Translation corrections
README editions and website editions have separate translation sources:
- Find the language's `readme` path in the
[language inventory](website/locales/languages.json) and edit that file.
Locale codes and README suffixes can differ. Use the
[English README](README.md) to check commands, requirements, and analysis claims.
- Edit `msgstr` values in `website/locales/<language>/messages.po` for website
text. Follow the [website translation guide](website/locales/README.md) for
the Python environment, catalog validation, and local preview. Preserve source
strings, contexts, placeholders, commands, paths, and recorded evidence.
Before submitting a website translation, run the catalog and website checks in
that guide. Translated HTML in `website/_site/` is generated; edit the PO catalog
rather than the generated pages.
## Development feedback and PR verification
For routine merges, wait for the required checks to pass for the current PR
+13 -5
View File
@@ -783,14 +783,22 @@ def _within_response_limit(value: Any) -> bool:
current = pending.pop()
if isinstance(current, str):
size += 2
if len(current) > MAX_FRAME_BYTES - size:
return False
if current.isascii():
size += len(current)
size += current.count('"') + current.count("\\")
size += sum(5 for character in current if ord(character) < 0x20)
else:
size += 4 * len(current)
size += current.count('"') + current.count("\\")
size += sum(5 for character in current if ord(character) < 0x20)
# Count UTF-8 bytes without allocating a copy of the whole output.
try:
size += sum(
len(current[start:start + 65536].encode("utf-8"))
for start in range(0, len(current), 65536)
)
except UnicodeEncodeError:
# Unencodable output also needs the signed omission fallback.
return False
size += current.count('"') + current.count("\\")
size += sum(5 for character in current if ord(character) < 0x20)
elif isinstance(current, dict):
identity = id(current)
if identity in seen:
+18
View File
@@ -140,6 +140,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
"params"
);
private static final String[] CAPABILITIES = {
"read_binary_inventory_summary",
"annotate_native_function",
"set_address_name",
"set_addresses_names",
@@ -427,6 +428,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
);
}
return switch (request.method) {
case "read_binary_inventory_summary" -> binaryInventorySummary(request.params);
case "inspect_native_load_image" -> inspectNativeLoadImage(request.params);
case "read_bytes" -> readMemoryBytes(request.params);
case "search_bytes" -> searchMemoryBytes(request.params, descriptor);
@@ -1974,6 +1976,18 @@ public final class ReaGhidraBridge extends HeadlessScript {
return result;
}
private JsonObject binaryInventorySummary(JsonObject params) throws Exception {
requireKeys(params, Set.of());
JsonObject result = new JsonObject();
result.addProperty("document", currentProgram.getName());
result.add("segments", segments());
// Count the owning inventories, preserving their exact discovery rules
// without serializing function identities or string values for counts.
result.addProperty("procedure_count", procedures().size());
result.addProperty("string_count", strings().size());
return result;
}
private JsonArray listNames(JsonObject params) throws Exception {
requireKeys(params, Set.of("document", "address"));
requireDocument(params);
@@ -2000,6 +2014,10 @@ public final class ReaGhidraBridge extends HeadlessScript {
private JsonArray listSegments(JsonObject params) {
requireKeys(params, Set.of("document"));
requireDocument(params);
return segments();
}
private JsonArray segments() {
JsonArray result = new JsonArray();
String imageBase = canonicalAddress(currentProgram.getImageBase());
MemoryBlock[] blocks = currentProgram.getMemory().getBlocks();
+23 -5
View File
@@ -98,11 +98,25 @@ existing analysis families directly, for example
| `collect_adb_bugreport` | read | Device-generated bugreport zip, digested |
| `push_adb_file` | **mutates device** | One local file to a device path (e.g. pushing tools into `/data/local/tmp`); checks for existing targets and refuses them without explicit overwrite (the check is not atomic); compares digests when device `sha256sum` is available |
| `install_adb_package` | **mutates device** | Local APK via `adb install [-r]`; preserves its Success/Failure verdict, records `replace_requested`, and leaves actual replacement `null` because prior package state is not observed |
| `uninstall_adb_package` | **mutates device** | Removes a package and its data for user 0 |
| `uninstall_adb_package` | **mutates device** | Requests package removal for user 0 via `adb shell pm uninstall --user 0`, without data retention |
| `start_adb_app` | **mutates device** | Resolves the package's launcher activity (`cmd package resolve-activity --brief`) and starts it (`am start -n`) |
| `start_adb_activity` | **mutates device** | Starts an intent (`am start -a` with optional URI, component, and up to 16 typed extras); preserves the activity manager's verdict |
| `stop_adb_app` | **mutates device** | Force-stops every process of a package, ending a dynamic-analysis run |
For `inspect_adb_window`, `focus_contexts` retains each display header, its exact
display ID when recognized, and every focus declaration with its original line.
Declarations without a display header are explicitly `unscoped`. A missing
declaration differs from a declaration whose `value` is JSON `null`.
`source_section` identifies the selected dumpsys section; `observed_lines`
preserves its focus declarations and display headers in source order.
`focus_summary` is `single_context` only for one context with no repeated focus
field. The singular `focused_window` and `focused_app` summarize that context;
they do not establish device-wide focus. Multiple contexts or repeated fields
produce `ambiguous` with null singular fields and all declarations retained.
When no focus field was declared, the status is `unavailable`. REA reads the
displays section only when the windows section declares neither focus field.
For `install_adb_package`, `replace_requested` records whether `-r` was sent.
The `replaced` result is `null`: the successful install response does not say
whether the package was installed fresh or replaced, and REA does not query the
@@ -153,8 +167,10 @@ and during shutdown.
- **Mutations are explicit.** `push_adb_file`, `install_adb_package`, and
`uninstall_adb_package` declare `mutatesTarget` and never run implicitly:
pushes check for existing device files before transfer and refuse them
unless `overwrite` is set. The check is not atomic, so a different process
can create the path before `adb push` writes it. When available, the device's
unless `overwrite` is set. A failed or unexpected existence probe stops
transfer even with `overwrite`; only a clean `test -e` exit of 1 with no
output is interpreted as absence. The check is not atomic, so a different
process can create the path before `adb push` writes it. When available, the device's
`sha256sum` verifies the transfer; installs preserve the
device's `Failure [...]` reason verbatim.
- **Producer identity stays attached.** Every result embeds the producing
@@ -167,8 +183,10 @@ and during shutdown.
The app/activity lifecycle tools use `am start -W` and `am force-stop`.
- Per-invocation budgets: client/devices 10-20 s, shell reads 20-60 s with
64 KiB-8 MiB capture caps (logcat 4 MiB, screen 32 MiB), transfers and
bugreports 600 s. Exceeded budgets report `resource_constraint`; stopped
commands report `provider_timeout`.
bugreports 600 s. Output capture overflow reports `resource_constraint`;
deadline expiration, including the client-version probe, reports
`provider_timeout`. Command cancellation preserves the caller's exact reason;
a later abort does not replace an already completed command failure.
- Pull and capture outputs are additive: the destination must not already
exist. Re-running a completed pull refuses the existing directory instead
of overwriting evidence.
+6 -2
View File
@@ -233,8 +233,12 @@ rea project-android-application-graph '{"inventory_evidence":[<inventory_artifac
```
The projection reports exact component paths and hashes, runtime-family hints,
and path-based bridge hypotheses. It does not decode DEX or claim observed
runtime calls. Framework hints match complete native library names, such as
and path-based bridge hypotheses. File and slice occurrences supply component
endpoints; directory trees and symlinks retain their identities in the source
inventory without being classified as code files. It does not decode DEX or
claim observed runtime calls. DEX/JVM runtime hints summarize the classified
bytecode component lists. Framework hints match complete native library names,
such as
`libreactnativejni.so`, `libreactnative.so`, `libhermes.so`,
`libhermes_executor.so`, `libflutter.so`, and `libunity.so`; unrelated names
such as `libcommunity.so` and `libreactivity.so` do not identify those families.
+2
View File
@@ -52,6 +52,8 @@ select Android's `values-pt-rBR`; Android qualifiers such as `pt-rBR` and
- **An owned APK snapshot is digested and decoded.** `target.sha256` and
`target.bytes` bind the observation to the exact APK that was decoded.
The Evidence subject carries the same snapshot digest and caller-selected
path, including when the original file changes during decoding.
- **Metadata comes from apktool.yml as apktool printed it.** Version codes
keep their own spelling (`'7'` versus `7`); `package_name` is read from
the decoded manifest, not inferred.
+18
View File
@@ -60,6 +60,15 @@ states that absence is unknown. Component arrays remain complete when candidate
hypotheses are omitted; component arrays still include every component from
the supplied inventory pages, and overall coverage is `partial` in that case.
Inventoried files whose bytes were not read, such as encrypted archive members,
are listed in `unavailable_components` with their original path, format,
encryption flag, limitations, and null content identity. The same application-root
and AppleDouble attribution rules apply. These files make overall coverage
`partial`; component absence is unknown even when `inventory_complete` is true.
Directories have no file bytes and symlinks remain in `symlinks`, so neither is
an unavailable file component. The additive field participates in `projection_id`,
including when empty, so Apple projection identifiers change.
## Application roots
| Inventory root | Application root |
@@ -68,6 +77,15 @@ the supplied inventory pages, and overall coverage is `partial` in that case.
| `.app` directory | `.`, the inventoried bundle itself |
| Directory, ZIP, or DMG containing apps | Each outermost `<Name>.app` followed by `Contents/` |
File and slice occurrences supply component endpoints. Directory tree identities
remain in source inventory and bundle anatomy, and links remain in `symlinks`.
Root and layout discovery use known directories and ancestors implied by deeper
inventory paths. A leaf file or symlink named `*.app`, `Contents`, or `Versions`
does not establish that directory. Directory and authenticated-file identity are
selected before applying anatomy case preferences, so a file cannot shadow a
folded directory name or a directory shadow a metadata/signature file.
ZIP archives made with `ditto -c -k --keepParent` report `<Name>.app`. A DMG
reports `<image>.dmg/<Volume>/<Name>.app`; DMG child inventory needs the native
macOS mounting adapter. When a DMG has only its root identity, the projection is
+6
View File
@@ -26,6 +26,7 @@ graph TD
end
subgraph "Provider Layers"
PROVIDER_OWNERS["src/providers/<br/>shared deferred client and inspection owners<br/>exact cleanup retry and attributed failures"]
PWNTOOLS["src/native/pwntools/ + src/native/pwndbg/<br/>BYO offline ELF/core adapters<br/>owned snapshots/Python and optional core-only GDB<br/>unchanged upstream parsers, ABI structures and mapping API"]
WASM["src/wasm/<br/>caller-supplied WABT; immutable artifact inspection<br/>owned process supervision and retryable workspace cleanup"]
EVM["src/evm/<br/>unchanged EVMole/WASM in bounded owned worker<br/>explicit carrier parsing and inferred interface adapter"]
@@ -138,6 +139,11 @@ graph TD
GHIDRA --> GHIDRA_BRIDGE
HOPPER --> PROCESS
GHIDRA --> PROCESS
GHIDRA --> PROVIDER_OWNERS
APP --> PROVIDER_OWNERS
PROVIDER_OWNERS --> PROCESS
PROVIDER_OWNERS --> AUX
PROVIDER_OWNERS --> DOMAIN
GHIDRA --> WINDOWS
PROCESS --> WINDOWS
WINDOWS --> WINDOWS_NATIVE
+9 -1
View File
@@ -285,6 +285,14 @@ REA establishes a main-frame origin boundary before enabling Runtime, Debugger,
## Data minimization
The Link projection parses repeated CDP header field lines independently and
retains their declarations in order, including derived untrusted agent hints.
Commas inside URI targets or quoted parameters remain part of those values.
COOP and COEP projections retain the exact declared token after validating the
complete Structured Field Item, including parameters such as `report-to`.
These are declarations, not an assessment of the document's effective policy;
absent, malformed or non-token items project as `null`.
REA retains local observation data and removes only URL userinfo credentials, explicitly declared scenario secrets, and values from structured credential/storage fields:
- URL observations preserve the complete local URL, including query values, fragments, duplicate parameters, and their order. Only username/password userinfo is removed; malformed original URL strings remain available with `origin: null`.
@@ -320,7 +328,7 @@ REA retains local observation data and removes only URL userinfo credentials, ex
an omitted type leaves equality unknown rather than proving a modification.
- Console observations with a stack source retain call type, argument types, timestamp, and source location. When console text capture is selected, already-delivered primitive values are retained verbatim; objects, getters, and remote properties are never expanded.
- WebSocket observations retain direction, opcode, and payload byte length. When shape capture is selected, text frames are classified as text or complete value-free JSON shape; binary bytes, hashes, prefixes, and raw frames are never retained.
- Storage observations always redact values. Key names, IndexedDB names, and cache names require `include_storage_keys`; stable content fingerprints require the additional `include_storage_fingerprints` selection. Cookie fingerprints cover cookies applicable to the current main-frame URL, including its path, rather than every cookie path on that origin. Other storage remains scoped to the origin. Cache bodies above 64 KiB and partial IndexedDB remote objects make fingerprint coverage incomplete, so identical observations remain `unknown`.
- Storage observations always redact values. Key names, IndexedDB names, and cache names require `include_storage_keys`; stable content fingerprints require the additional `include_storage_fingerprints` selection. Cookie fingerprints cover cookies applicable to the current main-frame URL, including its path, rather than every cookie path on that origin. Other storage remains scoped to the origin. Cache fingerprints hash all decoded response bytes, including bodies above 64 KiB, subject to the [64 MiB per-message CDP limit](#request-boundary). Missing or undecodable cache bodies and partial IndexedDB remote objects make fingerprint coverage incomplete, so identical observations remain `unknown`.
- Unavailable or malformed storage inventories mark `storage_keys` unavailable, even without fingerprints. Comparisons suppress inferred key additions and removals while preserving modifications proven by complete fingerprints on both sides. Previously exported captures are not retroactively corrected; recapture them to obtain these coverage markers.
- Script metadata is included only when CDP supplies a URL on an allowed origin. Stable keys exclude transient CDP script IDs, and exact transient raw URLs are used only during script/resource reconciliation. URL-less scripts are excluded because their origin cannot be established. When CDP supplies an execution-context association, the accepted script retains its authorized frame ID for later attribution. Script metadata admission bounds retained identity and source-map strings to 8 MiB and 50,000 records. When a source-map URL cannot fit, the script identity remains and source-map coverage reports `resource_budget_exhausted`; rejected script observations are counted without retaining their IDs. Source content is omitted unless explicitly requested and is returned inline as a self-verifying artifact with its SHA-256 digest.
+12
View File
@@ -144,6 +144,18 @@ distinction: it closes and deletes provider-owned profiles, but only disconnects
from an external CDP browser. Real-browser verification checks both outcomes and
confirms that an attached external browser remains alive.
That cleanup requires REA to remain running. Abrupt termination, including
Windows process-tree termination or POSIX `SIGKILL`, can leave the launch
profile and Playwright artifacts behind. A profile can retain cookies, history,
cache and origin storage. Later REA runs currently do not reclaim these paths.
Playwright 1.58.2 can also leave an artifacts directory when launch preparation
fails before a browser process exists, such as for a missing executable.
POSIX wrappers can request awaited cancellation with `SIGINT` or `SIGTERM`;
Windows process termination is not an equivalent graceful-cancellation channel.
Do not identify owned artifacts by directory prefix or creation time alone.
See [#4659](https://github.com/morluto/rea/issues/4659) for the remaining recovery
and wrapper-cancellation work.
Keeping that browser alive does not guarantee its emulation state survives.
Chromium can clear caller-owned `navigator.platform` emulation when a page CDP
session disconnects, including sessions that sent no emulation commands. REA
+71 -2
View File
@@ -56,8 +56,15 @@ rea inspect-electron-page http://127.0.0.1:9223 TARGET_ID \
The normalized result contains canonical local paths, frame and DOM
structure, resource metadata, stable script/resource identities, explicit
completeness, and content-addressed script artifacts. Script metadata
retains its execution-context frame ID when CDP supplies one. The capture also
completeness, and content-addressed script artifacts. Resource sizes are unknown
when omitted or invalid; supplied invalid sizes also produce an explicit
completeness diagnostic.
DOM node indices retain their snapshot positions. Invalid rows are omitted with
completeness diagnostics, so `total_nodes` can exceed the retained node count
and a parent index can refer to an omitted source node.
Script metadata retains its execution-context frame ID when CDP supplies one. The capture also
inventories worker, service-worker, and shared-worker targets with
validated opener-target and parent-frame IDs. Worker discovery uses passive
target metadata; REA does not attach to or execute code in those targets.
@@ -114,6 +121,11 @@ window, remains pending until cancelled. After cancellation, the final state
read has 5 seconds before REA keeps the initial snapshot instead; the
cancellation keeps the caller's reason and records that expiry in its
diagnostics.
Blocked synchronous shell APIs keep their return contracts:
`showItemInFolder` returns `undefined` and `writeShortcutLink` returns `false`.
Their `shell-attempt` events record the blocked phase and argument shapes;
neither result means the requested host operation occurred. The existing
`openExternal` and `openPath` blocks continue to reject their promises.
CLI SIGINT/SIGTERM requests cancellation and awaits owned-process cleanup.
Window IDs, URLs, and cached titles come from one main-process Electron snapshot,
so a crashed renderer does not require a successful renderer-side title query.
@@ -127,6 +139,63 @@ If cleanup cannot be verified, the error retains collected observations in
`details.partial_observation` and marks their application cleanup `unverified`.
Cleanup uncertainty does not authorize signaling unrelated processes.
Main-process IPC recording keeps original-function removal for `on` and `once`
registrations and records a fired `once` callback once. Native Node.js owns
`once` registration, firing and callable dispatch as well as removal order and
traversal, including listeners registered by removal observers;
the hook translates only its own recording wrappers at the removal boundary.
Wrapper metadata uses weak keys and does not retain removed registrations.
Wrapper metadata operations, private collection methods and traversal constructors are captured
before target startup; later replacement of their global bindings does not
change listener registration or removal.
Native runtimes differ in whether `once` consults callback `.call` or `.apply`.
The hook observes actual callback entry without imposing another runtime's
property reads or argument-list representation. Application methods selected by
native dispatch keep their original receiver and argument-list object; opaque
methods produce a `custom-call` or `custom-apply` observation gap when callback
execution cannot be established.
If native wrapper creation or registration-method lookup could expose private
instrumentation through an application getter, setter or Proxy, registration
stays native and unwrapped. The `once-metadata` observation gap identifies that
unobserved registration attempt; it does not prove registration or delivery. The original callback,
metadata writes and native method receivers are preserved, including when
registration fails. Later registrations resume normal observation. Non-callable
`call`/`apply` properties produce failed observation gaps when native dispatch reads
them; callback entry is not fabricated.
An `on` listener's application-defined `.apply` receives the original argument-list
object unchanged. Borrowing the exposed method preserves its dynamic receiver.
Because that method owns argument interpretation and may not
invoke the callback, the hook records a bounded `ipc-observation-gap` timeline
entry with the registered channel rather than an IPC invocation. Its empty
`argument_shapes` means unobserved arguments, not a zero-argument callback;
callback execution and result remain unknown. Coverage and retention include
these gaps even when their individual records are dropped. Normal native
dispatch continues to record actual invocation frames. Evidence snapshots copy
retained arrays and counters without invoking application iterators or Array
species.
Listener-chain recognition distinguishes owned, unowned and unknown metadata.
Data links may be inherited; absence is established only after completing the
bounded prototype lookup. Exhausting either walk, encountering a getter or seeing a Proxy leaves
ownership unknown. The hook preserves native registration without adding another
recording wrapper and retains a `listener-chain` observation gap after successful
registration. This gap does not prove IPC delivery. An existing inner recording
wrapper can still report its actual invocation; otherwise callback activity
remains unobserved. Setter-only properties have a known undefined read.
Data-descriptor parsing is shared with removal normalization.
This does not establish complete JavaScript function introspection equivalence.
Caller-defined `.listener` properties can change the identity exposed by
`listeners()` and meta-events; accessors and Proxy metadata are not followed for
wrapper recognition. Re-registering a raw recording wrapper on another channel
retains the original registration's channel attribution.
Mutating `.listener` on a raw recording wrapper can also prevent removal through
application decorators; the reproduced identity conflict is tracked in
[#4513](https://github.com/morluto/rea/issues/4513).
### Target fuse prerequisites
Active scenario capture requires that the target Electron executable honors Node CLI inspector flags (`--inspect` or `--remote-debugging-port`) so that Playwright can establish its DevTools protocol session.
+3 -1
View File
@@ -153,7 +153,9 @@ tree semantics, vulnerability scanning, emulation or hardware runtime capture.
## Resource and lifecycle policy
- Nonempty regular firmware inputs: at most **128 MiB**, copied and hashed into
a private workspace before analysis. No in-place source modification.
a private workspace before analysis. Growth beyond that allowance during
copying receives the same typed input refusal as an already oversized file.
Supply a stable copy within the allowance. No in-place source modification.
- One queued heavy operation per provider instance; Binwalk one thread and
Unblob one worker. No automatic parallel extraction engines.
- **120-second** deadline; inherited **1 GiB per-process address-space** limit,
+8
View File
@@ -124,6 +124,10 @@ FileBytes digests, every source mapping and its memory digest, relocation record
module partitions, relocation fixups and linear coordinates rather than relying
on the imported executable SHA alone.
Expected DOS relocation records retain each fixup's pre-application bytes and
applied value. Their memory-byte windows come from the final relocated image,
including changes made by later overlapping fixups.
The result is `verified`, `mismatch`, or `unsupported`. Every check retains its
expected and observed values. A memory-digest mismatch identifies the source
range's file offset and analysis address; it does not locate the first differing
@@ -134,6 +138,10 @@ their contents and DOS runtime semantics are not verified. Independent verificat
also covers explicit [raw-binary imports](raw-binary.md); other formats return
`unsupported` with measured observations.
MZ verification retains the initialized image described by the header's page
count and streams appended overlay bytes into the complete original and modified
source digests. Overlay size does not increase the retained image allocation.
`read_bytes` returns initialized provider memory, including loader fixups; an
unmapped or uninitialized byte ends the read with an explicit returned length and
`complete: false`. `address_to_file_offset` uses the provider's source mapping;
+15
View File
@@ -65,6 +65,14 @@ PDB, or change analyzer settings.
## Loaded addresses and file offsets
Ghidra computes overview procedure and string counts from the same inventories
used by `list_procedures` and `list_strings`, without transporting those rows
solely to count them. Counts include external functions and only Ghidra-defined
strings; they do not establish discovery of unidentified functions or undefined
string bytes. Complete inventories remain available through the list tools.
Overview reuse follows the target/profile snapshot lifetime and is invalidated
by session metadata edits.
`rea analyze <path> --provider ghidra --json` and MCP `binary_overview` report
the observed loaded `image_base` in `normalized_result`. They retain each
segment's reported `image_base` and `address_space`. A provider that does not
@@ -145,6 +153,13 @@ New attempts wait for that cleanup; failed cleanup retains its original error
and owned directories until a successful close. Closing the provider reports a
provider-state failure to live callers rather than cancelling their signals.
Configured analysis extensions and seeds use owned file reads during profile
resolution and launch snapshots. A changed read extent or descriptor state
refuses the commitment. Failed source or snapshot descriptor closes remain
owned and block new admission until cleanup succeeds; the physical failure
and exact cancellation reason remain available. Provider shutdown joins all
profile stages before retrying their retained descriptors.
| Concern | Ghidra contract |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Decompiler lifetime | One persistent `DecompInterface` is opened for the imported Program and disposed during bridge shutdown. Native decompilation has no fixed per-function deadline. External functions or functions without bodies return `null`; cancellation and native failure remain distinct. |
+4
View File
@@ -91,6 +91,10 @@ host permission denial, changed artifacts and cancellation retain distinct
errors. The selected file is never modified; this inspection creates no
temporary files, starts no subprocesses and accesses no network endpoint.
An input above 256 MiB returns `resource_constraint` with resource `file-size`
and `reported_limits.maximum_input_bytes`. Select a suitable input within that
fixed allowance; changing an output-write or transport budget does not admit it.
Resource guards bound the selected file and cumulative header searches to 256 MiB
each, aggregate embedded strings
to 1 MiB, structural table decoding to 16 MiB and the complete normalized metadata
+6
View File
@@ -45,6 +45,12 @@ The MCP tool `project_harmony_application_graph` takes the same
- path-based N-API bridge hypotheses between bytecode and `libs/<abi>` native
libraries
File and slice occurrences supply component and child-package paths; directory
trees and symlinks retain their identities in the source inventory without
being classified as code files. Ark and JavaScript runtime hints summarize the
classified bytecode and JavaScript lists; supported source extensions share the
same classification rule as their component rows.
Every claim is path-presence only. Manifest semantics (module identity,
abilities, entry points, permissions, dependencies), Ark bytecode contents and
versions, resource resolution, signing verification, and observed native
+3 -1
View File
@@ -8,6 +8,8 @@ Choose one lifecycle mode in an upstream MCP JSON registration. Set `REA_IDA_MCP
Each mode accepts one upstream tool profile, checked against `tools/list` before any analysis runs. The attached profile requires the legacy GUI tool names; upstream 2.0.0 renamed those tools, and REA has not established an equivalent attached-mode lifecycle and ownership contract for that catalog. It therefore fails closed with the missing tool names in the error diagnostics.
Startup failures identify the accepted REA analysis operation. Missing-profile and typed startup transport failures retain the original stage in `details.diagnostics.startup_operation`; existing diagnostics, timeout budgets and resource limits remain available. Startup remains shared within a session; each caller receives its own operation attribution. Cancellation retains live cleanup ownership, so a successful retry releases the originating cancellation outcome as well.
The headless profile is defined by the database-supervisor API, not by an upstream version string. The upstream source snapshot [`c133c385`](https://github.com/mrexodia/ida-pro-mcp/tree/c133c3853faa111a9b00ee615c013b720d0c4acd) includes `idb_close` (introduced by [upstream PR #495](https://github.com/mrexodia/ida-pro-mcp/pull/495)) alongside `idb_open` and `idb_list`. Earlier builds may report the same `2.0.0` version while lacking that tool, so REA checks the live catalog rather than treating a version number or branch name as a compatibility guarantee.
| Mode | Upstream profile | Known working |
@@ -39,7 +41,7 @@ rea function /absolute/path/to/program main --json
rea decompile /absolute/path/to/program main --json
```
Open the **original input binary** in REA, not an `.idb` or `.i64` database. Its SHA-256 must match IDA's recorded input hash. REA checks the attached identity and image base around each analysis request. A mismatch or target switch fails with recovery guidance. The original input must remain readable by REA; WSL can use its mounted alias while the GUI runs on Windows.
Open the **original input binary** in REA, not an `.idb` or `.i64` database. Its SHA-256 must match IDA's recorded input hash. REA checks the attached identity and image base around each analysis request. A mismatch or target switch fails with recovery guidance. The original input must remain readable by REA; WSL can use its mounted alias while the GUI runs on Windows. Attached profiles, including native mode, refuse universal Mach-O inputs at admission: every slice shares the recorded whole-file hash, so REA cannot verify which slice the GUI loaded. Open a thin file extracted with `lipo -thin` in both IDA and REA, or use headless mode, which analyzes the selected slice.
REA's close operation releases its MCP connection or proxy. It never saves or closes the existing GUI database. External edits remain visible because results are live and are not replayed from snapshots. These checks do not lock the GUI or prove that the database is unpatched.
+53 -4
View File
@@ -59,10 +59,12 @@ Hopper installation or any file write. Declining or pressing Ctrl-C makes no
changes. Agent configuration writes preserve unrelated entries and comments,
create backups, use atomic replacement, and verify their result. Setup and
uninstall retain an existing `.rea.backup` rather than replacing the first snapshot.
If that snapshot differs from the current configuration, setup previews and
preserves a verified `.rea.backup.<sha256>.recovery` copy of the current bytes.
Both snapshots remain available; a failed rollback reports the current recovery
copy's path and the restore failure.
If that snapshot differs from the current configuration, setup and uninstall
preserve a verified `.rea.backup.<sha256>.recovery` copy of the current bytes
before editing; setup also previews it. Both snapshots remain available.
When an edit made a backup, a failed uninstall update or restore, and a failed
setup readback or rollback, name the backup path that holds the bytes from just
before that edit.
Setup also retains valid environment overrides in REA's existing server entry,
such as `REA_MCP_INPUT_SCHEMA_PROFILE`, `REA_MCP_OUTPUT_SCHEMA_PROFILE`, and
`REA_MCP_MAX_RESPONSE_BYTES`.
@@ -833,6 +835,16 @@ rea doctor --json
rea setup
```
On macOS, [Homebrew OpenJDK is keg-only](https://formulae.brew.sh/formula/openjdk)
and may be unavailable to the system `java` wrapper. Set `JAVA_HOME` to the
selected formula's `libexec/openjdk.jdk/Contents/Home`, for example
`/opt/homebrew/opt/openjdk/libexec/openjdk.jdk/Contents/Home` or
`/usr/local/opt/openjdk/libexec/openjdk.jdk/Contents/Home`. Use a JDK inside the
Java version bounds declared by your Ghidra installation; REA does not select
another installed JDK automatically. Doctor distinguishes an absent Java
command from a command that runs and fails, and retains bounded probe output
and any parsed properties when the selected runtime cannot be admitted.
On Windows, configure the existing installation in PowerShell:
```powershell
@@ -882,6 +894,19 @@ the launcher fails before preparing its home/cache/config/temp directories or
starting Java. The client may already have allocated private runtime/endpoint
resources and a target snapshot; existing startup-failure cleanup still applies.
Restore the selected release's properties instead of renaming its installation folder.
Inspection follows selected properties symlinks, admits only a regular file,
and caps the metadata payload at 1 MiB under REA's resource budget. A final
one-byte probe checks for growth. This is not a Java
Properties format limit. Reads preserve Ghidra's Latin-1 byte mapping and check
the descriptor and selected name for changes. Permission failures and resource
refusals remain explicit; failed descriptor cleanup must succeed before that
owner admits another inspection.
Native decompiler compatibility inspection follows the selected executable symlink
and reads at most the first 4096 bytes through the same descriptor owner. It
checks the full extent and selected name for changes without reading the remainder.
An unreadable or unrecognized header leaves compatibility unknown; diagnostics
retain read failures. Cancellation and incomplete cleanup propagate as failures,
and failed cleanup retains the exact descriptor for retry.
This coordinate selects only REA's session-private configuration directory, not
the installation or an existing user configuration directory.
@@ -900,6 +925,10 @@ ownership inspection, so retaining those wrappers would prevent verified
process-group cancellation during startup.
If ownership remains unverifiable, it reports the reason and retains the process
supervisor and private runtime instead of removing files beneath a live provider.
Startup preserves an observed launch failure when cancellation arrives during
rollback. Producer cancellation retains the caller's exact reason separately
from LaunchSupport warnings and cleanup failures. Failed rollback keeps the
original process owner for a later cleanup retry.
The experimental Windows transport uses authenticated IPv4 loopback with a
private native-owned bearer descriptor and Job Object process ownership.
@@ -911,6 +940,9 @@ bridge, and health readiness. Large binaries can need more: set
`REA_GHIDRA_STARTUP_TIMEOUT_MS` to an integer between 1 and 2,147,483,647
milliseconds in the server environment. An absent setting uses the default;
empty, invalid or out-of-range supplied values fail configuration validation.
When the deadline elapses, the `provider_timeout` error names
`REA_GHIDRA_STARTUP_TIMEOUT_MS` with the deadline that was applied and suggests
`REA_GHIDRA_DISABLED_ANALYZERS` for analyzers that dominate the time.
REA parses the supplied configuration environment and
passes the deadline to each provider client; a startup failure is returned by the query that triggered it,
without exposing partial analysis. This deadline is separate from MCP transport
@@ -992,6 +1024,8 @@ export REA_GHIDRA_COMPILER_SPEC_ID="borlandcpp" # optional; requires a languag
REA passes `-processor`/`-cspec`, checks both against the installation's `.ldefs`, and records them in the analysis profile. DOS imports reject an override.
Language definitions must be well-formed XML encoded as UTF-8 or BOM-marked UTF-16, with any encoding declaration matching the bytes. Literal Unicode replacement characters (U+FFFD) are accepted; malformed source bytes are rejected.
To seed code that auto-analysis cannot reach (computed jumps, pointers in data):
```bash
@@ -1202,6 +1236,21 @@ on the shell `PATH`. Use the setup command above for the guided setup
journey, `npx -y rea-agents@latest` for unattended one-off commands, or install globally
with `npm install --global rea-agents` for a shell-visible `rea` command.
For many concurrent agent sessions, run setup directly from a global installation
to avoid keeping a separate npm launcher process for each MCP connection:
```bash
npm install --global rea-agents
rea setup
```
Review the plan, approve the selected client changes, then restart those clients.
This registers the installed entrypoint directly; installing the global package
alone does not replace existing `npx` registrations. Each client still has its own
REA process and analysis state. Direct registrations follow the installed CLI;
use `rea update` to update it. Running setup through `npx` again selects the pinned
package-runner command. Do not register paths inside npm's temporary package cache.
The optional curl wrapper installs only the global npm package:
```bash
+21
View File
@@ -288,6 +288,27 @@ and the result includes its basis, candidates, changed dimensions, Evidence
links, limitations, and a `changed_from` graph containing all compared nodes,
their observations, and relationships.
A matched pair whose only differing dimension is `location` counts as
`unchanged` when both input graphs have complete coverage and its neighboring
relationship identities are resolved. This includes an
identical module shifted by an earlier bundle edit and a named IPC channel
observed at a new offset. The item still reports `dimensions: ["location"]`
and both node IDs; the graph retains both original observations and coordinates.
Content, property, and complete relationship differences still count as
`changed`. With incomplete coverage, relocation alone remains `unknown` and
retains the location and coverage dimensions. Matching confidence is unchanged:
an inferred pair is not promoted to exact identity by this classification.
Complete inventory does not resolve ambiguous or unmatched relationship targets.
A matched module with identical bytes can therefore remain `unknown` when a
neighbor, including its containing chunk, has no cross-version match. The item
retains its exact content match, coordinates, candidate inventory, and an explicit
relationship-identity limitation. `coverage` is added to its dimensions only for
incomplete inventory. Definite content, property, or complete topology differences
remain `changed` while retaining any separate relationship uncertainty.
An unknown item with no observed content, property, or relationship difference
does not emit a `changed_from` edge; its pairing and uncertainty remain in the item.
One-sided absence is `added` or `removed` only when the opposite input graph has
complete coverage. With partial, unavailable, or truncated input, the same
condition is `unknown`. The comparison retains every classified item and
+5 -2
View File
@@ -133,8 +133,11 @@ Each operation uses one worker, a 1 GiB address-space ceiling and a 120-second
deadline; it accepts up to 64 MiB of total input across all selected files and
128 MiB/10000 entries of staged
output, with 8 MiB per diagnostic/report stream. Exceeding a limit is an explicit
failure with rollback. These limits apply per operation and do not claim aggregate
host containment across concurrent callers.
failure with rollback. Selected inputs beyond the total input allowance return a
non-retryable `resource_constraint` with `resource: "file-size"` and
`reported_limits.maximum_input_bytes`/`remaining_input_bytes`; input that is not
valid UTF-8 is an `invalid_format` request issue. These limits apply per
operation and do not claim aggregate host containment across concurrent callers.
See [testing.md](testing.md#javascript-source-recovery) for the focused real
Wakaru CLI/MCP lane and its source-owned compiler fixtures. Other host platforms
+3 -1
View File
@@ -109,7 +109,9 @@ The direct observations are:
- `Runtime.executionContextCreated`,
`Runtime.executionContextDestroyed`, and
`Runtime.executionContextsCleared`, establishing context lifecycle within
the capture window.
the capture window. A created or destroyed event whose context ID is
missing or not a safe integer adds no context record; it is retained only
as a count reported in `unknowns`.
The Inspector protocol does not directly expose require/import caller edges,
EventEmitter emissions or listener invocation, Electron IPC messages or
+1
View File
@@ -52,6 +52,7 @@ All four are read-only with respect to the target file; only `open_jeb_project`
- The engine caps `list_units` pages at 100; a full page is reported as `coverage: "partial"` because more units may exist. An empty remainder is `complete` only when fewer units than requested were returned.
- When `decompile_jeb_item` omits `unit_path`, the engine implicitly selects the project's first code unit; REA reports `unit_path: null` and a limitation instead of guessing the effective unit.
- Engine refusals (`success: false` envelopes) are preserved as unsupported-target errors carrying the engine's own message, for example a nonexistent item address.
- Successfully parsed client information is reused for the lifetime of the MCP connection, including when the engine omits its version. Explicit client inspection and availability probes refresh it.
- An unreachable endpoint is a provider-availability failure with recovery guidance; REA does not retry or spawn the engine.
Real-engine verification lives in the JEB lane described in [testing.md](testing.md); mock transport tests do not establish real-engine compatibility.
+21 -1
View File
@@ -22,6 +22,8 @@ Select it explicitly with `--provider kuna`, MCP's `open_binary.provider_id: "ku
| `REA_KUNA_TIMEOUT_MS` | 120 s | Deadline for one single-function decompile, and for the inventory run REA makes when the export ends without one. |
| `REA_KUNA_JOBS` | 1 | `decompile-project --jobs`. 1 is Kuna's reference answer; more jobs finish sooner but lose callee-first prototypes, and change about 40–60% of function bodies on the measured binaries. |
A single-function decompile, inventory, string, graph or xref run that exceeds its deadline fails as `provider_timeout`, naming the setting that owns the deadline and that raising it takes a restart. A project export that exceeds `REA_KUNA_PROJECT_TIMEOUT_MS` is an export failure instead: requests that need the export report `capability_unavailable` with the export's own retry guidance.
Windows is not supported yet. Supported targets are ELF and thin Mach-O executables for x86-64, AArch64 and ARM; PE and universal Mach-O are rejected with a typed reason.
## What Kuna answers
@@ -68,7 +70,7 @@ Single-function subprocess output and each cached source facet share a 16 MiB bu
Results keep exact u64 virtual addresses, decoded text, native encoding, terminator status and byte length including any terminator. Kuna scans mapped sections or segments for runs of at least five characters; unterminated fragments can include instruction bytes. `all` includes ASCII, UTF-8 and the engine's Latin-range UTF-16LE reading. Other text, including short strings, non-Latin wide text and UTF-16BE, can remain undiscovered. An empty result does not prove absence.
Use `search_strings` for the matching rows or `list_strings` with an exact address for that location. Literal and regex matching run in REA, preserving the existing case and ECMAScript Unicode semantics instead of substituting Kuna's different regex dialect. Procedure and string regex queries share one worker slot per session. Input projections are materialized only after admission, so queued queries retain no per-query inventory arrays. Close cancels and drains matching. Native metadata queries also share one admission slot, including references; a failed stop retains admission until cleanup is proven.
Use `search_strings` for the matching rows or `list_strings` with an exact address for that location. The validated inventory is sorted by numeric address and encoding. Once cached, exact-address reads use binary search and return every encoding at that address without scanning the full inventory or retaining another lookup index. Literal and regex matching run in REA, preserving the existing case and ECMAScript Unicode semantics instead of substituting Kuna's different regex dialect. Procedure and string regex queries share one worker slot per session. Input projections are materialized only after admission, so queued queries retain no per-query inventory arrays. Close cancels and drains matching. Native metadata queries also share one admission slot, including references; a failed stop retains admission until cleanup is proven.
`raw_result.strings` reports the source JSON digest/size, scanned scope, extraction policy, complete inventory count and selected count without copying unrequested text into the answer. Omit the address from `list_strings` to retrieve the complete inventory as Evidence; normal session retention and bundle export apply, including transport-capacity errors for oversized inline replies. These operations preserve their complete-result contracts and do not add paging. Function-referenced string facets in `analyze_function` remain unavailable; call `xrefs` explicitly when references to one returned string are needed.
@@ -82,6 +84,24 @@ REA parses the graph as Kuna writes it, like the string scan, so the graph has n
Evidence includes only selected addresses or dossier neighbors, the producing JSON's SHA-256/byte count, coverage and relevant function facts; unrequested program-wide JSON and instruction text are omitted. Reference caches are in memory and expire on close: a fresh CLI process or MCP server rebuilds them. A first call query therefore costs a whole-program reference walk even when a C export is warm. These operations do not establish full Ghidra/IDA feature parity.
Failures from shared installation or inventory work identify the operation the
caller requested. `diagnostics.producer_operation` retains the internal producer
name when it differs. Caller-specific failures keep the producer's typed outcome,
captured output, partial facts and live cleanup dependencies; cleanup retry
updates every projection rather than preserving an independent stale marker.
Installation acquisition is shared, with independently cancellable caller waits.
The final departing caller stops the probe and joins its cleanup; session close
also stops and joins acquisition. A cleanly cancelled acquisition can be retried,
while a retained cleanup failure blocks replacement until its owner is closed.
Shared string and reference queries preserve caller cancellation reasons and
join the final departing caller's command cleanup. A command cleanup error keeps
its typed primary failure and captured output. Its live resource list follows
both process and workspace ownership: proving process exit does not prove that
the private workspace was removed, and retry updates earlier caller errors.
If workspace setup fails, an unsuccessful rollback retains the original target
or read failure together with its cleanup owner. Replacement stays blocked until
that rollback is resolved; a clean rollback permits another setup attempt.
## Provenance and limitations
- Addresses and bytes are Kuna's observations of the image. Procedure entries and extents are its derivations: extents are upper bounds clipped at the next entry, and Kuna can split one function at an entry its own analysis infers. Pseudocode is its reconstruction, never original source.
+28
View File
@@ -435,6 +435,9 @@ Provider calls receive the request cancellation signal. Artifact traversal,
hashing, version comparisons, Hopper requests, and process capture
check the same signal. Cancellation is distinct from timeout. A cleanup failure
uses `cleanup_incomplete` and lists only the owned resource kinds that remain.
Once those owners confirm cleanup, composed failures recover their original
typed primary outcome and recovery guidance. Historical composition diagnostics
remain in `details.cleanup_history`, separate from the primary diagnostics.
An atomic Evidence bundle or snapshot write can publish its complete output
before staging cleanup fails. In that case, `details.published: true`,
`details.published_path`, and `details.bytes` retain the publication receipt.
@@ -521,6 +524,12 @@ analysis runtime. These failures are reported at the operation that needs
them. A configured provider or an endpoint alone does not establish that a
target is supported.
Composite Hopper regex searches report failures against the accepted
`search_strings` or `search_procedures` operation. The underlying inventory
method and request ID remain in `details.diagnostics.provider_request`, with
the bridge error code and diagnostic type when available. Reattribution
preserves the failure class, captured output, and live cleanup ownership.
Evidence bundles, snapshots, and extraction use the paths and output behavior
declared by their tools. Artifact extraction materializes the selected regular
files into a fresh REA-chosen temporary directory and reports its path.
@@ -531,6 +540,25 @@ temporary source files are declared as `launchesProcess` and `writesFilesystem`,
including for a single JavaScript source. This internal analysis does not execute
the supplied application code.
## Static Ghidra bytes and source mappings
`read_bytes` returns initialized analysis memory, including loader fixups, rather
than live process memory. Check `returned_bytes` and `complete` before decoding:
reads stop at unmapped or uninitialized bytes. Missing bytes are unknown; do not
pad an incomplete read with zeros and treat the result as an observation.
`list_segments` reports memory-block initialization and address-space facts.
`address_to_file_offset` maps one address through the provider's source-byte
mapping. A successful start-address mapping does not establish that an entire
range is contiguous in the original file, or that its bytes equal analysis
memory after loader fixups. Uninitialized, missing, and ambiguous mappings fail
explicitly; preserve the reported failure rather than guessing an offset.
For constructor-populated pointer or resource slots, a static zero is not proof
of a later null value. Inspect the slot's `xrefs` and the writer's assembly or
pseudocode, retaining uncertainty about execution order and runtime values.
These operations inspect static evidence and do not execute the target.
## Ghidra first-query deadlines and recovery
A successful MCP initialize handshake establishes the REA connection.
+26 -3
View File
@@ -261,6 +261,13 @@ Mach-O fixtures check each case against source-owned return values.
## Static inspection
`inspect_plist.format` describes the selected source, not plutil's conversion
format. Sources explicitly identified as XML, binary or JSON retain those
labels. For generic text, a bounded source-prefix probe recognizes leading XML
markup, including UTF-8 and BOM-marked UTF-16 sources. Unidentified text and
OpenStep plists, including a data-only value such as `<deadbeef>`, report
`unknown` even when plutil decodes their values through an XML conversion.
`inspect_macho.selected_architecture` identifies the exact selected lipo slice
and its `container_file_offset` in the input file. A thin Mach-O has a known
offset of zero; an unknown universal-slice offset stays null. The Evidence
@@ -317,12 +324,16 @@ complete observation.
shared/cyclic references, class descriptors, raw serialized fields and
malformed/unresolved/nil references. Missing fields stay absent; unknown
classes are preserved. Select `root`, `offset` and `limit` explicitly.
Object `kind` describes the decoded archive value: data, dates and numbers
remain `scalar` when their JSON value uses a `$plist_type` wrapper. An actual
dictionary with the same fields remains `dictionary`.
UID 0 cannot distinguish conditional nil from ordinary nil. Limits are
64 MiB of input, 200,000 objects/references and 128 nesting levels.
XML dictionary entries decoded as `__proto__` are omitted with an explicit
count because REA results cannot represent that key. XML tag whitespace and
comments do not change that handling. Other values and the original archive
digest are preserved; key interpretation follows the pinned plist decoder.
comments do not change that handling. XML key names combine their text and
CDATA segments without trimming or expanding literal CDATA entities. Other
values and the original archive digest are preserved.
- `decode_interface_builder` / `rea decode-interface-builder <app>` reads
compiled storyboard and nib resources into objects, outlets, actions,
connections and controller/class names. A full app graph is often several
@@ -398,6 +409,10 @@ complete observation.
coroutine tables, and inherited overrides, have explicit unsupported or
partial coverage. Other providers retain the
existing symbol-based inventory with its narrower coverage.
Symbols without a reported location remain in that inventory: Swift symbol
`address` and Objective-C `implementation_address` are null. An empty provider
address is retained as `reported_address` in the symbol's evidence, whose
location also remains unknown. Nonempty provider address syntax is preserved.
## Native desktop observation
@@ -414,7 +429,11 @@ decrement scrolling and text-value entry, or bounded waits. No global event
injection is used. Unsupported AX actions fail explicitly. The result preserves
ordered before/after captures and gaps; an action may have occurred before a
post-action capture fails. Application state is left as-is; REA does not attempt
to restore it.
to restore it. Failed or cancelled scenarios return a typed error with the
collected report in `partial_observation`, including ordered step outcomes.
Cancellation preserves the caller's reason and remains observable through helper
cleanup. A later abort does not replace an earlier helper or output failure.
Cleanup failures retain their runtime owner until a confirmed cleanup retry.
Scenarios accept caller-selected action lists and accessibility node counts.
Individual waits cannot exceed the operation's 180-second deadline, and the
@@ -568,6 +587,10 @@ digests identify the original serialized bytes.
## Keyed archive integer precision
The archive reader observes XML number element types and binary number markers.
XML values and their number facts are interpreted together from the validated
DOM. Hexadecimal `<integer>` text, including an optional sign, retains its value;
`<real>` text and CDATA segments are combined without inferring text from nested
elements. The XML path does not serialize or reparse character references.
Integers beyond the exact range of a JSON number are reported with the original
decimal text as `{ "$plist_type": "integer", "decimal": "<exact digits>" }` when
that source value can be associated unambiguously. Integral real values remain
+4
View File
@@ -309,6 +309,10 @@ REA에 도움을 보태 주세요! [issue를 등록](https://github.com/morluto/
개발 환경과 검사 항목은 [CONTRIBUTING.md](../../CONTRIBUTING.md), 검증 절차는 [테스트 가이드](../testing.md), 프로젝트 구조는 [아키텍처 지도](../architecture.mermaid)를 참고하세요.
한국어 번역도 개선할 수 있습니다. 이 README를 수정할 때는 [영문 README](../../README.md)의 명령어, 요구 사항, 분석 범위와 일치하는지 확인하세요. 웹사이트 번역은 `website/locales/ko/messages.po`의 `msgstr` 값을 수정하세요.
웹사이트 번역의 수정 방법, 검사 명령, 로컬 미리보기는 `website/locales/README.md`를 따르세요. 위 기여 문서의 **Translation corrections** 절에서 이 가이드로 이동할 수 있습니다. 명령어, 경로, 자리표시자, 기록된 근거는 그대로 보존하세요. `website/_site/`의 한국어 HTML은 자동 생성되므로 직접 수정하지 마세요.
## 라이선스
[MIT](../../LICENSE)
+6 -1
View File
@@ -73,7 +73,12 @@ invocation. A signed status response also confirms completion of older bridge
connections after its UI dispatch, preserving recovery proof for a caller whose
receipt another caller has acknowledged. Neither receipt recovery nor a status
probe retries the original command. Failed acknowledgements remain pending and
block new commands until they can be retried successfully.
block new commands until they can be retried successfully. Status proof failures
identify `cutter_command` and retain `status` in
`details.diagnostics.auxiliary_operation`; malformed proof still blocks dispatch.
Descriptor failures likewise retain the requesting operation through cleanup.
Command admission reports typed cancellation and treats the caller's abort reason
as opaque metadata, without inferring cleanup ownership from it.
## Configuration and coverage
+4 -1
View File
@@ -57,7 +57,10 @@ triggers or arbitrary SQL.
Rows are optional. `table` selects one exact ordinary-table name. `columns`
(CLI repeated `--column`) selects exact visible column names in the requested
order; omission selects all visible columns, including generated columns.
Unselected cells are not evaluated or projected. Column selection requires a
Unselected cells are not evaluated or projected. Malformed column-name bytes
cause a format refusal when all columns are requested. In inventory scope, an explicit valid subset can
still be sampled without decoding unrelated malformed column names.
Column selection requires a
table and at least one name; empty-string identifiers are valid. `row_limit`
requires `table`, defaults to 20 and accepts positive safe integers. Samples
remain subject to the byte and worker resource limits below, regardless of row count. The result reports
+5 -2
View File
@@ -503,7 +503,7 @@ costs. Its queue, execution, and failures are independent of the required gate
and publication. CPU time and peak process-family memory are not measured by this API.
The build job saves portable Turbo outputs across runs. Eight Linux real-system
lanes restore this cache without waiting for the build job. The shared action
lanes reuse its current-run artifact; standalone dispatches restore the cross-run cache. The shared action
derives its key from `test:prepare --dry=json` task hashes, runner OS and
architecture, `.npmrc`, `.nvmrc`, and the lockfile; test-only changes can reuse compiled outputs. Every
build and verification command still runs, so Turbo checks task inputs and
@@ -513,7 +513,10 @@ that generator's entry. Native outputs are not stored in this cache.
Native package jobs, the Windows curated lane, and the Apple artifact lane reuse
the same workflow's Turbo cache for portable compiled JavaScript, skills, and
catalogs. The Windows lane relies on the Linux static job for typechecking. They retain the normal build and prepack
catalogs. The producer stages only the completed `test:prepare` task hashes into
`.cache/ci-build-cache/`, preserving their archives, metadata, and available manifests.
Missing required entries fail publication; older cross-run entries stay in the
original cache. The Windows lane relies on the Linux static job for typechecking. They retain the normal build and prepack
commands, which restore matching outputs or rebuild on a cache miss. Native
artifacts, dependency installs, package archives, and runtime verification remain
host-local. Keep platform-dependent outputs out of this shared cache.
+14
View File
@@ -83,6 +83,9 @@ providers need not expose identical state machines.
recovery handshakes. Bound abandoned unauthenticated listeners where the
protocol permits it; that deadline must not expire authenticated idle sessions
or long-running native work. Listener expiry does not prove target closure.
- Readiness observations may await owned prerequisite commands. Status must
remain readable during owner finalization without starting replacement work;
report readiness as unknown when it cannot be re-observed.
- Shared startup has one producer and independently cancellable caller waits.
Keep it running while any caller still needs it. The final departing caller
waits for owned cleanup and retains its failure. Provider closure is distinct
@@ -95,6 +98,9 @@ providers need not expose identical state machines.
primary failure and let incomplete cleanup retain its own classification.
Preserve the caller's abort reason as the typed cancellation error's cause,
including when cancellation is observed only after provider completion.
Shared command owners must retain the observed cancellation reason separately
from physical failures and cleanup ownership. Carry that fact through failure
wrapping so adapters can preserve the exact reason without interpreting it.
Client cancellation alone does not prove native work stopped.
For atomic publication, document the cancellation cutoff and preserve any
committed output's publication receipt.
@@ -132,6 +138,14 @@ providers need not expose identical state machines.
while it remains unresolved. Link the originating cleanup error to its exact
owner so confirmed release resolves its cleanup marker while preserving the
earlier diagnostic. Shared CLI/MCP adapters cannot infer that confirmation.
- Provider profile selection owns every asynchronous candidate through its
terminal result and finalization. The registry owns profile-file cleanup,
joins all automatic-selection children, and closes those file owners before
reporting candidate shutdown. Final caller cancellation joins the selection;
an adapter that ignores cancellation remains active and owned until it settles.
Block new admission during shutdown and while cleanup remains unresolved;
reopen after confirmed retry. Preserve failed results before considering a
later signal, and retain every child's attributed cleanup in mixed outcomes.
- Release child descriptors before removing their containing root. Retain a
composite owner when either stage fails so retry preserves that ordering.
A Windows command leader's exit does not establish Job Object closure. Keep
+2 -2
View File
@@ -43,7 +43,7 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement
31,src/application/ReferenceSourceImportTypes.ts:limit,Import reports terminal limit failure,No reachable constructor found,Declared state may be unreachable; false verification risk,Remove unreachable top-level limit variant; per-entry limit guidance remains caller-visible.,removed,"verified by typecheck, exhaustive projection test, and source search"
32,src/application/Setup.ts:configureDetectedClients(backup),Agent configuration backup fails,"{clientName} configuration backup verification failed; no successful configuration was reported.",Exposes machine client key and obscures no-write guarantee,"Agent configuration could not be backed up, so no change was made. Check file permissions, then rerun setup.",implemented,focused assertions passed
33,src/application/Setup.ts:configureDetectedClients(write),Agent configuration write fails,"{clientName} configuration write verification failed; no successful configuration was reported.",Exposes machine client key and gives no recovery,"Agent configuration could not be updated. Check file permissions, then rerun setup.",implemented,focused assertions passed
34,src/application/Setup.ts:configureDetectedClients(readback),Written agent configuration cannot be verified,"{clientName} configuration readback verification failed; no successful configuration was reported.",Exposes machine client key and omits backup recovery,"Agent configuration could not be verified after writing. Repair the configuration file or restore its `.rea.backup`, then rerun setup.",implemented,focused assertions passed
34,src/application/Setup.ts:configureDetectedClients(readback),Written agent configuration cannot be verified,"{clientName} configuration readback verification failed; no successful configuration was reported.",Exposes machine client key and omits backup recovery,"Agent configuration could not be verified after writing. The original was restored and its backup at {backupPath} was retained. Repair the configuration file or restore that backup, then rerun setup. Without a backup: Agent configuration could not be verified. Repair the configuration file, then rerun setup.",implemented,focused assertions passed
35,src/application/Setup.ts:installSkill,Skill install or semantic readback fails,Agent skill installation or readback failed.,No useful recovery action,"REA analysis skill could not be installed or verified. Check permissions for `~/.agents/skills`, then rerun setup.",implemented,focused assertions passed
36,src/main.ts:serveStdio.onerror,Established MCP client connection reports an error,MCP stdio transport error,Exposes transport implementation and gives no recovery,REA lost its MCP client connection. Restart REA from your MCP client.,implemented,verified by injected transport onerror runtime test
37,src/main.ts:serveStdio throw,MCP connection cannot be created,Failed to start MCP stdio transport,Exposes transport implementation and gives no recovery,REA could not start its MCP connection. Restart REA from your MCP client; run `rea doctor` if it fails again.,implemented,verified by injected transport startup-throw runtime test
@@ -55,7 +55,7 @@ harm_rank,location,trigger,current_copy,user_risk,proposed_replacement,implement
43,src/application/Uninstall.ts:removeClient parse,Agent configuration is malformed,Malformed configuration was preserved without mutation.,Does not name required repair format or retry,"Configuration is not valid JSON/TOML and was not changed. Repair it, then rerun uninstall.",implemented,focused JSON assertion passed; TOML path uses same branch
44,src/application/Uninstall.ts:removeClient backup,Configuration backup copy fails,Configuration backup failed; no mutation was attempted.,No recovery step,"Configuration could not be backed up, so no change was made. Check file permissions, then rerun uninstall.",implemented,verified by injected filesystem backup failure test
45,src/application/Uninstall.ts:removeClient update,Write or semantic readback fails,Configuration update failed; the original was restored and {backupPath} was retained.,Exposes local path and lacks clear next sequence,"Report whether the original was restored; if restoration also fails, direct the user to the retained backup.",implemented,verified by injected update and restoration failure tests
45.5,src/application/Uninstall.ts:removeClient restore,Configuration update fails and restoring the original also fails,"Configuration could not be updated or restored. Restore its `.rea.backup` manually, then rerun uninstall.","High risk; persistent configuration may be damaged, but retained backup is authoritative",Keep new recovery copy,implemented,verified by injected restoration failure test
45.5,src/application/Uninstall.ts:removeClient restore,Configuration update fails and restoring the original also fails,"Configuration could not be updated or restored. Restore the backup at {backupPath} manually, then rerun uninstall.","High risk; persistent configuration may be damaged, but retained backup is authoritative",Keep new recovery copy,implemented,verified by injected restoration failure test
46,src/application/Uninstall.ts:removeManagedPath symlink,Managed cache/state/skill path is a symbolic link,Refused to follow symlink {path}.,Exposes local path and does not explain manual safety check,"REA did not remove this item because its managed path is a symbolic link. Verify the link target before removing it manually.",implemented,focused assertion passed
47,src/application/Uninstall.ts:removeManagedPath failure,Managed item removal fails,{path} could not be removed.,Exposes local path and gives no recovery,"This item could not be removed. Check file permissions, then rerun uninstall.",implemented,verified by injected managed-path removal failure test
48,src/application/DoctorDiagnostics.ts:node,Runtime version is outside supported ranges,"Install Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+.",Low risk; exact dependency recovery,Keep current copy,unchanged,verified by doctor unit test
1 harm_rank location trigger current_copy user_risk proposed_replacement implementation_status verification_result
43 31 src/application/ReferenceSourceImportTypes.ts:limit Import reports terminal limit failure No reachable constructor found Declared state may be unreachable; false verification risk Remove unreachable top-level limit variant; per-entry limit guidance remains caller-visible. removed verified by typecheck, exhaustive projection test, and source search
44 32 src/application/Setup.ts:configureDetectedClients(backup) Agent configuration backup fails {clientName} configuration backup verification failed; no successful configuration was reported. Exposes machine client key and obscures no-write guarantee Agent configuration could not be backed up, so no change was made. Check file permissions, then rerun setup. implemented focused assertions passed
45 33 src/application/Setup.ts:configureDetectedClients(write) Agent configuration write fails {clientName} configuration write verification failed; no successful configuration was reported. Exposes machine client key and gives no recovery Agent configuration could not be updated. Check file permissions, then rerun setup. implemented focused assertions passed
46 34 src/application/Setup.ts:configureDetectedClients(readback) Written agent configuration cannot be verified {clientName} configuration readback verification failed; no successful configuration was reported. Exposes machine client key and omits backup recovery Agent configuration could not be verified after writing. Repair the configuration file or restore its `.rea.backup`, then rerun setup. Agent configuration could not be verified after writing. The original was restored and its backup at {backupPath} was retained. Repair the configuration file or restore that backup, then rerun setup. Without a backup: Agent configuration could not be verified. Repair the configuration file, then rerun setup. implemented focused assertions passed
47 35 src/application/Setup.ts:installSkill Skill install or semantic readback fails Agent skill installation or readback failed. No useful recovery action REA analysis skill could not be installed or verified. Check permissions for `~/.agents/skills`, then rerun setup. implemented focused assertions passed
48 36 src/main.ts:serveStdio.onerror Established MCP client connection reports an error MCP stdio transport error Exposes transport implementation and gives no recovery REA lost its MCP client connection. Restart REA from your MCP client. implemented verified by injected transport onerror runtime test
49 37 src/main.ts:serveStdio throw MCP connection cannot be created Failed to start MCP stdio transport Exposes transport implementation and gives no recovery REA could not start its MCP connection. Restart REA from your MCP client; run `rea doctor` if it fails again. implemented verified by injected transport startup-throw runtime test
55 43 src/application/Uninstall.ts:removeClient parse Agent configuration is malformed Malformed configuration was preserved without mutation. Does not name required repair format or retry Configuration is not valid JSON/TOML and was not changed. Repair it, then rerun uninstall. implemented focused JSON assertion passed; TOML path uses same branch
56 44 src/application/Uninstall.ts:removeClient backup Configuration backup copy fails Configuration backup failed; no mutation was attempted. No recovery step Configuration could not be backed up, so no change was made. Check file permissions, then rerun uninstall. implemented verified by injected filesystem backup failure test
57 45 src/application/Uninstall.ts:removeClient update Write or semantic readback fails Configuration update failed; the original was restored and {backupPath} was retained. Exposes local path and lacks clear next sequence Report whether the original was restored; if restoration also fails, direct the user to the retained backup. implemented verified by injected update and restoration failure tests
58 45.5 src/application/Uninstall.ts:removeClient restore Configuration update fails and restoring the original also fails Configuration could not be updated or restored. Restore its `.rea.backup` manually, then rerun uninstall. Configuration could not be updated or restored. Restore the backup at {backupPath} manually, then rerun uninstall. High risk; persistent configuration may be damaged, but retained backup is authoritative Keep new recovery copy implemented verified by injected restoration failure test
59 46 src/application/Uninstall.ts:removeManagedPath symlink Managed cache/state/skill path is a symbolic link Refused to follow symlink {path}. Exposes local path and does not explain manual safety check REA did not remove this item because its managed path is a symbolic link. Verify the link target before removing it manually. implemented focused assertion passed
60 47 src/application/Uninstall.ts:removeManagedPath failure Managed item removal fails {path} could not be removed. Exposes local path and gives no recovery This item could not be removed. Check file permissions, then rerun uninstall. implemented verified by injected managed-path removal failure test
61 48 src/application/DoctorDiagnostics.ts:node Runtime version is outside supported ranges Install Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+. Low risk; exact dependency recovery Keep current copy unchanged verified by doctor unit test
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,592 @@
# Electron IPC listener verification, 2026-10-16
[#4390](https://github.com/morluto/rea/issues/4390), existing
[#4389](https://github.com/morluto/rea/pull/4389), umbrella
[#4027](https://github.com/morluto/rea/issues/4027).
All 73 IPC boundary regressions pass, alongside shared projection/actions and
converted MCP schema checks. Real draw.io operation phases succeed.
Every real capture still **fails overall provider cleanup**. These are distinct
results; neither completed-task acceleration nor token savings is established.
## Producing boundary and regressions
The existing PR `efd452948` was integrated with main `180ba03bf` in `5c3b02468`.
It fixes removal through the caller's original function and duplicate recording
of `once` invocations. Independent review found two further producing-boundary
problems: wrapper recognition evaluated caller `.listener` getters, and a copied
`removeAllListeners` snapshot loop differed from native live-array traversal when
removal observers prepended a listener.
Both new differential cases failed on that implementation (14 passed, two
failed). Fix `3c330f259` delegates traversal to Node, translating only owned `on`
wrappers at `removeListener`, and preserves the `off` alias. Descriptor reads
avoid evaluating getters. A further Proxy case exposed that descriptor reflection
can itself run a trap (16 passed, one failed); `2fb7f5523` uses Node's Proxy
identity first and removes the superseded catch. Those initial **17 cases** pass;
the later decorator finding and final 23-case validation are recorded below.
The native EventEmitter controls are semantic regressions, not real-app proof.
The change uses weak wrapper keys, removes the copied traversal and its snapshot
allocation, and adds no tool, cache, schema or normal reply field. Related
compiled provider/service modules and catalog metadata were byte-identical to
baseline main `180ba03bf` at the initial source freeze. This does not prove
arbitrary function introspection equivalence: custom
`.listener` properties and raw wrapper re-registration retain the limitations
documented in [Electron observation](../electron-observation.md).
## Unchanged real application and observed scenario
Official [draw.io Desktop 32.4.1](https://github.com/jgraph/drawio-desktop/releases/tag/v32.4.1)
AppImage SHA-256 is
`c687a9f1ddcaa146b78547452d264c03c2afcf125639cba5deeb65f3e9554c9e`;
both the release asset digest and published checksum file match. Its unchanged
62,941,673-byte ASAR is
`e64b9650c0b8241ca9a7d33b56386a385bceee63270cec701d3a467347b697e6`.
Complete accompanying resources were extracted; no application source was edited.
Public fuse inspection reports the official executable's Inspector fuse disabled.
No vendor Inspector attempt was made. The following trials explicitly select
generic Electron **44.7.0**, Xvfb and fresh profiles; they establish no packaged
runtime compatibility.
The real renderer initially displays an **Invalid file data** dialog. A public
CLI startup smoke and independent DOM/argv probes preserve this failure, tracked
separately in [#4494](https://github.com/morluto/rea/issues/4494). Its exact cause
is not proven. Two attempts to click a palette item without handling the dialog
timed out and were cancelled; they remain failed diagnostics. An initial helper
also incorrectly queried `application.process()` after transport closure; the
corrected helper retains the original ChildProcess handle instead.
The final scenario uses declared REA clicks to dismiss that actual error and
insert a visible palette item, then waits for rendering. Independent DOM reads
confirm SVG elements **0 → 2**, disappearance of the error and the real unsaved
changes indicator. The verifier exercises the real app callbacks and channels;
the production hook wraps their registration as described above.
The source also registers `app-load-finished` through both `once` and `on`;
recording its once callback twice adds a spurious event.
## Initial fixed-source ABBA observations
Verifier/source revision is `d02cc934c`; its hashes were recorded before all four
trials. Baseline is main `180ba03bf`. The same script, runtime, artifact and
declared actions run in the order baseline, candidate, candidate, baseline.
| Variant/run | IPC events | `app-load-finished` | SVG change | Pipeline wall s | Family CPU s | Peak cgroup bytes |
| ----------- | ---------- | ------------------- | ---------- | --------------- | ------------ | ----------------- |
| Baseline 1 | 8 | 3 | 0 → 2 | 11.084 | 6.270053 | 560,615,424 |
| Candidate 1 | 7 | 2 | 0 → 2 | 11.135 | 6.395461 | 550,383,616 |
| Candidate 2 | 7 | 2 | 0 → 2 | 11.898 | 6.907123 | 525,631,488 |
| Baseline 2 | 8 | 3 | 0 → 2 | 11.469 | 6.526674 | 558,456,832 |
Every declared action completed, every insertion oracle passed, and no verifier
deadline fired. Every provider returned `cleanup_failed`; the captures remain
partial/failed, including possible dirty-editor teardown and the known Linux
ownership gap [#4282](https://github.com/morluto/rea/issues/4282). Baseline 2 also
reports Xvfb temporary-directory cleanup failure (payload exit 5). Its directory
identity was not captured; no broad path-based deletion was attempted.
Runs are serial on CPU 10 with one-CPU quota, 8 GiB hard memory, 6 GiB early
stop, no swap and 192 tasks. Counters cover the cgroup family at payload exit
while the observer remains alive; they are **not final whole-scope counters**.
Memory includes file cache, not RSS. Some scopes still contain Xvfb at that
point; all recorded scopes were later independently confirmed inactive/dead.
External recovery does not establish provider cleanup.
OS/build caches are uncontrolled; these are first/repeat fresh-process runs,
not verified cold/warm system pairs. Two samples per variant and a cleanup-bound
failed workflow cannot establish a speedup. Token billing, peak agent context,
complete schema/call costs and actual MCP-wire parity are unmeasured. Fewer
events alone do not establish lower token cost.
Full static checks passed (two tasks, no cache hits): 89.441 s wall,
88.630288 family CPU s and 4,337,172,480 peak cgroup bytes, with no OOM.
Documentation checks also passed (6 tasks plus five README consistency cases).
The [selected JSON record](electron-ipc-listeners-2026-10-16.json) retains exact
source/input identities, failed attempts, coverage, IPC and resource receipts.
Historical diagnostic helper hashes were not captured and remain explicitly
unknown; the frozen hash is not assigned to them retrospectively.
## Reproduction
Build both pinned checkouts. Under the existing resource observer and a private
Xvfb instance, use the committed helper for each runtime root in ABBA order:
```sh
node experiments/analysis-efficiency/drawio-electron-lifecycle.mjs \
--runtime-root /absolute/pinned-checkout \
--executable /absolute/electron-44.7.0/electron \
--application /absolute/drawio/resources/app.asar \
--profile-root /absolute/temporary-profiles --dismiss-error \
--selector '.geSidebar a.geItem:visible >> nth=0'
```
The helper returns failure on incomplete provider cleanup. This shared-service
probe does not establish successful public CLI/MCP workflows. App images,
extractions/profiles and full Result files are regenerable and excluded from Git.
## Review follow-up: normalization before app decorators
Fresh review of `f3bb9b9ed` found that translation inside a patched
`removeListener` happened after an application's outer decorator had already
seen the recording wrapper. Two native controls reproduced this with stable and
replaced decorators (17 passed, two failed).
`c8c494b5c` delegates the original Node traversal through a private forwarding
receiver. It shares the real emitter's live state and translates owned `on`
entries **before** invoking the current application removal method. App methods
and getters receive the real emitter; read-only method properties remain valid.
Native once entries pass unchanged. The public `removeListener`/`off` patches
are removed. Five decorator variants pass: stable, replaced, read-only, getter
and Proxy.
An additional mutation case proved that reading a changed public `.listener`
getter before delegation also changed deletion timing (22 passed, one failed).
Final source `5a094a0df` retains canonical removal targets in the weak owner
registry. Only unchanged data links are translated; changed getters run when
Node performs removal, preserving the exception and state transition. All
**23 cases** pass, including the original retention/fatal-error coverage.
The same frozen draw.io verifier was reused. The follow-up ABBA attempt ran
its first baseline, then the resource launcher refused the other three requests
because 8 GiB plus its 4 GiB host reserve was unavailable. Those are **not
executed**, with no performance measurements. The final source was separately
admitted and completed dialog recovery and insertion with seven IPC events;
overall cleanup still failed, and Xvfb teardown also returned an error. No new
completed-task speedup or cold/warm pair is claimed.
Final static checks passed: two uncached tasks, 135.173 s wall, 133.786166 family
CPU s and 4,274,634,752 peak cgroup bytes, no OOM. The JSON record appends exact
source pins, denial logs, final real-app/public cleanup diagnostics and checks
under `review_followup`; the initial records were not overwritten.
## Callback invocation and remaining raw-metadata limitation
Source `514b73b78` invokes `handle` callbacks through captured `Reflect.apply`,
preserving the receiver and actual arguments without reading an app-defined
`.call` property. The added direct-call control and hook case pass; all **24
IPC regressions** pass (3.661 s wall, 3.404 family CPU s, 318,291,968 peak cgroup
bytes, no OOM). This is a Node boundary check, not a real Electron handle claim.
An initial three-case experiment incorrectly assumed native EventEmitter avoided
callable method-property reads. Its `on` and `once` controls themselves failed;
they do not demonstrate hook regressions. The attempted intrinsic event-call
change was reverted and those invalid cases removed. Both failed runs remain
in `callable_followup`; no successful result is assigned to them.
The same unchanged draw.io verifier at this source completes actual dialog
recovery and shape insertion, with seven IPC events, SVG elements 0 → 2 and an
unsaved-changes indicator. Overall provider cleanup still fails. Pipeline wall
time is 12.610 s, family CPU 7.062834 s and peak cgroup memory 634,187,776 bytes;
there is no paired performance or token claim. All recorded scopes were later
independently inactive/dead, which does not establish provider cleanup.
Fresh review also found a distinct remaining metadata case: changing a raw
recording wrapper's writable `.listener` property can make an app decorator
reject removal. A production-hook/native-Node probe reproduces this (plain:
removed; hook: decorator throws and one listener remains). Exact reproduction
and the ownership conflict are tracked in [#4513](https://github.com/morluto/rea/issues/4513).
No affected real app has been identified. Removing the descriptor guard alone
does not make native removal match the mutated wrapper; temporarily rewriting
public app metadata would add further visible mutations. This limitation is
explicit in the observation guide and is not represented as fixed by this PR.
Current-source static checks also pass: two uncached tasks, 92.912 s wall,
92.031383 family CPU s and 4,340,375,552 peak cgroup bytes, no OOM.
## Native single/array dispatch follow-up
Fresh review of `6fc2d2769` identified that the earlier native controls covered
only one listener. Node 24.18.0 reads `.apply` during single-listener dispatch,
but invokes each callback intrinsically during array dispatch. Native `once`
uses `.call` for zero arguments and `.apply` otherwise. Unconditional `.apply`
in the observation helper changed two of those paths: a multi-listener callback
could throw before later callbacks ran, and zero-argument once read the wrong
property. Four explicit native controls reproduce two failures (26 passed,
two failed), rather than assuming all callable-property reads should disappear.
Source `e8d7e8f20` keeps one observation boundary. An `on` recording wrapper
delegates direct calls intrinsically and forwards native `.apply` calls through
its own nonenumerable method. Node selects the route; the hook does not inspect
or copy listener storage. `once` preserves its native zero/nonzero-argument
invocation. All **28 cases** pass, including the prior callback identity,
retention, reentrant-removal and fatal-error coverage. `handle` remains intrinsic.
The unchanged real draw.io verifier again completes dialog recovery and shape
insertion, with seven IPC events, SVG elements 0 → 2 and the unsaved indicator.
Overall provider cleanup still fails; no deadline fires. Its 12.290 s pipeline
run is a validation observation, not a paired performance comparison. Exact
source hashes, failures, resource measurements and post-scope states are appended
under `native_dispatch_followup`; all previous records remain unchanged.
The separate raw metadata limitation [#4513](https://github.com/morluto/rea/issues/4513)
remains open and is not represented as fixed.
Final-source static checks pass: two uncached tasks, 171.430 s wall,
169.139239 family CPU s and 4,091,392,000 peak cgroup bytes, no OOM.
## Argument-list ownership follow-up
Review of `23700fd2c` found two related representation errors. A raw callback's
`.apply` accepts Arguments objects, other array-like objects, null or omitted
argument lists; the recording helper assumed an Array. Native once also passes
its actual Arguments object to a callback-defined `.apply`, whereas the hook
passed a rest Array. Seven explicit native controls reproduce six failures
(29 passed, six failed of 35).
Source `a82d935ba` binds captured native Function apply to the method-forwarding
function. JavaScript itself converts argument lists, treats null/undefined as
empty and rejects invalid primitives before recording reads values. The
array-like getter control is read exactly once. Once retains its actual
Arguments object for invocation; recording alone takes an array slice. The
captured slice helper is bound once, avoiding an extra helper-argument array
on each invocation. No custom array-like parser or exception repair is added.
All **35 cases** pass. The same frozen real draw.io verifier again completes
dialog recovery and shape insertion with seven IPC events; no deadline fires,
and overall provider cleanup still fails. Exact source identities, both failed
boundaries, intermediate/final tests, resource costs and post-scope states are
appended under `argument_list_followup`; previous records remain unchanged.
Current-source static checks pass: two uncached tasks, 122.454 s wall,
121.287057 family CPU s and 4,185,915,392 peak cgroup bytes, no OOM.
The unchanged ASAR digest was checked again after the real-app trial.
## Owned metadata and successful-Evidence verifier follow-up
Fresh review of `41a513d99` found that `on` wrapper metadata used strict
assignment. Native on does not write such metadata, so inherited Function or
Object prototype setters/read-only fields could prevent registration only under
capture. Four native controls fail before `5dcf79f29` (35 passed, four failed).
The recording wrapper now defines its own data property directly; native once
assignment behavior is retained. All **39 cases** pass.
The verifier also incorrectly treated a successful Evidence envelope as a raw
capture. Real cleanup failures had exercised only the separate partial path.
`4a9a01f7a` reads successful `value.normalized_result` and preserves failed
`error.partialObservation.capture`. The new
`experiments/analysis-efficiency/drawio-electron-lifecycle-oracles.py` exercises
the actual verifier with controlled success, partial cleanup, action failure,
insertion failure and absent capture. The old verifier rejects valid successful
Evidence before producing a summary; all five corrected controls pass.
**These are controlled reply/exit checks, not real-provider evidence.**
The corrected verifier was separately frozen before a real unchanged draw.io
trial. Actual dialog recovery and shape insertion again complete with seven IPC
events, no deadline and failed overall provider cleanup (pipeline 12.777 s).
Success with real provider cleanup remains unverified. Earlier trials retain
their earlier verifier hashes; the corrected hash is not assigned to them.
Exact source pins, failed controls, resource costs and post-scope states are
appended under `owned_metadata_and_verifier_followup` without replacing history.
Final-source static checks pass: two uncached tasks, 99.992 s wall,
98.956939 family CPU s and 4,237,795,328 peak cgroup bytes, no OOM.
## Species-free recording and snapshot ownership
Fresh review of `e2cf2d89b` found that slicing a rest Array invokes application
`Array[Symbol.species]` before dispatch. The first control reproduced failed on
dispatch (40 passed, one failed); once dispatch already used Arguments.
Extending the check to retrieve the retained snapshot while the throwing getter
remains installed exposed the same representation problem in snapshot maps
(39 passed, two failed).
Source `3141ac3f4` records from actual invocation frames while preserving native
callback argument-list types: the method-based on path still passes an Array,
and once passes Arguments. Group validation uses the existing recording loops
without a filtered intermediate Array. Captured `Array.from` produces snapshots
without species lookup, reading dropped-counter Maps directly rather than
spreading them into another Array first. The initial **41 cases** pass, including both
live snapshots under the throwing getter with zero getter reads.
The frozen-source real unchanged draw.io operation again completes recovery and
insertion with seven IPC events, no deadline and failed overall cleanup
(pipeline 12.779 s). This does not establish that draw.io customizes species.
The corrected verifier and its five previously passing controlled oracles are
byte-identical to `4a9a01f7a`, so those unchanged oracle sources were reused.
Exact hashes, failures, resource receipts and scope states are appended under
`array_species_followup`; earlier records remain unchanged. There is no new
completed-task performance or token/context claim.
The native multiple-listener route was then included in the same live-species
control: all **42 cases** pass. Only test coverage expanded; production hook
and verifier bytes remain identical to `3141ac3f4`. Exact validation identities
and the additional result are recorded under `all_native_dispatch_paths`.
A fresh-profile real negative control omits `--selector` while retaining dialog
recovery and the same 500 ms wait. SVG elements remain **0 → 0** and unsaved
changes remain false; insertion gives **0 → 2** and unsaved true. This supports
the operation oracle rather than attributing startup rendering to the click.
The control also returns seven IPC events and failed provider cleanup. These
are different tasks, not a performance pair or a model-cell-count proof.
Final 42-case source validation includes two uncached static tasks passing in
94.234 s wall, 93.308800 family CPU s and 4,406,538,240 peak cgroup bytes,
with no OOM. These are verification costs, not an application speedup.
## Opaque apply inputs and iterator-free snapshots
Fresh review of `84d9cf5db` found two producing-boundary regressions: native
array-like conversion replaced the argument-list object before a custom
listener `.apply` received it, and `Array.from` snapshots consulted application
iterators on retained arrays/Maps. Four exact-input native controls and two
snapshot controls fail before correction (42 passed, six failed of 48).
Source `e442ca27f` forwards custom `.apply` the original receiver and method
arguments without interpreting its opaque list. Custom apply may transform
arguments or skip callback execution, so the hook emits a bounded
`ipc-observation-gap` timeline record with the known registration channel. It
does not fabricate a main-event invocation, argument shapes or callback result.
The shared result projection retains explicit uncertainty and counts dropped
gaps through existing retention. The canonical timeline enum adds that kind;
the captured-IPC subset does not. Native apply still performs its own conversion
before an actual callback frame is observed.
Snapshots now copy arrays through private null-prototype indexed views and
counters through captured native Map traversal. Neither retained buffers nor
counter Maps are exposed to application iterators; returned records and shape
arrays remain independent copies. No cache, tool or normal-dispatch metadata
is added. The raw-wrapper metadata limitation in #4513 remains unfixed.
The focused hook/projection/actions gate passes **68 cases** (48 + 4 + 16).
The separate MCP/schema gate passes **23 cases**: an actual SDK client receives
the gap alongside ordinary IPC Evidence, and Ajv2020 validates the advertised
converted output; all canonical tool schemas also pass their existing checks.
These tests use controlled providers and establish no real-provider success.
The verifier and its five controlled exit/summary oracles are hash-identical to
the earlier passing source and are reused without another redundant run.
## Unrelated CI baseline failure
CI run `38935598884` tested synthetic merge `eedae92d406015895a6d0eced5035da1b9ead07a`
(head `84d9cf5db`, base `9d940c2f2`). Two firmware cleanup tests failed; another
shard was cancelled by fail-fast. A fresh, unmodified detached checkout of that
exact base reproduces the same failures: **33 passed, two failed** in
`tests/boundary/firmware/firmwareIntegration.test.ts`. Evidence is posted on
[the PR](https://github.com/morluto/rea/pull/4389#issuecomment-6221054273) and
[existing #4475](https://github.com/morluto/rea/issues/4475#issuecomment-6221054775).
This establishes that these failures predate the Electron changes; it does not
resolve whether the firmware producer or its expectations need correction. No
firmware code or assertions are changed here, and required CI is not bypassed.
The first final-source real request fails before application launch because the
previous `/tmp` runtime is absent. It is retained as an input-resolution failure,
not a provider measurement. The cached Electron 44.7.0 archive is verified against
the official release SHASUMS256 and extracted into a new owned task directory;
its executable/archive digests are pinned before the next request. No target
source, vendor fuse or scenario success requirement is changed.
The recovered-runtime real operation completes all three declared actions:
SVG elements **0 → 2**, unsaved changes true, seven IPC events, no deadline.
Overall provider cleanup still fails. Current-source static validation passes
two uncached tasks in **94.646 s wall, 93.878644 family CPU s and 4,300,570,624
peak cgroup bytes**, no OOM. The initial static attempt's test-only SDK/Ajv type
error is retained; parsing that boundary fixes it, and 12 affected client and
projection cases pass again. `opaque_apply_and_iterator_followup` preserves all
source pins, failures, resource receipts and independently checked scope states
without replacing earlier measurements. Draw.io does not establish that real
applications exercise the synthetic custom-apply/iterator cases.
The final documentation gate also passes six tasks and five README consistency
cases. Source and unchanged ASAR hashes are checked again after validation.
## Borrowed apply receivers
Fresh review of `7541a900c` supplied the borrowed custom-method case
`raw.apply.call(g, emitter, list)`: the plain emitter runs `g`, while the hook
hard-coded its registered callback as the method receiver. The same hard-coded
receiver also affected borrowed native apply. Both native differential controls
fail before correction (48 passed, two failed of 50).
Source `da04465fe` maps only the hook wrapper itself to the original listener
when forwarding custom apply; other method receivers pass unchanged. Native
apply uses its actual receiver, preserving both ordinary wrapper recording and
borrowed-call dispatch. The hook does not fabricate a registered callback
invocation when another callable is selected. The method-forwarding boundary is
corrected directly; no new helper, state, tool or schema is added. All **70**
hook/projection/actions cases pass (50 + 4 + 16). Shared schema/client, projection
and verifier/oracle sources are byte-identical to the prior passing versions,
so their checks are reused by verified hash.
CI run `38943919946` is confirmed by the successful build checkout log and Git
parents to use merge `98c14dfc01fe4698a50eedc8b1cd1ac9cd03267d`
(head `7541a900c`, main `4838f4449`). Its fifth shard fails the same two firmware
cleanup tests as the unmodified `9d940c2f2` base control. The relevant firmware
source/test and package paths have no changes between these main revisions.
The REST PR `base.sha` remains stale at `2c7583599`; it is not used as the CI
identity. No firmware code/assertion or required-check bypass is included.
The frozen-source unchanged draw.io operation completes recovery/insertion
again with seven IPC events, SVG **0 → 2**, unsaved changes true and failed
overall provider cleanup. Pipeline: **12.038 s wall, 6.662295 family CPU s,
598,835,200 peak cgroup bytes**. This remains a partial workflow, not a speedup
claim. Final static checks pass two uncached tasks in **108.067 s wall,
106.996233 family CPU s and 4,430,647,296 peak cgroup bytes**, no OOM.
All prior measurements remain unchanged under `borrowed_apply_followup`.
The final documentation gate also passes six tasks and five README consistency
cases. Current source digests are checked again after validation.
## Listener-owner intrinsic lifetime
Fresh review of `f5b489c1b` found that late application replacement of
`Object.defineProperty` prevents the hook's new wrapper properties from being
created, while a plain native emitter still registers/invokes its callbacks.
Inspection of this same new registration/removal owner found its other late
property/constructor lookups. Seven independent native controls replace
`Object.defineProperty`, `Object.getOwnPropertyDescriptor`, `Object.create`,
`Reflect.get`, `Reflect.set`, global `Set` or global `Proxy`; each fails before
correction (50 passed, seven failed of 57). The controls include both on/once
execution and complete removal, with globals restored after the scenario.
Source `a9c5611d0` captures the owner's descriptor/property operations and private
constructors before application startup. The private receiver uses a direct
null-prototype object literal instead of looking up Object.create. All **77**
hook/projection/actions cases pass (57 + 4 + 16). Application-owned callback
methods and actual receivers retain their existing semantics. No tool, schema,
state, target authority or general-purpose hardening layer is introduced; this
is not complete JavaScript introspection equivalence. Unchanged schema/client,
projection and verifier/oracle checks are reused by verified source hashes.
The preceding CI run `38946503183` tests confirmed merge
`fcd7a9d92a2e64d6eb96d4fabfed7fc7f44b0d45` (head `f5b489c1b`, main `4838f4449`).
Its 36 jobs finish with 29 successes, the same two firmware failures causing
three failed jobs including summaries, and four fail-fast cancellations. The
fifth-shard log again names the two cases reproduced on untouched main. No
firmware code/assertions or required checks are altered.
Frozen-source real unchanged draw.io actions complete again with seven IPC
events, SVG **0 → 2**, unsaved changes true, no deadline and failed overall
provider cleanup. Pipeline wall is **11.429 s**. Observed payload-exit cgroup
counters are 6.441029 CPU s and 550,363,136 peak bytes, but the observer still
sees one additional scope descendant: **family resource observation is
unverified**, not a complete-family performance measurement. The scope is later
inactive/dead after external recovery; this does not verify provider cleanup.
No identity or cleanup cause is inferred from the remaining PID.
Final static checks pass two uncached tasks in **90.980 s wall, 90.119351 family
CPU s and 4,338,749,440 peak cgroup bytes**, no OOM. Source identities, all seven
failures and resource limitations are appended under `listener_intrinsics_followup`
without replacing any previous measurements.
The final docs gate passes six tasks and five README consistency cases; source
digests are checked again after validation.
## Private collection method lifetime
Fresh review of `e6c373d0` found that replacing `WeakMap.prototype.has` after
hook initialization still intercepts its private wrapper registry. Inspecting
the same owner found analogous get/set and visited-Set has/add lookups. All five
independent native controls fail before correction (57 passed, five failed of
62); plain EventEmitter registration, on/once calls and complete removal succeed.
Source `774ed6aa6` binds these private WeakMap/Set methods before application
startup and reuses captured Set membership for event-family classification.
One initialization-only binding helper replaces duplicated binding construction
for existing Map traversal/argument slicing too. It adds no helper argument
arrays per invocation, cache, schema, tool or target authority. Application-owned
callback methods/receivers remain dynamic. All **82** hook/projection/actions
cases pass (62 + 4 + 16). The previous seven global-binding controls also remain
green, and unchanged schema/client/projection/verifier/oracle checks are reused
by verified source hashes. Arbitrary introspection and #4513 remain unsupported.
Frozen-source unchanged draw.io actions complete again with seven IPC events,
SVG **0 → 2**, unsaved changes true and failed overall provider cleanup. Pipeline
wall **11.238 s**; observed payload-exit cgroup CPU **6.285321 s**, peak
**556,285,952 bytes**. Another scope descendant remains at payload exit, so complete-family resource observation is unverified. Later external scope teardown does not prove provider cleanup.
Final static checks pass two uncached tasks in **92.999 s wall, 92.227652 family
CPU s and 4,371,951,616 peak cgroup bytes**, no OOM. Preceding CI38949129899 checks
confirmed e6c3 + main4838, repeats the same two firmware failures and finishes
31 successful /3 failed jobs including summaries /2 fail-fast cancellations.
Sources, failures and resource limitations are appended under
`private_collection_methods_followup`; prior fields remain unchanged.
The final docs gate passes six tasks and five README consistency cases; source
digests are verified again afterward.
## Unknown listener-chain ownership
Fresh review of `0b4f2e81` found that the bounded Boolean chain test treats a
still-unexamined function as fresh when 17 app decorators hide an owned once
wrapper. Two emits then produce three invocation records for one actual once
callback. The same unknown-as-fresh error affects accessor/Proxy links. Four
native controls fail before correction (63 passed, four failed of 67); the
16-decorator boundary already passes. A deep ordinary on control confirms
missing ownership cannot justify fabricated captured IPC either.
Source `ef46b8f0b` classifies metadata as owned, unowned or unknown. Bounded-walk
exhaustion, accessor links and Proxies preserve native registration without
adding another recording wrapper. Successful unknown registrations retain a
bounded `listener-chain` gap; this is not proof of IPC delivery. An existing
inner wrapper may still report its actual once invocation. Shared native
data-descriptor parsing preserves accessor uncertainty at recognition/removal
and avoids inherited descriptor-value interpretation. The superseded Boolean
classification is removed.
The shared projection explains both gap boundaries and retention/coverage, and
an actual MCP SDK client receives custom-apply and listener-chain gaps alongside
ordinary IPC Evidence, validated by its converted advertised schema. All **96**
hook/projection/actions/MCP cases pass (67 + 5 + 16 + 8). A test-only lint length
failure is corrected by generating repeated fixture rows from one table without
relaxing rules; all **13** affected client/projection cases pass again. The
canonical schema and unchanged verifier/five controlled oracle sources retain
their earlier passing checks by verified hashes; the changed projection/client
are freshly tested rather than claimed unchanged.
Prior CI run `38951509761` uses confirmed merge
`fa090a6bde3a5dadeb71066824477292520fb43b` (0b4f + mainf31b231a). All **36 jobs
succeed**. The earlier unrelated firmware failures do not recur in this
combination; their cause is not inferred or claimed fixed here. Current-source
CI/review remain required. No firmware code/assertions were modified.
Frozen-source unchanged draw.io recovery/insertion again completes with seven
IPC events, SVG **0 → 2**, unsaved true, no deadline and failed overall provider
cleanup. Pipeline wall **11.163 s**; observed payload-exit cgroup CPU **6.203177 s**,
peak **547,418,112 bytes**. An extra scope descendant leaves complete-family
resource observation unverified; later inactive/dead scope after external
recovery does not establish provider cleanup.
Final static checks pass two uncached tasks in **88.383 s wall, 87.726306 family
CPU s and 4,399,632,384 peak cgroup bytes**, no OOM. All source identities, failed
controls, corrected fixture-only lint failure and resource caveats are appended
under `chain_ownership_followup`; historical fields are unchanged.
Final docs validation passes six tasks and five README consistency cases;
current source digests are verified again afterward.
## Inherited listener-link coverage
Fresh review of `ae2cd8d4` found that a missing own descriptor is not absence:
an app decorator can inherit its data link to an owned once wrapper. Native
removal follows that link and removes the decorator; the hook hid it under
another recorder, left a listener installed and produced three invocation
records for one callback. Five native inherited-link cases fail before correction
(68 passed, five failed of 73): data/getter/Proxy prototypes and prototype
depths16/17. A late Object.getPrototypeOf replacement control already passes.
Source `e22ce9aaf` completes bounded native prototype lookup at the descriptor
owner, capturing getPrototypeOf before startup. Data links retain their actual
source value. A setter-only property has a known undefined read that shadows
ancestors; a getter, Proxy or incomplete walk remains unknown. Missing metadata becomes absent only after the prototype chain ends; found
properties retain native read/shadowing semantics. Registration and removal consume
the same parsed descriptor, and redundant outer Proxy interpretation is removed.
Neither existing resource bound is raised. All **102** hook/projection/actions/
MCP cases pass (73 + 5 + 16 + 8). Projection/client are freshly tested unchanged
sources; canonical schema and verifier/oracles retain prior checks by hash.
Prior CI run38955237255 tests confirmed merge9c0ceb152c902c1339806608f8b4a010c3d22fc6
(ae2c + main8116dc06). All36 jobs succeed; current-head CI/review remain required.
Frozen-source unchanged draw.io recovery/insertion again completes with seven
IPC events, SVG **0 → 2**, unsaved true and failed overall provider cleanup.
Pipeline: **11.264 s wall /6.376574 family CPU s /557,068,288 peak cgroup bytes**.
Payload-exit observation is verified at that phase while the observer lives;
these are not final whole-scope counters or provider cleanup proof.
Final static checks pass two uncached tasks in **90.797 s wall, 90.096799 family
CPU s and 4,335,267,840 peak cgroup bytes**, no OOM. All source identities, native
failures and current results are appended under `inherited_listener_links_followup`;
previous fields and earlier resource caveats remain unchanged.
Final docs validation passes six tasks and five README consistency cases;
current source digests are verified again afterward.
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,142 @@
# Native Electron once dispatch — 2026-10-16
#4593 was reported by review of #4562, but its source was already in main from
#4389. The initial report was explicitly source-only; subsequent paired controls
confirmed the bug. This change delegates `once` creation, firing and callable
selection to the actual runtime and observes only established callback entry.
CLI and MCP continue to share the same hook, event model, projection and retained
Evidence. No new tool, schema, execution authority or analysis cache is introduced.
## Paired native controls
The baseline is `709b5cbdc15aa757f89c38ecd12e5a47710747a6`. Final source is
`4ebd0f60e860daa57a7033c6e77ea7f5f4b8ac97`, including the two review-gap fixes below. Earlier source cohort
`c0b52b5408fa97013798e6ecee885ccac08e7356` contains the fix; the final cleanup
commit is `e60c4a1098321ed2a7f564ae35909a31d707694f`, which removes the obsolete fifth helper argument and
unreachable copied call/apply dispatch branches after inspecting every caller.
The pre-review gap-owner follow-up is `7f05f476c52498adba41d943ead1a86e7216ca76`. The adjacent JSON preserves
source hashes and separate measurement cohorts, including the initial 93-row prefix.
The final review JSONL contains 117 paired rows (39 inputs × 3 runtimes). The
earlier 96-row JSONL remains unchanged, including its initial 93-row prefix. Both
comparisons have identical native
control facts in each before/after pair. Regenerable binaries, apps, profiles
and multi-MB raw captures are excluded from Git.
| Actual engine | Cases | Baseline behavioral differences | Fixed differences |
| ---------------------------------------------------------- | ----: | ------------------------------: | ----------------: |
| Node 24.18.0 (pinned) | 39 | 4 | 0 |
| Official Node 24.21.0 | 39 | 16 | 0 |
| Generic Electron 44.7.0 embedded Node 24.21.0, run as Node | 39 | 16 | 0 |
The original 21-case reproduction found 0/9/9 differences respectively. The
pre-review 32-case cohort found 1/13/13. The final 39-case suite adds readonly
callback methods, emitter method invariants, native metadata writes, failure
recovery and non-callable callback methods. The extra pinned-runtime difference is registration
metadata ordering. On the newer runtimes, opaque callback methods can suppress
callbacks under the old hook although native dispatch executes them. The pinned
runtime also catches false invocation records when an opaque method legitimately
skips the callback. The added regression fails on the baseline for that false fact.
These are **controlled producer cases with a minimal Electron module**, including
the actual embedded Node binary, not a real GUI application's natural callback
loss. Native traces, receiver checks, failures and remaining listeners are compared
exactly. Expected property reads are derived from each runtime rather than copied
from one Node implementation. The custom-apply guard still checks its original
Arguments representation when the runtime actually invokes that method.
## Ownership and uncertainty
Native once wrappers keep their public callback identity and descriptor attributes.
The registration receiver forwards methods/getters to the original emitter and
supports readonly method descriptors. A private callable receives an actual
Arguments frame, avoiding application Array species/iterators when recording.
Metadata recognition and admission share one bounded descriptor lookup.
An application-selected opaque call/apply can bypass observation; a bounded
`custom-call`/`custom-apply` gap replaces invented invocation facts. Opaque native
metadata writes or registration lookups keep the exact original callback/native
operation and emit `once-metadata` gaps. The temporary unobserved scope is restored
even if registration throws. The 39-case suite checks this recovery, including a native alias that bypasses
patched `on`; the once owner retains the observation gap for that path. #4513 raw
recording-wrapper mutation remains explicitly unfixed.
Review found two missing failure facts: a decorator can register through an
unhooked alias and then throw, and a runtime can read a non-callable call/apply
property before dispatch fails. Both new regressions fail on the pre-review
source. The once owner now finalizes its missing gap in `finally`, after restoring
the scope; the callable metadata owner records a failed gap while preserving the
exact non-callable value. Error identity and native callback behavior are retained.
Two short-lived proxies are used for an observable once registration. Metadata
remains weakly retained, and existing event/serialization budgets remain in force.
The added registration cost was not isolated; no speed or token improvement is
claimed. The saved producer comparison is a correctness experiment, not a whole
application performance benchmark. Its runner reports differences instead of
exiting unsuccessfully for an expected bad baseline; the regression tests assert
that fixed behavior matches native behavior and observation claims are truthful.
## Real application verification
Unchanged Joplin Desktop 3.7.21 ASAR and explicit generic Electron 44.7.0 run the
shipped welcome note/1075×666 PNG → actual resource context menu → app bridge →
blocked reveal workflow. The image stays visible, page errors remain empty,
reveal events increase 0→1, and the shared retained capture contains the exact
event. Both staged hook files are hashed against the selected source before launch.
No synthetic application IPC channels or replacement app callbacks are added.
The argv shim is an explicit test adapter for the still-failing standard startup
#2974. **Overall provider cleanup remains failed (#4282)** and the verifier exits
nonzero. External scope teardown is not provider cleanup. The first frozen real
pipeline takes 17.680 s; observed payload-exit CPU 16.272793 s and peak 1,395,253,248 B
have an extra descendant, so complete-family observation is **unverified**. The
cleanup cohort is recorded separately. The pre-review 32-case source repeats the real
operation successfully, but overall cleanup still fails: 18.074 s pipeline, observed
CPU 17.011774 s / peak 1,422,839,808 B; complete-family measurement remains unverified. No completed public CLI/MCP workflow,
real Windows/macOS run, natural app callback-loss reproduction or full check:pr
is claimed.
The final review source repeats the same real operation: 18.353 s pipeline,
observed CPU 17.098599 s / peak 1,387,556,864 B; complete-family measurement remains
unverified and overall provider cleanup fails. An earlier trial failed to initialize
the display because the provider environment omits XAUTHORITY. That launch is
preserved separately; the fresh final trial uses local Xvfb with `-nolisten tcp -ac`,
not a production provider change.
Frozen local validation: 75 hook cases on both Node versions; 5 projection + 8 MCP
cases on the unchanged shared contracts; static checks pass (final 118.946 s wall / 117.752622 s CPU / 4,282,826,752 B
cgroup peak, verified payload-exit family observation). The pinned final run uses
`test:focused`; an attempted `test:local` invocation was rejected before tests ran
and is recorded as not executed. See JSON for exact
commands/cohorts, failed candidate attempts, source pins and measured resources.
Failures include the Array-species frame, readonly receiver invariants, metadata
setter identity, on-getter ordering, a lint/interoperability correction and helper
preparation failures. They are not relabeled as results from the final source.
All heavy work runs serially with CPU 10/quota 1, 8 GiB hard/6 GiB early/no swap/192 tasks.
Counters are captured at payload exit with the observer alive, not final whole-scope
counters; cgroup peak includes file cache. Each final Joplin run has a new private
configuration mount. Filesystem/runtime/build caches are warm; cold-system and
warm-repeat costs are unmeasured. Agent/billed tokens and public reply/context
costs are unmeasured. The broad internal error capture size is not an agent-token
measurement. All owned scopes are checked inactive/dead independently.
## Reproduce the producer comparison
Use the pinned project toolchain and explicitly provided alternate runtimes.
The alternate Node 24.21 archive is checked against [official checksums](https://nodejs.org/download/release/v24.21.0/SHASUMS256.txt);
its archive/binary hashes are recorded. No global toolchain is upgraded.
```sh
python3 experiments/analysis-efficiency/electron-once-runtimes.py \
--hook-root /absolute/checkout-to-measure \
--node /absolute/pinned-node \
--node /absolute/node24.21 \
--electron /absolute/generic-electron44.7 \
--output /absolute/owned-output
```
Run this under the recorded resource wrapper. For a baseline comparison, use a
clean checkout of 709b5cbd as hook-root while keeping the same current oracle/runner.
The runner retains full JSON and prints a compact summary. The real Joplin recipe
is in [the shell verifier record](electron-shell-sync-2026-10-16.md); select the
current runtime-root. That workflow must remain nonzero while cleanup fails.
@@ -0,0 +1,117 @@
{"runtime":0,"mode":"plain","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":0,"mode":"plain","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":0,"mode":"plain","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":0,"mode":"call-opaque","arguments":0,"native":{"body_calls":0,"correct_receiver":true,"trace":["opaque:call"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:call"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:call"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"custom-call","error":false}]}
{"runtime":0,"mode":"call-opaque","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":0,"mode":"call-opaque","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":0,"mode":"apply-opaque","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":0,"mode":"apply-opaque","arguments":1,"native":{"body_calls":0,"correct_receiver":true,"trace":["opaque:apply"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:apply"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:apply"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"custom-apply","error":false}]}
{"runtime":0,"mode":"apply-opaque","arguments":3,"native":{"body_calls":0,"correct_receiver":true,"trace":["opaque:apply"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:apply"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:apply"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"custom-apply","error":false}]}
{"runtime":0,"mode":"call-readonly","arguments":0,"native":{"body_calls":0,"correct_receiver":true,"trace":["opaque:call"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:call"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:call"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"custom-call","error":false}]}
{"runtime":0,"mode":"call-readonly","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":0,"mode":"call-readonly","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":0,"mode":"apply-readonly","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":0,"mode":"apply-readonly","arguments":1,"native":{"body_calls":0,"correct_receiver":true,"trace":["opaque:apply"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:apply"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:apply"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"custom-apply","error":false}]}
{"runtime":0,"mode":"apply-readonly","arguments":3,"native":{"body_calls":0,"correct_receiver":true,"trace":["opaque:apply"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:apply"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:apply"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"custom-apply","error":false}]}
{"runtime":0,"mode":"call-getter","arguments":0,"native":{"body_calls":0,"correct_receiver":true,"trace":["get:call","opaque:call"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:call","opaque:call"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:call","opaque:call"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"custom-call","error":false}]}
{"runtime":0,"mode":"call-getter","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":0,"mode":"call-getter","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":0,"mode":"apply-getter","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":0,"mode":"apply-getter","arguments":1,"native":{"body_calls":0,"correct_receiver":true,"trace":["get:apply","opaque:apply"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:apply","opaque:apply"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:apply","opaque:apply"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"custom-apply","error":false}]}
{"runtime":0,"mode":"apply-getter","arguments":3,"native":{"body_calls":0,"correct_receiver":true,"trace":["get:apply","opaque:apply"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:apply","opaque:apply"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:apply","opaque:apply"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"custom-apply","error":false}]}
{"runtime":0,"mode":"call-throwing-getter","arguments":0,"native":{"body_calls":0,"correct_receiver":true,"trace":["get:call"],"error":"Error: getter:call","listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:call"],"error":"Error: getter:call","listeners_after":0},"candidate_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:call"],"error":"Error: getter:call","listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":true}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"custom-call","error":true}]}
{"runtime":0,"mode":"call-throwing-getter","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":0,"mode":"call-throwing-getter","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":0,"mode":"apply-throwing-getter","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":0,"mode":"apply-throwing-getter","arguments":1,"native":{"body_calls":0,"correct_receiver":true,"trace":["get:apply"],"error":"Error: getter:apply","listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:apply"],"error":"Error: getter:apply","listeners_after":0},"candidate_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:apply"],"error":"Error: getter:apply","listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":true}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"custom-apply","error":true}]}
{"runtime":0,"mode":"apply-throwing-getter","arguments":3,"native":{"body_calls":0,"correct_receiver":true,"trace":["get:apply"],"error":"Error: getter:apply","listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:apply"],"error":"Error: getter:apply","listeners_after":0},"candidate_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:apply"],"error":"Error: getter:apply","listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":true}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"custom-apply","error":true}]}
{"runtime":0,"mode":"call-non-callable","arguments":0,"native":{"body_calls":0,"correct_receiver":true,"trace":[],"error":"TypeError: this.listener.call is not a function","listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":[],"error":"TypeError: listener.call is not a function","listeners_after":0},"candidate_hooked":{"body_calls":0,"correct_receiver":true,"trace":[],"error":"TypeError: this.listener.call is not a function","listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":true}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"custom-call","error":true}]}
{"runtime":0,"mode":"call-non-callable","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":0,"mode":"call-non-callable","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":0,"mode":"apply-non-callable","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":0,"mode":"apply-non-callable","arguments":1,"native":{"body_calls":0,"correct_receiver":true,"trace":[],"error":"TypeError: this.listener.apply is not a function","listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":[],"error":"TypeError: listener.apply is not a function","listeners_after":0},"candidate_hooked":{"body_calls":0,"correct_receiver":true,"trace":[],"error":"TypeError: this.listener.apply is not a function","listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":true}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"custom-apply","error":true}]}
{"runtime":0,"mode":"apply-non-callable","arguments":3,"native":{"body_calls":0,"correct_receiver":true,"trace":[],"error":"TypeError: this.listener.apply is not a function","listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":[],"error":"TypeError: listener.apply is not a function","listeners_after":0},"candidate_hooked":{"body_calls":0,"correct_receiver":true,"trace":[],"error":"TypeError: this.listener.apply is not a function","listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":true}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"custom-apply","error":true}]}
{"runtime":0,"mode":"inherited-listener-setter","arguments":0,"native":{"body_calls":1,"writes":[true],"public_identity":false,"error":null},"baseline_hooked":{"body_calls":1,"writes":[true],"public_identity":false,"error":null},"candidate_hooked":{"body_calls":1,"writes":[true],"public_identity":false,"error":null},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"once-metadata","error":false}]}
{"runtime":0,"mode":"on-getter-listener-setter","arguments":0,"native":{"body_calls":1,"reads":1,"writes":[true],"public_identity":false,"error":null},"baseline_hooked":{"body_calls":1,"reads":1,"writes":[],"public_identity":true,"error":null},"candidate_hooked":{"body_calls":1,"reads":1,"writes":[true],"public_identity":false,"error":null},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"once-metadata","error":false}]}
{"runtime":0,"mode":"on-getter-native-alias","arguments":0,"native":{"body_calls":1,"reads":1,"listeners_after":0},"baseline_hooked":{"body_calls":1,"reads":1,"listeners_after":0},"candidate_hooked":{"body_calls":1,"reads":1,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"once-metadata","error":false}]}
{"runtime":0,"mode":"on-getter-native-alias-throws","arguments":0,"native":{"body_calls":1,"same_error":true,"public_identity":true,"listeners_after":0},"baseline_hooked":{"body_calls":1,"same_error":true,"public_identity":true,"listeners_after":0},"candidate_hooked":{"body_calls":1,"same_error":true,"public_identity":true,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"once-metadata","error":true}]}
{"runtime":0,"mode":"throwing-listener-setter","arguments":0,"native":{"body_calls":1,"writes":[true],"error":"Error: metadata-write","listeners_after":1},"baseline_hooked":{"body_calls":1,"writes":[true],"error":"Error: metadata-write","listeners_after":1},"candidate_hooked":{"body_calls":1,"writes":[true],"error":"Error: metadata-write","listeners_after":1},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"once-metadata","error":true},{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":0,"mode":"readonly-emitter-methods","arguments":0,"native":{"body_calls":1,"seen":["on:true","removeListener:true"],"error":null},"baseline_hooked":{"body_calls":1,"seen":["on:true","removeListener:true"],"error":null},"candidate_hooked":{"body_calls":1,"seen":["on:true","removeListener:true"],"error":null},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"plain","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"plain","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"plain","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"call-opaque","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:call"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"call-opaque","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"call-opaque","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"apply-opaque","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"apply-opaque","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:apply"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"apply-opaque","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:apply"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"call-readonly","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:call"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"call-readonly","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"call-readonly","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"apply-readonly","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"apply-readonly","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:apply"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"apply-readonly","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:apply"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"call-getter","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:call","opaque:call"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"call-getter","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"call-getter","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"apply-getter","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"apply-getter","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:apply","opaque:apply"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"apply-getter","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:apply","opaque:apply"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"call-throwing-getter","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:call"],"error":"Error: getter:call","listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":true}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"call-throwing-getter","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"call-throwing-getter","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"apply-throwing-getter","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"apply-throwing-getter","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:apply"],"error":"Error: getter:apply","listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":true}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"apply-throwing-getter","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:apply"],"error":"Error: getter:apply","listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":true}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"call-non-callable","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":[],"error":"TypeError: listener.call is not a function","listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":true}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"call-non-callable","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"call-non-callable","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"apply-non-callable","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"apply-non-callable","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":[],"error":"TypeError: listener.apply is not a function","listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":true}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"apply-non-callable","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":[],"error":"TypeError: listener.apply is not a function","listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":true}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"inherited-listener-setter","arguments":0,"native":{"body_calls":1,"writes":[true],"public_identity":false,"error":null},"baseline_hooked":{"body_calls":1,"writes":[true],"public_identity":false,"error":null},"candidate_hooked":{"body_calls":1,"writes":[true],"public_identity":false,"error":null},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"once-metadata","error":false}]}
{"runtime":1,"mode":"on-getter-listener-setter","arguments":0,"native":{"body_calls":1,"reads":1,"writes":[true],"public_identity":false,"error":null},"baseline_hooked":{"body_calls":1,"reads":1,"writes":[],"public_identity":true,"error":null},"candidate_hooked":{"body_calls":1,"reads":1,"writes":[true],"public_identity":false,"error":null},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"once-metadata","error":false}]}
{"runtime":1,"mode":"on-getter-native-alias","arguments":0,"native":{"body_calls":1,"reads":1,"listeners_after":0},"baseline_hooked":{"body_calls":1,"reads":1,"listeners_after":0},"candidate_hooked":{"body_calls":1,"reads":1,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"once-metadata","error":false}]}
{"runtime":1,"mode":"on-getter-native-alias-throws","arguments":0,"native":{"body_calls":1,"same_error":true,"public_identity":true,"listeners_after":0},"baseline_hooked":{"body_calls":1,"same_error":true,"public_identity":true,"listeners_after":0},"candidate_hooked":{"body_calls":1,"same_error":true,"public_identity":true,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"once-metadata","error":true}]}
{"runtime":1,"mode":"throwing-listener-setter","arguments":0,"native":{"body_calls":1,"writes":[true],"error":"Error: metadata-write","listeners_after":1},"baseline_hooked":{"body_calls":1,"writes":[true],"error":"Error: metadata-write","listeners_after":1},"candidate_hooked":{"body_calls":1,"writes":[true],"error":"Error: metadata-write","listeners_after":1},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"once-metadata","error":true},{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"readonly-emitter-methods","arguments":0,"native":{"body_calls":1,"seen":["on:true","removeListener:true"],"error":null},"baseline_hooked":{"body_calls":1,"seen":["on:true","removeListener:true"],"error":null},"candidate_hooked":{"body_calls":1,"seen":["on:true","removeListener:true"],"error":null},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"plain","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"plain","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"plain","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"call-opaque","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:call"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"call-opaque","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"call-opaque","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"apply-opaque","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"apply-opaque","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:apply"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"apply-opaque","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:apply"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"call-readonly","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:call"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"call-readonly","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"call-readonly","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"apply-readonly","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"apply-readonly","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:apply"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"apply-readonly","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:apply"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"call-getter","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:call","opaque:call"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"call-getter","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"call-getter","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"apply-getter","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"apply-getter","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:apply","opaque:apply"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"apply-getter","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:apply","opaque:apply"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"call-throwing-getter","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:call"],"error":"Error: getter:call","listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":true}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"call-throwing-getter","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"call-throwing-getter","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"apply-throwing-getter","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"apply-throwing-getter","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:apply"],"error":"Error: getter:apply","listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":true}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"apply-throwing-getter","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:apply"],"error":"Error: getter:apply","listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":true}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"call-non-callable","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":[],"error":"TypeError: listener.call is not a function","listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":true}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"call-non-callable","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"call-non-callable","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"apply-non-callable","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"apply-non-callable","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":[],"error":"TypeError: listener.apply is not a function","listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":true}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"apply-non-callable","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":[],"error":"TypeError: listener.apply is not a function","listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":true}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"inherited-listener-setter","arguments":0,"native":{"body_calls":1,"writes":[true],"public_identity":false,"error":null},"baseline_hooked":{"body_calls":1,"writes":[true],"public_identity":false,"error":null},"candidate_hooked":{"body_calls":1,"writes":[true],"public_identity":false,"error":null},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"once-metadata","error":false}]}
{"runtime":2,"mode":"on-getter-listener-setter","arguments":0,"native":{"body_calls":1,"reads":1,"writes":[true],"public_identity":false,"error":null},"baseline_hooked":{"body_calls":1,"reads":1,"writes":[],"public_identity":true,"error":null},"candidate_hooked":{"body_calls":1,"reads":1,"writes":[true],"public_identity":false,"error":null},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"once-metadata","error":false}]}
{"runtime":2,"mode":"on-getter-native-alias","arguments":0,"native":{"body_calls":1,"reads":1,"listeners_after":0},"baseline_hooked":{"body_calls":1,"reads":1,"listeners_after":0},"candidate_hooked":{"body_calls":1,"reads":1,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"once-metadata","error":false}]}
{"runtime":2,"mode":"on-getter-native-alias-throws","arguments":0,"native":{"body_calls":1,"same_error":true,"public_identity":true,"listeners_after":0},"baseline_hooked":{"body_calls":1,"same_error":true,"public_identity":true,"listeners_after":0},"candidate_hooked":{"body_calls":1,"same_error":true,"public_identity":true,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"once-metadata","error":true}]}
{"runtime":2,"mode":"throwing-listener-setter","arguments":0,"native":{"body_calls":1,"writes":[true],"error":"Error: metadata-write","listeners_after":1},"baseline_hooked":{"body_calls":1,"writes":[true],"error":"Error: metadata-write","listeners_after":1},"candidate_hooked":{"body_calls":1,"writes":[true],"error":"Error: metadata-write","listeners_after":1},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"once-metadata","error":true},{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"readonly-emitter-methods","arguments":0,"native":{"body_calls":1,"seen":["on:true","removeListener:true"],"error":null},"baseline_hooked":{"body_calls":1,"seen":["on:true","removeListener:true"],"error":null},"candidate_hooked":{"body_calls":1,"seen":["on:true","removeListener:true"],"error":null},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
@@ -0,0 +1,96 @@
{"runtime":0,"mode":"plain","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":0,"mode":"plain","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":0,"mode":"plain","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":0,"mode":"call-opaque","arguments":0,"native":{"body_calls":0,"correct_receiver":true,"trace":["opaque:call"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:call"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:call"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"custom-call","error":false}]}
{"runtime":0,"mode":"call-opaque","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":0,"mode":"call-opaque","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":0,"mode":"apply-opaque","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":0,"mode":"apply-opaque","arguments":1,"native":{"body_calls":0,"correct_receiver":true,"trace":["opaque:apply"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:apply"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:apply"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"custom-apply","error":false}]}
{"runtime":0,"mode":"apply-opaque","arguments":3,"native":{"body_calls":0,"correct_receiver":true,"trace":["opaque:apply"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:apply"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:apply"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"custom-apply","error":false}]}
{"runtime":0,"mode":"call-readonly","arguments":0,"native":{"body_calls":0,"correct_receiver":true,"trace":["opaque:call"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:call"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:call"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"custom-call","error":false}]}
{"runtime":0,"mode":"call-readonly","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":0,"mode":"call-readonly","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":0,"mode":"apply-readonly","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":0,"mode":"apply-readonly","arguments":1,"native":{"body_calls":0,"correct_receiver":true,"trace":["opaque:apply"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:apply"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:apply"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"custom-apply","error":false}]}
{"runtime":0,"mode":"apply-readonly","arguments":3,"native":{"body_calls":0,"correct_receiver":true,"trace":["opaque:apply"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:apply"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:apply"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"custom-apply","error":false}]}
{"runtime":0,"mode":"call-getter","arguments":0,"native":{"body_calls":0,"correct_receiver":true,"trace":["get:call","opaque:call"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:call","opaque:call"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:call","opaque:call"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"custom-call","error":false}]}
{"runtime":0,"mode":"call-getter","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":0,"mode":"call-getter","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":0,"mode":"apply-getter","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":0,"mode":"apply-getter","arguments":1,"native":{"body_calls":0,"correct_receiver":true,"trace":["get:apply","opaque:apply"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:apply","opaque:apply"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:apply","opaque:apply"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"custom-apply","error":false}]}
{"runtime":0,"mode":"apply-getter","arguments":3,"native":{"body_calls":0,"correct_receiver":true,"trace":["get:apply","opaque:apply"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:apply","opaque:apply"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:apply","opaque:apply"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"custom-apply","error":false}]}
{"runtime":0,"mode":"call-throwing-getter","arguments":0,"native":{"body_calls":0,"correct_receiver":true,"trace":["get:call"],"error":"Error: getter:call","listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:call"],"error":"Error: getter:call","listeners_after":0},"candidate_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:call"],"error":"Error: getter:call","listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":true}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"custom-call","error":true}]}
{"runtime":0,"mode":"call-throwing-getter","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":0,"mode":"call-throwing-getter","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":0,"mode":"apply-throwing-getter","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":0,"mode":"apply-throwing-getter","arguments":1,"native":{"body_calls":0,"correct_receiver":true,"trace":["get:apply"],"error":"Error: getter:apply","listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:apply"],"error":"Error: getter:apply","listeners_after":0},"candidate_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:apply"],"error":"Error: getter:apply","listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":true}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"custom-apply","error":true}]}
{"runtime":0,"mode":"apply-throwing-getter","arguments":3,"native":{"body_calls":0,"correct_receiver":true,"trace":["get:apply"],"error":"Error: getter:apply","listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:apply"],"error":"Error: getter:apply","listeners_after":0},"candidate_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:apply"],"error":"Error: getter:apply","listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":true}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"custom-apply","error":true}]}
{"runtime":0,"mode":"inherited-listener-setter","arguments":0,"native":{"body_calls":1,"writes":[true],"public_identity":false,"error":null},"baseline_hooked":{"body_calls":1,"writes":[true],"public_identity":false,"error":null},"candidate_hooked":{"body_calls":1,"writes":[true],"public_identity":false,"error":null},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"once-metadata","error":false}]}
{"runtime":0,"mode":"on-getter-listener-setter","arguments":0,"native":{"body_calls":1,"reads":1,"writes":[true],"public_identity":false,"error":null},"baseline_hooked":{"body_calls":1,"reads":1,"writes":[],"public_identity":true,"error":null},"candidate_hooked":{"body_calls":1,"reads":1,"writes":[true],"public_identity":false,"error":null},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"once-metadata","error":false}]}
{"runtime":0,"mode":"throwing-listener-setter","arguments":0,"native":{"body_calls":1,"writes":[true],"error":"Error: metadata-write","listeners_after":1},"baseline_hooked":{"body_calls":1,"writes":[true],"error":"Error: metadata-write","listeners_after":1},"candidate_hooked":{"body_calls":1,"writes":[true],"error":"Error: metadata-write","listeners_after":1},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":0,"mode":"readonly-emitter-methods","arguments":0,"native":{"body_calls":1,"seen":["on:true","removeListener:true"],"error":null},"baseline_hooked":{"body_calls":1,"seen":["on:true","removeListener:true"],"error":null},"candidate_hooked":{"body_calls":1,"seen":["on:true","removeListener:true"],"error":null},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"plain","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"plain","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"plain","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"call-opaque","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:call"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"call-opaque","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"call-opaque","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"apply-opaque","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"apply-opaque","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:apply"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"apply-opaque","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:apply"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"call-readonly","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:call"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"call-readonly","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"call-readonly","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"apply-readonly","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"apply-readonly","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:apply"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"apply-readonly","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:apply"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"call-getter","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:call","opaque:call"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"call-getter","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"call-getter","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"apply-getter","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"apply-getter","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:apply","opaque:apply"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"apply-getter","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:apply","opaque:apply"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"call-throwing-getter","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:call"],"error":"Error: getter:call","listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":true}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"call-throwing-getter","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"call-throwing-getter","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"apply-throwing-getter","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"apply-throwing-getter","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:apply"],"error":"Error: getter:apply","listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":true}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"apply-throwing-getter","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:apply"],"error":"Error: getter:apply","listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":true}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"inherited-listener-setter","arguments":0,"native":{"body_calls":1,"writes":[true],"public_identity":false,"error":null},"baseline_hooked":{"body_calls":1,"writes":[true],"public_identity":false,"error":null},"candidate_hooked":{"body_calls":1,"writes":[true],"public_identity":false,"error":null},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"once-metadata","error":false}]}
{"runtime":1,"mode":"on-getter-listener-setter","arguments":0,"native":{"body_calls":1,"reads":1,"writes":[true],"public_identity":false,"error":null},"baseline_hooked":{"body_calls":1,"reads":1,"writes":[],"public_identity":true,"error":null},"candidate_hooked":{"body_calls":1,"reads":1,"writes":[true],"public_identity":false,"error":null},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"once-metadata","error":false}]}
{"runtime":1,"mode":"throwing-listener-setter","arguments":0,"native":{"body_calls":1,"writes":[true],"error":"Error: metadata-write","listeners_after":1},"baseline_hooked":{"body_calls":1,"writes":[true],"error":"Error: metadata-write","listeners_after":1},"candidate_hooked":{"body_calls":1,"writes":[true],"error":"Error: metadata-write","listeners_after":1},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":1,"mode":"readonly-emitter-methods","arguments":0,"native":{"body_calls":1,"seen":["on:true","removeListener:true"],"error":null},"baseline_hooked":{"body_calls":1,"seen":["on:true","removeListener:true"],"error":null},"candidate_hooked":{"body_calls":1,"seen":["on:true","removeListener:true"],"error":null},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"plain","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"plain","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"plain","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"call-opaque","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:call"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"call-opaque","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"call-opaque","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"apply-opaque","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"apply-opaque","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:apply"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"apply-opaque","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:apply"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"call-readonly","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:call"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"call-readonly","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"call-readonly","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"apply-readonly","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"apply-readonly","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:apply"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"apply-readonly","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["opaque:apply"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"call-getter","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:call","opaque:call"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"call-getter","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"call-getter","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"apply-getter","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"apply-getter","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:apply","opaque:apply"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"apply-getter","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:apply","opaque:apply"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"call-throwing-getter","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:call"],"error":"Error: getter:call","listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":true}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"call-throwing-getter","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"call-throwing-getter","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"apply-throwing-getter","arguments":0,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:0"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"apply-throwing-getter","arguments":1,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:apply"],"error":"Error: getter:apply","listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:1"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":true}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"apply-throwing-getter","arguments":3,"native":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_hooked":{"body_calls":0,"correct_receiver":true,"trace":["get:apply"],"error":"Error: getter:apply","listeners_after":0},"candidate_hooked":{"body_calls":1,"correct_receiver":true,"trace":["body:3"],"error":null,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":true}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"inherited-listener-setter","arguments":0,"native":{"body_calls":1,"writes":[true],"public_identity":false,"error":null},"baseline_hooked":{"body_calls":1,"writes":[true],"public_identity":false,"error":null},"candidate_hooked":{"body_calls":1,"writes":[true],"public_identity":false,"error":null},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"once-metadata","error":false}]}
{"runtime":2,"mode":"on-getter-listener-setter","arguments":0,"native":{"body_calls":1,"reads":1,"writes":[true],"public_identity":false,"error":null},"baseline_hooked":{"body_calls":1,"reads":1,"writes":[],"public_identity":true,"error":null},"candidate_hooked":{"body_calls":1,"reads":1,"writes":[true],"public_identity":false,"error":null},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"once-metadata","error":false}]}
{"runtime":2,"mode":"throwing-listener-setter","arguments":0,"native":{"body_calls":1,"writes":[true],"error":"Error: metadata-write","listeners_after":1},"baseline_hooked":{"body_calls":1,"writes":[true],"error":"Error: metadata-write","listeners_after":1},"candidate_hooked":{"body_calls":1,"writes":[true],"error":"Error: metadata-write","listeners_after":1},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":2,"mode":"readonly-emitter-methods","arguments":0,"native":{"body_calls":1,"seen":["on:true","removeListener:true"],"error":null},"baseline_hooked":{"body_calls":1,"seen":["on:true","removeListener:true"],"error":null},"candidate_hooked":{"body_calls":1,"seen":["on:true","removeListener:true"],"error":null},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"main-event-invocation","event":null,"error":false}]}
{"runtime":0,"mode":"on-getter-native-alias","arguments":0,"native":{"body_calls":1,"reads":1,"listeners_after":0},"baseline_hooked":{"body_calls":1,"reads":1,"listeners_after":0},"candidate_hooked":{"body_calls":1,"reads":1,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"once-metadata","error":false}]}
{"runtime":1,"mode":"on-getter-native-alias","arguments":0,"native":{"body_calls":1,"reads":1,"listeners_after":0},"baseline_hooked":{"body_calls":1,"reads":1,"listeners_after":0},"candidate_hooked":{"body_calls":1,"reads":1,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"once-metadata","error":false}]}
{"runtime":2,"mode":"on-getter-native-alias","arguments":0,"native":{"body_calls":1,"reads":1,"listeners_after":0},"baseline_hooked":{"body_calls":1,"reads":1,"listeners_after":0},"candidate_hooked":{"body_calls":1,"reads":1,"listeners_after":0},"baseline_observation":[{"kind":"main-event-invocation","event":null,"error":false}],"candidate_observation":[{"kind":"ipc-observation-gap","event":"once-metadata","error":false}]}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,91 @@
# Electron synchronous shell boundary verification — 2026-10-16
Fix #4393 reuses Nitin13-git's existing PR #4562. The boundary owns a fixed
method/return table: existing asynchronous blocks still reject, while
`showItemInFolder` returns `undefined` and `writeShortcutLink` returns `false`.
Both new blocks use the existing shape-only event recorder, shared by CLI/MCP.
No tool, schema, capture authority, analysis cache or launch behavior is added.
## Source and controlled regression
The fresh main base is `f26973a52918a938631e90fdc9116f7b85a62a52`;
implementation integration is `0386b60fb` and frozen verifier source is
`3f231735c3fbc28918590ddc61461871c8a2aa4d`.
The adjacent JSON records frozen final script/runtime/input hashes and all attempts;
unpinned exploratory helper revisions are explicitly identified.
Original contributor attribution remains in Git ancestry.
Using the candidate test with the base boundary produces **73 passes / 1 failure**:
the fake native shortcut implementation executes and returns true. With the
candidate boundary, **87 cases pass** (74 staged-hook, 5 projection, 8 MCP).
These test native dispatch suppression, synchronous results, shape-only
retention and the shared event schema. The fake Electron cases and controlled
MCP provider do not prove a real Windows shell implementation was exercised.
`npm run check:fast` passes both uncached tasks. Build-cache differences and
different case counts make the red/green timings unsuitable as a speed comparison.
## Real Joplin workflow and limits
Inputs: unchanged Joplin Desktop **3.7.21** ASAR (233,575,014 bytes), and the
explicit generic Electron **44.7.0** runtime. Acquisition/checksum references
are in `javascript-native-handoff-identity-2026-10-15.json` and
`electron-cancellation-2026-10-16.json`; this is not vendor-executable compatibility.
No executable fuse is changed. Large downloadable inputs and generated profiles
are not committed or retained as an experiment bundle.
The verifier opens Joplin's shipped **1. Welcome to Joplin!** note and real PNG
attachment. It selects Joplin's actual editor layout and resource context-menu
callbacks; Joplin's existing bridge calls the real patched shell method. The
native popup presentation is captured for automation; its application callbacks
are not replaced. Before any app actions, the verifier requires the active hook.
It requires zero earlier reveal events, one new blocked shape-only reveal event,
a still-rendered attachment and an identical event in the shared retained capture.
**Standard launch remains unsuccessful.** The earlier standard probe reports
`Unknown flag: -r` in Joplin's start-flag parser; the frozen standard verifier
times out waiting for the application search input. This is relevant to #2974,
not fixed here. A discarded NODE_OPTIONS experiment was invalid: Playwright
removes that variable, so no hook was installed. Those attempts are never used
as interception or performance evidence.
The explicit `--argv-shim-transport` test adapter loads the provider's staged
canonical hook and removes only its own exact `-r`/shim pair from app argv before
Joplin starts. It does not modify the ASAR or production provider. Its successful
operation phase is not a successful standard public CLI/MCP workflow. Provider
cleanup still fails (#4282); the verifier exits nonzero, preserving the distinction.
External owned-scope teardown does not establish provider cleanup.
The first valid shim probe reaches one blocked reveal, visible 1075×666 PNG,
no page errors and retained capture evidence. The final frozen rerun additionally
checks event causality and exact capture equality. Detailed measured results and
whole-family observation status are in the JSON. The final controlled pipeline
takes16.989s wall /16.055812 family CPU s /1,376,382,976 peak cgroup bytes
with verified payload-exit observation; the overall workflow still fails cleanup. Each final trial has a new
private config mount; earlier exploratory shared-config trials are not independent
cold runs. Filesystem/runtime/build caches are warm; no cold-system or warm-repeat
speedup is claimed. Agent/billed tokens are unmeasured; there is no token benefit claim.
## Reproduction
Use the pinned Node/npm toolchain. Install dependencies and build with
`npm ci` and `npm run build:cached`. Create an owned writable packet directory
with `tmp`, `profiles`, and `config/joplin-desktop`. On Linux, bind that private
config over the actual user config directory in a disposable `bwrap` mount
namespace (do not change HOME), keep the application resources read-only and
run under Xvfb. Set TMPDIR to the owned writable packet `tmp` directory. Run:
```sh
node experiments/analysis-efficiency/joplin-shell-boundary.mjs \
--runtime-root /absolute/rea-checkout \
--executable /absolute/generic-electron \
--application /absolute/Joplin/resources/app.asar \
--profile-root /absolute/owned-packet/profiles \
--argv-shim-transport
```
Omit the last flag for the failing standard launch. Run heavy work serially under
`resource-run.py --memory-gib 8 --observe-payload-exit`, one CPU, no swap, the
6 GiB early-stop margin and 192-task ceiling. CPU/peak memory receipts are at
payload exit with the observer alive; memory includes file cache, not just RSS.
Unverified complete-family observations are explicitly marked. No Windows host,
macOS host, vendor packaged runtime or full `check:pr` was executed here.
+4 -1
View File
@@ -46,7 +46,10 @@ Equivalent MCP input:
```
Each selected regular file is read through the existing stable artifact reader;
symlinks, changing files and files over 8 MiB fail closed. WASM bytes are copied
symlinks, changing files and files over 8 MiB fail closed. An oversized input
returns `resource_constraint` with resource `file-size` and its fixed byte
allowance in `reported_limits.maximum_input_bytes`; retrying the unchanged file
does not raise that allowance. WASM bytes are copied
into a private workspace as read-only `module.wasm`; its digest is rechecked
before and after each tool. WABT validates with `--enable-all`,
reports sections through `wasm-objdump -h -x`, and decodes through
@@ -0,0 +1,115 @@
#!/usr/bin/env python3
"""Check verifier result/exit semantics with controlled replies, not real apps."""
import argparse
import json
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
import textwrap
def write(root, name, source):
path = root / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(textwrap.dedent(source))
def check(arguments, root, case):
shutil.copyfile(arguments.verifier, root / "probe.mjs")
write(root, "package.json", '{"type":"module"}')
write(root, "node_modules/playwright-core/package.json",
'{"type":"module","exports":"./index.js"}')
write(root, "node_modules/playwright-core/index.js", """
let reads = 0;
const page = {
locator: () => ({ waitFor: async () => {} }),
getByText: () => ({ waitFor: async () => {} }),
async evaluate() {
const after = reads++ > 0;
return { title: 'controlled oracle', invalid_file_dialog: !after,
unsaved_changes: after,
diagram_svg_elements: after && process.env.REA_DRAWIO_ORACLE_CASE !== 'insertion-failure' ? 2 : 0,
cells: 0 };
},
};
export const _electron = { async launch() {
return { process: () => ({exitCode: 0, signalCode: null}),
firstWindow: async () => page, close: async () => {} };
}};
""")
write(root, "dist/browser/PlaywrightElectronActiveProvider.js", """
export class PlaywrightElectronActiveProvider {
constructor(environment, launch) { this.launch = launch; }
}
""")
write(root, "dist/browser/PlaywrightElectronActiveActions.js", """
export const runWithExecutionLimits = operation => operation;
""")
write(root, "dist/application/javascript/ElectronActiveObservationService.js", """
export async function captureElectronScenario(provider, input) {
const app = await provider.launch({});
await app.close();
const name = process.env.REA_DRAWIO_ORACLE_CASE;
const capture = { actions: input.actions.map(action => ({...action,
status: name === 'action-failure' ? 'failed' : 'completed'})),
coverage: {}, retention: {}, ipc: {events: [], observed: 0, dropped: 0} };
if (name === 'no-capture') return {ok: false, error: {reason: 'startup_failed'}};
if (name === 'partial') return {ok: false, error: {reason: 'cleanup_failed',
partialObservation: {capture}}};
return {ok: true, value: {evidence_id: 'ev_controlled',
normalized_result: capture}};
}
""")
result = subprocess.run(
[arguments.node, str(root / "probe.mjs"), "--runtime-root", str(root),
"--executable", arguments.node, "--application", str(root),
"--profile-root", str(root), "--dismiss-error", "--selector", "controlled"],
env={**os.environ, "REA_DRAWIO_ORACLE_CASE": case},
capture_output=True, text=True, timeout=15,
)
replies = []
for line in result.stdout.splitlines():
try:
replies.append(json.loads(line))
except ValueError:
pass
receipt = next((value for value in replies if "workflow_status" in value), None)
expected_success = case == "complete"
expected_provider = case not in ("partial", "no-capture")
expected_actions = case not in ("action-failure", "no-capture")
expected_inserted = case != "insertion-failure"
matched = (
(result.returncode == 0) is expected_success
and receipt is not None
and receipt["provider_ok"] is expected_provider
and receipt["cleanup_verified"] is expected_provider
and receipt["actions_completed"] is expected_actions
and receipt["shape_inserted"] is expected_inserted
and (receipt["workflow_status"] == "verified") is expected_success
)
return {"case": case, "exit_code": result.returncode,
"expected_success": expected_success, "summary_present": receipt is not None,
"matched_expected": matched}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--node", required=True)
parser.add_argument("--verifier", type=Path,
default=Path(__file__).with_name("drawio-electron-lifecycle.mjs"))
arguments = parser.parse_args()
receipts = []
with tempfile.TemporaryDirectory(prefix="rea-drawio-oracles-") as directory:
for case in ("complete", "partial", "action-failure", "insertion-failure", "no-capture"):
root = Path(directory) / case
root.mkdir()
receipts.append(check(arguments, root, case))
print(json.dumps({"oracle_cases": receipts, "real_provider_verified": False}))
assert all(receipt["matched_expected"] for receipt in receipts), receipts
if __name__ == "__main__":
main()
@@ -0,0 +1,191 @@
#!/usr/bin/env node
// Real draw.io ASAR, renderer and declared actions; DOM reads are independent
// verification probes. No fabricated IPC channels or replacement app callbacks.
import assert from "node:assert/strict";
import { mkdtemp, writeFile } from "node:fs/promises";
import { join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { parseArgs } from "node:util";
import { _electron } from "playwright-core";
const { values } = parseArgs({
options: {
"runtime-root": { type: "string" },
executable: { type: "string" },
application: { type: "string" },
"profile-root": { type: "string" },
selector: { type: "string" },
"dismiss-error": { type: "boolean", default: false },
},
});
for (const key of ["runtime-root", "executable", "application", "profile-root"])
assert.ok(values[key], `Missing --${key}`);
const runtime = resolve(values["runtime-root"]);
const { PlaywrightElectronActiveProvider } = await import(
pathToFileURL(
join(runtime, "dist/browser/PlaywrightElectronActiveProvider.js"),
)
);
const { captureElectronScenario } = await import(
pathToFileURL(
join(
runtime,
"dist/application/javascript/ElectronActiveObservationService.js",
),
)
);
const { runWithExecutionLimits } = await import(
pathToFileURL(
join(runtime, "dist/browser/PlaywrightElectronActiveActions.js"),
)
);
const profile = await mkdtemp(
join(resolve(values["profile-root"]), "drawio-ui-"),
);
const controller = new AbortController();
const timer = setTimeout(
() => controller.abort(new Error("Verifier deadline")),
40_000,
);
let application;
let child;
let page;
let before;
let after;
let checkedFinalState = false;
const inspect = () =>
page.evaluate(() => {
const diagram = document.querySelector(".geDiagramContainer svg");
return {
title: document.title,
invalid_file_dialog:
document.body.innerText.includes("Invalid file data"),
unsaved_changes: document.body.innerText.includes("Unsaved changes"),
diagram_svg_elements:
diagram?.querySelectorAll("rect,path,ellipse,text").length ?? null,
cells: document.querySelectorAll(".geDiagramContainer [data-cell-id]")
.length,
};
});
const started = performance.now();
try {
const actions = values["dismiss-error"]
? [
{
step_id: "dismiss-invalid-file",
kind: "click",
window_index: 0,
selector: 'button:has-text("OK"):visible',
},
]
: [];
if (values.selector !== undefined)
actions.push({
step_id: "insert-shape",
kind: "click",
window_index: 0,
selector: values.selector,
});
actions.push({
step_id: "settle-renderer",
kind: "wait",
window_index: 0,
duration_ms: 500,
});
const result = await captureElectronScenario(
new PlaywrightElectronActiveProvider(process.env, async (options) => {
application = await _electron.launch(options);
child = application.process();
page = await application.firstWindow({ timeout: 15_000 });
await page
.locator(".geDiagramContainer")
.waitFor({ state: "visible", timeout: 15_000 });
if (values["dismiss-error"])
await page
.getByText("Invalid file data", { exact: true })
.waitFor({ state: "visible", timeout: 15_000 });
before = await inspect();
return new Proxy(application, {
get(target, key) {
if (key === "close")
return async () => {
if (!checkedFinalState) {
checkedFinalState = true;
after = await inspect();
}
return target.close();
};
const value = Reflect.get(target, key, target);
return typeof value === "function" ? value.bind(target) : value;
},
});
}),
{
executable_path: resolve(values.executable),
application_path: resolve(values.application),
args: ["--no-sandbox", "--disable-gpu", `--user-data-dir=${profile}`],
actions,
},
{ signal: controller.signal },
);
const capture = result.ok
? result.value.normalized_result
: result.error.partialObservation?.capture;
await writeFile(join(profile, "result.json"), JSON.stringify(result));
const actionsCompleted =
capture?.actions.every((action) => action.status === "completed") === true;
const inserted =
values.selector === undefined
? null
: after?.diagram_svg_elements > before?.diagram_svg_elements;
console.log(
JSON.stringify({
profile,
runtime,
elapsed_ms: performance.now() - started,
deadline_triggered: controller.signal.aborted,
selected_shape: values.selector ?? null,
before,
after,
actions_completed: actionsCompleted,
shape_inserted: inserted,
provider_ok: result.ok,
cleanup_verified: result.ok,
reason: result.ok ? null : result.error.reason,
coverage: capture?.coverage,
retention: capture?.retention,
ipc: capture?.ipc,
actions: capture?.actions,
workflow_status:
result.ok && actionsCompleted && inserted !== false
? "verified"
: "partial-or-failed",
}),
);
assert.ok(actionsCompleted, "Declared real-app actions did not complete");
assert.ok(before && after, "Real renderer state was not verified");
assert.notEqual(inserted, false, "Click did not insert a diagram shape");
if (values["dismiss-error"]) assert.equal(after.invalid_file_dialog, false);
assert.equal(
controller.signal.aborted,
false,
"Verifier deadline was reached",
);
assert.ok(result.ok, "Provider cleanup remains unverified");
} finally {
clearTimeout(timer);
if (
application !== undefined &&
child?.exitCode === null &&
child?.signalCode === null
)
await runWithExecutionLimits(
application.close(),
undefined,
performance.now() + 5_000,
).catch(() => {
process.exitCode = 1;
});
// The resource scope owns any unresolved descendants; no PID/path-only kill.
if (child?.exitCode === null && child?.signalCode === null) process.exit(1);
}
@@ -0,0 +1,335 @@
// Controlled producer oracle, not real-application/provider evidence. Stage
// this file beside a minimal Electron module and the actual unmodified hook.
import { EventEmitter } from "node:events";
import { createRequire } from "node:module";
import { pathToFileURL } from "node:url";
if (process.argv[2]) await import(pathToFileURL(process.argv[2]));
// Match the hook's CommonJS module identity. Electron's ESM bare-module
// resolver can select its builtin even when run as Node with a staged fixture.
const { ipcMain } = createRequire(import.meta.url)("electron");
if (globalThis.__reaElectronActiveSnapshot?.().hook_error !== false)
throw new Error("The canonical hook must be installed without errors");
const modes = [
"plain",
"call-opaque",
"apply-opaque",
"call-readonly",
"apply-readonly",
"call-getter",
"apply-getter",
"call-throwing-getter",
"apply-throwing-getter",
"call-non-callable",
"apply-non-callable",
];
const execute = (emitter, channel, mode, argumentCount) => {
let bodyCalls = 0;
let correctReceiver = true;
const trace = [];
const listener = function listener() {
bodyCalls += 1;
correctReceiver &&= this === emitter;
trace.push(`body:${arguments.length}`);
};
if (mode !== "plain") {
const method = mode.startsWith("call") ? "call" : "apply";
const opaque = () => trace.push(`opaque:${method}`);
if (mode.includes("getter"))
Object.defineProperty(listener, method, {
get() {
trace.push(`get:${method}`);
if (mode.includes("throwing")) throw new Error(`getter:${method}`);
return opaque;
},
});
else if (mode.includes("readonly"))
Object.defineProperty(listener, method, { value: opaque });
else listener[method] = mode.includes("non-callable") ? null : opaque;
}
emitter.once(channel, listener);
let error = null;
try {
emitter.emit(
channel,
...Array.from({ length: argumentCount }, (_, i) => i),
);
emitter.emit(channel, "must-not-run-twice");
} catch (cause) {
error = String(cause);
}
return {
body_calls: bodyCalls,
correct_receiver: correctReceiver,
trace,
error,
listeners_after: emitter.listenerCount(channel),
};
};
const rows = [];
for (const mode of modes)
for (const argumentCount of [0, 1, 3]) {
const channel = `${mode}:${argumentCount}`;
const native = execute(new EventEmitter(), channel, mode, argumentCount);
const hooked = execute(ipcMain, channel, mode, argumentCount);
const events = globalThis
.__reaElectronActiveSnapshot()
.events.filter((event) => event.channel === channel);
rows.push({
mode,
argument_count: argumentCount,
native,
hooked,
semantic_match: JSON.stringify(native) === JSON.stringify(hooked),
observation: events.map(({ kind, event, error }) => ({
kind,
event,
error,
})),
});
}
const additional = (mode, scenario) => {
const channel = `metadata:${mode}`;
const native = scenario(new EventEmitter(), channel);
const hooked = scenario(ipcMain, channel);
rows.push({
mode,
argument_count: 0,
native,
hooked,
semantic_match: JSON.stringify(native) === JSON.stringify(hooked),
observation: globalThis
.__reaElectronActiveSnapshot()
.events.filter((event) => event.channel === channel)
.map(({ kind, event, error }) => ({ kind, event, error })),
});
};
additional("inherited-listener-setter", (emitter, channel) => {
const original = Object.getOwnPropertyDescriptor(
Function.prototype,
"listener",
);
const writes = [];
let bodyCalls = 0;
const listener = () => {
bodyCalls += 1;
};
Object.defineProperty(Function.prototype, "listener", {
configurable: true,
set(value) {
writes.push(value === listener);
},
});
let error = null;
try {
emitter.once(channel, listener);
} catch (cause) {
error = String(cause);
} finally {
if (original)
Object.defineProperty(Function.prototype, "listener", original);
else delete Function.prototype.listener;
}
const publicIdentity =
emitter.rawListeners(channel)[0]?.listener === listener;
emitter.emit(channel);
return {
body_calls: bodyCalls,
writes,
public_identity: publicIdentity,
error,
};
});
additional("on-getter-listener-setter", (emitter, channel) => {
const prototypeMetadata = Object.getOwnPropertyDescriptor(
Function.prototype,
"listener",
);
const onMetadata = Object.getOwnPropertyDescriptor(emitter, "on");
const on = emitter.on;
let reads = 0,
bodyCalls = 0,
error = null;
const writes = [];
const listener = () => {
bodyCalls += 1;
};
Object.defineProperty(emitter, "on", {
configurable: true,
get() {
reads += 1;
Object.defineProperty(Function.prototype, "listener", {
configurable: true,
set(value) {
writes.push(value === listener);
},
});
return on;
},
});
try {
emitter.once(channel, listener);
} catch (cause) {
error = String(cause);
} finally {
if (prototypeMetadata)
Object.defineProperty(Function.prototype, "listener", prototypeMetadata);
else delete Function.prototype.listener;
if (onMetadata) Object.defineProperty(emitter, "on", onMetadata);
else delete emitter.on;
}
const publicIdentity =
emitter.rawListeners(channel)[0]?.listener === listener;
emitter.emit(channel);
return {
body_calls: bodyCalls,
reads,
writes,
public_identity: publicIdentity,
error,
};
});
additional("on-getter-native-alias", (emitter, channel) => {
const original = Object.getOwnPropertyDescriptor(emitter, "on");
let reads = 0,
bodyCalls = 0;
const listener = () => {
bodyCalls += 1;
};
Object.defineProperty(emitter, "on", {
configurable: true,
get() {
reads += 1;
return EventEmitter.prototype.on;
},
});
try {
emitter.once(channel, listener);
} finally {
if (original) Object.defineProperty(emitter, "on", original);
else delete emitter.on;
}
emitter.emit(channel);
return {
body_calls: bodyCalls,
reads,
listeners_after: emitter.listenerCount(channel),
};
});
additional("on-getter-native-alias-throws", (emitter, channel) => {
const original = Object.getOwnPropertyDescriptor(emitter, "on");
const failure = new Error("registered-before-error");
let bodyCalls = 0,
sameError = false;
const listener = () => {
bodyCalls += 1;
};
Object.defineProperty(emitter, "on", {
configurable: true,
get() {
return function registerThenThrow(name, callback) {
EventEmitter.prototype.on.call(this, name, callback);
throw failure;
};
},
});
try {
emitter.once(channel, listener);
} catch (cause) {
sameError = cause === failure;
} finally {
if (original) Object.defineProperty(emitter, "on", original);
else delete emitter.on;
}
const publicIdentity =
emitter.rawListeners(channel)[0]?.listener === listener;
emitter.emit(channel);
return {
body_calls: bodyCalls,
same_error: sameError,
public_identity: publicIdentity,
listeners_after: emitter.listenerCount(channel),
};
});
additional("throwing-listener-setter", (emitter, channel) => {
const original = Object.getOwnPropertyDescriptor(
Function.prototype,
"listener",
);
const writes = [];
let bodyCalls = 0,
error = null;
const listener = () => {
bodyCalls += 1;
};
Object.defineProperty(Function.prototype, "listener", {
configurable: true,
set(value) {
writes.push(value === listener);
throw new Error("metadata-write");
},
});
try {
emitter.once(channel, listener);
} catch (cause) {
error = String(cause);
} finally {
if (original)
Object.defineProperty(Function.prototype, "listener", original);
else delete Function.prototype.listener;
}
// A failed registration must not leave later registrations unobservable.
emitter.on(channel, listener);
emitter.emit(channel);
return {
body_calls: bodyCalls,
writes,
error,
listeners_after: emitter.listenerCount(channel),
};
});
additional("readonly-emitter-methods", (emitter, channel) => {
const seen = [];
for (const key of ["on", "removeListener"]) {
const original = emitter[key];
Object.defineProperty(emitter, key, {
value: function readonlyMethod(...args) {
seen.push(`${key}:${this === emitter}`);
return Reflect.apply(original, this, args);
},
configurable: false,
writable: false,
});
}
let bodyCalls = 0,
error = null;
const listener = () => {
bodyCalls += 1;
};
try {
emitter.once(channel, listener);
emitter.emit(channel);
} catch (cause) {
error = String(cause);
}
return { body_calls: bodyCalls, seen, error };
});
console.log(
JSON.stringify({
node: process.versions.node,
electron: process.versions.electron ?? null,
cases: rows.length,
disagreements: rows.filter((row) => !row.semantic_match).length,
rows,
}),
);
@@ -0,0 +1,89 @@
#!/usr/bin/env python3
"""Serial native/hooked producer controls; not real-application evidence."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
def sha256(path):
with path.open("rb") as stream:
return hashlib.file_digest(stream, "sha256").hexdigest()
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--hook-root", type=Path, required=True)
parser.add_argument("--node", type=Path, action="append", required=True)
parser.add_argument("--electron", type=Path)
parser.add_argument("--output", type=Path, required=True)
arguments = parser.parse_args()
arguments.output.mkdir(parents=True, exist_ok=True)
packet = Path(tempfile.mkdtemp(prefix="native-once-", dir=arguments.output))
stage = packet / "staged"
module = stage / "node_modules/electron"
module.mkdir(parents=True)
fixture = module / "index.js"
fixture.write_text(
'const { EventEmitter } = require("node:events");\n'
"module.exports = { app: new EventEmitter(), ipcMain: new EventEmitter() };\n"
)
sources = [
arguments.hook_root / "scripts/electron-active-hook.cjs",
arguments.hook_root / "scripts/electron-active-hook-boundaries.cjs",
Path(__file__).with_name("electron-once-runtime.mjs"),
]
pins = {path.name: sha256(path) for path in sources}
pins["electron-fixture"] = sha256(fixture)
for source in sources:
shutil.copyfile(source, stage / source.name)
engines = [(engine, False) for engine in arguments.node]
if arguments.electron is not None:
engines.append((arguments.electron, True))
results = []
for index, (engine, run_as_node) in enumerate(engines):
selected = engine.absolute()
environment = dict(os.environ)
if run_as_node:
environment["ELECTRON_RUN_AS_NODE"] = "1"
execution = subprocess.run(
[str(selected), str(stage / sources[-1].name), str(stage / sources[0].name)],
env=environment,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=15,
check=False,
)
(packet / f"engine-{index}.stdout").write_bytes(execution.stdout)
(packet / f"engine-{index}.stderr").write_bytes(execution.stderr)
receipt = {
"selected_binary": str(engine),
"absolute_binary": str(selected),
"binary_sha256": sha256(selected),
"electron_run_as_node": run_as_node,
"exit_code": execution.returncode,
"stdout_sha256": hashlib.sha256(execution.stdout).hexdigest(),
"stderr_sha256": hashlib.sha256(execution.stderr).hexdigest(),
}
if execution.returncode == 0:
receipt["result"] = json.loads(execution.stdout)
results.append(receipt)
report = {"hook_root": str(arguments.hook_root), "source_sha256": pins,
"engines": results, "scope": __doc__}
(packet / "report.json").write_text(json.dumps(report, indent=2) + "\n")
if execution.returncode != 0:
raise RuntimeError(f"Engine failed; preserved receipt at {packet}")
print(json.dumps({"packet": str(packet), "engines": [
{"node": row["result"]["node"], "electron": row["result"]["electron"],
"cases": row["result"]["cases"],
"disagreements": row["result"]["disagreements"]} for row in results
]}))
if __name__ == "__main__":
main()
@@ -0,0 +1,326 @@
#!/usr/bin/env node
// Unchanged Joplin ASAR and its shipped welcome note/attachment. The actual
// resource context menu, bridge and shell call execute; menu automation is explicit.
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { mkdtemp, readFile, writeFile } from "node:fs/promises";
import { dirname, join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { parseArgs } from "node:util";
import { _electron } from "playwright-core";
const { values } = parseArgs({
options: {
"runtime-root": { type: "string" },
executable: { type: "string" },
application: { type: "string" },
"profile-root": { type: "string" },
"argv-shim-transport": { type: "boolean", default: false },
},
});
for (const key of ["runtime-root", "executable", "application", "profile-root"])
assert.ok(values[key], `Missing --${key}`);
const runtime = resolve(values["runtime-root"]);
const { PlaywrightElectronActiveProvider } = await import(
pathToFileURL(
join(runtime, "dist/browser/PlaywrightElectronActiveProvider.js"),
)
);
const { captureElectronScenario } = await import(
pathToFileURL(
join(
runtime,
"dist/application/javascript/ElectronActiveObservationService.js",
),
)
);
const profile = await mkdtemp(
join(resolve(values["profile-root"]), "joplin-shell-"),
);
let application;
let child;
let probe;
let stagedHookHashes;
const started = performance.now();
const controller = new AbortController();
const deadline = setTimeout(
() => controller.abort(new Error("Verifier deadline")),
45_000,
);
const verifyHookSources = async (stagedHook) => {
const hashes = {};
for (const name of [
"electron-active-hook.cjs",
"electron-active-hook-boundaries.cjs",
]) {
const hash = (data) => createHash("sha256").update(data).digest("hex");
const actualPath =
name === "electron-active-hook.cjs"
? stagedHook
: join(dirname(stagedHook), name);
const [actual, expected] = await Promise.all([
readFile(actualPath).then(hash),
readFile(join(runtime, "scripts", name)).then(hash),
]);
assert.equal(
actual,
expected,
`The staged ${name} must match the selected runtime source`,
);
hashes[name] = actual;
}
return hashes;
};
const launch = async (options) => {
const stagedIndex = options.args.indexOf("-r");
assert.notEqual(
stagedIndex,
-1,
"Expected the provider's staged require pair",
);
stagedHookHashes = await verifyHookSources(options.args[stagedIndex + 1]);
let launchOptions = options;
if (values["argv-shim-transport"]) {
// Explicit test adapter for #2974, not the public provider launch path.
const args = [...options.args];
const index = args.indexOf("-r");
assert.notEqual(index, -1, "Expected the provider's staged require pair");
const hook = args[index + 1];
const shim = join(profile, "launch-shim.cjs");
await writeFile(
shim,
`require(${JSON.stringify(hook)});\nconst index = process.argv.indexOf("-r");\nif (index < 0 || process.argv[index + 1] !== __filename) throw new Error("Unexpected staged argv");\nprocess.argv.splice(index, 2);\n`,
);
args[index + 1] = shim;
launchOptions = { ...options, args };
}
application = await _electron.launch(launchOptions);
child = application.process();
assert.equal(
await application.evaluate(
() => typeof globalThis.__reaElectronActiveSnapshot,
),
"function",
"The staged active hook must be installed before app actions",
);
const page = await application.firstWindow({ timeout: 15_000 });
const pageErrors = [];
page.on("pageerror", (error) => {
if (pageErrors.length < 5) pageErrors.push(String(error).slice(0, 600));
});
await page.getByPlaceholder("Search...").waitFor({ timeout: 15_000 });
await page
.getByText("1. Welcome to Joplin!", { exact: true })
.first()
.click({ timeout: 20_000 });
const viewer = page.locator("iframe.noteTextViewer");
for (
let attempt = 0;
attempt < 3 && !(await viewer.isVisible());
attempt += 1
) {
await application.evaluate(({ Menu }) => {
const item =
Menu.getApplicationMenu()?.getMenuItemById("toggleVisiblePanes");
if (!item) throw new Error("Missing Joplin editor layout menu");
item.click();
});
await viewer
.waitFor({ state: "visible", timeout: 3_000 })
.catch(() => undefined);
}
const image = page
.frameLocator("iframe.noteTextViewer")
.locator("img")
.first();
try {
await image.waitFor({ timeout: 15_000 });
} catch (cause) {
console.log(
JSON.stringify({
viewer_visible: await viewer.isVisible(),
frames: await Promise.all(
page
.frames()
.slice(0, 5)
.map(async (frame) => ({
url: frame.url(),
state: await frame.evaluate(() => ({
text: document.body?.innerText.slice(0, 500),
images: document.images.length,
})),
})),
),
}),
);
throw cause;
}
const attachment = await image.evaluate((element) => ({
src: element.getAttribute("src"),
alt: element.getAttribute("alt"),
width: element.naturalWidth,
height: element.naturalHeight,
}));
assert.ok(
attachment.width > 0 && attachment.height > 0,
"The shipped welcome attachment must render",
);
const beforeShellCount = await application.evaluate(
() =>
globalThis
.__reaElectronActiveSnapshot()
.events.filter(
(event) =>
event.kind === "shell-attempt" &&
event.event === "showItemInFolder",
).length,
);
assert.equal(
beforeShellCount,
0,
"A startup event cannot prove the requested reveal",
);
// Capture the app's actual context menu instead of opening a native popup.
await application.evaluate(({ Menu }) => {
const original = Menu.prototype.popup;
globalThis.__reaShellMenuProbe = { original, menu: null };
Menu.prototype.popup = function captureMenu() {
globalThis.__reaShellMenuProbe.menu = this;
};
});
await image.click({ button: "right" });
const revealed = await application.evaluate(async ({ Menu }) => {
const state = globalThis.__reaShellMenuProbe;
const deadline = Date.now() + 5_000;
while (state.menu === null && Date.now() < deadline) {
await new Promise((resolve) => setTimeout(resolve, 50));
}
Menu.prototype.popup = state.original;
delete globalThis.__reaShellMenuProbe;
const item = state.menu?.items.find(
(entry) => entry.label === "Reveal file in folder",
);
if (!item) throw new Error("Missing actual Joplin resource reveal menu");
await item.click();
return item.label;
});
const shell = await application.evaluate(async () => {
const read = () =>
globalThis
.__reaElectronActiveSnapshot()
.events.filter(
(event) =>
event.kind === "shell-attempt" &&
event.event === "showItemInFolder",
);
const deadline = Date.now() + 5_000;
let events = read();
while (events.length === 0 && Date.now() < deadline) {
await new Promise((resolve) => setTimeout(resolve, 50));
events = read();
}
return events;
});
assert.equal(shell.length, 1);
assert.equal(shell[0].phase, "blocked");
assert.deepEqual(shell[0].argument_shapes, ["string"]);
probe = {
before_shell_count: beforeShellCount,
note: "1. Welcome to Joplin!",
attachment,
revealed,
image_visible: await image.isVisible(),
shell,
page_errors: pageErrors,
};
return application;
};
try {
const result = await captureElectronScenario(
new PlaywrightElectronActiveProvider(process.env, launch),
{
executable_path: resolve(values.executable),
application_path: resolve(values.application),
args: [
"--no-sandbox",
"--disable-gpu",
`--user-data-dir=${profile}`,
"--profile",
profile,
],
actions: [
{
step_id: "settle-after-reveal",
kind: "wait",
window_index: 0,
duration_ms: 100,
},
],
},
{ signal: controller.signal },
);
await writeFile(join(profile, "result.json"), JSON.stringify(result));
const capture = result.ok
? result.value.normalized_result
: result.error.partialObservation?.capture;
const retainedShell = capture?.timeline.events.filter(
(event) =>
event.kind === "shell-attempt" && event.event === "showItemInFolder",
);
if (probe)
assert.deepEqual(
retainedShell,
probe.shell,
"The shared capture must retain the observed shell event",
);
console.log(
JSON.stringify({
profile,
runtime,
staged_hook_sha256: stagedHookHashes,
elapsed_ms: performance.now() - started,
launch_transport: values["argv-shim-transport"]
? "controlled-argv-shim"
: "standard-staged-argv",
probe: probe ?? null,
provider_ok: result.ok,
error: result.ok
? null
: {
tag: result.error._tag,
reason: result.error.reason,
diagnostics: result.error.diagnostics,
},
coverage: capture?.coverage,
retention: capture?.retention,
actions: capture?.actions,
deadline_triggered: controller.signal.aborted,
workflow_status:
result.ok && probe !== undefined ? "verified" : "partial-or-failed",
}),
);
assert.ok(probe, "Real Joplin resource reveal was not verified");
assert.ok(result.ok, "Provider cleanup remains unverified");
} catch (cause) {
console.log(
JSON.stringify({ verifier_failure: String(cause), probe: probe ?? null }),
);
process.exitCode = 1;
} finally {
clearTimeout(deadline);
if (application && child?.exitCode === null && child?.signalCode === null) {
let timer;
await Promise.race([
application.close().catch(() => undefined),
new Promise((resolve) => {
timer = setTimeout(resolve, 5_000);
}),
]);
clearTimeout(timer);
}
}
// The external owned scope recovers leftovers; that is not provider cleanup.
process.exit(process.exitCode ?? 0);
+86
View File
@@ -0,0 +1,86 @@
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { copyFile, lstat, mkdir, rm } from "node:fs/promises";
import { join } from "node:path";
import { z } from "zod";
const output = ".cache/ci-build-cache";
function readTasks() {
const { tasks } = z
.object({
tasks: z
.array(
z.object({
taskId: z.string().min(1),
// The pinned Turbo version uses eager 64-bit hexadecimal hashes.
hash: z.string().regex(/^[0-9a-f]{16}$/u),
}),
)
.min(1),
})
.parse(JSON.parse(readFileSync(0, "utf8")));
const identities = new Set();
for (const { taskId } of tasks) {
if (identities.has(taskId))
throw new Error(`Duplicate portable build task: ${taskId}`);
identities.add(taskId);
}
return tasks;
}
async function snapshot(tasks) {
await mkdir(output, { recursive: true });
let files = 0;
let bytes = 0;
try {
for (const hash of new Set(tasks.map((task) => task.hash))) {
for (const suffix of [".tar.zst", "-meta.json", "-manifest.json"]) {
const name = `${hash}${suffix}`;
const source = join(".turbo/cache", name);
const entry = await lstat(source).catch((error) => {
// Manifests are not present in every supported local cache entry.
if (suffix === "-manifest.json" && error.code === "ENOENT")
return undefined;
throw error;
});
if (!entry) continue;
if (!entry.isFile())
throw new Error(
`Portable build cache entry must be a regular file: ${source}`,
);
await copyFile(source, join(output, name));
files++;
bytes += entry.size;
}
}
} catch (error) {
await rm(output, { recursive: true, force: true });
throw error;
}
console.log(
JSON.stringify({ tasks: tasks.length, files, bytes, path: output }),
);
}
const [mode, ...extra] = process.argv.slice(2);
if (extra.length || !["key", "snapshot"].includes(mode))
throw new Error(
"Usage: portable-build-cache.mjs key|snapshot < turbo-dry-plan.json",
);
// Remove any earlier snapshot before parsing: a failed second invocation must
// never leave an old successful artifact eligible for upload.
if (mode === "snapshot") await rm(output, { recursive: true, force: true });
const tasks = readTasks();
if (mode === "key") {
const prefix = z
.string()
.min(1)
.regex(/^[^\r\n]+$/u)
.parse(process.env.BUILD_CACHE_PREFIX);
const hashes = tasks.map(({ taskId, hash }) => `${taskId}:${hash}`).sort();
const digest = createHash("sha256").update(hashes.join("\n")).digest("hex");
console.log(`key=${prefix}-${digest}\nprefix=${prefix}-`);
} else {
await snapshot(tasks);
}
+2
View File
@@ -118,6 +118,7 @@ export const SCOPES = {
"scripts/verify/inspector/**",
"scripts/verify-package.mjs",
"scripts/verify-package-*",
"scripts/verify-packed-bridge.*",
"scripts/verify-windows-*.mjs",
"scripts/build-windows-native.mjs",
"scripts/windows-capabilities.mjs",
@@ -145,6 +146,7 @@ export const SCOPES = {
"tests/boundary/filesystem/webScriptManifestPaths.test.ts",
"tests/boundary/process/ProcessEvidence.host.test.ts",
"tests/boundary/process/packageNpmInvocation.test.ts",
"tests/boundary/process/packedBridgeVerification.test.ts",
"tests/boundary/process/windowsOutputCapture.test.ts",
"tests/acceptance/webModuleCli.test.ts",
"tests/acceptance/webSourceLocationCli.test.ts",
+15 -2
View File
@@ -335,9 +335,22 @@ const createElectronActiveBoundaryPatches = ({
};
};
// Each blocked method keeps the return contract of the Electron API it
// replaces, so a target that checks the result sees an ordinary refusal.
const blockedShellResults = {
openExternal: () =>
Promise.reject(new Error("External shell effects are blocked")),
openPath: () =>
Promise.reject(new Error("External shell effects are blocked")),
// Synchronous `void`: reveals a path in the host file manager.
showItemInFolder: () => undefined,
// Synchronous `boolean`: creates or replaces a Windows shortcut file.
writeShortcutLink: () => false,
};
const patchShell = (shell) => {
if (shell === null || shell === undefined) return;
for (const method of ["openExternal", "openPath"]) {
for (const [method, blockedResult] of Object.entries(blockedShellResults)) {
const original = shell[method];
if (typeof original !== "function") continue;
shell[method] = function blockedShellCall(...args) {
@@ -345,7 +358,7 @@ const createElectronActiveBoundaryPatches = ({
argument_shape_values: args,
error: true,
});
return Promise.reject(new Error("External shell effects are blocked"));
return blockedResult();
};
}
};
+411 -49
View File
@@ -1,10 +1,35 @@
"use strict";
const applyFunction = Reflect.apply;
const applyArguments = Function.prototype.apply;
const bindFunction = Function.prototype.bind;
const callFunction = Function.prototype.call;
const arrayFrom = Array.from;
const defineProperty = Object.defineProperty;
const ownPropertyDescriptor = Object.getOwnPropertyDescriptor;
const getPrototypeOf = Object.getPrototypeOf;
const nativeFunctionPrototype = getPrototypeOf(callFunction);
const getProperty = Reflect.get;
const setProperty = Reflect.set;
const SetConstructor = Set;
const ProxyConstructor = Proxy;
const bindBuiltinMethod = (method) =>
applyFunction(bindFunction, callFunction, [method]);
const forEachMapEntry = bindBuiltinMethod(Map.prototype.forEach);
const sliceArguments = bindBuiltinMethod(Array.prototype.slice);
const weakMapHas = bindBuiltinMethod(WeakMap.prototype.has);
const weakMapGet = bindBuiltinMethod(WeakMap.prototype.get);
const weakMapSet = bindBuiltinMethod(WeakMap.prototype.set);
const setHas = bindBuiltinMethod(Set.prototype.has);
const setAdd = bindBuiltinMethod(Set.prototype.add);
const hasOwnProperty = bindBuiltinMethod(Object.prototype.hasOwnProperty);
const { isProxy } = require("node:util").types;
const {
createElectronActiveBoundaryPatches,
} = require("./electron-active-hook-boundaries.cjs");
const ipcEventKinds = new Set([
const ipcEventKinds = new SetConstructor([
"main-handler-invocation",
"main-event-invocation",
"utility-process-fork",
@@ -98,7 +123,7 @@ const eventFamily = (kind, processTypeValue) => {
return "preload-configuration";
if (kind.startsWith("ipc-renderer"))
return processTypeValue === "renderer" ? "renderer-ipc" : "ipc";
return ipcEventKinds.has(kind) ? "ipc" : kind;
return setHas(ipcEventKinds, kind) ? "ipc" : kind;
};
const noteDropped = (kind, processTypeValue, isIpc) => {
@@ -163,7 +188,7 @@ const frameIdentity = (event) => {
const record = (event) => {
observed += 1;
const isIpc = ipcEventKinds.has(event.kind);
const isIpc = setHas(ipcEventKinds, event.kind);
if (isIpc) observedIpc += 1;
else observedRuntime += 1;
const raw = {
@@ -191,7 +216,7 @@ const record = (event) => {
...event,
};
const shapeGroups = Array.isArray(raw.argument_shape_value_groups)
? raw.argument_shape_value_groups.filter(Array.isArray)
? raw.argument_shape_value_groups
: Array.isArray(raw.argument_shape_values)
? [raw.argument_shape_values]
: [];
@@ -242,11 +267,13 @@ const record = (event) => {
eventBytes + estimatedRetainedBytes + snapshotEnvelopeReserveBytes >
retentionBudgetBytes;
};
for (const group of shapeGroups)
for (const group of shapeGroups) {
if (!Array.isArray(group)) continue;
for (const argument of group) {
countShape(argument);
if (shapeOverflow) break;
}
}
if (shapeOverflow) {
noteDropped(raw.kind, retainedEvent.process_type, isIpc);
return null;
@@ -290,9 +317,11 @@ const record = (event) => {
noteDropped(raw.kind, retainedEvent.process_type, isIpc);
return null;
}
for (const group of shapeGroups)
for (const group of shapeGroups) {
if (!Array.isArray(group)) continue;
for (const argument of group)
retainedEvent.argument_shapes.push(shape(argument));
}
for (const valueShape of existingShapes)
retainedEvent.argument_shapes.push(
typeof valueShape === "string" ? valueShape : "unknown",
@@ -317,6 +346,14 @@ const recordIpc = (kind, channel, args, details = {}) =>
});
})();
const recordIpcObservationGap = (channel, event, error = false) =>
recordRuntime("ipc-observation-gap", event, error ? "failed" : "observed", {
channel,
target: "ipcMain.on",
process_type: "main",
error,
});
const patchEmitter = (emitter, observeEvent) => {
if (emitter === null || emitter === undefined) return;
const original = emitter.emit;
@@ -343,43 +380,360 @@ const recordInvocation = (kind, channel, event, args) =>
error: false,
});
const invokeObservedEventListener = (thisArg, channel, listener, args) => {
// Recording slices an actual invocation frame, avoiding Array species;
// callback dispatch keeps its native Array/Arguments representation.
const recorded = recordInvocation(
"main-event-invocation",
channel,
args[0],
sliceArguments(args, 1),
);
try {
return applyFunction(listener, thisArg, args);
} catch (cause) {
if (recorded !== null) recorded.error = true;
throw cause;
}
};
// Recording wrappers follow EventEmitter's own wrapper convention: `.listener`
// names the caller's function so native removal can match it. Node retains
// ownership of listener traversal and meta-events; hook-created "on" entries
// are translated before calling the application's removal method/decorators.
// Only hook-created wrappers are tracked, by the registration they stand in
// for: they already record, so patched on() registers them as is. An "on"
// wrapper stands in for the caller's raw function and a "once" wrapper for
// EventEmitter's once wrapper, which removeAllListeners reports accordingly.
// The value is the native removal argument: the app function for an "on"
// wrapper, or the wrapper itself for "once". Public .listener is mutable;
// it is not the authority for the original function's identity.
const hookListenerRemovalTargets = new WeakMap();
const MAX_LISTENER_CHAIN_HOPS = 16;
// Parse native property lookup without invoking application getters or Proxy
// traps. Undefined means absent; null means the lookup is unknown.
const lookupPropertyDescriptor = (listener, key) => {
let candidate = listener;
for (
let hops = 0;
hops <= MAX_LISTENER_CHAIN_HOPS && candidate !== null;
hops += 1
) {
if (isProxy(candidate)) return null;
const descriptor = ownPropertyDescriptor(candidate, key);
if (descriptor !== undefined) return descriptor;
candidate = getPrototypeOf(candidate);
}
return candidate === null ? undefined : null;
};
const listenerDataDescriptor = (listener) => {
const descriptor = lookupPropertyDescriptor(listener, "listener");
if (descriptor === null || descriptor === undefined) return descriptor;
if (hasOwnProperty(descriptor, "value")) return descriptor;
// A setter-only property shadows ancestors with a known undefined read.
return descriptor.get === undefined ? undefined : null;
};
// A decorator that wraps once's hook wrapper and names it through `.listener`
// already records through that wrapper. Registering the decorator as is keeps
// native self-removal working: the once wrapper removes itself by identity, and
// removeListener matches the decorator through its `.listener`.
const classifyHookListenerWrapper = (listener) => {
const visited = new SetConstructor();
let candidate = listener;
for (
let hops = 0;
hops <= MAX_LISTENER_CHAIN_HOPS && typeof candidate === "function";
hops += 1
) {
if (weakMapHas(hookListenerRemovalTargets, candidate)) return "owned";
if (setHas(visited, candidate)) return "unowned";
setAdd(visited, candidate);
const descriptor = listenerDataDescriptor(candidate);
if (descriptor === null) return "unknown";
candidate = descriptor?.value;
}
return typeof candidate === "function" ? "unknown" : "unowned";
};
const createObservedOnceCallback = (target, channel, listener, receiver) => {
// A private callable target avoids Proxy get invariants on an app's
// non-configurable, read-only call/apply properties. It never escapes
// through the native once wrapper's public listener link.
const callback = new ProxyConstructor(
function observedOnceListener() {
return invokeObservedEventListener(
this === receiver ? target : this,
channel,
listener,
arguments,
);
},
{
get: (_callable, key) => {
let value;
try {
value = getProperty(listener, key, listener);
} catch (cause) {
if (key === "call" || key === "apply")
recordIpcObservationGap(channel, `custom-${key}`, true);
throw cause;
}
if (key !== "call" && key !== "apply") return value;
if (typeof value !== "function") {
recordIpcObservationGap(channel, `custom-${key}`, true);
return value;
}
if (value === callFunction || value === applyArguments) return value;
// A runtime that selects an application method must preserve that
// method's receiver and original argument-list object. Its body may
// bypass the callable Proxy, so retain a gap rather than an invocation.
return new ProxyConstructor(value, {
apply: (method, thisArg, args) => {
const gap = recordIpcObservationGap(channel, `custom-${key}`);
if (args[0] === receiver) args[0] = target;
try {
return applyFunction(
method,
thisArg === callback ? listener : thisArg,
args,
);
} catch (cause) {
if (gap !== null) {
gap.error = true;
gap.phase = "failed";
}
throw cause;
}
},
});
},
},
);
return callback;
};
const patchIpcMain = (ipcMain) => {
if (ipcMain === null || ipcMain === undefined) return;
for (const [method, kind] of [
["handle", "main-handler-invocation"],
["on", "main-event-invocation"],
["once", "main-event-invocation"],
]) {
const original = ipcMain[method];
if (typeof original !== "function") continue;
ipcMain[method] = function patchedIpcMain(channel, listener) {
const { handle, on, once, removeAllListeners } = ipcMain;
let unobservedOnceScope = null;
if (typeof handle === "function")
ipcMain.handle = function patchedIpcMainHandle(channel, listener) {
if (typeof listener !== "function")
return original.call(this, channel, listener);
const wrapped =
kind === "main-handler-invocation"
? async function wrappedHandler(event, ...args) {
const recorded = recordInvocation(kind, channel, event, args);
try {
const result = await listener.call(this, event, ...args);
if (recorded !== null) recorded.result_shape = shape(result);
return result;
} catch (cause) {
if (recorded !== null) recorded.error = true;
throw cause;
}
}
: function wrappedEventListener(event, ...args) {
const recorded = recordInvocation(kind, channel, event, args);
try {
return listener.call(this, event, ...args);
} catch (cause) {
if (recorded !== null) recorded.error = true;
throw cause;
}
};
return original.call(this, channel, wrapped);
return handle.call(this, channel, listener);
return handle.call(
this,
channel,
async function wrappedHandler(event, ...args) {
const recorded = recordInvocation(
"main-handler-invocation",
channel,
event,
args,
);
try {
const result = await applyFunction(listener, this, arguments);
if (recorded !== null) recorded.result_shape = shape(result);
return result;
} catch (cause) {
if (recorded !== null) recorded.error = true;
throw cause;
}
},
);
};
}
if (typeof on !== "function") return;
ipcMain.on = function patchedIpcMainOn(channel, listener) {
if (typeof listener !== "function") return on.call(this, channel, listener);
if (unobservedOnceScope !== null) {
const scope = unobservedOnceScope;
const result = on.call(this, channel, listener);
recordIpcObservationGap(channel, "once-metadata");
if (scope.channel === channel) scope.gapRecorded = true;
return result;
}
const wrapperStatus = classifyHookListenerWrapper(listener);
if (wrapperStatus !== "unowned") {
const result = on.call(this, channel, listener);
if (wrapperStatus === "unknown")
recordIpcObservationGap(channel, "listener-chain");
return result;
}
const wrapped = function wrappedEventListener() {
return invokeObservedEventListener(this, channel, listener, arguments);
};
// Native single-listener dispatch reads .apply; array dispatch invokes the
// function directly. Let Node select that path without inspecting or
// copying the emitter's listener storage, including during reentrant emits.
defineProperty(wrapped, "apply", {
configurable: true,
writable: true,
value: function nativeApply(...methodArgs) {
let applicationApply;
try {
applicationApply = listener.apply;
} catch (cause) {
recordIpcObservationGap(channel, "custom-apply", true);
throw cause;
}
if (applicationApply !== applyArguments) {
// Custom apply owns the original list's interpretation. Forward it
// unchanged rather than executing getters to invent callback facts.
const gap = recordIpcObservationGap(channel, "custom-apply");
try {
return applyFunction(
applicationApply,
this === wrapped ? listener : this,
methodArgs,
);
} catch (cause) {
if (gap !== null) {
gap.error = true;
gap.phase = "failed";
}
throw cause;
}
}
// Native Function.apply performs its own list conversion before the
// recording wrapper observes an actual invocation frame.
// Borrowing the exposed method keeps its dynamic receiver.
return applyFunction(applyArguments, this, methodArgs);
},
});
defineProperty(wrapped, "listener", {
configurable: true,
enumerable: true,
writable: true,
value: listener,
});
weakMapSet(hookListenerRemovalTargets, wrapped, listener);
return on.call(this, channel, wrapped);
};
// Native once owns firing, removal and callback dispatch. Node versions
// differ in whether they read callback.call/apply. Observe actual invocation
// through a private callable Proxy instead of copying either implementation.
if (typeof once === "function")
ipcMain.once = function patchedIpcMainOnce(channel, listener) {
if (
typeof listener !== "function" ||
this === null ||
(typeof this !== "object" && typeof this !== "function")
)
return once.call(this, channel, listener);
const target = this;
const metadata = lookupPropertyDescriptor(
nativeFunctionPrototype,
"listener",
);
const onMetadata = lookupPropertyDescriptor(target, "on");
if (
metadata === null ||
(metadata !== undefined && !hasOwnProperty(metadata, "value")) ||
onMetadata === null ||
(onMetadata !== undefined && !hasOwnProperty(onMetadata, "value"))
) {
// Native wrapper creation can expose its callback through an inherited
// setter/Proxy, including one installed by an on getter. Pass the exact
// app callback; registrations in this synchronous scope stay unwrapped.
const previousScope = unobservedOnceScope;
const scope = { channel, gapRecorded: false };
let failed = true;
unobservedOnceScope = scope;
try {
const result = applyFunction(once, target, [channel, listener]);
failed = false;
return result;
} finally {
unobservedOnceScope = previousScope;
// An app decorator can register through an unhooked alias and then
// throw. Preserve a gap for either outcome, not proof of registration.
if (!scope.gapRecorded)
recordIpcObservationGap(channel, "once-metadata", failed);
}
}
const receiver = new ProxyConstructor(
{ __proto__: null },
{
get: (_target, key) => {
const value = getProperty(target, key, target);
if (
(key !== "on" && key !== "removeListener") ||
typeof value !== "function"
)
return value;
return function forwardOnceOperation(...args) {
if (key === "on") {
const wrapped = args[1];
defineProperty(wrapped, "listener", {
...ownPropertyDescriptor(wrapped, "listener"),
value: listener,
});
weakMapSet(hookListenerRemovalTargets, wrapped, wrapped);
}
return applyFunction(value, target, args);
};
},
set: (_target, key, value) => setProperty(target, key, value, target),
},
);
const callback = createObservedOnceCallback(
target,
channel,
listener,
receiver,
);
const result = applyFunction(once, receiver, [channel, callback]);
return result === receiver ? target : result;
};
if (typeof removeAllListeners !== "function") return;
// Node owns its live-array traversal. A forwarding receiver translates only
// our "on" entries before calling the current application removal method,
// including decorators installed/replaced during traversal. Application
// getters and methods always receive the real emitter; its public methods
// are never temporarily replaced. Native once entries remain unchanged.
ipcMain.removeAllListeners = function patchedIpcMainRemoveAllListeners(
...args
) {
const emitter = this;
// A separate empty target also permits forwarding a non-writable app
// method; a Proxy of the emitter itself would violate get invariants.
const receiver = new ProxyConstructor(
{ __proto__: null },
{
get(_target, key) {
const value = getProperty(emitter, key, emitter);
if (
(key !== "removeListener" && key !== "removeAllListeners") ||
typeof value !== "function"
)
return value;
return (...methodArgs) => {
if (key === "removeListener") {
const entry = methodArgs[1];
const removalTarget = weakMapGet(
hookListenerRemovalTargets,
entry,
);
// Translate only a still-valid data link. If app code changed it
// to an accessor, native removal owns when that getter executes.
if (
removalTarget !== undefined &&
removalTarget !== entry &&
listenerDataDescriptor(entry)?.value === removalTarget
)
methodArgs[1] = removalTarget;
}
return applyFunction(value, emitter, methodArgs);
};
},
set: (_target, key, value) => setProperty(emitter, key, value, emitter),
},
);
const result = applyFunction(removeAllListeners, receiver, args);
return result === receiver ? emitter : result;
};
};
const patchNavigationMethods = (webContents, contentsId) => {
@@ -649,10 +1003,24 @@ try {
);
}
// Never expose retained buffers to application iterators or array constructors.
const snapshotArray = (values, project) =>
arrayFrom({ __proto__: null, length: values.length }, (_unused, index) =>
project === undefined ? values[index] : project(values[index]),
);
const snapshotCounts = (counts, field) => {
const values = { __proto__: null, length: 0 };
forEachMapEntry(counts, (count, value) => {
values[values.length++] = { [field]: value, count };
});
return arrayFrom(values);
};
globalThis.__reaElectronActiveSnapshot = () => ({
events: events.map((event) => ({
events: snapshotArray(events, (event) => ({
...event,
argument_shapes: [...event.argument_shapes],
argument_shapes: snapshotArray(event.argument_shapes),
})),
retention_budget_bytes: retentionBudgetBytes,
estimated_retained_bytes: estimatedRetainedBytes,
@@ -661,14 +1029,8 @@ globalThis.__reaElectronActiveSnapshot = () => ({
dropped,
dropped_ipc: droppedIpc,
dropped_runtime: droppedRuntime,
dropped_event_families: [...droppedFamilies].map(([family, count]) => ({
family,
count,
})),
dropped_event_roles: [...droppedRoles].map(([role, count]) => ({
role,
count,
})),
dropped_event_families: snapshotCounts(droppedFamilies, "family"),
dropped_event_roles: snapshotCounts(droppedRoles, "role"),
observed,
observed_ipc: observedIpc,
observed_runtime: observedRuntime,
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
import { realpath } from "node:fs/promises";
import { runKunaCommand } from "../../dist/kuna/KunaCommand.js";
import { kunaProjectDeadline } from "../../dist/kuna/KunaConfiguration.js";
import { inspectKunaInstallation } from "../../dist/kuna/KunaInstallation.js";
import { KUNA_XREF_OUTPUT_BYTES } from "../../dist/kuna/KunaReferenceFiles.js";
@@ -24,14 +25,15 @@ export const nativeKunaIncomingSites = async ({
);
assert.ok(installed.ok, "Native reference oracle could not inspect Kuna");
const input = await realpath(binary);
const timeoutMs = installed.value.configuration.functionTimeoutMs;
// Match the provider's own xrefs scan, which runs under the project deadline.
const deadline = kunaProjectDeadline(installed.value.configuration);
const outcome = await runKunaCommand({
installation: installed.value,
arguments: ["xrefs", input, "--to", address, "--json"],
workspace,
operation: "verify:kuna-native-xrefs",
timeoutMs,
reportedTimeoutMs: timeoutMs,
timeoutMs: deadline.ms,
deadline,
outputBytes: KUNA_XREF_OUTPUT_BYTES,
signal: new AbortController().signal,
retainFailedFileClose: (owner, resource) =>
+2 -1
View File
@@ -3,6 +3,7 @@ import { readdir, realpath } from "node:fs/promises";
import { join } from "node:path";
import { runKunaCommand } from "../../dist/kuna/KunaCommand.js";
import { kunaProjectDeadline } from "../../dist/kuna/KunaConfiguration.js";
import { inspectKunaInstallation } from "../../dist/kuna/KunaInstallation.js";
const entries = (path) =>
@@ -46,7 +47,7 @@ export const nativeKunaStrings = async ({
workspace,
operation: "verify:kuna-native-strings",
timeoutMs: installed.value.configuration.projectTimeoutMs,
reportedTimeoutMs: installed.value.configuration.projectTimeoutMs,
deadline: kunaProjectDeadline(installed.value.configuration),
outputBytes: 16 * 1024 * 1024,
signal: new AbortController().signal,
retainFailedFileClose: (owner, resource) =>
+10
View File
@@ -33,6 +33,16 @@ const run = async (operation, input) => {
failures += 1;
return null;
}
if (operation === "decode_android_resources") {
const evidence = result.value;
const target = evidence.normalized_result.target;
check(
"resource Evidence preserves the decoded APK subject identity",
evidence.subject?.digest.sha256 === target.sha256 &&
evidence.subject?.local_path === target.path &&
evidence.subject?.format === "apk",
);
}
return result.value.normalized_result;
};
+1 -1
View File
@@ -66,6 +66,6 @@ export async function verifyPackagePack({ root, workspace }) {
) {
throw new Error("package contained generated Python bytecode");
}
await verifyPackedBridge({ workspace, tarball, packedFiles });
await verifyPackedBridge({ tarball, packedFiles });
return { tarball };
}
+10 -9
View File
@@ -442,15 +442,16 @@ try {
return evidence.normalized_result;
},
);
const { GHIDRA_MUTATING_OPERATIONS, GHIDRA_SESSION_CAPABILITIES } =
await import(
pathToFileURL(join(packageRoot, "dist/ghidra/GhidraSessionValues.js"))
);
const expected = GHIDRA_SESSION_CAPABILITIES.filter(
(name) =>
name !== "ping" &&
name !== "shutdown" &&
!GHIDRA_MUTATING_OPERATIONS.has(name),
const { GHIDRA_MUTATING_OPERATIONS } = await import(
pathToFileURL(join(packageRoot, "dist/ghidra/GhidraSessionValues.js"))
);
const { GHIDRA_OPERATIONS } = await import(
pathToFileURL(
join(packageRoot, "dist/ghidra/GhidraProviderCapabilities.js"),
)
);
const expected = GHIDRA_OPERATIONS.filter(
(name) => !GHIDRA_MUTATING_OPERATIONS.has(name),
);
const expectedOperations = new Set([
...expected,
+5
View File
@@ -0,0 +1,5 @@
/** Validate required shipped bridges and parse packed Python without extraction. */
export function verifyPackedBridge(input: {
readonly tarball: string;
readonly packedFiles: readonly string[];
}): Promise<void>;
+13 -7
View File
@@ -1,5 +1,4 @@
import { execFile } from "node:child_process";
import { join } from "node:path";
import { promisify } from "node:util";
import { GHIDRA_BRIDGE_SOURCES } from "../src/ghidra/GhidraBridgeSources.ts";
@@ -8,7 +7,7 @@ const packedHopperBridge = "package/bridge/hopper_bridge.py";
const packedCutterBridge = "package/bridge/cutter/rea_bridge.py";
/** Check shipped bridge files; real-provider lanes verify runtime behavior. */
export async function verifyPackedBridge({ workspace, tarball, packedFiles }) {
export async function verifyPackedBridge({ tarball, packedFiles }) {
for (const path of [
packedHopperBridge,
packedCutterBridge,
@@ -18,12 +17,19 @@ export async function verifyPackedBridge({ workspace, tarball, packedFiles }) {
]) {
if (!packedFiles.includes(path)) throw new Error(`package omitted ${path}`);
}
await exec("tar", ["-xf", tarball, "-C", workspace]);
// Parse the actual packed Python source without loading Hopper globals or
// generating bytecode. This check has no dependency on test-only fixtures.
// Read the packed source without extracting into the caller's leased root.
await exec("python3", [
"-c",
"import ast, pathlib, sys; p = pathlib.Path(sys.argv[1]); ast.parse(p.read_text(encoding='utf-8'), filename=str(p))",
join(workspace, packedHopperBridge),
[
"import ast, sys, tarfile",
"with tarfile.open(sys.argv[1]) as archive:",
" member = archive.getmember(sys.argv[2])",
" if not member.isfile():",
" raise ValueError(f'{member.name} is not a regular file')",
" with archive.extractfile(member) as source:",
" ast.parse(source.read().decode('utf-8'), filename=sys.argv[2])",
].join("\n"),
tarball,
packedHopperBridge,
]);
}
@@ -90,6 +90,20 @@ try {
),
);
const procedures = parseEvidence(await call("list_procedures"));
const strings = parseEvidence(await call("list_strings"));
assert.equal(
overview.normalized_result.procedure_count,
procedures.normalized_result.length,
);
assert.equal(
overview.normalized_result.string_count,
strings.normalized_result.length,
);
assert.ok(
overview.limitations.some((text) =>
text.includes("Ghidra-defined string Data"),
),
);
const entry = procedures.normalized_result.find(
(item) => item.value === "rea_pie_entry",
);
+10
View File
@@ -166,6 +166,16 @@ try {
!Array.isArray(strings)
)
throw new Error("Windows Ghidra inventory proof was incomplete");
const overview = await inventory("read_binary_inventory_summary", {});
if (
overview.document !== documents[0] ||
overview.procedure_count !== procedures.length ||
overview.string_count !== strings.length ||
overview.segment_count !== segments.length
)
throw new Error(
"Windows Ghidra overview aggregates disagree with inventories",
);
const procedure = procedures.find(
(item) => item.procedure?.external === false,
);
+15 -4
View File
@@ -6,6 +6,7 @@ import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { parseBinaryTarget } from "../../../dist/application/BinaryTargetResolver.js";
import { ArtifactResourceScope } from "../../../dist/artifacts/ArtifactResourceScope.js";
import { inspectAppleDispatchMetadata } from "../../../dist/native/AppleDispatchMetadata.js";
import {
artifactCliEvidence,
@@ -52,10 +53,20 @@ const fixupCoverage = (metadata) =>
const inspect = async (path) => {
const target = await parseBinaryTarget(path);
if (!target.ok) throw target.error;
return {
target: target.value,
metadata: (await inspectAppleDispatchMetadata(target.value, 5000)).result,
};
const resources = new ArtifactResourceScope();
try {
return {
target: target.value,
metadata: (
await resources.run(() =>
inspectAppleDispatchMetadata(target.value, 5000, resources),
)
).result,
};
} finally {
// Retry any descriptor whose close failed, surfacing unresolved cleanup.
await resources.close();
}
};
const checkObjc = (metadata, mode) => {
+174
View File
@@ -0,0 +1,174 @@
import { expect, it, vi } from "vitest";
import {
ExecFileOutputCancelledError,
execFileBytes,
} from "../process/ExecFileOutput.js";
import { probeAdbPathExistence, runAdbCapture } from "./AdbCommand.js";
vi.mock("../process/ExecFileOutput.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../process/ExecFileOutput.js")>()),
execFileBytes: vi.fn(),
}));
const probe = (signal?: AbortSignal) =>
probeAdbPathExistence(
"push_adb_file",
"device",
"/device/path",
"/host/adb",
{
timeoutMs: 100,
maxOutputBytes: 1024,
},
signal,
);
it("recognizes only the completed empty false result as absence", async () => {
vi.mocked(execFileBytes).mockRejectedValueOnce(
Object.assign(new Error("test false"), {
code: 1,
stdout: "",
stderr: "",
signal: null,
killed: false,
}),
);
await expect(probe()).resolves.toEqual({ ok: true, value: false });
});
it.each([
{ code: 2 },
{ code: "ENOENT" },
{ signal: "SIGTERM" },
{ collectionFailure: true },
{ stdout: "unexpected" },
{ stderr: " " },
])("keeps nonstandard false completion unknown: %j", async (extra) => {
vi.mocked(execFileBytes).mockRejectedValueOnce(
Object.assign(new Error("probe failed"), {
code: 1,
stdout: "",
stderr: "",
signal: null,
killed: false,
...extra,
}),
);
const result = await probe();
expect(result).toMatchObject({
ok: false,
error: {
_tag: "AnalysisProtocolError",
diagnostics: { destination_existence: "unknown" },
},
});
});
it.each([
[{ killed: true }, "AnalysisTimeoutError"],
[{ outputTruncated: true }, "AnalysisResourceConstraintError"],
] as const)(
"preserves a stopped or incomplete probe: %j",
async (extra, tag) => {
vi.mocked(execFileBytes).mockRejectedValueOnce(
Object.assign(new Error("probe failed"), {
code: 1,
stdout: "",
stderr: "",
signal: null,
killed: false,
...extra,
}),
);
await expect(probe()).resolves.toMatchObject({
ok: false,
error: { _tag: tag },
});
},
);
it("recognizes an empty successful probe as existence", async () => {
vi.mocked(execFileBytes).mockResolvedValueOnce({
stdout: Buffer.alloc(0),
stderr: Buffer.alloc(0),
});
await expect(probe()).resolves.toEqual({ ok: true, value: true });
});
it.each(["stdout", "stderr"] as const)(
"retains unexpected successful %s as unknown",
async (stream) => {
vi.mocked(execFileBytes).mockResolvedValueOnce({
stdout: Buffer.alloc(0),
stderr: Buffer.alloc(0),
[stream]: Buffer.from("unexpected"),
});
await expect(probe()).resolves.toMatchObject({
ok: false,
error: {
_tag: "AnalysisProtocolError",
capturedOutput: { [stream]: "unexpected", exit_code: 0 },
},
});
},
);
it.each([null, "caller stopped", new Error("caller stopped")] as const)(
"uses the command owner's exact %s cancellation reason",
async (reason) => {
const cancelled = Object.assign(new ExecFileOutputCancelledError(reason), {
stdout: "partial",
stderr: "diagnostic",
stdoutBytes: 7,
stderrBytes: 10,
});
vi.mocked(execFileBytes).mockRejectedValueOnce(cancelled);
const result = await probe();
expect(result).toMatchObject({
ok: false,
error: { _tag: "AnalysisCancelledError" },
});
if (result.ok) throw new Error("Expected owned cancellation");
expect(result.error.cause).toBe(reason);
expect(result.error.capturedOutput).toMatchObject({
stdout: "partial",
stderr: "diagnostic",
stdout_bytes: 7,
stderr_bytes: 10,
});
},
);
it("does not infer cancellation from a foreign ABORT_ERR code", async () => {
const failure = Object.assign(new Error("foreign code"), {
code: "ABORT_ERR",
stdout: "",
stderr: "",
});
vi.mocked(execFileBytes).mockRejectedValueOnce(failure);
expect(await probe()).toMatchObject({
ok: false,
error: { _tag: "AnalysisProtocolError" },
});
});
it("keeps a physical failure even when the later abort reason is that same error", async () => {
const controller = new AbortController();
const failure = new Error("physical command failure");
vi.mocked(execFileBytes).mockImplementationOnce(async () => {
controller.abort(failure);
throw failure;
});
const result = await runAdbCapture(
"inspect_adb_device",
"device",
"/host/adb",
[],
{ timeoutMs: 100, maxOutputBytes: 1024 },
controller.signal,
);
expect(result).toMatchObject({
ok: false,
error: { _tag: "AnalysisProtocolError", cause: failure },
});
});
+260
View File
@@ -0,0 +1,260 @@
import {
AnalysisCancelledError,
AnalysisProtocolError,
AnalysisResourceConstraintError,
AnalysisTimeoutError,
AnalysisUnsupportedTargetError,
} from "../domain/analysisErrorCore.js";
import type {
AnalysisCapturedOutput,
AnalysisError,
} from "../domain/analysisErrorBase.js";
import { err, ok, type Result } from "../domain/result.js";
import {
execFileBytes,
ExecFileOutputCancelledError,
execFileOutputFailure,
type ExecFileOutputFailure,
} from "../process/ExecFileOutput.js";
import { packageChangeFailureReason } from "./AdbShellOutput.js";
const STDERR_EXCERPT_BYTES = 512;
/** Project parsed command-owner facts without reinterpreting the rejection. */
export const adbCommandCapturedOutput = (
failure: ExecFileOutputFailure,
): AnalysisCapturedOutput => ({
stdout: failure.stdout,
stderr: failure.stderr,
...(failure.stdoutBytes === undefined
? {}
: { stdout_bytes: failure.stdoutBytes }),
...(failure.stderrBytes === undefined
? {}
: { stderr_bytes: failure.stderrBytes }),
exit_code: typeof failure.code === "number" ? failure.code : null,
signal: failure.signal,
truncated: failure.outputTruncated,
});
/** Parsed command failure owned by this adapter, separate from its public tag. */
class AdbProtocolError extends AnalysisProtocolError {
constructor(
message: string,
readonly commandFailure: ExecFileOutputFailure,
cause: unknown,
) {
super(message, { cause });
}
}
/** adb joins shell operands into a remote POSIX command; quote at that boundary. */
const quoteShellOperand = (value: string): string =>
/^[A-Za-z0-9_./:=@+,-]+$/u.test(value)
? value
: "'" + value.replaceAll("'", "'\\''") + "'";
export const adbArguments = (
serial: string | undefined,
arguments_: readonly string[],
): readonly string[] => {
const operands =
arguments_[0] === "shell"
? ["shell", ...arguments_.slice(1).map(quoteShellOperand)]
: [...arguments_];
return serial === undefined ? operands : ["-s", serial, ...operands];
};
export interface AdbRunLimits {
readonly timeoutMs: number;
readonly maxOutputBytes: number;
}
/** Run one bounded adb invocation and keep its exact failure shape. */
export const runAdbCapture = async (
operation: string,
targetPath: string,
binary: string,
arguments_: readonly string[],
limits: AdbRunLimits,
signal: AbortSignal | undefined,
): Promise<Result<{ stdout: Buffer; stderr: Buffer }, AnalysisError>> => {
if (signal?.aborted === true)
return err(new AnalysisCancelledError(operation, { cause: signal.reason }));
try {
return ok(
await execFileBytes(binary, arguments_, {
timeout: limits.timeoutMs,
maxBuffer: limits.maxOutputBytes,
stopSignal: "SIGTERM",
...(signal === undefined ? {} : { signal }),
}),
);
} catch (cause) {
const failure = execFileOutputFailure(cause);
if (cause instanceof ExecFileOutputCancelledError)
return err(
new AnalysisCancelledError(operation, {
cause: cause.cause,
...(failure === undefined
? {}
: { capturedOutput: adbCommandCapturedOutput(failure) }),
}),
);
if (failure === undefined)
return err(
new AnalysisProtocolError(`adb failed for ${operation}`, { cause }),
);
if (failure.outputTruncated)
return err(
new AnalysisResourceConstraintError(
operation,
"memory",
`adb output exceeded the ${String(limits.maxOutputBytes)}-byte capture budget`,
{ max_output_bytes: limits.maxOutputBytes },
{ cause },
),
);
const excerpt = failure.stderr.trim().slice(0, STDERR_EXCERPT_BYTES);
if (/unauthorized/iu.test(excerpt))
return err(
new AnalysisUnsupportedTargetError(operation, targetPath, excerpt, {
cause,
}),
);
if (
/device[^\n]*not found|device offline|still authorizing|more than one device/iu.test(
excerpt,
)
)
return err(
new AnalysisUnsupportedTargetError(operation, targetPath, excerpt, {
cause,
}),
);
if (
/No such file or directory|Permission denied|read: Permission denied/iu.test(
excerpt,
)
)
return err(
new AnalysisUnsupportedTargetError(operation, targetPath, excerpt, {
cause,
}),
);
if (failure.killed)
return err(
new AnalysisTimeoutError(operation, limits.timeoutMs, { cause }),
);
return err(
new AdbProtocolError(
excerpt === ""
? `adb exited with code ${String(failure.code)} for ${operation}`
: excerpt,
failure,
cause,
),
);
}
};
/** Decode textual adb protocols after the shared byte capture and error boundary. */
export const runAdb = async (
...arguments_: Parameters<typeof runAdbCapture>
): Promise<
Result<{ stdout: string; stderr: string; stdoutBytes: number }, AnalysisError>
> => {
const captured = await runAdbCapture(...arguments_);
if (!captured.ok) return captured;
return ok({
stdout: captured.value.stdout.toString("utf8"),
stderr: captured.value.stderr.toString("utf8"),
// Byte count of the captured output before UTF-8 decoding, so malformed
// sequences replaced by U+FFFD still report their true captured size.
stdoutBytes: captured.value.stdout.byteLength,
});
};
/**
* adb exits nonzero when the device refuses a package change, printing the
* `Failure [...]` verdict to stderr (install) or stdout (uninstall). Read the
* refusal from that failed run so it is not reported as unreadable output.
*/
export const packageChangeRefusal = (
operation: string,
serial: string,
error: AnalysisError,
): Result<never, AnalysisError> | undefined => {
if (!(error instanceof AdbProtocolError)) return undefined;
const failure = error.commandFailure;
const reason = packageChangeFailureReason(
`${failure.stdout}\n${failure.stderr}`,
);
return reason === null
? undefined
: err(
new AnalysisUnsupportedTargetError(operation, serial, reason, {
cause: error,
}),
);
};
/** Only the clean false result of test -e establishes absence. */
export const probeAdbPathExistence = async (
operation: string,
serial: string,
path: string,
binary: string,
limits: AdbRunLimits,
signal: AbortSignal | undefined,
): Promise<Result<boolean, AnalysisError>> => {
const probed = await runAdbCapture(
operation,
path,
binary,
adbArguments(serial, ["shell", "test", "-e", path]),
limits,
signal,
);
if (probed.ok) {
if (probed.value.stdout.length === 0 && probed.value.stderr.length === 0)
return ok(true);
return err(
new AnalysisProtocolError(
"The ADB path existence probe returned unexpected output; destination existence is unknown",
{
diagnostics: { destination_existence: "unknown" },
capturedOutput: {
stdout: probed.value.stdout.toString("utf8"),
stderr: probed.value.stderr.toString("utf8"),
stdout_bytes: probed.value.stdout.length,
stderr_bytes: probed.value.stderr.length,
exit_code: 0,
signal: null,
truncated: false,
},
},
),
);
}
const error = probed.error;
if (!(error instanceof AdbProtocolError)) return probed;
const failure = error.commandFailure;
if (
failure.code === 1 &&
failure.signal === null &&
!failure.killed &&
!failure.outputTruncated &&
failure.collectionFailure !== true &&
failure.stdout === "" &&
failure.stderr === ""
)
return ok(false);
return err(
new AnalysisProtocolError(error.message, {
cause: error.cause,
diagnostics: { destination_existence: "unknown" },
capturedOutput: adbCommandCapturedOutput(failure),
}),
);
};
+40 -3
View File
@@ -1,13 +1,19 @@
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
import {
AnalysisCancelledError,
AnalysisResourceConstraintError,
AnalysisTimeoutError,
} from "../domain/analysisErrorCore.js";
import { isAbsolute } from "node:path";
import {
execFileOutput,
ExecFileOutputCancelledError,
execFileOutputFailure,
} from "../process/ExecFileOutput.js";
import type { ProviderAvailability } from "../application/AnalysisProvider.js";
import type { ProviderRejectionCode } from "../contracts/providerSelection.js";
import type { JsonValue } from "../domain/jsonValue.js";
import { adbCommandCapturedOutput } from "./AdbCommand.js";
/** Environment key selecting the caller's adb binary. */
export const ADB_BINARY_ENV = "REA_ADB_PATH";
@@ -115,8 +121,39 @@ export const inspectAdbClient = async (
} catch (cause) {
if (cause instanceof AdbConfigurationFailure) throw cause;
const failure = execFileOutputFailure(cause);
if (signal?.aborted || failure?.code === "ABORT_ERR")
throw new AnalysisCancelledError("inspect_adb_client", { cause });
if (cause instanceof ExecFileOutputCancelledError)
throw new AnalysisCancelledError("inspect_adb_client", {
cause: cause.cause,
...(failure === undefined
? {}
: { capturedOutput: adbCommandCapturedOutput(failure) }),
});
const diagnostics = {
selected_binary: selection.binary,
path_source: selection.pathSource,
...(failure === undefined ? {} : { exit_code: failure.code }),
};
const options = {
cause,
diagnostics,
...(failure === undefined
? {}
: { capturedOutput: adbCommandCapturedOutput(failure) }),
};
if (failure?.outputTruncated === true)
throw new AnalysisResourceConstraintError(
"inspect_adb_client",
"memory",
`adb version output exceeded the ${String(VERSION_MAX_OUTPUT_BYTES)}-byte capture budget`,
{ max_output_bytes: VERSION_MAX_OUTPUT_BYTES },
options,
);
if (failure?.code === "ETIMEDOUT")
throw new AnalysisTimeoutError(
"inspect_adb_client",
VERSION_TIMEOUT_MS,
options,
);
if (failure?.code === "ENOENT")
throw new AdbConfigurationFailure(
"binary_missing",
+86 -5
View File
@@ -706,6 +706,50 @@ exit 1`,
expect(result.total_bytes).toBe(34);
});
it("marks coverage partial when `pkg=null` leaves permissions unknown", async () => {
const { binary } = await writeAdbStub(
shellStub({
"dumpsys:package": `Packages:
Package [com.example] (abcdef):
userId=10234
pkg=null
versionCode=45 minSdk=24 targetSdk=34
codePath=/data/app/com.example
`,
}),
);
const result = await evidenceResult(providerOver(binary), {
operation: "inspect_adb_package",
input: { serial: "emulator-5554", package: "com.example" },
});
expect(result).toMatchObject({
requested_permissions_count: null,
coverage: "partial",
});
});
it("keeps permission counts unknown when package state is not established", async () => {
const { binary } = await writeAdbStub(
shellStub({
"dumpsys:package": `Packages:
Package [com.example] (abcdef):
appId=10234
codePath=/data/app/com.example
versionCode=45 minSdk=24 targetSdk=34
`,
}),
);
const result = await evidenceResult(providerOver(binary), {
operation: "inspect_adb_package",
input: { serial: "emulator-5554", package: "com.example" },
});
expect(result).toMatchObject({
version_code: "45",
requested_permissions_count: null,
coverage: "partial",
});
});
it("projects package details from dumpsys package output", async () => {
const { binary } = await writeAdbStub(
shellStub({
@@ -749,6 +793,7 @@ it.each(["9007199254740993", "-4294967296"])(
shellStub({
"dumpsys:package": `Packages:
Package [com.example] (abcdef):
pkg=Package{abcdef com.example}
versionCode=${versionCode} minSdk=24 targetSdk=34
`,
}),
@@ -759,6 +804,7 @@ it.each(["9007199254740993", "-4294967296"])(
});
expect(result).toMatchObject({
version_code: versionCode,
requested_permissions_count: 0,
coverage: "complete",
});
},
@@ -1316,8 +1362,8 @@ it("reads Android 16 focus declarations from displays when windows omits them",
focused_window: "Window{abc u0 com.example/.Main}",
focused_app: "ActivityRecord{def u0 com.example/.Main t1}",
observed_lines: [
"Window{abc u0 com.example/.Main}",
"ActivityRecord{def u0 com.example/.Main t1}",
"mCurrentFocus=Window{abc u0 com.example/.Main}",
"mFocusedApp=ActivityRecord{def u0 com.example/.Main t1}",
],
});
const execution = await providerOver(binary).execute({
@@ -1354,6 +1400,41 @@ it("describes missing window focus as unknown after both sections omit declarati
);
});
it.each(["windows", "displays"])(
"preserves explicitly absent focus from %s without another fallback",
async (section) => {
const declarations = " mCurrentFocus=null\n mFocusedApp=null\n";
const { binary } = await writeAdbStub(
shellStub({
"dumpsys:window:windows":
section === "windows" ? declarations : "no focus\n",
...(section === "displays"
? { "dumpsys:window:displays": declarations }
: {}),
}),
);
const execution = await providerOver(binary).execute({
operation: "inspect_adb_window",
input: { serial: "emulator-5554" },
});
expect(execution.ok).toBe(true);
if (!execution.ok) throw execution.error;
expect(execution.value.result).toMatchObject({
focused_window: null,
focused_app: null,
observed_lines: [" mCurrentFocus=null", " mFocusedApp=null"],
});
expect(execution.value.limitations).toContain(
`Focus declarations were read from \`dumpsys window ${section}\``,
);
expect(
execution.value.limitations.some((line) =>
line.includes("focus is unknown"),
),
).toBe(false);
},
);
it("reads one settings value and reports device-side nulls", async () => {
const { binary } = await writeAdbStub(shellStub({ "settings:get": "1\n" }));
const enabled = await evidenceResult(providerOver(binary), {
@@ -1480,7 +1561,7 @@ exit 1`,
it("uninstalls a package and reports the device's refusal for absent ones", async () => {
const { binary } = await writeAdbStub(
() => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi
if [ "$1" = "-s" ] && [ "$3" = "uninstall" ]; then printf 'Success\\n'; exit 0; fi
if [ "$#" = 8 ] && [ "$1" = "-s" ] && [ "$3" = "shell" ] && [ "$4" = "pm" ] && [ "$5" = "uninstall" ] && [ "$6" = "--user" ] && [ "$7" = 0 ]; then printf 'Success\\n'; exit 0; fi
exit 1`,
);
const removed = await evidenceResult(providerOver(binary), {
@@ -1491,7 +1572,7 @@ exit 1`,
const { binary: refusing } = await writeAdbStub(
() => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi
if [ "$1" = "-s" ] && [ "$3" = "uninstall" ]; then printf 'Failure [not installed for 0]\\n'; exit 0; fi
if [ "$#" = 8 ] && [ "$1" = "-s" ] && [ "$3" = "shell" ] && [ "$4" = "pm" ] && [ "$5" = "uninstall" ] && [ "$6" = "--user" ] && [ "$7" = 0 ]; then printf 'Failure [not installed for 0]\\n'; exit 0; fi
exit 1`,
);
const projected = await refusedBy(providerOver(refusing), {
@@ -1535,7 +1616,7 @@ exit 1`,
it("preserves the device's uninstall refusal when adb exits nonzero", async () => {
const { binary } = await writeAdbStub(
() => `if [ "$1" = "version" ]; then ${VERSION_BLOCK}; exit 0; fi
if [ "$1" = "-s" ] && [ "$3" = "uninstall" ]; then printf 'Failure [not installed for 0]\\n'; exit 1; fi
if [ "$#" = 8 ] && [ "$1" = "-s" ] && [ "$3" = "shell" ] && [ "$4" = "pm" ] && [ "$5" = "uninstall" ] && [ "$6" = "--user" ] && [ "$7" = 0 ]; then printf 'Failure [not installed for 0]\\n'; exit 1; fi
exit 1`,
);
const projected = await refusedBy(providerOver(binary), {
+83 -213
View File
@@ -56,11 +56,16 @@ import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.
import { err, ok, type Result } from "../domain/result.js";
import type { JsonValue } from "../domain/jsonValue.js";
import { jsonObjectSchema } from "../domain/jsonValue.js";
import { hashStableFile } from "../filesystem/StableFileHash.js";
import {
hashStableFile,
stableFileHashCause,
} from "../filesystem/StableFileHash.js";
import {
regularFileCancellation,
RegularFileCancelled,
RegularFileCleanupFailure,
} from "../filesystem/OwnedRegularFile.js";
import { RegularFileOperationCancelled } from "../filesystem/RegularFile.js";
import { OwnedFileHandle } from "../filesystem/OwnedFileHandle.js";
import { createEvidence } from "../domain/evidence.js";
import {
@@ -70,9 +75,13 @@ import {
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
import { PrivateRuntimeRootUnavailableError } from "../process/PrivateRuntimeRootUnavailableError.js";
import {
execFileBytes,
execFileOutputFailure,
} from "../process/ExecFileOutput.js";
adbArguments,
runAdb,
runAdbCapture,
packageChangeRefusal,
probeAdbPathExistence,
type AdbRunLimits,
} from "./AdbCommand.js";
import {
AdbConfigurationFailure,
adbConfigurationAvailability,
@@ -102,7 +111,6 @@ import {
parseBugreportCopiedPath,
parseDumpsysPackageOutput,
parseFeatureListOutput,
packageChangeFailureReason,
parsePackageChangeOutput,
parseProcessListOutput,
parsePngDimensions,
@@ -144,7 +152,6 @@ const SCREEN_TIMEOUT_MS = 60_000;
const SCREEN_MAX_OUTPUT_BYTES = 32 * 1024 * 1024;
const BUGREPORT_TIMEOUT_MS = 600_000;
const BUGREPORT_MAX_OUTPUT_BYTES = 1024 * 1024;
const STDERR_EXCERPT_BYTES = 512;
const UNPARSED_LINES_REPORTED = 10;
/** Carry the exact digest owner through caller-specific partial-result mapping. */
@@ -170,7 +177,7 @@ const hashAdbFile = async (
return await hashStableFile(path, {
...(signal === undefined ? {} : { signal }),
retainFailedClose: (handle) => {
closeLease = new AnalysisCleanupLease(() => handle.close());
closeLease = new AnalysisCleanupLease(() => handle.close(), [path]);
resources.retain({
kind: "lease",
resource: `${path} (ADB digest handle)`,
@@ -180,9 +187,8 @@ const hashAdbFile = async (
});
} catch (cause: unknown) {
if (
cause !== signal?.reason &&
(cause instanceof RegularFileCleanupFailure ||
cause instanceof RegularFileCancelled)
cause instanceof RegularFileCleanupFailure ||
cause instanceof RegularFileCancelled
)
throw new AdbDigestFailure(
cause as
@@ -192,8 +198,8 @@ const hashAdbFile = async (
closeLease?.bindCleanupError(error);
},
);
if (signal?.aborted === true)
throw new AnalysisCancelledError(operation, { cause });
if (cause instanceof RegularFileOperationCancelled)
throw new AnalysisCancelledError(operation, { cause: cause.reason });
throw cause;
}
};
@@ -201,7 +207,6 @@ const hashAdbFile = async (
const adbDigestError = (
cause: unknown,
operation: string,
signal: AbortSignal | undefined,
partialObservation:
| AdbAcquiredFilePartialObservation
| AdbPackagePullPartialObservation,
@@ -236,12 +241,10 @@ const adbDigestError = (
);
const primaryCause =
digestOutcome.kind === "failed" ? digestOutcome.cause : undefined;
if (
signal?.aborted === true ||
primaryCause instanceof AnalysisCancelledError
) {
const cancellation = regularFileCancellation(digestFailure);
if (cancellation !== undefined) {
const error = new AnalysisCancelledError(operation, {
cause: digestFailure,
cause: cancellation.reason,
cleanup: { reason, resources: [resource] },
partialObservation,
});
@@ -256,7 +259,8 @@ const adbDigestError = (
operation,
`The pulled file could not be fully digested or its handle could not be closed: ${digestFailure.path}`,
{
cause: primaryCause ?? digestFailure,
cause:
digestOutcome.kind === "failed" ? primaryCause : digestFailure,
partialObservation,
},
);
@@ -326,7 +330,10 @@ const syncStagedAdbFile = async (
}
const resource = `${path} (ADB staged-output handle)`;
const stagedOwner = owner;
const closeLease = new AnalysisCleanupLease(() => stagedOwner.close());
const closeLease = new AnalysisCleanupLease(
() => stagedOwner.close(),
[resource],
);
const released = await resources.release({
kind: "lease",
resource,
@@ -361,150 +368,6 @@ const syncStagedAdbFile = async (
if (primary !== undefined) throw primary;
};
/** adb joins shell operands into a remote POSIX command; quote at that boundary. */
const quoteShellOperand = (value: string): string =>
/^[A-Za-z0-9_./:=@+,-]+$/u.test(value)
? value
: "'" + value.replaceAll("'", "'\\''") + "'";
const adbArguments = (
serial: string | undefined,
arguments_: readonly string[],
): readonly string[] => {
const operands =
arguments_[0] === "shell"
? ["shell", ...arguments_.slice(1).map(quoteShellOperand)]
: [...arguments_];
return serial === undefined ? operands : ["-s", serial, ...operands];
};
interface AdbRunLimits {
readonly timeoutMs: number;
readonly maxOutputBytes: number;
}
/** Run one bounded adb invocation and keep its exact failure shape. */
const runAdbCapture = async (
operation: string,
targetPath: string,
binary: string,
arguments_: readonly string[],
limits: AdbRunLimits,
signal: AbortSignal | undefined,
): Promise<Result<{ stdout: Buffer; stderr: Buffer }, AnalysisError>> => {
if (signal?.aborted === true)
return err(new AnalysisCancelledError(operation, { cause: signal.reason }));
try {
return ok(
await execFileBytes(binary, arguments_, {
timeout: limits.timeoutMs,
maxBuffer: limits.maxOutputBytes,
stopSignal: "SIGTERM",
...(signal === undefined ? {} : { signal }),
}),
);
} catch (cause) {
if (signal?.aborted || execFileOutputFailure(cause)?.code === "ABORT_ERR")
return err(new AnalysisCancelledError(operation, { cause }));
const failure = execFileOutputFailure(cause);
if (failure === undefined)
return err(
new AnalysisProtocolError(`adb failed for ${operation}`, { cause }),
);
if (failure.outputTruncated)
return err(
new AnalysisResourceConstraintError(
operation,
"memory",
`adb output exceeded the ${String(limits.maxOutputBytes)}-byte capture budget`,
{ max_output_bytes: limits.maxOutputBytes },
{ cause },
),
);
const excerpt = failure.stderr.trim().slice(0, STDERR_EXCERPT_BYTES);
if (/unauthorized/iu.test(excerpt))
return err(
new AnalysisUnsupportedTargetError(operation, targetPath, excerpt, {
cause,
}),
);
if (
/device[^\n]*not found|device offline|still authorizing|more than one device/iu.test(
excerpt,
)
)
return err(
new AnalysisUnsupportedTargetError(operation, targetPath, excerpt, {
cause,
}),
);
if (
/No such file or directory|Permission denied|read: Permission denied/iu.test(
excerpt,
)
)
return err(
new AnalysisUnsupportedTargetError(operation, targetPath, excerpt, {
cause,
}),
);
if (failure.killed)
return err(
new AnalysisTimeoutError(operation, limits.timeoutMs, { cause }),
);
return err(
new AnalysisProtocolError(
excerpt === ""
? `adb exited with code ${String(failure.code)} for ${operation}`
: excerpt,
{ cause },
),
);
}
};
/** Decode textual adb protocols after the shared byte capture and error boundary. */
const runAdb = async (
...arguments_: Parameters<typeof runAdbCapture>
): Promise<
Result<{ stdout: string; stderr: string; stdoutBytes: number }, AnalysisError>
> => {
const captured = await runAdbCapture(...arguments_);
if (!captured.ok) return captured;
return ok({
stdout: captured.value.stdout.toString("utf8"),
stderr: captured.value.stderr.toString("utf8"),
// Byte count of the captured output before UTF-8 decoding, so malformed
// sequences replaced by U+FFFD still report their true captured size.
stdoutBytes: captured.value.stdout.byteLength,
});
};
/**
* adb exits nonzero when the device refuses a package change, printing the
* `Failure [...]` verdict to stderr (install) or stdout (uninstall). Read the
* refusal from that failed run so it is not reported as unreadable output.
*/
const packageChangeRefusal = (
operation: string,
serial: string,
error: AnalysisError,
): Result<never, AnalysisError> | undefined => {
if (!(error instanceof AnalysisProtocolError)) return undefined;
const failure = execFileOutputFailure(error.cause);
if (failure === undefined) return undefined;
const reason = packageChangeFailureReason(
`${failure.stdout}\n${failure.stderr}`,
);
return reason === null
? undefined
: err(
new AnalysisUnsupportedTargetError(operation, serial, reason, {
cause: error,
}),
);
};
const clientIdentityResult = (
selection: AdbBinarySelection,
identity: AdbClientIdentity,
@@ -605,7 +468,10 @@ const publishScreenExclusively = async (
// the visible file despite later cancellation so its receipt remains known.
const ownedOutput = new OwnedFileHandle(output);
const handleResource = `${outputPath} (ADB publication handle)`;
const closeOnly = new AnalysisCleanupLease(() => ownedOutput.close());
const closeOnly = new AnalysisCleanupLease(
() => ownedOutput.close(),
[handleResource],
);
const removeOutput = async (
expected: Awaited<ReturnType<typeof output.stat>>,
): Promise<void> => {
@@ -1427,7 +1293,7 @@ export class AdbProvider {
selection = this.selection();
identity = await inspectAdbClient(selection, signal);
} catch (cause) {
if (cause instanceof AnalysisCancelledError) return err(cause);
if (cause instanceof AnalysisError) return err(cause);
if (cause instanceof AdbConfigurationFailure)
return err(
providerConfigurationRefusal(
@@ -1671,7 +1537,8 @@ export class AdbProvider {
pkg_flags: details.pkgFlags,
requested_permissions_count: details.requestedPermissions,
coverage:
details.versionCode === null && details.versionName === null
details.requestedPermissions === null ||
(details.versionCode === null && details.versionName === null)
? "partial"
: "complete",
},
@@ -1679,7 +1546,8 @@ export class AdbProvider {
{
limitations: [
"Fields are a fixed projection of dumpsys package output; version-dependent sections are ignored rather than guessed",
"requested_permissions_count counts the listed requested permissions section only",
"requested_permissions_count counts the listed requested permissions section; an absent section is zero only when dumpsys confirms a loaded parsed package",
"A null requested_permissions_count means dumpsys reported pkg=null or did not establish that an omitted permission section is an empty inventory",
],
},
),
@@ -1870,38 +1738,30 @@ export class AdbProvider {
);
}
case "inspect_adb_window": {
let section = "windows";
let section: "windows" | "displays" = "windows";
const ran = await shell(["dumpsys", "window", section], {
timeoutMs: SHELL_TIMEOUT_MS,
maxOutputBytes: DUMPSYS_WINDOW_MAX_OUTPUT_BYTES,
});
if (!ran.ok) return ran;
let { focusedWindow, focusedApp } = parseWindowFocusOutput(
ran.value.stdout,
);
if (focusedWindow === null && focusedApp === null) {
let focus = parseWindowFocusOutput(ran.value.stdout);
if (focus.focus_summary === "unavailable") {
section = "displays";
const fallback = await shell(["dumpsys", "window", section], {
timeoutMs: SHELL_TIMEOUT_MS,
maxOutputBytes: DUMPSYS_WINDOW_MAX_OUTPUT_BYTES,
});
if (!fallback.ok) return fallback;
({ focusedWindow, focusedApp } = parseWindowFocusOutput(
fallback.value.stdout,
));
focus = parseWindowFocusOutput(fallback.value.stdout);
}
const declared = focusedWindow !== null || focusedApp !== null;
const declared = focus.focus_summary !== "unavailable";
return ok(
createAnalysisExecution(
{
client,
serial: request.input.serial,
focused_window: focusedWindow,
focused_app: focusedApp,
observed_lines: [
...(focusedWindow === null ? [] : [focusedWindow]),
...(focusedApp === null ? [] : [focusedApp]),
],
source_section: section,
...focus,
},
provider,
{
@@ -1910,6 +1770,11 @@ export class AdbProvider {
declared
? `Focus declarations were read from \`dumpsys window ${section}\``
: "Neither `dumpsys window windows` nor `dumpsys window displays` declared mCurrentFocus or mFocusedApp; focus is unknown, not absent",
...(focus.focus_summary === "ambiguous"
? [
"Multiple focus contexts or repeated declarations prevent a singular focus summary; inspect focus_contexts for every declaration",
]
: []),
],
},
),
@@ -2221,7 +2086,14 @@ export class AdbProvider {
device_path: string | null;
stage: "resolve_paths" | "pull" | "digest";
message: string;
cause: unknown;
}[] = [];
const observedFailures = () =>
failures.map(({ device_path, stage, message }) => ({
device_path,
stage,
message,
}));
const cancelledPackagePull = (
error: AnalysisCancelledError,
localPath: string,
@@ -2238,7 +2110,7 @@ export class AdbProvider {
output_directory: directory.value,
artifacts,
failures: [
...failures,
...observedFailures(),
{
device_path: devicePath,
stage,
@@ -2275,6 +2147,7 @@ export class AdbProvider {
device_path: entry.device_path,
stage: "pull",
message: pulled.error.message,
cause: pulled.error,
});
continue;
}
@@ -2325,7 +2198,7 @@ export class AdbProvider {
},
],
failures: [
...failures,
...observedFailures(),
{
device_path: entry.device_path,
stage: "digest",
@@ -2343,7 +2216,6 @@ export class AdbProvider {
const cleanupError = adbDigestError(
cause,
operation,
signal,
partialObservation,
);
if (cleanupError !== undefined) return err(cleanupError);
@@ -2354,16 +2226,24 @@ export class AdbProvider {
entry.device_path,
"digest",
);
const physicalCause = stableFileHashCause(cause);
failures.push({
device_path: entry.device_path,
stage: "digest",
message: `The pulled file could not be read back after pulling: ${localPath}`,
message: `The pulled file could not be read back after pulling: ${localPath}: ${physicalCause instanceof Error ? physicalCause.message : String(physicalCause)}`,
cause,
});
}
}
if (artifacts.length === 0 && failures.length > 0) {
const primary = new AnalysisProtocolError(
`Every APK pull failed for ${packageName} on ${serial}: ${failures[0]!.message}`,
{
cause: new AggregateError(
failures.map(({ cause }) => cause),
"APK pulls failed",
),
},
);
return failOwnedPackage(primary);
}
@@ -2375,7 +2255,7 @@ export class AdbProvider {
package_name: packageName,
output_directory: directory.value,
artifacts,
failures,
failures: observedFailures(),
coverage: failures.length === 0 ? "complete" : "partial",
},
provider,
@@ -2466,12 +2346,7 @@ export class AdbProvider {
digest_status: "unknown",
},
};
const cleanupError = adbDigestError(
cause,
operation,
signal,
partialObservation,
);
const cleanupError = adbDigestError(cause, operation, partialObservation);
if (cleanupError !== undefined) return err(cleanupError);
if (cause instanceof AnalysisCancelledError) {
cause.retainPartialObservation(partialObservation);
@@ -2529,28 +2404,16 @@ export class AdbProvider {
operation,
signal,
);
// `test -e` exits nonzero with no adb diagnosis when the path is absent.
// Any other probe failure leaves existence unknown, so it is reported as
// such rather than treated as absence or followed by a push.
const existsProbe = await runAdb(
const existsProbe = await probeAdbPathExistence(
operation,
input.serial,
input.device_path,
selection.binary,
adbArguments(input.serial, [
"shell",
"test",
"-e",
input.device_path,
]),
{ timeoutMs: SETTINGS_TIMEOUT_MS, maxOutputBytes: 1024 },
signal,
);
if (
!existsProbe.ok &&
!(existsProbe.error instanceof AnalysisProtocolError)
)
return existsProbe;
const deviceExists = existsProbe.ok;
if (!existsProbe.ok) return existsProbe;
const deviceExists = existsProbe.value;
if (deviceExists && !input.overwrite)
return err(
new AnalysisOutputError(
@@ -2760,7 +2623,14 @@ export class AdbProvider {
operation,
input.serial,
selection.binary,
adbArguments(input.serial, ["uninstall", input.package]),
adbArguments(input.serial, [
"shell",
"pm",
"uninstall",
"--user",
"0",
input.package,
]),
{
timeoutMs: PACKAGE_LIST_TIMEOUT_MS,
maxOutputBytes: PULL_MAX_OUTPUT_BYTES,
@@ -2791,7 +2661,7 @@ export class AdbProvider {
provider,
{
limitations: [
"Uninstall removes the package and its data for user 0; keeping data (-k) is not exposed",
"Uninstall explicitly requests user 0 without data retention (-k); device enforcement is not independently verified",
],
},
),
@@ -3293,7 +3163,7 @@ export class AdbProvider {
const cleanupError =
retainedObservation === undefined
? undefined
: adbDigestError(cause, operation, signal, retainedObservation);
: adbDigestError(cause, operation, retainedObservation);
if (cleanupError !== undefined) return err(cleanupError);
if (cause instanceof AnalysisCancelledError) {
if (retainedObservation !== undefined)
+199 -1
View File
@@ -1,6 +1,204 @@
import { describe, expect, it } from "vitest";
import { parseAmStartOutput, parseWmDensityOutput } from "./AdbShellOutput.js";
import {
parseAmStartOutput,
parseDumpsysPackageOutput,
parseWmDensityOutput,
parseWindowFocusOutput,
} from "./AdbShellOutput.js";
describe("parseWindowFocusOutput display contexts", () => {
it.each([false, true])(
"retains both display owners independent of block order: %s",
(reverse) => {
const inactive =
" Display: mDisplayId=3 rootTasks=0\n mCurrentFocus=null\n mFocusedApp=null\n";
const active =
" Display: mDisplayId=0 rootTasks=12\n mCurrentFocus=Window{active}\n mFocusedApp=ActivityRecord{active}\n";
const result = parseWindowFocusOutput(
reverse ? active + inactive : inactive + active,
);
expect(result).toMatchObject({
focused_window: null,
focused_app: null,
focus_summary: "ambiguous",
focus_contexts: expect.arrayContaining([
{
scope: "display",
display_id: "3",
header_line: " Display: mDisplayId=3 rootTasks=0",
declarations: [
{
field: "mCurrentFocus",
value: null,
raw_line: " mCurrentFocus=null",
},
{
field: "mFocusedApp",
value: null,
raw_line: " mFocusedApp=null",
},
],
},
{
scope: "display",
display_id: "0",
header_line: " Display: mDisplayId=0 rootTasks=12",
declarations: [
{
field: "mCurrentFocus",
value: "Window{active}",
raw_line: " mCurrentFocus=Window{active}",
},
{
field: "mFocusedApp",
value: "ActivityRecord{active}",
raw_line: " mFocusedApp=ActivityRecord{active}",
},
],
},
]),
});
},
);
it("does not pair declarations from different displays", () => {
expect(
parseWindowFocusOutput(
"Display: mDisplayId=0\nmCurrentFocus=Window{one}\nDisplay: mDisplayId=1\nmFocusedApp=ActivityRecord{two}\n",
),
).toMatchObject({
focus_summary: "ambiguous",
focused_window: null,
focused_app: null,
focus_contexts: [
{
display_id: "0",
declarations: [{ field: "mCurrentFocus", value: "Window{one}" }],
},
{
display_id: "1",
declarations: [
{ field: "mFocusedApp", value: "ActivityRecord{two}" },
],
},
],
});
});
it("retains an unknown display identity and missing fields without borrowing its neighbor", () => {
expect(
parseWindowFocusOutput(
"Display: vendor-format\nmCurrentFocus=null\nDisplay: mDisplayId=0\n",
),
).toMatchObject({
focus_summary: "ambiguous",
focus_contexts: [
{
scope: "display",
display_id: null,
header_line: "Display: vendor-format",
declarations: [{ field: "mCurrentFocus", value: null }],
},
{ scope: "display", display_id: "0", declarations: [] },
],
});
});
});
describe("parseWindowFocusOutput declaration presence", () => {
it("separates missing declarations from explicit absence and retains source lines", () => {
expect(parseWindowFocusOutput("no declarations\n")).toEqual({
focused_window: null,
focused_app: null,
focus_summary: "unavailable",
focus_contexts: [],
observed_lines: [],
});
expect(
parseWindowFocusOutput(" mCurrentFocus=null\nmFocusedApp=null\n"),
).toEqual({
focused_window: null,
focused_app: null,
focus_summary: "single_context",
focus_contexts: [
{
scope: "unscoped",
display_id: null,
header_line: null,
declarations: [
{
field: "mCurrentFocus",
value: null,
raw_line: " mCurrentFocus=null",
},
{ field: "mFocusedApp", value: null, raw_line: "mFocusedApp=null" },
],
},
],
observed_lines: [" mCurrentFocus=null", "mFocusedApp=null"],
});
});
it("retains repeated declarations without choosing one or erasing explicit absence", () => {
expect(
parseWindowFocusOutput(
"mCurrentFocus=null\nmCurrentFocus=Window{later}\nmFocusedApp=ActivityRecord{current}\n",
),
).toMatchObject({
focus_summary: "ambiguous",
focused_window: null,
focused_app: null,
focus_contexts: [
{
declarations: [
{ field: "mCurrentFocus", value: null },
{ field: "mCurrentFocus", value: "Window{later}" },
{ field: "mFocusedApp", value: "ActivityRecord{current}" },
],
},
],
observed_lines: [
"mCurrentFocus=null",
"mCurrentFocus=Window{later}",
"mFocusedApp=ActivityRecord{current}",
],
});
});
});
describe("parseDumpsysPackageOutput", () => {
it("reports unknown requested permissions when the package is null", () => {
expect(
parseDumpsysPackageOutput("Package [com.example] (abcdef):\n pkg=null\n")
.requestedPermissions,
).toBeNull();
});
it("reports unknown when package state and the permission section are absent", () => {
expect(
parseDumpsysPackageOutput(
"Package [com.example] (abcdef):\n versionCode=45\n",
).requestedPermissions,
).toBeNull();
});
it("reports zero only for a confirmed loaded package without the section", () => {
expect(
parseDumpsysPackageOutput(
"Package [com.example] (abcdef):\n pkg=Package{abcdef com.example}\n",
).requestedPermissions,
).toBe(0);
});
it("counts listed permissions when the section is available", () => {
expect(
parseDumpsysPackageOutput(
"Package [com.example] (abcdef):\n requested permissions:\n android.permission.INTERNET\n",
).requestedPermissions,
).toBe(1);
});
});
describe("parseWmDensityOutput", () => {
it("reads labeled physical and override densities", () => {
+88 -14
View File
@@ -1,3 +1,8 @@
import type {
AdbWindowFocusContext,
AdbWindowFocusObservation,
} from "../domain/adb/adbDeviceAnalysis.js";
/**
* Parsers for fixed observation commands REA runs through `adb shell`.
*
@@ -181,23 +186,75 @@ export const parseWmDensityOutput = (
return { physical: physical ?? bare, override };
};
/** Parse focus declarations from `dumpsys window windows` or `displays`. */
/** Own declaration identity and omission before deriving a singular summary. */
export const parseWindowFocusOutput = (
stdout: string,
): {
focusedWindow: string | null;
focusedApp: string | null;
} => {
let focusedWindow = null;
let focusedApp = null;
): AdbWindowFocusObservation => {
const contexts: AdbWindowFocusContext[] = [];
const observedLines: string[] = [];
let current: AdbWindowFocusContext | undefined;
for (const line of stdout.split(/\r?\n/u)) {
const trimmed = line.trim();
if (focusedWindow === null && trimmed.startsWith("mCurrentFocus="))
focusedWindow = trimmed.slice("mCurrentFocus=".length).trim();
if (focusedApp === null && trimmed.startsWith("mFocusedApp="))
focusedApp = trimmed.slice("mFocusedApp=".length).trim();
if (trimmed.startsWith("Display:")) {
current = {
scope: "display",
display_id:
/^Display:\s+mDisplayId=(-?[0-9]+)(?:\s|$)/u.exec(trimmed)?.[1] ??
null,
header_line: line,
declarations: [],
};
contexts.push(current);
observedLines.push(line);
continue;
}
const field = trimmed.startsWith("mCurrentFocus=")
? "mCurrentFocus"
: trimmed.startsWith("mFocusedApp=")
? "mFocusedApp"
: undefined;
if (field === undefined) continue;
if (current === undefined) {
current = {
scope: "unscoped",
display_id: null,
header_line: null,
declarations: [],
};
contexts.push(current);
}
const value = trimmed.slice(field.length + 1).trim();
current.declarations.push({
field,
value: value === "null" ? null : value,
raw_line: line,
});
observedLines.push(line);
}
return { focusedWindow, focusedApp };
const windows =
contexts.length === 1
? contexts[0]!.declarations.filter(
({ field }) => field === "mCurrentFocus",
)
: [];
const apps =
contexts.length === 1
? contexts[0]!.declarations.filter(({ field }) => field === "mFocusedApp")
: [];
const declared = contexts.some(({ declarations }) => declarations.length > 0);
const summary = !declared
? "unavailable"
: contexts.length === 1 && windows.length <= 1 && apps.length <= 1
? "single_context"
: "ambiguous";
return {
focused_window:
summary === "single_context" ? (windows[0]?.value ?? null) : null,
focused_app: summary === "single_context" ? (apps[0]?.value ?? null) : null,
focus_summary: summary,
focus_contexts: contexts,
observed_lines: observedLines,
};
};
export interface AdbPackageDetails {
@@ -240,9 +297,12 @@ export const parseDumpsysPackageOutput = (
pkgFlags: null,
};
let inPermissions = false;
let hasPermissionsSection = false;
let permissions = 0;
let parsedPackageAvailable: boolean | null = null;
for (const line of stdout.split(/\r?\n/u)) {
if (line.trim().startsWith("requested permissions:")) {
hasPermissionsSection = true;
inPermissions = true;
continue;
}
@@ -259,7 +319,11 @@ export const parseDumpsysPackageOutput = (
if (match === null) continue;
const key = match[1]!;
const value = match[2]!.trim();
if (key === "versionName" && details.versionName === null)
if (key === "pkg" && parsedPackageAvailable === null) {
if (value === "null") parsedPackageAvailable = false;
else if (value.startsWith("Package{") && value.endsWith("}"))
parsedPackageAvailable = true;
} else if (key === "versionName" && details.versionName === null)
details.versionName = value === "" ? null : value;
else if (key === "versionCode" && details.versionCode === null) {
const parsed = /^(-?\d+)/u.exec(value);
@@ -279,7 +343,17 @@ export const parseDumpsysPackageOutput = (
} else if (key === "pkgFlags" && details.pkgFlags === null)
details.pkgFlags = value === "" ? null : value;
}
return { ...details, requestedPermissions: permissions };
// `dumpsys` omits this section both for known-empty inventories and when
// Package Manager has no parsed package; only the former is a known zero.
const requestedPermissions =
parsedPackageAvailable === false
? null
: hasPermissionsSection
? permissions
: parsedPackageAvailable === true
? 0
: null;
return { ...details, requestedPermissions };
};
/**
+7 -6
View File
@@ -10,6 +10,7 @@ import {
} from "../domain/analysisErrorCore.js";
import {
openRegularFile,
assertRegularFileActive,
sameRegularFileState,
} from "../filesystem/RegularFile.js";
import type { AndroidOperation } from "../domain/android/androidAnalysis.js";
@@ -25,7 +26,7 @@ export const snapshotAndroidTarget = async (
const snapshot = join(root, "target.apk");
try {
await copyAndroidSnapshot(path, snapshot, sha256, signal);
signal?.throwIfAborted();
assertRegularFileActive(signal);
await chmod(snapshot, 0o400);
return snapshot;
} catch (cause: unknown) {
@@ -51,7 +52,7 @@ export const snapshotAndroidEngine = async (
const path = join(root, "engine.jar");
try {
const observed = await copyAndroidSnapshot(source, path, sha256, signal);
signal?.throwIfAborted();
assertRegularFileActive(signal);
await chmod(path, 0o400);
return { path, sha256: observed };
} catch (cause: unknown) {
@@ -71,7 +72,7 @@ const copyAndroidSnapshot = async (
expectedSha256: string,
signal?: AbortSignal,
): Promise<string> => {
signal?.throwIfAborted();
assertRegularFileActive(signal);
const source = await openRegularFile(sourcePath, {
symlinks: "follow",
signal,
@@ -84,7 +85,7 @@ const copyAndroidSnapshot = async (
const hash = createHash("sha256");
try {
const initial = await source.stat();
signal?.throwIfAborted();
assertRegularFileActive(signal);
const digest = new Transform({
transform(
chunk: Buffer,
@@ -106,7 +107,7 @@ const copyAndroidSnapshot = async (
});
try {
destination = await open(snapshotPath, "wx", 0o600);
signal?.throwIfAborted();
assertRegularFileActive(signal);
inputStream = source.createReadStream({
start: 0,
end: initial.size,
@@ -114,7 +115,7 @@ const copyAndroidSnapshot = async (
});
outputStream = destination.createWriteStream({ autoClose: false });
await pipeline(inputStream, digest, outputStream, { signal });
signal?.throwIfAborted();
assertRegularFileActive(signal);
await verifySourceState(source, sourcePath, initial, copiedBytes);
const observedSha256 = hash.digest("hex");
if (observedSha256 !== expectedSha256)
+42 -31
View File
@@ -27,10 +27,17 @@ import {
snapshotAndroidEngine,
snapshotAndroidTarget,
} from "./AndroidTargetSnapshot.js";
import { hashStableFile } from "../filesystem/StableFileHash.js";
import {
hashStableFile,
stableFileHashCause,
StableFileHashFailure,
} from "../filesystem/StableFileHash.js";
import type { OwnedFileHandle } from "../filesystem/OwnedFileHandle.js";
import { RegularFileChangedError } from "../filesystem/RegularFile.js";
import { RegularFileCleanupFailure } from "../filesystem/OwnedRegularFile.js";
import {
RegularFileCleanupFailure,
regularFileCancellation,
} from "../filesystem/OwnedRegularFile.js";
import {
inspectJadxAvailability,
resolveJadxConfigurationInputs,
@@ -70,35 +77,33 @@ const executionError = (context: {
session: JadxSession | undefined;
}): AnalysisError => {
const { cause, request, timeout, timeoutMs, signal, session } = context;
const cancellation = regularFileCancellation(cause);
const cancellationError =
cancellation === undefined
? undefined
: timeout.aborted && cancellation.reason === timeout.reason
? new AnalysisTimeoutError(request.operation, timeoutMs, {
cause: cancellation.reason,
})
: new AnalysisCancelledError(request.operation, {
cause: cancellation.reason,
});
if (cause instanceof RegularFileCleanupFailure) {
const primaryCause =
cause.outcome.kind === "failed"
? cause.outcome.cause
: signal?.aborted
? signal.reason
: timeout.aborted
? timeout.reason
: undefined;
const primaryOptions =
primaryCause === undefined ? {} : { cause: primaryCause };
? stableFileHashCause(cause.outcome.cause)
: cause;
const primary =
signal?.aborted === true
? new AnalysisCancelledError(request.operation, primaryOptions)
: timeout.aborted
? new AnalysisTimeoutError(
request.operation,
timeoutMs,
primaryOptions,
)
: primaryCause instanceof AnalysisError
? primaryCause
: new ProviderAdapterError("jadx", request.operation, {
cause: primaryCause ?? cause,
userMessage:
primaryCause instanceof Error
? primaryCause.message
: cause.message,
});
cancellationError ??
(primaryCause instanceof AnalysisError
? primaryCause
: new ProviderAdapterError("jadx", request.operation, {
cause: primaryCause,
userMessage:
primaryCause instanceof Error
? primaryCause.message
: String(primaryCause),
}));
const cleanup = new ProviderCleanupError(
"jadx",
[cause.path],
@@ -112,11 +117,17 @@ const executionError = (context: {
);
return analysisErrorWithCleanupFailure(primary, cleanup, request.operation);
}
if (cancellationError !== undefined) return cancellationError;
if (cause instanceof StableFileHashFailure)
return new ProviderAdapterError("jadx", request.operation, {
cause: cause.cause,
userMessage: cause.message,
});
if (cause instanceof AnalysisError) return cause;
if (signal?.aborted === true)
return new AnalysisCancelledError(request.operation, { cause });
if (timeout.aborted)
return new AnalysisTimeoutError(request.operation, timeoutMs);
if (cause instanceof AnalysisError) return cause;
const protocolFailure = session?.transport.failureReason();
if (protocolFailure !== undefined && protocolFailure !== null)
return new AnalysisOutputError(request.operation, protocolFailure, {
@@ -193,9 +204,9 @@ const hashAndroidSource = async (
try {
return (await hashStableFile(path, { signal, retainFailedClose })).sha256;
} catch (cause: unknown) {
if (signal.aborted) throw cause;
if (cause instanceof RegularFileChangedError)
throw androidSourceChanged(operation, path, cause);
const physical = stableFileHashCause(cause);
if (physical instanceof RegularFileChangedError)
throw androidSourceChanged(operation, path, physical);
throw cause;
}
};
+40
View File
@@ -127,6 +127,22 @@ describe("parseManifestPackage", () => {
expect(parseManifestPackage("<manifest>\n</manifest>\n")).toBeNull();
});
it("reads the package when unrelated manifest text contains U+FFFD", () => {
expect(
parseManifestPackage(
'<manifest package="org.rea.app"><application label="\uFFFD"/></manifest>',
),
).toBe("org.rea.app");
});
it("still rejects malformed attributes alongside U+FFFD", () => {
expect(() =>
parseManifestPackage(
'<manifest package=org.rea.app><application label="\uFFFD"/></manifest>',
),
).toThrow(/AndroidManifest.xml is not well-formed XML/u);
});
it("rejects a well-formed document with the wrong root element", () => {
expect(() => parseManifestPackage("<resources/>")).toThrowError(
new TypeError("AndroidManifest.xml root must be <manifest>"),
@@ -172,6 +188,30 @@ describe("parseDecodedStrings", () => {
);
});
it("preserves literal U+FFFD and unaffected sibling strings", () => {
expect(
parseDecodedStrings(
'<resources><string name="good">Hello</string><string name="edge">\uFFFD</string></resources>',
),
).toEqual({
entries: [
{ name: "good", value: "Hello" },
{ name: "edge", value: "\uFFFD" },
],
unparsedCount: 0,
});
});
it.each([
"<resources><string name=unquoted>\uFFFD</string></resources>",
'<resources><string name="broken">\uFFFD</resources>',
])("still rejects malformed XML containing U+FFFD: %s", (xml) => {
expect(parseDecodedStrings(xml)).toEqual({
entries: [],
unparsedCount: 1,
});
});
it("keeps empty, self-closing, and reordered string attributes", () => {
expect(
parseDecodedStrings(
+10 -14
View File
@@ -1,9 +1,5 @@
import {
DOMParser,
type Document,
type Element,
type Node,
} from "@xmldom/xmldom";
import type { Document, Element, Node } from "@xmldom/xmldom";
import { parseXmlDocument } from "../domain/xmlDocument.js";
import {
isMap,
isScalar,
@@ -147,16 +143,16 @@ const yamlScalar = (
};
const parseXml = (xml: string, label: string) => {
const errors: string[] = [];
const document = new DOMParser({
normalizeLineEndings: (source) => source,
onError: (_level, message) => errors.push(message),
}).parseFromString(xml, "application/xml");
if (errors.length > 0 || document.documentElement === null)
const parsed = parseXmlDocument(xml, {
doctype: "allow",
lineEndings: "preserve",
});
if (!parsed.ok)
throw new TypeError(
`${label} is not well-formed XML${errors[0] === undefined ? "" : `: ${errors[0]}`}`,
`${label} is not well-formed XML: ${parsed.error.message}`,
{ cause: parsed.error },
);
return document;
return parsed.value;
};
const childNodes = (node: Node): Node[] => {
+77 -4
View File
@@ -11,8 +11,12 @@ import { join } from "node:path";
import { describe, expect, onTestFinished, test } from "vitest";
import { ApktoolProvider } from "./ApktoolProvider.js";
import { ApktoolResourceAnalysisService } from "../application/apktool/ApktoolResourceAnalysisService.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { apktoolRequestSchema } from "../domain/apktool/apktoolResourceAnalysis.js";
import {
apktoolRequestSchema,
apktoolResultSchemas,
} from "../domain/apktool/apktoolResourceAnalysis.js";
import type { ApktoolRequest } from "../domain/apktool/apktoolResourceAnalysis.js";
const it = test.skipIf(process.platform === "win32");
@@ -263,6 +267,59 @@ it("preserves empty resource strings and reports markup omissions as partial", a
expect(result.coverage).toBe("partial");
});
it("preserves literal U+FFFD in decoded XML with complete coverage", async () => {
const stringsXml =
'<resources><string name="good">Hello</string><string name="edge">\uFFFD</string></resources>';
const { command, root } = await writeApktoolStub((root) =>
decodeStub({
stringsXml,
manifestFile: join(root, "AndroidManifest.xml"),
})(root),
);
const manifest = REAL_MANIFEST.replace("@string/app_name", "\uFFFD");
await writeFile(join(root, "AndroidManifest.xml"), manifest, "utf8");
const apk = await apkFixture(root);
const result = await evidenceResult(providerOver(command), {
operation: "decode_android_resources",
input: { path: apk.path, include_strings: true },
});
expect(result).toMatchObject({
manifest,
metadata: { package_name: "com.rea.apktool.probe" },
strings: [
{ name: "good", value: "Hello" },
{ name: "edge", value: "\uFFFD" },
],
coverage: "complete",
});
});
it("still refuses invalid UTF-8 bytes in decoded strings", async () => {
const { command, root } = await writeApktoolStub((root) =>
decodeStub({ stringsFile: join(root, "strings.xml") })(root),
);
await writeFile(
join(root, "strings.xml"),
Buffer.concat([
Buffer.from('<resources><string name="bad">'),
Buffer.from([0xff]),
Buffer.from("</string></resources>"),
]),
);
const apk = await apkFixture(root);
const result = await providerOver(command).execute({
operation: "decode_android_resources",
input: { path: apk.path, include_strings: true },
});
expect(result.ok).toBe(false);
if (result.ok) throw new Error("expected a refusal");
expect(projectAnalysisError(result.error)).toMatchObject({
code: "unreadable_output",
});
expect(result.error.message).toContain("invalid UTF-8");
expect(result.error.message).toContain("strings.xml");
});
it("ignores Android resource qualifiers when discovering locales", async () => {
const { command, root } = await writeApktoolStub(
decodeStub({ extraValuesDirectories: ["car", "night", "v21"] }),
@@ -438,10 +495,26 @@ it("uses an owned APK snapshot and framework cache and resolves regional locales
),
);
const apk = await apkFixture(root);
const result = await evidenceResult(providerOver(command), {
operation: "decode_android_resources",
input: { path: apk.path, include_strings: true, locale: "pt-BR" },
const observation = await new ApktoolResourceAnalysisService(
providerOver(command),
).execute("decode_android_resources", {
path: apk.path,
include_strings: true,
locale: "pt-BR",
});
if (!observation.ok) throw observation.error;
expect(observation.value.subject).toMatchObject({
local_path: apk.path,
format: "apk",
digest: { sha256: apk.sha256 },
});
expect(observation.value.limitations).not.toContain(
"Artifact identity is unavailable for this observation.",
);
expect(await readFile(apk.path, "utf8")).toBe("changed original");
const result = apktoolResultSchemas.decode_android_resources.parse(
observation.value.normalized_result,
);
expect(result.target).toMatchObject({
sha256: apk.sha256,
bytes: apk.bytes,
+11
View File
@@ -1,3 +1,4 @@
import { regularFileCancellation } from "../filesystem/OwnedRegularFile.js";
import { createHash, randomUUID } from "node:crypto";
import { createWriteStream } from "node:fs";
import {
@@ -800,6 +801,7 @@ export class ApktoolProvider {
},
provider,
{
subject: { path: input.path, sha256, format: "apk" },
limitations: [
"Decoding skips smali (--no-src); Java-source recovery belongs to the JADX family",
...(input.locale !== undefined &&
@@ -846,6 +848,15 @@ export class ApktoolProvider {
);
} catch (cause) {
await removeWorkspace();
const cancellation = regularFileCancellation(cause);
if (cancellation !== undefined)
return err(
failureWithCleanup(
new AnalysisCancelledError(operation, {
cause: cancellation.reason,
}),
),
);
return err(
failureWithCleanup(
signal?.aborted === true
+53 -6
View File
@@ -1,3 +1,4 @@
import type { ProviderInspectionScope } from "../providers/ProviderInspectionScope.js";
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import type { OfficialToolName } from "../contracts/officialToolContracts.js";
import type { EnhancedToolName } from "../contracts/enhancedInputs.js";
@@ -8,9 +9,13 @@ import type { AnalysisError } from "../domain/analysisErrorBase.js";
import type { AnalysisProfileCommitment } from "../domain/analysisProfile.js";
import type { JsonValue } from "../domain/jsonValue.js";
import type { Result } from "../domain/result.js";
import type { EvidenceLocation } from "../domain/evidence.js";
import type { EvidenceSubjectTarget } from "../domain/evidence.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
import {
createEvidence,
type Evidence,
type EvidenceLocation,
type EvidenceSubjectTarget,
} from "../domain/evidence.js";
import { jsonObjectSchema, jsonValueSchema } from "../domain/jsonValue.js";
import type { ProgressReporter } from "./ProgressReporter.js";
import type { ProviderRejectionCode } from "../contracts/providerSelection.js";
import type { ProcessLineageObservation } from "../process/ProcessOwnership.js";
@@ -39,6 +44,9 @@ export type AnalysisOperation =
| NativeToolName
| ArtifactAnalysisOperation
| ManagedToolName
// Provider aggregate and composed public overview have distinct Evidence and
// snapshot identities. This capability does not add a CLI/MCP tool.
| "read_binary_inventory_summary"
| "health";
/** Atomic successful provider observation with exact producing provenance. */
@@ -119,6 +127,26 @@ export function createAnalysisExecution(
};
}
/**
* Compose one completed provider execution into its caller-visible Evidence,
* so success and a late cancellation that retains it carry identical facts.
*/
export const createAnalysisExecutionEvidence = (
operation: string,
input: unknown,
execution: AnalysisExecution,
confidence: "observed" | "derived",
): Evidence =>
createEvidence(execution.subject ?? undefined, execution.provider, {
operation,
parameters: jsonObjectSchema.parse(input),
result: execution.result,
rawResult: execution.rawResult,
limitations: execution.limitations,
locations: execution.locations,
confidence,
});
/** Provider-neutral application capability for one analysis operation. */
export interface AnalysisOperationPort {
execute(
@@ -158,6 +186,8 @@ export interface AnalysisClient extends AnalysisOperationPort {
runtimeLineageSnapshots?(): readonly ProviderRuntimeLineageSnapshot[];
/** Current provider work that may outlive a timed-out or cancelled caller. */
requestActivitySnapshots?(): readonly ProviderRequestActivitySnapshot[];
/** Local reuse admission is separate from observed native process health. */
reuseReadiness?(): "ready" | "blocked" | "unknown";
/** Current operational health for the active provider bridge. */
operationHealthSnapshot?(): ProviderOperationHealth | undefined;
/** Close owned resources and retain failed cleanup for a later retry. */
@@ -191,8 +221,12 @@ export interface AnalysisProfileResolution {
}
/** Cancellation context for side-effect-free provider profile resolution. */
export interface AnalysisProfileResolutionOptions {
export interface AnalysisProviderInspectionOptions {
readonly signal?: AbortSignal;
readonly scope?: ProviderInspectionScope;
}
export interface AnalysisProfileResolutionOptions extends AnalysisProviderInspectionOptions {
readonly fileCloseResources?: FileCloseResourceOwner;
}
@@ -290,12 +324,25 @@ export interface AnalysisProviderReadinessSnapshot {
/** Deep provider candidate discoverable before one target-bound route is chosen. */
export interface AnalysisProviderCandidate extends AnalysisProvider {
/**
* Own inspection through final reads and cleanup. Observe the supplied owner
* signal cooperatively; resolve only after finalization settles, retaining any
* failed cleanup capability. Registry cancellation and shutdown join this
* completion rather than abandoning the promise.
*/
resolveAnalysisProfile(
target: BinaryTarget,
options?: AnalysisProfileResolutionOptions,
): Promise<Result<AnalysisProfileResolution, AnalysisError>>;
inspectAvailability(): ProviderAvailability;
inspectReadinessSnapshot?(): AnalysisProviderReadinessSnapshot;
/** Await prerequisite observations and finalization under the supplied owner signal. */
inspectAvailability(
options?: AnalysisProviderInspectionOptions,
): ProviderAvailability | Promise<ProviderAvailability>;
inspectReadinessSnapshot?(
options?: AnalysisProviderInspectionOptions,
):
| AnalysisProviderReadinessSnapshot
| Promise<AnalysisProviderReadinessSnapshot>;
inspectTargetSupport(target: BinaryTarget): ProviderTargetSupport;
/** Observe retained candidate cleanup without starting or retrying work. */
hasPendingOwnedResources?(): boolean;
+61
View File
@@ -0,0 +1,61 @@
import { expect, it } from "vitest";
import { EnhancedTools } from "./EnhancedTools.js";
import { createAnalysisExecution } from "./AnalysisProvider.js";
import {
AnalysisCancelledError,
AnalysisCapabilityUnavailableError,
AnalysisOutputError,
} from "../domain/analysisErrorCore.js";
import { err, ok } from "../domain/result.js";
it("uses the provider-owned overview without requesting complete inventories", async () => {
const calls: string[] = [];
const result = {
document: "selected",
procedure_count: 12,
string_count: 34,
};
const tools = new EnhancedTools({
execute: async (operation) => {
calls.push(operation);
return ok(
createAnalysisExecution(result, {
id: "fixture",
name: "Fixture",
version: "1",
}),
);
},
});
expect(await tools.execute("binary_overview", {})).toEqual(ok(result));
expect(calls).toEqual(["read_binary_inventory_summary"]);
});
it.each([
new AnalysisOutputError("binary_overview", "Malformed aggregate"),
new AnalysisCancelledError("binary_overview"),
new AnalysisCapabilityUnavailableError(
"fixture",
"binary_overview",
"Cleanup pending",
{
cleanup: {
reason: "Owned process is still closing",
resources: ["session"],
},
},
),
])(
"preserves an aggregate failure rather than silently repeating analysis: %s",
async (failure) => {
const calls: string[] = [];
const tools = new EnhancedTools({
execute: async (operation) => {
calls.push(operation);
return err(failure);
},
});
expect(await tools.execute("binary_overview", {})).toEqual(err(failure));
expect(calls).toEqual(["read_binary_inventory_summary"]);
},
);
+8
View File
@@ -1,3 +1,4 @@
import { regularFileCancellation } from "../filesystem/OwnedRegularFile.js";
import { constants, type Stats } from "node:fs";
import { createHash } from "node:crypto";
import {
@@ -224,6 +225,13 @@ export const parseBinaryTarget = async (
}
} catch (cause: unknown) {
if (cause instanceof AnalysisCancelledError) return err(cause);
const cancellation = regularFileCancellation(cause);
if (cancellation !== undefined)
return err(
new AnalysisCancelledError("resolve_binary_target", {
cause: cancellation.reason,
}),
);
if (signal?.aborted === true && cause === signal.reason)
return err(
new AnalysisCancelledError("resolve_binary_target", { cause }),
@@ -561,6 +561,24 @@ it("reports host denial before target mismatch but keeps composed workflow prere
});
describe("binary_overview capability requirements", () => {
it("admits a native overview without requiring full inventory operations", () => {
expect(
entry(
"binary_overview",
status({
open: true,
kind: "executable",
capabilities: [
{
operation: "read_binary_inventory_summary",
available: true,
reason: null,
},
],
}),
),
).toMatchObject({ available: true, reason: "available" });
});
it("admits a provider with current_document and other mandatory capabilities", () => {
const capabilities = [
"current_document",
+5
View File
@@ -679,6 +679,11 @@ const composedAvailability = (
name: string,
descriptors: ReadonlyMap<string, ProviderDescriptor>,
): Availability => {
if (
name === "binary_overview" &&
descriptors.get("read_binary_inventory_summary")?.available === true
)
return { reason: "available", remediation: null };
const requirements = ENHANCED_REQUIREMENTS[name];
if (requirements === undefined)
return {
+18 -2
View File
@@ -82,6 +82,16 @@ type DirectAnalysisTool =
| "trace_native_ui_action"
| "trace_native_values";
// These one-shot CLI questions require native utilities, not a deep engine.
// Runtime observation retains the ordinary session and authority checks.
const NATIVE_INSPECTION_OPERATIONS: ReadonlySet<string> = new Set([
"inspect_macho",
"inspect_signature",
"list_architectures",
"inspect_plist",
"demangle_swift",
] satisfies readonly NativeToolName[]);
const ARTIFACT_ANALYSIS_OPERATION_SET: ReadonlySet<string> = new Set(
ARTIFACT_ANALYSIS_OPERATIONS,
);
@@ -152,7 +162,9 @@ export const runProviderAnalysis = async (
signal: operationSignal,
...(ARTIFACT_ANALYSIS_OPERATION_SET.has(tool)
? { createSession: dependencies.createArtifactBinarySession }
: {}),
: NATIVE_INSPECTION_OPERATIONS.has(tool)
? { createSession: dependencies.createNativeBinarySession }
: {}),
}),
);
};
@@ -236,7 +248,11 @@ const runAnalysis = async (
} else {
const config = dependencies.readConfiguration();
if (!config.ok) return cliError(config.error);
session = await dependencies.createBinarySession(config.value, logger);
session = await (
tool === "inspect_native_dispatch_metadata" && snapshotPath === undefined
? dependencies.createNativeDispatchBinarySession
: dependencies.createBinarySession
)(config.value, logger);
}
return withSessionCleanup(
session,
@@ -11,10 +11,19 @@ export interface DirectAnalysisDependencies {
config: AppConfig,
logger: Logger,
) => BinarySession | Promise<BinarySession>;
/** Prefer byte-backed dispatch inspection while preserving explicit selection. */
readonly createNativeDispatchBinarySession: (
config: AppConfig,
logger: Logger,
) => BinarySession | Promise<BinarySession>;
/** Create an artifact-only session without selecting native analysis engines. */
readonly createArtifactBinarySession: () =>
| BinarySession
| Promise<BinarySession>;
/** Create a native utility session without selecting deep analysis engines. */
readonly createNativeBinarySession: () =>
| BinarySession
| Promise<BinarySession>;
readonly createManagedBinarySession: () =>
| BinarySession
| Promise<BinarySession>;
+1 -1
View File
@@ -21,7 +21,7 @@ export const runSessionStatus = async (
if (!config.ok) return { error: projectAnalysisError(config.error) };
const session = await dependencies.createBinarySession(config.value, logger);
try {
const status = jsonObjectSchema.parse(session.status());
const status = jsonObjectSchema.parse(await session.status());
return {
...status,
capabilities: [
+133
View File
@@ -0,0 +1,133 @@
import { expect, it } from "vitest";
import { AnalysisError } from "../domain/analysisErrorBase.js";
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
import { createDoctorHostFixture } from "./Doctor.fixture.js";
import { runDoctorOwned, systemDoctorHost } from "./Doctor.js";
it("joins a late provider inspection and retries only its unresolved cleanup owners", async () => {
let enter!: (signal: AbortSignal) => void;
const entered = new Promise<AbortSignal>((resolve) => {
enter = resolve;
});
let handOff!: () => void;
const handoff = new Promise<void>((resolve) => {
handOff = resolve;
});
let finish!: () => void;
const finishing = new Promise<void>((resolve) => {
finish = resolve;
});
let denied = true;
let closed = false;
const closes = { successful: 0, refused: 0 };
const host = systemDoctorHost({
environment: {},
providerInspections: async (owner, signal) => {
enter(signal);
await handoff;
owner.retain(
{
close: async () => {
closes.successful++;
await finishing;
},
},
"successful-prerequisite",
);
owner.retain(
{
close: async () => {
closes.refused++;
if (denied) throw new Error("controlled cleanup refusal");
},
},
"refused-prerequisite",
);
return [];
},
});
const inspection = host.providerInspections!();
const signal = await entered;
const closing = host.close!().then(
() => {
closed = true;
return undefined;
},
(cause: unknown) => {
closed = true;
return cause;
},
);
try {
expect(signal.aborted).toBe(true);
expect(closed).toBe(false);
handOff();
await inspection;
expect(closed).toBe(false);
finish();
const failure = await closing;
if (!(failure instanceof AnalysisError))
throw new Error("Expected attributed cleanup failure");
expect(failure.cleanupResources).toEqual(["refused-prerequisite"]);
expect(failure.cleanupIncomplete).toBe(true);
expect(closes).toEqual({ successful: 1, refused: 1 });
denied = false;
await host.close!();
expect(failure.cleanupIncomplete).toBe(false);
expect(closes).toEqual({ successful: 1, refused: 2 });
await expect(host.providerInspections!()).rejects.toThrow(
"scope is closed",
);
} finally {
denied = false;
handOff();
finish();
await inspection;
await closing;
await host.close!();
}
});
it("retains a completed healthy report when cancellation arrives during final cleanup", async () => {
const controller = new AbortController();
const reason = { request: "cancel final doctor cleanup" };
await expect(
runDoctorOwned(
undefined,
createDoctorHostFixture({
signal: controller.signal,
close: async () => {
controller.abort(reason);
return undefined;
},
}),
),
).rejects.toMatchObject({
_tag: "AnalysisCancelledError",
cause: reason,
diagnostics: { doctor_report: { healthy: true } },
});
});
it("preserves an inspection-authored physical failure when final cleanup also observes cancellation", async () => {
const controller = new AbortController();
const physical = new ProviderAdapterError("ghidra", "doctor", {
diagnostics: { reason: "controlled inspection fault" },
});
await expect(
runDoctorOwned(
undefined,
createDoctorHostFixture({
signal: controller.signal,
providerInspections: async () => {
throw physical;
},
close: async () => {
controller.abort(physical);
return undefined;
},
}),
),
).rejects.toBe(physical);
expect(physical.cause).toBeUndefined();
});
+77 -9
View File
@@ -1,3 +1,12 @@
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { AnalysisError } from "../domain/analysisErrorBase.js";
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
import { bindArtifactFileCleanup } from "../artifacts/ArtifactFileOperation.js";
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import { AnalysisCleanupLease } from "../artifacts/AnalysisCleanupLease.js";
import { STARTUP_OWNER_CLOSED } from "../process/SharedStartupAttempt.js";
import { constants } from "node:fs";
import { access, readdir, stat } from "node:fs/promises";
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
@@ -9,7 +18,7 @@ import { analysisErrorRemediationAction } from "../domain/analysisErrorPresentat
import { BinaryTargetError } from "../domain/configurationErrors.js";
import { parseBinaryTarget } from "./BinaryTargetResolver.js";
import { execFileOutput } from "../process/ExecFileOutput.js";
import type { JsonValue } from "../domain/jsonValue.js";
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
import {
javaScriptRuntime,
type JavaScriptRuntime,
@@ -93,6 +102,7 @@ export interface DoctorProviderInspection {
}
/** Read-only host capabilities required by diagnostics. */
export interface DoctorHost {
readonly signal?: AbortSignal;
readonly platform: NodeJS.Platform;
readonly architecture: NodeJS.Architecture;
readonly runtime: JavaScriptRuntime;
@@ -203,15 +213,36 @@ export const runDoctorOwned = async (
report = await inspectDoctor(target, host, requestedScope);
} catch (cause: unknown) {
const cleanup = await closeDoctorHost(host);
if (cleanup !== undefined && cause instanceof AnalysisError) {
const cleanupError =
typeof cleanup === "string"
? new ProviderCleanupError(
"rea",
[],
{ reason: cleanup },
{ operation: "doctor" },
)
: cleanup;
throw analysisErrorWithCleanupFailure(
cause,
cleanupError,
"doctor",
).bindCleanupDependencies([cause, cleanupError]);
}
if (cleanup !== undefined)
throw new AggregateError(
[cause, new Error(cleanup)],
[cause, typeof cleanup === "string" ? new Error(cleanup) : cleanup],
"Doctor failed and its resource cleanup remains incomplete.",
{ cause },
);
throw cause;
}
const cleanup = await closeDoctorHost(host);
if (cleanup === undefined && report.healthy && host.signal?.aborted)
throw new AnalysisCancelledError("doctor", {
cause: host.signal.reason,
diagnostics: { doctor_report: jsonValueSchema.parse(report) },
});
return cleanup === undefined
? report
: doctorReportWithCleanup(report, cleanup);
@@ -222,6 +253,8 @@ const inspectDoctor = async (
host: DoctorHost,
requestedScope?: DoctorScope,
): Promise<DoctorReport> => {
if (host.signal?.aborted)
throw new AnalysisCancelledError("doctor", { cause: host.signal.reason });
const scope = normalizeDoctorScope(requestedScope, target);
const {
checks: diagnosticChecks,
@@ -283,23 +316,30 @@ const registrationAdvisoryChecks = (
const closeDoctorHost = async (
host: DoctorHost,
): Promise<string | undefined> => {
): Promise<string | AnalysisError | undefined> => {
try {
return await host.close?.();
} catch (cause: unknown) {
if (cause instanceof AnalysisError) return cause;
return `Doctor host cleanup failed: ${cause instanceof Error ? cause.message : String(cause)}`;
}
};
const doctorReportWithCleanup = (
report: DoctorReport,
detail: string,
cleanup: string | AnalysisError,
): DoctorReport => {
const check: DoctorCheck = {
name: "doctor-resource-cleanup",
ok: false,
classification: "config_drift",
detail,
detail:
typeof cleanup === "string"
? cleanup
: `${cleanup.message} ${cleanup.cleanupResources.join(", ")}`,
...(typeof cleanup === "string"
? {}
: { details: { cleanup_failure: projectAnalysisError(cleanup) } }),
remediation: "Resolve the reported cleanup failure, then rerun rea doctor.",
};
return {
@@ -340,7 +380,10 @@ const collectDoctorIdentity = async (
cli_package_version: PRODUCT_IDENTITY.packageVersion,
expected_skill_version: PRODUCT_IDENTITY.skillVersion,
sdk: SDK_IDENTITY,
catalog: CATALOG_IDENTITY,
catalog: {
counts: CATALOG_IDENTITY.counts,
digests: CATALOG_IDENTITY.digests,
},
live_server: {
state: "unknown",
remediation:
@@ -430,12 +473,14 @@ const installationState = (
/** Optional outer-adapter diagnostics composed without reversing dependencies. */
export interface SystemDoctorHostOptions {
readonly signal?: AbortSignal;
readonly platform?: NodeJS.Platform;
readonly architecture?: NodeJS.Architecture;
readonly environment?: NodeJS.ProcessEnv;
readonly execFileOutput?: typeof execFileOutput;
readonly providerInspections?: (
fileCloseResources: FileCloseResourceOwner,
signal: AbortSignal,
) => Promise<readonly DoctorProviderInspection[]>;
readonly providerRegistrationEnvironment?: () => Readonly<
Record<string, string>
@@ -449,6 +494,11 @@ export const systemDoctorHost = (
options: SystemDoctorHostOptions = {},
): DoctorHost => {
const resources = new ArtifactResourceScope();
const controller = new AbortController();
const inspectionSignal =
options.signal === undefined
? controller.signal
: AbortSignal.any([options.signal, controller.signal]);
const platform = options.platform ?? process.platform;
const architecture = options.architecture ?? process.arch;
const environment = options.environment ?? process.env;
@@ -460,11 +510,12 @@ export const systemDoctorHost = (
resources.retain({
kind: "lease",
resource,
lease: { release: () => owner.close() },
lease: new AnalysisCleanupLease(() => owner.close(), [resource]),
}),
run: (operation) => resources.run(operation),
};
return {
...(options.signal === undefined ? {} : { signal: options.signal }),
platform,
architecture,
runtime: javaScriptRuntime(process.versions),
@@ -561,7 +612,12 @@ export const systemDoctorHost = (
? {}
: {
providerInspections: () =>
options.providerInspections!(fileCloseResources),
resources.run(() =>
options.providerInspections!(
fileCloseResources,
inspectionSignal,
),
),
}),
...(options.providerRegistrationEnvironment === undefined
? {}
@@ -612,11 +668,23 @@ export const systemDoctorHost = (
resources,
),
close: async () => {
controller.abort(STARTUP_OWNER_CLOSED);
try {
await resources.close();
return undefined;
} catch (cause: unknown) {
return `Doctor resource cleanup failed: ${cause instanceof Error ? cause.message : String(cause)}`;
const observation = ArtifactReaderFailure.cleanupObservation(
cause,
"doctor resources",
);
const failure = new ProviderCleanupError(
"rea",
observation.resources,
{ reason: observation.reason },
{ cause, operation: "doctor" },
);
bindArtifactFileCleanup(cause, failure);
throw failure;
}
},
clientRegistrations: () =>
+20 -29
View File
@@ -16,13 +16,12 @@ import {
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import {
addressDistance,
functionDossierSchema,
parseFunctionDossier,
parseListCount,
parseRelatedAddresses,
parseSegments,
parseSegmentImageBase,
projectBinaryOverview,
} from "../domain/hopperValues.js";
import { err, ok, type Result } from "../domain/result.js";
import {
@@ -617,6 +616,17 @@ export class EnhancedTools {
}
async #binaryOverview(signal?: AbortSignal): EnhancedResult {
const direct = await this.#call(
"read_binary_inventory_summary",
{},
signal,
);
if (
direct.ok ||
!(direct.error instanceof AnalysisCapabilityUnavailableError) ||
direct.error.cleanupIncomplete
)
return direct;
const document = await resolveAnalysisDocument(
this.analysis,
undefined,
@@ -647,33 +657,14 @@ export class EnhancedTools {
const stringCount = parseListCount(stringsResult.value, "strings");
if (!stringCount.ok) return stringCount;
const observedSegments = segments?.ok ? segments.value : undefined;
const imageBase =
observedSegments === undefined
? undefined
: parseSegmentImageBase(observedSegments);
return ok({
document: document.value,
...(imageBase === undefined ? {} : { image_base: imageBase }),
...(observedSegments === undefined
? {}
: {
segments: observedSegments.map(
({ name, start, end, image_base, address_space }) => ({
name,
start,
end,
length: addressDistance(start, end),
...(image_base === undefined ? {} : { image_base }),
...(address_space === undefined ? {} : { address_space }),
}),
),
segment_count: observedSegments.length,
}),
procedure_count: procedureCount.value,
string_count: stringCount.value,
});
return ok(
projectBinaryOverview({
document: document.value,
...(segments?.ok ? { segments: segments.value } : {}),
procedure_count: procedureCount.value,
string_count: stringCount.value,
}),
);
}
async #allAddressed(
+1 -1
View File
@@ -288,7 +288,7 @@ const publishFile = async (
const staged = resolve(stagingDirectory, "content");
try {
const file = await open(staged, "wx", 0o600);
await owner.setFile(new OwnedFileHandle(file));
owner.setFile(new OwnedFileHandle(file));
let written: Result<number, unknown>;
try {
const bytes = await writeStagedParts(file, parts, cancellation);
+29 -8
View File
@@ -10,9 +10,16 @@ import {
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import { homeDirectoryFromEnvironment } from "../config/homeDirectory.js";
import { err, ok, type Result } from "../domain/result.js";
import { hashStableFile } from "../filesystem/StableFileHash.js";
import {
hashStableFile,
StableFileHashFailure,
} from "../filesystem/StableFileHash.js";
import type { OwnedFileHandle } from "../filesystem/OwnedFileHandle.js";
import { RegularFileCleanupFailure } from "../filesystem/OwnedRegularFile.js";
import {
RegularFileCleanupFailure,
RegularFileCancelled,
regularFileCancellation,
} from "../filesystem/OwnedRegularFile.js";
import { readBoundedFileBytes } from "../process/BoundedFileBytes.js";
import { verifyDetachedPgpSignature } from "../process/DetachedPgpSignature.js";
import { execFileOutput } from "../process/ExecFileOutput.js";
@@ -705,8 +712,17 @@ export class LinuxHopperPublisher {
},
);
}
if (hashFailure.outcome.kind === "failed")
throw hashFailure.outcome.cause;
if (hashFailure.outcome.kind === "failed") {
if (regularFileCancellation(hashFailure) !== undefined)
throw hashFailure.outcome.cause;
throw new StableFileHashFailure(path, hashFailure.outcome.cause);
}
const cancellation = regularFileCancellation(hashFailure);
if (cancellation !== undefined)
throw new RegularFileCancelled(path, hashFailure.owner, {
outcome: hashFailure.outcome,
reason: cancellation.reason,
});
return hashFailure.outcome.value;
}
}
@@ -1084,21 +1100,26 @@ const releaseFailure = (
signal: AbortSignal | undefined,
fallback: HopperReleaseError["reason"],
): HopperReleaseError => {
if (cause instanceof HopperReleaseError && cause.reason === "cleanup_failed")
return cause;
if (cause instanceof HopperReleaseError) return cause;
if (cause instanceof ArtifactReaderFailure && cause.cleanup !== undefined)
return new HopperReleaseError(
"cleanup_failed",
"Hopper publisher cleanup remains incomplete.",
{ cause },
);
if (signal?.aborted === true)
const cancellation = regularFileCancellation(cause);
if (cancellation !== undefined)
return new HopperReleaseError(
"cancelled",
"Hopper publisher verification was cancelled.",
{ cause: cancellation.reason },
);
if (!(cause instanceof StableFileHashFailure) && signal?.aborted === true)
return new HopperReleaseError(
"cancelled",
"Hopper publisher verification was cancelled.",
{ cause },
);
if (cause instanceof HopperReleaseError) return cause;
return new HopperReleaseError(
fallback,
"Hopper publisher verification could not complete.",
+10 -18
View File
@@ -7,12 +7,12 @@ import {
AnalysisCancelledError,
AnalysisInputError,
} from "../domain/analysisErrorCore.js";
import { createEvidence, type Evidence } from "../domain/evidence.js";
import { jsonObjectSchema } from "../domain/jsonValue.js";
import type { Evidence } from "../domain/evidence.js";
import { err, ok, type Result } from "../domain/result.js";
import type {
AnalysisExecution,
ExecutionOptions,
import {
createAnalysisExecutionEvidence,
type AnalysisExecution,
type ExecutionOptions,
} from "./AnalysisProvider.js";
/** Validated caller-selected request carrying its operation input. */
@@ -55,19 +55,11 @@ export const executeProviderOperation = async <
return err(new AnalysisInputError(operation, { cause: request.error }));
const executed = await provider.execute(request.data, options);
if (!executed.ok) return executed;
const execution = executed.value;
const evidence = createEvidence(
execution.subject ?? undefined,
execution.provider,
{
operation,
parameters: jsonObjectSchema.parse(request.data.input),
result: execution.result,
rawResult: execution.rawResult,
limitations: execution.limitations,
locations: execution.locations,
confidence,
},
const evidence = createAnalysisExecutionEvidence(
operation,
request.data.input,
executed.value,
confidence,
);
return completeProviderEvidence(evidence, options?.signal);
};
@@ -108,12 +108,13 @@ const buildIgnored = async (
try {
text = await readRegularFileText(policyPath, { signal });
} catch (cause: unknown) {
if (signal?.aborted === true && cause === signal.reason) throw cause;
if (!(cause instanceof RegularFileCleanupFailure)) throw cause;
const retry = await retryRegularFileCleanup(cause, resources);
const retry = await retryRegularFileCleanup(cause, resources, signal);
if (retry.cleanup !== undefined) {
const primary =
cause.outcome.kind === "failed"
? cause.outcome.cause
retry.outcome.kind === "failed"
? retry.outcome.cause
: cause.cleanupCause;
return err({
...failure(
+2 -1
View File
@@ -88,8 +88,9 @@ const resolveSourceHead = async (
try {
return await readRegularFile(path, { signal });
} catch (cause: unknown) {
if (signal?.aborted === true && cause === signal.reason) throw cause;
if (!(cause instanceof RegularFileCleanupFailure)) throw cause;
const retry = await retryRegularFileCleanup(cause, resources);
const retry = await retryRegularFileCleanup(cause, resources, signal);
if (
retry.outcome.kind === "failed" &&
errorCode(retry.outcome.cause) !== "ENOENT"
+18 -5
View File
@@ -4,6 +4,8 @@ import { lstat, stat, type FileHandle } from "node:fs/promises";
import {
sameRegularFileState,
RegularFileChangedError,
RegularFileOperationCancelled,
assertRegularFileActive,
} from "../filesystem/RegularFile.js";
import {
RegularFileCleanupFailure,
@@ -41,18 +43,25 @@ export type RegularFileReadOutcome<Value> = RegularFileOperationOutcome<Value>;
export const retryRegularFileCleanup = async <Value>(
failure: RegularFileCleanupFailure<Value>,
resources: ArtifactResourceScope,
signal?: AbortSignal,
): Promise<{
readonly outcome: RegularFileReadOutcome<Value>;
readonly cleanup?: AnalysisCleanupObservation;
}> => {
const outcome: RegularFileReadOutcome<Value> =
failure.outcome.kind === "failed" &&
failure.outcome.cause instanceof RegularFileOperationCancelled &&
!(signal?.aborted === true && failure.outcome.cause === signal.reason)
? { kind: "failed", cause: failure.outcome.cause.reason }
: failure.outcome;
const released = await resources.release({
kind: "file-handle",
handle: failure.owner,
resource: failure.path,
});
if (released.kind === "released") return { outcome: failure.outcome };
if (released.kind === "released") return { outcome };
return {
outcome: failure.outcome,
outcome,
cleanup: {
reason: `${failureMessage(failure.cleanupCause)}; retry: ${failureMessage(released.cause)}`,
resources: [failure.path],
@@ -82,9 +91,9 @@ export const withRegularFile = async <Value>(
async (handle) => {
admitted = handle;
const stats = await handle.stat();
signal?.throwIfAborted();
assertRegularFileActive(signal);
const value = await read(handle, stats);
signal?.throwIfAborted();
assertRegularFileActive(signal);
const [opened, currentPath] = await Promise.all([
handle.stat(),
options.symlinks === "reject" ? lstat(path) : stat(path),
@@ -94,11 +103,15 @@ export const withRegularFile = async <Value>(
!sameRegularFileState(stats, currentPath)
)
throw new RegularFileChangedError(path);
assertRegularFileActive(signal);
return value;
},
);
} catch (cause: unknown) {
// Caller reasons are opaque, including objects shaped like file failures.
// They do not transfer ownership of any descriptor to this operation.
if (signal?.aborted === true && cause === signal.reason) throw cause;
if (cause instanceof RegularFileOperationCancelled) throw cause.reason;
if (
cause instanceof RegularFileCancelled &&
cause.owner.handle === admitted
@@ -109,7 +122,7 @@ export const withRegularFile = async <Value>(
resources === undefined
)
throw cause;
const retry = await retryRegularFileCleanup(cause, resources);
const retry = await retryRegularFileCleanup(cause, resources, signal);
if (retry.cleanup !== undefined) throw cause;
if (retry.outcome.kind === "failed") throw retry.outcome.cause;
return retry.outcome.value;
+20 -1
View File
@@ -360,7 +360,7 @@ describe("setup workflow action selection", () => {
],
[
"readback",
"Agent configuration could not be verified after writing. Repair the configuration file or restore its `.rea.backup`, then rerun setup.",
"Agent configuration could not be verified. Repair the configuration file, then rerun setup.",
],
] as const)("explains %s configuration recovery", async (reason, message) => {
const host = new FakeSetupHost();
@@ -375,6 +375,25 @@ describe("setup workflow action selection", () => {
expect(result.remediation).toBe(message);
expect(result.remediation).not.toContain("internal_client_key");
});
it("names the recovery backup that holds the restored original after a readback failure", async () => {
const host = new FakeSetupHost();
host.hopper = "/Applications/Hopper";
host.clients = [{ name: "cursor", configPath: "/agent.json" }];
const backupPath = `/agent.json.rea.backup.${"a".repeat(64)}.recovery`;
host.clientResults.set("cursor", {
status: "failed",
reason: "readback",
backupPath,
});
const result = await runSetup(
{ ...options(true), clientIds: ["cursor"] },
host,
);
expect(result.remediation).toBe(
`Agent configuration could not be verified after writing. The original was restored and its backup at ${backupPath} was retained. Repair the configuration file or restore that backup, then rerun setup.`,
);
});
});
describe("MSIX Claude Desktop setup", () => {
+16 -22
View File
@@ -44,7 +44,7 @@ import {
RegularFileCleanupFailure,
} from "./RegularFileRead.js";
import {
preserveRecoveryBackup,
preserveCurrentRecoveryBackup,
selectCurrentRecoveryBackup,
} from "./SetupRecoveryBackup.js";
import type { ClientConfigurationFileError } from "./ClientConfigurationFiles.js";
@@ -74,6 +74,8 @@ export class ClientConfigurationCleanupRollbackFailure extends RegularFileCleanu
[failure.cleanupCause, new Error(configurationFailure.failure.detail)],
`Configuration readback cleanup and rollback failed: ${configurationFailure.failure.path}`,
),
failure.retention,
failure.completionCancellation,
);
}
}
@@ -247,27 +249,15 @@ const persistClientConfiguration = async ({
};
if (target.value) return { status: "failed", reason: "target_changed" };
}
const backupPath =
original === undefined ? undefined : `${client.configPath}.rea.backup`;
let recoveryBackupPath: string | undefined;
if (backupPath !== undefined && original !== undefined) {
const selection = await selectCurrentRecoveryBackup(backupPath, original);
if (!selection.ok) return { status: "failed", reason: "backup" };
if (
!(await preserveRecoveryBackup(
transactionPath,
selection.value.backupPath,
))
)
return { status: "failed", reason: "backup" };
const verified = await selectCurrentRecoveryBackup(backupPath, original);
if (
!verified.ok ||
verified.value.backupPath !== selection.value.backupPath ||
!verified.value.present
)
return { status: "failed", reason: "backup" };
recoveryBackupPath = verified.value.backupPath;
if (original !== undefined) {
const preserved = await preserveCurrentRecoveryBackup(
`${client.configPath}.rea.backup`,
transactionPath,
original,
);
if (!preserved.ok) return { status: "failed", reason: "backup" };
recoveryBackupPath = preserved.value;
}
// A native OpenCode V2 entry replaces REA's legacy V1 entry, which would
// otherwise conflict with it.
@@ -664,7 +654,11 @@ const rollbackOrReadbackFailure = async (
): Promise<ClientConfigurationResult> => {
const rollback = await restoreConfig(targetPath, original);
return rollback.ok
? { status: "failed", reason: "readback" }
? {
status: "failed",
reason: "readback",
...(backupPath === undefined ? {} : { backupPath }),
}
: {
status: "failed",
reason: "rollback",
+20 -15
View File
@@ -9,21 +9,26 @@ import type { SetupClient } from "./SupportedClients.js";
export const failedConfigurationMessage = (
result: Extract<ClientConfigurationResult, { readonly status: "failed" }>,
): string => {
if (result.reason === "target_changed")
return "The client began reading a different configuration file after the plan was shown, so no change was made. Rerun setup to review the new target.";
if (result.reason === "target_unavailable")
return `The client configuration read target could not be established because ${result.failure.path} could not be inspected: ${result.failure.detail}. Repair access to that path, then rerun setup.`;
if (result.reason === "path")
return "Agent configuration path could not be safely verified. Check its permissions and, if it is a symbolic link, verify that the link resolves to a regular file owned by the current user, then rerun setup.";
if (result.reason === "backup")
return "Agent configuration could not be backed up, so no change was made. Check file permissions, then rerun setup.";
if (result.reason === "write")
return "Agent configuration could not be updated. Check file permissions, then rerun setup.";
if (result.reason === "rollback")
return result.backupPath === undefined
? `Agent configuration rollback failed at ${result.failure.path}: ${result.failure.detail}; the failed first-run file may remain. Remove or repair it before rerunning setup.`
: `Agent configuration rollback failed at ${result.failure.path}: ${result.failure.detail}. Inspect the preserved backup at ${result.backupPath} and restore the intended configuration, then rerun setup.`;
return "Agent configuration could not be verified after writing. Repair the configuration file or restore its `.rea.backup`, then rerun setup.";
switch (result.reason) {
case "target_changed":
return "The client began reading a different configuration file after the plan was shown, so no change was made. Rerun setup to review the new target.";
case "target_unavailable":
return `The client configuration read target could not be established because ${result.failure.path} could not be inspected: ${result.failure.detail}. Repair access to that path, then rerun setup.`;
case "path":
return "Agent configuration path could not be safely verified. Check its permissions and, if it is a symbolic link, verify that the link resolves to a regular file owned by the current user, then rerun setup.";
case "backup":
return "Agent configuration could not be backed up, so no change was made. Check file permissions, then rerun setup.";
case "write":
return "Agent configuration could not be updated. Check file permissions, then rerun setup.";
case "rollback":
return result.backupPath === undefined
? `Agent configuration rollback failed at ${result.failure.path}: ${result.failure.detail}; the failed first-run file may remain. Remove or repair it before rerunning setup.`
: `Agent configuration rollback failed at ${result.failure.path}: ${result.failure.detail}. Inspect the preserved backup at ${result.backupPath} and restore the intended configuration, then rerun setup.`;
case "readback":
return result.backupPath === undefined
? "Agent configuration could not be verified. Repair the configuration file, then rerun setup."
: `Agent configuration could not be verified after writing. The original was restored and its backup at ${result.backupPath} was retained. Repair the configuration file or restore that backup, then rerun setup.`;
}
};
/** Configure every detected agent and report the first failed transaction. */
+68 -13
View File
@@ -6,6 +6,7 @@ import {
readRegularFile,
RegularFileCleanupFailure,
} from "./RegularFileRead.js";
import type { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
export interface RecoveryBackupSelection {
readonly backupPath: string;
@@ -28,15 +29,19 @@ const errorDetail = (cause: unknown): string =>
export const selectCurrentRecoveryBackup = async (
firstBackupPath: string,
original: string | Buffer,
resources?: ArtifactResourceScope,
): Promise<Result<RecoveryBackupSelection, RecoveryBackupSelectionFailure>> => {
const read = (path: string) =>
readRegularFile(path, {
symlinks: "reject",
...(resources === undefined ? {} : { resources }),
});
const expected = Buffer.isBuffer(original)
? original
: Buffer.from(original, "utf8");
let firstBackup: Buffer | undefined;
try {
firstBackup = await readRegularFile(firstBackupPath, {
symlinks: "reject",
});
firstBackup = await read(firstBackupPath);
} catch (cause: unknown) {
if (cause instanceof RegularFileCleanupFailure) throw cause;
if (!isMissing(cause))
@@ -55,7 +60,7 @@ export const selectCurrentRecoveryBackup = async (
const recoveryPath = `${firstBackupPath}.${digest}.recovery`;
let recovery: Buffer | undefined;
try {
recovery = await readRegularFile(recoveryPath, { symlinks: "reject" });
recovery = await read(recoveryPath);
} catch (cause: unknown) {
if (cause instanceof RegularFileCleanupFailure) throw cause;
if (!isMissing(cause))
@@ -74,15 +79,65 @@ export const selectCurrentRecoveryBackup = async (
return ok({ backupPath: recoveryPath, present: recovery !== undefined });
};
/** Preserve a snapshot without replacing an earlier recovery file. */
export const preserveRecoveryBackup = async (
export interface RecoveryBackupPreservationFailure {
readonly kind: RecoveryBackupSelectionFailure["kind"] | "copy" | "unverified";
readonly path: string;
readonly detail: string;
}
const copyExclusive = (source: string, destination: string): Promise<void> =>
copyFile(source, destination, fsConstants.COPYFILE_EXCL);
export interface RecoveryBackupPreservationOptions {
/** Copy without replacing an existing destination; reject one with EEXIST. */
readonly copy?: (source: string, destination: string) => Promise<void>;
/** Scope that retains a backup read whose handle cleanup failed. */
readonly resources?: ArtifactResourceScope;
}
/**
* Preserve a transaction's original bytes before it edits the file: keep a
* matching first backup, otherwise copy them to the content-addressed recovery
* path without replacing an earlier file, then verify the selected snapshot.
*/
export const preserveCurrentRecoveryBackup = async (
firstBackupPath: string,
source: string,
destination: string,
): Promise<boolean> => {
try {
await copyFile(source, destination, fsConstants.COPYFILE_EXCL);
return true;
} catch (cause: unknown) {
return cause instanceof Error && "code" in cause && cause.code === "EEXIST";
original: string | Buffer,
{ copy = copyExclusive, resources }: RecoveryBackupPreservationOptions = {},
): Promise<Result<string, RecoveryBackupPreservationFailure>> => {
const selection = await selectCurrentRecoveryBackup(
firstBackupPath,
original,
resources,
);
if (!selection.ok) return selection;
const { backupPath } = selection.value;
if (!selection.value.present) {
try {
await copy(source, backupPath);
} catch (cause: unknown) {
if (
!(cause instanceof Error && "code" in cause && cause.code === "EEXIST")
)
return err({
kind: "copy",
path: backupPath,
detail: `The recovery backup could not be written: ${errorDetail(cause)}`,
});
}
}
const verified = await selectCurrentRecoveryBackup(
firstBackupPath,
original,
resources,
);
if (!verified.ok) return verified;
if (verified.value.backupPath !== backupPath || !verified.value.present)
return err({
kind: "unverified",
path: backupPath,
detail: `The recovery backup does not match the configuration it preserves: ${backupPath}`,
});
return ok(backupPath);
};
+12 -42
View File
@@ -17,7 +17,7 @@ import {
withRegularFile,
} from "./RegularFileRead.js";
import { clientSkillDirectories } from "./SupportedClients.js";
import { selectCurrentRecoveryBackup } from "./SetupRecoveryBackup.js";
import { preserveCurrentRecoveryBackup } from "./SetupRecoveryBackup.js";
export const MANAGED_SKILL_FILES = [
"SKILL.md",
@@ -153,15 +153,8 @@ const systemSkillInstallFileOperations: SkillInstallFileOperations = {
mkdir: async (path) => {
await mkdir(path, { recursive: true });
},
backup: async (source, destination) => {
try {
await copyFile(source, destination, fsConstants.COPYFILE_EXCL);
} catch (cause: unknown) {
if (cause instanceof Error && "code" in cause && cause.code === "EEXIST")
return;
throw cause;
}
},
backup: (source, destination) =>
copyFile(source, destination, fsConstants.COPYFILE_EXCL),
write: (path, content, mode) =>
writeFileAtomic(path, content, { encoding: "utf8", mode }),
remove: (path) => rm(path, { force: true }),
@@ -513,32 +506,6 @@ const restoreSkillFiles = async (
return failures;
};
const preserveCurrentSkillBackup = async (
destination: string,
original: Buffer,
operations: SkillInstallFileOperations,
): Promise<string> => {
const firstBackupPath = `${destination}.rea.backup`;
const selection = await selectCurrentRecoveryBackup(
firstBackupPath,
original,
);
if (!selection.ok) throw new Error(selection.error.detail);
if (selection.value.present) return selection.value.backupPath;
await operations.backup(destination, selection.value.backupPath);
const verified = await selectCurrentRecoveryBackup(firstBackupPath, original);
if (
!verified.ok ||
verified.value.backupPath !== selection.value.backupPath ||
!verified.value.present
)
throw new Error(
`The current file was not preserved at ${selection.value.backupPath}`,
);
return verified.value.backupPath;
};
/** Transactionally install the canonical skill bundle and optional references. */
export const installCanonicalSkill = async (
home: string,
@@ -571,14 +538,17 @@ export const installCanonicalSkill = async (
failingPath = destination;
await operations.mkdir(dirname(destination));
if (original !== undefined) {
recoveryPaths.set(
const preserved = await preserveCurrentRecoveryBackup(
`${destination}.rea.backup`,
destination,
await preserveCurrentSkillBackup(
destination,
original.bytes,
operations,
),
original.bytes,
{
copy: operations.backup,
...(resources === undefined ? {} : { resources }),
},
);
if (!preserved.ok) throw new Error(preserved.error.detail);
recoveryPaths.set(destination, preserved.value);
}
}
stage = "write";
Loaded 100 of 559 files, more files were not shown because too many files have changed in this diff. Show more