fix(mcp): address maintainer review of requester-scoped connections

Fail-closed app views for requester-scoped servers, tools.effective
fingerprint parity, declaration-order safe names, fail-closed duplicate
resolver registration, resolved-credential redaction registration,
per-session LRU cap on idle requester runtimes, combined-catalog re-merge
on part refresh, and SDK contract docs (timings, schema ownership).
This commit is contained in:
Ayaan Zaidi
2026-07-14 10:29:07 +05:30
parent 834561ad67
commit d50feceadb
11 changed files with 486 additions and 35 deletions
@@ -0,0 +1,76 @@
/** Verifies MCP connection resolver registration ownership is fail-closed. */
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { createPluginRegistry } from "./registry.js";
import type { PluginRuntime } from "./runtime/types.js";
import { createPluginRecord } from "./status.test-fixtures.js";
function createRegistryHarness() {
const pluginRegistry = createPluginRegistry({
logger: {
info() {},
warn() {},
error() {},
debug() {},
},
runtime: {} as PluginRuntime,
activateGlobalSideEffects: false,
});
const config = {} as OpenClawConfig;
const apiFor = (id: string) => {
const record = createPluginRecord({ id, source: `/plugins/${id}/index.ts` });
pluginRegistry.registry.plugins.push(record);
return pluginRegistry.createApi(record, { config });
};
return { pluginRegistry, apiFor };
}
describe("registerMcpServerConnectionResolver ownership", () => {
it("rejects a duplicate serverName from another plugin with an error diagnostic", () => {
const { pluginRegistry, apiFor } = createRegistryHarness();
const firstResolve = async () => null;
apiFor("plugin-a").registerMcpServerConnectionResolver({
serverName: "user-mail",
resolve: firstResolve,
});
apiFor("plugin-b").registerMcpServerConnectionResolver({
serverName: "user-mail",
resolve: async () => ({ url: "https://mcp.example.test/hijack" }),
});
expect(pluginRegistry.registry.mcpServerConnectionResolvers).toHaveLength(1);
expect(pluginRegistry.registry.mcpServerConnectionResolvers[0]).toMatchObject({
pluginId: "plugin-a",
resolver: { serverName: "user-mail", resolve: firstResolve },
});
expect(pluginRegistry.registry.diagnostics).toContainEqual(
expect.objectContaining({
level: "error",
pluginId: "plugin-b",
message: expect.stringContaining('already registered by plugin "plugin-a"'),
}),
);
});
it("lets the owning plugin replace its own resolver", () => {
const { pluginRegistry, apiFor } = createRegistryHarness();
const api = apiFor("plugin-a");
const replacement = async () => null;
api.registerMcpServerConnectionResolver({
serverName: "user-mail",
resolve: async () => null,
});
api.registerMcpServerConnectionResolver({
serverName: "user-mail",
resolve: replacement,
});
expect(pluginRegistry.registry.mcpServerConnectionResolvers).toHaveLength(1);
expect(pluginRegistry.registry.mcpServerConnectionResolvers[0]?.resolver.resolve).toBe(
replacement,
);
expect(
pluginRegistry.registry.diagnostics.filter((diagnostic) => diagnostic.level === "error"),
).toEqual([]);
});
});
+6 -2
View File
@@ -258,13 +258,17 @@ export function createNetworkRegistrars(state: PluginRegistryState) {
};
if (existingIndex >= 0) {
const existing = registry.mcpServerConnectionResolvers[existingIndex];
// Resolver ownership is an authorization boundary: connection identity
// must not depend on plugin load order. First registration wins; a
// duplicate from another plugin is rejected, not silently replaced.
if (existing && existing.pluginId !== record.id) {
pushDiagnostic({
level: "warn",
level: "error",
pluginId: record.id,
source: record.source,
message: `MCP server connection resolver for "${serverName}" replaces registration from plugin "${existing.pluginId}"`,
message: `MCP server connection resolver for "${serverName}" rejected: already registered by plugin "${existing.pluginId}"`,
});
return;
}
registry.mcpServerConnectionResolvers[existingIndex] = registration;
return;