refactor: declare auxiliary capabilities from source (#853)
This commit is contained in:
parent
c9f49db907
commit
4a6c17ce83
|
|
@ -145,3 +145,26 @@ reads, Evidence identities and deterministic bundle transfer keep their existing
|
|||
behavior. Snapshots still require an active target and concrete analysis profile;
|
||||
extracting investigation ownership does not make them a target-free record store.
|
||||
#721 remains responsible for any future lifetime-policy change.
|
||||
|
||||
## Auxiliary source declaration pilot
|
||||
|
||||
Artifact, macOS native and managed static metadata now belongs to each adapter's
|
||||
`*ProviderMetadata.ts` module. The implementation and
|
||||
`src/composition/auxiliaryAnalysisProviders.ts` reuse the same identity and exact
|
||||
capability descriptors. The declarations reference existing AnalysisProvider
|
||||
loaders; they cover these three disjoint auxiliary implementations only. Deep
|
||||
candidate selection and the five typed observation ports remain separate.
|
||||
|
||||
The binary runtime reads these source declarations while auxiliary construction
|
||||
stays lazy. The separate managed-only entrypoint retains direct construction for
|
||||
its execution-free workflow. The MCP catalog generator reads declarations without
|
||||
constructing implementations, using its canonical macOS platform; actual runtime
|
||||
host restrictions remain explicit.
|
||||
Discovery and binary operation-kind routing now consume canonical source contracts
|
||||
rather than their generated JSON artifact. No declaration imports its generated
|
||||
output. Generated catalog facts and public schemas stay unchanged.
|
||||
|
||||
All observation registrars resolve contracts by name. An isolated compiled-process
|
||||
test reverses presentation arrays and performs actual MCP SDK calls to verify
|
||||
advertised schemas and handler meaning. Reordering metadata must not select a
|
||||
different operation, schema, provider or deep-provider priority.
|
||||
|
|
|
|||
|
|
@ -7,9 +7,7 @@ import { McpServer } from "@modelcontextprotocol/server";
|
|||
|
||||
import { TOOL_CONTRACTS } from "../dist/contracts/toolContracts.js";
|
||||
import { MANAGED_WORKFLOW_TOOL_CONTRACTS } from "../dist/contracts/managedWorkflowToolContracts.js";
|
||||
import { ArtifactProvider } from "../dist/artifacts/ArtifactProvider.js";
|
||||
import { ManagedStaticProvider } from "../dist/dotnet/ManagedStaticProvider.js";
|
||||
import { NativeMacOSProvider } from "../dist/native/NativeMacOSProvider.js";
|
||||
import { auxiliaryAnalysisProviderDeclarations } from "../dist/composition/auxiliaryAnalysisProviders.js";
|
||||
import { toolRegistrationOptions } from "../dist/server/toolRegistrationOptions.js";
|
||||
import { ensureGeneratedFile } from "./lib/generated-file.mjs";
|
||||
|
||||
|
|
@ -88,14 +86,9 @@ async function sdkToolCatalog() {
|
|||
await Promise.allSettled([client.close(), server.close()]);
|
||||
}
|
||||
}
|
||||
const auxiliaryProviders = [
|
||||
new ArtifactProvider(CATALOG_PLATFORM),
|
||||
new NativeMacOSProvider(undefined, CATALOG_PLATFORM),
|
||||
new ManagedStaticProvider(),
|
||||
].map((provider) => ({
|
||||
identity: provider.identity(),
|
||||
capabilities: provider.capabilities(),
|
||||
}));
|
||||
const auxiliaryProviders = auxiliaryAnalysisProviderDeclarations(
|
||||
CATALOG_PLATFORM,
|
||||
).map(({ identity, capabilities }) => ({ identity, capabilities }));
|
||||
|
||||
const canonicalizeJson = (value) => {
|
||||
if (Array.isArray(value)) return value.map(canonicalizeJson);
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ const SOURCE_PATHS = {
|
|||
hopperProvider: "dist/hopper/HopperProvider.js",
|
||||
ghidraProvider: "dist/ghidra/GhidraProvider.js",
|
||||
idaProvider: "dist/ida/IdaProviderCapabilities.js",
|
||||
nativeProvider: "dist/native/NativeMacOSProvider.js",
|
||||
nativeProvider: "dist/native/NativeMacOSProviderMetadata.js",
|
||||
artifactProviders: "dist/application/InvestigationProviders.js",
|
||||
browserProvider: "dist/browser/CdpBrowserProvider.js",
|
||||
browserScenarioProvider: "dist/browser/PlaywrightBrowserScenarioProvider.js",
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ import type {
|
|||
ProviderIdentity,
|
||||
} from "./AnalysisProvider.js";
|
||||
import type { AnalysisOperation } from "./AnalysisProvider.js";
|
||||
import { GENERATED_MCP_TOOL_CATALOG } from "../generatedMcpToolCatalog.js";
|
||||
import { OFFICIAL_TOOL_CONTRACTS } from "../contracts/officialToolContracts.js";
|
||||
import { ENHANCED_TOOL_CONTRACTS } from "../contracts/enhancedToolContracts.js";
|
||||
import type { BinarySessionPort } from "./BinarySessionPort.js";
|
||||
import {
|
||||
SessionProviderRouter,
|
||||
|
|
@ -39,14 +40,10 @@ import {
|
|||
import { closeAnalysisClient } from "./AnalysisClientCleanup.js";
|
||||
export type { BinarySessionPort } from "./BinarySessionPort.js";
|
||||
const OFFICIAL_OPERATIONS: ReadonlySet<string> = new Set(
|
||||
GENERATED_MCP_TOOL_CATALOG.filter(
|
||||
({ kind }) => kind === "official-proxy",
|
||||
).map(({ name }) => name),
|
||||
OFFICIAL_TOOL_CONTRACTS.map(({ name }) => name),
|
||||
);
|
||||
const ENHANCED_OPERATIONS: ReadonlySet<string> = new Set(
|
||||
GENERATED_MCP_TOOL_CATALOG.filter(({ kind }) => kind === "enhanced").map(
|
||||
({ name }) => name,
|
||||
),
|
||||
ENHANCED_TOOL_CONTRACTS.map(({ name }) => name),
|
||||
);
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import {
|
|||
} from "../contracts/toolOutputSchemaPrimitives.js";
|
||||
import type { ToolKind } from "../contracts/toolContractTypes.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import { GENERATED_MCP_TOOL_CATALOG } from "../generatedMcpToolCatalog.js";
|
||||
import { TOOL_CONTRACTS } from "../contracts/toolContracts.js";
|
||||
import {
|
||||
clientRequirementsFor,
|
||||
NO_CLIENT_FEATURES,
|
||||
|
|
@ -131,7 +131,7 @@ export const buildCapabilityInventory = (
|
|||
const descriptors = new Map<string, ProviderDescriptor>(
|
||||
status.capabilities.map((descriptor) => [descriptor.operation, descriptor]),
|
||||
);
|
||||
return GENERATED_MCP_TOOL_CATALOG.map((contract): ToolAvailability => {
|
||||
return TOOL_CONTRACTS.map((contract): ToolAvailability => {
|
||||
const availability = availabilityFor({
|
||||
name: contract.name,
|
||||
kind: contract.kind,
|
||||
|
|
|
|||
|
|
@ -40,12 +40,12 @@ export class LazyAnalysisProvider implements AnalysisProvider {
|
|||
this.#loadProvider = options.load;
|
||||
}
|
||||
|
||||
/** Return generated provider identity without loading its implementation. */
|
||||
/** Return source-declared provider identity without loading its implementation. */
|
||||
identity(): ProviderIdentity {
|
||||
return this.#providerIdentity;
|
||||
}
|
||||
|
||||
/** Return generated capability metadata without loading its implementation. */
|
||||
/** Return source-declared capability metadata without loading its implementation. */
|
||||
capabilities(): readonly CapabilityDescriptor[] {
|
||||
return this.#capabilityDescriptors;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,9 @@ import { HopperProvider } from "../hopper/HopperProvider.js";
|
|||
import { GhidraProvider } from "../ghidra/GhidraProvider.js";
|
||||
import { IdaProvider } from "../ida/IdaProvider.js";
|
||||
import { silentLogger, type Logger } from "../logger.js";
|
||||
import { GENERATED_AUXILIARY_PROVIDERS } from "../generatedMcpToolCatalog.js";
|
||||
import { auxiliaryAnalysisProviderDeclarations } from "../composition/auxiliaryAnalysisProviders.js";
|
||||
import { AnalysisProviderRegistry } from "./AnalysisProviderRegistry.js";
|
||||
import { composeBinarySession } from "./BinarySessionComposition.js";
|
||||
import { nativeHostCapabilities } from "../native/NativeHostCapabilities.js";
|
||||
import { LazyAnalysisProvider } from "./LazyAnalysisProvider.js";
|
||||
import { ManagedStaticProvider } from "../dotnet/ManagedStaticProvider.js";
|
||||
import { SessionProviderRouter } from "./SessionProviderRouter.js";
|
||||
|
|
@ -24,47 +23,14 @@ export const createBinarySession = (
|
|||
const hopper = new HopperProvider(config, logger);
|
||||
const ghidra = new GhidraProvider(config, logger);
|
||||
const ida = new IdaProvider(config);
|
||||
const auxiliary = new Map(
|
||||
GENERATED_AUXILIARY_PROVIDERS.map((provider) => [
|
||||
provider.identity.id,
|
||||
provider,
|
||||
]),
|
||||
);
|
||||
const lazyProvider = (
|
||||
id: string,
|
||||
load: ConstructorParameters<typeof LazyAnalysisProvider>[0]["load"],
|
||||
) => {
|
||||
const generated = auxiliary.get(id);
|
||||
if (generated === undefined)
|
||||
throw new TypeError(`Missing generated provider metadata for ${id}`);
|
||||
return new LazyAnalysisProvider({
|
||||
...generated,
|
||||
capabilities: nativeHostCapabilities(generated.capabilities),
|
||||
load,
|
||||
});
|
||||
};
|
||||
return composeBinarySession(
|
||||
new AnalysisProviderRegistry(
|
||||
[hopper, ghidra, ida],
|
||||
config.analysisProvider,
|
||||
),
|
||||
[
|
||||
lazyProvider("rea-artifact-graph", async () => {
|
||||
const { ArtifactProvider } =
|
||||
await import("../artifacts/ArtifactProvider.js");
|
||||
return new ArtifactProvider();
|
||||
}),
|
||||
lazyProvider("native-macos", async () => {
|
||||
const { NativeMacOSProvider } =
|
||||
await import("../native/NativeMacOSProvider.js");
|
||||
return new NativeMacOSProvider();
|
||||
}),
|
||||
lazyProvider("rea-dotnet-static", async () => {
|
||||
const { ManagedStaticProvider } =
|
||||
await import("../dotnet/ManagedStaticProvider.js");
|
||||
return new ManagedStaticProvider();
|
||||
}),
|
||||
],
|
||||
auxiliaryAnalysisProviderDeclarations().map(
|
||||
(declaration) => new LazyAnalysisProvider(declaration),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -27,54 +27,20 @@ import type { JsonValue } from "../domain/jsonValue.js";
|
|||
import { interfaceBuilderLimitsSchema } from "../domain/interfaceBuilderGraph.js";
|
||||
import { err, ok } from "../domain/result.js";
|
||||
import { ArtifactReaderFailure } from "./ArtifactReader.js";
|
||||
import { ARTIFACT_GRAPH_PROVIDER } from "../application/InvestigationProviders.js";
|
||||
import {
|
||||
ARTIFACT_PROVIDER_IDENTITY as IDENTITY,
|
||||
artifactCapabilities,
|
||||
} from "./ArtifactProviderMetadata.js";
|
||||
import { createEvidence } from "../domain/evidence.js";
|
||||
import { createArtifactInspection } from "../domain/artifactInspection.js";
|
||||
import { resolveArtifactIntegrityPolicy } from "../application/ArtifactInventory/policy.js";
|
||||
|
||||
const IDENTITY: ProviderIdentity = Object.freeze(ARTIFACT_GRAPH_PROVIDER);
|
||||
|
||||
/** Read-only inventory and exclusively owned extraction provider. */
|
||||
export class ArtifactProvider implements AnalysisProvider {
|
||||
readonly #capabilities: readonly CapabilityDescriptor[];
|
||||
|
||||
constructor(platform: NodeJS.Platform = process.platform) {
|
||||
this.#capabilities = Object.freeze(
|
||||
ARTIFACT_ANALYSIS_OPERATIONS.map((operation) => {
|
||||
const common = {
|
||||
provider: IDENTITY,
|
||||
operation,
|
||||
effects: Object.freeze({
|
||||
mutatesArtifact: false,
|
||||
launchesProcess:
|
||||
operation !== "decode_interface_builder" &&
|
||||
operation !== "inspect_keyed_archive",
|
||||
mayShowUi: false,
|
||||
mayAccessNetwork: false,
|
||||
mayWriteFilesystem:
|
||||
operation === "extract_artifact" ||
|
||||
operation === "inspect_artifact" ||
|
||||
operation === "inventory_artifact",
|
||||
changesPermissions: false,
|
||||
requiresRoot: false,
|
||||
}),
|
||||
limitations: Object.freeze([
|
||||
"DMG child inventory automatically uses a read-only native macOS mount; PKG remains root-hash-only.",
|
||||
"ASAR files discovered in filesystem-backed inventories are expanded without bulk extraction; other nested containers remain recorded only.",
|
||||
]),
|
||||
};
|
||||
return Object.freeze(
|
||||
operation === "inspect_asset_catalog" && platform !== "darwin"
|
||||
? {
|
||||
...common,
|
||||
available: false as const,
|
||||
availabilityCode: "unsupported_host" as const,
|
||||
reason: "Apple asset catalogs require macOS assetutil.",
|
||||
}
|
||||
: { ...common, available: true as const, reason: null },
|
||||
);
|
||||
}),
|
||||
);
|
||||
this.#capabilities = artifactCapabilities(platform);
|
||||
}
|
||||
|
||||
identity(): ProviderIdentity {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
import type {
|
||||
CapabilityDescriptor,
|
||||
ProviderIdentity,
|
||||
} from "../application/AnalysisProvider.js";
|
||||
import { ARTIFACT_GRAPH_PROVIDER } from "../application/InvestigationProviders.js";
|
||||
import { ARTIFACT_ANALYSIS_OPERATIONS } from "../contracts/artifactToolContracts.js";
|
||||
|
||||
/** Identity of artifact inventory and owned extraction observations. */
|
||||
export const ARTIFACT_PROVIDER_IDENTITY: ProviderIdentity = Object.freeze(
|
||||
ARTIFACT_GRAPH_PROVIDER,
|
||||
);
|
||||
const IDENTITY = ARTIFACT_PROVIDER_IDENTITY;
|
||||
|
||||
/** Declare artifact coverage without constructing readers or acquiring resources. */
|
||||
export const artifactCapabilities = (
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
): readonly CapabilityDescriptor[] =>
|
||||
Object.freeze(
|
||||
ARTIFACT_ANALYSIS_OPERATIONS.map((operation) => {
|
||||
const common = {
|
||||
provider: IDENTITY,
|
||||
operation,
|
||||
effects: Object.freeze({
|
||||
mutatesArtifact: false,
|
||||
launchesProcess:
|
||||
operation !== "decode_interface_builder" &&
|
||||
operation !== "inspect_keyed_archive",
|
||||
mayShowUi: false,
|
||||
mayAccessNetwork: false,
|
||||
mayWriteFilesystem:
|
||||
operation === "extract_artifact" ||
|
||||
operation === "inspect_artifact" ||
|
||||
operation === "inventory_artifact",
|
||||
changesPermissions: false,
|
||||
requiresRoot: false,
|
||||
}),
|
||||
limitations: Object.freeze([
|
||||
"DMG child inventory automatically uses a read-only native macOS mount; PKG remains root-hash-only.",
|
||||
"ASAR files discovered in filesystem-backed inventories are expanded without bulk extraction; other nested containers remain recorded only.",
|
||||
]),
|
||||
};
|
||||
return Object.freeze(
|
||||
operation === "inspect_asset_catalog" && platform !== "darwin"
|
||||
? {
|
||||
...common,
|
||||
available: false as const,
|
||||
availabilityCode: "unsupported_host" as const,
|
||||
reason: "Apple asset catalogs require macOS assetutil.",
|
||||
}
|
||||
: { ...common, available: true as const, reason: null },
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import type { LazyAnalysisProvider } from "../application/LazyAnalysisProvider.js";
|
||||
import {
|
||||
ARTIFACT_PROVIDER_IDENTITY,
|
||||
artifactCapabilities,
|
||||
} from "../artifacts/ArtifactProviderMetadata.js";
|
||||
import {
|
||||
MANAGED_STATIC_PROVIDER_IDENTITY,
|
||||
managedStaticCapabilities,
|
||||
} from "../dotnet/ManagedStaticProviderMetadata.js";
|
||||
import {
|
||||
NATIVE_MACOS_PROVIDER_IDENTITY,
|
||||
nativeMacOSCapabilities,
|
||||
} from "../native/NativeMacOSProviderMetadata.js";
|
||||
|
||||
type AuxiliaryProviderDeclaration = ConstructorParameters<
|
||||
typeof LazyAnalysisProvider
|
||||
>[0];
|
||||
|
||||
/** Wire the three existing auxiliary AnalysisProvider implementations lazily. */
|
||||
export const auxiliaryAnalysisProviderDeclarations = (
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
): readonly AuxiliaryProviderDeclaration[] => [
|
||||
{
|
||||
identity: ARTIFACT_PROVIDER_IDENTITY,
|
||||
capabilities: artifactCapabilities(platform),
|
||||
load: async () => {
|
||||
const { ArtifactProvider } =
|
||||
await import("../artifacts/ArtifactProvider.js");
|
||||
return new ArtifactProvider(platform);
|
||||
},
|
||||
},
|
||||
{
|
||||
identity: NATIVE_MACOS_PROVIDER_IDENTITY,
|
||||
capabilities: nativeMacOSCapabilities(platform),
|
||||
load: async () => {
|
||||
const { NativeMacOSProvider } =
|
||||
await import("../native/NativeMacOSProvider.js");
|
||||
return new NativeMacOSProvider(undefined, platform);
|
||||
},
|
||||
},
|
||||
{
|
||||
identity: MANAGED_STATIC_PROVIDER_IDENTITY,
|
||||
capabilities: managedStaticCapabilities(),
|
||||
load: async () => {
|
||||
const { ManagedStaticProvider } =
|
||||
await import("../dotnet/ManagedStaticProvider.js");
|
||||
return new ManagedStaticProvider();
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
@ -11,7 +11,10 @@ import {
|
|||
type ExecutionOptions,
|
||||
type ProviderIdentity,
|
||||
} from "../application/AnalysisProvider.js";
|
||||
import { MANAGED_STATIC_PROVIDER } from "../application/InvestigationProviders.js";
|
||||
import {
|
||||
MANAGED_STATIC_PROVIDER_IDENTITY as IDENTITY,
|
||||
managedStaticCapabilities,
|
||||
} from "./ManagedStaticProviderMetadata.js";
|
||||
import {
|
||||
MANAGED_TOOL_CONTRACTS,
|
||||
managedArtifactInputSchema,
|
||||
|
|
@ -38,30 +41,10 @@ import { inspectManagedArtifactBytes } from "./ManagedArtifactInspector.js";
|
|||
import { inspectManagedMembersBytes } from "./ManagedMemberInspector.js";
|
||||
import { inspectManagedNativeBoundariesBytes } from "./ManagedNativeBoundaryInspector.js";
|
||||
|
||||
const IDENTITY: ProviderIdentity = Object.freeze(MANAGED_STATIC_PROVIDER);
|
||||
|
||||
/** Execution-free managed PE/CLI auxiliary provider. */
|
||||
export class ManagedStaticProvider implements AnalysisProvider {
|
||||
readonly #capabilities: readonly CapabilityDescriptor[] = Object.freeze(
|
||||
MANAGED_TOOL_CONTRACTS.map((contract) =>
|
||||
Object.freeze({
|
||||
provider: IDENTITY,
|
||||
operation: contract.name,
|
||||
available: true as const,
|
||||
reason: null,
|
||||
effects: Object.freeze({
|
||||
mutatesArtifact: false,
|
||||
launchesProcess: false,
|
||||
mayShowUi: false,
|
||||
mayAccessNetwork: false,
|
||||
mayWriteFilesystem: false,
|
||||
changesPermissions: false,
|
||||
requiresRoot: false,
|
||||
}),
|
||||
limitations: limitationsFor(contract.name),
|
||||
}),
|
||||
),
|
||||
);
|
||||
readonly #capabilities: readonly CapabilityDescriptor[] =
|
||||
managedStaticCapabilities();
|
||||
|
||||
identity(): ProviderIdentity {
|
||||
return IDENTITY;
|
||||
|
|
@ -196,24 +179,6 @@ const isManagedOperation = (
|
|||
): operation is ManagedToolName =>
|
||||
MANAGED_TOOL_CONTRACTS.some(({ name }) => name === operation);
|
||||
|
||||
const limitationsFor = (operation: ManagedToolName): readonly string[] =>
|
||||
Object.freeze(
|
||||
operation === "inspect_managed_artifact"
|
||||
? [
|
||||
"This capability inventories PE/CLI identity only; inspect_managed_members admits metadata members, signatures, and CIL anchors.",
|
||||
"It never loads the target assembly, resolves dependencies through a CLR, decompiles C#, or executes target code.",
|
||||
]
|
||||
: operation === "inspect_managed_native_boundaries"
|
||||
? [
|
||||
"This capability inventories managed declarations for native boundaries only; native exports, thunks, and addresses require separate native-provider evidence.",
|
||||
"It never loads the target assembly, resolves dependencies through a CLR, decompiles C#, or executes target code.",
|
||||
]
|
||||
: [
|
||||
"This capability decodes PE/CLI metadata members, signatures, and file-backed method bodies; decompiled C# reconstruction and cross-build member comparison are separate operations.",
|
||||
"It never loads the target assembly, resolves dependencies through a CLR, decompiles C#, or executes target code.",
|
||||
],
|
||||
);
|
||||
|
||||
const inspectManagedOperation = (
|
||||
operation: ManagedToolName,
|
||||
parameters: Readonly<Record<string, JsonValue>>,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
import type {
|
||||
CapabilityDescriptor,
|
||||
ProviderIdentity,
|
||||
} from "../application/AnalysisProvider.js";
|
||||
import { MANAGED_STATIC_PROVIDER } from "../application/InvestigationProviders.js";
|
||||
import {
|
||||
MANAGED_TOOL_CONTRACTS,
|
||||
type ManagedToolName,
|
||||
} from "../contracts/managedToolContracts.js";
|
||||
|
||||
/** Identity of execution-free managed metadata observations. */
|
||||
export const MANAGED_STATIC_PROVIDER_IDENTITY: ProviderIdentity = Object.freeze(
|
||||
MANAGED_STATIC_PROVIDER,
|
||||
);
|
||||
const IDENTITY = MANAGED_STATIC_PROVIDER_IDENTITY;
|
||||
|
||||
/** Declare managed metadata coverage without opening or loading an assembly. */
|
||||
export const managedStaticCapabilities = (): readonly CapabilityDescriptor[] =>
|
||||
Object.freeze(
|
||||
MANAGED_TOOL_CONTRACTS.map((contract) =>
|
||||
Object.freeze({
|
||||
provider: IDENTITY,
|
||||
operation: contract.name,
|
||||
available: true as const,
|
||||
reason: null,
|
||||
effects: Object.freeze({
|
||||
mutatesArtifact: false,
|
||||
launchesProcess: false,
|
||||
mayShowUi: false,
|
||||
mayAccessNetwork: false,
|
||||
mayWriteFilesystem: false,
|
||||
changesPermissions: false,
|
||||
requiresRoot: false,
|
||||
}),
|
||||
limitations: limitationsFor(contract.name),
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const limitationsFor = (operation: ManagedToolName): readonly string[] =>
|
||||
Object.freeze(
|
||||
operation === "inspect_managed_artifact"
|
||||
? [
|
||||
"This capability inventories PE/CLI identity only; inspect_managed_members admits metadata members, signatures, and CIL anchors.",
|
||||
"It never loads the target assembly, resolves dependencies through a CLR, decompiles C#, or executes target code.",
|
||||
]
|
||||
: operation === "inspect_managed_native_boundaries"
|
||||
? [
|
||||
"This capability inventories managed declarations for native boundaries only; native exports, thunks, and addresses require separate native-provider evidence.",
|
||||
"It never loads the target assembly, resolves dependencies through a CLR, decompiles C#, or executes target code.",
|
||||
]
|
||||
: [
|
||||
"This capability decodes PE/CLI metadata members, signatures, and file-backed method bodies; decompiled C# reconstruction and cross-build member comparison are separate operations.",
|
||||
"It never loads the target assembly, resolves dependencies through a CLR, decompiles C#, or executes target code.",
|
||||
],
|
||||
);
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
import type { CapabilityDescriptor } from "../application/AnalysisProvider.js";
|
||||
|
||||
/** Apply native host restrictions to portable provider discovery metadata. */
|
||||
export const nativeHostCapabilities = (
|
||||
capabilities: readonly CapabilityDescriptor[],
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
): readonly CapabilityDescriptor[] => {
|
||||
if (platform === "darwin") return capabilities;
|
||||
return capabilities.map((capability): CapabilityDescriptor => {
|
||||
if (capability.provider.id === "native-macos")
|
||||
return {
|
||||
...capability,
|
||||
available: false,
|
||||
availabilityCode: "unsupported_host",
|
||||
reason: "Native macOS utilities require macOS.",
|
||||
};
|
||||
if (
|
||||
capability.provider.id === "rea-artifact-graph" &&
|
||||
capability.operation === "inspect_asset_catalog"
|
||||
)
|
||||
return {
|
||||
...capability,
|
||||
available: false,
|
||||
availabilityCode: "unsupported_host",
|
||||
reason: "Apple asset catalogs require macOS assetutil.",
|
||||
};
|
||||
return capability;
|
||||
});
|
||||
};
|
||||
|
|
@ -1,3 +1,8 @@
|
|||
import {
|
||||
NATIVE_MACOS_PROVIDER_IDENTITY as IDENTITY,
|
||||
nativeMacOSCapabilities,
|
||||
} from "./NativeMacOSProviderMetadata.js";
|
||||
export { NATIVE_MACOS_PROVIDER_IDENTITY } from "./NativeMacOSProviderMetadata.js";
|
||||
import { inspectAppleDispatchMetadata } from "./AppleDispatchMetadata.js";
|
||||
import { observeNativeUi } from "./NativeUiObservation.js";
|
||||
import { dirname, isAbsolute, resolve } from "node:path";
|
||||
|
|
@ -51,14 +56,6 @@ import {
|
|||
inspectNativeMacho,
|
||||
} from "./NativeMachoInspection.js";
|
||||
|
||||
/** Public identity committed by macOS-native inspection observations. */
|
||||
export const NATIVE_MACOS_PROVIDER_IDENTITY: ProviderIdentity = Object.freeze({
|
||||
id: "native-macos",
|
||||
name: "macOS native inspection utilities",
|
||||
version: null,
|
||||
});
|
||||
const IDENTITY = NATIVE_MACOS_PROVIDER_IDENTITY;
|
||||
|
||||
/** Read-only semantic provider composed from Xcode command-line utilities. */
|
||||
export class NativeMacOSProvider implements AnalysisProvider {
|
||||
readonly #capabilities: readonly CapabilityDescriptor[];
|
||||
|
|
@ -67,41 +64,7 @@ export class NativeMacOSProvider implements AnalysisProvider {
|
|||
private readonly runner: NativeCommandRunner = new XcrunCommandRunner(),
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
) {
|
||||
const available = platform === "darwin";
|
||||
this.#capabilities = Object.freeze(
|
||||
[
|
||||
...NATIVE_TOOL_CONTRACTS,
|
||||
{ name: "inspect_native_dispatch_metadata" as const },
|
||||
].map((contract): CapabilityDescriptor => {
|
||||
const availability = available
|
||||
? ({ available: true, reason: null } as const)
|
||||
: ({
|
||||
available: false,
|
||||
availabilityCode: "unsupported_host",
|
||||
reason: "Native macOS utilities require macOS.",
|
||||
} as const);
|
||||
return Object.freeze({
|
||||
provider: IDENTITY,
|
||||
operation: contract.name,
|
||||
...availability,
|
||||
effects: Object.freeze({
|
||||
mutatesArtifact: false,
|
||||
launchesProcess:
|
||||
contract.name !== "inspect_native_dispatch_metadata",
|
||||
mayShowUi: contract.name === "capture_native_ui_scenario",
|
||||
mayAccessNetwork: contract.name === "capture_native_ui_scenario",
|
||||
mayWriteFilesystem:
|
||||
contract.name === "capture_native_ui_scenario" ||
|
||||
contract.name === "observe_native_ui",
|
||||
changesPermissions: false,
|
||||
requiresRoot: false,
|
||||
}),
|
||||
limitations: Object.freeze([
|
||||
"Availability and textual formats depend on the installed macOS/Xcode toolchain.",
|
||||
]),
|
||||
});
|
||||
}),
|
||||
);
|
||||
this.#capabilities = nativeMacOSCapabilities(platform);
|
||||
}
|
||||
|
||||
identity(): ProviderIdentity {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
import type {
|
||||
CapabilityDescriptor,
|
||||
ProviderIdentity,
|
||||
} from "../application/AnalysisProvider.js";
|
||||
import { NATIVE_TOOL_CONTRACTS } from "../contracts/nativeToolContracts.js";
|
||||
|
||||
/** Public identity committed by macOS-native inspection observations. */
|
||||
export const NATIVE_MACOS_PROVIDER_IDENTITY: ProviderIdentity = Object.freeze({
|
||||
id: "native-macos",
|
||||
name: "macOS native inspection utilities",
|
||||
version: null,
|
||||
});
|
||||
const IDENTITY = NATIVE_MACOS_PROVIDER_IDENTITY;
|
||||
|
||||
/** Declare native host coverage without creating a command runner. */
|
||||
export const nativeMacOSCapabilities = (
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
): readonly CapabilityDescriptor[] => {
|
||||
const available = platform === "darwin";
|
||||
return Object.freeze(
|
||||
[
|
||||
...NATIVE_TOOL_CONTRACTS,
|
||||
{ name: "inspect_native_dispatch_metadata" as const },
|
||||
].map((contract): CapabilityDescriptor => {
|
||||
const availability = available
|
||||
? ({ available: true, reason: null } as const)
|
||||
: ({
|
||||
available: false,
|
||||
availabilityCode: "unsupported_host",
|
||||
reason: "Native macOS utilities require macOS.",
|
||||
} as const);
|
||||
return Object.freeze({
|
||||
provider: IDENTITY,
|
||||
operation: contract.name,
|
||||
...availability,
|
||||
effects: Object.freeze({
|
||||
mutatesArtifact: false,
|
||||
launchesProcess: contract.name !== "inspect_native_dispatch_metadata",
|
||||
mayShowUi: contract.name === "capture_native_ui_scenario",
|
||||
mayAccessNetwork: contract.name === "capture_native_ui_scenario",
|
||||
mayWriteFilesystem:
|
||||
contract.name === "capture_native_ui_scenario" ||
|
||||
contract.name === "observe_native_ui",
|
||||
changesPermissions: false,
|
||||
requiresRoot: false,
|
||||
}),
|
||||
limitations: Object.freeze([
|
||||
"Availability and textual formats depend on the installed macOS/Xcode toolchain.",
|
||||
]),
|
||||
});
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
|
@ -8,7 +8,7 @@ import type { McpServer } from "@modelcontextprotocol/server";
|
|||
|
||||
import type { BrowserScenarioCapturePort } from "../application/BrowserScenarioCapturePort.js";
|
||||
import { captureBrowserScenario } from "../application/BrowserScenarioCaptureService.js";
|
||||
import { BROWSER_SCENARIO_TOOL_CONTRACTS } from "../contracts/browserScenarioToolContracts.js";
|
||||
import { toolContract } from "../contracts/toolContracts.js";
|
||||
import { browserScenarioSchema } from "../domain/browserScenario.js";
|
||||
import type { Logger } from "../logger.js";
|
||||
import { logToolExecution } from "./toolLogging.js";
|
||||
|
|
@ -27,7 +27,7 @@ export const registerBrowserScenarioTool = (
|
|||
server: McpServer,
|
||||
options: BrowserScenarioToolRegistration,
|
||||
): void => {
|
||||
const [contract] = BROWSER_SCENARIO_TOOL_CONTRACTS;
|
||||
const contract = toolContract("capture_browser_scenario");
|
||||
server.registerTool(
|
||||
contract.name,
|
||||
toolRegistrationOptions(contract),
|
||||
|
|
|
|||
|
|
@ -18,8 +18,7 @@ import {
|
|||
observeWebSession,
|
||||
} from "../application/BrowserObservationService.js";
|
||||
import type { ProgressReporter } from "../application/ProgressReporter.js";
|
||||
import { BROWSER_TOOL_CONTRACTS } from "../contracts/browserToolContracts.js";
|
||||
import type { ToolContract } from "../contracts/toolContracts.js";
|
||||
import { toolContract, type ToolContract } from "../contracts/toolContracts.js";
|
||||
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import type { Evidence } from "../domain/evidence.js";
|
||||
import { analyzeWebBundleInputSchema } from "../domain/webBundleAnalysis.js";
|
||||
|
|
@ -49,16 +48,14 @@ export const registerBrowserTools = (
|
|||
server: McpServer,
|
||||
options: BrowserToolRegistration,
|
||||
): void => {
|
||||
const [
|
||||
listContract,
|
||||
inspectContract,
|
||||
analyzeContract,
|
||||
sessionContract,
|
||||
webMcpContract,
|
||||
captureDiffContract,
|
||||
screenshotContract,
|
||||
screenshotDiffContract,
|
||||
] = BROWSER_TOOL_CONTRACTS;
|
||||
const listContract = toolContract("list_browser_targets");
|
||||
const inspectContract = toolContract("inspect_web_page");
|
||||
const analyzeContract = toolContract("analyze_web_bundle");
|
||||
const sessionContract = toolContract("observe_web_session");
|
||||
const webMcpContract = toolContract("discover_webmcp_tools");
|
||||
const captureDiffContract = toolContract("compare_web_captures");
|
||||
const screenshotContract = toolContract("capture_web_screenshot");
|
||||
const screenshotDiffContract = toolContract("compare_web_screenshots");
|
||||
|
||||
server.registerTool(
|
||||
listContract.name,
|
||||
|
|
|
|||
|
|
@ -16,8 +16,7 @@ import {
|
|||
import { analyzeJavaScriptApplicationValidated } from "../application/JavaScriptApplicationService.js";
|
||||
import { reconcileJavaScriptRuntimeEvidenceValidated } from "../application/JavaScriptRuntimeReconciliationService.js";
|
||||
import type { ProgressReporter } from "../application/ProgressReporter.js";
|
||||
import { ELECTRON_TOOL_CONTRACTS } from "../contracts/electronToolContracts.js";
|
||||
import type { ToolContract } from "../contracts/toolContracts.js";
|
||||
import { toolContract, type ToolContract } from "../contracts/toolContracts.js";
|
||||
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import type { Evidence } from "../domain/evidence.js";
|
||||
import type { Result } from "../domain/result.js";
|
||||
|
|
@ -48,13 +47,11 @@ export const registerElectronTools = (
|
|||
server: McpServer,
|
||||
options: ElectronToolRegistration,
|
||||
): void => {
|
||||
const [
|
||||
listContract,
|
||||
inspectContract,
|
||||
analyzeContract,
|
||||
reconcileContract,
|
||||
activeContract,
|
||||
] = ELECTRON_TOOL_CONTRACTS;
|
||||
const listContract = toolContract("list_electron_targets");
|
||||
const inspectContract = toolContract("inspect_electron_page");
|
||||
const analyzeContract = toolContract("analyze_javascript_application");
|
||||
const reconcileContract = toolContract("reconcile_javascript_runtime");
|
||||
const activeContract = toolContract("capture_electron_scenario");
|
||||
|
||||
server.registerTool(
|
||||
listContract.name,
|
||||
|
|
|
|||
|
|
@ -11,8 +11,7 @@ import {
|
|||
listJavaScriptRuntimeTargets,
|
||||
observeJavaScriptRuntime,
|
||||
} from "../application/JavaScriptRuntimeObservationService.js";
|
||||
import { JAVASCRIPT_RUNTIME_OBSERVATION_TOOL_CONTRACTS } from "../contracts/javascriptRuntimeObservationToolContracts.js";
|
||||
import type { ToolContract } from "../contracts/toolContracts.js";
|
||||
import { toolContract, type ToolContract } from "../contracts/toolContracts.js";
|
||||
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import type { Evidence } from "../domain/evidence.js";
|
||||
import type { Result } from "../domain/result.js";
|
||||
|
|
@ -34,8 +33,8 @@ export const registerJavaScriptRuntimeObservationTools = (
|
|||
server: McpServer,
|
||||
options: RuntimeToolRegistration,
|
||||
): void => {
|
||||
const [listContract, observeContract] =
|
||||
JAVASCRIPT_RUNTIME_OBSERVATION_TOOL_CONTRACTS;
|
||||
const listContract = toolContract("list_javascript_runtime_targets");
|
||||
const observeContract = toolContract("observe_javascript_runtime");
|
||||
server.registerTool(
|
||||
listContract.name,
|
||||
toolRegistrationOptions(listContract),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
import { execFile } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { promisify } from "node:util";
|
||||
import { expect, it } from "vitest";
|
||||
import { z } from "zod";
|
||||
|
||||
import { GENERATED_MCP_TOOL_CATALOG } from "../../src/generatedMcpToolCatalog.js";
|
||||
|
||||
const execute = promisify(execFile);
|
||||
const fixture = fileURLToPath(
|
||||
new URL("../fixtures/contractPresentation.mjs", import.meta.url),
|
||||
);
|
||||
const resultSchema = z.object({
|
||||
catalog: z.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
inputSchema: z.record(z.string(), z.unknown()),
|
||||
outputSchema: z.record(z.string(), z.unknown()),
|
||||
}),
|
||||
),
|
||||
responses: z.array(
|
||||
z.object({ name: z.string(), response: z.record(z.string(), z.unknown()) }),
|
||||
),
|
||||
});
|
||||
|
||||
it.each(["original", "reversed"])(
|
||||
"preserves advertised schemas and handler meaning with %s presentation",
|
||||
async (presentation) => {
|
||||
const { stdout } = await execute(
|
||||
process.execPath,
|
||||
[fixture, presentation],
|
||||
{
|
||||
timeout: 20_000,
|
||||
maxBuffer: 16 * 1024 * 1024,
|
||||
},
|
||||
);
|
||||
const result = resultSchema.parse(JSON.parse(stdout));
|
||||
expect(result.catalog).toHaveLength(GENERATED_MCP_TOOL_CATALOG.length);
|
||||
expect(new Set(result.catalog.map(({ name }) => name))).toEqual(
|
||||
new Set(GENERATED_MCP_TOOL_CATALOG.map(({ name }) => name)),
|
||||
);
|
||||
for (const advertised of result.catalog) {
|
||||
const canonical = GENERATED_MCP_TOOL_CATALOG.find(
|
||||
({ name }) => name === advertised.name,
|
||||
);
|
||||
expect(advertised.inputSchema).toEqual(canonical?.inputSchema);
|
||||
expect(advertised.outputSchema).toEqual(canonical?.outputSchema);
|
||||
}
|
||||
for (const { name, response } of result.responses) {
|
||||
if (name === "reconcile_javascript_runtime") {
|
||||
expect(response.isError).not.toBe(true);
|
||||
expect(response.structuredContent).toMatchObject({
|
||||
evidence: { operation: name },
|
||||
});
|
||||
} else {
|
||||
expect(response.isError, name).toBe(true);
|
||||
expect(response.structuredContent, name).toMatchObject({
|
||||
error: { details: { operation: name } },
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
import { expect, it } from "vitest";
|
||||
|
||||
import { auxiliaryAnalysisProviderDeclarations } from "../../../src/composition/auxiliaryAnalysisProviders.js";
|
||||
import { LazyAnalysisProvider } from "../../../src/application/LazyAnalysisProvider.js";
|
||||
import { parseBinaryTarget } from "../../../src/application/BinaryTargetResolver.js";
|
||||
import { GENERATED_AUXILIARY_PROVIDERS } from "../../../src/generatedMcpToolCatalog.js";
|
||||
|
||||
it.each(["darwin", "linux", "win32"] as const)(
|
||||
"preserves canonical auxiliary metadata and host facts on %s",
|
||||
async (platform) => {
|
||||
const declarations = auxiliaryAnalysisProviderDeclarations(platform);
|
||||
expect(declarations.map(({ identity }) => identity)).toEqual(
|
||||
GENERATED_AUXILIARY_PROVIDERS.map(({ identity }) => identity),
|
||||
);
|
||||
const operations = new Set<string>();
|
||||
for (const declaration of declarations) {
|
||||
const canonical = GENERATED_AUXILIARY_PROVIDERS.find(
|
||||
({ identity }) => identity.id === declaration.identity.id,
|
||||
);
|
||||
if (canonical === undefined)
|
||||
throw new Error("Missing canonical provider");
|
||||
const expected = canonical.capabilities.map((capability) => {
|
||||
if (platform === "darwin") return capability;
|
||||
if (capability.provider.id === "native-macos")
|
||||
return {
|
||||
...capability,
|
||||
available: false,
|
||||
availabilityCode: "unsupported_host",
|
||||
reason: "Native macOS utilities require macOS.",
|
||||
};
|
||||
if (capability.operation === "inspect_asset_catalog")
|
||||
return {
|
||||
...capability,
|
||||
available: false,
|
||||
availabilityCode: "unsupported_host",
|
||||
reason: "Apple asset catalogs require macOS assetutil.",
|
||||
};
|
||||
return capability;
|
||||
});
|
||||
expect(declaration.capabilities).toEqual(expected);
|
||||
const first = await declaration.load();
|
||||
const second = await declaration.load();
|
||||
expect(first).not.toBe(second);
|
||||
expect(first.identity()).toEqual(declaration.identity);
|
||||
expect(first.capabilities()).toEqual(declaration.capabilities);
|
||||
expect(second.capabilities()).toEqual(declaration.capabilities);
|
||||
for (const capability of declaration.capabilities) {
|
||||
expect(operations.has(capability.operation)).toBe(false);
|
||||
operations.add(capability.operation);
|
||||
expect(Object.isFrozen(capability)).toBe(true);
|
||||
expect(Object.isFrozen(capability.effects)).toBe(true);
|
||||
expect(Object.isFrozen(capability.limitations)).toBe(true);
|
||||
}
|
||||
}
|
||||
expect(operations.has("inspect_native_dispatch_metadata")).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps declaration discovery and client creation lazy and independently owned", async () => {
|
||||
const target = await parseBinaryTarget(process.execPath);
|
||||
if (!target.ok) throw target.error;
|
||||
for (const declaration of auxiliaryAnalysisProviderDeclarations()) {
|
||||
let loads = 0;
|
||||
const provider = new LazyAnalysisProvider({
|
||||
...declaration,
|
||||
load: async () => {
|
||||
loads += 1;
|
||||
return declaration.load();
|
||||
},
|
||||
});
|
||||
expect(provider.identity()).toEqual(declaration.identity);
|
||||
expect(provider.capabilities()).toEqual(declaration.capabilities);
|
||||
const client = provider.createClient(target.value);
|
||||
const unusedClient = provider.createClient(target.value);
|
||||
await unusedClient.close();
|
||||
expect(loads).toBe(0);
|
||||
try {
|
||||
expect(await client.execute("health", {})).toMatchObject({
|
||||
ok: true,
|
||||
value: { provider: declaration.identity },
|
||||
});
|
||||
expect(loads).toBe(1);
|
||||
expect(await client.execute("health", {})).toMatchObject({ ok: true });
|
||||
expect(loads).toBe(1);
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
||||
|
||||
import { BROWSER_TOOL_CONTRACTS } from "../../dist/contracts/browserToolContracts.js";
|
||||
import { BROWSER_SCENARIO_TOOL_CONTRACTS } from "../../dist/contracts/browserScenarioToolContracts.js";
|
||||
import { ELECTRON_TOOL_CONTRACTS } from "../../dist/contracts/electronToolContracts.js";
|
||||
import { JAVASCRIPT_RUNTIME_OBSERVATION_TOOL_CONTRACTS } from "../../dist/contracts/javascriptRuntimeObservationToolContracts.js";
|
||||
import { TOOL_CONTRACTS } from "../../dist/contracts/toolContracts.js";
|
||||
import { parseConfig } from "../../dist/config.js";
|
||||
import { createBinarySession } from "../../dist/application/runtime.js";
|
||||
|
||||
// Mutate only this child's presentation arrays, before importing registration.
|
||||
if (process.argv[2] === "reversed") {
|
||||
for (const contracts of [
|
||||
BROWSER_TOOL_CONTRACTS,
|
||||
BROWSER_SCENARIO_TOOL_CONTRACTS,
|
||||
ELECTRON_TOOL_CONTRACTS,
|
||||
JAVASCRIPT_RUNTIME_OBSERVATION_TOOL_CONTRACTS,
|
||||
TOOL_CONTRACTS,
|
||||
])
|
||||
contracts.reverse();
|
||||
}
|
||||
const { createServer } = await import("../../dist/server/createServer.js");
|
||||
const config = parseConfig({});
|
||||
if (!config.ok) throw config.error;
|
||||
const session = createBinarySession(config.value);
|
||||
const server = createServer(session, session);
|
||||
const client = new Client({ name: "contract-presentation", version: "1" });
|
||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
||||
try {
|
||||
await server.connect(serverTransport);
|
||||
await client.connect(clientTransport);
|
||||
const catalog = await client.listTools();
|
||||
const responses = [];
|
||||
for (const contract of [
|
||||
...BROWSER_TOOL_CONTRACTS,
|
||||
...BROWSER_SCENARIO_TOOL_CONTRACTS,
|
||||
...ELECTRON_TOOL_CONTRACTS.filter(
|
||||
({ name }) => name !== "analyze_javascript_application",
|
||||
),
|
||||
...JAVASCRIPT_RUNTIME_OBSERVATION_TOOL_CONTRACTS,
|
||||
]) {
|
||||
const example = contract.examples[0];
|
||||
if (example === undefined)
|
||||
throw new Error(`Missing example for ${contract.name}`);
|
||||
responses.push({
|
||||
name: contract.name,
|
||||
response: await client.callTool({
|
||||
name: contract.name,
|
||||
arguments: example.input,
|
||||
}),
|
||||
});
|
||||
}
|
||||
process.stdout.write(JSON.stringify({ catalog: catalog.tools, responses }));
|
||||
} finally {
|
||||
await client.close();
|
||||
await server.close();
|
||||
await session.close();
|
||||
}
|
||||
Loading…
Reference in New Issue