refactor(native): centralize procedure and metadata input contracts

This commit is contained in:
morluto committed 2026-10-23 01:43:23 +08:00
1 parent 0d5c7b6d79
commit 44a4c1ba87
8 files changed
+76 -74

No files matched your search

+12 -19
View File
@@ -1,3 +1,4 @@
import { nativeProcedureInputSchema } from "../domain/native/nativeProcedureInput.js";
import { CALL_RELATIONSHIP_TRAVERSALS } from "./CallRelationshipTraversal.js";
import { z } from "zod";
@@ -9,7 +10,6 @@ import type {
} from "./AnalysisProvider.js";
import {
enhancedInputSchemas,
MAX_ENHANCED_RECORDS,
type CallRelationshipDirection,
type EnhancedToolName,
} from "../contracts/enhancedInputs.js";
@@ -21,7 +21,6 @@ import {
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import {
functionDossierSchema,
parseFunctionDossier,
parseListCount,
parseRelatedAddresses,
@@ -40,7 +39,10 @@ import { traceNativeValues } from "./native/NativeValueTrace.js";
import { traceNativeUiAction } from "./native/NativeUiActionTrace.js";
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
import { addressedValueSchema } from "../domain/native/nativeInventory.js";
import { nativeDispatchMetadataResultSchema } from "../domain/native/objcSwiftMetadata.js";
import {
nativeDispatchMetadataResultSchema,
MAX_DISPATCH_METADATA_RECORDS,
} from "../domain/native/objcSwiftMetadata.js";
import { isMachOFormat } from "../domain/binaryTargetTypes.js";
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
import {
@@ -269,12 +271,7 @@ export class EnhancedTools {
input: unknown,
signal?: AbortSignal,
): EnhancedResult {
if (name === "analyze_function") {
const parsed = enhancedInputSchemas.analyze_function.safeParse(input);
if (!parsed.success) return invalidEnhancedInput(name, parsed.error);
return this.executeValidated({ name, input: parsed.data }, signal);
}
const parsed = enhancedInputSchemas.inspect_native_api.safeParse(input);
const parsed = nativeProcedureInputSchema.safeParse(input);
if (!parsed.success) return invalidEnhancedInput(name, parsed.error);
return this.executeValidated({ name, input: parsed.data }, signal);
}
@@ -288,18 +285,14 @@ export class EnhancedTools {
}
async #inspectNativeApi(
input: z.output<typeof enhancedInputSchemas.inspect_native_api>,
input: z.output<typeof nativeProcedureInputSchema>,
signal?: AbortSignal,
): EnhancedResult {
const analysisInput = enhancedInputSchemas.analyze_function.parse({
procedure: input.procedure,
});
const result = await this.#call("analyze_function", analysisInput, signal);
const result = await this.#call("analyze_function", input, signal);
if (!result.ok) return result;
const parsed = parseFunctionDossier(result.value);
if (!parsed.ok) return parsed;
const dossier = functionDossierSchema.parse(parsed.value);
return ok(jsonValueSchema.parse(projectNativeApiInspection(dossier)));
return ok(jsonValueSchema.parse(projectNativeApiInspection(parsed.value)));
}
async #objcClasses(pattern: string, signal?: AbortSignal): EnhancedResult {
@@ -320,14 +313,14 @@ export class EnhancedTools {
);
const addresses = [...new Set(candidates.map(({ address }) => address))];
const scannedAddresses = addresses.slice(0, MAX_ENHANCED_RECORDS);
const scannedAddresses = addresses.slice(0, MAX_DISPATCH_METADATA_RECORDS);
const bindingsExecution = await this.#observe(
"inspect_native_dispatch_metadata",
{
max_records: MAX_ENHANCED_RECORDS,
max_records: MAX_DISPATCH_METADATA_RECORDS,
objc_class_import_addresses: scannedAddresses,
objc_class_import_addresses_complete:
addresses.length <= MAX_ENHANCED_RECORDS,
addresses.length <= MAX_DISPATCH_METADATA_RECORDS,
},
signal,
);
+4 -5
View File
@@ -2,11 +2,11 @@ import type {
AnalysisOperationPort,
ExecutionOptions,
} from "./AnalysisProvider.js";
import { toolContract } from "../contracts/toolContracts.js";
import { nativeProcedureInputSchema } from "../domain/native/nativeProcedureInput.js";
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import { createEvidence } from "../domain/evidence.js";
import { parseFunctionDossier } from "../domain/hopperValues.js";
import { jsonObjectSchema, type JsonValue } from "../domain/jsonValue.js";
import type { JsonValue } from "../domain/jsonValue.js";
import { err } from "../domain/result.js";
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
@@ -25,8 +25,7 @@ export const executeFunctionAnalysisEvidence = async (
cause: options.signal.reason,
}),
);
const input =
toolContract("analyze_function").inputSchema.safeParse(parameters);
const input = nativeProcedureInputSchema.safeParse(parameters);
if (!input.success)
return err(
analysisInputErrorFromIssues(
@@ -36,7 +35,7 @@ export const executeFunctionAnalysisEvidence = async (
{ cause: input.error },
),
);
const arguments_ = jsonObjectSchema.parse(input.data);
const arguments_ = input.data;
const execution = await analysis.execute(
"analyze_function",
arguments_,
+5 -11
View File
@@ -1,4 +1,4 @@
import { enhancedInputSchemas } from "../contracts/enhancedInputs.js";
import { nativeDispatchMetadataInputSchema } from "../domain/native/objcSwiftMetadata.js";
import { z } from "incur";
import type { DirectAnalysis } from "../composition/directAnalysis.js";
@@ -266,16 +266,10 @@ const registerNativeDispatchMetadataCommand = (
path: z.string().describe("Target path used to bind the result evidence"),
}),
options: z.object({
maxRecords: z
.number()
.int()
.min(1)
.max(20_000)
.default(5_000)
.describe("Maximum symbol records to inspect"),
maxInputBytes:
enhancedInputSchemas.inspect_native_dispatch_metadata.shape
.max_input_bytes,
maxRecords: nativeDispatchMetadataInputSchema.shape.max_records.describe(
"Maximum symbol records to inspect",
),
maxInputBytes: nativeDispatchMetadataInputSchema.shape.max_input_bytes,
snapshot: snapshotSelectionOption.describe(
"Load or update the local analysis snapshot",
),
+5 -33
View File
@@ -1,5 +1,6 @@
import { z } from "zod";
import { MAXIMUM_DISPATCH_METADATA_INPUT_BYTES } from "../domain/native/objcSwiftMetadata.js";
import { nativeDispatchMetadataInputSchema } from "../domain/native/objcSwiftMetadata.js";
import { nativeProcedureInputSchema } from "../domain/native/nativeProcedureInput.js";
import { nativeValueTraceInputSchema } from "../domain/native/nativeValueTrace.js";
const traceLiteralInputSchema = z.strictObject({
query: z.string().min(1),
@@ -7,7 +8,6 @@ const traceLiteralInputSchema = z.strictObject({
});
export const DEFAULT_CALL_RELATIONSHIP_DIRECTION = "forward";
export const MAX_ENHANCED_RECORDS = 20_000;
const callRelationshipDirectionSchema = z
.enum(["forward", "backward"])
.default(DEFAULT_CALL_RELATIONSHIP_DIRECTION);
@@ -18,31 +18,7 @@ export type CallRelationshipDirection = z.output<
/** Input schemas shared by MCP registration and enhanced application dispatch. */
export const enhancedInputSchemas = {
trace_native_values: nativeValueTraceInputSchema,
inspect_native_dispatch_metadata: z.strictObject({
max_records: z.int().min(1).max(MAX_ENHANCED_RECORDS).default(5_000),
objc_class_import_addresses: z
.array(z.string())
.max(MAX_ENHANCED_RECORDS)
.describe(
"Optional exact pointer-slot addresses from the selected name inventory; used to report decoded Objective-C class import bindings.",
)
.optional(),
objc_class_import_addresses_complete: z
.boolean()
.describe(
"Whether the candidate addresses include every selected class-label row; false keeps binding coverage partial when the existing record budget truncates the scan.",
)
.optional(),
max_input_bytes: z
.number()
.int()
.min(1)
.max(MAXIMUM_DISPATCH_METADATA_INPUT_BYTES)
.optional()
.describe(
"Largest target file REA reads into memory for binary metadata, in bytes. Omitted: 67108864 (64 MiB). The whole file counts, including every slice of a universal Mach-O. At most 1073741824 (1 GiB).",
),
}),
inspect_native_dispatch_metadata: nativeDispatchMetadataInputSchema,
get_objc_classes: z.strictObject({ pattern: z.string().default("") }),
get_objc_protocols: z.strictObject({}),
batch_decompile: z.strictObject({
@@ -74,12 +50,8 @@ export const enhancedInputSchemas = {
}),
find_xrefs_to_name: z.strictObject({ name: z.string() }),
binary_overview: z.strictObject({}),
analyze_function: z.strictObject({
procedure: z.string().describe("A procedure name or address"),
}),
inspect_native_api: z.strictObject({
procedure: z.string().describe("A procedure name or address"),
}),
analyze_function: nativeProcedureInputSchema,
inspect_native_api: nativeProcedureInputSchema,
trace_feature: traceLiteralInputSchema,
trace_call_path: z.strictObject({
start: z.string().describe("A starting procedure symbol or address"),
+1 -1
View File
@@ -435,7 +435,7 @@ export type FunctionDossier = z.infer<typeof functionDossierSchema>;
/** Strictly parse a complete provider-neutral function dossier. */
export const parseFunctionDossier = (
value: JsonValue,
): Result<JsonValue, AnalysisOutputError> => {
): Result<FunctionDossier, AnalysisOutputError> => {
const parsed = functionDossierSchema.safeParse(value);
return parsed.success
? ok(parsed.data)
@@ -0,0 +1,6 @@
import { z } from "zod";
/** Selected procedure for function dossier and native API analysis. */
export const nativeProcedureInputSchema = z.strictObject({
procedure: z.string().describe("A procedure name or address"),
});
+38 -2
View File
@@ -356,7 +356,6 @@ export const objcSwiftMetadataSchema = z.strictObject({
});
export type ObjcSwiftMetadata = z.infer<typeof objcSwiftMetadataSchema>;
/** Provider and target identity for one normalized dispatch-metadata result. */
/** Retained-image budget when the caller selects none. */
export const DEFAULT_DISPATCH_METADATA_INPUT_BYTES = 64 * 1024 * 1024;
@@ -367,6 +366,42 @@ export const DEFAULT_DISPATCH_METADATA_INPUT_BYTES = 64 * 1024 * 1024;
*/
export const MAXIMUM_DISPATCH_METADATA_INPUT_BYTES = 1024 * 1024 * 1024;
/** Shared metadata row allowance for provider reads and class-import discovery. */
export const MAX_DISPATCH_METADATA_RECORDS = 20_000;
export const DEFAULT_DISPATCH_METADATA_RECORDS = 5_000;
/** Selected dispatch facts and retained-image budget, shared by providers and adapters. */
export const nativeDispatchMetadataInputSchema = z.strictObject({
max_records: z
.number()
.int()
.min(1)
.max(MAX_DISPATCH_METADATA_RECORDS)
.default(DEFAULT_DISPATCH_METADATA_RECORDS),
objc_class_import_addresses: z
.array(z.string())
.max(MAX_DISPATCH_METADATA_RECORDS)
.describe(
"Optional exact pointer-slot addresses from the selected name inventory; used to report decoded Objective-C class import bindings.",
)
.optional(),
objc_class_import_addresses_complete: z
.boolean()
.describe(
"Whether the candidate addresses include every selected class-label row; false keeps binding coverage partial when the existing record budget truncates the scan.",
)
.optional(),
max_input_bytes: z
.number()
.int()
.min(1)
.max(MAXIMUM_DISPATCH_METADATA_INPUT_BYTES)
.optional()
.describe(
"Largest target file REA reads into memory for binary metadata, in bytes. Omitted: 67108864 (64 MiB). The whole file counts, including every slice of a universal Mach-O. At most 1073741824 (1 GiB).",
),
});
/** Effective input budget of one binary metadata read and the bytes it retained. */
export const dispatchMetadataInputAdmissionSchema = z.strictObject({
maximum_bytes: z
@@ -381,6 +416,7 @@ export type DispatchMetadataInputAdmission = z.infer<
typeof dispatchMetadataInputAdmissionSchema
>;
/** Provider and target identity for one normalized dispatch-metadata result. */
export const nativeDispatchMetadataResultSchema = z.strictObject({
target_sha256: z
.string()
@@ -401,7 +437,7 @@ export const nativeDispatchMetadataResultSchema = z.strictObject({
/** Decode only facts visible in a provider's existing name inventory. */
export const inspectNativeDispatchMetadata = (
names: readonly { readonly address: string; readonly name: string }[],
maxRecords = 5_000,
maxRecords = DEFAULT_DISPATCH_METADATA_RECORDS,
): ObjcSwiftMetadata => {
const symbolEvidence = (
entry: (typeof names)[number],
+5 -3
View File
@@ -46,8 +46,10 @@ import {
isMachOFormat,
type BinaryTarget,
} from "../domain/binaryTargetTypes.js";
import { isAppleDispatchMetadataTarget } from "../domain/native/objcSwiftMetadata.js";
import { toolContract } from "../contracts/toolContracts.js";
import {
isAppleDispatchMetadataTarget,
nativeDispatchMetadataInputSchema,
} from "../domain/native/objcSwiftMetadata.js";
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
import type { EvidenceLocation } from "../domain/evidence.js";
import {
@@ -270,7 +272,7 @@ class NativeMacOSClient implements AnalysisClient {
),
);
if (operation === "inspect_native_dispatch_metadata") {
const input = toolContract(operation).inputSchema.safeParse(parameters);
const input = nativeDispatchMetadataInputSchema.safeParse(parameters);
if (!input.success)
return err(
analysisInputErrorFromIssues(