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:
morluto 2026-08-10 02:28:11 +08:00 committed by GitHub
parent 6901edc05d
commit b8a9da0c0f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
271 changed files with 5970 additions and 5699 deletions

View File

@ -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"
}
}

View File

@ -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": [

View File

@ -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",

View File

@ -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 }),
]);

View File

@ -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),

View File

@ -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;
};

View File

@ -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,
},
{

View File

@ -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 (

View File

@ -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,

View File

@ -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,

View File

@ -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();
}

View File

@ -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

View File

@ -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;

View File

@ -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;
};

View File

@ -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;

View File

@ -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()

View File

@ -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",

View File

@ -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. */

View File

@ -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();
}

View File

@ -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

View File

@ -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,

View File

@ -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],
),
};
};

View File

@ -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 => {

View File

@ -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,

View File

@ -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>> => {

View File

@ -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(

View File

@ -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",

View File

@ -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}`,
});

View File

@ -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]);
};

View File

@ -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);
};

View File

@ -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(

View File

@ -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,

View File

@ -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(

View File

@ -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, {

View File

@ -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",

View File

@ -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,
),
},
);
}

View File

@ -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",

View File

@ -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,

View File

@ -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 = (

View File

@ -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(

View File

@ -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) => ({

View File

@ -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,

View File

@ -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,

View File

@ -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,
};
};

View File

@ -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,
};
};

View File

@ -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,
};
};

View File

@ -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,
};
};

View File

@ -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)

View File

@ -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 = {

View File

@ -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;
}
};

View File

@ -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;
};

View File

@ -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,
];

View File

@ -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",
}),
);
};

View File

@ -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,
),
);
};

View File

@ -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,

View File

@ -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",
}),
);
};

View File

@ -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]);
};

View File

@ -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;

View File

@ -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;
};
}

View File

@ -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

View File

@ -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. */

View File

@ -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: [
{

View File

@ -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",
]);

View File

@ -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. */

View File

@ -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 = [

View File

@ -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, {

View File

@ -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 = [

View File

@ -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. */

View File

@ -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[];

View File

@ -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 = [

View File

@ -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,

View File

@ -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(),

View File

@ -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 };
};

View File

@ -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);

View File

@ -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,

View File

@ -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]

View File

@ -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]

View File

@ -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("&quot;", '"')
.replaceAll("&apos;", "'");
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,

View File

@ -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";
});

View File

@ -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

View File

@ -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

View File

@ -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 ||

View File

@ -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;
}

View File

@ -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

View File

@ -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 {

View File

@ -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);

View File

@ -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>,

View File

@ -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,
}),
};
};

View File

@ -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",

View File

@ -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;
};

View File

@ -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

View File

@ -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>;

View File

@ -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",

View File

@ -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[],

View File

@ -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

View File

@ -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,
};
};

View File

@ -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(),

View File

@ -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<

View File

@ -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");
}
};

View File

@ -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