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:
morluto authored and GitHub committed 2026-11-10 05:11:20 -05:00
1 parent 55b6d2ca20
commit cb4e5cfbff
515 files changed
+21066 -3637

No files matched your search

@@ -90,6 +90,9 @@ const parameters = (
input: AnalyzeJavaScriptApplicationInput,
): EvidenceObservation["parameters"] => ({
input_path: input.input_path,
...(input.semantic_precision === undefined
? {}
: { semantic_precision: input.semantic_precision }),
format: input.format,
integrity_policy: input.integrity_policy,
...(input.max_semantic_nodes === undefined
@@ -3,6 +3,7 @@ import {
ASAR_HEADER_ADMISSION_SETTING,
parseAsarHeaderAdmission,
} from "../../config/asarHeaderAdmission.js";
import { parseAsarListingAdmission } from "../../config/asarListingAdmission.js";
import { z } from "zod";
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
@@ -69,12 +70,22 @@ export const analyzeJavaScriptApplicationValidated = async (
options.environment?.[ASAR_HEADER_ADMISSION_SETTING],
);
if (!asarHeaderAdmission.ok) return asarHeaderAdmission;
const listingAdmission = parseAsarListingAdmission(
options.environment?.REA_ASAR_MAX_LISTING_BYTES,
);
if (!listingAdmission.ok) return listingAdmission;
const overrides = options.settings?.().overrides;
const header = overrides?.["asar.max_header_bytes"];
const acceptedAdmission =
header === undefined
const listing = overrides?.["asar.max_listing_bytes"];
const acceptedAdmission = {
...(header === undefined
? asarHeaderAdmission.value
: { maximumBytes: header, source: "runtime" as const };
: { maximumBytes: header, source: "runtime" as const }),
listing:
listing === undefined
? listingAdmission.value
: { maximumBytes: listing, source: "runtime" as const },
};
let reconstructed: JavaScriptArtifactReconstructionResult | undefined;
let result: JavaScriptApplicationAnalysisResult | undefined;
let evidence: Evidence | undefined;
@@ -317,7 +317,9 @@ const graphCoverage = (context: JavaScriptArtifactGraphContext) => {
const limits = [
...new Map(
[
...semanticResourceLimitCoverage(resourceLimits),
...semanticResourceBounds(context).flatMap(({ limit, precision }) =>
semanticResourceLimitCoverage([limit], precision),
),
...executionLimits,
...(inlineScriptsTruncated
? [HTML_INLINE_SCRIPT_LOCATION_COVERAGE_LIMIT]
@@ -340,17 +342,24 @@ const graphCoverage = (context: JavaScriptArtifactGraphContext) => {
return completeApplicationCoverage();
};
const semanticResourceBounds = (context: JavaScriptArtifactGraphContext) => [
...new Map(
context.analysis.files.flatMap(({ semantic }) =>
semantic === null
? []
: semanticCoverageResourceLimits(semantic.ir.coverage).map((limit) => {
const fact = { limit, precision: semantic.ir.coverage.precision };
return [JSON.stringify(fact), fact] as const;
}),
),
).values(),
];
const semanticResourceLimits = (
context: JavaScriptArtifactGraphContext,
): JavaScriptSemanticResourceLimit[] =>
[
...new Set(
context.analysis.files.flatMap(({ semantic }) =>
semantic === null
? []
: semanticCoverageResourceLimits(semantic.ir.coverage),
),
),
...new Set(semanticResourceBounds(context).map(({ limit }) => limit)),
].sort();
const graphLimitations = (
@@ -426,7 +435,9 @@ const graphLimitations = (
"Sender, frame, URL, and origin checks are observations only; REA does not claim that they enforce a complete authorization policy.",
]
: []),
...semanticResourceLimits(context).map(semanticResourceLimitReason),
...semanticResourceBounds(context).map(({ limit, precision }) =>
semanticResourceLimitReason(limit, precision),
),
...(context.analysis.files.some(
({ javascript }) =>
(javascript?.electron.native_addon_bindings.length ?? 0) > 0,
@@ -182,6 +182,9 @@ export const reconstructJavaScriptArtifact = async (
: { fileNodes: input.max_semantic_nodes_per_file }),
});
const sourceLimits = {
...(input.semantic_precision === undefined
? {}
: { semanticPrecision: input.semantic_precision }),
heapMb:
input.max_heap_mb ??
overrides?.["javascript.max_heap_mb"] ??
@@ -518,7 +518,11 @@ const relationshipEvidence = (
limitations: [
...input.semantic.ir.limitations,
...semanticCoverageResourceLimits(input.semantic.ir.coverage).map(
semanticResourceLimitReason,
(limit) =>
semanticResourceLimitReason(
limit,
input.semantic.ir.coverage.precision,
),
),
...limitations,
],
@@ -531,7 +535,10 @@ const semanticCoverage = (
if (semantic.ir.coverage.status === "complete" && resourceLimits.length === 0)
return completeApplicationCoverage();
return partialApplicationCoverage(
semanticResourceLimitCoverage(resourceLimits),
semanticResourceLimitCoverage(
resourceLimits,
semantic.ir.coverage.precision,
),
semantic.ir.coverage.omittedCount,
);
};
@@ -723,15 +723,17 @@ const projectFileResourceLimits = (
family: "data-flow",
relation_kinds: ["defines"],
reason: "resource-limit",
detail: semanticResourceLimitReason(limit),
detail: semanticResourceLimitReason(limit, context.ir.coverage.precision),
candidate_node_ids: [],
evidence: {
...unknownSemanticEvidence(context.file, null),
coverage: partialApplicationCoverage(
semanticResourceLimitCoverage([limit]),
semanticResourceLimitCoverage([limit], context.ir.coverage.precision),
null,
),
limitations: [semanticResourceLimitReason(limit)],
limitations: [
semanticResourceLimitReason(limit, context.ir.coverage.precision),
],
},
});
}
@@ -859,15 +861,17 @@ function* createCallSiteNodes(
family: "call-flow",
relation_kinds: ["argument-to-parameter"],
reason: "resource-limit",
detail: semanticResourceLimitReason(limit),
detail: semanticResourceLimitReason(limit, ir.coverage.precision),
candidate_node_ids: [],
evidence: {
...unknownSemanticEvidence(file, call.location),
coverage: partialApplicationCoverage(
semanticResourceLimitCoverage([limit]),
semanticResourceLimitCoverage([limit], ir.coverage.precision),
null,
),
limitations: [semanticResourceLimitReason(limit)],
limitations: [
semanticResourceLimitReason(limit, ir.coverage.precision),
],
},
});
}
@@ -42,7 +42,7 @@ it("cancels actual call-path work while its first seed indexes a long block", as
).rejects.toMatchObject({ reason: "cancelled", cause: reason });
expect(work.next()).toEqual({ done: true, value: undefined });
expect(accumulator.ipc).toEqual([]);
expect(accumulator.electronCallPathLimitReached).toBeUndefined();
expect(accumulator.electronCallPathLimits).toBeUndefined();
// A new analysis owns a new index and can still complete the same source.
const completed = createJavaScriptAnalysisAccumulator(source.length);
@@ -57,5 +57,5 @@ it("cancels actual call-path work while its first seed indexes a long block", as
),
),
).toContainEqual(expect.objectContaining({ entry: "first", conditions: [] }));
expect(completed.electronCallPathLimitReached).toBeUndefined();
expect(completed.electronCallPathLimits).toBeUndefined();
});