refactor(mcp): make analysis states unrepresentable (#468)
* refactor(config): parse capability settings into exact states * refactor(runtime): encode replay and process outcomes exactly * refactor: make analysis states unrepresentable * refactor(mcp): register canonical schemas with SDK * fix: preserve capability and catalog invariants * fix(build): remove stale compiled modules * fix(replay): update real verifier policy dependency * fix(package): verify the complete MCP catalog * fix(test): supply complete replay reload policy * refactor(contracts): centralize output schema lookup * refactor(mcp): delegate protocol boundaries to SDK * fix(browser): approve real source capture * fix(mcp): preserve catalog guidance
This commit is contained in:
parent
6901edc05d
commit
b8a9da0c0f
|
|
@ -680,10 +680,10 @@
|
|||
"mcp_resource_templates": 11
|
||||
},
|
||||
"digests": {
|
||||
"tools_sha256": "8021786c901ca9225ba8e2477f220c8c15d61b8df5618b221f61417cae8c8a36",
|
||||
"tools_sha256": "b0e8b9c894ef347b6680a3a5bfdb4cba5c10e9129eeaa43a5ea22ce4d398d417",
|
||||
"prompts_sha256": "3886a3bf55a448def79e3eb429e6a56eb2ba075423c5cb1e5747642b389ef8fe",
|
||||
"resources_sha256": "e0e872276ebf6f88e916ef24930a99eed27ce4c93e494e60820e9bd8cc8497c3",
|
||||
"combined_sha256": "2da444fd95a40c28a6c0332a5970edca6b984f0c2922e4c02eaf7458a6f3dd0e",
|
||||
"combined_sha256": "bab5657d9525ec4f798d0a9ef6001412d9c11f77fb0ddc925c7076fd10921dc9",
|
||||
"providers_sha256": "7446b51788734ebf6f4f702e3a5a8c9f0b61967490c245f30bd37b2060827e6b"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"schema_version": 1,
|
||||
"manifest_id": "ecm_aba877529714e59795e8a7b51c5513b17081bd701cea1ffee31422a1d95dfa31",
|
||||
"manifest_id": "ecm_c44391ed8e365e21871664422aa38fa6c12598006cde731d65a9d9e271b0fa5f",
|
||||
"verifier": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
"skill_digests": [
|
||||
{
|
||||
"skill_id": "reverse-engineer-anything",
|
||||
"sha256": "43fccde70ab600dfc86785a37b414865cb8c0f2cb479eb4ed6c2a90998e0f6a6"
|
||||
"sha256": "97a5bc5d29f162fe171ddbb8f4f5ba9fcba34ad336d4cdcae0c564223aa4cfe8"
|
||||
}
|
||||
],
|
||||
"claims": [
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@
|
|||
"mcp-registry:check": "node scripts/verify-mcp-registry-metadata.mjs",
|
||||
"mcp-catalog:generate": "npm run build:cached && node scripts/generate-mcp-tool-catalog.mjs",
|
||||
"mcp-catalog:check": "node scripts/generate-mcp-tool-catalog.mjs --check",
|
||||
"prebuild": "npm run deps:check && npm run metadata:check",
|
||||
"prebuild": "npm run deps:check && npm run metadata:check && node scripts/clean-build-output.mjs",
|
||||
"build": "tsc -p tsconfig.build.json",
|
||||
"precheck": "npm run deps:check",
|
||||
"check": "turbo run typecheck lint format:check knip metadata:check",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
import { rm } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
|
||||
await Promise.all([
|
||||
rm(join(root, "dist"), { recursive: true, force: true }),
|
||||
rm(join(root, ".cache", "tsconfig.build.tsbuildinfo"), { force: true }),
|
||||
]);
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { format } from "prettier";
|
||||
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
||||
import { McpServer } from "@modelcontextprotocol/server";
|
||||
|
||||
import { TOOL_CONTRACTS } from "../dist/contracts/toolContracts.js";
|
||||
import { MANAGED_WORKFLOW_TOOL_CONTRACTS } from "../dist/contracts/managedWorkflowToolContracts.js";
|
||||
|
|
@ -8,7 +10,6 @@ import { ArtifactProvider } from "../dist/artifacts/ArtifactProvider.js";
|
|||
import { ManagedStaticProvider } from "../dist/dotnet/ManagedStaticProvider.js";
|
||||
import { NativeMacOSProvider } from "../dist/native/NativeMacOSProvider.js";
|
||||
import { toolRegistrationOptions } from "../dist/server/toolRegistrationOptions.js";
|
||||
import { applyToolInputJsonSchemaOverride } from "../dist/contracts/toolSchemaJsonOverrides.js";
|
||||
import { ensureGeneratedFile } from "./lib/generated-file.mjs";
|
||||
|
||||
const arguments_ = new Set(process.argv.slice(2));
|
||||
|
|
@ -26,8 +27,11 @@ const sessionToolNames = new Set([
|
|||
),
|
||||
...MANAGED_WORKFLOW_TOOL_CONTRACTS.map(({ name }) => name),
|
||||
]);
|
||||
const advertisedTools = await sdkToolCatalog();
|
||||
const catalog = TOOL_CONTRACTS.map((contract) => {
|
||||
const options = toolRegistrationOptions(contract);
|
||||
const advertised = advertisedTools.get(contract.name);
|
||||
if (advertised === undefined)
|
||||
throw new Error(`MCP SDK omitted registered tool ${contract.name}`);
|
||||
return {
|
||||
name: contract.name,
|
||||
analysisOperation: [
|
||||
|
|
@ -39,23 +43,40 @@ const catalog = TOOL_CONTRACTS.map((contract) => {
|
|||
].includes(contract.kind)
|
||||
? contract.name
|
||||
: null,
|
||||
title: options.title,
|
||||
description: options.description,
|
||||
title: advertised.title,
|
||||
description: advertised.description,
|
||||
kind: contract.kind,
|
||||
requiresSession: sessionToolNames.has(contract.name),
|
||||
inputSchema: applyToolInputJsonSchemaOverride(
|
||||
contract.name,
|
||||
options.inputSchema["~standard"].jsonSchema.input({
|
||||
target: "draft-2020-12",
|
||||
}),
|
||||
),
|
||||
outputSchema: options.outputSchema["~standard"].jsonSchema.output({
|
||||
target: "draft-2020-12",
|
||||
}),
|
||||
annotations: options.annotations,
|
||||
inputSchema: advertised.inputSchema,
|
||||
outputSchema: advertised.outputSchema,
|
||||
annotations: advertised.annotations,
|
||||
effects: contract.effects,
|
||||
};
|
||||
});
|
||||
|
||||
async function sdkToolCatalog() {
|
||||
const server = new McpServer({ name: "rea-catalog-generator", version: "0" });
|
||||
for (const contract of TOOL_CONTRACTS)
|
||||
server.registerTool(
|
||||
contract.name,
|
||||
toolRegistrationOptions(contract),
|
||||
async () => ({
|
||||
content: [{ type: "text", text: "Catalog-only handler" }],
|
||||
isError: true,
|
||||
}),
|
||||
);
|
||||
const client = new Client({ name: "rea-catalog-generator", version: "0" });
|
||||
const [clientTransport, serverTransport] =
|
||||
InMemoryTransport.createLinkedPair();
|
||||
try {
|
||||
await server.connect(serverTransport);
|
||||
await client.connect(clientTransport);
|
||||
const { tools } = await client.listTools();
|
||||
return new Map(tools.map((tool) => [tool.name, tool]));
|
||||
} finally {
|
||||
await Promise.allSettled([client.close(), server.close()]);
|
||||
}
|
||||
}
|
||||
const auxiliaryProviders = [
|
||||
new ArtifactProvider(false, false),
|
||||
new NativeMacOSProvider(undefined, CATALOG_PLATFORM),
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@ export const runWithStatus = async (command, args, env) => {
|
|||
/** Parse JSON from a command output string. */
|
||||
export const json = (text) => JSON.parse(text);
|
||||
|
||||
/** Verify that the advertised MCP catalog matches the session's available tools. */
|
||||
export const verifyAvailableToolCatalog = async (client, options) => {
|
||||
/** Verify that the complete advertised MCP catalog matches session metadata. */
|
||||
export const verifyCompleteToolCatalog = async (client, options) => {
|
||||
const listed = await client.listTools(undefined, options);
|
||||
const status = await client.callTool(
|
||||
{ name: "binary_session", arguments: { detail: "full" } },
|
||||
|
|
@ -38,14 +38,11 @@ export const verifyAvailableToolCatalog = async (client, options) => {
|
|||
const availability = status.structuredContent?.result?.tool_availability;
|
||||
if (!Array.isArray(availability))
|
||||
throw new Error("packaged MCP omitted tool availability");
|
||||
const expected = availability
|
||||
.filter(({ available }) => available === true)
|
||||
.map(({ name }) => name)
|
||||
.sort();
|
||||
const expected = availability.map(({ name }) => name).sort();
|
||||
const observed = listed.tools.map(({ name }) => name).sort();
|
||||
if (JSON.stringify(observed) !== JSON.stringify(expected))
|
||||
throw new Error(
|
||||
"packaged MCP tool inventory diverged from session availability",
|
||||
"packaged MCP tool inventory diverged from session metadata",
|
||||
);
|
||||
return observed;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -344,11 +344,11 @@ try {
|
|||
assert.equal(reconstructionImport.value.confidence, "inferred");
|
||||
const runtimePlan = await planManagedRuntimeCorrelationEvidence(
|
||||
{
|
||||
policy: {
|
||||
enabled: true,
|
||||
policy: () => ({
|
||||
status: "enabled",
|
||||
roots: [workspace],
|
||||
executablePath: runtimeExecutable,
|
||||
},
|
||||
}),
|
||||
authority: runtimeAuthority.value,
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -7,15 +7,12 @@ import { Client } from "@modelcontextprotocol/client";
|
|||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { MCP_STARTUP_POLICY } from "../dist/mcpStartupPolicy.js";
|
||||
import * as prompts from "./verify-package-prompts.mjs";
|
||||
import {
|
||||
json,
|
||||
verifyAvailableToolCatalog,
|
||||
} from "./lib/verify-package-core.mjs";
|
||||
import { json, verifyCompleteToolCatalog } from "./lib/verify-package-core.mjs";
|
||||
|
||||
const execute = promisify(execFile);
|
||||
|
||||
const verifyMcpToolsAndPrompts = async (client, mcpOptions) => {
|
||||
await verifyAvailableToolCatalog(client, mcpOptions);
|
||||
await verifyCompleteToolCatalog(client, mcpOptions);
|
||||
await prompts.verifyPromptCatalog(client, mcpOptions, prompts.names);
|
||||
await prompts.verifyPromptCompletion(client, mcpOptions, false);
|
||||
};
|
||||
|
|
@ -43,11 +40,21 @@ const verifyMcpReplay = async (client, mcpOptions, investigationReplay) => {
|
|||
};
|
||||
|
||||
const verifyMcpTargetFree = async (client, mcpOptions) => {
|
||||
const names = (await client.listTools(undefined, mcpOptions)).tools.map(
|
||||
({ name }) => name,
|
||||
const status = await client.callTool(
|
||||
{ name: "binary_session", arguments: { detail: "full" } },
|
||||
mcpOptions,
|
||||
);
|
||||
if (names.includes("current_document"))
|
||||
throw new Error("packaged target-free MCP advertised a target-bound tool");
|
||||
const currentDocument =
|
||||
status.structuredContent?.result?.tool_availability?.find(
|
||||
({ name }) => name === "current_document",
|
||||
);
|
||||
if (
|
||||
currentDocument?.available !== false ||
|
||||
currentDocument.reason !== "target_required"
|
||||
)
|
||||
throw new Error(
|
||||
"packaged target-free MCP omitted target-bound availability metadata",
|
||||
);
|
||||
};
|
||||
|
||||
const verifyMcpUnknownProvider = async (client, mcpOptions) => {
|
||||
|
|
@ -96,11 +103,12 @@ const verifyMcpOpenAndBind = async (client, mcpOptions) => {
|
|||
};
|
||||
|
||||
const verifyMcpLinuxToolAvailability = async (client, mcpOptions) => {
|
||||
const names = (await client.listTools(undefined, mcpOptions)).tools.map(
|
||||
({ name }) => name,
|
||||
const current = await client.callTool(
|
||||
{ name: "current_document", arguments: {} },
|
||||
mcpOptions,
|
||||
);
|
||||
if (names.includes("current_document"))
|
||||
throw new Error("packaged Linux MCP advertised unavailable Hopper tools");
|
||||
if (current.isError !== true)
|
||||
throw new Error("packaged Linux MCP executed an unavailable Hopper tool");
|
||||
};
|
||||
|
||||
const verifyMcpNonLinuxCurrentDocument = async (
|
||||
|
|
|
|||
|
|
@ -114,6 +114,7 @@ try {
|
|||
observation_ms: 1_000,
|
||||
include_accessibility_text: true,
|
||||
include_script_sources: true,
|
||||
source_capture_approved: true,
|
||||
include_console_text: true,
|
||||
console_text_approved: true,
|
||||
include_json_body_shapes: true,
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ const roots = [
|
|||
),
|
||||
];
|
||||
const policy = {
|
||||
enabled: true,
|
||||
status: "enabled",
|
||||
roots,
|
||||
nodePath: process.execPath,
|
||||
bubblewrapPath:
|
||||
|
|
@ -93,7 +93,7 @@ const authority = new PermissionAuthority(
|
|||
),
|
||||
);
|
||||
const dependencies = {
|
||||
policy,
|
||||
policy: () => policy,
|
||||
host: new SystemJavaScriptReplayHost(),
|
||||
runner: new LinuxJavaScriptReplayRunner(),
|
||||
authority,
|
||||
|
|
@ -131,18 +131,20 @@ const defaultInput = {
|
|||
{ case_id: "plain", arguments: ["plain"] },
|
||||
],
|
||||
generator: { preset: "parser-boundaries", seed: 7, count: 4 },
|
||||
approved: false,
|
||||
};
|
||||
const input =
|
||||
const suppliedPlanInput =
|
||||
suppliedInput === undefined
|
||||
? undefined
|
||||
: (({ approved: _approved, plan_digest: _planDigest, ...planInput }) =>
|
||||
planInput)(suppliedInput);
|
||||
const input =
|
||||
suppliedPlanInput === undefined
|
||||
? defaultInput
|
||||
: {
|
||||
...suppliedInput,
|
||||
...suppliedPlanInput,
|
||||
mode: "plan",
|
||||
left: suppliedLeft,
|
||||
...(suppliedRight === undefined ? {} : { right: suppliedRight }),
|
||||
approved: false,
|
||||
plan_digest: undefined,
|
||||
reproducer_export: undefined,
|
||||
};
|
||||
|
||||
|
|
@ -177,7 +179,21 @@ if (
|
|||
({ status }) => status === "unknown",
|
||||
) === true
|
||||
)
|
||||
throw new Error("Real replay verifier received incomplete Evidence");
|
||||
throw new Error(
|
||||
`Real replay verifier received incomplete Evidence: ${JSON.stringify({
|
||||
authority: evidence?.authority ?? null,
|
||||
isolation: evidence?.environment?.isolation ?? null,
|
||||
termination: evidence?.normalized_result?.termination ?? null,
|
||||
cleanup: evidence?.normalized_result?.cleanup?.state ?? null,
|
||||
outcomes:
|
||||
evidence?.normalized_result?.outcomes?.map(({ outcome }) => outcome) ??
|
||||
[],
|
||||
stderr: evidence?.normalized_result?.stderr ?? null,
|
||||
comparison_statuses:
|
||||
evidence?.normalized_result?.comparison?.map(({ status }) => status) ??
|
||||
[],
|
||||
})}`,
|
||||
);
|
||||
|
||||
if (suppliedInput !== undefined) {
|
||||
process.stdout.write(
|
||||
|
|
@ -222,7 +238,6 @@ const runSingle = async (path, limits = undefined) => {
|
|||
},
|
||||
cases: [{ case_id: "probe", arguments: [] }],
|
||||
...(limits === undefined ? {} : { limits }),
|
||||
approved: false,
|
||||
};
|
||||
const proposed = await runControlledReplay(dependencies, manifest);
|
||||
if (!proposed.ok || proposed.value.plan === null)
|
||||
|
|
@ -288,7 +303,6 @@ const differentialTimeoutInput = {
|
|||
},
|
||||
cases: [{ case_id: "probe", arguments: [] }],
|
||||
limits: { wall_time_ms: 100 },
|
||||
approved: false,
|
||||
};
|
||||
const differentialTimeoutPlan = await runControlledReplay(
|
||||
dependencies,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { promisify } from "node:util";
|
|||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
|
||||
import { verifyAvailableToolCatalog } from "./lib/verify-package-core.mjs";
|
||||
import { verifyCompleteToolCatalog } from "./lib/verify-package-core.mjs";
|
||||
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
|
||||
|
||||
const exec = promisify(execFile);
|
||||
|
|
@ -84,7 +84,7 @@ try {
|
|||
let toolCount = 0;
|
||||
try {
|
||||
await client.connect(server);
|
||||
toolCount = (await verifyAvailableToolCatalog(client)).length;
|
||||
toolCount = (await verifyCompleteToolCatalog(client)).length;
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ description: Reverse engineer native, managed, Electron/JavaScript, packaged, an
|
|||
metadata:
|
||||
version: "23"
|
||||
tool_count: 116
|
||||
catalog_digest: "2da444fd95a40c28a6c0332a5970edca6b984f0c2922e4c02eaf7458a6f3dd0e"
|
||||
catalog_digest: "bab5657d9525ec4f798d0a9ef6001412d9c11f77fb0ddc925c7076fd10921dc9"
|
||||
---
|
||||
|
||||
# REA
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
type TraceApplicationFeatureRequest,
|
||||
type TraceJavaScriptSemanticsRequest,
|
||||
} from "../contracts/applicationWorkflowInputContracts.js";
|
||||
import type { Evidence } from "../domain/evidence.js";
|
||||
import { AnalysisInputError, type AnalysisError } from "../domain/errors.js";
|
||||
import { projectInputIssues } from "../domain/inputIssueProjection.js";
|
||||
import { compareApplicationVersionsInputSchema } from "../domain/javascriptApplicationVersionComparisonSchemas.js";
|
||||
|
|
@ -69,11 +70,7 @@ export const resolveTraceApplicationFeatureRequestValidated = (
|
|||
input: TraceApplicationFeatureRequest,
|
||||
lookup?: EvidenceLookup,
|
||||
): Result<TraceInput, AnalysisError> => {
|
||||
const application = graphEvidence(
|
||||
input.application,
|
||||
input.application_evidence_id,
|
||||
lookup,
|
||||
);
|
||||
const application = graphEvidence(input, lookup);
|
||||
if (!application.ok) return application;
|
||||
const native = resolveEvidenceReferences(
|
||||
lookup,
|
||||
|
|
@ -109,11 +106,7 @@ export const resolveTraceJavaScriptSemanticsRequestValidated = (
|
|||
input: TraceJavaScriptSemanticsRequest,
|
||||
lookup?: EvidenceLookup,
|
||||
): Result<SemanticTraceInput, AnalysisError> => {
|
||||
const application = graphEvidence(
|
||||
input.application,
|
||||
input.application_evidence_id,
|
||||
lookup,
|
||||
);
|
||||
const application = graphEvidence(input, lookup);
|
||||
if (!application.ok) return application;
|
||||
const raw = { application: application.value, query: input.query };
|
||||
const parsed = traceJavaScriptSemanticsInputSchema.safeParse(raw);
|
||||
|
|
@ -138,9 +131,19 @@ export const resolveCompareApplicationVersionsRequestValidated = (
|
|||
input: CompareApplicationVersionsRequest,
|
||||
lookup?: EvidenceLookup,
|
||||
): Result<ComparisonInput, AnalysisError> => {
|
||||
const left = graphEvidence(input.left, input.left_evidence_id, lookup);
|
||||
const left = graphEvidence(
|
||||
"left" in input
|
||||
? { application: input.left }
|
||||
: { application_evidence_id: input.left_evidence_id },
|
||||
lookup,
|
||||
);
|
||||
if (!left.ok) return left;
|
||||
const right = graphEvidence(input.right, input.right_evidence_id, lookup);
|
||||
const right = graphEvidence(
|
||||
"right" in input
|
||||
? { application: input.right }
|
||||
: { application_evidence_id: input.right_evidence_id },
|
||||
lookup,
|
||||
);
|
||||
if (!right.ok) return right;
|
||||
const leftNative = resolveEvidenceReferences(
|
||||
lookup,
|
||||
|
|
@ -190,11 +193,7 @@ export const resolveCompareSourceToBundleRequestValidated = (
|
|||
input: CompareSourceToBundleRequest,
|
||||
lookup?: EvidenceLookup,
|
||||
): Result<SourceToBundleInput, AnalysisError> => {
|
||||
const application = graphEvidence(
|
||||
input.application,
|
||||
input.application_evidence_id,
|
||||
lookup,
|
||||
);
|
||||
const application = graphEvidence(input, lookup);
|
||||
if (!application.ok) return application;
|
||||
const raw = {
|
||||
reference: input.reference,
|
||||
|
|
@ -226,9 +225,19 @@ export const resolveCompareJavaScriptExportShapesRequestValidated = (
|
|||
input: CompareJavaScriptExportShapesRequest,
|
||||
lookup?: EvidenceLookup,
|
||||
): Result<ExportShapeComparisonInput, AnalysisError> => {
|
||||
const left = graphEvidence(input.left, input.left_evidence_id, lookup);
|
||||
const left = graphEvidence(
|
||||
"left" in input
|
||||
? { application: input.left }
|
||||
: { application_evidence_id: input.left_evidence_id },
|
||||
lookup,
|
||||
);
|
||||
if (!left.ok) return left;
|
||||
const right = graphEvidence(input.right, input.right_evidence_id, lookup);
|
||||
const right = graphEvidence(
|
||||
"right" in input
|
||||
? { application: input.right }
|
||||
: { application_evidence_id: input.right_evidence_id },
|
||||
lookup,
|
||||
);
|
||||
if (!right.ok) return right;
|
||||
const raw = {
|
||||
left: left.value,
|
||||
|
|
@ -249,19 +258,15 @@ export const resolveCompareJavaScriptExportShapesRequestValidated = (
|
|||
};
|
||||
|
||||
const graphEvidence = (
|
||||
evidence:
|
||||
| TraceApplicationFeatureRequest["application"]
|
||||
| TraceJavaScriptSemanticsRequest["application"]
|
||||
| CompareApplicationVersionsRequest["left"]
|
||||
| CompareJavaScriptExportShapesRequest["left"]
|
||||
| CompareSourceToBundleRequest["application"],
|
||||
evidenceId: string | undefined,
|
||||
source:
|
||||
| { readonly application: Evidence }
|
||||
| { readonly application_evidence_id: string },
|
||||
lookup: EvidenceLookup | undefined,
|
||||
) => {
|
||||
if (evidence !== undefined) return ok(evidence);
|
||||
if ("application" in source) return ok(source.application);
|
||||
const resolved = resolveEvidenceReferences(
|
||||
lookup,
|
||||
evidenceId === undefined ? [] : [evidenceId],
|
||||
[source.application_evidence_id],
|
||||
APPLICATION_GRAPH_IDENTITIES,
|
||||
);
|
||||
return resolved.ok ? ok(resolved.value[0]) : resolved;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
|
||||
import type {
|
||||
ArtifactIntegrityPolicy,
|
||||
ArtifactNativeMountPolicy,
|
||||
} from "./types.js";
|
||||
|
||||
/** Parsed caller intent for integrity mismatch handling. */
|
||||
export type ArtifactIntegrityIntent =
|
||||
| { readonly mode: "fail" }
|
||||
| {
|
||||
readonly mode: "record-and-continue";
|
||||
readonly maxMismatches: number;
|
||||
};
|
||||
|
||||
/** Resolve caller approval and operator policy before native reader selection. */
|
||||
export const resolveNativeMountPolicy = (
|
||||
approved: boolean,
|
||||
enabled: boolean,
|
||||
): ArtifactNativeMountPolicy => {
|
||||
if (!approved) return { status: "disabled" };
|
||||
if (!enabled)
|
||||
throw new ArtifactReaderFailure(
|
||||
"unavailable",
|
||||
"Native DMG mounting is disabled by operator policy",
|
||||
);
|
||||
return { status: "approved" };
|
||||
};
|
||||
|
||||
/** Resolve parsed caller intent and operator policy before artifact scanning. */
|
||||
export const resolveArtifactIntegrityPolicy = (
|
||||
intent: ArtifactIntegrityIntent,
|
||||
enabled: boolean,
|
||||
): ArtifactIntegrityPolicy => {
|
||||
if (intent.mode === "fail") return intent;
|
||||
if (!enabled)
|
||||
throw new ArtifactReaderFailure(
|
||||
"policy",
|
||||
"Integrity continuation requires explicit approval and operator policy",
|
||||
);
|
||||
return intent;
|
||||
};
|
||||
|
|
@ -1,8 +1,5 @@
|
|||
import { AsarArtifactReader } from "../../artifacts/AsarArtifactReader.js";
|
||||
import {
|
||||
ArtifactReaderFailure,
|
||||
type ArtifactReader,
|
||||
} from "../../artifacts/ArtifactReader.js";
|
||||
import type { ArtifactReader } from "../../artifacts/ArtifactReader.js";
|
||||
import { DirectoryArtifactReader } from "../../artifacts/DirectoryArtifactReader.js";
|
||||
import { MachOSliceArtifactReader } from "../../artifacts/MachOSliceArtifactReader.js";
|
||||
import { NativeDmgArtifactReader } from "../../artifacts/NativeDmgArtifactReader.js";
|
||||
|
|
@ -32,12 +29,7 @@ export const createReader = async (
|
|||
? new MachOSliceArtifactReader(path)
|
||||
: undefined;
|
||||
case "dmg":
|
||||
if (!nativeMount.nativeMountApproved) return undefined;
|
||||
if (!nativeMount.nativeMountEnabled)
|
||||
throw new ArtifactReaderFailure(
|
||||
"unavailable",
|
||||
"Native DMG mounting is disabled by operator policy",
|
||||
);
|
||||
if (nativeMount.status === "disabled") return undefined;
|
||||
return NativeDmgArtifactReader.create(path, signal);
|
||||
default:
|
||||
return undefined;
|
||||
|
|
|
|||
|
|
@ -44,14 +44,6 @@ export const scanCanonicalArtifactInventory = async (
|
|||
options: ArtifactInventoryOptions = {},
|
||||
): Promise<ArtifactInventorySnapshot> => {
|
||||
const integrity = options.integrity ?? STRICT_INTEGRITY_POLICY;
|
||||
if (
|
||||
integrity.mode === "record-and-continue" &&
|
||||
(!integrity.approved || !integrity.enabled)
|
||||
)
|
||||
throw new ArtifactReaderFailure(
|
||||
"policy",
|
||||
"Integrity continuation requires explicit approval and operator policy",
|
||||
);
|
||||
const metadata = await lstat(path);
|
||||
const rootFormat = await classifyRoot(path, metadata.isDirectory());
|
||||
const rootDigest = metadata.isDirectory()
|
||||
|
|
|
|||
|
|
@ -166,19 +166,19 @@ const digestArtifactEntry = async (
|
|||
context.totalBytes += digest.bytes;
|
||||
const mismatched =
|
||||
entry.declaredSha256 !== null && entry.declaredSha256 !== digest.sha256;
|
||||
if (mismatched && context.integrity.mode === "fail")
|
||||
throw new ArtifactReaderFailure(
|
||||
"integrity",
|
||||
`Artifact integrity metadata disagrees with content: ${logicalPath}`,
|
||||
undefined,
|
||||
{
|
||||
logicalPath,
|
||||
declaredSha256: entry.declaredSha256,
|
||||
calculatedSha256: digest.sha256,
|
||||
unpacked: entry.unpacked,
|
||||
},
|
||||
);
|
||||
if (mismatched && entry.declaredSha256 !== null) {
|
||||
if (context.integrity.mode === "fail")
|
||||
throw new ArtifactReaderFailure(
|
||||
"integrity",
|
||||
`Artifact integrity metadata disagrees with content: ${logicalPath}`,
|
||||
undefined,
|
||||
{
|
||||
logicalPath,
|
||||
declaredSha256: entry.declaredSha256,
|
||||
calculatedSha256: digest.sha256,
|
||||
unpacked: entry.unpacked,
|
||||
},
|
||||
);
|
||||
if (context.pendingContradictions.length >= context.integrity.maxMismatches)
|
||||
throw new ArtifactReaderFailure(
|
||||
"limit",
|
||||
|
|
|
|||
|
|
@ -13,30 +13,25 @@ export interface InventoryPageInput {
|
|||
readonly edgeLimit: number;
|
||||
}
|
||||
|
||||
/** Per-request authority combined with operator-owned native mount policy. */
|
||||
export interface ArtifactNativeMountPolicy {
|
||||
readonly nativeMountApproved: boolean;
|
||||
readonly nativeMountEnabled: boolean;
|
||||
}
|
||||
/** Resolved native mount authority admitted to the artifact reader. */
|
||||
export type ArtifactNativeMountPolicy =
|
||||
| { readonly status: "disabled" }
|
||||
| { readonly status: "approved" };
|
||||
|
||||
export const NATIVE_MOUNT_DISABLED: ArtifactNativeMountPolicy = {
|
||||
nativeMountApproved: false,
|
||||
nativeMountEnabled: false,
|
||||
status: "disabled",
|
||||
};
|
||||
|
||||
/** Explicit caller approval bounded by operator-owned integrity policy. */
|
||||
export interface ArtifactIntegrityPolicy {
|
||||
readonly mode: "fail" | "record-and-continue";
|
||||
readonly approved: boolean;
|
||||
readonly enabled: boolean;
|
||||
readonly maxMismatches: number;
|
||||
}
|
||||
/** Resolved integrity behavior admitted to the artifact scanner. */
|
||||
export type ArtifactIntegrityPolicy =
|
||||
| { readonly mode: "fail" }
|
||||
| {
|
||||
readonly mode: "record-and-continue";
|
||||
readonly maxMismatches: number;
|
||||
};
|
||||
|
||||
export const STRICT_INTEGRITY_POLICY: ArtifactIntegrityPolicy = {
|
||||
mode: "fail",
|
||||
approved: false,
|
||||
enabled: false,
|
||||
maxMismatches: 1,
|
||||
};
|
||||
|
||||
/** Options shared by artifact inventory scans. */
|
||||
|
|
|
|||
|
|
@ -74,9 +74,9 @@ export class BinarySession
|
|||
#transition: Promise<void> = Promise.resolve();
|
||||
readonly #calls = new Set<Promise<unknown>>();
|
||||
readonly #providerRouter: SessionProviderRouter;
|
||||
readonly #runtimeAvailability = new Map<
|
||||
readonly #runtimeUnavailability = new Map<
|
||||
string,
|
||||
{ readonly available: boolean; readonly reason: string | null }
|
||||
{ readonly reason: string }
|
||||
>();
|
||||
readonly #availabilityListeners = new Set<() => void | Promise<void>>();
|
||||
|
||||
|
|
@ -272,7 +272,7 @@ export class BinarySession
|
|||
target: this.#active?.target,
|
||||
route: this.#currentRoute(),
|
||||
router: this.#providerRouter,
|
||||
runtimeAvailability: this.#runtimeAvailability,
|
||||
runtimeUnavailability: this.#runtimeUnavailability,
|
||||
runId: this.#active?.runId,
|
||||
runtimeLineageSnapshots:
|
||||
this.#active?.client.runtimeLineageSnapshots?.() ?? [],
|
||||
|
|
@ -344,12 +344,12 @@ export class BinarySession
|
|||
result: Result<AnalysisExecution, AnalysisError>,
|
||||
): void {
|
||||
if (result.ok) {
|
||||
if (this.#setRuntimeAvailability(operation, true, null))
|
||||
if (this.#markRuntimeAvailable(operation))
|
||||
this.#emitAvailabilityChanged();
|
||||
return;
|
||||
}
|
||||
if (result.error._tag === "AnalysisCapabilityUnavailableError") {
|
||||
if (this.#setRuntimeAvailability(operation, false, result.error.message))
|
||||
if (this.#markRuntimeUnavailable(operation, result.error.message))
|
||||
this.#emitAvailabilityChanged();
|
||||
return;
|
||||
}
|
||||
|
|
@ -367,32 +367,28 @@ export class BinarySession
|
|||
for (const descriptor of capabilities?.values() ?? [])
|
||||
if (providerId !== undefined && descriptor.provider.id === providerId)
|
||||
changed =
|
||||
this.#setRuntimeAvailability(
|
||||
this.#markRuntimeUnavailable(
|
||||
descriptor.operation,
|
||||
false,
|
||||
"Provider became unavailable during this session.",
|
||||
) || changed;
|
||||
if (changed) this.#emitAvailabilityChanged();
|
||||
}
|
||||
}
|
||||
|
||||
#setRuntimeAvailability(
|
||||
operation: string,
|
||||
available: boolean,
|
||||
reason: string | null,
|
||||
): boolean {
|
||||
const current = this.#runtimeAvailability.get(operation);
|
||||
if (current?.available === available && current.reason === reason)
|
||||
return false;
|
||||
if (available && current === undefined) return false;
|
||||
if (available) this.#runtimeAvailability.delete(operation);
|
||||
else this.#runtimeAvailability.set(operation, { available, reason });
|
||||
#markRuntimeAvailable(operation: string): boolean {
|
||||
return this.#runtimeUnavailability.delete(operation);
|
||||
}
|
||||
|
||||
#markRuntimeUnavailable(operation: string, reason: string): boolean {
|
||||
const current = this.#runtimeUnavailability.get(operation);
|
||||
if (current?.reason === reason) return false;
|
||||
this.#runtimeUnavailability.set(operation, { reason });
|
||||
return true;
|
||||
}
|
||||
|
||||
#clearRuntimeAvailability(): void {
|
||||
if (this.#runtimeAvailability.size === 0) return;
|
||||
this.#runtimeAvailability.clear();
|
||||
if (this.#runtimeUnavailability.size === 0) return;
|
||||
this.#runtimeUnavailability.clear();
|
||||
this.#emitAvailabilityChanged();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ interface BinarySessionStatusInput {
|
|||
readonly target: BinaryTarget | undefined;
|
||||
readonly route: SessionProviderRoute;
|
||||
readonly router: SessionProviderRouter;
|
||||
readonly runtimeAvailability: ReadonlyMap<
|
||||
readonly runtimeUnavailability: ReadonlyMap<
|
||||
string,
|
||||
{ readonly available: boolean; readonly reason: string | null }
|
||||
{ readonly reason: string }
|
||||
>;
|
||||
readonly runId: string | undefined;
|
||||
readonly runtimeLineageSnapshots: readonly ProviderRuntimeLineageSnapshot[];
|
||||
|
|
@ -28,7 +28,7 @@ export const binarySessionStatus = ({
|
|||
target,
|
||||
route,
|
||||
router,
|
||||
runtimeAvailability,
|
||||
runtimeUnavailability,
|
||||
runId,
|
||||
runtimeLineageSnapshots,
|
||||
requestActivitySnapshots,
|
||||
|
|
@ -38,10 +38,16 @@ export const binarySessionStatus = ({
|
|||
const providers = router.providerIdentities(route).map(providerSummary);
|
||||
const capabilities = [...(route.capabilities?.values() ?? [])]
|
||||
.sort((left, right) => left.operation.localeCompare(right.operation))
|
||||
.map((descriptor) => ({
|
||||
...descriptor,
|
||||
...runtimeAvailability.get(descriptor.operation),
|
||||
}))
|
||||
.map((descriptor) => {
|
||||
const unavailable = runtimeUnavailability.get(descriptor.operation);
|
||||
return unavailable === undefined
|
||||
? descriptor
|
||||
: {
|
||||
...descriptor,
|
||||
available: false as const,
|
||||
reason: unavailable.reason,
|
||||
};
|
||||
})
|
||||
.map(capabilityStatus);
|
||||
const analysisProviderBinding =
|
||||
route.binding === null
|
||||
|
|
|
|||
|
|
@ -93,11 +93,7 @@ const browserParameters = (
|
|||
input: BrowserEvidenceInput,
|
||||
): EvidenceObservation["parameters"] => {
|
||||
if (!("cdp_endpoint" in input)) {
|
||||
if (
|
||||
"before_scenario" in input &&
|
||||
input.before_scenario !== undefined &&
|
||||
input.after_scenario !== undefined
|
||||
)
|
||||
if ("before_scenario" in input)
|
||||
return {
|
||||
comparison_kind: "browser_scenario",
|
||||
before_browser: input.before_scenario.browser,
|
||||
|
|
@ -166,9 +162,9 @@ const browserParameters = (
|
|||
include_storage_keys: input.include_storage_keys,
|
||||
include_storage_fingerprints: input.include_storage_fingerprints,
|
||||
limits: input.limits,
|
||||
...(input.include_script_sources && "source_capture_approved" in input
|
||||
source_capture_approved: input.source_capture_approved,
|
||||
...(input.include_script_sources && "analysis_limits" in input
|
||||
? {
|
||||
source_capture_approved: input.source_capture_approved,
|
||||
fetch_source_maps: input.fetch_source_maps,
|
||||
source_map_fetch_approved: input.source_map_fetch_approved,
|
||||
analysis_limits: input.analysis_limits,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
import type { ClientFeatureAvailability } from "../contracts/toolOutputSchemaPrimitives.js";
|
||||
|
||||
type ClientFeatureName = keyof ClientFeatureAvailability;
|
||||
|
||||
/** Empty MCP client feature declaration used when the caller supplies none. */
|
||||
export const NO_CLIENT_FEATURES: ClientFeatureAvailability = {
|
||||
elicitation_form: false,
|
||||
elicitation_url: false,
|
||||
roots: false,
|
||||
sampling: false,
|
||||
};
|
||||
|
||||
const CLIENT_FEATURE_REQUIREMENTS: Readonly<
|
||||
Record<
|
||||
string,
|
||||
{
|
||||
readonly required: readonly ClientFeatureName[];
|
||||
readonly optional: readonly ClientFeatureName[];
|
||||
}
|
||||
>
|
||||
> = {
|
||||
capture_process_scenario: {
|
||||
required: [],
|
||||
optional: ["elicitation_form"],
|
||||
},
|
||||
};
|
||||
|
||||
/** Project a tool's client requirements against the negotiated feature set. */
|
||||
export const clientRequirementsFor = (
|
||||
name: string,
|
||||
clientFeatures: ClientFeatureAvailability,
|
||||
) => {
|
||||
const requirements = CLIENT_FEATURE_REQUIREMENTS[name] ?? {
|
||||
required: [],
|
||||
optional: [],
|
||||
};
|
||||
return {
|
||||
required: [...requirements.required],
|
||||
optional: [...requirements.optional],
|
||||
missing_required: requirements.required.filter(
|
||||
(feature) => !clientFeatures[feature],
|
||||
),
|
||||
missing_optional: requirements.optional.filter(
|
||||
(feature) => !clientFeatures[feature],
|
||||
),
|
||||
};
|
||||
};
|
||||
|
|
@ -1,39 +1,31 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import type { ClientFeatureAvailability } from "../contracts/toolOutputSchemaPrimitives.js";
|
||||
import {
|
||||
providerCapability,
|
||||
type ClientFeatureAvailability,
|
||||
type ProviderCapability,
|
||||
type ToolAvailability,
|
||||
type ToolAvailabilityMode,
|
||||
type ToolUnavailabilityReason,
|
||||
} from "../contracts/toolOutputSchemaPrimitives.js";
|
||||
import type { ToolKind } from "../contracts/toolContractTypes.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import { GENERATED_MCP_TOOL_CATALOG } from "../generatedMcpToolCatalog.js";
|
||||
import {
|
||||
clientRequirementsFor,
|
||||
NO_CLIENT_FEATURES,
|
||||
} from "./CapabilityClientRequirements.js";
|
||||
|
||||
const statusSchema = z.object({
|
||||
open: z.boolean(),
|
||||
kind: z.enum(["executable", "database", "archive", "artifact"]).optional(),
|
||||
format: z.string().optional(),
|
||||
capabilities: z.array(
|
||||
z.object({
|
||||
operation: z.string(),
|
||||
available: z.boolean(),
|
||||
reason: z.string().nullable(),
|
||||
availability_code: z.string().nullable().optional(),
|
||||
}),
|
||||
),
|
||||
capabilities: z.array(providerCapability),
|
||||
});
|
||||
|
||||
type ToolAvailabilityReason =
|
||||
| "available"
|
||||
| "client_capability_missing"
|
||||
| "target_required"
|
||||
| "provider_missing"
|
||||
| "provider_unavailable"
|
||||
| "target_unsupported"
|
||||
| "unsupported_host"
|
||||
| "policy_disabled";
|
||||
type ToolAvailabilityReason = "available" | ToolUnavailabilityReason;
|
||||
|
||||
type ProviderDescriptor = {
|
||||
readonly available: boolean;
|
||||
readonly reason: string | null;
|
||||
readonly availability_code?: string | null;
|
||||
};
|
||||
type ProviderDescriptor = ProviderCapability;
|
||||
export type AvailabilityPolicy = {
|
||||
readonly processCaptureEnabled: boolean;
|
||||
readonly evidenceFileRoots: number;
|
||||
|
|
@ -47,44 +39,25 @@ export type AvailabilityPolicy = {
|
|||
readonly managedRuntimeEnabled?: boolean;
|
||||
};
|
||||
|
||||
type ClientFeatureName = keyof ClientFeatureAvailability;
|
||||
|
||||
const NO_CLIENT_FEATURES: ClientFeatureAvailability = {
|
||||
elicitation_form: false,
|
||||
elicitation_url: false,
|
||||
roots: false,
|
||||
sampling: false,
|
||||
};
|
||||
|
||||
const CLIENT_FEATURE_REQUIREMENTS: Readonly<
|
||||
Record<
|
||||
string,
|
||||
{
|
||||
readonly required: readonly ClientFeatureName[];
|
||||
readonly optional: readonly ClientFeatureName[];
|
||||
}
|
||||
>
|
||||
> = {
|
||||
capture_process_scenario: {
|
||||
required: [],
|
||||
optional: ["elicitation_form"],
|
||||
},
|
||||
};
|
||||
type Availability = {
|
||||
readonly reason: ToolAvailabilityReason;
|
||||
readonly remediation: string | null;
|
||||
type AvailabilityFacts = {
|
||||
readonly defaultModeAvailable?: boolean;
|
||||
readonly modes?: readonly CapabilityMode[];
|
||||
};
|
||||
type CapabilityMode = {
|
||||
readonly name: string;
|
||||
readonly available: boolean;
|
||||
readonly missing_operations: readonly string[];
|
||||
readonly remediation: string | null;
|
||||
};
|
||||
type NavigationModeResult = CapabilityMode & {
|
||||
readonly reason: ToolAvailabilityReason;
|
||||
readonly modes?: readonly ToolAvailabilityMode[];
|
||||
};
|
||||
type Availability = AvailabilityFacts &
|
||||
(
|
||||
| { readonly reason: "available"; readonly remediation: null }
|
||||
| {
|
||||
readonly reason: ToolUnavailabilityReason;
|
||||
readonly remediation: string | null;
|
||||
}
|
||||
);
|
||||
type NavigationModeResult =
|
||||
| (Extract<ToolAvailabilityMode, { available: true }> & {
|
||||
readonly reason: "available";
|
||||
})
|
||||
| (Extract<ToolAvailabilityMode, { available: false }> & {
|
||||
readonly reason: ToolUnavailabilityReason;
|
||||
});
|
||||
type AvailabilityContext = {
|
||||
readonly name: string;
|
||||
readonly kind: ToolKind;
|
||||
|
|
@ -143,19 +116,12 @@ export const buildCapabilityInventory = (
|
|||
sessionStatus: JsonValue,
|
||||
policy: AvailabilityPolicy,
|
||||
clientFeatures: ClientFeatureAvailability = NO_CLIENT_FEATURES,
|
||||
) => {
|
||||
): readonly ToolAvailability[] => {
|
||||
const status = statusSchema.parse(sessionStatus);
|
||||
const descriptors = new Map<string, ProviderDescriptor>(
|
||||
status.capabilities.map((descriptor) => [
|
||||
descriptor.operation,
|
||||
{
|
||||
available: descriptor.available,
|
||||
reason: descriptor.reason,
|
||||
availability_code: descriptor.availability_code ?? null,
|
||||
},
|
||||
]),
|
||||
status.capabilities.map((descriptor) => [descriptor.operation, descriptor]),
|
||||
);
|
||||
return GENERATED_MCP_TOOL_CATALOG.map((contract) => {
|
||||
return GENERATED_MCP_TOOL_CATALOG.map((contract): ToolAvailability => {
|
||||
const availability = availabilityFor({
|
||||
name: contract.name,
|
||||
kind: contract.kind,
|
||||
|
|
@ -169,22 +135,15 @@ export const buildCapabilityInventory = (
|
|||
clientFeatures,
|
||||
);
|
||||
const clientBlocked = clientRequirements.missing_required.length > 0;
|
||||
return {
|
||||
const facts = {
|
||||
name: contract.name,
|
||||
surface: contract.kind,
|
||||
available: availability.reason === "available" && !clientBlocked,
|
||||
reason: clientBlocked
|
||||
? ("client_capability_missing" as const)
|
||||
: availability.reason,
|
||||
remediation: clientBlocked
|
||||
? `Use an MCP client that supports: ${clientRequirements.missing_required.join(", ")}.`
|
||||
: availability.remediation,
|
||||
...(availability.defaultModeAvailable === undefined
|
||||
? {}
|
||||
: { default_mode_available: availability.defaultModeAvailable }),
|
||||
...(availability.modes === undefined
|
||||
? {}
|
||||
: { modes: availability.modes }),
|
||||
: { modes: [...availability.modes] }),
|
||||
client_requirements: clientRequirements,
|
||||
effects: { ...contract.effects },
|
||||
annotations: {
|
||||
|
|
@ -194,29 +153,29 @@ export const buildCapabilityInventory = (
|
|||
open_world: contract.annotations.openWorldHint ?? true,
|
||||
},
|
||||
};
|
||||
if (clientBlocked)
|
||||
return {
|
||||
...facts,
|
||||
available: false,
|
||||
reason: "client_capability_missing",
|
||||
remediation: `Use an MCP client that supports: ${clientRequirements.missing_required.join(", ")}.`,
|
||||
};
|
||||
return availability.reason === "available"
|
||||
? {
|
||||
...facts,
|
||||
available: true,
|
||||
reason: availability.reason,
|
||||
remediation: null,
|
||||
}
|
||||
: {
|
||||
...facts,
|
||||
available: false,
|
||||
reason: availability.reason,
|
||||
remediation: availability.remediation,
|
||||
};
|
||||
}).sort((left, right) => left.name.localeCompare(right.name));
|
||||
};
|
||||
|
||||
const clientRequirementsFor = (
|
||||
name: string,
|
||||
clientFeatures: ClientFeatureAvailability,
|
||||
) => {
|
||||
const requirements = CLIENT_FEATURE_REQUIREMENTS[name] ?? {
|
||||
required: [],
|
||||
optional: [],
|
||||
};
|
||||
return {
|
||||
required: [...requirements.required],
|
||||
optional: [...requirements.optional],
|
||||
missing_required: requirements.required.filter(
|
||||
(feature) => !clientFeatures[feature],
|
||||
),
|
||||
missing_optional: requirements.optional.filter(
|
||||
(feature) => !clientFeatures[feature],
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
const availabilityFor = (context: AvailabilityContext): Availability => {
|
||||
if (context.name === "get_navigation_context")
|
||||
return navigationContextAvailability(context.descriptors);
|
||||
|
|
@ -408,10 +367,7 @@ const providerAvailability = ({
|
|||
const composedAvailability = (
|
||||
name: string,
|
||||
descriptors: ReadonlyMap<string, ProviderDescriptor>,
|
||||
): {
|
||||
readonly reason: ToolAvailabilityReason;
|
||||
readonly remediation: string | null;
|
||||
} => {
|
||||
): Availability => {
|
||||
const requirements = ENHANCED_REQUIREMENTS[name];
|
||||
if (requirements === undefined)
|
||||
return {
|
||||
|
|
@ -455,9 +411,17 @@ const navigationContextAvailability = (
|
|||
mode.requirements,
|
||||
descriptors,
|
||||
);
|
||||
if (availability.reason === "available")
|
||||
return {
|
||||
name: mode.name,
|
||||
available: true,
|
||||
reason: availability.reason,
|
||||
missing_operations: [],
|
||||
remediation: null,
|
||||
};
|
||||
return {
|
||||
name: mode.name,
|
||||
available: availability.reason === "available",
|
||||
available: false,
|
||||
reason: availability.reason,
|
||||
missing_operations: mode.requirements.filter((operation) => {
|
||||
const descriptor = descriptors.get(operation);
|
||||
|
|
@ -467,39 +431,47 @@ const navigationContextAvailability = (
|
|||
};
|
||||
},
|
||||
);
|
||||
const modes = modeResults.map(
|
||||
({ name, available, missing_operations, remediation }) => ({
|
||||
name,
|
||||
available,
|
||||
missing_operations,
|
||||
remediation,
|
||||
}),
|
||||
const modes: ToolAvailabilityMode[] = modeResults.map((mode) =>
|
||||
mode.available
|
||||
? {
|
||||
name: mode.name,
|
||||
available: true,
|
||||
missing_operations: [],
|
||||
remediation: null,
|
||||
}
|
||||
: {
|
||||
name: mode.name,
|
||||
available: false,
|
||||
missing_operations: [...mode.missing_operations],
|
||||
remediation: mode.remediation,
|
||||
},
|
||||
);
|
||||
const availableMode = modeResults.find((mode) => mode.available);
|
||||
const failure = modeResults
|
||||
.filter((mode) => !mode.available)
|
||||
.sort(
|
||||
(left, right) =>
|
||||
navigationFailurePriority(left.reason) -
|
||||
navigationFailurePriority(right.reason),
|
||||
)[0];
|
||||
const failures = modeResults.flatMap((mode) =>
|
||||
mode.available ? [] : [mode],
|
||||
);
|
||||
const failure = failures.sort(
|
||||
(left, right) =>
|
||||
navigationFailurePriority(left.reason) -
|
||||
navigationFailurePriority(right.reason),
|
||||
)[0];
|
||||
const specificFailureRemediation =
|
||||
failure?.reason === "provider_missing" ? undefined : failure?.remediation;
|
||||
return {
|
||||
reason:
|
||||
availableMode === undefined
|
||||
? (failure?.reason ?? "provider_missing")
|
||||
: "available",
|
||||
remediation:
|
||||
availableMode === undefined
|
||||
? (specificFailureRemediation ??
|
||||
`Configure a provider for one of: ${modes
|
||||
.flatMap((mode) => mode.missing_operations)
|
||||
.join(", ")}.`)
|
||||
: null,
|
||||
const facts = {
|
||||
defaultModeAvailable: modes[0]?.available ?? false,
|
||||
modes,
|
||||
};
|
||||
if (availableMode !== undefined)
|
||||
return { ...facts, reason: "available", remediation: null };
|
||||
return {
|
||||
...facts,
|
||||
reason: failure?.reason ?? "provider_missing",
|
||||
remediation:
|
||||
specificFailureRemediation ??
|
||||
`Configure a provider for one of: ${modes
|
||||
.flatMap((mode) => mode.missing_operations)
|
||||
.join(", ")}.`,
|
||||
};
|
||||
};
|
||||
|
||||
const navigationFailurePriority = (reason: ToolAvailabilityReason): number => {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import {
|
|||
} from "./ArtifactInventory.js";
|
||||
import { scanAuthorizedArtifactInventory } from "./AuthorizedArtifactInventory.js";
|
||||
import { ARTIFACT_GRAPH_PROVIDER } from "./InvestigationProviders.js";
|
||||
import { resolveArtifactIntegrityPolicy } from "./ArtifactInventory/policy.js";
|
||||
|
||||
export const AUTOMATIC_RUN_LIMITATION =
|
||||
"Automatic cross-version runs compare shipped artifact structure only; they do not execute either version.";
|
||||
|
|
@ -76,12 +77,15 @@ export const scanVersions = async (
|
|||
if (signal?.aborted === true)
|
||||
return err(new AnalysisCancelledError("find_changed_behavior"));
|
||||
const limits = artifactLimits(input.options);
|
||||
const integrity: ArtifactIntegrityPolicy = {
|
||||
mode: input.integrity_policy,
|
||||
approved: input.integrity_continue_approved,
|
||||
enabled: integrityContinueEnabled,
|
||||
maxMismatches: input.max_integrity_mismatches,
|
||||
};
|
||||
const integrity = resolveArtifactIntegrityPolicy(
|
||||
input.integrity_policy === "fail"
|
||||
? { mode: "fail" }
|
||||
: {
|
||||
mode: input.integrity_policy,
|
||||
maxMismatches: input.max_integrity_mismatches,
|
||||
},
|
||||
integrityContinueEnabled,
|
||||
);
|
||||
return ok(
|
||||
await scanVersionInventories({
|
||||
leftPath: input.left_path,
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ export interface CrossVersionInvestigationExecution {
|
|||
|
||||
/** Run or resume a deterministic cross-version artifact investigation. */
|
||||
export const runCrossVersionInvestigation = async (
|
||||
input: CrossVersionInvestigationInput,
|
||||
input: unknown,
|
||||
policy: EvidenceFilePolicy,
|
||||
execution: CrossVersionInvestigationExecution,
|
||||
): Promise<Result<CrossVersionInvestigationOutcome, AnalysisError>> => {
|
||||
|
|
|
|||
|
|
@ -164,23 +164,19 @@ export class EnhancedTools {
|
|||
input: unknown,
|
||||
signal?: AbortSignal,
|
||||
): EnhancedResult {
|
||||
const parsed = enhancedInputSchemas[name].safeParse(input);
|
||||
if (name === "trace_call_path") {
|
||||
const parsed = enhancedInputSchemas.trace_call_path.safeParse(input);
|
||||
if (!parsed.success) return invalidEnhancedInput(name, parsed.error);
|
||||
return this.executeValidated({ name, input: parsed.data }, signal);
|
||||
}
|
||||
if (name === "trace_feature") {
|
||||
const parsed = enhancedInputSchemas.trace_feature.safeParse(input);
|
||||
if (!parsed.success) return invalidEnhancedInput(name, parsed.error);
|
||||
return this.executeValidated({ name, input: parsed.data }, signal);
|
||||
}
|
||||
const parsed = enhancedInputSchemas.find_code_for_string.safeParse(input);
|
||||
if (!parsed.success) return invalidEnhancedInput(name, parsed.error);
|
||||
if (name === "trace_call_path")
|
||||
return this.executeValidated(
|
||||
{
|
||||
name,
|
||||
input: enhancedInputSchemas.trace_call_path.parse(parsed.data),
|
||||
},
|
||||
signal,
|
||||
);
|
||||
return this.executeValidated(
|
||||
{
|
||||
name,
|
||||
input: enhancedInputSchemas[name].parse(parsed.data),
|
||||
},
|
||||
signal,
|
||||
);
|
||||
return this.executeValidated({ name, input: parsed.data }, signal);
|
||||
}
|
||||
|
||||
#executeFunctionAnalysis(
|
||||
|
|
@ -188,23 +184,14 @@ export class EnhancedTools {
|
|||
input: unknown,
|
||||
signal?: AbortSignal,
|
||||
): EnhancedResult {
|
||||
const parsed = enhancedInputSchemas[name].safeParse(input);
|
||||
if (name === "analyze_function") {
|
||||
const parsed = enhancedInputSchemas.analyze_function.safeParse(input);
|
||||
if (!parsed.success) return invalidEnhancedInput(name, parsed.error);
|
||||
return this.executeValidated({ name, input: parsed.data }, signal);
|
||||
}
|
||||
const parsed = enhancedInputSchemas.inspect_native_api.safeParse(input);
|
||||
if (!parsed.success) return invalidEnhancedInput(name, parsed.error);
|
||||
if (name === "analyze_function")
|
||||
return this.executeValidated(
|
||||
{
|
||||
name,
|
||||
input: enhancedInputSchemas.analyze_function.parse(parsed.data),
|
||||
},
|
||||
signal,
|
||||
);
|
||||
return this.executeValidated(
|
||||
{
|
||||
name,
|
||||
input: enhancedInputSchemas.inspect_native_api.parse(parsed.data),
|
||||
},
|
||||
signal,
|
||||
);
|
||||
return this.executeValidated({ name, input: parsed.data }, signal);
|
||||
}
|
||||
|
||||
async #analyzeFunction(
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
type EvidenceProvider,
|
||||
type EvidenceSubjectTarget,
|
||||
} from "../domain/evidence.js";
|
||||
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
|
||||
import { jsonValueSchema } from "../domain/jsonValue.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
|
||||
interface InventoryProjectionInput {
|
||||
|
|
@ -71,7 +71,7 @@ export const projectInventoryEvidence = <
|
|||
inventory_evidence_ids: [...result.source_evidence_ids],
|
||||
limits: jsonValueSchema.parse(parsed.data.limits),
|
||||
},
|
||||
result: jsonValueSchema.parse(result) as JsonValue,
|
||||
result: jsonValueSchema.parse(result),
|
||||
rawResult: null,
|
||||
confidence: "inferred",
|
||||
authority: "analyst-inference",
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
import type { z } from "zod";
|
||||
|
||||
import type { controlledReplayInputSchema } from "../domain/javascriptReplay.js";
|
||||
import type { JavaScriptReplayPolicy } from "./JavaScriptReplayPlanning.js";
|
||||
import type { ControlledReplayInput } from "../domain/javascriptReplay.js";
|
||||
import type { EnabledJavaScriptReplayPolicy } from "./JavaScriptReplayPlanning.js";
|
||||
|
||||
/** Build the exact permission request for one controlled replay operation. */
|
||||
export const replayPermissionRequest = (
|
||||
input: z.output<typeof controlledReplayInputSchema>,
|
||||
policy: JavaScriptReplayPolicy,
|
||||
input: ControlledReplayInput,
|
||||
policy: EnabledJavaScriptReplayPolicy,
|
||||
) => ({
|
||||
capability: "javascript_replay" as const,
|
||||
roots: [...input.left.modules, ...(input.right?.modules ?? [])].map(
|
||||
|
|
@ -22,5 +20,5 @@ export const replayPermissionRequest = (
|
|||
environment_names: [],
|
||||
network: "none" as const,
|
||||
mount: true,
|
||||
operation_identity: `run_controlled_replay:${input.plan_digest ?? "plan"}`,
|
||||
operation_identity: `run_controlled_replay:${input.mode === "plan" ? "plan" : input.plan_digest}`,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -37,9 +37,10 @@ export interface ReplayRuntimeFileIdentity {
|
|||
readonly sha256: string;
|
||||
}
|
||||
|
||||
export interface JavaScriptReplayPolicy {
|
||||
readonly enabled: boolean;
|
||||
readonly roots: readonly string[];
|
||||
/** Complete executable and source authority for enabled JavaScript replay. */
|
||||
export interface EnabledJavaScriptReplayPolicy {
|
||||
readonly status: "enabled";
|
||||
readonly roots: readonly [string, ...string[]];
|
||||
readonly nodePath: string;
|
||||
readonly bubblewrapPath: string;
|
||||
readonly systemdRunPath: string;
|
||||
|
|
@ -47,6 +48,11 @@ export interface JavaScriptReplayPolicy {
|
|||
readonly shellPath: string;
|
||||
}
|
||||
|
||||
/** Parsed JavaScript replay configuration. */
|
||||
export type JavaScriptReplayPolicy =
|
||||
| { readonly status: "disabled" }
|
||||
| EnabledJavaScriptReplayPolicy;
|
||||
|
||||
export interface JavaScriptReplayHost {
|
||||
readonly readSource: (
|
||||
path: string,
|
||||
|
|
@ -61,7 +67,7 @@ export interface JavaScriptReplayHost {
|
|||
nodePath: string,
|
||||
) => Promise<readonly ReplayRuntimeFileIdentity[]>;
|
||||
readonly seccompDigest: () => string;
|
||||
readonly probe: (policy: JavaScriptReplayPolicy) => Promise<void>;
|
||||
readonly probe: (policy: EnabledJavaScriptReplayPolicy) => Promise<void>;
|
||||
}
|
||||
|
||||
export interface PreparedReplayPlan {
|
||||
|
|
@ -73,7 +79,7 @@ export interface PreparedReplayPlan {
|
|||
export interface JavaScriptReplayRunner {
|
||||
readonly execute: (
|
||||
prepared: PreparedReplayPlan,
|
||||
policy: JavaScriptReplayPolicy,
|
||||
policy: EnabledJavaScriptReplayPolicy,
|
||||
signal?: AbortSignal,
|
||||
) => Promise<ReplayExecutionResult>;
|
||||
}
|
||||
|
|
@ -81,7 +87,7 @@ export interface JavaScriptReplayRunner {
|
|||
/** Read, normalize, and content-address every input to one replay experiment. */
|
||||
export const prepareReplayPlan = async (
|
||||
input: ControlledReplayInput,
|
||||
policy: JavaScriptReplayPolicy,
|
||||
policy: EnabledJavaScriptReplayPolicy,
|
||||
host: JavaScriptReplayHost,
|
||||
): Promise<PreparedReplayPlan> => {
|
||||
const cases = replayCases(input).map((item) => ({
|
||||
|
|
@ -243,13 +249,13 @@ const replayCases = (
|
|||
return state >>> 0;
|
||||
};
|
||||
const corpus = boundaryCorpus(input.generator.preset);
|
||||
const generated = Array.from(
|
||||
{ length: input.generator.count },
|
||||
(_, index) => ({
|
||||
case_id: `generated-${input.generator?.preset ?? "boundary"}-${String(index)}`,
|
||||
arguments: [corpus[next() % corpus.length] ?? ""] as JsonValue[],
|
||||
}),
|
||||
);
|
||||
const generated: {
|
||||
readonly case_id: string;
|
||||
readonly arguments: JsonValue[];
|
||||
}[] = Array.from({ length: input.generator.count }, (_, index) => ({
|
||||
case_id: `generated-${input.generator?.preset ?? "boundary"}-${String(index)}`,
|
||||
arguments: [corpus[next() % corpus.length] ?? ""],
|
||||
}));
|
||||
return uniqueCases([...explicit, ...generated]);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import {
|
|||
replayExecutionResultSchema,
|
||||
controlledReplayOutputSchema,
|
||||
replayEvidenceSchema,
|
||||
type ControlledReplayExecutionInput,
|
||||
type ControlledReplayInput,
|
||||
} from "../domain/javascriptReplay.js";
|
||||
import {
|
||||
AnalysisCapabilityUnavailableError,
|
||||
|
|
@ -26,6 +28,7 @@ import { replayPermissionRequest } from "./JavaScriptReplayPermission.js";
|
|||
import {
|
||||
digestBytes,
|
||||
prepareReplayPlan,
|
||||
type EnabledJavaScriptReplayPolicy,
|
||||
type JavaScriptReplayHost,
|
||||
type JavaScriptReplayPolicy,
|
||||
type JavaScriptReplayRunner,
|
||||
|
|
@ -36,7 +39,7 @@ const isAborted = (signal: AbortSignal | undefined): boolean =>
|
|||
signal?.aborted === true;
|
||||
|
||||
export interface JavaScriptReplayDependencies {
|
||||
readonly policy: JavaScriptReplayPolicy;
|
||||
readonly policy: () => JavaScriptReplayPolicy;
|
||||
readonly host: JavaScriptReplayHost;
|
||||
readonly runner: JavaScriptReplayRunner;
|
||||
readonly authority: PermissionAuthority | undefined;
|
||||
|
|
@ -63,14 +66,20 @@ export const runControlledReplay = async (
|
|||
/** Plan or execute replay input already parsed by a trusted adapter. */
|
||||
export const runControlledReplayValidated = async (
|
||||
dependencies: JavaScriptReplayDependencies,
|
||||
input: z.output<typeof controlledReplayInputSchema>,
|
||||
input: ControlledReplayInput,
|
||||
options: ExecutionOptions = {},
|
||||
): Promise<Result<JsonValue, AnalysisError>> => {
|
||||
const authorized = await authorizeReplay(dependencies, input, options.signal);
|
||||
const authorized = await authorizeReplay(
|
||||
dependencies,
|
||||
dependencies.policy(),
|
||||
input,
|
||||
options.signal,
|
||||
);
|
||||
if (!authorized.ok) return authorized;
|
||||
|
||||
const prepared = await prepareValidatedReplay(
|
||||
dependencies,
|
||||
authorized.value,
|
||||
input,
|
||||
options.signal,
|
||||
);
|
||||
|
|
@ -91,7 +100,7 @@ export const runControlledReplayValidated = async (
|
|||
if (input.plan_digest !== prepared.value.publicPlan.plan_digest)
|
||||
return err(
|
||||
new ReplayPlanStaleError(
|
||||
input.plan_digest ?? "",
|
||||
input.plan_digest,
|
||||
prepared.value.publicPlan.plan_digest,
|
||||
),
|
||||
);
|
||||
|
|
@ -103,12 +112,13 @@ export const runControlledReplayValidated = async (
|
|||
);
|
||||
if (!exportContext.ok) return exportContext;
|
||||
|
||||
const executed = await executeValidatedReplay(
|
||||
dependencies,
|
||||
prepared.value,
|
||||
exportContext.value,
|
||||
const executed = await executeValidatedReplay({
|
||||
runner: dependencies.runner,
|
||||
policy: authorized.value,
|
||||
prepared: prepared.value,
|
||||
exportContext: exportContext.value,
|
||||
options,
|
||||
);
|
||||
});
|
||||
if (!executed.ok) return executed;
|
||||
|
||||
try {
|
||||
|
|
@ -133,10 +143,11 @@ export const runControlledReplayValidated = async (
|
|||
|
||||
const authorizeReplay = async (
|
||||
dependencies: JavaScriptReplayDependencies,
|
||||
input: z.output<typeof controlledReplayInputSchema>,
|
||||
policy: JavaScriptReplayPolicy,
|
||||
input: ControlledReplayInput,
|
||||
signal: AbortSignal | undefined,
|
||||
): Promise<Result<true, AnalysisError>> => {
|
||||
if (!dependencies.policy.enabled)
|
||||
): Promise<Result<EnabledJavaScriptReplayPolicy, AnalysisError>> => {
|
||||
if (policy.status === "disabled")
|
||||
return err(
|
||||
new AnalysisCapabilityUnavailableError(
|
||||
"rea-javascript-replay",
|
||||
|
|
@ -154,7 +165,7 @@ const authorizeReplay = async (
|
|||
);
|
||||
if (isAborted(signal)) return err(new AnalysisCancelledError(OPERATION));
|
||||
|
||||
const request = replayPermissionRequest(input, dependencies.policy);
|
||||
const request = replayPermissionRequest(input, policy);
|
||||
const authorized =
|
||||
input.mode === "plan"
|
||||
? await dependencies.authority.explain(request, "read")
|
||||
|
|
@ -167,21 +178,20 @@ const authorizeReplay = async (
|
|||
cause: authorized.error,
|
||||
}),
|
||||
);
|
||||
return ok(true);
|
||||
return ok(policy);
|
||||
};
|
||||
|
||||
const prepareValidatedReplay = async (
|
||||
dependencies: JavaScriptReplayDependencies,
|
||||
input: z.output<typeof controlledReplayInputSchema>,
|
||||
policy: EnabledJavaScriptReplayPolicy,
|
||||
input: ControlledReplayInput,
|
||||
signal: AbortSignal | undefined,
|
||||
): Promise<
|
||||
Result<Awaited<ReturnType<typeof prepareReplayPlan>>, AnalysisError>
|
||||
> => {
|
||||
if (isAborted(signal)) return err(new AnalysisCancelledError(OPERATION));
|
||||
try {
|
||||
return ok(
|
||||
await prepareReplayPlan(input, dependencies.policy, dependencies.host),
|
||||
);
|
||||
return ok(await prepareReplayPlan(input, policy, dependencies.host));
|
||||
} catch (cause: unknown) {
|
||||
return err(
|
||||
new AnalysisCapabilityUnavailableError(
|
||||
|
|
@ -200,12 +210,10 @@ interface ReproducerExportContext {
|
|||
|
||||
const authorizeReproducerExport = async (
|
||||
authority: PermissionAuthority | undefined,
|
||||
input: z.output<typeof controlledReplayInputSchema>,
|
||||
input: ControlledReplayExecutionInput,
|
||||
prepared: Awaited<ReturnType<typeof prepareReplayPlan>>,
|
||||
): Promise<Result<ReproducerExportContext | undefined, AnalysisError>> => {
|
||||
if (input.reproducer_export === undefined) return ok(undefined);
|
||||
if (input.reproducer_export.approved !== true)
|
||||
return err(new AnalysisInputError(`${OPERATION}:reproducer_export`));
|
||||
if (authority === undefined)
|
||||
return err(
|
||||
new AnalysisCapabilityUnavailableError(
|
||||
|
|
@ -249,11 +257,16 @@ const authorizeReproducerExport = async (
|
|||
});
|
||||
};
|
||||
|
||||
interface ReplayExecutionContext {
|
||||
readonly runner: JavaScriptReplayRunner;
|
||||
readonly policy: EnabledJavaScriptReplayPolicy;
|
||||
readonly prepared: Awaited<ReturnType<typeof prepareReplayPlan>>;
|
||||
readonly exportContext: ReproducerExportContext | undefined;
|
||||
readonly options: ExecutionOptions;
|
||||
}
|
||||
|
||||
const executeValidatedReplay = async (
|
||||
dependencies: JavaScriptReplayDependencies,
|
||||
prepared: Awaited<ReturnType<typeof prepareReplayPlan>>,
|
||||
exportContext: ReproducerExportContext | undefined,
|
||||
options: ExecutionOptions,
|
||||
context: ReplayExecutionContext,
|
||||
): Promise<
|
||||
Result<
|
||||
{
|
||||
|
|
@ -263,6 +276,7 @@ const executeValidatedReplay = async (
|
|||
AnalysisError
|
||||
>
|
||||
> => {
|
||||
const { runner, policy, prepared, exportContext, options } = context;
|
||||
await options.progress?.report({
|
||||
phase: OPERATION,
|
||||
completed: 0,
|
||||
|
|
@ -274,11 +288,7 @@ const executeValidatedReplay = async (
|
|||
|
||||
try {
|
||||
let executed = replayExecutionResultSchema.parse(
|
||||
await dependencies.runner.execute(
|
||||
prepared,
|
||||
dependencies.policy,
|
||||
options.signal,
|
||||
),
|
||||
await runner.execute(prepared, policy, options.signal),
|
||||
);
|
||||
await options.progress?.report({
|
||||
phase: OPERATION,
|
||||
|
|
@ -505,9 +515,9 @@ const createReplayEvidence = (
|
|||
);
|
||||
};
|
||||
|
||||
const asJson = (value: unknown): JsonValue =>
|
||||
jsonValueSchema.parse(
|
||||
JSON.parse(
|
||||
JSON.stringify(controlledReplayOutputSchema.parse(value)),
|
||||
) as unknown,
|
||||
const asJson = (value: unknown): JsonValue => {
|
||||
const serialized: unknown = JSON.parse(
|
||||
JSON.stringify(controlledReplayOutputSchema.parse(value)),
|
||||
);
|
||||
return jsonValueSchema.parse(serialized);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -23,14 +23,23 @@ import { digestJson } from "./JavaScriptReplayPlanning.js";
|
|||
|
||||
const OPERATION = "plan_managed_runtime_correlation" as const;
|
||||
|
||||
export interface ManagedRuntimePolicy {
|
||||
readonly enabled: boolean;
|
||||
readonly roots: readonly string[];
|
||||
readonly executablePath: string;
|
||||
}
|
||||
/** Parsed availability and authority scope for managed runtime correlation. */
|
||||
export type ManagedRuntimePolicy =
|
||||
| { readonly status: "disabled" }
|
||||
| {
|
||||
readonly status: "enabled";
|
||||
readonly roots: readonly [string, ...string[]];
|
||||
readonly executablePath: string;
|
||||
};
|
||||
|
||||
/** A managed-runtime policy that grants no runtime-correlation authority. */
|
||||
export const MANAGED_RUNTIME_DISABLED = {
|
||||
status: "disabled",
|
||||
} as const satisfies ManagedRuntimePolicy;
|
||||
|
||||
/** Live policy and authority dependencies for managed runtime correlation. */
|
||||
export interface ManagedRuntimeCorrelationDependencies {
|
||||
readonly policy: ManagedRuntimePolicy;
|
||||
readonly policy: () => ManagedRuntimePolicy;
|
||||
readonly authority: PermissionAuthority | undefined;
|
||||
}
|
||||
|
||||
|
|
@ -53,7 +62,8 @@ export const planManagedRuntimeCorrelationEvidenceValidated = async (
|
|||
dependencies: ManagedRuntimeCorrelationDependencies,
|
||||
input: ManagedRuntimeCorrelationInput,
|
||||
): Promise<Result<Evidence, AnalysisError>> => {
|
||||
if (!dependencies.policy.enabled)
|
||||
const policy = dependencies.policy();
|
||||
if (policy.status === "disabled")
|
||||
return err(
|
||||
new AnalysisCapabilityUnavailableError(
|
||||
MANAGED_WORKFLOW_PROVIDER.id,
|
||||
|
|
@ -69,8 +79,7 @@ export const planManagedRuntimeCorrelationEvidenceValidated = async (
|
|||
"managed runtime permission policy is not configured",
|
||||
),
|
||||
);
|
||||
let staticEvidence: Evidence;
|
||||
let artifactPath: string;
|
||||
let staticEvidence: Evidence, artifactPath: string;
|
||||
try {
|
||||
staticEvidence = parseEvidence(input.static_members);
|
||||
artifactPath = artifactPathFor(staticEvidence);
|
||||
|
|
@ -81,7 +90,7 @@ export const planManagedRuntimeCorrelationEvidenceValidated = async (
|
|||
{
|
||||
capability: "managed_runtime",
|
||||
roots: [artifactPath],
|
||||
executables: [dependencies.policy.executablePath],
|
||||
executables: [policy.executablePath],
|
||||
environment_names: [],
|
||||
network: "none",
|
||||
mount: false,
|
||||
|
|
@ -101,7 +110,7 @@ export const planManagedRuntimeCorrelationEvidenceValidated = async (
|
|||
try {
|
||||
const result = planManagedRuntimeCorrelation(
|
||||
input,
|
||||
dependencies.policy.executablePath,
|
||||
policy.executablePath,
|
||||
authorized.value.grant_id,
|
||||
);
|
||||
return ok(
|
||||
|
|
|
|||
|
|
@ -16,7 +16,10 @@ import {
|
|||
type AnalysisError,
|
||||
} from "../domain/errors.js";
|
||||
import { createEvidence, type Evidence } from "../domain/evidence.js";
|
||||
import { controlledReplayOutputSchema } from "../domain/javascriptReplay.js";
|
||||
import {
|
||||
controlledReplayOutputSchema,
|
||||
type ControlledReplayExecutionOutput,
|
||||
} from "../domain/javascriptReplay.js";
|
||||
import type { ReplayPlan } from "../domain/javascriptReplay.js";
|
||||
import type { JavaScriptExportInstrumentation } from "../domain/javascriptExportInstrumentation.js";
|
||||
import {
|
||||
|
|
@ -89,7 +92,7 @@ export const executeNodeCharacterization = async (
|
|||
const plannedReplay = controlledReplayOutputSchema.parse(
|
||||
prepared.value.replay,
|
||||
);
|
||||
if (plannedReplay.phase !== "plan" || plannedReplay.plan === null)
|
||||
if (plannedReplay.phase !== "plan")
|
||||
return err(
|
||||
new AnalysisProtocolError(
|
||||
"Node characterization preparation did not produce a replay plan",
|
||||
|
|
@ -110,6 +113,13 @@ export const executeNodeCharacterization = async (
|
|||
options,
|
||||
);
|
||||
if (!executed.ok) return executed;
|
||||
const replayOutput = controlledReplayOutputSchema.parse(executed.value);
|
||||
if (replayOutput.phase !== "execute")
|
||||
return err(
|
||||
new AnalysisProtocolError(
|
||||
"Node characterization execution omitted replay Evidence",
|
||||
),
|
||||
);
|
||||
if (host.instrumentation === null)
|
||||
return err(
|
||||
new AnalysisProtocolError(
|
||||
|
|
@ -124,11 +134,8 @@ export const executeNodeCharacterization = async (
|
|||
plan: prepared.value.plan,
|
||||
transformation: host.instrumentation.manifest,
|
||||
transformation_evidence: prepared.value.transformation_evidence,
|
||||
evidence: createCharacterizationEvidence(
|
||||
prepared.value,
|
||||
controlledReplayOutputSchema.parse(executed.value),
|
||||
),
|
||||
replay: controlledReplayOutputSchema.parse(executed.value),
|
||||
evidence: createCharacterizationEvidence(prepared.value, replayOutput),
|
||||
replay: replayOutput,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
|
@ -164,11 +171,7 @@ const prepareValidated = async (
|
|||
);
|
||||
if (!replay.ok) return replay;
|
||||
const output = controlledReplayOutputSchema.parse(replay.value);
|
||||
if (
|
||||
output.phase !== "plan" ||
|
||||
output.plan === null ||
|
||||
host.instrumentation === null
|
||||
)
|
||||
if (output.phase !== "plan" || host.instrumentation === null)
|
||||
return err(
|
||||
new AnalysisProtocolError(
|
||||
"Node characterization preparation omitted committed plan evidence",
|
||||
|
|
@ -240,12 +243,8 @@ const createCharacterizationEvidence = (
|
|||
readonly plan: RuntimeCharacterizationPlan;
|
||||
readonly transformation_evidence: Evidence;
|
||||
},
|
||||
replay: z.output<typeof controlledReplayOutputSchema>,
|
||||
replay: ControlledReplayExecutionOutput,
|
||||
): Evidence => {
|
||||
if (replay.phase !== "execute" || replay.evidence === null)
|
||||
throw new AnalysisProtocolError(
|
||||
"Node characterization execution omitted replay Evidence",
|
||||
);
|
||||
return createEvidence(
|
||||
{
|
||||
path: prepared.plan.artifact.path,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { realpath } from "node:fs/promises";
|
||||
|
||||
import type {
|
||||
ProcessExecutionPolicy,
|
||||
EnabledProcessExecutionPolicy,
|
||||
ProcessScenario,
|
||||
} from "../domain/processCapture.js";
|
||||
import {
|
||||
|
|
@ -16,7 +16,7 @@ const isWithin = (candidate: string, root: string): boolean =>
|
|||
|
||||
export const assertRealPathAuthority = async (
|
||||
scenario: ProcessScenario,
|
||||
policy: ProcessExecutionPolicy,
|
||||
policy: EnabledProcessExecutionPolicy,
|
||||
): Promise<void> => {
|
||||
const executable = await realpath(scenario.executable);
|
||||
const executableRoots = await canonicalizeConfiguredRoots(
|
||||
|
|
|
|||
|
|
@ -420,6 +420,11 @@ export const prepareProcessCapture = async (
|
|||
readonly home: string;
|
||||
readonly before: SnapshotResult;
|
||||
}> => {
|
||||
if (policy.status === "disabled")
|
||||
throw new ProcessCaptureError("process capture is disabled", {
|
||||
userCategory: "permission_required",
|
||||
userMessage: processPolicyMessage("process capture is disabled"),
|
||||
});
|
||||
const decision = authorizeProcessScenario(scenario, policy);
|
||||
if (!decision.allowed)
|
||||
throw new ProcessCaptureError(decision.reason, {
|
||||
|
|
|
|||
|
|
@ -187,7 +187,8 @@ const readJson = async (path: string): Promise<unknown> => {
|
|||
"Process input file is too large. Reduce it below 64 MiB, then try again.",
|
||||
);
|
||||
try {
|
||||
return JSON.parse(bytes.toString("utf8")) as unknown;
|
||||
const parsed: unknown = JSON.parse(bytes.toString("utf8"));
|
||||
return parsed;
|
||||
} catch {
|
||||
throw new ProcessCliFailure(
|
||||
"invalid_input",
|
||||
|
|
|
|||
|
|
@ -31,6 +31,10 @@ import {
|
|||
import { ARTIFACT_GRAPH_PROVIDER } from "../application/InvestigationProviders.js";
|
||||
import { createEvidence } from "../domain/evidence.js";
|
||||
import { createArtifactInspection } from "../domain/artifactInspection.js";
|
||||
import {
|
||||
resolveArtifactIntegrityPolicy,
|
||||
resolveNativeMountPolicy,
|
||||
} from "../application/ArtifactInventory/policy.js";
|
||||
|
||||
const IDENTITY: ProviderIdentity = Object.freeze(ARTIFACT_GRAPH_PROVIDER);
|
||||
|
||||
|
|
@ -276,16 +280,19 @@ class ArtifactClient implements AnalysisClient {
|
|||
},
|
||||
{
|
||||
...(options?.signal === undefined ? {} : { signal: options.signal }),
|
||||
nativeMount: {
|
||||
nativeMountApproved: parsed.native_mount_approved === true,
|
||||
nativeMountEnabled: this.nativeMountEnabled,
|
||||
},
|
||||
integrity: {
|
||||
mode: parsed.integrity_policy,
|
||||
approved: parsed.integrity_continue_approved === true,
|
||||
enabled: this.integrityContinueEnabled,
|
||||
maxMismatches: parsed.max_integrity_mismatches,
|
||||
},
|
||||
nativeMount: resolveNativeMountPolicy(
|
||||
parsed.native_mount_approved === true,
|
||||
this.nativeMountEnabled,
|
||||
),
|
||||
integrity: resolveArtifactIntegrityPolicy(
|
||||
parsed.integrity_policy === "fail"
|
||||
? { mode: "fail" }
|
||||
: {
|
||||
mode: parsed.integrity_policy,
|
||||
maxMismatches: parsed.max_integrity_mismatches,
|
||||
},
|
||||
this.integrityContinueEnabled,
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,12 +21,14 @@ export interface SafeOutputFile {
|
|||
readonly bytesWritten: number;
|
||||
}
|
||||
|
||||
/** Cleanup facts established while rolling back an uncommitted tree. */
|
||||
export interface SafeOutputCleanup {
|
||||
readonly attempted: boolean;
|
||||
readonly verified: boolean;
|
||||
readonly residualPaths: readonly string[];
|
||||
}
|
||||
/** Cleanup state established while rolling back an uncommitted tree. */
|
||||
export type SafeOutputCleanup =
|
||||
| { readonly status: "not-required" }
|
||||
| { readonly status: "complete"; readonly residualPaths: readonly [] }
|
||||
| {
|
||||
readonly status: "incomplete";
|
||||
readonly residualPaths: readonly [string, ...string[]];
|
||||
};
|
||||
|
||||
/** Symlink-resistant materialization in an exclusively owned, initially absent tree. */
|
||||
export class SafeOutputTree {
|
||||
|
|
@ -37,9 +39,7 @@ export class SafeOutputTree {
|
|||
#totalBytes = 0;
|
||||
#published = false;
|
||||
#cleanup: SafeOutputCleanup = {
|
||||
attempted: false,
|
||||
verified: false,
|
||||
residualPaths: [],
|
||||
status: "not-required",
|
||||
};
|
||||
|
||||
private constructor(
|
||||
|
|
@ -208,11 +208,12 @@ export class SafeOutputTree {
|
|||
if (this.#published) return structuredClone(this.#cleanup);
|
||||
await rm(this.#stagingRoot, { recursive: true, force: true });
|
||||
const absent = await isAbsent(this.#stagingRoot);
|
||||
this.#cleanup = {
|
||||
attempted: true,
|
||||
verified: absent,
|
||||
residualPaths: absent ? [] : [basename(this.#stagingRoot)],
|
||||
};
|
||||
this.#cleanup = absent
|
||||
? { status: "complete", residualPaths: [] }
|
||||
: {
|
||||
status: "incomplete",
|
||||
residualPaths: [basename(this.#stagingRoot)],
|
||||
};
|
||||
if (!absent)
|
||||
throw new ArtifactReaderFailure(
|
||||
"integrity",
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ export const captureStorage = async (
|
|||
caches: caches.slice(0, maximum),
|
||||
limitations,
|
||||
})
|
||||
: { items: [], complete: false, truncated: false };
|
||||
: { items: [], complete: false as const, truncated: false };
|
||||
const truncated =
|
||||
[local.items, session.items, indexed, caches].some(
|
||||
(items) => items.length > maximum,
|
||||
|
|
|
|||
|
|
@ -58,6 +58,13 @@ interface CaptureStorageFingerprintInput {
|
|||
readonly limitations: string[];
|
||||
}
|
||||
|
||||
type StorageFingerprintCapture = {
|
||||
readonly items: readonly StorageFingerprint[];
|
||||
} & (
|
||||
| { readonly complete: true; readonly truncated: false }
|
||||
| { readonly complete: false; readonly truncated: boolean }
|
||||
);
|
||||
|
||||
/** Capture stable hashes of approved storage content without returning values. */
|
||||
export const captureStorageFingerprints = async ({
|
||||
context,
|
||||
|
|
@ -67,11 +74,7 @@ export const captureStorageFingerprints = async ({
|
|||
indexedDbNames,
|
||||
caches,
|
||||
limitations,
|
||||
}: CaptureStorageFingerprintInput): Promise<{
|
||||
readonly items: readonly StorageFingerprint[];
|
||||
readonly complete: boolean;
|
||||
readonly truncated: boolean;
|
||||
}> => {
|
||||
}: CaptureStorageFingerprintInput): Promise<StorageFingerprintCapture> => {
|
||||
const state: FingerprintState = {
|
||||
items: [],
|
||||
maximum: context.input.limits.max_storage_keys,
|
||||
|
|
@ -90,14 +93,17 @@ export const captureStorageFingerprints = async ({
|
|||
`${right.scope}:${right.identity_sha256}`,
|
||||
),
|
||||
);
|
||||
return {
|
||||
items: state.items,
|
||||
complete:
|
||||
state.complete &&
|
||||
!state.truncated &&
|
||||
state.items.every(({ complete }) => complete),
|
||||
truncated: state.truncated,
|
||||
};
|
||||
const complete =
|
||||
state.complete &&
|
||||
!state.truncated &&
|
||||
state.items.every(({ complete }) => complete);
|
||||
return complete
|
||||
? { items: state.items, complete: true, truncated: false }
|
||||
: {
|
||||
items: state.items,
|
||||
complete: false,
|
||||
truncated: state.truncated,
|
||||
};
|
||||
};
|
||||
|
||||
const addStorageItems = (
|
||||
|
|
|
|||
|
|
@ -99,7 +99,9 @@ const runElectronInspection = async (
|
|||
startedAt: string,
|
||||
): Promise<ElectronPageInspection> => {
|
||||
const frames = await authorizeInitialFrames(context, state);
|
||||
const main = frames[0]!;
|
||||
const main = frames.at(0);
|
||||
if (main === undefined)
|
||||
throw new BrowserObservationError("inspect_web_page", "target_not_allowed");
|
||||
state.mainFrameId = main.frame_id;
|
||||
await enableElectronCapture(context);
|
||||
await delayWithCancellation(
|
||||
|
|
|
|||
|
|
@ -16,12 +16,18 @@ const toolCatalog = TOOL_CONTRACTS.map((contract) => ({
|
|||
description: contract.description,
|
||||
effects: { ...contract.effects },
|
||||
annotations: contract.annotations,
|
||||
input_schema: z.toJSONSchema(contract.inputSchema, {
|
||||
unrepresentable: "any",
|
||||
}),
|
||||
output_schema: z.toJSONSchema(contract.outputSchema, {
|
||||
unrepresentable: "any",
|
||||
}),
|
||||
input_schema: {
|
||||
type: "object",
|
||||
...z.toJSONSchema(contract.inputSchema, {
|
||||
unrepresentable: "any",
|
||||
}),
|
||||
},
|
||||
output_schema: {
|
||||
type: "object",
|
||||
...z.toJSONSchema(contract.outputSchema, {
|
||||
unrepresentable: "any",
|
||||
}),
|
||||
},
|
||||
})).sort((left, right) => left.name.localeCompare(right.name));
|
||||
|
||||
const promptCatalog = PROMPT_CONTRACTS.map((contract) => ({
|
||||
|
|
|
|||
|
|
@ -489,7 +489,7 @@ const registerPlanManagedRuntimeCorrelation = (
|
|||
if (!authority.ok) return projectAnalysisError(authority.error);
|
||||
const result = await planManagedRuntimeCorrelationEvidence(
|
||||
{
|
||||
policy: config.value.managedRuntimePolicy,
|
||||
policy: () => config.value.managedRuntimePolicy,
|
||||
authority: authority.value,
|
||||
},
|
||||
input.value,
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ export const registerApplicationCommands = (
|
|||
if (!authority.ok) return projectAnalysisError(authority.error);
|
||||
const result = await runControlledReplay(
|
||||
{
|
||||
policy: config.value.javascriptReplayPolicy,
|
||||
policy: () => config.value.javascriptReplayPolicy,
|
||||
host: new SystemJavaScriptReplayHost(),
|
||||
runner: new LinuxJavaScriptReplayRunner(),
|
||||
authority: authority.value,
|
||||
|
|
@ -318,7 +318,7 @@ const replayDependencies = (
|
|||
config: AppConfig,
|
||||
authority: PermissionAuthority,
|
||||
) => ({
|
||||
policy: config.javascriptReplayPolicy,
|
||||
policy: () => config.javascriptReplayPolicy,
|
||||
host: new SystemJavaScriptReplayHost(),
|
||||
runner: new LinuxJavaScriptReplayRunner(),
|
||||
authority,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,11 @@ import { loadConfiguredPermissionAuthority } from "./application/PermissionConfi
|
|||
import { CdpBrowserProvider } from "./browser/CdpBrowserProvider.js";
|
||||
import { logCliCommand } from "./cliLogging.js";
|
||||
import { parseConfig } from "./config.js";
|
||||
import { AnalysisInputError, projectAnalysisError } from "./domain/errors.js";
|
||||
import {
|
||||
AnalysisCapabilityUnavailableError,
|
||||
AnalysisInputError,
|
||||
projectAnalysisError,
|
||||
} from "./domain/errors.js";
|
||||
import { browserCaptureComparisonInputSchema } from "./domain/browserCaptureComparison.js";
|
||||
import { discoverWebMcpToolsInputSchema } from "./domain/webMcpDiscovery.js";
|
||||
import {
|
||||
|
|
@ -92,7 +96,7 @@ const registerWebMcp = (
|
|||
}),
|
||||
run: ({ args, options }) =>
|
||||
logCliCommand(logger, "discover-webmcp-tools", async () => {
|
||||
const context = await browserContext();
|
||||
const context = await browserContext("discover_webmcp_tools");
|
||||
if (!context.ok) return context.error;
|
||||
const parsed = discoverWebMcpToolsInputSchema.safeParse({
|
||||
cdp_endpoint: args.endpoint,
|
||||
|
|
@ -197,7 +201,7 @@ const registerScreenshot = (
|
|||
}),
|
||||
run: ({ args, options }) =>
|
||||
logCliCommand(logger, "capture-web-screenshot", async () => {
|
||||
const context = await browserContext();
|
||||
const context = await browserContext("capture_web_screenshot");
|
||||
if (!context.ok) return context.error;
|
||||
const parsed = captureWebScreenshotInputSchema.safeParse({
|
||||
cdp_endpoint: args.endpoint,
|
||||
|
|
@ -263,9 +267,21 @@ const registerScreenshotDiff = (
|
|||
});
|
||||
};
|
||||
|
||||
const browserContext = async () => {
|
||||
const browserContext = async (operation: string) => {
|
||||
const config = parseConfig(process.env);
|
||||
if (!config.ok) return { ok: false as const, error: cliError(config.error) };
|
||||
const policy = config.value.browserObservationPolicy;
|
||||
if (policy.status === "disabled")
|
||||
return {
|
||||
ok: false as const,
|
||||
error: cliError(
|
||||
new AnalysisCapabilityUnavailableError(
|
||||
"rea-cdp-browser",
|
||||
operation,
|
||||
"browser observation is disabled; configure exact endpoints and origins before enabling it",
|
||||
),
|
||||
),
|
||||
};
|
||||
const authority = await loadConfiguredPermissionAuthority(config.value);
|
||||
if (!authority.ok)
|
||||
return { ok: false as const, error: cliError(authority.error) };
|
||||
|
|
@ -273,7 +289,7 @@ const browserContext = async () => {
|
|||
ok: true as const,
|
||||
authority: authority.value,
|
||||
provider: new CdpBrowserProvider(),
|
||||
allowedBrowserOrigins: config.value.browserAllowedOrigins,
|
||||
allowedBrowserOrigins: policy.allowedOrigins,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,11 @@ import {
|
|||
} from "./domain/browserObservation.js";
|
||||
import { analyzeWebBundleInputSchema } from "./domain/webBundleAnalysis.js";
|
||||
import { observeWebSessionInputSchema } from "./domain/browserSession.js";
|
||||
import { AnalysisInputError, projectAnalysisError } from "./domain/errors.js";
|
||||
import {
|
||||
AnalysisCapabilityUnavailableError,
|
||||
AnalysisInputError,
|
||||
projectAnalysisError,
|
||||
} from "./domain/errors.js";
|
||||
import type { JsonValue } from "./domain/jsonValue.js";
|
||||
import type { Logger } from "./logger.js";
|
||||
import { CLI_COMMANDS } from "./cliCommandNames.js";
|
||||
|
|
@ -81,7 +85,7 @@ const registerTargetList = (
|
|||
}),
|
||||
run: ({ args, options }) =>
|
||||
logCliCommand(logger, "list-browser-targets", async () => {
|
||||
const context = await browserContext();
|
||||
const context = await browserContext("list_browser_targets");
|
||||
if (!context.ok) return context.error;
|
||||
const parsed = listBrowserTargetsInputSchema.safeParse({
|
||||
cdp_endpoint: args.endpoint,
|
||||
|
|
@ -116,7 +120,7 @@ const registerPageInspection = (
|
|||
options: browserPageInspectionOptions,
|
||||
run: ({ args, options }) =>
|
||||
logCliCommand(logger, "inspect-web-page", async () => {
|
||||
const context = await browserContext();
|
||||
const context = await browserContext("inspect_web_page");
|
||||
if (!context.ok) return context.error;
|
||||
const parsed = inspectWebPageInputSchema.safeParse({
|
||||
cdp_endpoint: args.endpoint,
|
||||
|
|
@ -133,6 +137,7 @@ const registerPageInspection = (
|
|||
include_websocket_shapes: options.includeWebsocketShapes,
|
||||
websocket_shape_approved: options.websocketShapeApproved,
|
||||
include_script_sources: options.includeScriptSources,
|
||||
source_capture_approved: options.sourceCaptureApproved,
|
||||
include_storage_keys: options.includeStorageKeys,
|
||||
include_storage_fingerprints: options.includeStorageFingerprints,
|
||||
limits: {
|
||||
|
|
@ -227,7 +232,7 @@ const registerBundleAnalysis = (
|
|||
}),
|
||||
run: ({ args, options }) =>
|
||||
logCliCommand(logger, "analyze-web-bundle", async () => {
|
||||
const context = await browserContext();
|
||||
const context = await browserContext("analyze_web_bundle");
|
||||
if (!context.ok) return context.error;
|
||||
const parsed = analyzeWebBundleInputSchema.safeParse({
|
||||
cdp_endpoint: args.endpoint,
|
||||
|
|
@ -283,7 +288,7 @@ const registerObservationSession = (
|
|||
}),
|
||||
run: ({ args, options }) =>
|
||||
logCliCommand(logger, "observe-web-session", async () => {
|
||||
const context = await browserContext();
|
||||
const context = await browserContext("observe_web_session");
|
||||
if (!context.ok) return context.error;
|
||||
const parsed = observeWebSessionInputSchema.safeParse({
|
||||
cdp_endpoint: args.endpoint,
|
||||
|
|
@ -306,9 +311,21 @@ const registerObservationSession = (
|
|||
});
|
||||
};
|
||||
|
||||
const browserContext = async () => {
|
||||
const browserContext = async (operation: string) => {
|
||||
const config = parseConfig(process.env);
|
||||
if (!config.ok) return { ok: false as const, error: cliError(config.error) };
|
||||
const policy = config.value.browserObservationPolicy;
|
||||
if (policy.status === "disabled")
|
||||
return {
|
||||
ok: false as const,
|
||||
error: cliError(
|
||||
new AnalysisCapabilityUnavailableError(
|
||||
"rea-cdp-browser",
|
||||
operation,
|
||||
"browser observation is disabled; configure exact endpoints and origins before enabling it",
|
||||
),
|
||||
),
|
||||
};
|
||||
const authority = await loadConfiguredPermissionAuthority(config.value);
|
||||
if (!authority.ok)
|
||||
return { ok: false as const, error: cliError(authority.error) };
|
||||
|
|
@ -316,7 +333,7 @@ const browserContext = async () => {
|
|||
ok: true as const,
|
||||
authority: authority.value,
|
||||
provider: new CdpBrowserProvider(),
|
||||
allowedBrowserOrigins: config.value.browserAllowedOrigins,
|
||||
allowedBrowserOrigins: policy.allowedOrigins,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,11 @@ import {
|
|||
listElectronTargetsInputSchema,
|
||||
} from "./domain/electronObservation.js";
|
||||
import { electronActiveObservationInputSchema } from "./domain/electronActiveObservation.js";
|
||||
import { AnalysisInputError, projectAnalysisError } from "./domain/errors.js";
|
||||
import {
|
||||
AnalysisCapabilityUnavailableError,
|
||||
AnalysisInputError,
|
||||
projectAnalysisError,
|
||||
} from "./domain/errors.js";
|
||||
import type { JsonValue } from "./domain/jsonValue.js";
|
||||
import type { Logger } from "./logger.js";
|
||||
import { CLI_COMMANDS } from "./cliCommandNames.js";
|
||||
|
|
@ -150,7 +154,9 @@ const registerElectronTargetList = (
|
|||
}),
|
||||
run: ({ args, options }) =>
|
||||
logCliCommand(logger, "list-electron-targets", async () => {
|
||||
const context = await electronContext();
|
||||
const context = await electronObservationContext(
|
||||
"list_electron_targets",
|
||||
);
|
||||
if (!context.ok) return context.error;
|
||||
const parsed = listElectronTargetsInputSchema.safeParse({
|
||||
cdp_endpoint: args.endpoint,
|
||||
|
|
@ -186,7 +192,9 @@ const registerElectronPageInspection = (
|
|||
options: electronPageInspectionOptions,
|
||||
run: ({ args, options }) =>
|
||||
logCliCommand(logger, "inspect-electron-page", async () => {
|
||||
const context = await electronContext();
|
||||
const context = await electronObservationContext(
|
||||
"inspect_electron_page",
|
||||
);
|
||||
if (!context.ok) return context.error;
|
||||
const parsed = inspectElectronPageInputSchema.safeParse({
|
||||
cdp_endpoint: args.endpoint,
|
||||
|
|
@ -268,7 +276,27 @@ const electronContext = async () => {
|
|||
authority: authority.value,
|
||||
provider: new CdpElectronProvider(),
|
||||
activeProvider: new PlaywrightElectronActiveProvider(),
|
||||
allowedFileRoots: config.value.electronFileRoots,
|
||||
observationPolicy: config.value.electronObservationPolicy,
|
||||
};
|
||||
};
|
||||
|
||||
const electronObservationContext = async (operation: string) => {
|
||||
const context = await electronContext();
|
||||
if (!context.ok) return context;
|
||||
if (context.observationPolicy.status === "disabled")
|
||||
return {
|
||||
ok: false as const,
|
||||
error: cliError(
|
||||
new AnalysisCapabilityUnavailableError(
|
||||
"rea-cdp-electron",
|
||||
operation,
|
||||
"Electron observation is disabled; configure exact endpoints and file roots before enabling it",
|
||||
),
|
||||
),
|
||||
};
|
||||
return {
|
||||
...context,
|
||||
allowedFileRoots: context.observationPolicy.fileRoots,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -9,12 +9,17 @@ import { V8InspectorProvider } from "./browser/V8InspectorProvider.js";
|
|||
import { CLI_COMMANDS } from "./cliCommandNames.js";
|
||||
import { logCliCommand } from "./cliLogging.js";
|
||||
import { parseConfig } from "./config.js";
|
||||
import { v8InspectorLocationScopes } from "./config/passiveObservation.js";
|
||||
import {
|
||||
javascriptRuntimeKindSchema,
|
||||
listJavaScriptRuntimeTargetsInputSchema,
|
||||
observeJavaScriptRuntimeInputSchema,
|
||||
} from "./domain/javascriptRuntimeObservation.js";
|
||||
import { AnalysisInputError, projectAnalysisError } from "./domain/errors.js";
|
||||
import {
|
||||
AnalysisCapabilityUnavailableError,
|
||||
AnalysisInputError,
|
||||
projectAnalysisError,
|
||||
} from "./domain/errors.js";
|
||||
import type { JsonValue } from "./domain/jsonValue.js";
|
||||
import type { Logger } from "./logger.js";
|
||||
|
||||
|
|
@ -114,7 +119,9 @@ export const registerJavaScriptRuntimeObservationCommands = (
|
|||
logger,
|
||||
CLI_COMMANDS.listJavaScriptRuntimeTargets,
|
||||
async () => {
|
||||
const context = await runtimeContext();
|
||||
const context = await runtimeContext(
|
||||
"list_javascript_runtime_targets",
|
||||
);
|
||||
if (!context.ok) return context.error;
|
||||
const parsed = listJavaScriptRuntimeTargetsInputSchema.safeParse({
|
||||
inspector_endpoint: args.endpoint,
|
||||
|
|
@ -148,7 +155,7 @@ export const registerJavaScriptRuntimeObservationCommands = (
|
|||
options: observeOptionsSchema,
|
||||
run: ({ args, options }) =>
|
||||
logCliCommand(logger, CLI_COMMANDS.observeJavaScriptRuntime, async () => {
|
||||
const context = await runtimeContext();
|
||||
const context = await runtimeContext("observe_javascript_runtime");
|
||||
if (!context.ok) return context.error;
|
||||
const parsed = observeJavaScriptRuntimeInputSchema.safeParse({
|
||||
inspector_endpoint: args.endpoint,
|
||||
|
|
@ -178,18 +185,31 @@ export const registerJavaScriptRuntimeObservationCommands = (
|
|||
});
|
||||
};
|
||||
|
||||
const runtimeContext = async () => {
|
||||
const runtimeContext = async (operation: string) => {
|
||||
const config = parseConfig(process.env);
|
||||
if (!config.ok) return { ok: false as const, error: cliError(config.error) };
|
||||
const policy = config.value.v8InspectorObservationPolicy;
|
||||
if (policy.status === "disabled")
|
||||
return {
|
||||
ok: false as const,
|
||||
error: cliError(
|
||||
new AnalysisCapabilityUnavailableError(
|
||||
"rea-v8-inspector",
|
||||
operation,
|
||||
"V8 Inspector observation is disabled; configure exact endpoints and file or origin scopes before enabling it",
|
||||
),
|
||||
),
|
||||
};
|
||||
const authority = await loadConfiguredPermissionAuthority(config.value);
|
||||
if (!authority.ok)
|
||||
return { ok: false as const, error: cliError(authority.error) };
|
||||
const scopes = v8InspectorLocationScopes(policy.locations);
|
||||
return {
|
||||
ok: true as const,
|
||||
authority: authority.value,
|
||||
provider: new V8InspectorProvider(),
|
||||
allowedFileRoots: config.value.v8InspectorFileRoots,
|
||||
allowedOrigins: config.value.v8InspectorAllowedOrigins,
|
||||
allowedFileRoots: scopes.fileRoots,
|
||||
allowedOrigins: scopes.allowedOrigins,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -70,6 +70,10 @@ export const browserPageInspectionOptions = z.object({
|
|||
.boolean()
|
||||
.default(false)
|
||||
.describe("Include bounded JavaScript source text"),
|
||||
sourceCaptureApproved: z
|
||||
.boolean()
|
||||
.default(false)
|
||||
.describe("Approve capturing bounded script source text"),
|
||||
includeStorageKeys: z
|
||||
.boolean()
|
||||
.default(false)
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ export const sanitizeCliOutput = (output: string): string => {
|
|||
const trimmed = output.trimStart();
|
||||
if (trimmed.startsWith("{")) {
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed) as unknown;
|
||||
const parsed: unknown = JSON.parse(trimmed);
|
||||
const error = validationError(parsed);
|
||||
if (error !== undefined) {
|
||||
const safeError = {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,148 @@
|
|||
import { ConfigurationError } from "../domain/errors.js";
|
||||
import type { PermissionCeiling } from "../domain/permissionPolicy.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import {
|
||||
browserEndpointSchema,
|
||||
browserOriginSchema,
|
||||
} from "../domain/browserObservation.js";
|
||||
import type { Environment } from "./environment.js";
|
||||
import {
|
||||
parseAbsoluteRoots,
|
||||
parseBrowserArray,
|
||||
parseStringArray,
|
||||
} from "./parsers.js";
|
||||
import { browserNetworkScope, permissionScope } from "./permissions.js";
|
||||
|
||||
type BrowserScenarioTargetPolicy =
|
||||
| {
|
||||
readonly access: "launch";
|
||||
readonly executableRoots: readonly [string, ...string[]];
|
||||
}
|
||||
| {
|
||||
readonly access: "attach";
|
||||
readonly cdpEndpoints: readonly [string, ...string[]];
|
||||
}
|
||||
| {
|
||||
readonly access: "launch-or-attach";
|
||||
readonly executableRoots: readonly [string, ...string[]];
|
||||
readonly cdpEndpoints: readonly [string, ...string[]];
|
||||
};
|
||||
|
||||
/** Parsed availability and authority scope for controlled browser scenarios. */
|
||||
export type BrowserScenarioPolicy =
|
||||
| { readonly status: "disabled" }
|
||||
| {
|
||||
readonly status: "enabled";
|
||||
readonly target: BrowserScenarioTargetPolicy;
|
||||
readonly allowedOrigins: readonly [string, ...string[]];
|
||||
readonly allowedEnvironment: readonly string[];
|
||||
};
|
||||
|
||||
/** Parse controlled browser scenarios into a closed disabled or enabled policy. */
|
||||
export const parseBrowserScenarioPolicy = (
|
||||
env: Environment,
|
||||
): Result<BrowserScenarioPolicy, ConfigurationError> => {
|
||||
const executableRoots = parseAbsoluteRoots(
|
||||
env.REA_BROWSER_SCENARIO_EXECUTABLE_ROOTS_JSON,
|
||||
"REA_BROWSER_SCENARIO_EXECUTABLE_ROOTS_JSON",
|
||||
);
|
||||
if (!executableRoots.ok) return executableRoots;
|
||||
const cdpEndpoints = parseBrowserArray(
|
||||
env.REA_BROWSER_SCENARIO_CDP_ENDPOINTS_JSON,
|
||||
"REA_BROWSER_SCENARIO_CDP_ENDPOINTS_JSON",
|
||||
browserEndpointSchema,
|
||||
16,
|
||||
);
|
||||
if (!cdpEndpoints.ok) return cdpEndpoints;
|
||||
const allowedOrigins = parseBrowserArray(
|
||||
env.REA_BROWSER_SCENARIO_ALLOWED_ORIGINS_JSON,
|
||||
"REA_BROWSER_SCENARIO_ALLOWED_ORIGINS_JSON",
|
||||
browserOriginSchema,
|
||||
32,
|
||||
);
|
||||
if (!allowedOrigins.ok) return allowedOrigins;
|
||||
const allowedEnvironment = parseStringArray(
|
||||
env.REA_BROWSER_SCENARIO_ALLOWED_ENV_JSON,
|
||||
"REA_BROWSER_SCENARIO_ALLOWED_ENV_JSON",
|
||||
);
|
||||
if (!allowedEnvironment.ok) return allowedEnvironment;
|
||||
if (env.REA_BROWSER_SCENARIO_ENABLED !== "true")
|
||||
return ok({ status: "disabled" });
|
||||
const [firstOrigin, ...remainingOrigins] = allowedOrigins.value;
|
||||
if (firstOrigin === undefined)
|
||||
return err(
|
||||
new ConfigurationError(
|
||||
"REA_BROWSER_SCENARIO_ALLOWED_ORIGINS_JSON must encode at least one exact origin when browser scenarios are enabled",
|
||||
),
|
||||
);
|
||||
const target = parseTargetPolicy(executableRoots.value, cdpEndpoints.value);
|
||||
if (!target.ok) return target;
|
||||
return ok({
|
||||
status: "enabled",
|
||||
target: target.value,
|
||||
allowedOrigins: [firstOrigin, ...remainingOrigins],
|
||||
allowedEnvironment: allowedEnvironment.value,
|
||||
});
|
||||
};
|
||||
|
||||
/** Add the separately permissioned controlled-browser ceiling. */
|
||||
export const appendBrowserScenarioCeiling = (
|
||||
ceilings: PermissionCeiling[],
|
||||
policy: BrowserScenarioPolicy,
|
||||
): void => {
|
||||
if (policy.status === "disabled") return;
|
||||
const { executableRoots, cdpEndpoints } = targetScopes(policy.target);
|
||||
ceilings.push(
|
||||
permissionScope("browser_automate", [], {
|
||||
executables: executableRoots,
|
||||
environment_names: policy.allowedEnvironment,
|
||||
origins: [...cdpEndpoints, ...policy.allowedOrigins],
|
||||
network: browserNetworkScope(policy.allowedOrigins),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const parseTargetPolicy = (
|
||||
executableRoots: readonly string[],
|
||||
cdpEndpoints: readonly string[],
|
||||
): Result<BrowserScenarioTargetPolicy, ConfigurationError> => {
|
||||
const [firstExecutableRoot, ...remainingExecutableRoots] = executableRoots;
|
||||
const [firstCdpEndpoint, ...remainingCdpEndpoints] = cdpEndpoints;
|
||||
if (firstExecutableRoot !== undefined && firstCdpEndpoint !== undefined)
|
||||
return ok({
|
||||
access: "launch-or-attach",
|
||||
executableRoots: [firstExecutableRoot, ...remainingExecutableRoots],
|
||||
cdpEndpoints: [firstCdpEndpoint, ...remainingCdpEndpoints],
|
||||
});
|
||||
if (firstExecutableRoot !== undefined)
|
||||
return ok({
|
||||
access: "launch",
|
||||
executableRoots: [firstExecutableRoot, ...remainingExecutableRoots],
|
||||
});
|
||||
if (firstCdpEndpoint !== undefined)
|
||||
return ok({
|
||||
access: "attach",
|
||||
cdpEndpoints: [firstCdpEndpoint, ...remainingCdpEndpoints],
|
||||
});
|
||||
return err(
|
||||
new ConfigurationError(
|
||||
"Browser scenarios require at least one executable root or loopback CDP endpoint when enabled",
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const targetScopes = (
|
||||
target: BrowserScenarioTargetPolicy,
|
||||
): {
|
||||
readonly executableRoots: readonly string[];
|
||||
readonly cdpEndpoints: readonly string[];
|
||||
} => {
|
||||
switch (target.access) {
|
||||
case "launch":
|
||||
return { executableRoots: target.executableRoots, cdpEndpoints: [] };
|
||||
case "attach":
|
||||
return { executableRoots: [], cdpEndpoints: target.cdpEndpoints };
|
||||
case "launch-or-attach":
|
||||
return target;
|
||||
}
|
||||
};
|
||||
|
|
@ -1,21 +1,23 @@
|
|||
import { isAbsolute } from "node:path";
|
||||
|
||||
import type { PermissionCeiling } from "../domain/permissionPolicy.js";
|
||||
import { ConfigurationError } from "../domain/errors.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import { permissionScope } from "./permissions.js";
|
||||
import { parseStringArray } from "./parsers.js";
|
||||
import { parseAbsoluteRoots } from "./parsers.js";
|
||||
import type { Environment } from "./environment.js";
|
||||
|
||||
export interface ElectronAutomationArrays {
|
||||
readonly electronAutomationExecutableRoots: readonly string[];
|
||||
readonly electronAutomationApplicationRoots: readonly string[];
|
||||
}
|
||||
/** Parsed availability and authority scope for active Electron automation. */
|
||||
export type ElectronAutomationPolicy =
|
||||
| { readonly status: "disabled" }
|
||||
| {
|
||||
readonly status: "enabled";
|
||||
readonly executableRoots: readonly [string, ...string[]];
|
||||
readonly applicationRoots: readonly [string, ...string[]];
|
||||
};
|
||||
|
||||
/** Parse the exact filesystem roots admitted for active Electron automation. */
|
||||
export const parseElectronAutomationArrays = (
|
||||
/** Parse active Electron automation into a closed disabled or enabled policy. */
|
||||
export const parseElectronAutomationPolicy = (
|
||||
env: Environment,
|
||||
): Result<ElectronAutomationArrays, ConfigurationError> => {
|
||||
): Result<ElectronAutomationPolicy, ConfigurationError> => {
|
||||
const executableRoots = parseAbsoluteRoots(
|
||||
env.REA_ELECTRON_AUTOMATE_EXECUTABLE_ROOTS_JSON,
|
||||
"REA_ELECTRON_AUTOMATE_EXECUTABLE_ROOTS_JSON",
|
||||
|
|
@ -26,38 +28,41 @@ export const parseElectronAutomationArrays = (
|
|||
"REA_ELECTRON_AUTOMATE_APPLICATION_ROOTS_JSON",
|
||||
);
|
||||
if (!applicationRoots.ok) return applicationRoots;
|
||||
if (env.REA_ELECTRON_AUTOMATE_ENABLED !== "true")
|
||||
return ok({ status: "disabled" });
|
||||
const [firstExecutableRoot, ...remainingExecutableRoots] =
|
||||
executableRoots.value;
|
||||
if (firstExecutableRoot === undefined)
|
||||
return err(
|
||||
new ConfigurationError(
|
||||
"REA_ELECTRON_AUTOMATE_EXECUTABLE_ROOTS_JSON must encode at least one absolute root when Electron automation is enabled",
|
||||
),
|
||||
);
|
||||
const [firstApplicationRoot, ...remainingApplicationRoots] =
|
||||
applicationRoots.value;
|
||||
if (firstApplicationRoot === undefined)
|
||||
return err(
|
||||
new ConfigurationError(
|
||||
"REA_ELECTRON_AUTOMATE_APPLICATION_ROOTS_JSON must encode at least one absolute root when Electron automation is enabled",
|
||||
),
|
||||
);
|
||||
return ok({
|
||||
electronAutomationExecutableRoots: executableRoots.value,
|
||||
electronAutomationApplicationRoots: applicationRoots.value,
|
||||
status: "enabled",
|
||||
executableRoots: [firstExecutableRoot, ...remainingExecutableRoots],
|
||||
applicationRoots: [firstApplicationRoot, ...remainingApplicationRoots],
|
||||
});
|
||||
};
|
||||
|
||||
/** Add the separately permissioned, no-network Electron automation ceiling. */
|
||||
export const appendElectronAutomationCeiling = (
|
||||
ceilings: PermissionCeiling[],
|
||||
env: Environment,
|
||||
arrays: ElectronAutomationArrays,
|
||||
policy: ElectronAutomationPolicy,
|
||||
): void => {
|
||||
if (env.REA_ELECTRON_AUTOMATE_ENABLED !== "true") return;
|
||||
if (policy.status === "disabled") return;
|
||||
ceilings.push(
|
||||
permissionScope(
|
||||
"electron_automate",
|
||||
arrays.electronAutomationApplicationRoots,
|
||||
{
|
||||
executables: arrays.electronAutomationExecutableRoots,
|
||||
network: "external",
|
||||
},
|
||||
),
|
||||
permissionScope("electron_automate", policy.applicationRoots, {
|
||||
executables: policy.executableRoots,
|
||||
network: "external",
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const parseAbsoluteRoots = (
|
||||
value: string,
|
||||
name: string,
|
||||
): Result<readonly string[], ConfigurationError> => {
|
||||
const parsed = parseStringArray(value, name);
|
||||
if (!parsed.ok) return parsed;
|
||||
return parsed.value.some((root) => !isAbsolute(root))
|
||||
? err(new ConfigurationError(`${name} must encode absolute roots`))
|
||||
: parsed;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
import type {
|
||||
EnabledJavaScriptReplayPolicy,
|
||||
JavaScriptReplayPolicy,
|
||||
} from "../application/JavaScriptReplayPlanning.js";
|
||||
import { ConfigurationError } from "../domain/errors.js";
|
||||
import type { PermissionCeiling } from "../domain/permissionPolicy.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import type { Environment } from "./environment.js";
|
||||
import { parseAbsoluteRoots } from "./parsers.js";
|
||||
import { permissionScope } from "./permissions.js";
|
||||
|
||||
/** Parse controlled replay configuration into disabled or fully enabled state. */
|
||||
export const parseJavaScriptReplayPolicy = (
|
||||
env: Environment,
|
||||
): Result<JavaScriptReplayPolicy, ConfigurationError> => {
|
||||
const roots = parseAbsoluteRoots(
|
||||
env.REA_JAVASCRIPT_REPLAY_ROOTS_JSON,
|
||||
"REA_JAVASCRIPT_REPLAY_ROOTS_JSON",
|
||||
);
|
||||
if (!roots.ok) return roots;
|
||||
if (env.REA_JAVASCRIPT_REPLAY_ENABLED !== "true")
|
||||
return ok({ status: "disabled" });
|
||||
const [firstRoot, ...remainingRoots] = roots.value;
|
||||
if (firstRoot === undefined)
|
||||
return err(
|
||||
new ConfigurationError(
|
||||
"REA_JAVASCRIPT_REPLAY_ROOTS_JSON must encode at least one absolute root when JavaScript replay is enabled",
|
||||
),
|
||||
);
|
||||
return ok({
|
||||
status: "enabled",
|
||||
roots: [firstRoot, ...remainingRoots],
|
||||
nodePath: env.REA_JAVASCRIPT_REPLAY_NODE_PATH,
|
||||
bubblewrapPath: env.REA_JAVASCRIPT_REPLAY_BWRAP_PATH,
|
||||
systemdRunPath: env.REA_JAVASCRIPT_REPLAY_SYSTEMD_RUN_PATH,
|
||||
systemctlPath: env.REA_JAVASCRIPT_REPLAY_SYSTEMCTL_PATH,
|
||||
shellPath: env.REA_JAVASCRIPT_REPLAY_SHELL_PATH,
|
||||
});
|
||||
};
|
||||
|
||||
/** Add the exact controlled replay ceiling when replay is enabled. */
|
||||
export const appendJavaScriptReplayCeiling = (
|
||||
ceilings: PermissionCeiling[],
|
||||
policy: JavaScriptReplayPolicy,
|
||||
): void => {
|
||||
if (policy.status === "disabled") return;
|
||||
ceilings.push(
|
||||
permissionScope("javascript_replay", policy.roots, {
|
||||
executables: replayExecutables(policy),
|
||||
network: "none",
|
||||
mount: true,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const replayExecutables = (
|
||||
policy: EnabledJavaScriptReplayPolicy,
|
||||
): readonly string[] => [
|
||||
policy.nodePath,
|
||||
policy.bubblewrapPath,
|
||||
policy.systemdRunPath,
|
||||
policy.systemctlPath,
|
||||
policy.shellPath,
|
||||
];
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
import type { ManagedRuntimePolicy } from "../application/ManagedRuntimeCorrelationService.js";
|
||||
import { ConfigurationError } from "../domain/errors.js";
|
||||
import type { PermissionCeiling } from "../domain/permissionPolicy.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import type { Environment } from "./environment.js";
|
||||
import { permissionScope } from "./permissions.js";
|
||||
|
||||
/** Parse managed runtime correlation into a closed disabled or enabled policy. */
|
||||
export const parseManagedRuntimePolicy = (
|
||||
env: Environment,
|
||||
roots: readonly string[],
|
||||
): Result<ManagedRuntimePolicy, ConfigurationError> => {
|
||||
if (env.REA_MANAGED_RUNTIME_ENABLED !== "true")
|
||||
return ok({ status: "disabled" });
|
||||
const [firstRoot, ...remainingRoots] = roots;
|
||||
if (firstRoot === undefined)
|
||||
return err(
|
||||
new ConfigurationError(
|
||||
"REA_MANAGED_RUNTIME_ROOTS_JSON must encode at least one absolute root when managed runtime correlation is enabled",
|
||||
),
|
||||
);
|
||||
return ok({
|
||||
status: "enabled",
|
||||
roots: [firstRoot, ...remainingRoots],
|
||||
executablePath: env.REA_MANAGED_RUNTIME_EXECUTABLE_PATH,
|
||||
});
|
||||
};
|
||||
|
||||
/** Add the separately permissioned, no-network managed runtime ceiling. */
|
||||
export const appendManagedRuntimeCeiling = (
|
||||
ceilings: PermissionCeiling[],
|
||||
policy: ManagedRuntimePolicy,
|
||||
): void => {
|
||||
if (policy.status === "disabled") return;
|
||||
ceilings.push(
|
||||
permissionScope("managed_runtime", policy.roots, {
|
||||
executables: [policy.executablePath],
|
||||
network: "none",
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
|
@ -1,31 +1,48 @@
|
|||
import { isAbsolute } from "node:path";
|
||||
|
||||
import { ConfigurationError } from "../domain/errors.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import { ok, type Result } from "../domain/result.js";
|
||||
import { parseEnvironment, type Environment } from "./environment.js";
|
||||
import {
|
||||
parseStringArray,
|
||||
parseBrowserArray,
|
||||
parseElectronFileRoots,
|
||||
parseAbsoluteRoots,
|
||||
parseLoaderArgs,
|
||||
} from "./parsers.js";
|
||||
import {
|
||||
filePolicy,
|
||||
permissionScope,
|
||||
administratorGrants,
|
||||
browserNetworkScope,
|
||||
} from "./permissions.js";
|
||||
import type { AppConfig } from "./types.js";
|
||||
import type { PermissionCeiling } from "../domain/permissionPolicy.js";
|
||||
import {
|
||||
browserEndpointSchema,
|
||||
browserOriginSchema,
|
||||
} from "../domain/browserObservation.js";
|
||||
import {
|
||||
appendElectronAutomationCeiling,
|
||||
parseElectronAutomationArrays,
|
||||
type ElectronAutomationArrays,
|
||||
parseElectronAutomationPolicy,
|
||||
type ElectronAutomationPolicy,
|
||||
} from "./electronAutomation.js";
|
||||
import type { ManagedRuntimePolicy } from "../application/ManagedRuntimeCorrelationService.js";
|
||||
import {
|
||||
appendBrowserScenarioCeiling,
|
||||
parseBrowserScenarioPolicy,
|
||||
type BrowserScenarioPolicy,
|
||||
} from "./browserScenario.js";
|
||||
import {
|
||||
appendManagedRuntimeCeiling,
|
||||
parseManagedRuntimePolicy,
|
||||
} from "./managedRuntime.js";
|
||||
import {
|
||||
appendJavaScriptReplayCeiling,
|
||||
parseJavaScriptReplayPolicy,
|
||||
} from "./javascriptReplay.js";
|
||||
import type { JavaScriptReplayPolicy } from "../application/JavaScriptReplayPlanning.js";
|
||||
import {
|
||||
appendProcessCaptureCeiling,
|
||||
parseProcessExecutionPolicy,
|
||||
} from "./processCapture.js";
|
||||
import type { ProcessExecutionPolicy } from "../domain/processCapture.js";
|
||||
import {
|
||||
appendPassiveObservationCeilings,
|
||||
parsePassiveObservationPolicies,
|
||||
type PassiveObservationPolicies,
|
||||
} from "./passiveObservation.js";
|
||||
|
||||
const DEFAULT_HOPPER_LAUNCHER_PATH =
|
||||
"/Applications/Hopper Disassembler.app/Contents/MacOS/hopper";
|
||||
|
|
@ -34,66 +51,30 @@ const defaultHopperLauncherPath = (): string =>
|
|||
? "/opt/hopper/bin/Hopper"
|
||||
: DEFAULT_HOPPER_LAUNCHER_PATH;
|
||||
|
||||
interface ParsedArrays extends ElectronAutomationArrays {
|
||||
interface ParsedArrays {
|
||||
readonly loaderArgs: readonly string[];
|
||||
readonly executableRoots: readonly string[];
|
||||
readonly workingRoots: readonly string[];
|
||||
readonly allowedEnvironment: readonly string[];
|
||||
readonly evidenceRoots: readonly string[];
|
||||
readonly investigationInputRoots: readonly string[];
|
||||
readonly analysisSnapshotRoots: readonly string[];
|
||||
readonly referenceRoots: readonly string[];
|
||||
readonly secretPatterns: readonly string[];
|
||||
readonly browserEndpoints: readonly string[];
|
||||
readonly browserOrigins: readonly string[];
|
||||
readonly browserScenarioExecutableRoots: readonly string[];
|
||||
readonly browserScenarioEndpoints: readonly string[];
|
||||
readonly browserScenarioOrigins: readonly string[];
|
||||
readonly browserScenarioEnvironment: readonly string[];
|
||||
readonly electronEndpoints: readonly string[];
|
||||
readonly electronFileRoots: readonly string[];
|
||||
readonly v8InspectorEndpoints: readonly string[];
|
||||
readonly v8InspectorFileRoots: readonly string[];
|
||||
readonly v8InspectorOrigins: readonly string[];
|
||||
readonly javascriptReplayRoots: readonly string[];
|
||||
readonly managedRuntimeRoots: readonly string[];
|
||||
}
|
||||
|
||||
type ParsedObservationArrays = Pick<
|
||||
ParsedArrays,
|
||||
| "browserEndpoints"
|
||||
| "browserOrigins"
|
||||
| "browserScenarioExecutableRoots"
|
||||
| "browserScenarioEndpoints"
|
||||
| "browserScenarioOrigins"
|
||||
| "browserScenarioEnvironment"
|
||||
| "electronEndpoints"
|
||||
| "electronFileRoots"
|
||||
| "v8InspectorEndpoints"
|
||||
| "v8InspectorFileRoots"
|
||||
| "v8InspectorOrigins"
|
||||
>;
|
||||
interface ParsedPolicies {
|
||||
readonly passiveObservation: PassiveObservationPolicies;
|
||||
readonly processCapture: ProcessExecutionPolicy;
|
||||
readonly browserScenario: BrowserScenarioPolicy;
|
||||
readonly electronAutomation: ElectronAutomationPolicy;
|
||||
readonly javascriptReplay: JavaScriptReplayPolicy;
|
||||
readonly managedRuntime: ManagedRuntimePolicy;
|
||||
}
|
||||
|
||||
const parseAllArrays = (
|
||||
env: Environment,
|
||||
): Result<ParsedArrays, ConfigurationError> => {
|
||||
const loaderArgs = parseLoaderArgs(env.HOPPER_LOADER_ARGS_JSON);
|
||||
if (!loaderArgs.ok) return loaderArgs;
|
||||
const executableRoots = parseStringArray(
|
||||
env.REA_PROCESS_EXECUTABLE_ROOTS_JSON,
|
||||
"REA_PROCESS_EXECUTABLE_ROOTS_JSON",
|
||||
);
|
||||
if (!executableRoots.ok) return executableRoots;
|
||||
const workingRoots = parseStringArray(
|
||||
env.REA_PROCESS_WORKING_ROOTS_JSON,
|
||||
"REA_PROCESS_WORKING_ROOTS_JSON",
|
||||
);
|
||||
if (!workingRoots.ok) return workingRoots;
|
||||
const allowedEnvironment = parseStringArray(
|
||||
env.REA_PROCESS_ALLOWED_ENV_JSON,
|
||||
"REA_PROCESS_ALLOWED_ENV_JSON",
|
||||
);
|
||||
if (!allowedEnvironment.ok) return allowedEnvironment;
|
||||
const evidenceRoots = parseStringArray(
|
||||
env.REA_EVIDENCE_ROOTS_JSON,
|
||||
"REA_EVIDENCE_ROOTS_JSON",
|
||||
|
|
@ -119,15 +100,6 @@ const parseAllArrays = (
|
|||
"REA_REFERENCE_SECRET_PATTERNS_JSON",
|
||||
);
|
||||
if (!secretPatterns.ok) return secretPatterns;
|
||||
const observations = parseObservationArrays(env);
|
||||
if (!observations.ok) return observations;
|
||||
const electronAutomation = parseElectronAutomationArrays(env);
|
||||
if (!electronAutomation.ok) return electronAutomation;
|
||||
const javascriptReplayRoots = parseAbsoluteRoots(
|
||||
env.REA_JAVASCRIPT_REPLAY_ROOTS_JSON,
|
||||
"REA_JAVASCRIPT_REPLAY_ROOTS_JSON",
|
||||
);
|
||||
if (!javascriptReplayRoots.ok) return javascriptReplayRoots;
|
||||
const managedRuntimeRoots = parseAbsoluteRoots(
|
||||
env.REA_MANAGED_RUNTIME_ROOTS_JSON,
|
||||
"REA_MANAGED_RUNTIME_ROOTS_JSON",
|
||||
|
|
@ -135,195 +107,15 @@ const parseAllArrays = (
|
|||
if (!managedRuntimeRoots.ok) return managedRuntimeRoots;
|
||||
return ok({
|
||||
loaderArgs: loaderArgs.value,
|
||||
executableRoots: executableRoots.value,
|
||||
workingRoots: workingRoots.value,
|
||||
allowedEnvironment: allowedEnvironment.value,
|
||||
evidenceRoots: evidenceRoots.value,
|
||||
investigationInputRoots: investigationInputRoots.value,
|
||||
analysisSnapshotRoots: analysisSnapshotRoots.value,
|
||||
referenceRoots: referenceRoots.value,
|
||||
secretPatterns: secretPatterns.value,
|
||||
...observations.value,
|
||||
...electronAutomation.value,
|
||||
javascriptReplayRoots: javascriptReplayRoots.value,
|
||||
managedRuntimeRoots: managedRuntimeRoots.value,
|
||||
});
|
||||
};
|
||||
|
||||
const parseObservationArrays = (
|
||||
env: Environment,
|
||||
): Result<ParsedObservationArrays, ConfigurationError> => {
|
||||
const browserEndpoints = parseBrowserArray(
|
||||
env.REA_BROWSER_CDP_ENDPOINTS_JSON,
|
||||
"REA_BROWSER_CDP_ENDPOINTS_JSON",
|
||||
browserEndpointSchema,
|
||||
16,
|
||||
);
|
||||
if (!browserEndpoints.ok) return browserEndpoints;
|
||||
const browserOrigins = parseBrowserArray(
|
||||
env.REA_BROWSER_ALLOWED_ORIGINS_JSON,
|
||||
"REA_BROWSER_ALLOWED_ORIGINS_JSON",
|
||||
browserOriginSchema,
|
||||
32,
|
||||
);
|
||||
if (!browserOrigins.ok) return browserOrigins;
|
||||
const browserScenarioExecutableRoots = parseAbsoluteRoots(
|
||||
env.REA_BROWSER_SCENARIO_EXECUTABLE_ROOTS_JSON,
|
||||
"REA_BROWSER_SCENARIO_EXECUTABLE_ROOTS_JSON",
|
||||
);
|
||||
if (!browserScenarioExecutableRoots.ok) return browserScenarioExecutableRoots;
|
||||
const browserScenarioEndpoints = parseBrowserArray(
|
||||
env.REA_BROWSER_SCENARIO_CDP_ENDPOINTS_JSON,
|
||||
"REA_BROWSER_SCENARIO_CDP_ENDPOINTS_JSON",
|
||||
browserEndpointSchema,
|
||||
16,
|
||||
);
|
||||
if (!browserScenarioEndpoints.ok) return browserScenarioEndpoints;
|
||||
const browserScenarioOrigins = parseBrowserArray(
|
||||
env.REA_BROWSER_SCENARIO_ALLOWED_ORIGINS_JSON,
|
||||
"REA_BROWSER_SCENARIO_ALLOWED_ORIGINS_JSON",
|
||||
browserOriginSchema,
|
||||
32,
|
||||
);
|
||||
if (!browserScenarioOrigins.ok) return browserScenarioOrigins;
|
||||
const browserScenarioEnvironment = parseStringArray(
|
||||
env.REA_BROWSER_SCENARIO_ALLOWED_ENV_JSON,
|
||||
"REA_BROWSER_SCENARIO_ALLOWED_ENV_JSON",
|
||||
);
|
||||
if (!browserScenarioEnvironment.ok) return browserScenarioEnvironment;
|
||||
const electronEndpoints = parseBrowserArray(
|
||||
env.REA_ELECTRON_CDP_ENDPOINTS_JSON,
|
||||
"REA_ELECTRON_CDP_ENDPOINTS_JSON",
|
||||
browserEndpointSchema,
|
||||
16,
|
||||
);
|
||||
if (!electronEndpoints.ok) return electronEndpoints;
|
||||
const electronFileRoots = parseElectronFileRoots(
|
||||
env.REA_ELECTRON_FILE_ROOTS_JSON,
|
||||
);
|
||||
if (!electronFileRoots.ok) return electronFileRoots;
|
||||
const v8InspectorEndpoints = parseBrowserArray(
|
||||
env.REA_V8_INSPECTOR_ENDPOINTS_JSON,
|
||||
"REA_V8_INSPECTOR_ENDPOINTS_JSON",
|
||||
browserEndpointSchema,
|
||||
16,
|
||||
);
|
||||
if (!v8InspectorEndpoints.ok) return v8InspectorEndpoints;
|
||||
const v8InspectorFileRoots = parseAbsoluteRoots(
|
||||
env.REA_V8_INSPECTOR_FILE_ROOTS_JSON,
|
||||
"REA_V8_INSPECTOR_FILE_ROOTS_JSON",
|
||||
);
|
||||
if (!v8InspectorFileRoots.ok) return v8InspectorFileRoots;
|
||||
const v8InspectorOrigins = parseBrowserArray(
|
||||
env.REA_V8_INSPECTOR_ALLOWED_ORIGINS_JSON,
|
||||
"REA_V8_INSPECTOR_ALLOWED_ORIGINS_JSON",
|
||||
browserOriginSchema,
|
||||
32,
|
||||
);
|
||||
if (!v8InspectorOrigins.ok) return v8InspectorOrigins;
|
||||
return ok({
|
||||
browserEndpoints: browserEndpoints.value,
|
||||
browserOrigins: browserOrigins.value,
|
||||
browserScenarioExecutableRoots: browserScenarioExecutableRoots.value,
|
||||
browserScenarioEndpoints: browserScenarioEndpoints.value,
|
||||
browserScenarioOrigins: browserScenarioOrigins.value,
|
||||
browserScenarioEnvironment: browserScenarioEnvironment.value,
|
||||
electronEndpoints: electronEndpoints.value,
|
||||
electronFileRoots: electronFileRoots.value,
|
||||
v8InspectorEndpoints: v8InspectorEndpoints.value,
|
||||
v8InspectorFileRoots: v8InspectorFileRoots.value,
|
||||
v8InspectorOrigins: v8InspectorOrigins.value,
|
||||
});
|
||||
};
|
||||
|
||||
const parseAbsoluteRoots = (
|
||||
value: string,
|
||||
name: string,
|
||||
): Result<readonly string[], ConfigurationError> => {
|
||||
const parsed = parseStringArray(value, name);
|
||||
if (!parsed.ok) return parsed;
|
||||
return parsed.value.some((root) => !isAbsolute(root))
|
||||
? err(new ConfigurationError(`${name} must encode absolute roots`))
|
||||
: parsed;
|
||||
};
|
||||
|
||||
const appendProcessCaptureCeiling = (
|
||||
ceilings: PermissionCeiling[],
|
||||
env: Environment,
|
||||
arrays: ParsedArrays,
|
||||
): void => {
|
||||
if (env.REA_PROCESS_CAPTURE_ENABLED !== "true") return;
|
||||
ceilings.push(
|
||||
permissionScope("process_capture", arrays.workingRoots, {
|
||||
executables: arrays.executableRoots,
|
||||
environment_names: arrays.allowedEnvironment,
|
||||
network:
|
||||
env.REA_PROCESS_ALLOW_EXTERNAL_NETWORK === "true" ? "external" : "none",
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const appendBrowserObservationCeiling = (
|
||||
ceilings: PermissionCeiling[],
|
||||
env: Environment,
|
||||
arrays: ParsedArrays,
|
||||
): void => {
|
||||
if (env.REA_BROWSER_OBSERVE_ENABLED !== "true") return;
|
||||
ceilings.push(
|
||||
permissionScope("browser_observe", [], {
|
||||
origins: [...arrays.browserEndpoints, ...arrays.browserOrigins],
|
||||
network: browserNetworkScope(arrays.browserOrigins),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const appendBrowserScenarioCeiling = (
|
||||
ceilings: PermissionCeiling[],
|
||||
env: Environment,
|
||||
arrays: ParsedArrays,
|
||||
): void => {
|
||||
if (env.REA_BROWSER_SCENARIO_ENABLED !== "true") return;
|
||||
ceilings.push(
|
||||
permissionScope("browser_automate", [], {
|
||||
executables: arrays.browserScenarioExecutableRoots,
|
||||
environment_names: arrays.browserScenarioEnvironment,
|
||||
origins: [
|
||||
...arrays.browserScenarioEndpoints,
|
||||
...arrays.browserScenarioOrigins,
|
||||
],
|
||||
network: browserNetworkScope(arrays.browserScenarioOrigins),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const appendElectronObservationCeiling = (
|
||||
ceilings: PermissionCeiling[],
|
||||
env: Environment,
|
||||
arrays: ParsedArrays,
|
||||
): void => {
|
||||
if (env.REA_ELECTRON_OBSERVE_ENABLED !== "true") return;
|
||||
ceilings.push(
|
||||
permissionScope("electron_observe", arrays.electronFileRoots, {
|
||||
origins: arrays.electronEndpoints,
|
||||
network: "loopback",
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const appendV8InspectorObservationCeiling = (
|
||||
ceilings: PermissionCeiling[],
|
||||
env: Environment,
|
||||
arrays: ParsedArrays,
|
||||
): void => {
|
||||
if (env.REA_V8_INSPECTOR_OBSERVE_ENABLED !== "true") return;
|
||||
ceilings.push(
|
||||
permissionScope("v8_inspector_observe", arrays.v8InspectorFileRoots, {
|
||||
origins: [...arrays.v8InspectorEndpoints, ...arrays.v8InspectorOrigins],
|
||||
network: "loopback",
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const appendNativeMountCeiling = (
|
||||
ceilings: PermissionCeiling[],
|
||||
env: Environment,
|
||||
|
|
@ -332,44 +124,10 @@ const appendNativeMountCeiling = (
|
|||
ceilings.push(permissionScope("native_mount", [], { mount: true }));
|
||||
};
|
||||
|
||||
const appendJavaScriptReplayCeiling = (
|
||||
ceilings: PermissionCeiling[],
|
||||
env: Environment,
|
||||
arrays: ParsedArrays,
|
||||
): void => {
|
||||
if (env.REA_JAVASCRIPT_REPLAY_ENABLED !== "true") return;
|
||||
ceilings.push(
|
||||
permissionScope("javascript_replay", arrays.javascriptReplayRoots, {
|
||||
executables: [
|
||||
env.REA_JAVASCRIPT_REPLAY_NODE_PATH,
|
||||
env.REA_JAVASCRIPT_REPLAY_BWRAP_PATH,
|
||||
env.REA_JAVASCRIPT_REPLAY_SYSTEMD_RUN_PATH,
|
||||
env.REA_JAVASCRIPT_REPLAY_SYSTEMCTL_PATH,
|
||||
env.REA_JAVASCRIPT_REPLAY_SHELL_PATH,
|
||||
],
|
||||
network: "none",
|
||||
mount: true,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const appendManagedRuntimeCeiling = (
|
||||
ceilings: PermissionCeiling[],
|
||||
env: Environment,
|
||||
arrays: ParsedArrays,
|
||||
): void => {
|
||||
if (env.REA_MANAGED_RUNTIME_ENABLED !== "true") return;
|
||||
ceilings.push(
|
||||
permissionScope("managed_runtime", arrays.managedRuntimeRoots, {
|
||||
executables: [env.REA_MANAGED_RUNTIME_EXECUTABLE_PATH],
|
||||
network: "none",
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const buildPermissionCeilings = (
|
||||
env: Environment,
|
||||
arrays: ParsedArrays,
|
||||
policies: ParsedPolicies,
|
||||
): readonly PermissionCeiling[] => {
|
||||
const ceilings: PermissionCeiling[] = [
|
||||
permissionScope("evidence_read", arrays.evidenceRoots),
|
||||
|
|
@ -382,15 +140,13 @@ const buildPermissionCeilings = (
|
|||
permissionScope("artifact_extract", ["/"]),
|
||||
permissionScope("reference_read", arrays.referenceRoots),
|
||||
];
|
||||
appendProcessCaptureCeiling(ceilings, env, arrays);
|
||||
appendBrowserObservationCeiling(ceilings, env, arrays);
|
||||
appendBrowserScenarioCeiling(ceilings, env, arrays);
|
||||
appendElectronObservationCeiling(ceilings, env, arrays);
|
||||
appendElectronAutomationCeiling(ceilings, env, arrays);
|
||||
appendV8InspectorObservationCeiling(ceilings, env, arrays);
|
||||
appendProcessCaptureCeiling(ceilings, policies.processCapture);
|
||||
appendPassiveObservationCeilings(ceilings, policies.passiveObservation);
|
||||
appendBrowserScenarioCeiling(ceilings, policies.browserScenario);
|
||||
appendElectronAutomationCeiling(ceilings, policies.electronAutomation);
|
||||
appendNativeMountCeiling(ceilings, env);
|
||||
appendJavaScriptReplayCeiling(ceilings, env, arrays);
|
||||
appendManagedRuntimeCeiling(ceilings, env, arrays);
|
||||
appendJavaScriptReplayCeiling(ceilings, policies.javascriptReplay);
|
||||
appendManagedRuntimeCeiling(ceilings, policies.managedRuntime);
|
||||
return ceilings;
|
||||
};
|
||||
|
||||
|
|
@ -398,6 +154,7 @@ const buildAppConfig = (
|
|||
env: Environment,
|
||||
arrays: ParsedArrays,
|
||||
permissionCeilings: readonly PermissionCeiling[],
|
||||
policies: ParsedPolicies,
|
||||
): AppConfig => ({
|
||||
analysisProvider: env.REA_ANALYSIS_PROVIDER,
|
||||
ghidraInstallDir: env.GHIDRA_INSTALL_DIR,
|
||||
|
|
@ -408,13 +165,7 @@ const buildAppConfig = (
|
|||
hopperTargetKind: env.HOPPER_TARGET_KIND,
|
||||
hopperLoaderArgs: arrays.loaderArgs,
|
||||
logLevel: env.REA_LOG_LEVEL,
|
||||
processExecutionPolicy: {
|
||||
enabled: env.REA_PROCESS_CAPTURE_ENABLED === "true",
|
||||
executableRoots: arrays.executableRoots,
|
||||
workingRoots: arrays.workingRoots,
|
||||
allowedEnvironment: arrays.allowedEnvironment,
|
||||
allowExternalNetwork: env.REA_PROCESS_ALLOW_EXTERNAL_NETWORK === "true",
|
||||
},
|
||||
processExecutionPolicy: policies.processCapture,
|
||||
artifactNativeMountEnabled: env.REA_ARTIFACT_NATIVE_MOUNT_ENABLED === "true",
|
||||
artifactIntegrityContinueEnabled:
|
||||
env.REA_ARTIFACT_INTEGRITY_CONTINUE_ENABLED === "true",
|
||||
|
|
@ -429,43 +180,13 @@ const buildAppConfig = (
|
|||
maxDepth: 32,
|
||||
maxPathBytes: 4_096,
|
||||
},
|
||||
browserObservationEnabled: env.REA_BROWSER_OBSERVE_ENABLED === "true",
|
||||
browserCdpEndpoints: arrays.browserEndpoints,
|
||||
browserAllowedOrigins: arrays.browserOrigins,
|
||||
browserScenarioPolicy: {
|
||||
enabled: env.REA_BROWSER_SCENARIO_ENABLED === "true",
|
||||
executableRoots: arrays.browserScenarioExecutableRoots,
|
||||
cdpEndpoints: arrays.browserScenarioEndpoints,
|
||||
allowedOrigins: arrays.browserScenarioOrigins,
|
||||
allowedEnvironment: arrays.browserScenarioEnvironment,
|
||||
},
|
||||
electronObservationEnabled: env.REA_ELECTRON_OBSERVE_ENABLED === "true",
|
||||
electronCdpEndpoints: arrays.electronEndpoints,
|
||||
electronFileRoots: arrays.electronFileRoots,
|
||||
electronAutomationPolicy: {
|
||||
enabled: env.REA_ELECTRON_AUTOMATE_ENABLED === "true",
|
||||
executableRoots: arrays.electronAutomationExecutableRoots,
|
||||
applicationRoots: arrays.electronAutomationApplicationRoots,
|
||||
},
|
||||
v8InspectorObservationEnabled:
|
||||
env.REA_V8_INSPECTOR_OBSERVE_ENABLED === "true",
|
||||
v8InspectorEndpoints: arrays.v8InspectorEndpoints,
|
||||
v8InspectorFileRoots: arrays.v8InspectorFileRoots,
|
||||
v8InspectorAllowedOrigins: arrays.v8InspectorOrigins,
|
||||
javascriptReplayPolicy: {
|
||||
enabled: env.REA_JAVASCRIPT_REPLAY_ENABLED === "true",
|
||||
roots: arrays.javascriptReplayRoots,
|
||||
nodePath: env.REA_JAVASCRIPT_REPLAY_NODE_PATH,
|
||||
bubblewrapPath: env.REA_JAVASCRIPT_REPLAY_BWRAP_PATH,
|
||||
systemdRunPath: env.REA_JAVASCRIPT_REPLAY_SYSTEMD_RUN_PATH,
|
||||
systemctlPath: env.REA_JAVASCRIPT_REPLAY_SYSTEMCTL_PATH,
|
||||
shellPath: env.REA_JAVASCRIPT_REPLAY_SHELL_PATH,
|
||||
},
|
||||
managedRuntimePolicy: {
|
||||
enabled: env.REA_MANAGED_RUNTIME_ENABLED === "true",
|
||||
roots: arrays.managedRuntimeRoots,
|
||||
executablePath: env.REA_MANAGED_RUNTIME_EXECUTABLE_PATH,
|
||||
},
|
||||
browserObservationPolicy: policies.passiveObservation.browser,
|
||||
browserScenarioPolicy: policies.browserScenario,
|
||||
electronObservationPolicy: policies.passiveObservation.electron,
|
||||
electronAutomationPolicy: policies.electronAutomation,
|
||||
v8InspectorObservationPolicy: policies.passiveObservation.v8Inspector,
|
||||
javascriptReplayPolicy: policies.javascriptReplay,
|
||||
managedRuntimePolicy: policies.managedRuntime,
|
||||
permissionCeilings,
|
||||
administratorPermissionGrants: administratorGrants(
|
||||
permissionCeilings.filter(
|
||||
|
|
@ -495,11 +216,44 @@ export const parseConfig = (
|
|||
if (!envResult.ok) return envResult;
|
||||
const arraysResult = parseAllArrays(envResult.value);
|
||||
if (!arraysResult.ok) return arraysResult;
|
||||
const passiveObservationPolicies = parsePassiveObservationPolicies(
|
||||
envResult.value,
|
||||
);
|
||||
if (!passiveObservationPolicies.ok) return passiveObservationPolicies;
|
||||
const processCapturePolicy = parseProcessExecutionPolicy(envResult.value);
|
||||
if (!processCapturePolicy.ok) return processCapturePolicy;
|
||||
const browserScenarioPolicy = parseBrowserScenarioPolicy(envResult.value);
|
||||
if (!browserScenarioPolicy.ok) return browserScenarioPolicy;
|
||||
const electronAutomationPolicy = parseElectronAutomationPolicy(
|
||||
envResult.value,
|
||||
);
|
||||
if (!electronAutomationPolicy.ok) return electronAutomationPolicy;
|
||||
const javascriptReplayPolicy = parseJavaScriptReplayPolicy(envResult.value);
|
||||
if (!javascriptReplayPolicy.ok) return javascriptReplayPolicy;
|
||||
const managedRuntimePolicy = parseManagedRuntimePolicy(
|
||||
envResult.value,
|
||||
arraysResult.value.managedRuntimeRoots,
|
||||
);
|
||||
if (!managedRuntimePolicy.ok) return managedRuntimePolicy;
|
||||
const policies = {
|
||||
passiveObservation: passiveObservationPolicies.value,
|
||||
processCapture: processCapturePolicy.value,
|
||||
browserScenario: browserScenarioPolicy.value,
|
||||
electronAutomation: electronAutomationPolicy.value,
|
||||
javascriptReplay: javascriptReplayPolicy.value,
|
||||
managedRuntime: managedRuntimePolicy.value,
|
||||
} satisfies ParsedPolicies;
|
||||
const permissionCeilings = buildPermissionCeilings(
|
||||
envResult.value,
|
||||
arraysResult.value,
|
||||
policies,
|
||||
);
|
||||
return ok(
|
||||
buildAppConfig(envResult.value, arraysResult.value, permissionCeilings),
|
||||
buildAppConfig(
|
||||
envResult.value,
|
||||
arraysResult.value,
|
||||
permissionCeilings,
|
||||
policies,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { z } from "zod";
|
||||
import { isAbsolute } from "node:path";
|
||||
|
||||
import { ConfigurationError } from "../domain/errors.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import {} from "../domain/browserObservation.js";
|
||||
import { electronFileRootsSchema } from "../domain/electronObservation.js";
|
||||
|
||||
export const parseStringArray = (
|
||||
|
|
@ -29,6 +29,18 @@ export const parseStringArray = (
|
|||
}
|
||||
};
|
||||
|
||||
/** Parse a JSON string array whose members must all be absolute paths. */
|
||||
export const parseAbsoluteRoots = (
|
||||
encoded: string,
|
||||
name: string,
|
||||
): Result<readonly string[], ConfigurationError> => {
|
||||
const parsed = parseStringArray(encoded, name);
|
||||
if (!parsed.ok) return parsed;
|
||||
return parsed.value.some((root) => !isAbsolute(root))
|
||||
? err(new ConfigurationError(`${name} must encode absolute roots`))
|
||||
: parsed;
|
||||
};
|
||||
|
||||
export const parseBrowserArray = (
|
||||
encoded: string,
|
||||
name: string,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,294 @@
|
|||
import { ConfigurationError } from "../domain/errors.js";
|
||||
import type { PermissionCeiling } from "../domain/permissionPolicy.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import {
|
||||
browserEndpointSchema,
|
||||
browserOriginSchema,
|
||||
} from "../domain/browserObservation.js";
|
||||
import type { Environment } from "./environment.js";
|
||||
import {
|
||||
parseAbsoluteRoots,
|
||||
parseBrowserArray,
|
||||
parseElectronFileRoots,
|
||||
} from "./parsers.js";
|
||||
import { browserNetworkScope, permissionScope } from "./permissions.js";
|
||||
|
||||
type DisabledObservationPolicy = { readonly status: "disabled" };
|
||||
|
||||
/** Parsed authority scope for passive browser observation. */
|
||||
export type BrowserObservationPolicy =
|
||||
| DisabledObservationPolicy
|
||||
| {
|
||||
readonly status: "enabled";
|
||||
readonly cdpEndpoints: readonly [string, ...string[]];
|
||||
readonly allowedOrigins: readonly [string, ...string[]];
|
||||
};
|
||||
|
||||
/** Parsed authority scope for passive Electron observation. */
|
||||
export type ElectronObservationPolicy =
|
||||
| DisabledObservationPolicy
|
||||
| {
|
||||
readonly status: "enabled";
|
||||
readonly cdpEndpoints: readonly [string, ...string[]];
|
||||
readonly fileRoots: readonly [string, ...string[]];
|
||||
};
|
||||
|
||||
/** Parsed location authority for passive V8 Inspector observation. */
|
||||
export type V8InspectorLocationPolicy =
|
||||
| {
|
||||
readonly scope: "files";
|
||||
readonly fileRoots: readonly [string, ...string[]];
|
||||
}
|
||||
| {
|
||||
readonly scope: "origins";
|
||||
readonly allowedOrigins: readonly [string, ...string[]];
|
||||
}
|
||||
| {
|
||||
readonly scope: "files-and-origins";
|
||||
readonly fileRoots: readonly [string, ...string[]];
|
||||
readonly allowedOrigins: readonly [string, ...string[]];
|
||||
};
|
||||
|
||||
/** Parsed authority scope for passive V8 Inspector observation. */
|
||||
export type V8InspectorObservationPolicy =
|
||||
| DisabledObservationPolicy
|
||||
| {
|
||||
readonly status: "enabled";
|
||||
readonly endpoints: readonly [string, ...string[]];
|
||||
readonly locations: V8InspectorLocationPolicy;
|
||||
};
|
||||
|
||||
/** Complete parsed passive-observation policy family. */
|
||||
export interface PassiveObservationPolicies {
|
||||
readonly browser: BrowserObservationPolicy;
|
||||
readonly electron: ElectronObservationPolicy;
|
||||
readonly v8Inspector: V8InspectorObservationPolicy;
|
||||
}
|
||||
|
||||
/** Parse every passive observation capability into closed policy variants. */
|
||||
export const parsePassiveObservationPolicies = (
|
||||
env: Environment,
|
||||
): Result<PassiveObservationPolicies, ConfigurationError> => {
|
||||
const browser = parseBrowserObservationPolicy(env);
|
||||
if (!browser.ok) return browser;
|
||||
const electron = parseElectronObservationPolicy(env);
|
||||
if (!electron.ok) return electron;
|
||||
const v8Inspector = parseV8InspectorObservationPolicy(env);
|
||||
if (!v8Inspector.ok) return v8Inspector;
|
||||
return ok({
|
||||
browser: browser.value,
|
||||
electron: electron.value,
|
||||
v8Inspector: v8Inspector.value,
|
||||
});
|
||||
};
|
||||
|
||||
/** Add permission ceilings for each enabled passive observation policy. */
|
||||
export const appendPassiveObservationCeilings = (
|
||||
ceilings: PermissionCeiling[],
|
||||
policies: PassiveObservationPolicies,
|
||||
): void => {
|
||||
appendBrowserCeiling(ceilings, policies.browser);
|
||||
appendElectronCeiling(ceilings, policies.electron);
|
||||
appendV8InspectorCeiling(ceilings, policies.v8Inspector);
|
||||
};
|
||||
|
||||
/** Project the legal file and origin scopes from an enabled V8 policy. */
|
||||
export const v8InspectorLocationScopes = (
|
||||
locations: V8InspectorLocationPolicy,
|
||||
): {
|
||||
readonly fileRoots: readonly string[];
|
||||
readonly allowedOrigins: readonly string[];
|
||||
} => {
|
||||
switch (locations.scope) {
|
||||
case "files":
|
||||
return { fileRoots: locations.fileRoots, allowedOrigins: [] };
|
||||
case "origins":
|
||||
return { fileRoots: [], allowedOrigins: locations.allowedOrigins };
|
||||
case "files-and-origins":
|
||||
return locations;
|
||||
}
|
||||
};
|
||||
|
||||
const parseBrowserObservationPolicy = (
|
||||
env: Environment,
|
||||
): Result<BrowserObservationPolicy, ConfigurationError> => {
|
||||
const endpoints = parseBrowserArray(
|
||||
env.REA_BROWSER_CDP_ENDPOINTS_JSON,
|
||||
"REA_BROWSER_CDP_ENDPOINTS_JSON",
|
||||
browserEndpointSchema,
|
||||
16,
|
||||
);
|
||||
if (!endpoints.ok) return endpoints;
|
||||
const origins = parseBrowserArray(
|
||||
env.REA_BROWSER_ALLOWED_ORIGINS_JSON,
|
||||
"REA_BROWSER_ALLOWED_ORIGINS_JSON",
|
||||
browserOriginSchema,
|
||||
32,
|
||||
);
|
||||
if (!origins.ok) return origins;
|
||||
if (env.REA_BROWSER_OBSERVE_ENABLED !== "true")
|
||||
return ok({ status: "disabled" });
|
||||
const parsedEndpoints = requireFirst(
|
||||
endpoints.value,
|
||||
"REA_BROWSER_CDP_ENDPOINTS_JSON must encode at least one loopback endpoint when browser observation is enabled",
|
||||
);
|
||||
if (!parsedEndpoints.ok) return parsedEndpoints;
|
||||
const parsedOrigins = requireFirst(
|
||||
origins.value,
|
||||
"REA_BROWSER_ALLOWED_ORIGINS_JSON must encode at least one exact origin when browser observation is enabled",
|
||||
);
|
||||
if (!parsedOrigins.ok) return parsedOrigins;
|
||||
return ok({
|
||||
status: "enabled",
|
||||
cdpEndpoints: parsedEndpoints.value,
|
||||
allowedOrigins: parsedOrigins.value,
|
||||
});
|
||||
};
|
||||
|
||||
const parseElectronObservationPolicy = (
|
||||
env: Environment,
|
||||
): Result<ElectronObservationPolicy, ConfigurationError> => {
|
||||
const endpoints = parseBrowserArray(
|
||||
env.REA_ELECTRON_CDP_ENDPOINTS_JSON,
|
||||
"REA_ELECTRON_CDP_ENDPOINTS_JSON",
|
||||
browserEndpointSchema,
|
||||
16,
|
||||
);
|
||||
if (!endpoints.ok) return endpoints;
|
||||
const roots = parseElectronFileRoots(env.REA_ELECTRON_FILE_ROOTS_JSON);
|
||||
if (!roots.ok) return roots;
|
||||
if (env.REA_ELECTRON_OBSERVE_ENABLED !== "true")
|
||||
return ok({ status: "disabled" });
|
||||
const parsedEndpoints = requireFirst(
|
||||
endpoints.value,
|
||||
"REA_ELECTRON_CDP_ENDPOINTS_JSON must encode at least one loopback endpoint when Electron observation is enabled",
|
||||
);
|
||||
if (!parsedEndpoints.ok) return parsedEndpoints;
|
||||
const parsedRoots = requireFirst(
|
||||
roots.value,
|
||||
"REA_ELECTRON_FILE_ROOTS_JSON must encode at least one absolute root when Electron observation is enabled",
|
||||
);
|
||||
if (!parsedRoots.ok) return parsedRoots;
|
||||
return ok({
|
||||
status: "enabled",
|
||||
cdpEndpoints: parsedEndpoints.value,
|
||||
fileRoots: parsedRoots.value,
|
||||
});
|
||||
};
|
||||
|
||||
const parseV8InspectorObservationPolicy = (
|
||||
env: Environment,
|
||||
): Result<V8InspectorObservationPolicy, ConfigurationError> => {
|
||||
const endpoints = parseBrowserArray(
|
||||
env.REA_V8_INSPECTOR_ENDPOINTS_JSON,
|
||||
"REA_V8_INSPECTOR_ENDPOINTS_JSON",
|
||||
browserEndpointSchema,
|
||||
16,
|
||||
);
|
||||
if (!endpoints.ok) return endpoints;
|
||||
const roots = parseAbsoluteRoots(
|
||||
env.REA_V8_INSPECTOR_FILE_ROOTS_JSON,
|
||||
"REA_V8_INSPECTOR_FILE_ROOTS_JSON",
|
||||
);
|
||||
if (!roots.ok) return roots;
|
||||
const origins = parseBrowserArray(
|
||||
env.REA_V8_INSPECTOR_ALLOWED_ORIGINS_JSON,
|
||||
"REA_V8_INSPECTOR_ALLOWED_ORIGINS_JSON",
|
||||
browserOriginSchema,
|
||||
32,
|
||||
);
|
||||
if (!origins.ok) return origins;
|
||||
if (env.REA_V8_INSPECTOR_OBSERVE_ENABLED !== "true")
|
||||
return ok({ status: "disabled" });
|
||||
const parsedEndpoints = requireFirst(
|
||||
endpoints.value,
|
||||
"REA_V8_INSPECTOR_ENDPOINTS_JSON must encode at least one loopback endpoint when V8 Inspector observation is enabled",
|
||||
);
|
||||
if (!parsedEndpoints.ok) return parsedEndpoints;
|
||||
const locations = parseV8Locations(roots.value, origins.value);
|
||||
if (!locations.ok) return locations;
|
||||
return ok({
|
||||
status: "enabled",
|
||||
endpoints: parsedEndpoints.value,
|
||||
locations: locations.value,
|
||||
});
|
||||
};
|
||||
|
||||
const parseV8Locations = (
|
||||
roots: readonly string[],
|
||||
origins: readonly string[],
|
||||
): Result<V8InspectorLocationPolicy, ConfigurationError> => {
|
||||
const [firstRoot, ...remainingRoots] = roots;
|
||||
const [firstOrigin, ...remainingOrigins] = origins;
|
||||
if (firstRoot !== undefined && firstOrigin !== undefined)
|
||||
return ok({
|
||||
scope: "files-and-origins",
|
||||
fileRoots: [firstRoot, ...remainingRoots],
|
||||
allowedOrigins: [firstOrigin, ...remainingOrigins],
|
||||
});
|
||||
if (firstRoot !== undefined)
|
||||
return ok({
|
||||
scope: "files",
|
||||
fileRoots: [firstRoot, ...remainingRoots],
|
||||
});
|
||||
if (firstOrigin !== undefined)
|
||||
return ok({
|
||||
scope: "origins",
|
||||
allowedOrigins: [firstOrigin, ...remainingOrigins],
|
||||
});
|
||||
return err(
|
||||
new ConfigurationError(
|
||||
"V8 Inspector observation requires at least one file root or exact origin when enabled",
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const requireFirst = (
|
||||
values: readonly string[],
|
||||
message: string,
|
||||
): Result<readonly [string, ...string[]], ConfigurationError> => {
|
||||
const [first, ...remaining] = values;
|
||||
return first === undefined
|
||||
? err(new ConfigurationError(message))
|
||||
: ok([first, ...remaining]);
|
||||
};
|
||||
|
||||
const appendBrowserCeiling = (
|
||||
ceilings: PermissionCeiling[],
|
||||
policy: BrowserObservationPolicy,
|
||||
): void => {
|
||||
if (policy.status === "disabled") return;
|
||||
ceilings.push(
|
||||
permissionScope("browser_observe", [], {
|
||||
origins: [...policy.cdpEndpoints, ...policy.allowedOrigins],
|
||||
network: browserNetworkScope(policy.allowedOrigins),
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const appendElectronCeiling = (
|
||||
ceilings: PermissionCeiling[],
|
||||
policy: ElectronObservationPolicy,
|
||||
): void => {
|
||||
if (policy.status === "disabled") return;
|
||||
ceilings.push(
|
||||
permissionScope("electron_observe", policy.fileRoots, {
|
||||
origins: policy.cdpEndpoints,
|
||||
network: "loopback",
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const appendV8InspectorCeiling = (
|
||||
ceilings: PermissionCeiling[],
|
||||
policy: V8InspectorObservationPolicy,
|
||||
): void => {
|
||||
if (policy.status === "disabled") return;
|
||||
const scopes = v8InspectorLocationScopes(policy.locations);
|
||||
ceilings.push(
|
||||
permissionScope("v8_inspector_observe", scopes.fileRoots, {
|
||||
origins: [...policy.endpoints, ...scopes.allowedOrigins],
|
||||
network: "loopback",
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
import type {
|
||||
EnabledProcessExecutionPolicy,
|
||||
ProcessExecutionPolicy,
|
||||
} from "../domain/processCapture.js";
|
||||
import { ConfigurationError } from "../domain/errors.js";
|
||||
import type { PermissionCeiling } from "../domain/permissionPolicy.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import type { Environment } from "./environment.js";
|
||||
import { parseAbsoluteRoots, parseStringArray } from "./parsers.js";
|
||||
import { permissionScope } from "./permissions.js";
|
||||
|
||||
/** Parse process capture into disabled or fully authorized state. */
|
||||
export const parseProcessExecutionPolicy = (
|
||||
env: Environment,
|
||||
): Result<ProcessExecutionPolicy, ConfigurationError> => {
|
||||
const executableRoots = parseAbsoluteRoots(
|
||||
env.REA_PROCESS_EXECUTABLE_ROOTS_JSON,
|
||||
"REA_PROCESS_EXECUTABLE_ROOTS_JSON",
|
||||
);
|
||||
if (!executableRoots.ok) return executableRoots;
|
||||
const workingRoots = parseAbsoluteRoots(
|
||||
env.REA_PROCESS_WORKING_ROOTS_JSON,
|
||||
"REA_PROCESS_WORKING_ROOTS_JSON",
|
||||
);
|
||||
if (!workingRoots.ok) return workingRoots;
|
||||
const allowedEnvironment = parseStringArray(
|
||||
env.REA_PROCESS_ALLOWED_ENV_JSON,
|
||||
"REA_PROCESS_ALLOWED_ENV_JSON",
|
||||
);
|
||||
if (!allowedEnvironment.ok) return allowedEnvironment;
|
||||
if (env.REA_PROCESS_CAPTURE_ENABLED !== "true")
|
||||
return ok({ status: "disabled" });
|
||||
const parsedExecutables = requireRoots(
|
||||
executableRoots.value,
|
||||
"REA_PROCESS_EXECUTABLE_ROOTS_JSON must encode at least one absolute root when process capture is enabled",
|
||||
);
|
||||
if (!parsedExecutables.ok) return parsedExecutables;
|
||||
const parsedWorking = requireRoots(
|
||||
workingRoots.value,
|
||||
"REA_PROCESS_WORKING_ROOTS_JSON must encode at least one absolute root when process capture is enabled",
|
||||
);
|
||||
if (!parsedWorking.ok) return parsedWorking;
|
||||
return ok({
|
||||
status: "enabled",
|
||||
executableRoots: parsedExecutables.value,
|
||||
workingRoots: parsedWorking.value,
|
||||
allowedEnvironment: allowedEnvironment.value,
|
||||
networkAccess:
|
||||
env.REA_PROCESS_ALLOW_EXTERNAL_NETWORK === "true" ? "external" : "none",
|
||||
});
|
||||
};
|
||||
|
||||
/** Add the exact process-capture ceiling when capture is enabled. */
|
||||
export const appendProcessCaptureCeiling = (
|
||||
ceilings: PermissionCeiling[],
|
||||
policy: ProcessExecutionPolicy,
|
||||
): void => {
|
||||
if (policy.status === "disabled") return;
|
||||
ceilings.push(
|
||||
permissionScope("process_capture", policy.workingRoots, {
|
||||
executables: policy.executableRoots,
|
||||
environment_names: policy.allowedEnvironment,
|
||||
network: policy.networkAccess,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const requireRoots = (
|
||||
roots: readonly string[],
|
||||
message: string,
|
||||
): Result<
|
||||
EnabledProcessExecutionPolicy["workingRoots"],
|
||||
ConfigurationError
|
||||
> => {
|
||||
const [first, ...remaining] = roots;
|
||||
return first === undefined
|
||||
? err(new ConfigurationError(message))
|
||||
: ok([first, ...remaining]);
|
||||
};
|
||||
|
|
@ -7,6 +7,15 @@ import type {
|
|||
PermissionGrant,
|
||||
} from "../domain/permissionPolicy.js";
|
||||
import type { AnalysisProviderSelector } from "../contracts/providerSelection.js";
|
||||
import type { ManagedRuntimePolicy } from "../application/ManagedRuntimeCorrelationService.js";
|
||||
import type { JavaScriptReplayPolicy } from "../application/JavaScriptReplayPlanning.js";
|
||||
import type { ElectronAutomationPolicy } from "./electronAutomation.js";
|
||||
import type { BrowserScenarioPolicy } from "./browserScenario.js";
|
||||
import type {
|
||||
BrowserObservationPolicy,
|
||||
ElectronObservationPolicy,
|
||||
V8InspectorObservationPolicy,
|
||||
} from "./passiveObservation.js";
|
||||
|
||||
export interface AppConfig {
|
||||
readonly analysisProvider: AnalysisProviderSelector;
|
||||
|
|
@ -25,42 +34,13 @@ export interface AppConfig {
|
|||
readonly investigationInputRoots: readonly string[];
|
||||
readonly analysisSnapshotFilePolicy: EvidenceFilePolicy;
|
||||
readonly referenceSourcePolicy: ReferenceSourcePolicy;
|
||||
readonly browserObservationEnabled: boolean;
|
||||
readonly browserCdpEndpoints: readonly string[];
|
||||
readonly browserAllowedOrigins: readonly string[];
|
||||
readonly browserScenarioPolicy: {
|
||||
readonly enabled: boolean;
|
||||
readonly executableRoots: readonly string[];
|
||||
readonly cdpEndpoints: readonly string[];
|
||||
readonly allowedOrigins: readonly string[];
|
||||
readonly allowedEnvironment: readonly string[];
|
||||
};
|
||||
readonly electronObservationEnabled: boolean;
|
||||
readonly electronCdpEndpoints: readonly string[];
|
||||
readonly electronFileRoots: readonly string[];
|
||||
readonly electronAutomationPolicy: {
|
||||
readonly enabled: boolean;
|
||||
readonly executableRoots: readonly string[];
|
||||
readonly applicationRoots: readonly string[];
|
||||
};
|
||||
readonly v8InspectorObservationEnabled: boolean;
|
||||
readonly v8InspectorEndpoints: readonly string[];
|
||||
readonly v8InspectorFileRoots: readonly string[];
|
||||
readonly v8InspectorAllowedOrigins: readonly string[];
|
||||
readonly javascriptReplayPolicy: {
|
||||
readonly enabled: boolean;
|
||||
readonly roots: readonly string[];
|
||||
readonly nodePath: string;
|
||||
readonly bubblewrapPath: string;
|
||||
readonly systemdRunPath: string;
|
||||
readonly systemctlPath: string;
|
||||
readonly shellPath: string;
|
||||
};
|
||||
readonly managedRuntimePolicy: {
|
||||
readonly enabled: boolean;
|
||||
readonly roots: readonly string[];
|
||||
readonly executablePath: string;
|
||||
};
|
||||
readonly browserObservationPolicy: BrowserObservationPolicy;
|
||||
readonly browserScenarioPolicy: BrowserScenarioPolicy;
|
||||
readonly electronObservationPolicy: ElectronObservationPolicy;
|
||||
readonly electronAutomationPolicy: ElectronAutomationPolicy;
|
||||
readonly v8InspectorObservationPolicy: V8InspectorObservationPolicy;
|
||||
readonly javascriptReplayPolicy: JavaScriptReplayPolicy;
|
||||
readonly managedRuntimePolicy: ManagedRuntimePolicy;
|
||||
readonly permissionCeilings: readonly PermissionCeiling[];
|
||||
readonly administratorPermissionGrants: readonly PermissionGrant[];
|
||||
readonly permissionProjectRoot: string | undefined;
|
||||
|
|
|
|||
|
|
@ -114,7 +114,6 @@ const NODE_PREPARATION_EXAMPLE = jsonObjectSchema.parse({
|
|||
entry_export: "selected",
|
||||
},
|
||||
cases: [{ case_id: "empty", arguments: [""] }],
|
||||
approved: false,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -280,7 +279,6 @@ export const APPLICATION_TOOL_CONTRACTS = [
|
|||
result_depth: 16,
|
||||
result_nodes: 10000,
|
||||
},
|
||||
approved: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
@ -409,13 +407,49 @@ export const APPLICATION_TOOL_CONTRACTS = [
|
|||
] as const satisfies readonly ToolContract[];
|
||||
|
||||
/** Resolve one named application contract without relying on array position. */
|
||||
export const applicationToolContract = (
|
||||
export function applicationToolContract(
|
||||
name: "trace_application_feature",
|
||||
): (typeof APPLICATION_TOOL_CONTRACTS)[0];
|
||||
export function applicationToolContract(
|
||||
name: "trace_javascript_semantics",
|
||||
): (typeof APPLICATION_TOOL_CONTRACTS)[1];
|
||||
export function applicationToolContract(
|
||||
name: "compare_application_versions",
|
||||
): (typeof APPLICATION_TOOL_CONTRACTS)[2];
|
||||
export function applicationToolContract(
|
||||
name: "compare_source_to_bundle",
|
||||
): (typeof APPLICATION_TOOL_CONTRACTS)[3];
|
||||
export function applicationToolContract(
|
||||
name: "compare_javascript_export_shapes",
|
||||
): (typeof APPLICATION_TOOL_CONTRACTS)[4];
|
||||
export function applicationToolContract(
|
||||
name: "run_controlled_replay",
|
||||
): (typeof APPLICATION_TOOL_CONTRACTS)[5];
|
||||
export function applicationToolContract(
|
||||
name: "prepare_node_characterization",
|
||||
): (typeof APPLICATION_TOOL_CONTRACTS)[6];
|
||||
export function applicationToolContract(
|
||||
name: "execute_node_characterization",
|
||||
): (typeof APPLICATION_TOOL_CONTRACTS)[7];
|
||||
export function applicationToolContract(
|
||||
name: "build_reconstruction_obligation_ledger",
|
||||
): (typeof APPLICATION_TOOL_CONTRACTS)[8];
|
||||
export function applicationToolContract(
|
||||
name: "evaluate_reconstruction_readiness",
|
||||
): (typeof APPLICATION_TOOL_CONTRACTS)[9];
|
||||
export function applicationToolContract(
|
||||
name: "commit_reconstruction_coverage",
|
||||
): (typeof APPLICATION_TOOL_CONTRACTS)[10];
|
||||
export function applicationToolContract(
|
||||
name: "query_reconstruction_coverage",
|
||||
): (typeof APPLICATION_TOOL_CONTRACTS)[11];
|
||||
export function applicationToolContract(
|
||||
name: (typeof APPLICATION_TOOL_CONTRACTS)[number]["name"],
|
||||
): (typeof APPLICATION_TOOL_CONTRACTS)[number] => {
|
||||
): (typeof APPLICATION_TOOL_CONTRACTS)[number] {
|
||||
const contract = APPLICATION_TOOL_CONTRACTS.find(
|
||||
({ name: candidate }) => candidate === name,
|
||||
);
|
||||
if (contract === undefined)
|
||||
throw new Error(`Missing application tool contract: ${name}`);
|
||||
return contract;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,149 +12,140 @@ const evidenceIdSchema = z
|
|||
.regex(/^ev_[a-f0-9]{64}$/u)
|
||||
.describe("Evidence ID returned earlier in this session");
|
||||
|
||||
const requireExactlyOne = (
|
||||
context: z.RefinementCtx,
|
||||
fields: readonly [string, unknown, string, unknown],
|
||||
): void => {
|
||||
const [leftName, left, rightName, right] = fields;
|
||||
if ((left === undefined) !== (right === undefined)) return;
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: [left === undefined ? leftName : rightName],
|
||||
message: `Supply exactly one of ${leftName} or ${rightName}`,
|
||||
});
|
||||
};
|
||||
const traceApplicationFeatureFacts = {
|
||||
native_observations:
|
||||
traceApplicationFeatureInputSchema.shape.native_observations,
|
||||
native_observation_evidence_ids: z
|
||||
.array(evidenceIdSchema)
|
||||
.max(64)
|
||||
.default([]),
|
||||
seed: traceApplicationFeatureInputSchema.shape.seed,
|
||||
direction: traceApplicationFeatureInputSchema.shape.direction,
|
||||
limits: traceApplicationFeatureInputSchema.shape.limits,
|
||||
} as const;
|
||||
|
||||
/** MCP/CLI trace request accepting full Evidence or a ledger reference. */
|
||||
export const traceApplicationFeatureRequestSchema = z
|
||||
.strictObject({
|
||||
application: evidenceSchema.optional(),
|
||||
application_evidence_id: evidenceIdSchema.optional(),
|
||||
native_observations:
|
||||
traceApplicationFeatureInputSchema.shape.native_observations,
|
||||
native_observation_evidence_ids: z
|
||||
.array(evidenceIdSchema)
|
||||
.max(64)
|
||||
.default([]),
|
||||
seed: traceApplicationFeatureInputSchema.shape.seed,
|
||||
direction: traceApplicationFeatureInputSchema.shape.direction,
|
||||
limits: traceApplicationFeatureInputSchema.shape.limits,
|
||||
})
|
||||
.superRefine((input, context) => {
|
||||
requireExactlyOne(context, [
|
||||
"application",
|
||||
input.application,
|
||||
"application_evidence_id",
|
||||
input.application_evidence_id,
|
||||
]);
|
||||
});
|
||||
export const traceApplicationFeatureRequestSchema = z.union([
|
||||
z.strictObject({
|
||||
...traceApplicationFeatureFacts,
|
||||
application: evidenceSchema,
|
||||
}),
|
||||
z.strictObject({
|
||||
...traceApplicationFeatureFacts,
|
||||
application_evidence_id: evidenceIdSchema,
|
||||
}),
|
||||
]);
|
||||
|
||||
/** MCP/CLI semantic trace request accepting full Evidence or a ledger reference. */
|
||||
export const traceJavaScriptSemanticsRequestSchema = z
|
||||
.strictObject({
|
||||
application: evidenceSchema.optional(),
|
||||
application_evidence_id: evidenceIdSchema.optional(),
|
||||
export const traceJavaScriptSemanticsRequestSchema = z.union([
|
||||
z.strictObject({
|
||||
application: evidenceSchema,
|
||||
query: javaScriptSemanticQueryInputSchema,
|
||||
})
|
||||
.superRefine((input, context) => {
|
||||
requireExactlyOne(context, [
|
||||
"application",
|
||||
input.application,
|
||||
"application_evidence_id",
|
||||
input.application_evidence_id,
|
||||
]);
|
||||
});
|
||||
}),
|
||||
z.strictObject({
|
||||
application_evidence_id: evidenceIdSchema,
|
||||
query: javaScriptSemanticQueryInputSchema,
|
||||
}),
|
||||
]);
|
||||
|
||||
const compareApplicationVersionsFacts = {
|
||||
left_native_observations:
|
||||
compareApplicationVersionsInputSchema.shape.left_native_observations,
|
||||
left_native_observation_evidence_ids: z
|
||||
.array(evidenceIdSchema)
|
||||
.max(64)
|
||||
.default([]),
|
||||
right_native_observations:
|
||||
compareApplicationVersionsInputSchema.shape.right_native_observations,
|
||||
right_native_observation_evidence_ids: z
|
||||
.array(evidenceIdSchema)
|
||||
.max(64)
|
||||
.default([]),
|
||||
limits: compareApplicationVersionsInputSchema.shape.limits,
|
||||
unknown_registry_approved:
|
||||
compareApplicationVersionsInputSchema.shape.unknown_registry_approved,
|
||||
} as const;
|
||||
|
||||
/** MCP/CLI comparison request accepting full Evidence or ledger references. */
|
||||
export const compareApplicationVersionsRequestSchema = z
|
||||
.strictObject({
|
||||
left: evidenceSchema.optional(),
|
||||
left_evidence_id: evidenceIdSchema.optional(),
|
||||
right: evidenceSchema.optional(),
|
||||
right_evidence_id: evidenceIdSchema.optional(),
|
||||
left_native_observations:
|
||||
compareApplicationVersionsInputSchema.shape.left_native_observations,
|
||||
left_native_observation_evidence_ids: z
|
||||
.array(evidenceIdSchema)
|
||||
.max(64)
|
||||
.default([]),
|
||||
right_native_observations:
|
||||
compareApplicationVersionsInputSchema.shape.right_native_observations,
|
||||
right_native_observation_evidence_ids: z
|
||||
.array(evidenceIdSchema)
|
||||
.max(64)
|
||||
.default([]),
|
||||
limits: compareApplicationVersionsInputSchema.shape.limits,
|
||||
unknown_registry_approved:
|
||||
compareApplicationVersionsInputSchema.shape.unknown_registry_approved,
|
||||
})
|
||||
.superRefine((input, context) => {
|
||||
requireExactlyOne(context, [
|
||||
"left",
|
||||
input.left,
|
||||
"left_evidence_id",
|
||||
input.left_evidence_id,
|
||||
]);
|
||||
requireExactlyOne(context, [
|
||||
"right",
|
||||
input.right,
|
||||
"right_evidence_id",
|
||||
input.right_evidence_id,
|
||||
]);
|
||||
});
|
||||
export const compareApplicationVersionsRequestSchema = z.union([
|
||||
z.strictObject({
|
||||
...compareApplicationVersionsFacts,
|
||||
left: evidenceSchema,
|
||||
right: evidenceSchema,
|
||||
}),
|
||||
z.strictObject({
|
||||
...compareApplicationVersionsFacts,
|
||||
left: evidenceSchema,
|
||||
right_evidence_id: evidenceIdSchema,
|
||||
}),
|
||||
z.strictObject({
|
||||
...compareApplicationVersionsFacts,
|
||||
left_evidence_id: evidenceIdSchema,
|
||||
right: evidenceSchema,
|
||||
}),
|
||||
z.strictObject({
|
||||
...compareApplicationVersionsFacts,
|
||||
left_evidence_id: evidenceIdSchema,
|
||||
right_evidence_id: evidenceIdSchema,
|
||||
}),
|
||||
]);
|
||||
|
||||
const compareSourceToBundleFacts = {
|
||||
reference: compareSourceToBundleInputSchema.shape.reference,
|
||||
limits: compareSourceToBundleInputSchema.shape.limits,
|
||||
unknown_registry_approved:
|
||||
compareSourceToBundleInputSchema.shape.unknown_registry_approved,
|
||||
} as const;
|
||||
|
||||
/** Historical-source comparison accepting full application Evidence or a ledger reference. */
|
||||
export const compareSourceToBundleRequestSchema = z
|
||||
.strictObject({
|
||||
reference: compareSourceToBundleInputSchema.shape.reference,
|
||||
application: evidenceSchema.optional(),
|
||||
application_evidence_id: evidenceIdSchema.optional(),
|
||||
limits: compareSourceToBundleInputSchema.shape.limits,
|
||||
unknown_registry_approved:
|
||||
compareSourceToBundleInputSchema.shape.unknown_registry_approved,
|
||||
})
|
||||
.superRefine((input, context) => {
|
||||
requireExactlyOne(context, [
|
||||
"application",
|
||||
input.application,
|
||||
"application_evidence_id",
|
||||
input.application_evidence_id,
|
||||
]);
|
||||
});
|
||||
export const compareSourceToBundleRequestSchema = z.union([
|
||||
z.strictObject({
|
||||
...compareSourceToBundleFacts,
|
||||
application: evidenceSchema,
|
||||
}),
|
||||
z.strictObject({
|
||||
...compareSourceToBundleFacts,
|
||||
application_evidence_id: evidenceIdSchema,
|
||||
}),
|
||||
]);
|
||||
|
||||
const compareJavaScriptExportShapesFacts = {
|
||||
left_module_path:
|
||||
compareJavaScriptExportShapesInputSchema.shape.left_module_path,
|
||||
left_export_name:
|
||||
compareJavaScriptExportShapesInputSchema.shape.left_export_name,
|
||||
right_module_path:
|
||||
compareJavaScriptExportShapesInputSchema.shape.right_module_path,
|
||||
right_export_name:
|
||||
compareJavaScriptExportShapesInputSchema.shape.right_export_name,
|
||||
limits: compareJavaScriptExportShapesInputSchema.shape.limits,
|
||||
unknown_registry_approved:
|
||||
compareJavaScriptExportShapesInputSchema.shape.unknown_registry_approved,
|
||||
} as const;
|
||||
|
||||
/** MCP/CLI export-shape request accepting full Evidence or ledger references. */
|
||||
export const compareJavaScriptExportShapesRequestSchema = z
|
||||
.strictObject({
|
||||
left: evidenceSchema.optional(),
|
||||
left_evidence_id: evidenceIdSchema.optional(),
|
||||
right: evidenceSchema.optional(),
|
||||
right_evidence_id: evidenceIdSchema.optional(),
|
||||
left_module_path:
|
||||
compareJavaScriptExportShapesInputSchema.shape.left_module_path,
|
||||
left_export_name:
|
||||
compareJavaScriptExportShapesInputSchema.shape.left_export_name,
|
||||
right_module_path:
|
||||
compareJavaScriptExportShapesInputSchema.shape.right_module_path,
|
||||
right_export_name:
|
||||
compareJavaScriptExportShapesInputSchema.shape.right_export_name,
|
||||
limits: compareJavaScriptExportShapesInputSchema.shape.limits,
|
||||
unknown_registry_approved:
|
||||
compareJavaScriptExportShapesInputSchema.shape.unknown_registry_approved,
|
||||
})
|
||||
.superRefine((input, context) => {
|
||||
requireExactlyOne(context, [
|
||||
"left",
|
||||
input.left,
|
||||
"left_evidence_id",
|
||||
input.left_evidence_id,
|
||||
]);
|
||||
requireExactlyOne(context, [
|
||||
"right",
|
||||
input.right,
|
||||
"right_evidence_id",
|
||||
input.right_evidence_id,
|
||||
]);
|
||||
});
|
||||
export const compareJavaScriptExportShapesRequestSchema = z.union([
|
||||
z.strictObject({
|
||||
...compareJavaScriptExportShapesFacts,
|
||||
left: evidenceSchema,
|
||||
right: evidenceSchema,
|
||||
}),
|
||||
z.strictObject({
|
||||
...compareJavaScriptExportShapesFacts,
|
||||
left: evidenceSchema,
|
||||
right_evidence_id: evidenceIdSchema,
|
||||
}),
|
||||
z.strictObject({
|
||||
...compareJavaScriptExportShapesFacts,
|
||||
left_evidence_id: evidenceIdSchema,
|
||||
right: evidenceSchema,
|
||||
}),
|
||||
z.strictObject({
|
||||
...compareJavaScriptExportShapesFacts,
|
||||
left_evidence_id: evidenceIdSchema,
|
||||
right_evidence_id: evidenceIdSchema,
|
||||
}),
|
||||
]);
|
||||
|
||||
export type TraceApplicationFeatureRequest = z.output<
|
||||
typeof traceApplicationFeatureRequestSchema
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import type { ToolContract } from "./toolContracts.js";
|
|||
import { artifactOutputSchemas } from "./toolOutputSchemas.js";
|
||||
import { jsonValueSchema } from "../domain/jsonValue.js";
|
||||
import { toolContractMetadata } from "./toolEffects.js";
|
||||
import { requireOutputSchema } from "./toolOutputSchemaPrimitives.js";
|
||||
import { artifactInspectionLimitsSchema } from "../domain/artifactInspection.js";
|
||||
|
||||
const pageInput = {
|
||||
|
|
@ -35,27 +36,30 @@ const traversalLimitsInput = {
|
|||
max_path_bytes: z.number().int().min(1).max(65_535).default(4_096),
|
||||
};
|
||||
|
||||
/** Exact caller boundary for deterministic artifact inventory. */
|
||||
export const artifactInventoryInputSchema = z
|
||||
.object({
|
||||
native_mount_approved: z.boolean().default(false),
|
||||
integrity_policy: z.enum(["fail", "record-and-continue"]).default("fail"),
|
||||
integrity_continue_approved: z.boolean().default(false),
|
||||
const integrityInput = {
|
||||
fail: {
|
||||
integrity_policy: z.literal("fail").default("fail"),
|
||||
integrity_continue_approved: z.literal(false).default(false),
|
||||
max_integrity_mismatches: z.number().int().min(1).max(100).default(10),
|
||||
...pageInput,
|
||||
...traversalLimitsInput,
|
||||
})
|
||||
.superRefine((input, context) => {
|
||||
if (
|
||||
input.integrity_policy === "record-and-continue" &&
|
||||
input.integrity_continue_approved !== true
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["integrity_continue_approved"],
|
||||
message: "record-and-continue requires explicit approval",
|
||||
});
|
||||
});
|
||||
},
|
||||
continue: {
|
||||
integrity_policy: z.literal("record-and-continue"),
|
||||
integrity_continue_approved: z.literal(true),
|
||||
max_integrity_mismatches: z.number().int().min(1).max(100).default(10),
|
||||
},
|
||||
} as const;
|
||||
|
||||
const artifactInventoryFacts = {
|
||||
native_mount_approved: z.boolean().default(false),
|
||||
...pageInput,
|
||||
...traversalLimitsInput,
|
||||
} as const;
|
||||
|
||||
/** Exact caller boundary for deterministic artifact inventory. */
|
||||
export const artifactInventoryInputSchema = z.union([
|
||||
z.object({ ...artifactInventoryFacts, ...integrityInput.fail }),
|
||||
z.object({ ...artifactInventoryFacts, ...integrityInput.continue }),
|
||||
]);
|
||||
|
||||
/** Exact caller boundary for approved artifact extraction. */
|
||||
export const artifactExtractionInputSchema = z.object({
|
||||
|
|
@ -75,26 +79,15 @@ export const artifactExtractionInputSchema = z.object({
|
|||
});
|
||||
|
||||
/** Bounded provider-neutral inspection using one atomic inventory substep. */
|
||||
export const artifactInspectionInputSchema = z
|
||||
.object({
|
||||
native_mount_approved: z.boolean().default(false),
|
||||
integrity_policy: z.enum(["fail", "record-and-continue"]).default("fail"),
|
||||
integrity_continue_approved: z.boolean().default(false),
|
||||
max_integrity_mismatches: z.number().int().min(1).max(100).default(10),
|
||||
...artifactInspectionLimitsSchema.shape,
|
||||
...traversalLimitsInput,
|
||||
})
|
||||
.superRefine((input, context) => {
|
||||
if (
|
||||
input.integrity_policy === "record-and-continue" &&
|
||||
input.integrity_continue_approved !== true
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["integrity_continue_approved"],
|
||||
message: "record-and-continue requires explicit approval",
|
||||
});
|
||||
});
|
||||
const artifactInspectionFacts = {
|
||||
native_mount_approved: z.boolean().default(false),
|
||||
...artifactInspectionLimitsSchema.shape,
|
||||
...traversalLimitsInput,
|
||||
} as const;
|
||||
export const artifactInspectionInputSchema = z.union([
|
||||
z.object({ ...artifactInspectionFacts, ...integrityInput.fail }),
|
||||
z.object({ ...artifactInspectionFacts, ...integrityInput.continue }),
|
||||
]);
|
||||
|
||||
const exampleInputSchema = z.record(z.string(), jsonValueSchema);
|
||||
const examples: Readonly<Record<string, Readonly<Record<string, unknown>>>> = {
|
||||
|
|
@ -107,14 +100,15 @@ const examples: Readonly<Record<string, Readonly<Record<string, unknown>>>> = {
|
|||
},
|
||||
};
|
||||
|
||||
const artifact = <Name extends string>(
|
||||
const artifact = <
|
||||
Name extends string,
|
||||
Schema extends z.ZodType<Readonly<Record<string, unknown>>>,
|
||||
>(
|
||||
name: Name,
|
||||
description: string,
|
||||
inputSchema: z.ZodObject,
|
||||
): ToolContract<Name> => {
|
||||
const outputSchema = artifactOutputSchemas[name];
|
||||
if (outputSchema === undefined)
|
||||
throw new Error(`Missing artifact output schema for ${name}`);
|
||||
inputSchema: Schema,
|
||||
) => {
|
||||
const outputSchema = requireOutputSchema(artifactOutputSchemas, name);
|
||||
return {
|
||||
name,
|
||||
...toolContractMetadata(name),
|
||||
|
|
@ -128,7 +122,7 @@ const artifact = <Name extends string>(
|
|||
input: exampleInputSchema.parse(examples[name] ?? {}),
|
||||
},
|
||||
],
|
||||
};
|
||||
} satisfies ToolContract<Name, Schema, typeof outputSchema>;
|
||||
};
|
||||
|
||||
/** Artifact-graph inventory and explicitly approved safe extraction contracts. */
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import {
|
|||
webMcpDiscoverySchema,
|
||||
} from "../domain/webMcpDiscovery.js";
|
||||
import {
|
||||
browserCaptureComparisonWireSchema,
|
||||
browserCaptureComparisonInputSchema,
|
||||
browserCaptureComparisonSchema,
|
||||
} from "../domain/browserCaptureComparison.js";
|
||||
import {
|
||||
|
|
@ -282,7 +282,7 @@ export const BROWSER_TOOL_CONTRACTS = [
|
|||
description:
|
||||
"Compare either passive web captures or step-indexed browser scenarios. Scenario comparison aligns exact step IDs, records deterministic literal normalization, and exposes bounded artifact-level changes plus alignment failures. Missing or truncated evidence never proves equality.",
|
||||
kind: "browser-provider",
|
||||
inputSchema: browserCaptureComparisonWireSchema,
|
||||
inputSchema: browserCaptureComparisonInputSchema,
|
||||
outputSchema: captureDiffOutputSchema,
|
||||
examples: [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { z } from "zod";
|
||||
|
||||
const traceLiteralInputSchema = z.object({
|
||||
const traceLiteralInputSchema = z.strictObject({
|
||||
query: z.string().min(1),
|
||||
case_sensitive: z.boolean().default(false),
|
||||
limit: z.number().int().min(1).max(100).default(20),
|
||||
|
|
@ -13,27 +13,27 @@ const traceLiteralInputSchema = z.object({
|
|||
|
||||
/** Input schemas shared by MCP registration and enhanced application dispatch. */
|
||||
export const enhancedInputSchemas = {
|
||||
swift_classes: z.object({ pattern: z.string().default("") }),
|
||||
get_objc_classes: z.object({ pattern: z.string().default("") }),
|
||||
get_objc_protocols: z.object({}),
|
||||
batch_decompile: z.object({
|
||||
swift_classes: z.strictObject({ pattern: z.string().default("") }),
|
||||
get_objc_classes: z.strictObject({ pattern: z.string().default("") }),
|
||||
get_objc_protocols: z.strictObject({}),
|
||||
batch_decompile: z.strictObject({
|
||||
addresses: z
|
||||
.array(z.string().describe("A provider-normalized procedure address"))
|
||||
.max(20)
|
||||
.default([]),
|
||||
}),
|
||||
get_call_graph: z.object({
|
||||
get_call_graph: z.strictObject({
|
||||
address: z.string().describe("A provider-normalized procedure address"),
|
||||
direction: z.enum(["forward", "backward"]).default("forward"),
|
||||
depth: z.number().int().min(1).max(5).default(2),
|
||||
}),
|
||||
analyze_swift_types: z.object({}),
|
||||
find_xrefs_to_name: z.object({ name: z.string() }),
|
||||
binary_overview: z.object({
|
||||
analyze_swift_types: z.strictObject({}),
|
||||
find_xrefs_to_name: z.strictObject({ name: z.string() }),
|
||||
binary_overview: z.strictObject({
|
||||
detail: z.enum(["concise", "detailed"]).default("concise"),
|
||||
limit: z.number().int().min(1).max(50).default(10),
|
||||
}),
|
||||
analyze_function: z.object({
|
||||
analyze_function: z.strictObject({
|
||||
procedure: z.string().describe("A procedure name or address"),
|
||||
include_assembly: z.boolean().default(false),
|
||||
limit: z.number().int().min(1).max(500).default(100),
|
||||
|
|
@ -42,7 +42,7 @@ export const enhancedInputSchemas = {
|
|||
pseudocode_offset: z.number().int().min(0).default(0),
|
||||
assembly_offset: z.number().int().min(0).default(0),
|
||||
collection_offset: z
|
||||
.object({
|
||||
.strictObject({
|
||||
comments: z.number().int().min(0).default(0),
|
||||
callers: z.number().int().min(0).default(0),
|
||||
callees: z.number().int().min(0).default(0),
|
||||
|
|
@ -63,7 +63,7 @@ export const enhancedInputSchemas = {
|
|||
basic_blocks: 0,
|
||||
}),
|
||||
}),
|
||||
inspect_native_api: z.object({
|
||||
inspect_native_api: z.strictObject({
|
||||
procedure: z.string().describe("A procedure name or address"),
|
||||
max_pseudocode_chars: z.number().int().min(1).max(100_000).default(20_000),
|
||||
max_instructions: z.number().int().min(1).max(5_000).default(500),
|
||||
|
|
@ -76,7 +76,7 @@ export const enhancedInputSchemas = {
|
|||
}),
|
||||
trace_feature: traceLiteralInputSchema,
|
||||
find_code_for_string: traceLiteralInputSchema,
|
||||
trace_call_path: z.object({
|
||||
trace_call_path: z.strictObject({
|
||||
start: z.string().describe("A provider-normalized procedure address"),
|
||||
goal: z
|
||||
.string()
|
||||
|
|
@ -94,20 +94,3 @@ export const enhancedInputSchemas = {
|
|||
} as const;
|
||||
|
||||
export type EnhancedToolName = keyof typeof enhancedInputSchemas;
|
||||
|
||||
/** Runtime parser for dispatching only the closed enhanced names. */
|
||||
export const enhancedToolNameSchema = z.enum([
|
||||
"swift_classes",
|
||||
"get_objc_classes",
|
||||
"get_objc_protocols",
|
||||
"batch_decompile",
|
||||
"get_call_graph",
|
||||
"analyze_swift_types",
|
||||
"find_xrefs_to_name",
|
||||
"binary_overview",
|
||||
"analyze_function",
|
||||
"inspect_native_api",
|
||||
"trace_feature",
|
||||
"find_code_for_string",
|
||||
"trace_call_path",
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -4,17 +4,16 @@ import { TOOL_EXAMPLE_OVERRIDES } from "./toolContractExamples.js";
|
|||
import type { ToolContract } from "./toolContractTypes.js";
|
||||
import { toolContractMetadata } from "./toolEffects.js";
|
||||
import { enhancedOutputSchemas } from "./toolOutputSchemas.js";
|
||||
import { requireOutputSchema } from "./toolOutputSchemaPrimitives.js";
|
||||
|
||||
type FunctionWorkflowName = "analyze_function" | "inspect_native_api";
|
||||
|
||||
const functionWorkflow = <Name extends FunctionWorkflowName>(
|
||||
name: Name,
|
||||
description: string,
|
||||
): ToolContract<Name> => {
|
||||
) => {
|
||||
const inputSchema = enhancedInputSchemas[name];
|
||||
const outputSchema = enhancedOutputSchemas[name];
|
||||
if (outputSchema === undefined)
|
||||
throw new Error(`Missing function workflow output schema for ${name}`);
|
||||
const outputSchema = requireOutputSchema(enhancedOutputSchemas, name);
|
||||
return {
|
||||
name,
|
||||
...toolContractMetadata(name),
|
||||
|
|
@ -30,7 +29,7 @@ const functionWorkflow = <Name extends FunctionWorkflowName>(
|
|||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
} satisfies ToolContract<Name, typeof inputSchema, typeof outputSchema>;
|
||||
};
|
||||
|
||||
/** Function dossier and native API reconstruction workflow contracts. */
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { z } from "zod";
|
|||
import type { ToolContract } from "./toolContracts.js";
|
||||
import { managedOutputSchemas } from "./toolOutputSchemas.js";
|
||||
import { toolContractMetadata } from "./toolEffects.js";
|
||||
import { requireOutputSchema } from "./toolOutputSchemaPrimitives.js";
|
||||
|
||||
/** Exact caller boundary for execution-free PE/CLI triage and identity. */
|
||||
export const managedArtifactInputSchema = z.object({
|
||||
|
|
@ -110,18 +111,18 @@ export const managedNativeBoundaryInputSchema = z.object({
|
|||
.default(1_048_576),
|
||||
});
|
||||
|
||||
const outputSchema = managedOutputSchemas.inspect_managed_artifact;
|
||||
if (outputSchema === undefined)
|
||||
throw new Error("Missing managed output schema for inspect_managed_artifact");
|
||||
const memberOutputSchema = managedOutputSchemas.inspect_managed_members;
|
||||
if (memberOutputSchema === undefined)
|
||||
throw new Error("Missing managed output schema for inspect_managed_members");
|
||||
const nativeBoundaryOutputSchema =
|
||||
managedOutputSchemas.inspect_managed_native_boundaries;
|
||||
if (nativeBoundaryOutputSchema === undefined)
|
||||
throw new Error(
|
||||
"Missing managed output schema for inspect_managed_native_boundaries",
|
||||
);
|
||||
const outputSchema = requireOutputSchema(
|
||||
managedOutputSchemas,
|
||||
"inspect_managed_artifact",
|
||||
);
|
||||
const memberOutputSchema = requireOutputSchema(
|
||||
managedOutputSchemas,
|
||||
"inspect_managed_members",
|
||||
);
|
||||
const nativeBoundaryOutputSchema = requireOutputSchema(
|
||||
managedOutputSchemas,
|
||||
"inspect_managed_native_boundaries",
|
||||
);
|
||||
|
||||
/** Read-only managed artifact contracts. */
|
||||
export const MANAGED_TOOL_CONTRACTS = [
|
||||
|
|
|
|||
|
|
@ -57,15 +57,18 @@ const emptyManagedMembers = (
|
|||
limitations: [],
|
||||
});
|
||||
|
||||
const emptyPage = <Item>(limit: number) => ({
|
||||
items: [] as Item[],
|
||||
offset: 0,
|
||||
limit,
|
||||
total: 0,
|
||||
returned: 0,
|
||||
dropped: 0,
|
||||
complete: true,
|
||||
});
|
||||
const emptyPage = <Item>(limit: number) => {
|
||||
const items: Item[] = [];
|
||||
return {
|
||||
items,
|
||||
offset: 0,
|
||||
limit,
|
||||
total: 0,
|
||||
returned: 0,
|
||||
dropped: 0,
|
||||
complete: true,
|
||||
};
|
||||
};
|
||||
|
||||
const evidence = (result: ManagedMemberInspection) =>
|
||||
createEvidence(undefined, MANAGED_STATIC_EXAMPLE_PROVIDER, {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
MANAGED_RUNTIME_CORRELATION_EXAMPLE,
|
||||
} from "./managedWorkflowExamples.js";
|
||||
import { toolContractMetadata } from "./toolEffects.js";
|
||||
import { requireOutputSchema } from "./toolOutputSchemaPrimitives.js";
|
||||
|
||||
const evidenceIdSchema = z.string().regex(/^ev_[a-f0-9]{64}$/u);
|
||||
const managedEvidenceIdSchema = evidenceIdSchema.describe(
|
||||
|
|
@ -96,26 +97,40 @@ export const managedNativeVerificationReferenceInputSchema = z
|
|||
}
|
||||
});
|
||||
|
||||
/** MCP references for projecting managed Evidence into an application graph. */
|
||||
const managedApplicationGraphReferenceFacts = {
|
||||
limits: projectManagedApplicationGraphInputSchema.shape.limits,
|
||||
} as const;
|
||||
|
||||
/** MCP references requiring at least one managed Evidence source. */
|
||||
export const managedApplicationGraphReferenceInputSchema = z
|
||||
.strictObject({
|
||||
managed_artifact_evidence_id: managedArtifactEvidenceIdSchema.optional(),
|
||||
managed_members_evidence_id: managedEvidenceIdSchema.optional(),
|
||||
managed_native_boundaries_evidence_id:
|
||||
managedBoundaryEvidenceIdSchema.optional(),
|
||||
limits: projectManagedApplicationGraphInputSchema.shape.limits,
|
||||
})
|
||||
.union([
|
||||
z.strictObject({
|
||||
...managedApplicationGraphReferenceFacts,
|
||||
managed_artifact_evidence_id: managedArtifactEvidenceIdSchema,
|
||||
managed_members_evidence_id: managedEvidenceIdSchema.optional(),
|
||||
managed_native_boundaries_evidence_id:
|
||||
managedBoundaryEvidenceIdSchema.optional(),
|
||||
}),
|
||||
z.strictObject({
|
||||
...managedApplicationGraphReferenceFacts,
|
||||
managed_artifact_evidence_id: managedArtifactEvidenceIdSchema.optional(),
|
||||
managed_members_evidence_id: managedEvidenceIdSchema,
|
||||
managed_native_boundaries_evidence_id:
|
||||
managedBoundaryEvidenceIdSchema.optional(),
|
||||
}),
|
||||
z.strictObject({
|
||||
...managedApplicationGraphReferenceFacts,
|
||||
managed_artifact_evidence_id: managedArtifactEvidenceIdSchema.optional(),
|
||||
managed_members_evidence_id: managedEvidenceIdSchema.optional(),
|
||||
managed_native_boundaries_evidence_id: managedBoundaryEvidenceIdSchema,
|
||||
}),
|
||||
])
|
||||
.superRefine((input, context) => {
|
||||
const ids = [
|
||||
input.managed_artifact_evidence_id,
|
||||
input.managed_members_evidence_id,
|
||||
input.managed_native_boundaries_evidence_id,
|
||||
].filter((id): id is string => id !== undefined);
|
||||
if (ids.length === 0)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "At least one managed Evidence ID is required",
|
||||
});
|
||||
if (new Set(ids).size !== ids.length)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
|
|
@ -123,36 +138,26 @@ export const managedApplicationGraphReferenceInputSchema = z
|
|||
});
|
||||
});
|
||||
|
||||
const comparisonOutputSchema =
|
||||
managedWorkflowOutputSchemas.compare_managed_members;
|
||||
if (comparisonOutputSchema === undefined)
|
||||
throw new Error(
|
||||
"Missing managed workflow output schema for compare_managed_members",
|
||||
);
|
||||
const runtimeOutputSchema =
|
||||
managedWorkflowOutputSchemas.plan_managed_runtime_correlation;
|
||||
if (runtimeOutputSchema === undefined)
|
||||
throw new Error(
|
||||
"Missing managed workflow output schema for plan_managed_runtime_correlation",
|
||||
);
|
||||
const reconstructionOutputSchema =
|
||||
managedWorkflowOutputSchemas.import_managed_reconstruction;
|
||||
if (reconstructionOutputSchema === undefined)
|
||||
throw new Error(
|
||||
"Missing managed workflow output schema for import_managed_reconstruction",
|
||||
);
|
||||
const nativeVerificationOutputSchema =
|
||||
managedWorkflowOutputSchemas.verify_managed_native_boundaries;
|
||||
if (nativeVerificationOutputSchema === undefined)
|
||||
throw new Error(
|
||||
"Missing managed workflow output schema for verify_managed_native_boundaries",
|
||||
);
|
||||
const managedApplicationGraphOutputSchema =
|
||||
managedWorkflowOutputSchemas.project_managed_application_graph;
|
||||
if (managedApplicationGraphOutputSchema === undefined)
|
||||
throw new Error(
|
||||
"Missing managed workflow output schema for project_managed_application_graph",
|
||||
);
|
||||
const comparisonOutputSchema = requireOutputSchema(
|
||||
managedWorkflowOutputSchemas,
|
||||
"compare_managed_members",
|
||||
);
|
||||
const runtimeOutputSchema = requireOutputSchema(
|
||||
managedWorkflowOutputSchemas,
|
||||
"plan_managed_runtime_correlation",
|
||||
);
|
||||
const reconstructionOutputSchema = requireOutputSchema(
|
||||
managedWorkflowOutputSchemas,
|
||||
"import_managed_reconstruction",
|
||||
);
|
||||
const nativeVerificationOutputSchema = requireOutputSchema(
|
||||
managedWorkflowOutputSchemas,
|
||||
"verify_managed_native_boundaries",
|
||||
);
|
||||
const managedApplicationGraphOutputSchema = requireOutputSchema(
|
||||
managedWorkflowOutputSchemas,
|
||||
"project_managed_application_graph",
|
||||
);
|
||||
|
||||
/** Provider-neutral managed-code workflow contracts. */
|
||||
export const MANAGED_WORKFLOW_TOOL_CONTRACTS = [
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import type { ToolContract } from "./toolContracts.js";
|
|||
import { nativeOutputSchemas } from "./toolOutputSchemas.js";
|
||||
import { jsonValueSchema } from "../domain/jsonValue.js";
|
||||
import { toolContractMetadata } from "./toolEffects.js";
|
||||
import { requireOutputSchema } from "./toolOutputSchemaPrimitives.js";
|
||||
|
||||
const examples: Readonly<Record<string, Readonly<Record<string, unknown>>>> = {
|
||||
inspect_macho: {},
|
||||
|
|
@ -13,14 +14,12 @@ const examples: Readonly<Record<string, Readonly<Record<string, unknown>>>> = {
|
|||
demangle_swift: { symbols: ["$s4Test3fooyyF"] },
|
||||
};
|
||||
|
||||
const native = <Name extends string>(
|
||||
const native = <Name extends string, Schema extends z.ZodObject>(
|
||||
name: Name,
|
||||
description: string,
|
||||
inputSchema: z.ZodObject,
|
||||
): ToolContract<Name> => {
|
||||
const outputSchema = nativeOutputSchemas[name];
|
||||
if (outputSchema === undefined)
|
||||
throw new Error(`Missing native output schema for ${name}`);
|
||||
inputSchema: Schema,
|
||||
) => {
|
||||
const outputSchema = requireOutputSchema(nativeOutputSchemas, name);
|
||||
return {
|
||||
name,
|
||||
...toolContractMetadata(name),
|
||||
|
|
@ -36,7 +35,7 @@ const native = <Name extends string>(
|
|||
.parse(examples[name] ?? {}),
|
||||
},
|
||||
],
|
||||
};
|
||||
} satisfies ToolContract<Name, Schema, typeof outputSchema>;
|
||||
};
|
||||
|
||||
/** Provider-neutral semantic operations backed initially by macOS utilities. */
|
||||
|
|
|
|||
|
|
@ -35,13 +35,21 @@ export const TOOL_KINDS = [
|
|||
export type ToolKind = (typeof TOOL_KINDS)[number];
|
||||
|
||||
/** Single source of truth for a public MCP tool. */
|
||||
export interface ToolContract<Name extends string = string> {
|
||||
export interface ToolContract<
|
||||
Name extends string = string,
|
||||
InputSchema extends z.ZodType<Readonly<Record<string, unknown>>> = z.ZodType<
|
||||
Readonly<Record<string, unknown>>
|
||||
>,
|
||||
OutputSchema extends z.ZodType<Readonly<Record<string, unknown>>> = z.ZodType<
|
||||
Readonly<Record<string, unknown>>
|
||||
>,
|
||||
> {
|
||||
readonly name: Name;
|
||||
readonly title: string;
|
||||
readonly description: string;
|
||||
readonly kind: ToolKind;
|
||||
readonly inputSchema: z.ZodObject;
|
||||
readonly outputSchema: z.ZodObject;
|
||||
readonly inputSchema: InputSchema;
|
||||
readonly outputSchema: OutputSchema;
|
||||
readonly effects: ToolEffects;
|
||||
readonly annotations: ToolAnnotations;
|
||||
readonly examples: readonly ToolExample[];
|
||||
|
|
|
|||
|
|
@ -67,11 +67,11 @@ export {
|
|||
|
||||
export type { ToolContract, ToolExample } from "./toolContractTypes.js";
|
||||
|
||||
const official = <Name extends string>(
|
||||
const official = <Name extends string, Schema extends z.ZodObject>(
|
||||
name: Name,
|
||||
description: string,
|
||||
inputSchema: z.ZodObject,
|
||||
): ToolContract<Name> => {
|
||||
inputSchema: Schema,
|
||||
) => {
|
||||
const trackedInputSchema = inputSchema.extend({
|
||||
unknown_registry_approved: z
|
||||
.literal(true)
|
||||
|
|
@ -88,36 +88,38 @@ const official = <Name extends string>(
|
|||
inputSchema: trackedInputSchema,
|
||||
outputSchema: requireOutputSchema(officialOutputSchemas, name),
|
||||
examples: examplesFor(name, trackedInputSchema),
|
||||
};
|
||||
} satisfies ToolContract<Name, typeof trackedInputSchema>;
|
||||
};
|
||||
|
||||
const enhanced = <Name extends string>(
|
||||
const enhanced = <Name extends string, Schema extends z.ZodObject>(
|
||||
name: Name,
|
||||
description: string,
|
||||
inputSchema: z.ZodObject,
|
||||
): ToolContract<Name> => ({
|
||||
name,
|
||||
...toolContractMetadata(name),
|
||||
description,
|
||||
kind: "enhanced",
|
||||
inputSchema,
|
||||
outputSchema: requireOutputSchema(enhancedOutputSchemas, name),
|
||||
examples: examplesFor(name, inputSchema),
|
||||
});
|
||||
inputSchema: Schema,
|
||||
) =>
|
||||
({
|
||||
name,
|
||||
...toolContractMetadata(name),
|
||||
description,
|
||||
kind: "enhanced",
|
||||
inputSchema,
|
||||
outputSchema: requireOutputSchema(enhancedOutputSchemas, name),
|
||||
examples: examplesFor(name, inputSchema),
|
||||
}) satisfies ToolContract<Name, Schema>;
|
||||
|
||||
const session = <Name extends string>(
|
||||
const session = <Name extends string, Schema extends z.ZodObject>(
|
||||
name: Name,
|
||||
description: string,
|
||||
inputSchema: z.ZodObject,
|
||||
): ToolContract<Name> => ({
|
||||
name,
|
||||
...toolContractMetadata(name),
|
||||
description,
|
||||
kind: "session",
|
||||
inputSchema,
|
||||
outputSchema: requireOutputSchema(sessionOutputSchemas, name),
|
||||
examples: examplesFor(name, inputSchema),
|
||||
});
|
||||
inputSchema: Schema,
|
||||
) =>
|
||||
({
|
||||
name,
|
||||
...toolContractMetadata(name),
|
||||
description,
|
||||
kind: "session",
|
||||
inputSchema,
|
||||
outputSchema: requireOutputSchema(sessionOutputSchemas, name),
|
||||
examples: examplesFor(name, inputSchema),
|
||||
}) satisfies ToolContract<Name, Schema>;
|
||||
|
||||
/** Bridge operations exposed without additional application composition. */
|
||||
export const OFFICIAL_TOOL_CONTRACTS = [
|
||||
|
|
|
|||
|
|
@ -194,6 +194,7 @@ export const TOOL_EFFECTS: Readonly<Record<string, ToolEffects>> = {
|
|||
mutatesSession: true,
|
||||
writesFilesystem: true,
|
||||
launchesProcess: true,
|
||||
accessesNetwork: true,
|
||||
idempotent: false,
|
||||
}),
|
||||
compare_process_captures: sessionEvidence,
|
||||
|
|
|
|||
|
|
@ -62,11 +62,8 @@ export const targetKindSchema = z.enum([
|
|||
"artifact",
|
||||
]);
|
||||
|
||||
const providerCapability = z.object({
|
||||
const providerCapabilityFacts = {
|
||||
operation: z.string(),
|
||||
available: z.boolean(),
|
||||
reason: z.string().nullable(),
|
||||
availability_code: z.enum(PROVIDER_REJECTION_CODES).nullable(),
|
||||
input_contract_version: z.number().int().min(1),
|
||||
output_contract_version: z.number().int().min(1),
|
||||
pagination: z.enum(["none", "offset", "cursor"]),
|
||||
|
|
@ -86,7 +83,23 @@ const providerCapability = z.object({
|
|||
timeout_ms: z.number().int().min(0).nullable(),
|
||||
}),
|
||||
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(),
|
||||
|
|
@ -225,32 +238,27 @@ export type ClientFeatureAvailability = z.infer<
|
|||
typeof clientFeatureAvailabilitySchema
|
||||
>;
|
||||
|
||||
export const toolAvailability = z.object({
|
||||
const toolAvailabilityMode = z.discriminatedUnion("available", [
|
||||
z.object({
|
||||
name: z.string(),
|
||||
available: z.literal(true),
|
||||
missing_operations: z.array(z.string()).max(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(),
|
||||
available: z.boolean(),
|
||||
reason: z.enum([
|
||||
"available",
|
||||
"client_capability_missing",
|
||||
"target_required",
|
||||
"provider_missing",
|
||||
"provider_unavailable",
|
||||
"target_unsupported",
|
||||
"unsupported_host",
|
||||
"policy_disabled",
|
||||
]),
|
||||
remediation: z.string().nullable(),
|
||||
default_mode_available: z.boolean().optional(),
|
||||
modes: z
|
||||
.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
available: z.boolean(),
|
||||
missing_operations: z.array(z.string()),
|
||||
remediation: z.string().nullable(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
modes: z.array(toolAvailabilityMode).optional(),
|
||||
client_requirements: z.object({
|
||||
required: z.array(clientFeatureNameSchema),
|
||||
optional: z.array(clientFeatureNameSchema),
|
||||
|
|
@ -273,7 +281,36 @@ export const toolAvailability = z.object({
|
|||
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",
|
||||
"policy_disabled",
|
||||
]);
|
||||
|
||||
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,
|
||||
|
|
@ -377,14 +414,25 @@ const availableMemoryPermissions = z.object({
|
|||
source: z.literal("ghidra-memory-block"),
|
||||
});
|
||||
|
||||
export const bounded = (item: z.ZodType) =>
|
||||
z.object({
|
||||
export const bounded = (item: z.ZodType) => {
|
||||
const facts = {
|
||||
items: z.array(item),
|
||||
total: z.number().int().min(0).nullable(),
|
||||
returned: z.number().int().min(0),
|
||||
truncated: z.boolean(),
|
||||
next_offset: z.number().int().min(0).nullable(),
|
||||
});
|
||||
} as const;
|
||||
return z.discriminatedUnion("truncated", [
|
||||
z.object({
|
||||
...facts,
|
||||
truncated: z.literal(false),
|
||||
next_offset: z.null(),
|
||||
}),
|
||||
z.object({
|
||||
...facts,
|
||||
truncated: z.literal(true),
|
||||
next_offset: z.number().int().min(0).nullable(),
|
||||
}),
|
||||
]);
|
||||
};
|
||||
|
||||
const addressedValue = z.object({
|
||||
address: z.string(),
|
||||
|
|
|
|||
|
|
@ -1,155 +0,0 @@
|
|||
import { READINESS_STAGE_IDS } from "../domain/reconstructionReadinessJourneyFindings.js";
|
||||
|
||||
type JsonSchema = Readonly<Record<string, unknown>>;
|
||||
const conditionResultKey = ["t", "hen"].join("");
|
||||
|
||||
const exactlyOne = (left: string, right: string): JsonSchema => ({
|
||||
oneOf: [{ required: [left] }, { required: [right] }],
|
||||
});
|
||||
|
||||
const exclusivePairs = (
|
||||
left: [string, string],
|
||||
right: [string, string],
|
||||
): JsonSchema => ({
|
||||
oneOf: [
|
||||
{
|
||||
required: left,
|
||||
not: {
|
||||
anyOf: right.map((field) => ({ required: [field] })),
|
||||
},
|
||||
},
|
||||
{
|
||||
required: right,
|
||||
not: {
|
||||
anyOf: left.map((field) => ({ required: [field] })),
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const twoEvidenceSources = (
|
||||
left: [string, string],
|
||||
right: [string, string],
|
||||
): JsonSchema => ({
|
||||
allOf: [exactlyOne(left[0], left[1]), exactlyOne(right[0], right[1])],
|
||||
});
|
||||
|
||||
const approvalDependency = (flag: string, approval: string): JsonSchema => ({
|
||||
if: {
|
||||
required: [flag],
|
||||
properties: {
|
||||
[flag]: {
|
||||
const: true,
|
||||
description: `Whether ${flag} was requested`,
|
||||
},
|
||||
},
|
||||
},
|
||||
[conditionResultKey]: {
|
||||
required: [approval],
|
||||
properties: {
|
||||
[approval]: {
|
||||
const: true,
|
||||
description: `Approval for ${flag}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const exactReadinessStages: JsonSchema = {
|
||||
allOf: [
|
||||
{
|
||||
properties: {
|
||||
stages: {
|
||||
description: "Complete reconstruction-readiness stage set",
|
||||
allOf: READINESS_STAGE_IDS.map((stageId) => ({
|
||||
contains: {
|
||||
type: "object",
|
||||
properties: {
|
||||
stage_id: {
|
||||
const: stageId,
|
||||
description: "Readiness stage identifier",
|
||||
},
|
||||
},
|
||||
required: ["stage_id"],
|
||||
description: "One reconstruction-readiness stage",
|
||||
},
|
||||
minContains: 1,
|
||||
maxContains: 1,
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/** Public JSON-Schema constraints that Standard Schema cannot derive from refinements. */
|
||||
export const TOOL_INPUT_JSON_SCHEMA_OVERRIDES: Readonly<
|
||||
Record<string, JsonSchema>
|
||||
> = {
|
||||
compare_web_captures: {
|
||||
...exclusivePairs(
|
||||
["before", "after"],
|
||||
["before_scenario", "after_scenario"],
|
||||
),
|
||||
},
|
||||
inspect_web_page: {
|
||||
allOf: [
|
||||
approvalDependency("include_console_text", "console_text_approved"),
|
||||
approvalDependency(
|
||||
"include_json_body_shapes",
|
||||
"json_body_schema_approved",
|
||||
),
|
||||
approvalDependency(
|
||||
"include_websocket_shapes",
|
||||
"websocket_shape_approved",
|
||||
),
|
||||
],
|
||||
},
|
||||
analyze_web_bundle: {
|
||||
allOf: [
|
||||
approvalDependency("include_console_text", "console_text_approved"),
|
||||
approvalDependency(
|
||||
"include_json_body_shapes",
|
||||
"json_body_schema_approved",
|
||||
),
|
||||
approvalDependency(
|
||||
"include_websocket_shapes",
|
||||
"websocket_shape_approved",
|
||||
),
|
||||
approvalDependency("fetch_source_maps", "source_map_fetch_approved"),
|
||||
],
|
||||
},
|
||||
inspect_electron_page: {
|
||||
allOf: [
|
||||
approvalDependency("include_script_sources", "source_capture_approved"),
|
||||
],
|
||||
},
|
||||
trace_application_feature: {
|
||||
...exactlyOne("application", "application_evidence_id"),
|
||||
},
|
||||
trace_javascript_semantics: {
|
||||
...exactlyOne("application", "application_evidence_id"),
|
||||
},
|
||||
compare_application_versions: twoEvidenceSources(
|
||||
["left", "left_evidence_id"],
|
||||
["right", "right_evidence_id"],
|
||||
),
|
||||
compare_javascript_export_shapes: twoEvidenceSources(
|
||||
["left", "left_evidence_id"],
|
||||
["right", "right_evidence_id"],
|
||||
),
|
||||
compare_source_to_bundle: exactlyOne(
|
||||
"application",
|
||||
"application_evidence_id",
|
||||
),
|
||||
evaluate_reconstruction_readiness: exactReadinessStages,
|
||||
};
|
||||
|
||||
/** Merge a contract-specific public constraint into generated or advertised input JSON Schema. */
|
||||
export const applyToolInputJsonSchemaOverride = (
|
||||
name: string,
|
||||
schema: JsonSchema,
|
||||
): JsonSchema => {
|
||||
const override = TOOL_INPUT_JSON_SCHEMA_OVERRIDES[name];
|
||||
return override === undefined ? schema : { ...schema, ...override };
|
||||
};
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import type { ToolContract } from "./toolContractTypes.js";
|
||||
|
||||
const PROPERTY_DESCRIPTIONS: Readonly<Record<string, string>> = {
|
||||
addresses: "Ordered provider-normalized procedure addresses to analyze.",
|
||||
approved: "Explicit operator approval to perform this operation.",
|
||||
after: "The later or right-hand observation to compare.",
|
||||
before: "The earlier or left-hand observation to compare.",
|
||||
boundary_id: "Exact reconstruction boundary identifier to evaluate.",
|
||||
case_sensitive:
|
||||
"Whether text matching distinguishes uppercase and lowercase.",
|
||||
cdp_endpoint: "Approved literal loopback Chrome DevTools Protocol endpoint.",
|
||||
comment: "Exact analyst comment text to write.",
|
||||
comparisons: "Validated comparison Evidence records to aggregate.",
|
||||
coverage: "Exact reconstruction-coverage commitment to verify.",
|
||||
detail: "Requested response detail level.",
|
||||
direction: "Direction in which to traverse or compare relationships.",
|
||||
document: "Exact provider document or program identity.",
|
||||
error: "Structured, caller-actionable error when the operation fails.",
|
||||
evidence: "Evidence v2 record produced by this operation.",
|
||||
evidence_id: "Stable identifier of the recorded Evidence v2 observation.",
|
||||
evidence_uri:
|
||||
"Session resource URI for the recorded Evidence v2 observation.",
|
||||
executable: "Approved absolute path of the executable to run.",
|
||||
format: "Declared input artifact format.",
|
||||
name: "Exact name used by this operation.",
|
||||
pattern: "Text or bounded pattern used to filter matching results.",
|
||||
phase: "Current plan or execution phase of the operation.",
|
||||
plan: "Content-bound execution plan and its approval commitment.",
|
||||
query: "Non-empty feature or text query to investigate.",
|
||||
question: "Concrete unresolved question to retain for later investigation.",
|
||||
result: "Primary structured result returned by this operation.",
|
||||
left: "Left-hand input used for comparison or differential execution.",
|
||||
limit: "Maximum number of results to return in this page.",
|
||||
limits:
|
||||
"Bounded resource-consumption and result-size limits for this operation.",
|
||||
mode: "Operation mode that selects the requested behavior.",
|
||||
offset: "Zero-based index of the first result to return.",
|
||||
overwrite: "Whether an existing destination may be replaced.",
|
||||
path: "Local filesystem path used by this operation.",
|
||||
provider_id:
|
||||
"Exact deep-analysis provider ID, or automatic selection when omitted.",
|
||||
right: "Right-hand input used for comparison or differential execution.",
|
||||
schema_version: "Version of this structured result schema.",
|
||||
source_evidence:
|
||||
"Evidence records supporting the prepared source transformation.",
|
||||
status: "Current lifecycle or verification status.",
|
||||
summary: "Concise evidence-backed summary of the result.",
|
||||
symbols: "Ordered bounded Swift symbols to demangle.",
|
||||
target_id: "Exact authorized CDP target identifier.",
|
||||
unknown_id: "Exact residual-unknown identifier.",
|
||||
unknown_registry_approved:
|
||||
"Explicit approval to record bounded residual uncertainty in the session registry.",
|
||||
workspace_path:
|
||||
"Approved local path of the persistent investigation workspace.",
|
||||
};
|
||||
|
||||
/** Attach caller guidance to a canonical schema for the SDK wire projection. */
|
||||
export const toolInputSchemaWithMetadata = <Contract extends ToolContract>(
|
||||
contract: Contract,
|
||||
): Contract["inputSchema"] => {
|
||||
const schema = contract.inputSchema.meta(
|
||||
z.globalRegistry.get(contract.inputSchema) ?? {},
|
||||
);
|
||||
const standard = schema["~standard"];
|
||||
const inputJsonSchema = standard.jsonSchema?.input;
|
||||
if (inputJsonSchema === undefined)
|
||||
throw new TypeError(
|
||||
"Tool input schema does not expose Standard JSON Schema",
|
||||
);
|
||||
|
||||
// Zod's input projection drops root metadata when a descendant transforms.
|
||||
// Preserve the parser and let the SDK own conversion of everything else.
|
||||
Object.defineProperty(schema, "~standard", {
|
||||
value: {
|
||||
...standard,
|
||||
jsonSchema: {
|
||||
...standard.jsonSchema,
|
||||
input: (options: Parameters<typeof inputJsonSchema>[0]) => ({
|
||||
...describeProperties(inputJsonSchema(options)),
|
||||
examples: contract.examples.map(({ input }) => input),
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
return schema;
|
||||
};
|
||||
|
||||
const describeProperties = (
|
||||
value: Readonly<Record<string, unknown>>,
|
||||
): Record<string, unknown> =>
|
||||
Object.fromEntries(
|
||||
Object.entries(value).map(([key, child]) => {
|
||||
if (key !== "properties" || !isObject(child))
|
||||
return [key, describeValue(child)];
|
||||
return [
|
||||
key,
|
||||
Object.fromEntries(
|
||||
Object.entries(child).map(([property, propertySchema]) => [
|
||||
property,
|
||||
isObject(propertySchema) &&
|
||||
typeof propertySchema.description !== "string"
|
||||
? {
|
||||
...describeProperties(propertySchema),
|
||||
description: fallbackPropertyDescription(property),
|
||||
}
|
||||
: describeValue(propertySchema),
|
||||
]),
|
||||
),
|
||||
];
|
||||
}),
|
||||
);
|
||||
|
||||
const describeValue = (value: unknown): unknown => {
|
||||
if (Array.isArray(value)) return value.map(describeValue);
|
||||
return isObject(value) ? describeProperties(value) : value;
|
||||
};
|
||||
|
||||
const fallbackPropertyDescription = (property: string): string => {
|
||||
const explicit = PROPERTY_DESCRIPTIONS[property];
|
||||
if (explicit !== undefined) return explicit;
|
||||
const words = property.replaceAll("_", " ");
|
||||
if (property.endsWith("_approved"))
|
||||
return `Explicit operator approval for ${words.slice(0, -9)}.`;
|
||||
if (property.endsWith("_offset"))
|
||||
return `Zero-based index of the first ${words.slice(0, -7)} to return.`;
|
||||
if (property.endsWith("_limit"))
|
||||
return `Maximum number of ${words.slice(0, -6)} entries to return.`;
|
||||
if (property.startsWith("max_"))
|
||||
return `Maximum permitted ${words.slice(4)} for this operation.`;
|
||||
if (property.startsWith("include_"))
|
||||
return `Whether to include ${words.slice(8)} in the result.`;
|
||||
if (property.startsWith("expected_"))
|
||||
return `Expected ${words.slice(9)} used to reject stale or mismatched input.`;
|
||||
if (property.endsWith("_sha256"))
|
||||
return `Exact SHA-256 digest of ${words.slice(0, -7)}.`;
|
||||
if (property.endsWith("_evidence_id"))
|
||||
return `Exact Evidence v2 identifier for the ${words.slice(0, -12)} observation.`;
|
||||
if (property.endsWith("_evidence_ids"))
|
||||
return `Ordered Evidence v2 identifiers for the ${words.slice(0, -13)} observations.`;
|
||||
if (property.endsWith("_path"))
|
||||
return `Local filesystem path for ${words.slice(0, -5)}.`;
|
||||
if (property.endsWith("_uri"))
|
||||
return `Canonical URI for ${words.slice(0, -4)}.`;
|
||||
if (property.endsWith("_bytes"))
|
||||
return `Bounded byte count for ${words.slice(0, -6)}.`;
|
||||
if (property.endsWith("_root") || property.endsWith("_roots"))
|
||||
return `Approved canonical filesystem ${words}.`;
|
||||
if (property.startsWith("is_") || property.startsWith("has_"))
|
||||
return `Whether ${words}.`;
|
||||
return `Value for ${words}.`;
|
||||
};
|
||||
|
||||
const isObject = (value: unknown): value is Readonly<Record<string, unknown>> =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
|
|
@ -71,7 +71,7 @@ const inspectJavaScriptReplay = async (): Promise<DoctorCheck> => {
|
|||
detail: config.error.message,
|
||||
remediation: "Fix the reported REA_JAVASCRIPT_REPLAY_* configuration.",
|
||||
};
|
||||
if (!config.value.javascriptReplayPolicy.enabled)
|
||||
if (config.value.javascriptReplayPolicy.status === "disabled")
|
||||
return {
|
||||
name: "javascript-replay",
|
||||
ok: true,
|
||||
|
|
|
|||
|
|
@ -202,6 +202,8 @@ const retain = <Groups extends Record<string, readonly Component[]>>(
|
|||
maximum: number,
|
||||
): { [Key in keyof Groups]: Component[] } => {
|
||||
let remaining = maximum;
|
||||
// SAFETY: the projection preserves every key from Groups and replaces only
|
||||
// each readonly Component array with a bounded mutable Component array.
|
||||
return Object.fromEntries(
|
||||
Object.entries(groups).map(([key, values]) => {
|
||||
const selected = [...values]
|
||||
|
|
|
|||
|
|
@ -201,6 +201,8 @@ const retainComponents = <Groups extends Record<string, readonly Component[]>>(
|
|||
maximum: number,
|
||||
): { [Key in keyof Groups]: Component[] } => {
|
||||
let remaining = maximum;
|
||||
// SAFETY: the projection preserves every key from Groups and replaces only
|
||||
// each readonly Component array with a bounded mutable Component array.
|
||||
return Object.fromEntries(
|
||||
Object.entries(groups).map(([key, values]) => {
|
||||
const retained = [...values]
|
||||
|
|
|
|||
|
|
@ -13,45 +13,13 @@ import { extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|||
import { promisify } from "node:util";
|
||||
|
||||
import { BinaryTargetError } from "./errors.js";
|
||||
import type { BinaryArchitecture, BinaryTarget } from "./binaryTargetTypes.js";
|
||||
import { err, ok, type Result } from "./result.js";
|
||||
import { zipPackageFormatForPath } from "./zipPackageFormat.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
export type { BinaryArchitecture, BinaryTarget } from "./binaryTargetTypes.js";
|
||||
|
||||
/** Provider-neutral CPU families detected from supported executable headers. */
|
||||
export type BinaryArchitecture = "x86" | "x86_64" | "arm" | "arm64";
|
||||
/**
|
||||
* Canonical local target identity and provider-neutral file classification.
|
||||
* Provider adapters translate this metadata into their own open options.
|
||||
*/
|
||||
export interface BinaryTarget {
|
||||
readonly path: string;
|
||||
readonly sourcePath?: string;
|
||||
readonly sha256: string;
|
||||
readonly kind: "executable" | "database" | "archive" | "artifact";
|
||||
readonly format:
|
||||
| "analysis-database"
|
||||
| "mach-o"
|
||||
| "elf"
|
||||
| "pe"
|
||||
| "zip"
|
||||
| "ipa"
|
||||
| "apk"
|
||||
| "msix"
|
||||
| "appx"
|
||||
| "asar"
|
||||
| "dmg"
|
||||
| "pkg"
|
||||
| "plist"
|
||||
| "javascript"
|
||||
| "source-map";
|
||||
readonly architecture?: BinaryArchitecture;
|
||||
readonly availableArchitectures?: readonly BinaryArchitecture[];
|
||||
/** Provider-neutral PE image role observed from COFF characteristics. */
|
||||
readonly executableRole?: "application" | "shared-library" | "non-executable";
|
||||
/** Whether a PE declares a non-empty CLI header data-directory entry. */
|
||||
readonly managed?: boolean;
|
||||
}
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
/**
|
||||
* Resolve and classify a readable local target before a provider is selected.
|
||||
|
|
@ -87,14 +55,16 @@ export const parseBinaryTarget = async (
|
|||
format: "analysis-database",
|
||||
});
|
||||
const artifactFormat = await detectArtifactFormat(path, handle);
|
||||
if (artifactFormat !== undefined)
|
||||
return ok({
|
||||
if (artifactFormat !== undefined) {
|
||||
const identity = {
|
||||
path,
|
||||
sourcePath: canonical,
|
||||
sha256: await sha256Handle(handle),
|
||||
kind: isArchiveFormat(artifactFormat) ? "archive" : "artifact",
|
||||
format: artifactFormat,
|
||||
});
|
||||
};
|
||||
return isArchiveFormat(artifactFormat)
|
||||
? ok({ ...identity, kind: "archive", format: artifactFormat })
|
||||
: ok({ ...identity, kind: "artifact", format: artifactFormat });
|
||||
}
|
||||
const detected = await readExecutableMetadata(handle, hostArchitecture);
|
||||
if (!detected.ok) return err(new BinaryTargetError(path, detected.error));
|
||||
return ok({
|
||||
|
|
@ -165,7 +135,12 @@ const namedArtifactFormat = (
|
|||
return /\.(?:m?js|cjs)$/u.test(lowerPath) ? "javascript" : undefined;
|
||||
};
|
||||
|
||||
const isArchiveFormat = (format: BinaryTarget["format"]): boolean =>
|
||||
const isArchiveFormat = (
|
||||
format: Exclude<
|
||||
BinaryTarget["format"],
|
||||
"analysis-database" | "mach-o" | "elf" | "pe"
|
||||
>,
|
||||
): format is Extract<BinaryTarget, { kind: "archive" }>["format"] =>
|
||||
["zip", "ipa", "apk", "msix", "appx", "asar", "dmg", "pkg"].includes(format);
|
||||
|
||||
const sha256Handle = async (handle: FileHandle): Promise<string> => {
|
||||
|
|
@ -268,14 +243,22 @@ const decodeXml = (value: string): string =>
|
|||
.replaceAll(""", '"')
|
||||
.replaceAll("'", "'");
|
||||
|
||||
type ExecutableMetadata = Pick<
|
||||
BinaryTarget,
|
||||
| "format"
|
||||
| "architecture"
|
||||
| "availableArchitectures"
|
||||
| "executableRole"
|
||||
| "managed"
|
||||
>;
|
||||
type ExecutableMetadata =
|
||||
| {
|
||||
readonly format: "mach-o" | "elf";
|
||||
readonly architecture: BinaryArchitecture;
|
||||
readonly availableArchitectures: readonly BinaryArchitecture[];
|
||||
}
|
||||
| {
|
||||
readonly format: "pe";
|
||||
readonly architecture: BinaryArchitecture;
|
||||
readonly availableArchitectures: readonly BinaryArchitecture[];
|
||||
readonly executableRole:
|
||||
| "application"
|
||||
| "shared-library"
|
||||
| "non-executable";
|
||||
readonly managed: boolean;
|
||||
};
|
||||
|
||||
const readExecutableMetadata = async (
|
||||
handle: FileHandle,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
/** Provider-neutral CPU families detected from supported executable headers. */
|
||||
export type BinaryArchitecture = "x86" | "x86_64" | "arm" | "arm64";
|
||||
|
||||
interface BinaryTargetIdentity {
|
||||
readonly path: string;
|
||||
readonly sourcePath?: string;
|
||||
readonly sha256: string;
|
||||
}
|
||||
|
||||
type NonExecutableMetadata = {
|
||||
readonly architecture?: never;
|
||||
readonly availableArchitectures?: never;
|
||||
readonly executableRole?: never;
|
||||
readonly managed?: never;
|
||||
};
|
||||
|
||||
type ExecutableTarget = BinaryTargetIdentity & {
|
||||
readonly kind: "executable";
|
||||
readonly architecture: BinaryArchitecture;
|
||||
readonly availableArchitectures: readonly BinaryArchitecture[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Canonical local target identity and provider-neutral file classification.
|
||||
* Each variant carries only metadata established for that exact file kind.
|
||||
*/
|
||||
export type BinaryTarget =
|
||||
| (ExecutableTarget & {
|
||||
readonly format: "mach-o" | "elf";
|
||||
readonly executableRole?: never;
|
||||
readonly managed?: never;
|
||||
})
|
||||
| (ExecutableTarget & {
|
||||
readonly format: "pe";
|
||||
/** Provider-neutral PE image role observed from COFF characteristics. */
|
||||
readonly executableRole:
|
||||
| "application"
|
||||
| "shared-library"
|
||||
| "non-executable";
|
||||
/** Whether the PE declares a non-empty CLI header data-directory entry. */
|
||||
readonly managed: boolean;
|
||||
})
|
||||
| (BinaryTargetIdentity &
|
||||
NonExecutableMetadata & {
|
||||
readonly kind: "database";
|
||||
readonly format: "analysis-database";
|
||||
})
|
||||
| (BinaryTargetIdentity &
|
||||
NonExecutableMetadata & {
|
||||
readonly kind: "archive";
|
||||
readonly format:
|
||||
| "zip"
|
||||
| "ipa"
|
||||
| "apk"
|
||||
| "msix"
|
||||
| "appx"
|
||||
| "asar"
|
||||
| "dmg"
|
||||
| "pkg";
|
||||
})
|
||||
| (BinaryTargetIdentity &
|
||||
NonExecutableMetadata & {
|
||||
readonly kind: "artifact";
|
||||
readonly format: "plist" | "javascript" | "source-map";
|
||||
});
|
||||
|
|
@ -11,92 +11,41 @@ import {
|
|||
compareWebCaptures,
|
||||
captureSnapshotSchema,
|
||||
webCaptureDiffSchema,
|
||||
type CompareWebCapturesInput,
|
||||
type WebCaptureDiff,
|
||||
} from "./webCaptureDiff.js";
|
||||
|
||||
/** Top-level MCP shape for legacy page or step-indexed scenario comparison. */
|
||||
export const browserCaptureComparisonWireSchema = z
|
||||
.strictObject({
|
||||
before: captureSnapshotSchema
|
||||
.optional()
|
||||
.describe("Earlier passive web-page capture; pair with after."),
|
||||
after: captureSnapshotSchema
|
||||
.optional()
|
||||
.describe("Later passive web-page capture; pair with before."),
|
||||
before_scenario: compareBrowserScenariosInputSchema.shape.before_scenario
|
||||
.optional()
|
||||
.describe("Earlier browser scenario capture; pair with after_scenario."),
|
||||
after_scenario: compareBrowserScenariosInputSchema.shape.after_scenario
|
||||
.optional()
|
||||
.describe("Later browser scenario capture; pair with before_scenario."),
|
||||
const maxChangesSchema = z
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(20_000)
|
||||
.default(2_000)
|
||||
.describe("Maximum normalized change records to retain.");
|
||||
|
||||
/** Top-level MCP shape for exactly one complete passive or scenario pair. */
|
||||
export const browserCaptureComparisonInputSchema = z.union([
|
||||
z.strictObject({
|
||||
before: captureSnapshotSchema.describe("Earlier passive web-page capture."),
|
||||
after: captureSnapshotSchema.describe("Later passive web-page capture."),
|
||||
max_changes: maxChangesSchema,
|
||||
}),
|
||||
z.strictObject({
|
||||
before_scenario:
|
||||
compareBrowserScenariosInputSchema.shape.before_scenario.describe(
|
||||
"Earlier browser scenario capture.",
|
||||
),
|
||||
after_scenario:
|
||||
compareBrowserScenariosInputSchema.shape.after_scenario.describe(
|
||||
"Later browser scenario capture.",
|
||||
),
|
||||
normalization:
|
||||
compareBrowserScenariosInputSchema.shape.normalization.describe(
|
||||
"Scenario-only exact-literal normalization policy.",
|
||||
"Exact-literal normalization policy.",
|
||||
),
|
||||
max_changes: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(20_000)
|
||||
.default(2_000)
|
||||
.describe("Maximum normalized change records to retain."),
|
||||
})
|
||||
.superRefine((input, context) => {
|
||||
const hasPassive = input.before !== undefined || input.after !== undefined;
|
||||
const hasScenario =
|
||||
input.before_scenario !== undefined || input.after_scenario !== undefined;
|
||||
if (hasPassive === hasScenario)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message:
|
||||
"Provide exactly one complete pair: before/after or before_scenario/after_scenario",
|
||||
});
|
||||
if (hasPassive && (input.before === undefined || input.after === undefined))
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "Passive comparison requires both before and after",
|
||||
});
|
||||
if (
|
||||
hasScenario &&
|
||||
(input.before_scenario === undefined ||
|
||||
input.after_scenario === undefined)
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message:
|
||||
"Browser scenario comparison requires both before_scenario and after_scenario",
|
||||
});
|
||||
if (hasPassive && input.normalization.rules.length > 0)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["normalization"],
|
||||
message:
|
||||
"Literal normalization rules apply only to browser scenario captures",
|
||||
});
|
||||
});
|
||||
max_changes: maxChangesSchema,
|
||||
}),
|
||||
]);
|
||||
|
||||
/** Parsed legacy page snapshots or step-indexed browser scenarios. */
|
||||
export const browserCaptureComparisonInputSchema =
|
||||
browserCaptureComparisonWireSchema.transform((input) => {
|
||||
if (
|
||||
input.before_scenario !== undefined &&
|
||||
input.after_scenario !== undefined
|
||||
)
|
||||
return {
|
||||
before_scenario: input.before_scenario,
|
||||
after_scenario: input.after_scenario,
|
||||
normalization: input.normalization,
|
||||
max_changes: input.max_changes,
|
||||
} satisfies CompareBrowserScenariosInput;
|
||||
if (input.before !== undefined && input.after !== undefined)
|
||||
return {
|
||||
before: input.before,
|
||||
after: input.after,
|
||||
max_changes: input.max_changes,
|
||||
} satisfies CompareWebCapturesInput;
|
||||
throw new TypeError("Capture comparison pair refinement failed");
|
||||
});
|
||||
/** Parsed browser capture comparison input. */
|
||||
export type BrowserCaptureComparisonInput = z.output<
|
||||
typeof browserCaptureComparisonInputSchema
|
||||
|
|
|
|||
|
|
@ -207,103 +207,126 @@ const DEFAULT_BROWSER_INSPECTION_LIMITS = {
|
|||
max_total_websocket_shape_bytes: 1_024 * 1_024,
|
||||
} as const;
|
||||
|
||||
const inspectWebPageInputFacts = {
|
||||
...approvedBrowserInput,
|
||||
target_id: z.string().trim().min(1).max(256),
|
||||
observation_ms: z.number().int().min(0).max(10_000).default(500),
|
||||
include_accessibility_text: z.boolean().default(false),
|
||||
include_console_text: z.boolean().default(false),
|
||||
console_text_approved: z.boolean().default(false),
|
||||
include_json_body_shapes: z.boolean().default(false),
|
||||
json_body_schema_approved: z.boolean().default(false),
|
||||
include_websocket_shapes: z.boolean().default(false),
|
||||
websocket_shape_approved: z.boolean().default(false),
|
||||
include_storage_keys: z.boolean().default(false),
|
||||
include_storage_fingerprints: z.boolean().default(false),
|
||||
limits: browserInspectionLimitsSchema.default(
|
||||
DEFAULT_BROWSER_INSPECTION_LIMITS,
|
||||
),
|
||||
} as const;
|
||||
|
||||
const inspectWebPageWithoutSourceSchema = z.strictObject({
|
||||
...inspectWebPageInputFacts,
|
||||
include_script_sources: z.literal(false).default(false),
|
||||
source_capture_approved: z.literal(false).default(false),
|
||||
});
|
||||
|
||||
const inspectWebPageWithSourceShapeSchema = z.strictObject({
|
||||
...inspectWebPageInputFacts,
|
||||
include_script_sources: z.literal(true).default(true),
|
||||
source_capture_approved: z.literal(true),
|
||||
});
|
||||
|
||||
type InspectWebPageShape =
|
||||
| z.output<typeof inspectWebPageWithoutSourceSchema>
|
||||
| z.output<typeof inspectWebPageWithSourceShapeSchema>;
|
||||
|
||||
const refineInspectWebPageInput = (
|
||||
input: InspectWebPageShape,
|
||||
context: z.RefinementCtx,
|
||||
): void => {
|
||||
if (input.include_storage_fingerprints && !input.include_storage_keys)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["include_storage_fingerprints"],
|
||||
message: "Storage fingerprints require storage key capture",
|
||||
});
|
||||
if (
|
||||
input.limits.max_script_source_bytes >
|
||||
input.limits.max_total_script_source_bytes
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["limits", "max_script_source_bytes"],
|
||||
message: "Per-script source limit cannot exceed the total source limit",
|
||||
});
|
||||
for (const [include, approved, path, message] of [
|
||||
[
|
||||
input.include_console_text,
|
||||
input.console_text_approved,
|
||||
"console_text_approved",
|
||||
"Console text capture requires separate approval",
|
||||
],
|
||||
[
|
||||
input.include_json_body_shapes,
|
||||
input.json_body_schema_approved,
|
||||
"json_body_schema_approved",
|
||||
"JSON body schema capture requires separate approval",
|
||||
],
|
||||
[
|
||||
input.include_websocket_shapes,
|
||||
input.websocket_shape_approved,
|
||||
"websocket_shape_approved",
|
||||
"WebSocket shape capture requires separate approval",
|
||||
],
|
||||
] as const)
|
||||
if (include && !approved)
|
||||
context.addIssue({ code: "custom", path: [path], message });
|
||||
if (
|
||||
input.limits.max_console_text_field_bytes >
|
||||
input.limits.max_total_console_text_bytes
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["limits", "max_console_text_field_bytes"],
|
||||
message:
|
||||
"Per-field console text limit cannot exceed the total text limit",
|
||||
});
|
||||
if (input.limits.max_json_body_bytes > input.limits.max_total_json_body_bytes)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["limits", "max_json_body_bytes"],
|
||||
message: "Per-body JSON limit cannot exceed the total body limit",
|
||||
});
|
||||
if (
|
||||
input.limits.max_websocket_shape_bytes >
|
||||
input.limits.max_total_websocket_shape_bytes
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["limits", "max_websocket_shape_bytes"],
|
||||
message: "Per-frame WebSocket limit cannot exceed the total frame limit",
|
||||
});
|
||||
if (
|
||||
input.limits.max_ax_text_field_bytes > input.limits.max_total_ax_text_bytes
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["limits", "max_ax_text_field_bytes"],
|
||||
message:
|
||||
"Per-field accessibility text limit cannot exceed the total text limit",
|
||||
});
|
||||
};
|
||||
|
||||
/** Source-capturing inspection input, reusable by static bundle analysis. */
|
||||
export const inspectWebPageWithSourceInputSchema =
|
||||
inspectWebPageWithSourceShapeSchema.superRefine(refineInspectWebPageInput);
|
||||
|
||||
/** Public input for one passive, bounded inspection of an existing page. */
|
||||
export const inspectWebPageInputSchema = z
|
||||
.object({
|
||||
...approvedBrowserInput,
|
||||
target_id: z.string().trim().min(1).max(256),
|
||||
observation_ms: z.number().int().min(0).max(10_000).default(500),
|
||||
include_accessibility_text: z.boolean().default(false),
|
||||
include_console_text: z.boolean().default(false),
|
||||
console_text_approved: z.boolean().default(false),
|
||||
include_json_body_shapes: z.boolean().default(false),
|
||||
json_body_schema_approved: z.boolean().default(false),
|
||||
include_websocket_shapes: z.boolean().default(false),
|
||||
websocket_shape_approved: z.boolean().default(false),
|
||||
include_script_sources: z.boolean().default(false),
|
||||
include_storage_keys: z.boolean().default(false),
|
||||
include_storage_fingerprints: z.boolean().default(false),
|
||||
limits: browserInspectionLimitsSchema.default(
|
||||
DEFAULT_BROWSER_INSPECTION_LIMITS,
|
||||
),
|
||||
})
|
||||
.superRefine((input, context) => {
|
||||
if (input.include_storage_fingerprints && !input.include_storage_keys)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["include_storage_fingerprints"],
|
||||
message: "Storage fingerprints require storage key capture",
|
||||
});
|
||||
if (
|
||||
input.limits.max_script_source_bytes >
|
||||
input.limits.max_total_script_source_bytes
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["limits", "max_script_source_bytes"],
|
||||
message: "Per-script source limit cannot exceed the total source limit",
|
||||
});
|
||||
for (const [include, approved, path, message] of [
|
||||
[
|
||||
input.include_console_text,
|
||||
input.console_text_approved,
|
||||
"console_text_approved",
|
||||
"Console text capture requires separate approval",
|
||||
],
|
||||
[
|
||||
input.include_json_body_shapes,
|
||||
input.json_body_schema_approved,
|
||||
"json_body_schema_approved",
|
||||
"JSON body schema capture requires separate approval",
|
||||
],
|
||||
[
|
||||
input.include_websocket_shapes,
|
||||
input.websocket_shape_approved,
|
||||
"websocket_shape_approved",
|
||||
"WebSocket shape capture requires separate approval",
|
||||
],
|
||||
] as const)
|
||||
if (include && !approved)
|
||||
context.addIssue({ code: "custom", path: [path], message });
|
||||
if (
|
||||
input.limits.max_console_text_field_bytes >
|
||||
input.limits.max_total_console_text_bytes
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["limits", "max_console_text_field_bytes"],
|
||||
message:
|
||||
"Per-field console text limit cannot exceed the total text limit",
|
||||
});
|
||||
if (
|
||||
input.limits.max_json_body_bytes > input.limits.max_total_json_body_bytes
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["limits", "max_json_body_bytes"],
|
||||
message: "Per-body JSON limit cannot exceed the total body limit",
|
||||
});
|
||||
if (
|
||||
input.limits.max_websocket_shape_bytes >
|
||||
input.limits.max_total_websocket_shape_bytes
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["limits", "max_websocket_shape_bytes"],
|
||||
message:
|
||||
"Per-frame WebSocket limit cannot exceed the total frame limit",
|
||||
});
|
||||
if (
|
||||
input.limits.max_ax_text_field_bytes >
|
||||
input.limits.max_total_ax_text_bytes
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["limits", "max_ax_text_field_bytes"],
|
||||
message:
|
||||
"Per-field accessibility text limit cannot exceed the total text limit",
|
||||
});
|
||||
});
|
||||
export const inspectWebPageInputSchema = z.union([
|
||||
inspectWebPageWithoutSourceSchema.superRefine(refineInspectWebPageInput),
|
||||
inspectWebPageWithSourceInputSchema,
|
||||
]);
|
||||
|
||||
export type ListBrowserTargetsInput = z.infer<
|
||||
typeof listBrowserTargetsInputSchema
|
||||
|
|
|
|||
|
|
@ -230,8 +230,9 @@ const parseSnapshots = (
|
|||
const assertCompatible = (
|
||||
snapshots: ReadonlyMap<string, FunctionSnapshot>,
|
||||
): void => {
|
||||
const first = snapshots.values().next().value as FunctionSnapshot | undefined;
|
||||
if (first === undefined) return;
|
||||
const firstEntry = snapshots.values().next();
|
||||
if (firstEntry.done) return;
|
||||
const first = firstEntry.value;
|
||||
for (const snapshot of snapshots.values()) {
|
||||
if (
|
||||
snapshot.subject.digest.sha256 !== first.subject.digest.sha256 ||
|
||||
|
|
|
|||
|
|
@ -166,8 +166,8 @@ export function failedAuthEvents(
|
|||
export function authFlowDuration(
|
||||
events: readonly AuthFlowEvent[],
|
||||
): number | null {
|
||||
if (events.length === 0) return null;
|
||||
const first = events[0]!;
|
||||
const last = events[events.length - 1]!;
|
||||
const first = events.at(0);
|
||||
const last = events.at(-1);
|
||||
if (first === undefined || last === undefined) return null;
|
||||
return last.at_ms - first.at_ms;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,50 +36,54 @@ export type ListElectronTargetsInput = z.infer<
|
|||
>;
|
||||
|
||||
/** Input for passive structural inspection of one Electron file page. */
|
||||
const inspectElectronPageFacts = {
|
||||
...approvedElectronInput,
|
||||
target_id: z.string().trim().min(1).max(256),
|
||||
observation_ms: z.number().int().min(0).max(10_000).default(100),
|
||||
limits: z
|
||||
.object({
|
||||
max_frames: z.number().int().min(1).max(1_000).default(200),
|
||||
max_dom_nodes: z.number().int().min(1).max(10_000).default(2_000),
|
||||
max_scripts: z.number().int().min(1).max(2_000).default(500),
|
||||
max_resources: z.number().int().min(1).max(10_000).default(2_000),
|
||||
max_workers: z.number().int().min(1).max(5_000).default(500),
|
||||
max_script_source_bytes: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(4 * 1_024 * 1_024)
|
||||
.default(1_024 * 1_024),
|
||||
max_total_script_source_bytes: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(16 * 1_024 * 1_024)
|
||||
.default(4 * 1_024 * 1_024),
|
||||
})
|
||||
.default({
|
||||
max_frames: 200,
|
||||
max_dom_nodes: 2_000,
|
||||
max_scripts: 500,
|
||||
max_resources: 2_000,
|
||||
max_workers: 500,
|
||||
max_script_source_bytes: 1_024 * 1_024,
|
||||
max_total_script_source_bytes: 4 * 1_024 * 1_024,
|
||||
}),
|
||||
} as const;
|
||||
export const inspectElectronPageInputSchema = z
|
||||
.object({
|
||||
...approvedElectronInput,
|
||||
target_id: z.string().trim().min(1).max(256),
|
||||
observation_ms: z.number().int().min(0).max(10_000).default(100),
|
||||
include_script_sources: z.boolean().default(false),
|
||||
source_capture_approved: z.boolean().default(false),
|
||||
limits: z
|
||||
.object({
|
||||
max_frames: z.number().int().min(1).max(1_000).default(200),
|
||||
max_dom_nodes: z.number().int().min(1).max(10_000).default(2_000),
|
||||
max_scripts: z.number().int().min(1).max(2_000).default(500),
|
||||
max_resources: z.number().int().min(1).max(10_000).default(2_000),
|
||||
max_workers: z.number().int().min(1).max(5_000).default(500),
|
||||
max_script_source_bytes: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(4 * 1_024 * 1_024)
|
||||
.default(1_024 * 1_024),
|
||||
max_total_script_source_bytes: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(16 * 1_024 * 1_024)
|
||||
.default(4 * 1_024 * 1_024),
|
||||
})
|
||||
.default({
|
||||
max_frames: 200,
|
||||
max_dom_nodes: 2_000,
|
||||
max_scripts: 500,
|
||||
max_resources: 2_000,
|
||||
max_workers: 500,
|
||||
max_script_source_bytes: 1_024 * 1_024,
|
||||
max_total_script_source_bytes: 4 * 1_024 * 1_024,
|
||||
}),
|
||||
})
|
||||
.union([
|
||||
z.object({
|
||||
...inspectElectronPageFacts,
|
||||
include_script_sources: z.literal(false).default(false),
|
||||
source_capture_approved: z.literal(false).default(false),
|
||||
}),
|
||||
z.object({
|
||||
...inspectElectronPageFacts,
|
||||
include_script_sources: z.literal(true),
|
||||
source_capture_approved: z.literal(true),
|
||||
}),
|
||||
])
|
||||
.superRefine((input, context) => {
|
||||
if (input.include_script_sources && !input.source_capture_approved)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["source_capture_approved"],
|
||||
message: "Electron script source capture requires separate approval",
|
||||
});
|
||||
if (
|
||||
input.limits.max_script_source_bytes >
|
||||
input.limits.max_total_script_source_bytes
|
||||
|
|
|
|||
|
|
@ -93,7 +93,10 @@ export const collectContextBridgeMembers = (
|
|||
unknown: 1,
|
||||
omitted: 0,
|
||||
};
|
||||
const state = { members: [] as string[], unknown: 0 };
|
||||
const state: { members: string[]; unknown: number } = {
|
||||
members: [],
|
||||
unknown: 0,
|
||||
};
|
||||
collectMembersAt(node, "", 0, state);
|
||||
const members = [...new Set(state.members)].sort(compareCodePoints);
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -209,7 +209,8 @@ export function getDescendants(
|
|||
const result: number[] = [];
|
||||
const queue: number[] = [rootPid];
|
||||
while (queue.length > 0) {
|
||||
const pid = queue.shift()!;
|
||||
const pid = queue.shift();
|
||||
if (pid === undefined) break;
|
||||
if (visited.has(pid)) continue;
|
||||
visited.add(pid);
|
||||
const node = byPid.get(pid);
|
||||
|
|
|
|||
|
|
@ -177,15 +177,22 @@ export const sorted = (values: readonly unknown[]): readonly unknown[] =>
|
|||
export const combineCoverage = <Item>(
|
||||
left: FunctionCollection<Item>,
|
||||
right: FunctionCollection<Item>,
|
||||
): FunctionCollection<Item> => ({
|
||||
items: [...left.items, ...right.items],
|
||||
total:
|
||||
left.total === null || right.total === null
|
||||
? null
|
||||
: left.total + right.total,
|
||||
complete: left.complete && right.complete,
|
||||
truncated: left.truncated || right.truncated,
|
||||
});
|
||||
): FunctionCollection<Item> => {
|
||||
const base = {
|
||||
items: [...left.items, ...right.items],
|
||||
total:
|
||||
left.total === null || right.total === null
|
||||
? null
|
||||
: left.total + right.total,
|
||||
};
|
||||
return left.complete && right.complete
|
||||
? { ...base, complete: true, truncated: false }
|
||||
: {
|
||||
...base,
|
||||
complete: false,
|
||||
truncated: left.truncated || right.truncated,
|
||||
};
|
||||
};
|
||||
|
||||
export const project = <Input, Output>(
|
||||
collection: FunctionCollection<Input>,
|
||||
|
|
|
|||
|
|
@ -24,12 +24,14 @@ type DossierCollectionField = (typeof DOSSIER_COLLECTION_FIELDS)[number];
|
|||
type DossierItem<Field extends DossierCollectionField> =
|
||||
FunctionDossier[Field]["items"][number];
|
||||
|
||||
export interface FunctionCollection<Item = unknown> {
|
||||
type FunctionCoverage =
|
||||
| { readonly complete: true; readonly truncated: false }
|
||||
| { readonly complete: false; readonly truncated: boolean };
|
||||
|
||||
export type FunctionCollection<Item = unknown> = {
|
||||
readonly items: readonly Item[];
|
||||
readonly total: number | null;
|
||||
readonly complete: boolean;
|
||||
readonly truncated: boolean;
|
||||
}
|
||||
} & FunctionCoverage;
|
||||
|
||||
type FunctionCollections = {
|
||||
readonly [Field in DossierCollectionField]: FunctionCollection<
|
||||
|
|
@ -46,9 +48,10 @@ export interface FunctionSnapshot {
|
|||
readonly pseudocode: {
|
||||
readonly text: string;
|
||||
readonly total: number;
|
||||
readonly complete: boolean;
|
||||
readonly truncated: boolean;
|
||||
};
|
||||
} & (
|
||||
| { readonly complete: true; readonly truncated: false }
|
||||
| { readonly complete: false; readonly truncated: true }
|
||||
);
|
||||
readonly collections: FunctionCollections;
|
||||
readonly instructionScan: FunctionDossier["instruction_scan"];
|
||||
readonly limitations: readonly string[];
|
||||
|
|
@ -172,8 +175,9 @@ const mergePseudocode = (
|
|||
return {
|
||||
text: orderedValues(characters).join(""),
|
||||
total,
|
||||
complete,
|
||||
truncated: !complete,
|
||||
...(complete
|
||||
? { complete: true as const, truncated: false as const }
|
||||
: { complete: false as const, truncated: true as const }),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -210,8 +214,12 @@ const mergeCollection = <Field extends DossierCollectionField>(
|
|||
return {
|
||||
items: orderedValues(items),
|
||||
total,
|
||||
complete,
|
||||
truncated: scanLimited || (!unavailable && !complete),
|
||||
...(complete
|
||||
? { complete: true as const, truncated: false as const }
|
||||
: {
|
||||
complete: false as const,
|
||||
truncated: scanLimited || !unavailable,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import { jsonValueSchema, type JsonValue } from "./jsonValue.js";
|
||||
import type { JsonValue } from "./jsonValue.js";
|
||||
import { AnalysisOutputError, HopperProtocolError } from "./errors.js";
|
||||
import { err, ok, type Result } from "./result.js";
|
||||
import { nativeApiBoundarySchema } from "./nativeApiBoundary.js";
|
||||
|
|
@ -62,31 +62,24 @@ const segmentSchema = z.object({
|
|||
writable: z.boolean().nullable().default(null),
|
||||
executable: z.boolean().nullable().default(null),
|
||||
});
|
||||
const addressedPageSchema = z
|
||||
.object({
|
||||
items: z.array(z.object({ address: z.string(), value: z.string() })),
|
||||
offset: z.number().int().min(0),
|
||||
limit: z.number().int().min(1),
|
||||
total: z.number().int().min(0),
|
||||
next_offset: z.number().int().min(0).nullable(),
|
||||
has_more: z.boolean(),
|
||||
})
|
||||
.superRefine((value, context) => {
|
||||
if (value.has_more && value.next_offset === null) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "a page with more results must provide next_offset",
|
||||
path: ["next_offset"],
|
||||
});
|
||||
}
|
||||
if (!value.has_more && value.next_offset !== null) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "a complete page must not provide next_offset",
|
||||
path: ["next_offset"],
|
||||
});
|
||||
}
|
||||
});
|
||||
const addressedPageFacts = {
|
||||
items: z.array(z.object({ address: z.string(), value: z.string() })),
|
||||
offset: z.number().int().min(0),
|
||||
limit: z.number().int().min(1),
|
||||
total: z.number().int().min(0),
|
||||
} as const;
|
||||
const addressedPageSchema = z.discriminatedUnion("has_more", [
|
||||
z.object({
|
||||
...addressedPageFacts,
|
||||
has_more: z.literal(true),
|
||||
next_offset: z.number().int().min(0),
|
||||
}),
|
||||
z.object({
|
||||
...addressedPageFacts,
|
||||
has_more: z.literal(false),
|
||||
next_offset: z.null(),
|
||||
}),
|
||||
]);
|
||||
const unavailableAnalysisFactSchema = z
|
||||
.object({ available: z.literal(false), reason: z.string() })
|
||||
.strict();
|
||||
|
|
@ -111,16 +104,25 @@ export const localVariableSchema = z
|
|||
provenance: z.string().min(1),
|
||||
})
|
||||
.strict();
|
||||
const boundedSchema = <T extends z.ZodType>(item: T) =>
|
||||
z
|
||||
.object({
|
||||
items: z.array(item),
|
||||
total: z.number().int().min(0).nullable(),
|
||||
returned: z.number().int().min(0),
|
||||
truncated: z.boolean(),
|
||||
next_offset: z.number().int().min(0).nullable(),
|
||||
})
|
||||
.strict()
|
||||
const boundedSchema = <T extends z.ZodType>(item: T) => {
|
||||
const boundedFacts = {
|
||||
items: z.array(item),
|
||||
total: z.number().int().min(0).nullable(),
|
||||
returned: z.number().int().min(0),
|
||||
} as const;
|
||||
return z
|
||||
.discriminatedUnion("truncated", [
|
||||
z.strictObject({
|
||||
...boundedFacts,
|
||||
truncated: z.literal(false),
|
||||
next_offset: z.null(),
|
||||
}),
|
||||
z.strictObject({
|
||||
...boundedFacts,
|
||||
truncated: z.literal(true),
|
||||
next_offset: z.number().int().min(0).nullable(),
|
||||
}),
|
||||
])
|
||||
.superRefine((value, context) => {
|
||||
if (value.returned !== value.items.length) {
|
||||
context.addIssue({
|
||||
|
|
@ -136,14 +138,8 @@ const boundedSchema = <T extends z.ZodType>(item: T) =>
|
|||
path: ["total"],
|
||||
});
|
||||
}
|
||||
if (value.next_offset !== null && !value.truncated) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "a continuation requires truncated output",
|
||||
path: ["next_offset"],
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** Provider-neutral bounded raw-instruction window for one analyzed function. */
|
||||
export const functionInstructionWindowSchema = z
|
||||
|
|
@ -201,14 +197,22 @@ export const functionDossierSchema = z
|
|||
})
|
||||
.strict(),
|
||||
pseudocode: z
|
||||
.object({
|
||||
text: z.string(),
|
||||
total_chars: z.number().int().min(0),
|
||||
returned_chars: z.number().int().min(0),
|
||||
truncated: z.boolean(),
|
||||
next_offset: z.number().int().min(0).nullable(),
|
||||
})
|
||||
.strict()
|
||||
.discriminatedUnion("truncated", [
|
||||
z.strictObject({
|
||||
text: z.string(),
|
||||
total_chars: z.number().int().min(0),
|
||||
returned_chars: z.number().int().min(0),
|
||||
truncated: z.literal(false),
|
||||
next_offset: z.null(),
|
||||
}),
|
||||
z.strictObject({
|
||||
text: z.string(),
|
||||
total_chars: z.number().int().min(0),
|
||||
returned_chars: z.number().int().min(0),
|
||||
truncated: z.literal(true),
|
||||
next_offset: z.number().int().min(0).nullable(),
|
||||
}),
|
||||
])
|
||||
.superRefine((value, context) => {
|
||||
if (value.returned_chars !== [...value.text].length) {
|
||||
context.addIssue({
|
||||
|
|
@ -224,13 +228,6 @@ export const functionDossierSchema = z
|
|||
path: ["total_chars"],
|
||||
});
|
||||
}
|
||||
if (value.next_offset !== null && !value.truncated) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "a continuation requires truncated pseudocode",
|
||||
path: ["next_offset"],
|
||||
});
|
||||
}
|
||||
}),
|
||||
assembly: boundedSchema(z.string()),
|
||||
comments: boundedSchema(
|
||||
|
|
@ -276,7 +273,7 @@ export const functionDossierSchema = z
|
|||
instruction_scan: z
|
||||
.object({ scanned: z.number().int().min(0), truncated: z.boolean() })
|
||||
.strict(),
|
||||
native_api: nativeApiBoundarySchema.optional(),
|
||||
native_api: nativeApiBoundarySchema.nullable().default(null),
|
||||
limitations: z.array(z.string()).default([]),
|
||||
})
|
||||
.strict();
|
||||
|
|
@ -290,7 +287,7 @@ export const parseFunctionDossier = (
|
|||
): Result<JsonValue, AnalysisOutputError> => {
|
||||
const parsed = functionDossierSchema.safeParse(value);
|
||||
return parsed.success
|
||||
? ok(jsonValueSchema.parse(parsed.data))
|
||||
? ok(parsed.data)
|
||||
: err(
|
||||
new AnalysisOutputError(
|
||||
"analyze_function",
|
||||
|
|
|
|||
|
|
@ -16,6 +16,10 @@ const projectIssue = (
|
|||
const path = issue.path.flatMap((part) =>
|
||||
typeof part === "string" || typeof part === "number" ? [part] : [],
|
||||
);
|
||||
if (issue.code === "invalid_union")
|
||||
return issue.errors.flatMap((branch) =>
|
||||
branch.flatMap((branchIssue) => projectIssue(branchIssue, input)),
|
||||
);
|
||||
if (issue.code === "unrecognized_keys")
|
||||
return issue.keys.map((key) => ({
|
||||
path: [...path, key],
|
||||
|
|
@ -62,7 +66,8 @@ const valueAtPath = (
|
|||
let current = input;
|
||||
for (const part of path) {
|
||||
if (typeof current !== "object" || current === null) return undefined;
|
||||
current = (current as Readonly<Record<string | number, unknown>>)[part];
|
||||
const next: unknown = Reflect.get(current, part);
|
||||
current = next;
|
||||
}
|
||||
return current;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -53,36 +53,35 @@ export type InvestigationRunOptions = z.infer<
|
|||
>;
|
||||
|
||||
/** Explicit write approval and two local versions for an automatic run. */
|
||||
export const crossVersionInvestigationInputSchema = z
|
||||
.object({
|
||||
approved: z.literal(true),
|
||||
workspace_path: z.string().min(1).max(4_096),
|
||||
workspace_name: z.string().trim().min(1).max(200).default("default"),
|
||||
expected_workspace_revision: z.number().int().min(1).optional(),
|
||||
replay_run_id: z
|
||||
.string()
|
||||
.regex(/^run_[a-f0-9]{64}$/u)
|
||||
.optional(),
|
||||
left_path: z.string().min(1).max(4_096),
|
||||
right_path: z.string().min(1).max(4_096),
|
||||
integrity_policy: z.enum(["fail", "record-and-continue"]).default("fail"),
|
||||
integrity_continue_approved: z.boolean().default(false),
|
||||
const crossVersionInvestigationFacts = {
|
||||
approved: z.literal(true),
|
||||
workspace_path: z.string().min(1).max(4_096),
|
||||
workspace_name: z.string().trim().min(1).max(200).default("default"),
|
||||
expected_workspace_revision: z.number().int().min(1).optional(),
|
||||
replay_run_id: z
|
||||
.string()
|
||||
.regex(/^run_[a-f0-9]{64}$/u)
|
||||
.optional(),
|
||||
left_path: z.string().min(1).max(4_096),
|
||||
right_path: z.string().min(1).max(4_096),
|
||||
options: investigationRunOptionsSchema.default(
|
||||
DEFAULT_INVESTIGATION_RUN_OPTIONS,
|
||||
),
|
||||
} as const;
|
||||
export const crossVersionInvestigationInputSchema = z.union([
|
||||
z.object({
|
||||
...crossVersionInvestigationFacts,
|
||||
integrity_policy: z.literal("fail").default("fail"),
|
||||
integrity_continue_approved: z.literal(false).default(false),
|
||||
max_integrity_mismatches: z.number().int().min(1).max(100).default(10),
|
||||
options: investigationRunOptionsSchema.default(
|
||||
DEFAULT_INVESTIGATION_RUN_OPTIONS,
|
||||
),
|
||||
})
|
||||
.superRefine((input, context) => {
|
||||
if (
|
||||
input.integrity_policy === "record-and-continue" &&
|
||||
input.integrity_continue_approved !== true
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["integrity_continue_approved"],
|
||||
message: "record-and-continue requires explicit approval",
|
||||
});
|
||||
});
|
||||
}),
|
||||
z.object({
|
||||
...crossVersionInvestigationFacts,
|
||||
integrity_policy: z.literal("record-and-continue"),
|
||||
integrity_continue_approved: z.literal(true),
|
||||
max_integrity_mismatches: z.number().int().min(1).max(100).default(10),
|
||||
}),
|
||||
]);
|
||||
|
||||
export type CrossVersionInvestigationInput = z.infer<
|
||||
typeof crossVersionInvestigationInputSchema
|
||||
|
|
|
|||
|
|
@ -114,46 +114,61 @@ const determinismSchema = z
|
|||
})
|
||||
.strict();
|
||||
|
||||
const controlledReplayInputShape = {
|
||||
left: replaySideSchema,
|
||||
right: replaySideSchema.optional(),
|
||||
cases: z.array(explicitCaseSchema).max(128).default([]),
|
||||
generator: generatorSchema.optional(),
|
||||
determinism: determinismSchema.default({
|
||||
clock_iso: "2000-01-01T00:00:00.000Z",
|
||||
random_seed: 0,
|
||||
locale: "en-US",
|
||||
timezone: "UTC",
|
||||
platform: "linux",
|
||||
}),
|
||||
limits: replayLimitsSchema.default({
|
||||
wall_time_ms: 3_000,
|
||||
memory_bytes: 128 * 1024 * 1024,
|
||||
tasks: 8,
|
||||
cpu_quota_percent: 50,
|
||||
tmpfs_bytes: 16 * 1024 * 1024,
|
||||
module_bytes: 4 * 1024 * 1024,
|
||||
input_bytes: 256 * 1024,
|
||||
protocol_bytes: 16 * 1024 * 1024,
|
||||
output_bytes: 512 * 1024,
|
||||
stderr_bytes: 32 * 1024,
|
||||
result_depth: 16,
|
||||
result_nodes: 10_000,
|
||||
}),
|
||||
reproducer_export: z
|
||||
.strictObject({
|
||||
path: absolutePathSchema,
|
||||
approved: z.literal(true),
|
||||
include_sources: z.boolean().default(false),
|
||||
})
|
||||
.optional(),
|
||||
} as const;
|
||||
|
||||
/** Parse a replay request that can only construct a plan. */
|
||||
export const controlledReplayPlanInputSchema = z.strictObject({
|
||||
...controlledReplayInputShape,
|
||||
mode: z.literal("plan"),
|
||||
});
|
||||
|
||||
/** Parse a replay request carrying literal approval for one exact plan. */
|
||||
export const controlledReplayExecutionInputSchema = z.strictObject({
|
||||
...controlledReplayInputShape,
|
||||
mode: z.literal("execute"),
|
||||
approved: z.literal(true),
|
||||
plan_digest: digestSchema,
|
||||
});
|
||||
|
||||
/** Parse a controlled replay request into its legal plan or execution state. */
|
||||
export const controlledReplayInputSchema = z
|
||||
.object({
|
||||
mode: z.enum(["plan", "execute"]),
|
||||
left: replaySideSchema,
|
||||
right: replaySideSchema.optional(),
|
||||
cases: z.array(explicitCaseSchema).max(128).default([]),
|
||||
generator: generatorSchema.optional(),
|
||||
determinism: determinismSchema.default({
|
||||
clock_iso: "2000-01-01T00:00:00.000Z",
|
||||
random_seed: 0,
|
||||
locale: "en-US",
|
||||
timezone: "UTC",
|
||||
platform: "linux",
|
||||
}),
|
||||
limits: replayLimitsSchema.default({
|
||||
wall_time_ms: 3_000,
|
||||
memory_bytes: 128 * 1024 * 1024,
|
||||
tasks: 8,
|
||||
cpu_quota_percent: 50,
|
||||
tmpfs_bytes: 16 * 1024 * 1024,
|
||||
module_bytes: 4 * 1024 * 1024,
|
||||
input_bytes: 256 * 1024,
|
||||
protocol_bytes: 16 * 1024 * 1024,
|
||||
output_bytes: 512 * 1024,
|
||||
stderr_bytes: 32 * 1024,
|
||||
result_depth: 16,
|
||||
result_nodes: 10_000,
|
||||
}),
|
||||
approved: z.boolean().default(false),
|
||||
plan_digest: digestSchema.optional(),
|
||||
reproducer_export: z
|
||||
.object({
|
||||
path: absolutePathSchema,
|
||||
approved: z.boolean(),
|
||||
include_sources: z.boolean().default(false),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
})
|
||||
.strict()
|
||||
.discriminatedUnion("mode", [
|
||||
controlledReplayPlanInputSchema,
|
||||
controlledReplayExecutionInputSchema,
|
||||
])
|
||||
.superRefine((value, context) => {
|
||||
if (value.cases.length === 0 && value.generator === undefined)
|
||||
context.addIssue({
|
||||
|
|
@ -167,15 +182,6 @@ export const controlledReplayInputSchema = z
|
|||
path: ["generator", "count"],
|
||||
message: "Explicit and generated replay cases must total at most 128",
|
||||
});
|
||||
if (
|
||||
value.mode === "execute" &&
|
||||
(value.approved !== true || value.plan_digest === undefined)
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["approved"],
|
||||
message: "Execution requires literal approval and a plan digest",
|
||||
});
|
||||
});
|
||||
|
||||
const moduleCommitmentSchema = z
|
||||
|
|
@ -216,7 +222,7 @@ const replayPlanSchema = z
|
|||
filesystem: z
|
||||
.object({
|
||||
host_writes: z.literal(false),
|
||||
private_tmpfs_bytes: z.number().int(),
|
||||
private_tmpfs_bytes: z.number().int().nonnegative(),
|
||||
})
|
||||
.strict(),
|
||||
runtime: z
|
||||
|
|
@ -273,34 +279,45 @@ const replayPlanSchema = z
|
|||
})
|
||||
.strict();
|
||||
|
||||
const replayOutcomeSchema = z
|
||||
const replayOutcomeFacts = {
|
||||
case_id: z.string(),
|
||||
input_sha256: digestSchema,
|
||||
} as const;
|
||||
const replayExceptionSchema = z
|
||||
.object({
|
||||
case_id: z.string(),
|
||||
outcome: z.enum([
|
||||
"return",
|
||||
"exception",
|
||||
"serialization_error",
|
||||
"denied",
|
||||
"timeout",
|
||||
"oom",
|
||||
"crash",
|
||||
"cancelled",
|
||||
"protocol_error",
|
||||
]),
|
||||
value: z.json().optional(),
|
||||
exception: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
message: z.string(),
|
||||
stack: z.string().nullable(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
input_sha256: digestSchema,
|
||||
output_sha256: digestSchema.nullable(),
|
||||
truncated: z.boolean(),
|
||||
name: z.string(),
|
||||
message: z.string(),
|
||||
stack: z.string().nullable(),
|
||||
})
|
||||
.strict();
|
||||
const replayOutcomeSchema = z.discriminatedUnion("outcome", [
|
||||
z.strictObject({
|
||||
...replayOutcomeFacts,
|
||||
outcome: z.literal("return"),
|
||||
value: z.json(),
|
||||
output_sha256: digestSchema,
|
||||
truncated: z.literal(false),
|
||||
}),
|
||||
z.strictObject({
|
||||
...replayOutcomeFacts,
|
||||
outcome: z.enum(["exception", "serialization_error", "denied"]),
|
||||
exception: z.object(replayExceptionSchema.shape).strict(),
|
||||
output_sha256: digestSchema,
|
||||
truncated: z.literal(false),
|
||||
}),
|
||||
z.strictObject({
|
||||
...replayOutcomeFacts,
|
||||
outcome: z.enum(["timeout", "oom", "crash", "cancelled"]),
|
||||
output_sha256: z.null(),
|
||||
truncated: z.literal(false),
|
||||
}),
|
||||
z.strictObject({
|
||||
...replayOutcomeFacts,
|
||||
outcome: z.literal("protocol_error"),
|
||||
output_sha256: z.null(),
|
||||
truncated: z.literal(true),
|
||||
}),
|
||||
]);
|
||||
|
||||
export const replayExecutionResultSchema = z
|
||||
.object({
|
||||
|
|
@ -360,31 +377,37 @@ export const replayEvidenceSchema = evidenceEnvelopeSchema
|
|||
.omit({ normalized_result: true })
|
||||
.extend({ normalized_result: replayExecutionResultSchema });
|
||||
|
||||
export const controlledReplayOutputSchema = z
|
||||
.object({
|
||||
phase: z.enum(["plan", "execute"]),
|
||||
plan: replayPlanSchema.nullable(),
|
||||
source_evidence: z.array(replayEvidenceSchema),
|
||||
evidence: replayEvidenceSchema.nullable(),
|
||||
})
|
||||
.strict()
|
||||
.superRefine((value, context) => {
|
||||
if (
|
||||
(value.phase === "plan" &&
|
||||
(value.plan === null ||
|
||||
value.evidence !== null ||
|
||||
value.source_evidence.length !== 0)) ||
|
||||
(value.phase === "execute" &&
|
||||
(value.plan !== null ||
|
||||
value.evidence === null ||
|
||||
value.source_evidence.length === 0))
|
||||
)
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: "Replay output phase does not match its payload",
|
||||
});
|
||||
});
|
||||
/** A replay plan result with no claimed runtime observation. */
|
||||
export const controlledReplayPlanOutputSchema = z.strictObject({
|
||||
phase: z.literal("plan"),
|
||||
plan: replayPlanSchema,
|
||||
source_evidence: z
|
||||
.array(replayEvidenceSchema)
|
||||
.length(0)
|
||||
.brand<"NoReplayEvidence">(),
|
||||
evidence: z.null(),
|
||||
});
|
||||
|
||||
/** An executed replay result with its required source and aggregate Evidence. */
|
||||
export const controlledReplayExecutionOutputSchema = z.strictObject({
|
||||
phase: z.literal("execute"),
|
||||
plan: z.null(),
|
||||
source_evidence: z.array(replayEvidenceSchema).min(1),
|
||||
evidence: replayEvidenceSchema,
|
||||
});
|
||||
|
||||
/** Parse replay output into its legal plan or execution state. */
|
||||
export const controlledReplayOutputSchema = z.discriminatedUnion("phase", [
|
||||
controlledReplayPlanOutputSchema,
|
||||
controlledReplayExecutionOutputSchema,
|
||||
]);
|
||||
|
||||
export type ControlledReplayInput = z.infer<typeof controlledReplayInputSchema>;
|
||||
export type ControlledReplayExecutionInput = z.infer<
|
||||
typeof controlledReplayExecutionInputSchema
|
||||
>;
|
||||
export type ControlledReplayExecutionOutput = z.infer<
|
||||
typeof controlledReplayExecutionOutputSchema
|
||||
>;
|
||||
export type ReplayPlan = z.infer<typeof replayPlanSchema>;
|
||||
export type ReplayExecutionResult = z.infer<typeof replayExecutionResultSchema>;
|
||||
|
|
|
|||
|
|
@ -24,10 +24,8 @@ interface GraphCoverageFacts {
|
|||
readonly projectionOmissions: number;
|
||||
readonly knownOmissions: number;
|
||||
readonly omissionsKnown: boolean;
|
||||
readonly inputTruncated: boolean;
|
||||
readonly inputStatus: "complete" | "partial" | "truncated";
|
||||
readonly outputTruncated: boolean;
|
||||
readonly truncated: boolean;
|
||||
readonly complete: boolean;
|
||||
}
|
||||
|
||||
/** Combined graph plus omissions introduced while merging its observations. */
|
||||
|
|
@ -66,7 +64,7 @@ export const buildReconciledApplicationGraph = (
|
|||
"Runtime presence is bounded to passive capture windows and does not prove feature execution.",
|
||||
"Cross-layer observed_as edges are inferences; digest equality proves byte identity only.",
|
||||
"Source-map relationships retain their original static authority and are not promoted into runtime matches.",
|
||||
...(facts.inputTruncated
|
||||
...(facts.inputStatus === "truncated"
|
||||
? [
|
||||
"One or more source graphs or passive captures were incomplete or truncated.",
|
||||
]
|
||||
|
|
@ -154,6 +152,12 @@ const graphCoverageFacts = (
|
|||
const inputTruncated =
|
||||
input.layers.some(({ graph }) => graph.coverage.truncated) ||
|
||||
captureTruncated;
|
||||
const inputComplete =
|
||||
input.layers.every(({ graph }) => graph.coverage.status === "complete") &&
|
||||
input.captures.every(
|
||||
({ inspection }) =>
|
||||
inspection.completeness.status === "complete_within_window",
|
||||
);
|
||||
const outputTruncated = projectionOmissions > 0;
|
||||
const sourceOmissions = input.layers.reduce(
|
||||
(total, { graph }) => total + (graph.coverage.omitted_count ?? 0),
|
||||
|
|
@ -167,17 +171,12 @@ const graphCoverageFacts = (
|
|||
projectionOmissions,
|
||||
knownOmissions: projectionOmissions + sourceOmissions,
|
||||
omissionsKnown,
|
||||
inputTruncated,
|
||||
inputStatus: inputTruncated
|
||||
? "truncated"
|
||||
: inputComplete
|
||||
? "complete"
|
||||
: "partial",
|
||||
outputTruncated,
|
||||
truncated: inputTruncated || outputTruncated,
|
||||
complete:
|
||||
!inputTruncated &&
|
||||
!outputTruncated &&
|
||||
input.layers.every(({ graph }) => graph.coverage.status === "complete") &&
|
||||
input.captures.every(
|
||||
({ inspection }) =>
|
||||
inspection.completeness.status === "complete_within_window",
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -186,15 +185,15 @@ const graphCoverage = (
|
|||
facts: GraphCoverageFacts,
|
||||
nodeCount: number,
|
||||
): JavaScriptApplicationGraph["coverage"] =>
|
||||
facts.complete
|
||||
facts.inputStatus === "complete" && !facts.outputTruncated
|
||||
? { status: "complete", truncated: false, omitted_count: 0, limits: [] }
|
||||
: facts.truncated
|
||||
: facts.inputStatus === "truncated" || facts.outputTruncated
|
||||
? {
|
||||
status: "partial",
|
||||
truncated: true,
|
||||
omitted_count: facts.omissionsKnown ? facts.knownOmissions : null,
|
||||
limits: [
|
||||
...(facts.inputTruncated
|
||||
...(facts.inputStatus === "truncated"
|
||||
? [
|
||||
{
|
||||
name: "runtime-reconciliation-input-coverage",
|
||||
|
|
|
|||
|
|
@ -81,14 +81,16 @@ const reconcileEntity = (
|
|||
const combined = content.filter((candidate) =>
|
||||
locatedKeys.has(candidateKey(candidate)),
|
||||
);
|
||||
if (combined.length === 1)
|
||||
return matched(entity, combined[0]!, {
|
||||
const combinedCandidate = combined.at(0);
|
||||
if (combined.length === 1 && combinedCandidate !== undefined)
|
||||
return matched(entity, combinedCandidate, {
|
||||
basis: "content-and-location",
|
||||
confidence: "high",
|
||||
reason: "content-and-location-match",
|
||||
});
|
||||
if (content.length === 1) {
|
||||
const candidate = content[0]!;
|
||||
const contentCandidate = content.at(0);
|
||||
if (content.length === 1 && contentCandidate !== undefined) {
|
||||
const candidate = contentCandidate;
|
||||
const digestBasis = candidate.digests.find(
|
||||
({ sha256 }) => sha256 === entity.sourceSha256,
|
||||
)?.basis;
|
||||
|
|
@ -123,8 +125,9 @@ const reconcileEntity = (
|
|||
});
|
||||
}
|
||||
|
||||
if (locations.length === 1) {
|
||||
const located = locations[0]!;
|
||||
const location = locations.at(0);
|
||||
if (locations.length === 1 && location !== undefined) {
|
||||
const located = location;
|
||||
return matched(entity, located.candidate, {
|
||||
basis: located.mapping.basis,
|
||||
confidence: "medium",
|
||||
|
|
@ -314,52 +317,57 @@ const reconciliationEvidence = (
|
|||
layer: ParsedStaticLayer,
|
||||
basis: JavaScriptRuntimeReconciliationItem["basis"],
|
||||
confidence: JavaScriptRuntimeReconciliationItem["confidence"],
|
||||
): ApplicationGraphEvidence => ({
|
||||
authority: "cross-layer-reconciliation",
|
||||
state: "inferred",
|
||||
confidence: confidence === "medium" ? "medium" : "high",
|
||||
artifact:
|
||||
entity.sourceSha256 === null
|
||||
? {
|
||||
available: false,
|
||||
reason: "not-observed",
|
||||
detail: "Runtime source bytes were not captured for this mapping.",
|
||||
}
|
||||
: {
|
||||
available: true,
|
||||
artifact_id: `art_${entity.sourceSha256}`,
|
||||
sha256: entity.sourceSha256,
|
||||
},
|
||||
location: entity.node.observations[0]!.evidence.location,
|
||||
extractor: {
|
||||
name: "rea-javascript-runtime-reconciliation",
|
||||
version: "1",
|
||||
operation: "reconcile_javascript_runtime",
|
||||
executable_sha256: null,
|
||||
},
|
||||
coverage:
|
||||
layer.graph.coverage.status === "complete" &&
|
||||
entity.capture.scriptsCompleteWithinScope
|
||||
? { status: "complete", truncated: false, omitted_count: 0, limits: [] }
|
||||
: {
|
||||
status: "partial",
|
||||
truncated: false,
|
||||
omitted_count: null,
|
||||
limits: [],
|
||||
},
|
||||
limitations: [
|
||||
basis === "content-sha256" ||
|
||||
basis === "module-source-sha256" ||
|
||||
basis === "content-and-location"
|
||||
? "Equal content digests establish byte identity, not execution causality."
|
||||
: "Location correspondence is inferred from an artifact root or caller-declared mapping.",
|
||||
"Passive script parsing or target presence does not prove feature execution.",
|
||||
],
|
||||
evidence_ids: [
|
||||
layer.evidence.evidence_id,
|
||||
entity.capture.evidence.evidence_id,
|
||||
].sort(),
|
||||
});
|
||||
): ApplicationGraphEvidence => {
|
||||
const observation = entity.node.observations.at(0);
|
||||
if (observation === undefined)
|
||||
throw new TypeError("Runtime reconciliation entity has no observation");
|
||||
return {
|
||||
authority: "cross-layer-reconciliation",
|
||||
state: "inferred",
|
||||
confidence: confidence === "medium" ? "medium" : "high",
|
||||
artifact:
|
||||
entity.sourceSha256 === null
|
||||
? {
|
||||
available: false,
|
||||
reason: "not-observed",
|
||||
detail: "Runtime source bytes were not captured for this mapping.",
|
||||
}
|
||||
: {
|
||||
available: true,
|
||||
artifact_id: `art_${entity.sourceSha256}`,
|
||||
sha256: entity.sourceSha256,
|
||||
},
|
||||
location: observation.evidence.location,
|
||||
extractor: {
|
||||
name: "rea-javascript-runtime-reconciliation",
|
||||
version: "1",
|
||||
operation: "reconcile_javascript_runtime",
|
||||
executable_sha256: null,
|
||||
},
|
||||
coverage:
|
||||
layer.graph.coverage.status === "complete" &&
|
||||
entity.capture.scriptsCompleteWithinScope
|
||||
? { status: "complete", truncated: false, omitted_count: 0, limits: [] }
|
||||
: {
|
||||
status: "partial",
|
||||
truncated: false,
|
||||
omitted_count: null,
|
||||
limits: [],
|
||||
},
|
||||
limitations: [
|
||||
basis === "content-sha256" ||
|
||||
basis === "module-source-sha256" ||
|
||||
basis === "content-and-location"
|
||||
? "Equal content digests establish byte identity, not execution causality."
|
||||
: "Location correspondence is inferred from an artifact root or caller-declared mapping.",
|
||||
"Passive script parsing or target presence does not prove feature execution.",
|
||||
],
|
||||
evidence_ids: [
|
||||
layer.evidence.evidence_id,
|
||||
entity.capture.evidence.evidence_id,
|
||||
].sort(),
|
||||
};
|
||||
};
|
||||
|
||||
const strongestLocationBasis = (
|
||||
values: readonly LocatedCandidate[],
|
||||
|
|
|
|||
|
|
@ -27,8 +27,7 @@ interface ReconciliationProjection {
|
|||
|
||||
interface CompletionFlags {
|
||||
readonly outputTruncated: boolean;
|
||||
readonly inputTruncated: boolean;
|
||||
readonly inputPartial: boolean;
|
||||
readonly inputStatus: "complete" | "partial" | "truncated";
|
||||
}
|
||||
|
||||
/** Build and verify the deterministic caller-visible reconciliation result. */
|
||||
|
|
@ -150,36 +149,44 @@ const reconciliationCoverage = (
|
|||
completion: CompletionFlags,
|
||||
): JavaScriptRuntimeReconciliationResult["coverage"] => ({
|
||||
status:
|
||||
completion.outputTruncated || completion.inputTruncated
|
||||
completion.outputTruncated || completion.inputStatus === "truncated"
|
||||
? "truncated"
|
||||
: completion.inputPartial
|
||||
: completion.inputStatus === "partial"
|
||||
? "partial"
|
||||
: "complete-within-inputs",
|
||||
truncated: completion.outputTruncated || completion.inputTruncated,
|
||||
truncated:
|
||||
completion.outputTruncated || completion.inputStatus === "truncated",
|
||||
omitted_runtime_entities: input.runtime.omittedEntities,
|
||||
omitted_reconciliation_items: input.matching.omittedItems,
|
||||
omitted_static_load_states: input.loadStates.omittedStates,
|
||||
omitted_graph_items: input.omittedGraphItems,
|
||||
});
|
||||
|
||||
const completionFlags = (input: ReconciliationProjection): CompletionFlags => ({
|
||||
outputTruncated:
|
||||
input.runtime.omittedEntities > 0 ||
|
||||
input.matching.omittedItems > 0 ||
|
||||
input.loadStates.omittedStates > 0 ||
|
||||
input.omittedGraphItems > 0,
|
||||
inputTruncated:
|
||||
const completionFlags = (input: ReconciliationProjection): CompletionFlags => {
|
||||
const inputTruncated =
|
||||
input.layers.some(({ graph }) => graph.coverage.truncated) ||
|
||||
input.captures.some(({ inspection }) =>
|
||||
inspection.completeness.conditions.includes("truncated"),
|
||||
),
|
||||
inputPartial:
|
||||
);
|
||||
const inputPartial =
|
||||
input.layers.some(({ graph }) => graph.coverage.status !== "complete") ||
|
||||
input.captures.some(
|
||||
({ inspection }) =>
|
||||
inspection.completeness.status !== "complete_within_window",
|
||||
),
|
||||
});
|
||||
);
|
||||
return {
|
||||
outputTruncated:
|
||||
input.runtime.omittedEntities > 0 ||
|
||||
input.matching.omittedItems > 0 ||
|
||||
input.loadStates.omittedStates > 0 ||
|
||||
input.omittedGraphItems > 0,
|
||||
inputStatus: inputTruncated
|
||||
? "truncated"
|
||||
: inputPartial
|
||||
? "partial"
|
||||
: "complete",
|
||||
};
|
||||
};
|
||||
|
||||
const reconciliationLimitations = (completion: CompletionFlags): string[] =>
|
||||
uniqueSorted([
|
||||
|
|
@ -189,7 +196,7 @@ const reconciliationLimitations = (completion: CompletionFlags): string[] =>
|
|||
"Not-observed means absent from the bounded captures in scope, never globally unloaded.",
|
||||
"Caller-declared file and URL mappings are inference inputs and do not expand CDP origin or filesystem-root authority.",
|
||||
"Source-map authority remains separate from passive runtime authority.",
|
||||
...(completion.inputPartial
|
||||
...(completion.inputStatus !== "complete"
|
||||
? ["One or more static graphs or passive captures were incomplete."]
|
||||
: []),
|
||||
...(completion.outputTruncated
|
||||
|
|
|
|||
|
|
@ -9,13 +9,19 @@ export interface KnownCollectionPage<Item> {
|
|||
}
|
||||
|
||||
/** Fail-closed completeness derived from a known-total source page. */
|
||||
export interface KnownPageCoverageAssessment {
|
||||
readonly complete: boolean;
|
||||
readonly sourceComplete: boolean;
|
||||
export type KnownPageCoverageAssessment = {
|
||||
readonly includedCount: number;
|
||||
readonly omittedCount: number;
|
||||
readonly sourceOmittedCount: number;
|
||||
}
|
||||
} & (
|
||||
| {
|
||||
readonly complete: true;
|
||||
readonly sourceComplete: true;
|
||||
readonly omittedCount: 0;
|
||||
readonly sourceOmittedCount: 0;
|
||||
}
|
||||
| { readonly complete: false; readonly sourceComplete: boolean }
|
||||
);
|
||||
|
||||
/**
|
||||
* Assess source-page and downstream-projection completeness without treating an
|
||||
|
|
@ -37,11 +43,19 @@ export const assessKnownPageCoverage = <Item>(
|
|||
page.returned === page.total &&
|
||||
sourceOmittedCount === 0;
|
||||
const omittedCount = Math.max(0, page.total - retainedCount);
|
||||
return {
|
||||
complete: sourceComplete && omittedCount === 0,
|
||||
sourceComplete,
|
||||
includedCount: retainedCount,
|
||||
omittedCount,
|
||||
sourceOmittedCount,
|
||||
};
|
||||
return sourceComplete && omittedCount === 0
|
||||
? {
|
||||
complete: true,
|
||||
sourceComplete: true,
|
||||
includedCount: retainedCount,
|
||||
omittedCount: 0,
|
||||
sourceOmittedCount: 0,
|
||||
}
|
||||
: {
|
||||
complete: false,
|
||||
sourceComplete,
|
||||
includedCount: retainedCount,
|
||||
omittedCount,
|
||||
sourceOmittedCount,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,25 +16,35 @@ const inferenceEvidenceSchema = z
|
|||
})
|
||||
.strict();
|
||||
|
||||
const inferredBoundaryTypeSchema = z
|
||||
.object({
|
||||
role: z.enum(["return", "parameter"]),
|
||||
ordinal: z.number().int().min(0).nullable(),
|
||||
name: z.string().min(1).nullable(),
|
||||
data_type: z.string().min(1),
|
||||
size_bytes: z.number().int().min(0).nullable(),
|
||||
storage: z.string().min(1).nullable(),
|
||||
confidence: inferenceConfidenceSchema,
|
||||
evidence: z.array(inferenceEvidenceSchema).min(1),
|
||||
decompiler_artifacts: z.array(
|
||||
z.enum([
|
||||
"recovered-signature",
|
||||
"register-or-stack-storage",
|
||||
"compiler-generated-variable",
|
||||
]),
|
||||
),
|
||||
})
|
||||
.strict();
|
||||
const inferredBoundaryTypeFacts = {
|
||||
ordinal: z.number().int().min(0).nullable(),
|
||||
name: z.string().min(1).nullable(),
|
||||
data_type: z.string().min(1),
|
||||
size_bytes: z.number().int().min(0).nullable(),
|
||||
storage: z.string().min(1).nullable(),
|
||||
confidence: inferenceConfidenceSchema,
|
||||
evidence: z.array(inferenceEvidenceSchema).min(1),
|
||||
decompiler_artifacts: z.array(
|
||||
z.enum([
|
||||
"recovered-signature",
|
||||
"register-or-stack-storage",
|
||||
"compiler-generated-variable",
|
||||
]),
|
||||
),
|
||||
} as const;
|
||||
|
||||
const inferredReturnTypeSchema = z.strictObject({
|
||||
...inferredBoundaryTypeFacts,
|
||||
role: z.literal("return"),
|
||||
ordinal: z.null(),
|
||||
name: z.null(),
|
||||
});
|
||||
|
||||
const inferredParameterTypeSchema = z.strictObject({
|
||||
...inferredBoundaryTypeFacts,
|
||||
role: z.literal("parameter"),
|
||||
ordinal: z.number().int().min(0),
|
||||
});
|
||||
|
||||
const jumpTableDataSourceSchema = z
|
||||
.object({
|
||||
|
|
@ -77,8 +87,8 @@ const availableNativeApiBoundarySchema = z
|
|||
provenance: z.string().min(1),
|
||||
signature_source: z.string().min(1),
|
||||
calling_convention: z.string().min(1),
|
||||
return_type: inferredBoundaryTypeSchema,
|
||||
parameters: z.array(inferredBoundaryTypeSchema),
|
||||
return_type: inferredReturnTypeSchema,
|
||||
parameters: z.array(inferredParameterTypeSchema),
|
||||
parameters_truncated: z.boolean(),
|
||||
jump_tables: z.array(inferredJumpTableSchema),
|
||||
jump_tables_truncated: z.boolean(),
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
controlledReplayInputSchema,
|
||||
controlledReplayOutputSchema,
|
||||
controlledReplayExecutionOutputSchema,
|
||||
controlledReplayPlanInputSchema,
|
||||
controlledReplayPlanOutputSchema,
|
||||
} from "./javascriptReplay.js";
|
||||
import {
|
||||
javascriptExportInstrumentationInputSchema,
|
||||
|
|
@ -12,22 +13,24 @@ import { runtimeCharacterizationPlanSchema } from "./runtimeCharacterization.js"
|
|||
import { evidenceRecordSchema } from "./evidence.js";
|
||||
|
||||
const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u);
|
||||
const nodeCharacterizationExpectedEffectSchema = z.enum([
|
||||
"pure",
|
||||
"observation-only",
|
||||
]);
|
||||
const nodeRuntimeCharacterizationPlanSchema =
|
||||
runtimeCharacterizationPlanSchema.extend({
|
||||
expected_effect: nodeCharacterizationExpectedEffectSchema,
|
||||
});
|
||||
|
||||
export const nodeCharacterizationPreparationInputSchema = z
|
||||
.strictObject({
|
||||
preparation_approved: z.literal(true),
|
||||
selected_alias: z.string().min(1).max(200),
|
||||
expected_effect: z.enum(["pure", "observation-only"]),
|
||||
expected_effect: nodeCharacterizationExpectedEffectSchema,
|
||||
instrumentation: javascriptExportInstrumentationInputSchema,
|
||||
replay: controlledReplayInputSchema,
|
||||
replay: controlledReplayPlanInputSchema,
|
||||
})
|
||||
.superRefine((input, context) => {
|
||||
if (input.replay.mode !== "plan")
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
path: ["replay", "mode"],
|
||||
message: "Characterization preparation requires replay plan mode",
|
||||
});
|
||||
const selected = input.replay.left.modules.filter(
|
||||
({ alias }) => alias === input.selected_alias,
|
||||
);
|
||||
|
|
@ -68,20 +71,20 @@ export const nodeCharacterizationExecutionInputSchema = z.strictObject({
|
|||
export const nodeCharacterizationPreparationOutputSchema = z.strictObject({
|
||||
schema_version: z.literal(1),
|
||||
phase: z.literal("preparation"),
|
||||
plan: runtimeCharacterizationPlanSchema,
|
||||
plan: nodeRuntimeCharacterizationPlanSchema,
|
||||
transformation: javascriptExportTransformationManifestSchema,
|
||||
transformation_evidence: evidenceRecordSchema,
|
||||
replay: controlledReplayOutputSchema,
|
||||
replay: controlledReplayPlanOutputSchema,
|
||||
});
|
||||
|
||||
export const nodeCharacterizationExecutionOutputSchema = z.strictObject({
|
||||
schema_version: z.literal(1),
|
||||
phase: z.literal("execution"),
|
||||
plan: runtimeCharacterizationPlanSchema,
|
||||
plan: nodeRuntimeCharacterizationPlanSchema,
|
||||
transformation: javascriptExportTransformationManifestSchema,
|
||||
transformation_evidence: evidenceRecordSchema,
|
||||
evidence: evidenceRecordSchema,
|
||||
replay: controlledReplayOutputSchema,
|
||||
replay: controlledReplayExecutionOutputSchema,
|
||||
});
|
||||
|
||||
export type NodeCharacterizationPreparationInput = z.infer<
|
||||
|
|
|
|||
|
|
@ -206,14 +206,16 @@ const replayTransitions = (
|
|||
(run.controls[controlIndex]?.after_capture_order ??
|
||||
Number.MAX_SAFE_INTEGER) < observation.capture_order
|
||||
) {
|
||||
const control = run.controls[controlIndex]!;
|
||||
const control = run.controls[controlIndex];
|
||||
if (control === undefined) break;
|
||||
advanceControl(control, controlIndex);
|
||||
controlIndex += 1;
|
||||
}
|
||||
advance({ kind: "observation", observation });
|
||||
}
|
||||
for (; controlIndex < run.controls.length; controlIndex += 1) {
|
||||
const control = run.controls[controlIndex]!;
|
||||
const control = run.controls[controlIndex];
|
||||
if (control === undefined) break;
|
||||
advanceControl(control, controlIndex);
|
||||
}
|
||||
require(transitionIndex ===
|
||||
|
|
@ -237,10 +239,11 @@ const validateControls = (
|
|||
require(admittedOrders.has(
|
||||
control.after_capture_order,
|
||||
), `reactive_run.controls[${String(index)}].after_capture_order`, "reactive control cutoff must identify the last admitted observation");
|
||||
const previous = run.controls.at(index - 1);
|
||||
require(index === 0 ||
|
||||
control.after_capture_order >=
|
||||
run.controls[index - 1]!
|
||||
.after_capture_order, `reactive_run.controls[${String(index)}].after_capture_order`, "reactive control cutoffs must be ordered");
|
||||
(previous !== undefined &&
|
||||
control.after_capture_order >=
|
||||
previous.after_capture_order), `reactive_run.controls[${String(index)}].after_capture_order`, "reactive control cutoffs must be ordered");
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -18,9 +18,16 @@ type RequireInvariant = (
|
|||
const orderedTimestamps = (
|
||||
values: readonly { readonly at_ms: number }[],
|
||||
): boolean =>
|
||||
values.every(
|
||||
(value, index) => index === 0 || value.at_ms >= values[index - 1]!.at_ms,
|
||||
);
|
||||
values.every((value, index) => {
|
||||
const previous = values[index - 1];
|
||||
return previous === undefined || value.at_ms >= previous.at_ms;
|
||||
});
|
||||
|
||||
const transitionsAreContiguous = (
|
||||
previous: ProcessCapture["replay_transitions"][number] | undefined,
|
||||
current: ProcessCapture["replay_transitions"][number],
|
||||
): boolean =>
|
||||
previous !== undefined && previous.state_after === current.state_before;
|
||||
|
||||
const validateCommitments = (
|
||||
capture: ProcessCapture,
|
||||
|
|
@ -156,7 +163,7 @@ const shimPlans = (capture: ProcessCapture) =>
|
|||
typeof shim.name === "string" &&
|
||||
Array.isArray(shim.routes)
|
||||
? shim.routes.map((route, routeIndex) => ({
|
||||
command: shim.name as string,
|
||||
command: shim.name,
|
||||
route,
|
||||
routeIndex,
|
||||
}))
|
||||
|
|
@ -295,9 +302,12 @@ const validateReplayEvents = (
|
|||
if (index === 0)
|
||||
require(transition.state_before ===
|
||||
machine?.initial_state, "replay_transitions", "first replay transition does not start at the declared initial state");
|
||||
const previous = capture.replay_transitions[index - 1];
|
||||
if (index > 0)
|
||||
require(capture.replay_transitions[index - 1]!.state_after ===
|
||||
transition.state_before, "replay_transitions", "replay transition states are not contiguous");
|
||||
require(transitionsAreContiguous(
|
||||
previous,
|
||||
transition,
|
||||
), "replay_transitions", "replay transition states are not contiguous");
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue