Commit Graph
7591 Commits
Author SHA1 Message Date
morluto 79cc07ecf1 refactor(cli): project JSON-text options and pwntools inspectors from contracts (#10589)
Add a jsonText declaration to the contract command description: a
structured contract field is taken as one JSON text argument, decoded at
argv parsing, and admitted by the contract field's own schema, so
--schema advertises the contract's structure. This generalizes the
per-command producerLimitsOption wiring.

Migrate inspect-binary-layout and inspect-recorded-crash (producer_limits
as JSON text; recorded-crash keeps --debugger-context for
include_debugger_context) and delete their hand-written options and input
spreads.

The parity guard probes JSON-text fields with valid, malformed and
contract-refused JSON.
2026-11-15 12:34:26 +08:00
morluto 441aa170d3 fix(ghidra): name the failing fields when a bridge reply misses its contract (#10749) 2026-11-15 12:33:55 +08:00
morluto 2e1e6593f2 refactor(application): own the finalization precedence in runOwned (#10645)
* refactor(application): own the finalization precedence in runOwned

runOwned runs owned work, closes its owner and applies the documented
precedence once: a recorded failure stays primary, cancellation before
commit keeps the completed value as a partial observation, cancellation
after commit keeps the value, and unconfirmed cleanup reports
cleanup_incomplete while keeping the completed value. finishAnalysisCommand
now projects its Finalized outcome; its 31 callers are unchanged.

* fix(application): preserve finalization outcomes

* fix(application): classify finalization by the primary error and keep cancellation through recovered publication

A producer composite whose primary is a cancellation finalized as failed because the kind read only the top-level error class; it now follows primaryAnalysisError. A cancellation that arrived while a recovered value's publication failed was dropped because the signal was read before that last await; it is now attached to the publication failure as a secondary.

* fix(cli): keep the initial failure snapshot whenever close itself succeeded
2026-11-15 10:24:25 +08:00
morluto 61fd6336bf refactor(hopper): build Hopper cancellations with a required reason (#10601)
* refactor(errors): require a reason for every analysis cancellation

Add cancelledBy, the one constructor for AnalysisCancelledError. Its
reason argument is required: an aborted signal, an earlier cancellation,
a reason carried from an inner failure, or an OwnerCancellation when REA
closed the work's owner itself.

Closed adb, apktool, firmware, sqlite, gdb, web-bundle analyzer and
binary-session record owners now report OwnerCancellation.ownerClosed
instead of an absent reason. Firmware's predecessor wait returns its
cancellation as a Result, removing an unreachable reason-less fallback.

An architecture guard rejects new direct constructions, with a counted
per-file baseline that can only shrink.

* refactor(errors): share one aborted-signal predicate with cancelledBy

* refactor(process): return a departing caller's reason from a shared startup wait

* refactor(providers): build decompiler-provider cancellations with a required reason (#10587)

Migrate every AnalysisCancelledError construction under src/ghidra,
src/ida, src/cutter, src/jeb and src/kuna to cancelledBy and lower the
guard baseline from 671 to 572. Closed Kuna target commands report
OwnerCancellation.ownerClosed instead of an absent reason, and Kuna and
JEB close abort their lifetimes with that typed reason.

* fix(errors): read the reason of any signal-shaped cancellation source

* test(ci): cover the counted-baseline comparison behind the architecture guard

* refactor(hopper): build Hopper cancellations with a required reason

Migrate the remaining AnalysisCancelledError constructions under
src/hopper to cancelledBy. Regex-search close aborts its workers with
OwnerCancellation.ownerClosed. HopperCleanup and HopperClient are not
touched.

* test(hopper): cover cancellation during search shutdown
2026-11-15 10:20:08 +08:00
morluto 59cd5552c1 fix(parsers): keep each ParseFailure constructor's kind in its return type (#10746) 2026-11-15 10:14:06 +08:00
morluto d9daf09ea5 refactor(capture): report decoder failures through ParseFailure with named limits (#10708) 2026-11-15 10:13:55 +08:00
morluto 44fedc8fdd fix(pcap): report a refused tool launch as an unavailable tool (#10553)
* fix(pcap): report a refused tool launch as an unavailable tool

When a capture read's tool launch was refused with EPERM, pcap reported it
as unreadable tool output (AnalysisProtocolError) instead of an unavailable
tool. Pcap also never told "not executable" apart from "missing".

Add classifyLaunchFailure in src/process as the one owner of which spawn
errnos mean missing (ENOENT, ENOTDIR) and which mean present but not
executable (EACCES, EPERM, ENOEXEC, EISDIR). Pcap, apktool and gdb used three
different errno lists; all three now use it.

* refactor(process): classify Java and adb launch refusals with the shared owner (#10598)

The Ghidra and JADX Java probes and adb's version probe now take
missing-versus-not-executable from classifyLaunchFailure instead of their
own errno checks. ELOOP and ENAMETOOLONG join the owner's missing set, so
adb keeps its existing classification and both Java probes now treat a
non-directory path component as a missing runtime.

* fix(pcap): preserve command launch attribution

* test(pcap): expect an unknown failing stage when a limited capture tool exits unsuccessfully

* fix(pcap): attribute a limited capture's exit through the prlimit wrapper

* fix(process): identify the refused executable for every launcher shape

Name the executable on errnos Node throws synchronously from spawn
(ENOEXEC, ENOTDIR, ...) and read the Windows native launcher's
executable-launch constraint, so pcap attributes those refusals to the
selected tool or limiter instead of an unknown stage.

* test(pcap): prove synchronous launch naming with ENOTDIR on every POSIX host
2026-11-14 21:11:59 -05:00
morluto 47925f3bef fix(browser): type screenshot, WebMCP and domain toggle CDP replies (#10647) 2026-11-14 21:11:43 -05:00
morluto 3a29b6b8ea refactor(il2cpp): report format failures through ParseFailure (#10701) 2026-11-14 21:11:29 -05:00
morluto cd5ea9a9fb fix(hopper): name the failing fields when a bridge reply misses its contract (#10713)
* fix(hopper): name the failing fields when a bridge reply misses its contract

* fix(ci): run the package baseline when the fake Hopper bridge changes

* fix(hopper): name nested union failures and cover every native schema in the package scope
2026-11-14 21:11:08 -05:00
morluto 89640a446a fix(parsers): stop reading every RangeError as a format failure (#10602)
* refactor(parsers): report format failures through one ParseFailure algebra

Add ParseFailure (malformed, truncated, unsupported, limit, with format,
offset and field) and its one carrier, ParseFailureError, in the domain.
BinaryReader now names truncation and the field it read, and its native
format and resource failures are ParseFailureError subclasses.

Providers projected a format failure with their own instanceof ladder,
and some dropped the selected path from the invalid_format message.
parseFailureAnalysisError now owns that projection for the native,
Go, devicetree, V8, Windows Shortcut and Electron fuse providers.
The V8 code cache, Windows Shortcut and Electron fuse format classes
are removed in favour of the algebra.

The architecture guard counts raw Buffer and DataView reads, plain
Error throws in parser folders and plain JSON.parse of parser input
against baselines that can only shrink.

* fix(parsers): stop reading every RangeError as a format failure

Mach-O, Swift and Objective-C metadata parsers threw RangeError for a
truncated or malformed record, so their catch sites treated any
RangeError as a partial decode: a call stack overflow or a host string
limit became an "unresolved" metadata reason, and the IL2CPP Mach-O
path reported it as a malformed container.

Those parsers and the CIL method body reader now throw ParseFailureError.
hostRangeErrorKind is the one interpreter of a host RangeError (stack
overflow, host capacity, raw out-of-bounds read), and
parseFailureFromCause admits only a parse failure or a raw
out-of-bounds read as evidence about the input. Fixup stream catch-alls
rethrow host limits. The architecture guard counts the remaining direct
instanceof RangeError checks.

* fix(parsers): preserve Apple metadata failure ownership

* test(architecture): shrink parser failure baseline

* fix: preserve uncertainty for host range errors
2026-11-15 10:03:26 +08:00
morluto 0e662ac8bd fix(artifacts): classify ELF and thin Mach-O only when their headers decode (#10650)
* fix(artifacts): classify ELF and thin Mach-O only when their headers decode

Inventory classified a file as elf or mach-o on its magic number alone,
while target resolution requires a decodable ELF fixed header and a
walkable Mach-O load-command table. A magic-only stub or a Mach-O with
corrupt load commands was inventoried as an executable that open_binary
then refused. Both routes now share readNativeHeaderFormat; such files
are classified unknown.

* test(applications): give mobile application fixtures decodable native headers

Inventory now classifies ELF and thin Mach-O only when their headers decode, so the 8-byte magic stubs in the Android and Apple application fixtures no longer count as native code. Use the shared minimal header fixtures instead.
2026-11-15 09:52:48 +08:00
morluto e652f4409e fix(verify): rewrite counted baselines only when a count was lowered (#10669)
* fix(verify): rewrite counted baselines only when a count was lowered

* test(verify): own the baseline write decision in a tested helper
2026-11-15 09:42:02 +08:00
morluto 133e7762b6 refactor(source-maps): report decoder failures through ParseFailure (#10690)
SourceMapFormatFailure and its format/unsupported/limit/section-overlap reasons are replaced by ParseFailureError with format source-map. Limits carry the selected allowance name and maximum; a section-boundary overlap is a malformed failure on the section-boundary field. The codec reply protocol and every public error are unchanged.
2026-11-15 09:36:58 +08:00
morluto 6f19681551 fix(browser): type DOM listener, metrics and emulation CDP replies (#10641)
* fix(browser): type CDP command replies and parse frame trees once

CdpConnection.send is typed by method name. A command registered in
src/browser/protocol/CdpCommands.ts has its parameters typed from the
protocol table Playwright ships and its reply parsed once by its own result
schema; a mismatch rejects that command with CdpMalformedReply
(protocol_error, command, JSON pointer) without poisoning the connection.
Playwright storage sessions admit replies through the same table.

Page.getFrameTree and Page.getResourceTree are the first registered
commands. Their one parser keeps per-frame facts tolerant (malformed child
entries remain explicit omissions) and replaces the per-site readers:
observeMainFrameUrl, mainFrameUrl, walkFrameTrees, frameTreeResources,
runtimeFrameTreeSchema and the scenario storage frame schema.

A reply without a frame tree used to be retried for two seconds and
reported as target_not_allowed; it is now one protocol_error.

A guard test counts literal CDP command names without a result schema per
source file; the baseline can only shrink.

* fix(browser): consume parsed CDP facts across boundaries

* refactor(browser): register target, storage and acknowledgement CDP replies

Register result schemas for Target.attachToTarget, Target.getTargetInfo,
Storage.getStorageKey, Page.addScriptToEvaluateOnNewDocument,
Debugger.setBreakpointByUrl, Debugger.evaluateOnCallFrame and fourteen
commands whose reply REA reads only as success. Their callers use the
parsed reply; the per-site readers are removed (attachedSessionId and the
scenario storage and service-worker zod schemas, which threw raw ZodError).

Playwright's Target.getTargetInfo in scenario attach now goes through the
same admission. The untyped-command baseline drops from 96 to 48.

* fix(browser): report malformed script source and coverage replies as protocol errors

* fix(browser): type DOM listener, metrics and emulation CDP replies

* fix(browser): report malformed script source and coverage replies as protocol errors (#10638)
2026-11-15 09:26:29 +08:00
morluto da53dce1f7 refactor(cli): project Go, V8 and delivery inspectors from their contracts (#10576)
Migrate inspect-go-binary, inspect-v8-code-cache, inspect-devicetree and
inspect-native-exports onto the contract command description and delete
their hand-written options and input spreads.

The description gains evidenceDelivery. It adds the shared --detail and
--evidence_output options, and the run context carries the selected
delivery. A contract detail field selects that delivery and is not passed
to the analysis, as the hand-written commands did. Go's cross-field
budget rules stay with the service's contract parse, so standalone
--max-string-literals still returns invalid_request.

The parity guard accepts the declared CLI-only delivery options as the
only options without a contract field.
2026-11-15 09:14:56 +08:00
morluto a11a54e797 fix(providers): treat undeclared bridge coverage and completion as unknown (#10651)
IDA's processor-neutral callgraph reader reported coverage complete when
upstream omitted the truncation flag or the per-function cap. Cutter
reported a command complete when a successful reply omitted
execution_state. Both now report unknown, so an omitted marker is never
read as a declaration of completeness.
2026-11-15 09:10:14 +08:00
morluto d482bb9369 refactor(apple-ips): report crash-report format failures through ParseFailure (#10665) 2026-11-15 08:59:47 +08:00
morluto 5f98047a88 fix(providers): decode MCP text replies by declared kind instead of guessing (#10654)
parseMcpJsonOrText returned the raw string whenever JSON.parse failed, so a
truncated JSON reply from IDA or JEB became an ordinary string and IDA's
legacy decompile_function accepted it as pseudocode. Text blocks are now
decoded by the tool's declared reply kind: JSON unless the tool returns
plain text (IDA legacy decompile_function). Malformed JSON is a typed
protocol failure.
2026-11-15 08:49:20 +08:00
morluto f56b417a58 fix(javascript): expose operands of unmodelled implicit execution (#10631)
* fix(javascript): expose operands of unmodelled implicit execution

The member-mutation collectors now escape the operands of every execution
kind they did not apply: yielded values, JSX element operands, dynamic
import specifiers and options, decorated classes, with-statement scope
objects, iteration inside accessors and syntax the classification does not
describe. Each of these left a literal where the equivalent explicit call
already yields unknown.

Decorators are classified at the class definition that calls them and are
evaluated as definition-time expressions.

* fix(javascript): expose decorated instance members and keep a decorator's outer this

A class's decorator execution now exposes decorated members' functions and initializers, which a decorator can call without the class running them. The getter receiver walk uses the shared javaScriptDefinitionExpression owner, so class and member decorators see the enclosing this like heritage and computed keys.
2026-11-15 08:37:09 +08:00
morluto 8c052bdec5 test(architecture): ratchet top-level layer direction and mutual imports (#10584)
* test(architecture): ratchet top-level layer direction and mutual imports

Declare a layer for every top-level src/ directory and root source file
(domain, contracts, foundation, providers, application, composition,
adapters) in scripts/lib/module-layers.mjs. verify:module-boundaries now also
builds the production import graph between those units and compares it with
a counted baseline: 45 unit pairs import a higher layer (248 imports) and 23
unit pairs import each other. A new upward pair, more imports on a recorded
pair, a new mutual import, or an undeclared unit fails; shrinkage is
reported so the baseline can be lowered.

* fix(architecture): enforce layers for isolated units
2026-11-15 08:37:04 +08:00
morluto 351c2f4a03 fix(ghidra): carry only the latest launcher output in error diagnostics (#10555)
* fix(ghidra): carry only the latest launcher output in error diagnostics

Ghidra retains up to 8 MiB of launcher output so it can scan for failures,
and every error projected all of it into details.diagnostics. A startup
timeout on a large binary produced an 11 MB error payload (#10365).

Project the last 64 KiB of each retained stream, the bound gdb, rizin and
native commands already use, and record text_bytes next to the existing
observed and retained byte counts. Redaction runs on the whole retained text
before the cut. Add utf8TailExcerpt as the shared helper that keeps the end
without splitting a character.

Refs #10365

* fix(ghidra): preserve actionable launcher diagnostics

* test(ghidra): keep launcher scenarios independently scoped

* refactor(ghidra): parse launcher failures once in the diagnostics owner

ghidraFailureOutput parses every retained line with the single launcher-failure parser and carries bounded, redacted facts with their source line. The error projection reads those facts instead of re-scanning stream text, and the superseded line filter and ghidraJavaHomeSaveDenial text scanner are removed.
2026-11-14 19:01:26 -05:00
morluto 6c062c9c2a fix(setup): stop writing the default compact input profile for Kimi and LM Studio (#10559)
* fix(setup): stop writing the default compact input profile for Kimi and LM Studio

The server already advertises compact input schemas when
REA_MCP_INPUT_SCHEMA_PROFILE is unset, so setup no longer adds that value to
Kimi Code and LM Studio registrations, and doctor/status no longer report a
registration without it as stale. Explicit stored or setup-environment
profiles are still preserved.

Refs #10514

* fix(setup): validate LM Studio registration environment

* test(hopper): retry cleanup after confirmed application exit
2026-11-14 19:01:15 -05:00
morluto a0d96e520e refactor(artifacts): classify container formats through one recognizer table (#10629)
Inventory classification and binary target resolution each kept their
own ordered chain of container sniffers (iNES, ZIP, Godot PCK, UnityFS,
wxapkg, uImage, HEX/S-record, UF2, DfuSe, ESP flash and image, FIT,
NTFS MFT, binary plist). Both now walk ARTIFACT_FORMAT_RECOGNIZERS in
src/domain/artifactFormatRecognition.ts.

A recognizer reports recognized, refused (the header names a format
this file cannot be read as) or needs-input (the DFU trailer or an ESP
flash layout). Each route keeps only its policy: resolution reads the
trailer or locates the flash layout on demand and refuses with
file_format_rejected; inventory falls through on a refusal or a missing
input, as before.
2026-11-14 19:01:03 -05:00
morluto fd555be8b4 refactor(errors): type 14 baselined resource refusals as fixed ceilings or measured exhaustion (#10639)
* refactor(errors): type 14 baselined resource refusals as fixed ceilings or measured exhaustion

Fixed ceilings (NTFS journal file, web-bundle worker transfer, Playwright trace parser, lipo field retention, exactly representable file sizes) now name their allowance; observed heap or result-capacity exhaustion and occupied admission declare measuredExhaustion. The typed-resource-refusal baseline drops from 24 sites in 19 files to 10 in 10.

* fix(browser): name the trace parser allowance the record actually exhausted

* test(ntfs): compare the oversized-acquisition recovery with its owning formatter
2026-11-14 19:00:38 -05:00
morluto d8c3be9ba5 fix(ghidra): deliver the bridge's invalid_request rejection to the caller (#10659)
The Java bridge answered an unparseable request with a fixed id 1 and
closed the connection. The client ignored that reply because id 1 was
already settled, so the caller saw a socket-close process failure instead
of invalid_request. The bridge now reserves id 0 for connection-level
failures, and the client fails the request in flight with the remote
reason (or reports a protocol failure when nothing is in flight).
2026-11-14 18:59:58 -05:00
morluto f88d8964f9 fix(hopper): validate every bridge reply against one contract table (#10662)
* fix(hopper): validate inventory replies against their contracts at the provider edge

Hopper validated only the response envelope. list_names replies outside
the inventory contract were returned unchanged (projectHopperNameInventory
passed them through), and the regex search re-parsed its inventory with
its own policy. A per-method reply contract table built from the shared
native inventory schemas now validates list_names, list_procedures,
list_strings, search_procedures and search_strings once; a reply outside
the contract is an AnalysisOutputError. The remaining 31 operations are a
counted pending list that may only shrink.

* fix(hopper): add reply contracts for scalar, boolean and address-list operations

Twenty-two more Hopper operations are validated against reply contracts
built from the shared native schemas. The pending list shrinks from 31
to 9 operations.

* refactor(hopper): validate every bridge reply through one contract table

The reply table now covers every bridge method REA reads, including the
private caller-reconciliation methods. HopperFileOffset receives a
validated observation and HopperCallerReconciliation reads typed replies
instead of keeping its own schemas and re-parsing the native result. The
segment list schema moves from contracts into its domain owner.

* fix(hopper): apply reply contracts on the ready path and test every entry path

executeIfReady validated only list_procedures, so a malformed
list_documents reply could reach the completion path. Every contracted
operation is now validated there, and provider-level tests cover the
direct and ready paths for each inventory and document operation.
2026-11-14 18:59:39 -05:00
morluto a22fb6e60b fix(model): report malformed GGUF and Safetensors headers as invalid_format through ParseFailure (#10667)
* refactor(model): report GGUF and Safetensors format failures through ParseFailure

* fix(model): report malformed model headers as invalid_format through the shared mapping

Malformed and truncated GGUF and Safetensors headers now take the shared ParseFailure mapping (invalid_request, invalid_format on path) instead of the model provider's own BinaryTargetError file_format_rejected. Unsupported forms stay unsupported_target and model allowances keep their resource-constraint recovery.
2026-11-14 18:58:59 -05:00
morluto 2705aad365 fix(il2cpp): select universal binary slices through the shared Mach-O selection (#10661)
* fix(il2cpp): select universal binary slices through the shared Mach-O selection

* test(il2cpp): assert selection facts instead of full sentences
2026-11-14 18:58:48 -05:00
morluto ef15353c69 refactor(android): build mobile-provider cancellations with a required reason (#10590)
* refactor(errors): require a reason for every analysis cancellation

Add cancelledBy, the one constructor for AnalysisCancelledError. Its
reason argument is required: an aborted signal, an earlier cancellation,
a reason carried from an inner failure, or an OwnerCancellation when REA
closed the work's owner itself.

Closed adb, apktool, firmware, sqlite, gdb, web-bundle analyzer and
binary-session record owners now report OwnerCancellation.ownerClosed
instead of an absent reason. Firmware's predecessor wait returns its
cancellation as a Result, removing an unreachable reason-less fallback.

An architecture guard rejects new direct constructions, with a counted
per-file baseline that can only shrink.

* refactor(errors): share one aborted-signal predicate with cancelledBy

* refactor(process): return a departing caller's reason from a shared startup wait

* refactor(android): build mobile-provider cancellations with a required reason

Migrate every remaining AnalysisCancelledError construction under
src/adb, src/android, src/apktool, src/apkSignature, src/flutter,
src/il2cpp, src/dotnet and src/frida to cancelledBy and lower the guard
baseline from 671 to 608. A closed adb provider reports
OwnerCancellation.ownerClosed for later probes, and JADX and apktool
shutdown abort with that typed reason.

* refactor(providers): build decompiler-provider cancellations with a required reason (#10587)

Migrate every AnalysisCancelledError construction under src/ghidra,
src/ida, src/cutter, src/jeb and src/kuna to cancelledBy and lower the
guard baseline from 671 to 572. Closed Kuna target commands report
OwnerCancellation.ownerClosed instead of an absent reason, and Kuna and
JEB close abort their lifetimes with that typed reason.

* fix(errors): read the reason of any signal-shaped cancellation source

* test(ci): cover the counted-baseline comparison behind the architecture guard
2026-11-14 18:58:38 -05:00
morluto 959af004df docs: refuse literal tool counts in living prose (#10585)
* docs: refuse literal tool counts in living prose

docs:check now refuses a count of ten or more tools in README, AGENTS,
CONTRIBUTING, docs/ (except dated releases, specs, verification and ADR
records), the authored skill and the public website. The catalog owns that
number and every hand-written copy goes stale when a tool is added. The three
current statements (240, 172 and 254 tools, all measured on earlier builds)
now refer to the catalog of the measured build instead.

* fix(docs): exclude generated pages from prose checks

* fix(docs): rerun the tool-count check when any scanned prose changes

The docs:check:built cache inputs and the CI docs lane did not cover AGENTS.md, CONTRIBUTING.md, every .agents/skills tree or website/public, so an edit there could reuse a cached pass or skip the docs job. The scanner now owns one isLivingProsePath predicate; the CI plan selects the docs lane with it, and a test binds the turbo inputs to the scanned roots.
2026-11-14 18:58:24 -05:00
morluto b342254f9a refactor(macho): read universal Mach-O slices only where admission selected them (#10642)
Hopper and native dispatch metadata re-ranked FAT slices from the target's
architecture name instead of reading the admitted universalSlice. Both now
consume the admitted range and refuse when it is missing or undeclared.
Mach-O load-command codes move to one table with an architecture guard.
2026-11-14 18:57:55 -05:00
morluto 3080a63729 fix(gpu): report malformed GPU containers as invalid_format (#10644)
fix(gpu): report malformed GPU containers as invalid_format through the shared parse-failure mapping

GpuCodeObjectsProvider mapped a malformed BinaryFormatFailure to a
BinaryTargetError (target_unavailable / file_format_rejected) while every
other format parser reports malformed bytes as an invalid path
(invalid_request / invalid_format). It now uses parseFailureAnalysisError.
2026-11-14 18:57:45 -05:00
morluto 3e9400873e refactor(browser): register target, storage and acknowledgement CDP replies (#10630)
* fix(browser): type CDP command replies and parse frame trees once

CdpConnection.send is typed by method name. A command registered in
src/browser/protocol/CdpCommands.ts has its parameters typed from the
protocol table Playwright ships and its reply parsed once by its own result
schema; a mismatch rejects that command with CdpMalformedReply
(protocol_error, command, JSON pointer) without poisoning the connection.
Playwright storage sessions admit replies through the same table.

Page.getFrameTree and Page.getResourceTree are the first registered
commands. Their one parser keeps per-frame facts tolerant (malformed child
entries remain explicit omissions) and replaces the per-site readers:
observeMainFrameUrl, mainFrameUrl, walkFrameTrees, frameTreeResources,
runtimeFrameTreeSchema and the scenario storage frame schema.

A reply without a frame tree used to be retried for two seconds and
reported as target_not_allowed; it is now one protocol_error.

A guard test counts literal CDP command names without a result schema per
source file; the baseline can only shrink.

* fix(browser): consume parsed CDP facts across boundaries

* refactor(browser): register target, storage and acknowledgement CDP replies

Register result schemas for Target.attachToTarget, Target.getTargetInfo,
Storage.getStorageKey, Page.addScriptToEvaluateOnNewDocument,
Debugger.setBreakpointByUrl, Debugger.evaluateOnCallFrame and fourteen
commands whose reply REA reads only as success. Their callers use the
parsed reply; the per-site readers are removed (attachedSessionId and the
scenario storage and service-worker zod schemas, which threw raw ZodError).

Playwright's Target.getTargetInfo in scenario attach now goes through the
same admission. The untyped-command baseline drops from 96 to 48.
2026-11-14 23:40:44 +00:00
morluto f3b697da5a refactor(cli): derive contract-backed command options from the tool contract (#10571)
* refactor(cli): derive contract-backed command options from the tool contract

Each contract-backed CLI command restated its contract's input fields as
hand-written incur options and mapped them back to snake_case input with
conditional spreads. Nothing checked that the two schemas agreed.

Add registerContractCommand: a per-command projection names only what the
CLI spells differently (positionals, caller-relative path fields, option
renames, CLI help wording, omitted optional fields with a reason). Option
schemas, decimal argv tokens and the contract's cross-field rules are
derived from the contract input schema, and parsed options map back to the
contract input mechanically.

Migrate inspect-sqlite-database and the thirteen flat binary-diagnostics
inspectors. Their --schema output is unchanged apart from property order,
and help lists options in contract order. Delete the SQLite superRefine that
mirrored the contract and the per-command spreads.

Add a parity test: every projected command exposes each contract field or
declares its omission, and each field admits exactly what the contract
admits for empty, NUL, relative-path, bound and numeric-spelling probes.

* refactor(cli): refuse unprojected contract input forms at registration

Address review findings on the contract projection:

- Bind inspect-sqlite-database to its contract by name instead of catalog
  position.
- Accept only field forms argv can carry (string, number, boolean, enum,
  and arrays of strings or enums) instead of rejecting a short list, so
  arrays of objects, tuples and other forms fail at registration.
- Refuse root input keywords other than the projected object constraints
  and presentation annotations, so a contract rule such as `not` cannot
  disappear from --schema.

* refactor(cli): describe contract commands independently of the CLI framework

Split the contract projection into a framework-neutral description and a
thin Incur adapter, so a later REA-owned CLI layer (#10583) can render the
same description.

- contractCliDescription.ts derives each positional and option from the
  contract and projection: key and short flag, help, argv admission
  schema, boolean-flag/repeatable/decimal/caller-path facts, object
  constraints renamed to CLI keys, the optional argv-time cross-field
  check, and the mapping back to contract input.
- contractCommand.ts only renders that description as Incur args and
  options and runs the handler.

Cross-field rules now refuse at argv parsing only when a projection asks
(inspect-sqlite-database does, keeping its behavior); otherwise the
service's contract parse refuses them as before. Exact-optional fields are
projected as optional because argv cannot carry an explicit undefined and
Incur's help reads flag and choice types only through optional.

The parity guard reads the descriptions, and also checks that boolean
fields parse as bare flags and that no projected field stays
exact-optional.
2026-11-15 04:30:18 +08:00
morluto 29fb608559 refactor(rust): report cargo-auditable section failures through ParseFailure (#10652)
* refactor(rust): report cargo-auditable section failures through ParseFailure

RustAuditableFormatFailure restated the malformed/unsupported algebra and
the provider translated it by hand; container BinaryFormatFailures were
re-wrapped and lost their kind. The reader now throws ParseFailureError
(format rust-auditable), container failures propagate unchanged, and the
provider maps both through parseFailureAnalysisError. A counted guard
keeps new per-format failure classes from returning.

* test(rust): pin the container's truncated failure facts through the Rust reader
2026-11-14 15:28:33 -05:00
morluto 3e0dc29041 fix(browser): type CDP command replies and parse frame trees once (#10604)
* fix(browser): type CDP command replies and parse frame trees once

CdpConnection.send is typed by method name. A command registered in
src/browser/protocol/CdpCommands.ts has its parameters typed from the
protocol table Playwright ships and its reply parsed once by its own result
schema; a mismatch rejects that command with CdpMalformedReply
(protocol_error, command, JSON pointer) without poisoning the connection.
Playwright storage sessions admit replies through the same table.

Page.getFrameTree and Page.getResourceTree are the first registered
commands. Their one parser keeps per-frame facts tolerant (malformed child
entries remain explicit omissions) and replaces the per-site readers:
observeMainFrameUrl, mainFrameUrl, walkFrameTrees, frameTreeResources,
runtimeFrameTreeSchema and the scenario storage frame schema.

A reply without a frame tree used to be retried for two seconds and
reported as target_not_allowed; it is now one protocol_error.

A guard test counts literal CDP command names without a result schema per
source file; the baseline can only shrink.

* fix(browser): consume parsed CDP facts across boundaries
2026-11-14 15:21:00 -05:00
morluto ef1b92bf1e test(mcp): check advertised schemas against a portable dialect in every profile (#10567)
* test(mcp): check advertised schemas against a portable dialect in every profile

* fix(mcp): validate advertised dialect by selected profile

* fix(mcp): refuse zero-padded repeat bounds and record host-specific path patterns
2026-11-14 15:20:31 -05:00
morluto 85da0bd4ee refactor(errors): require a reason for every analysis cancellation (#10566)
* refactor(errors): require a reason for every analysis cancellation

Add cancelledBy, the one constructor for AnalysisCancelledError. Its
reason argument is required: an aborted signal, an earlier cancellation,
a reason carried from an inner failure, or an OwnerCancellation when REA
closed the work's owner itself.

Closed adb, apktool, firmware, sqlite, gdb, web-bundle analyzer and
binary-session record owners now report OwnerCancellation.ownerClosed
instead of an absent reason. Firmware's predecessor wait returns its
cancellation as a Result, removing an unreachable reason-less fallback.

An architecture guard rejects new direct constructions, with a counted
per-file baseline that can only shrink.

* refactor(errors): share one aborted-signal predicate with cancelledBy

* refactor(process): return a departing caller's reason from a shared startup wait

* refactor(providers): build decompiler-provider cancellations with a required reason (#10587)

Migrate every AnalysisCancelledError construction under src/ghidra,
src/ida, src/cutter, src/jeb and src/kuna to cancelledBy and lower the
guard baseline from 671 to 572. Closed Kuna target commands report
OwnerCancellation.ownerClosed instead of an absent reason, and Kuna and
JEB close abort their lifetimes with that typed reason.

* fix(errors): read the reason of any signal-shaped cancellation source

* test(ci): cover the counted-baseline comparison behind the architecture guard
2026-11-14 15:20:03 -05:00
morluto 2f4c588584 fix(artifacts): require each reader traversal to declare its coverage (#10572)
* fix(artifacts): require each reader traversal to declare its coverage

* docs(artifacts): state that silent traversals report unknown coverage
2026-11-15 04:07:21 +08:00
morluto e53212655b refactor(contracts): derive the public tool catalog from one contract-group list (#10564)
* refactor(contracts): derive the public tool catalog from one contract-group list

toolContracts.ts and toolFamilies.ts each imported and listed the same
contract groups. toolContracts.ts now owns one ordered group object;
TOOL_CONTRACTS is its flattened values in the same catalog order, and
toolFamilies.ts reads TOOL_CONTRACT_GROUPS from it instead of re-importing
every group. The static catalog-source lint rule learns the
Object.values(groups).flat() composition so positional-contract enforcement
still discovers every group without executing modules.

* fix(lint): reject computed tool catalog groups
2026-11-15 04:07:17 +08:00
morluto d7250f5d46 refactor(ghidra): build the invalid-regex refusal with the shared owner (#10570)
Ghidra projected the bridge's invalid_regex_pattern code through an inline
copy of the refusal that RegexMatcher's invalidRegexPatternError already owns
for Kuna, Hopper and IDA. Ghidra now calls that owner, passing the bridge's
own message, which already names the pattern and Java's syntax error, as the
user message. The owner accepts error options so Ghidra keeps its cause and
diagnostics.
2026-11-14 15:03:11 -05:00
David Muñoz Rey 57150091d2 fix(ghidra): name the declared-seed recovery when no function covers an address (#10623)
Problem: code that Ghidra auto-analysis never reaches, such as DOS MZ
handlers stored only in runtime-filled dispatch tables, has no Ghidra
function. procedure_* lookups then returned a plain procedure_not_found
whose remediation pointed only at list_procedures, and
resolve_containing_procedure returned not_in_procedure with no caveat.
Neither outcome named the supported recovery: declaring the entry as a
function seed in REA_GHIDRA_SEED_FILE.

Fix: one Ghidra-owned recovery, ghidraFunctionSeedRecovery(source), names
the seed mechanism without claiming that the address holds code. It
follows the session source, because seeds apply only to a fresh import:
selected projects and owned checkpoints name the setting to unset first.
The procedure_not_found remediation appends it. A not_in_procedure
resolution now carries ghidraNotInProcedureLimitation, attached through
the same absence-caveat path as the empty-reference caveats and read from
the domain containing-procedure parser. outside_segments and found
results are unchanged. The installation guide's seed section says where
the recovery appears.

Validation: src/ghidra/GhidraProvider.behavior.test.ts fails on the base
(4 failed, 291 passed: both procedure_not_found rows lack the setting,
the selected-project row lacks the fresh-import seed recovery, and
not_in_procedure has no seed limitation) and passes after the fix
(295 passed). npm run check:fast passes. Real Ghidra, including seeding
real-mode DOS linear addresses, was not exercised.

Refs morluto/rea#3767
2026-11-14 14:52:52 -05:00
morluto c6474cc8e9 refactor(ghidra): drop the unreleased checkpoint v1 reader and a one-time pattern proof (#10560)
* refactor(ghidra): drop the unreleased checkpoint v1 reader and a one-time pattern proof

The Ghidra checkpoint manifest v1 shape and its upgrade to v2 arrived in the
same commit (c07ab4b994, first released in 6.9.0), and every release writes
v2, so no released build produced v1. A v1 manifest is now refused as
malformed and left unchanged, like any other unknown manifest.

portableSchemaPatterns.test.ts proved two advertised patterns equal the
look-ahead regexes they replaced by enumerating 90,000 strings. The lasting
invariant (no look-around in advertised patterns) is asserted catalog-wide by
advertisedPatternsVMode.test.ts for the default and full input profiles. The
current accept/refuse contract for dylib roots and the reserved process
environment name moves to the owning schema tests as small tables.

Refs #10515

* test(process): retain malformed environment name coverage
2026-11-14 14:51:45 -05:00
morluto dfad81a48b test(cli): publish polled readiness files only when complete (#10563)
The doctor cancellation fixture wrote its readiness JSON with
writeFileSync, which truncates and then writes. The test polls only for
the file's existence, so a read between the two steps parsed an empty
file and failed with "Unexpected end of JSON input" (main run
43604752864, PR run 43606296871).

Write to a pending path and rename it into place, as ghidraJavaDoctor and
darwinProcessRunTokenReader fixtures already do. Apply the same fix to the
two sibling fixtures that poll for a PID file's existence and then parse
it: jadxConfiguration and runnerCancellation, where an empty read becomes
PID 0.
2026-11-14 14:51:30 -05:00
morluto 3fbdf0057b fix(ghidra): commit analysis profiles to the bridge script sources (#10562)
* fix(ghidra): commit analysis profiles to the bridge script sources

Snapshot reuse for Ghidra depends on the analysis profile digest, but the
profile committed only to hand-bumped semantic markers. A bridge change that
forgot to bump its marker left earlier snapshots admissible (#3664).

Add ghidraBridgeSourcesDigest, a SHA-256 over every packaged bridge source by
name, and commit it as bridge_sources_sha256 in every Ghidra profile, for new
imports and existing projects alike. A changed script now changes the profile
digest without anyone remembering a marker. Existing snapshots are not reused
once, after upgrading.

GhidraRawProfileDigest.test.ts pinned literal digests that every bridge or
marker change had to re-record; it now checks the committed parameters and
that the digest is the profile owner's digest of them. A new test checks that
the source list covers every packaged .java file.

* fix(ghidra): frame bridge digest metadata as a tuple
2026-11-14 14:51:19 -05:00
morluto e68e548a3d fix(cli): validate plist and dylib-root options with their contract fields (#10557)
* fix(cli): validate plist and dylib-root options with their contract fields

inspect-plist --relative-path and trace-dylib-resolution --root were
declared as bare strings on the CLI. --schema under-advertised them, and an
empty or NUL value reached the operation contract, whose refusal named the
contract field (path, roots) instead of the CLI option.

Declare both options with the contract's own field schemas
(localPathStringSchema; dylibResolutionInputSchema.shape.roots), so the CLI
refuses them first and names --relative-path or --root.

Fixes #9764
Fixes #9823

* fix(mcp): observe SDK initiated cleanup failures
2026-11-14 14:50:59 -05:00
morluto 01192ab737 test(hopper): assert retained cleanup without optional diagnostics (#10636) 2026-11-14 14:40:59 -05:00
morluto f5c8c1f9e0 fix(setup): derive client config roots from each client's own reader (#10596)
Configuration roots read from XDG_CONFIG_HOME, VSCODE_APPDATA and
VSCODE_PORTABLE now come from one module whose functions reproduce a named
reader: xdg-basedir (OpenCode, Kilo), the XDG rule used by Rust dirs and
etcetera (Zed, Goose), and VS Code's getUserDataPath. mcp add's ad hoc
platformPaths() is removed. Zed no longer takes a relative XDG_CONFIG_HOME
that Zed ignores, and the Cline extension and Kilo's VS Code marker follow
VS Code's portable and VSCODE_APPDATA selection.
2026-11-14 14:40:10 -05:00
morluto a09d3bed94 refactor(filesystem): hash admitted target descriptors with the shared stable hasher (#10573)
BinaryTargetResolver kept a private copy of StableFileHash's descriptor read
loop because it must hash the same descriptor it classified, admitted with
symlinks rejected, while hashStableFile opens its own descriptor and follows
symlinks. The loop is now hashStableRegularFileDescriptor, which takes the
admitted descriptor and the caller's symlink setting; readStableFile and the
resolver both use it. The resolver keeps symlinks: reject and maps a change
to its existing BinaryTargetError and cancellation to AnalysisCancelledError.
2026-11-14 14:37:30 -05:00