537 lines
17 KiB
JavaScript
537 lines
17 KiB
JavaScript
import { pathToFileURL } from "node:url";
|
|
import { join } from "node:path";
|
|
|
|
import { Cli } from "incur";
|
|
import { format } from "prettier";
|
|
import { z } from "zod";
|
|
|
|
const load = (root, path) => import(pathToFileURL(join(root, path)).href);
|
|
const names = (contracts) =>
|
|
contracts
|
|
.map(({ name }) => name)
|
|
.sort((left, right) => left.localeCompare(right));
|
|
|
|
const valuesAt = (node, path) => {
|
|
if (typeof node !== "object" || node === null) return [];
|
|
if ("anyOf" in node && Array.isArray(node.anyOf))
|
|
return node.anyOf.flatMap((alternative) => valuesAt(alternative, path));
|
|
if (path.length === 0)
|
|
return "const" in node &&
|
|
(typeof node.const === "number" || typeof node.const === "string")
|
|
? [node.const]
|
|
: [];
|
|
if (
|
|
!("properties" in node) ||
|
|
typeof node.properties !== "object" ||
|
|
node.properties === null
|
|
)
|
|
return [];
|
|
const [segment, ...remaining] = path;
|
|
return segment !== undefined && segment in node.properties
|
|
? valuesAt(node.properties[segment], remaining)
|
|
: [];
|
|
};
|
|
|
|
const versionAt = (schema, path) => {
|
|
const versions = [
|
|
...new Set(valuesAt(z.toJSONSchema(schema), path).map(String)),
|
|
];
|
|
if (versions.length !== 1)
|
|
throw new Error(
|
|
`Schema version path ${path.join(".")} has ${String(versions.length)} constant values`,
|
|
);
|
|
const version = versions[0];
|
|
if (version === undefined) throw new Error("Schema version disappeared");
|
|
return /^\d+$/u.test(version) ? Number(version) : version;
|
|
};
|
|
|
|
/** Read primary names and aliases from the actual Incur command router. */
|
|
export const createCliInventory = (cli) => {
|
|
const commands = Cli.toCommands.get(cli);
|
|
if (commands === undefined)
|
|
throw new Error("Incur did not expose the registered REA CLI commands");
|
|
const primary = [];
|
|
const aliases = [];
|
|
for (const [name, entry] of commands) {
|
|
if ("_alias" in entry) aliases.push({ name, target: entry.target });
|
|
else primary.push(name);
|
|
}
|
|
return {
|
|
primary: primary.sort((left, right) => left.localeCompare(right)),
|
|
aliases: aliases.sort((left, right) => left.name.localeCompare(right.name)),
|
|
};
|
|
};
|
|
|
|
/** Read one primary command's declared option names from the Incur router. */
|
|
export const cliCommandOptionNames = (cli, name) => {
|
|
const commands = Cli.toCommands.get(cli);
|
|
const command = commands?.get(name);
|
|
if (
|
|
command === undefined ||
|
|
"_alias" in command ||
|
|
!("options" in command) ||
|
|
command.options === undefined
|
|
)
|
|
return [];
|
|
const schema = z.toJSONSchema(command.options);
|
|
return Object.keys(schema.properties ?? {}).sort((left, right) =>
|
|
left.localeCompare(right),
|
|
);
|
|
};
|
|
|
|
/** Report primary CLI fields whose generated JSON Schema lacks help text. */
|
|
export const cliCommandDescriptionIssues = (cli) => {
|
|
const commands = Cli.toCommands.get(cli);
|
|
if (commands === undefined)
|
|
throw new Error("Incur did not expose the registered REA CLI commands");
|
|
const missing = [];
|
|
for (const [commandName, command] of commands) {
|
|
if ("_alias" in command) continue;
|
|
for (const groupName of ["args", "options"]) {
|
|
if (!(groupName in command) || command[groupName] === undefined) continue;
|
|
const properties = z.toJSONSchema(command[groupName]).properties ?? {};
|
|
for (const [propertyName, property] of Object.entries(properties)) {
|
|
if (
|
|
typeof property !== "object" ||
|
|
property === null ||
|
|
typeof property.description !== "string" ||
|
|
property.description.trim() === ""
|
|
)
|
|
missing.push(`${commandName}.${groupName}.${propertyName}`);
|
|
}
|
|
}
|
|
}
|
|
return missing;
|
|
};
|
|
|
|
const assertSameNames = (label, actual, expected) => {
|
|
const actualSet = new Set(actual);
|
|
const expectedSet = new Set(expected);
|
|
const missing = [...expectedSet].filter((name) => !actualSet.has(name));
|
|
const extra = [...actualSet].filter((name) => !expectedSet.has(name));
|
|
if (missing.length === 0 && extra.length === 0) return;
|
|
throw new Error(
|
|
`${label} drifted (missing: ${missing.join(", ") || "none"}; extra: ${extra.join(", ") || "none"})`,
|
|
);
|
|
};
|
|
|
|
const SOURCE_PATHS = {
|
|
packageMetadata: "dist/generatedPackageMetadata.js",
|
|
catalogIdentity: "dist/catalogIdentity.js",
|
|
cli: "dist/cli.js",
|
|
toolContracts: "dist/contracts/toolContracts.js",
|
|
nativeContracts: "dist/contracts/nativeToolContracts.js",
|
|
artifactContracts: "dist/contracts/artifactToolContracts.js",
|
|
managedContracts: "dist/contracts/managedToolContracts.js",
|
|
managedWorkflowContracts: "dist/contracts/managedWorkflowToolContracts.js",
|
|
browserContracts: "dist/contracts/browserToolContracts.js",
|
|
electronContracts: "dist/contracts/electronToolContracts.js",
|
|
applicationContracts: "dist/contracts/applicationToolContracts.js",
|
|
supportedClients: "dist/application/SupportedClients.js",
|
|
hopperProvider: "dist/hopper/HopperProvider.js",
|
|
ghidraProvider: "dist/ghidra/GhidraProvider.js",
|
|
nativeProvider: "dist/native/NativeMacOSProvider.js",
|
|
artifactProviders: "dist/application/InvestigationProviders.js",
|
|
browserProvider: "dist/browser/CdpBrowserProvider.js",
|
|
electronProvider: "dist/browser/CdpElectronProvider.js",
|
|
evidence: "dist/domain/evidence.js",
|
|
evidenceBundle: "dist/domain/evidenceBundle.js",
|
|
processCapture: "dist/domain/processCapture.js",
|
|
analysisSnapshot: "dist/domain/analysisSnapshot.js",
|
|
artifactGraph: "dist/domain/artifactGraph.js",
|
|
investigationWorkspace: "dist/domain/investigationWorkspace.js",
|
|
browserObservation: "dist/domain/browserObservation.js",
|
|
browserSession: "dist/domain/browserSession.js",
|
|
electronObservation: "dist/domain/electronObservation.js",
|
|
webBundleAnalysis: "dist/domain/webBundleAnalysis.js",
|
|
webCaptureDiff: "dist/domain/webCaptureDiff.js",
|
|
managedArtifact: "dist/domain/managedArtifact.js",
|
|
managedComparison: "dist/domain/managedMemberComparison.js",
|
|
managedNativeVerification: "dist/domain/managedNativeVerification.js",
|
|
webMcpDiscovery: "dist/domain/webMcpDiscovery.js",
|
|
webScreenshot: "dist/domain/webScreenshot.js",
|
|
javascriptApplicationGraph: "dist/domain/javascriptApplicationGraph.js",
|
|
javascriptApplicationAnalysis: "dist/domain/javascriptApplicationAnalysis.js",
|
|
javascriptFeatureTrace: "dist/domain/javascriptFeatureTraceSchemas.js",
|
|
javascriptVersionComparison:
|
|
"dist/domain/javascriptApplicationVersionComparisonSchemas.js",
|
|
reconstructionVerification:
|
|
"dist/domain/reconstructionVerificationSchemas.js",
|
|
residualUnknown: "dist/domain/residualUnknown.js",
|
|
};
|
|
|
|
const loadSources = async (root) =>
|
|
Object.fromEntries(
|
|
await Promise.all(
|
|
Object.entries(SOURCE_PATHS).map(async ([key, path]) => [
|
|
key,
|
|
await load(root, path),
|
|
]),
|
|
),
|
|
);
|
|
|
|
const toolFamilyCatalog = (sources) => {
|
|
const families = [
|
|
{
|
|
id: "direct",
|
|
surface: "official-proxy",
|
|
contracts: sources.toolContracts.OFFICIAL_TOOL_CONTRACTS,
|
|
},
|
|
{
|
|
id: "enhanced",
|
|
surface: "enhanced",
|
|
contracts: sources.toolContracts.ENHANCED_TOOL_CONTRACTS,
|
|
},
|
|
{
|
|
id: "native",
|
|
surface: "native-provider",
|
|
contracts: sources.nativeContracts.NATIVE_TOOL_CONTRACTS,
|
|
},
|
|
{
|
|
id: "artifact",
|
|
surface: "artifact-provider",
|
|
contracts: sources.artifactContracts.ARTIFACT_TOOL_CONTRACTS,
|
|
},
|
|
{
|
|
id: "managed",
|
|
surface: "managed",
|
|
contracts: [
|
|
...sources.managedContracts.MANAGED_TOOL_CONTRACTS,
|
|
...sources.managedWorkflowContracts.MANAGED_WORKFLOW_TOOL_CONTRACTS,
|
|
],
|
|
},
|
|
{
|
|
id: "browser",
|
|
surface: "browser-provider",
|
|
contracts: sources.browserContracts.BROWSER_TOOL_CONTRACTS,
|
|
},
|
|
{
|
|
id: "electron",
|
|
surface: "electron-provider",
|
|
contracts: sources.electronContracts.ELECTRON_TOOL_CONTRACTS,
|
|
},
|
|
{
|
|
id: "application",
|
|
surface: "application-workflow",
|
|
contracts: sources.applicationContracts.APPLICATION_TOOL_CONTRACTS,
|
|
},
|
|
{
|
|
id: "session",
|
|
surface: "session",
|
|
contracts: sources.toolContracts.SESSION_TOOL_CONTRACTS,
|
|
},
|
|
].map(({ id, surface, contracts }) => ({
|
|
id,
|
|
surface,
|
|
count: contracts.length,
|
|
tools: names(contracts),
|
|
}));
|
|
const familyTotal = families.reduce(
|
|
(total, family) => total + family.count,
|
|
0,
|
|
);
|
|
if (
|
|
familyTotal !== sources.toolContracts.TOOL_CONTRACTS.length ||
|
|
familyTotal !== sources.catalogIdentity.CATALOG_IDENTITY.counts.mcp_tools
|
|
)
|
|
throw new Error("Tool family inventory does not match TOOL_CONTRACTS");
|
|
return { total: familyTotal, families };
|
|
};
|
|
|
|
const providerCatalog = (sources) => {
|
|
const applicationContracts =
|
|
sources.electronContracts.ELECTRON_TOOL_CONTRACTS.filter(
|
|
({ name }) => name === "analyze_javascript_application",
|
|
);
|
|
const reconciliationContracts =
|
|
sources.electronContracts.ELECTRON_TOOL_CONTRACTS.filter(
|
|
({ name }) => name === "reconcile_javascript_runtime",
|
|
);
|
|
const observationContracts =
|
|
sources.electronContracts.ELECTRON_TOOL_CONTRACTS.filter(
|
|
({ name }) =>
|
|
name !== "analyze_javascript_application" &&
|
|
name !== "reconcile_javascript_runtime",
|
|
);
|
|
if (
|
|
applicationContracts.length !== 1 ||
|
|
reconciliationContracts.length !== 1 ||
|
|
observationContracts.length !== 2
|
|
)
|
|
throw new Error("Electron provider capability ownership drifted");
|
|
return [
|
|
{
|
|
identity: sources.hopperProvider.HOPPER_PROVIDER_IDENTITY,
|
|
contracts: sources.hopperProvider.HOPPER_PROVIDER_TOOL_CONTRACTS,
|
|
},
|
|
{
|
|
identity: sources.ghidraProvider.GHIDRA_PROVIDER_IDENTITY,
|
|
contracts: sources.ghidraProvider.GHIDRA_PROVIDER_TOOL_CONTRACTS,
|
|
},
|
|
{
|
|
identity: sources.nativeProvider.NATIVE_MACOS_PROVIDER_IDENTITY,
|
|
contracts: sources.nativeContracts.NATIVE_TOOL_CONTRACTS,
|
|
},
|
|
{
|
|
identity: sources.artifactProviders.ARTIFACT_GRAPH_PROVIDER,
|
|
contracts: sources.artifactContracts.ARTIFACT_TOOL_CONTRACTS,
|
|
},
|
|
{
|
|
identity: sources.artifactProviders.MANAGED_STATIC_PROVIDER,
|
|
contracts: sources.managedContracts.MANAGED_TOOL_CONTRACTS,
|
|
},
|
|
{
|
|
identity: sources.artifactProviders.MANAGED_WORKFLOW_PROVIDER,
|
|
contracts:
|
|
sources.managedWorkflowContracts.MANAGED_WORKFLOW_TOOL_CONTRACTS,
|
|
},
|
|
{
|
|
identity: sources.browserProvider.CDP_BROWSER_PROVIDER_IDENTITY,
|
|
contracts: sources.browserContracts.BROWSER_TOOL_CONTRACTS,
|
|
},
|
|
{
|
|
identity: sources.electronProvider.CDP_ELECTRON_PROVIDER_IDENTITY,
|
|
contracts: observationContracts,
|
|
},
|
|
{
|
|
identity: sources.artifactProviders.JAVASCRIPT_APPLICATION_PROVIDER,
|
|
contracts: applicationContracts,
|
|
},
|
|
{
|
|
identity:
|
|
sources.artifactProviders.JAVASCRIPT_RUNTIME_RECONCILIATION_PROVIDER,
|
|
contracts: reconciliationContracts,
|
|
},
|
|
{
|
|
identity:
|
|
sources.artifactProviders.JAVASCRIPT_APPLICATION_WORKFLOW_PROVIDER,
|
|
contracts: sources.applicationContracts.APPLICATION_TOOL_CONTRACTS,
|
|
},
|
|
]
|
|
.map(({ identity, contracts }) => ({
|
|
id: identity.id,
|
|
name: identity.name,
|
|
version: identity.version,
|
|
capabilities: names(contracts),
|
|
}))
|
|
.sort((left, right) => left.id.localeCompare(right.id));
|
|
};
|
|
|
|
const durableSchemaDefinitions = (sources) => [
|
|
[
|
|
"analysis_snapshot",
|
|
sources.analysisSnapshot.analysisSnapshotSchema,
|
|
["snapshot_version"],
|
|
],
|
|
[
|
|
"artifact_graph",
|
|
sources.artifactGraph.artifactInventoryResultSchema,
|
|
["manifest", "schema_version"],
|
|
],
|
|
[
|
|
"artifact_extraction",
|
|
sources.artifactGraph.artifactExtractionResultSchema,
|
|
["extraction_manifest", "schema_version"],
|
|
],
|
|
["evidence", sources.evidence.evidenceSchema, ["schema_version"]],
|
|
[
|
|
"evidence_bundle",
|
|
sources.evidenceBundle.evidenceBundleSchema,
|
|
["bundle_version"],
|
|
],
|
|
[
|
|
"investigation_run",
|
|
sources.investigationWorkspace.investigationRunSchema,
|
|
["schema_version"],
|
|
],
|
|
[
|
|
"investigation_run_summary",
|
|
sources.investigationWorkspace.investigationRunSummarySchema,
|
|
["schema_version"],
|
|
],
|
|
[
|
|
"investigation_workspace",
|
|
sources.investigationWorkspace.investigationWorkspaceSchema,
|
|
["workspace_version"],
|
|
],
|
|
[
|
|
"javascript_application_graph",
|
|
sources.javascriptApplicationGraph.javascriptApplicationGraphSchema,
|
|
["schema_version"],
|
|
],
|
|
[
|
|
"javascript_application_version_comparison",
|
|
sources.javascriptVersionComparison
|
|
.applicationVersionComparisonResultSchema,
|
|
["schema_version"],
|
|
],
|
|
[
|
|
"javascript_feature_trace",
|
|
sources.javascriptFeatureTrace.applicationFeatureTraceResultSchema,
|
|
["schema_version"],
|
|
],
|
|
[
|
|
"process_capture",
|
|
sources.processCapture.processCaptureSchema,
|
|
["schema_version"],
|
|
],
|
|
[
|
|
"reconstruction_verification",
|
|
sources.reconstructionVerification.reconstructionSpecificationSchema,
|
|
["schema_version"],
|
|
],
|
|
[
|
|
"residual_unknown",
|
|
sources.residualUnknown.residualUnknownSchema,
|
|
["registry_version"],
|
|
],
|
|
];
|
|
|
|
const observationSchemaDefinitions = (sources) => [
|
|
[
|
|
"managed_artifact_inspection",
|
|
sources.managedArtifact.managedArtifactInspectionSchema,
|
|
["schema_version"],
|
|
],
|
|
[
|
|
"managed_member_inspection",
|
|
sources.managedArtifact.managedMemberInspectionSchema,
|
|
["schema_version"],
|
|
],
|
|
[
|
|
"managed_native_boundary_inspection",
|
|
sources.managedArtifact.managedNativeBoundaryInspectionSchema,
|
|
["schema_version"],
|
|
],
|
|
[
|
|
"managed_member_comparison",
|
|
sources.managedComparison.managedMemberComparisonResultSchema,
|
|
["schema_version"],
|
|
],
|
|
[
|
|
"managed_native_verification",
|
|
sources.managedNativeVerification.managedNativeVerificationResultSchema,
|
|
["schema_version"],
|
|
],
|
|
[
|
|
"browser_target_list",
|
|
sources.browserObservation.browserTargetListSchema,
|
|
["schema_version"],
|
|
],
|
|
[
|
|
"electron_page_inspection",
|
|
sources.electronObservation.electronPageInspectionSchema,
|
|
["schema_version"],
|
|
],
|
|
[
|
|
"electron_target_list",
|
|
sources.electronObservation.electronTargetListSchema,
|
|
["schema_version"],
|
|
],
|
|
[
|
|
"javascript_application_analysis",
|
|
sources.javascriptApplicationAnalysis
|
|
.javascriptApplicationAnalysisResultSchema,
|
|
["schema_version"],
|
|
],
|
|
[
|
|
"web_bundle_analysis",
|
|
sources.webBundleAnalysis.webBundleAnalysisSchema,
|
|
["schema_version"],
|
|
],
|
|
[
|
|
"web_capture_diff",
|
|
sources.webCaptureDiff.webCaptureDiffSchema,
|
|
["schema_version"],
|
|
],
|
|
[
|
|
"web_mcp_discovery",
|
|
sources.webMcpDiscovery.webMcpDiscoverySchema,
|
|
["schema_version"],
|
|
],
|
|
[
|
|
"web_observation_session",
|
|
sources.browserSession.webObservationSessionSchema,
|
|
["schema_version"],
|
|
],
|
|
[
|
|
"web_page_inspection",
|
|
sources.browserObservation.webPageInspectionSchema,
|
|
["schema_version"],
|
|
],
|
|
[
|
|
"web_screenshot",
|
|
sources.webScreenshot.webScreenshotSchema,
|
|
["schema_version"],
|
|
],
|
|
[
|
|
"web_screenshot_diff",
|
|
sources.webScreenshot.webScreenshotDiffSchema,
|
|
["schema_version"],
|
|
],
|
|
];
|
|
|
|
const schemaCatalog = (sources) =>
|
|
[
|
|
...durableSchemaDefinitions(sources),
|
|
...observationSchemaDefinitions(sources),
|
|
]
|
|
.map(([id, schema, path]) => {
|
|
try {
|
|
return { id, version: versionAt(schema, path) };
|
|
} catch (cause) {
|
|
throw new Error(`Could not derive ${id} schema version`, { cause });
|
|
}
|
|
})
|
|
.sort((left, right) => left.id.localeCompare(right.id));
|
|
|
|
/** Project current runtime contracts into deterministic, machine-readable facts. */
|
|
export const createProductCatalog = async (root) => {
|
|
const sources = await loadSources(root);
|
|
const cli = createCliInventory(sources.cli.createCli());
|
|
assertSameNames(
|
|
"Primary CLI inventory",
|
|
cli.primary,
|
|
sources.catalogIdentity.CLI_COMMAND_NAMES,
|
|
);
|
|
const tools = toolFamilyCatalog(sources);
|
|
const metadata = sources.packageMetadata.PACKAGE_METADATA;
|
|
return {
|
|
catalog_schema_version: 1,
|
|
package: {
|
|
name: metadata.name,
|
|
version: metadata.version,
|
|
sdk: {
|
|
server: metadata.serverSdkVersion,
|
|
client: metadata.clientSdkVersion,
|
|
core: metadata.coreSdkVersion,
|
|
},
|
|
skill_version: metadata.skillVersion,
|
|
},
|
|
tools,
|
|
providers: providerCatalog(sources),
|
|
setup_clients: sources.supportedClients.SUPPORTED_CLIENT_DEFINITIONS.map(
|
|
({ name, displayName, format }) => ({
|
|
id: name,
|
|
display_name: displayName,
|
|
format,
|
|
configuration: format === "unsupported" ? "detect-only" : "managed",
|
|
}),
|
|
),
|
|
schemas: schemaCatalog(sources),
|
|
cli: {
|
|
primary_count: cli.primary.length,
|
|
commands: cli.primary,
|
|
aliases: cli.aliases,
|
|
},
|
|
runtime_catalog: {
|
|
counts: sources.catalogIdentity.CATALOG_IDENTITY.counts,
|
|
digests: sources.catalogIdentity.CATALOG_IDENTITY.digests,
|
|
},
|
|
};
|
|
};
|
|
|
|
/** Stable checked-in representation of the product catalog. */
|
|
export const serializeProductCatalog = (catalog) =>
|
|
format(JSON.stringify(catalog, null, 2), { parser: "json" });
|