mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
refactor: centralize input validation and schema interpretation
Migrate twelve application service files to the existing provider input owner, preserving schema causes, cancellation precedence, caller selection, and retained Evidence. Keep flows with distinct error policies separate. Replace the DeepSeek verifier's recursive object scan with schema-position traversal using the canonical keyword and local-reference owners. Literal pattern fields no longer cause false regex failures; patternProperties keys are checked alongside actual pattern constraints in native regex modes. Validation: 271 focused workflow/service tests and six verifier regressions passed; static checks, discovery, and actual Node source-loader smoke passed. The baseline scanner reproduced the new regressions. Installed DeepSeek client execution was not run.
This commit is contained in:
1 parent
8a65787507
commit
6c35d74a7d
15 files changed
+324
-240
No files matched your search
@@ -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<Record<string, unknown>> =>
|
||||
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<object>();
|
||||
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;
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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<Result<Evidence, AnalysisError>> => {
|
||||
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);
|
||||
};
|
||||
@@ -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<Result<Evidence, AnalysisError>> {
|
||||
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,
|
||||
),
|
||||
|
||||
@@ -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<Result<Evidence, AnalysisError>> {
|
||||
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,
|
||||
),
|
||||
|
||||
@@ -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<Result<Evidence, AnalysisError>> {
|
||||
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,
|
||||
|
||||
@@ -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<Result<Evidence, AnalysisError>> => {
|
||||
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);
|
||||
|
||||
@@ -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<Evidence, AnalysisError> {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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<Result<Evidence, AnalysisError>> {
|
||||
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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<ResolvedManagedReconstructionImport, AnalysisError> => {
|
||||
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. */
|
||||
|
||||
@@ -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<Result<Evidence, AnalysisError>> {
|
||||
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",
|
||||
|
||||
@@ -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<Result<Evidence, AnalysisError>> {
|
||||
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
|
||||
? {}
|
||||
|
||||
@@ -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<Result<Evidence, AnalysisError>> {
|
||||
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",
|
||||
),
|
||||
|
||||
@@ -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"]);
|
||||
});
|
||||
Reference in new issue
Block a user