diff --git a/scripts/lib/client-schema-patterns.mts b/scripts/lib/client-schema-patterns.mts new file mode 100644 index 0000000000..2ac721492a --- /dev/null +++ b/scripts/lib/client-schema-patterns.mts @@ -0,0 +1,48 @@ +import { JsonSchemaReferences } from "../../src/contracts/jsonSchemaReferences.js"; +import { + schemaArrays, + schemaChildren, + schemaMaps, +} from "../../src/contracts/jsonSchemaKeywords.js"; + +const isSchemaObject = ( + value: unknown, +): value is Readonly> => + value !== null && typeof value === "object" && !Array.isArray(value); + +/** Check forwarded regex constraints in native plain/Unicode/Unicode-sets modes. */ +export const assertClientSchemaPatterns = ( + schema: unknown, +): readonly string[] => { + if (!isSchemaObject(schema)) return []; + const patterns: string[] = []; + const seen = new Set(); + const checkPattern = (pattern: string): void => { + for (const flags of ["", "u", "v"]) new RegExp(pattern, flags); + patterns.push(pattern); + }; + const visit = (value: unknown): void => { + if (!isSchemaObject(value) || seen.has(value)) return; + seen.add(value); + for (const [key, child] of Object.entries(value)) { + if (key === "pattern" && typeof child === "string") checkPattern(child); + else if (schemaMaps.has(key) && isSchemaObject(child)) { + for (const [name, nested] of Object.entries(child)) { + if (key === "patternProperties") checkPattern(name); + visit(nested); + } + } else if ( + (schemaArrays.has(key) || schemaChildren.has(key)) && + Array.isArray(child) + ) { + for (const nested of child) visit(nested); + } else if (schemaChildren.has(key)) visit(child); + } + }; + visit(schema); + // Literal instance values stay opaque unless the reference owner resolves a + // local target into them. External/dynamic/ambiguous targets remain unknown. + for (const { target } of new JsonSchemaReferences(schema).references()) + if (target.kind === "target") visit(target.value); + return patterns; +}; diff --git a/scripts/verify-deepseek-client.mjs b/scripts/verify-deepseek-client.mjs index 2a061df833..9b800422f9 100644 --- a/scripts/verify-deepseek-client.mjs +++ b/scripts/verify-deepseek-client.mjs @@ -20,11 +20,17 @@ import { isClientJavaScriptEvidence, } from "./verify/clients/result-discovery.mjs"; import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs"; +import { importVerifierSource } from "./lib/verifier-source-loader.mjs"; import { createOpenAiModelFixture } from "./verify/clients/model-fixture.mjs"; const mode = process.argv[2] ?? "call"; assert(["chat", "call"].includes(mode), "Select chat or call."); assert(process.platform !== "win32", "Native Windows coverage is unverified."); +const { assertClientSchemaPatterns } = await importVerifierSource( + new URL("./lib/client-schema-patterns.mts", import.meta.url).href, + import.meta.url, +); + const repo = fileURLToPath(new URL("..", import.meta.url)); const runtimeRoot = process.env.REA_VERIFY_RUNTIME_ROOT ?? repo; const command = process.env.REA_VERIFY_DEEPSEEK_COMMAND ?? "dsh"; @@ -123,17 +129,6 @@ await withVerifierCleanup(async () => { }); const ajv = new Ajv2020({ strict: false, validateFormats: false }); - const validatePatterns = (value) => { - if (!value || typeof value !== "object") return; - for (const [key, child] of Object.entries(value)) { - if (key === "pattern" && typeof child === "string") { - // Check the representation the native client actually forwards to its model adapter. - for (const flags of ["", "u", "v"]) new RegExp(child, flags); - patterns.add(child); - patternCount++; - } else validatePatterns(child); - } - }; fixture = await createOpenAiModelFixture({ directory: lab, prefix: `deepseek-${mode}`, @@ -160,7 +155,11 @@ await withVerifierCleanup(async () => { ajv.validateSchema(tool.parameters), `${tool.name}: ${ajv.errorsText(ajv.errors)}`, ); - validatePatterns(tool.parameters); + const forwardedPatterns = assertClientSchemaPatterns( + tool.parameters, + ); + for (const pattern of forwardedPatterns) patterns.add(pattern); + patternCount += forwardedPatterns.length; } assert( patternCount > 0, diff --git a/src/application/android/AndroidPackageInspection.ts b/src/application/android/AndroidPackageInspection.ts index 765ac7a988..a879b79727 100644 --- a/src/application/android/AndroidPackageInspection.ts +++ b/src/application/android/AndroidPackageInspection.ts @@ -6,11 +6,10 @@ import { type AnalysisViewRetention, } from "../analysisView/AnalysisViewService.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; -import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js"; import { androidPackageInspectionInputSchema } from "../../domain/android/androidAnalysis.js"; import type { Evidence } from "../../domain/evidence.js"; -import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js"; -import { err, type Result } from "../../domain/result.js"; +import type { Result } from "../../domain/result.js"; +import { parseProviderOperationInput } from "../ProviderOperationExecution.js"; /** Preserve the complete provider request; select retained delivery explicitly. */ export const inspectAndroidPackage = async ( @@ -20,26 +19,22 @@ export const inspectAndroidPackage = async ( options?: ExecutionOptions, ): Promise> => { const operation = "inspect_android_package"; - if (options?.signal?.aborted === true) - return err( - new AnalysisCancelledError(operation, { cause: options.signal.reason }), - ); - const parsed = androidPackageInspectionInputSchema.safeParse(rawInput); - if (!parsed.success) - return err( - analysisInputErrorFromIssues(operation, parsed.error.issues, rawInput, { - cause: parsed.error, - }), - ); - if (parsed.data.detail === "summary") { + const parsed = parseProviderOperationInput( + operation, + rawInput, + androidPackageInspectionInputSchema, + options?.signal, + ); + if (!parsed.ok) return parsed; + if (parsed.value.detail === "summary") { const ready = requireAnalysisViewRetention(operation, retention); if (!ready.ok) return ready; } const executed = await service.execute( operation, - { path: parsed.data.path }, + { path: parsed.value.path }, options, ); - if (!executed.ok || parsed.data.detail !== "summary") return executed; + if (!executed.ok || parsed.value.detail !== "summary") return executed; return summarizeRetainedAnalysis(executed.value, retention); }; diff --git a/src/application/artifacts/DesktopApplicationService.ts b/src/application/artifacts/DesktopApplicationService.ts index 9710936c70..7fd4773043 100644 --- a/src/application/artifacts/DesktopApplicationService.ts +++ b/src/application/artifacts/DesktopApplicationService.ts @@ -1,13 +1,11 @@ import type { ExecutionOptions } from "../AnalysisProvider.js"; import type { DesktopApplicationPort } from "./DesktopApplicationPort.js"; import { ARTIFACT_GRAPH_PROVIDER } from "../../domain/investigationProviders.js"; -import { completeProviderEvidence } from "../ProviderOperationExecution.js"; import { - AnalysisInputError, - AnalysisOutputError, - AnalysisCancelledError, -} from "../../domain/analysisErrorCore.js"; -import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js"; + completeProviderEvidence, + parseProviderOperationInput, +} from "../ProviderOperationExecution.js"; +import { AnalysisOutputError } from "../../domain/analysisErrorCore.js"; import type { Evidence } from "../../domain/evidence.js"; import { createDesktopApplicationEvidence } from "../../domain/desktopApplicationEvidence.js"; import { @@ -29,21 +27,17 @@ export class DesktopApplicationService { options?: ExecutionOptions, ): Promise> { const operation = "inspect_desktop_application"; - if (options?.signal?.aborted) - return err( - new AnalysisCancelledError(operation, { cause: options.signal.reason }), - ); - const parsed = inspectDesktopApplicationInputSchema.safeParse(rawInput); - if (!parsed.success) - return err( - analysisInputErrorFromIssues(operation, parsed.error.issues, rawInput, { - cause: parsed.error, - }), - ); - const result = await this.reader.inspect(parsed.data, options); + const parsed = parseProviderOperationInput( + operation, + rawInput, + inspectDesktopApplicationInputSchema, + options?.signal, + ); + if (!parsed.ok) return parsed; + const result = await this.reader.inspect(parsed.value, options); if (!result.ok) return result; const report = desktopApplicationSchema.safeParse(result.value); - if (!report.success || report.data.root.path !== parsed.data.path) + if (!report.success || report.data.root.path !== parsed.value.path) return err( new AnalysisOutputError( operation, @@ -52,7 +46,7 @@ export class DesktopApplicationService { ); return completeProviderEvidence( createDesktopApplicationEvidence( - parsed.data, + parsed.value, report.data, ARTIFACT_GRAPH_PROVIDER, ), diff --git a/src/application/electron/ElectronFuseService.ts b/src/application/electron/ElectronFuseService.ts index dbe5429203..4290ebdf4b 100644 --- a/src/application/electron/ElectronFuseService.ts +++ b/src/application/electron/ElectronFuseService.ts @@ -1,19 +1,18 @@ import type { ExecutionOptions } from "../AnalysisProvider.js"; import type { ElectronFusePort } from "./ElectronFusePort.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; -import { - AnalysisCancelledError, - AnalysisOutputError, -} from "../../domain/analysisErrorCore.js"; +import { AnalysisOutputError } from "../../domain/analysisErrorCore.js"; import { electronFuseReportSchema, inspectElectronFusesInputSchema, } from "../../domain/electron/electronFuses.js"; import type { Evidence } from "../../domain/evidence.js"; import { createElectronFuseEvidence } from "../../domain/electron/electronFuseEvidence.js"; -import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js"; import { err, ok, type Result } from "../../domain/result.js"; -import { completeProviderEvidence } from "../ProviderOperationExecution.js"; +import { + completeProviderEvidence, + parseProviderOperationInput, +} from "../ProviderOperationExecution.js"; const OPERATION = "inspect_electron_fuses"; @@ -31,23 +30,19 @@ export class ElectronFuseService { rawInput: unknown, options?: ExecutionOptions, ): Promise> { - if (options?.signal?.aborted) - return err( - new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }), - ); - const input = inspectElectronFusesInputSchema.safeParse(rawInput); - if (!input.success) - return err( - analysisInputErrorFromIssues(OPERATION, input.error.issues, rawInput, { - cause: input.error, - }), - ); - const inspected = await this.provider.inspect(input.data, options); + const input = parseProviderOperationInput( + OPERATION, + rawInput, + inspectElectronFusesInputSchema, + options?.signal, + ); + if (!input.ok) return input; + const inspected = await this.provider.inspect(input.value, options); if (!inspected.ok) return inspected; const report = electronFuseReportSchema.safeParse(inspected.value); if ( !report.success || - report.data.artifact.selected_path !== input.data.path + report.data.artifact.selected_path !== input.value.path ) return err( new AnalysisOutputError( @@ -57,7 +52,7 @@ export class ElectronFuseService { ); return completeProviderEvidence( createElectronFuseEvidence( - input.data, + input.value, report.data, this.provider.identity, ), diff --git a/src/application/erlang/BeamService.ts b/src/application/erlang/BeamService.ts index 483ed45798..46fa29a988 100644 --- a/src/application/erlang/BeamService.ts +++ b/src/application/erlang/BeamService.ts @@ -1,19 +1,18 @@ import type { ExecutionOptions } from "../AnalysisProvider.js"; import type { BeamPort } from "./BeamPort.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; -import { - AnalysisCancelledError, - AnalysisOutputError, -} from "../../domain/analysisErrorCore.js"; +import { AnalysisOutputError } from "../../domain/analysisErrorCore.js"; import { createEvidence, type Evidence } from "../../domain/evidence.js"; import { inspectErlangBeamInputSchema, erlangBeamSchema, type ErlangBeam, } from "../../domain/erlang/beam.js"; -import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js"; import { err, type Result } from "../../domain/result.js"; -import { completeProviderEvidence } from "../ProviderOperationExecution.js"; +import { + completeProviderEvidence, + parseProviderOperationInput, +} from "../ProviderOperationExecution.js"; const OPERATION = "inspect_erlang_beam"; @@ -29,30 +28,24 @@ export class BeamService { rawInput: unknown, options?: ExecutionOptions, ): Promise> { - if (options?.signal?.aborted) - return err( - new AnalysisCancelledError(OPERATION, { - cause: options.signal.reason, - }), - ); - const input = inspectErlangBeamInputSchema.safeParse(rawInput); - if (!input.success) - return err( - analysisInputErrorFromIssues(OPERATION, input.error.issues, rawInput, { - cause: input.error, - }), - ); - const result = await this.provider.inspect(input.data, options); + const input = parseProviderOperationInput( + OPERATION, + rawInput, + inspectErlangBeamInputSchema, + options?.signal, + ); + if (!input.ok) return input; + const result = await this.provider.inspect(input.value, options); if (!result.ok) return result; const report = erlangBeamSchema.safeParse(result.value); if ( !report.success || - report.data.artifact.path !== input.data.path || + report.data.artifact.path !== input.value.path || !validReport( report.data, - input.data.max_chunks, - input.data.max_atoms, - input.data.max_file_bytes, + input.value.max_chunks, + input.value.max_atoms, + input.value.max_file_bytes, ) ) return err( @@ -72,7 +65,7 @@ export class BeamService { this.provider.identity, { operation: OPERATION, - parameters: input.data, + parameters: input.value, result: value, confidence: "observed", limitations: value.limitations, diff --git a/src/application/flutter/DartAotInspection.ts b/src/application/flutter/DartAotInspection.ts index df49188388..ab3deb6388 100644 --- a/src/application/flutter/DartAotInspection.ts +++ b/src/application/flutter/DartAotInspection.ts @@ -6,11 +6,10 @@ import { type AnalysisViewRetention, } from "../analysisView/AnalysisViewService.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; -import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js"; import { dartAotInspectionInputSchema } from "../../domain/flutter/flutterBuildAnalysis.js"; import type { Evidence } from "../../domain/evidence.js"; -import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js"; -import { err, type Result } from "../../domain/result.js"; +import type { Result } from "../../domain/result.js"; +import { parseProviderOperationInput } from "../ProviderOperationExecution.js"; /** Keep the caller's ABI choice and complete analysis; select only its delivery. */ export const inspectDartAot = async ( @@ -20,24 +19,18 @@ export const inspectDartAot = async ( options?: ExecutionOptions, ): Promise> => { const operation = "inspect_dart_aot"; - if (options?.signal?.aborted === true) - return err( - new AnalysisCancelledError(operation, { - cause: options.signal.reason, - }), - ); - const parsed = dartAotInspectionInputSchema.safeParse(rawInput); - if (!parsed.success) - return err( - analysisInputErrorFromIssues(operation, parsed.error.issues, rawInput, { - cause: parsed.error, - }), - ); - if (parsed.data.detail === "summary") { + const parsed = parseProviderOperationInput( + operation, + rawInput, + dartAotInspectionInputSchema, + options?.signal, + ); + if (!parsed.ok) return parsed; + if (parsed.value.detail === "summary") { const ready = requireAnalysisViewRetention(operation, retention); if (!ready.ok) return ready; } - const { detail, ...providerInput } = parsed.data; + const { detail, ...providerInput } = parsed.value; const executed = await service.execute(operation, providerInput, options); if (!executed.ok || detail !== "summary") return executed; return summarizeRetainedAnalysis(executed.value, retention); diff --git a/src/application/frida/FridaInstrumentationService.ts b/src/application/frida/FridaInstrumentationService.ts index b0f52f7c67..08084743ec 100644 --- a/src/application/frida/FridaInstrumentationService.ts +++ b/src/application/frida/FridaInstrumentationService.ts @@ -16,7 +16,7 @@ import { } from "../../domain/analysisErrorCore.js"; import { err, ok, type Result } from "../../domain/result.js"; import { fridaSessionStatusInputSchema } from "../../domain/fridaSessionStatus.js"; -import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js"; +import { parseProviderOperationInput } from "../ProviderOperationExecution.js"; import { requireAnalysisViewRetention, summarizeRetainedAnalysis, @@ -252,36 +252,32 @@ export class FridaInstrumentationService { evidenceById: undefined, }, ): Result { - const input = fridaSessionStatusInputSchema.safeParse(rawInput); - if (!input.success) - return err( - analysisInputErrorFromIssues( - "frida_session_status", - input.error.issues, - rawInput, - { cause: input.error }, - ), - ); - if (input.data.detail === "summary") { + const input = parseProviderOperationInput( + "frida_session_status", + rawInput, + fridaSessionStatusInputSchema, + ); + if (!input.ok) return input; + if (input.value.detail === "summary") { const ready = requireAnalysisViewRetention( "frida_session_status", retention, ); if (!ready.ok) return ready; } - const status = this.#provider.status(input.data.session_id); + const status = this.#provider.status(input.value.session_id); if (status === undefined) return err( - unknownFridaSession("frida_session_status", input.data.session_id), + unknownFridaSession("frida_session_status", input.value.session_id), ); const evidence = fridaEvidence( "frida_session_status", sessionStatusOutput(status), { - session_id: input.data.session_id, + session_id: input.value.session_id, }, ); - return input.data.detail === "summary" + return input.value.detail === "summary" ? summarizeRetainedAnalysis(evidence, retention) : ok(evidence); } diff --git a/src/application/il2cpp/Il2cppMetadataService.ts b/src/application/il2cpp/Il2cppMetadataService.ts index 76cdae05f2..2e8a04ab75 100644 --- a/src/application/il2cpp/Il2cppMetadataService.ts +++ b/src/application/il2cpp/Il2cppMetadataService.ts @@ -1,3 +1,4 @@ +import { parseProviderOperationInput } from "../ProviderOperationExecution.js"; import { analysisErrorWithCleanupFailure } from "../../domain/analysisErrorCleanup.js"; import { ProviderCleanupError } from "../../domain/providerCleanupError.js"; import type { @@ -25,7 +26,6 @@ import { type Il2cppMetadata, type Il2cppMetadataProviderInput, } from "../../domain/il2cpp/il2cppMetadata.js"; -import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js"; import { err, ok, type Result } from "../../domain/result.js"; const OPERATION = "inspect_il2cpp_metadata"; @@ -99,20 +99,16 @@ export class Il2cppMetadataService { retention: AnalysisViewRetention, output?: Il2cppSeedOutput, ): Promise> { - if (options?.signal?.aborted) - return err( - new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }), - ); - const input = inspectIl2cppMetadataInputSchema.safeParse(rawInput); - if (!input.success) - return err( - analysisInputErrorFromIssues(OPERATION, input.error.issues, rawInput, { - cause: input.error, - }), - ); + const input = parseProviderOperationInput( + OPERATION, + rawInput, + inspectIl2cppMetadataInputSchema, + options?.signal, + ); + if (!input.ok) return input; if ( - input.data.binary_architecture !== undefined && - input.data.binary_path === undefined + input.value.binary_architecture !== undefined && + input.value.binary_path === undefined ) return err( new AnalysisInputError(OPERATION, undefined, [ @@ -124,8 +120,8 @@ export class Il2cppMetadataService { ]), ); if ( - input.data.generic_instances === true && - input.data.binary_path === undefined + input.value.generic_instances === true && + input.value.binary_path === undefined ) return err( new AnalysisInputError(OPERATION, undefined, [ @@ -137,7 +133,7 @@ export class Il2cppMetadataService { ]), ); if (output !== undefined) { - if (input.data.binary_path === undefined) + if (input.value.binary_path === undefined) return err( new AnalysisInputError(OPERATION, undefined, [ { @@ -158,7 +154,7 @@ export class Il2cppMetadataService { const ready = await this.seeds.check(output, options); if (!ready.ok) return ready; } - const { detail, ...providerInput } = input.data; + const { detail, ...providerInput } = input.value; if (detail === "summary") { const ready = requireAnalysisViewRetention(OPERATION, retention); if (!ready.ok) return ready; diff --git a/src/application/javascript/JavaScriptApplicationWorkflowService.ts b/src/application/javascript/JavaScriptApplicationWorkflowService.ts index fe4a4b2880..703dbbb4b9 100644 --- a/src/application/javascript/JavaScriptApplicationWorkflowService.ts +++ b/src/application/javascript/JavaScriptApplicationWorkflowService.ts @@ -3,6 +3,7 @@ import { type EvidenceLookup, } from "../EvidenceInputResolver.js"; import { retainedEvidenceReferenceSchema } from "../../domain/evidenceReference.js"; +import { parseProviderOperationInput } from "../ProviderOperationExecution.js"; import type { ResolvedSourceToBundleReference } from "./SourceToBundleReference.js"; import { z } from "zod"; @@ -121,16 +122,15 @@ export const traceApplicationFeatureEvidence = ( AnalysisError > => { const operation = "trace_application_feature"; - const request = featureTraceFrameSchema.safeParse(rawInput); - if (!request.success) - return err( - analysisInputErrorFromIssues(operation, request.error.issues, rawInput, { - cause: request.error, - }), - ); - const selected = resolveWorkflowEvidence(request.data.application, lookup); + const request = parseProviderOperationInput( + operation, + rawInput, + featureTraceFrameSchema, + ); + if (!request.ok) return request; + const selected = resolveWorkflowEvidence(request.value.application, lookup); if (!selected.ok) return selected; - const input = { ...request.data, application: selected.value }; + const input = { ...request.value, application: selected.value }; const sourceResult = parseApplicationGraphEvidence(input.application, [ "application", ]); @@ -187,17 +187,16 @@ export const compareApplicationVersionsEvidence = ( AnalysisError > => { const operation = "compare_application_versions"; - const request = versionComparisonFrameSchema.safeParse(rawInput); - if (!request.success) - return err( - analysisInputErrorFromIssues(operation, request.error.issues, rawInput, { - cause: request.error, - }), - ); - const pair = resolveApplicationGraphPair(operation, request.data, lookup); + const request = parseProviderOperationInput( + operation, + rawInput, + versionComparisonFrameSchema, + ); + if (!request.ok) return request; + const pair = resolveApplicationGraphPair(operation, request.value, lookup); if (!pair.ok) return pair; const { left, right } = pair.value; - const input = { ...request.data, ...pair.value.selected }; + const input = { ...request.value, ...pair.value.selected }; const leftNativeResult = parseNativeApplicationEvidence( input.left_native_observations, ["left_native_observations"], @@ -306,17 +305,16 @@ export const compareJavaScriptExportShapesEvidence = ( AnalysisError > => { const operation = "compare_javascript_export_shapes"; - const request = exportShapeFrameSchema.safeParse(rawInput); - if (!request.success) - return err( - analysisInputErrorFromIssues(operation, request.error.issues, rawInput, { - cause: request.error, - }), - ); - const pair = resolveApplicationGraphPair(operation, request.data, lookup); + const request = parseProviderOperationInput( + operation, + rawInput, + exportShapeFrameSchema, + ); + if (!request.ok) return request; + const pair = resolveApplicationGraphPair(operation, request.value, lookup); if (!pair.ok) return pair; const { left, right } = pair.value; - const input = { ...request.data, ...pair.value.selected }; + const input = { ...request.value, ...pair.value.selected }; const identities = javaScriptExportShapeIdentitiesSchema.safeParse({ left: left.evidence, right: right.evidence, diff --git a/src/application/managed/ManagedReconstructionService.ts b/src/application/managed/ManagedReconstructionService.ts index 03f5786d7a..70e6323838 100644 --- a/src/application/managed/ManagedReconstructionService.ts +++ b/src/application/managed/ManagedReconstructionService.ts @@ -8,9 +8,9 @@ import { type ManagedReconstructionImportResult, type ResolvedManagedReconstructionImport, } from "../../domain/managed/managedReconstruction.js"; -import { err, ok, type Result } from "../../domain/result.js"; +import { ok, type Result } from "../../domain/result.js"; import { MANAGED_WORKFLOW_PROVIDER } from "../../domain/investigationProviders.js"; -import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js"; +import { parseProviderOperationInput } from "../ProviderOperationExecution.js"; const OPERATION = "import_managed_reconstruction" as const; @@ -18,14 +18,13 @@ const OPERATION = "import_managed_reconstruction" as const; export const resolveManagedReconstructionEvidenceInput = ( rawInput: unknown, ): Result => { - const parsed = managedReconstructionImportRequestSchema.safeParse(rawInput); - if (!parsed.success) - return err( - analysisInputErrorFromIssues(OPERATION, parsed.error.issues, rawInput, { - cause: parsed.error, - }), - ); - return resolveManagedReconstructionImport(parsed.data); + const parsed = parseProviderOperationInput( + OPERATION, + rawInput, + managedReconstructionImportRequestSchema, + ); + if (!parsed.ok) return parsed; + return resolveManagedReconstructionImport(parsed.value); }; /** Execute from the source facts returned by the resolver. */ diff --git a/src/application/reverse/ReverseEngineeringService.ts b/src/application/reverse/ReverseEngineeringService.ts index a4adcba067..726abb4475 100644 --- a/src/application/reverse/ReverseEngineeringService.ts +++ b/src/application/reverse/ReverseEngineeringService.ts @@ -47,6 +47,7 @@ import { } from "../../process/OwnedCommand.js"; import type { ProviderProcessSupervisor } from "../../process/ProviderProcess.js"; import { absoluteCallerPath } from "../../filesystem/callerPath.js"; +import { parseProviderOperationInput } from "../ProviderOperationExecution.js"; export type ReverseEngineeringOperation = | "inspect_with_objdump" @@ -214,21 +215,17 @@ export class ReverseEngineeringService { recordEvidence: undefined, }, ): Promise> { - const parsed = objdumpInputSchema.safeParse(rawInput); - if (!parsed.success) - return err( - analysisInputErrorFromIssues( - "inspect_with_objdump", - parsed.error.issues, - rawInput, - { cause: parsed.error }, - ), - ); + const parsed = parseProviderOperationInput( + "inspect_with_objdump", + rawInput, + objdumpInputSchema, + ); + if (!parsed.ok) return parsed; const { detail, command_timeout_ms: commandTimeoutMs, ...input - } = parsed.data; + } = parsed.value; if (detail === "summary") { const ready = requireAnalysisViewRetention( "inspect_with_objdump", diff --git a/src/application/sqlite/SqliteDatabaseService.ts b/src/application/sqlite/SqliteDatabaseService.ts index f1cc74eecd..ba206261d7 100644 --- a/src/application/sqlite/SqliteDatabaseService.ts +++ b/src/application/sqlite/SqliteDatabaseService.ts @@ -1,10 +1,8 @@ +import { parseProviderOperationInput } from "../ProviderOperationExecution.js"; import type { ExecutionOptions } from "../AnalysisProvider.js"; import type { SqliteDatabasePort } from "./SqliteDatabasePort.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; -import { - AnalysisCancelledError, - AnalysisOutputError, -} from "../../domain/analysisErrorCore.js"; +import { AnalysisOutputError } from "../../domain/analysisErrorCore.js"; import { createEvidence, type Evidence, @@ -21,7 +19,6 @@ import { sqliteSchemaScope, sqliteInspectionParameters, } from "../../domain/sqlite/sqliteDatabase.js"; -import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js"; import { err, ok, type Result } from "../../domain/result.js"; const OPERATION = "inspect_sqlite_database"; @@ -82,27 +79,21 @@ export class SqliteDatabaseService { rawInput: unknown, options?: ExecutionOptions, ): Promise> { - if (options?.signal?.aborted) - return err( - new AnalysisCancelledError(OPERATION, { - cause: options.signal.reason, - }), - ); - const input = inspectSqliteDatabaseInputSchema.safeParse(rawInput); - if (!input.success) - return err( - analysisInputErrorFromIssues(OPERATION, input.error.issues, rawInput, { - cause: input.error, - }), - ); - const inspected = await this.provider.inspect(input.data, options); + const input = parseProviderOperationInput( + OPERATION, + rawInput, + inspectSqliteDatabaseInputSchema, + options?.signal, + ); + if (!input.ok) return input; + const inspected = await this.provider.inspect(input.value, options); if (!inspected.ok) return inspected; const execution = inspected.value; const report = sqliteDatabaseSchema.safeParse(execution.result); if ( !report.success || execution.subject == null || - !matchesSelectedDatabase(report.data, input.data, execution.subject) + !matchesSelectedDatabase(report.data, input.value, execution.subject) ) return err( new AnalysisOutputError( @@ -113,7 +104,7 @@ export class SqliteDatabaseService { return ok( createEvidence(execution.subject, execution.provider, { operation: OPERATION, - parameters: sqliteInspectionParameters(input.data), + parameters: sqliteInspectionParameters(input.value), result: report.data, ...(execution.rawResult === null ? {} diff --git a/src/application/wasm/WasmArtifactService.ts b/src/application/wasm/WasmArtifactService.ts index 24a952dca6..a95ab38014 100644 --- a/src/application/wasm/WasmArtifactService.ts +++ b/src/application/wasm/WasmArtifactService.ts @@ -3,10 +3,7 @@ import type { ExecutionOptions } from "../AnalysisProvider.js"; import { createAnalysisExecutionEvidence } from "../../domain/analysisObservation.js"; import type { WasmArtifactPort } from "./WasmArtifactPort.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; -import { - AnalysisCancelledError, - AnalysisOutputError, -} from "../../domain/analysisErrorCore.js"; +import { AnalysisOutputError } from "../../domain/analysisErrorCore.js"; import type { Evidence } from "../../domain/evidence.js"; import { inspectWasmArtifactInputSchema, @@ -14,9 +11,11 @@ import { wasmArtifactMatchesSubject, DEFAULT_WASM_COMMAND_TIMEOUT_MS, } from "../../domain/wasm/wasmArtifact.js"; -import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js"; import { err, type Result } from "../../domain/result.js"; -import { completeProviderEvidence } from "../ProviderOperationExecution.js"; +import { + completeProviderEvidence, + parseProviderOperationInput, +} from "../ProviderOperationExecution.js"; const OPERATION = "inspect_wasm_artifact"; @@ -37,24 +36,18 @@ export class WasmArtifactService { rawInput: unknown, options?: ExecutionOptions, ): Promise> { - if (options?.signal?.aborted) - return err( - new AnalysisCancelledError(OPERATION, { - cause: options.signal.reason, - }), - ); - const input = inspectWasmArtifactInputSchema.safeParse(rawInput); - if (!input.success) - return err( - analysisInputErrorFromIssues(OPERATION, input.error.issues, rawInput, { - cause: input.error, - }), - ); + const input = parseProviderOperationInput( + OPERATION, + rawInput, + inspectWasmArtifactInputSchema, + options?.signal, + ); + if (!input.ok) return input; const timeout = - input.data.command_timeout_ms ?? + input.value.command_timeout_ms ?? this.settings?.().overrides["wasm.command_timeout_ms"] ?? DEFAULT_WASM_COMMAND_TIMEOUT_MS; - const inspected = await this.provider.inspect(input.data, { + const inspected = await this.provider.inspect(input.value, { ...options, commandTimeoutMs: timeout, }); @@ -63,14 +56,14 @@ export class WasmArtifactService { const report = wasmArtifactSchema.safeParse(execution.result); if ( !report.success || - report.data.artifact.path !== input.data.path || + report.data.artifact.path !== input.value.path || report.data.tool_profile.timeout_ms !== timeout || !wasmArtifactMatchesSubject(report.data.artifact, execution.subject) || JSON.stringify(report.data.glue.map((item) => item.artifact.path)) !== - JSON.stringify(input.data.glue_paths) || + JSON.stringify(input.value.glue_paths) || JSON.stringify(report.data.candidates.map((item) => item.path)) !== JSON.stringify([ - ...new Set([input.data.path, ...input.data.candidate_paths]), + ...new Set([input.value.path, ...input.value.candidate_paths]), ]) ) return err( @@ -82,7 +75,7 @@ export class WasmArtifactService { return completeProviderEvidence( createAnalysisExecutionEvidence( OPERATION, - input.data, + input.value, { ...execution, result: report.data }, "observed", ), diff --git a/tests/conformance/verifiers/clientSchemaPatterns.test.ts b/tests/conformance/verifiers/clientSchemaPatterns.test.ts new file mode 100644 index 0000000000..caec43e797 --- /dev/null +++ b/tests/conformance/verifiers/clientSchemaPatterns.test.ts @@ -0,0 +1,97 @@ +import { expect, it } from "vitest"; +import { Ajv2020 } from "ajv/dist/2020.js"; +import { assertClientSchemaPatterns } from "../../../scripts/lib/client-schema-patterns.mjs"; + +it("keeps pattern-looking fields inside valid literal values opaque", () => { + const schema = { + type: "object", + properties: { + payload: { + const: { pattern: "[" }, + enum: [{ pattern: "[" }], + default: { pattern: "[" }, + examples: [{ pattern: "[" }], + }, + }, + }; + const ajv = new Ajv2020({ strict: false, validateFormats: false }); + expect(ajv.validateSchema(schema)).toBe(true); + expect(assertClientSchemaPatterns(schema)).toEqual([]); +}); + +it("validates regex keys in patternProperties", () => { + expect(() => + assertClientSchemaPatterns({ + patternProperties: { "[": { type: "string" } }, + }), + ).toThrow(SyntaxError); + expect( + assertClientSchemaPatterns({ + patternProperties: { "^name": { pattern: "value$" } }, + }), + ).toEqual(["^name", "value$"]); +}); + +it("visits nested schema positions owned by the schema keyword catalog", () => { + const schema = { + $defs: { named: { pattern: "^named" } }, + properties: { + value: { + allOf: [ + { contains: { pattern: "^contained" } }, + { + if: { propertyNames: { pattern: "^property" } }, + then: { additionalProperties: { pattern: "^additional" } }, + else: { not: { pattern: "^negated" } }, + }, + ], + }, + }, + prefixItems: [{ anyOf: [{ pattern: "^first" }, { pattern: "^second" }] }], + unevaluatedItems: { pattern: "^unevaluated" }, + }; + expect(assertClientSchemaPatterns(schema)).toEqual([ + "^named", + "^contained", + "^property", + "^additional", + "^negated", + "^first", + "^second", + "^unevaluated", + ]); +}); + +it("treats a literal object as a schema only when a local reference selects it", () => { + const schema = { const: { pattern: "[" } }; + expect(assertClientSchemaPatterns(schema)).toEqual([]); + expect(() => + assertClientSchemaPatterns({ ...schema, $ref: "#/const" }), + ).toThrow(SyntaxError); +}); + +it("uses existing reference resolution without loading external or dynamic targets", () => { + expect( + assertClientSchemaPatterns({ + $ref: "https://example.invalid/schema", + const: { pattern: "[" }, + }), + ).toEqual([]); + expect( + assertClientSchemaPatterns({ + $dynamicRef: "#external", + examples: [{ pattern: "[" }], + }), + ).toEqual([]); +}); + +it("keeps native Unicode-mode failures and repeated pattern occurrences visible", () => { + expect(() => assertClientSchemaPatterns({ pattern: "\\a" })).toThrow( + SyntaxError, + ); + expect( + assertClientSchemaPatterns({ + anyOf: [{ pattern: "^same" }, { pattern: "^same" }], + }), + ).toEqual(["^same", "^same"]); +});