mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(browser): match exact WebMCP receiver segments
Inspect AST property segments instead of flattened suffixes. Reject dotted-key and suffix collisions while retaining page-declared trust and source evidence through the CLI. Refs: #4205, #4279 Co-authored-by: Geekwolf <geekwolf@163.com>
This commit is contained in:
@@ -1161,6 +1161,27 @@ it("retains direct and literal computed call names", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("requires WebMCP receiver and method names to occupy exact member segments", () => {
|
||||
const result = analyzeCapturedWebBundle(
|
||||
inspection(`
|
||||
navigator.modelContext.registerTool({ name: "navigator" });
|
||||
local.modelContext.registerTool({ name: "exact nested" });
|
||||
const appmodelContext = { registerTool() {} };
|
||||
appmodelContext.registerTool({ name: "receiver suffix" });
|
||||
const app = { analyticsmodelContext: { registerTool() {} } };
|
||||
app.analyticsmodelContext.registerTool({ name: "nested suffix" });
|
||||
window["modelContext.registerTool"]({ name: "dotted property" });
|
||||
window["modelContext"].registerTool({ name: "computed receiver" });
|
||||
`),
|
||||
);
|
||||
|
||||
expect(result.observations.webmcp_declarations).toEqual([
|
||||
expect.objectContaining({ name: "navigator" }),
|
||||
expect.objectContaining({ name: "exact nested" }),
|
||||
expect.objectContaining({ name: "computed receiver" }),
|
||||
]);
|
||||
});
|
||||
|
||||
it("recognizes tRPC procedures and named GraphQL operations in captured scripts", () => {
|
||||
const result = analyzeCapturedWebBundle(
|
||||
inspection(`
|
||||
|
||||
@@ -11,6 +11,7 @@ import type { JavaScriptOpenReceiverFact } from "./javascript/javascriptSemantic
|
||||
import {
|
||||
semanticStaticPropertyKey,
|
||||
semanticStaticPropertyName,
|
||||
unwrapJavaScriptExpression,
|
||||
} from "./javascript/javascriptAstValues.js";
|
||||
import {
|
||||
calleeName,
|
||||
@@ -208,13 +209,31 @@ const inspectCall = (
|
||||
node,
|
||||
accumulator,
|
||||
});
|
||||
if (
|
||||
name.endsWith("modelContext.registerTool") ||
|
||||
name === "modelContext.registerTool"
|
||||
)
|
||||
if (isWebMcpRegistrationCallee(node.callee))
|
||||
addWebMcpDeclaration(script, node, accumulator);
|
||||
};
|
||||
|
||||
/** Match exact WebMCP receiver and method properties without flattening keys. */
|
||||
const isWebMcpRegistrationCallee = (callee: t.Node): boolean => {
|
||||
const method = unwrapJavaScriptExpression(callee).node;
|
||||
if (!t.isMemberExpression(method) && !t.isOptionalMemberExpression(method))
|
||||
return false;
|
||||
if (
|
||||
semanticStaticPropertyKey(method.property, method.computed) !==
|
||||
"registerTool"
|
||||
)
|
||||
return false;
|
||||
|
||||
const receiver = unwrapJavaScriptExpression(method.object).node;
|
||||
if (t.isIdentifier(receiver, { name: "modelContext" })) return true;
|
||||
return (
|
||||
(t.isMemberExpression(receiver) ||
|
||||
t.isOptionalMemberExpression(receiver)) &&
|
||||
semanticStaticPropertyKey(receiver.property, receiver.computed) ===
|
||||
"modelContext"
|
||||
);
|
||||
};
|
||||
|
||||
const inspectRouteProperty = (
|
||||
script: IncludedScript,
|
||||
node: t.ObjectProperty,
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { expect } from "vitest";
|
||||
|
||||
import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
|
||||
cliTest(
|
||||
"keeps WebMCP receiver segments exact through the public CLI",
|
||||
async ({ cli }) => {
|
||||
const source = `
|
||||
navigator.modelContext.registerTool({ name: "navigator" });
|
||||
const app = { analyticsmodelContext: { registerTool() {} } };
|
||||
app.analyticsmodelContext.registerTool({ name: "nested suffix" });
|
||||
window["modelContext.registerTool"]({ name: "dotted property" });
|
||||
`;
|
||||
const browser = await startFakeCdpBrowser({
|
||||
commandResult: ({ method }) =>
|
||||
method === "Debugger.getScriptSource"
|
||||
? { scriptSource: source }
|
||||
: undefined,
|
||||
});
|
||||
try {
|
||||
const result = await cli.run({
|
||||
arguments: [
|
||||
"analyze-web-bundle",
|
||||
browser.endpoint,
|
||||
"allowed-page",
|
||||
"--allowed-origins",
|
||||
browser.allowedOrigin,
|
||||
"--observation-ms",
|
||||
"0",
|
||||
"--json",
|
||||
],
|
||||
});
|
||||
expect(result.exitCode, result.stdout).toBe(0);
|
||||
expect(result.json).toMatchObject({
|
||||
normalized_result: {
|
||||
observations: {
|
||||
webmcp_declarations: [
|
||||
expect.objectContaining({
|
||||
name: "navigator",
|
||||
trust: "page-declared-untrusted",
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const inspected = await cli.run({
|
||||
arguments: [
|
||||
"inspect-web-page",
|
||||
browser.endpoint,
|
||||
"allowed-page",
|
||||
"--allowed-origins",
|
||||
browser.allowedOrigin,
|
||||
"--observation-ms",
|
||||
"0",
|
||||
"--include-script-sources",
|
||||
"--json",
|
||||
],
|
||||
});
|
||||
expect(inspected.exitCode, inspected.stdout).toBe(0);
|
||||
expect(inspected.json).toMatchObject({
|
||||
normalized_result: {
|
||||
scripts: {
|
||||
items: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
source: expect.objectContaining({
|
||||
artifact: expect.objectContaining({ text: source }),
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(browser.activeConnectionCount()).toBe(0);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user