Compare commits

...
11 changed files with 394 additions and 17 deletions

No files matched your search

+12
View File
@@ -972,6 +972,18 @@ 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_values` includes `value_flow_support` for the provider routed to
`analyze_function`. `supported` declares that the provider may supply value flow;
it does not guarantee a model for the selected function. `unavailable` includes
the provider's reason, and `unknown` means no declaration. The provider ID is
preserved, or null if no analysis provider is routed. This declaration is separate
from operation readiness and the availability of call-target resolution. When
call-target resolution is missing, a direct trace may still return a local graph
with call-resolution unknowns; even `max_depth: 0` attempts to resolve call sites.
When support is unknown but the declared operation prerequisites are ready,
availability can still be true; it does not promise that value flow is present.
The trace's returned observations and unknowns remain authoritative for that call.
`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,
+4
View File
@@ -317,6 +317,10 @@ export type CapabilityDescriptor = CapabilityAvailability & {
readonly operation: Exclude<AnalysisOperation, "health">;
readonly effects: CapabilityEffects;
readonly limitations: readonly string[];
/** Whether analyze_function can supply value flow for some procedures. */
readonly nativeValueFlow?:
| { readonly status: "supported" }
| { readonly status: "unavailable"; readonly reason: string };
};
/** Factory and capability declaration for an analysis implementation. */
+52 -9
View File
@@ -24,7 +24,6 @@ import {
toolAvailabilitySummaryInventory,
TOOL_ANNOTATION_NAMES,
TOOL_EFFECT_NAMES,
type ProviderCapability,
type ToolAvailabilityInventory,
type ToolAvailabilitySummaryInventory,
type ToolAvailability,
@@ -71,7 +70,7 @@ const statusSchema = z.object({
type ToolAvailabilityReason = "available" | ToolUnavailabilityReason;
type ProviderDescriptor = ProviderCapability;
type ProviderDescriptor = ReturnType<typeof expandProviderCapabilities>[number];
type UnavailableDescriptor = Extract<ProviderDescriptor, { available: false }>;
/** Optional integrations whose verified hosts exclude some platforms. */
@@ -310,6 +309,22 @@ export const buildCapabilityInventory = (
const facts = {
name: contract.name,
surface: contract.kind,
...(contract.name === "trace_native_values"
? {
value_flow_support: (() => {
const source = descriptors.get("analyze_function");
return source?.native_value_flow === undefined
? {
status: "unknown" as const,
provider_id: source?.provider.id ?? null,
}
: {
...source.native_value_flow,
provider_id: source.provider.id,
};
})(),
}
: {}),
...(availability.defaultModeAvailable === undefined
? {}
: { default_mode_available: availability.defaultModeAvailable }),
@@ -377,6 +392,9 @@ export function compactToolAvailability(
available: entry.available,
reason: entry.reason,
remediation: entry.remediation,
...(entry.value_flow_support === undefined
? {}
: { value_flow_support: entry.value_flow_support }),
...(entry.default_mode_available === undefined
? {}
: { default_mode_available: entry.default_mode_available }),
@@ -933,13 +951,38 @@ const composedAvailability = (
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;
const result =
"modes" in requirements
? availabilityForModes(
requirements.modes,
requirements.defaultMode,
descriptors,
)
: composedAvailabilityFor(requirements, descriptors).availability;
if (name !== "trace_native_values") return result;
const source = descriptors.get("analyze_function");
if (source?.available !== true) return result;
if (source.native_value_flow?.status === "unavailable")
return {
reason:
result.reason === "provider_unavailable" ||
result.reason === "unsupported_host"
? result.reason
: "provider_missing",
remediation: `${source.native_value_flow.reason} Select a provider that can report native value-flow observations; cross-function tracing also needs call-target resolution.${(result.reason === "provider_unavailable" || result.reason === "unsupported_host") && result.remediation !== null ? ` ${result.remediation}` : ""}`,
};
if (
result.reason === "provider_missing" &&
descriptors.get("resolve_native_call_targets") === undefined
)
return {
...result,
remediation:
source.native_value_flow?.status === "supported"
? "A direct trace may still return local value-flow observations with call-resolution unknowns. Configure a provider with resolve_native_call_targets for call resolution and cross-function tracing."
: "Value-flow support is undeclared. A direct trace may return partial results or an unknown. Configure a provider with resolve_native_call_targets for call resolution and cross-function tracing.",
};
return result;
};
/** Evaluate source groups once so discovery and missing-operation facts agree. */
@@ -10,6 +10,7 @@ import {
analysisProviderCandidate,
analysisProviderCandidateSummary,
expandProviderCapabilities,
expandToolAvailability,
sessionProviderFacts,
type ProviderCapabilitySet,
} from "../../contracts/toolOutputSchemaPrimitives.js";
@@ -30,6 +31,7 @@ import {
import { NATIVE_MACOS_PROVIDER_IDENTITY } from "../../native/NativeMacosProviderIdentity.js";
import {
buildCapabilityInventory,
compactToolAvailability,
MULTI_MODE_REMEDIATION,
} from "../CapabilityInventory.js";
@@ -74,7 +76,9 @@ type DescriptorOptions = {
| {
readonly available: false;
readonly reason?: string;
readonly availabilityCode?: "architecture_unsupported";
readonly availabilityCode?:
| "architecture_unsupported"
| "unsupported_host";
}
);
@@ -850,3 +854,91 @@ it("groups readiness repairs exactly and drops one replaced by a runtime observa
}),
]);
});
it.each([
{ detail: "summary", supported: true },
{ detail: "complete", supported: true },
{ detail: "summary", supported: false },
{ detail: "complete", supported: false },
] as const)(
"retains routed value-flow ownership and runtime failures in $detail discovery when supported=$supported",
async ({ detail, supported }) => {
const source = {
...descriptor(providerA, "analyze_function", []),
nativeValueFlow: supported
? { status: "supported" as const }
: { status: "unavailable" as const, reason: "No value flow." },
};
const resolver = descriptor(providerB, "resolve_native_call_targets", []);
const providerC = { id: "provider-c", name: "Provider C", version: "3" };
const unselected = {
...descriptor(providerC, "analyze_function", []),
nativeValueFlow: { status: "unavailable" as const, reason: "Unselected" },
};
const status = await projectStatus(
[source, resolver],
[{ ...candidate(providerC, [unselected]), selected: false }],
{
target: dispatchTarget("elf"),
detail,
runtimeUnavailability: new Map([
["resolve_native_call_targets", { reason: "Resolver failed." }],
]),
},
);
const projected = z
.object({ capabilities: sessionProviderFacts.shape.capabilities })
.parse(status);
const route = expandProviderCapabilities(projected.capabilities);
expect(
route.find((item) => item.operation === "analyze_function"),
).toMatchObject({
provider: providerA,
native_value_flow: { status: supported ? "supported" : "unavailable" },
});
const inventory = buildCapabilityInventory(status, {
processCaptureEnabled: false,
});
const item = expandToolAvailability(
compactToolAvailability(inventory, detail),
).find((candidate) => candidate.name === "trace_native_values");
expect(item).toMatchObject({
available: false,
reason: "provider_unavailable",
remediation: expect.stringContaining("Resolver failed."),
value_flow_support: {
status: supported ? "supported" : "unavailable",
provider_id: providerA.id,
},
});
if (!supported) expect(item?.remediation).toContain("No value flow.");
},
);
it("preserves an unsupported resolver host alongside unavailable value flow", async () => {
const source = {
...descriptor(providerA, "analyze_function", []),
nativeValueFlow: {
status: "unavailable" as const,
reason: "No value flow.",
},
};
const resolver = {
...descriptor(providerB, "resolve_native_call_targets", [], {
available: false,
reason: "Resolver host is unsupported.",
availabilityCode: "unsupported_host",
}),
remediation: "Use a supported resolver host.",
};
const status = await projectStatus([source, resolver], [], {
target: dispatchTarget("elf"),
detail: "summary",
});
const item = buildCapabilityInventory(status, {
processCaptureEnabled: false,
}).find((entry) => entry.name === "trace_native_values");
expect(item).toMatchObject({ available: false, reason: "unsupported_host" });
expect(item?.remediation).toContain("No value flow.");
expect(item?.remediation).toContain("Use a supported resolver host.");
});
@@ -417,6 +417,7 @@ export const providerCapabilitySets = (
const key = JSON.stringify([
effects,
limitations,
descriptor.nativeValueFlow ?? null,
status.available,
status.reason,
status.availability_code,
@@ -429,6 +430,9 @@ export const providerCapabilitySets = (
operations: [descriptor.operation],
effects,
limitations,
...(descriptor.nativeValueFlow === undefined
? {}
: { native_value_flow: descriptor.nativeValueFlow }),
...status,
});
else group.operations.push(descriptor.operation);
@@ -53,6 +53,29 @@ export const requireOutputSchema = <
/** The readiness owner's repair instruction; never the failure description. */
const providerRemediation = z.string().min(1);
const nativeValueFlowDeclaration = z.discriminatedUnion("status", [
z.strictObject({ status: z.literal("supported") }),
z.strictObject({
status: z.literal("unavailable"),
reason: z.string().min(1),
}),
]);
const nativeValueFlowSupport = z.discriminatedUnion("status", [
z.strictObject({
status: z.literal("supported"),
provider_id: z.string().min(1),
}),
z.strictObject({
status: z.literal("unavailable"),
provider_id: z.string().min(1),
reason: z.string().min(1),
}),
z.strictObject({
status: z.literal("unknown"),
provider_id: z.string().min(1).nullable(),
}),
]);
const providerCapabilityFacts = {
operation: z.string(),
effects: z.object({
@@ -65,6 +88,7 @@ const providerCapabilityFacts = {
requires_root: z.boolean(),
}),
limitations: z.array(z.string()),
native_value_flow: nativeValueFlowDeclaration.exactOptional(),
};
export const providerCapability = z.discriminatedUnion("available", [
@@ -100,6 +124,7 @@ const providerCapabilityGroupFacts = {
operations: z.array(z.string().min(1)).min(1),
effects: z.array(providerCapabilityFacts.effects.keyof()),
limitations: z.array(z.string()),
native_value_flow: nativeValueFlowDeclaration.exactOptional(),
};
const providerCapabilityGroup = z.discriminatedUnion("available", [
@@ -138,6 +163,9 @@ export const expandProviderCapabilities = (
group.operations.map((operation) => ({
...providerCapability.parse({
operation,
...(group.native_value_flow === undefined
? {}
: { native_value_flow: group.native_value_flow }),
available: group.available,
reason: group.reason,
availability_code: group.availability_code,
@@ -375,6 +403,11 @@ const toolAvailabilityFacts = {
surface: z.string(),
default_mode_available: z.boolean().optional(),
modes: z.array(toolAvailabilityMode).optional(),
value_flow_support: nativeValueFlowSupport
.describe(
"The named provider's declared ability to supply value flow for some functions, separate from runtime readiness. Supported does not guarantee a model for a particular function; unknown means no declaration.",
)
.exactOptional(),
client_requirements: z.object({
required: z.array(clientFeatureNameSchema),
optional: z.array(clientFeatureNameSchema),
@@ -440,6 +473,7 @@ const toolAvailabilityStatusGroupFacts = {
names: z.array(z.string().min(1)).min(1),
default_mode_available: toolAvailabilityFacts.default_mode_available,
modes: toolAvailabilityFacts.modes,
value_flow_support: toolAvailabilityFacts.value_flow_support,
client_requirements: toolAvailabilityFacts.client_requirements.optional(),
};
@@ -449,6 +483,7 @@ const toolAvailabilityGroupFacts = {
default_mode_available:
toolAvailabilityStatusGroupFacts.default_mode_available,
modes: toolAvailabilityStatusGroupFacts.modes,
value_flow_support: toolAvailabilityStatusGroupFacts.value_flow_support,
client_requirements: toolAvailabilityStatusGroupFacts.client_requirements,
effects: z.array(toolAvailabilityFacts.effects.keyof()),
annotations: z.array(toolAvailabilityFacts.annotations.keyof()),
+3
View File
@@ -234,6 +234,9 @@ export const CAPABILITIES: readonly CapabilityDescriptor[] = Object.freeze(
return Object.freeze({
provider: GHIDRA_PROVIDER_IDENTITY,
operation,
...(operation === "analyze_function"
? { nativeValueFlow: { status: "supported" as const } }
: {}),
available: true,
reason: null,
// Project writes are private import machinery. Analysis reads are bound
+11
View File
@@ -29,6 +29,9 @@ export const KUNA_STRING_LIMITATION =
export const KUNA_XREF_LIMITATION =
"Kuna's static reference walk unifies forwarding aliases and omits references internal to that alias class. REA keeps only exact-address incoming edges; an empty result does not prove no reference exists.";
export const KUNA_VALUE_FLOW_UNAVAILABLE =
"REA's Kuna provider does not report native def-use observations.";
/** Whether an operation is one Kuna answers. */
export const isKunaOperation = (
operation: AnalysisOperation,
@@ -104,6 +107,14 @@ export const kunaCapabilities = (
KUNA_OPERATIONS.map((operation) => ({
provider: KUNA_PROVIDER_IDENTITY,
operation,
...(operation === "analyze_function"
? {
nativeValueFlow: {
status: "unavailable" as const,
reason: KUNA_VALUE_FLOW_UNAVAILABLE,
},
}
: {}),
...(availability.status === "available"
? ({ available: true, reason: null } as const)
: ({
+2 -5
View File
@@ -86,6 +86,7 @@ import {
isKunaOperation,
KUNA_RELATIONSHIP_LIMITATION,
KUNA_SINGLE_FUNCTION_LIMITATION,
KUNA_VALUE_FLOW_UNAVAILABLE,
kunaLimitations,
type KunaOperation,
} from "./KunaProviderCapabilities.js";
@@ -332,10 +333,6 @@ const kunaVerificationLimitations = (
/** Kuna names a procedure by its inventory entry or by an exact entry address. */
const procedureSelectorSchema = z.string().min(1);
/** REA's Kuna integration does not supply a def-use model for any procedure. */
export const KUNA_NATIVE_VALUE_FLOW_UNAVAILABLE =
"REA's Kuna provider does not report native def-use observations.";
/**
* One Kuna analysis session for one target.
*
@@ -1574,7 +1571,7 @@ export class KunaSessionClient implements AnalysisClient {
native_api: null,
native_value_flow: {
available: false,
reason: KUNA_NATIVE_VALUE_FLOW_UNAVAILABLE,
reason: KUNA_VALUE_FLOW_UNAVAILABLE,
limitations: [],
},
facet_availability: {
@@ -0,0 +1,176 @@
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { afterAll, expect, it } from "vitest";
import type { AnalysisProvider } from "../../../src/application/AnalysisProvider.js";
import {
valueTraceDiamond,
valueTraceProvider,
} from "../../../src/application/native/NativeValueTrace.fixture.js";
import { createAnalysisProfile } from "../../../src/domain/analysisProfile.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { functionDossierSchema } from "../../../src/domain/native/functionDossier.js";
import { ok } from "../../../src/domain/result.js";
import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js";
import { createTestBinarySession } from "../../fixtures/binarySession.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { advertisedMcpToolSchemas } from "../../fixtures/advertisedMcpTool.js";
import { createMcpSchemaValidation } from "../../fixtures/mcpSchemaValidation.js";
const schemaValidation = createMcpSchemaValidation();
afterAll(() => schemaValidation.clear());
it.each([
{ profile: "compact", state: "supported" },
{ profile: "full", state: "supported" },
{ profile: "compact", state: "unknown" },
{ profile: "full", state: "unknown" },
{ profile: "compact", state: "unavailable" },
{ profile: "full", state: "unavailable" },
] as const)(
"reports $state value flow without a resolver in $profile MCP",
async ({ profile, state }) => {
const target = join(
await createTestTempDirectory("flow-capability-"),
"fixture.com",
);
await writeFile(target, Buffer.from([0xb8, 0x00, 0x4c, 0xcd, 0x21]));
const graph = valueTraceDiamond({ sessionSubject: true });
const provider: AnalysisProvider = {
identity: () => valueTraceProvider,
capabilities: () => [
{
provider: valueTraceProvider,
operation: "analyze_function",
available: true,
reason: null,
...(state === "unknown"
? {}
: state === "supported"
? { nativeValueFlow: { status: "supported" as const } }
: {
nativeValueFlow: {
status: "unavailable" as const,
reason: "Fixture has no value flow.",
},
}),
effects: {
mutatesArtifact: false,
launchesProcess: false,
mayShowUi: false,
mayAccessNetwork: false,
mayWriteFilesystem: false,
changesPermissions: false,
requiresRoot: false,
},
limitations: [],
},
],
resolveAnalysisProfile: async () =>
ok({
profile: createAnalysisProfile(valueTraceProvider, { fixture: true }),
}),
createClient: () => ({
execute: async (operation, parameters, options) => {
const execution = await graph.port.execute(
operation,
parameters,
options,
);
if (
!execution.ok ||
state !== "unavailable" ||
operation !== "analyze_function"
)
return execution;
const dossier = functionDossierSchema.parse(execution.value.result);
return ok({
...execution.value,
result: {
...dossier,
native_value_flow: {
available: false,
reason: "Fixture has no value flow.",
limitations: [],
},
},
});
},
close: async () => ok(null),
}),
};
const session = createTestBinarySession(provider);
const { client } = await createApplicationMcpHarness({
session,
schemaValidator: schemaValidation,
serverOptions: {
environment: {
...process.env,
REA_MCP_INPUT_SCHEMA_PROFILE: profile,
REA_MCP_OUTPUT_SCHEMA_PROFILE: profile,
},
},
});
const call = async (name: string, args: Record<string, unknown>) => {
const schema = await advertisedMcpToolSchemas(
client,
name,
schemaValidation.assertions,
);
const reply = await client.callTool(
{ name, arguments: args },
{ toolDefinition: schema.toolDefinition },
);
expect(reply.isError, JSON.stringify(reply)).not.toBe(true);
expect(
schema.ajv.validate(schema.outputSchema, reply.structuredContent),
JSON.stringify(schema.ajv.errors),
).toBe(true);
return reply.structuredContent;
};
await call("open_binary", { path: target, format: "dos-com" });
const status = (await call("binary_session", { detail: "summary" })) as {
result: {
tool_availability: {
groups: Array<{
names: string[];
available: boolean;
value_flow_support?: { status: string; provider_id: string };
remediation: string | null;
}>;
};
};
};
const traceStatus = status.result.tool_availability.groups.find((g) =>
g.names.includes("trace_native_values"),
);
expect(traceStatus).toMatchObject({
available: false,
value_flow_support: { status: state, provider_id: "fixture" },
remediation: expect.any(String),
});
if (state === "unavailable")
expect(traceStatus?.remediation).toContain("Fixture has no value flow.");
else
expect(traceStatus?.remediation).toContain("resolve_native_call_targets");
const trace = parseEvidence(
await call("trace_native_values", { procedure: "0x1000", max_depth: 0 }),
);
const value = trace.normalized_result as {
total_nodes: number;
unknowns: Array<{ reason: string }>;
};
if (state === "unavailable") {
expect(value.total_nodes).toBe(0);
expect(value.unknowns).toContainEqual(
expect.objectContaining({ reason: "Fixture has no value flow." }),
);
} else {
expect(value.total_nodes).toBeGreaterThan(0);
expect(
value.unknowns.some((unknown) =>
unknown.reason.includes("resolve_native_call_targets"),
),
).toBe(true);
}
},
);
@@ -20,7 +20,7 @@ import { OwnedFileHandle } from "../../../../src/filesystem/OwnedFileHandle.js";
import { AnalysisProviderRegistry } from "../../../../src/application/binary/AnalysisProviderRegistry.js";
import { composeBinarySession } from "../../../../src/application/binary/BinarySessionComposition.js";
import { projectAnalysisError } from "../../../../src/domain/analysisErrorProjection.js";
import { KUNA_NATIVE_VALUE_FLOW_UNAVAILABLE } from "../../../../src/kuna/KunaSessionClient.js";
import { KUNA_VALUE_FLOW_UNAVAILABLE } from "../../../../src/kuna/KunaProviderCapabilities.js";
import { KunaProvider } from "../../../../src/kuna/KunaProvider.js";
import { KUNA_FUNCTION_TEXT_BYTES } from "../../../../src/kuna/KunaLimits.js";
import { KunaProjectCache } from "../../../../src/kuna/KunaProjectCache.js";
@@ -512,7 +512,7 @@ it("builds a dossier with text and call relationships, leaving unsupported facet
assembly: ["main: ; 0x1000", " 00001000: c3 RET"],
native_value_flow: {
available: false,
reason: KUNA_NATIVE_VALUE_FLOW_UNAVAILABLE,
reason: KUNA_VALUE_FLOW_UNAVAILABLE,
},
facet_availability: {
pseudocode: { available: true },