mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
fix(native): report dispatch target applicability (#8400)
* fix(native): report dispatch target applicability * fix(native): clarify unavailable mode recovery * test(kuna): avoid overlapping verifier imports * docs(native): explain mode-specific discovery * fix(native): preserve unbound provider recovery
This commit is contained in:
1 parent
d684125eed
commit
bafe3784cd
9 files changed
+246
-7
No files matched your search
@@ -765,6 +765,13 @@ finish without reading relationships, even when neither traversal mode is
|
||||
available. Discovery does not block that direct invocation or replace its typed
|
||||
partial/error outcomes.
|
||||
|
||||
`trace_native_ui_action` reports authored-UI and native-seed modes separately.
|
||||
Apple dispatch metadata requires a Mach-O target; another provider may supply
|
||||
the symbol-only alternative on other formats. If neither mode is available,
|
||||
read each mode's missing operations and remediation: a remedy for an authored
|
||||
app-bundle route does not by itself describe the native-seed route. Availability
|
||||
does not guarantee that every selector or provider operation will succeed.
|
||||
|
||||
With complete detail, the `effects` and `annotations` arrays list the true
|
||||
members of their fixed vocabularies. A known member omitted from either array is false. This applies
|
||||
only to these closed flag sets; it does not make an omitted tool, provider, mode,
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
verifyKunaStrings,
|
||||
} from "./lib/kuna-string-verifier.mjs";
|
||||
import { nativeKunaIncomingSites } from "./lib/kuna-native-reference-verifier.mjs";
|
||||
import { MULTI_MODE_REMEDIATION } from "../dist/application/CapabilityInventory.js";
|
||||
import {
|
||||
assertVerifierCleanupComplete,
|
||||
completeVerifierRun,
|
||||
@@ -298,6 +299,36 @@ const verifyKuna = async (
|
||||
const answers = [];
|
||||
let coldFailure;
|
||||
try {
|
||||
const status = await cold.call("binary_session", { detail: "complete" });
|
||||
if (status.value.format === "elf") {
|
||||
const availability = status.value.tool_availability;
|
||||
const dispatch = availability.find(
|
||||
({ name }) => name === "inspect_native_dispatch_metadata",
|
||||
);
|
||||
const trace = availability.find(
|
||||
({ name }) => name === "trace_native_ui_action",
|
||||
);
|
||||
assert.equal(dispatch?.available, false);
|
||||
assert.equal(
|
||||
trace?.modes?.find(({ name }) => name === "native_seed_call_trace")
|
||||
?.available,
|
||||
false,
|
||||
);
|
||||
if (process.platform === "darwin") {
|
||||
const authored = trace?.modes?.find(
|
||||
({ name }) => name === "authored_ui_dispatch",
|
||||
);
|
||||
const nativeSeed = trace?.modes?.find(
|
||||
({ name }) => name === "native_seed_call_trace",
|
||||
);
|
||||
assert.equal(trace?.remediation, MULTI_MODE_REMEDIATION);
|
||||
assert.notEqual(authored?.remediation, nativeSeed?.remediation);
|
||||
assert.ok(
|
||||
authored?.missing_operations.includes("decode_interface_builder"),
|
||||
);
|
||||
assert.ok(nativeSeed?.missing_operations.includes("list_names"));
|
||||
}
|
||||
}
|
||||
answers.push({
|
||||
strings: await verifyKunaStrings({
|
||||
call: cold.call,
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { AnalysisWorkScope } from "./AnalysisWorkScope.js";
|
||||
import type { AnalysisObservation } from "../domain/analysisObservation.js";
|
||||
import type { ProviderInspectionScope } from "./ProviderInspectionScope.js";
|
||||
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
||||
import type { TargetApplicability } from "../domain/targetApplicability.js";
|
||||
import type { OfficialToolName } from "../contracts/officialToolContracts.js";
|
||||
import type { EnhancedToolName } from "../contracts/enhancedInputs.js";
|
||||
import type { NativeToolName } from "../contracts/native/nativeToolContracts.js";
|
||||
@@ -260,6 +261,8 @@ type CapabilityAvailability =
|
||||
};
|
||||
|
||||
export type CapabilityDescriptor = CapabilityAvailability & {
|
||||
/** Provider-owned static target requirements, evaluated for an open target. */
|
||||
readonly targetApplicability?: TargetApplicability;
|
||||
/**
|
||||
* Snapshot opts immutable, profile-bound reads into replay despite incidental
|
||||
* private filesystem writes. Live state must always be re-observed. Omitting
|
||||
|
||||
@@ -126,6 +126,10 @@ type AvailabilityModeResult =
|
||||
| (Extract<ToolAvailabilityMode, { available: false }> & {
|
||||
readonly reason: ToolUnavailabilityReason;
|
||||
});
|
||||
|
||||
/** Recovery when unavailable modes require different actions. */
|
||||
export const MULTI_MODE_REMEDIATION =
|
||||
"No mode is currently advertised as available. Inspect each mode's missing operations and remediation.";
|
||||
type AvailabilityContext = {
|
||||
readonly name: string;
|
||||
readonly kind: ToolKind;
|
||||
@@ -782,6 +786,7 @@ const targetAvailability = ({
|
||||
const descriptor = descriptors.get(name);
|
||||
if (
|
||||
targetOpen &&
|
||||
kind !== "enhanced" &&
|
||||
descriptor?.available === false &&
|
||||
(descriptor.availability_code === "target_kind_unsupported" ||
|
||||
descriptor.availability_code === "target_format_unsupported")
|
||||
@@ -979,6 +984,10 @@ const availabilityForModes = (
|
||||
)[0];
|
||||
const specificFailureRemediation =
|
||||
failure?.reason === "provider_missing" ? undefined : failure?.remediation;
|
||||
const distinctModeRemediations =
|
||||
defaultMode === undefined &&
|
||||
failures.length > 1 &&
|
||||
new Set(failures.map((mode) => mode.remediation)).size > 1;
|
||||
const facts = {
|
||||
...(defaultMode === undefined
|
||||
? {}
|
||||
@@ -996,9 +1005,10 @@ const availabilityForModes = (
|
||||
return {
|
||||
...facts,
|
||||
reason: failure?.reason ?? "provider_missing",
|
||||
remediation:
|
||||
specificFailureRemediation ??
|
||||
`Configure a provider for one of: ${missingOperations.join(", ")}.`,
|
||||
remediation: distinctModeRemediations
|
||||
? MULTI_MODE_REMEDIATION
|
||||
: (specificFailureRemediation ??
|
||||
`Configure a provider for one of: ${missingOperations.join(", ")}.`),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -28,6 +28,10 @@ import {
|
||||
nativeMacOSCapabilities,
|
||||
} from "../../native/NativeMacOSProviderMetadata.js";
|
||||
import { NATIVE_MACOS_PROVIDER_IDENTITY } from "../../native/NativeMacosProviderIdentity.js";
|
||||
import {
|
||||
buildCapabilityInventory,
|
||||
MULTI_MODE_REMEDIATION,
|
||||
} from "../CapabilityInventory.js";
|
||||
|
||||
const projectedStatusSchema = z.object({
|
||||
capabilities: sessionProviderFacts.shape.capabilities,
|
||||
@@ -517,6 +521,146 @@ it.each(["elf", "pe"] as const)(
|
||||
},
|
||||
);
|
||||
|
||||
const dispatchTarget = (format: "elf" | "mach-o"): BinaryTarget => ({
|
||||
kind: "executable",
|
||||
format,
|
||||
path: "/fixture",
|
||||
sha256: "a".repeat(64),
|
||||
architecture: "x86_64",
|
||||
availableArchitectures: ["x86_64"],
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ format: "elf", symbols: false, nativeAvailable: false, route: false },
|
||||
{ format: "elf", symbols: true, nativeAvailable: false, route: true },
|
||||
{ format: "mach-o", symbols: false, nativeAvailable: true, route: true },
|
||||
] as const)(
|
||||
"projects $format dispatch applicability and symbol fallback ($symbols)",
|
||||
async ({ format, symbols, nativeAvailable, route }) => {
|
||||
const target = dispatchTarget(format);
|
||||
const native = nativeMacOSCapabilities("darwin").find(
|
||||
(item) => item.operation === "inspect_native_dispatch_metadata",
|
||||
);
|
||||
if (native === undefined) throw new Error("Missing native dispatch reader");
|
||||
const capabilities = [
|
||||
native,
|
||||
descriptor(providerA, "decode_interface_builder", []),
|
||||
descriptor(providerA, "procedure_address", []),
|
||||
descriptor(providerA, "procedure_callees", []),
|
||||
...(symbols ? [descriptor(providerA, "list_names", [])] : []),
|
||||
];
|
||||
const status = await projectStatus(capabilities, [], { target });
|
||||
const parsed = projectedStatusSchema.parse(status);
|
||||
const projected = expandCanonicalCapabilities(parsed.capabilities).find(
|
||||
(item) => item.operation === "inspect_native_dispatch_metadata",
|
||||
);
|
||||
expect(projected).toMatchObject({
|
||||
available: nativeAvailable,
|
||||
availability_code: nativeAvailable ? null : "target_format_unsupported",
|
||||
});
|
||||
const inventory = buildCapabilityInventory(status, {
|
||||
processCaptureEnabled: false,
|
||||
});
|
||||
const dispatch = inventory.find(
|
||||
(item) => item.name === "inspect_native_dispatch_metadata",
|
||||
);
|
||||
expect(dispatch?.available).toBe(route);
|
||||
const trace = inventory.find(
|
||||
(item) => item.name === "trace_native_ui_action",
|
||||
);
|
||||
expect(
|
||||
trace?.modes?.find((mode) => mode.name === "native_seed_call_trace"),
|
||||
).toMatchObject({
|
||||
available: route,
|
||||
missing_operations: route
|
||||
? []
|
||||
: ["inspect_native_dispatch_metadata", "list_names"],
|
||||
});
|
||||
if (!route) {
|
||||
expect(trace?.remediation).toBe(MULTI_MODE_REMEDIATION);
|
||||
const authored = trace?.modes?.find(
|
||||
(mode) => mode.name === "authored_ui_dispatch",
|
||||
);
|
||||
const nativeSeed = trace?.modes?.find(
|
||||
(mode) => mode.name === "native_seed_call_trace",
|
||||
);
|
||||
expect(authored?.remediation).not.toBe(nativeSeed?.remediation);
|
||||
expect(authored?.missing_operations).toContain(
|
||||
"decode_interface_builder",
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("does not impose the Apple reader's target rule on another provider", async () => {
|
||||
const target = dispatchTarget("elf");
|
||||
const status = await projectStatus(
|
||||
[descriptor(providerA, "inspect_native_dispatch_metadata", [])],
|
||||
[],
|
||||
{ target },
|
||||
);
|
||||
const projected = expandCanonicalCapabilities(
|
||||
projectedStatusSchema.parse(status).capabilities,
|
||||
);
|
||||
expect(projected).toMatchObject([
|
||||
{ operation: "inspect_native_dispatch_metadata", available: true },
|
||||
]);
|
||||
const inventory = buildCapabilityInventory(status, {
|
||||
processCaptureEnabled: false,
|
||||
});
|
||||
expect(
|
||||
inventory.find((item) => item.name === "inspect_native_dispatch_metadata"),
|
||||
).toMatchObject({ available: true });
|
||||
});
|
||||
|
||||
it("keeps distinct recovery for an unbound non-bundle session", async () => {
|
||||
const native = nativeMacOSCapabilities("darwin").find(
|
||||
(item) => item.operation === "inspect_native_dispatch_metadata",
|
||||
);
|
||||
if (native === undefined) throw new Error("Missing native dispatch reader");
|
||||
const status = await projectStatus(
|
||||
[native, descriptor(providerA, "decode_interface_builder", [])],
|
||||
[],
|
||||
{ target: dispatchTarget("mach-o") },
|
||||
);
|
||||
const inventory = buildCapabilityInventory(status, {
|
||||
processCaptureEnabled: false,
|
||||
});
|
||||
const trace = inventory.find(
|
||||
(item) => item.name === "trace_native_ui_action",
|
||||
);
|
||||
expect(trace).toMatchObject({
|
||||
available: false,
|
||||
reason: "provider_unavailable",
|
||||
remediation: MULTI_MODE_REMEDIATION,
|
||||
});
|
||||
const authored = trace?.modes?.find(
|
||||
(mode) => mode.name === "authored_ui_dispatch",
|
||||
);
|
||||
const nativeSeed = trace?.modes?.find(
|
||||
(mode) => mode.name === "native_seed_call_trace",
|
||||
);
|
||||
expect(authored?.missing_operations).toContain("decode_interface_builder");
|
||||
expect(nativeSeed?.missing_operations).toContain("procedure_address");
|
||||
expect(authored?.remediation).not.toBe(nativeSeed?.remediation);
|
||||
});
|
||||
|
||||
it("keeps host unavailability ahead of target applicability", async () => {
|
||||
const native = nativeMacOSCapabilities("linux").find(
|
||||
(item) => item.operation === "inspect_native_dispatch_metadata",
|
||||
);
|
||||
if (native === undefined) throw new Error("Missing native dispatch reader");
|
||||
const target = dispatchTarget("elf");
|
||||
const projected = await project([native], [], new Map(), target);
|
||||
expect(expandCanonicalCapabilities(projected.capabilities)).toMatchObject([
|
||||
{
|
||||
operation: "inspect_native_dispatch_metadata",
|
||||
available: false,
|
||||
availability_code: "unsupported_host",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
describe("binary session summary projection", () => {
|
||||
it("keeps each candidate's status, code and reason in summary without diagnostics or capability sets", async () => {
|
||||
const unavailable: AnalysisProviderCandidateStatus = {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { z } from "zod";
|
||||
import type { InvestigationStatistics } from "../../domain/investigationLifecycle.js";
|
||||
import { artifactTargetRefusal } from "../../domain/apple/artifactTargetApplicability.js";
|
||||
import { targetApplicabilityRefusal } from "../../domain/targetApplicability.js";
|
||||
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
|
||||
import type { ResolvedAppBundleResourceRoot } from "../AppBundleExecutable.js";
|
||||
import { binaryTargetSummary } from "./BinaryTargetSummary.js";
|
||||
@@ -76,7 +77,12 @@ export const binarySessionStatus = async ({
|
||||
const targetRefusal =
|
||||
target === undefined
|
||||
? undefined
|
||||
: artifactTargetRefusal(descriptor.operation, target);
|
||||
: descriptor.targetApplicability === undefined
|
||||
? artifactTargetRefusal(descriptor.operation, target)
|
||||
: targetApplicabilityRefusal(
|
||||
descriptor.targetApplicability,
|
||||
target,
|
||||
);
|
||||
if (targetRefusal !== undefined && descriptor.available)
|
||||
return {
|
||||
...descriptor,
|
||||
|
||||
@@ -2,10 +2,13 @@ import { objcClassSymbol } from "../objcClassSymbol.js";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
BINARY_ARCHITECTURES,
|
||||
isMachOFormat,
|
||||
type BinaryTarget,
|
||||
} from "../binaryTargetTypes.js";
|
||||
import { providerSchema } from "../evidence.js";
|
||||
import {
|
||||
targetApplicabilityRefusal,
|
||||
type TargetApplicability,
|
||||
} from "../targetApplicability.js";
|
||||
import { isSwiftSymbolName } from "../swiftSymbolClassification.js";
|
||||
import { nativeCallReferenceSchema } from "./nativeCallReference.js";
|
||||
import { machoTargetSelectionSchema } from "../apple/machoTargetSelection.js";
|
||||
@@ -20,8 +23,15 @@ import {
|
||||
} from "../unicodeCodePointOrder.js";
|
||||
|
||||
/** Targets admitted to the Apple byte reader; the reader validates slice support. */
|
||||
export const APPLE_DISPATCH_TARGET_APPLICABILITY: TargetApplicability = {
|
||||
kinds: ["executable"],
|
||||
formats: ["mach-o", "mach-o-universal"],
|
||||
reason:
|
||||
"Binary Apple metadata requires Mach-O; other providers can report symbol-only metadata",
|
||||
};
|
||||
export const isAppleDispatchMetadataTarget = (target: BinaryTarget): boolean =>
|
||||
target.kind === "executable" && isMachOFormat(target.format);
|
||||
targetApplicabilityRefusal(APPLE_DISPATCH_TARGET_APPLICABILITY, target) ===
|
||||
undefined;
|
||||
|
||||
/** Objective-C method type. */
|
||||
export const objcMethodTypeSchema = z.enum([
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { BinaryTarget } from "./binaryTargetTypes.js";
|
||||
|
||||
/** Target facts required by an individual provider operation. */
|
||||
export interface TargetApplicability {
|
||||
readonly kinds: readonly BinaryTarget["kind"][];
|
||||
readonly formats: readonly BinaryTarget["format"][];
|
||||
readonly reason: string;
|
||||
}
|
||||
|
||||
/** Evaluate provider-owned requirements against the admitted target. */
|
||||
export const targetApplicabilityRefusal = (
|
||||
applicability: TargetApplicability,
|
||||
target: BinaryTarget,
|
||||
):
|
||||
| {
|
||||
readonly code: "target_kind_unsupported" | "target_format_unsupported";
|
||||
readonly reason: string;
|
||||
}
|
||||
| undefined =>
|
||||
!applicability.kinds.includes(target.kind)
|
||||
? { code: "target_kind_unsupported", reason: applicability.reason }
|
||||
: !applicability.formats.includes(target.format)
|
||||
? { code: "target_format_unsupported", reason: applicability.reason }
|
||||
: undefined;
|
||||
@@ -2,6 +2,7 @@ import { NATIVE_MACOS_PROVIDER_IDENTITY } from "./NativeMacosProviderIdentity.js
|
||||
import type { CapabilityDescriptor } from "../application/AnalysisProvider.js";
|
||||
import { createAnalysisProfile } from "../domain/analysisProfile.js";
|
||||
import { NATIVE_TOOL_CONTRACTS } from "../contracts/native/nativeToolContracts.js";
|
||||
import { APPLE_DISPATCH_TARGET_APPLICABILITY } from "../domain/native/objcSwiftMetadata.js";
|
||||
|
||||
/** Source-owned commitment for the pure Apple dispatch metadata reader. */
|
||||
export const APPLE_DISPATCH_METADATA_ANALYSIS_PROFILE = createAnalysisProfile(
|
||||
@@ -43,7 +44,10 @@ export const nativeMacOSCapabilities = (
|
||||
provider: NATIVE_MACOS_PROVIDER_IDENTITY,
|
||||
operation: contract.name,
|
||||
...(contract.name === "inspect_native_dispatch_metadata"
|
||||
? { analysisProfile: APPLE_DISPATCH_METADATA_ANALYSIS_PROFILE }
|
||||
? {
|
||||
analysisProfile: APPLE_DISPATCH_METADATA_ANALYSIS_PROFILE,
|
||||
targetApplicability: APPLE_DISPATCH_TARGET_APPLICABILITY,
|
||||
}
|
||||
: {}),
|
||||
...availability,
|
||||
effects: Object.freeze({
|
||||
|
||||
Reference in new issue
Block a user