fix(browser): preserve unknown WebMCP comparison facts (#7833)

This commit is contained in:
morluto authored and GitHub committed 2026-10-29 05:09:22 +08:00
1 parent 6c76a7ffb0
commit cc5a4062ff
2 files changed
+199 -31

No files matched your search

+122 -31
View File
@@ -827,7 +827,6 @@ const webMcpDimension = (
before: WebMcpDiscovery | null,
after: WebMcpDiscovery | null,
): Dimension => {
type Tool = WebMcpDiscovery["tools"]["items"][number];
const groups = (discovery: WebMcpDiscovery | null) => {
const result = new Map<string, Tool[]>();
for (const tool of discovery?.tools.items ?? []) {
@@ -875,41 +874,21 @@ const webMcpDimension = (
}
continue;
}
const beforeTool = beforeTools[0];
const afterTool = afterTools[0];
const consequentialKnown =
beforeTool?.annotations.consequential !== undefined &&
afterTool?.annotations.consequential !== undefined;
if (
beforeTool !== undefined &&
afterTool !== undefined &&
!consequentialKnown
)
valuesComplete = false;
const project = (tool: Tool) => {
const {
tool_key: _key,
frame_id: _frameId,
annotations,
...content
} = tool;
const { consequential, ...otherAnnotations } = annotations;
return digestCanonicalValue({
...content,
annotations: {
...otherAnnotations,
...(consequentialKnown ? { consequential } : {}),
},
});
};
if (beforeTool !== undefined) left.set(identity, project(beforeTool));
if (afterTool !== undefined) right.set(identity, project(afterTool));
const projection = projectWebMcpTools(
beforeTools[0],
afterTools[0],
beforeComplete,
afterComplete,
);
if (!projection.complete) valuesComplete = false;
if (projection.before !== undefined) left.set(identity, projection.before);
if (projection.after !== undefined) right.set(identity, projection.after);
}
return compareDimension(
left,
right,
inventoryComplete && valuesComplete,
"WebMCP discovery, owner correspondence, or consequential annotation evidence was unavailable or incomplete in at least one capture.",
"WebMCP discovery, owner correspondence, or declaration evidence was unavailable or incomplete in at least one capture.",
(identity) =>
!previous.has(identity)
? beforeComplete
@@ -919,6 +898,118 @@ const webMcpDimension = (
);
};
type Tool = WebMcpDiscovery["tools"]["items"][number];
/** Compare only declaration facts retained by both captures. */
const projectWebMcpTools = (
beforeTool: Tool | undefined,
afterTool: Tool | undefined,
beforeComplete: boolean,
afterComplete: boolean,
): {
before: string | undefined;
after: string | undefined;
complete: boolean;
} => {
let valuesComplete = true;
const inventoryComplete = beforeComplete && afterComplete;
const paired = beforeTool !== undefined && afterTool !== undefined;
const consequentialKnown =
beforeTool?.annotations.consequential !== undefined &&
afterTool?.annotations.consequential !== undefined;
if (
beforeTool !== undefined &&
afterTool !== undefined &&
!consequentialKnown
)
valuesComplete = false;
// A missing stack or malformed coordinate is not a different source.
// Compare only jointly observed components, but retain unknown coverage.
const beforeSource = beforeTool?.registration_source;
const afterSource = afterTool?.registration_source;
const sourcePresent = beforeSource != null && afterSource != null;
const bothAbsent =
beforeSource === null && afterSource === null && inventoryComplete;
const sourceKnown = sourcePresent || bothAbsent;
const lineKnown =
sourcePresent && beforeSource.line !== null && afterSource.line !== null;
const columnKnown =
sourcePresent &&
beforeSource.column !== null &&
afterSource.column !== null;
if (
paired &&
(!sourceKnown || (sourcePresent && (!lineKnown || !columnKnown)))
)
valuesComplete = false;
const schemaKnown =
!paired ||
((beforeComplete || beforeTool.input_schema_sha256 !== null) &&
(afterComplete || afterTool.input_schema_sha256 !== null));
const annotationKeys = [
"read_only",
"untrusted_content",
"autosubmit",
] as const;
const knownAnnotations = new Set(
annotationKeys.filter(
(key) =>
!paired ||
((beforeComplete || beforeTool.annotations[key] !== null) &&
(afterComplete || afterTool.annotations[key] !== null)),
),
);
if (
paired &&
(!schemaKnown || knownAnnotations.size !== annotationKeys.length)
)
valuesComplete = false;
const project = (tool: Tool) => {
const {
tool_key: _key,
frame_id: _frameId,
annotations,
registration_source: source,
input_schema: schema,
input_schema_sha256: schemaDigest,
...content
} = tool;
return digestCanonicalValue({
...content,
...(schemaKnown
? { input_schema: schema, input_schema_sha256: schemaDigest }
: {}),
...(sourceKnown
? {
registration_source:
source === null
? null
: {
url: source.url,
...(lineKnown ? { line: source.line } : {}),
...(columnKnown ? { column: source.column } : {}),
},
}
: {}),
annotations: {
...Object.fromEntries(
annotationKeys
.filter((key) => knownAnnotations.has(key))
.map((key) => [key, annotations[key]]),
),
...(consequentialKnown
? { consequential: annotations.consequential }
: {}),
},
});
};
return {
before: beforeTool === undefined ? undefined : project(beforeTool),
after: afterTool === undefined ? undefined : project(afterTool),
complete: valuesComplete,
};
};
const webMcpComplete = (discovery: WebMcpDiscovery | null): boolean =>
discovery !== null &&
discovery.status === "available" &&
@@ -251,6 +251,81 @@ add(
"webmcp",
"changed",
);
const knownSource = structuredClone(discovery);
knownSource.tools.items[0]!.registration_source = {
url: "https://app.example.test/app.js",
line: 12,
column: 3,
};
const malformedSource = structuredClone(discovery);
malformedSource.completeness.unavailable_sections.push("webmcp_tools");
malformedSource.completeness.excluded.push({
section: "webmcp_tools",
reason: "invalid_protocol_value",
count: 1,
});
for (const [name, before, after] of [
["WebMCP unknown registration source", knownSource, malformedSource],
["WebMCP unknown registration source reversed", malformedSource, knownSource],
] as const)
add(
name,
{ inspection, webmcp: before },
{ inspection, webmcp: after },
"webmcp",
"unknown",
);
const malformedChanged = structuredClone(malformedSource);
malformedChanged.tools.items[0]!.description = "Changed description";
add(
"WebMCP known description change despite unknown source",
{ inspection, webmcp: knownSource },
{ inspection, webmcp: malformedChanged },
"webmcp",
"changed",
);
const otherSource = structuredClone(knownSource);
otherSource.tools.items[0]!.registration_source!.line = 13;
add(
"WebMCP known registration source change",
{ inspection, webmcp: knownSource },
{ inspection, webmcp: otherSource },
"webmcp",
"changed",
);
const missingCoordinate = structuredClone(knownSource);
missingCoordinate.tools.items[0]!.registration_source!.line = null;
add(
"WebMCP unknown registration coordinate",
{ inspection, webmcp: knownSource },
{ inspection, webmcp: missingCoordinate },
"webmcp",
"unknown",
);
const malformedAnnotation = structuredClone(discovery);
malformedAnnotation.completeness = structuredClone(
malformedSource.completeness,
);
malformedAnnotation.tools.items[0]!.annotations.read_only = null;
add(
"WebMCP malformed annotation",
{ inspection, webmcp: discovery },
{ inspection, webmcp: malformedAnnotation },
"webmcp",
"unknown",
);
const knownSchema = structuredClone(discovery);
knownSchema.tools.items[0]!.input_schema = { type: "object" };
knownSchema.tools.items[0]!.input_schema_sha256 = "b".repeat(64);
const malformedSchema = structuredClone(discovery);
malformedSchema.completeness = structuredClone(malformedSource.completeness);
add(
"WebMCP malformed schema",
{ inspection, webmcp: knownSchema },
{ inspection, webmcp: malformedSchema },
"webmcp",
"unknown",
);
const droppedTools = structuredClone(discovery);
droppedTools.completeness = withPassiveDroppedEvents(inspection, {
webmcp_tools: 1,
@@ -413,6 +488,8 @@ const cliCaseNames = new Set([
"observed HTTP value absent from complete peer",
"same-length partial WebSocket streams",
"WebMCP capture-local identity renamed",
"WebMCP unknown registration source",
"WebMCP known description change despite unknown source",
"script retained digest change without producer hash",
"script digest unavailable",
]);