mirror of
https://github.com/morluto/rea.git
synced 2026-11-09 00:07:18 +00:00
* fix(comparisons)!: preserve unknowns across partial observations Share absence and digest semantics, keep unobserved managed bodies unknown, and match unique managed type/name/signatures before structural fallback. BREAKING CHANGE: Managed comparison metadata adds exact-signature matching and its count, and name_matching now reports exact-signature-fallback. * fix(javascript): preserve projected result binding semantics Share syntax-aware property reads and assigned-result resolution. Retain destructuring paths and downgrade downstream whole-result relationships instead of resolving nested or wrapped calls as aliases. * fix(native)!: admit complete parser values and preserve typed failures Decode Mach headers by named columns, validate complete slice keys and lipo alignment, and preserve codesign runtime flags and tagged parser errors. Rename the Ghidra test dossier fixture and connect fake-backed suites to real verification lanes. BREAKING CHANGE: Native load-command results no longer contain the unused, always-null file_offset field. * fix(process): preserve host identity and actionable boundary failures Thread selected platform and environment through process and Doctor seams, stamp observed producer identity in Evidence, and retain PTY, filesystem, lease and lazy-provider failure causes. Drain cancelled lazy loads and omit exited descendants from verified lineage. * fix(browser): preserve cleanup and document-transition evidence Make V8 and Playwright teardown cancellation-aware, expose primary failures alongside incomplete cleanup, share frame-tree and navigation rules, and clear stale WebMCP registrations without pruning unrelated child frames. * fix(adapters): align managed targets and typed CLI outcomes Inspect managed PE/CLI targets independently of native provider selection, reuse successful managed selections, and keep evidence bound to the actual inspected target. Classify canonical CLI error envelopes and exhaustive setup/update/uninstall outcomes, audit registered CLI coverage, and assert platform-specific runtime results. * docs: refresh boundary contracts and shipped roadmap behavior
267 lines
8.8 KiB
TypeScript
267 lines
8.8 KiB
TypeScript
import { homedir } from "node:os";
|
|
|
|
import { supportsNodeVersion } from "../domain/runtimeVersion.js";
|
|
import { linuxHopperLauncherPath } from "./LinuxHopper.js";
|
|
import type {
|
|
DoctorCheck,
|
|
DoctorHost,
|
|
DoctorProviderInspection,
|
|
} from "./Doctor.js";
|
|
|
|
const DEFAULT_HOPPER =
|
|
"/Applications/Hopper Disassembler.app/Contents/MacOS/hopper";
|
|
const SYSTEM_LINUX_HOPPER = "/opt/hopper/bin/Hopper";
|
|
|
|
/** Ordered environment diagnostics consumed by the Doctor report assembler. */
|
|
export interface DoctorDiagnostics {
|
|
readonly checks: readonly DoctorCheck[];
|
|
readonly hopperPath?: string;
|
|
readonly providerInspections?: readonly DoctorProviderInspection[];
|
|
}
|
|
|
|
/** Collect host, provider, and optional-target diagnostics without mutation. */
|
|
export const collectDoctorDiagnostics = async (
|
|
target: string | undefined,
|
|
host: DoctorHost,
|
|
): Promise<DoctorDiagnostics> => {
|
|
const checks: DoctorCheck[] = [nodeCheck(host)];
|
|
checks.push(await hostCheck(host));
|
|
|
|
const hopperPath = await findHopper(host);
|
|
checks.push(hopperCheck(host, hopperPath));
|
|
const hopperVersionCheck = await optionalHopperVersionCheck(host, hopperPath);
|
|
if (hopperVersionCheck !== undefined) checks.push(hopperVersionCheck);
|
|
|
|
const providerInspections = await host.providerInspections?.();
|
|
if (providerInspections !== undefined)
|
|
checks.push(...providerInspections.flatMap(providerDoctorChecks));
|
|
|
|
const ilspyCheck = await optionalIlspyCheck(host);
|
|
if (ilspyCheck !== undefined) checks.push(ilspyCheck);
|
|
const hopperDemoCheck = await optionalHopperDemoCheck(host, hopperPath);
|
|
if (hopperDemoCheck !== undefined) checks.push(hopperDemoCheck);
|
|
const targetCheck = await optionalTargetCheck(target, host);
|
|
if (targetCheck !== undefined) checks.push(targetCheck);
|
|
|
|
return {
|
|
checks,
|
|
...(hopperPath === undefined ? {} : { hopperPath }),
|
|
...(providerInspections === undefined ? {} : { providerInspections }),
|
|
};
|
|
};
|
|
|
|
/** Evaluate core health while treating unconfigured providers as optional. */
|
|
export const doctorHealthy = (
|
|
checks: readonly DoctorCheck[],
|
|
providers: {
|
|
readonly hopperAvailable: boolean;
|
|
readonly hopperConfigured: boolean;
|
|
readonly providerInspections:
|
|
| readonly DoctorProviderInspection[]
|
|
| undefined;
|
|
},
|
|
): boolean => {
|
|
if (providers.providerInspections === undefined)
|
|
return checks.every(({ ok }) => ok);
|
|
const providerChecks = checks.filter(
|
|
({ name }) =>
|
|
name.startsWith("hopper") ||
|
|
providers.providerInspections?.some((inspection) =>
|
|
providerCheckName(inspection.id, name),
|
|
) === true,
|
|
);
|
|
const coreHealthy = checks
|
|
.filter((candidate) => !providerChecks.includes(candidate))
|
|
.every(({ ok }) => ok);
|
|
const configuredProvidersHealthy = providerChecks
|
|
.filter(({ name }) => {
|
|
if (name.startsWith("hopper")) return providers.hopperConfigured;
|
|
return providers.providerInspections?.some(
|
|
(inspection) =>
|
|
inspection.configured && providerCheckName(inspection.id, name),
|
|
);
|
|
})
|
|
.every(({ ok }) => ok);
|
|
return (
|
|
coreHealthy &&
|
|
configuredProvidersHealthy &&
|
|
(providers.hopperAvailable ||
|
|
providers.providerInspections.some(({ available }) => available))
|
|
);
|
|
};
|
|
|
|
const nodeCheck = (host: DoctorHost): DoctorCheck =>
|
|
check("node", supportsNodeVersion(host.nodeVersion), host.nodeVersion, {
|
|
remediation: "Install Node.js 22.19+ or 24.11+.",
|
|
classification: "missing_dependency",
|
|
});
|
|
|
|
const hostCheck = async (host: DoctorHost): Promise<DoctorCheck> => {
|
|
const macosVersion =
|
|
host.platform === "darwin" ? await host.macosVersion() : undefined;
|
|
const linuxDistribution =
|
|
host.platform === "linux" ? await host.linuxDistribution() : undefined;
|
|
const supported =
|
|
(host.platform === "darwin" && parseMajor(macosVersion) >= 12) ||
|
|
(host.platform === "linux" && linuxDistribution?.supported === true) ||
|
|
(host.platform === "win32" && host.architecture === "x64");
|
|
const detail =
|
|
macosVersion ??
|
|
(linuxDistribution === undefined
|
|
? `${host.platform} ${host.architecture}`
|
|
: `${linuxDistribution.id} ${linuxDistribution.versionId ?? "unknown"}`);
|
|
return check("host", supported, detail, {
|
|
remediation:
|
|
"REA supports macOS 12+, Ubuntu 24.04+, Fedora 41+, 64-bit Arch Linux, and the experimental Windows x64 Ghidra P0 boundary.",
|
|
classification: "unsupported_host",
|
|
});
|
|
};
|
|
|
|
const findHopper = async (host: DoctorHost): Promise<string | undefined> => {
|
|
const candidates =
|
|
host.configuredHopperPath === undefined
|
|
? [
|
|
DEFAULT_HOPPER,
|
|
SYSTEM_LINUX_HOPPER,
|
|
linuxHopperLauncherPath(host.homeDirectory ?? homedir()),
|
|
...(await host.manualHopperPaths()),
|
|
]
|
|
: [host.configuredHopperPath];
|
|
const hopperPath = await firstExecutable(host, candidates);
|
|
if (hopperPath !== undefined || host.configuredHopperPath !== undefined)
|
|
return hopperPath;
|
|
const brewCandidate = await host.brewHopperPath();
|
|
return firstExecutable(
|
|
host,
|
|
brewCandidate === undefined ? [] : [brewCandidate],
|
|
);
|
|
};
|
|
|
|
const firstExecutable = async (
|
|
host: DoctorHost,
|
|
candidates: readonly string[],
|
|
): Promise<string | undefined> => {
|
|
for (const candidate of new Set(candidates))
|
|
if (await host.executable(candidate)) return candidate;
|
|
return undefined;
|
|
};
|
|
|
|
const hopperCheck = (
|
|
host: DoctorHost,
|
|
hopperPath: string | undefined,
|
|
): DoctorCheck =>
|
|
check(
|
|
"hopper",
|
|
hopperPath !== undefined,
|
|
hopperPath ?? host.configuredHopperPath,
|
|
{
|
|
remediation:
|
|
host.configuredHopperPath === undefined
|
|
? "Run rea setup to install Hopper, or set HOPPER_LAUNCHER_PATH."
|
|
: "Unset or update HOPPER_LAUNCHER_PATH to an executable Hopper launcher.",
|
|
classification:
|
|
host.configuredHopperPath === undefined
|
|
? "missing_analysis_engine"
|
|
: "config_drift",
|
|
},
|
|
);
|
|
|
|
const optionalHopperVersionCheck = async (
|
|
host: DoctorHost,
|
|
hopperPath: string | undefined,
|
|
): Promise<DoctorCheck | undefined> => {
|
|
if (host.platform !== "linux" || hopperPath === undefined) return undefined;
|
|
return check(
|
|
"hopper-version",
|
|
await host.supportedLinuxHopper(hopperPath),
|
|
hopperPath,
|
|
{
|
|
remediation: unsupportedHopperRemediation(
|
|
host.configuredHopperPath === hopperPath,
|
|
),
|
|
classification: "config_drift",
|
|
},
|
|
);
|
|
};
|
|
|
|
const optionalHopperDemoCheck = async (
|
|
host: DoctorHost,
|
|
hopperPath: string | undefined,
|
|
): Promise<DoctorCheck | undefined> =>
|
|
host.platform !== "linux" || hopperPath === undefined
|
|
? undefined
|
|
: host.linuxDemoRuntimeCheck();
|
|
|
|
const optionalIlspyCheck = async (
|
|
host: DoctorHost,
|
|
): Promise<DoctorCheck | undefined> => {
|
|
if (host.configuredIlspyCmdPath === undefined) return undefined;
|
|
const version = await host.ilspyCmdVersion?.(host.configuredIlspyCmdPath);
|
|
return check(
|
|
"ilspycmd",
|
|
version !== undefined,
|
|
version === undefined
|
|
? host.configuredIlspyCmdPath
|
|
: `${host.configuredIlspyCmdPath} (${version})`,
|
|
{
|
|
remediation:
|
|
"Unset REA_ILSPY_CMD_PATH or point it at a runnable ilspycmd executable.",
|
|
classification: "config_drift",
|
|
},
|
|
);
|
|
};
|
|
|
|
const optionalTargetCheck = async (
|
|
target: string | undefined,
|
|
host: DoctorHost,
|
|
): Promise<DoctorCheck | undefined> =>
|
|
target === undefined
|
|
? undefined
|
|
: check("target", await host.validTarget(target), target, {
|
|
remediation: "Supply a readable local app or program path.",
|
|
classification: "config_drift",
|
|
});
|
|
|
|
const check = (
|
|
name: string,
|
|
ok: boolean,
|
|
detail: string | undefined,
|
|
failure: {
|
|
readonly remediation: string;
|
|
readonly classification: Exclude<DoctorCheck["classification"], "healthy">;
|
|
},
|
|
): DoctorCheck => ({
|
|
name,
|
|
ok,
|
|
classification: ok ? "healthy" : failure.classification,
|
|
...(detail === undefined ? {} : { detail }),
|
|
...(ok ? {} : { remediation: failure.remediation }),
|
|
});
|
|
|
|
const providerDoctorChecks = (
|
|
inspection: DoctorProviderInspection,
|
|
): readonly DoctorCheck[] =>
|
|
inspection.checks.map((candidate) => ({
|
|
name:
|
|
candidate.name === "configuration"
|
|
? inspection.id
|
|
: `${inspection.id}-${candidate.name}`,
|
|
ok: candidate.ok,
|
|
classification: candidate.ok ? "healthy" : candidate.classification,
|
|
detail: candidate.detail,
|
|
...(candidate.remediation === null
|
|
? {}
|
|
: { remediation: candidate.remediation }),
|
|
}));
|
|
|
|
const unsupportedHopperRemediation = (configured: boolean): string =>
|
|
configured
|
|
? "Unset or update HOPPER_LAUNCHER_PATH, install the Hopper build supported by this REA release, or update REA."
|
|
: "Install the Hopper build supported by this REA release, or update REA for a newer Hopper build.";
|
|
|
|
const parseMajor = (version: string | undefined): number =>
|
|
Number.parseInt(version?.split(".")[0] ?? "0", 10);
|
|
|
|
const providerCheckName = (providerId: string, name: string): boolean =>
|
|
name === providerId || name.startsWith(`${providerId}-`);
|