diff --git a/apps/desktop/src/main/runtime-host-boot.ts b/apps/desktop/src/main/runtime-host-boot.ts index 69c98b496c..949ad70fcc 100644 --- a/apps/desktop/src/main/runtime-host-boot.ts +++ b/apps/desktop/src/main/runtime-host-boot.ts @@ -64,7 +64,8 @@ import { openRuntimeHostPeerEndpointOwner, type RuntimeHostPeerEndpointOwner, } from '@maka/runtime-host/peer-reachability'; -import { clientCapabilityEntityId, type WorkspaceTarget } from "@maka/runtime-host/protocol"; +import { clientCapabilityEntityId } from "@maka/runtime-host/capability-entity-id"; +import { type WorkspaceTarget } from "@maka/runtime-host/protocol"; import { runtimeHostProfileUsesHostWorkspace } from "@maka/runtime-host/profile-kind"; import { createCredentialMcpOAuthStorage, McpClientManager } from "@maka/mcp"; import { createWorkBoardStore } from "@maka/storage/work-board-store"; diff --git a/apps/desktop/src/main/runtime-host-native-capabilities.ts b/apps/desktop/src/main/runtime-host-native-capabilities.ts index ad6698a2b9..6f9fdd436b 100644 --- a/apps/desktop/src/main/runtime-host-native-capabilities.ts +++ b/apps/desktop/src/main/runtime-host-native-capabilities.ts @@ -30,7 +30,6 @@ import { CLIENT_CAPABILITY_MAX_OFFERS, CLIENT_CAPABILITY_MAX_TOOLS, CLIENT_CAPABILITY_MAX_TOOLS_PER_OFFER, - clientCapabilityEntityId, decodeClientCapabilityReplaceInput, decodeClientCapabilityToolDescriptor, type ClientCapabilityCallFrame, @@ -42,6 +41,7 @@ import { type ClientCapabilityServiceOffer, type ClientCapabilityToolDescriptor, } from "@maka/runtime-host/protocol"; +import { clientCapabilityEntityId } from "@maka/runtime-host/capability-entity-id"; import { toJSONSchema, z } from "zod"; import { withBrowserOriginAdmission } from './browser/browser-origin-admission.js'; import type { DesktopTargetScope } from '../shared/runtime-host-identity.js'; diff --git a/packages/cli/src/mcp-capability-provider.ts b/packages/cli/src/mcp-capability-provider.ts index 99c2fae840..79a990265b 100644 --- a/packages/cli/src/mcp-capability-provider.ts +++ b/packages/cli/src/mcp-capability-provider.ts @@ -24,11 +24,11 @@ import type { ClientCapabilityProvider } from '@maka/runtime-host/client'; import { CLIENT_CAPABILITY_MAX_TOOLS, CLIENT_CAPABILITY_MAX_TOOLS_PER_OFFER, - clientCapabilityEntityId, decodeClientCapabilityReplaceInput, type ClientCapabilityCallResult, type ClientCapabilityOffer, } from '@maka/runtime-host/protocol'; +import { clientCapabilityEntityId } from '@maka/runtime-host/capability-entity-id'; import type { McpCallResult, McpToolDescriptor } from '@maka/core/mcp'; const CAPABILITY_VERSION = '0'; diff --git a/packages/runtime-host/package.json b/packages/runtime-host/package.json index 75a7a3f099..80950e4011 100644 --- a/packages/runtime-host/package.json +++ b/packages/runtime-host/package.json @@ -15,6 +15,7 @@ "./operator": "./dist/operator/index.js", "./operator/update-package-evidence": "./dist/operator/update-package-evidence.js", "./profile-kind": "./dist/profile-kind.js", + "./capability-entity-id": "./dist/capability-entity-id.js", "./execution-candidate-main": "./dist/execution-candidate-main.js", "./server": "./dist/server/index.js", "./test-only/client-capability-host": "./dist/test-only/client-capability-host.js", diff --git a/packages/runtime-host/src/__tests__/capability-entity-id.test.ts b/packages/runtime-host/src/__tests__/capability-entity-id.test.ts new file mode 100644 index 0000000000..cc89778133 --- /dev/null +++ b/packages/runtime-host/src/__tests__/capability-entity-id.test.ts @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { clientCapabilityEntityId } from '../capability-entity-id.js'; + +test('passes an already wire-safe identity through unchanged', () => { + assert.equal(clientCapabilityEntityId('my-server-01'), 'my-server-01'); + assert.equal(clientCapabilityEntityId('my_server_01'), 'my_server_01'); +}); + +test('normalizes spaces and punctuation into a readable label plus digest', () => { + const id = clientCapabilityEntityId('My Server #01'); + assert.match(id, /^My_Server_01_[0-9a-f]{24}$/u); +}); + +test('truncates an over-long identity to a bounded label with a digest', () => { + const value = 'x'.repeat(300); + const id = clientCapabilityEntityId(value); + assert.ok(id.length <= 128, `expected <=128, got ${id.length}`); + assert.match(id, /_[0-9a-f]{24}$/u); +}); + +test('distinct over-long values produce distinct digests', () => { + const a = clientCapabilityEntityId('a'.repeat(300)); + const b = clientCapabilityEntityId('b'.repeat(300)); + assert.notEqual(a, b); +}); + +test('honors an explicit max length', () => { + const id = clientCapabilityEntityId('server name here', 40); + assert.ok(id.length <= 40, `expected <=40, got ${id.length}`); +}); diff --git a/packages/runtime-host/src/capability-entity-id.ts b/packages/runtime-host/src/capability-entity-id.ts new file mode 100644 index 0000000000..5d71402b26 --- /dev/null +++ b/packages/runtime-host/src/capability-entity-id.ts @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/** + * Node-only helper for turning an MCP server/tool identity into a wire-safe + * capability entity id. + * + * This lives outside `protocol/` on purpose: `clientCapabilityEntityId` uses + * `node:crypto`, and the protocol barrel is re-exported by the Desktop + * renderer's startup graph. A value import from the barrel evaluates every + * `export *` module in the browser, so a Node builtin anywhere in that + * closure silently breaks `vite dev`. Keeping the hashing helper in its own + * Node-only module — the same shape as `profile-kind` — lets the renderer keep + * importing browser-safe values from the barrel without ever loading this one. + */ + +import { createHash } from 'node:crypto'; + +export function clientCapabilityEntityId(value: string, maxLength = 128): string { + if (/^[A-Za-z0-9_-]+$/u.test(value) && value.length <= maxLength) return value; + const label = value.replace(/[^A-Za-z0-9_-]+/gu, '_').slice(0, maxLength - 25) || 'mcp'; + const digest = createHash('sha256').update(value).digest('hex').slice(0, 24); + return `${label}_${digest}`; +} diff --git a/packages/runtime-host/src/protocol/client-capability.ts b/packages/runtime-host/src/protocol/client-capability.ts index 9e9c43c535..924cf66462 100644 --- a/packages/runtime-host/src/protocol/client-capability.ts +++ b/packages/runtime-host/src/protocol/client-capability.ts @@ -17,7 +17,6 @@ * under the License. */ -import { createHash } from 'node:crypto'; import { TOOL_ACTIVITY_KINDS, type ToolActivityKind } from '@maka/core/events'; import { decodeInteractionAnswer, @@ -85,18 +84,6 @@ export const CLIENT_CAPABILITY_MAX_OFFERS = 32; export const CLIENT_CAPABILITY_MAX_SERVICES = 32; export const CLIENT_CAPABILITY_MAX_TOOLS_PER_OFFER = 64; -/** - * Normalize an arbitrary Client Capability identity (an MCP server id or tool - * name from user configuration) into a wire-safe entity id: identities that - * already fit pass through unchanged, anything else becomes a readable label - * plus a collision-proof digest of the original value. - */ -export function clientCapabilityEntityId(value: string, maxLength = 128): string { - if (/^[A-Za-z0-9_-]+$/u.test(value) && value.length <= maxLength) return value; - const label = value.replace(/[^A-Za-z0-9_-]+/gu, '_').slice(0, maxLength - 25) || 'mcp'; - const digest = createHash('sha256').update(value).digest('hex').slice(0, 24); - return `${label}_${digest}`; -} export const CLIENT_CAPABILITY_MAX_TOOLS = 256; export const CLIENT_CAPABILITY_MAX_MANIFEST_BYTES = 56 * 1024; export const CLIENT_CAPABILITY_MAX_RESULT_BYTES = 24 * 1024 * 1024;