mirror of
https://github.com/morluto/rea.git
synced 2026-11-09 08:17:02 +00:00
fix(discovery): carry provider repair instructions into capability discovery (#8532)
* fix(discovery): carry provider repair instructions into capability discovery Provider readiness reported only a failure description, so capability discovery copied that description into tool_availability remediation and dropped repairs the owners already had: the JEB discovery-deadline setting, the ADB/Apktool execute-permission repair kept in diagnostics, and the IDA and Kuna configuration recovery. Unavailable ProviderAvailability now requires the owner's remediation next to its reason, and readiness-derived unavailable capability descriptors carry it too. Every producer supplies it (ADB, Apktool, JADX, JEB, Ghidra, Hopper, IDA, Kuna, pcap, owned-process prerequisites, profile rejection); the inventory uses it verbatim, falling back to the reason only for static capability facts that declare no repair. ADB/Apktool configuration failures own remediation as a field instead of diagnostics, and execution refusals project it as remediation.action. JADX doctor reads the availability's remediation instead of re-deriving it. binary_session complete candidate availability and unavailable capability groups expose remediation (CLI capabilities/providers too); summary omits it and lists it in omitted_fields. The superseded IDA configuration_remediation and windows_native_failure.remediation diagnostics are removed. Fixes #8506 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(discovery): keep owner repairs through provider selection and capability maps Address review findings on the readiness-remediation contract: - Provider selection rejections carry the availability owner's remediation, CLI/MCP error details project it, and an explicit selection refused by readiness uses it as remediation.action. IDA's unreadable registration and Ghidra's Windows readiness repair no longer degrade to generic selection advice now that the diagnostics copies are gone. - Ghidra's Windows readiness and existing-project capability maps carry the same repair as the availability. - pcap readiness keeps the limiter owner's repair (now a typed remediationAction) and falls back to Wireshark guidance only when the failure has none. - ADB/Apktool repairs are owner constants that are complete sentences. The execution refusal keeps its previous message-plus-repair recovery, which the compiled CLI contract asserts. - Regression tests compare with the owning formatter or constant and add public MCP and CLI coverage for an explicit IDA selection. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
eb39a07966
commit
cbc69537cd
56 files changed
+913
-259
No files matched your search
@@ -17,6 +17,7 @@ import type {
|
||||
} from "../AnalysisProvider.js";
|
||||
import type { AnalysisProviderCandidateStatus } from "./AnalysisProviderRegistry.js";
|
||||
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
|
||||
import { analysisErrorRemediationAction } from "../../domain/analysisErrorPresentation.js";
|
||||
|
||||
/** Profile-resolution truth for one discovered provider candidate. */
|
||||
export interface AnalysisProviderCandidateEvaluation {
|
||||
@@ -76,9 +77,15 @@ export const evaluateAnalysisProviderCandidate = async (
|
||||
},
|
||||
},
|
||||
}
|
||||
: rejectedProfile(candidate, status, error.message, {
|
||||
error_tag: error._tag,
|
||||
});
|
||||
: rejectedProfile(
|
||||
candidate,
|
||||
status,
|
||||
{
|
||||
reason: error.message,
|
||||
remediation: analysisErrorRemediationAction(error),
|
||||
},
|
||||
{ error_tag: error._tag },
|
||||
);
|
||||
return ok({ ...rejection, profileError: error });
|
||||
}
|
||||
if (signalIsAborted(signal))
|
||||
@@ -88,11 +95,10 @@ export const evaluateAnalysisProviderCandidate = async (
|
||||
try {
|
||||
if (resolved.value.profile === null)
|
||||
return ok(
|
||||
rejectedProfile(
|
||||
candidate,
|
||||
status,
|
||||
"Provider version or analysis profile could not be resolved",
|
||||
),
|
||||
rejectedProfile(candidate, status, {
|
||||
reason: "Provider version or analysis profile could not be resolved",
|
||||
remediation: PROFILE_REMEDIATION,
|
||||
}),
|
||||
);
|
||||
const profile = analysisProfileSchema.parse(resolved.value.profile);
|
||||
const identity = candidate.identity();
|
||||
@@ -103,11 +109,10 @@ export const evaluateAnalysisProviderCandidate = async (
|
||||
profile.provider.version !== identity.version)
|
||||
)
|
||||
return ok(
|
||||
rejectedProfile(
|
||||
candidate,
|
||||
status,
|
||||
"Resolved analysis profile has mismatched provider identity",
|
||||
),
|
||||
rejectedProfile(candidate, status, {
|
||||
reason: "Resolved analysis profile has mismatched provider identity",
|
||||
remediation: PROFILE_REMEDIATION,
|
||||
}),
|
||||
);
|
||||
return ok({
|
||||
candidate,
|
||||
@@ -120,11 +125,10 @@ export const evaluateAnalysisProviderCandidate = async (
|
||||
} catch (cause: unknown) {
|
||||
void cause;
|
||||
return ok(
|
||||
rejectedProfile(
|
||||
candidate,
|
||||
status,
|
||||
"Provider returned an invalid analysis profile",
|
||||
),
|
||||
rejectedProfile(candidate, status, {
|
||||
reason: "Provider returned an invalid analysis profile",
|
||||
remediation: PROFILE_REMEDIATION,
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -132,10 +136,13 @@ export const evaluateAnalysisProviderCandidate = async (
|
||||
const signalIsAborted = (signal: AbortSignal | undefined): boolean =>
|
||||
signal?.aborted === true;
|
||||
|
||||
const PROFILE_REMEDIATION =
|
||||
"Run rea doctor to check this provider's installation and version, then reopen the target, or select another analysis provider.";
|
||||
|
||||
const rejectedProfile = (
|
||||
candidate: AnalysisProviderCandidate,
|
||||
status: AnalysisProviderCandidateStatus,
|
||||
reason: string,
|
||||
rejection: { readonly reason: string; readonly remediation: string },
|
||||
diagnostics: Readonly<Record<string, JsonValue>> = {},
|
||||
): AnalysisProviderCandidateEvaluation => ({
|
||||
candidate,
|
||||
@@ -144,7 +151,7 @@ const rejectedProfile = (
|
||||
availability: {
|
||||
status: "unavailable",
|
||||
code: "version_unresolved",
|
||||
reason,
|
||||
...rejection,
|
||||
diagnostics: jsonObjectSchema.parse({
|
||||
...status.availability.diagnostics,
|
||||
...diagnostics,
|
||||
|
||||
@@ -93,6 +93,7 @@ it("uses one owner readiness snapshot for candidate availability and capabilitie
|
||||
status: "unavailable",
|
||||
code: "runtime_unavailable",
|
||||
reason: "native inspection failed",
|
||||
remediation: "Repair the fixture provider.",
|
||||
diagnostics: { failure_stage: "self-test" },
|
||||
},
|
||||
capabilities: [capability(base.identity())],
|
||||
@@ -462,6 +463,7 @@ describe("analysis provider registry: rejection origins", () => {
|
||||
status: "unavailable",
|
||||
code: "open_options_invalid",
|
||||
reason: "Configured registration is invalid",
|
||||
remediation: "Repair the fixture provider.",
|
||||
diagnostics: { registration_path: "/configured/alpha.json" },
|
||||
}),
|
||||
},
|
||||
@@ -727,6 +729,7 @@ const candidate = (
|
||||
status: "unavailable",
|
||||
code: options.unconfigured ? "not_configured" : "runtime_missing",
|
||||
reason: `${id} runtime is unavailable`,
|
||||
remediation: "Repair the fixture provider.",
|
||||
diagnostics: { executable_path: `/opt/${id}/bin/analyze` },
|
||||
},
|
||||
inspectTargetSupport: (target): ProviderTargetSupport =>
|
||||
|
||||
@@ -914,6 +914,7 @@ const selectedRejection = (
|
||||
origin,
|
||||
code: rejection.code,
|
||||
reason: rejection.reason,
|
||||
...("remediation" in rejection ? { remediation: rejection.remediation } : {}),
|
||||
// ProviderSelectionError owns the single defensive diagnostic copy.
|
||||
diagnostics: rejection.diagnostics,
|
||||
});
|
||||
|
||||
@@ -668,7 +668,8 @@ describe("binary session summary projection", () => {
|
||||
availability: {
|
||||
status: "unavailable",
|
||||
code: "not_configured",
|
||||
reason: "Set PROVIDER_B_HOME; see docs/provider-b.md.",
|
||||
reason: "PROVIDER_B_HOME is not set.",
|
||||
remediation: "Set PROVIDER_B_HOME; see docs/provider-b.md.",
|
||||
diagnostics: { checks: [{ name: "configuration", ok: false }] },
|
||||
},
|
||||
targetSupport: {
|
||||
@@ -726,7 +727,7 @@ describe("binary session summary projection", () => {
|
||||
availability: {
|
||||
status: "unavailable",
|
||||
code: "not_configured",
|
||||
reason: "Set PROVIDER_B_HOME; see docs/provider-b.md.",
|
||||
reason: "PROVIDER_B_HOME is not set.",
|
||||
},
|
||||
target_support: {
|
||||
status: "unsupported",
|
||||
@@ -737,6 +738,13 @@ describe("binary session summary projection", () => {
|
||||
},
|
||||
]);
|
||||
expect(JSON.stringify(raw)).not.toContain("diagnostics");
|
||||
// Complete detail keeps the owner's repair; summary omits it.
|
||||
expect(
|
||||
complete.analysis_provider_candidates[1]?.availability,
|
||||
).toMatchObject({
|
||||
remediation: "Set PROVIDER_B_HOME; see docs/provider-b.md.",
|
||||
});
|
||||
expect(JSON.stringify(raw)).not.toContain("PROVIDER_B_HOME;");
|
||||
expect(
|
||||
analysisProviderCandidate
|
||||
.array()
|
||||
@@ -775,3 +783,68 @@ it("narrows summary capabilities without inventing omitted candidate facts", asy
|
||||
reason: "No target is open.",
|
||||
});
|
||||
});
|
||||
|
||||
it("groups readiness repairs exactly and drops one replaced by a runtime observation", async () => {
|
||||
const unavailable = (
|
||||
operation: CapabilityDescriptor["operation"],
|
||||
remediation: string,
|
||||
): CapabilityDescriptor => {
|
||||
const declared = descriptor(providerA, operation, [], {
|
||||
available: false,
|
||||
reason: "Configured registration is invalid.",
|
||||
});
|
||||
if (declared.available) throw new Error("Expected an unavailable fixture");
|
||||
return { ...declared, remediation };
|
||||
};
|
||||
const status = await project(
|
||||
[
|
||||
unavailable("procedure_info", "Correct REA_FIXTURE_CONFIG."),
|
||||
unavailable("read_bytes", "Correct REA_FIXTURE_CONFIG."),
|
||||
unavailable("list_names", "Unset REA_FIXTURE_CACHE."),
|
||||
unavailable("xrefs", "Correct REA_FIXTURE_CONFIG."),
|
||||
],
|
||||
[],
|
||||
new Map([["xrefs", { reason: "Runtime operation failure" }]]),
|
||||
);
|
||||
const groups = status.capabilities.flatMap(({ groups }) => groups);
|
||||
expect(groups).toEqual([
|
||||
expect.objectContaining({
|
||||
operations: ["list_names"],
|
||||
remediation: "Unset REA_FIXTURE_CACHE.",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
operations: ["procedure_info", "read_bytes"],
|
||||
reason: "Configured registration is invalid.",
|
||||
remediation: "Correct REA_FIXTURE_CONFIG.",
|
||||
}),
|
||||
expect.not.objectContaining({ remediation: expect.anything() }),
|
||||
]);
|
||||
expect(groups[2]).toMatchObject({
|
||||
operations: ["xrefs"],
|
||||
reason: "Runtime operation failure",
|
||||
});
|
||||
const inventory = new Map(
|
||||
buildCapabilityInventory(
|
||||
{ open: true, kind: "executable", capabilities: status.capabilities },
|
||||
{ processCaptureEnabled: true },
|
||||
).map((entry) => [entry.name, entry]),
|
||||
);
|
||||
expect(inventory.get("procedure_info")?.remediation).toBe(
|
||||
"Correct REA_FIXTURE_CONFIG.",
|
||||
);
|
||||
// A static or runtime-only refusal has no separate repair to prefer.
|
||||
expect(inventory.get("xrefs")?.remediation).toBe("Runtime operation failure");
|
||||
// A scoped registration narrows operations without dropping their repair.
|
||||
const scoped = projectedStatusSchema.parse(
|
||||
restrictAdvertisedCapabilities(
|
||||
status,
|
||||
(operation) => operation === "procedure_info",
|
||||
),
|
||||
);
|
||||
expect(scoped.capabilities.flatMap(({ groups }) => groups)).toEqual([
|
||||
expect.objectContaining({
|
||||
operations: ["procedure_info"],
|
||||
remediation: "Correct REA_FIXTURE_CONFIG.",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
@@ -93,11 +93,7 @@ export const binarySessionStatus = async ({
|
||||
const unavailable = runtimeUnavailability.get(descriptor.operation);
|
||||
return unavailable === undefined
|
||||
? descriptor
|
||||
: {
|
||||
...descriptor,
|
||||
available: false as const,
|
||||
reason: unavailable.reason,
|
||||
};
|
||||
: runtimeUnavailable(descriptor, unavailable.reason);
|
||||
});
|
||||
const capabilities = providerCapabilitySets(routeCapabilities);
|
||||
const analysisProviderBinding =
|
||||
@@ -178,6 +174,19 @@ export const binarySessionStatus = async ({
|
||||
};
|
||||
};
|
||||
|
||||
/** A runtime observation replaces the declared state and its repair instruction. */
|
||||
const runtimeUnavailable = (
|
||||
descriptor: CapabilityDescriptor,
|
||||
reason: string,
|
||||
): CapabilityDescriptor => {
|
||||
if (descriptor.available) {
|
||||
const { availabilityCode: _available, ...declared } = descriptor;
|
||||
return { ...declared, available: false, reason };
|
||||
}
|
||||
const { remediation: _superseded, ...declared } = descriptor;
|
||||
return { ...declared, reason };
|
||||
};
|
||||
|
||||
/** Capability surfaces a registration may narrow without hiding provider facts. */
|
||||
const advertisedCapabilitySurfaces = z.object({
|
||||
capabilities: z.array(providerCapabilitySet),
|
||||
@@ -338,7 +347,11 @@ type CapabilityProjection = Omit<
|
||||
"operation" | "available" | "reason"
|
||||
> & { readonly operation: string } & (
|
||||
| { readonly available: true; readonly reason: null }
|
||||
| { readonly available: false; readonly reason: string }
|
||||
| {
|
||||
readonly available: false;
|
||||
readonly reason: string;
|
||||
readonly remediation?: string;
|
||||
}
|
||||
);
|
||||
type ProviderCapabilityGroup = ProviderCapabilitySet["groups"][number];
|
||||
|
||||
@@ -388,6 +401,9 @@ export const providerCapabilitySets = (
|
||||
available: false as const,
|
||||
reason: descriptor.reason,
|
||||
availability_code: availabilityCode,
|
||||
...(descriptor.remediation === undefined
|
||||
? {}
|
||||
: { remediation: descriptor.remediation }),
|
||||
};
|
||||
const key = JSON.stringify([
|
||||
effects,
|
||||
@@ -395,6 +411,7 @@ export const providerCapabilitySets = (
|
||||
status.available,
|
||||
status.reason,
|
||||
status.availability_code,
|
||||
status.available ? null : (status.remediation ?? null),
|
||||
]);
|
||||
const group = groups.get(key);
|
||||
if (group === undefined)
|
||||
|
||||
Reference in new issue
Block a user