mirror of
https://github.com/morluto/rea.git
synced 2026-11-11 17:28:08 +00:00
feat(analysis): make resource budgets recoverable across CLI and MCP (#9706)
* refactor(analysis): centralize resource budgets and recovery facts * feat(analysis): expose ASAR and Ghidra runtime admission controls * feat(artifacts): expose reader admission and preserve limited recognition * feat(javascript): expose recoverable semantic precision budgets * feat(web): add recoverable source-map budgets and compact retained views * feat(web): make historical capture decoding budgets recoverable * fix(desktop): enforce selected view policies across capture and export * feat(runtime): make Frida and LLDB retention budgets selectable * feat(native): expose diagnostic budgets and preserve exhaustion causes * feat(managed): expose recoverable snapshot and representation budgets * feat(metadata): make Go and Rust inspection budgets recoverable * feat(wasm): expose representation and producer admissions * feat(evm): expose per-call worker resource budgets * feat(android): make resource and string-pool omissions recoverable * feat(producers): expose command and recorded-capture budgets * docs(analysis): record recovery contracts and integrated validation * fix(docs): link calibration oracle from published verification pages * fix(docs): restore desktop task anchors and receipt links * test(analysis): align fixtures with selectable budget contracts * fix(analysis): remove source-location projector dependency cycle * test(analysis): refresh budget recovery and verifier regressions * test(analysis): preserve format and publication recovery expectations * test(windows): align conformance with owned runtime admission * fix(desktop): preserve selected alias kinds in exported state * test(analysis): align provider fixtures with recoverable budgets * test(desktop): validate emitted alias metadata structurally * fix(ghidra): classify bounded Windows endpoint refusals * test(cli): assert selectable artifact admission recovery * fix(cli): preserve capture provenance in environment builds
This commit is contained in:
1 parent
55b6d2ca20
commit
cb4e5cfbff
515 files changed
+21066
-3637
No files matched your search
@@ -348,6 +348,9 @@ export class JavaScriptAnalysisWorker {
|
||||
id,
|
||||
node_budget: capacity.nodes,
|
||||
unknown_budget: capacity.unknowns,
|
||||
...(this.limits.semanticPrecision === undefined
|
||||
? {}
|
||||
: { semantic_precision: this.limits.semanticPrecision }),
|
||||
text_sha256: createHash("sha256").update(file.text.value).digest("hex"),
|
||||
file: metadata,
|
||||
package_type: packageType,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { javaScriptSemanticPrecisionSchema } from "../../domain/javascript/javascriptSemanticPrecision.js";
|
||||
import { z } from "zod";
|
||||
import { ANALYSIS_RESOURCE_KINDS } from "../../domain/analysisErrorCore.js";
|
||||
import { jsonValueSchema } from "../../domain/jsonValue.js";
|
||||
@@ -25,6 +26,7 @@ export const javaScriptCheckpointCeilingRemediation = (
|
||||
/** Caller-selected artifact identity, separate from the decoded source-text digest. */
|
||||
export const javaScriptWorkerRequestSchema = z.strictObject({
|
||||
id: z.uuid(),
|
||||
semantic_precision: javaScriptSemanticPrecisionSchema.optional(),
|
||||
unknown_budget: z.int().min(0).max(JAVASCRIPT_SEMANTIC_ITEM_MAXIMUM),
|
||||
node_budget: z.int().min(0).max(JAVASCRIPT_SEMANTIC_ITEM_MAXIMUM),
|
||||
text_sha256: digestSchema,
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
resolveJavaScriptSemanticPrecision,
|
||||
javaScriptSemanticPrecisionSchema,
|
||||
} from "../../domain/javascript/javascriptSemanticPrecision.js";
|
||||
import {
|
||||
javaScriptSemanticUnknownOmissionsSchema,
|
||||
javaScriptSemanticNodeSchema,
|
||||
@@ -609,18 +613,27 @@ const coverage = z
|
||||
status: z.literal("complete"),
|
||||
omittedCount: z.literal(0),
|
||||
resourceLimits: z.array(javaScriptSemanticResourceLimitSchema).optional(),
|
||||
precision: javaScriptSemanticPrecisionSchema
|
||||
.transform((value) => resolveJavaScriptSemanticPrecision(value))
|
||||
.exactOptional(),
|
||||
widenedReturnedReferences: z.array(range).optional(),
|
||||
}),
|
||||
z.strictObject({
|
||||
status: z.literal("partial"),
|
||||
omittedCount: z.union([z.literal(0), z.null()]),
|
||||
resourceLimits: z.array(javaScriptSemanticResourceLimitSchema).optional(),
|
||||
precision: javaScriptSemanticPrecisionSchema
|
||||
.transform((value) => resolveJavaScriptSemanticPrecision(value))
|
||||
.exactOptional(),
|
||||
widenedReturnedReferences: z.array(range).optional(),
|
||||
}),
|
||||
z.strictObject({
|
||||
status: z.literal("failed"),
|
||||
omittedCount: z.null(),
|
||||
resourceLimits: z.array(javaScriptSemanticResourceLimitSchema).optional(),
|
||||
precision: javaScriptSemanticPrecisionSchema
|
||||
.transform((value) => resolveJavaScriptSemanticPrecision(value))
|
||||
.exactOptional(),
|
||||
widenedReturnedReferences: z.array(range).optional(),
|
||||
}),
|
||||
])
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { JavaScriptSemanticPrecisionInput } from "../../domain/javascript/javascriptSemanticPrecision.js";
|
||||
import { fitJavaScriptRouteSourceFacts } from "../../domain/javascript/javascriptRouteResultBudget.js";
|
||||
import { jsonValueByteLength } from "../../domain/jsonValueByteLength.js";
|
||||
import type { JsonValue } from "../../domain/jsonValue.js";
|
||||
@@ -46,6 +47,7 @@ export interface JavaScriptSourceWorkerAnalyzer {
|
||||
},
|
||||
packageType: JavaScriptPackageType,
|
||||
purpose: "analysis" | "preload-discovery",
|
||||
precision?: JavaScriptSemanticPrecisionInput,
|
||||
): Promise<{
|
||||
readonly module: JavaScriptModuleSemanticIr;
|
||||
readonly projection: JavaScriptSemanticFileProjection;
|
||||
@@ -191,6 +193,7 @@ const analyzeJob = async (
|
||||
},
|
||||
request.package_type,
|
||||
request.purpose,
|
||||
request.semantic_precision,
|
||||
);
|
||||
await send({ kind: "phase", id: request.id, phase: "transfer" });
|
||||
const descriptor =
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BOUNDED_PRODUCER_LIMITS } from "../../process/LinuxResourceLimits.js";
|
||||
import { BOUNDED_PRODUCER_LIMITS } from "../../domain/producerResourceLimits.js";
|
||||
|
||||
/**
|
||||
* Releases the report parser accepts. Wakaru changes `--version`, `--json`
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import { webSourceLocationInputSchema } from "../../domain/webSourceLocation.js";
|
||||
import { analyzeWebBundleInputSchema } from "../../domain/webBundleAnalysis.js";
|
||||
import { webSourceLocationArgs } from "../../../tests/fixtures/webSourceLocation.js";
|
||||
import { selectedSourceMapLimits } from "../../domain/sourceMapLimits.js";
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { inspectSourceMapEnvelope } from "../../domain/sourceMapEnvelope.js";
|
||||
@@ -49,3 +53,76 @@ it("admits numeric version 3 through the same consumers", () => {
|
||||
}).matches,
|
||||
).toMatchObject([{ section_path: [0, 0], reported_source: "original.ts" }]);
|
||||
});
|
||||
|
||||
it("recovers the same finite map by raising the exact decoder record admission", () => {
|
||||
const text = JSON.stringify({
|
||||
version: 3,
|
||||
sources: ["a.ts"],
|
||||
sourcesContent: ["original"],
|
||||
names: [],
|
||||
mappings: "AAAA",
|
||||
});
|
||||
const position = { line: 1, column: 0 };
|
||||
expect(() =>
|
||||
traceSourceMap(
|
||||
text,
|
||||
"https://example.test/app.map",
|
||||
position,
|
||||
selectedSourceMapLimits({ decoded_records: 4 }),
|
||||
),
|
||||
).toThrow(
|
||||
expect.objectContaining({
|
||||
reason: "limit",
|
||||
message: expect.stringContaining(
|
||||
"source_map_limits.decoded_records is 4",
|
||||
),
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
traceSourceMap(
|
||||
text,
|
||||
"https://example.test/app.map",
|
||||
position,
|
||||
selectedSourceMapLimits({ decoded_records: 5 }),
|
||||
).matches,
|
||||
).toMatchObject([{ reported_source: "a.ts", content: { text: "original" } }]);
|
||||
});
|
||||
|
||||
it("rejects invalid and inapplicable budget selections at the public owning contracts", () => {
|
||||
for (const selection of [
|
||||
{ map_bytes: 0 },
|
||||
{ output_bytes: 536870888 },
|
||||
{ decoded_records: 0 },
|
||||
{ decoded_records: undefined },
|
||||
{ codec_heap_mib: 127 },
|
||||
{ fetch_bytes: 1024 },
|
||||
]) {
|
||||
expect(
|
||||
webSourceLocationInputSchema.safeParse({
|
||||
...webSourceLocationArgs,
|
||||
source_map_limits: selection,
|
||||
}).success,
|
||||
).toBe(false);
|
||||
}
|
||||
const bundle = {
|
||||
cdp_endpoint: "http://127.0.0.1:9222",
|
||||
allowed_origins: ["https://app.test"],
|
||||
target_id: "page",
|
||||
fetch_source_maps: true,
|
||||
};
|
||||
expect(
|
||||
analyzeWebBundleInputSchema.safeParse({
|
||||
...bundle,
|
||||
fetch_source_maps: false,
|
||||
source_map_limits: { decoded_records: 5 },
|
||||
}).success,
|
||||
).toBe(false);
|
||||
for (const selection of [{ map_bytes: 1024 }, { codec_heap_mib: 256 }]) {
|
||||
expect(
|
||||
analyzeWebBundleInputSchema.safeParse({
|
||||
...bundle,
|
||||
source_map_limits: selection,
|
||||
}).success,
|
||||
).toBe(false);
|
||||
}
|
||||
});
|
||||
@@ -1,5 +1,9 @@
|
||||
import { readStableArtifact } from "../../artifacts/readStableArtifact.js";
|
||||
import { WEB_SOURCE_MAP_LIMITS } from "../../domain/webSourceLocation.js";
|
||||
import {
|
||||
selectedSourceMapLimits,
|
||||
sourceMapLimitRecovery,
|
||||
sourceMapLimitsSchema,
|
||||
} from "../../domain/sourceMapLimits.js";
|
||||
import { traceSourceMap } from "./TraceSourceMap.js";
|
||||
import { SourceMapFormatFailure } from "./SourceMapFormat.js";
|
||||
import {
|
||||
@@ -17,26 +21,32 @@ const execute = async (): Promise<string> => {
|
||||
"format",
|
||||
"Codec request path is missing.",
|
||||
);
|
||||
const data = await readStableArtifact(
|
||||
path,
|
||||
WEB_SOURCE_MAP_LIMITS.outputBytes,
|
||||
const selected = sourceMapLimitsSchema.parse(
|
||||
JSON.parse(process.argv[3] ?? "{}"),
|
||||
);
|
||||
const limits = selectedSourceMapLimits(selected);
|
||||
const data = await readStableArtifact(path, limits.outputBytes);
|
||||
const text = new TextDecoder("utf-8", { fatal: true }).decode(data.bytes);
|
||||
const value: unknown = JSON.parse(text);
|
||||
const input = sourceMapCodecInputSchema.parse(value);
|
||||
if (Buffer.byteLength(input.text) > WEB_SOURCE_MAP_LIMITS.mapBytes)
|
||||
if (Buffer.byteLength(input.text) > limits.mapBytes)
|
||||
throw new SourceMapFormatFailure(
|
||||
"limit",
|
||||
"Source map exceeds the 4 MiB codec input budget.",
|
||||
`Source map exceeds codec input admission. ${sourceMapLimitRecovery("map_bytes", limits.mapBytes)}`,
|
||||
);
|
||||
const report = traceSourceMap(input.text, input.url, input.position);
|
||||
const report = traceSourceMap(
|
||||
input.text,
|
||||
input.url,
|
||||
input.position,
|
||||
limits,
|
||||
);
|
||||
const reply = JSON.stringify(
|
||||
sourceMapCodecReplySchema.parse({ state: "success", report }),
|
||||
);
|
||||
if (Buffer.byteLength(reply) > WEB_SOURCE_MAP_LIMITS.outputBytes)
|
||||
if (Buffer.byteLength(reply) > limits.outputBytes)
|
||||
throw new SourceMapFormatFailure(
|
||||
"limit",
|
||||
"Complete source-map point evidence exceeds the 32 MiB output budget; no partial evidence was returned.",
|
||||
`Complete source-map point evidence exceeds output admission; no partial evidence was returned. ${sourceMapLimitRecovery("output_bytes", limits.outputBytes)}`,
|
||||
);
|
||||
return reply;
|
||||
} catch (cause: unknown) {
|
||||
|
||||
@@ -21,10 +21,14 @@ import {
|
||||
import { traceSourceMap } from "./TraceSourceMap.js";
|
||||
import {
|
||||
runSourceMapCommand,
|
||||
SOURCE_MAP_CODEC_MEMORY_LIMIT_BYTES,
|
||||
type SourceMapCleanupRetention,
|
||||
} from "./SourceMapCommand.js";
|
||||
|
||||
const expectedProcessMemoryLimitBytes =
|
||||
process.platform === "win32" && process.versions.bun !== undefined
|
||||
? 704 * 1024 * 1024
|
||||
: null;
|
||||
|
||||
const report = traceSourceMap(
|
||||
JSON.stringify({ version: 3, sources: [], names: [], mappings: "" }),
|
||||
"https://app.test/app.map",
|
||||
@@ -37,7 +41,7 @@ const completedReport: WebSourceMapReport = {
|
||||
? { ...report.runtime, v8_heap_limit_bytes: 200 * 1024 * 1024 }
|
||||
: {
|
||||
...report.runtime,
|
||||
process_memory_limit_bytes: SOURCE_MAP_CODEC_MEMORY_LIMIT_BYTES,
|
||||
process_memory_limit_bytes: expectedProcessMemoryLimitBytes,
|
||||
},
|
||||
};
|
||||
const completedCodec = `process.stdout.write(${JSON.stringify(JSON.stringify({ state: "success", report: completedReport }))})`;
|
||||
@@ -361,7 +365,7 @@ it("preserves an observed process-memory limit over the codec exit status", asyn
|
||||
expect(result.error).toMatchObject({
|
||||
resource: "memory",
|
||||
reportedLimits: {
|
||||
codec_process_memory_limit_bytes: SOURCE_MAP_CODEC_MEMORY_LIMIT_BYTES,
|
||||
codec_process_memory_limit_bytes: expectedProcessMemoryLimitBytes,
|
||||
},
|
||||
capturedOutput: { stderr: "memory limit reached" },
|
||||
});
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import {
|
||||
sourceMapLimitSelection,
|
||||
sourceMapLimitRecovery,
|
||||
type SourceMapLimits,
|
||||
} from "../../domain/sourceMapLimits.js";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
@@ -15,7 +20,6 @@ import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
|
||||
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
|
||||
import type { JsonValue } from "../../domain/jsonValue.js";
|
||||
import { err, type Result } from "../../domain/result.js";
|
||||
import { noRuntimeOverrideSentence } from "../../domain/browserDeadline.js";
|
||||
import {
|
||||
DEFAULT_WEB_SOURCE_MAP_DECODE_TIMEOUT_MS,
|
||||
WEB_SOURCE_MAP_LIMITS,
|
||||
@@ -43,9 +47,6 @@ import { prepareProcessOwnershipInspection } from "../../process/ProcessOwnershi
|
||||
|
||||
const OPERATION = "trace_web_source_location";
|
||||
const PROVIDER = "source-map-decoder";
|
||||
/** Process commit limit enforced on the codec; null where V8 heap flags apply. */
|
||||
export const SOURCE_MAP_CODEC_MEMORY_LIMIT_BYTES =
|
||||
javaScriptWorkerMemoryLimitBytes(WEB_SOURCE_MAP_LIMITS.codecHeapMiB);
|
||||
/** Launcher seam retains the actual codec protocol and ownership supervisor. */
|
||||
export type SourceMapCodecLauncher = (
|
||||
options: OwnedProviderProcessSpawnOptions,
|
||||
@@ -83,7 +84,7 @@ type CodecPhysicalObservation =
|
||||
readonly message: string;
|
||||
readonly cause: Error;
|
||||
}
|
||||
| { readonly kind: "output-limit" };
|
||||
| { readonly kind: "output-limit"; readonly stream: "stdout" | "stderr" };
|
||||
type CodecOutcome =
|
||||
| {
|
||||
readonly kind: "physical";
|
||||
@@ -109,6 +110,7 @@ export const runSourceMapCommand = async (
|
||||
dependencies: SourceMapDecoderDependencies,
|
||||
retainedCleanup: SourceMapCleanupRetention,
|
||||
decodeTimeoutMs: number = DEFAULT_WEB_SOURCE_MAP_DECODE_TIMEOUT_MS,
|
||||
limits: SourceMapLimits = WEB_SOURCE_MAP_LIMITS,
|
||||
): Promise<Result<WebSourceMapReport, AnalysisError>> => {
|
||||
try {
|
||||
await (dependencies.prepareOwnership ?? prepareProcessOwnershipInspection)(
|
||||
@@ -136,14 +138,21 @@ export const runSourceMapCommand = async (
|
||||
decodeTimeoutMs,
|
||||
options?.signal,
|
||||
);
|
||||
const command = new OwnedCodec(path, dependencies, deadline, decodeTimeoutMs);
|
||||
const command = new OwnedCodec(
|
||||
path,
|
||||
dependencies,
|
||||
deadline,
|
||||
decodeTimeoutMs,
|
||||
limits,
|
||||
);
|
||||
let result: Result<WebSourceMapReport, AnalysisError>;
|
||||
try {
|
||||
await command.start(request);
|
||||
result = parseSourceMapReply(
|
||||
await command.collect(),
|
||||
path,
|
||||
SOURCE_MAP_CODEC_MEMORY_LIMIT_BYTES,
|
||||
javaScriptWorkerMemoryLimitBytes(limits.codecHeapMiB),
|
||||
limits.codecHeapMiB,
|
||||
);
|
||||
} catch (cause: unknown) {
|
||||
result = err(command.failure(cause));
|
||||
@@ -189,6 +198,7 @@ class OwnedCodec implements SourceMapCleanupOwner {
|
||||
readonly dependencies: SourceMapDecoderDependencies,
|
||||
readonly deadline: ProviderStartupDeadline,
|
||||
readonly decodeTimeoutMs: number,
|
||||
readonly limits: SourceMapLimits,
|
||||
) {
|
||||
this.#onInterruption = () => this.#observeInterruption();
|
||||
if (deadline.signal.aborted) this.#observeInterruption();
|
||||
@@ -227,20 +237,25 @@ class OwnedCodec implements SourceMapCleanupOwner {
|
||||
)({
|
||||
command: process.execPath,
|
||||
arguments: [
|
||||
`--max-old-space-size=${String(WEB_SOURCE_MAP_LIMITS.codecHeapMiB)}`,
|
||||
`--max-old-space-size=${String(this.limits.codecHeapMiB)}`,
|
||||
"--max-semi-space-size=8",
|
||||
"--v8-pool-size=1",
|
||||
"--single-threaded",
|
||||
"--stack_size=2048",
|
||||
fileURLToPath(new URL("./SourceMapCodecProcess.js", import.meta.url)),
|
||||
requestPath,
|
||||
JSON.stringify(sourceMapLimitSelection(this.limits)),
|
||||
],
|
||||
runId: this.#runId,
|
||||
cwd: this.#runtime.path,
|
||||
hostEnvironment: environment,
|
||||
...(SOURCE_MAP_CODEC_MEMORY_LIMIT_BYTES === null
|
||||
...(javaScriptWorkerMemoryLimitBytes(this.limits.codecHeapMiB) === null
|
||||
? {}
|
||||
: { memoryLimitBytes: SOURCE_MAP_CODEC_MEMORY_LIMIT_BYTES }),
|
||||
: {
|
||||
memoryLimitBytes: javaScriptWorkerMemoryLimitBytes(
|
||||
this.limits.codecHeapMiB,
|
||||
)!,
|
||||
}),
|
||||
signal: this.deadline.signal,
|
||||
});
|
||||
this.#supervisor = new ProviderProcessSupervisor(
|
||||
@@ -252,7 +267,7 @@ class OwnedCodec implements SourceMapCleanupOwner {
|
||||
(() => cleanupOwnedProcessGroup(spawned.ownership)),
|
||||
},
|
||||
{
|
||||
maxDiagnosticBytes: WEB_SOURCE_MAP_LIMITS.outputBytes + 1024 * 1024,
|
||||
maxDiagnosticBytes: this.limits.outputBytes + 1024 * 1024,
|
||||
onDiagnostic: (event) => {
|
||||
if (
|
||||
event.type === "exit" &&
|
||||
@@ -274,10 +289,13 @@ class OwnedCodec implements SourceMapCleanupOwner {
|
||||
(event.truncated === true ||
|
||||
event.totalBytes >
|
||||
(event.stream === "stdout"
|
||||
? WEB_SOURCE_MAP_LIMITS.outputBytes
|
||||
? this.limits.outputBytes
|
||||
: 1024 * 1024))
|
||||
) {
|
||||
this.#recordPhysicalFailure({ kind: "output-limit" });
|
||||
this.#recordPhysicalFailure({
|
||||
kind: "output-limit",
|
||||
stream: event.stream,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -375,7 +393,7 @@ class OwnedCodec implements SourceMapCleanupOwner {
|
||||
}
|
||||
if (observation.kind === "process-error")
|
||||
return this.#processErrorFailure(observation, snapshot);
|
||||
return this.#outputLimitFailure(snapshot);
|
||||
return this.#outputLimitFailure(snapshot, observation.stream);
|
||||
}
|
||||
|
||||
#exitFailure(
|
||||
@@ -425,6 +443,7 @@ class OwnedCodec implements SourceMapCleanupOwner {
|
||||
|
||||
#outputLimitFailure(
|
||||
snapshot: ProviderProcessSnapshot | undefined,
|
||||
stream: "stdout" | "stderr",
|
||||
): AnalysisError {
|
||||
const outputComplete = this.#supervisor?.observedOutputClose() ?? false;
|
||||
const capturedOutput = this.#capturedOutput(
|
||||
@@ -434,8 +453,14 @@ class OwnedCodec implements SourceMapCleanupOwner {
|
||||
);
|
||||
return new AnalysisOutputError(
|
||||
OPERATION,
|
||||
"Codec output exceeds its complete-evidence or diagnostic budget.",
|
||||
stream === "stdout"
|
||||
? "Codec output exceeds its complete-evidence budget."
|
||||
: "Codec diagnostic output exceeds its 1048576-byte allowance.",
|
||||
{
|
||||
remediationAction:
|
||||
stream === "stdout"
|
||||
? sourceMapLimitRecovery("output_bytes", this.limits.outputBytes)
|
||||
: "The codec stderr diagnostic allowance is fixed at 1048576 bytes; no source_map_limits field raises it. Inspect retained diagnostics before another call.",
|
||||
capturedOutput: {
|
||||
stdout: capturedOutput?.stdout ?? "",
|
||||
stderr: capturedOutput?.stderr ?? "",
|
||||
@@ -469,9 +494,11 @@ class OwnedCodec implements SourceMapCleanupOwner {
|
||||
return new AnalysisResourceConstraintError(
|
||||
OPERATION,
|
||||
"memory",
|
||||
`Source-map codec reached its ${SOURCE_MAP_CODEC_MEMORY_LIMIT_BYTES === null ? "" : `${String(SOURCE_MAP_CODEC_MEMORY_LIMIT_BYTES)}-byte `}process memory limit for ${this.path}; no partial evidence was returned. ${noRuntimeOverrideSentence(`The ${String(WEB_SOURCE_MAP_LIMITS.codecHeapMiB)} MiB codec heap stays fixed.`, "decode_timeout_ms")}`,
|
||||
`Source-map codec reached its ${javaScriptWorkerMemoryLimitBytes(this.limits.codecHeapMiB) === null ? "" : `${String(javaScriptWorkerMemoryLimitBytes(this.limits.codecHeapMiB))}-byte `}process memory limit for ${this.path}; no partial evidence was returned. ${sourceMapLimitRecovery("codec_heap_mib", this.limits.codecHeapMiB)}`,
|
||||
{
|
||||
codec_process_memory_limit_bytes: SOURCE_MAP_CODEC_MEMORY_LIMIT_BYTES,
|
||||
codec_process_memory_limit_bytes: javaScriptWorkerMemoryLimitBytes(
|
||||
this.limits.codecHeapMiB,
|
||||
),
|
||||
},
|
||||
{
|
||||
capturedOutput: {
|
||||
@@ -481,9 +508,9 @@ class OwnedCodec implements SourceMapCleanupOwner {
|
||||
snapshot.diagnosticTruncated === true ||
|
||||
!(this.#supervisor?.observedOutputClose() ?? false),
|
||||
},
|
||||
remediationAction: noRuntimeOverrideSentence(
|
||||
`The ${String(WEB_SOURCE_MAP_LIMITS.codecHeapMiB)} MiB codec heap, ${String(WEB_SOURCE_MAP_LIMITS.mapBytes)}-byte map budget, and ${String(WEB_SOURCE_MAP_LIMITS.outputBytes)}-byte reply budget stay fixed.`,
|
||||
"decode_timeout_ms",
|
||||
remediationAction: sourceMapLimitRecovery(
|
||||
"codec_heap_mib",
|
||||
this.limits.codecHeapMiB,
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import {
|
||||
selectedSourceMapLimits,
|
||||
localSourceMapLimitsSchema,
|
||||
sourceMapLimitRecovery,
|
||||
} from "../../domain/sourceMapLimits.js";
|
||||
import type { ExecutionOptions } from "../../application/AnalysisProvider.js";
|
||||
import type { WebSourceMapPort } from "../../application/WebSourceLocationPorts.js";
|
||||
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
@@ -7,10 +12,8 @@ import {
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import { ArtifactOperationError } from "../../domain/artifactOperationError.js";
|
||||
import { err, type Result } from "../../domain/result.js";
|
||||
import { noRuntimeOverrideSentence } from "../../domain/browserDeadline.js";
|
||||
import {
|
||||
DEFAULT_WEB_SOURCE_MAP_DECODE_TIMEOUT_MS,
|
||||
WEB_SOURCE_MAP_LIMITS,
|
||||
type WebSourceMapReport,
|
||||
} from "../../domain/webSourceLocation.js";
|
||||
import { sourceMapCodecInputSchema } from "./SourceMapCodecProtocol.js";
|
||||
@@ -84,21 +87,31 @@ export class SourceMapDecoder implements WebSourceMapPort {
|
||||
});
|
||||
if (!request.success)
|
||||
return err(new AnalysisInputError(OPERATION, { cause: request.error }));
|
||||
const capacityFailure = () =>
|
||||
new ArtifactOperationError(
|
||||
const selected = localSourceMapLimitsSchema.safeParse(input.limits ?? {});
|
||||
if (!selected.success)
|
||||
return err(new AnalysisInputError(OPERATION, { cause: selected.error }));
|
||||
const limits = selectedSourceMapLimits(selected.data);
|
||||
const capacityFailure = (
|
||||
field: "map_bytes" | "output_bytes",
|
||||
value: number,
|
||||
) => {
|
||||
const recovery = sourceMapLimitRecovery(field, value);
|
||||
return new ArtifactOperationError(
|
||||
OPERATION,
|
||||
"limit",
|
||||
undefined,
|
||||
`${input.path}: Source-map input exceeds the ${String(WEB_SOURCE_MAP_LIMITS.mapBytes)}-byte map or ${String(WEB_SOURCE_MAP_LIMITS.outputBytes)}-byte encoded request budget. ${noRuntimeOverrideSentence("Those budgets stay fixed.", "decode_timeout_ms")}`,
|
||||
`${input.path}: Source-map admission exhausted. ${recovery}`,
|
||||
{ remediationAction: recovery },
|
||||
);
|
||||
if (Buffer.byteLength(input.text) > WEB_SOURCE_MAP_LIMITS.mapBytes)
|
||||
return err(capacityFailure());
|
||||
};
|
||||
if (Buffer.byteLength(input.text) > limits.mapBytes)
|
||||
return err(capacityFailure("map_bytes", limits.mapBytes));
|
||||
const encoded: string[] = [];
|
||||
let encodedBytes = 0;
|
||||
for (const part of jsonParts(request.data)) {
|
||||
const bytes = Buffer.byteLength(part);
|
||||
if (bytes > WEB_SOURCE_MAP_LIMITS.outputBytes - encodedBytes)
|
||||
return err(capacityFailure());
|
||||
if (bytes > limits.outputBytes - encodedBytes)
|
||||
return err(capacityFailure("output_bytes", limits.outputBytes));
|
||||
encoded.push(part);
|
||||
encodedBytes += bytes;
|
||||
}
|
||||
@@ -115,6 +128,7 @@ export class SourceMapDecoder implements WebSourceMapPort {
|
||||
this.dependencies,
|
||||
retainedCleanup,
|
||||
input.decodeTimeoutMs ?? DEFAULT_WEB_SOURCE_MAP_DECODE_TIMEOUT_MS,
|
||||
limits,
|
||||
);
|
||||
if (retainedCleanup.owner !== undefined)
|
||||
this.#pendingCleanup = retainedCleanup.owner;
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { z } from "zod";
|
||||
import { WEB_SOURCE_MAP_LIMITS } from "../../domain/webSourceLocation.js";
|
||||
import {
|
||||
WEB_SOURCE_MAP_LIMITS,
|
||||
sourceMapLimitRecovery,
|
||||
type SourceMapLimits,
|
||||
} from "../../domain/sourceMapLimits.js";
|
||||
import {
|
||||
classifySourceMapSection,
|
||||
classifySourceMapSections,
|
||||
@@ -41,13 +45,14 @@ interface PendingMap {
|
||||
readonly stop: Offset | null;
|
||||
}
|
||||
|
||||
/** Shared structural constraints configurable only for stricter consumers. */
|
||||
/** Aggregate structural work shared by maps in one selected inspection. */
|
||||
export interface SourceMapStructureBudget {
|
||||
records: number;
|
||||
}
|
||||
|
||||
export interface SourceMapStructureOptions {
|
||||
readonly profile?: "browser-collection";
|
||||
readonly limits?: SourceMapLimits;
|
||||
readonly budget?: SourceMapStructureBudget;
|
||||
readonly check?: () => void;
|
||||
}
|
||||
@@ -98,7 +103,11 @@ const codecLeaf = (
|
||||
|
||||
const compare = (a: Offset, b: Offset): number =>
|
||||
a.line - b.line || a.column - b.column;
|
||||
const addOffset = (base: Offset, offset: Offset): Offset => {
|
||||
const addOffset = (
|
||||
base: Offset,
|
||||
offset: Offset,
|
||||
limits: SourceMapLimits,
|
||||
): Offset => {
|
||||
const result = {
|
||||
line: base.line + offset.line,
|
||||
column: offset.column + (offset.line === 0 ? base.column : 0),
|
||||
@@ -106,20 +115,24 @@ const addOffset = (base: Offset, offset: Offset): Offset => {
|
||||
if (
|
||||
!Number.isSafeInteger(result.line) ||
|
||||
!Number.isSafeInteger(result.column) ||
|
||||
result.line >= WEB_SOURCE_MAP_LIMITS.decodedRows
|
||||
result.line >= limits.decodedRows
|
||||
)
|
||||
throw new SourceMapFormatFailure(
|
||||
"limit",
|
||||
"Indexed offset exceeds the 262144 generated-row decoder budget.",
|
||||
`Indexed offset exceeds the generated-row decoder budget. ${sourceMapLimitRecovery("decoded_rows", limits.decodedRows)}`,
|
||||
);
|
||||
return result;
|
||||
};
|
||||
|
||||
const consumeRecords = (budget: { records: number }, count: number): void => {
|
||||
if (count > WEB_SOURCE_MAP_LIMITS.decodedRecords - budget.records)
|
||||
const consumeRecords = (
|
||||
budget: { records: number },
|
||||
limits: SourceMapLimits,
|
||||
count: number,
|
||||
): void => {
|
||||
if (count > limits.decodedRecords - budget.records)
|
||||
throw new SourceMapFormatFailure(
|
||||
"limit",
|
||||
`Source-map structure exceeds the ${WEB_SOURCE_MAP_LIMITS.decodedRecords}-record decoder budget.`,
|
||||
`Source-map structure exceeds the decoder budget. ${sourceMapLimitRecovery("decoded_records", limits.decodedRecords)}`,
|
||||
);
|
||||
budget.records += count;
|
||||
};
|
||||
@@ -137,8 +150,9 @@ const addSections = ({
|
||||
readonly budget: { records: number };
|
||||
readonly options: SourceMapStructureOptions;
|
||||
}): void => {
|
||||
const limits = options.limits ?? WEB_SOURCE_MAP_LIMITS;
|
||||
options.check?.();
|
||||
consumeRecords(budget, rawSections.length);
|
||||
consumeRecords(budget, limits, rawSections.length);
|
||||
const sections = z.array(sectionSchema).parse(rawSections);
|
||||
const embeddedMaps: Extract<
|
||||
SourceMapSection,
|
||||
@@ -176,14 +190,14 @@ const addSections = ({
|
||||
);
|
||||
const next = sections[index + 1];
|
||||
const nextOffset =
|
||||
next === undefined ? null : addOffset(node.offset, next.offset);
|
||||
next === undefined ? null : addOffset(node.offset, next.offset, limits);
|
||||
const stop =
|
||||
nextOffset === null
|
||||
? node.stop
|
||||
: node.stop === null || compare(nextOffset, node.stop) < 0
|
||||
? nextOffset
|
||||
: node.stop;
|
||||
const offset = addOffset(node.offset, section.offset);
|
||||
const offset = addOffset(node.offset, section.offset, limits);
|
||||
pending.push({
|
||||
value: map,
|
||||
path: [...node.path, index],
|
||||
@@ -195,6 +209,7 @@ const addSections = ({
|
||||
|
||||
const inspectMappingLayout = (
|
||||
mappings: string,
|
||||
limits: SourceMapLimits,
|
||||
check?: () => void,
|
||||
): { lines: number; segments: number } => {
|
||||
let lines = 1;
|
||||
@@ -241,13 +256,10 @@ const inspectMappingLayout = (
|
||||
digits = 0;
|
||||
}
|
||||
}
|
||||
if (
|
||||
segments > WEB_SOURCE_MAP_LIMITS.decodedSegments ||
|
||||
lines > WEB_SOURCE_MAP_LIMITS.decodedRows
|
||||
)
|
||||
if (segments > limits.decodedSegments || lines > limits.decodedRows)
|
||||
throw new SourceMapFormatFailure(
|
||||
"limit",
|
||||
"Source-map layout exceeds the 262144 segment/generated-row decoder budget.",
|
||||
`Source-map layout exceeds decoder admission. ${sourceMapLimitRecovery(segments > limits.decodedSegments ? "decoded_segments" : "decoded_rows", segments > limits.decodedSegments ? limits.decodedSegments : limits.decodedRows)}`,
|
||||
);
|
||||
}
|
||||
finish();
|
||||
@@ -259,8 +271,9 @@ const inspectLeaf = (
|
||||
budget: { records: number },
|
||||
options: SourceMapStructureOptions,
|
||||
): z.output<typeof leafSchema> => {
|
||||
const limits = options.limits ?? WEB_SOURCE_MAP_LIMITS;
|
||||
options.check?.();
|
||||
consumeRecords(budget, 1);
|
||||
consumeRecords(budget, limits, 1);
|
||||
if (
|
||||
typeof node.value !== "object" ||
|
||||
node.value === null ||
|
||||
@@ -271,20 +284,28 @@ const inspectLeaf = (
|
||||
const rawSources = rawMap.sources;
|
||||
const rawNames = rawMap.names;
|
||||
const rawContents = rawMap.sourcesContent;
|
||||
consumeRecords(budget, Array.isArray(rawSources) ? rawSources.length : 0);
|
||||
consumeRecords(budget, Array.isArray(rawNames) ? rawNames.length : 0);
|
||||
consumeRecords(budget, Array.isArray(rawContents) ? rawContents.length : 0);
|
||||
consumeRecords(
|
||||
budget,
|
||||
limits,
|
||||
Array.isArray(rawSources) ? rawSources.length : 0,
|
||||
);
|
||||
consumeRecords(budget, limits, Array.isArray(rawNames) ? rawNames.length : 0);
|
||||
consumeRecords(
|
||||
budget,
|
||||
limits,
|
||||
Array.isArray(rawContents) ? rawContents.length : 0,
|
||||
);
|
||||
options.check?.();
|
||||
const leaf = leafSchema.parse(node.value);
|
||||
const layout = inspectMappingLayout(leaf.mappings, options.check);
|
||||
consumeRecords(budget, layout.segments + layout.lines);
|
||||
const layout = inspectMappingLayout(leaf.mappings, limits, options.check);
|
||||
consumeRecords(budget, limits, layout.segments + layout.lines);
|
||||
if (
|
||||
layout.segments > WEB_SOURCE_MAP_LIMITS.decodedSegments ||
|
||||
node.offset.line + layout.lines > WEB_SOURCE_MAP_LIMITS.decodedRows
|
||||
layout.segments > limits.decodedSegments ||
|
||||
node.offset.line + layout.lines > limits.decodedRows
|
||||
)
|
||||
throw new SourceMapFormatFailure(
|
||||
"limit",
|
||||
"Source-map layout exceeds the 262144 segment/generated-row decoder budget.",
|
||||
`Source-map layout exceeds decoder admission. ${sourceMapLimitRecovery(layout.segments > limits.decodedSegments ? "decoded_segments" : "decoded_rows", layout.segments > limits.decodedSegments ? limits.decodedSegments : limits.decodedRows)}`,
|
||||
);
|
||||
return leaf;
|
||||
};
|
||||
@@ -292,10 +313,11 @@ const inspectLeaf = (
|
||||
/** Validate structure before allocation, retaining raw declarations and section boundaries. */
|
||||
export const inspectSourceMap = (
|
||||
text: string,
|
||||
options: SourceMapStructureOptions = {},
|
||||
): ReturnType<typeof inspectSourceMapValue> => {
|
||||
const jsonText = sourceMapJsonText(text);
|
||||
const value: unknown = JSON.parse(jsonText);
|
||||
return inspectSourceMapValue(value);
|
||||
return inspectSourceMapValue(value, options);
|
||||
};
|
||||
|
||||
/** Return validated leaves when a consumer needs decoded rows after preflight. */
|
||||
@@ -308,6 +330,7 @@ export const inspectSourceMapValue = (
|
||||
readonly declarations: readonly SourceMapSourceDeclaration[];
|
||||
readonly leaves: readonly SourceMapLeaf[];
|
||||
} => {
|
||||
const limits = options.limits ?? WEB_SOURCE_MAP_LIMITS;
|
||||
options.check?.();
|
||||
const root = mapHeaderSchema.parse(value);
|
||||
const declarations: SourceMapSourceDeclaration[] = [];
|
||||
@@ -320,10 +343,10 @@ export const inspectSourceMapValue = (
|
||||
options.check?.();
|
||||
const node = pending.pop();
|
||||
if (node === undefined) break;
|
||||
if (node.path.length > WEB_SOURCE_MAP_LIMITS.sectionDepth)
|
||||
if (node.path.length > limits.sectionDepth)
|
||||
throw new SourceMapFormatFailure(
|
||||
"limit",
|
||||
"Indexed source-map nesting exceeds the 64-level decoder stack budget.",
|
||||
`Indexed source-map nesting exceeds decoder admission. ${sourceMapLimitRecovery("section_depth", limits.sectionDepth)}`,
|
||||
);
|
||||
const sections = classifySourceMapSections(node.value);
|
||||
if (sections.kind === "invalid")
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { WEB_SOURCE_MAP_LIMITS } from "../../domain/sourceMapLimits.js";
|
||||
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import {
|
||||
AnalysisCapabilityUnavailableError,
|
||||
@@ -17,6 +18,7 @@ export const parseSourceMapReply = (
|
||||
text: string,
|
||||
path: string,
|
||||
processMemoryLimitBytes: number | null,
|
||||
heapMiB: number = WEB_SOURCE_MAP_LIMITS.codecHeapMiB,
|
||||
): Result<WebSourceMapReport, AnalysisError> => {
|
||||
let value: unknown;
|
||||
try {
|
||||
@@ -42,6 +44,7 @@ export const parseSourceMapReply = (
|
||||
const runtimeFailure = codecRuntimeFailure(
|
||||
reply.data.report.runtime,
|
||||
processMemoryLimitBytes,
|
||||
heapMiB,
|
||||
);
|
||||
return runtimeFailure === undefined
|
||||
? ok(reply.data.report)
|
||||
@@ -52,6 +55,7 @@ export const parseSourceMapReply = (
|
||||
const codecRuntimeFailure = (
|
||||
runtime: WebSourceMapReport["runtime"],
|
||||
processMemoryLimitBytes: number | null,
|
||||
heapMiB: number,
|
||||
): string | undefined => {
|
||||
if (
|
||||
runtime.id !==
|
||||
@@ -60,7 +64,7 @@ const codecRuntimeFailure = (
|
||||
return `Codec reported runtime ${runtime.id}, not the runtime that launched it.`;
|
||||
if (
|
||||
runtime.id === "node-v8" &&
|
||||
runtime.v8_heap_limit_bytes > 256 * 1024 * 1024
|
||||
runtime.v8_heap_limit_bytes > (heapMiB + 64) * 1024 * 1024
|
||||
)
|
||||
return "Codec reported a V8 heap limit exceeding its independent process budget.";
|
||||
if (
|
||||
@@ -83,6 +87,7 @@ const codecFailure = (
|
||||
"limit",
|
||||
undefined,
|
||||
`${path}: ${detail}`,
|
||||
{ remediationAction: detail },
|
||||
)
|
||||
: reason === "unsupported"
|
||||
? new AnalysisCapabilityUnavailableError(
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import {
|
||||
WEB_SOURCE_MAP_LIMITS,
|
||||
sourceMapLimitRecovery,
|
||||
type SourceMapLimits,
|
||||
} from "../../domain/sourceMapLimits.js";
|
||||
import { createHash } from "node:crypto";
|
||||
import { createRequire } from "node:module";
|
||||
import { getHeapStatistics } from "node:v8";
|
||||
@@ -15,7 +20,6 @@ import { resolveJavaScriptSourceMapUrl } from "../../domain/javascript/javascrip
|
||||
import {
|
||||
webSourceMapReportSchema,
|
||||
type WebSourceMapReport,
|
||||
WEB_SOURCE_MAP_LIMITS,
|
||||
} from "../../domain/webSourceLocation.js";
|
||||
import type { WebSourcePosition } from "../../domain/webSourcePosition.js";
|
||||
import {
|
||||
@@ -30,8 +34,9 @@ export const traceSourceMap = (
|
||||
text: string,
|
||||
url: string,
|
||||
position: WebSourcePosition,
|
||||
limits: SourceMapLimits = WEB_SOURCE_MAP_LIMITS,
|
||||
): WebSourceMapReport => {
|
||||
const inspected = inspectSourceMap(text);
|
||||
const inspected = inspectSourceMap(text, { limits });
|
||||
const decodedLeaves = decodeValidatedSourceMapLeaves(inspected.leaves, url);
|
||||
let map: TraceMap;
|
||||
if (inspected.format === "regular") {
|
||||
@@ -71,10 +76,10 @@ export const traceSourceMap = (
|
||||
for (const segment of segments) {
|
||||
const match = originalMatch(segment, map, url, inspected.declarations);
|
||||
bytes += Buffer.byteLength(JSON.stringify(match)) + 1;
|
||||
if (bytes > WEB_SOURCE_MAP_LIMITS.outputBytes)
|
||||
if (bytes > limits.outputBytes)
|
||||
throw new SourceMapFormatFailure(
|
||||
"limit",
|
||||
"Complete source-map point evidence exceeds the 32 MiB output budget; no partial evidence was returned.",
|
||||
`Complete source-map point evidence exceeds output admission; no partial evidence was returned. ${sourceMapLimitRecovery("output_bytes", limits.outputBytes)}`,
|
||||
);
|
||||
matches.push(match);
|
||||
}
|
||||
@@ -104,7 +109,7 @@ export const traceSourceMap = (
|
||||
node_compatibility_version: runtime.nodeCompatibilityVersion,
|
||||
v8_heap_limit_bytes: null,
|
||||
process_memory_limit_bytes: javaScriptWorkerMemoryLimitBytes(
|
||||
WEB_SOURCE_MAP_LIMITS.codecHeapMiB,
|
||||
limits.codecHeapMiB,
|
||||
),
|
||||
},
|
||||
format: inspected.format,
|
||||
|
||||
Reference in new issue
Block a user