You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by zh...@apache.org on 2026/08/29 15:59:46 UTC

(maka) 02/02: feat(runtime-host): select providers by Client owner

This is an automated email from the ASF dual-hosted git repository.

me2seeks pushed a commit to branch feat/3838-mcp-provider-association
in repository https://gitbox.apache.org/repos/asf/maka.git

commit 4f4b159fd450cae7d512ab68d8134a7fb0419be9
Author: me2seeks <me...@gmail.com>
AuthorDate: Sat Aug 29 23:50:38 2026 +0800

    feat(runtime-host): select providers by Client owner
    
    Prefer the initiating Client's direct provider, then one exact Host-bound companion. Keep unrelated providers invisible to remote Clients and fail closed when more than one companion claims the same owner.
    
    Generated-by: Codex
---
 .../client-capability-coordinator.test.ts          | 130 +++++++++++++++++++++
 .../src/__tests__/fixtures/client-capability.ts    |   9 +-
 .../src/server/client-capability-coordinator.ts    | 108 ++++++++++++++---
 3 files changed, 227 insertions(+), 20 deletions(-)

diff --git a/packages/runtime-host/src/__tests__/client-capability-coordinator.test.ts b/packages/runtime-host/src/__tests__/client-capability-coordinator.test.ts
index b8537d82e..262acc2f7 100644
--- a/packages/runtime-host/src/__tests__/client-capability-coordinator.test.ts
+++ b/packages/runtime-host/src/__tests__/client-capability-coordinator.test.ts
@@ -301,6 +301,119 @@ describe('Host Client Capability coordinator', () => {
     await coordinator.close();
   });
 
+  test('selects only the provider bound to the exact initiating Client identity', async () => {
+    const coordinator = createCoordinator();
+    const owner = coordinator.attachConnection(
+      clientCapabilityConnectionIdentity(
+        'owner-connection',
+        'owner-client',
+        'owner-principal',
+        'remote_owner',
+      ),
+      { send: async () => {} },
+    );
+    const unrelated = coordinator.attachConnection(
+      clientCapabilityConnectionIdentity(
+        'unrelated-provider',
+        'unrelated-provider-client',
+        'unrelated-provider-principal',
+        'capability_provider',
+        { principalId: 'other-principal', clientInstanceId: 'other-client' },
+      ),
+      { send: async () => {} },
+    );
+    await replaceTrustedProvider(
+      coordinator,
+      'unrelated-provider',
+      'unrelated-registration',
+      'inspect',
+    );
+
+    assert.deepEqual(await coordinator.bindSession('unrelated-only', 'owner-connection'), {
+      ok: true,
+    });
+    assert.equal(coordinator.snapshotForSession('unrelated-only'), undefined);
+
+    const associated = coordinator.attachConnection(
+      clientCapabilityConnectionIdentity(
+        'associated-provider',
+        'associated-provider-client',
+        'associated-provider-principal',
+        'capability_provider',
+        { principalId: 'owner-principal', clientInstanceId: 'owner-client' },
+      ),
+      { send: async () => {} },
+    );
+    await replaceTrustedProvider(
+      coordinator,
+      'associated-provider',
+      'associated-registration',
+      'inspect',
+    );
+    assert.throws(
+      () =>
+        coordinator.attachConnection(
+          clientCapabilityConnectionIdentity(
+            'changed-owner-provider',
+            'associated-provider-client',
+            'associated-provider-principal',
+            'capability_provider',
+            { principalId: 'other-principal', clientInstanceId: 'other-client' },
+          ),
+          { send: async () => {} },
+        ),
+      /provider owner changed/u,
+    );
+
+    assert.deepEqual(await coordinator.bindSession('associated', 'owner-connection'), { ok: true });
+    const snapshot = coordinator.snapshotForSession('associated');
+    assert.deepEqual(snapshot?.registrationIds, ['associated-registration']);
+    snapshot?.release();
+
+    const otherClient = coordinator.attachConnection(
+      clientCapabilityConnectionIdentity(
+        'other-client-connection',
+        'different-client',
+        'owner-principal',
+        'remote_owner',
+      ),
+      { send: async () => {} },
+    );
+    assert.deepEqual(await coordinator.bindSession('different-client', 'other-client-connection'), {
+      ok: true,
+    });
+    assert.equal(coordinator.snapshotForSession('different-client'), undefined);
+
+    const duplicate = coordinator.attachConnection(
+      clientCapabilityConnectionIdentity(
+        'duplicate-provider',
+        'duplicate-provider-client',
+        'duplicate-provider-principal',
+        'capability_provider',
+        { principalId: 'owner-principal', clientInstanceId: 'owner-client' },
+      ),
+      { send: async () => {} },
+    );
+    await replaceTrustedProvider(
+      coordinator,
+      'duplicate-provider',
+      'duplicate-registration',
+      'inspect',
+    );
+    const ambiguous = await coordinator.bindSession('duplicate-owner', 'owner-connection');
+    assert.equal(ambiguous.ok, false);
+    if (!ambiguous.ok) assert.match(ambiguous.message, /bound to the initiating Client/u);
+
+    await Promise.all([
+      owner.close(),
+      unrelated.close(),
+      associated.close(),
+      otherClient.close(),
+      duplicate.close(),
+    ]);
+    await coordinator.close();
+  });
+
   test('previews the initiating provider without persisting a Session binding', async () => {
     const coordinator = createCoordinator();
     const connection = coordinator.attachConnection(
@@ -1190,6 +1303,23 @@ async function replace(
   assert.equal(outcome.ok, true);
 }
 
+async function replaceTrustedProvider(
+  coordinator: HostClientCapabilityCoordinator,
+  connectionId: string,
+  registrationId: string,
+  toolName: string,
+): Promise<void> {
+  const input = replacementInput(registrationId, toolName);
+  const outcome = await coordinator.handlers['client.capability.replace'](
+    {
+      ...input,
+      offers: input.offers.map((offer) => ({ ...offer, hostPathAccess: 'none' as const })),
+    },
+    connectionContext(connectionId),
+  );
+  assert.equal(outcome.ok, true);
+}
+
 function replacementInput(
   registrationId: string,
   toolName: string,
diff --git a/packages/runtime-host/src/__tests__/fixtures/client-capability.ts b/packages/runtime-host/src/__tests__/fixtures/client-capability.ts
index eb4ced75d..d743609b7 100644
--- a/packages/runtime-host/src/__tests__/fixtures/client-capability.ts
+++ b/packages/runtime-host/src/__tests__/fixtures/client-capability.ts
@@ -24,6 +24,13 @@ export function clientCapabilityConnectionIdentity(
   clientInstanceId = connectionId,
   principalId = 'test-principal',
   principalKind: ClientCapabilityConnectionIdentity['principalKind'] = 'local_owner',
+  capabilityOwner?: ClientCapabilityConnectionIdentity['capabilityOwner'],
 ): ClientCapabilityConnectionIdentity {
-  return { connectionId, principalId, clientInstanceId, principalKind };
+  return {
+    connectionId,
+    principalId,
+    clientInstanceId,
+    principalKind,
+    ...(capabilityOwner ? { capabilityOwner } : {}),
+  };
 }
diff --git a/packages/runtime-host/src/server/client-capability-coordinator.ts b/packages/runtime-host/src/server/client-capability-coordinator.ts
index 1e54236d9..f036185b5 100644
--- a/packages/runtime-host/src/server/client-capability-coordinator.ts
+++ b/packages/runtime-host/src/server/client-capability-coordinator.ts
@@ -25,6 +25,7 @@ import type { RootExecutionDescriptor } from '@maka/core/agent-run';
 import { type ToolGroup } from '@maka/runtime/tool-availability';
 import {
   type ClientCapabilityOffer,
+  type ClientCapabilityOwnerIdentity,
   type ClientCapabilityReplaceInput,
   type ClientCapabilityServiceOffer,
   type ClientCapabilityToolDescriptor,
@@ -65,7 +66,9 @@ interface ClientProviderState {
   readonly providerId: string;
   readonly principalId: string;
   readonly clientInstanceId: string;
+  readonly principalKind: ClientCapabilityConnectionIdentity['principalKind'];
   readonly trustedProvider: boolean;
+  readonly capabilityOwner?: ClientCapabilityOwnerIdentity;
   activeConnectionId?: string;
   current?: CapabilityRegistration;
   readonly registrations: Map<string, CapabilityRegistration>;
@@ -333,14 +336,41 @@ export class HostClientCapabilityCoordinator implements ClientCapabilityService
     initiatingConnectionId: string,
     mode: SessionBindingMode,
   ): SessionBindingSelection {
-    const initiatingProviderId = this.#connections.get(initiatingConnectionId)?.provider.providerId;
-    const initiatingProvider = initiatingProviderId
-      ? this.#providers.get(initiatingProviderId)
-      : undefined;
+    const initiatingProvider = this.#connections.get(initiatingConnectionId)?.provider;
+    const directProvider =
+      initiatingProvider?.current && this.#activeConnection(initiatingProvider)
+        ? initiatingProvider
+        : undefined;
+    const associatedProviders = initiatingProvider
+      ? [...this.#providers.values()].filter(
+          (provider) =>
+            provider.trustedProvider &&
+            provider.current !== undefined &&
+            this.#activeConnection(provider) !== undefined &&
+            provider.capabilityOwner?.principalId === initiatingProvider.principalId &&
+            provider.capabilityOwner.clientInstanceId === initiatingProvider.clientInstanceId,
+        )
+      : [];
+    if (!directProvider && associatedProviders.length > 1) {
+      return {
+        ok: false,
+        message: 'Multiple Client Capability providers are bound to the initiating Client',
+      };
+    }
+    const selectedInitiatingProvider = directProvider ?? associatedProviders[0];
+    // A remote Client must never inherit an unrelated provider merely because
+    // it is the only candidate. Local-owner and recovery flows retain their
+    // existing provider-independent fallback when no provider was selected.
+    const initiatingProviderId =
+      selectedInitiatingProvider?.providerId ??
+      (initiatingProvider?.principalKind === 'remote_owner'
+        ? initiatingProvider.providerId
+        : undefined);
     const previousState = this.#sessions.get(sessionId);
     const serviceProviderId =
       previousState?.serviceProviderId ??
-      (initiatingProvider?.current && initiatingProvider.current.servicesByContract.size > 0
+      (selectedInitiatingProvider?.current &&
+      selectedInitiatingProvider.current.servicesByContract.size > 0
         ? initiatingProviderId
         : undefined);
     const previous = previousState?.sessionBindings ?? new Map();
@@ -388,10 +418,8 @@ export class HostClientCapabilityCoordinator implements ClientCapabilityService
           };
         }
       } else {
-        candidate =
-          candidates.find((entry) => entry.registration.providerId === initiatingProviderId) ??
-          (candidates.length === 1 ? candidates[0] : undefined);
-        if (!candidate && candidates.length > 1) {
+        candidate = selectProviderCandidate(candidates, initiatingProviderId);
+        if (!candidate && initiatingProviderId === undefined && candidates.length > 1) {
           if (mode === 'degrade') continue;
           return {
             ok: false,
@@ -436,9 +464,7 @@ export class HostClientCapabilityCoordinator implements ClientCapabilityService
       left.localeCompare(right),
     )) {
       if (candidates[0]?.offer.offer.affinity !== 'turn') continue;
-      const candidate =
-        candidates.find((entry) => entry.registration.providerId === initiatingProviderId) ??
-        (candidates.length === 1 ? candidates[0] : undefined);
+      const candidate = selectProviderCandidate(candidates, initiatingProviderId);
       if (!candidate || offerConflictsWithProxyNames(candidate.offer, proxyNames)) continue;
       nextTurn.set(contractId, {
         kind: 'bound',
@@ -510,7 +536,13 @@ export class HostClientCapabilityCoordinator implements ClientCapabilityService
     for (const [contractId, candidates] of [...eligible].sort(([left], [right]) =>
       left.localeCompare(right),
     )) {
-      const offer = candidates[0]?.offer;
+      // A provider-independent snapshot keeps one representative descriptor so
+      // a dynamic call can report ambiguity. A remote Client's explicit
+      // selector must instead hide every unrelated provider.
+      const offer =
+        state?.initiatingProviderId === undefined
+          ? candidates[0]?.offer
+          : selectProviderCandidate(candidates, state.initiatingProviderId)?.offer;
       if (
         !offer ||
         offer.offer.affinity !== 'call' ||
@@ -987,18 +1019,32 @@ export class HostClientCapabilityCoordinator implements ClientCapabilityService
   #provider(identity: ClientCapabilityConnectionIdentity): ClientProviderState {
     const providerId = clientProviderId(identity.principalId, identity.clientInstanceId);
     const trustedProvider = identity.principalKind === 'capability_provider';
+    if (identity.capabilityOwner && !trustedProvider) {
+      throw new Error('Only a capability provider may declare a Client Capability owner');
+    }
     let provider = this.#providers.get(providerId);
     if (!provider) {
       provider = {
         providerId,
         principalId: identity.principalId,
         clientInstanceId: identity.clientInstanceId,
+        principalKind: identity.principalKind,
         trustedProvider,
+        ...(identity.capabilityOwner
+          ? { capabilityOwner: Object.freeze({ ...identity.capabilityOwner }) }
+          : {}),
         registrations: new Map(),
       };
       this.#providers.set(providerId, provider);
-    } else if (provider.trustedProvider !== trustedProvider) {
+    } else if (
+      provider.principalKind !== identity.principalKind ||
+      provider.trustedProvider !== trustedProvider
+    ) {
       throw new Error('Client Capability provider authority changed across connections');
+    } else if (
+      !clientCapabilityOwnerIdentitiesEqual(provider.capabilityOwner, identity.capabilityOwner)
+    ) {
+      throw new Error('Client Capability provider owner changed across connections');
     }
     return provider;
   }
@@ -1033,13 +1079,13 @@ export class HostClientCapabilityCoordinator implements ClientCapabilityService
     readonly tool: FrozenToolBinding;
   } {
     const candidates = this.#eligibleOffersByContract().get(contractId) ?? [];
-    const candidate =
-      candidates.find((entry) => entry.registration.providerId === initiatingProviderId) ??
-      (candidates.length === 1 ? candidates[0] : undefined);
+    const candidate = selectProviderCandidate(candidates, initiatingProviderId);
     if (!candidate) {
       throw new ClientCapabilityInvocationError(
-        candidates.length > 1 ? 'capability_ambiguous' : 'capability_lost',
-        candidates.length > 1
+        initiatingProviderId === undefined && candidates.length > 1
+          ? 'capability_ambiguous'
+          : 'capability_lost',
+        initiatingProviderId === undefined && candidates.length > 1
           ? 'Multiple Client Capability providers offer this call-affine contract'
           : 'Client Capability provider is unavailable',
       );
@@ -1370,6 +1416,30 @@ function rememberOfferProxyNames(offer: FrozenOfferBinding, proxyNames: Map<stri
   }
 }
 
+function selectProviderCandidate(
+  candidates: readonly SelectedOfferBinding[],
+  initiatingProviderId: string | undefined,
+): SelectedOfferBinding | undefined {
+  return initiatingProviderId === undefined
+    ? candidates.length === 1
+      ? candidates[0]
+      : undefined
+    : candidates.find((candidate) => candidate.registration.providerId === initiatingProviderId);
+}
+
+function clientCapabilityOwnerIdentitiesEqual(
+  left: ClientProviderState['capabilityOwner'],
+  right: ClientCapabilityConnectionIdentity['capabilityOwner'],
+): boolean {
+  return (
+    left === right ||
+    (left !== undefined &&
+      right !== undefined &&
+      left.principalId === right.principalId &&
+      left.clientInstanceId === right.clientInstanceId)
+  );
+}
+
 function canonicalJson(value: unknown): string {
   if (value === null || typeof value === 'boolean' || typeof value === 'number') {
     return JSON.stringify(value);


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org