diff --git a/src/application/EnhancedTools.ts b/src/application/EnhancedTools.ts index 707e6318fe..e00d2e83ed 100644 --- a/src/application/EnhancedTools.ts +++ b/src/application/EnhancedTools.ts @@ -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, + input: z.output, 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, ); diff --git a/src/application/FunctionAnalysisEvidence.ts b/src/application/FunctionAnalysisEvidence.ts index e12b0831e8..eed135ca34 100644 --- a/src/application/FunctionAnalysisEvidence.ts +++ b/src/application/FunctionAnalysisEvidence.ts @@ -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_, diff --git a/src/cli/coreNativeCommands.ts b/src/cli/coreNativeCommands.ts index 6eaff992e5..df28430346 100644 --- a/src/cli/coreNativeCommands.ts +++ b/src/cli/coreNativeCommands.ts @@ -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", ), diff --git a/src/contracts/enhancedInputs.ts b/src/contracts/enhancedInputs.ts index 3fb1b9e3b7..1d715e90ee 100644 --- a/src/contracts/enhancedInputs.ts +++ b/src/contracts/enhancedInputs.ts @@ -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"), diff --git a/src/domain/hopperValues.ts b/src/domain/hopperValues.ts index 5babb652d7..2d88397686 100644 --- a/src/domain/hopperValues.ts +++ b/src/domain/hopperValues.ts @@ -435,7 +435,7 @@ export type FunctionDossier = z.infer; /** Strictly parse a complete provider-neutral function dossier. */ export const parseFunctionDossier = ( value: JsonValue, -): Result => { +): Result => { const parsed = functionDossierSchema.safeParse(value); return parsed.success ? ok(parsed.data) diff --git a/src/domain/native/nativeProcedureInput.ts b/src/domain/native/nativeProcedureInput.ts new file mode 100644 index 0000000000..31dacbce24 --- /dev/null +++ b/src/domain/native/nativeProcedureInput.ts @@ -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"), +}); diff --git a/src/domain/native/objcSwiftMetadata.ts b/src/domain/native/objcSwiftMetadata.ts index b056c894ab..2ff35353ce 100644 --- a/src/domain/native/objcSwiftMetadata.ts +++ b/src/domain/native/objcSwiftMetadata.ts @@ -356,7 +356,6 @@ export const objcSwiftMetadataSchema = z.strictObject({ }); export type ObjcSwiftMetadata = z.infer; -/** 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], diff --git a/src/native/NativeMacOSProvider.ts b/src/native/NativeMacOSProvider.ts index 157b73d4fa..a0357ccba8 100644 --- a/src/native/NativeMacOSProvider.ts +++ b/src/native/NativeMacOSProvider.ts @@ -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(