499 lines
13 KiB
TypeScript
499 lines
13 KiB
TypeScript
import { z } from "zod";
|
|
|
|
import {
|
|
procedureClassificationSchema,
|
|
procedureIdentitySchema,
|
|
localVariableSchema,
|
|
functionDossierSchema,
|
|
functionBodySchema,
|
|
} from "../domain/hopperValues.js";
|
|
import { analysisProfileSchema } from "../domain/analysisProfile.js";
|
|
import { evidenceEnvelopeSchema } from "../domain/evidence.js";
|
|
import { jsonValueSchema } from "../domain/jsonValue.js";
|
|
import {
|
|
PROVIDER_REJECTION_CODES,
|
|
type ProviderRejectionCode,
|
|
} from "./providerSelection.js";
|
|
import { analysisErrorProjectionSchema } from "./errorSchemas.js";
|
|
import { prefixedDigestSchema } from "./../domain/digests.js";
|
|
|
|
/** Inline result with its complete Evidence record. */
|
|
export const inlineEvidenceRecordSchema = evidenceEnvelopeSchema;
|
|
|
|
export const evidenceResultOf = (schema: z.ZodType) =>
|
|
z.strictObject({
|
|
result: schema,
|
|
evidence_id: prefixedDigestSchema("ev"),
|
|
evidence: inlineEvidenceRecordSchema,
|
|
});
|
|
|
|
const resultOf = evidenceResultOf;
|
|
export const lifecycleResultOf = (schema: z.ZodType) =>
|
|
z.object({ result: schema });
|
|
|
|
/** Resolve a required named output schema or reject contract drift. */
|
|
export const requireOutputSchema = (
|
|
schemas: Readonly<Record<string, z.ZodObject>>,
|
|
name: string,
|
|
): z.ZodObject => {
|
|
const schema = schemas[name];
|
|
if (schema === undefined)
|
|
throw new Error(`Missing output schema for ${name}`);
|
|
return schema;
|
|
};
|
|
|
|
export const targetFormatSchema = z.enum([
|
|
"hopper",
|
|
"analysis-database",
|
|
"mach-o",
|
|
"elf",
|
|
"pe",
|
|
"dos-mz",
|
|
"dos-com",
|
|
"zip",
|
|
"ipa",
|
|
"apk",
|
|
"msix",
|
|
"appx",
|
|
"asar",
|
|
"dmg",
|
|
"pkg",
|
|
"plist",
|
|
"javascript",
|
|
"source-map",
|
|
]);
|
|
|
|
export const targetKindSchema = z.enum([
|
|
"executable",
|
|
"database",
|
|
"archive",
|
|
"artifact",
|
|
]);
|
|
|
|
const providerCapabilityFacts = {
|
|
operation: z.string(),
|
|
effects: z.object({
|
|
mutates_artifact: z.boolean(),
|
|
launches_process: z.boolean(),
|
|
may_show_ui: z.boolean(),
|
|
may_access_network: z.boolean(),
|
|
may_write_filesystem: z.boolean(),
|
|
changes_permissions: z.boolean(),
|
|
requires_root: z.boolean(),
|
|
}),
|
|
limitations: z.array(z.string()),
|
|
};
|
|
|
|
export const providerCapability = z.discriminatedUnion("available", [
|
|
z.object({
|
|
...providerCapabilityFacts,
|
|
available: z.literal(true),
|
|
reason: z.null(),
|
|
availability_code: z.null(),
|
|
}),
|
|
z.object({
|
|
...providerCapabilityFacts,
|
|
available: z.literal(false),
|
|
reason: z.string().nullable(),
|
|
availability_code: z.enum(PROVIDER_REJECTION_CODES).nullable(),
|
|
}),
|
|
]);
|
|
export type ProviderCapability = z.output<typeof providerCapability>;
|
|
|
|
export const providerIdentity = z.object({
|
|
id: z.string(),
|
|
name: z.string(),
|
|
version: z.string().nullable(),
|
|
});
|
|
|
|
const processCoordinates = {
|
|
launcher_pid: z.number().int().min(1),
|
|
process_group_id: z.number().int().min(1),
|
|
};
|
|
|
|
const processLineageObservation = z.discriminatedUnion("status", [
|
|
z.object({
|
|
status: z.literal("unavailable"),
|
|
observed_at: z.iso.datetime(),
|
|
...processCoordinates,
|
|
reason: z.string().min(1),
|
|
}),
|
|
z.object({
|
|
status: z.literal("verified"),
|
|
observed_at: z.iso.datetime(),
|
|
...processCoordinates,
|
|
launcher_parent_pid: z.number().int().min(0),
|
|
descendants: z.array(
|
|
z.object({
|
|
pid: z.number().int().min(1),
|
|
parent_pid: z.number().int().min(0),
|
|
process_group_id: z.number().int().min(1),
|
|
}),
|
|
),
|
|
}),
|
|
]);
|
|
|
|
const analysisRun = z
|
|
.object({
|
|
run_id: z.string().uuid(),
|
|
process_lineage: z.discriminatedUnion("status", [
|
|
z.object({ status: z.literal("not_observed") }),
|
|
z.object({
|
|
status: z.literal("snapshots"),
|
|
snapshots: z.array(
|
|
z.object({
|
|
provider: providerIdentity,
|
|
observation: processLineageObservation,
|
|
}),
|
|
),
|
|
}),
|
|
]),
|
|
})
|
|
.nullable();
|
|
|
|
export const analysisActivity = z.object({
|
|
status: z.enum(["not_observed", "idle", "busy"]),
|
|
providers: z.array(
|
|
z.object({
|
|
provider: providerIdentity,
|
|
active: z
|
|
.object({
|
|
request_id: z.number().int().min(1),
|
|
operation: z.string().min(1),
|
|
elapsed_ms: z.number().int().min(0),
|
|
caller_state: z.enum(["waiting", "cancelled"]),
|
|
})
|
|
.nullable(),
|
|
queued_requests: z.number().int().min(0),
|
|
}),
|
|
),
|
|
});
|
|
|
|
const providerRejectionCode: z.ZodType<ProviderRejectionCode> = z.enum(
|
|
PROVIDER_REJECTION_CODES,
|
|
);
|
|
|
|
const providerDiagnostics = z.record(z.string(), jsonValueSchema);
|
|
|
|
const providerAvailability = z.discriminatedUnion("status", [
|
|
z.object({
|
|
status: z.literal("available"),
|
|
code: z.null(),
|
|
reason: z.null(),
|
|
diagnostics: providerDiagnostics,
|
|
}),
|
|
z.object({
|
|
status: z.literal("unavailable"),
|
|
code: providerRejectionCode,
|
|
reason: z.string().min(1),
|
|
diagnostics: providerDiagnostics,
|
|
}),
|
|
]);
|
|
|
|
const providerTargetSupport = z.discriminatedUnion("status", [
|
|
z.object({
|
|
status: z.literal("unknown"),
|
|
code: z.null(),
|
|
reason: z.string().min(1),
|
|
diagnostics: providerDiagnostics,
|
|
}),
|
|
z.object({
|
|
status: z.literal("supported"),
|
|
code: z.null(),
|
|
reason: z.null(),
|
|
diagnostics: providerDiagnostics,
|
|
}),
|
|
z.object({
|
|
status: z.literal("unsupported"),
|
|
code: z.enum([
|
|
"target_kind_unsupported",
|
|
"target_format_unsupported",
|
|
"architecture_unsupported",
|
|
"target_role_unsupported",
|
|
"managed_target_unsupported",
|
|
]),
|
|
reason: z.string().min(1),
|
|
diagnostics: providerDiagnostics,
|
|
}),
|
|
]);
|
|
|
|
const clientFeatureNameSchema = z.enum([
|
|
"elicitation_form",
|
|
"elicitation_url",
|
|
"roots",
|
|
"sampling",
|
|
]);
|
|
|
|
/** Negotiated MCP client features that affect REA workflow availability. */
|
|
export const clientFeatureAvailabilitySchema = z.object({
|
|
elicitation_form: z.boolean(),
|
|
elicitation_url: z.boolean(),
|
|
roots: z.boolean(),
|
|
sampling: z.boolean(),
|
|
});
|
|
export type ClientFeatureAvailability = z.infer<
|
|
typeof clientFeatureAvailabilitySchema
|
|
>;
|
|
|
|
const toolAvailabilityMode = z.discriminatedUnion("available", [
|
|
z.object({
|
|
name: z.string(),
|
|
available: z.literal(true),
|
|
missing_operations: z.array(z.string()).length(0),
|
|
remediation: z.null(),
|
|
}),
|
|
z.object({
|
|
name: z.string(),
|
|
available: z.literal(false),
|
|
missing_operations: z.array(z.string()).min(1),
|
|
remediation: z.string().nullable(),
|
|
}),
|
|
]);
|
|
export type ToolAvailabilityMode = z.output<typeof toolAvailabilityMode>;
|
|
|
|
const toolAvailabilityFacts = {
|
|
name: z.string(),
|
|
surface: z.string(),
|
|
default_mode_available: z.boolean().optional(),
|
|
modes: z.array(toolAvailabilityMode).optional(),
|
|
client_requirements: z.object({
|
|
required: z.array(clientFeatureNameSchema),
|
|
optional: z.array(clientFeatureNameSchema),
|
|
missing_required: z.array(clientFeatureNameSchema),
|
|
missing_optional: z.array(clientFeatureNameSchema),
|
|
}),
|
|
effects: z.strictObject({
|
|
mutatesTarget: z.boolean(),
|
|
mutatesSession: z.boolean(),
|
|
writesFilesystem: z.boolean(),
|
|
launchesProcess: z.boolean(),
|
|
accessesNetwork: z.boolean(),
|
|
changesUiState: z.boolean(),
|
|
mayDiscardData: z.boolean(),
|
|
idempotent: z.boolean(),
|
|
}),
|
|
annotations: z.object({
|
|
read_only: z.boolean(),
|
|
destructive: z.boolean(),
|
|
idempotent: z.boolean(),
|
|
open_world: z.boolean(),
|
|
}),
|
|
};
|
|
|
|
export const toolUnavailabilityReason = z.enum([
|
|
"client_capability_missing",
|
|
"target_required",
|
|
"provider_missing",
|
|
"provider_unavailable",
|
|
"target_unsupported",
|
|
"unsupported_host",
|
|
]);
|
|
|
|
export const toolAvailability = z.discriminatedUnion("available", [
|
|
z.object({
|
|
...toolAvailabilityFacts,
|
|
available: z.literal(true),
|
|
reason: z.literal("available"),
|
|
remediation: z.null(),
|
|
}),
|
|
z.object({
|
|
...toolAvailabilityFacts,
|
|
available: z.literal(false),
|
|
reason: toolUnavailabilityReason,
|
|
remediation: z.string().nullable(),
|
|
}),
|
|
]);
|
|
export type ToolAvailability = z.output<typeof toolAvailability>;
|
|
export type ToolUnavailabilityReason = z.output<
|
|
typeof toolUnavailabilityReason
|
|
>;
|
|
|
|
export const sessionProvider = z
|
|
.object({
|
|
provider: providerIdentity,
|
|
providers: z.array(providerIdentity),
|
|
capabilities: z.array(providerCapability),
|
|
analysis_run: analysisRun,
|
|
analysis_activity: analysisActivity,
|
|
analysis_provider_binding: z
|
|
.object({
|
|
provider: providerIdentity,
|
|
selection_source: z.enum([
|
|
"request",
|
|
"environment",
|
|
"auto-single-candidate",
|
|
]),
|
|
analysis_profile: analysisProfileSchema,
|
|
})
|
|
.nullable(),
|
|
analysis_provider_candidates: z.array(
|
|
z.object({
|
|
provider: providerIdentity,
|
|
availability: providerAvailability,
|
|
target_support: providerTargetSupport,
|
|
selected: z.boolean(),
|
|
capabilities: z.array(providerCapability),
|
|
}),
|
|
),
|
|
tool_availability: z.array(toolAvailability),
|
|
client_features: z.object({
|
|
elicitation_form: z.boolean(),
|
|
elicitation_url: z.boolean(),
|
|
roots: z.boolean(),
|
|
sampling: z.boolean(),
|
|
}),
|
|
server_identity: z.object({
|
|
package: z.object({
|
|
name: z.string(),
|
|
version: z.string(),
|
|
root_path: z.string(),
|
|
build_commit: z.string().nullable(),
|
|
}),
|
|
server: z.object({
|
|
name: z.string(),
|
|
version: z.string(),
|
|
started_at: z.string(),
|
|
command_path: z.string(),
|
|
}),
|
|
sdk: z.object({
|
|
server: z.string(),
|
|
client_test: z.string(),
|
|
core: z.string(),
|
|
}),
|
|
negotiated_protocol_version: z.string().nullable(),
|
|
client: z.object({ name: z.string(), version: z.string() }).nullable(),
|
|
skill: z.object({ name: z.string(), expected_version: z.string() }),
|
|
catalog: z.record(z.string(), z.json()),
|
|
protocol_features: z.object({
|
|
progress: z.boolean(),
|
|
cancellation: z.boolean(),
|
|
elicitation: z.boolean(),
|
|
}),
|
|
alignment: z.object({
|
|
state: z.enum(["aligned", "mcp_server_restart_required", "unknown"]),
|
|
reasons: z.array(z.string()),
|
|
remediation: z.string().nullable(),
|
|
}),
|
|
}),
|
|
})
|
|
.passthrough();
|
|
|
|
export const nullableText = z.string().nullable();
|
|
export const addressList = z.array(z.string());
|
|
export const addressedEntry = z.object({
|
|
address: z.string(),
|
|
name: z.string(),
|
|
});
|
|
export const procedureIdentity = procedureIdentitySchema;
|
|
const localVariable = localVariableSchema;
|
|
|
|
export const containingProcedureResolution = z.discriminatedUnion("found", [
|
|
z.object({
|
|
query_address: z.string(),
|
|
found: z.literal(true),
|
|
procedure: procedureIdentity,
|
|
}),
|
|
z.object({
|
|
query_address: z.string(),
|
|
found: z.literal(false),
|
|
procedure: z.null(),
|
|
reason: z.enum(["outside_segments", "not_in_procedure"]),
|
|
}),
|
|
]);
|
|
|
|
const unavailable = z.object({
|
|
available: z.literal(false),
|
|
reason: z.string(),
|
|
});
|
|
|
|
const availableMemoryPermissions = z.object({
|
|
available: z.literal(true),
|
|
source: z.literal("ghidra-memory-block"),
|
|
});
|
|
|
|
export const addressedValue = z.object({
|
|
address: z.string(),
|
|
value: z.string(),
|
|
symbol: z
|
|
.object({
|
|
primary: z.boolean(),
|
|
dynamic: z.boolean(),
|
|
external: z.boolean(),
|
|
type: z.string(),
|
|
source: z.enum(["default", "analysis", "ai", "imported", "user_defined"]),
|
|
})
|
|
.optional(),
|
|
procedure: z
|
|
.object({
|
|
external: z.boolean(),
|
|
thunk: z.boolean(),
|
|
thunk_target: z.string().nullable(),
|
|
})
|
|
.optional(),
|
|
string: z
|
|
.object({
|
|
encoding: z.string(),
|
|
termination: z.enum(["missing", "present_or_not_required"]),
|
|
byte_length: z.number().int().min(0),
|
|
})
|
|
.optional(),
|
|
});
|
|
|
|
const memoryRegionOutput = z.object({
|
|
name: z.string(),
|
|
start: z.string(),
|
|
end: z.string(),
|
|
readable: z.boolean().nullable(),
|
|
writable: z.boolean().nullable(),
|
|
executable: z.boolean().nullable(),
|
|
permissions: z.union([unavailable, availableMemoryPermissions]),
|
|
provenance: z.enum(["hopper-public-python-api", "ghidra-memory-block"]),
|
|
address_space: z.string().optional(),
|
|
image_base: z.string().optional(),
|
|
initialized: z.boolean().optional(),
|
|
overlay: z.boolean().optional(),
|
|
});
|
|
|
|
export const segmentOutput = resultOf(
|
|
z.array(memoryRegionOutput.extend({ sections: z.array(memoryRegionOutput) })),
|
|
);
|
|
|
|
export const procedureInfoOutput = resultOf(
|
|
z.object({
|
|
name: z.string(),
|
|
entrypoint: z.string(),
|
|
basicblock_count: z.number().int().min(0),
|
|
length: z.number().min(0),
|
|
signature: nullableText,
|
|
locals: z.array(localVariable),
|
|
classification: procedureClassificationSchema.nullable().default(null),
|
|
body: functionBodySchema.default({
|
|
available: false,
|
|
reason: "The provider did not report complete function body ranges.",
|
|
}),
|
|
}),
|
|
);
|
|
|
|
export const symbolDiscoveryOutput = (property: "classes" | "protocols") =>
|
|
resultOf(
|
|
z.object({
|
|
count: z.number().int().min(0),
|
|
[property]: z.array(addressedEntry),
|
|
}),
|
|
);
|
|
|
|
export const graphNode = z.discriminatedUnion("status", [
|
|
z.object({
|
|
address: z.string(),
|
|
status: z.literal("ok"),
|
|
calls: z.array(z.string()),
|
|
}),
|
|
z.object({
|
|
address: z.string(),
|
|
status: z.literal("error"),
|
|
error: analysisErrorProjectionSchema,
|
|
}),
|
|
]);
|
|
|
|
export const functionDossierOutput = resultOf(functionDossierSchema);
|