fix(javascript): isolate source analysis and retain bounded partial evidence (#1651)

* refactor(javascript): validate transferable file projections

* test(javascript): consume decoded module and static facts

* fix(javascript): isolate source analysis and retain bounded partial evidence

* test(javascript): verify cleanup retention and repeated source paths
This commit is contained in:
Ysopwnable authored and GitHub committed 2026-10-11 07:22:50 +08:00
1 parent 92cc0edd8e
commit 4999e4be2c
41 files changed
+3748 -176

No files matched your search

@@ -0,0 +1,56 @@
import { z } from "zod";
import type { JsonValue } from "../jsonValue.js";
import type { ApplicationCoverage } from "./javascriptApplicationEvidenceSchemas.js";
/** Shared operational defaults for the isolated source-analysis boundary. */
export const JAVASCRIPT_ANALYSIS_RESOURCE_DEFAULTS = {
heapMb: 1024,
timeoutMs: 300_000,
} as const;
/** Project observed execution limits without inventing unmeasured values. */
export const javaScriptAnalysisCoverageLimits = (
limits: Readonly<Record<string, JsonValue>> | null,
): ApplicationCoverage["limits"] => {
const result: ApplicationCoverage["limits"] = [];
for (const key of [
"worker_heap_limit_bytes",
"worker_timeout_ms",
"parent_result_budget_bytes",
"semantic_result_budget_bytes",
"transfer_record_bytes",
"transfer_file_bytes",
]) {
const value = limits?.[key];
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0)
continue;
result.push({
name: `javascript_analysis_${key}`,
value,
unit: key.endsWith("_ms") ? "milliseconds" : "bytes",
});
}
return result;
};
/** Optional operational choices; omission keeps them out of Evidence parameters. */
export const javaScriptAnalysisResourceControlsSchema = z.strictObject({
max_heap_mb: z
.number()
.int()
.min(128)
.max(16_384)
.optional()
.describe(
"Maximum V8 old-space heap in MiB for the isolated JavaScript source analyzer; defaults to 1024. This does not change the CLI/MCP process heap.",
),
analysis_timeout_ms: z
.number()
.int()
.min(1)
.max(2_147_483_647)
.optional()
.describe(
"Maximum startup, analysis and transfer time per JavaScript source, in milliseconds; defaults to 300000. On timeout, return collected partial Evidence and stop the analysis.",
),
});
@@ -15,6 +15,7 @@ import { prefixedDigestSchema } from "../digests.js";
import { integrityContradictionSchema } from "../artifactGraph.js";
import { artifactIntegrityPolicySchema } from "../artifactIntegrityPolicy.js";
import { localPathStringSchema } from "../localPath.js";
import { javaScriptAnalysisResourceControlsSchema } from "./javascriptAnalysisResourceControls.js";
const countSchema = z.number().int().min(0);
@@ -23,6 +24,7 @@ export const analyzeJavaScriptApplicationInputSchema = z.strictObject({
input_path: localPathStringSchema,
format: z.enum(["auto", "asar", "directory"]).default("auto"),
integrity_policy: artifactIntegrityPolicySchema,
...javaScriptAnalysisResourceControlsSchema.shape,
});
/** Compact counts for the high-level Electron architecture/security surface. */
@@ -8,7 +8,10 @@ import { canonicalJson } from "../comparisonSemantics.js";
import { digestCanonicalValue } from "../canonicalDigest.js";
import { uniqueSorted } from "../canonicalOrdering.js";
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
import { freezeOwnedJsonSnapshotSteps } from "../immutableJson.js";
import {
freezeJsonSnapshot,
freezeOwnedJsonSnapshotSteps,
} from "../immutableJson.js";
import type {
ApplicationGraphEvidence,
@@ -449,6 +452,10 @@ export const createJavaScriptApplicationNode = (
input: unknown,
): ApplicationNode => {
const parsed = applicationNodeInputSchema.parse(input);
// These properties are newly parsed and exclusively owned. Authenticate
// them before the observation/node schemas and final graph validate again.
for (const observation of parsed.observations)
freezeJsonSnapshot(observation.properties);
const identity = normalizeIdentity(parsed.identity);
const identifier = nodeId({ kind: parsed.kind, identity });
const observations = parsed.observations
@@ -479,6 +486,7 @@ export const createJavaScriptApplicationEdge = (
input: unknown,
): ApplicationEdge => {
const parsed = applicationEdgeInputSchema.parse(input);
freezeJsonSnapshot(parsed.properties);
const semantic = {
...parsed,
evidence: normalizeEvidence(parsed.evidence),
@@ -0,0 +1,42 @@
import type { JavaScriptStaticAnalysis } from "./javascriptStaticAnalysisTypes.js";
/** Forecast static-fact expansion into observations, nodes and relationships. */
export const estimateJavaScriptStaticApplicationProjection = (
facts: JavaScriptStaticAnalysis,
checkpointBytes: number,
sourcePath: string,
): number => {
// A finding can produce a node, a separate observation and several edges.
// Allow for identifiers, locations, authority, coverage and schema copies,
// independently of the producer's compact checkpoint representation.
const findings =
facts.references.length +
facts.endpoints.length +
facts.storage.length +
facts.role_paths.length +
facts.source_map_urls.length +
facts.electron.browser_windows.length +
facts.electron.context_bridge_apis.length +
facts.electron.ipc.length +
facts.electron.sender_validations.length +
facts.electron.utility_processes.length +
facts.electron.native_addon_bindings.length;
let records = findings * 8;
let repeatedBytes = 0;
for (const registration of facts.bundler_registrations) {
records +=
3 +
registration.modules.length * 3 +
registration.entry_module_keys.length * 3 +
registration.async_chunk_keys.length * 3;
// Every module observation and containment edge repeats its chunk keys.
repeatedBytes +=
Buffer.byteLength(JSON.stringify(registration.chunk_keys)) *
registration.modules.length *
2;
}
for (const binding of facts.electron.native_addon_bindings)
records += binding.members.length * 3;
const metadataBytes = 2048 + Buffer.byteLength(sourcePath) * 4;
return (checkpointBytes * 4 + repeatedBytes + records * metadataBytes) * 8;
};
@@ -0,0 +1,36 @@
import type { JavaScriptSemanticIr } from "./javascriptSemanticIr.js";
/** Module facts needed after file-local semantic projection has finished. */
export type JavaScriptModuleSemanticIr = Pick<
JavaScriptSemanticIr,
| "scopes"
| "bindings"
| "callables"
| "moduleLinks"
| "coverage"
| "limitations"
>;
/** Retain facts consumed by source-module and export-return composition. */
export const projectJavaScriptModuleSemantics = (
ir: JavaScriptSemanticIr,
): JavaScriptModuleSemanticIr => {
const scopes = ir.scopes.filter(({ kind }) => kind === "program");
const programScopeId = scopes[0]?.scopeId;
const localNames = new Set(ir.moduleLinks.map(({ localName }) => localName));
const callableIds = new Set(
ir.moduleLinks.map(({ callableId }) => callableId),
);
return {
scopes,
bindings: ir.bindings.filter(
({ name, scopeId }) => scopeId === programScopeId && localNames.has(name),
),
callables: ir.callables.filter(({ callableId }) =>
callableIds.has(callableId),
),
moduleLinks: ir.moduleLinks,
coverage: ir.coverage,
limitations: ir.limitations,
};
};
@@ -0,0 +1,18 @@
import type {
JavaScriptSemanticEvidenceContext,
JavaScriptSemanticFingerprint,
JavaScriptSemanticGraphNode,
JavaScriptSemanticGraphRelation,
JavaScriptSemanticGraphUnknown,
} from "./javascriptSemanticGraphSchemas.js";
/** File-local facts transported before binding them to the containing application graph. */
export interface JavaScriptSemanticFileProjection {
readonly roots: readonly string[];
readonly evidenceContexts: readonly JavaScriptSemanticEvidenceContext[];
readonly nodes: readonly JavaScriptSemanticGraphNode[];
readonly relations: readonly JavaScriptSemanticGraphRelation[];
readonly unknowns: readonly JavaScriptSemanticGraphUnknown[];
readonly fingerprints: readonly JavaScriptSemanticFingerprint[];
readonly truncated: boolean;
}
@@ -19,6 +19,7 @@ import {
JAVASCRIPT_SEMANTIC_RELATION_FAMILIES,
JAVASCRIPT_SEMANTIC_RELATION_FAMILY,
javaScriptSemanticFingerprintInputSchema,
javaScriptSemanticEvidenceContextSchema,
javaScriptSemanticGraphInputSchema,
javaScriptSemanticGraphRecordSchema,
javaScriptSemanticNodeInputSchema,
@@ -109,6 +110,33 @@ export class JavaScriptSemanticEvidenceContextRegistry {
this.#contexts.clear();
this.#idsByCanonicalContext.clear();
}
/** Validate and intern provenance decoded in this realm from an analysis worker. */
adopt(input: unknown): void {
const context = javaScriptSemanticEvidenceContextSchema.parse(input);
const { context_id: identifier, ...value } = context;
if (identifier !== evidenceContextId(value))
throw new TypeError(
"Transferred semantic evidence context identifier is stale",
);
const canonical = canonicalJson(
value,
"JavaScript semantic evidence context",
);
const existing = this.#contexts.get(identifier);
if (existing !== undefined) {
const { context_id: _existingIdentifier, ...existingValue } = existing;
if (
canonical !==
canonicalJson(existingValue, "JavaScript semantic evidence context")
)
throw new TypeError(
"Transferred semantic evidence context digest collision",
);
}
this.#contexts.set(identifier, context);
this.#idsByCanonicalContext.set(canonical, identifier);
}
}
/** Derive a semantic node ID from its kind and exact artifact identity. */
@@ -0,0 +1,40 @@
import type { AnalysisError } from "../analysisErrorBase.js";
import type { AnalysisResourceConstraintError } from "../analysisErrorCore.js";
import type { Result } from "../result.js";
import type { JavaScriptArtifactFile } from "./javascriptArtifactFiles.js";
import type { JavaScriptStaticAnalysis } from "./javascriptStaticAnalysisTypes.js";
import type { JavaScriptModuleSemanticIr } from "./javascriptModuleSemanticIr.js";
import type { JavaScriptSemanticFileProjection } from "./javascriptSemanticFileProjection.js";
/** Portable facts collected from one inert source. */
export interface JavaScriptAnalyzedSource {
readonly javascript: JavaScriptStaticAnalysis;
readonly module: JavaScriptModuleSemanticIr | null;
readonly projection: JavaScriptSemanticFileProjection | null;
readonly applicationProjectionFailure?: AnalysisResourceConstraintError;
}
/** Execution failure alongside facts already collected by the consumer. */
export interface JavaScriptSourceAnalysisFailure {
readonly error: AnalysisError;
readonly javascript: JavaScriptStaticAnalysis | null;
readonly module: JavaScriptModuleSemanticIr | null;
readonly projection: JavaScriptSemanticFileProjection | null;
readonly applicationProjectionFailure?: AnalysisResourceConstraintError;
}
/** Operational choices for source analysis, independent of transport framing. */
export interface JavaScriptSourceAnalysisLimits {
readonly heapMb: number;
readonly timeoutMs: number;
}
/** Sequential source-analysis authority with independently verified cleanup. */
export interface JavaScriptSourceAnalysisPort {
analyze(
file: JavaScriptArtifactFile,
nodeBudget: number,
signal?: AbortSignal,
): Promise<Result<JavaScriptAnalyzedSource, JavaScriptSourceAnalysisFailure>>;
close(): Promise<void>;
}