Merge remote-tracking branch 'origin/main' into feat/pkg-inventory

# Conflicts:
#	docs/product-catalog.json
#	docs/verification/managed-conformance-manifest.json
#	skills/reverse-engineer-anything/SKILL.md
#	src/generatedMcpToolCatalog.ts
This commit is contained in:
Kaoru0822 2026-10-07 12:44:32 +00:00
commit 667bf164f9
144 changed files with 2774 additions and 573 deletions

View File

@ -239,7 +239,7 @@ jobs:
if-no-files-found: error
package-e2e:
name: Package/install integration (${{ matrix.os }})
name: Native package baseline (${{ matrix.platform }} ${{ matrix.architecture }})
needs: [changes, windows-native-build]
if: github.event_name == 'push' || needs.changes.outputs.package == 'true'
strategy:
@ -253,7 +253,7 @@ jobs:
- os: ubuntu-24.04-arm
platform: linux
architecture: arm64
- os: macos-14
- os: macos-15
platform: darwin
architecture: arm64
- os: macos-15-intel
@ -286,12 +286,15 @@ jobs:
path: native/windows/build/
- run: npm run verify:windows-native:artifact
- run: npm run verify:package
- name: Verify real Node Inspector discovery and observation
if: matrix.platform == 'darwin'
run: npm run verify:inspector
apple-artifacts:
name: Real Apple artifact CLI/MCP E2E
needs: changes
if: github.event_name == 'push' || needs.changes.outputs.code == 'true'
runs-on: macos-14
runs-on: macos-15
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
@ -355,12 +358,6 @@ jobs:
- os: ubuntu-24.04-arm
platform: linux
architecture: arm64
- os: macos-14
platform: darwin
architecture: arm64
- os: macos-15-intel
platform: darwin
architecture: x64
- os: windows-latest
platform: win32
architecture: x64

View File

@ -17,7 +17,9 @@
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&amp;utm_medium=badge&amp;utm_campaign=badge-repository-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/82054" alt="morluto%2Frea | Trendshift" width="250" height="55"/></a>
[Documentation](https://morluto.github.io/rea/) · [Quick start](#quick-start) · [Current status](#current-status) · [Investigation model](#the-investigation-model) · [Tool catalog](#tool-catalog-for-investigation) · [Roadmap](#roadmap) · [How it works](#how-it-works)
**[Website](https://morluto.github.io/rea/) · [Guides](https://morluto.github.io/rea/guides/) · [DX-Ball showcase](https://morluto.github.io/rea/showcase/dx-ball/)**
[Quick start](#quick-start) · [Current status](#current-status) · [Investigation model](#the-investigation-model) · [Tool catalog](#tool-catalog-for-investigation) · [Roadmap](#roadmap) · [How it works](#how-it-works)
<code>npx rea-agents setup</code>

View File

@ -17,6 +17,8 @@
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&amp;utm_medium=badge&amp;utm_campaign=badge-repository-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/82054" alt="morluto%2Frea | Trendshift" width="250" height="55"/></a>
**[الموقع (بالإنجليزية)](https://morluto.github.io/rea/) · [أدلة الاستخدام](https://morluto.github.io/rea/guides/) · [دراسة حالة DX-Ball](https://morluto.github.io/rea/showcase/dx-ball/)**
[البدء السريع](#البدء-السريع) · [حالة الدعم](#حالة-الدعم) · [من الملف التنفيذي إلى السلوك](#من-الملف-التنفيذي-إلى-السلوك) · [منصة أدوات التحقيق](#منصة-أدوات-التحقيق) · [خطة العمل](#خطة-العمل) · [كيف يعمل؟](#كيف-يعمل)
<table aria-label="REA community">

View File

@ -17,6 +17,8 @@
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&amp;utm_medium=badge&amp;utm_campaign=badge-repository-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/82054" alt="morluto%2Frea | Trendshift" width="250" height="55"/></a>
**[ウェブサイト(英語)](https://morluto.github.io/rea/) · [ガイド](https://morluto.github.io/rea/guides/) · [DX-Ball の事例](https://morluto.github.io/rea/showcase/dx-ball/)**
[クイックスタート](#クイックスタート) · [現在の対応状況](#現在の対応状況) · [バイナリから動作へ](#バイナリから動作へ) · [調査ツールカタログ](#調査ツールカタログ) · [ロードマップ](#ロードマップ) · [仕組み](#仕組み)
<table aria-label="REA community">

View File

@ -17,6 +17,8 @@
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&amp;utm_medium=badge&amp;utm_campaign=badge-repository-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/82054" alt="morluto%2Frea | Trendshift" width="250" height="55"/></a>
**[웹사이트(영문)](https://morluto.github.io/rea/) · [가이드](https://morluto.github.io/rea/guides/) · [DX-Ball 사례](https://morluto.github.io/rea/showcase/dx-ball/)**
[빠른 시작](#빠른-시작) · [현재 지원 범위](#현재-지원-범위) · [바이너리에서 동작까지](#바이너리에서-동작까지) · [조사 도구 카탈로그](#조사-도구-카탈로그) · [로드맵](#로드맵) · [작동 방식](#작동-방식)
<table aria-label="REA community">

View File

@ -17,6 +17,8 @@
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&amp;utm_medium=badge&amp;utm_campaign=badge-repository-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/82054" alt="morluto%2Frea | Trendshift" width="250" height="55"/></a>
**[网站(英文)](https://morluto.github.io/rea/) · [使用指南](https://morluto.github.io/rea/guides/) · [DX-Ball 案例](https://morluto.github.io/rea/showcase/dx-ball/)**
[快速开始](#快速开始) · [当前状态](#当前状态) · [从二进制到行为](#从二进制到行为) · [调查工具目录](#调查工具目录) · [路线图](#路线图) · [工作原理](#工作原理)
<table aria-label="REA community">

View File

@ -0,0 +1,11 @@
struct ChildBatch<Element> {
let values: [Element]
let complete: Bool
}
func captureChildValues<Element>(requestedCount: Int, copy: () -> [Element]?) -> ChildBatch<Element> {
guard requestedCount > 0 else { return ChildBatch(values: [], complete: true) }
let returned = copy() ?? []
let values = Array(returned.prefix(requestedCount))
return ChildBatch(values: values, complete: values.count == requestedCount)
}

View File

@ -26,11 +26,12 @@ func childCount(_ element: AXUIElement) -> Int {
var count = 0
return AXUIElementGetAttributeValueCount(element, kAXChildrenAttribute as CFString, &count) == .success ? count : 0
}
func children(_ element: AXUIElement, limit: Int) -> [AXUIElement] {
var value: CFArray?
let count = min(limit, childCount(element))
if count == 0 { return [] }
return AXUIElementCopyAttributeValues(element, kAXChildrenAttribute as CFString, 0, count, &value) == .success ? value as? [AXUIElement] ?? [] : []
func children(_ element: AXUIElement, count: Int) -> ChildBatch<AXUIElement> {
captureChildValues(requestedCount: count) {
var value: CFArray?
guard AXUIElementCopyAttributeValues(element, kAXChildrenAttribute as CFString, 0, count, &value) == .success else { return nil }
return value as? [AXUIElement]
}
}
func text(_ element: AXUIElement, _ key: String) -> Any { (attribute(element, key) as? String) ?? NSNull() as Any }
func windowBounds(_ element: AXUIElement) -> CGRect? {
@ -115,9 +116,12 @@ func observe(_ request: Request) async throws -> [String: Any] {
if path.count >= 32 { if totalChildren > 0 { truncated = true }; continue }
let available = max(0, request.max_nodes - nodes.count - pending.count)
let count = min(available, totalChildren)
let items = children(element, limit: count)
let batch = children(element, count: count)
if !batch.complete { truncated = true }
if totalChildren > count { truncated = true }
for index in (0..<count).reversed() { pending.append((items[index], path + [index])) }
for (index, item) in batch.values.enumerated().reversed() {
pending.append((item, path + [index]))
}
}
}
var screenshot: Any = NSNull()

View File

@ -96,9 +96,11 @@ The components also list:
Symlinks are reported by path in `symlinks`. Their targets are not inventoried,
so a versioned framework is described through its real `Versions/<version>/`
directories rather than `Versions/Current`. When a framework has zero or several
`Versions/*/Resources/Info.plist` files, its `info_plist_path` is `null` and a
limitation explains why.
directories rather than `Versions/Current`. A framework's `info_plist_path` is
`null`, with a limitation explaining why, when it has more than one real
`Versions/<version>/` directory (even if only one of them holds a plist,
because `Versions/Current` decides which one applies) or when its single
version directory has no `Resources/Info.plist`.
AppleDouble sidecar files (`._*` and `__MACOSX/`) are inventory facts, but they
describe neighbouring files. They are excluded from roots, bundle roles, and the

View File

@ -295,6 +295,30 @@ lifecycles. Runtime ports retain their distinct Inspector, passive Electron and
active Electron effects; shipped Electron hooks remain at their existing
provider boundary.
## Managed capability ownership
`src/domain/managed/` owns managed artifact and member semantics, reconstruction
import, native-boundary verification and application graph projection.
`src/application/managed/` owns their shared Evidence workflows, and
`src/contracts/managed/` owns their named contracts and examples. Producer
metadata/IL parsing stays in `src/dotnet/`; target resolution, Evidence, ordering
and investigation records retain their shared owners. Path-based member
comparison preserves its actual target/byte-digest admission and existing parser
helper.
`scripts/verify/managed/` groups the portable conformance entrypoint and its
support, manifest, oracle and completion-report helpers. Its PE byte fixture
builder and declaration live in `scripts/fixtures/managed/`, shared with the
installed-package lane and existing managed conformance tests. `verify:managed`
keeps its command name and optional caller-cwd manifest resolution. Compiled
catalog and generator imports follow the owners; document-check cache inputs
include the relocated runtime verifier and fixture files.
Portable byte fixtures, optional operator-local manifests, actual ILSpy oracles
and real Ghidra NativeAOT checks retain their distinct proof levels and
prerequisites. The existing NativeAOT fixture/engine lane keeps its owner. This
move preserves implementations, Evidence identities and generated commitments.
## Binary application ownership
`src/application/binary/` owns the active session, deep-provider registry and

View File

@ -39,9 +39,20 @@ explicit limitation; architecture inventory locations remain available.
vtable descriptors. Records include exact virtual addresses/file offsets and
artifact evidence. Vtable indexes are metadata word offsets; witness indexes
start after the conformance header. Names unavailable in metadata remain null.
Chained fixups, external bindings, authenticated pointers, categories,
properties, generic/resilient/async/coroutine tables and inherited overrides
have explicit unsupported or partial coverage. Other providers retain the
Pointers are decoded through the image's fixups:
- `LC_DYLD_CHAINED_FIXUPS` rebases and binds, including the authenticated
arm64e formats;
- legacy `LC_DYLD_INFO` bind opcodes.
So an external superclass resolves from its `_OBJC_CLASS_$_` bind (for
example `NSObject`). The `pointer_fixups` coverage facet names the mechanism.
Class properties, with parsed attributes, and `__objc_catlist` categories are
also decoded. Categories record the extended class, local or external, plus
their methods, protocols and properties; category methods appear as
implementations with a `category`. Swift field-offset globals that are only
initialized at runtime stay unresolved. Generic, resilient, async and
coroutine tables, and inherited overrides, have explicit unsupported or
partial coverage. Other providers retain the
existing symbol-based inventory with its narrower coverage.
## Native instruction, call and type primitives
@ -165,8 +176,10 @@ GUI authority, and REA never falls back automatically to Hopper.
instruction/call facts, value dependencies and process/project cleanup.
- `npm run verify:ghidra:aarch64-jump-table`: optimized ELF and byte/halfword
relative tables, plus ARM64 Mach-O on an ARM64 macOS host.
- `npm run verify:apple-dispatch`: source-built Objective-C protocols/classes and
Swift conformances/vtables, repeated after stripping local symbols.
- `npm run verify:apple-dispatch`: source-built Objective-C protocols, classes,
properties and categories, and Swift conformances and vtables. Each is linked
with legacy `LC_DYLD_INFO` and with chained fixups (and as arm64e on Apple
silicon), then repeated after stripping local symbols.
- `npm run verify:native-ui`: one source-owned fixture window, successful
selected-window capture and actions, changed-target rejection, and cleanup.
Missing OS permissions fail this lane.

View File

@ -669,9 +669,9 @@
"mcp_prompts": 6
},
"digests": {
"tools_sha256": "620bc21113bcc2ed5775f3bdb0a59a3d6208e8aec9d918f220b5c7c85e9e4dd6",
"tools_sha256": "3381610341f922f75fdc2e2201fe54e66e7f431fee3b4316c95d85e2534749b8",
"prompts_sha256": "ef2de8814390b340623c32001610aa2709d90bed51537b6e562ab3abf144ab4d",
"combined_sha256": "d18386ad8f46e7035cfa49f075c42e5c143f5b3f05c9459eb2d1c23c11112871",
"combined_sha256": "ab7efdea56f2e319bafbf1e1fc7d8ff998f3da1845968acb1614fc5be4dd6250",
"providers_sha256": "a7d885531ee25d42c51385444faec544d34cd7eba6250a7dc75c4faedd4454a3"
}
}

View File

@ -61,6 +61,16 @@ needed, exercise the actual target through browser, Electron, or process
capture. Real model trials are manual; Vitest covers deterministic evaluator
logic.
`verify:managed` runs the portable PE byte-fixture conformance entrypoint under
`scripts/verify/managed/`, with its byte builder under
`scripts/fixtures/managed/`. It checks static classification, members,
reconstruction, native-boundary relationships and application graphs without
executing fixture PE files. Operator-local manifests and actual ILSpy oracles
remain optional, separately reported checks; the real Ghidra NativeAOT lane has
its own toolchain prerequisites. See [the managed guide](managed-code-analysis.md)
for those configurations. Generated completion-ledger checks use the same owning
entrypoint and include its verifier/fixture files in their cache inputs.
## End-to-end, integration and golden evidence
Full E2E tests invoke the production command dispatcher and real providers,
@ -335,8 +345,8 @@ CI exercises the pinned Node.js runtime on native hosted runners:
| --------------------- | ------------------ | ---------------------------------------------------------------------------------------- |
| Linux x64 | `ubuntu-latest` | Installed package and real Node Inspector CLI/MCP |
| Linux arm64 (aarch64) | `ubuntu-24.04-arm` | Installed package and real Node Inspector CLI/MCP |
| macOS arm64 | `macos-14` | Installed package and real Node Inspector CLI/MCP |
| macOS x64 | `macos-15-intel` | Installed package and real Node Inspector CLI/MCP |
| macOS 15 arm64 | `macos-15` | Installed package and real Node Inspector CLI/MCP |
| macOS 15 x64 | `macos-15-intel` | Installed package and real Node Inspector CLI/MCP |
| Windows x64 | `windows-latest` | Curated capabilities, native controls, installed package and real Node Inspector CLI/MCP |
Package and Inspector matrices assert the actual Node platform/architecture
@ -346,6 +356,11 @@ limits. Package checks cover installation, CLI/MCP discovery, target-free
analysis, configuration backups/recovery, Evidence and owned lifecycle; Inspector
checks execute source-owned loopback targets and special filename cases.
macOS uses one OS version with one native baseline job per architecture. Each job
runs package and Inspector checks after a single dependency installation. The
separate Inspector matrix covers Linux and Windows; Apple artifact checks retain
their own macOS 15 arm64 job for Xcode-dependent workflows.
These native baseline checks complement the Linux source-test shards and the
separate Apple-artifact and real-provider lanes. Actual Hopper, Ghidra, IDA,
browser and managed-tool claims require their corresponding verification lanes.
@ -456,17 +471,24 @@ that explicit lane, not the routine iteration requirement.
## Apple native metadata and UI
`npm run verify:apple-dispatch` compiles Objective-C class/protocol and Swift
conformance/vtable fixtures, inspects their bytes and repeats after stripping
local symbols. It requires macOS and the host Xcode toolchain; targets are not
executed. `npm run verify:native-ui` launches exactly one source-owned fixture
window and requires successful selected-window capture and selected actions.
An OS permission denial fails the positive lane. `npm run verify:native-ui:permissions`
allows a host-permission-boundary-only result and explicitly reports
`positive_e2e: false`; it must not be reported as capture/action proof.
Both commands reject a changed executable digest and clean up the fixture
process and helper. These lanes require an interactive macOS desktop. See [native investigation](native-investigation.md)
for the exact ABI, authority, graph and observation boundaries.
`npm run verify:apple-dispatch` compiles the Objective-C fixture (classes,
protocols, a property and an `NSString` category) and the Swift
conformance/vtable fixture.
- Each fixture is linked with legacy `LC_DYLD_INFO` binds and with chained
fixups; on Apple silicon the ObjC fixture is also built as arm64e, which uses
authenticated pointers.
- The lane inspects the bytes and repeats after stripping local symbols.
- It requires the bound `NSObject` superclass, the external category, and the
matching `pointer_fixups` coverage. It requires macOS and the host Xcode toolchain; targets are not
executed. `npm run verify:native-ui` launches exactly one source-owned fixture
window and requires successful selected-window capture and selected actions.
An OS permission denial fails the positive lane. `npm run verify:native-ui:permissions`
allows a host-permission-boundary-only result and explicitly reports
`positive_e2e: false`; it must not be reported as capture/action proof.
Both commands reject a changed executable digest and clean up the fixture
process and helper. These lanes require an interactive macOS desktop. See [native investigation](native-investigation.md)
for the exact ABI, authority, graph and observation boundaries.
### Firmware adapters

View File

@ -1,5 +1,5 @@
{
"manifest_id": "ecm_cba6ab6b66603cd59aa60b327d67efe09bb9d004c73d1c7e8e368d86887c686e",
"manifest_id": "ecm_9b3d014e67d8c2ccc8736694d59b38acf6d8ab15d1e9cdeb3998339ff33b5cc2",
"verifier": {
"id": "rea-managed-conformance",
"version": "1"
@ -13,7 +13,7 @@
"skill_digests": [
{
"skill_id": "reverse-engineer-anything",
"sha256": "21bd8b88040d593111905bb47863f5145690c4d5582e0975bab76d2b07fb25b9"
"sha256": "17120894d8defd8c28791e344b9241e8d3abcba5ac345b0c8cca92adda518dfd"
}
],
"claims": [

View File

@ -5,6 +5,7 @@
"vitepress": {
"entry": ["docs/.vitepress/config.ts"]
},
"ignoreBinaries": ["mkfifo"],
"ignoreDependencies": ["electron", "esbuild", "webpack"],
"ignoreExportsUsedInFile": true,
"rules": {

View File

@ -42,6 +42,7 @@
"dist",
"install.sh",
"bridge/hopper_bridge.py",
"bridge/native/NativeUIChildren.swift",
"bridge/native/ReaNativeUI.swift",
"bridge/ghidra/ReaGhidraBridge.java",
"bridge/ghidra/ReaGhidraExtensions.java",
@ -142,7 +143,7 @@
"verify:javascript-runtime": "npm run build:cached && node scripts/verify/javascript/runtime-observation.mjs",
"verify:javascript:digests": "npm run build:cached && node scripts/verify/javascript/digests.mjs",
"verify:application-workflows": "npm run build:cached && node scripts/verify/javascript/application-workflows.mjs",
"verify:managed": "npm run build:cached && node scripts/verify-managed-conformance.mjs",
"verify:managed": "npm run build:cached && node scripts/verify/managed/conformance.mjs",
"verify:firmware": "npm run build:cached && node scripts/verify/firmware/analysis.mjs",
"fixtures:firmware": "node scripts/fixtures/firmware/generate.mjs",
"verify:android": "npm run build:cached && node scripts/verify/android/analysis.mjs",

View File

@ -22,7 +22,7 @@ delete verifierEnvironment.REA_ILSPY_CMD_PATH;
const { stdout } = await exec(
process.execPath,
[join(root, "scripts/verify-managed-conformance.mjs")],
[join(root, "scripts/verify/managed/conformance.mjs")],
{
cwd: root,
env: verifierEnvironment,

View File

@ -6,7 +6,7 @@ import { Ajv2020 } from "ajv/dist/2020.js";
import { McpServer } from "@modelcontextprotocol/server";
import { TOOL_CONTRACTS } from "../dist/contracts/toolContracts.js";
import { MANAGED_WORKFLOW_TOOL_CONTRACTS } from "../dist/contracts/managedWorkflowToolContracts.js";
import { MANAGED_WORKFLOW_TOOL_CONTRACTS } from "../dist/contracts/managed/managedWorkflowToolContracts.js";
import { auxiliaryAnalysisProviderDeclarations } from "../dist/composition/auxiliaryAnalysisProviders.js";
import { toolRegistrationOptions } from "../dist/server/toolRegistrationOptions.js";
import { ensureGeneratedFile } from "./lib/generated-file.mjs";

View File

@ -20,7 +20,8 @@ import time
# Keep this digest aligned with LinuxHopper.ts and rerun real-Hopper verification
# before accepting another vendor build.
SUPPORTED_HOPPER_SHA256 = {
"0294ced141cc373468ee22d8343e7dac41980cb05a937994ca81c9f09afe7ded"
"0294ced141cc373468ee22d8343e7dac41980cb05a937994ca81c9f09afe7ded",
"1339f9e58377442b0c6fcb0dfc3cec20d593cc557408521fad9a00dbc6b8da13",
}
EXPECTED_SCREEN = (1280, 1024)
EXPECTED_DIALOG = (189, 370, 901, 284)

View File

@ -42,12 +42,13 @@ const SOURCE_PATHS = {
sessionContracts: "dist/contracts/sessionToolContracts.js",
nativeContracts: "dist/contracts/nativeToolContracts.js",
artifactContracts: "dist/contracts/artifactToolContracts.js",
managedContracts: "dist/contracts/managedToolContracts.js",
managedContracts: "dist/contracts/managed/managedToolContracts.js",
firmwareContracts: "dist/contracts/firmware/firmwareToolContracts.js",
firmwareProvider: "dist/firmware/FirmwareRelease.js",
androidContracts: "dist/contracts/android/androidToolContracts.js",
androidProvider: "dist/android/JadxRelease.js",
managedWorkflowContracts: "dist/contracts/managedWorkflowToolContracts.js",
managedWorkflowContracts:
"dist/contracts/managed/managedWorkflowToolContracts.js",
browserContracts: "dist/contracts/browserToolContracts.js",
browserScenarioContracts: "dist/contracts/browserScenarioToolContracts.js",
electronContracts: "dist/contracts/javascript/electronToolContracts.js",
@ -88,9 +89,9 @@ const SOURCE_PATHS = {
"dist/domain/javascript/javascriptRuntimeObservation.js",
webBundleAnalysis: "dist/domain/webBundleAnalysis.js",
webCaptureDiff: "dist/domain/webCaptureDiff.js",
managedArtifact: "dist/domain/managedArtifact.js",
managedComparison: "dist/domain/managedMemberComparison.js",
managedNativeVerification: "dist/domain/managedNativeVerification.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:

View File

@ -12,118 +12,190 @@ if (process.platform !== "darwin")
"Apple dispatch verification requires macOS with a host Objective-C compiler and Foundation SDK",
);
const exec = promisify(execFile);
const source = (name) =>
fileURLToPath(
new URL(`../tests/conformance/native/${name}`, import.meta.url),
);
/** Both link modes: legacy LC_DYLD_INFO binds and the default chained fixups. */
const LINK_MODES = [
{
mode: "dyld-info",
flags: ["-Wl,-no_fixup_chains"],
fixups: "LC_DYLD_INFO bind opcodes",
swift: true,
},
{ mode: "chained", flags: [], fixups: "chained fixups:", swift: true },
// arm64e uses authenticated chained pointers (DYLD_CHAINED_PTR_ARM64E_*).
...(process.arch === "arm64"
? [
{
mode: "chained-arm64e",
flags: ["-arch", "arm64e"],
fixups: "chained fixups: DYLD_CHAINED_PTR_ARM64E",
swift: false,
},
]
: []),
];
const fixupCoverage = (metadata) =>
metadata.coverage.find(({ facet }) => facet === "pointer_fixups");
const inspect = async (path) => {
const target = await parseBinaryTarget(path);
if (!target.ok) throw target.error;
return {
target: target.value,
metadata: (await inspectAppleDispatchMetadata(target.value, 5000)).result,
};
};
const checkObjc = (metadata, mode) => {
const fixture = metadata.objc_classes.find(
({ name, is_meta_class: meta }) => name === "ReaDispatchFixture" && !meta,
);
const category = metadata.objc_categories.find(
({ name }) => name === "ReaDispatchAdditions",
);
const objcCoverage = metadata.coverage.find(
({ facet }) => facet === "objc_class_method_ivar_metadata",
);
if (
!metadata.objc_dispatch_implementations.some(
(item) =>
item.class_name === "ReaDispatchFixture" &&
item.selector === "performAction:" &&
item.implementation_address !== null,
) ||
!metadata.objc_dispatch_implementations.some(
(item) =>
item.selector === "fixtureVersion" && item.method_type === "class",
) ||
!metadata.objc_ivars.some(
(item) => item.name === "state" && item.offset !== null,
) ||
!metadata.objc_protocol_records.some(
(protocol) =>
protocol.name === "ReaDispatchProtocol" &&
protocol.methods.some(
(method) => method.selector === "performAction:",
) &&
protocol.optional_methods.some(
(method) => method.selector === "optionalFixtureValue",
),
) ||
// The superclass lives in libobjc, so it comes from a bind fixup.
fixture?.super_class !== "NSObject" ||
fixture.decode.status !== "decoded" ||
!fixture.properties.some(
(property) =>
property.name === "fixtureName" &&
property.type_encoding === '@"NSString"' &&
property.attributes.some(({ name }) => name === "C"),
) ||
category?.class_name !== "NSString" ||
category.class_source !== "external" ||
!category.instance_methods.includes("reaFixtureLength") ||
!metadata.objc_dispatch_implementations.some(
(item) =>
item.class_name === "NSString" &&
item.category === "ReaDispatchAdditions" &&
item.selector === "reaFixtureLength",
) ||
objcCoverage?.status !== "complete" ||
fixupCoverage(metadata)?.reason?.startsWith(mode.fixups) !== true
)
throw new Error(
`Apple metadata fixture drifted (${mode.mode}): ${JSON.stringify(metadata)}`,
);
};
const checkSwift = (metadata, mode) => {
if (
!metadata.swift_dispatch_slots.some(
(slot) =>
slot.table_kind === "class_vtable" &&
slot.owner === "ReaVtableFixture" &&
slot.decode.status === "decoded",
)
)
throw new Error(
`Swift class vtable fixture unavailable (${mode.mode}): ${JSON.stringify(metadata.coverage)}`,
);
if (
!metadata.swift_conformances.some(
(item) =>
item.type_name === "ReaWitnessFixture" &&
item.protocol_name === "ReaWitnessFixtureProtocol",
) ||
!metadata.swift_dispatch_slots.some(
(item) =>
item.owner === "ReaWitnessFixture: ReaWitnessFixtureProtocol" &&
item.implementation_address !== null,
) ||
fixupCoverage(metadata)?.reason?.startsWith(mode.fixups) !== true
)
throw new Error(
`Swift metadata fixture drifted (${mode.mode}): ${JSON.stringify(metadata)}`,
);
};
const root = await mkdtemp(join(tmpdir(), "rea-dispatch-"));
try {
const targetPath = join(root, "fixture");
await exec("/usr/bin/xcrun", [
"clang",
"-O2",
"-framework",
"Foundation",
"-Wl,-no_fixup_chains",
fileURLToPath(
new URL("../tests/conformance/native/dispatch.m", import.meta.url),
),
"-o",
targetPath,
]);
const results = [];
for (const variant of ["symbols", "stripped"]) {
if (variant === "stripped")
await exec("/usr/bin/strip", ["-x", targetPath]);
const target = await parseBinaryTarget(targetPath);
if (!target.ok) throw target.error;
const observation = await inspectAppleDispatchMetadata(target.value, 5000);
const metadata = observation.result;
if (
!metadata.objc_dispatch_implementations.some(
(item) =>
item.class_name === "ReaDispatchFixture" &&
item.selector === "performAction:" &&
item.implementation_address !== null,
) ||
!metadata.objc_dispatch_implementations.some(
(item) =>
item.selector === "fixtureVersion" && item.method_type === "class",
) ||
!metadata.objc_ivars.some(
(item) => item.name === "state" && item.offset !== null,
) ||
!metadata.objc_protocol_records.some(
(protocol) =>
protocol.name === "ReaDispatchProtocol" &&
protocol.methods.some(
(method) => method.selector === "performAction:",
) &&
protocol.optional_methods.some(
(method) => method.selector === "optionalFixtureValue",
),
)
)
throw new Error(
`Apple metadata fixture drifted: ${JSON.stringify(observation)}`,
);
results.push({
variant,
target_sha256: target.value.sha256,
implementations: metadata.objc_dispatch_implementations.length,
ivars: metadata.objc_ivars.length,
coverage: metadata.coverage,
});
}
const swiftPath = join(root, "libReaWitnessFixture.dylib");
await exec("/usr/bin/xcrun", [
"swiftc",
"-emit-library",
"-O",
"-Xlinker",
"-no_fixup_chains",
fileURLToPath(
new URL("../tests/conformance/native/dispatch.swift", import.meta.url),
),
"-o",
swiftPath,
]);
for (const variant of ["swift-symbols", "swift-stripped"]) {
if (variant === "swift-stripped")
await exec("/usr/bin/strip", ["-x", swiftPath]);
const target = await parseBinaryTarget(swiftPath);
if (!target.ok) throw target.error;
const observation = await inspectAppleDispatchMetadata(target.value, 5000);
const metadata = observation.result;
if (
!metadata.swift_dispatch_slots.some(
(slot) =>
slot.table_kind === "class_vtable" &&
slot.owner === "ReaVtableFixture" &&
slot.decode.status === "decoded",
)
)
throw new Error(
`Swift class vtable fixture unavailable: ${JSON.stringify(metadata.coverage)}`,
);
if (
!metadata.swift_conformances.some(
(item) =>
item.type_name === "ReaWitnessFixture" &&
item.protocol_name === "ReaWitnessFixtureProtocol",
) ||
!metadata.swift_dispatch_slots.some(
(item) =>
item.owner === "ReaWitnessFixture: ReaWitnessFixtureProtocol" &&
item.implementation_address !== null,
)
)
throw new Error(
`Swift metadata fixture drifted: ${JSON.stringify(observation)}`,
);
results.push({
variant,
target_sha256: target.value.sha256,
conformances: metadata.swift_conformances.length,
slots: metadata.swift_dispatch_slots.length,
coverage: metadata.coverage,
});
for (const mode of LINK_MODES) {
const objcPath = join(root, `fixture-${mode.mode}`);
await exec("/usr/bin/xcrun", [
"clang",
"-O2",
"-framework",
"Foundation",
...mode.flags,
source("dispatch.m"),
"-o",
objcPath,
]);
for (const variant of ["symbols", "stripped"]) {
if (variant === "stripped")
await exec("/usr/bin/strip", ["-x", objcPath]);
const { target, metadata } = await inspect(objcPath);
checkObjc(metadata, mode);
results.push({
variant: `objc-${mode.mode}-${variant}`,
target_sha256: target.sha256,
classes: metadata.objc_classes.length,
categories: metadata.objc_categories.length,
implementations: metadata.objc_dispatch_implementations.length,
fixups: fixupCoverage(metadata)?.reason,
});
}
if (!mode.swift) continue;
const swiftPath = join(root, `libReaWitnessFixture-${mode.mode}.dylib`);
await exec("/usr/bin/xcrun", [
"swiftc",
"-emit-library",
"-O",
...mode.flags
.map((flag) => flag.replace(/^-Wl,/u, "-Xlinker\0"))
.flatMap((flag) => flag.split("\0")),
source("dispatch.swift"),
"-o",
swiftPath,
]);
for (const variant of ["symbols", "stripped"]) {
if (variant === "stripped")
await exec("/usr/bin/strip", ["-x", swiftPath]);
const { target, metadata } = await inspect(swiftPath);
checkSwift(metadata, mode);
results.push({
variant: `swift-${mode.mode}-${variant}`,
target_sha256: target.sha256,
conformances: metadata.swift_conformances.length,
slots: metadata.swift_dispatch_slots.length,
fixups: fixupCoverage(metadata)?.reason,
});
}
}
process.stdout.write(
`${JSON.stringify({ ok: true, target_executed: false, fixtures: results })}\n`,

View File

@ -15,6 +15,23 @@ const root = await mkdtemp(join(tmpdir(), "rea-ui-fixture-"));
let child;
let fixturePid;
try {
const childRetrievalTest = join(root, "child-retrieval-test");
await promisify(execFile)("/usr/bin/xcrun", [
"swiftc",
fileURLToPath(
new URL("../bridge/native/NativeUIChildren.swift", import.meta.url),
),
fileURLToPath(
new URL(
"../tests/conformance/native/native-ui-children/main.swift",
import.meta.url,
),
),
"-o",
childRetrievalTest,
]);
await promisify(execFile)(childRetrievalTest, []);
const contents = join(root, "Fixture.app", "Contents");
await mkdir(join(contents, "MacOS"), { recursive: true });
await writeFile(
@ -82,7 +99,6 @@ try {
"capture_native_ui_scenario",
{
...scope,
restore: "leave-as-is",
screenshot: false,
steps: [
{ kind: "click", path: button.path },
@ -111,8 +127,6 @@ try {
String(scope.pid),
"--window-id",
String(scope.window_id),
"--restore",
"leave-as-is",
"--steps",
JSON.stringify([
{ kind: "click", path: button.path },

View File

@ -2,7 +2,7 @@ import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { createEvidence } from "../dist/domain/evidence.js";
import { buildManagedPeFixture } from "./lib/managed-pe-fixture.mjs";
import { buildManagedPeFixture } from "./fixtures/managed/pe.mjs";
import { functionDossier, json, run } from "./lib/verify-package-core.mjs";
const buildManagedFixtures = async (workspace) => {

View File

@ -1,4 +1,4 @@
import { createEvidence } from "../../dist/domain/evidence.js";
import { createEvidence } from "../../../dist/domain/evidence.js";
const COMPLETION_PROVIDER = {
id: "rea-managed-conformance",

View File

@ -3,21 +3,21 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import assert from "node:assert/strict";
import { inspectManagedArtifactBytes } from "../dist/dotnet/ManagedArtifactInspector.js";
import { inspectManagedMembersBytes } from "../dist/dotnet/ManagedMemberInspector.js";
import { inspectManagedNativeBoundariesBytes } from "../dist/dotnet/ManagedNativeBoundaryInspector.js";
import { compareManagedMemberPaths } from "../dist/application/ManagedMemberComparisonService.js";
import { verifyManagedNativeBoundariesEvidence } from "../dist/application/ManagedNativeVerificationService.js";
import { importManagedReconstructionEvidence } from "../dist/application/ManagedReconstructionService.js";
import { projectManagedApplicationGraphEvidence } from "../dist/application/ManagedApplicationGraphService.js";
import { traceApplicationFeatureEvidence } from "../dist/application/javascript/JavaScriptApplicationWorkflowService.js";
import { MANAGED_STATIC_PROVIDER } from "../dist/application/InvestigationProviders.js";
import { createEvidence } from "../dist/domain/evidence.js";
import { inspectManagedArtifactBytes } from "../../../dist/dotnet/ManagedArtifactInspector.js";
import { inspectManagedMembersBytes } from "../../../dist/dotnet/ManagedMemberInspector.js";
import { inspectManagedNativeBoundariesBytes } from "../../../dist/dotnet/ManagedNativeBoundaryInspector.js";
import { compareManagedMemberPaths } from "../../../dist/application/managed/ManagedMemberComparisonService.js";
import { verifyManagedNativeBoundariesEvidence } from "../../../dist/application/managed/ManagedNativeVerificationService.js";
import { importManagedReconstructionEvidence } from "../../../dist/application/managed/ManagedReconstructionService.js";
import { projectManagedApplicationGraphEvidence } from "../../../dist/application/managed/ManagedApplicationGraphService.js";
import { traceApplicationFeatureEvidence } from "../../../dist/application/javascript/JavaScriptApplicationWorkflowService.js";
import { MANAGED_STATIC_PROVIDER } from "../../../dist/application/InvestigationProviders.js";
import { createEvidence } from "../../../dist/domain/evidence.js";
import {
alternateMvid,
buildNativePeFixture,
} from "./lib/managed-pe-fixture.mjs";
import { createManagedConformanceSupport } from "./lib/managed-conformance-support.mjs";
} from "../../fixtures/managed/pe.mjs";
import { createManagedConformanceSupport } from "./support.mjs";
import {
comparisonLimits,
defaultIlBody,
@ -25,9 +25,12 @@ import {
inspectionLimits,
memberLimits,
nativeBoundaryLimits,
} from "./lib/managed-conformance-config.mjs";
import { createManagedCompletionReport } from "./lib/managed-completion-report.mjs";
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
} from "./config.mjs";
import { createManagedCompletionReport } from "./completion-report.mjs";
import {
completeVerifierRun,
createVerifierRun,
} from "../../lib/verifier-run.mjs";
const verifierRun = createVerifierRun();
const workspace = await mkdtemp(join(tmpdir(), "rea-managed-conformance-"));

View File

@ -1,18 +1,18 @@
import { basename, dirname, isAbsolute, resolve } from "node:path";
import { readFile } from "node:fs/promises";
import { parseBinaryTarget } from "../../dist/application/BinaryTargetResolver.js";
import { inspectManagedArtifactBytes } from "../../dist/dotnet/ManagedArtifactInspector.js";
import { inspectManagedMembersBytes } from "../../dist/dotnet/ManagedMemberInspector.js";
import { projectManagedApplicationGraphEvidence } from "../../dist/application/ManagedApplicationGraphService.js";
import { traceApplicationFeatureEvidence } from "../../dist/application/javascript/JavaScriptApplicationWorkflowService.js";
import { createEvidence } from "../../dist/domain/evidence.js";
import { MANAGED_STATIC_PROVIDER } from "../../dist/application/InvestigationProviders.js";
import { parseBinaryTarget } from "../../../dist/application/BinaryTargetResolver.js";
import { inspectManagedArtifactBytes } from "../../../dist/dotnet/ManagedArtifactInspector.js";
import { inspectManagedMembersBytes } from "../../../dist/dotnet/ManagedMemberInspector.js";
import { projectManagedApplicationGraphEvidence } from "../../../dist/application/managed/ManagedApplicationGraphService.js";
import { traceApplicationFeatureEvidence } from "../../../dist/application/javascript/JavaScriptApplicationWorkflowService.js";
import { createEvidence } from "../../../dist/domain/evidence.js";
import { MANAGED_STATIC_PROVIDER } from "../../../dist/application/InvestigationProviders.js";
import {
ensure,
methodTokenRow,
parseManagedAppManifest,
} from "./managed-conformance-manifest-parser.mjs";
} from "./manifest-parser.mjs";
export const createManagedManifestVerifier = (context) => ({
verifyManagedAppManifest: (rawManifest, manifestPath) =>

View File

@ -5,12 +5,12 @@ import { basename, isAbsolute, join, resolve } from "node:path";
import assert from "node:assert/strict";
import { promisify } from "node:util";
import { inspectManagedArtifactBytes } from "../../dist/dotnet/ManagedArtifactInspector.js";
import { inspectManagedMembersBytes } from "../../dist/dotnet/ManagedMemberInspector.js";
import { importManagedReconstructionEvidence } from "../../dist/application/ManagedReconstructionService.js";
import { createEvidence } from "../../dist/domain/evidence.js";
import { MANAGED_STATIC_PROVIDER } from "../../dist/application/InvestigationProviders.js";
import { createManagedManifestVerifier } from "./managed-conformance-manifest.mjs";
import { inspectManagedArtifactBytes } from "../../../dist/dotnet/ManagedArtifactInspector.js";
import { inspectManagedMembersBytes } from "../../../dist/dotnet/ManagedMemberInspector.js";
import { importManagedReconstructionEvidence } from "../../../dist/application/managed/ManagedReconstructionService.js";
import { createEvidence } from "../../../dist/domain/evidence.js";
import { MANAGED_STATIC_PROVIDER } from "../../../dist/application/InvestigationProviders.js";
import { createManagedManifestVerifier } from "./manifest.mjs";
const execFileAsync = promisify(execFile);

View File

@ -2,9 +2,9 @@ import { createHash } from "node:crypto";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { parseBinaryTarget } from "../../dist/application/BinaryTargetResolver.js";
import { buildManagedPeFixture } from "./managed-pe-fixture.mjs";
import { createManagedConformanceOracleSupport } from "./managed-conformance-oracles.mjs";
import { parseBinaryTarget } from "../../../dist/application/BinaryTargetResolver.js";
import { buildManagedPeFixture } from "../../fixtures/managed/pe.mjs";
import { createManagedConformanceOracleSupport } from "./oracles.mjs";
export const createManagedConformanceSupport = (context) => {
const fixtureBytes = async (name, bytes) => {

View File

@ -4,7 +4,7 @@ description: Reverse engineer native, managed, Electron/JavaScript, packaged, fi
metadata:
version: "28"
tool_count: 131
catalog_digest: "d18386ad8f46e7035cfa49f075c42e5c143f5b3f05c9459eb2d1c23c11112871"
catalog_digest: "ab7efdea56f2e319bafbf1e1fc7d8ff998f3da1845968acb1614fc5be4dd6250"
---
# REA

View File

@ -32,7 +32,8 @@ export class JadxMcpTransport implements Transport {
readonly runId = randomUUID();
#spawned: SpawnedOwnedProviderProcess | undefined;
#supervisor: ProviderProcessSupervisor | undefined;
#buffer = Buffer.alloc(0);
#frameBuffer = Buffer.alloc(0);
#frameLength = 0;
#bytes = 0;
#started = false;
#closed = false;
@ -61,12 +62,16 @@ export class JadxMcpTransport implements Transport {
stdin: "pipe",
});
this.#spawned = spawned;
this.#supervisor = new ProviderProcessSupervisor({
...spawned,
ownsProcessLifetime: true,
cleanup:
spawned.cleanup ?? (() => cleanupOwnedProcessGroup(spawned.ownership)),
});
this.#supervisor = new ProviderProcessSupervisor(
{
...spawned,
ownsProcessLifetime: true,
cleanup:
spawned.cleanup ??
(() => cleanupOwnedProcessGroup(spawned.ownership)),
},
{ captureStdout: false },
);
if (
spawned.process.stdin === undefined ||
spawned.process.stdin === null ||
@ -117,6 +122,8 @@ export class JadxMcpTransport implements Transport {
async #stop(): Promise<void> {
this.#closed = true;
this.#frameBuffer = Buffer.alloc(0);
this.#frameLength = 0;
// A cancellation may race acquisition. Join it before inspecting ownership.
await this.#starting?.catch(() => undefined);
const process = this.#spawned?.process;
@ -161,22 +168,42 @@ export class JadxMcpTransport implements Transport {
);
return;
}
this.#buffer = Buffer.concat([this.#buffer, bytes]);
try {
let newline: number;
while ((newline = this.#buffer.indexOf(10)) !== -1) {
if (newline > MAX_FRAME_BYTES)
let offset = 0;
while (offset < bytes.length) {
const newline = bytes.indexOf(10, offset);
const end = newline === -1 ? bytes.length : newline;
const fragment = bytes.subarray(offset, end);
const frameBytes = this.#frameLength + fragment.length;
if (frameBytes > MAX_FRAME_BYTES)
throw new Error(`JADX MCP frame exceeds ${MAX_FRAME_BYTES} bytes`);
const line = this.#buffer.subarray(0, newline).toString("utf8");
this.#buffer = this.#buffer.subarray(newline + 1);
this.#appendFrame(fragment, frameBytes);
if (newline === -1) return;
const line = this.#frameBuffer
.subarray(0, this.#frameLength)
.toString("utf8");
this.#frameBuffer = Buffer.alloc(0);
this.#frameLength = 0;
this.onmessage?.(deserializeMessage(line));
offset = newline + 1;
}
if (this.#buffer.length > MAX_FRAME_BYTES)
throw new Error(`JADX MCP frame exceeds ${MAX_FRAME_BYTES} bytes`);
} catch (cause) {
this.#fail(cause instanceof Error ? cause : new Error(String(cause)));
}
};
#appendFrame(fragment: Buffer, frameBytes: number): void {
if (frameBytes > this.#frameBuffer.length) {
const capacity = Math.min(
MAX_FRAME_BYTES,
Math.max(frameBytes, this.#frameBuffer.length * 2, 1),
);
const grown = Buffer.allocUnsafe(capacity);
this.#frameBuffer.copy(grown, 0, 0, this.#frameLength);
this.#frameBuffer = grown;
}
fragment.copy(this.#frameBuffer, this.#frameLength);
this.#frameLength = frameBytes;
}
#fail(error: Error): void {
this.#failure ??= error.message;
this.onerror?.(error);

View File

@ -3,7 +3,7 @@ import type { OfficialToolName } from "../contracts/officialToolContracts.js";
import type { EnhancedToolName } from "../contracts/enhancedInputs.js";
import type { NativeToolName } from "../contracts/nativeToolContracts.js";
import type { ArtifactAnalysisOperation } from "../contracts/artifactToolContracts.js";
import type { ManagedToolName } from "../contracts/managedToolContracts.js";
import type { ManagedToolName } from "../contracts/managed/managedToolContracts.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import type { AnalysisProfileCommitment } from "../domain/analysisProfile.js";
import type { JsonValue } from "../domain/jsonValue.js";

View File

@ -12,7 +12,7 @@ import type { ArtifactAnalysisOperation } from "../contracts/artifactToolContrac
import {
isManagedToolName,
type ManagedToolName,
} from "../contracts/managedToolContracts.js";
} from "../contracts/managed/managedToolContracts.js";
import { EvidenceIntegrityError } from "../domain/evidenceErrors.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { type AnalysisError } from "../domain/analysisErrorBase.js";

View File

@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest";
import {
installLinuxHopper,
linuxHopperLauncherDigestSupported,
linuxHopperInstallDisclosure,
linuxPackageManagerCommands,
linuxSharedLibrariesAvailable,
@ -93,6 +94,20 @@ describe("Linux Hopper host classification", () => {
});
describe("Linux Hopper installation", () => {
it.each([
[
"Debian and Arch",
"0294ced141cc373468ee22d8343e7dac41980cb05a937994ca81c9f09afe7ded",
],
["RPM", "1339f9e58377442b0c6fcb0dfc3cec20d593cc557408521fad9a00dbc6b8da13"],
])("accepts the pinned %s launcher build", (_, digest) => {
expect(linuxHopperLauncherDigestSupported(digest)).toBe(true);
});
it("rejects an unverified launcher build", () => {
expect(linuxHopperLauncherDigestSupported("0".repeat(64))).toBe(false);
});
it("discloses the exact download, integrity evidence, and privileged command", () => {
expect(linuxHopperInstallDisclosure("deb", false)).toEqual({
downloadUrl:

View File

@ -11,8 +11,10 @@ import { execFileOutput } from "../process/ExecFileOutput.js";
const execFileAsync = promisify(execFile);
const DOWNLOAD_PREFIX = "https://www.hopperapp.com:443/downloader/public/";
const MAX_PACKAGE_BYTES = 100_000_000;
const SUPPORTED_HOPPER_SHA256 =
"0294ced141cc373468ee22d8343e7dac41980cb05a937994ca81c9f09afe7ded";
const SUPPORTED_HOPPER_SHA256 = new Set([
"0294ced141cc373468ee22d8343e7dac41980cb05a937994ca81c9f09afe7ded",
"1339f9e58377442b0c6fcb0dfc3cec20d593cc557408521fad9a00dbc6b8da13",
]);
interface LinuxHopperRelease {
readonly filename: string;
readonly file_length: string;
@ -262,7 +264,7 @@ export const linuxHopperBinarySupported = async (
try {
const hash = createHash("sha256");
for await (const chunk of createReadStream(path)) hash.update(chunk);
return hash.digest("hex") === SUPPORTED_HOPPER_SHA256;
return linuxHopperLauncherDigestSupported(hash.digest("hex"));
} catch (cause: unknown) {
// best-effort cleanup: optional build probing; unreadable means unsupported.
void cause;
@ -270,6 +272,10 @@ export const linuxHopperBinarySupported = async (
}
};
/** Match a launcher extracted from one of the pinned official Linux packages. */
export const linuxHopperLauncherDigestSupported = (digest: string): boolean =>
SUPPORTED_HOPPER_SHA256.has(digest);
/** Interpret ldd output conservatively so a present but broken launcher is unhealthy. */
export const linuxSharedLibrariesAvailable = (output: string): boolean =>
!/(?:=>\s+not found|error while loading shared libraries)/iu.test(output);

View File

@ -52,6 +52,11 @@ export class FakeSetupHost implements SetupHost {
linuxDistribution = (): Promise<LinuxDistribution | undefined> =>
Promise.resolve(this.distribution);
hopperPath = (): Promise<string | undefined> => Promise.resolve(this.hopper);
initialSetupState = async (scope?: DoctorScope) => ({
...(this.hopper === undefined ? {} : { hopperPath: this.hopper }),
providerEnvironment: await this.providerEnvironment(),
doctor: await this.doctor(scope),
});
providerEnvironment = (): Promise<SetupProviderEnvironment> =>
Promise.resolve({
...(this.hopper === undefined

View File

@ -0,0 +1,51 @@
import { expect, it } from "vitest";
import { runSetup } from "./Setup.js";
import { FakeSetupHost, options } from "./Setup.fixture.js";
it("uses the shared initial host snapshot for setup planning", async () => {
const host = new FakeSetupHost();
host.hopperPath = () => {
throw new Error("setup snapshot should provide the Hopper path");
};
host.providerEnvironment = () => {
throw new Error("setup snapshot should provide provider settings");
};
host.initialSetupState = async (scope) => ({
hopperPath: "/snapshot/Hopper",
providerEnvironment: { HOPPER_LAUNCHER_PATH: "/snapshot/Hopper" },
doctor: await host.doctor(scope),
});
host.clients = [{ name: "codex", configPath: "/codex.json" }];
const result = await runSetup(
{ ...options(false), dryRun: true, clientIds: ["codex"] },
host,
);
expect(result.status).toBe("planned");
expect(result.plannedActions.map(({ id }) => id)).toContain(
"configure_client:codex",
);
expect(host.doctorCalls).toBe(1);
});
it("refreshes doctor after approved setup changes", async () => {
const host = new FakeSetupHost();
host.doctorHealthy = false;
host.skill = "unchanged";
host.clients = [{ name: "codex", configPath: "/codex.json" }];
const configureClient = host.configureClient;
host.configureClient = (client, providerEnvironment, command) => {
host.doctorHealthy = true;
return configureClient(client, providerEnvironment, command);
};
const result = await runSetup(
{ ...options(true), clientIds: ["codex"], installSkill: false },
host,
);
expect(result.doctor.healthy).toBe(true);
expect(host.doctorCalls).toBe(2);
});

View File

@ -42,8 +42,15 @@ export const runSetup = async (
);
const unsupported = await hostRemediation(host, false);
if (unsupported !== undefined) return fail(unsupported);
let hopperPath = await host.hopperPath();
let providerEnvironment = await initialProviderEnvironment(host, hopperPath);
const initialState = await host.initialSetupState?.(options.readinessScope);
let hopperPath =
initialState === undefined
? await host.hopperPath()
: initialState.hopperPath;
let providerEnvironment =
initialState === undefined
? await initialProviderEnvironment(host, hopperPath)
: initialState.providerEnvironment;
const discovery = await discoverSetupState({
host,
providerEnvironment,
@ -51,6 +58,9 @@ export const runSetup = async (
proposeHopper:
options.proposeHopper ?? (confirm !== undefined && !options.structured),
doctorScope: options.readinessScope,
...(initialState === undefined
? {}
: { initialDoctor: initialState.doctor }),
});
clientStates = discovery.clientStates;
const clientSelectionAllowed =

View File

@ -22,7 +22,12 @@ import {
} from "./SetupClientConfiguration.js";
import { setupInstallFailure } from "./SetupInstallFailure.js";
import { providerRegistrationEnvironment } from "./SetupRegistrationEnvironment.js";
import type { SetupHost, SetupProviderEnvironment } from "./SetupTypes.js";
import type {
SetupHost,
SetupInitialState,
SetupProviderEnvironment,
} from "./SetupTypes.js";
import type { DoctorScope } from "./Doctor.js";
/** Resolve the executable and arguments used in a managed MCP registration. */
export const setupRegistrationCommand = (
@ -91,6 +96,25 @@ export const systemSetupHost = (
macosVersion: () => doctorHost.macosVersion(),
linuxDistribution: readLinuxDistribution,
hopperPath: async () => (await runDoctor(undefined, doctorHost)).hopperPath,
initialSetupState: async (
scope?: DoctorScope,
): Promise<SetupInitialState> => {
const diagnosis = await runDoctor(undefined, doctorHost, scope);
return {
...(diagnosis.hopperPath === undefined
? {}
: { hopperPath: diagnosis.hopperPath }),
providerEnvironment: {
...providerRegistrationEnvironment(
diagnosis.providerInspections ?? [],
),
...(diagnosis.hopperPath === undefined
? {}
: { HOPPER_LAUNCHER_PATH: diagnosis.hopperPath }),
},
doctor: diagnosis,
};
},
providerEnvironment: async () => {
const diagnosis = await runDoctor(undefined, doctorHost);
return {

View File

@ -16,6 +16,7 @@ import type {
} from "./SetupTypes.js";
import type { SetupClient } from "./SupportedClients.js";
import type { DoctorScope } from "./Doctor.js";
import type { DoctorReport } from "./Doctor.js";
/** Read-only setup facts shared by client selection and preflight. */
export interface SetupDiscovery {
@ -36,8 +37,10 @@ export const discoverSetupState = async (input: {
readonly forceHopperInstall: boolean;
readonly proposeHopper: boolean;
readonly doctorScope: DoctorScope | undefined;
readonly initialDoctor?: DoctorReport;
}): Promise<SetupDiscovery> => {
const initialDoctor = await input.host.doctor(input.doctorScope);
const initialDoctor =
input.initialDoctor ?? (await input.host.doctor(input.doctorScope));
const linuxHopperRepairNeeded = initialDoctor.checks.some(
({ name, ok, detail }) =>
!ok &&

View File

@ -9,6 +9,13 @@ import type {
/** Exact non-secret provider variables propagated into managed registrations. */
export type SetupProviderEnvironment = Readonly<Record<string, string>>;
/** Read-only host facts shared by the initial setup plan. */
export interface SetupInitialState {
readonly hopperPath?: string;
readonly providerEnvironment: SetupProviderEnvironment;
readonly doctor: DoctorReport;
}
/** Result of one backup/write/readback transaction. */
export type ClientConfigurationResult =
| {
@ -38,6 +45,7 @@ export interface SetupHost {
macosVersion(): Promise<string | undefined>;
linuxDistribution(): Promise<LinuxDistribution | undefined>;
hopperPath(): Promise<string | undefined>;
initialSetupState?(scope?: DoctorScope): Promise<SetupInitialState>;
installHopper(replaceExisting: boolean): Promise<SetupHopperInstallResult>;
providerEnvironment?(): Promise<SetupProviderEnvironment>;
detectedClients(): Promise<readonly SetupClient[]>;

View File

@ -12,7 +12,7 @@ import {
import type { JavaScriptApplicationGraph } from "../../domain/javascript/javascriptApplicationGraph.js";
import type { JavaScriptSemanticGraph } from "../../domain/javascript/javascriptSemanticGraph.js";
import { javascriptRuntimeReconciliationResultSchema } from "../../domain/javascript/javascriptRuntimeReconciliationSchemas.js";
import { managedApplicationGraphResultSchema } from "../../domain/managedApplicationGraph.js";
import { managedApplicationGraphResultSchema } from "../../domain/managed/managedApplicationGraph.js";
/** Supported immutable source for an application-level graph workflow. */
export interface ApplicationGraphEvidenceSource {

View File

@ -3,19 +3,19 @@ import { z } from "zod";
import {
AnalysisInputError,
AnalysisProtocolError,
} from "../domain/analysisErrorCore.js";
import { type AnalysisError } from "../domain/analysisErrorBase.js";
import { createEvidence, type Evidence } from "../domain/evidence.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
} from "../../domain/analysisErrorCore.js";
import { type AnalysisError } from "../../domain/analysisErrorBase.js";
import { createEvidence, type Evidence } from "../../domain/evidence.js";
import { jsonValueSchema } from "../../domain/jsonValue.js";
import {
managedApplicationGraphResultSchema,
projectManagedApplicationGraph,
projectManagedApplicationGraphInputSchema,
type ManagedApplicationGraphResult,
type ProjectManagedApplicationGraphInput,
} from "../domain/managedApplicationGraph.js";
import { err, ok, type Result } from "../domain/result.js";
import { MANAGED_WORKFLOW_PROVIDER } from "./InvestigationProviders.js";
} from "../../domain/managed/managedApplicationGraph.js";
import { err, ok, type Result } from "../../domain/result.js";
import { MANAGED_WORKFLOW_PROVIDER } from "../InvestigationProviders.js";
const OPERATION = "project_managed_application_graph" as const;

View File

@ -8,22 +8,22 @@ import {
managedMemberComparisonResultSchema,
parseManagedMemberEvidence,
type CompareManagedMembersInput,
} from "../domain/managedMemberComparison.js";
} from "../../domain/managed/managedMemberComparison.js";
import {
AnalysisInputError,
AnalysisProtocolError,
} from "../domain/analysisErrorCore.js";
import { EvidenceIntegrityError } from "../domain/evidenceErrors.js";
import { type AnalysisError } from "../domain/analysisErrorBase.js";
import { createEvidence, type Evidence } from "../domain/evidence.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
import { parseBinaryTarget } from "./BinaryTargetResolver.js";
import { err, ok, type Result } from "../domain/result.js";
import { inspectManagedMembersBytes } from "../dotnet/ManagedMemberInspector.js";
} from "../../domain/analysisErrorCore.js";
import { EvidenceIntegrityError } from "../../domain/evidenceErrors.js";
import { type AnalysisError } from "../../domain/analysisErrorBase.js";
import { createEvidence, type Evidence } from "../../domain/evidence.js";
import { jsonValueSchema } from "../../domain/jsonValue.js";
import { parseBinaryTarget } from "../BinaryTargetResolver.js";
import { err, ok, type Result } from "../../domain/result.js";
import { inspectManagedMembersBytes } from "../../dotnet/ManagedMemberInspector.js";
import {
MANAGED_STATIC_PROVIDER,
MANAGED_WORKFLOW_PROVIDER,
} from "./InvestigationProviders.js";
} from "../InvestigationProviders.js";
/** Compare managed members from input parsed by a trusted adapter. */
export const compareManagedMembersEvidenceValidated = (

View File

@ -3,18 +3,18 @@ import { z } from "zod";
import {
AnalysisInputError,
AnalysisProtocolError,
} from "../domain/analysisErrorCore.js";
import { type AnalysisError } from "../domain/analysisErrorBase.js";
import { createEvidence, type Evidence } from "../domain/evidence.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
} from "../../domain/analysisErrorCore.js";
import { type AnalysisError } from "../../domain/analysisErrorBase.js";
import { createEvidence, type Evidence } from "../../domain/evidence.js";
import { jsonValueSchema } from "../../domain/jsonValue.js";
import {
managedNativeVerificationInputSchema,
verifyManagedNativeBoundaries,
type ManagedNativeVerificationInput,
type ManagedNativeVerificationResult,
} from "../domain/managedNativeVerification.js";
import { err, ok, type Result } from "../domain/result.js";
import { MANAGED_WORKFLOW_PROVIDER } from "./InvestigationProviders.js";
} from "../../domain/managed/managedNativeVerification.js";
import { err, ok, type Result } from "../../domain/result.js";
import { MANAGED_WORKFLOW_PROVIDER } from "../InvestigationProviders.js";
const OPERATION = "verify_managed_native_boundaries" as const;

View File

@ -2,13 +2,13 @@ import { createHash } from "node:crypto";
import { describe, expect, it } from "vitest";
import { MANAGED_RECONSTRUCTION_IMPORT_EXAMPLE } from "../contracts/managedWorkflowExamples.js";
import { MANAGED_RECONSTRUCTION_IMPORT_EXAMPLE } from "../../contracts/managed/managedWorkflowExamples.js";
import {
importManagedReconstruction,
managedReconstructionImportInputSchema,
} from "../domain/managedReconstruction.js";
import { managedMemberInspectionSchema } from "../domain/managedArtifact.js";
import { createEvidence } from "../domain/evidence.js";
} from "../../domain/managed/managedReconstruction.js";
import { managedMemberInspectionSchema } from "../../domain/managed/managedArtifact.js";
import { createEvidence } from "../../domain/evidence.js";
import { importManagedReconstructionEvidence } from "./ManagedReconstructionService.js";
const exampleInput = () =>

View File

@ -3,18 +3,18 @@ import { z } from "zod";
import {
AnalysisInputError,
AnalysisProtocolError,
} from "../domain/analysisErrorCore.js";
import { type AnalysisError } from "../domain/analysisErrorBase.js";
import { createEvidence, type Evidence } from "../domain/evidence.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
} from "../../domain/analysisErrorCore.js";
import { type AnalysisError } from "../../domain/analysisErrorBase.js";
import { createEvidence, type Evidence } from "../../domain/evidence.js";
import { jsonValueSchema } from "../../domain/jsonValue.js";
import {
importManagedReconstruction,
managedReconstructionImportInputSchema,
type ManagedReconstructionImportInput,
type ManagedReconstructionImportResult,
} from "../domain/managedReconstruction.js";
import { err, ok, type Result } from "../domain/result.js";
import { MANAGED_WORKFLOW_PROVIDER } from "./InvestigationProviders.js";
} from "../../domain/managed/managedReconstruction.js";
import { err, ok, type Result } from "../../domain/result.js";
import { MANAGED_WORKFLOW_PROVIDER } from "../InvestigationProviders.js";
const OPERATION = "import_managed_reconstruction" as const;

View File

@ -143,14 +143,29 @@ const asarFailure = (
path: string,
operation: string,
cause: unknown,
): ArtifactReaderFailure =>
cause instanceof ArtifactReaderFailure
? cause
: new ArtifactReaderFailure(
"format",
`Malformed or unreadable ASAR during ${operation}: ${path}`,
{ cause },
);
): ArtifactReaderFailure => {
if (cause instanceof ArtifactReaderFailure) return cause;
if (isFilesystemFailure(cause))
return new ArtifactReaderFailure(
"io",
`Could not ${operation} ASAR at ${path}: ${cause.message}`,
{ cause },
);
return new ArtifactReaderFailure(
"format",
`Malformed ASAR during ${operation}: ${path}`,
{ cause },
);
};
const isFilesystemFailure = (
cause: unknown,
): cause is NodeJS.ErrnoException & Error =>
cause instanceof Error &&
"errno" in cause &&
typeof cause.errno === "number" &&
"syscall" in cause &&
typeof cause.syscall === "string";
const isMissingFile = (cause: unknown): boolean =>
typeof cause === "object" &&

View File

@ -85,7 +85,7 @@ export class DirectoryArtifactReader implements ArtifactReader {
);
const handle = await open(
entry.adapterKey,
constants.O_RDONLY | constants.O_NOFOLLOW,
constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK,
);
try {
const observed = await handle.stat();

View File

@ -1,8 +1,8 @@
import { z } from "incur";
import { compareManagedMemberPaths } from "../application/ManagedMemberComparisonService.js";
import { verifyManagedNativeBoundariesEvidence } from "../application/ManagedNativeVerificationService.js";
import { importManagedReconstructionEvidence } from "../application/ManagedReconstructionService.js";
import { compareManagedMemberPaths } from "../application/managed/ManagedMemberComparisonService.js";
import { verifyManagedNativeBoundariesEvidence } from "../application/managed/ManagedNativeVerificationService.js";
import { importManagedReconstructionEvidence } from "../application/managed/ManagedReconstructionService.js";
import { runProviderAnalysis } from "../application/DirectAnalysis.js";
import { parseCliJsonInput } from "../cliJsonInput.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";

View File

@ -1,6 +1,6 @@
import { z } from "incur";
import { projectManagedApplicationGraphEvidence } from "../application/ManagedApplicationGraphService.js";
import { projectManagedApplicationGraphEvidence } from "../application/managed/ManagedApplicationGraphService.js";
import { CLI_COMMANDS } from "../cliCommandNames.js";
import { parseCliJsonInput } from "../cliJsonInput.js";
import { logCliCommand } from "../cliLogging.js";

View File

@ -8,8 +8,8 @@ import { NATIVE_TOOL_CONTRACTS } from "./nativeToolContracts.js";
import { ARTIFACT_TOOL_CONTRACTS } from "./artifactToolContracts.js";
import { FIRMWARE_TOOL_CONTRACTS } from "./firmware/firmwareToolContracts.js";
import { ANDROID_TOOL_CONTRACTS } from "./android/androidToolContracts.js";
import { MANAGED_TOOL_CONTRACTS } from "./managedToolContracts.js";
import { MANAGED_WORKFLOW_TOOL_CONTRACTS } from "./managedWorkflowToolContracts.js";
import { MANAGED_TOOL_CONTRACTS } from "./managed/managedToolContracts.js";
import { MANAGED_WORKFLOW_TOOL_CONTRACTS } from "./managed/managedWorkflowToolContracts.js";
import { BROWSER_TOOL_CONTRACTS } from "./browserToolContracts.js";
import { BROWSER_SCENARIO_TOOL_CONTRACTS } from "./browserScenarioToolContracts.js";
import { ELECTRON_TOOL_CONTRACTS } from "./javascript/electronToolContracts.js";

View File

@ -1,9 +1,9 @@
import { z } from "zod";
import type { ToolContract } from "./toolContracts.js";
import { managedOutputSchemas } from "./toolOutputSchemas.js";
import { toolContractMetadata } from "./toolEffects.js";
import { requireOutputSchema } from "./toolOutputSchemaPrimitives.js";
import type { ToolContract } from "../toolContracts.js";
import { managedOutputSchemas } from "../toolOutputSchemas.js";
import { toolContractMetadata } from "../toolEffects.js";
import { requireOutputSchema } from "../toolOutputSchemaPrimitives.js";
/** Exact caller boundary for execution-free PE/CLI triage and identity. */
const managedTargetInputSchema = z.object({

View File

@ -1,9 +1,9 @@
import { createEvidence } from "../domain/evidence.js";
import { createEvidence } from "../../domain/evidence.js";
import type {
ManagedMemberInspection,
ManagedNativeBoundaryInspection,
} from "../domain/managedArtifact.js";
import type { InspectMacho } from "../domain/nativeInspection.js";
} from "../../domain/managed/managedArtifact.js";
import type { InspectMacho } from "../../domain/nativeInspection.js";
const MANAGED_STATIC_EXAMPLE_PROVIDER = {
id: "rea-dotnet-static",

View File

@ -1,17 +1,17 @@
import { z } from "zod";
import { evidenceSchema } from "../domain/evidence.js";
import { evidenceSchema } from "../../domain/evidence.js";
import { managedReconstructionImportInputSchema } from "../domain/managedReconstruction.js";
import type { ToolContract } from "./toolContracts.js";
import { managedWorkflowOutputSchemas } from "./toolOutputSchemas.js";
import { managedReconstructionImportInputSchema } from "../../domain/managed/managedReconstruction.js";
import type { ToolContract } from "../toolContracts.js";
import { managedWorkflowOutputSchemas } from "../toolOutputSchemas.js";
import {
MANAGED_MEMBER_COMPARISON_EXAMPLE,
MANAGED_NATIVE_VERIFICATION_EXAMPLE,
MANAGED_APPLICATION_GRAPH_EXAMPLE,
MANAGED_RECONSTRUCTION_IMPORT_EXAMPLE,
} from "./managedWorkflowExamples.js";
import { toolContractMetadata } from "./toolEffects.js";
import { requireOutputSchema } from "./toolOutputSchemaPrimitives.js";
import { toolContractMetadata } from "../toolEffects.js";
import { requireOutputSchema } from "../toolOutputSchemaPrimitives.js";
/** Inputs for comparing two managed observations carried inline. */
export const compareManagedMembersReferenceInputSchema = z

View File

@ -80,7 +80,7 @@ export const SESSION_TOOL_CONTRACTS = [
),
session(
"capture_process_scenario",
"Run one caller-selected command under a PTY and return process capture Evidence with residual unknowns. A command name resolves through the inherited PATH; the working directory defaults to the caller's current directory; host environment variables are inherited with scenario overrides. Filesystem snapshots are opt-in through filesystem_observation_paths. The target runs with the current user's permissions; this is not a security sandbox.",
"Run one caller-selected command under a PTY and return process capture Evidence with residual unknowns. A command name resolves through the inherited PATH; the working directory defaults to the caller's current directory; host environment variables are inherited with scenario overrides. Environment keys cannot contain '=' or NUL, and REA_PROCESS_RUN_ID is reserved for process ownership. Filesystem snapshots are opt-in through filesystem_observation_paths. The target runs with the current user's permissions; this is not a security sandbox.",
processScenarioSchema,
),
session(

View File

@ -5,10 +5,10 @@ import { OFFICIAL_TOOL_CONTRACTS } from "./officialToolContracts.js";
import { SESSION_TOOL_CONTRACTS } from "./sessionToolContracts.js";
import { TOOL_CONTRACTS } from "./toolContracts.js";
import { ARTIFACT_TOOL_CONTRACTS } from "./artifactToolContracts.js";
import { MANAGED_TOOL_CONTRACTS } from "./managedToolContracts.js";
import { MANAGED_TOOL_CONTRACTS } from "./managed/managedToolContracts.js";
import { FIRMWARE_TOOL_CONTRACTS } from "./firmware/firmwareToolContracts.js";
import { ANDROID_TOOL_CONTRACTS } from "./android/androidToolContracts.js";
import { MANAGED_WORKFLOW_TOOL_CONTRACTS } from "./managedWorkflowToolContracts.js";
import { MANAGED_WORKFLOW_TOOL_CONTRACTS } from "./managed/managedWorkflowToolContracts.js";
import { NATIVE_TOOL_CONTRACTS } from "./nativeToolContracts.js";
import { BROWSER_PROVIDER_TOOL_CONTRACTS } from "./browserProviderToolContracts.js";
import { ELECTRON_TOOL_CONTRACTS } from "./javascript/electronToolContracts.js";

View File

@ -1,9 +1,9 @@
import { NATIVE_TOOL_CONTRACTS } from "./nativeToolContracts.js";
import { ARTIFACT_TOOL_CONTRACTS } from "./artifactToolContracts.js";
import { MANAGED_TOOL_CONTRACTS } from "./managedToolContracts.js";
import { MANAGED_TOOL_CONTRACTS } from "./managed/managedToolContracts.js";
import { FIRMWARE_TOOL_CONTRACTS } from "./firmware/firmwareToolContracts.js";
import { ANDROID_TOOL_CONTRACTS } from "./android/androidToolContracts.js";
import { MANAGED_WORKFLOW_TOOL_CONTRACTS } from "./managedWorkflowToolContracts.js";
import { MANAGED_WORKFLOW_TOOL_CONTRACTS } from "./managed/managedWorkflowToolContracts.js";
import { BROWSER_PROVIDER_TOOL_CONTRACTS } from "./browserProviderToolContracts.js";
import { ELECTRON_TOOL_CONTRACTS } from "./javascript/electronToolContracts.js";
import { JAVASCRIPT_RUNTIME_OBSERVATION_TOOL_CONTRACTS } from "./javascript/javascriptRuntimeObservationToolContracts.js";

View File

@ -37,11 +37,11 @@ import {
managedArtifactInspectionSchema,
managedMemberInspectionSchema,
managedNativeBoundaryInspectionSchema,
} from "../domain/managedArtifact.js";
import { managedMemberComparisonResultSchema } from "../domain/managedMemberComparison.js";
import { managedNativeVerificationResultSchema } from "../domain/managedNativeVerification.js";
import { managedReconstructionImportResultSchema } from "../domain/managedReconstruction.js";
import { managedApplicationGraphResultSchema } from "../domain/managedApplicationGraph.js";
} from "../domain/managed/managedArtifact.js";
import { managedMemberComparisonResultSchema } from "../domain/managed/managedMemberComparison.js";
import { managedNativeVerificationResultSchema } from "../domain/managed/managedNativeVerification.js";
import { managedReconstructionImportResultSchema } from "../domain/managed/managedReconstruction.js";
import { managedApplicationGraphResultSchema } from "../domain/managed/managedApplicationGraph.js";
import { artifactComparisonResultSchema } from "../domain/artifactComparison.js";
import { functionComparisonResultSchema } from "../domain/functionComparison.js";
import { bundleComparisonResultSchema } from "../domain/bundleComparison.js";

View File

@ -7,12 +7,12 @@ import {
MANAGED_APPLICATION_GRAPH_EXAMPLE,
MANAGED_MEMBER_COMPARISON_EXAMPLE,
MANAGED_NATIVE_VERIFICATION_EXAMPLE,
} from "./managedWorkflowExamples.js";
} from "./managed/managedWorkflowExamples.js";
import {
compareManagedMembersReferenceInputSchema,
managedApplicationGraphReferenceInputSchema,
managedNativeVerificationReferenceInputSchema,
} from "./managedWorkflowToolContracts.js";
} from "./managed/managedWorkflowToolContracts.js";
describe("workflow input contracts", () => {
it("rejects application Evidence ID references and duplicate inline records", () => {

View File

@ -309,6 +309,29 @@ describe("Apple bundle layouts and containers", () => {
expect(
result.bundles.find(({ path }) => path === framework)?.info_plist_path,
).toBeNull();
// Files beside the only real version are not versions.
const withFiles = project(
inventoryEvidence("zip", "Fixture.zip", [
macho("Fixture.app/Contents/MacOS/Fixture"),
{ path: `${framework}/Versions/A/Resources/Info.plist` },
{ path: `${framework}/Versions/.DS_Store` },
{ path: `${framework}/Versions/Current`, kind: "symlink" },
]),
);
expect(
withFiles.bundles.find(({ path }) => path === framework)?.info_plist_path,
).toBe(`${framework}/Versions/A/Resources/Info.plist`);
// Only A has a plist, but Versions/Current may select B.
const onlyA = project(
inventoryEvidence("zip", "Fixture.zip", [
macho("Fixture.app/Contents/MacOS/Fixture"),
{ path: `${framework}/Versions/A/Resources/Info.plist` },
macho(`${framework}/Versions/B/Core`),
]),
);
expect(
onlyA.bundles.find(({ path }) => path === framework)?.info_plist_path,
).toBeNull();
expect(result.limitations).toContainEqual(
expect.stringContaining("Versions/Current selects is unknown"),
);
@ -438,3 +461,37 @@ describe("macOS application scope", () => {
});
});
});
describe("Apple application bridge candidates", () => {
it("pairs scripts and native code only within one application root", () => {
const result = project(
inventoryEvidence("zip", "Suite.zip", [
macho("First.app/Contents/MacOS/First"),
{
path: "First.app/Contents/Resources/first.js",
format: "javascript-bundle",
},
macho("Second.app/Contents/MacOS/Second"),
{
path: "Second.app/Contents/Resources/second.js",
format: "javascript-bundle",
},
]),
);
expect(result.application_roots).toEqual(["First.app", "Second.app"]);
expect(
result.bridge_candidates.map(
({ source_path: source, native_path: target }) => [source, target],
),
).toEqual([
[
"First.app/Contents/Resources/first.js",
"First.app/Contents/MacOS/First",
],
[
"Second.app/Contents/Resources/second.js",
"Second.app/Contents/MacOS/Second",
],
]);
});
});

View File

@ -149,6 +149,7 @@ export const projectAppleApplication = (
...classified.executables,
...classified.native_libraries,
]),
roots,
);
const uninventoriedImage =
rootFormat === "dmg" && inventory.occurrences.length === 1;
@ -305,18 +306,27 @@ const identifyRuntimeFamilies = (all: readonly Component[]) => {
return [...families].sort(compare);
};
/** Pair scripts and native code only within the same application root. */
const identifyBridgeCandidates = (
scripts: readonly Component[],
native: readonly Component[],
) => ({
candidates: scripts.flatMap((script) =>
native.map((item) => ({
source_path: script.path,
native_path: item.path,
basis: bridgeBasis(item.path),
})),
),
});
roots: readonly string[],
) => {
const owner = (path: string): string | null =>
roots.find((root) => isWithin(path, root)) ?? null;
return {
candidates: scripts.flatMap((script) => {
const root = owner(script.path);
return native
.filter(({ path }) => owner(path) === root)
.map((item) => ({
source_path: script.path,
native_path: item.path,
basis: bridgeBasis(item.path),
}));
}),
};
};
const bridgeBasis = (
path: string,
@ -360,7 +370,7 @@ const projectionLimitations = (facts: {
layout === "versioned-framework" && plist === null,
)
? [
"A versioned framework has zero or several Versions/*/Resources/Info.plist files; the one Versions/Current selects is unknown because symlink targets are not inventoried.",
"A versioned framework has several version directories or no Versions/*/Resources/Info.plist; the Info.plist that Versions/Current selects is unknown because symlink targets are not inventoried.",
]
: []),
...(facts.symlinks

View File

@ -141,14 +141,18 @@ export const detectBundles = (
interface Tree {
readonly children: ReadonlyMap<string, ReadonlySet<string>>;
readonly files: ReadonlyMap<string, Component>;
/** Paths observed as directories, or implied by a deeper path. */
readonly directories: ReadonlySet<string>;
}
const indexTree = (entries: readonly Entry[]): Tree => {
const children = new Map<string, Set<string>>();
const files = new Map<string, Component>();
const directories = new Set<string>();
for (const { path, kind, component } of entries) {
if (isSidecar(path)) continue;
if (kind === "file" && component !== undefined) files.set(path, component);
if (kind === "directory") directories.add(path);
const segments = path.split("/");
let parent = "";
for (const segment of segments) {
@ -156,9 +160,10 @@ const indexTree = (entries: readonly Entry[]): Tree => {
names.add(segment);
children.set(parent, names);
parent = joinPath(parent, segment);
if (parent !== path) directories.add(parent);
}
}
return { children, files };
return { children, files, directories };
};
const joinPath = (directory: string, name: string): string =>
@ -223,9 +228,12 @@ const contentDirectories = (
if (layout === "macos-deep") return [joinPath(directory, "Contents")];
if (layout === "shallow") return [directory];
const versions = joinPath(directory, "Versions");
// Only real version directories count: Current is a symlink, and files
// such as .DS_Store are not versions.
return [...(tree.children.get(versions) ?? [])]
.filter((version) => version !== "Current")
.map((version) => joinPath(versions, version))
.filter((version) => tree.directories.has(version))
.sort(compare);
};
@ -234,7 +242,11 @@ const infoPlistPath = (
layout: Bundle["layout"],
tree: Tree,
): string | null => {
const candidates = contentDirectories(bundle, layout, tree)
const directories = contentDirectories(bundle, layout, tree);
// With several real versions, Versions/Current (a symlink outside the
// inventory) decides which plist applies, so the plist stays unknown.
if (directories.length !== 1) return null;
const candidates = directories
.map((directory) =>
joinPath(
directory,

View File

@ -119,14 +119,30 @@ const addExport = (output: Set<string>, name: string): void => {
};
const memberPath = (node: t.Node | null | undefined): string => {
if (node === undefined || node === null) return "";
if (t.isIdentifier(node) || t.isPrivateName(node))
return t.isIdentifier(node) ? node.name : "";
if (t.isThisExpression(node)) return "this";
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
const object = t.isNode(node.object) ? memberPath(node.object) : "";
const property = propertyName(node.property);
return object === "" ? property : `${object}.${property}`;
const properties: string[] = [];
let current = node;
while (
current !== undefined &&
current !== null &&
(t.isMemberExpression(current) || t.isOptionalMemberExpression(current))
) {
properties.push(propertyName(current.property));
current = t.isNode(current.object) ? current.object : undefined;
}
return "";
const root =
current !== undefined && t.isIdentifier(current)
? current.name
: current !== undefined && t.isThisExpression(current)
? "this"
: "";
if (properties.length === 0) return root;
properties.reverse();
const path = root === "" ? [] : [root];
for (const property of properties) {
if (path.length === 0 && property === "") continue;
path.push(property);
}
return path.join(".");
};

View File

@ -192,6 +192,51 @@ describe("JavaScript semantic analysis: calls 2", () => {
).not.toBe(left.functionFingerprints[0]?.components.literalSetSha256);
});
it("keeps fingerprint relations ordered and effects scoped to their owner", () => {
const ordered = analyzeJavaScriptSemantics(`
function first() {}
function second(one, two) {}
function ordered() { first(); second(1, 2); }
function timerOwner() { setTimeout(work, 10); }
function networkOwner() { fetch('/data'); }
function noEffects() { return 1; }
`);
const reversed = analyzeJavaScriptSemantics(`
function first() {}
function second(one, two) {}
function ordered() { second(1, 2); first(); }
function timerOwner() { setTimeout(work, 10); }
function networkOwner() { fetch('/data'); }
function noEffects() { return 1; }
`);
const fingerprint = (ir: typeof ordered, name: string) =>
ir.functionFingerprints.find((value) =>
ir.callables.some(
(callable) =>
callable.callableId === value.callableId && callable.name === name,
),
);
expect(
fingerprint(ordered, "ordered")?.components.relationShapeSha256,
).not.toBe(
fingerprint(reversed, "ordered")?.components.relationShapeSha256,
);
expect(fingerprint(ordered, "timerOwner")?.components.effects).toContain(
"timer",
);
expect(fingerprint(ordered, "networkOwner")?.components.effects).toContain(
"network",
);
expect(fingerprint(ordered, "noEffects")?.components.effects).toEqual([]);
expect(
fingerprint(ordered, "timerOwner")?.components.effects,
).not.toContain("network");
expect(
fingerprint(ordered, "networkOwner")?.components.effects,
).not.toContain("timer");
});
it("retains ambiguous local callees and explicit dynamic frontiers", () => {
const ir = analyzeJavaScriptSemantics(`
function left(value) { return value; }

View File

@ -149,6 +149,31 @@ describe("JavaScript semantic analysis: dataflow 1", () => {
});
});
describe("JavaScript semantic call result flows", () => {
it("links only directly assigned scalar call results", () => {
const ir = analyzeJavaScriptSemantics(`
function run() {
const direct = produce();
const { field } = unpack();
const nested = outer(inner());
return direct;
}
`);
const bindingNames = new Map(
ir.bindings.map(({ bindingId, name }) => [bindingId, name]),
);
expect(
ir.callResultFlows.map(({ bindingId }) => bindingNames.get(bindingId)),
).toEqual(["direct", "nested"]);
expect(ir.callSites).toHaveLength(4);
expect(ir.callResultFlows.map(({ callSiteId }) => callSiteId)).toEqual([
ir.callSites[0]?.callSiteId,
ir.callSites[2]?.callSiteId,
]);
});
});
describe("JavaScript semantic analysis: read-modify-write", () => {
it.each(["+= 1", "++", "--", "||= 1", "&&= 1", "??= 1"])(
"retains the property read in a read-modify-write operation: %s",

View File

@ -183,9 +183,7 @@ const collectCallResultFlow = (
context.ancestors,
context.state,
).filter(({ projectionPath }) => projectionPath.length === 0)) {
const binding = [...context.state.bindingsById.values()].find(
({ bindingId }) => bindingId === assigned.bindingId,
);
const binding = context.state.bindingsById.get(assigned.bindingId);
if (binding === undefined) continue;
const identifierRange = range(assigned.identifier);
const definition = binding.definitions.find(

View File

@ -50,30 +50,36 @@ export const collectJavaScriptSemanticFingerprints = (
resourceOperations,
parserPartial,
} = input;
const callSitesByCaller = groupByOwner(
calls.callSites,
({ callerCallableId }) => callerCallableId,
);
const captureCountByCallable = countByOwner(
calls.closureCaptures,
({ callableId }) => callableId,
);
const promiseEffectsByOwner = groupByOwner(promises, ownerId);
const eventEffectsByOwner = groupByOwner(events, ownerId);
const timerEffectsByOwner = groupByOwner(timers, ownerId);
const childProcessEffectsByOwner = groupByOwner(
[...childProcessSpawns, ...childProcessInteractions],
ownerId,
);
const networkEffectsByOwner = groupByOwner(requestOperations, ownerId);
const resourceEffectsByOwner = groupByOwner(resourceOperations, ownerId);
return callables.flatMap((callable) => {
if (callable.kind === "class") return [];
const node = state.callableNodesById.get(callable.callableId);
if (node === undefined) return [];
const syntax = fingerprintSyntax(node, callable.callableId);
const promiseEffects = promises.filter(
({ ownerCallableId }) => ownerCallableId === callable.callableId,
);
const eventEffects = events.filter(
({ ownerCallableId }) => ownerCallableId === callable.callableId,
);
const timerEffects = timers.filter(
({ ownerCallableId }) => ownerCallableId === callable.callableId,
);
const childProcessEffects = [
...childProcessSpawns,
...childProcessInteractions,
].filter(({ ownerCallableId }) => ownerCallableId === callable.callableId);
const networkEffects = requestOperations.filter(
({ ownerCallableId }) => ownerCallableId === callable.callableId,
);
const resourceEffects = resourceOperations.filter(
({ ownerCallableId }) => ownerCallableId === callable.callableId,
);
const promiseEffects = promiseEffectsByOwner.get(callable.callableId) ?? [];
const eventEffects = eventEffectsByOwner.get(callable.callableId) ?? [];
const timerEffects = timerEffectsByOwner.get(callable.callableId) ?? [];
const childProcessEffects =
childProcessEffectsByOwner.get(callable.callableId) ?? [];
const networkEffects = networkEffectsByOwner.get(callable.callableId) ?? [];
const resourceEffects =
resourceEffectsByOwner.get(callable.callableId) ?? [];
return [
{
callableId: callable.callableId,
@ -86,7 +92,12 @@ export const collectJavaScriptSemanticFingerprints = (
normalizedAstSha256: digest(syntax.normalizedTokens),
controlFlowSha256: digest(syntax.controlFlowTokens),
relationShapeSha256: digest(
relationShape(callable, calls, promiseEffects),
relationShape(
callable,
callSitesByCaller.get(callable.callableId) ?? [],
captureCountByCallable.get(callable.callableId) ?? 0,
promiseEffects,
),
),
literalSetSha256: digest(
[...new Set(syntax.literals)].sort(compareCodePoints),
@ -114,6 +125,36 @@ export const collectJavaScriptSemanticFingerprints = (
});
};
const ownerId = <T extends { readonly ownerCallableId: string | null }>(
value: T,
): string | null => value.ownerCallableId;
const groupByOwner = <T, K extends string | null>(
values: readonly T[],
getOwner: (value: T) => K,
): Map<K, T[]> => {
const grouped = new Map<K, T[]>();
for (const value of values) {
const owner = getOwner(value);
const ownedValues = grouped.get(owner);
if (ownedValues === undefined) grouped.set(owner, [value]);
else ownedValues.push(value);
}
return grouped;
};
const countByOwner = <T, K extends string | null>(
values: readonly T[],
getOwner: (value: T) => K,
): Map<K, number> => {
const counts = new Map<K, number>();
for (const value of values) {
const owner = getOwner(value);
counts.set(owner, (counts.get(owner) ?? 0) + 1);
}
return counts;
};
interface FingerprintSyntax {
readonly normalizedTokens: readonly string[];
readonly controlFlowTokens: readonly string[];
@ -229,21 +270,16 @@ const semanticLiteral = (node: t.Node): string | null => {
const relationShape = (
callable: JavaScriptSemanticCallable,
calls: JavaScriptSemanticCallAnalysis,
callSites: JavaScriptSemanticCallAnalysis["callSites"],
captureCount: number,
promises: readonly JavaScriptSemanticPromiseOperation[],
): string[] => [
`returns:${String(callable.returnSites.length)}:${callable.returnCoverage.status}`,
...calls.callSites
.filter(({ callerCallableId }) => callerCallableId === callable.callableId)
.map(
({ kind, resolution, arguments: args, calleeCallableIds }) =>
`call:${kind}:${resolution}:${String(args.length)}:${String(calleeCallableIds.length)}`,
),
`captures:${String(
calls.closureCaptures.filter(
({ callableId }) => callableId === callable.callableId,
).length,
)}`,
...callSites.map(
({ kind, resolution, arguments: args, calleeCallableIds }) =>
`call:${kind}:${resolution}:${String(args.length)}:${String(calleeCallableIds.length)}`,
),
`captures:${String(captureCount)}`,
...promises.map(
({ kind, method, ownership, sourceResolution }) =>
`promise:${kind}:${method}:${ownership}:${sourceResolution}`,

View File

@ -51,3 +51,16 @@ it("preserves source-tree enter/exit order and detached ancestor snapshots", ()
],
]);
});
it("skips sparse child slots without disturbing sibling visit order", () => {
const parsed = parseJavaScriptSource("const values = [first, , second];");
if (parsed === null)
throw new TypeError("Expected a parsed JavaScript fixture");
const identifiers: string[] = [];
traverseJavaScriptAst(parsed.program, {
enter: (node) => {
if (t.isIdentifier(node)) identifiers.push(node.name);
},
});
expect(identifiers).toEqual(["values", "first", "second"]);
});

View File

@ -51,11 +51,19 @@ interface TraversalFrame {
}
const childNodes = (node: t.Node): t.Node[] => {
const keys: readonly string[] = t.VISITOR_KEYS[node.type] ?? [];
return keys.flatMap((key) => {
const keys = t.VISITOR_KEYS[node.type];
if (keys === undefined) return [];
const children: t.Node[] = [];
for (const key of keys) {
const value: unknown = Reflect.get(node, key);
if (t.isNode(value)) return [value];
if (!Array.isArray(value)) return [];
return value.filter((item): item is t.Node => t.isNode(item));
});
if (t.isNode(value)) {
children.push(value);
continue;
}
if (!Array.isArray(value)) continue;
for (const item of value) {
if (t.isNode(item)) children.push(item);
}
}
return children;
};

View File

@ -0,0 +1,63 @@
import { expect, it } from "vitest";
import { collectJavaScriptExports } from "./javascriptAstFingerprint.js";
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
import { parseJavaScriptSource } from "./javascriptSourceParser.js";
const memberChain = ".next".repeat(12_000);
it("collects a 12,000-member CommonJS export path without recursion", () => {
const file = parseJavaScriptSource(`exports${memberChain}.last = 1;`);
if (file === null) throw new Error("Expected valid JavaScript");
expect(collectJavaScriptExports(file).values).toEqual([
`${"next.".repeat(12_000)}last`,
]);
});
it("preserves empty, dynamic, this, and private member path behavior", () => {
const file = parseJavaScriptSource(`
exports[""].value = 1;
exports[""] = 0;
exports[key].value = 2;
this.exports.ignored = 3;
class Example { #private = 0; read() { this.#private = 1; } }
`);
if (file === null) throw new Error("Expected valid JavaScript");
expect(collectJavaScriptExports(file).values).toEqual([
"",
".value",
"key.value",
]);
});
it("keeps left-first and outer-member-first bundler runtime matches", () => {
const source = `
(globalThis.webpackChunkLeft || globalThis.webpackChunkRight).push([[1], { 1: function() {} }]);
globalThis.webpackChunkOuter.webpackChunkInner.push([[2], { 2: function() {} }]);
`;
const analysis = analyzeJavaScriptStaticSource(source);
expect(
analysis.bundler_registrations.find(({ chunk_keys }) =>
chunk_keys.includes("1"),
)?.runtime,
).toBe("webpackChunkLeft");
expect(
analysis.bundler_registrations.find(({ chunk_keys }) =>
chunk_keys.includes("2"),
)?.runtime,
).toBe("webpackChunkInner");
});
it("analyzes a 12,000-member bundler runtime chain without recursion", () => {
const source = `globalThis.webpackChunkApp${memberChain}.push([[1], { 1: function(module) { module.exports.ok = 42; } }]);`;
const analysis = analyzeJavaScriptStaticSource(source);
expect(analysis.parse_status).toBe("complete");
expect(analysis.parse_error_count).toBe(0);
expect(analysis.bundler_registrations).toHaveLength(1);
expect(analysis.bundler_registrations[0]).toMatchObject({
bundler: "webpack",
runtime: "webpackChunkApp",
chunk_keys: ["1"],
modules: [expect.objectContaining({ exports: ["ok"] })],
});
});

View File

@ -73,19 +73,38 @@ export const chunkRuntime = (call: t.CallExpression): string | undefined => {
};
const findChunkRuntime = (node: t.Node): string | undefined => {
if (t.isIdentifier(node) && /(?:webpack|rspack)Chunk/iu.test(node.name))
return node.name;
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
const property = semanticStaticPropertyName(node.property, node.computed);
if (/(?:webpack|rspack)Chunk/iu.test(property)) return property;
return t.isNode(node.object) ? findChunkRuntime(node.object) : undefined;
const pending: t.Node[] = [node];
while (pending.length > 0) {
const current = pending.pop();
if (current === undefined) continue;
if (
t.isIdentifier(current) &&
/(?:webpack|rspack)Chunk/iu.test(current.name)
)
return current.name;
if (
t.isMemberExpression(current) ||
t.isOptionalMemberExpression(current)
) {
const property = semanticStaticPropertyName(
current.property,
current.computed,
);
if (/(?:webpack|rspack)Chunk/iu.test(property)) return property;
if (t.isNode(current.object)) pending.push(current.object);
continue;
}
if (t.isAssignmentExpression(current)) {
pending.push(current.right, current.left);
continue;
}
if (t.isLogicalExpression(current) || t.isBinaryExpression(current)) {
pending.push(current.right, current.left);
continue;
}
if (t.isParenthesizedExpression(current) || t.isTSAsExpression(current))
pending.push(current.expression);
}
if (t.isAssignmentExpression(node))
return findChunkRuntime(node.left) ?? findChunkRuntime(node.right);
if (t.isLogicalExpression(node) || t.isBinaryExpression(node))
return findChunkRuntime(node.left) ?? findChunkRuntime(node.right);
if (t.isParenthesizedExpression(node) || t.isTSAsExpression(node))
return findChunkRuntime(node.expression);
return undefined;
};

View File

@ -3,13 +3,13 @@ import { createHash } from "node:crypto";
import canonicalize from "canonicalize";
import { z } from "zod";
import { evidenceSchema, parseEvidence, type Evidence } from "./evidence.js";
import { evidenceSchema, parseEvidence, type Evidence } from "../evidence.js";
import {
createJavaScriptApplicationGraph,
javascriptApplicationGraphSchema,
type ApplicationEdge,
type ApplicationNode,
} from "./javascript/javascriptApplicationGraph.js";
} from "../javascript/javascriptApplicationGraph.js";
import {
managedArtifactInspectionSchema,
managedMemberInspectionSchema,
@ -18,7 +18,7 @@ import {
type ManagedMemberInspection,
type ManagedNativeBoundaryInspection,
} from "./managedArtifact.js";
import type { JsonValue } from "./jsonValue.js";
import type { JsonValue } from "../jsonValue.js";
import {
assessManagedGraphOmissions,
managedGraphEvidenceCoverage,
@ -30,8 +30,8 @@ import {
addBoundaryNodes,
addMemberNodes,
} from "./managedApplicationGraphNodes.js";
import { digestSchema } from "./../domain/digests.js";
import { prefixedDigestSchema } from "./../domain/digests.js";
import { digestSchema } from "../digests.js";
import { prefixedDigestSchema } from "../digests.js";
const evidenceIdSchema = prefixedDigestSchema("ev");
const textSchema = z.string().min(1);

View File

@ -1,4 +1,4 @@
import type { ApplicationGraphEvidence } from "./javascript/javascriptApplicationGraph.js";
import type { ApplicationGraphEvidence } from "../javascript/javascriptApplicationGraph.js";
import type {
ManagedArtifactInspection,
ManagedMemberInspection,

View File

@ -4,7 +4,7 @@ import {
createJavaScriptApplicationNode,
type ApplicationGraphEvidence,
type ApplicationNode,
} from "./javascript/javascriptApplicationGraph.js";
} from "../javascript/javascriptApplicationGraph.js";
import { managedSourceCoverage } from "./managedApplicationGraphCoverage.js";
import {
type ManagedMemberInspection,

View File

@ -1,5 +1,5 @@
import { z } from "zod";
import { digestSchema } from "./../domain/digests.js";
import { digestSchema } from "../digests.js";
const tokenSchema = z.string().regex(/^0x[0-9a-f]{8}$/u);
const offsetSchema = z.number().int().min(0);

View File

@ -1,7 +1,7 @@
import { z } from "zod";
import { emptyArraySchema } from "./emptyArraySchema.js";
import { evidenceSchema } from "./evidence.js";
import { emptyArraySchema } from "../emptyArraySchema.js";
import { evidenceSchema } from "../evidence.js";
import {
cliMetadataGuidSchema,
type ManagedMemberInspection,
@ -18,8 +18,8 @@ import {
buildMethodItems,
} from "./managedMemberComparisonItems.js";
import { keyMembers, sha256 } from "./managedMemberComparisonMatch.js";
import { digestSchema } from "./../domain/digests.js";
import { prefixedDigestSchema } from "./../domain/digests.js";
import { digestSchema } from "../digests.js";
import { prefixedDigestSchema } from "../digests.js";
const evidenceIdSchema = prefixedDigestSchema("ev");
const tokenSchema = z.string().regex(/^0x[0-9a-f]{8}$/u);

View File

@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
import canonicalize from "canonicalize";
import { parseEvidence } from "./evidence.js";
import { parseEvidence } from "../evidence.js";
import {
managedMemberInspectionSchema,
type ManagedMemberInspection,
@ -11,7 +11,7 @@ import type {
ManagedMemberComparisonResult,
ManagedMemberComparisonSide,
} from "./managedMemberComparison.js";
import type { JsonValue } from "./jsonValue.js";
import type { JsonValue } from "../jsonValue.js";
export const sha256 = (value: JsonValue): string => {
const serialized = canonicalize(value);

View File

@ -2,12 +2,12 @@ import { createHash } from "node:crypto";
import canonicalize from "canonicalize";
import { parseEvidence } from "./evidence.js";
import { parseEvidence } from "../evidence.js";
import {
managedNativeBoundaryInspectionSchema,
type ManagedNativeBoundaryInspection,
} from "./managedArtifact.js";
import type { JsonValue } from "./jsonValue.js";
import type { JsonValue } from "../jsonValue.js";
import {
managedNativeVerificationResultSchema,
type ManagedNativeVerificationInput,

View File

@ -3,11 +3,11 @@ import { basename } from "node:path";
import canonicalize from "canonicalize";
import { parseEvidence } from "./evidence.js";
import { functionDossierSchema } from "./hopperValues.js";
import { inspectMachoSchema } from "./nativeInspection.js";
import { parseEvidence } from "../evidence.js";
import { functionDossierSchema } from "../hopperValues.js";
import { inspectMachoSchema } from "../nativeInspection.js";
import type { ManagedNativeBoundaryInspection } from "./managedArtifact.js";
import type { JsonValue } from "./jsonValue.js";
import type { JsonValue } from "../jsonValue.js";
import {
pinvokeVerificationSchema,
type ManagedNativeVerificationInput,

View File

@ -1,10 +1,10 @@
import { z } from "zod";
import { emptyArraySchema } from "./emptyArraySchema.js";
import { evidenceSchema } from "./evidence.js";
import { emptyArraySchema } from "../emptyArraySchema.js";
import { evidenceSchema } from "../evidence.js";
import { cliMetadataGuidSchema } from "./managedArtifact.js";
import { digestSchema } from "./../domain/digests.js";
import { prefixedDigestSchema } from "./../domain/digests.js";
import { digestSchema } from "../digests.js";
import { prefixedDigestSchema } from "../digests.js";
const evidenceIdSchema = prefixedDigestSchema("ev");
const tokenSchema = z.string().regex(/^0x[0-9a-f]{8}$/u);

View File

@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import { createEvidence } from "./evidence.js";
import { createEvidence } from "../evidence.js";
import { managedReconstructionImportInputSchema } from "./managedReconstruction.js";
describe("managed reconstruction import input", () => {

View File

@ -3,15 +3,15 @@ import { createHash } from "node:crypto";
import canonicalize from "canonicalize";
import { z } from "zod";
import { evidenceSchema, parseEvidence } from "./evidence.js";
import { evidenceSchema, parseEvidence } from "../evidence.js";
import {
cliMetadataGuidSchema,
managedMemberInspectionSchema,
type ManagedMemberInspection,
} from "./managedArtifact.js";
import type { JsonValue } from "./jsonValue.js";
import { digestSchema } from "./../domain/digests.js";
import { prefixedDigestSchema } from "./../domain/digests.js";
import type { JsonValue } from "../jsonValue.js";
import { digestSchema } from "../digests.js";
import { prefixedDigestSchema } from "../digests.js";
const evidenceIdSchema = prefixedDigestSchema("ev");
const tokenSchema = z.string().regex(/^0x[0-9a-f]{8}$/u);

View File

@ -166,6 +166,7 @@ describe("Interface Builder dispatch joins", () => {
const joined = joinInterfaceBuilderDispatch(ui.graph, {
objc_classes: [],
objc_protocols: [],
objc_categories: [],
swift_decls: [],
objc_ivars: [],
objc_protocol_records: [],
@ -227,6 +228,7 @@ describe("Interface Builder dispatch joins", () => {
const metadata = {
objc_classes: [],
objc_protocols: [],
objc_categories: [],
swift_decls: [],
objc_ivars: [],
objc_protocol_records: [],
@ -304,6 +306,7 @@ describe("placeholder dispatch joins", () => {
const joined = joinInterfaceBuilderDispatch(ui.graph, {
objc_classes: [],
objc_protocols: [],
objc_categories: [],
swift_decls: [],
objc_ivars: [],
objc_protocol_records: [],

View File

@ -156,6 +156,8 @@ export const objcDispatchImplementationSchema = z.strictObject({
selector: z.string().min(1),
method_type: z.enum(["instance", "class"]),
implementation_address: z.string().nullable(),
/** Category that adds this method, when it is not defined by the class itself. */
category: z.string().nullable().optional(),
location: nativeMetadataLocationSchema,
decode: nativeMetadataDecodeSchema,
evidence: z.array(nativeMetadataEvidenceSchema).min(1),
@ -164,6 +166,21 @@ export type ObjcDispatchImplementation = z.infer<
typeof objcDispatchImplementationSchema
>;
/** Objective-C category: methods, protocols and properties added to a class. */
export const objcCategorySchema = z.strictObject({
name: z.string().min(1),
/** Extended class; external classes come from the category's bind symbol. */
class_name: z.string().min(1).nullable(),
class_source: z.enum(["local", "external", "unresolved"]),
instance_methods: z.array(z.string()),
class_methods: z.array(z.string()),
protocols: z.array(z.string()),
properties: z.array(objcPropertySchema),
location: nativeMetadataLocationSchema,
decode: nativeMetadataDecodeSchema,
evidence: z.array(nativeMetadataEvidenceSchema).min(1),
});
/** Swift protocol conformance and its associated witness-table location. */
export const swiftConformanceSchema = z.strictObject({
type_name: z.string().min(1),
@ -279,6 +296,7 @@ export type DbSaveResult = z.infer<typeof dbSaveResultSchema>;
export const objcSwiftMetadataSchema = z.strictObject({
objc_classes: z.array(objcClassSchema).default([]),
objc_protocols: z.array(objcProtocolSchema).default([]),
objc_categories: z.array(objcCategorySchema).default([]),
swift_decls: z.array(swiftDeclSchema).default([]),
objc_ivars: z.array(objcIvarSchema).default([]),
objc_protocol_records: z.array(objcProtocolRecordSchema).default([]),

View File

@ -28,13 +28,37 @@ describe("direct process scenarios", () => {
});
it("rejects malformed environment names and adapter-reserved identity", () => {
for (const name of ["", "KEY=VALUE", "KEY\0VALUE", "REA_PROCESS_RUN_ID"])
expect(
processScenarioSchema.safeParse({
executable: "node",
environment: { [name]: "value" },
}).success,
).toBe(false);
for (const name of ["", "KEY=VALUE", "KEY\0VALUE", "REA_PROCESS_RUN_ID"]) {
const result = processScenarioSchema.safeParse({
executable: "node",
environment: { [name]: "value" },
});
expect(result.success).toBe(false);
if (name === "" && !result.success) {
const invalidKey = result.error.issues[0];
expect(invalidKey?.code).toBe("invalid_key");
if (invalidKey?.code === "invalid_key") {
expect(invalidKey.issues[0]?.message).toBe(
"Environment names must not be empty",
);
}
}
if (name === "REA_PROCESS_RUN_ID" && !result.success) {
const invalidKey = result.error.issues[0];
expect(invalidKey?.code).toBe("invalid_key");
if (invalidKey?.code === "invalid_key") {
expect(invalidKey.issues[0]?.message).toBe(
"REA_PROCESS_RUN_ID is reserved by the process adapter",
);
}
}
}
expect(
processScenarioSchema.safeParse({
executable: "node",
environment: { "REA_PROCESS_RUN_ID\n": "caller-value" },
}).success,
).toBe(true);
});
it("keeps bounded capture controls and ordered scheduled interaction", () => {

View File

@ -4,10 +4,16 @@ import { z } from "zod";
const positiveBudget = z.number().int().safe().positive();
const timedEventBase = { at_ms: z.number().int().safe().nonnegative() };
const reservedRunIdEnvironmentName = "REA_PROCESS_RUN_ID";
const environmentName = z
.string()
.regex(/^[^=\0]+$/u, "Environment names cannot contain '=' or NUL");
const reservedEnvironment = new Set(["REA_PROCESS_RUN_ID"]);
.min(1, "Environment names must not be empty")
.regex(/^(?!REA_PROCESS_RUN_ID(?![\s\S]))[^=\0]+$/u, {
error: (issue) =>
issue.input === reservedRunIdEnvironmentName
? `${reservedRunIdEnvironmentName} is reserved by the process adapter`
: "Environment names cannot contain '=' or NUL",
});
export const normalizationSchema = z.object({
paths: z.boolean(),
pids: z.boolean(),
@ -170,16 +176,6 @@ export const processScenarioSchema = z
});
}
}
const explicit = new Set(Object.keys(scenario.environment));
for (const name of explicit) {
if (reservedEnvironment.has(name)) {
context.addIssue({
code: "custom",
message: `${name} is reserved by the process adapter`,
path: ["environment", name],
});
}
}
for (const event of scenario.events) {
if (event.at_ms > scenario.timeout_ms) {
context.addIssue({

View File

@ -3,7 +3,7 @@ import {
managedArtifactInspectionSchema,
type ManagedArtifactInspection,
type ManagedParseIssue,
} from "../domain/managedArtifact.js";
} from "../domain/managed/managedArtifact.js";
import {
readManagedPeLayout,
type ManagedCliHeader,

View File

@ -3,7 +3,7 @@ import {
managedMemberInspectionSchema,
type ManagedMemberInspection,
type ManagedParseIssue,
} from "../domain/managedArtifact.js";
} from "../domain/managed/managedArtifact.js";
import {
readManagedPeLayout,
type ManagedPeLayout,

View File

@ -2,7 +2,7 @@ import {
managedFailure,
ManagedReaderFailure,
} from "./ManagedReaderFailure.js";
import type { ManagedMemberInspection } from "../domain/managedArtifact.js";
import type { ManagedMemberInspection } from "../domain/managed/managedArtifact.js";
import {
type ManagedMetadataLayout,
type MetadataTableLayout,

View File

@ -1,7 +1,7 @@
import type {
ManagedArtifactInspection,
ManagedParseIssue,
} from "../domain/managedArtifact.js";
} from "../domain/managed/managedArtifact.js";
import {
METADATA_TABLE_NAMES,
type ManagedMetadataLayout,

View File

@ -1,7 +1,7 @@
import type {
ManagedArtifactInspection,
ManagedParseIssue,
} from "../domain/managedArtifact.js";
} from "../domain/managed/managedArtifact.js";
import {
metadataRowOffset,
type ManagedMetadataLayout,

View File

@ -3,7 +3,7 @@ import {
managedNativeBoundaryInspectionSchema,
type ManagedNativeBoundaryInspection,
type ManagedParseIssue,
} from "../domain/managedArtifact.js";
} from "../domain/managed/managedArtifact.js";
import { type ManagedMetadataLayout } from "./ManagedMetadataLayout.js";
import {
metadataRowCursor,

View File

@ -3,7 +3,7 @@ import {
managedNativeBoundaryInspectionSchema,
type ManagedNativeBoundaryInspection,
type ManagedParseIssue,
} from "../domain/managedArtifact.js";
} from "../domain/managed/managedArtifact.js";
import {
readManagedMetadataInventory,
type ManagedResourceDirectory,

Some files were not shown because too many files have changed in this diff Show More