diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 242253783f..55221d1261 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,10 +16,9 @@ import inventory. For current provider wiring, start with [`src/composition/binary.ts`](src/composition/binary.ts) for deep candidates and the other `src/composition/` factories for tool services, including [`gpu.ts`](src/composition/gpu.ts) and [`model.ts`](src/composition/model.ts). -Live Frida and Cutter adapters are constructed directly in -[`src/cli/fridaCommands.ts`](src/cli/fridaCommands.ts), -[`src/cli/cutterCommands.ts`](src/cli/cutterCommands.ts), and -[`src/server/createServer.ts`](src/server/createServer.ts). +Live Frida, Cutter, GDB and Rizin construction also uses shared composition +factories. For service ownership, availability and catalog declarations, follow +the [capability composition guide](docs/capability-composition.md). Production factories construct fresh providers without starting engines or acquiring targets; optional adapter failures must preserve successful peers. Run `npm run verify:test-discovery` after adding or moving tests. Resolved import diff --git a/docs/architecture.mermaid b/docs/architecture.mermaid index 4f36c28aac..0a54a27025 100644 --- a/docs/architecture.mermaid +++ b/docs/architecture.mermaid @@ -8,7 +8,7 @@ graph TD end subgraph "Production Composition" - COMPOSITION["src/composition/
fresh typed provider and session factories
one-shot binding and independent optional loading
static WASM/EVM descriptors: named contracts, factories, retryable lifetimes"] + COMPOSITION["src/composition/
fresh typed provider and session factories
one-shot binding and independent optional loading
typed capability bindings: construction, availability and retryable lifetimes
pure capability declarations feed provider catalogs"] end subgraph "Adapters" @@ -23,6 +23,7 @@ graph TD SESSION["SessionProviderRouter + BinarySession
one immutable deep binding per target
disjoint auxiliary routing"] REGISTRY["AnalysisProviderRegistry
sorted deep candidates
deterministic selection, no fallback
ProviderInspectionScope: prerequisite observation lifetime"] RECORDS["src/application/investigation/
composed InvestigationRecords + narrow ports
EvidenceLedger and Unknown ownership"] + LIFETIMES["CapabilityScope
host-owned service close capabilities
released peers removed; failed cleanup retained for retry"] SNAPSHOT["BinarySessionRecords + AnalysisSnapshotCache
binary target/profile snapshots and session file ownership
record coordination and post-commit notifications"] JS_RECON["src/application/javascript/ + src/artifacts/javascript/
JavaScript artifact reconstruction
directory/ASAR reads
AST-only bundle projection"] end @@ -99,6 +100,7 @@ graph TD MAIN --> COMPOSITION CLI --> COMPOSITION SERVER --> COMPOSITION + COMPOSITION --> LIFETIMES COMPOSITION --> SESSION APP --> FIRMWARE FIRMWARE --> PROCESS @@ -149,8 +151,7 @@ graph TD OBJDUMP --> DOMAIN COMPOSITION --> RIZIN RIZIN --> DOMAIN - CLI --> GDB - SERVER --> GDB + COMPOSITION --> GDB GDB --> PROCESS GDB --> DOMAIN HOPPER --> HOPPER_BRIDGE @@ -206,8 +207,7 @@ graph TD NATIVE_READERS --> DOMAIN COMPOSITION --> FORMAT_READERS FORMAT_READERS --> DOMAIN - SERVER --> RUNTIME - CLI --> RUNTIME + COMPOSITION --> RUNTIME RUNTIME --> DOMAIN PROCESS --> FILESYSTEM AUX --> FILESYSTEM diff --git a/docs/capability-composition.md b/docs/capability-composition.md new file mode 100644 index 0000000000..e39076993f --- /dev/null +++ b/docs/capability-composition.md @@ -0,0 +1,107 @@ +# Capability composition and ownership + +REA keeps domain contracts and shared workflows separate from provider adapters +and CLI/MCP translation. A capability can have several provider identities; a +provider can contribute operations to several capabilities. Those associations +are source facts, while host availability is an observation. + +## Source declarations and runtime bindings + +`src/composition/capabilityDeclarations.ts` contains static provider identities, +named contract-group references and precise operation subsets. Complete groups +refer to `TOOL_CONTRACT_GROUPS` in the existing contract owner; family definitions +and provider catalogs read the same arrays, so a new group operation is included +without updating a copied provider list. Metadata imports only the group ID type +and pure metadata leaves; it must not load factories, schemas, engines or +generated catalogs. Catalog generation resolves groups and subsets against the +canonical contracts and rejects unknown groups/operations and conflicting +identities. New provider provenance must come from the implementation +that actually produces the Evidence, including format-specific capture adapters +and derived workflows. + +`src/composition/capabilityServices.ts` binds typed service factories to those +capability keys. Each runtime entry owns construction, cleanup and any available +host/configuration facts or live probes. A service entry requires a close +capability; entries adapting managers retain their existing `closeAll` operation. +The host maps entries without erasing their individual service/port types. There +is no generic JSON operation dispatcher. + +These two declarations have different loading needs. Static facts can be read +without loading a schema or provider graph. The full runtime composition is +materialized only when the MCP runtime needs it. One-shot CLI adapters import the +selected factory rather than constructing a complete capability host. CLI and MCP +reuse the same provider/service factories and application workflows, while +keeping their respective argument translation and lifetime. + +## Resource lifetime + +`CapabilityScope` registers a module's actual close capability when its instance +is composed. There is no independent server provider-close list to update. +Concurrent shutdown calls join the same cleanup attempt. A successful peer is +removed from pending cleanup; failed peers remain owned and are retried on the +next close. Synchronous throws and rejections without a reason remain failures, +and cannot prevent other peers from attempting cleanup. Shutdown seals further +ownership registration. + +The host distinguishes these lifetimes: + +- Host-owned service instances are released by that host. +- Caller-owned optional observations remain borrowed by SDK server instances; + their process-level owner closes them, including discarded discovery probes. +- CLI invocations use their existing finalizers to join cleanup and preserve the + original observation, cancellation and cleanup errors. +- Binary engines, bundle readers and investigation records retain their existing + owners. The `BinarySession` transition/drain barrier still coordinates target + changes, reset and disposal. A module refactor must not create independent + barriers that allow late work to repopulate reset records. + +An application workflow may borrow a shared resource scope, such as byte entropy +using the host's artifact scope. Its wrapper does not acquire a second ownership +claim over the borrowed scope. + +## Availability and registration + +Module bindings provide host support, selected configuration and precisely typed +live availability probes through `CapabilityAvailabilityFacts`. Constructors do +not run these probes or launch engines. Session discovery invokes the existing +provider-owned probes only for advertised families; live failures remain fresh +observations. Caller policy, selected environments and injected providers retain +their existing precedence. + +MCP registration remains in the MCP adapter. It consumes composed services, +record ports, admission and logging; it must not add fallback constructors that +can escape the host owner. Named contracts bind handlers and schemas. The complete +tool-family registrar map is checked by TypeScript, and catalog order never +selects a handler or schema. + +## Extending a capability + +1. Add or reuse the domain contract and application workflow, with explicit + Evidence provenance, coverage, unknowns and caller choices. +2. Put concrete construction in `src/composition/` and keep its default factory + lazy. Reference adapter-owned pure identity metadata from both the provider + and its static capability declaration. +3. Add the runtime binding if a host-owned service is needed. Bind cleanup and + availability/probes there, preserving borrowed versus owned resources. +4. Add the CLI/MCP argument adapters and their named registrations. A one-shot + command uses the shared selected factory and existing finalization helpers. +5. Run source-boundary, discovery, SDK-schema and relevant public workflow checks. + Exercise partial cleanup, retry, cancellation and caller-owned peers at the + applicable production seam; use actual applications/providers for routing and + complete Evidence verification. Recording ports do not prove real providers. + +The constructor boundary guard covers the centralized Frida, Cutter, GDB and +Rizin constructors without rejecting legitimate type-only injected interfaces. +Other settled pure/provider boundaries continue checking type imports too. The +shared parser records module-edge erasure once. Whole-declaration type imports +are erased; inline type bindings retain their runtime module edge under +`verbatimModuleSyntax`. Static dynamic imports remain runtime edges too. +Tests belong with their semantic owner and must be +found exactly once; provider roots need the corresponding adapter discovery lane. + +This is an internal composition contract. External plugin distribution, automatic +provider installation and a universal provider interface are separate concerns. + +The [initial verification report](verification/capability-composition-2026-11-01.md) +records the executed public workflows, cleanup boundaries, resource measurements +and remaining provider qualification gaps for this composition increment. diff --git a/docs/verification/capability-composition-2026-11-01.md b/docs/verification/capability-composition-2026-11-01.md new file mode 100644 index 0000000000..f17a7c252c --- /dev/null +++ b/docs/verification/capability-composition-2026-11-01.md @@ -0,0 +1,246 @@ +# Capability composition verification — 2026-11-01 + +Scope: issue #8429, the service composition/ownership increment of #740. Baseline +`6e9ee97a69cb16c7150d8b550e705cba055ee4f0`; implementation `bd9bf76d8`, followed +by type-only binding cleanup `161c3c146`. This changes existing composition, +metadata ownership and cleanup bookkeeping; no new provider/target authority. +`BinarySession` keeps its existing transition/admission barrier. + +## Executed source and boundary checks + +Full typecheck, module boundaries, architecture guards, test discovery, +production reachability, documentation consistency and repository formatting +passed. Changed-file typed lint passed with one worker and `GOMEMLIMIT=512MiB`: +39.622 s elapsed, 39.261 s process-family CPU, 2,945,368,064 B kernel peak. +Initial whole-repository typed and ordinary lint attempts hit the configured +3 GiB early stop; no OOM occurred. Whole-repository lint is not locally +qualified; changed-file checks and the final ownership-boundary typed checks +passed separately. + +Relevant executed groups: 216 parser/boundary/metadata/provider-owner tests; +5 scope/factory tests; 27 MCP availability/composition tests; 28 deferred +startup/protocol/publication tests; 50 CLI factory/cleanup/target-input tests; +16 SDK schema tests; 4 compiled-verifier metadata link tests; and investigation +lifecycle, unknown-history and transport cleanup regressions. These groups +contain overlapping repeated checks and are not a unique total. The final +Android transport group passed all 4 cases, including construction failure and +failed cleanup retry. The moved Frida test body was preserved byte-for-byte; +its discovery owner changed to the adapter lane. + +The old manual provider-close list was temporarily restored as a negative +control: the new successful-peer cleanup regression failed, then the current +source was restored. Synchronous close throws, undefined rejection reasons, +concurrent close joining, borrowed optional observations and failed-owner retry +are controlled tests, rather than claims of natural failures in real engines. +An initial new transport assertion omitted the existing outer +`OwnedMcpServers` AggregateError; its expected nesting was corrected. Other +preserved harness failures were missing SDK catalog preparation and two wrong +CLI test paths; corrected commands passed. One formatting issue was corrected. + +Ordinary Knip reports one unused exported `DarwinHelperCache` type at +`src/process/DarwinHelperCache.ts:465`. An independent run on the clean pinned +baseline reports the identical sole issue; that file is unchanged here. + +## Real applications and complete-result parity + +Every case used actual public CLI/MCP routes. Inputs were hashed before/after, +with no source mutation. Both arms used pinned Node 24.22.1/npm 11.16.0 and +fresh isolated processes/configuration; OS page caches were uncontrolled. + +| Domain | Genuine application/artifact | Independent fact oracle | +| ---------- | ------------------------------------------------------------------- | ----------------------------------------------- | +| Go | installed GitHub CLI `/usr/bin/gh`, Go 1.19.6 | ELF Go build-info bytes/module data | +| Managed | SDK 8.0.427 `NuGet.CommandLine.XPlat.dll`, selected Program members | independent dnfile metadata/CIL facts | +| JavaScript | installed agent-browser 0.27, selected entry source | original source declarations/imports | +| Android | NewPipe 0.29.1 APK inventory/application graph | complete ZIP member census and 9 member digests | +| WASM | actual CSS WASM artifact with WABT 1.0.42 | independent raw sections/types/exports | + +All five full public CLI Evidence results matched across arms. Explicit Go/JS +per-arm bundle output paths were verified separately, and their complete saved +parent Evidence matched. MCP performed 117 calls per arm, including six fresh +producer calls, retained derivation, repeated close, checkpoint/reset, expected +missing references, same-process restore and actual fresh-process restore. +Complete producer bundles, full derived Evidence, exact requests/IDs and +complete checkpoints matched across arms. The analyst Unknown revision 1→2 +history and every canonical mutation/history record remained authenticated; +fresh re-exported checkpoints were byte-identical. Fresh consumers ran no +producer calls. Ten independent domain fact assessments passed. + +| Completed workflow | Baseline elapsed / CPU (s) | Feature elapsed / CPU (s) | Baseline / feature kernel peak (B) | +| ------------------------ | -------------------------- | ------------------------- | ---------------------------------- | +| Five CLI tasks | 17.675 / 16.268 | 22.192 / 20.667 | 393,936,896 / 390,504,448 | +| Five MCP lifecycle tasks | 68.270 / 65.500 | 61.201 / 58.041 | 731,680,768 / 776,331,264 | + +Times are single observations including setup and all calls, not a causal +speedup/regression claim. Fresh-process tasks are reported separately from +same-process/restored consumers; neither is a controlled cold OS-cache run. +All tasks ran serially under a one-CPU, 4 GiB hard/3 GiB early-stop, no-swap, +192-task guard. CPU and peak memory are kernel process-family counters at +payload exit while the observer remained alive; all completed tasks verified +owned cleanup and no OOM. Sampling/RSS is not substituted for these counters. +Scripted checks do not incur model tokens; model usage is recorded separately. + +## Native engine and Codex CLI + +Real Ghidra 12.1.4/JDK 21 on `/usr/bin/jq` passed 37 public MCP calls per +arm. Two new light address queries before/after healthy investigation reset +returned identical Evidence. The same MCP client retained the same owned JVM +PID, start time, process group and public run identity. Three retained native +compositions used authenticated historical dossiers; these dossiers were not +regenerated. Full derived results, original bundles and Unknown histories +matched across arms and survived same/fresh-process restore. + +A mixed-routing task passed 58 calls per arm, with explicit `jq → APK → managed` +metadata target switches and five new static producer families through the final +registry. These metadata switches do not imply launching every target engine. +Complete originals, derived results and checkpoint history matched across arms. + +| Completed task | Baseline elapsed / CPU (s) | Feature elapsed / CPU (s) | Baseline / feature kernel peak (B) | +| --------------------------- | -------------------------- | ------------------------- | ---------------------------------- | +| Ghidra/native lifecycle | 38.196 / 36.804 | 41.741 / 40.369 | 1,111,789,568 / 1,098,362,880 | +| Mixed target/domain routing | 27.557 / 26.984 | 27.255 / 26.527 | 1,039,253,504 / 1,037,910,016 | + +The native task includes a fresh engine start; its second query is a healthy +same-engine observation. Historical dossier reuse and fresh process consumers +are separate phases. These are single observations with uncontrolled OS caches. + +Local `codex-cli 0.165.0` ran one actual `client-code` trial with the requested +`gpt-6.1-terra` alias, reasoning `high`, and 19 selected tools. The resolved +server-side model identity is not inferred. It completed 32 MCP calls across +JavaScript, managed, Android and WASM: five new producer calls, retained +compositions, four expected missing-reference controls, close/reset/save/import +and Unknown revision 1→2. There were no unexpected MCP errors or timeout. + +The model's JS self-check remained explicitly **partial/unverified**: it used +`JSON.stringify`, whose property ordering changed after canonical import. +Independent full structural comparisons of all three actual results and the +baseline proved identical fields, values, array order and Evidence ID. The +first ordering difference was only the keys of +`graph.nodes[0].observations[0].properties`. This distinction is retained in the +assessment; the model's self-check is not relabeled a complete pass. + +An actual new, target-free MCP process replayed the model's checkpoint in +9 calls. Complete Evidence and Unknown revision/mutation histories authenticated, +and its 18,968,472 B export was byte-identical (17 Evidence records, +2 Unknown revisions, 1 head), SHA-256 +`165188d74298f0173c5730973ecb5354a1b966ca54ec46ee177efa049ebbc755`. +All four derived results and their complete producer records matched the +independent baseline. The copied authentication profile was removed; runtime, +production source and selected input hashes remained unchanged. + +Actual client usage for the single attempt: **336,490 input tokens**, of which +**295,680 were cached**; **3,659 output tokens** and **250 reasoning output +tokens** were reported. Cached input is a subset, not an additional token total. +Baseline model usage was not measured; neither token savings nor a speedup is +claimed. Scripted source-fact and fresh-process verifiers incur no model API +tokens and are separate from this model usage. + +| Completed verification | Elapsed / process-family CPU (s) | Kernel family peak (B) | +| ----------------------------------- | -------------------------------- | ---------------------- | +| Actual Codex CLI trial | 74.644 / 26.818 | 1,061,597,184 | +| Independent fresh checkpoint replay | 6.142 / 5.934 | 757,895,168 | + +Manual final-answer review verified the reported IDs, counts, digests, coverage +and Unknown state against complete baseline values, all actual model calls and +the independent replay. The outcome is supported with the explicit model JS +self-check limitation resolved independently. Maximum observed in-flight MCP +calls was one; no target command, web or file-change tool items occurred. +Factual-assessment SHA-256: +`33451102e5faf57f11c627481899f16c4fc47fa5cba5f12cd2ed781a3de54ccf`. +Final immutable input/runtime audit SHA-256: +`73abce493182a7b433b4b78737720640ceb41d6df435cb0fec915468a0a304c8`. + +## Catalog and build identity + +Both compact/full SDK catalogs are byte-identical to baseline: 234 tools, +6 prompts, 180 CLI commands. The provider projection changes from 63 to 67 rows +only for source-proven web-source/module tracing, Harmony and QLOG provenance +corrections. All other product sections are unchanged. Its expected provider +SHA-256 is `4234ed15d7e53f34599b59dfba1f678369c0ae95b2135c7ba3414d944ab156b6`. +The 66 static declarations have 49 runtime keys and 17 explicit source-owned +workflow/engine groups; 70 bindings union to 67 identities. Static imports reach +44 pure modules without schema, engine or factory runtime imports. + +The baseline runtime is `0e94962f64f3f532cc96b2432916f328f60058e97cd0e8e25f5c3e6ed3cdec10` +(2,670 files). Static pairs used clean `bd9bf76d8`, runtime +`62018a023b185dd9c6839799ed1fe4f085810161c723e508fc817d2fccdb59e3` +(2,713 files). Final type-only cleanup `161c3c146` rebuilt to +`d7be1d17dc6b5c23f33468398656ac4d3961380368be1f70dc767f0ba1fac6bf`. +The complete earlier manifest is cryptographically reconstructed by removing +one emitted trailing comma in `dist/composition/capabilityServices.js`'s +existing named import; every other runtime file is byte-identical. This is not +a claim that the two runtime hashes are identical. Native/mixed/model cases use +the final runtime. + +Raw inputs, outputs, commands, hashes, recorded failed checks and resource receipts +remain outside Git under +`/home/yann/yann/.rea-worktree-artifacts/20261101/capability-module-ownership/`. +Static-pair audit receipt SHA-256: +`801707569577c7cec848334cf72bfab26a7ff4a95c2a87872d71d78eef7765a3`. + +## Review corrections and final qualification + +Two actionable review findings were fixed at their producing owners: + +- `980991573`: import-edge facts now distinguish fully erased declarations from + inline type bindings, which still load their module under + `verbatimModuleSyntax`. Compiler-backed parser/boundary regressions passed; + the old parser fails the new expectations. A separate three-file probe through + the actually selected compiler emitted both inline runtime edges and erased + whole-declaration/type-query edges, without compiling the repository. +- `de6a61636`: the existing contract owner exposes 47 named canonical groups. + Thirty-seven full provider bindings reference those shared arrays, while + 33 deliberate provider-subset/engine selections remain precise. The actual + ADB metadata now includes a new valid canonical ADB operation without an edit + to the provider declaration; reverting to the copied production lists fails + the regression. Unknown groups, unknown names and identity conflicts fail + catalog generation. + +Final full typecheck, module boundaries, test discovery and changed-file typed +lint passed. Post-review source regressions passed 228 cases and SDK/MCP +regressions passed 34; the two strengthened boundary/metadata files passed +31 cases separately. These overlap earlier groups. Product catalog SHA-256 +remains exactly `e942c72047db651939dd47a31042e6818caaca7470092d63645801954a72b523`; +both SDK catalogs remain byte-identical to the original baseline. Static metadata +retains its pure loading boundary; canonical group IDs are imported only as types. + +The final implementation freeze is `de6a61636f3f84862cb4263c9ea219b7d2d4de36`, +with 2,713 runtime files, SHA-256 +`be1d60c7dcca57eeb7ad09d682efd172c7f1679215e796a291b29eb52e29bda1`. +Earlier model/engine measurements remain attributed to their executed revisions; +they are not relabeled as runs on this later freeze. + +A new actual mixed-routing task on this final freeze passed all 58 calls: +32.970 s elapsed, 32.180282 s process-family CPU, 1,042,538,496 B kernel peak. +Independent five-domain source facts, complete producer records and all five +full derived results/requests/IDs matched baseline and the prior qualified +implementation. Same-process and true fresh-process restore preserved the full +Unknown 1→2 mutation history and byte-identical 18,978,040 B checkpoint +(20 Evidence records, 2 Unknown revisions, 1 head), SHA-256 +`0316bd6271c6d401a18c10d552b2005d39c98f3bbbc5945ae4823875af13fd5f`. +Owned cleanup, runtime/production-source/input hashes and no OOM were verified. +Append-only final-routing receipt SHA-256: +`9c410c2c31bf38d7edc569deb95b93b7f9a2cf68a7b9a7f84d3ec924f0ad75cb`. +This bounded requalification adds no model invocation or repeated native +analysis query; earlier token measurements remain unchanged. + +CodeQL's six reported alert IDs (215, 490, 491, 493, 494, 495) all have open +instances on exact-base main `6e9ee97`. All 24 SARIF paths were reviewed and +independently checked: the new factory-default `process.env` sources retain the +existing command/directory selection and explicit local authority. The shared +filesystem/process sink files are byte-identical to base; Cutter's sink line +shift only removes its old identity constant. No source suppression or global +alert dismissal is included. The PR records the exact-base instance evidence +and the owner's authorized exception for this unrelated existing-alert check. + +## Qualification limits + +NewPipe coverage is static inventory/graph, not real JADX decompilation: JADX, +apktool and aapt2 were unavailable. No genuine local GGUF/safetensors target was +available. Real Hopper, IDA, JEB, Cutter GUI, GDB/Rizin debugger and Frida target +attachment are not qualified by these application cases; relevant factory, +protocol and cleanup tests use controlled seams. Historical native dossiers +are explicitly labeled reused evidence, not newly reproduced function analysis. +Retained records remain bounded by existing investigation policies; this does +not promise unlimited retention or a new durability backend. Further module +qualification and `BinarySession` simplification remain under parent #740. diff --git a/scripts/lib/catalog-builders.d.mts b/scripts/lib/catalog-builders.d.mts new file mode 100644 index 0000000000..d400f62084 --- /dev/null +++ b/scripts/lib/catalog-builders.d.mts @@ -0,0 +1,57 @@ +interface ProviderBinding { + readonly identity: { + readonly id: string; + readonly name: string; + readonly version: string | null; + }; + readonly operations: readonly string[] | { readonly group: string }; +} + +interface ProviderCatalogSources { + readonly toolContracts: { + readonly TOOL_CONTRACTS: readonly { readonly name: string }[]; + }; + readonly toolFamilies: { + readonly TOOL_CONTRACT_GROUPS: Readonly< + Record + >; + }; + readonly capabilityDeclarations: { + readonly CAPABILITY_DECLARATIONS: Readonly< + Record + >; + }; +} + +export function providerCatalog(sources: ProviderCatalogSources): { + readonly id: string; + readonly name: string; + readonly version: string | null; + readonly capabilities: readonly string[]; +}[]; + +export function toolFamilyCatalog(sources: { + readonly toolFamilies: { + readonly TOOL_FAMILY_DEFINITIONS: readonly { + readonly id: string; + readonly surface: string; + readonly contracts: readonly { readonly name: string }[]; + }[]; + }; + readonly toolContracts: { + readonly TOOL_CONTRACTS: readonly unknown[]; + }; + readonly catalogIdentity: { + readonly FULL_CATALOG_IDENTITY: { + readonly counts: { readonly mcp_tools: number }; + }; + }; +}): { + readonly total: number; + readonly families: readonly { + readonly id: string; + readonly surface: string; + readonly count: number; + readonly tools: readonly string[]; + }[]; +}; diff --git a/scripts/lib/catalog-builders.mjs b/scripts/lib/catalog-builders.mjs index 1e59826ac0..cb42ebba92 100644 --- a/scripts/lib/catalog-builders.mjs +++ b/scripts/lib/catalog-builders.mjs @@ -23,414 +23,55 @@ export const toolFamilyCatalog = (sources) => { return { total: familyTotal, families }; }; -/** Build provider identities with their sorted capability names. */ +/** Derive provider capabilities from the same static declarations used by composition. */ export const providerCatalog = (sources) => { - const contractsByName = new Map( - sources.toolContracts.TOOL_CONTRACTS.map((contract) => [ - contract.name, - contract, - ]), + const contractNames = new Set( + sources.toolContracts.TOOL_CONTRACTS.map(({ name }) => name), ); - const declaredContracts = (providerId, operations) => - operations.map((operation) => { - const contract = contractsByName.get(operation); - if (contract === undefined) + const providers = new Map(); + for (const declaration of Object.values( + sources.capabilityDeclarations.CAPABILITY_DECLARATIONS, + )) { + for (const { identity, operations } of declaration.providers) { + let provider = providers.get(identity.id); + if (provider === undefined) { + provider = { ...identity, capabilities: new Set() }; + providers.set(identity.id, provider); + } else if ( + provider.name !== identity.name || + provider.version !== identity.version + ) { throw new TypeError( - `Provider ${providerId} declares an unknown analyst operation: ${operation}`, + `Provider ${identity.id} has conflicting source identities`, ); - return contract; - }); - const { - applicationContracts, - reconciliationContracts, - observationContracts, - activeContracts, - fuseContracts, - } = electronContractSlices(sources); - return [ - { - identity: sources.cutterProvider.CUTTER_PROVIDER_IDENTITY, - contracts: sources.cutterContracts.CUTTER_TOOL_CONTRACTS, - }, - { - identity: sources.objdumpProvider.OBJDUMP_PROVIDER_IDENTITY, - contracts: - sources.reverseEngineeringContracts.REVERSE_ENGINEERING_TOOL_CONTRACTS.filter( - ({ name }) => name === "inspect_with_objdump", - ), - }, - { - identity: sources.rizinProvider.RIZIN_PROVIDER_IDENTITY, - contracts: - sources.reverseEngineeringContracts.REVERSE_ENGINEERING_TOOL_CONTRACTS.filter( - ({ name }) => name === "execute_rizin_command", - ), - }, - { - identity: sources.gdbProvider.GDB_PROVIDER_IDENTITY, - contracts: sources.gdbContracts.GDB_TOOL_CONTRACTS, - }, - { - identity: sources.rizinDebugProvider.RIZIN_DEBUGGER_PROVIDER_IDENTITY, - contracts: sources.rizinDebugContracts.RIZIN_DEBUG_TOOL_CONTRACTS, - }, - { - identity: sources.fridaProvider.FRIDA_PROVIDER_IDENTITY, - contracts: sources.fridaContracts.FRIDA_TOOL_CONTRACTS, - }, - { - identity: sources.hopperProvider.HOPPER_PROVIDER_IDENTITY, - contracts: declaredContracts( - "hopper", - sources.hopperProvider.HOPPER_OPERATIONS, + } + let selectedOperations = operations; + if (!Array.isArray(operations)) { + const contracts = + sources.toolFamilies.TOOL_CONTRACT_GROUPS[operations.group]; + if (contracts === undefined) + throw new TypeError( + `Provider ${identity.id} declares an unknown contract group: ${operations.group}`, + ); + selectedOperations = contracts.map(({ name }) => name); + } + for (const operation of selectedOperations) { + if (!contractNames.has(operation)) + throw new TypeError( + `Provider ${identity.id} declares an unknown analyst operation: ${operation}`, + ); + provider.capabilities.add(operation); + } + } + } + return [...providers.values()] + .map(({ id, name, version, capabilities }) => ({ + id, + name, + version, + capabilities: [...capabilities].sort((left, right) => + left.localeCompare(right), ), - }, - { - identity: sources.ghidraProvider.GHIDRA_PROVIDER_IDENTITY, - contracts: declaredContracts( - "ghidra", - sources.ghidraProvider.GHIDRA_OPERATIONS, - ), - }, - { - identity: sources.idaProvider.IDA_PROVIDER_IDENTITY, - contracts: declaredContracts("ida", sources.idaProvider.IDA_OPERATIONS), - }, - { - identity: sources.kunaProvider.KUNA_PROVIDER_IDENTITY, - contracts: declaredContracts( - "kuna", - sources.kunaProvider.KUNA_OPERATIONS, - ), - }, - { - identity: sources.nativeProvider.NATIVE_MACOS_PROVIDER_IDENTITY, - contracts: sources.nativeContracts.NATIVE_TOOL_CONTRACTS, - }, - { - identity: sources.pwntoolsProvider.PWNTOOLS_PROVIDER_IDENTITY, - contracts: - sources.binaryDiagnosticsContracts.BINARY_DIAGNOSTICS_TOOL_CONTRACTS.filter( - ({ name }) => - name === "inspect_binary_layout" || - name === "inspect_recorded_crash", - ), - }, - { - identity: sources.artifactProviders.PE_RESOURCES_PROVIDER, - contracts: - sources.binaryDiagnosticsContracts.BINARY_DIAGNOSTICS_TOOL_CONTRACTS.filter( - ({ name }) => name === "inspect_pe_resources", - ), - }, - { - identity: sources.artifactProviders.PE_CERTIFICATES_PROVIDER, - contracts: - sources.binaryDiagnosticsContracts.BINARY_DIAGNOSTICS_TOOL_CONTRACTS.filter( - ({ name }) => name === "inspect_pe_certificates", - ), - }, - { - identity: sources.artifactProviders.PE_IMPORTS_PROVIDER, - contracts: - sources.binaryDiagnosticsContracts.BINARY_DIAGNOSTICS_TOOL_CONTRACTS.filter( - ({ name }) => name === "inspect_pe_imports", - ), - }, - { - identity: sources.artifactProviders.DEVICETREE_PROVIDER, - contracts: - sources.binaryDiagnosticsContracts.BINARY_DIAGNOSTICS_TOOL_CONTRACTS.filter( - ({ name }) => name === "inspect_devicetree", - ), - }, - { - identity: sources.artifactProviders.PE_LOAD_CONFIG_PROVIDER, - contracts: - sources.binaryDiagnosticsContracts.BINARY_DIAGNOSTICS_TOOL_CONTRACTS.filter( - ({ name }) => name === "inspect_pe_load_config", - ), - }, - { - identity: sources.artifactProviders.ELF_NOTES_PROVIDER, - contracts: - sources.binaryDiagnosticsContracts.BINARY_DIAGNOSTICS_TOOL_CONTRACTS.filter( - ({ name }) => name === "inspect_elf_notes", - ), - }, - { - identity: sources.artifactProviders.APPLE_IPS_PROVIDER, - contracts: - sources.binaryDiagnosticsContracts.BINARY_DIAGNOSTICS_TOOL_CONTRACTS.filter( - ({ name }) => name === "inspect_apple_ips", - ), - }, - { - identity: sources.artifactProviders.KERNEL_MODULE_PROVIDER, - contracts: - sources.binaryDiagnosticsContracts.BINARY_DIAGNOSTICS_TOOL_CONTRACTS.filter( - ({ name }) => name === "inspect_kernel_module", - ), - }, - { - identity: sources.artifactProviders.BYTE_ENTROPY_PROVIDER, - contracts: - sources.binaryDiagnosticsContracts.BINARY_DIAGNOSTICS_TOOL_CONTRACTS.filter( - ({ name }) => name === "inspect_byte_entropy", - ), - }, - { - identity: sources.artifactProviders.NATIVE_EXPORTS_PROVIDER, - contracts: - sources.binaryDiagnosticsContracts.BINARY_DIAGNOSTICS_TOOL_CONTRACTS.filter( - ({ name }) => name === "inspect_native_exports", - ), - }, - { - identity: sources.captureProvider.TSHARK_PROVIDER_IDENTITY, - contracts: sources.captureContracts.CAPTURE_TOOL_CONTRACTS, - }, - { - identity: sources.signalProvider.SIGROK_PROVIDER_IDENTITY, - contracts: sources.signalContracts.SIGNAL_TOOL_CONTRACTS, - }, - { - identity: sources.wasmProvider.WABT_PROVIDER_IDENTITY, - contracts: sources.wasmContracts.WASM_TOOL_CONTRACTS.filter( - ({ kind }) => kind === "artifact-provider", - ), - }, - { - identity: sources.apkSignatureProvider.APK_SIGNATURE_PROVIDER_IDENTITY, - contracts: sources.apkSignatureContracts.APK_SIGNATURE_TOOL_CONTRACTS, - }, - { - identity: sources.evmProvider.EVMOLE_PROVIDER_IDENTITY, - contracts: sources.evmContracts.EVM_TOOL_CONTRACTS, - }, - { - identity: sources.goProvider.GO_BINARY_PROVIDER_IDENTITY, - contracts: sources.goContracts.GO_TOOL_CONTRACTS, - }, - { - identity: sources.gpuProvider.GPU_CODE_OBJECTS_PROVIDER_IDENTITY, - contracts: sources.gpuContracts.GPU_TOOL_CONTRACTS, - }, - { - identity: sources.il2cppProvider.IL2CPP_METADATA_PROVIDER_IDENTITY, - contracts: sources.il2cppContracts.IL2CPP_TOOL_CONTRACTS, - }, - { - identity: sources.modelProvider.MODEL_ARTIFACT_PROVIDER_IDENTITY, - contracts: sources.modelContracts.MODEL_TOOL_CONTRACTS, - }, - { - identity: sources.rustProvider.RUST_BINARY_PROVIDER_IDENTITY, - contracts: sources.rustContracts.RUST_TOOL_CONTRACTS, - }, - { - identity: sources.pythonPycProvider.PYTHON_PYC_PROVIDER_IDENTITY, - contracts: sources.pythonPycContracts.PYTHON_PYC_TOOL_CONTRACTS, - }, - { - identity: sources.erlangProvider.ERLANG_BEAM_PROVIDER_IDENTITY, - contracts: sources.erlangContracts.ERLANG_BEAM_TOOL_CONTRACTS, - }, - { - identity: sources.electronFuseProvider.ELECTRON_FUSE_PROVIDER_IDENTITY, - contracts: fuseContracts, - }, - { - identity: sources.sqliteDomain.SQLITE_PROVIDER_IDENTITY, - contracts: sources.sqliteContracts.SQLITE_TOOL_CONTRACTS, - }, - { - identity: sources.artifactProviders.ARTIFACT_GRAPH_PROVIDER, - contracts: [ - ...sources.artifactContracts.ARTIFACT_TOOL_CONTRACTS, - ...sources.desktopContracts.DESKTOP_APPLICATION_TOOL_CONTRACTS, - ], - }, - { - identity: sources.artifactProviders.MANAGED_STATIC_PROVIDER, - contracts: sources.managedContracts.MANAGED_TOOL_CONTRACTS, - }, - { - identity: sources.artifactProviders.MANAGED_WORKFLOW_PROVIDER, - contracts: - sources.managedWorkflowContracts.MANAGED_WORKFLOW_TOOL_CONTRACTS, - }, - { - identity: sources.firmwareProvider.BINWALK_PROVIDER_IDENTITY, - contracts: sources.firmwareContracts.FIRMWARE_TOOL_CONTRACTS.filter( - ({ name }) => name === "inspect_firmware_regions", - ), - }, - { - identity: sources.firmwareProvider.UNBLOB_PROVIDER_IDENTITY, - contracts: sources.firmwareContracts.FIRMWARE_TOOL_CONTRACTS.filter( - ({ name }) => name === "extract_firmware", - ), - }, - { - identity: sources.androidProvider.JADX_PROVIDER_IDENTITY, - contracts: sources.androidContracts.ANDROID_TOOL_CONTRACTS, - }, - { - identity: sources.pcapProvider.PCAP_PROVIDER_IDENTITY, - contracts: sources.pcapContracts.PCAP_TOOL_CONTRACTS, - }, - { - identity: sources.flutterProvider.FLUTTER_PROVIDER_IDENTITY, - contracts: sources.flutterContracts.FLUTTER_TOOL_CONTRACTS, - }, - { - identity: sources.apktoolProvider.APKTOOL_PROVIDER_IDENTITY, - contracts: sources.apktoolContracts.APKTOOL_TOOL_CONTRACTS, - }, - { - identity: sources.jebProvider.JEB_PROVIDER_IDENTITY, - contracts: sources.jebContracts.JEB_TOOL_CONTRACTS, - }, - { - identity: sources.adbProvider.ADB_PROVIDER_IDENTITY, - contracts: sources.adbContracts.ADB_TOOL_CONTRACTS, - }, - { - identity: sources.webNetworkCaptureProvider.HAR_CAPTURE_PROVIDER_IDENTITY, - contracts: - sources.webNetworkCaptureContracts.WEB_NETWORK_CAPTURE_TOOL_CONTRACTS, - }, - { - identity: sources.webNetworkCaptureProvider.SAZ_CAPTURE_PROVIDER_IDENTITY, - contracts: - sources.webNetworkCaptureContracts.WEB_NETWORK_CAPTURE_TOOL_CONTRACTS, - }, - { - identity: - sources.webNetworkCaptureProvider.MITMPROXY_CAPTURE_PROVIDER_IDENTITY, - contracts: - sources.webNetworkCaptureContracts.WEB_NETWORK_CAPTURE_TOOL_CONTRACTS, - }, - { - identity: sources.browserIdentities.CDP_BROWSER_PROVIDER_IDENTITY, - contracts: [ - ...sources.browserContracts.BROWSER_TOOL_CONTRACTS, - ...sources.webRuntimeContracts.WEB_RUNTIME_TOOL_CONTRACTS, - ], - }, - { - identity: - sources.browserIdentities.PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY, - contracts: - sources.browserScenarioContracts.BROWSER_SCENARIO_TOOL_CONTRACTS, - }, - { - identity: sources.browserIdentities.CDP_ELECTRON_PROVIDER_IDENTITY, - contracts: observationContracts, - }, - { - identity: - sources.browserIdentities.PLAYWRIGHT_ELECTRON_ACTIVE_PROVIDER_IDENTITY, - contracts: activeContracts, - }, - { - identity: sources.v8InspectorProvider.V8_INSPECTOR_PROVIDER_IDENTITY, - contracts: - sources.javascriptRuntimeObservationContracts - .JAVASCRIPT_RUNTIME_OBSERVATION_TOOL_CONTRACTS, - }, - { - identity: sources.artifactProviders.JAVASCRIPT_APPLICATION_PROVIDER, - contracts: applicationContracts, - }, - { - identity: - sources.artifactProviders.JAVASCRIPT_RUNTIME_RECONCILIATION_PROVIDER, - contracts: reconciliationContracts, - }, - { - identity: - sources.artifactProviders.JAVASCRIPT_APPLICATION_WORKFLOW_PROVIDER, - contracts: sources.applicationContracts.APPLICATION_TOOL_CONTRACTS.filter( - ({ name }) => - name !== "project_android_application_graph" && - name !== "project_apple_application_graph", - ), - }, - { - identity: sources.artifactProviders.ANALYSIS_VIEW_PROVIDER, - contracts: sources.analysisViewContracts.ANALYSIS_VIEW_TOOL_CONTRACTS, - }, - { - identity: sources.artifactProviders.ANDROID_APPLICATION_PROVIDER, - contracts: sources.applicationContracts.APPLICATION_TOOL_CONTRACTS.filter( - ({ name }) => name === "project_android_application_graph", - ), - }, - { - identity: sources.artifactProviders.APPLE_APPLICATION_PROVIDER, - contracts: sources.applicationContracts.APPLICATION_TOOL_CONTRACTS.filter( - ({ name }) => name === "project_apple_application_graph", - ), - }, - { - identity: sources.artifactProviders.WEB_SCRIPT_EXPORT_PROVIDER, - contracts: sources.webScriptContracts.WEB_SCRIPT_TOOL_CONTRACTS, - }, - { - identity: sources.javascriptRecoveryProvider.WAKARU_PROVIDER_IDENTITY, - contracts: - sources.javascriptRecoveryContracts.JAVASCRIPT_RECOVERY_TOOL_CONTRACTS, - }, - ] - .map(({ identity, contracts }) => ({ - id: identity.id, - name: identity.name, - version: identity.version, - capabilities: names(contracts), })) .sort((left, right) => left.id.localeCompare(right.id)); }; - -const electronContractSlices = (sources) => { - const applicationContracts = - sources.electronContracts.ELECTRON_TOOL_CONTRACTS.filter( - ({ name }) => name === "analyze_javascript_application", - ); - const reconciliationContracts = - sources.electronContracts.ELECTRON_TOOL_CONTRACTS.filter( - ({ name }) => name === "reconcile_javascript_runtime", - ); - const observationContracts = - sources.electronContracts.ELECTRON_TOOL_CONTRACTS.filter( - ({ name }) => - name !== "analyze_javascript_application" && - name !== "reconcile_javascript_runtime" && - name !== "capture_electron_scenario" && - name !== "inspect_electron_fuses", - ); - const activeContracts = - sources.electronContracts.ELECTRON_TOOL_CONTRACTS.filter( - ({ name }) => name === "capture_electron_scenario", - ); - const fuseContracts = - sources.electronContracts.ELECTRON_TOOL_CONTRACTS.filter( - ({ name }) => name === "inspect_electron_fuses", - ); - if ( - applicationContracts.length !== 1 || - reconciliationContracts.length !== 1 || - observationContracts.length !== 2 || - activeContracts.length !== 1 || - fuseContracts.length !== 1 - ) - throw new Error("Electron provider capability ownership drifted"); - return { - applicationContracts, - reconciliationContracts, - observationContracts, - activeContracts, - fuseContracts, - }; -}; diff --git a/scripts/lib/catalog-core.mjs b/scripts/lib/catalog-core.mjs index 102a9e3c1c..81ca68692e 100644 --- a/scripts/lib/catalog-core.mjs +++ b/scripts/lib/catalog-core.mjs @@ -39,131 +39,8 @@ const SOURCE_PATHS = { cli: "dist/cli.js", toolContracts: "dist/contracts/toolContracts.js", toolFamilies: "dist/contracts/toolFamilies.js", - binaryDiagnosticsContracts: - "dist/contracts/native/binaryDiagnosticsToolContracts.js", - cutterContracts: "dist/contracts/cutterToolContracts.js", - cutterProvider: "dist/cutter/CutterBridgeClient.js", - reverseEngineeringContracts: - "dist/contracts/reverseEngineeringToolContracts.js", - gdbContracts: "dist/contracts/gdbToolContracts.js", - gdbProvider: "dist/gdb/GdbSessionManager.js", - rizinDebugContracts: "dist/contracts/rizinDebugToolContracts.js", - objdumpProvider: "dist/objdump/ObjdumpCommand.js", - rizinProvider: "dist/rizin/RizinCommand.js", - rizinDebugProvider: "dist/rizin/RizinDebugSessionManager.js", - fridaContracts: "dist/contracts/fridaToolContracts.js", - fridaProvider: "dist/frida/FridaInstrumentationManager.js", - pwntoolsProvider: "dist/native/pwntools/PwntoolsRelease.js", - nativeContracts: "dist/contracts/native/nativeToolContracts.js", - captureContracts: "dist/contracts/captures/captureToolContracts.js", - captureProvider: "dist/captures/TsharkCaptureProvider.js", - signalContracts: "dist/contracts/signals/signalToolContracts.js", - signalProvider: "dist/signals/SigrokRecordingProvider.js", - wasmContracts: "dist/contracts/wasm/wasmToolContracts.js", - wasmProvider: "dist/wasm/WabtRelease.js", - apkSignatureContracts: - "dist/contracts/apkSignature/apkSignatureToolContracts.js", - apkSignatureProvider: "dist/apkSignature/ApkSignatureRelease.js", - evmContracts: "dist/contracts/evm/evmToolContracts.js", - sqliteContracts: "dist/contracts/sqlite/sqliteToolContracts.js", - sqliteDomain: "dist/domain/sqlite/sqliteDatabase.js", - evmProvider: "dist/evm/EvmoleRelease.js", - goContracts: "dist/contracts/go/goToolContracts.js", - goProvider: "dist/go/GoBinaryProvider.js", - gpuContracts: "dist/contracts/gpu/gpuToolContracts.js", - gpuProvider: "dist/gpu/GpuCodeObjectsProvider.js", - modelContracts: "dist/contracts/model/modelToolContracts.js", - modelProvider: "dist/model/ModelArtifactProvider.js", - il2cppContracts: "dist/contracts/il2cpp/il2cppToolContracts.js", - il2cppProvider: "dist/il2cpp/Il2cppMetadataProvider.js", - rustContracts: "dist/contracts/rust/rustToolContracts.js", - rustProvider: "dist/rust/RustBinaryProvider.js", - pythonPycContracts: "dist/contracts/python/pythonPycToolContracts.js", - pythonPycProvider: "dist/python/PythonPycProvider.js", - erlangContracts: "dist/contracts/erlang/beamToolContracts.js", - erlangProvider: "dist/erlang/BeamProvider.js", - artifactContracts: "dist/contracts/artifactToolContracts.js", - desktopContracts: "dist/contracts/desktopApplicationToolContracts.js", - managedContracts: "dist/contracts/managed/managedToolContracts.js", - firmwareContracts: "dist/contracts/firmware/firmwareToolContracts.js", - firmwareProvider: "dist/firmware/FirmwareRelease.js", - androidContracts: "dist/contracts/android/androidToolContracts.js", - flutterContracts: "dist/contracts/flutter/flutterToolContracts.js", - flutterProvider: "dist/flutter/FlutterBuildProvider.js", - androidProvider: "dist/android/JadxRelease.js", - apktoolContracts: "dist/contracts/apktool/apktoolToolContracts.js", - pcapContracts: "dist/contracts/pcap/pcapToolContracts.js", - pcapProvider: "dist/pcap/PcapProvider.js", - apktoolProvider: "dist/apktool/ApktoolProvider.js", - jebContracts: "dist/contracts/jeb/jebToolContracts.js", - jebProvider: "dist/jeb/JebProvider.js", - adbContracts: "dist/contracts/adb/adbToolContracts.js", - adbProvider: "dist/adb/AdbProvider.js", - managedWorkflowContracts: - "dist/contracts/managed/managedWorkflowToolContracts.js", - browserContracts: "dist/contracts/browserToolContracts.js", - webRuntimeContracts: "dist/contracts/webRuntimeToolContracts.js", - webNetworkCaptureContracts: - "dist/contracts/webNetworkCaptureToolContracts.js", - webNetworkCaptureProvider: "dist/browser/history/CaptureRelease.js", - browserScenarioContracts: "dist/contracts/browserScenarioToolContracts.js", - electronContracts: "dist/contracts/javascript/electronToolContracts.js", - electronFuseProvider: "dist/electron/ElectronFuseProvider.js", - javascriptRuntimeObservationContracts: - "dist/contracts/javascript/javascriptRuntimeObservationToolContracts.js", - applicationContracts: "dist/contracts/applicationToolContracts.js", - analysisViewContracts: "dist/contracts/analysisViewToolContracts.js", - webScriptContracts: "dist/contracts/webScriptToolContracts.js", - javascriptRecoveryContracts: - "dist/contracts/javascript/javascriptRecoveryToolContracts.js", - javascriptRecoveryProvider: "dist/javascript/recovery/WakaruRelease.js", + capabilityDeclarations: "dist/composition/capabilityDeclarations.js", supportedClients: "dist/application/SupportedClients.js", - hopperProvider: "dist/hopper/HopperProviderCapabilities.js", - ghidraProvider: "dist/ghidra/GhidraProviderCapabilities.js", - idaProvider: "dist/ida/IdaProviderCapabilities.js", - kunaProvider: "dist/kuna/KunaProviderCapabilities.js", - nativeProvider: "dist/native/NativeMacOSProviderMetadata.js", - artifactProviders: "dist/domain/investigationProviders.js", - browserIdentities: "dist/browser/providerIdentities.js", - v8InspectorProvider: "dist/inspector/providerIdentity.js", - evidence: "dist/domain/evidence.js", - evidenceBundle: "dist/domain/evidenceBundle.js", - processCapture: "dist/domain/process/processCapture.js", - analysisSnapshot: "dist/domain/analysisSnapshot.js", - artifactGraph: "dist/domain/artifactGraph.js", - browserObservation: "dist/domain/browserObservation.js", - browserScenario: "dist/domain/browserScenario.js", - browserScenarioCapture: "dist/domain/browserScenarioCapture.js", - browserScenarioDiff: "dist/domain/browserScenarioDiff.js", - browserSession: "dist/domain/browserSession.js", - electronObservation: "dist/domain/javascript/electronObservation.js", - electronActiveObservation: - "dist/domain/javascript/electronActiveObservation.js", - javascriptRuntimeObservation: - "dist/domain/javascript/javascriptRuntimeObservation.js", - webBundleAnalysis: "dist/domain/webBundleAnalysis.js", - webCaptureDiff: "dist/domain/webCaptureDiff.js", - managedArtifact: "dist/domain/managed/managedArtifact.js", - managedComparison: "dist/domain/managed/managedMemberComparison.js", - managedNativeVerification: "dist/domain/managed/managedNativeVerification.js", - webMcpDiscovery: "dist/domain/webMcpDiscovery.js", - webScreenshot: "dist/domain/webScreenshot.js", - javascriptApplicationGraph: - "dist/domain/javascript/javascriptApplicationGraph.js", - javascriptApplicationAnalysis: - "dist/domain/javascript/javascriptApplicationAnalysis.js", - javascriptSemanticGraph: "dist/domain/javascript/javascriptSemanticGraph.js", - javascriptSemanticQuery: - "dist/domain/javascript/javascriptSemanticQuerySchemas.js", - javascriptSemanticTrace: - "dist/domain/javascript/javascriptSemanticTraceSchemas.js", - javascriptFeatureTrace: - "dist/domain/javascript/javascriptFeatureTraceSchemas.js", - javascriptVersionComparison: - "dist/domain/javascript/javascriptApplicationVersionComparisonSchemas.js", - reconstructionVerification: - "dist/domain/reconstructionVerificationSchemas.js", - residualUnknown: "dist/domain/residualUnknown.js", }; /** Load every compiled source module referenced by the catalog. */ diff --git a/scripts/lib/import-specifiers.d.mts b/scripts/lib/import-specifiers.d.mts index ae29fe371b..beeef8cbba 100644 --- a/scripts/lib/import-specifiers.d.mts +++ b/scripts/lib/import-specifiers.d.mts @@ -2,6 +2,8 @@ export interface ImportSpecifierFact { readonly specifier: string; readonly line: number; + /** True only when TypeScript erases this module edge under verbatimModuleSyntax. */ + readonly typeOnly: boolean; } /** Static string specifier on one import or re-export node, when it has one. */ diff --git a/scripts/lib/import-specifiers.mjs b/scripts/lib/import-specifiers.mjs index 776b0440da..0f7877dd39 100644 --- a/scripts/lib/import-specifiers.mjs +++ b/scripts/lib/import-specifiers.mjs @@ -26,6 +26,22 @@ export const importSpecifier = (node) => { } }; +/** Whether TypeScript erases this module edge under verbatimModuleSyntax. */ +const isTypeOnly = (node) => { + switch (node.type) { + case "TSImportType": + return true; + case "ImportDeclaration": + case "TSImportEqualsDeclaration": + return node.importKind === "type"; + case "ExportNamedDeclaration": + case "ExportAllDeclaration": + return node.exportKind === "type"; + default: + return false; + } +}; + /** * Static import and re-export specifiers in source order. * Comments and string literals are not imports. Non-string dynamic imports @@ -37,7 +53,11 @@ export const readImportSpecifiers = (source) => { traverseFast(tree, (node) => { const specifier = importSpecifier(node); if (specifier === undefined) return; - specifiers.push({ specifier, line: node.loc?.start.line ?? 1 }); + specifiers.push({ + specifier, + line: node.loc?.start.line ?? 1, + typeOnly: isTypeOnly(node), + }); }); return specifiers; }; diff --git a/scripts/lib/module-boundaries.mjs b/scripts/lib/module-boundaries.mjs index a5dbc26615..7277d9e44c 100644 --- a/scripts/lib/module-boundaries.mjs +++ b/scripts/lib/module-boundaries.mjs @@ -24,6 +24,8 @@ const MIGRATED_PROVIDER_ROOTS = new Set([ "inspector", "javascript", ]); +const CENTRALIZED_CONSTRUCTOR_OWNER = + /^src\/(?:frida\/FridaInstrumentationManager|cutter\/CutterBridge(?:Client|Service)|gdb\/GdbSessionManager|rizin\/RizinDebugSessionManager)\.(?:js|ts)$/u; const repositoryPath = (root, path) => relative(root, path).replaceAll("\\", "/"); @@ -35,7 +37,7 @@ const sourceTarget = (file, specifier) => { : undefined; }; -const failedBoundary = (file, target) => { +const failedBoundary = (file, target, typeOnly) => { if (!file.startsWith("src/") || !target.startsWith("src/")) return undefined; const layer = file.split("/")[1]; const targetLayer = target.split("/")[1]; @@ -75,6 +77,12 @@ const failedBoundary = (file, target) => { } if (layer === "application" && targetLayer === "composition") return "application-composition"; + if ( + (layer === "application" || layer === "server") && + !typeOnly && + CENTRALIZED_CONSTRUCTOR_OWNER.test(target) + ) + return "provider-construction"; if ( (layer === "application" || layer === "server") && (MIGRATED_PROVIDER_ROOTS.has(targetLayer) || @@ -92,11 +100,11 @@ export const inspectModuleBoundaries = (file, source, root) => { const absoluteFile = resolve(root, file); const importer = repositoryPath(root, absoluteFile); const violations = []; - for (const { specifier, line } of readImportSpecifiers(source)) { + for (const { specifier, line, typeOnly } of readImportSpecifiers(source)) { const resolved = sourceTarget(absoluteFile, specifier); if (resolved === undefined) continue; const target = repositoryPath(root, resolved); - const boundary = failedBoundary(importer, target); + const boundary = failedBoundary(importer, target, typeOnly); if (boundary !== undefined) violations.push({ file: importer, line, specifier, target, boundary }); } diff --git a/scripts/lib/real-hopper-inventory.mjs b/scripts/lib/real-hopper-inventory.mjs index 30cb5f8a4d..9b9ea51fd2 100644 --- a/scripts/lib/real-hopper-inventory.mjs +++ b/scripts/lib/real-hopper-inventory.mjs @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import { HOPPER_PROVIDER_IDENTITY } from "../../dist/hopper/HopperProviderCapabilities.js"; +import { HOPPER_PROVIDER_IDENTITY } from "../../dist/hopper/HopperProviderMetadata.js"; import { requireMcpEvidenceResult, requireEvidenceProvider, diff --git a/scripts/lib/real-hopper-selection.mjs b/scripts/lib/real-hopper-selection.mjs index 5206c0ad61..3e4c858773 100644 --- a/scripts/lib/real-hopper-selection.mjs +++ b/scripts/lib/real-hopper-selection.mjs @@ -1,4 +1,4 @@ -import { HOPPER_PROVIDER_IDENTITY } from "../../dist/hopper/HopperProviderCapabilities.js"; +import { HOPPER_PROVIDER_IDENTITY } from "../../dist/hopper/HopperProviderMetadata.js"; /** Verify the selected Hopper candidate and concrete provider binding. */ export const requireHopperSelection = (status, expected) => { diff --git a/scripts/lib/test-projects.mjs b/scripts/lib/test-projects.mjs index 627254fd42..6e0be70299 100644 --- a/scripts/lib/test-projects.mjs +++ b/scripts/lib/test-projects.mjs @@ -19,7 +19,7 @@ export const SOURCE_TEST_PROJECTS = [ group: "source", include: [ "src/*.test.ts", - "src/{android,apkSignature,apktool,jeb,adb,artifacts,browser,config,cutter,dotnet,electron,erlang,evm,firmware,flutter,pcap,ghidra,go,gpu,hopper,ida,inspector,javascript,main,native,process,providers,python,reference,rust,server,sqlite,wasm,windows}/**/*.test.ts", + "src/{android,apkSignature,apktool,jeb,adb,artifacts,browser,config,cutter,dotnet,electron,erlang,evm,firmware,flutter,frida,pcap,ghidra,go,gpu,hopper,ida,inspector,javascript,main,native,process,providers,python,reference,rust,server,sqlite,wasm,windows}/**/*.test.ts", ], }, { diff --git a/scripts/verify-packaged-ghidra-windows.mjs b/scripts/verify-packaged-ghidra-windows.mjs index e48f1b546e..c37da5b673 100644 --- a/scripts/verify-packaged-ghidra-windows.mjs +++ b/scripts/verify-packaged-ghidra-windows.mjs @@ -510,7 +510,7 @@ try { ); const { GHIDRA_OPERATIONS } = await import( pathToFileURL( - join(packageRoot, "dist/ghidra/GhidraProviderCapabilities.js"), + join(packageRoot, "dist/ghidra/GhidraProviderMetadata.js"), ) ); const expected = GHIDRA_OPERATIONS.filter( diff --git a/scripts/verify-real-ghidra-windows.mjs b/scripts/verify-real-ghidra-windows.mjs index ca64f38040..bc8062206d 100644 --- a/scripts/verify-real-ghidra-windows.mjs +++ b/scripts/verify-real-ghidra-windows.mjs @@ -8,20 +8,20 @@ import { fileURLToPath } from "node:url"; import { parseConfig } from "../dist/config/parseConfig.js"; import { resolveGhidraAnalysisProfile } from "../dist/ghidra/GhidraAnalysisProfile.js"; import { GhidraClient } from "../dist/ghidra/GhidraClient.js"; +import { GHIDRA_FUNCTION_OPERATIONS } from "../dist/ghidra/GhidraFunctionOperations.js"; import { - GHIDRA_FUNCTION_OPERATIONS, parseGhidraFunctionInput, parseGhidraFunctionResult, } from "../dist/ghidra/GhidraFunctionValues.js"; +import { GHIDRA_INVENTORY_OPERATIONS } from "../dist/ghidra/GhidraInventoryOperations.js"; import { - GHIDRA_INVENTORY_OPERATIONS, parseGhidraInventoryInput, parseGhidraInventoryResult, } from "../dist/ghidra/GhidraInventoryValues.js"; import { inspectVerifierGhidraInstallation } from "./lib/ghidra-installation-inspection.mjs"; import { SUPPORTED_GHIDRA_VERSION } from "../dist/ghidra/GhidraReleasePolicy.js"; import { GhidraHeadlessLauncher } from "../dist/ghidra/GhidraLauncher.js"; -import { GHIDRA_PROVIDER_IDENTITY } from "../dist/ghidra/GhidraProviderCapabilities.js"; +import { GHIDRA_PROVIDER_IDENTITY } from "../dist/ghidra/GhidraProviderMetadata.js"; import { WINDOWS_P0_NATIVE_PE_ARCHITECTURES, windowsP0Capabilities, diff --git a/scripts/verify-real-ghidra.mjs b/scripts/verify-real-ghidra.mjs index 612e4f9f14..2d4aa94e9b 100644 --- a/scripts/verify-real-ghidra.mjs +++ b/scripts/verify-real-ghidra.mjs @@ -13,7 +13,7 @@ import { GhidraClient } from "../dist/ghidra/GhidraClient.js"; import { inspectVerifierGhidraInstallation } from "./lib/ghidra-installation-inspection.mjs"; import { SUPPORTED_GHIDRA_VERSION } from "../dist/ghidra/GhidraReleasePolicy.js"; import { GhidraHeadlessLauncher } from "../dist/ghidra/GhidraLauncher.js"; -import { GHIDRA_PROVIDER_IDENTITY } from "../dist/ghidra/GhidraProviderCapabilities.js"; +import { GHIDRA_PROVIDER_IDENTITY } from "../dist/ghidra/GhidraProviderMetadata.js"; import { assertCleanup, diff --git a/scripts/verify-real-hopper-deadlines.mjs b/scripts/verify-real-hopper-deadlines.mjs index af2213ec6f..77f3b6b329 100644 --- a/scripts/verify-real-hopper-deadlines.mjs +++ b/scripts/verify-real-hopper-deadlines.mjs @@ -11,7 +11,7 @@ import { AnalysisCleanupLease } from "../dist/artifacts/AnalysisCleanupLease.js" import { resolveHopperAnalysisProfile } from "../dist/hopper/HopperAnalysisProfile.js"; import { HopperApplicationLauncher } from "../dist/hopper/BridgeLauncher.js"; import { HopperClient } from "../dist/hopper/HopperClient.js"; -import { HOPPER_PROVIDER_IDENTITY } from "../dist/hopper/HopperProviderCapabilities.js"; +import { HOPPER_PROVIDER_IDENTITY } from "../dist/hopper/HopperProviderMetadata.js"; import { loadRealHopperFixtureTargets } from "./lib/real-hopper-fixture.mjs"; // These options belong to the native client, below the CLI/MCP tool surface. diff --git a/scripts/verify-real-hopper.mjs b/scripts/verify-real-hopper.mjs index 5f31565b4a..6e6cce01e5 100644 --- a/scripts/verify-real-hopper.mjs +++ b/scripts/verify-real-hopper.mjs @@ -11,7 +11,7 @@ import { requireFunctionDossier, requirePseudocode, } from "./lib/real-hopper-assertions.mjs"; -import { HOPPER_PROVIDER_IDENTITY } from "../dist/hopper/HopperProviderCapabilities.js"; +import { HOPPER_PROVIDER_IDENTITY } from "../dist/hopper/HopperProviderMetadata.js"; import { HOPPER_TARGET_LEASE_DIRECTORY } from "../dist/process/HopperLease.js"; import { REA_WORKFLOW_PROVIDER } from "../dist/domain/investigationProviders.js"; import { loadRealHopperFixtureTargets } from "./lib/real-hopper-fixture.mjs"; diff --git a/src/adb/AdbProvider.ts b/src/adb/AdbProvider.ts index 904f78db07..36724a187a 100644 --- a/src/adb/AdbProvider.ts +++ b/src/adb/AdbProvider.ts @@ -1,3 +1,4 @@ +import { ADB_PROVIDER_IDENTITY } from "./AdbProviderIdentity.js"; import { absoluteCallerPath, appendCallerPath, @@ -152,13 +153,6 @@ import { type AdbCommandContext, } from "./AdbDeviceOperations.js"; -/** Stable identity for the ADB device-acquisition provider. */ -export const ADB_PROVIDER_IDENTITY = { - id: "adb", - name: "Android Debug Bridge", - version: null, -} as const; - const SHELL_TIMEOUT_MS = 30_000; const PACKAGE_LIST_TIMEOUT_MS = 60_000; const PACKAGE_LIST_MAX_OUTPUT_BYTES = 8 * 1024 * 1024; diff --git a/src/adb/AdbProviderIdentity.ts b/src/adb/AdbProviderIdentity.ts new file mode 100644 index 0000000000..0e0281b56b --- /dev/null +++ b/src/adb/AdbProviderIdentity.ts @@ -0,0 +1,6 @@ +/** Stable identity for the ADB device-acquisition provider. */ +export const ADB_PROVIDER_IDENTITY = { + id: "adb", + name: "Android Debug Bridge", + version: null, +} as const; diff --git a/src/apktool/ApktoolProvider.ts b/src/apktool/ApktoolProvider.ts index 8b935d4857..0bb7ef28fc 100644 --- a/src/apktool/ApktoolProvider.ts +++ b/src/apktool/ApktoolProvider.ts @@ -1,3 +1,4 @@ +import { APKTOOL_PROVIDER_IDENTITY } from "./ApktoolProviderIdentity.js"; import { countDecodedFiles } from "./ApktoolDecodedInventory.js"; import { ApktoolWorkspace } from "./ApktoolWorkspace.js"; import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; @@ -70,13 +71,6 @@ import { parseManifestPackage, } from "./ApktoolDecodeOutput.js"; -/** Stable identity for the Apktool resource-decoding provider. */ -export const APKTOOL_PROVIDER_IDENTITY = { - id: "apktool", - name: "Apktool", - version: null, -} as const; - const DECODE_MAX_OUTPUT_BYTES = 4 * 1024 * 1024; const MANIFEST_MAX_BYTES = 512 * 1024; const MAX_DECODED_ENTRIES_VISITED = 100_000; diff --git a/src/apktool/ApktoolProviderIdentity.ts b/src/apktool/ApktoolProviderIdentity.ts new file mode 100644 index 0000000000..c19311e4b7 --- /dev/null +++ b/src/apktool/ApktoolProviderIdentity.ts @@ -0,0 +1,6 @@ +/** Stable identity for the Apktool resource-decoding provider. */ +export const APKTOOL_PROVIDER_IDENTITY = { + id: "apktool", + name: "Apktool", + version: null, +} as const; diff --git a/src/application/CapabilityAvailability.ts b/src/application/CapabilityAvailability.ts new file mode 100644 index 0000000000..73659b9762 --- /dev/null +++ b/src/application/CapabilityAvailability.ts @@ -0,0 +1,50 @@ +import type { ProviderAvailability } from "./AnalysisProvider.js"; +import type { JebAnalysisAvailability } from "./jeb/JebAnalysisPort.js"; +import type { AvailabilityPolicy } from "./CapabilityInventory.js"; +import type { OptionalProviderLoadFailures } from "./OptionalObservationProviders.js"; + +export interface CapabilityAvailabilityFacts { + readonly probes?: Partial; + readonly observeProviderLoadFailures?: () => OptionalProviderLoadFailures; + readonly optionalProviderLoadFailures?: + | OptionalProviderLoadFailures + | undefined; + readonly hostUnsupported?: AvailabilityPolicy["hostUnsupported"]; + readonly optionalFeatures?: Pick< + AvailabilityPolicy, + | "packetCaptureEnabled" + | "signalRecordingEnabled" + | "wasmArtifactEnabled" + | "evmInterfaceEnabled" + | "browserObservationEnabled" + | "browserScenarioEnabled" + | "electronObservationEnabled" + | "electronAutomationEnabled" + | "v8InspectorObservationEnabled" + | "javascriptRecoveryEnabled" + | "webModuleResolutionEnabled" + | "binaryLayoutEnabled" + | "recordedCrashEnabled" + | "firmwareInspectionEnabled" + | "firmwareExtractionEnabled" + >; +} + +/** Provider-owned live probes used by session discovery. */ +export interface CapabilityAvailabilityProbes { + readonly androidAnalysisAvailability: ( + signal: AbortSignal, + ) => Promise; + readonly adbAnalysisAvailability?: ( + signal: AbortSignal, + ) => Promise; + readonly apktoolAnalysisAvailability?: ( + signal: AbortSignal, + ) => Promise; + readonly jebAnalysisAvailability?: ( + signal: AbortSignal, + ) => Promise; + readonly evmLimiterAvailability?: ( + signal: AbortSignal, + ) => Promise; +} diff --git a/src/application/CapabilityScope.test.ts b/src/application/CapabilityScope.test.ts new file mode 100644 index 0000000000..4842d8fafe --- /dev/null +++ b/src/application/CapabilityScope.test.ts @@ -0,0 +1,116 @@ +import { expect, it, vi } from "vitest"; + +import { awaitAcknowledgement } from "../../tests/fixtures/lifecycleAcknowledgement.js"; +import { CapabilityScope } from "./CapabilityScope.js"; + +it("retains each failed cleanup while releasing successful peers across retries", async () => { + const scope = new CapabilityScope("Controlled mixed capability cleanup"); + const synchronousFailure = new Error("Synchronous close refusal"); + const asynchronousFailure = new Error("Asynchronous close refusal"); + let allowSynchronous = false; + let allowAsynchronous = false; + const successful = vi.fn(async () => undefined); + const synchronous = vi.fn(() => { + if (!allowSynchronous) throw synchronousFailure; + return Promise.resolve(); + }); + const asynchronous = vi.fn(async () => { + if (!allowAsynchronous) throw asynchronousFailure; + }); + const rejectedWithoutReason = vi.fn(() => + allowAsynchronous ? Promise.resolve() : Promise.reject(undefined), + ); + scope.own("released-peer", {}, successful); + scope.own("synchronous-owner", {}, synchronous); + scope.own("asynchronous-owner", {}, asynchronous); + scope.own("undefined-rejection-owner", {}, rejectedWithoutReason); + try { + const first: unknown = await scope.close().catch((cause: unknown) => cause); + expect(first).toBeInstanceOf(AggregateError); + if (!(first instanceof AggregateError)) + throw new Error("Expected all cleanup failures"); + expect(first.message).toBe("Controlled mixed capability cleanup"); + expect(first.errors).toEqual([ + synchronousFailure, + asynchronousFailure, + undefined, + ]); + for (const close of [ + successful, + synchronous, + asynchronous, + rejectedWithoutReason, + ]) + expect(close).toHaveBeenCalledTimes(1); + expect(() => scope.own("late-owner", {}, successful)).toThrow( + "after host shutdown", + ); + + allowAsynchronous = true; + const second: unknown = await scope + .close() + .catch((cause: unknown) => cause); + expect(second).toBeInstanceOf(AggregateError); + if (!(second instanceof AggregateError)) + throw new Error("Expected the remaining owner failure"); + expect(second.errors).toEqual([synchronousFailure]); + expect(successful).toHaveBeenCalledTimes(1); + expect(synchronous).toHaveBeenCalledTimes(2); + expect(asynchronous).toHaveBeenCalledTimes(2); + expect(rejectedWithoutReason).toHaveBeenCalledTimes(2); + + allowSynchronous = true; + await scope.close(); + await scope.close(); + expect(synchronous).toHaveBeenCalledTimes(3); + expect(successful).toHaveBeenCalledTimes(1); + expect(asynchronous).toHaveBeenCalledTimes(2); + expect(rejectedWithoutReason).toHaveBeenCalledTimes(2); + } finally { + allowSynchronous = true; + allowAsynchronous = true; + await scope.close(); + } +}); + +it("joins concurrent shutdowns and seals ownership before asynchronous cleanup settles", async () => { + const scope = new CapabilityScope(); + let acknowledge = () => {}; + const entered = new Promise((resolve) => { + acknowledge = resolve; + }); + let release = () => {}; + const released = new Promise((resolve) => { + release = resolve; + }); + const close = vi.fn(async () => { + acknowledge(); + await released; + }); + scope.own("controlled-owner", {}, close); + const first = scope.close(); + let second: Promise | undefined; + try { + await awaitAcknowledgement(entered, first, "capability cleanup"); + second = scope.close(); + expect(second).toBe(first); + expect(close).toHaveBeenCalledTimes(1); + expect(() => scope.own("later-owner", {}, async () => undefined)).toThrow( + "after host shutdown", + ); + release(); + await Promise.all([first, second]); + expect(() => + scope.own("post-shutdown-owner", {}, async () => undefined), + ).toThrow("after host shutdown"); + await scope.close(); + expect(close).toHaveBeenCalledTimes(1); + } finally { + release(); + await Promise.allSettled([ + first, + ...(second === undefined ? [] : [second]), + ]); + await scope.close(); + } +}); diff --git a/src/application/CapabilityScope.ts b/src/application/CapabilityScope.ts new file mode 100644 index 0000000000..68dc4ea198 --- /dev/null +++ b/src/application/CapabilityScope.ts @@ -0,0 +1,40 @@ +/** One host owns these exact close capabilities; a released peer stays released. */ +export class CapabilityScope { + readonly #pending = new Map Promise>(); + #accepting = true; + #closing: Promise | undefined; + + constructor(readonly failureMessage = "REA provider cleanup failed") {} + + own(id: string, value: Value, close: () => Promise): Value { + if (!this.#accepting) + throw new Error(`Cannot acquire capability ${id} after host shutdown`); + if (this.#pending.has(id)) + throw new Error(`Capability ${id} already belongs to this host`); + this.#pending.set(id, close); + return value; + } + + /** Join concurrent closes, retaining only failed owners for the next retry. */ + close(): Promise { + this.#accepting = false; + this.#closing ??= Promise.allSettled( + [...this.#pending].map(async ([id, close]) => { + // A synchronous adapter throw must not prevent other owners' cleanup. + await Promise.resolve().then(close); + this.#pending.delete(id); + }), + ) + .then((results) => { + const failures = results.flatMap((result) => + result.status === "rejected" ? [result.reason] : [], + ); + if (failures.length > 0) + throw new AggregateError(failures, this.failureMessage); + }) + .finally(() => { + this.#closing = undefined; + }); + return this.#closing; + } +} diff --git a/src/application/WebModuleTraceService.ts b/src/application/WebModuleTraceService.ts index ae8f599ab0..090ec95b30 100644 --- a/src/application/WebModuleTraceService.ts +++ b/src/application/WebModuleTraceService.ts @@ -1,3 +1,4 @@ +import { WEB_MODULE_TRACE_PROVIDER } from "../domain/webTraceProviders.js"; import { closeOwnedWebResources } from "./closeOwnedWebResources.js"; import { isDeepStrictEqual } from "node:util"; import { z } from "zod"; @@ -273,11 +274,7 @@ export class WebModuleTraceService { sha256: data.sourceFile.sha256, format: "file", }, - { - id: "rea-web-module-trace", - name: "REA captured module relationship trace", - version: "1", - }, + WEB_MODULE_TRACE_PROVIDER, { operation: OPERATION, parameters: input, diff --git a/src/application/WebSourceLocationService.ts b/src/application/WebSourceLocationService.ts index 90244172d0..a818eb9316 100644 --- a/src/application/WebSourceLocationService.ts +++ b/src/application/WebSourceLocationService.ts @@ -1,3 +1,4 @@ +import { WEB_SOURCE_LOCATION_PROVIDER } from "../domain/webTraceProviders.js"; import { closeOwnedWebResources } from "./closeOwnedWebResources.js"; import { createHash } from "node:crypto"; import { z } from "zod"; @@ -209,11 +210,7 @@ export class WebSourceLocationService { sha256: data.sourceFile.sha256, format: "file", }, - { - id: "rea-web-source-location", - name: "REA captured source-map point trace", - version: "1", - }, + WEB_SOURCE_LOCATION_PROVIDER, { operation: OPERATION, parameters: input, diff --git a/src/application/binary/BinarySessionStatus.test.ts b/src/application/binary/BinarySessionStatus.test.ts index d0a714d83a..c1d97d8eda 100644 --- a/src/application/binary/BinarySessionStatus.test.ts +++ b/src/application/binary/BinarySessionStatus.test.ts @@ -25,9 +25,9 @@ import { } from "./BinarySessionStatus.js"; import { APPLE_DISPATCH_METADATA_ANALYSIS_PROFILE, - NATIVE_MACOS_PROVIDER_IDENTITY, nativeMacOSCapabilities, } from "../../native/NativeMacOSProviderMetadata.js"; +import { NATIVE_MACOS_PROVIDER_IDENTITY } from "../../native/NativeMacosProviderIdentity.js"; const projectedStatusSchema = z.object({ capabilities: sessionProviderFacts.shape.capabilities, diff --git a/src/application/erlang/BeamService.test.ts b/src/application/erlang/BeamService.test.ts index 7df93684ae..88a5221490 100644 --- a/src/application/erlang/BeamService.test.ts +++ b/src/application/erlang/BeamService.test.ts @@ -1,6 +1,6 @@ import { expect, it } from "vitest"; import { BeamService } from "./BeamService.js"; -import { ERLANG_BEAM_PROVIDER_IDENTITY } from "../../erlang/BeamProvider.js"; +import { ERLANG_BEAM_PROVIDER_IDENTITY } from "../../erlang/ErlangBeamProviderIdentity.js"; import type { ErlangBeam } from "../../domain/erlang/beam.js"; import { ok } from "../../domain/result.js"; import { diff --git a/src/application/sqlite/SqliteDatabaseCapability.ts b/src/application/sqlite/SqliteDatabaseCapability.ts index f3c0308676..fe3283bee3 100644 --- a/src/application/sqlite/SqliteDatabaseCapability.ts +++ b/src/application/sqlite/SqliteDatabaseCapability.ts @@ -1,6 +1,6 @@ import { satisfies } from "semver"; import { toolContract } from "../../contracts/toolContracts.js"; -import { SQLITE_PROVIDER_IDENTITY } from "../../domain/sqlite/sqliteDatabase.js"; +import { SQLITE_PROVIDER_IDENTITY } from "../../domain/sqlite/SqliteProviderIdentity.js"; const SQLITE_RUNTIME_REQUIREMENT = "SQLite inspection requires Node.js 24.x >=24.15 or 26+ with the native DatabaseSync.limits API. Other REA features retain their published runtime requirements. The inspection worker verifies the API before querying the snapshot."; diff --git a/src/browser/history/CaptureProviderIdentities.ts b/src/browser/history/CaptureProviderIdentities.ts new file mode 100644 index 0000000000..2dd17e9f3e --- /dev/null +++ b/src/browser/history/CaptureProviderIdentities.ts @@ -0,0 +1,28 @@ +/** Upstream packages and owned schema corrections for offline HAR decoding. */ +export const HAR_CAPTURE_PROVIDER_IDENTITY = { + id: "har-schema/lossless-json", + name: "REA HAR capture adapter", + version: + "har-schema@2.0.0;har-schema-corrections@1;jsonc-parser@3.3.1;lossless-json@4.3.1", +} as const; + +/** Native SAZ member decoding with the existing ZIP and XML package profiles. */ +export const SAZ_CAPTURE_PROVIDER_IDENTITY = { + id: "fiddler-saz", + name: "REA Fiddler SAZ capture adapter", + version: "1;zip.js@2.15.0;xmldom@0.9.12", +} as const; + +/** Native executable profile verified with unchanged mitmproxy raw-state decoding. */ +export const MITMPROXY_CAPTURE_PROVIDER_IDENTITY = { + id: "mitmproxy-native-tnetstring", + name: "REA offline native mitmproxy adapter", + version: "12.2.3", +} as const; + +export const QLOG_CAPTURE_PROVIDER_IDENTITY = { + id: "aioquic-qlog-json", + name: "REA aioquic qlog capture adapter", + version: + "aioquic-profile@1.5.0;qlog@0.3;jsonc-parser@3.3.1;lossless-json@4.3.1", +} as const; diff --git a/src/browser/history/CaptureRelease.ts b/src/browser/history/CaptureRelease.ts index b02ec806f5..2702622e0e 100644 --- a/src/browser/history/CaptureRelease.ts +++ b/src/browser/history/CaptureRelease.ts @@ -5,14 +5,6 @@ import { import { javaScriptRuntime } from "../../domain/runtimeVersion.js"; import { javaScriptWorkerMemoryLimitBytes } from "../../process/JavaScriptWorkerMemoryLimit.js"; -/** Upstream packages and owned schema corrections for offline HAR decoding. */ -export const HAR_CAPTURE_PROVIDER_IDENTITY = { - id: "har-schema/lossless-json", - name: "REA HAR capture adapter", - version: - "har-schema@2.0.0;har-schema-corrections@1;jsonc-parser@3.3.1;lossless-json@4.3.1", -} as const; - /** * Default V8 heap settings for the owned Node capture decoder process. * The old generation is `REA_CAPTURE_DECODER_OLD_GENERATION_MIB`; the @@ -38,24 +30,4 @@ export const NODE_CAPTURE_PROCESS_MEMORY_LIMIT_BYTES = NODE_CAPTURE_HEAP_LIMITS.oldGenerationMiB, ); -/** Native executable profile verified with unchanged mitmproxy raw-state decoding. */ -export const MITMPROXY_CAPTURE_PROVIDER_IDENTITY = { - id: "mitmproxy-native-tnetstring", - name: "REA offline native mitmproxy adapter", - version: "12.2.3", -} as const; - -/** Native SAZ member decoding with the existing ZIP and XML package profiles. */ -export const SAZ_CAPTURE_PROVIDER_IDENTITY = { - id: "fiddler-saz", - name: "REA Fiddler SAZ capture adapter", - version: "1;zip.js@2.15.0;xmldom@0.9.12", -} as const; - /** Fixed offline interpretation profile, not an authenticated producer claim. */ -export const QLOG_CAPTURE_PROVIDER_IDENTITY = { - id: "aioquic-qlog-json", - name: "REA aioquic qlog capture adapter", - version: - "aioquic-profile@1.5.0;qlog@0.3;jsonc-parser@3.3.1;lossless-json@4.3.1", -} as const; diff --git a/src/browser/history/HarCapture.ts b/src/browser/history/HarCapture.ts index ddc2459118..91c67f9efe 100644 --- a/src/browser/history/HarCapture.ts +++ b/src/browser/history/HarCapture.ts @@ -15,7 +15,7 @@ import { capturePointer, } from "./CaptureRedaction.js"; import { createHarValidator } from "./HarSchema.js"; -import { HAR_CAPTURE_PROVIDER_IDENTITY } from "./CaptureRelease.js"; +import { HAR_CAPTURE_PROVIDER_IDENTITY } from "./CaptureProviderIdentities.js"; type DecodedCapture = Omit< WebNetworkCapture, diff --git a/src/browser/history/MitmproxyCaptureAdapter.ts b/src/browser/history/MitmproxyCaptureAdapter.ts index 51597d7c33..cd2bab3023 100644 --- a/src/browser/history/MitmproxyCaptureAdapter.ts +++ b/src/browser/history/MitmproxyCaptureAdapter.ts @@ -4,7 +4,7 @@ import { isAbsolute } from "node:path"; import { fileURLToPath } from "node:url"; import { AnalysisCapabilityUnavailableError } from "../../domain/analysisErrorCore.js"; import type { HistoricalCaptureFormatAdapter } from "./HistoricalCaptureFormatAdapter.js"; -import { MITMPROXY_CAPTURE_PROVIDER_IDENTITY } from "./CaptureRelease.js"; +import { MITMPROXY_CAPTURE_PROVIDER_IDENTITY } from "./CaptureProviderIdentities.js"; import { snapshotEnvironment } from "../../process/snapshotEnvironment.js"; const OPERATION = "inspect_web_network_capture"; const MITMDUMP_REQUIREMENT = diff --git a/src/browser/history/NodeCaptureAdapter.ts b/src/browser/history/NodeCaptureAdapter.ts index 3e51bd0089..785b157220 100644 --- a/src/browser/history/NodeCaptureAdapter.ts +++ b/src/browser/history/NodeCaptureAdapter.ts @@ -8,11 +8,13 @@ import { import { CAPTURE_DECODER_SEMI_SPACE_MIB } from "../../domain/analysisResourceLimits.js"; import { NODE_CAPTURE_HEAP_ENFORCED, - HAR_CAPTURE_PROVIDER_IDENTITY, - SAZ_CAPTURE_PROVIDER_IDENTITY, - QLOG_CAPTURE_PROVIDER_IDENTITY, captureDecoderProcessMemoryLimitBytes, } from "./CaptureRelease.js"; +import { QLOG_CAPTURE_PROVIDER_IDENTITY } from "./CaptureProviderIdentities.js"; +import { + HAR_CAPTURE_PROVIDER_IDENTITY, + SAZ_CAPTURE_PROVIDER_IDENTITY, +} from "./CaptureProviderIdentities.js"; import { AnalysisCapabilityUnavailableError } from "../../domain/analysisErrorCore.js"; /** Package-backed capture decoders share one bounded offline Node process profile. */ diff --git a/src/browser/history/QlogCapture.ts b/src/browser/history/QlogCapture.ts index 61a22d9ee7..61f09804b0 100644 --- a/src/browser/history/QlogCapture.ts +++ b/src/browser/history/QlogCapture.ts @@ -14,7 +14,7 @@ import { capturePointer, isCaptureCredentialHeader, } from "./CaptureRedaction.js"; -import { QLOG_CAPTURE_PROVIDER_IDENTITY } from "./CaptureRelease.js"; +import { QLOG_CAPTURE_PROVIDER_IDENTITY } from "./CaptureProviderIdentities.js"; const MAX_VARINT = (1n << 62n) - 1n; type Projection = Pick< diff --git a/src/browser/history/SazCapture.ts b/src/browser/history/SazCapture.ts index abb7f7caa2..a64ad3e2dc 100644 --- a/src/browser/history/SazCapture.ts +++ b/src/browser/history/SazCapture.ts @@ -7,7 +7,7 @@ import { WEB_NETWORK_CAPTURE_LIMITS, type WebNetworkCaptureRecord, } from "../../domain/webNetworkCapture.js"; -import { SAZ_CAPTURE_PROVIDER_IDENTITY } from "./CaptureRelease.js"; +import { SAZ_CAPTURE_PROVIDER_IDENTITY } from "./CaptureProviderIdentities.js"; import { CaptureFormatError } from "./CaptureFormatError.js"; import { decodeSazHttp } from "./SazHttp.js"; import { decodeSazMetadata } from "./SazMetadata.js"; diff --git a/src/captures/TsharkCaptureProvider.ts b/src/captures/TsharkCaptureProvider.ts index 55dadaa8d4..a66b2571c0 100644 --- a/src/captures/TsharkCaptureProvider.ts +++ b/src/captures/TsharkCaptureProvider.ts @@ -1,3 +1,4 @@ +import { TSHARK_PROVIDER_IDENTITY } from "./TsharkProviderIdentity.js"; import { artifactFileCancellationCause } from "../artifacts/ArtifactFileOperation.js"; import { type InspectPacketFieldsInput, @@ -71,11 +72,7 @@ import { import { privateWorkspaceFailure } from "../process/privateWorkspaceFailure.js"; const OPERATION = "inspect_packet_capture"; -export const TSHARK_PROVIDER_IDENTITY = { - id: "tshark-capture", - name: "REA TShark capture adapter", - version: "tshark@4", -} as const; + type Launcher = NonNullable[2]>["launcher"]; /** Packet inspection runs only on Linux; discovery and execution share this check. */ diff --git a/src/captures/TsharkProviderIdentity.ts b/src/captures/TsharkProviderIdentity.ts new file mode 100644 index 0000000000..66e61b4748 --- /dev/null +++ b/src/captures/TsharkProviderIdentity.ts @@ -0,0 +1,6 @@ +/** Source-owned static provider identity and public operation names. */ +export const TSHARK_PROVIDER_IDENTITY = { + id: "tshark-capture", + name: "REA TShark capture adapter", + version: "tshark@4", +} as const; diff --git a/src/cli/cutterCommands.ts b/src/cli/cutterCommands.ts index 40457eaa05..258175c4dd 100644 --- a/src/cli/cutterCommands.ts +++ b/src/cli/cutterCommands.ts @@ -2,8 +2,7 @@ import { projectCliAnalysisResult } from "./projectCliAnalysisResult.js"; import { z } from "incur"; import type { Logger } from "pino"; -import { CutterBridgeClient } from "../cutter/CutterBridgeClient.js"; -import { CutterBridgeService } from "../cutter/CutterBridgeService.js"; +import { createCutterBridgeService } from "../composition/cutter.js"; import { CLI_COMMANDS } from "../cliCommandNames.js"; import { logCliCommand } from "../cliLogging.js"; import { finishAnalysisCommand } from "./finishAnalysisCommand.js"; @@ -17,8 +16,7 @@ export const registerCutterCommands = ( logger: Logger, environment: Readonly, ): void => { - const createCutter = () => - new CutterBridgeService(new CutterBridgeClient(environment)); + const createCutter = () => createCutterBridgeService(environment); cli.command(CLI_COMMANDS.listCutterSessions, { description: diff --git a/src/cli/fridaCommands.ts b/src/cli/fridaCommands.ts index 2475e5b85a..485f28c9e5 100644 --- a/src/cli/fridaCommands.ts +++ b/src/cli/fridaCommands.ts @@ -6,7 +6,7 @@ import type { FridaInstrumentationPort, FridaRemoteConnection, } from "../application/frida/FridaInstrumentationPort.js"; -import { FridaInstrumentationManager } from "../frida/FridaInstrumentationManager.js"; +import { createFridaInstrumentationProvider } from "../composition/frida.js"; import { CLI_COMMANDS } from "../cliCommandNames.js"; import { logCliCommand } from "../cliLogging.js"; import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; @@ -27,7 +27,7 @@ const exactlyOneScriptSource = export const registerFridaCommands = ( cli: CliInstance, logger: Logger, - provider: FridaInstrumentationPort = new FridaInstrumentationManager(), + provider: FridaInstrumentationPort = createFridaInstrumentationProvider(), ): void => { const service = new FridaInstrumentationService(provider); // CLI calls own a disposable provider; MCP retains its shared provider until diff --git a/src/cli/reverseEngineeringCommands.ts b/src/cli/reverseEngineeringCommands.ts index 87d9ff95c1..b07da0176b 100644 --- a/src/cli/reverseEngineeringCommands.ts +++ b/src/cli/reverseEngineeringCommands.ts @@ -22,11 +22,11 @@ import type { CliInstance } from "./types.js"; import { withCommandCancellation } from "./commandCancellation.js"; import { CLI_COMMANDS } from "../cliCommandNames.js"; import type { ReverseEngineeringService } from "../application/reverse/ReverseEngineeringService.js"; +import { gdbOneShotCommandMayInvalidateDetachProof } from "../gdb/GdbSessionManager.js"; import { - GdbSessionManager, - gdbOneShotCommandMayInvalidateDetachProof, -} from "../gdb/GdbSessionManager.js"; -import { RizinDebugSessionManager } from "../rizin/RizinDebugSessionManager.js"; + createGdbSessionManager, + createRizinDebugSessionManager, +} from "../composition/debuggerSessions.js"; import { ProviderCleanupError } from "../domain/providerCleanupError.js"; import { analysisEvidenceDeliveryOptions, @@ -171,8 +171,7 @@ export const registerReverseEngineeringCommands = ( environment: Readonly, createGdbManager: ( environment: Readonly, - ) => GdbSessionManager = (providerEnvironment) => - new GdbSessionManager({ environment: providerEnvironment }), + ) => ReturnType = createGdbSessionManager, createService: typeof createReverseEngineeringService = createReverseEngineeringService, ): void => { const service = createService(environment); @@ -417,7 +416,7 @@ export const registerReverseEngineeringCommands = ( run: ({ args, options }) => withCommandCancellation((signal) => logCliCommand(logger, CLI_COMMANDS.debugWithRizin, async () => { - const manager = new RizinDebugSessionManager(environment); + const manager = createRizinDebugSessionManager(environment); let sessionId: string | undefined; return finishAnalysisCommand( async () => { diff --git a/src/composition/auxiliaryAnalysisProviders.ts b/src/composition/auxiliaryAnalysisProviders.ts index 2be36f8f49..ed58abcac2 100644 --- a/src/composition/auxiliaryAnalysisProviders.ts +++ b/src/composition/auxiliaryAnalysisProviders.ts @@ -7,10 +7,8 @@ import { MANAGED_STATIC_PROVIDER, } from "../domain/investigationProviders.js"; import { managedStaticCapabilities } from "../dotnet/ManagedStaticProviderMetadata.js"; -import { - NATIVE_MACOS_PROVIDER_IDENTITY, - nativeMacOSCapabilities, -} from "../native/NativeMacOSProviderMetadata.js"; +import { nativeMacOSCapabilities } from "../native/NativeMacOSProviderMetadata.js"; +import { NATIVE_MACOS_PROVIDER_IDENTITY } from "../native/NativeMacosProviderIdentity.js"; type AuxiliaryProviderDeclaration = ConstructorParameters< typeof LazyAnalysisProvider diff --git a/src/composition/capabilityDeclarations.ts b/src/composition/capabilityDeclarations.ts new file mode 100644 index 0000000000..cfbfd494d7 --- /dev/null +++ b/src/composition/capabilityDeclarations.ts @@ -0,0 +1,530 @@ +// oxlint-disable max-lines -- Keep the exhaustive static provenance bindings together; this table has no execution or host-availability logic. +import type { ProviderIdentity } from "../application/AnalysisProvider.js"; +import type { TOOL_CONTRACTS } from "../contracts/toolContracts.js"; +import type { ToolContractGroupId } from "../contracts/toolFamilies.js"; +import { ADB_PROVIDER_IDENTITY } from "../adb/AdbProviderIdentity.js"; +import { JADX_PROVIDER_IDENTITY } from "../android/JadxRelease.js"; +import { APK_SIGNATURE_PROVIDER_IDENTITY } from "../apkSignature/ApkSignatureRelease.js"; +import { APKTOOL_PROVIDER_IDENTITY } from "../apktool/ApktoolProviderIdentity.js"; +import { + HAR_CAPTURE_PROVIDER_IDENTITY, + MITMPROXY_CAPTURE_PROVIDER_IDENTITY, + QLOG_CAPTURE_PROVIDER_IDENTITY, + SAZ_CAPTURE_PROVIDER_IDENTITY, +} from "../browser/history/CaptureProviderIdentities.js"; +import { + CDP_BROWSER_PROVIDER_IDENTITY, + CDP_ELECTRON_PROVIDER_IDENTITY, + PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY, + PLAYWRIGHT_ELECTRON_ACTIVE_PROVIDER_IDENTITY, +} from "../browser/providerIdentities.js"; +import { TSHARK_PROVIDER_IDENTITY } from "../captures/TsharkProviderIdentity.js"; +import { CUTTER_PROVIDER_IDENTITY } from "../cutter/CutterProviderIdentity.js"; +import { + ANALYSIS_VIEW_PROVIDER, + ANDROID_APPLICATION_PROVIDER, + APPLE_APPLICATION_PROVIDER, + APPLE_IPS_PROVIDER, + ARTIFACT_GRAPH_PROVIDER, + BYTE_ENTROPY_PROVIDER, + DEVICETREE_PROVIDER, + ELF_NOTES_PROVIDER, + HARMONY_APPLICATION_PROVIDER, + JAVASCRIPT_APPLICATION_PROVIDER, + JAVASCRIPT_APPLICATION_WORKFLOW_PROVIDER, + JAVASCRIPT_RUNTIME_RECONCILIATION_PROVIDER, + KERNEL_MODULE_PROVIDER, + MANAGED_STATIC_PROVIDER, + MANAGED_WORKFLOW_PROVIDER, + NATIVE_EXPORTS_PROVIDER, + PE_CERTIFICATES_PROVIDER, + PE_IMPORTS_PROVIDER, + PE_LOAD_CONFIG_PROVIDER, + PE_RESOURCES_PROVIDER, + WEB_SCRIPT_EXPORT_PROVIDER, +} from "../domain/investigationProviders.js"; +import { SQLITE_PROVIDER_IDENTITY } from "../domain/sqlite/SqliteProviderIdentity.js"; +import { + WEB_MODULE_TRACE_PROVIDER, + WEB_SOURCE_LOCATION_PROVIDER, +} from "../domain/webTraceProviders.js"; +import { ELECTRON_FUSE_PROVIDER_IDENTITY } from "../electron/ElectronFuseProviderIdentity.js"; +import { ERLANG_BEAM_PROVIDER_IDENTITY } from "../erlang/ErlangBeamProviderIdentity.js"; +import { EVMOLE_PROVIDER_IDENTITY } from "../evm/EvmoleProviderIdentity.js"; +import { + BINWALK_PROVIDER_IDENTITY, + UNBLOB_PROVIDER_IDENTITY, +} from "../firmware/FirmwareProviderIdentities.js"; +import { FLUTTER_PROVIDER_IDENTITY } from "../flutter/FlutterProviderIdentity.js"; +import { FRIDA_PROVIDER_IDENTITY } from "../frida/FridaProviderIdentity.js"; +import { GDB_PROVIDER_IDENTITY } from "../gdb/GdbProviderIdentity.js"; +import { + GHIDRA_OPERATIONS, + GHIDRA_PROVIDER_IDENTITY, +} from "../ghidra/GhidraProviderMetadata.js"; +import { GO_BINARY_PROVIDER_IDENTITY } from "../go/GoBinaryProviderIdentity.js"; +import { GPU_CODE_OBJECTS_PROVIDER_IDENTITY } from "../gpu/GpuCodeObjectsProviderIdentity.js"; +import { + HOPPER_OPERATIONS, + HOPPER_PROVIDER_IDENTITY, +} from "../hopper/HopperProviderMetadata.js"; +import { + IDA_OPERATIONS, + IDA_PROVIDER_IDENTITY, +} from "../ida/IdaProviderMetadata.js"; +import { IL2CPP_METADATA_PROVIDER_IDENTITY } from "../il2cpp/Il2CppMetadataProviderIdentity.js"; +import { V8_INSPECTOR_PROVIDER_IDENTITY } from "../inspector/providerIdentity.js"; +import { WAKARU_PROVIDER_IDENTITY } from "../javascript/recovery/WakaruProviderIdentity.js"; +import { JEB_PROVIDER_IDENTITY } from "../jeb/JebProviderIdentity.js"; +import { + KUNA_OPERATIONS, + KUNA_PROVIDER_IDENTITY, +} from "../kuna/KunaProviderMetadata.js"; +import { MODEL_ARTIFACT_PROVIDER_IDENTITY } from "../model/ModelArtifactProviderIdentity.js"; +import { NATIVE_MACOS_PROVIDER_IDENTITY } from "../native/NativeMacosProviderIdentity.js"; +import { PWNTOOLS_PROVIDER_IDENTITY } from "../native/pwntools/PwntoolsProviderIdentity.js"; +import { OBJDUMP_PROVIDER_IDENTITY } from "../objdump/ObjdumpProviderIdentity.js"; +import { PCAP_PROVIDER_IDENTITY } from "../pcap/PcapProviderIdentity.js"; +import { PYTHON_PYC_PROVIDER_IDENTITY } from "../python/PythonPycProviderIdentity.js"; +import { RIZIN_DEBUGGER_PROVIDER_IDENTITY } from "../rizin/RizinDebuggerProviderIdentity.js"; +import { RIZIN_PROVIDER_IDENTITY } from "../rizin/RizinProviderIdentity.js"; +import { RUST_BINARY_PROVIDER_IDENTITY } from "../rust/RustBinaryProviderIdentity.js"; +import { SIGROK_PROVIDER_IDENTITY } from "../signals/SigrokProviderIdentity.js"; +import { WABT_PROVIDER_IDENTITY } from "../wasm/WabtRelease.js"; + +type CanonicalOperationName = (typeof TOOL_CONTRACTS)[number]["name"]; + +/** Whole source groups grow with their canonical contracts; provider subsets stay explicit. */ +export type CapabilityOperationSelection = + | readonly CanonicalOperationName[] + | { readonly group: ToolContractGroupId }; + +/** Static provenance bindings; host availability belongs to runtime inspection. */ +export interface CapabilityDeclaration { + readonly providers: readonly { + readonly identity: ProviderIdentity; + readonly operations: CapabilityOperationSelection; + }[]; +} + +const providerBinding = ( + identity: ProviderIdentity, + operations: Operations, +) => ({ identity, operations }); + +/** Shared source facts, with no factory, schema or generated-catalog imports. */ +export const CAPABILITY_DECLARATIONS = { + adbDevice: { + providers: [ + providerBinding(ADB_PROVIDER_IDENTITY, { group: "ADB_TOOL_CONTRACTS" }), + ], + }, + analysisView: { + providers: [ + providerBinding(ANALYSIS_VIEW_PROVIDER, { + group: "ANALYSIS_VIEW_TOOL_CONTRACTS", + }), + ], + }, + android: { + providers: [ + providerBinding(JADX_PROVIDER_IDENTITY, { + group: "ANDROID_TOOL_CONTRACTS", + }), + ], + }, + androidApplicationGraph: { + providers: [ + providerBinding(ANDROID_APPLICATION_PROVIDER, [ + "project_android_application_graph", + ]), + ], + }, + apkSignature: { + providers: [ + providerBinding(APK_SIGNATURE_PROVIDER_IDENTITY, { + group: "APK_SIGNATURE_TOOL_CONTRACTS", + }), + ], + }, + apktool: { + providers: [ + providerBinding(APKTOOL_PROVIDER_IDENTITY, { + group: "APKTOOL_TOOL_CONTRACTS", + }), + ], + }, + appleApplicationGraph: { + providers: [ + providerBinding(APPLE_APPLICATION_PROVIDER, [ + "project_apple_application_graph", + ]), + ], + }, + appleIps: { + providers: [providerBinding(APPLE_IPS_PROVIDER, ["inspect_apple_ips"])], + }, + applicationWorkflows: { + providers: [ + providerBinding(JAVASCRIPT_APPLICATION_WORKFLOW_PROVIDER, [ + "build_reconstruction_obligation_ledger", + "compare_application_versions", + "compare_javascript_export_shapes", + "compare_source_to_bundle", + "evaluate_reconstruction_coverage", + "guided_workflows", + "read_guidance_section", + "search_guidance", + "trace_application_feature", + "trace_javascript_semantics", + ]), + ], + }, + artifactGraph: { + providers: [ + providerBinding(ARTIFACT_GRAPH_PROVIDER, { + group: "ARTIFACT_TOOL_CONTRACTS", + }), + ], + }, + artifactResources: { providers: [] }, + binaryLayout: { + providers: [ + providerBinding(PWNTOOLS_PROVIDER_IDENTITY, ["inspect_binary_layout"]), + ], + }, + browserObservation: { + providers: [ + providerBinding(CDP_BROWSER_PROVIDER_IDENTITY, { + group: "BROWSER_TOOL_CONTRACTS", + }), + ], + }, + browserScenarioCapture: { + providers: [ + providerBinding(PLAYWRIGHT_BROWSER_SCENARIO_PROVIDER_IDENTITY, { + group: "BROWSER_SCENARIO_TOOL_CONTRACTS", + }), + ], + }, + byteEntropy: { + providers: [ + providerBinding(BYTE_ENTROPY_PROVIDER, ["inspect_byte_entropy"]), + ], + }, + cutter: { + providers: [ + providerBinding(CUTTER_PROVIDER_IDENTITY, { + group: "CUTTER_TOOL_CONTRACTS", + }), + ], + }, + desktopApplication: { + providers: [ + providerBinding(ARTIFACT_GRAPH_PROVIDER, { + group: "DESKTOP_APPLICATION_TOOL_CONTRACTS", + }), + ], + }, + devicetree: { + providers: [providerBinding(DEVICETREE_PROVIDER, ["inspect_devicetree"])], + }, + electronActiveObservation: { + providers: [ + providerBinding(PLAYWRIGHT_ELECTRON_ACTIVE_PROVIDER_IDENTITY, [ + "capture_electron_scenario", + ]), + ], + }, + electronFuses: { + providers: [ + providerBinding(ELECTRON_FUSE_PROVIDER_IDENTITY, [ + "inspect_electron_fuses", + ]), + ], + }, + electronObservation: { + providers: [ + providerBinding(CDP_ELECTRON_PROVIDER_IDENTITY, [ + "inspect_electron_page", + "list_electron_targets", + ]), + ], + }, + elfNotes: { + providers: [providerBinding(ELF_NOTES_PROVIDER, ["inspect_elf_notes"])], + }, + erlangBeam: { + providers: [ + providerBinding(ERLANG_BEAM_PROVIDER_IDENTITY, { + group: "ERLANG_BEAM_TOOL_CONTRACTS", + }), + ], + }, + evmInterface: { + providers: [ + providerBinding(EVMOLE_PROVIDER_IDENTITY, { + group: "EVM_TOOL_CONTRACTS", + }), + ], + }, + firmwareAnalysis: { + providers: [ + providerBinding(BINWALK_PROVIDER_IDENTITY, ["inspect_firmware_regions"]), + providerBinding(UNBLOB_PROVIDER_IDENTITY, ["extract_firmware"]), + ], + }, + flutter: { + providers: [ + providerBinding(FLUTTER_PROVIDER_IDENTITY, { + group: "FLUTTER_TOOL_CONTRACTS", + }), + ], + }, + fridaInstrumentation: { + providers: [ + providerBinding(FRIDA_PROVIDER_IDENTITY, { + group: "FRIDA_TOOL_CONTRACTS", + }), + ], + }, + gdbSessions: { + providers: [ + providerBinding(GDB_PROVIDER_IDENTITY, { group: "GDB_TOOL_CONTRACTS" }), + ], + }, + ghidra: { + providers: [providerBinding(GHIDRA_PROVIDER_IDENTITY, GHIDRA_OPERATIONS)], + }, + goBinary: { + providers: [ + providerBinding(GO_BINARY_PROVIDER_IDENTITY, { + group: "GO_TOOL_CONTRACTS", + }), + ], + }, + gpuCodeObjects: { + providers: [ + providerBinding(GPU_CODE_OBJECTS_PROVIDER_IDENTITY, { + group: "GPU_TOOL_CONTRACTS", + }), + ], + }, + harmonyApplicationGraph: { + providers: [ + providerBinding(HARMONY_APPLICATION_PROVIDER, [ + "project_harmony_application_graph", + ]), + ], + }, + hopper: { + providers: [providerBinding(HOPPER_PROVIDER_IDENTITY, HOPPER_OPERATIONS)], + }, + ida: { + providers: [providerBinding(IDA_PROVIDER_IDENTITY, IDA_OPERATIONS)], + }, + il2cppMetadata: { + providers: [ + providerBinding(IL2CPP_METADATA_PROVIDER_IDENTITY, { + group: "IL2CPP_TOOL_CONTRACTS", + }), + ], + }, + javascriptApplication: { + providers: [ + providerBinding(JAVASCRIPT_APPLICATION_PROVIDER, [ + "analyze_javascript_application", + ]), + ], + }, + javascriptRecovery: { + providers: [ + providerBinding(WAKARU_PROVIDER_IDENTITY, { + group: "JAVASCRIPT_RECOVERY_TOOL_CONTRACTS", + }), + ], + }, + javascriptRuntimeObservation: { + providers: [ + providerBinding(V8_INSPECTOR_PROVIDER_IDENTITY, { + group: "JAVASCRIPT_RUNTIME_OBSERVATION_TOOL_CONTRACTS", + }), + ], + }, + javascriptRuntimeReconciliation: { + providers: [ + providerBinding(JAVASCRIPT_RUNTIME_RECONCILIATION_PROVIDER, [ + "reconcile_javascript_runtime", + ]), + ], + }, + jeb: { + providers: [ + providerBinding(JEB_PROVIDER_IDENTITY, { group: "JEB_TOOL_CONTRACTS" }), + ], + }, + kernelModule: { + providers: [ + providerBinding(KERNEL_MODULE_PROVIDER, ["inspect_kernel_module"]), + ], + }, + kuna: { + providers: [providerBinding(KUNA_PROVIDER_IDENTITY, KUNA_OPERATIONS)], + }, + managedStatic: { + providers: [ + providerBinding(MANAGED_STATIC_PROVIDER, { + group: "MANAGED_TOOL_CONTRACTS", + }), + ], + }, + managedWorkflow: { + providers: [ + providerBinding(MANAGED_WORKFLOW_PROVIDER, { + group: "MANAGED_WORKFLOW_TOOL_CONTRACTS", + }), + ], + }, + modelArtifact: { + providers: [ + providerBinding(MODEL_ARTIFACT_PROVIDER_IDENTITY, { + group: "MODEL_TOOL_CONTRACTS", + }), + ], + }, + nativeExports: { + providers: [ + providerBinding(NATIVE_EXPORTS_PROVIDER, ["inspect_native_exports"]), + ], + }, + nativeMacOS: { + providers: [ + providerBinding(NATIVE_MACOS_PROVIDER_IDENTITY, { + group: "NATIVE_TOOL_CONTRACTS", + }), + ], + }, + packetCapture: { + providers: [ + providerBinding(TSHARK_PROVIDER_IDENTITY, { + group: "CAPTURE_TOOL_CONTRACTS", + }), + ], + }, + pcap: { + providers: [ + providerBinding(PCAP_PROVIDER_IDENTITY, { group: "PCAP_TOOL_CONTRACTS" }), + ], + }, + peCertificates: { + providers: [ + providerBinding(PE_CERTIFICATES_PROVIDER, ["inspect_pe_certificates"]), + ], + }, + peImports: { + providers: [providerBinding(PE_IMPORTS_PROVIDER, ["inspect_pe_imports"])], + }, + peLoadConfig: { + providers: [ + providerBinding(PE_LOAD_CONFIG_PROVIDER, ["inspect_pe_load_config"]), + ], + }, + peResources: { + providers: [ + providerBinding(PE_RESOURCES_PROVIDER, ["inspect_pe_resources"]), + ], + }, + pythonPyc: { + providers: [ + providerBinding(PYTHON_PYC_PROVIDER_IDENTITY, { + group: "PYTHON_PYC_TOOL_CONTRACTS", + }), + ], + }, + recordedCrash: { + providers: [ + providerBinding(PWNTOOLS_PROVIDER_IDENTITY, ["inspect_recorded_crash"]), + ], + }, + reverseEngineering: { + providers: [ + providerBinding(OBJDUMP_PROVIDER_IDENTITY, ["inspect_with_objdump"]), + providerBinding(RIZIN_PROVIDER_IDENTITY, ["execute_rizin_command"]), + ], + }, + rizinDebugSessions: { + providers: [ + providerBinding(RIZIN_DEBUGGER_PROVIDER_IDENTITY, { + group: "RIZIN_DEBUG_TOOL_CONTRACTS", + }), + ], + }, + rustBinary: { + providers: [ + providerBinding(RUST_BINARY_PROVIDER_IDENTITY, { + group: "RUST_TOOL_CONTRACTS", + }), + ], + }, + signalRecording: { + providers: [ + providerBinding(SIGROK_PROVIDER_IDENTITY, { + group: "SIGNAL_TOOL_CONTRACTS", + }), + ], + }, + sqliteDatabase: { + providers: [ + providerBinding(SQLITE_PROVIDER_IDENTITY, { + group: "SQLITE_TOOL_CONTRACTS", + }), + ], + }, + wasmArtifact: { + providers: [ + providerBinding(WABT_PROVIDER_IDENTITY, ["inspect_wasm_artifact"]), + ], + }, + webModuleTrace: { + providers: [ + providerBinding(WEB_MODULE_TRACE_PROVIDER, ["trace_web_module_imports"]), + ], + }, + webNetworkCapture: { + providers: [ + providerBinding(SAZ_CAPTURE_PROVIDER_IDENTITY, { + group: "WEB_NETWORK_CAPTURE_TOOL_CONTRACTS", + }), + providerBinding(HAR_CAPTURE_PROVIDER_IDENTITY, { + group: "WEB_NETWORK_CAPTURE_TOOL_CONTRACTS", + }), + providerBinding(MITMPROXY_CAPTURE_PROVIDER_IDENTITY, { + group: "WEB_NETWORK_CAPTURE_TOOL_CONTRACTS", + }), + providerBinding(QLOG_CAPTURE_PROVIDER_IDENTITY, { + group: "WEB_NETWORK_CAPTURE_TOOL_CONTRACTS", + }), + ], + }, + webRuntime: { + providers: [ + providerBinding(CDP_BROWSER_PROVIDER_IDENTITY, { + group: "WEB_RUNTIME_TOOL_CONTRACTS", + }), + ], + }, + webScriptExport: { + providers: [ + providerBinding(WEB_SCRIPT_EXPORT_PROVIDER, ["export_web_scripts"]), + ], + }, + webSourceLocation: { + providers: [ + providerBinding(WEB_SOURCE_LOCATION_PROVIDER, [ + "trace_web_source_location", + ]), + ], + }, +} as const satisfies Readonly>; + +export type CapabilityDeclarationId = keyof typeof CAPABILITY_DECLARATIONS; diff --git a/src/composition/capabilityServices.ts b/src/composition/capabilityServices.ts new file mode 100644 index 0000000000..67d28f2ace --- /dev/null +++ b/src/composition/capabilityServices.ts @@ -0,0 +1,645 @@ +import type { OptionalProviderLoadFailures } from "../application/OptionalObservationProviders.js"; +import { + locateWiresharkTool, + selectWiresharkTool, +} from "../pcap/PcapConfiguration.js"; +import type { AnalysisSettingsReader } from "../domain/analysisSettings.js"; +import { CapabilityScope } from "../application/CapabilityScope.js"; +import type { CapabilityAvailabilityFacts } from "../application/CapabilityAvailability.js"; +import { + CAPABILITY_DECLARATIONS, + type CapabilityDeclarationId, +} from "./capabilityDeclarations.js"; +import { isAbsolute } from "node:path"; +import { createGpuCodeObjectsService } from "./gpu.js"; +import { createPacketCaptureService, tsharkHostSupported } from "./captures.js"; +import { + createSignalRecordingService, + sigrokHostSupported, +} from "./signals.js"; +import { wasmArtifactModule } from "./wasm.js"; +import { apkSignatureModule } from "./apkSignature.js"; +import { + evmInterfaceModule, + evmHostSupported, + evmLimiterAvailability, +} from "./evm.js"; +import { createSqliteDatabaseService } from "./sqlite.js"; +import { createDesktopApplicationService } from "./desktop.js"; +import { createGoBinaryService } from "./go.js"; +import { createRustBinaryService } from "./rust.js"; +import { createBeamService } from "./erlang.js"; +import { createModelArtifactService } from "./model.js"; +import { createIl2cppMetadataService } from "./il2cpp.js"; +import { createElectronFuseService } from "./electronFuses.js"; +import { createPythonPycService } from "./pythonPyc.js"; +import { createWebNetworkCaptureService } from "./webNetworkCaptures.js"; +import { createWebSourceLocationService } from "./webSourceLocations.js"; +import { createWebModuleTraceService } from "./webModules.js"; +import { createWebRuntimeService } from "./webRuntime.js"; +import { + createJavaScriptRecoveryProvider, + wakaruHostSupported, +} from "./javascriptRecovery.js"; +import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; +import { createReverseEngineeringService } from "./reverseEngineering.js"; +import { FirmwareAnalysisService } from "../application/firmware/FirmwareAnalysisService.js"; +import { + createFirmwareAnalysisProvider, + firmwareHostSupported, +} from "./firmware.js"; +import { createAndroidAnalysisProvider } from "./android.js"; +import { createPcapAnalysisProvider } from "./pcap.js"; +import { createFlutterBuildAnalysisProvider } from "./flutter.js"; +import { createApktoolResourceAnalysisProvider } from "./apktool.js"; +import { createJebAnalysisProvider } from "./jeb.js"; +import { createAdbDeviceAnalysisProvider } from "./adb.js"; +import { createFridaInstrumentationProvider } from "./frida.js"; +import { createCutterBridgeService } from "./cutter.js"; +import { + createGdbSessionManager, + createRizinDebugSessionManager, +} from "./debuggerSessions.js"; +import { persistentDebuggerHostSupported } from "../process/PersistentDebuggerHost.js"; +import { pcapCaptureHostSupported } from "../domain/pcap/pcapAnalysis.js"; +import { + createPeLoadConfigService, + createPeCertificatesService, + createPeImportsService, + createNativeExportsService, + createPeResourcesService, + createRecordedCrashService, + createBinaryLayoutService, + pwntoolsHostSupported, + createDevicetreeService, + createElfNotesService, + createAppleIpsService, + createKernelModuleService, +} from "./binaryDiagnostics.js"; + +import type { PacketCaptureService } from "../application/captures/PacketCaptureService.js"; +import type { SignalRecordingService } from "../application/signals/SignalRecordingService.js"; +import type { WasmArtifactService } from "../application/wasm/WasmArtifactService.js"; +import type { ApkSignatureService } from "../application/apkSignature/ApkSignatureService.js"; +import type { EvmInterfaceService } from "../application/evm/EvmInterfaceService.js"; +import type { SqliteDatabaseService } from "../application/sqlite/SqliteDatabaseService.js"; +import type { Il2cppMetadataService } from "../application/il2cpp/Il2cppMetadataService.js"; +import type { GoBinaryService } from "../application/go/GoBinaryService.js"; +import type { ElectronFuseService } from "../application/electron/ElectronFuseService.js"; +import type { RustBinaryService } from "../application/rust/RustBinaryService.js"; +import type { PythonPycService } from "../application/python/PythonPycService.js"; +import type { BeamService } from "../application/erlang/BeamService.js"; +import type { RecordedCrashService } from "../application/binaryDiagnostics/RecordedCrashService.js"; +import type { BinaryLayoutService } from "../application/binaryDiagnostics/BinaryLayoutService.js"; +import type { BrowserObservationPort } from "../application/BrowserObservationPort.js"; +import type { BrowserScenarioCapturePort } from "../application/BrowserScenarioCapturePort.js"; +import type { ElectronActiveObservationPort } from "../application/javascript/ElectronActiveObservationPort.js"; +import type { ElectronObservationPort } from "../application/javascript/ElectronObservationPort.js"; +import type { JavaScriptRuntimeObservationPort } from "../application/javascript/JavaScriptRuntimeObservationPort.js"; +import type { FridaInstrumentationPort } from "../application/frida/FridaInstrumentationPort.js"; +import type { WebModuleTraceService } from "../application/WebModuleTraceService.js"; +import type { WebSourceLocationService } from "../application/WebSourceLocationService.js"; +import type { WebRuntimeService } from "../application/WebRuntimeService.js"; +import type { WebNetworkCaptureService } from "../application/WebNetworkCaptureService.js"; +import type { JavaScriptRecoveryPort } from "../application/javascript/JavaScriptRecoveryPort.js"; +import type { FirmwareAnalysisPort } from "../application/firmware/FirmwareAnalysisPort.js"; +import type { AndroidAnalysisPort } from "../application/android/AndroidAnalysisPort.js"; +import type { ApktoolResourceAnalysisPort } from "../application/apktool/ApktoolResourceAnalysisPort.js"; +import type { PcapAnalysisPort } from "../application/pcap/PcapAnalysisPort.js"; +import type { FlutterBuildAnalysisPort } from "../application/flutter/FlutterBuildAnalysisPort.js"; +import type { JebAnalysisPort } from "../application/jeb/JebAnalysisPort.js"; +import type { AdbDeviceAnalysisPort } from "../application/adb/AdbDeviceAnalysisPort.js"; + +/** Typed injection inputs shared by a capability host, independent of transport. */ +export interface CapabilityServiceOptions { + readonly packetCapture?: PacketCaptureService; + readonly signalRecording?: SignalRecordingService; + readonly wasmArtifact?: WasmArtifactService; + readonly apkSignature?: ApkSignatureService; + readonly evmInterface?: EvmInterfaceService; + readonly sqliteDatabase?: SqliteDatabaseService; + readonly binaryLayout?: BinaryLayoutService; + readonly goBinary?: GoBinaryService; + readonly il2cppMetadata?: Il2cppMetadataService; + readonly rustBinary?: RustBinaryService; + readonly pythonPyc?: PythonPycService; + readonly erlangBeam?: BeamService; + readonly electronFuses?: ElectronFuseService; + readonly recordedCrash?: RecordedCrashService; + readonly firmwareAnalysis?: FirmwareAnalysisPort; + readonly javascriptRecovery?: JavaScriptRecoveryPort; + readonly webModuleTrace?: WebModuleTraceService; + readonly webSourceLocation?: WebSourceLocationService; + readonly webRuntime?: WebRuntimeService; + readonly webNetworkCapture?: WebNetworkCaptureService; + readonly androidAnalysis?: AndroidAnalysisPort; + readonly apktoolAnalysis?: ApktoolResourceAnalysisPort; + readonly pcapAnalysis?: PcapAnalysisPort; + readonly flutterAnalysis?: FlutterBuildAnalysisPort; + readonly jebAnalysis?: JebAnalysisPort; + readonly adbDeviceAnalysis?: AdbDeviceAnalysisPort; + readonly browserObservation?: BrowserObservationPort; + readonly browserScenarioCapture?: BrowserScenarioCapturePort; + readonly electronObservation?: ElectronObservationPort; + readonly optionalObservationProviderOwner?: "server" | "caller"; + readonly optionalProviderLoadFailures?: OptionalProviderLoadFailures; + readonly electronActiveObservation?: ElectronActiveObservationPort; + readonly javascriptRuntimeObservation?: JavaScriptRuntimeObservationPort; + readonly fridaInstrumentation?: FridaInstrumentationPort; +} + +const tsharkLocatable = (environment: Readonly): boolean => { + try { + return ( + locateWiresharkTool( + selectWiresharkTool(environment, "tshark"), + environment, + ) !== undefined + ); + } catch { + // A relative REA_TSHARK_COMMAND is configuration the tool reports at execution. + return false; + } +}; + +export interface CapabilityHostContext { + readonly environment: Readonly; + readonly providerEnvironment: Readonly; + readonly settings: AnalysisSettingsReader; + readonly options: CapabilityServiceOptions; + readonly platform: NodeJS.Platform; + readonly arch: string; +} + +interface CapabilityInstance { + readonly value: Value; + readonly close: () => Promise; + readonly ownership: "host" | "caller"; + readonly availability?: CapabilityAvailabilityFacts; +} + +const capability = + ( + create: (context: CapabilityHostContext) => Value, + close: (value: Value) => Promise, + observe?: ( + context: CapabilityHostContext, + value: Value, + ) => CapabilityAvailabilityFacts, + ownership: (context: CapabilityHostContext) => "host" | "caller" = () => + "host", + ) => + (context: CapabilityHostContext): CapabilityInstance => { + const value = create(context); + return { + value, + close: () => close(value), + ownership: ownership(context), + ...(observe === undefined + ? {} + : { availability: observe(context, value) }), + }; + }; + +const service = }>( + create: (context: CapabilityHostContext) => Value, + observe?: ( + context: CapabilityHostContext, + value: Value, + ) => CapabilityAvailabilityFacts, +) => capability(create, (value) => value.close(), observe); + +const outsideHost = (injected: unknown, supported: boolean): boolean => + injected === undefined && !supported; +const configuredPwntools = (context: CapabilityHostContext): boolean => + pwntoolsHostSupported(context.platform, context.arch) && + isAbsolute(context.environment.REA_PWNTOOLS_PYTHON ?? ""); +const sharedObservationOwnership = (context: CapabilityHostContext) => + context.options.optionalObservationProviderOwner === "caller" + ? "caller" + : "host"; +const closeObservation = (value: { close?: () => Promise } | undefined) => + value?.close?.() ?? Promise.resolve(); + +/** Each entry binds construction, ownership and observed availability once. */ +const modules = { + wasmArtifact: service( + ({ environment, options, settings }) => + wasmArtifactModule.create(environment, options.wasmArtifact, settings), + ({ environment, options }) => ({ + optionalFeatures: { + wasmArtifactEnabled: + options.wasmArtifact !== undefined || + isAbsolute(environment.REA_WABT_BIN_DIRECTORY ?? ""), + }, + }), + ), + apkSignature: service(({ environment, options }) => + apkSignatureModule.create(environment, options.apkSignature), + ), + evmInterface: service( + ({ environment, options }) => + options.evmInterface ?? evmInterfaceModule.create(environment), + ({ environment, options, platform, arch }) => ({ + hostUnsupported: { + evmInterface: outsideHost( + options.evmInterface, + evmHostSupported(platform, arch), + ), + }, + optionalFeatures: { + evmInterfaceEnabled: + options.evmInterface !== undefined || + evmHostSupported(platform, arch), + }, + ...(options.evmInterface !== undefined || + !evmHostSupported(platform, arch) + ? {} + : { + probes: { + evmLimiterAvailability: (signal: AbortSignal) => + evmLimiterAvailability(environment, { signal }), + }, + }), + }), + ), + sqliteDatabase: service( + ({ environment, options }) => + options.sqliteDatabase ?? createSqliteDatabaseService(environment), + ), + binaryLayout: service( + ({ environment, options }) => + options.binaryLayout ?? createBinaryLayoutService(environment), + (context) => ({ + hostUnsupported: { + binaryLayout: outsideHost( + context.options.binaryLayout, + pwntoolsHostSupported(context.platform, context.arch), + ), + }, + optionalFeatures: { + binaryLayoutEnabled: + context.options.binaryLayout !== undefined || + configuredPwntools(context), + }, + }), + ), + recordedCrash: service( + ({ environment, options }) => + options.recordedCrash ?? createRecordedCrashService(environment), + (context) => ({ + hostUnsupported: { + recordedCrash: outsideHost( + context.options.recordedCrash, + pwntoolsHostSupported(context.platform, context.arch), + ), + }, + optionalFeatures: { + recordedCrashEnabled: + context.options.recordedCrash !== undefined || + configuredPwntools(context), + }, + }), + ), + webSourceLocation: service( + ({ environment, options }) => + options.webSourceLocation ?? createWebSourceLocationService(environment), + ), + webModuleTrace: service( + ({ environment, options }) => + options.webModuleTrace ?? createWebModuleTraceService(environment), + ({ environment, options }) => ({ + optionalFeatures: { + webModuleResolutionEnabled: + options.webModuleTrace !== undefined || + isAbsolute(environment.REA_BROWSER_EXECUTABLE ?? ""), + }, + }), + ), + desktopApplication: service(() => createDesktopApplicationService()), + goBinary: service( + ({ options }) => options.goBinary ?? createGoBinaryService(), + ), + rustBinary: service( + ({ options }) => options.rustBinary ?? createRustBinaryService(), + ), + erlangBeam: service( + ({ options }) => options.erlangBeam ?? createBeamService(), + ), + modelArtifact: service(() => createModelArtifactService()), + gpuCodeObjects: service(() => createGpuCodeObjectsService()), + il2cppMetadata: service( + ({ options }) => options.il2cppMetadata ?? createIl2cppMetadataService(), + ), + peResources: service(() => createPeResourcesService()), + nativeExports: service(() => createNativeExportsService()), + devicetree: service(() => createDevicetreeService()), + elfNotes: service(() => createElfNotesService()), + appleIps: service(() => createAppleIpsService()), + kernelModule: service(() => createKernelModuleService()), + peLoadConfig: service(() => createPeLoadConfigService()), + peCertificates: service(() => createPeCertificatesService()), + peImports: service(() => createPeImportsService()), + electronFuses: service( + ({ options }) => options.electronFuses ?? createElectronFuseService(), + ), + pythonPyc: service( + ({ options }) => options.pythonPyc ?? createPythonPycService(), + ), + webNetworkCapture: service( + ({ environment, options, settings }) => + options.webNetworkCapture ?? + createWebNetworkCaptureService(environment, settings), + ), + javascriptRecovery: capability( + ({ environment, options }) => + options.javascriptRecovery ?? + createJavaScriptRecoveryProvider(environment), + (value) => value.close?.() ?? Promise.resolve(), + ({ environment, options, platform, arch }) => ({ + hostUnsupported: { + javascriptRecovery: outsideHost( + options.javascriptRecovery, + wakaruHostSupported(platform, arch), + ), + }, + optionalFeatures: { + javascriptRecoveryEnabled: + options.javascriptRecovery !== undefined || + (wakaruHostSupported(platform, arch) && + isAbsolute(environment.REA_WAKARU_COMMAND ?? "")), + }, + }), + ), + artifactResources: service(() => new ArtifactResourceScope()), + reverseEngineering: service(({ providerEnvironment }) => + createReverseEngineeringService(providerEnvironment), + ), + firmwareAnalysis: service( + ({ environment, options }) => + new FirmwareAnalysisService( + options.firmwareAnalysis ?? createFirmwareAnalysisProvider(environment), + ), + ({ environment, options, platform }) => ({ + hostUnsupported: { + firmwareInspection: outsideHost( + options.firmwareAnalysis, + firmwareHostSupported(platform), + ), + firmwareExtraction: outsideHost( + options.firmwareAnalysis, + firmwareHostSupported(platform), + ), + }, + optionalFeatures: { + firmwareInspectionEnabled: + options.firmwareAnalysis !== undefined || + (firmwareHostSupported(platform) && + isAbsolute(environment.REA_BINWALK_COMMAND ?? "")), + firmwareExtractionEnabled: + options.firmwareAnalysis !== undefined || + (firmwareHostSupported(platform) && + isAbsolute(environment.REA_UNBLOB_COMMAND ?? "")), + }, + }), + ), + gdbSessions: capability( + ({ providerEnvironment }) => createGdbSessionManager(providerEnvironment), + (value) => value.closeAll(), + ({ platform }) => ({ + hostUnsupported: { + persistentDebugger: !persistentDebuggerHostSupported(platform), + }, + }), + ), + rizinDebugSessions: capability( + ({ providerEnvironment }) => + createRizinDebugSessionManager(providerEnvironment), + (value) => value.closeAll(), + ), + android: service( + ({ environment, options, settings }) => + options.androidAnalysis ?? + createAndroidAnalysisProvider(environment, undefined, settings), + (_context, value) => ({ + probes: { + androidAnalysisAvailability: (signal: AbortSignal) => + value.inspectAvailability(signal), + }, + }), + ), + pcap: service( + ({ environment, options }) => + options.pcapAnalysis ?? createPcapAnalysisProvider(environment), + ({ options, platform }) => ({ + hostUnsupported: { + pcapCapture: outsideHost( + options.pcapAnalysis, + pcapCaptureHostSupported(platform), + ), + }, + }), + ), + flutter: service( + ({ options }) => + options.flutterAnalysis ?? createFlutterBuildAnalysisProvider(), + ), + fridaInstrumentation: capability( + ({ options }) => + options.fridaInstrumentation ?? createFridaInstrumentationProvider(), + (value) => value.closeAll(), + ({ options }, value) => ({ + observeProviderLoadFailures: () => { + const failure = + options.optionalProviderLoadFailures?.fridaInstrumentation ?? + value.availabilityFailure?.(); + return failure === undefined ? {} : { fridaInstrumentation: failure }; + }, + }), + ), + packetCapture: service( + ({ environment, options }) => + options.packetCapture ?? createPacketCaptureService(environment), + ({ environment, options, platform }) => ({ + hostUnsupported: { + packetCapture: outsideHost( + options.packetCapture, + tsharkHostSupported(platform), + ), + }, + optionalFeatures: { + packetCaptureEnabled: + options.packetCapture !== undefined || + (tsharkHostSupported(platform) && tsharkLocatable(environment)), + }, + }), + ), + signalRecording: service( + ({ environment, options }) => + options.signalRecording ?? createSignalRecordingService(environment), + ({ environment, options, platform }) => ({ + hostUnsupported: { + signalRecording: outsideHost( + options.signalRecording, + sigrokHostSupported(platform), + ), + }, + optionalFeatures: { + signalRecordingEnabled: + options.signalRecording !== undefined || + (sigrokHostSupported(platform) && + isAbsolute(environment.REA_SIGROK_COMMAND ?? "")), + }, + }), + ), + webRuntime: capability( + ({ options }) => options.webRuntime ?? createWebRuntimeService(), + (value) => value.provider.close?.() ?? Promise.resolve(), + ), + apktool: service( + ({ environment, options }) => + options.apktoolAnalysis ?? + createApktoolResourceAnalysisProvider(environment), + (_context, value) => ({ + probes: { + apktoolAnalysisAvailability: (signal: AbortSignal) => + value.inspectAvailability(signal), + }, + }), + ), + jeb: service( + ({ environment, options }) => + options.jebAnalysis ?? createJebAnalysisProvider(environment), + (_context, value) => ({ + probes: { + jebAnalysisAvailability: (signal: AbortSignal) => + value.inspectAvailability(signal), + }, + }), + ), + adbDevice: service( + ({ environment, options }) => + options.adbDeviceAnalysis ?? createAdbDeviceAnalysisProvider(environment), + (_context, value) => ({ + probes: { + adbAnalysisAvailability: (signal: AbortSignal) => + value.inspectAvailability(signal), + }, + }), + ), + cutter: service(({ providerEnvironment }) => + createCutterBridgeService(providerEnvironment), + ), + browserObservation: capability( + ({ options }) => options.browserObservation, + closeObservation, + ({ options }) => ({ + optionalFeatures: { + browserObservationEnabled: options.browserObservation !== undefined, + }, + }), + sharedObservationOwnership, + ), + browserScenarioCapture: capability( + ({ options }) => options.browserScenarioCapture, + closeObservation, + ({ options }) => ({ + optionalFeatures: { + browserScenarioEnabled: options.browserScenarioCapture !== undefined, + }, + }), + sharedObservationOwnership, + ), + electronObservation: capability( + ({ options }) => options.electronObservation, + closeObservation, + ({ options }) => ({ + optionalFeatures: { + electronObservationEnabled: options.electronObservation !== undefined, + }, + }), + sharedObservationOwnership, + ), + electronActiveObservation: capability( + ({ options }) => options.electronActiveObservation, + closeObservation, + ({ options }) => ({ + optionalFeatures: { + electronAutomationEnabled: + options.electronActiveObservation !== undefined, + }, + }), + sharedObservationOwnership, + ), + javascriptRuntimeObservation: capability( + ({ options }) => options.javascriptRuntimeObservation, + closeObservation, + ({ options }) => ({ + optionalFeatures: { + v8InspectorObservationEnabled: + options.javascriptRuntimeObservation !== undefined, + }, + }), + sharedObservationOwnership, + ), +} satisfies Partial< + Record< + CapabilityDeclarationId, + (context: CapabilityHostContext) => CapabilityInstance + > +>; + +export type CapabilityServices = { + readonly [Key in keyof typeof modules]: ReturnType< + (typeof modules)[Key] + >["value"]; +}; + +/** Compose every service and its cleanup together, retaining precise typed ports. */ +export const createCapabilityServices = (context: CapabilityHostContext) => { + const scope = new CapabilityScope(); + const hostUnsupported: NonNullable< + CapabilityAvailabilityFacts["hostUnsupported"] + > = {}; + const optionalFeatures: NonNullable< + CapabilityAvailabilityFacts["optionalFeatures"] + > = {}; + const probes: NonNullable = {}; + const failureObservers: (() => OptionalProviderLoadFailures)[] = []; + const availability = { hostUnsupported, optionalFeatures }; + // This is a key-preserving map of a source-owned typed declaration, not an + // external value parser or a generic operation dispatcher. + const services = Object.fromEntries( + Object.entries(modules).map(([id, create]) => { + const instance = create(context); + const declaration = CAPABILITY_DECLARATIONS[id as keyof typeof modules]; + if (declaration === undefined) + throw new Error(`Undeclared capability ${id}`); + Object.assign( + availability.hostUnsupported, + instance.availability?.hostUnsupported, + ); + Object.assign( + availability.optionalFeatures, + instance.availability?.optionalFeatures, + ); + Object.assign(probes, instance.availability?.probes); + const observeFailure = instance.availability?.observeProviderLoadFailures; + if (observeFailure !== undefined) failureObservers.push(observeFailure); + return [ + id, + instance.ownership === "host" + ? scope.own(id, instance.value, instance.close) + : instance.value, + ]; + }), + ) as CapabilityServices; + return { + services, + availability, + probes, + observeProviderLoadFailures: (): OptionalProviderLoadFailures => + Object.assign({}, ...failureObservers.map((observe) => observe())), + close: () => scope.close(), + }; +}; diff --git a/src/composition/cutter.ts b/src/composition/cutter.ts new file mode 100644 index 0000000000..aa2df990cf --- /dev/null +++ b/src/composition/cutter.ts @@ -0,0 +1,11 @@ +import { CutterBridgeClient } from "../cutter/CutterBridgeClient.js"; +import { CutterBridgeService } from "../cutter/CutterBridgeService.js"; + +/** Compose a fresh bridge owner without discovering or contacting Cutter. */ +export const createCutterBridgeService = ( + environment: Readonly = process.env, + options: NonNullable< + ConstructorParameters[1] + > = {}, +): CutterBridgeService => + new CutterBridgeService(new CutterBridgeClient(environment, options)); diff --git a/src/composition/debuggerSessions.ts b/src/composition/debuggerSessions.ts new file mode 100644 index 0000000000..1a31db8ef9 --- /dev/null +++ b/src/composition/debuggerSessions.ts @@ -0,0 +1,20 @@ +import { GdbSessionManager } from "../gdb/GdbSessionManager.js"; +import { RizinDebugSessionManager } from "../rizin/RizinDebugSessionManager.js"; + +/** Allocate a GDB session owner; acquisition remains explicit in start(). */ +export const createGdbSessionManager = ( + environment: Readonly = process.env, + options: Omit< + NonNullable[0]>, + "environment" + > = {}, +): GdbSessionManager => new GdbSessionManager({ ...options, environment }); + +/** Allocate a Rizin debugger owner without launching a process or target. */ +export const createRizinDebugSessionManager = ( + environment: Readonly = process.env, + options: NonNullable< + ConstructorParameters[1] + > = {}, +): RizinDebugSessionManager => + new RizinDebugSessionManager(environment, options); diff --git a/src/composition/frida.ts b/src/composition/frida.ts new file mode 100644 index 0000000000..1de5ca82fa --- /dev/null +++ b/src/composition/frida.ts @@ -0,0 +1,10 @@ +import type { FridaInstrumentationPort } from "../application/frida/FridaInstrumentationPort.js"; +import { + FridaInstrumentationManager, + type FridaInstrumentationManagerOptions, +} from "../frida/FridaInstrumentationManager.js"; + +/** Own a fresh Frida lifecycle; native bindings load only when requested. */ +export const createFridaInstrumentationProvider = ( + options: FridaInstrumentationManagerOptions = {}, +): FridaInstrumentationPort => new FridaInstrumentationManager(options); diff --git a/src/composition/reverseEngineering.ts b/src/composition/reverseEngineering.ts index 493e86b9c2..e41561c8cb 100644 --- a/src/composition/reverseEngineering.ts +++ b/src/composition/reverseEngineering.ts @@ -1,14 +1,10 @@ import type { ReverseEngineeringServiceRuntimeOptions } from "../application/reverse/ReverseEngineeringService.js"; import type { ReverseEngineeringCommandPort } from "../application/reverse/ReverseEngineeringCommandPort.js"; import { ReverseEngineeringService } from "../application/reverse/ReverseEngineeringService.js"; -import { - objdumpCommand, - OBJDUMP_PROVIDER_IDENTITY, -} from "../objdump/ObjdumpCommand.js"; -import { - rizinCommand, - RIZIN_PROVIDER_IDENTITY, -} from "../rizin/RizinCommand.js"; +import { objdumpCommand } from "../objdump/ObjdumpCommand.js"; +import { OBJDUMP_PROVIDER_IDENTITY } from "../objdump/ObjdumpProviderIdentity.js"; +import { rizinCommand } from "../rizin/RizinCommand.js"; +import { RIZIN_PROVIDER_IDENTITY } from "../rizin/RizinProviderIdentity.js"; import { resolveWindowsProviderExecutable } from "./resolveWindowsProviderExecutable.js"; export const createReverseEngineeringCommandPort = ( diff --git a/src/contracts/toolFamilies.ts b/src/contracts/toolFamilies.ts index 1980cfcad9..b89851d2a0 100644 --- a/src/contracts/toolFamilies.ts +++ b/src/contracts/toolFamilies.ts @@ -32,6 +32,8 @@ import { JEB_TOOL_CONTRACTS } from "./jeb/jebToolContracts.js"; import { ADB_TOOL_CONTRACTS } from "./adb/adbToolContracts.js"; import { MANAGED_WORKFLOW_TOOL_CONTRACTS } from "./managed/managedWorkflowToolContracts.js"; import { BROWSER_PROVIDER_TOOL_CONTRACTS } from "./browserProviderToolContracts.js"; +import { BROWSER_TOOL_CONTRACTS } from "./browserToolContracts.js"; +import { BROWSER_SCENARIO_TOOL_CONTRACTS } from "./browserScenarioToolContracts.js"; import { ELECTRON_TOOL_CONTRACTS } from "./javascript/electronToolContracts.js"; import { JAVASCRIPT_RUNTIME_OBSERVATION_TOOL_CONTRACTS } from "./javascript/javascriptRuntimeObservationToolContracts.js"; import { APPLICATION_TOOL_CONTRACTS } from "./applicationToolContracts.js"; @@ -46,6 +48,59 @@ import { FRIDA_TOOL_CONTRACTS } from "./fridaToolContracts.js"; import { TOOL_CONTRACTS } from "./toolContracts.js"; import type { ToolFamilyId } from "./toolFamilyIds.js"; +/** Canonical contract-array owners, independent of broader advertising families. */ +export const TOOL_CONTRACT_GROUPS = { + ADB_TOOL_CONTRACTS, + ANALYSIS_SETTINGS_TOOL_CONTRACTS, + ANALYSIS_VIEW_TOOL_CONTRACTS, + ANDROID_TOOL_CONTRACTS, + APKTOOL_TOOL_CONTRACTS, + APK_SIGNATURE_TOOL_CONTRACTS, + APPLICATION_TOOL_CONTRACTS, + ARTIFACT_TOOL_CONTRACTS, + BINARY_DIAGNOSTICS_TOOL_CONTRACTS, + BROWSER_PROVIDER_TOOL_CONTRACTS, + BROWSER_SCENARIO_TOOL_CONTRACTS, + BROWSER_TOOL_CONTRACTS, + CAPTURE_TOOL_CONTRACTS, + CUTTER_TOOL_CONTRACTS, + DESKTOP_APPLICATION_TOOL_CONTRACTS, + ELECTRON_TOOL_CONTRACTS, + ENHANCED_TOOL_CONTRACTS, + ENVIRONMENT_TOOL_CONTRACTS, + ERLANG_BEAM_TOOL_CONTRACTS, + EVM_TOOL_CONTRACTS, + FIRMWARE_TOOL_CONTRACTS, + FLUTTER_TOOL_CONTRACTS, + FRIDA_TOOL_CONTRACTS, + GDB_TOOL_CONTRACTS, + GO_TOOL_CONTRACTS, + GPU_TOOL_CONTRACTS, + IL2CPP_TOOL_CONTRACTS, + JAVASCRIPT_RECOVERY_TOOL_CONTRACTS, + JAVASCRIPT_RUNTIME_OBSERVATION_TOOL_CONTRACTS, + JEB_TOOL_CONTRACTS, + MANAGED_TOOL_CONTRACTS, + MANAGED_WORKFLOW_TOOL_CONTRACTS, + MODEL_TOOL_CONTRACTS, + NATIVE_TOOL_CONTRACTS, + OFFICIAL_TOOL_CONTRACTS, + PCAP_TOOL_CONTRACTS, + PYTHON_PYC_TOOL_CONTRACTS, + REVERSE_ENGINEERING_TOOL_CONTRACTS, + RIZIN_DEBUG_TOOL_CONTRACTS, + RUST_TOOL_CONTRACTS, + SESSION_TOOL_CONTRACTS, + SIGNAL_TOOL_CONTRACTS, + SQLITE_TOOL_CONTRACTS, + WASM_TOOL_CONTRACTS, + WEB_NETWORK_CAPTURE_TOOL_CONTRACTS, + WEB_RUNTIME_TOOL_CONTRACTS, + WEB_SCRIPT_TOOL_CONTRACTS, +} as const; + +export type ToolContractGroupId = keyof typeof TOOL_CONTRACT_GROUPS; + /** One selectable family: a stable selection id and its complete contract set. */ export interface ToolFamilyDefinition { readonly id: ToolFamilyId; @@ -62,107 +117,131 @@ export const TOOL_FAMILY_DEFINITIONS = [ { id: "direct", surface: "official-proxy", - contracts: OFFICIAL_TOOL_CONTRACTS, + contracts: TOOL_CONTRACT_GROUPS.OFFICIAL_TOOL_CONTRACTS, + }, + { + id: "enhanced", + surface: "enhanced", + contracts: TOOL_CONTRACT_GROUPS.ENHANCED_TOOL_CONTRACTS, }, - { id: "enhanced", surface: "enhanced", contracts: ENHANCED_TOOL_CONTRACTS }, { id: "native", surface: "native-provider", contracts: [ - ...NATIVE_TOOL_CONTRACTS, - ...BINARY_DIAGNOSTICS_TOOL_CONTRACTS, - ...CUTTER_TOOL_CONTRACTS, - ...REVERSE_ENGINEERING_TOOL_CONTRACTS, - ...GDB_TOOL_CONTRACTS, - ...RIZIN_DEBUG_TOOL_CONTRACTS, + ...TOOL_CONTRACT_GROUPS.NATIVE_TOOL_CONTRACTS, + ...TOOL_CONTRACT_GROUPS.BINARY_DIAGNOSTICS_TOOL_CONTRACTS, + ...TOOL_CONTRACT_GROUPS.CUTTER_TOOL_CONTRACTS, + ...TOOL_CONTRACT_GROUPS.REVERSE_ENGINEERING_TOOL_CONTRACTS, + ...TOOL_CONTRACT_GROUPS.GDB_TOOL_CONTRACTS, + ...TOOL_CONTRACT_GROUPS.RIZIN_DEBUG_TOOL_CONTRACTS, ], }, { id: "artifact", surface: "artifact-provider", contracts: [ - ...ARTIFACT_TOOL_CONTRACTS, - ...DESKTOP_APPLICATION_TOOL_CONTRACTS, - ...EVM_TOOL_CONTRACTS, - ...GO_TOOL_CONTRACTS, - ...GPU_TOOL_CONTRACTS, - ...IL2CPP_TOOL_CONTRACTS, - ...MODEL_TOOL_CONTRACTS, - ...RUST_TOOL_CONTRACTS, - ...PYTHON_PYC_TOOL_CONTRACTS, - ...ERLANG_BEAM_TOOL_CONTRACTS, - ...WASM_TOOL_CONTRACTS, - ...APK_SIGNATURE_TOOL_CONTRACTS, - ...SQLITE_TOOL_CONTRACTS, - ...CAPTURE_TOOL_CONTRACTS, - ...SIGNAL_TOOL_CONTRACTS, + ...TOOL_CONTRACT_GROUPS.ARTIFACT_TOOL_CONTRACTS, + ...TOOL_CONTRACT_GROUPS.DESKTOP_APPLICATION_TOOL_CONTRACTS, + ...TOOL_CONTRACT_GROUPS.EVM_TOOL_CONTRACTS, + ...TOOL_CONTRACT_GROUPS.GO_TOOL_CONTRACTS, + ...TOOL_CONTRACT_GROUPS.GPU_TOOL_CONTRACTS, + ...TOOL_CONTRACT_GROUPS.IL2CPP_TOOL_CONTRACTS, + ...TOOL_CONTRACT_GROUPS.MODEL_TOOL_CONTRACTS, + ...TOOL_CONTRACT_GROUPS.RUST_TOOL_CONTRACTS, + ...TOOL_CONTRACT_GROUPS.PYTHON_PYC_TOOL_CONTRACTS, + ...TOOL_CONTRACT_GROUPS.ERLANG_BEAM_TOOL_CONTRACTS, + ...TOOL_CONTRACT_GROUPS.WASM_TOOL_CONTRACTS, + ...TOOL_CONTRACT_GROUPS.APK_SIGNATURE_TOOL_CONTRACTS, + ...TOOL_CONTRACT_GROUPS.SQLITE_TOOL_CONTRACTS, + ...TOOL_CONTRACT_GROUPS.CAPTURE_TOOL_CONTRACTS, + ...TOOL_CONTRACT_GROUPS.SIGNAL_TOOL_CONTRACTS, ], }, { id: "managed", surface: "managed", - contracts: [...MANAGED_TOOL_CONTRACTS, ...MANAGED_WORKFLOW_TOOL_CONTRACTS], + contracts: [ + ...TOOL_CONTRACT_GROUPS.MANAGED_TOOL_CONTRACTS, + ...TOOL_CONTRACT_GROUPS.MANAGED_WORKFLOW_TOOL_CONTRACTS, + ], }, { id: "firmware", surface: "firmware-provider", - contracts: FIRMWARE_TOOL_CONTRACTS, + contracts: TOOL_CONTRACT_GROUPS.FIRMWARE_TOOL_CONTRACTS, }, { id: "android", surface: "android-provider", - contracts: ANDROID_TOOL_CONTRACTS, + contracts: TOOL_CONTRACT_GROUPS.ANDROID_TOOL_CONTRACTS, + }, + { + id: "pcap", + surface: "pcap-provider", + contracts: TOOL_CONTRACT_GROUPS.PCAP_TOOL_CONTRACTS, }, - { id: "pcap", surface: "pcap-provider", contracts: PCAP_TOOL_CONTRACTS }, { id: "flutter", surface: "flutter-provider", - contracts: FLUTTER_TOOL_CONTRACTS, + contracts: TOOL_CONTRACT_GROUPS.FLUTTER_TOOL_CONTRACTS, }, { id: "frida", surface: "instrumentation-provider", - contracts: FRIDA_TOOL_CONTRACTS, + contracts: TOOL_CONTRACT_GROUPS.FRIDA_TOOL_CONTRACTS, }, { id: "apktool", surface: "apktool-provider", - contracts: APKTOOL_TOOL_CONTRACTS, + contracts: TOOL_CONTRACT_GROUPS.APKTOOL_TOOL_CONTRACTS, + }, + { + id: "jeb", + surface: "jeb-provider", + contracts: TOOL_CONTRACT_GROUPS.JEB_TOOL_CONTRACTS, + }, + { + id: "adb", + surface: "adb-provider", + contracts: TOOL_CONTRACT_GROUPS.ADB_TOOL_CONTRACTS, }, - { id: "jeb", surface: "jeb-provider", contracts: JEB_TOOL_CONTRACTS }, - { id: "adb", surface: "adb-provider", contracts: ADB_TOOL_CONTRACTS }, { id: "browser", surface: "browser-provider", contracts: [ - ...BROWSER_PROVIDER_TOOL_CONTRACTS, - ...WEB_RUNTIME_TOOL_CONTRACTS, - ...WEB_NETWORK_CAPTURE_TOOL_CONTRACTS, + ...TOOL_CONTRACT_GROUPS.BROWSER_PROVIDER_TOOL_CONTRACTS, + ...TOOL_CONTRACT_GROUPS.WEB_RUNTIME_TOOL_CONTRACTS, + ...TOOL_CONTRACT_GROUPS.WEB_NETWORK_CAPTURE_TOOL_CONTRACTS, ], }, { id: "electron", surface: "electron-provider", - contracts: ELECTRON_TOOL_CONTRACTS, + contracts: TOOL_CONTRACT_GROUPS.ELECTRON_TOOL_CONTRACTS, }, { id: "javascript-runtime", surface: "runtime-provider", - contracts: JAVASCRIPT_RUNTIME_OBSERVATION_TOOL_CONTRACTS, + contracts: + TOOL_CONTRACT_GROUPS.JAVASCRIPT_RUNTIME_OBSERVATION_TOOL_CONTRACTS, }, { id: "application", surface: "application-workflow", contracts: [ - ...APPLICATION_TOOL_CONTRACTS, - ...ANALYSIS_SETTINGS_TOOL_CONTRACTS, - ...ENVIRONMENT_TOOL_CONTRACTS, - ...ANALYSIS_VIEW_TOOL_CONTRACTS, - ...WEB_SCRIPT_TOOL_CONTRACTS, - ...JAVASCRIPT_RECOVERY_TOOL_CONTRACTS, + ...TOOL_CONTRACT_GROUPS.APPLICATION_TOOL_CONTRACTS, + ...TOOL_CONTRACT_GROUPS.ANALYSIS_SETTINGS_TOOL_CONTRACTS, + ...TOOL_CONTRACT_GROUPS.ENVIRONMENT_TOOL_CONTRACTS, + ...TOOL_CONTRACT_GROUPS.ANALYSIS_VIEW_TOOL_CONTRACTS, + ...TOOL_CONTRACT_GROUPS.WEB_SCRIPT_TOOL_CONTRACTS, + ...TOOL_CONTRACT_GROUPS.JAVASCRIPT_RECOVERY_TOOL_CONTRACTS, ], }, - { id: "session", surface: "session", contracts: SESSION_TOOL_CONTRACTS }, + { + id: "session", + surface: "session", + contracts: TOOL_CONTRACT_GROUPS.SESSION_TOOL_CONTRACTS, + }, ] as const satisfies readonly ToolFamilyDefinition[]; const familyByTool = new Map(); diff --git a/src/cutter/CutterBridgeClient.ts b/src/cutter/CutterBridgeClient.ts index 1bcbe9e43b..92f7497407 100644 --- a/src/cutter/CutterBridgeClient.ts +++ b/src/cutter/CutterBridgeClient.ts @@ -61,12 +61,6 @@ const descriptorSchema = z.object({ max_request_bytes: z.int().positive().optional(), }); -export const CUTTER_PROVIDER_IDENTITY = { - id: "cutter.python-plugin", - name: "Cutter Python plugin", - version: null, -} as const; - const responseSchema = z.object({ ok: z.boolean(), session_id: z.string().uuid().optional(), diff --git a/src/cutter/CutterBridgeService.ts b/src/cutter/CutterBridgeService.ts index 5fac87d6b4..987d4b4b16 100644 --- a/src/cutter/CutterBridgeService.ts +++ b/src/cutter/CutterBridgeService.ts @@ -16,8 +16,8 @@ import { CutterBridgeClient, type CutterBridgeExecution, type CutterDiscoveryOptions, - CUTTER_PROVIDER_IDENTITY, } from "./CutterBridgeClient.js"; +import { CUTTER_PROVIDER_IDENTITY } from "./CutterProviderIdentity.js"; interface CutterBridgePort { listSessions( diff --git a/src/cutter/CutterProviderIdentity.ts b/src/cutter/CutterProviderIdentity.ts new file mode 100644 index 0000000000..d38feb1767 --- /dev/null +++ b/src/cutter/CutterProviderIdentity.ts @@ -0,0 +1,6 @@ +/** Source-owned static provider identity and public operation names. */ +export const CUTTER_PROVIDER_IDENTITY = { + id: "cutter.python-plugin", + name: "Cutter Python plugin", + version: null, +} as const; diff --git a/src/domain/sqlite/SqliteProviderIdentity.ts b/src/domain/sqlite/SqliteProviderIdentity.ts new file mode 100644 index 0000000000..d3acee6d01 --- /dev/null +++ b/src/domain/sqlite/SqliteProviderIdentity.ts @@ -0,0 +1,6 @@ +/** Format identity is shared by discovery, catalog, and observed SQLite results. */ +export const SQLITE_PROVIDER_IDENTITY = { + id: "sqlite", + name: "REA SQLite database inspector", + version: null, +} as const; diff --git a/src/domain/sqlite/sqliteDatabase.ts b/src/domain/sqlite/sqliteDatabase.ts index 3f939dff20..ece4d30d62 100644 --- a/src/domain/sqlite/sqliteDatabase.ts +++ b/src/domain/sqlite/sqliteDatabase.ts @@ -2,13 +2,6 @@ import { z } from "zod"; import { absoluteLocalPathStringSchema } from "../localPath.js"; import type { JsonValue } from "../jsonValue.js"; -/** Format identity is shared by discovery, catalog, and observed SQLite results. */ -export const SQLITE_PROVIDER_IDENTITY = { - id: "sqlite", - name: "REA SQLite database inspector", - version: null, -} as const; - /** SQLite exposes this table as a row source but omits its own catalog entry. */ export const SQLITE_SCHEMA_TABLE_NAME = "sqlite_schema"; export const sqliteSchemaHasObjectDeclaration = (name: string): boolean => diff --git a/src/domain/webTraceProviders.ts b/src/domain/webTraceProviders.ts new file mode 100644 index 0000000000..90373d0e34 --- /dev/null +++ b/src/domain/webTraceProviders.ts @@ -0,0 +1,11 @@ +/** Identities of the captured-source interpretation workflows. */ +export const WEB_SOURCE_LOCATION_PROVIDER = { + id: "rea-web-source-location", + name: "REA captured source-map point trace", + version: "1", +} as const; +export const WEB_MODULE_TRACE_PROVIDER = { + id: "rea-web-module-trace", + name: "REA captured module relationship trace", + version: "1", +} as const; diff --git a/src/electron/ElectronFuseFailures.ts b/src/electron/ElectronFuseFailures.ts index 64714b3cc4..7a63d618d6 100644 --- a/src/electron/ElectronFuseFailures.ts +++ b/src/electron/ElectronFuseFailures.ts @@ -1,3 +1,4 @@ +import { ELECTRON_FUSE_PROVIDER_IDENTITY } from "./ElectronFuseProviderIdentity.js"; import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js"; import { ArtifactFileCleanupFailure, @@ -31,13 +32,6 @@ import { ElectronFuseResourceFailure, } from "./ElectronFuseReader.js"; -/** Bundled fuse-wire reader. It does not flip fuses or launch the application. */ -export const ELECTRON_FUSE_PROVIDER_IDENTITY = { - id: "electron-fuses", - name: "REA Electron fuse reader", - version: "1", -} as const; - const OPERATION = "inspect_electron_fuses"; /** Translate the shared fuse reader without replacing its producing outcome or cleanup owner. */ diff --git a/src/electron/ElectronFuseProvider.ts b/src/electron/ElectronFuseProvider.ts index a21d5cfbdc..ffae6fbc74 100644 --- a/src/electron/ElectronFuseProvider.ts +++ b/src/electron/ElectronFuseProvider.ts @@ -21,12 +21,8 @@ import { readElectronFuseImage, type ElectronFuseImage, } from "./ElectronFuseReader.js"; -import { - ELECTRON_FUSE_PROVIDER_IDENTITY, - electronFuseFailure, -} from "./ElectronFuseFailures.js"; - -export { ELECTRON_FUSE_PROVIDER_IDENTITY }; +import { electronFuseFailure } from "./ElectronFuseFailures.js"; +import { ELECTRON_FUSE_PROVIDER_IDENTITY } from "./ElectronFuseProviderIdentity.js"; const OPERATION = "inspect_electron_fuses"; diff --git a/src/electron/ElectronFuseProviderIdentity.ts b/src/electron/ElectronFuseProviderIdentity.ts new file mode 100644 index 0000000000..18b675fbd8 --- /dev/null +++ b/src/electron/ElectronFuseProviderIdentity.ts @@ -0,0 +1,6 @@ +/** Bundled fuse-wire reader. It does not flip fuses or launch the application. */ +export const ELECTRON_FUSE_PROVIDER_IDENTITY = { + id: "electron-fuses", + name: "REA Electron fuse reader", + version: "1", +} as const; diff --git a/src/erlang/BeamProvider.ts b/src/erlang/BeamProvider.ts index 4aa2301115..bbee47ca82 100644 --- a/src/erlang/BeamProvider.ts +++ b/src/erlang/BeamProvider.ts @@ -1,3 +1,4 @@ +import { ERLANG_BEAM_PROVIDER_IDENTITY } from "./ErlangBeamProviderIdentity.js"; import { projectStableArtifactFailure, projectArtifactCloseFailure, @@ -30,13 +31,6 @@ import { import { err, ok } from "../domain/result.js"; import { BeamCapacityFailure, parseErlangBeam } from "./BeamParser.js"; -/** Bundled format reader identity; module names are separate observed facts. */ -export const ERLANG_BEAM_PROVIDER_IDENTITY = { - id: "rea-erlang-beam", - name: "REA Erlang BEAM reader", - version: "1", -} as const; - const OPERATION = "inspect_erlang_beam"; /** Portable parser over one bounded, identity-checked regular file. */ diff --git a/src/erlang/ErlangBeamProviderIdentity.ts b/src/erlang/ErlangBeamProviderIdentity.ts new file mode 100644 index 0000000000..f26529202f --- /dev/null +++ b/src/erlang/ErlangBeamProviderIdentity.ts @@ -0,0 +1,6 @@ +/** Bundled format reader identity; module names are separate observed facts. */ +export const ERLANG_BEAM_PROVIDER_IDENTITY = { + id: "rea-erlang-beam", + name: "REA Erlang BEAM reader", + version: "1", +} as const; diff --git a/src/evm/EvmoleFailures.ts b/src/evm/EvmoleFailures.ts index 190e3b7f9e..3ae76c1b21 100644 --- a/src/evm/EvmoleFailures.ts +++ b/src/evm/EvmoleFailures.ts @@ -28,11 +28,11 @@ import { MAX_EVM_TIMEOUT_MS, } from "../domain/evm/evmInterface.js"; import { - EVMOLE_PROVIDER_IDENTITY, EVM_CPU_SECONDS, EVM_INTERFACE_LIMITS, EVM_FILE_SIZE_FAILURE_EXIT, } from "./EvmoleRelease.js"; +import { EVMOLE_PROVIDER_IDENTITY } from "./EvmoleProviderIdentity.js"; import { fixedAllowanceOptions } from "../domain/fixedAllowanceRemediation.js"; /** Both a structured worker refusal and reply-file admission name this allowance. */ diff --git a/src/evm/EvmoleInterfaceProvider.ts b/src/evm/EvmoleInterfaceProvider.ts index 4f50055207..b8f41cf152 100644 --- a/src/evm/EvmoleInterfaceProvider.ts +++ b/src/evm/EvmoleInterfaceProvider.ts @@ -76,11 +76,11 @@ import { projectEvmoleFunctions, } from "./EvmoleInterfaceAdapter.js"; import { - EVMOLE_PROVIDER_IDENTITY, EVM_CPU_SECONDS, EVM_INTERFACE_LIMITS, EVM_FILE_SIZE_FAILURE_EXIT, } from "./EvmoleRelease.js"; +import { EVMOLE_PROVIDER_IDENTITY } from "./EvmoleProviderIdentity.js"; import { decodeEvmBytecodeCarrier } from "./EvmBytecodeCarrier.js"; import { selectEvmWorkerLimits, diff --git a/src/evm/EvmoleProviderIdentity.ts b/src/evm/EvmoleProviderIdentity.ts new file mode 100644 index 0000000000..492171551b --- /dev/null +++ b/src/evm/EvmoleProviderIdentity.ts @@ -0,0 +1,6 @@ +/** Unchanged locked npm/WASM profile; provenance is independent of chain deployment authenticity. */ +export const EVMOLE_PROVIDER_IDENTITY = { + id: "evmole-interface", + name: "REA EVM interface adapter", + version: "evmole@0.9.3", +} as const; diff --git a/src/evm/EvmoleRelease.ts b/src/evm/EvmoleRelease.ts index 7dd3a181e6..4432ce366a 100644 --- a/src/evm/EvmoleRelease.ts +++ b/src/evm/EvmoleRelease.ts @@ -1,11 +1,5 @@ import { DEFAULT_EVM_TIMEOUT_MS } from "../domain/evm/evmInterface.js"; -/** Unchanged locked npm/WASM profile; provenance is independent of chain deployment authenticity. */ -export const EVMOLE_PROVIDER_IDENTITY = { - id: "evmole-interface", - name: "REA EVM interface adapter", - version: "evmole@0.9.3", -} as const; /** Worker-reserved status for an observed EFBIG write without a complete reply. */ export const EVM_FILE_SIZE_FAILURE_EXIT = 76; /** Complete offline inspection budgets; worker JavaScript heap is bounded separately. */ diff --git a/src/firmware/FirmwareProvider.ts b/src/firmware/FirmwareProvider.ts index 6fbc7cff96..377095073d 100644 --- a/src/firmware/FirmwareProvider.ts +++ b/src/firmware/FirmwareProvider.ts @@ -31,7 +31,7 @@ import { publishFirmwareExtraction } from "./FirmwarePublication.js"; import { BINWALK_PROVIDER_IDENTITY, UNBLOB_PROVIDER_IDENTITY, -} from "./FirmwareRelease.js"; +} from "./FirmwareProviderIdentities.js"; import { startFirmwareDeadline, type FirmwareDeadline, diff --git a/src/firmware/FirmwareProviderIdentities.ts b/src/firmware/FirmwareProviderIdentities.ts new file mode 100644 index 0000000000..cfe77d685e --- /dev/null +++ b/src/firmware/FirmwareProviderIdentities.ts @@ -0,0 +1,15 @@ +import { FIRMWARE_TOOL_REQUIREMENTS } from "../contracts/firmware/firmwareToolRequirements.js"; + +/** Catalog identity for the verified Binwalk release. Evidence replaces version with the observed banner. */ +export const BINWALK_PROVIDER_IDENTITY = { + id: "binwalk", + name: FIRMWARE_TOOL_REQUIREMENTS.binwalk.name, + version: FIRMWARE_TOOL_REQUIREMENTS.binwalk.version, +} as const; + +/** Catalog identity for explicit firmware extraction. */ +export const UNBLOB_PROVIDER_IDENTITY = { + id: "unblob", + name: FIRMWARE_TOOL_REQUIREMENTS.unblob.name, + version: FIRMWARE_TOOL_REQUIREMENTS.unblob.version, +} as const; diff --git a/src/firmware/FirmwareRelease.ts b/src/firmware/FirmwareRelease.ts index 42c96f3c2f..653cdea3b9 100644 --- a/src/firmware/FirmwareRelease.ts +++ b/src/firmware/FirmwareRelease.ts @@ -1,18 +1,4 @@ import { BOUNDED_PRODUCER_LIMITS } from "../process/LinuxResourceLimits.js"; -import { FIRMWARE_TOOL_REQUIREMENTS } from "../contracts/firmware/firmwareToolRequirements.js"; - -/** Catalog identity for the verified Binwalk release. Evidence replaces version with the observed banner. */ -export const BINWALK_PROVIDER_IDENTITY = { - id: "binwalk", - name: FIRMWARE_TOOL_REQUIREMENTS.binwalk.name, - version: FIRMWARE_TOOL_REQUIREMENTS.binwalk.version, -} as const; -/** Catalog identity for explicit firmware extraction. */ -export const UNBLOB_PROVIDER_IDENTITY = { - id: "unblob", - name: FIRMWARE_TOOL_REQUIREMENTS.unblob.name, - version: FIRMWARE_TOOL_REQUIREMENTS.unblob.version, -} as const; /** Resource policy for this Linux integration, not a claim of aggregate containment. */ export const FIRMWARE_LIMITS = { diff --git a/src/flutter/FlutterBuildProvider.ts b/src/flutter/FlutterBuildProvider.ts index 185979e052..8cf8cbc056 100644 --- a/src/flutter/FlutterBuildProvider.ts +++ b/src/flutter/FlutterBuildProvider.ts @@ -1,3 +1,4 @@ +import { FLUTTER_PROVIDER_IDENTITY } from "./FlutterProviderIdentity.js"; import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; import { AnalysisCleanupLease, @@ -66,13 +67,6 @@ import { import { readElfBuildIds } from "./ElfBuildId.js"; import { privateWorkspaceFailure } from "../process/privateWorkspaceFailure.js"; -/** Stable identity for the Flutter build-identification provider. */ -export const FLUTTER_PROVIDER_IDENTITY = { - id: "flutter", - name: "Flutter build identification", - version: null, -} as const; - const APK_METADATA_READ_MAX_BYTES = 8 * 1024 * 1024; const LIBRARY_MAX_BYTES = 256 * 1024 * 1024; /** Cumulative cap across every library entry in one identification. */ diff --git a/src/flutter/FlutterProviderIdentity.ts b/src/flutter/FlutterProviderIdentity.ts new file mode 100644 index 0000000000..60749ce6da --- /dev/null +++ b/src/flutter/FlutterProviderIdentity.ts @@ -0,0 +1,6 @@ +/** Stable identity for the Flutter build-identification provider. */ +export const FLUTTER_PROVIDER_IDENTITY = { + id: "flutter", + name: "Flutter build identification", + version: null, +} as const; diff --git a/src/application/frida/FridaInstrumentationManager.test.ts b/src/frida/FridaInstrumentationManager.test.ts similarity index 98% rename from src/application/frida/FridaInstrumentationManager.test.ts rename to src/frida/FridaInstrumentationManager.test.ts index af5c37a377..a0c4a0e1a0 100644 --- a/src/application/frida/FridaInstrumentationManager.test.ts +++ b/src/frida/FridaInstrumentationManager.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, onTestFinished } from "vitest"; -import { createDeferred } from "../../../tests/fixtures/binarySession.js"; +import { createDeferred } from "../../tests/fixtures/binarySession.js"; import type { Device, @@ -10,18 +10,18 @@ import type { Session, Signal, } from "frida"; -import { jsonObjectSchema } from "../../domain/jsonValue.js"; -import { FridaScriptLoadCancelledError } from "./FridaInstrumentationPort.js"; +import { jsonObjectSchema } from "../domain/jsonValue.js"; +import { FridaScriptLoadCancelledError } from "../application/frida/FridaInstrumentationPort.js"; import { AnalysisCapabilityUnavailableError, AnalysisInputError, AnalysisTimeoutError, -} from "../../domain/analysisErrorCore.js"; -import { FridaInstrumentationManager } from "../../frida/FridaInstrumentationManager.js"; -import { fridaCaptureLimitations } from "../../frida/fridaCaptureLimits.js"; -import { AnalysisError } from "../../domain/analysisErrorBase.js"; -import { projectAnalysisError } from "../../domain/analysisErrorProjection.js"; -import { FridaInstrumentationService } from "./FridaInstrumentationService.js"; +} from "../domain/analysisErrorCore.js"; +import { FridaInstrumentationManager } from "./FridaInstrumentationManager.js"; +import { fridaCaptureLimitations } from "./fridaCaptureLimits.js"; +import { AnalysisError } from "../domain/analysisErrorBase.js"; +import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; +import { FridaInstrumentationService } from "../application/frida/FridaInstrumentationService.js"; const signal = void>() => { let connected: Handler | undefined; diff --git a/src/frida/FridaInstrumentationManager.ts b/src/frida/FridaInstrumentationManager.ts index 9e6fc01276..bbde915936 100644 --- a/src/frida/FridaInstrumentationManager.ts +++ b/src/frida/FridaInstrumentationManager.ts @@ -68,12 +68,6 @@ const isFridaDeviceLookupTimeout = (cause: unknown): boolean => cause instanceof Error && cause.message === FRIDA_DEVICE_LOOKUP_TIMEOUT_MESSAGE; -export const FRIDA_PROVIDER_IDENTITY = { - id: "frida", - name: "Frida", - version: null, -} as const; - interface ScriptMessageCapture { readonly messages: MessageFifo; truncated: boolean; diff --git a/src/frida/FridaProviderIdentity.ts b/src/frida/FridaProviderIdentity.ts new file mode 100644 index 0000000000..620805bb2f --- /dev/null +++ b/src/frida/FridaProviderIdentity.ts @@ -0,0 +1,6 @@ +/** Source-owned static provider identity and public operation names. */ +export const FRIDA_PROVIDER_IDENTITY = { + id: "frida", + name: "Frida", + version: null, +} as const; diff --git a/src/gdb/GdbProviderIdentity.ts b/src/gdb/GdbProviderIdentity.ts new file mode 100644 index 0000000000..76baf799b2 --- /dev/null +++ b/src/gdb/GdbProviderIdentity.ts @@ -0,0 +1,6 @@ +/** Source-owned static provider identity and public operation names. */ +export const GDB_PROVIDER_IDENTITY = { + id: "gnu.gdb", + name: "GNU GDB", + version: null, +} as const; diff --git a/src/gdb/GdbSessionManager.ts b/src/gdb/GdbSessionManager.ts index 350b866741..cf9047bc6d 100644 --- a/src/gdb/GdbSessionManager.ts +++ b/src/gdb/GdbSessionManager.ts @@ -1,3 +1,4 @@ +import { GDB_PROVIDER_IDENTITY } from "./GdbProviderIdentity.js"; import { persistentDebuggerHostSupported } from "../process/PersistentDebuggerHost.js"; import { ABORTED, waitForAbortable } from "../process/AbortablePromise.js"; import { snapshotGdbCommandRecords } from "./GdbCommandRecords.js"; @@ -115,12 +116,6 @@ const cancelledWithCallerReason = ( return cancellation; }; -export const GDB_PROVIDER_IDENTITY = { - id: "gnu.gdb", - name: "GNU GDB", - version: null, -} as const; - interface PendingCommand { readonly token: number; readonly command: string; diff --git a/src/ghidra/GhidraFunctionOperations.ts b/src/ghidra/GhidraFunctionOperations.ts new file mode 100644 index 0000000000..9cd1d9ca96 --- /dev/null +++ b/src/ghidra/GhidraFunctionOperations.ts @@ -0,0 +1,16 @@ +/** Function analysis and annotation operations admitted by the Ghidra adapter. */ +export const GHIDRA_FUNCTION_OPERATIONS = [ + "annotate_native_function", + "inspect_native_data_type", + "inspect_native_instruction", + "resolve_native_call_targets", + "analyze_function", + "procedure_assembly", + "procedure_callees", + "procedure_callers", + "procedure_info", + "procedure_pseudo_code", + "read_function_instructions", + "procedure_references", + "xrefs", +] as const; diff --git a/src/ghidra/GhidraFunctionValues.ts b/src/ghidra/GhidraFunctionValues.ts index dc24c22871..57c78761a0 100644 --- a/src/ghidra/GhidraFunctionValues.ts +++ b/src/ghidra/GhidraFunctionValues.ts @@ -1,3 +1,4 @@ +import { GHIDRA_FUNCTION_OPERATIONS } from "./GhidraFunctionOperations.js"; import { nativeAddressObservationSchema } from "../domain/native/nativeAddress.js"; import { procedureInfoSchema } from "../domain/native/procedureInfo.js"; import { z } from "zod"; @@ -55,23 +56,6 @@ import { ghidraFunctionIdentitySchema as dossierProcedureIdentity, } from "./GhidraInventoryValues.js"; -/** Function analysis and annotation operations admitted by the Ghidra adapter. */ -export const GHIDRA_FUNCTION_OPERATIONS = [ - "annotate_native_function", - "inspect_native_data_type", - "inspect_native_instruction", - "resolve_native_call_targets", - "analyze_function", - "procedure_assembly", - "procedure_callees", - "procedure_callers", - "procedure_info", - "procedure_pseudo_code", - "read_function_instructions", - "procedure_references", - "xrefs", -] as const; - /** One function-analysis operation implemented by the packaged Java bridge. */ export type GhidraFunctionOperation = (typeof GHIDRA_FUNCTION_OPERATIONS)[number]; diff --git a/src/ghidra/GhidraInventoryOperations.ts b/src/ghidra/GhidraInventoryOperations.ts new file mode 100644 index 0000000000..7075da5c42 --- /dev/null +++ b/src/ghidra/GhidraInventoryOperations.ts @@ -0,0 +1,23 @@ +/** + * Direct inventory operations admitted by the Ghidra adapter. All are reads + * except the naming operations, which edit the ephemeral session database. + */ +export const GHIDRA_INVENTORY_OPERATIONS = [ + "read_binary_inventory_summary", + "inspect_native_load_image", + "read_bytes", + "search_bytes", + "address_to_file_offset", + "address_name", + "list_documents", + "list_names", + "list_procedures", + "list_segments", + "list_strings", + "procedure_address", + "resolve_containing_procedure", + "search_procedures", + "search_strings", + "set_address_name", + "set_addresses_names", +] as const; diff --git a/src/ghidra/GhidraInventoryValues.ts b/src/ghidra/GhidraInventoryValues.ts index 974050e777..56da3cdd2d 100644 --- a/src/ghidra/GhidraInventoryValues.ts +++ b/src/ghidra/GhidraInventoryValues.ts @@ -1,3 +1,4 @@ +import { GHIDRA_INVENTORY_OPERATIONS } from "./GhidraInventoryOperations.js"; import { nativeSearchFields } from "../domain/native/nativeSearch.js"; import { nativeSymbolMetadataSchema } from "../domain/native/nativeSymbol.js"; import { nativeAddressObservationSchema } from "../domain/native/nativeAddress.js"; @@ -30,30 +31,6 @@ import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js"; import { err, ok, type Result } from "../domain/result.js"; import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js"; -/** - * Direct inventory operations admitted by the Ghidra adapter. All are reads - * except the naming operations, which edit the ephemeral session database. - */ -export const GHIDRA_INVENTORY_OPERATIONS = [ - "read_binary_inventory_summary", - "inspect_native_load_image", - "read_bytes", - "search_bytes", - "address_to_file_offset", - "address_name", - "list_documents", - "list_names", - "list_procedures", - "list_segments", - "list_strings", - "procedure_address", - "resolve_containing_procedure", - "search_procedures", - "search_strings", - "set_address_name", - "set_addresses_names", -] as const; - /** Inventory operations that write analyst names into the session database. */ export const GHIDRA_NAMING_OPERATIONS = [ "set_address_name", diff --git a/src/ghidra/GhidraProvider.ts b/src/ghidra/GhidraProvider.ts index 8f40e43880..860c835746 100644 --- a/src/ghidra/GhidraProvider.ts +++ b/src/ghidra/GhidraProvider.ts @@ -75,9 +75,9 @@ import { existingProjectCapabilities, EXISTING_PROJECT_WINDOWS_UNAVAILABLE, windowsP0Capabilities, - GHIDRA_PROVIDER_IDENTITY, WINDOWS_P0_NATIVE_PE_ARCHITECTURES, } from "./GhidraProviderCapabilities.js"; +import { GHIDRA_PROVIDER_IDENTITY } from "./GhidraProviderMetadata.js"; import { createGhidraProviderClient, admitGhidraClient, diff --git a/src/ghidra/GhidraProviderCapabilities.ts b/src/ghidra/GhidraProviderCapabilities.ts index 4fe7604fb9..0714b6f7cc 100644 --- a/src/ghidra/GhidraProviderCapabilities.ts +++ b/src/ghidra/GhidraProviderCapabilities.ts @@ -1,13 +1,11 @@ -import type { - CapabilityDescriptor, - ProviderIdentity, -} from "../application/AnalysisProvider.js"; +import { GHIDRA_PROVIDER_IDENTITY } from "./GhidraProviderMetadata.js"; +import type { CapabilityDescriptor } from "../application/AnalysisProvider.js"; import { inspectWindowsNativeReadiness, type WindowsNativeReadiness, } from "../process/WindowsAuthority.js"; -import { GHIDRA_FUNCTION_OPERATIONS } from "./GhidraFunctionValues.js"; -import { GHIDRA_INVENTORY_OPERATIONS } from "./GhidraInventoryValues.js"; +import { GHIDRA_FUNCTION_OPERATIONS } from "./GhidraFunctionOperations.js"; +import { GHIDRA_INVENTORY_OPERATIONS } from "./GhidraInventoryOperations.js"; import { GHIDRA_MUTATING_OPERATIONS } from "./GhidraSessionValues.js"; import { ghidraLoadImageVerificationNote } from "./GhidraVerifiedLoadImageFormats.js"; @@ -15,28 +13,12 @@ import { ghidraLoadImageVerificationNote } from "./GhidraVerifiedLoadImageFormat export const WINDOWS_P0_NATIVE_PE_ARCHITECTURES: readonly string[] = Object.freeze(["x86", "x86_64", "arm", "arm64"]); -/** Public identity committed by every Ghidra-backed observation. */ -export const GHIDRA_PROVIDER_IDENTITY: ProviderIdentity = Object.freeze({ - id: "ghidra", - name: "Ghidra", - version: null, -}); - /** Provider observations, including internal aggregates consumed by workflows. */ const providerOperations = Object.freeze([ ...GHIDRA_INVENTORY_OPERATIONS, ...GHIDRA_FUNCTION_OPERATIONS, ]); -/** Public analyst tools backed by these observations; aggregates stay internal. */ -export const GHIDRA_OPERATIONS = Object.freeze( - providerOperations.map((operation) => - operation === "read_binary_inventory_summary" - ? "binary_overview" - : operation, - ), -); - /** Health limitations shared by every Ghidra-backed capability. */ export const healthLimitations = Object.freeze([ "The session serves operations only after Ghidra reports default auto-analysis complete; incomplete analysis does not expose partial results.", diff --git a/src/ghidra/GhidraProviderClient.ts b/src/ghidra/GhidraProviderClient.ts index 90d069c743..dc8f3e5876 100644 --- a/src/ghidra/GhidraProviderClient.ts +++ b/src/ghidra/GhidraProviderClient.ts @@ -101,11 +101,11 @@ import { attestGhidraNativeLoadImage } from "./GhidraLoadImageAttest.js"; import type { NativeAotPeResult } from "../domain/native/nativeAotPe.js"; import { GhidraNativeAotSnapshotOwner } from "./GhidraNativeAotSnapshotOwner.js"; import { - GHIDRA_PROVIDER_IDENTITY, ghidraHealthLimitations, windowsP0Limitations, limitationsFor, } from "./GhidraProviderCapabilities.js"; +import { GHIDRA_PROVIDER_IDENTITY } from "./GhidraProviderMetadata.js"; import { GHIDRA_STARTUP_DEADLINE_ELAPSED, isGhidraInputFailureCode, diff --git a/src/ghidra/GhidraProviderMetadata.ts b/src/ghidra/GhidraProviderMetadata.ts new file mode 100644 index 0000000000..f8bc9b8b73 --- /dev/null +++ b/src/ghidra/GhidraProviderMetadata.ts @@ -0,0 +1,20 @@ +import type { ProviderIdentity } from "../application/AnalysisProvider.js"; +import { GHIDRA_FUNCTION_OPERATIONS } from "./GhidraFunctionOperations.js"; +import { GHIDRA_INVENTORY_OPERATIONS } from "./GhidraInventoryOperations.js"; + +/** Public identity committed by every Ghidra-backed observation. */ +export const GHIDRA_PROVIDER_IDENTITY: ProviderIdentity = Object.freeze({ + id: "ghidra", + name: "Ghidra", + version: null, +}); + +/** Public analyst tools backed by these observations; aggregates stay internal. */ +export const GHIDRA_OPERATIONS = Object.freeze( + [...GHIDRA_INVENTORY_OPERATIONS, ...GHIDRA_FUNCTION_OPERATIONS].map( + (operation) => + operation === "read_binary_inventory_summary" + ? "binary_overview" + : operation, + ), +); diff --git a/src/ghidra/GhidraSessionValues.ts b/src/ghidra/GhidraSessionValues.ts index 917be2e46e..f0b7c031ac 100644 --- a/src/ghidra/GhidraSessionValues.ts +++ b/src/ghidra/GhidraSessionValues.ts @@ -5,11 +5,9 @@ import { z } from "zod"; import type { JsonValue } from "../domain/jsonValue.js"; import { err, ok, type Result } from "../domain/result.js"; -import { - GHIDRA_INVENTORY_OPERATIONS, - GHIDRA_NAMING_OPERATIONS, -} from "./GhidraInventoryValues.js"; -import { GHIDRA_FUNCTION_OPERATIONS } from "./GhidraFunctionValues.js"; +import { GHIDRA_NAMING_OPERATIONS } from "./GhidraInventoryValues.js"; +import { GHIDRA_INVENTORY_OPERATIONS } from "./GhidraInventoryOperations.js"; +import { GHIDRA_FUNCTION_OPERATIONS } from "./GhidraFunctionOperations.js"; import { ghidraExtensionResultSchema } from "./extensions/GhidraExtensions.js"; import { GHIDRA_MIPS_SOFT_FLOAT_STORAGE } from "./GhidraMipsProfile.js"; import { GHIDRA_RISCV_CALLING_CONVENTION_IDS } from "./GhidraRiscvProfile.js"; diff --git a/src/go/GoBinaryProvider.ts b/src/go/GoBinaryProvider.ts index 54b12fe726..5f59bf7f75 100644 --- a/src/go/GoBinaryProvider.ts +++ b/src/go/GoBinaryProvider.ts @@ -1,3 +1,4 @@ +import { GO_BINARY_PROVIDER_IDENTITY } from "./GoBinaryProviderIdentity.js"; import { projectStableArtifactFailure, projectArtifactCloseFailure, @@ -39,12 +40,6 @@ import { BinaryResourceFailure, } from "../native/metadata/BinaryContainer.js"; -/** Bundled format reader identity; embedded compiler versions are separate observed facts. */ -export const GO_BINARY_PROVIDER_IDENTITY = { - id: "go-build-info", - name: "REA Go build-information reader", - version: "1", -} as const; const OPERATION = "inspect_go_binary"; const INPUT_BYTES = 256 * 1024 * 1024; const OUTPUT_BYTES = 8 * 1024 * 1024; diff --git a/src/go/GoBinaryProviderIdentity.ts b/src/go/GoBinaryProviderIdentity.ts new file mode 100644 index 0000000000..979e183235 --- /dev/null +++ b/src/go/GoBinaryProviderIdentity.ts @@ -0,0 +1,6 @@ +/** Bundled format reader identity; embedded compiler versions are separate observed facts. */ +export const GO_BINARY_PROVIDER_IDENTITY = { + id: "go-build-info", + name: "REA Go build-information reader", + version: "1", +} as const; diff --git a/src/gpu/GpuCodeObjectsProvider.ts b/src/gpu/GpuCodeObjectsProvider.ts index 78c87e72d1..3b02ba7e63 100644 --- a/src/gpu/GpuCodeObjectsProvider.ts +++ b/src/gpu/GpuCodeObjectsProvider.ts @@ -1,3 +1,4 @@ +import { GPU_CODE_OBJECTS_PROVIDER_IDENTITY } from "./GpuCodeObjectsProviderIdentity.js"; import { lstat, type FileHandle } from "node:fs/promises"; import type { GpuCodeObjectsPort } from "../application/gpu/GpuCodeObjectsPort.js"; import { @@ -51,11 +52,6 @@ import { sameRegularFileState, } from "../filesystem/RegularFile.js"; -export const GPU_CODE_OBJECTS_PROVIDER_IDENTITY = { - id: "gpu-code-objects", - name: "REA static GPU code-object reader", - version: "1", -} as const; const OPERATION = "inspect_gpu_code_objects"; /** One admitted descriptor and bounded report at a time; retained cleanup blocks new work. */ diff --git a/src/gpu/GpuCodeObjectsProviderIdentity.ts b/src/gpu/GpuCodeObjectsProviderIdentity.ts new file mode 100644 index 0000000000..31aff05327 --- /dev/null +++ b/src/gpu/GpuCodeObjectsProviderIdentity.ts @@ -0,0 +1,6 @@ +/** Source-owned static provider identity and public operation names. */ +export const GPU_CODE_OBJECTS_PROVIDER_IDENTITY = { + id: "gpu-code-objects", + name: "REA static GPU code-object reader", + version: "1", +} as const; diff --git a/src/hopper/HopperProvider.ts b/src/hopper/HopperProvider.ts index c61e1d0c7e..15f44dc88a 100644 --- a/src/hopper/HopperProvider.ts +++ b/src/hopper/HopperProvider.ts @@ -50,10 +50,8 @@ import { } from "./HopperCallerReconciliation.js"; import { analysisClientFailureForOperation } from "../providers/AnalysisClientFailure.js"; -import { - hopperCapabilities, - HOPPER_PROVIDER_IDENTITY, -} from "./HopperProviderCapabilities.js"; +import { hopperCapabilities } from "./HopperProviderCapabilities.js"; +import { HOPPER_PROVIDER_IDENTITY } from "./HopperProviderMetadata.js"; /** Concrete analysis provider backed by REA's private Hopper bridge. */ export class HopperProvider implements AnalysisProviderCandidate { diff --git a/src/hopper/HopperProviderCapabilities.ts b/src/hopper/HopperProviderCapabilities.ts index 487a0b1733..d9c893b1ac 100644 --- a/src/hopper/HopperProviderCapabilities.ts +++ b/src/hopper/HopperProviderCapabilities.ts @@ -1,3 +1,7 @@ +import { + HOPPER_PROVIDER_IDENTITY, + HOPPER_OPERATIONS, +} from "./HopperProviderMetadata.js"; import { DEFAULT_REGEX_MATCHING_DEADLINE, type RegexMatchingDeadline, @@ -7,58 +11,7 @@ import { type RegexWorkerHeapRequest, } from "../config/regexWorkerHeapRequest.js"; import { launchRegexMatchingLimitation } from "../process/RegexMatcher.js"; -import type { - CapabilityDescriptor, - ProviderIdentity, -} from "../application/AnalysisProvider.js"; -import type { OfficialToolName } from "../contracts/officialToolContracts.js"; - -/** Public identity committed by every Hopper-backed observation. */ -export const HOPPER_PROVIDER_IDENTITY: ProviderIdentity = Object.freeze({ - id: "hopper", - name: "Hopper Disassembler", - version: null, -}); - -/** Analyst operations implemented by REA's private Hopper bridge. */ -export const HOPPER_OPERATIONS = Object.freeze([ - "address_name", - "comment", - "current_address", - "current_procedure", - "current_document", - "goto_address", - "inline_comment", - "list_bookmarks", - "list_documents", - "list_names", - "list_procedures", - "list_segments", - "list_strings", - "next_address", - "prev_address", - "procedure_address", - "procedure_assembly", - "procedure_callees", - "procedure_callers", - "procedure_info", - "read_function_instructions", - "read_bytes", - "address_to_file_offset", - "procedure_references", - "procedure_pseudo_code", - "resolve_containing_procedure", - "search_procedures", - "search_strings", - "set_address_name", - "set_addresses_names", - "set_bookmark", - "set_comment", - "set_inline_comment", - "unset_bookmark", - "xrefs", - "analyze_function", -] as const satisfies readonly (OfficialToolName | "analyze_function")[]); +import type { CapabilityDescriptor } from "../application/AnalysisProvider.js"; const MUTATING_OPERATIONS = new Set([ "set_address_name", diff --git a/src/hopper/HopperProviderMetadata.ts b/src/hopper/HopperProviderMetadata.ts new file mode 100644 index 0000000000..0d2f6f8a5b --- /dev/null +++ b/src/hopper/HopperProviderMetadata.ts @@ -0,0 +1,49 @@ +import type { ProviderIdentity } from "../application/AnalysisProvider.js"; +import type { OfficialToolName } from "../contracts/officialToolContracts.js"; + +/** Public identity committed by every Hopper-backed observation. */ +export const HOPPER_PROVIDER_IDENTITY: ProviderIdentity = Object.freeze({ + id: "hopper", + name: "Hopper Disassembler", + version: null, +}); + +/** Analyst operations implemented by REA's private Hopper bridge. */ +export const HOPPER_OPERATIONS = Object.freeze([ + "address_name", + "comment", + "current_address", + "current_procedure", + "current_document", + "goto_address", + "inline_comment", + "list_bookmarks", + "list_documents", + "list_names", + "list_procedures", + "list_segments", + "list_strings", + "next_address", + "prev_address", + "procedure_address", + "procedure_assembly", + "procedure_callees", + "procedure_callers", + "procedure_info", + "read_function_instructions", + "read_bytes", + "address_to_file_offset", + "procedure_references", + "procedure_pseudo_code", + "resolve_containing_procedure", + "search_procedures", + "search_strings", + "set_address_name", + "set_addresses_names", + "set_bookmark", + "set_comment", + "set_inline_comment", + "unset_bookmark", + "xrefs", + "analyze_function", +] as const satisfies readonly (OfficialToolName | "analyze_function")[]); diff --git a/src/ida/IdaOperationFailures.test.ts b/src/ida/IdaOperationFailures.test.ts index 45e89f8eab..350af553cd 100644 --- a/src/ida/IdaOperationFailures.test.ts +++ b/src/ida/IdaOperationFailures.test.ts @@ -12,7 +12,7 @@ import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; import { AnalysisInputError } from "../domain/analysisErrorCore.js"; import { createIdaDiagnosticRedactor } from "./IdaDiagnosticRedactor.js"; import { decodeIdaToolResult } from "./IdaMcpConnection.js"; -import { IDA_PROVIDER_IDENTITY } from "./IdaProviderCapabilities.js"; +import { IDA_PROVIDER_IDENTITY } from "./IdaProviderMetadata.js"; import type { AnalysisOperation } from "../application/AnalysisProvider.js"; import { IdaSessionClient } from "./IdaSessionClient.js"; diff --git a/src/ida/IdaProvider.ts b/src/ida/IdaProvider.ts index 47730e621a..9dd0594d6f 100644 --- a/src/ida/IdaProvider.ts +++ b/src/ida/IdaProvider.ts @@ -42,10 +42,8 @@ import { createIdaMcpConnection, type IdaMcpConnection, } from "./IdaMcpConnection.js"; -import { - idaCapabilities, - IDA_PROVIDER_IDENTITY, -} from "./IdaProviderCapabilities.js"; +import { idaCapabilities } from "./IdaProviderCapabilities.js"; +import { IDA_PROVIDER_IDENTITY } from "./IdaProviderMetadata.js"; import { IdaSessionClient } from "./IdaSessionClient.js"; import { inspectIdaWorkspaceReadiness } from "./IdaWorkspaceReadiness.js"; import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; diff --git a/src/ida/IdaProviderCapabilities.test.ts b/src/ida/IdaProviderCapabilities.test.ts index 607f051543..f533785bf1 100644 --- a/src/ida/IdaProviderCapabilities.test.ts +++ b/src/ida/IdaProviderCapabilities.test.ts @@ -1,10 +1,10 @@ import { describe, expect, it } from "vitest"; import { IDA_COMPATIBILITY_PROFILES } from "./IdaConfiguration.js"; import { - IDA_OPERATIONS, idaCapabilities, idaOperationUnavailableReason, } from "./IdaProviderCapabilities.js"; +import { IDA_OPERATIONS } from "./IdaProviderMetadata.js"; describe("IDA profile operation admission", () => { for (const [name, profile] of Object.entries(IDA_COMPATIBILITY_PROFILES)) { diff --git a/src/ida/IdaProviderCapabilities.ts b/src/ida/IdaProviderCapabilities.ts index 7fd61be3c2..f999817d24 100644 --- a/src/ida/IdaProviderCapabilities.ts +++ b/src/ida/IdaProviderCapabilities.ts @@ -1,34 +1,11 @@ +import { + IDA_PROVIDER_IDENTITY, + IDA_OPERATIONS, +} from "./IdaProviderMetadata.js"; import type { IdaCompatibilityProfile } from "./IdaConfiguration.js"; -import type { - AnalysisOperation, - CapabilityDescriptor, -} from "../application/AnalysisProvider.js"; +import type { CapabilityDescriptor } from "../application/AnalysisProvider.js"; import { REGEX_MATCHING_LIMITATION } from "../process/RegexMatcher.js"; -/** This version identifies REA's adapter, not IDA or the upstream MCP server. */ -export const IDA_PROVIDER_IDENTITY = Object.freeze({ - id: "ida", - name: "IDA MCP adapter", - version: "1", -}); - -/** Read-only analyst outcomes supported by the upstream compatibility profiles. */ -export const IDA_OPERATIONS = Object.freeze([ - "list_procedures", - "search_procedures", - "list_strings", - "search_strings", - "procedure_address", - "procedure_pseudo_code", - "procedure_assembly", - "procedure_callers", - "procedure_callees", - "read_function_instructions", - "xrefs", - "analyze_function", - "resolve_containing_procedure", - "list_documents", -] as const satisfies readonly Exclude[]); export type IdaOperation = (typeof IDA_OPERATIONS)[number]; /** Operations only the modern headless profile implements. */ diff --git a/src/ida/IdaProviderMetadata.ts b/src/ida/IdaProviderMetadata.ts new file mode 100644 index 0000000000..874ab45f86 --- /dev/null +++ b/src/ida/IdaProviderMetadata.ts @@ -0,0 +1,26 @@ +import type { AnalysisOperation } from "../application/AnalysisProvider.js"; + +/** This version identifies REA's adapter, not IDA or the upstream MCP server. */ +export const IDA_PROVIDER_IDENTITY = Object.freeze({ + id: "ida", + name: "IDA MCP adapter", + version: "1", +}); + +/** Read-only analyst outcomes supported by the upstream compatibility profiles. */ +export const IDA_OPERATIONS = Object.freeze([ + "list_procedures", + "search_procedures", + "list_strings", + "search_strings", + "procedure_address", + "procedure_pseudo_code", + "procedure_assembly", + "procedure_callers", + "procedure_callees", + "read_function_instructions", + "xrefs", + "analyze_function", + "resolve_containing_procedure", + "list_documents", +] as const satisfies readonly Exclude[]); diff --git a/src/ida/IdaSessionClient.ts b/src/ida/IdaSessionClient.ts index c0740155ea..8b488c4fee 100644 --- a/src/ida/IdaSessionClient.ts +++ b/src/ida/IdaSessionClient.ts @@ -32,11 +32,13 @@ import { isX86OnlyTarget } from "./IdaCallGraph.js"; import { IdaOperationRunner } from "./IdaOperationRunner.js"; import { IDA_LIMITATIONS, - IDA_OPERATIONS, - IDA_PROVIDER_IDENTITY, idaOperationUnavailableReason, type IdaOperation, } from "./IdaProviderCapabilities.js"; +import { + IDA_OPERATIONS, + IDA_PROVIDER_IDENTITY, +} from "./IdaProviderMetadata.js"; import { absentDatabaseCloseSchema, closeDatabaseSchema, diff --git a/src/il2cpp/Il2CppMetadataProviderIdentity.ts b/src/il2cpp/Il2CppMetadataProviderIdentity.ts new file mode 100644 index 0000000000..5bf016ad97 --- /dev/null +++ b/src/il2cpp/Il2CppMetadataProviderIdentity.ts @@ -0,0 +1,6 @@ +/** Bundled format reader identity; the metadata version is a separate observed fact. */ +export const IL2CPP_METADATA_PROVIDER_IDENTITY = { + id: "il2cpp-metadata", + name: "REA IL2CPP global-metadata reader", + version: "1", +} as const; diff --git a/src/il2cpp/Il2cppMetadataProvider.ts b/src/il2cpp/Il2cppMetadataProvider.ts index ec20e8cf85..0cdd5b8e2e 100644 --- a/src/il2cpp/Il2cppMetadataProvider.ts +++ b/src/il2cpp/Il2cppMetadataProvider.ts @@ -1,3 +1,4 @@ +import { IL2CPP_METADATA_PROVIDER_IDENTITY } from "./Il2CppMetadataProviderIdentity.js"; import { isSelectedRegularFileError } from "../filesystem/RegularFile.js"; import { Il2cppBinaryFormatFailure } from "./Il2cppBinaryFormat.js"; import { Il2cppMetadataFormatFailure } from "./Il2cppMetadataFormat.js"; @@ -55,12 +56,6 @@ import { type Il2cppBinaryLink, } from "./Il2cppBinaryFile.js"; -/** Bundled format reader identity; the metadata version is a separate observed fact. */ -export const IL2CPP_METADATA_PROVIDER_IDENTITY = { - id: "il2cpp-metadata", - name: "REA IL2CPP global-metadata reader", - version: "1", -} as const; const OPERATION = "inspect_il2cpp_metadata"; const INPUT_BYTES = 256 * 1024 * 1024; // GameAssembly binaries of large games reach several hundred megabytes. diff --git a/src/javascript/recovery/WakaruExecution.ts b/src/javascript/recovery/WakaruExecution.ts index bf09845cd3..4e8a41c21c 100644 --- a/src/javascript/recovery/WakaruExecution.ts +++ b/src/javascript/recovery/WakaruExecution.ts @@ -25,7 +25,8 @@ import { } from "./WakaruCommand.js"; import { parseWakaruReports } from "./WakaruReport.js"; import { publishWakaruArtifacts } from "./WakaruPublication.js"; -import { RECOVERY_LIMITS, WAKARU_PROVIDER_IDENTITY } from "./WakaruRelease.js"; +import { RECOVERY_LIMITS } from "./WakaruRelease.js"; +import { WAKARU_PROVIDER_IDENTITY } from "./WakaruProviderIdentity.js"; import { admitWakaruVersion } from "./WakaruVersion.js"; const OPERATION = "recover_javascript_sources"; diff --git a/src/javascript/recovery/WakaruProviderIdentity.ts b/src/javascript/recovery/WakaruProviderIdentity.ts new file mode 100644 index 0000000000..51d718f9a2 --- /dev/null +++ b/src/javascript/recovery/WakaruProviderIdentity.ts @@ -0,0 +1,13 @@ +/** Unchanged upstream source pin; executed bytes are identified separately. */ +export const WAKARU_RELEASE = { + version: "1.14.0", + revision: "8219bf5016a063d7fb215101a8c45d5959ec733f", + repository: "https://github.com/pionxzh/wakaru", +} as const; + +/** Catalog identity for the verified Wakaru release. Evidence replaces version with the observed banner. */ +export const WAKARU_PROVIDER_IDENTITY = { + id: "wakaru", + name: "Wakaru", + version: WAKARU_RELEASE.version, +} as const; diff --git a/src/javascript/recovery/WakaruPublication.ts b/src/javascript/recovery/WakaruPublication.ts index f381118c86..23a5ddea57 100644 --- a/src/javascript/recovery/WakaruPublication.ts +++ b/src/javascript/recovery/WakaruPublication.ts @@ -18,11 +18,8 @@ import { validateRecoverySourceMap, type parseWakaruReports, } from "./WakaruReport.js"; -import { - RECOVERY_LIMITATIONS, - RECOVERY_LIMITS, - WAKARU_RELEASE, -} from "./WakaruRelease.js"; +import { RECOVERY_LIMITATIONS, RECOVERY_LIMITS } from "./WakaruRelease.js"; +import { WAKARU_RELEASE } from "./WakaruProviderIdentity.js"; import type { resolveWakaruCommand, runWakaruCommand, diff --git a/src/javascript/recovery/WakaruRelease.ts b/src/javascript/recovery/WakaruRelease.ts index 399769d5b0..9153b2d9a3 100644 --- a/src/javascript/recovery/WakaruRelease.ts +++ b/src/javascript/recovery/WakaruRelease.ts @@ -1,12 +1,5 @@ import { BOUNDED_PRODUCER_LIMITS } from "../../process/LinuxResourceLimits.js"; -/** Unchanged upstream source pin; executed bytes are identified separately. */ -export const WAKARU_RELEASE = { - version: "1.14.0", - revision: "8219bf5016a063d7fb215101a8c45d5959ec733f", - repository: "https://github.com/pionxzh/wakaru", -} as const; - /** * Releases the report parser accepts. Wakaru changes `--version`, `--json` * and `provenance.json` only by addition within a major version @@ -15,13 +8,6 @@ export const WAKARU_RELEASE = { */ export const WAKARU_ACCEPTED_RANGE = "^1.13.0"; -/** Catalog identity for the verified Wakaru release. Evidence replaces version with the observed banner. */ -export const WAKARU_PROVIDER_IDENTITY = { - id: "wakaru", - name: "Wakaru", - version: WAKARU_RELEASE.version, -} as const; - /** Fixed Wakaru allowance. This release has no caller setting that raises it. */ export const wakaruFixedLimitRecovery = ( allowance: string, diff --git a/src/javascript/recovery/WakaruVersion.test.ts b/src/javascript/recovery/WakaruVersion.test.ts index 939c2e088b..8305b483c6 100644 --- a/src/javascript/recovery/WakaruVersion.test.ts +++ b/src/javascript/recovery/WakaruVersion.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import { admitWakaruVersion } from "./WakaruVersion.js"; -import { WAKARU_RELEASE } from "./WakaruRelease.js"; +import { WAKARU_RELEASE } from "./WakaruProviderIdentity.js"; describe("Wakaru release range", () => { it("accepts the verified banner without an extra limitation", () => { diff --git a/src/javascript/recovery/WakaruVersion.ts b/src/javascript/recovery/WakaruVersion.ts index f4c2d92ae5..3b99ac7470 100644 --- a/src/javascript/recovery/WakaruVersion.ts +++ b/src/javascript/recovery/WakaruVersion.ts @@ -1,5 +1,6 @@ import { satisfies, valid } from "semver"; -import { WAKARU_ACCEPTED_RANGE, WAKARU_RELEASE } from "./WakaruRelease.js"; +import { WAKARU_ACCEPTED_RANGE } from "./WakaruRelease.js"; +import { WAKARU_RELEASE } from "./WakaruProviderIdentity.js"; /** Admission of one observed `wakaru --version` banner. */ export type WakaruVersionAdmission = diff --git a/src/jeb/JebProvider.ts b/src/jeb/JebProvider.ts index ab085d7a92..235609d258 100644 --- a/src/jeb/JebProvider.ts +++ b/src/jeb/JebProvider.ts @@ -1,3 +1,4 @@ +import { JEB_PROVIDER_IDENTITY } from "./JebProviderIdentity.js"; import { JebMcpAcquisitionCleanupFailure, JebMcpPhysicalFailure, @@ -41,12 +42,6 @@ import { const JEB_CONNECTION_REMEDIATION = `Start a JEB client serving MCP (default ${DEFAULT_JEB_MCP_ENDPOINT}) or set REA_JEB_MCP_URL to the running endpoint.`; -export const JEB_PROVIDER_IDENTITY: ProviderIdentity = { - id: "jeb", - name: "JEB", - version: null, -} as const; - export interface JebProviderOptions { /** Caller environment carrying REA_JEB_MCP_URL. */ readonly environment?: Readonly>; diff --git a/src/jeb/JebProviderIdentity.ts b/src/jeb/JebProviderIdentity.ts new file mode 100644 index 0000000000..3d75f7ca89 --- /dev/null +++ b/src/jeb/JebProviderIdentity.ts @@ -0,0 +1,8 @@ +import type { ProviderIdentity } from "../application/AnalysisProvider.js"; + +/** Source-owned static provider identity and public operation names. */ +export const JEB_PROVIDER_IDENTITY: ProviderIdentity = { + id: "jeb", + name: "JEB", + version: null, +} as const; diff --git a/src/kuna/KunaProvider.ts b/src/kuna/KunaProvider.ts index c1ade5afcd..2d8a408906 100644 --- a/src/kuna/KunaProvider.ts +++ b/src/kuna/KunaProvider.ts @@ -40,10 +40,8 @@ import { inspectKunaInstallation, } from "./KunaInstallation.js"; import { KunaProjectCache } from "./KunaProjectCache.js"; -import { - KUNA_PROVIDER_IDENTITY, - kunaCapabilities, -} from "./KunaProviderCapabilities.js"; +import { kunaCapabilities } from "./KunaProviderCapabilities.js"; +import { KUNA_PROVIDER_IDENTITY } from "./KunaProviderMetadata.js"; import { KUNA_COMMAND_REMEDIATION, KUNA_PROVIDER_ID } from "./KunaRelease.js"; import { KunaRunSlots } from "./KunaRunSlots.js"; import { KunaSessionClient } from "./KunaSessionClient.js"; diff --git a/src/kuna/KunaProviderCapabilities.ts b/src/kuna/KunaProviderCapabilities.ts index 425b5fdf6a..1b325265a2 100644 --- a/src/kuna/KunaProviderCapabilities.ts +++ b/src/kuna/KunaProviderCapabilities.ts @@ -1,9 +1,13 @@ +import { + KUNA_PROVIDER_IDENTITY, + KUNA_OPERATIONS, +} from "./KunaProviderMetadata.js"; import type { AnalysisOperation, CapabilityDescriptor, } from "../application/AnalysisProvider.js"; import type { ProviderRejectionCode } from "../domain/providerSelection.js"; -import { KUNA_PROVIDER_ID } from "./KunaRelease.js"; + import { DEFAULT_REGEX_MATCHING_DEADLINE, type RegexMatchingDeadline, @@ -15,29 +19,6 @@ import { import { launchRegexMatchingLimitation } from "../process/RegexMatcher.js"; import { KUNA_FUNCTION_TEXT_BYTES } from "./KunaLimits.js"; -/** Static identity; the observed Kuna release is reported in each analysis profile. */ -export const KUNA_PROVIDER_IDENTITY = Object.freeze({ - id: KUNA_PROVIDER_ID, - name: "Kuna", - version: null, -}); - -/** Analyst outcomes Kuna's inventory and project export can answer. */ -export const KUNA_OPERATIONS = Object.freeze([ - "list_documents", - "list_procedures", - "search_procedures", - "list_strings", - "search_strings", - "procedure_address", - "address_name", - "procedure_pseudo_code", - "procedure_assembly", - "analyze_function", - "procedure_callers", - "procedure_callees", - "xrefs", -] as const satisfies readonly Exclude[]); export type KunaOperation = (typeof KUNA_OPERATIONS)[number]; export const KUNA_RELATIONSHIP_LIMITATION = diff --git a/src/kuna/KunaProviderMetadata.ts b/src/kuna/KunaProviderMetadata.ts new file mode 100644 index 0000000000..54b38571b7 --- /dev/null +++ b/src/kuna/KunaProviderMetadata.ts @@ -0,0 +1,26 @@ +import type { AnalysisOperation } from "../application/AnalysisProvider.js"; +import { KUNA_PROVIDER_ID } from "./KunaRelease.js"; + +/** Static identity; the observed Kuna release is reported in each analysis profile. */ +export const KUNA_PROVIDER_IDENTITY = Object.freeze({ + id: KUNA_PROVIDER_ID, + name: "Kuna", + version: null, +}); + +/** Analyst outcomes Kuna's inventory and project export can answer. */ +export const KUNA_OPERATIONS = Object.freeze([ + "list_documents", + "list_procedures", + "search_procedures", + "list_strings", + "search_strings", + "procedure_address", + "address_name", + "procedure_pseudo_code", + "procedure_assembly", + "analyze_function", + "procedure_callers", + "procedure_callees", + "xrefs", +] as const satisfies readonly Exclude[]); diff --git a/src/main/transport.ts b/src/main/transport.ts index 290bd9b7ad..8090ae613a 100644 --- a/src/main/transport.ts +++ b/src/main/transport.ts @@ -134,6 +134,11 @@ export const startMcpTransport = async ( serverContext.analysisSettings?.read, ); androidProviders.add(android); + const closeAndroidProvider = android.close.bind(android); + android.close = async () => { + await closeAndroidProvider(); + androidProviders.delete(android); + }; const server = (dependencies.createServer ?? createServer)( { kind: "session", session }, { diff --git a/src/model/ModelArtifactProvider.ts b/src/model/ModelArtifactProvider.ts index a696bab531..fe8d0c7d56 100644 --- a/src/model/ModelArtifactProvider.ts +++ b/src/model/ModelArtifactProvider.ts @@ -1,3 +1,4 @@ +import { MODEL_ARTIFACT_PROVIDER_IDENTITY } from "./ModelArtifactProviderIdentity.js"; import { lstat, type FileHandle } from "node:fs/promises"; import type { ModelArtifactPort } from "../application/model/ModelArtifactPort.js"; import { @@ -51,11 +52,6 @@ import { sameRegularFileState, } from "../filesystem/RegularFile.js"; -export const MODEL_ARTIFACT_PROVIDER_IDENTITY = { - id: "model-artifact", - name: "REA model header reader", - version: "1", -} as const; const OPERATION = "inspect_model_artifact"; /** One admitted descriptor and bounded report at a time; retained cleanup blocks new work. */ export class ModelArtifactProvider implements ModelArtifactPort { diff --git a/src/model/ModelArtifactProviderIdentity.ts b/src/model/ModelArtifactProviderIdentity.ts new file mode 100644 index 0000000000..d2d0ee530a --- /dev/null +++ b/src/model/ModelArtifactProviderIdentity.ts @@ -0,0 +1,6 @@ +/** Source-owned static provider identity and public operation names. */ +export const MODEL_ARTIFACT_PROVIDER_IDENTITY = { + id: "model-artifact", + name: "REA model header reader", + version: "1", +} as const; diff --git a/src/native/AppleDispatchMetadata.ts b/src/native/AppleDispatchMetadata.ts index f064327444..405ccaf7e8 100644 --- a/src/native/AppleDispatchMetadata.ts +++ b/src/native/AppleDispatchMetadata.ts @@ -34,10 +34,8 @@ import { AnalysisError } from "../domain/analysisErrorBase.js"; import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; import { ProviderAdapterError } from "../domain/providerAdapterError.js"; import { ProviderCleanupError } from "../domain/providerCleanupError.js"; -import { - APPLE_DISPATCH_METADATA_ANALYSIS_PROFILE, - NATIVE_MACOS_PROVIDER_IDENTITY as IDENTITY, -} from "./NativeMacOSProviderMetadata.js"; +import { APPLE_DISPATCH_METADATA_ANALYSIS_PROFILE } from "./NativeMacOSProviderMetadata.js"; +import { NATIVE_MACOS_PROVIDER_IDENTITY as IDENTITY } from "./NativeMacosProviderIdentity.js"; import { readBoundedFileBytes } from "../process/BoundedFileBytes.js"; import { EvidenceIntegrityError } from "../domain/evidenceErrors.js"; import { pushDispatchCoverage } from "./AppleDispatchCoverage.js"; diff --git a/src/native/NativeCallTarget.ts b/src/native/NativeCallTarget.ts index e55fd669fa..9f85c0731f 100644 --- a/src/native/NativeCallTarget.ts +++ b/src/native/NativeCallTarget.ts @@ -1,4 +1,4 @@ -import { NATIVE_MACOS_PROVIDER_IDENTITY as IDENTITY } from "./NativeMacOSProviderMetadata.js"; +import { NATIVE_MACOS_PROVIDER_IDENTITY as IDENTITY } from "./NativeMacosProviderIdentity.js"; import type { BinaryTarget } from "../domain/binaryTargetTypes.js"; import type { AnalysisError } from "../domain/analysisErrorBase.js"; import { AnalysisCancelledError } from "../domain/analysisErrorCore.js"; diff --git a/src/native/NativeCommandAnalysisError.ts b/src/native/NativeCommandAnalysisError.ts index a6ff201378..69fd40efad 100644 --- a/src/native/NativeCommandAnalysisError.ts +++ b/src/native/NativeCommandAnalysisError.ts @@ -6,7 +6,7 @@ import { AnalysisTimeoutError, } from "../domain/analysisErrorCore.js"; import { ProviderAdapterError } from "../domain/providerAdapterError.js"; -import { NATIVE_MACOS_PROVIDER_IDENTITY as IDENTITY } from "./NativeMacOSProviderMetadata.js"; +import { NATIVE_MACOS_PROVIDER_IDENTITY as IDENTITY } from "./NativeMacosProviderIdentity.js"; import { DEFAULT_NATIVE_COMMAND_TIMEOUT_MS, fixedNativeAllowance, diff --git a/src/native/NativeMacOSProvider.ts b/src/native/NativeMacOSProvider.ts index 17533d8955..9269614512 100644 --- a/src/native/NativeMacOSProvider.ts +++ b/src/native/NativeMacOSProvider.ts @@ -23,9 +23,9 @@ import { readMachoEncryption } from "../artifacts/apple/MachoEncryptionReader.js import { snapshotEnvironment } from "../process/snapshotEnvironment.js"; import { APPLE_DISPATCH_METADATA_ANALYSIS_PROFILE, - NATIVE_MACOS_PROVIDER_IDENTITY as IDENTITY, nativeMacOSCapabilities, } from "./NativeMacOSProviderMetadata.js"; +import { NATIVE_MACOS_PROVIDER_IDENTITY as IDENTITY } from "./NativeMacosProviderIdentity.js"; import { appleDispatchMetadataFailure, inspectAppleDispatchMetadata, diff --git a/src/native/NativeMacOSProviderMetadata.ts b/src/native/NativeMacOSProviderMetadata.ts index 2722e6c795..6c125f25f5 100644 --- a/src/native/NativeMacOSProviderMetadata.ts +++ b/src/native/NativeMacOSProviderMetadata.ts @@ -1,16 +1,8 @@ -import type { - CapabilityDescriptor, - ProviderIdentity, -} from "../application/AnalysisProvider.js"; +import { NATIVE_MACOS_PROVIDER_IDENTITY } from "./NativeMacosProviderIdentity.js"; +import type { CapabilityDescriptor } from "../application/AnalysisProvider.js"; import { createAnalysisProfile } from "../domain/analysisProfile.js"; import { NATIVE_TOOL_CONTRACTS } from "../contracts/native/nativeToolContracts.js"; -/** Public identity committed by macOS-native inspection observations. */ -export const NATIVE_MACOS_PROVIDER_IDENTITY: ProviderIdentity = Object.freeze({ - id: "native-macos", - name: "macOS native inspection utilities", - version: null, -}); /** Source-owned commitment for the pure Apple dispatch metadata reader. */ export const APPLE_DISPATCH_METADATA_ANALYSIS_PROFILE = createAnalysisProfile( { diff --git a/src/native/NativeMacosProviderIdentity.ts b/src/native/NativeMacosProviderIdentity.ts new file mode 100644 index 0000000000..317d20c684 --- /dev/null +++ b/src/native/NativeMacosProviderIdentity.ts @@ -0,0 +1,8 @@ +import type { ProviderIdentity } from "../application/AnalysisProvider.js"; + +/** Public identity committed by macOS-native inspection observations. */ +export const NATIVE_MACOS_PROVIDER_IDENTITY: ProviderIdentity = Object.freeze({ + id: "native-macos", + name: "macOS native inspection utilities", + version: null, +}); diff --git a/src/native/NativePlistSource.ts b/src/native/NativePlistSource.ts index fbdb628a1a..83f0a1ab7a 100644 --- a/src/native/NativePlistSource.ts +++ b/src/native/NativePlistSource.ts @@ -36,7 +36,7 @@ import { withArtifactRegularFile, } from "../artifacts/ArtifactFileOperation.js"; import type { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; -import { NATIVE_MACOS_PROVIDER_IDENTITY as IDENTITY } from "./NativeMacOSProviderMetadata.js"; +import { NATIVE_MACOS_PROVIDER_IDENTITY as IDENTITY } from "./NativeMacosProviderIdentity.js"; import { MAX_NATIVE_PLIST_BYTES, NATIVE_PLIST_INPUT_ALLOWANCE, diff --git a/src/native/SignatureTargetBinding.ts b/src/native/SignatureTargetBinding.ts index 8d7b4d6335..ca26b1dd63 100644 --- a/src/native/SignatureTargetBinding.ts +++ b/src/native/SignatureTargetBinding.ts @@ -31,7 +31,7 @@ import { import { bindArtifactFileCleanup } from "../artifacts/ArtifactFileOperation.js"; import type { ArtifactResourceOwner } from "../artifacts/ArtifactResourceScope.js"; -import { NATIVE_MACOS_PROVIDER_IDENTITY } from "./NativeMacOSProviderMetadata.js"; +import { NATIVE_MACOS_PROVIDER_IDENTITY } from "./NativeMacosProviderIdentity.js"; type SignatureOwner = Extract< ArtifactResourceOwner, diff --git a/src/native/pwntools/PwntoolsDecoder.ts b/src/native/pwntools/PwntoolsDecoder.ts index ae3342e5fe..60b8bc967a 100644 --- a/src/native/pwntools/PwntoolsDecoder.ts +++ b/src/native/pwntools/PwntoolsDecoder.ts @@ -65,10 +65,8 @@ import { readPwntoolsFailureEvidence, } from "./PwntoolsResourceLimits.js"; import { PrivateRuntimeRoot } from "../../process/PrivateRuntimeRoot.js"; -import { - PWNTOOLS_PROVIDER_IDENTITY, - PWNTOOLS_LIMITS, -} from "./PwntoolsRelease.js"; +import { PWNTOOLS_LIMITS } from "./PwntoolsRelease.js"; +import { PWNTOOLS_PROVIDER_IDENTITY } from "./PwntoolsProviderIdentity.js"; import { pwntoolsInputBytes } from "./PwntoolsInputLimit.js"; const HOST_REQUIREMENT = diff --git a/src/native/pwntools/PwntoolsFailures.ts b/src/native/pwntools/PwntoolsFailures.ts index 4529f82e3e..10da1660c5 100644 --- a/src/native/pwntools/PwntoolsFailures.ts +++ b/src/native/pwntools/PwntoolsFailures.ts @@ -25,11 +25,11 @@ import { OwnedCommandFailure } from "../../process/OwnedCommand.js"; import type { PwntoolsFailureEvidence } from "./PwntoolsResourceLimits.js"; import { nativeDeadlineRecovery } from "../../domain/native/nativeDeadlines.js"; import { - PWNTOOLS_PROVIDER_IDENTITY, PWNTOOLS_LIMITS, PWNTOOLS_MEMORY_FAILURE_EXIT, PWNTOOLS_FILE_SIZE_FAILURE_EXIT, } from "./PwntoolsRelease.js"; +import { PWNTOOLS_PROVIDER_IDENTITY } from "./PwntoolsProviderIdentity.js"; /** Operation identity supplied by each adapter sharing the pwntools boundary. */ export interface PwntoolsFailureContext { diff --git a/src/native/pwntools/PwntoolsInputLimit.test.ts b/src/native/pwntools/PwntoolsInputLimit.test.ts index d60c27b801..2595c03c50 100644 --- a/src/native/pwntools/PwntoolsInputLimit.test.ts +++ b/src/native/pwntools/PwntoolsInputLimit.test.ts @@ -2,10 +2,8 @@ import { describe, expect, it } from "vitest"; import { pwntoolsInputBytes } from "./PwntoolsInputLimit.js"; import { pwntoolsUnavailable } from "./PwntoolsFailures.js"; -import { - PWNTOOLS_LIMITS, - PWNTOOLS_PROVIDER_IDENTITY, -} from "./PwntoolsRelease.js"; +import { PWNTOOLS_LIMITS } from "./PwntoolsRelease.js"; +import { PWNTOOLS_PROVIDER_IDENTITY } from "./PwntoolsProviderIdentity.js"; const CONSTRAINT = "REA_PWNTOOLS_MAX_INPUT_BYTES must be a decimal integer from 1 to " + diff --git a/src/native/pwntools/PwntoolsLayoutProvider.ts b/src/native/pwntools/PwntoolsLayoutProvider.ts index f9346e397b..6e42ea1b42 100644 --- a/src/native/pwntools/PwntoolsLayoutProvider.ts +++ b/src/native/pwntools/PwntoolsLayoutProvider.ts @@ -6,7 +6,7 @@ import { } from "../../domain/native/binaryLayout.js"; import { AnalysisOutputError } from "../../domain/analysisErrorCore.js"; import { PwntoolsDecoder, type PwntoolsLauncher } from "./PwntoolsDecoder.js"; -import { PWNTOOLS_PROVIDER_IDENTITY } from "./PwntoolsRelease.js"; +import { PWNTOOLS_PROVIDER_IDENTITY } from "./PwntoolsProviderIdentity.js"; import { PrivateRuntimeRoot } from "../../process/PrivateRuntimeRoot.js"; import type { BinaryLayoutPort } from "../../application/binaryDiagnostics/BinaryLayoutPort.js"; import type { z } from "zod"; diff --git a/src/native/pwntools/PwntoolsProviderIdentity.ts b/src/native/pwntools/PwntoolsProviderIdentity.ts new file mode 100644 index 0000000000..6cbb90a329 --- /dev/null +++ b/src/native/pwntools/PwntoolsProviderIdentity.ts @@ -0,0 +1,6 @@ +/** Verified unchanged upstream profile for the Linux ELF adapter. */ +export const PWNTOOLS_PROVIDER_IDENTITY = { + id: "pwntools-elf", + name: "REA pwntools ELF adapter", + version: "pwntools@4.15.0;pyelftools@0.33;unicorn@2.1.2", +} as const; diff --git a/src/native/pwntools/PwntoolsRecordedCrashProvider.ts b/src/native/pwntools/PwntoolsRecordedCrashProvider.ts index 12d8f6408e..92fd9c1bc4 100644 --- a/src/native/pwntools/PwntoolsRecordedCrashProvider.ts +++ b/src/native/pwntools/PwntoolsRecordedCrashProvider.ts @@ -13,7 +13,7 @@ import { import type { RecordedCrashPort } from "../../application/binaryDiagnostics/RecordedCrashPort.js"; import { PrivateRuntimeRoot } from "../../process/PrivateRuntimeRoot.js"; import { PwntoolsDecoder, type PwntoolsLauncher } from "./PwntoolsDecoder.js"; -import { PWNTOOLS_PROVIDER_IDENTITY } from "./PwntoolsRelease.js"; +import { PWNTOOLS_PROVIDER_IDENTITY } from "./PwntoolsProviderIdentity.js"; import { validateCoreFileMappingAuthority } from "../pwndbg/CoreFileMappingAuthority.js"; import { validateRecordedCrashSources } from "./RecordedCrashSourceBindings.js"; import { validateRecordedCrashStructure } from "./RecordedCrashStructureBindings.js"; diff --git a/src/native/pwntools/PwntoolsRelease.ts b/src/native/pwntools/PwntoolsRelease.ts index 67bd73bd2b..b52baeb367 100644 --- a/src/native/pwntools/PwntoolsRelease.ts +++ b/src/native/pwntools/PwntoolsRelease.ts @@ -1,11 +1,5 @@ import { DEFAULT_PWNTOOLS_TIMEOUT_MS } from "../../domain/native/nativeDeadlines.js"; -/** Verified unchanged upstream profile for the Linux ELF adapter. */ -export const PWNTOOLS_PROVIDER_IDENTITY = { - id: "pwntools-elf", - name: "REA pwntools ELF adapter", - version: "pwntools@4.15.0;pyelftools@0.33;unicorn@2.1.2", -} as const; /** Bridge-reserved status for MemoryError when no structured reply can be written. */ export const PWNTOOLS_MEMORY_FAILURE_EXIT = 75; /** Bridge-reserved status for an observed EFBIG failure without a complete reply. */ diff --git a/src/objdump/ObjdumpCommand.ts b/src/objdump/ObjdumpCommand.ts index ae899672c9..40f49faabb 100644 --- a/src/objdump/ObjdumpCommand.ts +++ b/src/objdump/ObjdumpCommand.ts @@ -1,12 +1,6 @@ import type { ObjdumpSelection } from "../domain/reverseEngineering.js"; import { absoluteCallerPath } from "../filesystem/callerPath.js"; -export const OBJDUMP_PROVIDER_IDENTITY = { - id: "gnu.objdump", - name: "GNU objdump", - version: null, -} as const; - /** Build GNU objdump argv without invoking a shell or following DWARF links by default. */ export const objdumpCommand = (input: ObjdumpSelection): readonly string[] => { if (input.operation === "architectures") return ["-i"]; diff --git a/src/objdump/ObjdumpProviderIdentity.ts b/src/objdump/ObjdumpProviderIdentity.ts new file mode 100644 index 0000000000..aa4d074b42 --- /dev/null +++ b/src/objdump/ObjdumpProviderIdentity.ts @@ -0,0 +1,6 @@ +/** Source-owned static provider identity and public operation names. */ +export const OBJDUMP_PROVIDER_IDENTITY = { + id: "gnu.objdump", + name: "GNU objdump", + version: null, +} as const; diff --git a/src/pcap/PcapProvider.ts b/src/pcap/PcapProvider.ts index 521c9ba6ec..7cee0ed5c8 100644 --- a/src/pcap/PcapProvider.ts +++ b/src/pcap/PcapProvider.ts @@ -1,3 +1,4 @@ +import { PCAP_PROVIDER_IDENTITY } from "./PcapProviderIdentity.js"; import { PCAP_HTTP_URI_FIELDS } from "./PcapHttpUri.js"; import { type ExecutionOptions, @@ -67,11 +68,6 @@ import { type ArtifactCommandPath, } from "../process/ArtifactCommandPath.js"; -export const PCAP_PROVIDER_IDENTITY = { - id: "pcap", - name: "Packet capture analysis", - version: null, -} as const; const FIELDS = [ "frame.number", "frame.protocols", diff --git a/src/pcap/PcapProviderIdentity.ts b/src/pcap/PcapProviderIdentity.ts new file mode 100644 index 0000000000..a0276e8b25 --- /dev/null +++ b/src/pcap/PcapProviderIdentity.ts @@ -0,0 +1,6 @@ +/** Source-owned static provider identity and public operation names. */ +export const PCAP_PROVIDER_IDENTITY = { + id: "pcap", + name: "Packet capture analysis", + version: null, +} as const; diff --git a/src/python/PythonPycProvider.ts b/src/python/PythonPycProvider.ts index 18f7c0b278..a95fc5cb6c 100644 --- a/src/python/PythonPycProvider.ts +++ b/src/python/PythonPycProvider.ts @@ -1,3 +1,4 @@ +import { PYTHON_PYC_PROVIDER_IDENTITY } from "./PythonPycProviderIdentity.js"; import { projectStableArtifactFailure, projectArtifactCloseFailure, @@ -34,13 +35,6 @@ import { } from "./PythonPyc.errors.js"; import { parsePythonPyc } from "./PythonPycParser.js"; -/** Bundled reader identity. Header python_version is a separate observed mapping. */ -export const PYTHON_PYC_PROVIDER_IDENTITY = { - id: "rea-python-pyc", - name: "REA CPython pyc parser", - version: "1", -} as const; - const OPERATION = "inspect_python_pyc"; const parserLimitRecovery = ( diff --git a/src/python/PythonPycProviderIdentity.ts b/src/python/PythonPycProviderIdentity.ts new file mode 100644 index 0000000000..a5a90ca475 --- /dev/null +++ b/src/python/PythonPycProviderIdentity.ts @@ -0,0 +1,6 @@ +/** Bundled reader identity. Header python_version is a separate observed mapping. */ +export const PYTHON_PYC_PROVIDER_IDENTITY = { + id: "rea-python-pyc", + name: "REA CPython pyc parser", + version: "1", +} as const; diff --git a/src/rizin/RizinCommand.ts b/src/rizin/RizinCommand.ts index df45fb61da..c571a29c2f 100644 --- a/src/rizin/RizinCommand.ts +++ b/src/rizin/RizinCommand.ts @@ -1,12 +1,6 @@ import type { RizinInput } from "../domain/reverseEngineering.js"; import { absoluteCallerPath } from "../filesystem/callerPath.js"; -export const RIZIN_PROVIDER_IDENTITY = { - id: "rizin", - name: "Rizin", - version: null, -} as const; - /** Default one-shot presentation to plain text while retaining plugin availability. */ export const rizinCommand = (input: RizinInput): readonly string[] => [ "-N", diff --git a/src/rizin/RizinDebugSessionManager.ts b/src/rizin/RizinDebugSessionManager.ts index 050d9eb349..95766ac089 100644 --- a/src/rizin/RizinDebugSessionManager.ts +++ b/src/rizin/RizinDebugSessionManager.ts @@ -1,3 +1,4 @@ +import { RIZIN_DEBUGGER_PROVIDER_IDENTITY } from "./RizinDebuggerProviderIdentity.js"; import { persistentDebuggerHostSupported } from "../process/PersistentDebuggerHost.js"; import { randomUUID } from "node:crypto"; import { isUtf8 } from "node:buffer"; @@ -54,12 +55,6 @@ const MAX_COMMAND_FRAMES = 4096; const MAX_RECENT_FRAME_BYTES = 16 * 1024; const MAX_DIAGNOSTIC_BYTES = 64 * 1024; -export const RIZIN_DEBUGGER_PROVIDER_IDENTITY = { - id: "rizin.debugger", - name: "Rizin debugger", - version: null, -} as const; - interface RizinLaunchProvenance { readonly caller_selected_path: string; readonly resolved_path: string; diff --git a/src/rizin/RizinDebuggerProviderIdentity.ts b/src/rizin/RizinDebuggerProviderIdentity.ts new file mode 100644 index 0000000000..969d563383 --- /dev/null +++ b/src/rizin/RizinDebuggerProviderIdentity.ts @@ -0,0 +1,6 @@ +/** Source-owned static provider identity and public operation names. */ +export const RIZIN_DEBUGGER_PROVIDER_IDENTITY = { + id: "rizin.debugger", + name: "Rizin debugger", + version: null, +} as const; diff --git a/src/rizin/RizinProviderIdentity.ts b/src/rizin/RizinProviderIdentity.ts new file mode 100644 index 0000000000..003c3ebe40 --- /dev/null +++ b/src/rizin/RizinProviderIdentity.ts @@ -0,0 +1,6 @@ +/** Source-owned static provider identity and public operation names. */ +export const RIZIN_PROVIDER_IDENTITY = { + id: "rizin", + name: "Rizin", + version: null, +} as const; diff --git a/src/rust/RustBinaryProvider.ts b/src/rust/RustBinaryProvider.ts index c6712c5c2a..c80443d4c7 100644 --- a/src/rust/RustBinaryProvider.ts +++ b/src/rust/RustBinaryProvider.ts @@ -1,3 +1,4 @@ +import { RUST_BINARY_PROVIDER_IDENTITY } from "./RustBinaryProviderIdentity.js"; import { projectStableArtifactFailure, projectArtifactCloseFailure, @@ -45,12 +46,6 @@ import { RustAuditableResourceFailure, } from "./RustAuditableReader.js"; -/** Bundled reader identity. Embedded package versions are separate observed facts. */ -export const RUST_BINARY_PROVIDER_IDENTITY = { - id: "rust-auditable", - name: "REA Rust dependency reader", - version: "1", -} as const; const OPERATION = "inspect_rust_binary"; /** Stable in-process dependency inspection without subprocesses, temporary files, or a Rust toolchain. */ diff --git a/src/rust/RustBinaryProviderIdentity.ts b/src/rust/RustBinaryProviderIdentity.ts new file mode 100644 index 0000000000..3023bf5328 --- /dev/null +++ b/src/rust/RustBinaryProviderIdentity.ts @@ -0,0 +1,6 @@ +/** Bundled reader identity. Embedded package versions are separate observed facts. */ +export const RUST_BINARY_PROVIDER_IDENTITY = { + id: "rust-auditable", + name: "REA Rust dependency reader", + version: "1", +} as const; diff --git a/src/server/createServer.ts b/src/server/createServer.ts index c12b5e5a52..5808bd1ba0 100644 --- a/src/server/createServer.ts +++ b/src/server/createServer.ts @@ -1,47 +1,21 @@ +import { + createCapabilityServices, + type CapabilityServiceOptions, +} from "../composition/capabilityServices.js"; import { registerToolFamilies } from "../contracts/toolFamilyIds.js"; import { AnalysisSettings } from "../application/AnalysisSettings.js"; import { parseMcpInputBudget } from "../config/mcpInputBudget.js"; import { registerAnalysisSettingsTools } from "./registerAnalysisSettingsTools.js"; import { registerDevicetreeTool } from "./registerDevicetreeTool.js"; -import { pcapCaptureHostSupported } from "../domain/pcap/pcapAnalysis.js"; -import { createGpuCodeObjectsService } from "../composition/gpu.js"; import { registerGpuCodeObjectsTools } from "./registerGpuCodeObjectsTools.js"; -import type { PacketCaptureService } from "../application/captures/PacketCaptureService.js"; import { serverInstructions } from "./serverInstructions.js"; -import { - createPacketCaptureService, - tsharkHostSupported, -} from "../composition/captures.js"; import { registerCaptureTools } from "./registerCaptureTools.js"; -import type { SignalRecordingService } from "../application/signals/SignalRecordingService.js"; -import { - createSignalRecordingService, - sigrokHostSupported, -} from "../composition/signals.js"; import { registerSignalTools } from "./registerSignalTools.js"; -import type { WasmArtifactService } from "../application/wasm/WasmArtifactService.js"; -import { wasmArtifactModule } from "../composition/wasm.js"; import { registerWasmTools } from "./registerWasmTools.js"; -import type { ApkSignatureService } from "../application/apkSignature/ApkSignatureService.js"; -import { apkSignatureModule } from "../composition/apkSignature.js"; import { registerApkSignatureTools } from "./registerApkSignatureTools.js"; import { snapshotEnvironment } from "../process/snapshotEnvironment.js"; import { captureProviderEnvironment } from "./providerEnvironment.js"; -import { - createElfNotesService, - createAppleIpsService, - createDevicetreeService, - createKernelModuleService, - createPeLoadConfigService, - createPeCertificatesService, - createPeImportsService, - createNativeExportsService, - createPeResourcesService, - createRecordedCrashService, - createByteEntropyService, - createBinaryLayoutService, - pwntoolsHostSupported, -} from "../composition/binaryDiagnostics.js"; +import { createByteEntropyService } from "../composition/binaryDiagnostics.js"; import { registerElfNotesTool } from "./registerElfNotesTool.js"; import { registerAppleIpsTool } from "./registerAppleIpsTool.js"; import { registerKernelModuleTool } from "./registerKernelModuleTool.js"; @@ -50,44 +24,19 @@ import { registerPeCertificatesTool } from "./registerPeCertificatesTool.js"; import { registerPeImportsTool } from "./registerPeImportsTool.js"; import { registerPeResourcesTool } from "./registerPeResourcesTool.js"; import { registerNativeExportsTool } from "./registerNativeExportsTool.js"; -import type { EvmInterfaceService } from "../application/evm/EvmInterfaceService.js"; -import { - evmHostSupported, - evmInterfaceModule, - evmLimiterAvailability, -} from "../composition/evm.js"; import { registerEvmTools } from "./registerEvmTools.js"; -import type { SqliteDatabaseService } from "../application/sqlite/SqliteDatabaseService.js"; -import { createSqliteDatabaseService } from "../composition/sqlite.js"; import { registerSqliteTools } from "./registerSqliteTools.js"; import { registerRecordedCrashTools } from "./registerRecordedCrashTools.js"; -import { createDesktopApplicationService } from "../composition/desktop.js"; import { registerDesktopApplicationTool } from "./registerDesktopApplicationTool.js"; -import { createGoBinaryService } from "../composition/go.js"; -import { createModelArtifactService } from "../composition/model.js"; import { registerModelArtifactTools } from "./registerModelArtifactTools.js"; -import { createIl2cppMetadataService } from "../composition/il2cpp.js"; -import type { Il2cppMetadataService } from "../application/il2cpp/Il2cppMetadataService.js"; -import type { GoBinaryService } from "../application/go/GoBinaryService.js"; -import { createElectronFuseService } from "../composition/electronFuses.js"; -import type { ElectronFuseService } from "../application/electron/ElectronFuseService.js"; import { registerGoTools } from "./registerGoTools.js"; import { registerIl2cppTools } from "./registerIl2cppTools.js"; -import { createRustBinaryService } from "../composition/rust.js"; -import type { RustBinaryService } from "../application/rust/RustBinaryService.js"; import { registerRustTools } from "./registerRustTools.js"; -import { createPythonPycService } from "../composition/pythonPyc.js"; -import type { PythonPycService } from "../application/python/PythonPycService.js"; import { registerPythonPycTool } from "./registerPythonPycTool.js"; -import { createBeamService } from "../composition/erlang.js"; -import type { BeamService } from "../application/erlang/BeamService.js"; import { registerErlangTools } from "./registerErlangTools.js"; -import type { RecordedCrashService } from "../application/binaryDiagnostics/RecordedCrashService.js"; import { registerAnalysisViewTool } from "./registerAnalysisViewTool.js"; import { registerBinaryDiagnosticsTools } from "./registerBinaryDiagnosticsTools.js"; -import { FridaInstrumentationManager } from "../frida/FridaInstrumentationManager.js"; import { registerFridaTools } from "./registerFridaTools.js"; -import type { BinaryLayoutService } from "../application/binaryDiagnostics/BinaryLayoutService.js"; import { EvidenceMcpServer } from "./EvidenceMcpServer.js"; import { catalogIdentityForToolFamilies } from "../catalogIdentity.js"; import { parseMcpToolFamilies } from "../config/mcpToolFamilies.js"; @@ -103,16 +52,7 @@ import { parseMcpInputSchemaProfile, } from "../config/mcpInputSchemaProfile.js"; import type { CompactInputSchemaPresentation } from "../contracts/compactInputSchema.js"; -import { isAbsolute } from "node:path"; - import type { BinarySessionPort } from "../application/binary/BinarySessionPort.js"; -import type { BrowserObservationPort } from "../application/BrowserObservationPort.js"; -import type { BrowserScenarioCapturePort } from "../application/BrowserScenarioCapturePort.js"; -import type { ElectronActiveObservationPort } from "../application/javascript/ElectronActiveObservationPort.js"; -import type { ElectronObservationPort } from "../application/javascript/ElectronObservationPort.js"; -import type { JavaScriptRuntimeObservationPort } from "../application/javascript/JavaScriptRuntimeObservationPort.js"; -import type { FridaInstrumentationPort } from "../application/frida/FridaInstrumentationPort.js"; -import type { OptionalProviderLoadFailures } from "../application/OptionalObservationProviders.js"; import { PRODUCT_IDENTITY } from "../identity.js"; import { silentLogger } from "../logger.js"; import type { Logger } from "pino"; @@ -123,36 +63,16 @@ import { registerBrowserScenarioTool } from "./registerBrowserScenarioTool.js"; import { registerBrowserTools } from "./registerBrowserTools.js"; import { registerWebScriptTool } from "./registerWebScriptTool.js"; import { registerWebModuleTool } from "./registerWebModuleTool.js"; -import type { WebModuleTraceService } from "../application/WebModuleTraceService.js"; -import { createWebModuleTraceService } from "../composition/webModules.js"; -import { createWebSourceLocationService } from "../composition/webSourceLocations.js"; import { registerWebSourceLocationTool } from "./registerWebSourceLocationTool.js"; -import type { WebSourceLocationService } from "../application/WebSourceLocationService.js"; -import type { WebRuntimeService } from "../application/WebRuntimeService.js"; -import { createWebRuntimeService } from "../composition/webRuntime.js"; import { registerWebRuntimeTools } from "./registerWebRuntimeTools.js"; -import type { WebNetworkCaptureService } from "../application/WebNetworkCaptureService.js"; -import { createWebNetworkCaptureService } from "../composition/webNetworkCaptures.js"; import { registerWebNetworkCaptureTool } from "./registerWebNetworkCaptureTool.js"; import { registerJavaScriptRecoveryTool } from "./registerJavaScriptRecoveryTool.js"; import { JavaScriptRecoveryService } from "../application/javascript/JavaScriptRecoveryService.js"; -import type { JavaScriptRecoveryPort } from "../application/javascript/JavaScriptRecoveryPort.js"; -import { - createJavaScriptRecoveryProvider, - wakaruHostSupported, -} from "../composition/javascriptRecovery.js"; import { registerElectronTools } from "./registerElectronTools.js"; -import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; import { registerEnhancedTools } from "./registerEnhancedTools.js"; import { registerJavaScriptRuntimeObservationTools } from "./registerJavaScriptRuntimeObservationTools.js"; import { registerManagedTools } from "./registerManagedTools.js"; import { registerFirmwareTools } from "./registerFirmwareTools.js"; -import { FirmwareAnalysisService } from "../application/firmware/FirmwareAnalysisService.js"; -import type { FirmwareAnalysisPort } from "../application/firmware/FirmwareAnalysisPort.js"; -import { - createFirmwareAnalysisProvider, - firmwareHostSupported, -} from "../composition/firmware.js"; import { registerAndroidTools } from "./registerAndroidTools.js"; import { registerApktoolTools } from "./registerApktoolTools.js"; import { registerPcapTools } from "./registerPcapTools.js"; @@ -161,26 +81,10 @@ import { registerJebTools } from "./registerJebTools.js"; import { registerAdbTools } from "./registerAdbTools.js"; import { AndroidAnalysisService } from "../application/android/AndroidAnalysisService.js"; import { JebAnalysisService } from "../application/jeb/JebAnalysisService.js"; -import type { AndroidAnalysisPort } from "../application/android/AndroidAnalysisPort.js"; -import type { ApktoolResourceAnalysisPort } from "../application/apktool/ApktoolResourceAnalysisPort.js"; -import type { PcapAnalysisPort } from "../application/pcap/PcapAnalysisPort.js"; import { PcapAnalysisService } from "../application/pcap/PcapAnalysisService.js"; -import { - locateWiresharkTool, - selectWiresharkTool, -} from "../pcap/PcapConfiguration.js"; import { ApktoolResourceAnalysisService } from "../application/apktool/ApktoolResourceAnalysisService.js"; -import type { FlutterBuildAnalysisPort } from "../application/flutter/FlutterBuildAnalysisPort.js"; import { FlutterBuildAnalysisService } from "../application/flutter/FlutterBuildAnalysisService.js"; -import { createAndroidAnalysisProvider } from "../composition/android.js"; -import { createApktoolResourceAnalysisProvider } from "../composition/apktool.js"; -import { createPcapAnalysisProvider } from "../composition/pcap.js"; -import { createFlutterBuildAnalysisProvider } from "../composition/flutter.js"; -import type { JebAnalysisPort } from "../application/jeb/JebAnalysisPort.js"; -import { createJebAnalysisProvider } from "../composition/jeb.js"; -import type { AdbDeviceAnalysisPort } from "../application/adb/AdbDeviceAnalysisPort.js"; import { AdbDeviceAnalysisService } from "../application/adb/AdbDeviceAnalysisService.js"; -import { createAdbDeviceAnalysisProvider } from "../composition/adb.js"; import { registerManagedWorkflowTools } from "./registerManagedWorkflowTools.js"; import { registerOfficialTools } from "./registerOfficialTools.js"; import { registerGuidedPrompts } from "./registerPrompts.js"; @@ -191,18 +95,12 @@ import { withAdmittedAnalysis, type ServerAnalysisSource, } from "./analysisAdmission.js"; -import { createReverseEngineeringService } from "../composition/reverseEngineering.js"; import { registerReverseEngineeringTools } from "./registerReverseEngineeringTools.js"; -import { CutterBridgeClient } from "../cutter/CutterBridgeClient.js"; -import { CutterBridgeService } from "../cutter/CutterBridgeService.js"; import { registerCutterTools } from "./registerCutterTools.js"; -import { GdbSessionManager } from "../gdb/GdbSessionManager.js"; -import { persistentDebuggerHostSupported } from "../process/PersistentDebuggerHost.js"; import { registerGdbTools } from "./registerGdbTools.js"; -import { RizinDebugSessionManager } from "../rizin/RizinDebugSessionManager.js"; import { registerRizinDebugTools } from "./registerRizinDebugTools.js"; -export interface CreateServerOptions { +export interface CreateServerOptions extends CapabilityServiceOptions { readonly analysisSettings?: AnalysisSettings; /** Preserve the connection's creation time when materializing its runtime. */ readonly startedAt?: string; @@ -210,191 +108,11 @@ export interface CreateServerOptions { readonly mcpServer?: EvidenceMcpServer; readonly environment?: Readonly; readonly delivery?: ToolResultDelivery; - readonly packetCapture?: PacketCaptureService; - readonly signalRecording?: SignalRecordingService; - readonly wasmArtifact?: WasmArtifactService; - readonly apkSignature?: ApkSignatureService; readonly providerEnvironment?: Readonly; - readonly evmInterface?: EvmInterfaceService; - readonly sqliteDatabase?: SqliteDatabaseService; readonly logger?: Logger; - readonly binaryLayout?: BinaryLayoutService; - readonly goBinary?: GoBinaryService; - readonly il2cppMetadata?: Il2cppMetadataService; - readonly rustBinary?: RustBinaryService; - readonly pythonPyc?: PythonPycService; - readonly erlangBeam?: BeamService; - readonly electronFuses?: ElectronFuseService; - readonly recordedCrash?: RecordedCrashService; - readonly firmwareAnalysis?: FirmwareAnalysisPort; - readonly javascriptRecovery?: JavaScriptRecoveryPort; - readonly webModuleTrace?: WebModuleTraceService; - readonly webSourceLocation?: WebSourceLocationService; - readonly webRuntime?: WebRuntimeService; - readonly webNetworkCapture?: WebNetworkCaptureService; - readonly androidAnalysis?: AndroidAnalysisPort; - readonly apktoolAnalysis?: ApktoolResourceAnalysisPort; - readonly pcapAnalysis?: PcapAnalysisPort; - readonly flutterAnalysis?: FlutterBuildAnalysisPort; - readonly jebAnalysis?: JebAnalysisPort; - readonly adbDeviceAnalysis?: AdbDeviceAnalysisPort; - readonly browserObservation?: BrowserObservationPort; - readonly browserScenarioCapture?: BrowserScenarioCapturePort; - readonly electronObservation?: ElectronObservationPort; - /** Selects who closes the shared optional observation providers. */ - readonly optionalObservationProviderOwner?: "server" | "caller"; - readonly electronActiveObservation?: ElectronActiveObservationPort; - readonly javascriptRuntimeObservation?: JavaScriptRuntimeObservationPort; - readonly fridaInstrumentation?: FridaInstrumentationPort; readonly availabilityPolicy?: () => AvailabilityPolicy; - readonly optionalProviderLoadFailures?: OptionalProviderLoadFailures; } -/** Apply the shared TShark selection rule without starting the tool. */ -const tsharkLocatable = (environment: Readonly): boolean => { - try { - return ( - locateWiresharkTool( - selectWiresharkTool(environment, "tshark"), - environment, - ) !== undefined - ); - } catch { - // A relative REA_TSHARK_COMMAND is configuration the tool reports at execution. - return false; - } -}; - -const installSessionToolAvailability = ( - session: BinarySessionPort | undefined, - options: CreateServerOptions, - environment: Readonly, - fridaInstrumentation: FridaInstrumentationPort, -) => { - if (session === undefined) return undefined; - const { platform, arch } = process; - // Each provider owns its verified host; discovery and execution share it. - const verifiedHost = { - packetCapture: tsharkHostSupported(platform), - pcapCapture: pcapCaptureHostSupported(platform), - signalRecording: sigrokHostSupported(platform), - evmInterface: evmHostSupported(platform, arch), - pwntools: pwntoolsHostSupported(platform, arch), - firmware: firmwareHostSupported(platform), - javascriptRecovery: wakaruHostSupported(platform, arch), - }; - const pwntools = - verifiedHost.pwntools && isAbsolute(environment.REA_PWNTOOLS_PYTHON ?? ""); - // Injected services own their availability on any host. Otherwise no - // configuration lets an integration pass its execution host check. - const outsideVerifiedHost = (injected: unknown, supported: boolean) => - injected === undefined && !supported; - const basePolicy = sessionAvailabilityPolicy(options.availabilityPolicy, { - optionalProviderLoadFailures: options.optionalProviderLoadFailures, - hostUnsupported: { - persistentDebugger: !persistentDebuggerHostSupported(platform), - pcapCapture: outsideVerifiedHost( - options.pcapAnalysis, - verifiedHost.pcapCapture, - ), - packetCapture: outsideVerifiedHost( - options.packetCapture, - verifiedHost.packetCapture, - ), - signalRecording: outsideVerifiedHost( - options.signalRecording, - verifiedHost.signalRecording, - ), - evmInterface: outsideVerifiedHost( - options.evmInterface, - verifiedHost.evmInterface, - ), - binaryLayout: outsideVerifiedHost( - options.binaryLayout, - verifiedHost.pwntools, - ), - recordedCrash: outsideVerifiedHost( - options.recordedCrash, - verifiedHost.pwntools, - ), - firmwareInspection: outsideVerifiedHost( - options.firmwareAnalysis, - verifiedHost.firmware, - ), - firmwareExtraction: outsideVerifiedHost( - options.firmwareAnalysis, - verifiedHost.firmware, - ), - javascriptRecovery: outsideVerifiedHost( - options.javascriptRecovery, - verifiedHost.javascriptRecovery, - ), - }, - optionalFeatures: { - packetCaptureEnabled: - options.packetCapture !== undefined || - (verifiedHost.packetCapture && tsharkLocatable(environment)), - signalRecordingEnabled: - options.signalRecording !== undefined || - (verifiedHost.signalRecording && - isAbsolute(environment.REA_SIGROK_COMMAND ?? "")), - wasmArtifactEnabled: - options.wasmArtifact !== undefined || - isAbsolute(environment.REA_WABT_BIN_DIRECTORY ?? ""), - evmInterfaceEnabled: - options.evmInterface !== undefined || verifiedHost.evmInterface, - webModuleResolutionEnabled: - options.webModuleTrace !== undefined || - isAbsolute(environment.REA_BROWSER_EXECUTABLE ?? ""), - recordedCrashEnabled: options.recordedCrash !== undefined || pwntools, - binaryLayoutEnabled: options.binaryLayout !== undefined || pwntools, - firmwareInspectionEnabled: - options.firmwareAnalysis !== undefined || - (verifiedHost.firmware && - isAbsolute(environment.REA_BINWALK_COMMAND ?? "")), - firmwareExtractionEnabled: - options.firmwareAnalysis !== undefined || - (verifiedHost.firmware && - isAbsolute(environment.REA_UNBLOB_COMMAND ?? "")), - browserObservationEnabled: options.browserObservation !== undefined, - javascriptRecoveryEnabled: - options.javascriptRecovery !== undefined || - (verifiedHost.javascriptRecovery && - isAbsolute(environment.REA_WAKARU_COMMAND ?? "")), - browserScenarioEnabled: options.browserScenarioCapture !== undefined, - electronObservationEnabled: options.electronObservation !== undefined, - electronAutomationEnabled: - options.electronActiveObservation !== undefined, - v8InspectorObservationEnabled: - options.javascriptRuntimeObservation !== undefined, - }, - }); - const policy = (): AvailabilityPolicy => { - const value = basePolicy(); - const fridaFailure = - options.optionalProviderLoadFailures?.fridaInstrumentation ?? - fridaInstrumentation.availabilityFailure?.(); - return fridaFailure === undefined - ? value - : { - ...value, - optionalProviderLoadFailures: { - ...value.optionalProviderLoadFailures, - fridaInstrumentation: fridaFailure, - }, - }; - }; - return { - policy, - ...(options.evmInterface !== undefined || !verifiedHost.evmInterface - ? {} - : { - evmLimiterAvailability: (signal: AbortSignal) => - evmLimiterAvailability(environment, { signal }), - }), - }; -}; - const createMcpServer = ( session: BinarySessionPort | undefined, delivery: ToolResultDelivery, @@ -456,60 +174,60 @@ export const createServer = ( environment, ); const delivery = selectToolResultDelivery(environment, options.delivery); - const evmInterface = - options.evmInterface ?? evmInterfaceModule.create(environment); - const sqliteDatabase = - options.sqliteDatabase ?? createSqliteDatabaseService(environment); - const binaryLayout = - options.binaryLayout ?? createBinaryLayoutService(environment); - const recordedCrash = - options.recordedCrash ?? createRecordedCrashService(environment); - const webSourceLocation = - options.webSourceLocation ?? createWebSourceLocationService(environment); - const webModuleTrace = - options.webModuleTrace ?? createWebModuleTraceService(environment); - const desktopApplication = createDesktopApplicationService(); - const goBinary = options.goBinary ?? createGoBinaryService(); - const rustBinary = options.rustBinary ?? createRustBinaryService(); - const erlangBeam = options.erlangBeam ?? createBeamService(); - const modelArtifact = createModelArtifactService(); - const gpuCodeObjects = createGpuCodeObjectsService(); - const il2cppMetadata = - options.il2cppMetadata ?? createIl2cppMetadataService(); - const peResources = createPeResourcesService(); - const nativeExports = createNativeExportsService(); - const devicetree = createDevicetreeService(); - const elfNotes = createElfNotesService(); - const appleIps = createAppleIpsService(); - const kernelModule = createKernelModuleService(); - const peLoadConfig = createPeLoadConfigService(); - const peCertificates = createPeCertificatesService(); - const peImports = createPeImportsService(); - const electronFuses = options.electronFuses ?? createElectronFuseService(); - const pythonPyc = options.pythonPyc ?? createPythonPycService(); - const webNetworkCapture = - options.webNetworkCapture ?? - createWebNetworkCaptureService(environment, analysisSettings.read); - const javascriptRecovery = - options.javascriptRecovery ?? createJavaScriptRecoveryProvider(environment); - const artifactResources = new ArtifactResourceScope(); - const reverseEngineering = - createReverseEngineeringService(providerEnvironment); - const firmwareAnalysis = new FirmwareAnalysisService( - options.firmwareAnalysis ?? createFirmwareAnalysisProvider(environment), - ); - const selectedOptions = { - ...options, + const capabilities = createCapabilityServices({ environment, - delivery, + providerEnvironment, + settings: analysisSettings.read, + options, + platform: process.platform, + arch: process.arch, + }); + const { evmInterface, sqliteDatabase, binaryLayout, recordedCrash, webSourceLocation, webModuleTrace, + desktopApplication, + goBinary, + rustBinary, + erlangBeam, + modelArtifact, + gpuCodeObjects, + il2cppMetadata, + peResources, + nativeExports, + devicetree, + elfNotes, + appleIps, + kernelModule, + peLoadConfig, + peCertificates, + peImports, + electronFuses, + pythonPyc, + webNetworkCapture, javascriptRecovery, - }; + artifactResources, + reverseEngineering, + firmwareAnalysis, + gdbSessions, + rizinDebugSessions, + android, + pcap, + flutter, + fridaInstrumentation, + wasmArtifact, + apkSignature, + packetCapture, + signalRecording, + webRuntime, + apktool, + jeb, + adbDevice, + cutter, + } = capabilities.services; const startedAt = options.startedAt ?? new Date().toISOString(); const logger = options.logger ?? silentLogger; const server = @@ -536,122 +254,35 @@ export const createServer = ( ), ); } - const gdbSessions = new GdbSessionManager({ - environment: providerEnvironment, - }); - const rizinDebugSessions = new RizinDebugSessionManager(providerEnvironment); - const android = - options.androidAnalysis ?? - createAndroidAnalysisProvider( - environment, - undefined, - analysisSettings.read, - ); - const pcap = options.pcapAnalysis ?? createPcapAnalysisProvider(environment); - const flutter = - options.flutterAnalysis ?? createFlutterBuildAnalysisProvider(); - const fridaInstrumentation = - options.fridaInstrumentation ?? new FridaInstrumentationManager(); - const wasmArtifact = wasmArtifactModule.create( - environment, - options.wasmArtifact, - analysisSettings.read, - ); - const apkSignature = apkSignatureModule.create( - environment, - options.apkSignature, - ); - const packetCapture = - options.packetCapture ?? createPacketCaptureService(environment); - const signalRecording = - options.signalRecording ?? createSignalRecordingService(environment); - const webRuntime = options.webRuntime ?? createWebRuntimeService(); - const apktool = - options.apktoolAnalysis ?? - createApktoolResourceAnalysisProvider(environment); - const jeb = options.jebAnalysis ?? createJebAnalysisProvider(environment); - const adbDevice = - options.adbDeviceAnalysis ?? createAdbDeviceAnalysisProvider(environment); - const cutter = new CutterBridgeService( - new CutterBridgeClient(providerEnvironment), - ); - let providerClose: Promise | undefined; - const closeProviders = (): Promise => { - providerClose ??= Promise.allSettled([ - android.close(), - cutter.close(), - fridaInstrumentation.closeAll(), - wasmArtifact.close(), - apkSignature.close(), - packetCapture.close(), - signalRecording.close(), - apktool.close(), - jeb.close(), - adbDevice.close(), - flutter.close(), - pcap.close(), - evmInterface.close(), - sqliteDatabase.close(), - binaryLayout.close(), - recordedCrash.close(), - webSourceLocation.close(), - webModuleTrace.close(), - desktopApplication.close(), - goBinary.close(), - rustBinary.close(), - erlangBeam.close(), - il2cppMetadata.close(), - modelArtifact.close(), - gpuCodeObjects.close(), - peResources.close(), - nativeExports.close(), - devicetree.close(), - elfNotes.close(), - appleIps.close(), - kernelModule.close(), - peLoadConfig.close(), - peCertificates.close(), - peImports.close(), - electronFuses.close(), - pythonPyc.close(), - webNetworkCapture.close(), - javascriptRecovery.close?.() ?? Promise.resolve(), - ...(options.optionalObservationProviderOwner === "caller" - ? [] - : [ - options.browserObservation?.close?.() ?? Promise.resolve(), - options.browserScenarioCapture?.close?.() ?? Promise.resolve(), - options.electronObservation?.close?.() ?? Promise.resolve(), - options.electronActiveObservation?.close?.() ?? Promise.resolve(), - options.javascriptRuntimeObservation?.close?.() ?? - Promise.resolve(), - ]), - webRuntime.provider.close?.() ?? Promise.resolve(), - artifactResources.close(), - reverseEngineering.close(), - firmwareAnalysis.close(), - gdbSessions.closeAll(), - rizinDebugSessions.closeAll(), - ]) - .then((results) => { - const failures = results.flatMap((result) => - result.status === "rejected" ? [result.reason] : [], - ); - if (failures.length > 0) - throw new AggregateError(failures, "REA provider cleanup failed"); - }) - .catch((cause: unknown) => { - providerClose = undefined; - throw cause; - }); - return providerClose; - }; - const availability = installSessionToolAvailability( - session, - options, - environment, - fridaInstrumentation, - ); + const closeProviders = capabilities.close; + const availability = + session === undefined + ? undefined + : (() => { + const basePolicy = sessionAvailabilityPolicy( + options.availabilityPolicy, + { + ...capabilities.availability, + optionalProviderLoadFailures: + options.optionalProviderLoadFailures, + }, + ); + return { + policy: (): AvailabilityPolicy => { + const value = basePolicy(); + const failures = capabilities.observeProviderLoadFailures(); + return Object.keys(failures).length === 0 + ? value + : { + ...value, + optionalProviderLoadFailures: { + ...value.optionalProviderLoadFailures, + ...failures, + }, + }; + }, + }; + })(); const toolLogger = logger.child({ layer: "server" }); const { evidenceById, @@ -782,7 +413,7 @@ export const createServer = ( ); registerBinaryDiagnosticsTools( server, - selectedOptions.binaryLayout ?? createBinaryLayoutService(environment), + binaryLayout, createByteEntropyService(artifactResources), toolLogger, recordEvidence, @@ -856,8 +487,7 @@ export const createServer = ( ); registerRecordedCrashTools( server, - selectedOptions.recordedCrash ?? - createRecordedCrashService(environment), + recordedCrash, toolLogger, recordEvidence, analysisAdmission, @@ -955,8 +585,7 @@ export const createServer = ( ); registerSqliteTools( server, - selectedOptions.sqliteDatabase ?? - createSqliteDatabaseService(environment), + sqliteDatabase, toolLogger, recordEvidence, analysisAdmission, @@ -1059,16 +688,15 @@ export const createServer = ( registerBrowserTools(server, { ...common, evidenceById, - browser: selectedOptions.browserObservation, - loadFailure: - selectedOptions.optionalProviderLoadFailures?.browserObservation, + browser: options.browserObservation, + loadFailure: options.optionalProviderLoadFailures?.browserObservation, }); registerBrowserScenarioTool(server, { ...common, evidenceById, - provider: selectedOptions.browserScenarioCapture, + provider: options.browserScenarioCapture, loadFailure: - selectedOptions.optionalProviderLoadFailures?.browserScenarioCapture, + options.optionalProviderLoadFailures?.browserScenarioCapture, }); registerWebRuntimeTools( server, @@ -1092,22 +720,20 @@ export const createServer = ( artifactResources, environment, evidenceById, - electron: selectedOptions.electronObservation, - electronActive: selectedOptions.electronActiveObservation, + electron: options.electronObservation, + electronActive: options.electronActiveObservation, electronFuses, observationLoadFailure: - selectedOptions.optionalProviderLoadFailures?.electronObservation, + options.optionalProviderLoadFailures?.electronObservation, activeLoadFailure: - selectedOptions.optionalProviderLoadFailures - ?.electronActiveObservation, + options.optionalProviderLoadFailures?.electronActiveObservation, }), "javascript-runtime": () => registerJavaScriptRuntimeObservationTools(server, { ...common, - runtime: selectedOptions.javascriptRuntimeObservation, + runtime: options.javascriptRuntimeObservation, loadFailure: - selectedOptions.optionalProviderLoadFailures - ?.javascriptRuntimeObservation, + options.optionalProviderLoadFailures?.javascriptRuntimeObservation, }), application: () => { registerAnalysisSettingsTools(server, analysisSettings); @@ -1122,26 +748,21 @@ export const createServer = ( registerWebScriptTool(server, { ...common, artifactResources }); registerWebModuleTool( server, - selectedOptions.webModuleTrace ?? - createWebModuleTraceService(environment), + webModuleTrace, toolLogger, recordEvidence, analysisAdmission, ); registerWebSourceLocationTool( server, - selectedOptions.webSourceLocation ?? - createWebSourceLocationService(environment), + webSourceLocation, toolLogger, recordEvidence, analysisAdmission, ); registerJavaScriptRecoveryTool( server, - new JavaScriptRecoveryService( - selectedOptions.javascriptRecovery ?? - createJavaScriptRecoveryProvider(environment), - ), + new JavaScriptRecoveryService(javascriptRecovery), toolLogger, recordEvidence, analysisAdmission, @@ -1158,21 +779,12 @@ export const createServer = ( session: () => { if (session === undefined) return; registerSessionTools(server, session, toolLogger, { - ...selectedOptions, + environment, artifactResources, ...(availability === undefined ? {} : { availabilityPolicy: availability.policy }), - ...(availability?.evmLimiterAvailability === undefined - ? {} - : { evmLimiterAvailability: availability.evmLimiterAvailability }), - androidAnalysisAvailability: (signal) => - android.inspectAvailability(signal), - adbAnalysisAvailability: (signal) => - adbDevice.inspectAvailability(signal), - apktoolAnalysisAvailability: (signal) => - apktool.inspectAvailability(signal), - jebAnalysisAvailability: (signal) => jeb.inspectAvailability(signal), + ...capabilities.probes, startedAt, }); }, diff --git a/src/server/registerSessionStatusTool.ts b/src/server/registerSessionStatusTool.ts index 4de670527b..663b9fdd8b 100644 --- a/src/server/registerSessionStatusTool.ts +++ b/src/server/registerSessionStatusTool.ts @@ -1,3 +1,4 @@ +import type { CapabilityAvailabilityProbes } from "../application/CapabilityAvailability.js"; import { BINARY_SESSION_SUMMARY_OMITTED_FIELDS, type BinarySessionDetail, @@ -27,27 +28,8 @@ import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; import { err } from "../domain/result.js"; import { CURRENT_ENTRYPOINT_PATH } from "../process/CurrentEntrypointPath.js"; -/** Provider-owned live probes used by session discovery. */ -export interface SessionAvailabilityProbes { - readonly androidAnalysisAvailability: ( - signal: AbortSignal, - ) => Promise; - readonly adbAnalysisAvailability?: ( - signal: AbortSignal, - ) => Promise; - readonly apktoolAnalysisAvailability?: ( - signal: AbortSignal, - ) => Promise; - readonly jebAnalysisAvailability?: ( - signal: AbortSignal, - ) => Promise; - readonly evmLimiterAvailability?: ( - signal: AbortSignal, - ) => Promise; -} - /** Inputs required to register the binary session status tool. */ -export interface SessionStatusToolOptions extends SessionAvailabilityProbes { +export interface SessionStatusToolOptions extends CapabilityAvailabilityProbes { readonly server: EvidenceMcpServer; readonly session: BinarySessionPort; readonly contract: ReturnType>; @@ -61,7 +43,7 @@ export interface SessionStatusToolOptions extends SessionAvailabilityProbes { * cannot appear in the response. */ const probeSessionAvailability = ( - probes: SessionAvailabilityProbes, + probes: CapabilityAvailabilityProbes, signal: AbortSignal, advertisesFamily: (family: ToolFamilyId) => boolean, ): Promise<{ diff --git a/src/server/registerSessionTools.ts b/src/server/registerSessionTools.ts index fffed1dedb..8808ceadc9 100644 --- a/src/server/registerSessionTools.ts +++ b/src/server/registerSessionTools.ts @@ -1,3 +1,4 @@ +import type { CapabilityAvailabilityProbes } from "../application/CapabilityAvailability.js"; import { parseEspFlashAddress } from "../domain/firmware/espFlashSelection.js"; import { registerNativeFunctionMatchingTool } from "./registerNativeFunctionMatchingTool.js"; import type { EvidenceMcpServer } from "./EvidenceMcpServer.js"; @@ -34,10 +35,7 @@ import { registerEvidenceTools, registerUnknownTools, } from "./registerSessionRecordTools.js"; -import { - registerSessionStatusTool, - type SessionAvailabilityProbes, -} from "./registerSessionStatusTool.js"; +import { registerSessionStatusTool } from "./registerSessionStatusTool.js"; import { sessionAvailabilityPolicy } from "./sessionAvailabilityPolicy.js"; import type { AvailabilityPolicy } from "../application/CapabilityInventory.js"; import { logToolExecution } from "./toolLogging.js"; @@ -150,7 +148,7 @@ const registerProcessTools = ({ ); }; -export interface LifecycleToolRegistration extends SessionAvailabilityProbes { +export interface LifecycleToolRegistration extends CapabilityAvailabilityProbes { readonly server: EvidenceMcpServer; readonly session: BinarySessionPort; readonly logger: Logger; @@ -245,7 +243,7 @@ const registerOpenLifecycleTool = ({ }; /** Register MCP-only target lifecycle operations on a long-lived session. */ -export interface SessionToolOptions extends Partial { +export interface SessionToolOptions extends Partial { readonly artifactResources: ArtifactResourceScope; /** Server environment snapshot that process capture resolves and inherits. */ readonly environment: Readonly; diff --git a/src/server/sessionAvailabilityPolicy.ts b/src/server/sessionAvailabilityPolicy.ts index 47867f78f9..6883639778 100644 --- a/src/server/sessionAvailabilityPolicy.ts +++ b/src/server/sessionAvailabilityPolicy.ts @@ -3,33 +3,8 @@ import { type AvailabilityPolicy, type HostRestrictedFeature, } from "../application/CapabilityInventory.js"; -import type { OptionalProviderLoadFailures } from "../application/OptionalObservationProviders.js"; import { platform } from "node:process"; - -export interface SessionAvailabilityDefaults { - readonly optionalProviderLoadFailures?: - | OptionalProviderLoadFailures - | undefined; - readonly hostUnsupported?: AvailabilityPolicy["hostUnsupported"]; - readonly optionalFeatures?: Pick< - AvailabilityPolicy, - | "packetCaptureEnabled" - | "signalRecordingEnabled" - | "wasmArtifactEnabled" - | "evmInterfaceEnabled" - | "browserObservationEnabled" - | "browserScenarioEnabled" - | "electronObservationEnabled" - | "electronAutomationEnabled" - | "v8InspectorObservationEnabled" - | "javascriptRecoveryEnabled" - | "webModuleResolutionEnabled" - | "binaryLayoutEnabled" - | "recordedCrashEnabled" - | "firmwareInspectionEnabled" - | "firmwareExtractionEnabled" - >; -} +import type { CapabilityAvailabilityFacts } from "../application/CapabilityAvailability.js"; /** Either the caller or the observed host can mark an integration unsupported. */ const combineHostUnsupported = ( @@ -46,7 +21,7 @@ const combineHostUnsupported = ( /** Select configured availability reporting or the target-free defaults. */ export const sessionAvailabilityPolicy = ( configured: (() => AvailabilityPolicy) | undefined, - defaults: SessionAvailabilityDefaults, + defaults: CapabilityAvailabilityFacts, ): (() => AvailabilityPolicy) => { const policy = configured ?? diff --git a/src/signals/SigrokProviderIdentity.ts b/src/signals/SigrokProviderIdentity.ts new file mode 100644 index 0000000000..9f7a6cfa48 --- /dev/null +++ b/src/signals/SigrokProviderIdentity.ts @@ -0,0 +1,6 @@ +/** Source-owned static provider identity and public operation names. */ +export const SIGROK_PROVIDER_IDENTITY = { + id: "sigrok-recording", + name: "REA sigrok recording adapter", + version: "sigrok-cli@0.7.2", +} as const; diff --git a/src/signals/SigrokRecordingProvider.ts b/src/signals/SigrokRecordingProvider.ts index 1d588f0c7f..aac0f0237c 100644 --- a/src/signals/SigrokRecordingProvider.ts +++ b/src/signals/SigrokRecordingProvider.ts @@ -1,3 +1,4 @@ +import { SIGROK_PROVIDER_IDENTITY } from "./SigrokProviderIdentity.js"; import { snapshotEnvironment } from "../process/snapshotEnvironment.js"; import { artifactFileCancellationCause } from "../artifacts/ArtifactFileOperation.js"; import { @@ -68,11 +69,7 @@ import { PrivateRuntimeRoot } from "../process/PrivateRuntimeRoot.js"; import { privateWorkspaceFailure } from "../process/privateWorkspaceFailure.js"; const OPERATION = "decode_signal_recording"; -export const SIGROK_PROVIDER_IDENTITY = { - id: "sigrok-recording", - name: "REA sigrok recording adapter", - version: "sigrok-cli@0.7.2", -} as const; + type Launcher = NonNullable[2]>["launcher"]; /** Signal decoding runs only on Linux; discovery and execution share this check. */ diff --git a/src/sqlite/SqliteDatabaseFailures.ts b/src/sqlite/SqliteDatabaseFailures.ts index d9dd7b8b35..e049b61318 100644 --- a/src/sqlite/SqliteDatabaseFailures.ts +++ b/src/sqlite/SqliteDatabaseFailures.ts @@ -30,10 +30,10 @@ import { ProviderCleanupError } from "../domain/providerCleanupError.js"; import { OwnedCommandFailure } from "../process/OwnedCommand.js"; import type { ProviderProcessSnapshot } from "../process/ProviderProcess.js"; import { - SQLITE_PROVIDER_IDENTITY, DEFAULT_SQLITE_WORKER_TIMEOUT_MS, MAX_SQLITE_WORKER_TIMEOUT_MS, } from "../domain/sqlite/sqliteDatabase.js"; +import { SQLITE_PROVIDER_IDENTITY } from "../domain/sqlite/SqliteProviderIdentity.js"; import { SQLITE_DATABASE_LIMITS } from "./SqliteDatabaseLimits.js"; import { SqliteSnapshotAcquisitionFailure } from "./SqliteDatabaseSnapshot.js"; diff --git a/src/sqlite/SqliteDatabaseProvider.ts b/src/sqlite/SqliteDatabaseProvider.ts index 43a14636a7..f003f0a375 100644 --- a/src/sqlite/SqliteDatabaseProvider.ts +++ b/src/sqlite/SqliteDatabaseProvider.ts @@ -25,10 +25,10 @@ import { inspectSqliteDatabaseInputSchema, sqliteDatabaseSchema, type InspectSqliteDatabaseInput, - SQLITE_PROVIDER_IDENTITY, DEFAULT_SQLITE_WORKER_TIMEOUT_MS, sqliteInspectionParameters, } from "../domain/sqlite/sqliteDatabase.js"; +import { SQLITE_PROVIDER_IDENTITY } from "../domain/sqlite/SqliteProviderIdentity.js"; import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js"; import { PrivateRuntimeRoot } from "../process/PrivateRuntimeRoot.js"; import { diff --git a/src/sqlite/SqliteDatabaseSnapshot.ts b/src/sqlite/SqliteDatabaseSnapshot.ts index e11ab0d7d1..79daeec560 100644 --- a/src/sqlite/SqliteDatabaseSnapshot.ts +++ b/src/sqlite/SqliteDatabaseSnapshot.ts @@ -8,7 +8,7 @@ import { AnalysisInputError, AnalysisResourceConstraintError, } from "../domain/analysisErrorCore.js"; -import { SQLITE_PROVIDER_IDENTITY } from "../domain/sqlite/sqliteDatabase.js"; +import { SQLITE_PROVIDER_IDENTITY } from "../domain/sqlite/SqliteProviderIdentity.js"; import { withArtifactFileHandle, assertArtifactReadActive, diff --git a/tests/boundary/binaryDiagnostics/pwntoolsLayoutProvider.test.ts b/tests/boundary/binaryDiagnostics/pwntoolsLayoutProvider.test.ts index 99950d4545..1f2156ce19 100644 --- a/tests/boundary/binaryDiagnostics/pwntoolsLayoutProvider.test.ts +++ b/tests/boundary/binaryDiagnostics/pwntoolsLayoutProvider.test.ts @@ -7,7 +7,7 @@ import { join } from "node:path"; import { z } from "zod"; import { expect, it, onTestFinished } from "vitest"; import { PwntoolsLayoutProvider } from "../../../src/native/pwntools/PwntoolsLayoutProvider.js"; -import { PWNTOOLS_PROVIDER_IDENTITY } from "../../../src/native/pwntools/PwntoolsRelease.js"; +import { PWNTOOLS_PROVIDER_IDENTITY } from "../../../src/native/pwntools/PwntoolsProviderIdentity.js"; import { spawnOwnedProviderProcess } from "../../../src/process/ProviderProcess.js"; import { waitForProviderProcessReady } from "../../fixtures/providerProcess.js"; import { PrivateRuntimeRoot } from "../../../src/process/PrivateRuntimeRoot.js"; diff --git a/tests/boundary/binaryDiagnostics/recordedCrashProvider.test.ts b/tests/boundary/binaryDiagnostics/recordedCrashProvider.test.ts index f2d3d05db6..b39b3a6eed 100644 --- a/tests/boundary/binaryDiagnostics/recordedCrashProvider.test.ts +++ b/tests/boundary/binaryDiagnostics/recordedCrashProvider.test.ts @@ -10,7 +10,7 @@ import { removeTestWorkspace, } from "../../support/workspace/workspaceFixture.js"; import { PwntoolsRecordedCrashProvider } from "../../../src/native/pwntools/PwntoolsRecordedCrashProvider.js"; -import { PWNTOOLS_PROVIDER_IDENTITY } from "../../../src/native/pwntools/PwntoolsRelease.js"; +import { PWNTOOLS_PROVIDER_IDENTITY } from "../../../src/native/pwntools/PwntoolsProviderIdentity.js"; import { spawnOwnedProviderProcess, type OwnedProviderProcessSpawnOptions, diff --git a/tests/boundary/browser/historicalCaptureDecoder.test.ts b/tests/boundary/browser/historicalCaptureDecoder.test.ts index bdb757c4c5..dfb62de77a 100644 --- a/tests/boundary/browser/historicalCaptureDecoder.test.ts +++ b/tests/boundary/browser/historicalCaptureDecoder.test.ts @@ -22,7 +22,7 @@ import { MitmproxyCaptureAdapter } from "../../../src/browser/history/MitmproxyC import { MITMPROXY_CAPTURE_PROVIDER_IDENTITY, HAR_CAPTURE_PROVIDER_IDENTITY, -} from "../../../src/browser/history/CaptureRelease.js"; +} from "../../../src/browser/history/CaptureProviderIdentities.js"; import { inspectWebNetworkCaptureInputSchema, WEB_NETWORK_CAPTURE_LIMITS, diff --git a/tests/boundary/filesystem/directAnalysisPermissions.test.ts b/tests/boundary/filesystem/directAnalysisPermissions.test.ts index 34effe4d11..64344b9457 100644 --- a/tests/boundary/filesystem/directAnalysisPermissions.test.ts +++ b/tests/boundary/filesystem/directAnalysisPermissions.test.ts @@ -20,7 +20,7 @@ import { createEvidence } from "../../../src/domain/evidence.js"; import { createEvidenceBundle } from "../../../src/domain/evidenceBundle.js"; import { REA_WORKFLOW_PROVIDER } from "../../../src/domain/investigationProviders.js"; import { workflowAnalysisProfile } from "../../../src/application/WorkflowAnalysisProfile.js"; -import { HOPPER_PROVIDER_IDENTITY } from "../../../src/hopper/HopperProviderCapabilities.js"; +import { HOPPER_PROVIDER_IDENTITY } from "../../../src/hopper/HopperProviderMetadata.js"; import { resolveHopperAnalysisProfile } from "../../../src/hopper/HopperAnalysisProfile.js"; diff --git a/tests/boundary/filesystem/directAnalysisSnapshotReplay.test.ts b/tests/boundary/filesystem/directAnalysisSnapshotReplay.test.ts index 26b1ff06d4..5c418312ec 100644 --- a/tests/boundary/filesystem/directAnalysisSnapshotReplay.test.ts +++ b/tests/boundary/filesystem/directAnalysisSnapshotReplay.test.ts @@ -47,10 +47,8 @@ import { isProfilelessNativeDispatchSnapshot } from "../../../src/application/bi import { OwnedStagedOutput } from "../../../src/filesystem/OwnedStagedOutput.js"; import { ProviderAdapterError } from "../../../src/domain/providerAdapterError.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; -import { - CAPABILITIES as GHIDRA_CAPABILITIES, - GHIDRA_PROVIDER_IDENTITY, -} from "../../../src/ghidra/GhidraProviderCapabilities.js"; +import { CAPABILITIES as GHIDRA_CAPABILITIES } from "../../../src/ghidra/GhidraProviderCapabilities.js"; +import { GHIDRA_PROVIDER_IDENTITY } from "../../../src/ghidra/GhidraProviderMetadata.js"; import { nativeInventoryRowsSchemas } from "../../../src/domain/native/nativeInventory.js"; import { APPLE_DISPATCH_METADATA_ANALYSIS_PROFILE } from "../../../src/native/NativeMacOSProviderMetadata.js"; diff --git a/tests/boundary/filesystem/productCatalog.test.ts b/tests/boundary/filesystem/productCatalog.test.ts index a3aad84c34..7a2ce1256f 100644 --- a/tests/boundary/filesystem/productCatalog.test.ts +++ b/tests/boundary/filesystem/productCatalog.test.ts @@ -10,20 +10,20 @@ import { BYTE_ENTROPY_PROVIDER } from "../../../src/domain/investigationProvider import { SUPPORTED_CLIENT_DEFINITIONS } from "../../../src/application/SupportedClients.js"; import { CLI_COMMAND_NAMES } from "../../../src/cliCommandNames.js"; import { createCli } from "../../../src/cli.js"; -import { FRIDA_PROVIDER_IDENTITY } from "../../../src/frida/FridaInstrumentationManager.js"; +import { FRIDA_PROVIDER_IDENTITY } from "../../../src/frida/FridaProviderIdentity.js"; import { TOOL_CONTRACTS } from "../../../src/contracts/toolContracts.js"; import { HOPPER_OPERATIONS, HOPPER_PROVIDER_IDENTITY, -} from "../../../src/hopper/HopperProviderCapabilities.js"; +} from "../../../src/hopper/HopperProviderMetadata.js"; import { GHIDRA_PROVIDER_IDENTITY, GHIDRA_OPERATIONS, -} from "../../../src/ghidra/GhidraProviderCapabilities.js"; -import { OBJDUMP_PROVIDER_IDENTITY } from "../../../src/objdump/ObjdumpCommand.js"; -import { RIZIN_PROVIDER_IDENTITY } from "../../../src/rizin/RizinCommand.js"; -import { GDB_PROVIDER_IDENTITY } from "../../../src/gdb/GdbSessionManager.js"; -import { PWNTOOLS_PROVIDER_IDENTITY } from "../../../src/native/pwntools/PwntoolsRelease.js"; +} from "../../../src/ghidra/GhidraProviderMetadata.js"; +import { OBJDUMP_PROVIDER_IDENTITY } from "../../../src/objdump/ObjdumpProviderIdentity.js"; +import { RIZIN_PROVIDER_IDENTITY } from "../../../src/rizin/RizinProviderIdentity.js"; +import { GDB_PROVIDER_IDENTITY } from "../../../src/gdb/GdbProviderIdentity.js"; +import { PWNTOOLS_PROVIDER_IDENTITY } from "../../../src/native/pwntools/PwntoolsProviderIdentity.js"; import { documentationFactIssues, skillReferenceIssues, diff --git a/tests/boundary/mcp/createServerCleanup.test.ts b/tests/boundary/mcp/createServerCleanup.test.ts index 909b0fcd67..4ac315297f 100644 --- a/tests/boundary/mcp/createServerCleanup.test.ts +++ b/tests/boundary/mcp/createServerCleanup.test.ts @@ -6,6 +6,8 @@ import { InvestigationRecords } from "../../../src/application/investigation/Inv import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js"; import { EvidenceMcpServer } from "../../../src/server/EvidenceMcpServer.js"; import { createServer } from "../../../src/server/createServer.js"; +import { createBrowserObservationProvider } from "../../../src/composition/browserObservation.js"; +import { createEvmInterfaceService } from "../../../src/composition/evm.js"; afterEach(() => vi.restoreAllMocks()); @@ -47,3 +49,46 @@ it("preserves both shutdown failures and retries provider cleanup after onclose" transportClose.mockRestore(); await session.close(); }); + +it("retries only the failed injected capability after its peer has closed", async () => { + // Compose the real typed owners without starting browser observation or EVM + // analysis. Only their close outcomes are controlled by this regression. + const browser = createBrowserObservationProvider(); + const evm = createEvmInterfaceService({}); + const failure = new Error("Controlled browser shutdown refusal"); + const browserClose = vi + .spyOn(browser, "close") + .mockRejectedValueOnce(failure) + .mockResolvedValue(undefined); + const evmClose = vi.spyOn(evm, "close").mockResolvedValue(undefined); + const server = createServer( + { + kind: "fixed", + analysis: { + execute: async () => { + throw new Error("Cleanup must not execute analysis"); + }, + }, + }, + { environment: {}, browserObservation: browser, evmInterface: evm }, + ); + try { + const first: unknown = await server + .close() + .catch((cause: unknown) => cause); + const causes = (cause: unknown): unknown[] => + cause instanceof AggregateError ? cause.errors.flatMap(causes) : [cause]; + expect(causes(first)).toEqual([failure]); + expect(browserClose).toHaveBeenCalledTimes(1); + expect(evmClose).toHaveBeenCalledTimes(1); + await server.close(); + expect(browserClose).toHaveBeenCalledTimes(2); + expect(evmClose).toHaveBeenCalledTimes(1); + await server.close(); + expect(browserClose).toHaveBeenCalledTimes(2); + expect(evmClose).toHaveBeenCalledTimes(1); + } finally { + browserClose.mockResolvedValue(undefined); + await server.close(); + } +}); diff --git a/tests/boundary/mcp/rawBinaryMcp.test.ts b/tests/boundary/mcp/rawBinaryMcp.test.ts index d2c2326844..497f542898 100644 --- a/tests/boundary/mcp/rawBinaryMcp.test.ts +++ b/tests/boundary/mcp/rawBinaryMcp.test.ts @@ -19,7 +19,7 @@ import { } from "../../fixtures/binarySession.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { silentLogger } from "../../../src/logger.js"; -import { GHIDRA_PROVIDER_IDENTITY } from "../../../src/ghidra/GhidraProviderCapabilities.js"; +import { GHIDRA_PROVIDER_IDENTITY } from "../../../src/ghidra/GhidraProviderMetadata.js"; import { RAW_ONLY_ARCHITECTURES } from "../../../src/domain/binaryTargetTypes.js"; import type { CallToolResult } from "@modelcontextprotocol/server"; diff --git a/tests/boundary/mcp/serverLifecycleMcp.test.ts b/tests/boundary/mcp/serverLifecycleMcp.test.ts index 2bcdd86eb8..051cd1ea50 100644 --- a/tests/boundary/mcp/serverLifecycleMcp.test.ts +++ b/tests/boundary/mcp/serverLifecycleMcp.test.ts @@ -494,7 +494,7 @@ it("closes the Inspector owner with peers and retries its retained cleanup", asy await server.close(); expect(inspectorCloseAttempts).toBe(2); - expect(peerCloseAttempts).toBe(2); + expect(peerCloseAttempts).toBe(1); }); it("does not close a caller-owned Inspector observation provider", async () => { diff --git a/tests/boundary/mcp/transportCleanup.test.ts b/tests/boundary/mcp/transportCleanup.test.ts index 0b7247b2a2..2e2a485597 100644 --- a/tests/boundary/mcp/transportCleanup.test.ts +++ b/tests/boundary/mcp/transportCleanup.test.ts @@ -1,10 +1,37 @@ import { expect, it, vi } from "vitest"; +import { JadxProvider } from "../../../src/android/JadxProvider.js"; import { createManagedBinarySession } from "../../../src/composition/binary.js"; import { silentLogger } from "../../../src/logger.js"; import { startMcpTransport } from "../../../src/main/transport.js"; import { createServer } from "../../../src/server/createServer.js"; import { createToolResultDelivery } from "../../../src/server/toolResult.js"; +const startOneServer = ( + session: Parameters[1], + construct: typeof createServer, +) => + startMcpTransport( + { + env: {}, + serve: async (factory) => { + await factory({ era: "modern" }); + return { close: async () => undefined }; + }, + createServer: construct, + writeStderr: () => undefined, + setExitCode: () => undefined, + registerShutdown: () => () => undefined, + }, + session, + { + logger: silentLogger, + serverLogger: silentLogger, + environment: {}, + delivery: createToolResultDelivery(undefined), + loadOptionalProviders: async () => ({}), + }, + ); + it("preserves Android sibling cleanup failures and retries only unfinished owners", async () => { const session = createManagedBinarySession(); const failures = [ @@ -72,3 +99,71 @@ it("preserves Android sibling cleanup failures and retries only unfinished owner await session.close(); } }); + +it("releases the Android fallback when its actual server owner closes successfully", async () => { + const session = createManagedBinarySession(); + const underlyingClose = vi.spyOn(JadxProvider.prototype, "close"); + let server: ReturnType | undefined; + const transport = await startOneServer(session, (source, options) => { + server = createServer(source, options); + return server; + }); + try { + expect(transport.ok).toBe(true); + if (server === undefined) + throw new Error("Expected a constructed SDK server"); + await server.close(); + expect(underlyingClose).toHaveBeenCalledTimes(1); + await transport.closeAndroid(); + await transport.closeAndroid(); + expect(underlyingClose).toHaveBeenCalledTimes(1); + } finally { + await transport.handle.close(); + await transport.closeAndroid(); + await transport.closeOptionalProviders(); + await session.close(); + underlyingClose.mockRestore(); + } +}); + +it.each(["server close", "server construction"] as const)( + "keeps the Android fallback after failed %s until the exact provider closes", + async (stage) => { + const session = createManagedBinarySession(); + const failure = new Error("Controlled first Android cleanup refusal"); + const underlyingClose = vi + .spyOn(JadxProvider.prototype, "close") + .mockRejectedValueOnce(failure); + let server: ReturnType | undefined; + const transport = await startOneServer(session, (source, options) => { + if (stage === "server construction") + throw new Error("Controlled server construction failure"); + server = createServer(source, options); + return server; + }); + try { + expect(transport.ok).toBe(stage === "server close"); + if (stage === "server close") { + if (server === undefined) + throw new Error("Expected a constructed SDK server"); + await expect(server.close()).rejects.toMatchObject({ + name: "AggregateError", + errors: [{ name: "AggregateError", errors: [failure] }], + }); + } + // Failed construction also attempts cleanup before returning its partial + // owner. Both paths must preserve the same failed provider for fallback. + expect(underlyingClose).toHaveBeenCalledTimes(1); + await transport.closeAndroid(); + expect(underlyingClose).toHaveBeenCalledTimes(2); + await transport.closeAndroid(); + expect(underlyingClose).toHaveBeenCalledTimes(2); + } finally { + await transport.handle.close(); + await transport.closeAndroid(); + await transport.closeOptionalProviders(); + await session.close(); + underlyingClose.mockRestore(); + } + }, +); diff --git a/tests/boundary/native/pwntoolsArtifactCleanup.test.ts b/tests/boundary/native/pwntoolsArtifactCleanup.test.ts index c0f3cefb83..359b42ec67 100644 --- a/tests/boundary/native/pwntoolsArtifactCleanup.test.ts +++ b/tests/boundary/native/pwntoolsArtifactCleanup.test.ts @@ -55,7 +55,7 @@ import { prepareProcessOwnershipInspection } from "../../../src/process/ProcessO import { spawnOwnedProviderProcess } from "../../../src/process/ProviderProcess.js"; import { binaryLayoutFixture } from "../../fixtures/binaryDiagnostics/layout.js"; import { PwntoolsDecoder } from "../../../src/native/pwntools/PwntoolsDecoder.js"; -import { PWNTOOLS_PROVIDER_IDENTITY } from "../../../src/native/pwntools/PwntoolsRelease.js"; +import { PWNTOOLS_PROVIDER_IDENTITY } from "../../../src/native/pwntools/PwntoolsProviderIdentity.js"; import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js"; const makeDecoder = () => diff --git a/tests/boundary/providers/hopper/hopperAnalysisProfile.test.ts b/tests/boundary/providers/hopper/hopperAnalysisProfile.test.ts index 8234f25930..67fca95eba 100644 --- a/tests/boundary/providers/hopper/hopperAnalysisProfile.test.ts +++ b/tests/boundary/providers/hopper/hopperAnalysisProfile.test.ts @@ -11,7 +11,7 @@ import { hopperLoaderArgsForTarget, resolveHopperAnalysisProfile, } from "../../../../src/hopper/HopperAnalysisProfile.js"; -import { HOPPER_PROVIDER_IDENTITY } from "../../../../src/hopper/HopperProviderCapabilities.js"; +import { HOPPER_PROVIDER_IDENTITY } from "../../../../src/hopper/HopperProviderMetadata.js"; let inspectionResources = createInspectionResources(); let directory: string | undefined; diff --git a/tests/boundary/providers/hopper/hopperClient.protocol.test.ts b/tests/boundary/providers/hopper/hopperClient.protocol.test.ts index 02a6ae73b6..26196a19ef 100644 --- a/tests/boundary/providers/hopper/hopperClient.protocol.test.ts +++ b/tests/boundary/providers/hopper/hopperClient.protocol.test.ts @@ -3,7 +3,7 @@ import { once } from "node:events"; import { describe, expect, it, onTestFinished, vi } from "vitest"; import { HopperProcessError } from "../../../../src/domain/hopperErrors.js"; -import { HOPPER_OPERATIONS } from "../../../../src/hopper/HopperProviderCapabilities.js"; +import { HOPPER_OPERATIONS } from "../../../../src/hopper/HopperProviderMetadata.js"; import { HopperClient } from "../../../../src/hopper/HopperClient.js"; import type { HopperDiagnostic } from "../../../../src/hopper/HopperDiagnostics.js"; import { providerCleanupFailure } from "../../../../src/hopper/HopperDiagnostics.js"; diff --git a/tests/composition/idaProvider.test.ts b/tests/composition/idaProvider.test.ts index f12981cafc..2c6c08f934 100644 --- a/tests/composition/idaProvider.test.ts +++ b/tests/composition/idaProvider.test.ts @@ -15,7 +15,7 @@ import { createEvidence } from "../../src/domain/evidence.js"; import { createEvidenceBundle } from "../../src/domain/evidenceBundle.js"; import { createAnalysisExecution } from "../../src/application/AnalysisProvider.js"; import { EnhancedTools } from "../../src/application/EnhancedTools.js"; -import { IDA_PROVIDER_IDENTITY } from "../../src/ida/IdaProviderCapabilities.js"; +import { IDA_PROVIDER_IDENTITY } from "../../src/ida/IdaProviderMetadata.js"; import { parseBinaryTarget } from "../../src/application/BinaryTargetResolver.js"; import { fat } from "../../src/domain/binaryTarget.fixture.js"; import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; diff --git a/tests/composition/liveToolFactories.test.ts b/tests/composition/liveToolFactories.test.ts new file mode 100644 index 0000000000..be319d81f8 --- /dev/null +++ b/tests/composition/liveToolFactories.test.ts @@ -0,0 +1,103 @@ +import { readdir } from "node:fs/promises"; + +import { expect, it, vi } from "vitest"; + +import { createCutterBridgeService } from "../../src/composition/cutter.js"; +import { + createGdbSessionManager, + createRizinDebugSessionManager, +} from "../../src/composition/debuggerSessions.js"; +import { createFridaInstrumentationProvider } from "../../src/composition/frida.js"; +import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; + +it("keeps Frida binding acquisition lazy and one owner's shutdown local", async () => { + // Only the binding boundary is simulated; neither native Frida nor a target + // is acquired in this factory/lifetime test. + const load = vi.fn(async () => { + throw new Error("Authored fixture has no native Frida binding"); + }); + const first = createFridaInstrumentationProvider({ + deviceManagerLoader: load, + }); + const second = createFridaInstrumentationProvider({ + deviceManagerLoader: load, + }); + try { + expect(load).not.toHaveBeenCalled(); + await first.closeAll(); + expect(load).not.toHaveBeenCalled(); + expect(await first.listDevices()).toMatchObject({ + ok: false, + error: { reason: expect.stringContaining("shutting down") }, + }); + expect(load).not.toHaveBeenCalled(); + expect((await second.listDevices()).ok).toBe(false); + expect(load).toHaveBeenCalledTimes(1); + } finally { + await Promise.all([first.closeAll(), second.closeAll()]); + } +}); + +it("gives Cutter compositions independent descriptor ownership without startup writes", async () => { + const directory = await createTestTempDirectory("rea-cutter-factory-"); + const environment = { REA_CUTTER_BRIDGE_DIR: directory }; + const first = createCutterBridgeService(environment, { platform: "linux" }); + const second = createCutterBridgeService(environment, { platform: "linux" }); + try { + expect(await readdir(directory)).toEqual([]); + await first.close(); + // Closing one descriptor owner must not close another server/CLI owner's + // read admission. The empty private directory requires no live bridge. + expect((await first.listSessions()).ok).toBe(false); + expect(await second.listSessions()).toMatchObject({ + ok: true, + value: { + bridge_directory: directory, + discovery_status: "no_live_bridge_found", + sessions: [], + }, + }); + expect(await readdir(directory)).toEqual([]); + } finally { + await Promise.all([first.close(), second.close()]); + } +}); + +it("keeps debugger lifetimes fresh and honors platform admission before any launch", async () => { + const spawnProcess = vi.fn(async () => { + throw new Error("A factory or unsupported host must not launch a debugger"); + }); + const gdb = [ + createGdbSessionManager({}, { platform: "win32", spawnProcess }), + createGdbSessionManager({}, { platform: "win32", spawnProcess }), + ] as const; + const rizin = [ + createRizinDebugSessionManager({}, { platform: "win32", spawnProcess }), + createRizinDebugSessionManager({}, { platform: "win32", spawnProcess }), + ] as const; + try { + expect(spawnProcess).not.toHaveBeenCalled(); + await gdb[0].closeAll(); + await rizin[0].closeAll(); + expect(await gdb[0].start()).toMatchObject({ + ok: false, + error: { _tag: "AnalysisCancelledError" }, + }); + expect(await gdb[1].start()).toMatchObject({ + ok: false, + error: { _tag: "AnalysisCapabilityUnavailableError" }, + }); + const input = { path: "fixture", inferior_tty: "fixture-tty" }; + expect(await rizin[0].start(input)).toMatchObject({ + ok: false, + error: { _tag: "AnalysisInputError" }, + }); + expect(await rizin[1].start(input)).toMatchObject({ + ok: false, + error: { _tag: "AnalysisCapabilityUnavailableError" }, + }); + expect(spawnProcess).not.toHaveBeenCalled(); + } finally { + await Promise.all([...gdb, ...rizin].map((owner) => owner.closeAll())); + } +}); diff --git a/tests/conformance/capabilityDeclarations.test.ts b/tests/conformance/capabilityDeclarations.test.ts new file mode 100644 index 0000000000..0b71f93260 --- /dev/null +++ b/tests/conformance/capabilityDeclarations.test.ts @@ -0,0 +1,201 @@ +import { describe, expect, it } from "vitest"; + +import { providerCatalog } from "../../scripts/lib/catalog-builders.mjs"; +import { ADB_PROVIDER_IDENTITY } from "../../src/adb/AdbProviderIdentity.js"; +import { + CAPABILITY_DECLARATIONS, + type CapabilityDeclaration, +} from "../../src/composition/capabilityDeclarations.js"; +import { TOOL_CONTRACTS } from "../../src/contracts/toolContracts.js"; +import { TOOL_CONTRACT_GROUPS } from "../../src/contracts/toolFamilies.js"; +import { QLOG_CAPTURE_PROVIDER_IDENTITY } from "../../src/browser/history/CaptureProviderIdentities.js"; +import { HARMONY_APPLICATION_PROVIDER } from "../../src/domain/investigationProviders.js"; +import { + WEB_MODULE_TRACE_PROVIDER, + WEB_SOURCE_LOCATION_PROVIDER, +} from "../../src/domain/webTraceProviders.js"; + +const fixtureIdentity = { + id: "authored-fixture", + name: "Authored provider identity", + version: null, +}; +const catalog = ( + declarations: Parameters< + typeof providerCatalog + >[0]["capabilityDeclarations"]["CAPABILITY_DECLARATIONS"], +) => + providerCatalog({ + toolContracts: { TOOL_CONTRACTS }, + toolFamilies: { TOOL_CONTRACT_GROUPS }, + capabilityDeclarations: { CAPABILITY_DECLARATIONS: declarations }, + }); + +describe("whole canonical contract groups", () => { + it("includes a new canonical ADB operation without editing actual provider metadata", () => { + const before = catalog(CAPABILITY_DECLARATIONS); + const adb = before.find(({ id }) => id === ADB_PROVIDER_IDENTITY.id); + expect(adb).toBeDefined(); + if (adb === undefined) throw new Error("Missing actual ADB provider"); + // Simulate the next real contract being added to its existing source group. + // The production declaration is deliberately unchanged, so a copied list + // would miss this valid operation even if every other provider used groups. + const added = { name: "new_valid_adb_operation" }; + const after = providerCatalog({ + capabilityDeclarations: { CAPABILITY_DECLARATIONS }, + toolContracts: { TOOL_CONTRACTS: [...TOOL_CONTRACTS, added] }, + toolFamilies: { + TOOL_CONTRACT_GROUPS: { + ...TOOL_CONTRACT_GROUPS, + ADB_TOOL_CONTRACTS: [ + ...TOOL_CONTRACT_GROUPS.ADB_TOOL_CONTRACTS, + added, + ], + }, + }, + }); + expect(after.find(({ id }) => id === adb.id)).toEqual({ + ...adb, + capabilities: [...adb.capabilities, added.name].sort(), + }); + expect(after.filter(({ id }) => id !== adb.id)).toEqual( + before.filter(({ id }) => id !== adb.id), + ); + }); + + it("includes every canonical operation selected by real whole-group bindings", () => { + const providers = new Map( + catalog(CAPABILITY_DECLARATIONS).map((provider) => [ + provider.id, + provider, + ]), + ); + const declarations: readonly CapabilityDeclaration[] = Object.values( + CAPABILITY_DECLARATIONS, + ); + let examined = 0; + for (const declaration of declarations) + for (const { identity, operations } of declaration.providers) { + if (!("group" in operations)) continue; + const expected = TOOL_CONTRACT_GROUPS[operations.group].map( + ({ name }) => name, + ); + expect(providers.get(identity.id)?.capabilities).toEqual( + expect.arrayContaining(expected), + ); + examined += 1; + } + expect(examined).toBeGreaterThan(0); + }); +}); + +describe("static capability provider declarations", () => { + it("reports the actual trace, Harmony and qlog provenance without changing their public operations", () => { + const providers = catalog(CAPABILITY_DECLARATIONS); + for (const [identity, operation] of [ + [WEB_SOURCE_LOCATION_PROVIDER, "trace_web_source_location"], + [WEB_MODULE_TRACE_PROVIDER, "trace_web_module_imports"], + [HARMONY_APPLICATION_PROVIDER, "project_harmony_application_graph"], + [QLOG_CAPTURE_PROVIDER_IDENTITY, "inspect_web_network_capture"], + ] as const) + expect(providers.find(({ id }) => id === identity.id)).toEqual({ + ...identity, + capabilities: [operation], + }); + expect( + providers.find(({ id }) => id === "rea-web-script-export")?.capabilities, + ).toEqual(["export_web_scripts"]); + expect( + providers.find(({ id }) => id === "rea-javascript-application-workflows") + ?.capabilities, + ).not.toContain("project_harmony_application_graph"); + }); + + it("merges shared provider identities and operation unions independently of declaration order", () => { + const first = { + providers: [ + { + identity: fixtureIdentity, + operations: ["inspect_wasm_artifact", "inspect_apk_signature"], + }, + ], + }; + const second = { + providers: [ + { + identity: { ...fixtureIdentity }, + operations: ["inspect_apk_signature", "inspect_evm_interface"], + }, + ], + }; + const expected = [ + { + ...fixtureIdentity, + capabilities: [ + "inspect_apk_signature", + "inspect_evm_interface", + "inspect_wasm_artifact", + ], + }, + ]; + expect(catalog({ first, second, files: { providers: [] } })).toEqual( + expected, + ); + expect(catalog({ second, first })).toEqual(expected); + }); + + it.each([ + { name: "A different provider name" }, + { version: "a different release" }, + ])( + "refuses conflicting identities sharing the same provider ID: %j", + (change) => { + expect(() => + catalog({ + first: { + providers: [{ identity: fixtureIdentity, operations: [] }], + }, + second: { + providers: [ + { identity: { ...fixtureIdentity, ...change }, operations: [] }, + ], + }, + }), + ).toThrow("Provider authored-fixture has conflicting source identities"); + }, + ); + + it("refuses an unknown analyst operation instead of silently dropping the provider binding", () => { + expect(() => + catalog({ + fixture: { + providers: [ + { + identity: fixtureIdentity, + operations: ["not_an_advertised_operation"], + }, + ], + }, + }), + ).toThrow( + "Provider authored-fixture declares an unknown analyst operation: not_an_advertised_operation", + ); + }); + + it("refuses an unknown canonical contract group", () => { + expect(() => + catalog({ + fixture: { + providers: [ + { + identity: fixtureIdentity, + operations: { group: "not_a_contract_group" }, + }, + ], + }, + }), + ).toThrow( + "Provider authored-fixture declares an unknown contract group: not_a_contract_group", + ); + }); +}); diff --git a/tests/conformance/ida/providerContracts.test.ts b/tests/conformance/ida/providerContracts.test.ts index ae2599c5ea..bfa4044d25 100644 --- a/tests/conformance/ida/providerContracts.test.ts +++ b/tests/conformance/ida/providerContracts.test.ts @@ -4,10 +4,8 @@ import { rm } from "node:fs/promises"; import { afterEach, describe, expect, it } from "vitest"; import { createIdaTarget, RecordingIdaMcp } from "../../fixtures/idaMcp.js"; import { IdaSessionClient } from "../../../src/ida/IdaSessionClient.js"; -import { - IDA_OPERATIONS, - idaCapabilities, -} from "../../../src/ida/IdaProviderCapabilities.js"; +import { idaCapabilities } from "../../../src/ida/IdaProviderCapabilities.js"; +import { IDA_OPERATIONS } from "../../../src/ida/IdaProviderMetadata.js"; import { toolContract } from "../../../src/contracts/toolContracts.js"; import { createEvidence } from "../../../src/domain/evidence.js"; import type { JsonValue } from "../../../src/domain/jsonValue.js"; diff --git a/tests/conformance/verifierMetadataLinks.test.ts b/tests/conformance/verifierMetadataLinks.test.ts new file mode 100644 index 0000000000..594f820ece --- /dev/null +++ b/tests/conformance/verifierMetadataLinks.test.ts @@ -0,0 +1,239 @@ +import { parse } from "@babel/parser"; +import * as t from "@babel/types"; +import { readFile, readdir } from "node:fs/promises"; +import { dirname, join, relative, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { expect, it } from "vitest"; + +import { + importSpecifier, + readImportSpecifiers, +} from "../../scripts/lib/import-specifiers.mjs"; + +const root = fileURLToPath(new URL("../../", import.meta.url)); +const syntax = (source: string) => + parse(source, { sourceType: "module", plugins: ["typescript"] }); + +const exportedValues = (tree: ReturnType) => { + const names = new Set(); + for (const node of tree.program.body) { + if (!t.isExportNamedDeclaration(node) || node.exportKind === "type") + continue; + const declaration = node.declaration; + if (t.isVariableDeclaration(declaration)) { + for (const binding of declaration.declarations) + if (t.isIdentifier(binding.id)) names.add(binding.id.name); + } else if ( + (t.isFunctionDeclaration(declaration) || + t.isClassDeclaration(declaration)) && + declaration.id != null + ) + names.add(declaration.id.name); + for (const binding of node.specifiers) + if (!t.isExportSpecifier(binding) || binding.exportKind !== "type") + names.add( + t.isIdentifier(binding.exported) + ? binding.exported.name + : binding.exported.value, + ); + } + return names; +}; + +/** Derive migrated metadata symbols from their current source owners. */ +const metadataSymbols = async () => { + const pending = [resolve(root, "src/composition/capabilityDeclarations.ts")]; + const seen = new Set(); + const symbols = new Set(); + while (pending.length > 0) { + const file = pending.pop(); + if (file === undefined || seen.has(file)) continue; + seen.add(file); + const source = await readFile(file, "utf8"); + for (const name of exportedValues(syntax(source))) symbols.add(name); + for (const fact of readImportSpecifiers(source)) + if (!fact.typeOnly && fact.specifier.startsWith(".")) + pending.push( + resolve(dirname(file), fact.specifier).replace(/\.js$/u, ".ts"), + ); + } + return symbols; +}; + +const literal = (node: t.Node | null | undefined): string | undefined => + t.isStringLiteral(node) + ? node.value + : t.isTemplateLiteral(node) && node.expressions.length === 0 + ? (node.quasis[0]?.value.cooked ?? undefined) + : undefined; + +/** Interpret only the literal/fileURL/join patterns used by packaged verifiers. */ +const compiledReference = ( + node: t.Node | null | undefined, +): string | undefined => { + const value = literal(node); + if (value !== undefined) return value; + if (!t.isCallExpression(node) || !t.isIdentifier(node.callee)) + return undefined; + if (node.callee.name === "pathToFileURL") + return compiledReference(node.arguments[0]); + if (node.callee.name === "join") + return node.arguments + .map(literal) + .find((part) => part?.startsWith("dist/")); + return undefined; +}; + +const dynamicSource = (node: t.Node | null | undefined): t.Node | undefined => { + if (t.isAwaitExpression(node)) return dynamicSource(node.argument); + if (t.isImportExpression(node)) return node.source; + return undefined; +}; + +interface MetadataLink { + readonly name: string; + readonly reference: string | undefined; + readonly line: number; +} + +/** Static linking only: no verifier main or provider implementation is executed. */ +const metadataLinks = (source: string, names: ReadonlySet) => { + const tree = syntax(source); + const links: MetadataLink[] = []; + const namespaces = new Map(); + const add = (name: string, reference: string | undefined, node: t.Node) => { + if (names.has(name)) + links.push({ name, reference, line: node.loc?.start.line ?? 1 }); + }; + t.traverseFast(tree, (node) => { + if (t.isImportDeclaration(node) && node.importKind !== "type") { + const reference = importSpecifier(node); + for (const binding of node.specifiers) { + if (t.isImportSpecifier(binding) && binding.importKind !== "type") + add( + t.isIdentifier(binding.imported) + ? binding.imported.name + : binding.imported.value, + reference, + node, + ); + else if (t.isImportNamespaceSpecifier(binding)) + namespaces.set(binding.local.name, reference); + } + } + if (!t.isVariableDeclarator(node)) return; + const target = dynamicSource(node.init); + if (target === undefined) return; + const reference = compiledReference(target); + if (t.isIdentifier(node.id)) namespaces.set(node.id.name, reference); + if (t.isObjectPattern(node.id)) + for (const property of node.id.properties) + if (t.isObjectProperty(property)) { + const name = t.isIdentifier(property.key) + ? property.key.name + : literal(property.key); + if (name !== undefined) add(name, reference, node); + } + }); + t.traverseFast(tree, (node) => { + if ( + !t.isMemberExpression(node) || + !t.isIdentifier(node.object) || + !namespaces.has(node.object.name) + ) + return; + const name = + !node.computed && t.isIdentifier(node.property) + ? node.property.name + : literal(node.property); + if (name !== undefined) add(name, namespaces.get(node.object.name), node); + }); + return links; +}; + +const scriptsBelow = async (directory: string): Promise => { + const entries = await readdir(directory, { withFileTypes: true }); + const nested = await Promise.all( + entries.map((entry) => { + const path = join(directory, entry.name); + return entry.isDirectory() + ? scriptsBelow(path) + : Promise.resolve(/\.(?:mjs|js)$/u.test(path) ? [path] : []); + }), + ); + return nested.flat(); +}; + +const linkFailures = async ( + file: string, + source: string, + symbols: ReadonlySet, +) => { + const failures: string[] = []; + const links = metadataLinks(source, symbols); + for (const { name, reference, line } of links) { + if (reference === undefined) { + failures.push( + `${relative(root, file)}:${line}: unresolved ${name} import`, + ); + continue; + } + const target = reference.startsWith("file:") + ? fileURLToPath(reference) + : reference.startsWith("dist/") + ? resolve(root, reference) + : resolve(dirname(file), reference); + const compiled = relative(root, target).replaceAll("\\", "/"); + if (!compiled.startsWith("dist/")) continue; + const sourcePath = resolve( + root, + compiled.replace(/^dist\//u, "src/"), + ).replace(/\.js$/u, ".ts"); + const exports = exportedValues(syntax(await readFile(sourcePath, "utf8"))); + if (!exports.has(name)) + failures.push( + `${relative(root, file)}:${line}: ${compiled} has no ${name} export`, + ); + } + return { failures, checked: links.length }; +}; + +it("links verifier metadata exports through static, namespace and packaged dynamic imports", async () => { + const symbols = await metadataSymbols(); + const files = [ + ...(await scriptsBelow(resolve(root, "scripts"))), + ...(await scriptsBelow(resolve(root, "tests/fixtures"))), + ]; + const failures: string[] = []; + let checked = 0; + for (const file of files) { + const result = await linkFailures( + file, + await readFile(file, "utf8"), + symbols, + ); + failures.push(...result.failures); + checked += result.checked; + } + expect(checked).toBeGreaterThan(0); + expect(failures).toEqual([]); +}); + +it.each([ + 'import { HOPPER_PROVIDER_IDENTITY } from "../dist/hopper/HopperProviderCapabilities.js";', + 'const { GHIDRA_OPERATIONS } = await import(pathToFileURL(join(packageRoot, "dist/ghidra/GhidraProviderCapabilities.js")));', + 'const metadata = await import("../dist/ghidra/GhidraProviderCapabilities.js"); metadata.GHIDRA_OPERATIONS;', +])( + "detects a metadata consumer whose old owner no longer exports its requirement: %s", + async (source) => { + const result = await linkFailures( + resolve(root, "scripts/authored-negative-control.mjs"), + source, + await metadataSymbols(), + ); + expect(result.failures).toHaveLength(1); + expect(result.failures[0]).toMatch( + / has no (?:HOPPER_PROVIDER_IDENTITY|GHIDRA_OPERATIONS) export$/u, + ); + }, +); diff --git a/tests/evaluation/importSpecifiers.test.ts b/tests/evaluation/importSpecifiers.test.ts index db7225d73b..067aec1738 100644 --- a/tests/evaluation/importSpecifiers.test.ts +++ b/tests/evaluation/importSpecifiers.test.ts @@ -1,10 +1,26 @@ import { describe, expect, it } from "vitest"; +import { fileURLToPath } from "node:url"; +import ts from "typescript6"; import { importSpecifier, readImportSpecifiers, } from "../../scripts/lib/import-specifiers.mjs"; +// Convert only compiler options; this does not discover or compile the project. +const configuration = ts.readConfigFile( + fileURLToPath(new URL("../../tsconfig.json", import.meta.url)), + ts.sys.readFile.bind(ts.sys), +); +if (configuration.error !== undefined) + throw new Error("Could not read repository compiler options"); +const compilerOptions = ts.convertCompilerOptionsFromJson( + configuration.config.compilerOptions, + fileURLToPath(new URL("../../", import.meta.url)), +); +if (compilerOptions.errors.length > 0) + throw new Error("Could not convert repository compiler options"); + describe("import specifier reader", () => { it("reads side-effect imports and ignores import-like text", () => { expect( @@ -35,15 +51,55 @@ const computed = import(name); import "node:fs"; `); expect(facts).toEqual([ - { specifier: "./types.js", line: 1 }, - { specifier: "./all.js", line: 2 }, - { specifier: "./equals.js", line: 3 }, - { specifier: "./query.js", line: 4 }, - { specifier: "./template.js", line: 5 }, - { specifier: "node:fs", line: 7 }, + { specifier: "./types.js", line: 1, typeOnly: true }, + { specifier: "./all.js", line: 2, typeOnly: false }, + { specifier: "./equals.js", line: 3, typeOnly: false }, + { specifier: "./query.js", line: 4, typeOnly: true }, + { specifier: "./template.js", line: 5, typeOnly: false }, + { specifier: "node:fs", line: 7, typeOnly: false }, ]); }); + it.each([ + ['import type Default from "./owner.js";', true], + ['import type * as Namespace from "./owner.js";', true], + ['import { type Port, type Other as Alias } from "./owner.js";', false], + ['import { type Port, Constructor as Alias } from "./owner.js";', false], + ['import Default, { type Port } from "./owner.js";', false], + ['import {} from "./owner.js";', false], + ['import "./owner.js";', false], + ['export type { Port } from "./owner.js";', true], + ['export { type Port, type Other as Alias } from "./owner.js";', false], + ['export { type Port, Constructor as Alias } from "./owner.js";', false], + ['export {} from "./owner.js";', false], + ['export type * from "./owner.js";', true], + ['export type * as Namespace from "./owner.js";', true], + ['export * as Namespace from "./owner.js";', false], + ['import type Owner = require("./owner.js");', true], + ['import Owner = require("./owner.js");', false], + ['type Port = import("./owner.js").Port;', true], + ['type Constructor = typeof import("./owner.js").Constructor;', true], + ['const Owner = import("./owner.js");', false], + ["const Owner = import(`./owner.js`);", false], + ])("matches TypeScript's emitted runtime edge for %s", (source, typeOnly) => { + expect(readImportSpecifiers(source)).toEqual([ + { specifier: "./owner.js", line: 1, typeOnly }, + ]); + expect(compilerOptions.options.verbatimModuleSyntax).toBe(true); + const emitted = ts.transpileModule(source, { + compilerOptions: compilerOptions.options, + // NodeNext emits import-equals through createRequire in an ESM file. + fileName: fileURLToPath(new URL("./import-owner.mts", import.meta.url)), + reportDiagnostics: true, + }); + expect( + emitted.diagnostics?.filter( + (diagnostic) => diagnostic.category === ts.DiagnosticCategory.Error, + ), + ).toEqual([]); + expect(emitted.outputText.includes("./owner.js")).toBe(!typeOnly); + }); + it("ignores nodes that are not static imports", () => { expect(importSpecifier({ type: "Identifier" })).toBeUndefined(); }); diff --git a/tests/evaluation/moduleBoundaries.test.ts b/tests/evaluation/moduleBoundaries.test.ts index 15e6c0d0d7..75f0bd4f4c 100644 --- a/tests/evaluation/moduleBoundaries.test.ts +++ b/tests/evaluation/moduleBoundaries.test.ts @@ -6,6 +6,7 @@ import { promisify } from "node:util"; import { describe, expect, it } from "vitest"; import { inspectModuleBoundaries } from "../../scripts/lib/module-boundaries.mjs"; +import { projectForTest } from "../../scripts/lib/test-projects.mjs"; import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; import { writeRepositoryOxlintConfiguration } from "../fixtures/oxlintConfiguration.js"; @@ -65,6 +66,113 @@ describe("binary production construction ownership", () => { }); }); +describe("live capability constructor ownership", () => { + it.each([ + "frida/FridaInstrumentationManager.js", + "cutter/CutterBridgeClient.js", + "cutter/CutterBridgeService.js", + "gdb/GdbSessionManager.js", + "rizin/RizinDebugSessionManager.js", + ])( + "keeps %s values in composition while accepting injected type ports", + (owner) => { + const source = `import { Constructor as Alias } from "../${owner}";`; + const types = `import type { Port } from "../${owner}";`; + for (const file of ["src/application/probe.ts", "src/server/probe.ts"]) { + expect( + inspectModuleBoundaries(file, source, process.cwd()), + ).toMatchObject([{ boundary: "provider-construction" }]); + expect(inspectModuleBoundaries(file, types, process.cwd())).toEqual([]); + } + expect( + inspectModuleBoundaries( + "src/composition/probe.ts", + source, + process.cwd(), + ), + ).toEqual([]); + }, + ); + + it.each([ + [ + 'import { Constructor as Alias } from "../frida/FridaInstrumentationManager.js";', + false, + ], + [ + 'import * as Owner from "../frida/FridaInstrumentationManager.js";', + false, + ], + [ + 'import { type Port, Constructor } from "../frida/FridaInstrumentationManager.js";', + false, + ], + [ + 'import { type Port as Alias } from "../frida/FridaInstrumentationManager.js";', + false, + ], + ['import "../frida/FridaInstrumentationManager.js";', false], + [ + 'export { Constructor as Alias } from "../frida/FridaInstrumentationManager.js";', + false, + ], + [ + 'export { type Port as Alias } from "../frida/FridaInstrumentationManager.js";', + false, + ], + ['export type * from "../frida/FridaInstrumentationManager.js";', true], + ['export * from "../frida/FridaInstrumentationManager.js";', false], + [ + 'type Port = import("../frida/FridaInstrumentationManager.js").Port;', + true, + ], + ['const owner = import("../frida/FridaInstrumentationManager.js");', false], + ["const owner = import(`../frida/FridaInstrumentationManager.js`);", false], + [ + 'import owner = require("../frida/FridaInstrumentationManager.js");', + false, + ], + [ + 'import type Owner = require("../frida/FridaInstrumentationManager.js");', + true, + ], + [ + 'import { Constructor } from "../domain/../frida/FridaInstrumentationManager.js";', + false, + ], + [ + 'const owner = import("../%66rida/FridaInstrumentationManager.js?view=one#part");', + false, + ], + ])("checks type/value ownership in %s", (source, allowed) => { + const violations = inspectModuleBoundaries( + "src/server/probe.ts", + source, + process.cwd(), + ); + if (allowed) expect(violations).toEqual([]); + else + expect(violations).toMatchObject([{ boundary: "provider-construction" }]); + }); + + it("does not broaden the new guard to other provider-owned policy helpers", () => { + expect( + inspectModuleBoundaries( + "src/server/probe.ts", + 'import { policy } from "../frida/fridaCaptureLimits.js";', + process.cwd(), + ), + ).toEqual([]); + }); + + it.each([ + "src/frida/FridaInstrumentationManager.test.ts", + "src/frida/nested/FutureProvider.test.ts", + ])("assigns %s to the adapter test project", (path) => { + expect(projectForTest(path).name).toBe("adapters"); + }); +}); + const execute = promisify(execFile); const verifier = resolve("scripts/verify-module-boundaries.mjs"); diff --git a/tests/process-global/historicalCaptureSnapshotRedaction.test.ts b/tests/process-global/historicalCaptureSnapshotRedaction.test.ts index ff195957f4..f678533ad1 100644 --- a/tests/process-global/historicalCaptureSnapshotRedaction.test.ts +++ b/tests/process-global/historicalCaptureSnapshotRedaction.test.ts @@ -33,7 +33,7 @@ vi.mock("node:fs/promises", async (importOriginal) => { }); import { HistoricalCaptureDecoder } from "../../src/browser/history/HistoricalCaptureDecoder.js"; -import { HAR_CAPTURE_PROVIDER_IDENTITY } from "../../src/browser/history/CaptureRelease.js"; +import { HAR_CAPTURE_PROVIDER_IDENTITY } from "../../src/browser/history/CaptureProviderIdentities.js"; import { WebNetworkCaptureService } from "../../src/application/WebNetworkCaptureService.js"; import { projectAnalysisError } from "../../src/domain/analysisErrorProjection.js"; import { fileSnapshotPartialObservationSchema } from "../../src/domain/fileSnapshotObservation.js"; diff --git a/tests/process-global/historicalCaptureSourceSnapshots.test.ts b/tests/process-global/historicalCaptureSourceSnapshots.test.ts index 3010d6cc09..1f1575e6ae 100644 --- a/tests/process-global/historicalCaptureSourceSnapshots.test.ts +++ b/tests/process-global/historicalCaptureSourceSnapshots.test.ts @@ -59,7 +59,7 @@ vi.mock("node:fs/promises", async (importOriginal) => { }); import { HistoricalCaptureDecoder } from "../../src/browser/history/HistoricalCaptureDecoder.js"; -import { HAR_CAPTURE_PROVIDER_IDENTITY } from "../../src/browser/history/CaptureRelease.js"; +import { HAR_CAPTURE_PROVIDER_IDENTITY } from "../../src/browser/history/CaptureProviderIdentities.js"; import { decodeHarCapture } from "../../src/browser/history/HarCapture.js"; import { inspectWebNetworkCaptureInputSchema } from "../../src/domain/webNetworkCapture.js"; import { projectAnalysisError } from "../../src/domain/analysisErrorProjection.js"; diff --git a/tests/process-global/pwntoolsCliFinalization.test.ts b/tests/process-global/pwntoolsCliFinalization.test.ts index 97c0c09f0f..6f264db5fe 100644 --- a/tests/process-global/pwntoolsCliFinalization.test.ts +++ b/tests/process-global/pwntoolsCliFinalization.test.ts @@ -18,7 +18,7 @@ import { resolvedAnalysisError } from "../../src/domain/analysisErrorBase.js"; import { projectAnalysisError } from "../../src/domain/analysisErrorProjection.js"; import { PwntoolsLayoutProvider } from "../../src/native/pwntools/PwntoolsLayoutProvider.js"; import type { PwntoolsLauncher } from "../../src/native/pwntools/PwntoolsDecoder.js"; -import { PWNTOOLS_PROVIDER_IDENTITY } from "../../src/native/pwntools/PwntoolsRelease.js"; +import { PWNTOOLS_PROVIDER_IDENTITY } from "../../src/native/pwntools/PwntoolsProviderIdentity.js"; import { PrivateRuntimeRoot } from "../../src/process/PrivateRuntimeRoot.js"; import { cleanupOwnedProcessGroup } from "../../src/process/ProcessOwnership.js"; import { prepareProcessOwnershipInspection } from "../../src/process/ProcessOwnershipObservation.js"; diff --git a/tests/process-global/pwntoolsStageRetirement.test.ts b/tests/process-global/pwntoolsStageRetirement.test.ts index 1ea6b0a146..3e48a4e10d 100644 --- a/tests/process-global/pwntoolsStageRetirement.test.ts +++ b/tests/process-global/pwntoolsStageRetirement.test.ts @@ -23,10 +23,8 @@ import { primaryAnalysisError } from "../../src/domain/analysisErrorBase.js"; import { ProviderCleanupError } from "../../src/domain/providerCleanupError.js"; import { PwntoolsRecordedCrashProvider } from "../../src/native/pwntools/PwntoolsRecordedCrashProvider.js"; import type { PwntoolsLauncher } from "../../src/native/pwntools/PwntoolsDecoder.js"; -import { - PWNTOOLS_MEMORY_FAILURE_EXIT, - PWNTOOLS_PROVIDER_IDENTITY, -} from "../../src/native/pwntools/PwntoolsRelease.js"; +import { PWNTOOLS_MEMORY_FAILURE_EXIT } from "../../src/native/pwntools/PwntoolsRelease.js"; +import { PWNTOOLS_PROVIDER_IDENTITY } from "../../src/native/pwntools/PwntoolsProviderIdentity.js"; import { cleanupOwnedProcessGroup, type OwnedProcessGroup,