mirror of
https://github.com/morluto/rea.git
synced 2026-10-28 18:35:53 +00:00
* refactor(pcap): give TShark selection and version admission one owner
The pcap and packet-capture families wrapped the same caller-supplied
TShark with diverging rules. inspect_pcap_* accepted an absolute
REA_TSHARK_COMMAND or tshark from PATH, while inspect_packet_* and the
server availability policy required an absolute REA_TSHARK_COMMAND and
parsed the version banner with a separate regex.
PcapConfiguration now owns the rule: selectWiresharkTool selects an
absolute environment path or the bare name on PATH and refuses relative
values; locateWiresharkTool resolves a PATH selection to the first
absolute PATH entry holding an executable file; parseWiresharkVersion
admits Wireshark 4.x banners for both providers. TsharkCaptureProvider
and the session availability policy consume these facts instead of
reinterpreting REA_TSHARK_COMMAND, so all four tools accept and refuse
the same configurations. inspect_packet_* still resolves, hashes and
rechecks the located executable, and records selection_source in its
tool Evidence.
Host support and resource bounds are unchanged; unifying them is the
next milestone in #3403.
Refs #3403
* fix(pcap): execute the TShark the shared locator selects
resolvePcapTools still returned bare tool names for PATH selections, so
the operating system could run a tool from a relative PATH entry while
the packet tools skipped that entry and selected a different install.
On POSIX hosts, resolve PATH selections through locateWiresharkTool and
run that absolute executable, failing with the existing tshark_missing or
capinfos_missing code when no absolute PATH entry holds the tool. Both
selections are validated before PATH is consulted, so a relative
environment value still reports its own configuration error. Windows
keeps PATH/PATHEXT resolution in the process launcher.
Refs #3403
* chore: restore main's copies of files the merge hook reformatted
The pre-commit oxlint --fix touched two files this PR does not change; keep them identical to main.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(pcap): share selected tool identity verification
* perf(pcap): retain compact executable identity
* fix(pcap): report missing selected tools clearly
* fix(pcap): read historical capture evidence
* fix(pcap): preserve executable read failures
* test(pcap): cover version refusal and packet-tool availability selection
Address review follow-ups on the shared TShark owner:
- Send a 3.x TShark banner and a non-Wireshark banner through the packet
provider, pinning the capability-unavailable reason for each and that
only the version probe ran before refusal.
- Cover the session availability rule for inspect_packet_capture over
MCP: a PATH-only install and an absolute REA_TSHARK_COMMAND are
available on Linux, a relative REA_TSHARK_COMMAND is refused even when
PATH holds TShark, and no absolute PATH entry reports provider_missing.
- Rename the locator test's missing absolute PATH entry so it no longer
shadows node:path relative.
Refs #3403
* fix(pcap): name the configured path when a selected tool is missing
Resolving the selected executable before launch replaced the runner's
"Cannot run selected tool <path>" refusal with a message that kept the
configured path only in diagnostics, so the public CLI no longer told the
caller which REA_TSHARK_COMMAND or REA_CAPINFOS_COMMAND value to fix and
the pcap CLI boundary test failed. Include the configured command in the
capability-unavailable message and assert it there.
Refs #3403
* test(captures): give the late-cancellation fixture a TShark selection source
The service fixture added on main in 719d56ebf predates the required tool.selection_source field, so its report failed output validation before reaching the late-cancellation path the test covers.
Refs #3403
* fix(pcap): report a probe's pending cleanup when a post-run check fails
When a tool probe returned with its owned process still awaiting cleanup,
a failed post-run executable re-read or identity check threw before run()
reported that cleanup. Probes have no snapshot to carry it, so callers
saw an ordinary error with cleanupIncomplete false while the provider
kept the process and refused the next request.
After the command returns, combine such a failure with the probe's
ProviderCleanupError (cancellation and other command failures keep their
own cleanup outcome). Classify request failures through one
analysisFailure owner shared by run() and executeAdmitted(), and let the
availability probe rethrow any cleanup-incomplete failure instead of
reporting the tool unavailable.
Refs #3403
* fix(pcap): keep a failed executable re-read's cleanup live with the probe's
The combined post-run failure was bound only to the probe's pending
process cleanup. When the executable re-read had also failed to close its
file, the classified failure owned that cleanup, but the combined error's
live cleanupResources omitted the file and could clear cleanupIncomplete
once the process stopped. Bind both the classified failure and the probe
cleanup as dependencies.
Refs #3403
---------
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
941 lines
33 KiB
TypeScript
941 lines
33 KiB
TypeScript
import { DEFAULT_CALL_RELATIONSHIP_DIRECTION } from "../contracts/enhancedInputs.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 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 { TOOL_CONTRACTS } from "../contracts/toolContracts.js";
|
||
import { FRIDA_TOOL_CONTRACTS } from "../contracts/fridaToolContracts.js";
|
||
import {
|
||
clientRequirementsFor,
|
||
NO_CLIENT_FEATURES,
|
||
} from "./CapabilityClientRequirements.js";
|
||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||
|
||
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;
|
||
export type AvailabilityPolicy = {
|
||
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 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"],
|
||
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>>),
|
||
);
|
||
|
||
/** Contract kinds whose operations run on the active session target. */
|
||
const ACTIVE_TARGET_KINDS: ReadonlySet<ToolKind> = new Set([
|
||
"official-proxy",
|
||
"enhanced",
|
||
"native-provider",
|
||
"artifact-provider",
|
||
]);
|
||
const FRIDA_OPERATION_NAMES: ReadonlySet<string> = new Set(
|
||
FRIDA_TOOL_CONTRACTS.map(({ name }) => name),
|
||
);
|
||
|
||
/** 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,
|
||
targetKind: status.kind,
|
||
targetFormat: status.format,
|
||
descriptors,
|
||
policy,
|
||
});
|
||
const clientRequirements = clientRequirementsFor(
|
||
contract.name,
|
||
clientFeatures,
|
||
);
|
||
const clientBlocked = clientRequirements.missing_required.length > 0;
|
||
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: { ...contract.effects },
|
||
annotations: {
|
||
read_only: contract.annotations.readOnlyHint ?? false,
|
||
destructive: contract.annotations.destructiveHint ?? false,
|
||
idempotent: contract.annotations.idempotentHint ?? false,
|
||
open_world: contract.annotations.openWorldHint ?? true,
|
||
},
|
||
};
|
||
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")
|
||
return availabilityForModes(
|
||
NAVIGATION_CONTEXT_MODES,
|
||
"current_selection",
|
||
context.descriptors,
|
||
);
|
||
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 policy.javascriptRecoveryEnabled === true
|
||
? { reason: "available", remediation: null }
|
||
: {
|
||
reason: "provider_missing",
|
||
remediation:
|
||
"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 policy.binaryLayoutEnabled === true
|
||
? { reason: "available", remediation: null }
|
||
: {
|
||
reason: "provider_missing",
|
||
remediation:
|
||
"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 policy.packetCaptureEnabled === true
|
||
? { reason: "available", remediation: null }
|
||
: {
|
||
reason: "provider_missing",
|
||
remediation:
|
||
"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 policy.signalRecordingEnabled === true
|
||
? { reason: "available", remediation: null }
|
||
: {
|
||
reason: "provider_missing",
|
||
remediation:
|
||
"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")
|
||
return { reason: "available", remediation: null };
|
||
if (name === "inspect_elf_notes")
|
||
return { reason: "available", remediation: null };
|
||
if (name === "inspect_evm_interface") {
|
||
if (policy.evmInterfaceEnabled !== true)
|
||
return {
|
||
reason: "provider_missing",
|
||
remediation:
|
||
"The bundled offline EVM interface profile is currently real-verified on Linux x64. No active binary target or chain endpoint is required.",
|
||
};
|
||
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 policy.recordedCrashEnabled === true
|
||
? { reason: "available", remediation: null }
|
||
: {
|
||
reason: "provider_missing",
|
||
remediation:
|
||
"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 enabled =
|
||
name === "inspect_firmware_regions"
|
||
? policy.firmwareInspectionEnabled
|
||
: policy.firmwareExtractionEnabled;
|
||
return enabled === true
|
||
? { reason: "available", remediation: null }
|
||
: {
|
||
reason: "provider_missing",
|
||
remediation:
|
||
"On Linux, provide an absolute REA_BINWALK_COMMAND (Binwalk 3.1.x, verified with 3.1.0) or REA_UNBLOB_COMMAND (Unblob 26.6.x, verified with 26.6.4) 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 (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;
|
||
};
|
||
|
||
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,
|
||
descriptors,
|
||
}: AvailabilityContext): Availability | null => {
|
||
if (FRIDA_OPERATION_NAMES.has(name)) return null;
|
||
if (!targetOpen && ACTIVE_TARGET_KINDS.has(kind))
|
||
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;
|
||
}
|
||
};
|