* fix(mcp): report Linux-only integrations as unsupported_host off their hosts Session discovery folded the host check into each optional integration's enabled flag, so on macOS or Windows the Linux-only packet, signal, firmware, pwntools, EVM and Wakaru tools reported provider_missing. Their execution guards refuse those hosts whatever is configured, so installing a provider there could never make them available. The server now records, per integration, when the host is outside its verified host and no service is injected. Discovery checks that before configuration and reports unsupported_host with the existing remediation. Injected services and supported hosts behave as before. Fixes #5730 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(mcp): share provider host checks with session discovery Discovery restated each Linux-only integration's host rule in createServer while its provider kept a separate execution check, so the two could drift apart again. Each provider now exports its host predicate, uses it in its execution guard, and composition hands the same predicate to createServer for both the host restriction and the enabled flag. Observed host restrictions now also apply when an embedder supplies its own availability policy, as provider load failures already do, because no policy lets the default providers pass their host checks. The MCP regression moves to tests/process-global and emulates darwin arm64, linux arm64 and linux x64 through the real createServer, so CI on Linux x64 exercises the unsupported-host branch. It also covers a configured policy and the matching execution refusals. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(mcp): keep caller host restrictions beside observed ones The observed host restriction replaced a configured policy's whole hostUnsupported map. For an injected service the observed value is false, so a caller's own restriction was lost and the tool was reported available. Combine both per integration: either the caller or the observed host can mark it unsupported. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(pcap): expect unsupported_host for packet tools off Linux The host-independent PATH case asserted provider_missing on every host. Off Linux the packet tools are unavailable because of the host, so discovery now reports unsupported_host there; Linux still reports provider_missing for an unusable PATH. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
818 lines
24 KiB
TypeScript
818 lines
24 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
buildCapabilityInventory,
|
|
compactToolAvailability,
|
|
} from "./CapabilityInventory.js";
|
|
import { TOOL_CONTRACTS } from "../contracts/toolContracts.js";
|
|
import {
|
|
expandToolAvailability,
|
|
toolAvailabilityInventory,
|
|
type ToolAvailability,
|
|
} from "../contracts/toolOutputSchemaPrimitives.js";
|
|
|
|
it("reports module syntax and native resolution availability separately", () => {
|
|
const input = { open: false, capabilities: [] };
|
|
const unavailable = buildCapabilityInventory(input, {
|
|
processCaptureEnabled: false,
|
|
}).find((tool) => tool.name === "trace_web_module_imports");
|
|
expect(unavailable).toMatchObject({
|
|
available: true,
|
|
default_mode_available: false,
|
|
modes: [
|
|
expect.objectContaining({
|
|
name: "sources-without-literal-imports",
|
|
available: true,
|
|
}),
|
|
expect.objectContaining({
|
|
name: "native-literal-resolution",
|
|
available: false,
|
|
}),
|
|
],
|
|
});
|
|
const enabled = buildCapabilityInventory(input, {
|
|
processCaptureEnabled: false,
|
|
webModuleResolutionEnabled: true,
|
|
}).find((tool) => tool.name === "trace_web_module_imports");
|
|
expect(enabled).toMatchObject({
|
|
available: true,
|
|
default_mode_available: true,
|
|
});
|
|
});
|
|
|
|
const enabledPolicy: Parameters<typeof buildCapabilityInventory>[1] = {
|
|
processCaptureEnabled: true,
|
|
browserObservationEnabled: true,
|
|
browserScenarioEnabled: true,
|
|
electronObservationEnabled: true,
|
|
v8InspectorObservationEnabled: true,
|
|
};
|
|
|
|
const status = (
|
|
options: {
|
|
readonly open?: boolean;
|
|
readonly kind?: "executable" | "database" | "archive" | "artifact";
|
|
readonly format?: string;
|
|
readonly capabilities?: readonly {
|
|
readonly operation: string;
|
|
readonly available: boolean;
|
|
readonly reason: string | null;
|
|
readonly availability_code?: "unsupported_host";
|
|
}[];
|
|
} = {},
|
|
) => ({
|
|
open: options.open ?? false,
|
|
...(options.kind === undefined ? {} : { kind: options.kind }),
|
|
...(options.format === undefined ? {} : { format: options.format }),
|
|
capabilities:
|
|
options.capabilities === undefined || options.capabilities.length === 0
|
|
? []
|
|
: [
|
|
{
|
|
provider: { id: "fixture", name: "Fixture", version: "1" },
|
|
shared_limitations: [],
|
|
groups: options.capabilities.map((capability) => ({
|
|
operations: [capability.operation],
|
|
effects: [],
|
|
limitations: [],
|
|
available: capability.available,
|
|
reason: capability.reason,
|
|
availability_code: capability.available
|
|
? null
|
|
: (capability.availability_code ?? null),
|
|
})),
|
|
},
|
|
],
|
|
});
|
|
|
|
const entry = (
|
|
name: string,
|
|
sessionStatus: ReturnType<typeof status>,
|
|
policy = enabledPolicy,
|
|
) => {
|
|
const found = buildCapabilityInventory(sessionStatus, policy).find(
|
|
(candidate) => candidate.name === name,
|
|
);
|
|
if (found === undefined) throw new Error(`missing capability ${name}`);
|
|
return found;
|
|
};
|
|
|
|
describe("ADB and Apktool host availability", () => {
|
|
it("uses only their selected owner probes", () => {
|
|
const ready = {
|
|
status: "available" as const,
|
|
code: null,
|
|
reason: null,
|
|
diagnostics: {},
|
|
};
|
|
const configuredButInvalid = {
|
|
status: "unavailable" as const,
|
|
code: "not_configured" as const,
|
|
reason: "REA_ADB_PATH is invalid",
|
|
diagnostics: {},
|
|
};
|
|
const readyInventory = buildCapabilityInventory(status(), {
|
|
...enabledPolicy,
|
|
adbAnalysisAvailability: ready,
|
|
apktoolAnalysisAvailability: ready,
|
|
});
|
|
expect(
|
|
readyInventory.find(({ name }) => name === "inspect_adb_client"),
|
|
).toMatchObject({ available: true, reason: "available" });
|
|
expect(
|
|
readyInventory.find(({ name }) => name === "inspect_apktool_client"),
|
|
).toMatchObject({ available: true, reason: "available" });
|
|
|
|
const unavailableInventory = buildCapabilityInventory(status(), {
|
|
...enabledPolicy,
|
|
adbAnalysisAvailability: configuredButInvalid,
|
|
apktoolAnalysisAvailability: {
|
|
status: "unavailable",
|
|
code: "unsupported_version",
|
|
reason: "Apktool 2.6.3 is older than 2.7.0",
|
|
diagnostics: {},
|
|
},
|
|
});
|
|
expect(
|
|
unavailableInventory.find(({ name }) => name === "inspect_adb_client"),
|
|
).toMatchObject({ available: false, reason: "provider_missing" });
|
|
expect(
|
|
unavailableInventory.find(
|
|
({ name }) => name === "decode_android_resources",
|
|
),
|
|
).toMatchObject({ available: false, reason: "provider_unavailable" });
|
|
});
|
|
});
|
|
|
|
describe("capability failure reasons", () => {
|
|
it("keeps an unavailable Android runtime distinct from a missing provider", () => {
|
|
const inventory = buildCapabilityInventory(status(), {
|
|
...enabledPolicy,
|
|
androidAnalysisAvailability: {
|
|
status: "unavailable",
|
|
code: "runtime_unavailable",
|
|
reason: "The JADX native self-test failed.",
|
|
diagnostics: {},
|
|
},
|
|
});
|
|
expect(
|
|
inventory.find(({ name }) => name === "inspect_android_package"),
|
|
).toMatchObject({ available: false, reason: "provider_unavailable" });
|
|
});
|
|
|
|
it("admits physical universal Mach-O targets for native Mach-O operations", () => {
|
|
expect(
|
|
entry(
|
|
"inspect_native_dispatch_metadata",
|
|
status({
|
|
open: true,
|
|
kind: "executable",
|
|
format: "mach-o-universal",
|
|
capabilities: [
|
|
{
|
|
operation: "inspect_native_dispatch_metadata",
|
|
available: true,
|
|
reason: null,
|
|
},
|
|
],
|
|
}),
|
|
),
|
|
).toMatchObject({ available: true, reason: "available" });
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
label: "requires a target",
|
|
name: "current_address",
|
|
sessionStatus: status(),
|
|
reason: "target_required",
|
|
remediation: "open_binary",
|
|
},
|
|
{
|
|
label: "rejects an incompatible target family",
|
|
name: "current_address",
|
|
sessionStatus: status({ open: true, kind: "archive" }),
|
|
reason: "target_unsupported",
|
|
remediation: "native executable",
|
|
},
|
|
{
|
|
label: "reports a missing provider operation",
|
|
name: "current_address",
|
|
sessionStatus: status({ open: true, kind: "executable" }),
|
|
reason: "provider_missing",
|
|
remediation: "provider",
|
|
},
|
|
{
|
|
label: "distinguishes an unsupported host",
|
|
name: "current_address",
|
|
sessionStatus: status({
|
|
open: true,
|
|
kind: "executable",
|
|
capabilities: [
|
|
{
|
|
operation: "current_address",
|
|
available: false,
|
|
availability_code: "unsupported_host",
|
|
reason: "Operation requires macOS",
|
|
},
|
|
],
|
|
}),
|
|
reason: "unsupported_host",
|
|
remediation: "macOS",
|
|
},
|
|
{
|
|
label: "retains a provider failure",
|
|
name: "current_address",
|
|
sessionStatus: status({
|
|
open: true,
|
|
kind: "executable",
|
|
capabilities: [
|
|
{
|
|
operation: "current_address",
|
|
available: false,
|
|
reason: "Provider session is unhealthy",
|
|
},
|
|
],
|
|
}),
|
|
reason: "provider_unavailable",
|
|
remediation: "unhealthy",
|
|
},
|
|
{
|
|
label: "reports an available provider operation",
|
|
name: "current_address",
|
|
sessionStatus: status({
|
|
open: true,
|
|
kind: "executable",
|
|
capabilities: [
|
|
{
|
|
operation: "current_address",
|
|
available: true,
|
|
reason: null,
|
|
},
|
|
],
|
|
}),
|
|
reason: "available",
|
|
remediation: null,
|
|
},
|
|
])("$label", ({ name, sessionStatus, reason, remediation }) => {
|
|
const availability = entry(name, sessionStatus);
|
|
expect(availability).toMatchObject({
|
|
reason,
|
|
available: reason === "available",
|
|
});
|
|
if (remediation === null) expect(availability.remediation).toBeNull();
|
|
else expect(availability.remediation).toContain(remediation);
|
|
});
|
|
});
|
|
|
|
describe("compact tool availability", () => {
|
|
it("round-trips complete facts, omitted modes, and per-group client requirements", () => {
|
|
const full = buildCapabilityInventory(status(), enabledPolicy);
|
|
const alternateRequirements = {
|
|
required: ["roots"],
|
|
optional: [],
|
|
missing_required: ["roots"],
|
|
missing_optional: [],
|
|
} satisfies ToolAvailability["client_requirements"];
|
|
const withOverride = full.map((entry, index) =>
|
|
index === 1
|
|
? {
|
|
...entry,
|
|
available: false as const,
|
|
reason: "client_capability_missing" as const,
|
|
remediation: "Use an MCP client with roots support.",
|
|
client_requirements: alternateRequirements,
|
|
}
|
|
: entry,
|
|
);
|
|
const compact = compactToolAvailability(withOverride);
|
|
const expanded = expandToolAvailability(compact).toSorted((left, right) =>
|
|
left.name.localeCompare(right.name),
|
|
);
|
|
|
|
expect(expanded).toEqual(
|
|
withOverride.toSorted((left, right) =>
|
|
left.name.localeCompare(right.name),
|
|
),
|
|
);
|
|
expect(compact.client_requirements).toEqual(
|
|
withOverride[0]?.client_requirements,
|
|
);
|
|
expect(
|
|
compact.groups.find((group) => group.client_requirements !== undefined)
|
|
?.client_requirements,
|
|
).toEqual(alternateRequirements);
|
|
expect(compact.groups.every((group) => group.names.length > 0)).toBe(true);
|
|
const names = compact.groups.flatMap((group) => group.names).sort();
|
|
expect(names).toEqual(TOOL_CONTRACTS.map(({ name }) => name).sort());
|
|
expect(new Set(names).size).toBe(names.length);
|
|
expect(expanded.some((entry) => !Object.hasOwn(entry, "modes"))).toBe(true);
|
|
});
|
|
|
|
it("preserves an empty inventory with an explicit empty client baseline", () => {
|
|
const compact = compactToolAvailability([]);
|
|
expect(compact).toEqual({
|
|
client_requirements: {
|
|
required: [],
|
|
optional: [],
|
|
missing_required: [],
|
|
missing_optional: [],
|
|
},
|
|
groups: [],
|
|
});
|
|
expect(expandToolAvailability(compact)).toEqual([]);
|
|
});
|
|
|
|
it("rejects malformed compact facts and accepts an equivalent explicit baseline", () => {
|
|
const compact = compactToolAvailability(
|
|
buildCapabilityInventory(status(), enabledPolicy),
|
|
);
|
|
const firstGroup = compact.groups[0];
|
|
expect(firstGroup).toBeDefined();
|
|
if (firstGroup === undefined) throw new Error("Expected tool groups");
|
|
const availableIndex = compact.groups.findIndex((group) => group.available);
|
|
if (availableIndex < 0) throw new Error("Expected an available tool group");
|
|
|
|
const malformedInventories = [
|
|
{
|
|
...compact,
|
|
groups: compact.groups.map((group, index) =>
|
|
index === 0
|
|
? { ...group, effects: [...group.effects, "unknown_effect"] }
|
|
: group,
|
|
),
|
|
},
|
|
{
|
|
...compact,
|
|
groups: compact.groups.map((group, index) =>
|
|
index === 0
|
|
? {
|
|
...group,
|
|
annotations: [...group.annotations, "unknown_hint"],
|
|
}
|
|
: group,
|
|
),
|
|
},
|
|
{
|
|
...compact,
|
|
groups: compact.groups.map((group, index) =>
|
|
index === 0 ? { ...group, names: [] } : group,
|
|
),
|
|
},
|
|
{
|
|
...compact,
|
|
groups: compact.groups.map((group, index) =>
|
|
index === 0 ? { ...group, names: [""] } : group,
|
|
),
|
|
},
|
|
{
|
|
...compact,
|
|
groups: compact.groups.map((group, index) =>
|
|
index === availableIndex
|
|
? { ...group, reason: "provider_missing" }
|
|
: group,
|
|
),
|
|
},
|
|
{
|
|
...compact,
|
|
groups: compact.groups.map((group, index) =>
|
|
index === availableIndex
|
|
? { ...group, remediation: "unexpected remediation" }
|
|
: group,
|
|
),
|
|
},
|
|
];
|
|
for (const malformed of malformedInventories)
|
|
expect(toolAvailabilityInventory.safeParse(malformed).success).toBe(
|
|
false,
|
|
);
|
|
|
|
const inheritedGroup = compact.groups.find(
|
|
(group) => group.client_requirements === undefined,
|
|
);
|
|
expect(inheritedGroup).toBeDefined();
|
|
if (inheritedGroup === undefined)
|
|
throw new Error("Expected an inherited client-requirements group");
|
|
const explicitBaseline = {
|
|
...compact,
|
|
groups: compact.groups.map((group) =>
|
|
group === inheritedGroup
|
|
? { ...group, client_requirements: compact.client_requirements }
|
|
: group,
|
|
),
|
|
};
|
|
const parsedOverride =
|
|
toolAvailabilityInventory.safeParse(explicitBaseline);
|
|
expect(parsedOverride.success).toBe(true);
|
|
if (parsedOverride.success)
|
|
expect(expandToolAvailability(parsedOverride.data)).toEqual(
|
|
expandToolAvailability(compact),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("partial navigation availability", () => {
|
|
it("allows explicit documents when the provider has no current-document getter", () => {
|
|
const capabilities = [
|
|
"current_document",
|
|
"current_address",
|
|
"resolve_containing_procedure",
|
|
].map((operation) => ({
|
|
operation,
|
|
available: true,
|
|
reason: null,
|
|
}));
|
|
expect(
|
|
entry(
|
|
"get_navigation_context",
|
|
status({ open: true, kind: "executable", capabilities }),
|
|
),
|
|
).toMatchObject({
|
|
available: true,
|
|
reason: "available",
|
|
default_mode_available: true,
|
|
modes: [
|
|
{ name: "current_selection", available: true },
|
|
{ name: "explicit_document", available: true },
|
|
],
|
|
});
|
|
expect(
|
|
entry(
|
|
"get_navigation_context",
|
|
status({
|
|
open: true,
|
|
kind: "executable",
|
|
capabilities: capabilities.slice(1),
|
|
}),
|
|
),
|
|
).toMatchObject({
|
|
available: true,
|
|
reason: "available",
|
|
default_mode_available: false,
|
|
modes: [
|
|
{ name: "current_selection", available: false },
|
|
{ name: "explicit_document", available: true },
|
|
],
|
|
});
|
|
expect(
|
|
entry(
|
|
"get_navigation_context",
|
|
status({
|
|
open: true,
|
|
kind: "executable",
|
|
capabilities: capabilities.slice(1, 2),
|
|
}),
|
|
),
|
|
).toMatchObject({
|
|
available: false,
|
|
reason: "provider_missing",
|
|
remediation: expect.stringContaining("resolve_containing_procedure"),
|
|
});
|
|
});
|
|
|
|
it("preserves the most specific navigation mode failure", () => {
|
|
expect(
|
|
entry(
|
|
"get_navigation_context",
|
|
status({
|
|
open: true,
|
|
kind: "executable",
|
|
capabilities: [
|
|
{
|
|
operation: "current_address",
|
|
available: false,
|
|
availability_code: "unsupported_host",
|
|
reason: "Navigation requires macOS.",
|
|
},
|
|
{
|
|
operation: "resolve_containing_procedure",
|
|
available: true,
|
|
reason: null,
|
|
},
|
|
],
|
|
}),
|
|
),
|
|
).toMatchObject({
|
|
available: false,
|
|
reason: "unsupported_host",
|
|
remediation: "Navigation requires macOS.",
|
|
});
|
|
});
|
|
|
|
it("lists shared missing operations once in combined remediation", () => {
|
|
const availability = entry(
|
|
"get_navigation_context",
|
|
status({ open: true, kind: "executable" }),
|
|
);
|
|
|
|
expect(availability).toMatchObject({
|
|
reason: "provider_missing",
|
|
remediation:
|
|
"Configure a provider for one of: current_document, current_address, resolve_containing_procedure.",
|
|
modes: [
|
|
{
|
|
name: "current_selection",
|
|
missing_operations: [
|
|
"current_document",
|
|
"current_address",
|
|
"resolve_containing_procedure",
|
|
],
|
|
},
|
|
{
|
|
name: "explicit_document",
|
|
missing_operations: [
|
|
"current_address",
|
|
"resolve_containing_procedure",
|
|
],
|
|
},
|
|
],
|
|
});
|
|
});
|
|
});
|
|
|
|
it("reports host denial before target mismatch but keeps composed workflow prerequisites", () => {
|
|
const capability = {
|
|
operation: "inspect_macho",
|
|
available: false,
|
|
availability_code: "unsupported_host" as const,
|
|
reason: "Native macOS utilities require macOS.",
|
|
};
|
|
for (const target of [
|
|
{},
|
|
{ open: true, kind: "archive" as const, format: "zip" },
|
|
]) {
|
|
expect(
|
|
entry("inspect_macho", status({ ...target, capabilities: [capability] })),
|
|
).toMatchObject({
|
|
reason: "unsupported_host",
|
|
remediation: capability.reason,
|
|
});
|
|
}
|
|
expect(
|
|
entry(
|
|
"inspect_native_dispatch_metadata",
|
|
status({
|
|
capabilities: [
|
|
{ ...capability, operation: "inspect_native_dispatch_metadata" },
|
|
],
|
|
}),
|
|
).reason,
|
|
).toBe("target_required");
|
|
});
|
|
|
|
describe("binary_overview capability requirements", () => {
|
|
it("admits a native overview without requiring full inventory operations", () => {
|
|
expect(
|
|
entry(
|
|
"binary_overview",
|
|
status({
|
|
open: true,
|
|
kind: "executable",
|
|
capabilities: [
|
|
{
|
|
operation: "read_binary_inventory_summary",
|
|
available: true,
|
|
reason: null,
|
|
},
|
|
],
|
|
}),
|
|
),
|
|
).toMatchObject({ available: true, reason: "available" });
|
|
});
|
|
it("admits a provider with current_document and other mandatory capabilities", () => {
|
|
const capabilities = [
|
|
"current_document",
|
|
"list_procedures",
|
|
"list_strings",
|
|
].map((operation) => ({ operation, available: true, reason: null }));
|
|
expect(
|
|
entry(
|
|
"binary_overview",
|
|
status({ open: true, kind: "executable", capabilities }),
|
|
),
|
|
).toMatchObject({
|
|
available: true,
|
|
reason: "available",
|
|
remediation: null,
|
|
});
|
|
});
|
|
|
|
it("admits a provider with list_documents and other mandatory capabilities", () => {
|
|
const capabilities = [
|
|
"list_documents",
|
|
"list_procedures",
|
|
"list_strings",
|
|
].map((operation) => ({ operation, available: true, reason: null }));
|
|
expect(
|
|
entry(
|
|
"binary_overview",
|
|
status({ open: true, kind: "executable", capabilities }),
|
|
),
|
|
).toMatchObject({
|
|
available: true,
|
|
reason: "available",
|
|
remediation: null,
|
|
});
|
|
});
|
|
|
|
it("reports document selection remediation when neither current_document nor list_documents is declared", () => {
|
|
const capabilities = [
|
|
"list_segments",
|
|
"list_procedures",
|
|
"list_strings",
|
|
].map((operation) => ({ operation, available: true, reason: null }));
|
|
expect(
|
|
entry(
|
|
"binary_overview",
|
|
status({ open: true, kind: "executable", capabilities }),
|
|
),
|
|
).toMatchObject({
|
|
available: false,
|
|
reason: "provider_missing",
|
|
remediation:
|
|
"Configure a provider for one of: current_document, list_documents.",
|
|
});
|
|
});
|
|
|
|
it("admits a provider without optional segment enumeration", () => {
|
|
const capabilities = [
|
|
"current_document",
|
|
"list_procedures",
|
|
"list_strings",
|
|
].map((operation) => ({ operation, available: true, reason: null }));
|
|
expect(
|
|
entry(
|
|
"binary_overview",
|
|
status({ open: true, kind: "executable", capabilities }),
|
|
),
|
|
).toMatchObject({
|
|
available: true,
|
|
reason: "available",
|
|
remediation: null,
|
|
});
|
|
});
|
|
|
|
it("keeps binary overview available when a declared segment facet is unavailable", () => {
|
|
const capabilities = [
|
|
{
|
|
operation: "list_segments",
|
|
available: false,
|
|
reason: "This upstream profile does not expose segment enumeration.",
|
|
},
|
|
{ operation: "list_documents", available: true, reason: null },
|
|
{ operation: "list_procedures", available: true, reason: null },
|
|
{ operation: "list_strings", available: true, reason: null },
|
|
];
|
|
expect(
|
|
entry(
|
|
"binary_overview",
|
|
status({ open: true, kind: "executable", capabilities }),
|
|
),
|
|
).toMatchObject({ available: true, reason: "available" });
|
|
});
|
|
|
|
it("reports provider_unavailable when declared document selection capability is unavailable", () => {
|
|
const capabilities = [
|
|
{ operation: "list_segments", available: true, reason: null },
|
|
{
|
|
operation: "current_document",
|
|
available: false,
|
|
reason: "Document view is closed",
|
|
},
|
|
{ operation: "list_procedures", available: true, reason: null },
|
|
{ operation: "list_strings", available: true, reason: null },
|
|
];
|
|
expect(
|
|
entry(
|
|
"binary_overview",
|
|
status({ open: true, kind: "executable", capabilities }),
|
|
),
|
|
).toMatchObject({
|
|
available: false,
|
|
reason: "provider_unavailable",
|
|
remediation: "Document view is closed",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("trace_feature requirements", () => {
|
|
const capabilities = (operations: readonly string[]) =>
|
|
operations.map((operation) => ({
|
|
operation,
|
|
available: true,
|
|
reason: null,
|
|
}));
|
|
|
|
it("needs the search operations the workflow actually calls", () => {
|
|
expect(
|
|
entry(
|
|
"trace_feature",
|
|
status({
|
|
open: true,
|
|
kind: "executable",
|
|
capabilities: capabilities([
|
|
"search_strings",
|
|
"search_procedures",
|
|
"xrefs",
|
|
"resolve_containing_procedure",
|
|
]),
|
|
}),
|
|
),
|
|
).toMatchObject({ available: true, reason: "available" });
|
|
});
|
|
|
|
it.each(["search_strings", "search_procedures"])(
|
|
"is unavailable without %s",
|
|
(missing) => {
|
|
const operations = [
|
|
"search_strings",
|
|
"search_procedures",
|
|
"xrefs",
|
|
"resolve_containing_procedure",
|
|
].filter((operation) => operation !== missing);
|
|
|
|
expect(
|
|
entry(
|
|
"trace_feature",
|
|
status({
|
|
open: true,
|
|
kind: "executable",
|
|
capabilities: capabilities(operations),
|
|
}),
|
|
),
|
|
).toMatchObject({ available: false, reason: "provider_missing" });
|
|
},
|
|
);
|
|
|
|
it("is unavailable when only the list operations exist", () => {
|
|
expect(
|
|
entry(
|
|
"trace_feature",
|
|
status({
|
|
open: true,
|
|
kind: "executable",
|
|
capabilities: capabilities([
|
|
"list_strings",
|
|
"list_procedures",
|
|
"xrefs",
|
|
"resolve_containing_procedure",
|
|
]),
|
|
}),
|
|
),
|
|
).toMatchObject({ available: false, reason: "provider_missing" });
|
|
});
|
|
});
|
|
|
|
const HOST_RESTRICTED_TOOLS = [
|
|
["inspect_packet_capture", "packetCapture", "packetCaptureEnabled"],
|
|
["inspect_packet_fields", "packetCapture", "packetCaptureEnabled"],
|
|
["decode_signal_recording", "signalRecording", "signalRecordingEnabled"],
|
|
["inspect_signal_decoders", "signalRecording", "signalRecordingEnabled"],
|
|
["inspect_evm_interface", "evmInterface", "evmInterfaceEnabled"],
|
|
["inspect_binary_layout", "binaryLayout", "binaryLayoutEnabled"],
|
|
["inspect_recorded_crash", "recordedCrash", "recordedCrashEnabled"],
|
|
[
|
|
"inspect_firmware_regions",
|
|
"firmwareInspection",
|
|
"firmwareInspectionEnabled",
|
|
],
|
|
["extract_firmware", "firmwareExtraction", "firmwareExtractionEnabled"],
|
|
[
|
|
"recover_javascript_sources",
|
|
"javascriptRecovery",
|
|
"javascriptRecoveryEnabled",
|
|
],
|
|
] as const;
|
|
|
|
describe("host-restricted integrations", () => {
|
|
it.each(HOST_RESTRICTED_TOOLS)(
|
|
"reports %s on an unsupported host before its configuration",
|
|
(name, feature, enabledField) => {
|
|
const availability = (enabled: boolean, hostUnsupported: boolean) =>
|
|
buildCapabilityInventory(
|
|
{ open: false, capabilities: [] },
|
|
{
|
|
processCaptureEnabled: true,
|
|
...({ [enabledField]: enabled } as Record<
|
|
typeof enabledField,
|
|
boolean
|
|
>),
|
|
hostUnsupported: { [feature]: hostUnsupported },
|
|
},
|
|
).find((tool) => tool.name === name);
|
|
expect(availability(true, true)).toMatchObject({
|
|
available: false,
|
|
reason: "unsupported_host",
|
|
});
|
|
expect(availability(false, false)).toMatchObject({
|
|
available: false,
|
|
reason: "provider_missing",
|
|
});
|
|
expect(availability(true, false)).toMatchObject({
|
|
available: true,
|
|
reason: "available",
|
|
});
|
|
},
|
|
);
|
|
});
|