Files
reverse-engineering-anything/src/application/CapabilityInventory.ts
T
4ef916da03 feat(pe): inspect import and delay-load tables (#6509)
* feat(pe): inspect import and delay-load tables

Add inspect_pe_imports / inspect-pe-imports to read PE32/PE32+ import
and delay-load directories without executing the image. The report keeps
DLL names, named and ordinal functions, ILT/IAT RVAs, and a pefile
import hash of the standard import table. A missing directory is
observed absence, not proof the image never imported APIs.

* fix(pe): default PE import fixture options

Allow peImportImage() without an argument so CLI and filesystem tests
can use the KERNEL32 sample without repeating an empty options object.

* test(pe): keep import path cases on the dedicated MCP contract

inspect_pe_imports already rejects relative MCP paths in its own
boundary test. Leave the shared absolute-path matrix to tools whose
handler issue projection matches that suite's expected shape.

* fix(native): index virtual-to-file mapping lookups

mappedFileOffset scans every mapping twice per span, so a parser that
translates one span per table entry costs sections x entries. Add
fileMappingLookup, which builds an O(n log n) interval index once and
answers each span in O(log n) with the same accept, reject and
translation results. A differential test checks it against
mappedFileOffset over randomized overlapping mappings and zero fills.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ra7LM2NyQ7DumdssxbUq5g

* fix(pe): report partial import evidence and exact pefile hashes

- Withhold imphash with an explicit reason whenever the standard import
  table is partial (unreadable names or thunks, missing tables), or when
  pefile 2024.8.26 would hash a different row sequence: invalid or
  truncated names, ordinal 0, descriptors without functions, tables its
  bounds or repeat/spread heuristics discard, and its 8192-thunk import
  symbol cap. Delay-load issues do not withhold it.
- Hash ordinal imports with pefile's complete ordlookup tables and its
  ord<decimal> fallback instead of declining unmapped ordinals.
- Report a nonterminal descriptor whose FirstThunk is zero, and a
  delay-load descriptor whose IAT or INT RVA is zero, as malformed_entry
  partial coverage instead of a complete empty inventory.
- Directory evidence lists the file spans actually consumed for the
  descriptor array and terminator; discontiguous mappings produce
  separate spans and separate Evidence ranges.
- Carry exhausted limits as PeImportLimitFailure.limit and derive
  recovery from it; the fixed 16 MiB retained-text ceiling reports a
  fixed allowance instead of max_file_bytes advice.
- Translate RVAs through the indexed mapping lookup and yield with a
  cancellation check every 1024 table entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ra7LM2NyQ7DumdssxbUq5g

* fix(pe): read only own ordinal tables for DLL names

DLL names such as constructor and __proto__ pass pefile's name rule but
resolved inherited object members, throwing before the ord<n> fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ra7LM2NyQ7DumdssxbUq5g

* fix(native): preserve PE import facts and prove hash compatibility

* test(native): verify PE import workflows and measured costs

* refactor(native): narrow PE facts across the container union

* docs(native): record integrated PE import validation

* fix(native): bound every PE import RVA span

* docs(native): measure RVA guard completed workflows

* fix(pe): reconcile import coverage with retained rows

* fix(pe): retain partial imports at descriptor mapping gaps

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
2026-10-24 11:44:12 +08:00

1027 lines
36 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { PCAP_CAPTURE_UNSUPPORTED_HOST_REASON } from "../domain/pcap/pcapAnalysis.js";
import { DEFAULT_CALL_RELATIONSHIP_DIRECTION } from "../contracts/enhancedInputs.js";
import { FIRMWARE_TOOL_REQUIREMENTS } from "../contracts/firmware/firmwareToolRequirements.js";
import { CALL_RELATIONSHIP_TRAVERSALS } from "./CallRelationshipTraversal.js";
import { NATIVE_DISPATCH_METADATA_SOURCES } from "./native/NativeDispatchMetadataInspection.js";
import { NATIVE_UI_ACTION_OPERATIONS } from "./native/NativeUiActionTrace.js";
import { NATIVE_CALL_ROUTE_SOURCES } from "./native/NativeCallRoutes.js";
import { z } from "zod";
import { sqliteDatabaseCapability } from "./sqlite/SqliteDatabaseCapability.js";
import type {
OptionalProviderLoadFailures,
OptionalProviderLoadFailure,
} from "./OptionalObservationProviders.js";
import {
expandProviderCapabilities,
providerCapabilitySet,
toolAvailabilityInventory,
TOOL_ANNOTATION_NAMES,
TOOL_EFFECT_NAMES,
type ClientFeatureAvailability,
type ProviderCapability,
type ToolAvailabilityInventory,
type ToolAvailability,
type ToolAvailabilityMode,
type ToolUnavailabilityReason,
} from "../contracts/toolOutputSchemaPrimitives.js";
import {
annotationsFromEffects,
type ToolEffects,
} from "../contracts/toolEffects.js";
import type { ToolKind } from "../contracts/toolContractTypes.js";
import type { JsonValue } from "../domain/jsonValue.js";
import { isMachOFormat } from "../domain/binaryTargetTypes.js";
import type {
AnalysisOperation,
ProviderAvailability,
} from "./AnalysisProvider.js";
import type { JebAnalysisAvailability } from "./jeb/JebAnalysisPort.js";
import { FRIDA_TOOL_CONTRACTS } from "../contracts/fridaToolContracts.js";
import { TOOL_CONTRACTS } from "../contracts/toolContracts.js";
import {
clientRequirementsFor,
NO_CLIENT_FEATURES,
} from "./CapabilityClientRequirements.js";
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
const FRIDA_OPERATION_NAMES: ReadonlySet<string> = new Set(
FRIDA_TOOL_CONTRACTS.map(({ name }) => name),
);
const statusSchema = z.object({
open: z.boolean(),
kind: z.enum(["executable", "database", "archive", "artifact"]).optional(),
format: z.string().optional(),
capabilities: z
.array(providerCapabilitySet)
.transform(expandProviderCapabilities),
});
type ToolAvailabilityReason = "available" | ToolUnavailabilityReason;
type ProviderDescriptor = ProviderCapability;
/** Optional integrations whose verified hosts exclude some platforms. */
export const HOST_RESTRICTED_FEATURES = [
"packetCapture",
"pcapCapture",
"signalRecording",
"evmInterface",
"binaryLayout",
"recordedCrash",
"firmwareInspection",
"firmwareExtraction",
"javascriptRecovery",
] as const;
export type HostRestrictedFeature = (typeof HOST_RESTRICTED_FEATURES)[number];
export type AvailabilityPolicy = {
/** Integrations this host cannot run, whatever their configuration. */
readonly hostUnsupported?: Readonly<
Partial<Record<HostRestrictedFeature, boolean>>
>;
readonly packetCaptureEnabled?: boolean;
readonly signalRecordingEnabled?: boolean;
readonly wasmArtifactEnabled?: boolean;
readonly evmInterfaceEnabled?: boolean;
/** Fresh owner observation of the selected required EVM limiter. */
readonly evmLimiterAvailability?: ProviderAvailability;
readonly processCaptureEnabled: boolean;
readonly optionalProviderLoadFailures?: OptionalProviderLoadFailures;
readonly binaryLayoutEnabled?: boolean;
readonly recordedCrashEnabled?: boolean;
readonly firmwareInspectionEnabled?: boolean;
readonly firmwareExtractionEnabled?: boolean;
readonly androidAnalysisAvailability?: ProviderAvailability;
readonly adbAnalysisAvailability?: ProviderAvailability;
readonly apktoolAnalysisAvailability?: ProviderAvailability;
readonly jebAnalysisAvailability?: JebAnalysisAvailability;
readonly javascriptRecoveryEnabled?: boolean;
readonly webModuleResolutionEnabled?: boolean;
readonly browserObservationEnabled?: boolean;
readonly browserScenarioEnabled?: boolean;
readonly electronObservationEnabled?: boolean;
readonly electronAutomationEnabled?: boolean;
readonly v8InspectorObservationEnabled?: boolean;
};
type AvailabilityFacts = {
readonly defaultModeAvailable?: boolean;
readonly modes?: readonly ToolAvailabilityMode[];
};
type Availability = AvailabilityFacts &
(
| { readonly reason: "available"; readonly remediation: null }
| {
readonly reason: ToolUnavailabilityReason;
readonly remediation: string | null;
}
);
type AvailabilityModeResult =
| (Extract<ToolAvailabilityMode, { available: true }> & {
readonly reason: "available";
})
| (Extract<ToolAvailabilityMode, { available: false }> & {
readonly reason: ToolUnavailabilityReason;
});
type AvailabilityContext = {
readonly name: string;
readonly kind: ToolKind;
readonly targetOpen: boolean;
readonly requiresActiveTarget: boolean;
readonly targetKind:
| "executable"
| "database"
| "archive"
| "artifact"
| undefined;
readonly targetFormat: string | undefined;
readonly descriptors: ReadonlyMap<string, ProviderDescriptor>;
readonly policy: AvailabilityPolicy;
};
type EnhancedContractName = Extract<
(typeof TOOL_CONTRACTS)[number],
{ readonly kind: "enhanced" }
>["name"];
type CompositionRequirements = readonly (
| AnalysisOperation
| readonly AnalysisOperation[]
)[];
type AvailabilityMode = {
readonly name: string;
readonly requirements: CompositionRequirements;
};
type ModeComposition = {
readonly defaultMode?: string;
readonly modes: readonly AvailabilityMode[];
};
type ProviderComposition = CompositionRequirements | ModeComposition;
const CALL_RELATIONSHIP_COMPOSITION: ModeComposition = {
defaultMode: DEFAULT_CALL_RELATIONSHIP_DIRECTION,
modes: Object.entries(CALL_RELATIONSHIP_TRAVERSALS).map(
([name, traversal]) => ({
name,
requirements: ["procedure_address", traversal.operation],
}),
),
};
const NATIVE_UI_ACTION_COMPOSITION: ModeComposition = {
modes: [
{
name: "authored_ui_dispatch",
requirements: [
NATIVE_UI_ACTION_OPERATIONS.interfaceBuilder,
Object.values(NATIVE_DISPATCH_METADATA_SOURCES),
],
},
{
name: "native_seed_call_trace",
requirements: [
Object.values(NATIVE_DISPATCH_METADATA_SOURCES),
NATIVE_UI_ACTION_OPERATIONS.nativeSeed,
Object.values(NATIVE_CALL_ROUTE_SOURCES),
],
},
],
};
// Complete coverage is checked against canonical enhanced contracts, rather than
// allowing a newly registered workflow to silently look like a missing provider.
const ENHANCED_REQUIREMENTS: ReadonlyMap<string, ProviderComposition> = new Map(
Object.entries({
trace_native_values: ["analyze_function", "resolve_native_call_targets"],
trace_native_ui_action: NATIVE_UI_ACTION_COMPOSITION,
inspect_native_dispatch_metadata: [
Object.values(NATIVE_DISPATCH_METADATA_SOURCES),
],
get_objc_classes: ["list_names"],
get_objc_protocols: ["list_names"],
batch_decompile: ["procedure_pseudo_code"],
decompile_all: ["list_procedures", "procedure_pseudo_code"],
get_call_graph: CALL_RELATIONSHIP_COMPOSITION,
trace_call_path: CALL_RELATIONSHIP_COMPOSITION,
analyze_swift_types: ["list_procedures"],
find_xrefs_to_name: ["list_names", "xrefs"],
binary_overview: [
["current_document", "list_documents"],
"list_procedures",
"list_strings",
],
analyze_function: ["analyze_function"],
inspect_native_api: ["analyze_function"],
trace_feature: [
"search_strings",
"search_procedures",
"xrefs",
"resolve_containing_procedure",
],
} satisfies Readonly<Record<EnhancedContractName, ProviderComposition>>),
);
/** Native contracts defined only for an active Mach-O executable. */
const MACHO_TARGET_OPERATIONS: ReadonlySet<string> = new Set([
"inspect_signature",
"inspect_macho",
"list_architectures",
"observe_native_calls",
"observe_native_ui",
"capture_native_ui_scenario",
]);
const NAVIGATION_CONTEXT_MODES = [
{
name: "current_selection",
requirements: [
"current_document",
"current_address",
"resolve_containing_procedure",
],
},
{
name: "explicit_document",
requirements: ["current_address", "resolve_containing_procedure"],
},
] as const;
/** Build stable per-operation availability for discovery and tool visibility. */
export const buildCapabilityInventory = (
sessionStatus: JsonValue,
policy: AvailabilityPolicy,
clientFeatures: ClientFeatureAvailability = NO_CLIENT_FEATURES,
): readonly ToolAvailability[] => {
const status = statusSchema.parse(sessionStatus);
const descriptors = new Map<string, ProviderDescriptor>(
status.capabilities.map((descriptor) => [descriptor.operation, descriptor]),
);
return TOOL_CONTRACTS.map((contract): ToolAvailability => {
const availability = availabilityFor({
name: contract.name,
kind: contract.kind,
targetOpen: status.open,
requiresActiveTarget:
"requiresActiveTarget" in contract &&
contract.requiresActiveTarget === true,
targetKind: status.kind,
targetFormat: status.format,
descriptors,
policy,
});
const clientRequirements = clientRequirementsFor(
contract.name,
clientFeatures,
);
const clientBlocked = clientRequirements.missing_required.length > 0;
const effects = composedEffects(
contract.name,
contract.effects,
status.capabilities,
);
const annotations = annotationsFromEffects(effects);
const facts = {
name: contract.name,
surface: contract.kind,
...(availability.defaultModeAvailable === undefined
? {}
: { default_mode_available: availability.defaultModeAvailable }),
...(availability.modes === undefined
? {}
: { modes: [...availability.modes] }),
client_requirements: clientRequirements,
effects,
annotations: {
read_only: annotations.readOnlyHint,
destructive: annotations.destructiveHint,
idempotent: annotations.idempotentHint,
open_world: annotations.openWorldHint,
},
};
if (clientBlocked)
return {
...facts,
available: false,
reason: "client_capability_missing",
remediation: `Use an MCP client that supports: ${clientRequirements.missing_required.join(", ")}.`,
};
return availability.reason === "available"
? {
...facts,
available: true,
reason: availability.reason,
remediation: null,
}
: {
...facts,
available: false,
reason: availability.reason,
remediation: availability.remediation,
};
}).sort((left, right) => compareUnicodeCodePoints(left.name, right.name));
};
type ClientRequirements = ToolAvailability["client_requirements"];
type MutableAvailabilityGroup = Omit<
ToolAvailabilityInventory["groups"][number],
"names"
> & { names: string[] };
const EMPTY_CLIENT_REQUIREMENTS: ClientRequirements = {
required: [],
optional: [],
missing_required: [],
missing_optional: [],
};
/** Compact repeated metadata while retaining every operation's current availability inline. */
export const compactToolAvailability = (
availability: readonly ToolAvailability[],
): ToolAvailabilityInventory => {
const clientRequirements =
availability[0]?.client_requirements ?? EMPTY_CLIENT_REQUIREMENTS;
const groups = new Map<string, MutableAvailabilityGroup>();
for (const entry of availability) {
const effects = TOOL_EFFECT_NAMES.filter((name) => entry.effects[name]);
const annotations = TOOL_ANNOTATION_NAMES.filter(
(name) => entry.annotations[name],
);
const groupFacts = {
surface: entry.surface,
effects,
annotations,
available: entry.available,
reason: entry.reason,
remediation: entry.remediation,
...(entry.default_mode_available === undefined
? {}
: { default_mode_available: entry.default_mode_available }),
...(entry.modes === undefined ? {} : { modes: [...entry.modes] }),
...(JSON.stringify(entry.client_requirements) ===
JSON.stringify(clientRequirements)
? {}
: { client_requirements: entry.client_requirements }),
};
const key = JSON.stringify(groupFacts);
const existing = groups.get(key);
if (existing) existing.names.push(entry.name);
else groups.set(key, { ...groupFacts, names: [entry.name] });
}
return toolAvailabilityInventory.parse({
client_requirements: clientRequirements,
groups: [...groups.values()],
});
};
const availabilityFor = (context: AvailabilityContext): Availability => {
// These built-in workflows accept a caller-selected endpoint; they do not load the optional passive provider.
if (
context.name === "inspect_byte_entropy" ||
context.name === "observe_web_execution" ||
context.name === "inspect_web_event_listeners"
)
return { reason: "available", remediation: null };
if (context.name === "get_navigation_context") {
const modes = availabilityForModes(
NAVIGATION_CONTEXT_MODES,
"current_selection",
context.descriptors,
);
return { ...modes, ...targetAvailability(context) };
}
const javascriptApplication = javascriptApplicationAvailability(context);
if (javascriptApplication !== null) return javascriptApplication;
const failure = optionalProviderFailureFor(context);
if (failure !== undefined)
return {
reason: "provider_unavailable",
remediation: `Adapter ${failure.providerId} could not load: ${failure.reason}. Repair the adapter installation and restart REA; other capabilities remain available.`,
};
const workflowAvailability = workflowAvailabilityFor(context);
if (workflowAvailability !== null) return workflowAvailability;
const hostDecision = hostAvailability(context);
if (hostDecision !== null) return hostDecision;
const targetDecision = targetAvailability(context);
if (targetDecision !== null) return targetDecision;
return providerAvailability(context);
};
const optionalProviderFailureFor = ({
name,
kind,
policy,
}: AvailabilityContext): OptionalProviderLoadFailure | undefined => {
const failures = policy.optionalProviderLoadFailures;
if (name === "capture_browser_scenario")
return failures?.browserScenarioCapture;
if (kind === "browser-provider") return failures?.browserObservation;
if (name === "capture_electron_scenario")
return failures?.electronActiveObservation;
if (kind === "electron-provider") return failures?.electronObservation;
if (kind === "runtime-provider")
return failures?.javascriptRuntimeObservation;
if (FRIDA_OPERATION_NAMES.has(name)) return failures?.fridaInstrumentation;
return undefined;
};
const javascriptApplicationAvailability = ({
name,
}: AvailabilityContext): Availability | null => {
if (name === "analyze_javascript_application")
return { reason: "available", remediation: null };
return name === "reconcile_javascript_runtime"
? { reason: "available", remediation: null }
: null;
};
const workflowAvailabilityFor = ({
name,
kind,
policy,
}: AvailabilityContext): Availability | null => {
if (
name === "inspect_desktop_application" ||
name === "inspect_go_binary" ||
name === "inspect_il2cpp_metadata" ||
name === "inspect_model_artifact" ||
name === "inspect_gpu_code_objects" ||
name === "inspect_rust_binary" ||
name === "inspect_python_pyc" ||
name === "inspect_erlang_beam" ||
name === "inspect_electron_fuses"
)
return { reason: "available", remediation: null };
if (name === "inspect_sqlite_database") {
const capability = sqliteDatabaseCapability(process.versions.node);
return capability.available
? { reason: "available", remediation: null }
: {
reason: "provider_unavailable",
remediation: capability.reason,
};
}
if (name === "trace_web_module_imports")
return {
reason: "available",
remediation: null,
defaultModeAvailable: policy.webModuleResolutionEnabled === true,
modes: [
{
name: "sources-without-literal-imports",
available: true,
missing_operations: [],
remediation: null,
},
policy.webModuleResolutionEnabled === true
? {
name: "native-literal-resolution",
available: true,
missing_operations: [],
remediation: null,
}
: {
name: "native-literal-resolution",
available: false,
missing_operations: ["native-module-resolver"],
remediation:
"Provide an absolute REA_BROWSER_EXECUTABLE supporting import.meta.resolve; no binary target is required.",
},
],
};
if (name === "recover_javascript_sources")
return hostRestrictedAvailability(
policy,
"javascriptRecovery",
policy.javascriptRecoveryEnabled,
"On Linux x64, provide an absolute REA_WAKARU_COMMAND for Wakaru ^1.13.0 (verified with 1.14.0) and util-linux prlimit; see docs/javascript-recovery.md#install-wakaru. No binary target is required.",
);
if (name === "inspect_binary_layout")
return hostRestrictedAvailability(
policy,
"binaryLayout",
policy.binaryLayoutEnabled,
"On Linux x64, set absolute REA_PWNTOOLS_PYTHON to caller-supplied Python with pwntools 4.15.0. No active binary target is required.",
);
if (name === "inspect_packet_capture" || name === "inspect_packet_fields")
return hostRestrictedAvailability(
policy,
"packetCapture",
policy.packetCaptureEnabled,
"On Linux, supply TShark 4.x on PATH or through absolute REA_TSHARK_COMMAND, and util-linux prlimit (REA_TSHARK_PRLIMIT_COMMAND, default /usr/bin/prlimit). No active binary target or hardware is required; the tool is checked at execution.",
);
if (name === "decode_signal_recording" || name === "inspect_signal_decoders")
return hostRestrictedAvailability(
policy,
"signalRecording",
policy.signalRecordingEnabled,
"On Linux, supply sigrok-cli 0.7.2 via absolute REA_SIGROK_COMMAND and util-linux prlimit (REA_SIGROK_PRLIMIT_COMMAND, default /usr/bin/prlimit). No active binary target or hardware is required; the tool is checked at execution.",
);
if (name === "inspect_wasm_artifact")
return policy.wasmArtifactEnabled === true
? { reason: "available", remediation: null }
: {
reason: "provider_missing",
remediation:
"Supply WABT 1.0.42 via absolute REA_WABT_BIN_DIRECTORY. No active binary target is required; tools are checked at execution.",
};
if (name === "inspect_pe_resources" || name === "inspect_native_exports")
return { reason: "available", remediation: null };
if (name === "inspect_pe_certificates" || name === "inspect_pe_imports")
return { reason: "available", remediation: null };
if (name === "inspect_elf_notes")
return { reason: "available", remediation: null };
if (name === "inspect_evm_interface") {
const profile = hostRestrictedAvailability(
policy,
"evmInterface",
policy.evmInterfaceEnabled,
"The bundled offline EVM interface profile is currently real-verified on Linux x64. No active binary target or chain endpoint is required.",
);
if (profile.reason !== "available") return profile;
const limiter = policy.evmLimiterAvailability;
if (limiter?.status === "unavailable")
return { reason: "provider_missing", remediation: limiter.reason };
return { reason: "available", remediation: null };
}
if (name === "inspect_recorded_crash")
return hostRestrictedAvailability(
policy,
"recordedCrash",
policy.recordedCrashEnabled,
"On Linux x64, set absolute REA_PWNTOOLS_PYTHON to caller-supplied Python with pwntools 4.15.0. No active binary target is required.",
);
if (kind === "firmware-provider") {
const { binwalk, unblob } = FIRMWARE_TOOL_REQUIREMENTS;
const inspection = name === "inspect_firmware_regions";
return hostRestrictedAvailability(
policy,
inspection ? "firmwareInspection" : "firmwareExtraction",
inspection
? policy.firmwareInspectionEnabled
: policy.firmwareExtractionEnabled,
`On Linux, provide an absolute ${binwalk.commandEnvironment} (${binwalk.name} ${binwalk.line}, verified with ${binwalk.version}) or ${unblob.commandEnvironment} (${unblob.name} ${unblob.line}, verified with ${unblob.version}) path and util-linux prlimit. Extraction also requires the selected format’s external extractor.`,
);
}
if (kind === "android-provider")
return policy.androidAnalysisAvailability?.status === "available"
? { reason: "available", remediation: null }
: {
reason:
policy.androidAnalysisAvailability?.code === "unsupported_host"
? "unsupported_host"
: policy.androidAnalysisAvailability?.code ===
"runtime_unavailable"
? "provider_unavailable"
: "provider_missing",
remediation:
policy.androidAnalysisAvailability?.reason ??
"Set REA_JADX_MCP_JAR to a caller-supplied jadx-headless-mcp 0.7.1 JAR and provide a full JDK on Linux, macOS, or Windows x64 with its matching bundled native controls.",
};
if (kind === "adb-provider")
return ownerProbeAvailability(
policy.adbAnalysisAvailability,
"Set REA_ADB_PATH to a caller-supplied adb executable or make adb available on PATH. This checks the client version only; device and serial availability depend on the request.",
);
if (kind === "apktool-provider")
return ownerProbeAvailability(
policy.apktoolAnalysisAvailability,
"Set REA_APKTOOL_COMMAND to a caller-supplied Apktool executable or make Apktool 2.7.0 or newer available on PATH.",
);
if (kind === "jeb-provider") {
const availability = policy.jebAnalysisAvailability;
if (availability?.status === "available")
return availability.supportedOperations.some(
(operation) => operation === name,
)
? { reason: "available", remediation: null }
: {
reason: "provider_unavailable",
remediation: `The running JEB endpoint does not advertise support for ${name}. Use a JEB client build that provides this operation.`,
};
return ownerProbeAvailability(
availability,
"Start a JEB client serving MCP (GUI client, or headless via its programmatic launcher) and point REA_JEB_MCP_URL at it (default http://127.0.0.1:8425/mcp). REA does not install or launch JEB.",
);
}
const browser = browserProviderAvailability(name, kind, policy);
if (browser !== null) return browser;
if (name === "capture_process_scenario" && !policy.processCaptureEnabled)
return {
reason: "unsupported_host",
remediation: "Run process capture on a supported Linux or macOS host.",
};
if (kind === "application") return { reason: "available", remediation: null };
if (name === "inspect_pcap_capture")
return hostRestrictedAvailability(
policy,
"pcapCapture",
true,
PCAP_CAPTURE_UNSUPPORTED_HOST_REASON,
);
if (kind === "pcap-provider" || kind === "flutter-provider")
return { reason: "available", remediation: null };
const electron = electronProviderAvailability(name, kind, policy);
if (electron !== null) return electron;
if (kind === "runtime-provider")
return policy.v8InspectorObservationEnabled === true
? { reason: "available", remediation: null }
: {
reason: "provider_missing",
remediation:
"Start REA with its JavaScript runtime provider available.",
};
if (kind === "session") return { reason: "available", remediation: null };
return null;
};
/**
* An unsupported host takes precedence over configuration: no environment
* setting lets these integrations pass their execution host checks here.
*/
const hostRestrictedAvailability = (
policy: AvailabilityPolicy,
feature: HostRestrictedFeature,
enabled: boolean | undefined,
remediation: string,
): Availability => {
if (policy.hostUnsupported?.[feature] === true)
return { reason: "unsupported_host", remediation };
return enabled === true
? { reason: "available", remediation: null }
: { reason: "provider_missing", remediation };
};
const ownerProbeAvailability = (
observation: ProviderAvailability | undefined,
missingRemediation: string,
): Availability => {
if (observation === undefined)
return { reason: "provider_missing", remediation: missingRemediation };
if (observation.status === "available")
return { reason: "available", remediation: null };
return {
reason:
observation.code === "unsupported_host"
? "unsupported_host"
: observation.code === "not_configured"
? "provider_missing"
: "provider_unavailable",
remediation: observation.reason,
};
};
const electronProviderAvailability = (
name: string,
kind: ToolKind,
policy: AvailabilityPolicy,
): Availability | null => {
if (kind !== "electron-provider") return null;
if (name === "capture_electron_scenario")
return policy.electronAutomationEnabled === true
? { reason: "available", remediation: null }
: {
reason: "provider_missing",
remediation:
"Start REA with the Electron scenario provider available.",
};
return policy.electronObservationEnabled === true
? { reason: "available", remediation: null }
: {
reason: "provider_missing",
remediation:
"Start REA with its Electron observation provider available.",
};
};
const browserProviderAvailability = (
name: string,
kind: ToolKind,
policy: AvailabilityPolicy,
): Availability | null => {
if (name === "capture_browser_scenario")
return policy.browserScenarioEnabled === true
? { reason: "available", remediation: null }
: {
reason: "provider_missing",
remediation:
"Start REA with its browser scenario provider available.",
};
if (kind !== "browser-provider") return null;
return policy.browserObservationEnabled === true
? { reason: "available", remediation: null }
: {
reason: "provider_missing",
remediation:
"Start REA with its browser observation provider available.",
};
};
/**
* Opening another target cannot make an operation run on an unsupported host.
* Composed operations are decided by their requirements, which another
* provider may satisfy.
*/
const hostAvailability = ({
descriptors,
kind,
name,
}: AvailabilityContext): Availability | null => {
if (kind === "enhanced") return null;
const descriptor = descriptors.get(name);
return descriptor?.available === false &&
descriptor.availability_code === "unsupported_host"
? { reason: "unsupported_host", remediation: descriptor.reason }
: null;
};
const targetAvailability = ({
name,
kind,
targetKind,
targetFormat,
targetOpen,
requiresActiveTarget,
descriptors,
}: AvailabilityContext): Availability | null => {
if (!targetOpen && requiresActiveTarget)
return {
reason: "target_required",
remediation: "Call open_binary with a supported local target.",
};
const descriptor = descriptors.get(name);
if (
targetOpen &&
descriptor?.available === false &&
(descriptor.availability_code === "target_kind_unsupported" ||
descriptor.availability_code === "target_format_unsupported")
)
return { reason: "target_unsupported", remediation: descriptor.reason };
if (
targetKind !== undefined &&
targetKind !== "executable" &&
targetKind !== "database" &&
(kind === "official-proxy" || kind === "enhanced")
)
return {
reason: "target_unsupported",
remediation:
"Inventory or extract a native executable, then call open_binary on that executable.",
};
if (
targetOpen &&
MACHO_TARGET_OPERATIONS.has(name) &&
(targetKind !== "executable" || !isMachOFormat(targetFormat))
)
return {
reason: "target_unsupported",
remediation:
"Call open_binary with a Mach-O executable or an app bundle whose main executable is Mach-O.",
};
return null;
};
const providerAvailability = ({
descriptors,
kind,
name,
}: AvailabilityContext): Availability => {
if (kind === "enhanced") return composedAvailability(name, descriptors);
const descriptor = descriptors.get(name);
if (
descriptor === undefined &&
(kind === "artifact-provider" ||
kind === "managed-provider" ||
kind === "native-provider")
)
return { reason: "available", remediation: null };
if (descriptor === undefined)
return {
reason: "provider_missing",
remediation:
"Install or configure a provider that declares this operation.",
};
return descriptor.available
? { reason: "available", remediation: null }
: {
reason: "provider_unavailable",
remediation:
descriptor.reason ?? "Choose another target or configured provider.",
};
};
const composedAvailability = (
name: string,
descriptors: ReadonlyMap<string, ProviderDescriptor>,
): Availability => {
if (
name === "binary_overview" &&
descriptors.get("read_binary_inventory_summary")?.available === true
)
return { reason: "available", remediation: null };
const requirements = ENHANCED_REQUIREMENTS.get(name);
if (requirements === undefined)
return {
reason: "provider_missing",
remediation: "No provider composition is declared for this operation.",
};
return "modes" in requirements
? availabilityForModes(
requirements.modes,
requirements.defaultMode,
descriptors,
)
: composedAvailabilityFor(requirements, descriptors).availability;
};
/** Evaluate source groups once so discovery and missing-operation facts agree. */
const composedAvailabilityFor = (
requirements: CompositionRequirements,
descriptors: ReadonlyMap<string, ProviderDescriptor>,
): {
readonly availability: Availability;
readonly missingOperations: readonly AnalysisOperation[];
} => {
const unmet = requirements
.map((requirement) => {
const operations =
typeof requirement === "string" ? [requirement] : requirement;
const observations = operations.map((operation) =>
descriptors.get(operation),
);
return {
requirement,
operations,
available: observations.some(
(descriptor) => descriptor?.available === true,
),
unavailable: observations.find(
(descriptor) => descriptor?.available === false,
),
};
})
.filter((group) => !group.available);
const missingOperations = [
...new Set(unmet.flatMap((group) => group.operations)),
];
// Preserve the existing admission rule: report an undeclared prerequisite
// before an unhealthy declaration, in requirement order within each class.
const undeclared = unmet.find((group) => group.unavailable === undefined);
if (undeclared !== undefined)
return {
missingOperations,
availability: {
reason: "provider_missing",
remediation:
typeof undeclared.requirement === "string"
? `Configure a provider for required operation ${undeclared.requirement}.`
: `Configure a provider for one of: ${undeclared.operations.join(", ")}.`,
},
};
const unavailable = unmet[0]?.unavailable;
if (unavailable !== undefined)
return {
missingOperations,
availability: {
reason:
unavailable.availability_code === "unsupported_host"
? "unsupported_host"
: "provider_unavailable",
remediation:
unavailable.reason ?? "Choose another target or configured provider.",
},
};
return {
availability: { reason: "available", remediation: null },
missingOperations,
};
};
const availabilityForModes = (
declarations: readonly AvailabilityMode[],
defaultMode: string | undefined,
descriptors: ReadonlyMap<string, ProviderDescriptor>,
): Availability => {
const modeResults: AvailabilityModeResult[] = declarations.map((mode) => {
const { availability, missingOperations } = composedAvailabilityFor(
mode.requirements,
descriptors,
);
if (availability.reason === "available")
return {
name: mode.name,
available: true,
reason: availability.reason,
missing_operations: [],
remediation: null,
};
return {
name: mode.name,
available: false,
reason: availability.reason,
missing_operations: [...missingOperations],
remediation: availability.remediation,
};
});
const modes: ToolAvailabilityMode[] = modeResults.map((mode) =>
mode.available
? {
name: mode.name,
available: true,
missing_operations: [],
remediation: null,
}
: {
name: mode.name,
available: false,
missing_operations: [...mode.missing_operations],
remediation: mode.remediation,
},
);
const availableMode = modeResults.find((mode) => mode.available);
const failures = modeResults.flatMap((mode) =>
mode.available ? [] : [mode],
);
const failure = failures.sort(
(left, right) =>
availabilityFailurePriority(left.reason) -
availabilityFailurePriority(right.reason),
)[0];
const specificFailureRemediation =
failure?.reason === "provider_missing" ? undefined : failure?.remediation;
const facts = {
...(defaultMode === undefined
? {}
: {
defaultModeAvailable:
modes.find((mode) => mode.name === defaultMode)?.available ?? false,
}),
modes,
};
if (availableMode !== undefined)
return { ...facts, reason: "available", remediation: null };
const missingOperations = [
...new Set(modes.flatMap((mode) => mode.missing_operations)),
];
return {
...facts,
reason: failure?.reason ?? "provider_missing",
remediation:
specificFailureRemediation ??
`Configure a provider for one of: ${missingOperations.join(", ")}.`,
};
};
const availabilityFailurePriority = (
reason: ToolAvailabilityReason,
): number => {
switch (reason) {
case "unsupported_host":
return 0;
case "provider_unavailable":
return 1;
case "provider_missing":
return 2;
default:
return 3;
}
};
/** Use the same composition requirements as availability when exposing provider effects. */
const composedEffects = (
name: string,
base: ToolEffects,
descriptors: readonly ProviderDescriptor[],
): ToolEffects => {
const composition: ProviderComposition | undefined =
name === "get_navigation_context"
? { modes: NAVIGATION_CONTEXT_MODES }
: ENHANCED_REQUIREMENTS.get(name);
const requirements: CompositionRequirements =
composition === undefined
? []
: "modes" in composition
? composition.modes.flatMap(({ requirements }) => requirements)
: composition;
const operations = new Set<string>([
name,
...requirements.flatMap((group) =>
typeof group === "string" ? [group] : group,
),
]);
const effects = { ...base };
for (const descriptor of descriptors) {
if (!operations.has(descriptor.operation)) continue;
const source = descriptor.effects;
effects.mutatesTarget ||= source.mutates_artifact;
effects.launchesProcess ||= source.launches_process;
effects.changesUiState ||= source.may_show_ui;
effects.accessesNetwork ||= source.may_access_network;
effects.writesFilesystem ||=
source.may_write_filesystem || source.changes_permissions;
}
return effects;
};