feat(mcp): derive an API surface view from retained web network captures (#1891)

* feat(mcp): derive an API surface view from retained web network captures

* fix(mcp): fail closed on sensitive parents and budget api-surface path expansion

* fix(mcp): keep recorded api-surface paths and budget only templated segments

* fix(mcp): read api-surface paths only from canonical URLs and charge new values once

* fix(mcp): reject authority-less api-surface URLs and key groups by rule positions

* fix(mcp): label api-surface confidence from all groups and cap transient path memory

* fix(mcp): budget api-surface dedup sets and narrow detail outputs

Charge retained deduplication sets before allocation, advertise only
each analysis operation's summary from its detail output, and document
that any templated group makes every API-surface page inferred.

* fix(mcp): bound api-surface path text and record limitations

Charge retained literal path copies before allocation, publish only
valid media-type essences, and deduplicate examined record limitations
onto the view.

* fix(mcp): keep api-surface refusals and path splits inside the reply budget

Sensitive-parent refusals now use the token sensitive_values_unretained so declared literals are not echoed. A later path split is charged against bytes already retained, and the published example includes the HAR record limitation with its matching digest.

* fix(mcp): budget api-surface method text before group keys

Charge method and origin text against the reply budget before the group key is serialized. Document that samples merge only when method, origin, and exact segment rules match.

* fix(mcp): bound api-surface copies and control-character authorities

Reject authorities containing a tab or newline so WHATWG cannot move userinfo into the recorded path. Charge four path copies, two method and origin copies, and each distinct content type against bytes already retained.

* fix(mcp): preflight combined api-surface group charges

Total path, method, origin, and segment charges before retaining a new group. Track content types in a set so distinct MIME essences are not compared quadratically.

* fix(mcp): charge api-surface content-type sets on first use

Create the per-group content-type set when the first essence is retained, and charge it before allocation. Groups with no content type no longer keep an empty set.

* refactor(analysis): centralize API-surface ownership and admission

Own grouping, deduplication, and combined retained/temporary resource admission in one accumulator. Charge fixed group storage before allocation and materialize only the requested page.

Share recorded URL parsing between capture redaction and projection, preserving source paths and excluding ambiguous credential fields. Cover resource admission invariants and the compiled HAR-to-view workflow.

* fix(analysis): admit capture schema clone before parsing

* fix(javascript): share Electron mutation proof with wrapped calls

* test(mcp): align graph and catalog checks with current contracts

* fix(analysis): admit and validate portable capture views

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-10-12 10:44:17 +08:00
1 parent 64b71b180a
commit 880a603fe8
34 files changed
+2533 -189

No files matched your search

@@ -378,9 +378,24 @@ export const classifyParsedJavaScriptElectronBindings = (
export function* classifyParsedJavaScriptElectronBindingsSteps(
file: ParsedJavaScriptSource,
): Generator<void, ReadonlyMap<number, readonly string[]>> {
const facts = new Map<number, readonly string[]>();
const state = createState(file.program);
yield* collectDefinitionsSteps(file.program, state);
return (yield* indexJavaScriptElectronIdentitySteps(file.program, state))
.bindings;
}
/** Shared lexical identity and visible-write proof for direct and wrapped calls. */
export interface JavaScriptElectronIdentity {
readonly bindings: ReadonlyMap<number, readonly string[]>;
readonly intactExport: (path: readonly (string | null)[]) => boolean;
readonly exportPath: (root: t.Node) => readonly string[] | undefined;
}
export function* indexJavaScriptElectronIdentitySteps(
program: t.Program,
state: JavaScriptSemanticAnalysisState,
): Generator<void, JavaScriptElectronIdentity> {
const facts = new Map<number, readonly string[]>();
const interop = createSemanticModuleInterop(state);
const writes = new SemanticPropertyPathCoverage();
let collectedWrites = false;
@@ -411,7 +426,7 @@ export function* classifyParsedJavaScriptElectronBindingsSteps(
: undefined;
};
yield* collectJavaScriptIdentityWritesSteps(
file.program,
program,
(root, kind) => {
if (kind === "opaque-call") return interop.opaqueMutationPath(root);
const exported = kind === "binding" ? undefined : rootExport(root);
@@ -431,7 +446,10 @@ export function* classifyParsedJavaScriptElectronBindingsSteps(
},
);
collectedWrites = true;
yield* traverseJavaScriptAstSteps(file.program, {
const intactExport = (path: readonly (string | null)[]): boolean =>
ELECTRON_MODULE.test(path[0] ?? "") &&
!writes.covers(["electron", ...path.slice(1)]);
yield* traverseJavaScriptAstSteps(program, {
enter: (node) => {
if (
!t.isCallExpression(node) &&
@@ -443,12 +461,12 @@ export function* classifyParsedJavaScriptElectronBindingsSteps(
const exported = rootExport(root);
if (
exported !== undefined &&
!writes.covers(["electron", ...exported, ...members])
intactExport(["electron", ...exported, ...members])
)
facts.set(root.start ?? -1, exported);
},
});
return facts;
return { bindings: facts, intactExport, exportPath: rootExport };
}
// Read import and require origins directly; general value evaluation is