mirror of
https://github.com/morluto/rea.git
synced 2026-11-05 14:26:07 +00:00
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:
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>;
|
||||
}
|
||||
Reference in new issue
Block a user