maka-agent 0.2.0-dev.45.20260921 → 0.2.0-dev.47.20260922
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DISCLAIMER-WIP +6 -1
- package/README.md +6 -1
- package/dist/acp/maka-acp-agent.js +1 -1
- package/dist/acp/session-mcp.js +212 -0
- package/dist/acp/session-registry.js +52 -8
- package/dist/acp/stdio-server.js +3 -0
- package/dist/cli-core.js +1 -1
- package/dist/mcp-capability-provider.js +6 -1
- package/dist/mcp-capability-publication.js +139 -0
- package/dist/pi-tui-layout.js +46 -17
- package/dist/pi-tui-pickers.js +25 -14
- package/dist/pi-tui-runner.js +24 -15
- package/dist/runtime-host-service-manager.js +26 -2
- package/dist/tui-context-refresh.js +15 -26
- package/dist/tui-mcp-control.js +29 -88
- package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
- package/node_modules/@maka/core/dist/client-capability-grant.js +2 -2
- package/node_modules/@maka/core/dist/model-thinking.js +36 -1
- package/node_modules/@maka/core/dist/provider-registry.js +0 -1
- package/node_modules/@maka/core/dist/refresh-read-coordinator.js +62 -0
- package/node_modules/@maka/core/dist/runtime-policy/connection-catalog-codec.js +4 -0
- package/node_modules/@maka/core/dist/session.js +17 -1
- package/node_modules/@maka/core/dist/settings.js +2 -0
- package/node_modules/@maka/core/package.json +1 -0
- package/node_modules/@maka/runtime/dist/ai-sdk-message-projection.js +15 -60
- package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +18 -0
- package/node_modules/@maka/runtime/dist/apply-patch-profile.js +64 -14
- package/node_modules/@maka/runtime/dist/builtin-tools.js +3 -2
- package/node_modules/@maka/runtime/dist/codex-v4a-patch.js +18 -0
- package/node_modules/@maka/runtime/dist/model-adapter.js +80 -3
- package/node_modules/@maka/runtime/dist/model-runtime.js +6 -0
- package/node_modules/@maka/runtime/dist/openai-apply-patch.js +5 -0
- package/node_modules/@maka/runtime/dist/plugin-executor-backend.js +6 -0
- package/node_modules/@maka/runtime/dist/session-manager.js +15 -2
- package/node_modules/@maka/runtime-host/dist/client/capability-provider-service.js +2 -0
- package/node_modules/@maka/runtime-host/dist/client/client-capability-channel.js +59 -20
- package/node_modules/@maka/runtime-host/dist/client/connection.js +6 -4
- package/node_modules/@maka/runtime-host/dist/client/reconnecting-connection.js +4 -4
- package/node_modules/@maka/runtime-host/dist/protocol/client-capability.js +20 -3
- package/node_modules/@maka/runtime-host/dist/protocol/external-session.js +8 -5
- package/node_modules/@maka/runtime-host/dist/protocol/index.js +11 -1
- package/node_modules/@maka/runtime-host/dist/protocol/session-catalog.js +35 -3
- package/node_modules/@maka/runtime-host/dist/protocol/workhub-coordination.js +1 -5
- package/node_modules/@maka/runtime-host/dist/server/client-capability-coordinator.js +234 -70
- package/node_modules/@maka/runtime-host/dist/server/client-capability-invocation-broker.js +14 -8
- package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +29 -2
- package/node_modules/@maka/runtime-host/dist/server/execution-model-authority.js +3 -0
- package/node_modules/@maka/runtime-host/dist/server/external-session-coordinator.js +29 -7
- package/node_modules/@maka/runtime-host/dist/server/hosted-execution-tool-profile.js +1 -1
- package/node_modules/@maka/runtime-host/dist/server/session-bundle-coordinator.js +4 -1
- package/node_modules/@maka/runtime-host/dist/server/session-catalog-coordinator.js +32 -4
- package/node_modules/@maka/runtime-host/dist/server/session-retirement-coordinator.js +2 -2
- package/package.json +1 -1
|
@@ -20,11 +20,14 @@ import { createHash } from 'node:crypto';
|
|
|
20
20
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
21
21
|
import { buildMcpTools, mcpProxyToolName, } from '@maka/runtime/mcp-tools';
|
|
22
22
|
import { clientCapabilityScopeIdentity, } from '@maka/core/client-capability-grant';
|
|
23
|
+
import { decodeClientCapabilityReplaceInput, } from '../protocol/index.js';
|
|
23
24
|
import { ClientCapabilityInvocationBroker, ClientCapabilityInvocationError, } from './client-capability-invocation-broker.js';
|
|
24
25
|
import { clientCapabilityProviderId } from './client-capability-provider-id.js';
|
|
25
26
|
// Leave the Host deadline outside the provider's bounded action deadline so an
|
|
26
27
|
// accepted call can return its real terminal result instead of outcome_unknown.
|
|
27
28
|
const DEFAULT_CALL_TIMEOUT_MS = 150_000;
|
|
29
|
+
// Bound pre-creation Session publications even when a client invents Session IDs.
|
|
30
|
+
export const MAX_SESSION_REGISTRATIONS_PER_PROVIDER = 64;
|
|
28
31
|
const DESKTOP_BROWSER_SERVER_ID = 'desktop_browser';
|
|
29
32
|
const DESKTOP_SETTINGS_SERVER_ID = 'desktop_settings';
|
|
30
33
|
const DESKTOP_MCP_OFFER_PREFIX = 'desktop_mcp';
|
|
@@ -48,6 +51,7 @@ export class HostClientCapabilityCoordinator {
|
|
|
48
51
|
'client.capability.unregister': (input, context) => this.#unregister(input, context),
|
|
49
52
|
};
|
|
50
53
|
#activation;
|
|
54
|
+
#isSessionRetired;
|
|
51
55
|
#onModelToolsChanged;
|
|
52
56
|
#interactions;
|
|
53
57
|
#grants;
|
|
@@ -62,16 +66,12 @@ export class HostClientCapabilityCoordinator {
|
|
|
62
66
|
#draining = false;
|
|
63
67
|
constructor(options) {
|
|
64
68
|
this.#activation = options.activation;
|
|
69
|
+
this.#isSessionRetired = options.isSessionRetired;
|
|
65
70
|
this.#onModelToolsChanged = options.onModelToolsChanged;
|
|
66
71
|
this.#interactions = options.interactions;
|
|
67
72
|
this.#grants = options.grants;
|
|
68
73
|
this.#invocations = new ClientCapabilityInvocationBroker({
|
|
69
|
-
senderFor: (
|
|
70
|
-
const connection = this.#connections.get(connectionId);
|
|
71
|
-
return connection && this.#activeConnection(connection.provider) === connection
|
|
72
|
-
? connection.sender
|
|
73
|
-
: undefined;
|
|
74
|
-
},
|
|
74
|
+
senderFor: (registration) => this.#registrationConnection(registration)?.sender,
|
|
75
75
|
onRegistrationIdle: (registration) => this.#releaseRegistrationIfUnused(registration),
|
|
76
76
|
});
|
|
77
77
|
}
|
|
@@ -151,10 +151,14 @@ export class HostClientCapabilityCoordinator {
|
|
|
151
151
|
}
|
|
152
152
|
async bindRecoveredSession(sessionId, binding, toolNames) {
|
|
153
153
|
return this.#activation.runMutation(() => {
|
|
154
|
-
const provider = [...this.#providers.values()].find((candidate) => providerAuthorityDigest(candidate) === binding &&
|
|
154
|
+
const provider = [...this.#providers.values()].find((candidate) => providerAuthorityDigest(candidate) === binding &&
|
|
155
|
+
(this.#activeConnection(candidate) ||
|
|
156
|
+
(candidate.sessionRegistrations.get(sessionId) &&
|
|
157
|
+
this.#registrationConnection(candidate.sessionRegistrations.get(sessionId)))));
|
|
155
158
|
if (!provider)
|
|
156
159
|
return false;
|
|
157
|
-
const
|
|
160
|
+
const scoped = provider.sessionRegistrations.get(sessionId);
|
|
161
|
+
const connection = (scoped && this.#registrationConnection(scoped)) || this.#activeConnection(provider);
|
|
158
162
|
const selection = this.#selectSessionState(sessionId, connection.connectionId, 'strict');
|
|
159
163
|
if (!selection.ok || this.#toolProviderBinding(selection.state, toolNames) !== binding)
|
|
160
164
|
return false;
|
|
@@ -171,8 +175,9 @@ export class HostClientCapabilityCoordinator {
|
|
|
171
175
|
if (binding.kind !== 'bound')
|
|
172
176
|
continue;
|
|
173
177
|
const provider = this.#providers.get(binding.providerId);
|
|
174
|
-
const
|
|
175
|
-
|
|
178
|
+
const registration = this.#bindingRegistration(binding);
|
|
179
|
+
const offer = registration?.offersByContract.get(contractId);
|
|
180
|
+
if (!provider || !registration || !this.#registrationConnection(registration) || !offer)
|
|
176
181
|
continue;
|
|
177
182
|
for (const tool of offer.offer.tools) {
|
|
178
183
|
if (!missing.delete(mcpProxyToolName(tool.serverId, tool.name)))
|
|
@@ -225,7 +230,10 @@ export class HostClientCapabilityCoordinator {
|
|
|
225
230
|
}
|
|
226
231
|
#selectSessionState(sessionId, initiatingConnectionId, mode) {
|
|
227
232
|
const initiatingProvider = this.#connections.get(initiatingConnectionId)?.provider;
|
|
228
|
-
const
|
|
233
|
+
const initiatingScoped = initiatingProvider?.sessionRegistrations.get(sessionId);
|
|
234
|
+
const directProvider = initiatingProvider &&
|
|
235
|
+
((initiatingProvider.current && this.#activeConnection(initiatingProvider)) ||
|
|
236
|
+
(initiatingScoped && this.#registrationConnection(initiatingScoped)))
|
|
229
237
|
? initiatingProvider
|
|
230
238
|
: undefined;
|
|
231
239
|
const associatedProviders = initiatingProvider &&
|
|
@@ -257,7 +265,7 @@ export class HostClientCapabilityCoordinator {
|
|
|
257
265
|
? initiatingProviderId
|
|
258
266
|
: undefined);
|
|
259
267
|
const previous = previousState?.sessionBindings ?? new Map();
|
|
260
|
-
const eligible = this.#eligibleOffersByContract();
|
|
268
|
+
const eligible = this.#eligibleOffersByContract(sessionId);
|
|
261
269
|
const next = new Map();
|
|
262
270
|
const selected = [];
|
|
263
271
|
const sessionContractIds = new Set([
|
|
@@ -271,10 +279,11 @@ export class HostClientCapabilityCoordinator {
|
|
|
271
279
|
const candidates = eligible.get(contractId) ?? [];
|
|
272
280
|
let candidate;
|
|
273
281
|
if (previousBinding?.kind === 'bound') {
|
|
274
|
-
candidate = candidates.find((entry) => entry.registration.providerId === previousBinding.providerId
|
|
282
|
+
candidate = candidates.find((entry) => entry.registration.providerId === previousBinding.providerId &&
|
|
283
|
+
entry.registration.sessionId === previousBinding.sessionId);
|
|
275
284
|
if (!candidate) {
|
|
276
285
|
if (mode === 'degrade') {
|
|
277
|
-
next.set(contractId, { kind: 'lost'
|
|
286
|
+
next.set(contractId, { ...previousBinding, kind: 'lost' });
|
|
278
287
|
continue;
|
|
279
288
|
}
|
|
280
289
|
return {
|
|
@@ -284,7 +293,8 @@ export class HostClientCapabilityCoordinator {
|
|
|
284
293
|
}
|
|
285
294
|
}
|
|
286
295
|
else if (previousBinding?.kind === 'lost') {
|
|
287
|
-
candidate = candidates.find((entry) => entry.registration.providerId === previousBinding.providerId
|
|
296
|
+
candidate = candidates.find((entry) => entry.registration.providerId === previousBinding.providerId &&
|
|
297
|
+
entry.registration.sessionId === previousBinding.sessionId);
|
|
288
298
|
if (!candidate) {
|
|
289
299
|
if (mode === 'degrade') {
|
|
290
300
|
next.set(contractId, previousBinding);
|
|
@@ -297,8 +307,10 @@ export class HostClientCapabilityCoordinator {
|
|
|
297
307
|
}
|
|
298
308
|
}
|
|
299
309
|
else {
|
|
300
|
-
|
|
301
|
-
|
|
310
|
+
const selectable = candidates.filter((entry) => entry.registration.sessionId === undefined ||
|
|
311
|
+
entry.registration.providerId === initiatingProviderId);
|
|
312
|
+
candidate = selectProviderCandidate(selectable, initiatingProviderId);
|
|
313
|
+
if (!candidate && initiatingProviderId === undefined && selectable.length > 1) {
|
|
302
314
|
if (mode === 'degrade')
|
|
303
315
|
continue;
|
|
304
316
|
return {
|
|
@@ -312,6 +324,9 @@ export class HostClientCapabilityCoordinator {
|
|
|
312
324
|
next.set(contractId, {
|
|
313
325
|
kind: 'bound',
|
|
314
326
|
providerId: candidate.registration.providerId,
|
|
327
|
+
...(candidate.registration.sessionId === undefined
|
|
328
|
+
? {}
|
|
329
|
+
: { sessionId: candidate.registration.sessionId }),
|
|
315
330
|
});
|
|
316
331
|
selected.push(candidate);
|
|
317
332
|
}
|
|
@@ -323,6 +338,9 @@ export class HostClientCapabilityCoordinator {
|
|
|
323
338
|
next.set(candidate.offer.contractId, {
|
|
324
339
|
kind: 'lost',
|
|
325
340
|
providerId: candidate.registration.providerId,
|
|
341
|
+
...(candidate.registration.sessionId === undefined
|
|
342
|
+
? {}
|
|
343
|
+
: { sessionId: candidate.registration.sessionId }),
|
|
326
344
|
});
|
|
327
345
|
}
|
|
328
346
|
else {
|
|
@@ -375,9 +393,9 @@ export class HostClientCapabilityCoordinator {
|
|
|
375
393
|
if (binding.kind === 'lost')
|
|
376
394
|
continue;
|
|
377
395
|
const provider = this.#providers.get(binding.providerId);
|
|
378
|
-
const registration =
|
|
396
|
+
const registration = this.#bindingRegistration(binding);
|
|
379
397
|
const offer = registration?.offersByContract.get(contractId);
|
|
380
|
-
if (!provider || !
|
|
398
|
+
if (!provider || !registration || !this.#registrationConnection(registration) || !offer) {
|
|
381
399
|
throw new ClientCapabilityInvocationError('capability_lost', 'A Session-bound Client Capability provider is unavailable');
|
|
382
400
|
}
|
|
383
401
|
selected.push({ registration, offer });
|
|
@@ -553,10 +571,28 @@ export class HostClientCapabilityCoordinator {
|
|
|
553
571
|
provider.current?.servicesByContract.has(serviceContract(serviceId, version)));
|
|
554
572
|
}
|
|
555
573
|
retireSessions(sessionIds) {
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
574
|
+
const retiredSessionIds = new Set(sessionIds);
|
|
575
|
+
return this.#activation.runMutation(() => {
|
|
576
|
+
// Invalidate binding previews even when this Session has not committed a
|
|
577
|
+
// stored state or scoped registration yet. Otherwise a preview captured
|
|
578
|
+
// before retirement can commit afterwards and recreate its state.
|
|
579
|
+
for (const sessionId of retiredSessionIds) {
|
|
580
|
+
this.#sessions.delete(sessionId);
|
|
581
|
+
this.#revision += 1;
|
|
582
|
+
}
|
|
583
|
+
for (const provider of this.#providers.values()) {
|
|
584
|
+
for (const sessionId of retiredSessionIds) {
|
|
585
|
+
const registration = provider.sessionRegistrations.get(sessionId);
|
|
586
|
+
if (!registration)
|
|
587
|
+
continue;
|
|
588
|
+
provider.sessionRegistrations.delete(sessionId);
|
|
589
|
+
if (hasModelToolOffers(registration))
|
|
590
|
+
this.#onModelToolsChanged();
|
|
591
|
+
this.#releaseRegistrationIfUnused(registration);
|
|
592
|
+
}
|
|
593
|
+
this.#deleteProviderIfUnused(provider);
|
|
594
|
+
}
|
|
595
|
+
});
|
|
560
596
|
}
|
|
561
597
|
releaseConnection(connectionId) {
|
|
562
598
|
const invocationCleanup = this.#invocations.releaseConnection(connectionId);
|
|
@@ -579,16 +615,23 @@ export class HostClientCapabilityCoordinator {
|
|
|
579
615
|
return;
|
|
580
616
|
const { provider } = connection;
|
|
581
617
|
this.#connections.delete(connection.connectionId);
|
|
582
|
-
if (provider.activeConnectionId
|
|
583
|
-
|
|
584
|
-
|
|
618
|
+
if (provider.activeConnectionId === connection.connectionId) {
|
|
619
|
+
provider.activeConnectionId = undefined;
|
|
620
|
+
if (provider.current) {
|
|
621
|
+
const registration = provider.current;
|
|
622
|
+
provider.current = undefined;
|
|
623
|
+
this.#markBindingsLost(provider.providerId);
|
|
624
|
+
this.#removeTurnBindings(provider.providerId);
|
|
625
|
+
this.#revision += 1;
|
|
626
|
+
if (hasModelToolOffers(registration))
|
|
627
|
+
this.#onModelToolsChanged();
|
|
628
|
+
}
|
|
585
629
|
}
|
|
586
|
-
provider.
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
provider.
|
|
590
|
-
this.#markBindingsLost(provider.providerId);
|
|
591
|
-
this.#removeTurnBindings(provider.providerId);
|
|
630
|
+
for (const [sessionId, registration] of provider.sessionRegistrations) {
|
|
631
|
+
if (registration.connectionId !== connection.connectionId)
|
|
632
|
+
continue;
|
|
633
|
+
provider.sessionRegistrations.delete(sessionId);
|
|
634
|
+
this.#markBindingsLost(provider.providerId, undefined, sessionId);
|
|
592
635
|
this.#revision += 1;
|
|
593
636
|
if (hasModelToolOffers(registration))
|
|
594
637
|
this.#onModelToolsChanged();
|
|
@@ -621,6 +664,18 @@ export class HostClientCapabilityCoordinator {
|
|
|
621
664
|
},
|
|
622
665
|
};
|
|
623
666
|
}
|
|
667
|
+
// The lifecycle owner commits archive/removal before retiring capabilities.
|
|
668
|
+
// Read that authority inside the mutation lane: a queued refresh must not
|
|
669
|
+
// recreate a retired slot. Never-created IDs remain valid for ACP preparation.
|
|
670
|
+
if (input.sessionId !== undefined && (await this.#isSessionRetired(input.sessionId))) {
|
|
671
|
+
return {
|
|
672
|
+
ok: false,
|
|
673
|
+
error: {
|
|
674
|
+
code: 'invalid_request',
|
|
675
|
+
message: 'Client Capability target Session is retired',
|
|
676
|
+
},
|
|
677
|
+
};
|
|
678
|
+
}
|
|
624
679
|
const connection = this.#connections.get(context.connectionId);
|
|
625
680
|
if (!connection) {
|
|
626
681
|
return {
|
|
@@ -632,7 +687,18 @@ export class HostClientCapabilityCoordinator {
|
|
|
632
687
|
};
|
|
633
688
|
}
|
|
634
689
|
const { provider } = connection;
|
|
635
|
-
if (
|
|
690
|
+
if (input.sessionId !== undefined &&
|
|
691
|
+
!provider.sessionRegistrations.has(input.sessionId) &&
|
|
692
|
+
provider.sessionRegistrations.size >= MAX_SESSION_REGISTRATIONS_PER_PROVIDER) {
|
|
693
|
+
return {
|
|
694
|
+
ok: false,
|
|
695
|
+
error: {
|
|
696
|
+
code: 'invalid_request',
|
|
697
|
+
message: 'Client Capability Session registration limit reached',
|
|
698
|
+
},
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
if (connection.superseded && input.sessionId === undefined) {
|
|
636
702
|
return {
|
|
637
703
|
ok: false,
|
|
638
704
|
error: {
|
|
@@ -658,6 +724,12 @@ export class HostClientCapabilityCoordinator {
|
|
|
658
724
|
throw new Error('A trusted capability provider may publish only path-independent session capabilities');
|
|
659
725
|
}
|
|
660
726
|
registration = freezeRegistration(provider.providerId, context.connectionId, input, provider.trustedProvider);
|
|
727
|
+
const overlapping = registration.sessionId === undefined
|
|
728
|
+
? [...provider.sessionRegistrations.values()]
|
|
729
|
+
: provider.current
|
|
730
|
+
? [provider.current]
|
|
731
|
+
: [];
|
|
732
|
+
assertRegistrationToolsDoNotOverlap(registration, overlapping);
|
|
661
733
|
}
|
|
662
734
|
catch (error) {
|
|
663
735
|
return {
|
|
@@ -668,22 +740,43 @@ export class HostClientCapabilityCoordinator {
|
|
|
668
740
|
},
|
|
669
741
|
};
|
|
670
742
|
}
|
|
671
|
-
const previous = provider.
|
|
743
|
+
const previous = this.#currentRegistration(provider, registration.sessionId);
|
|
672
744
|
const previousConnectionId = provider.activeConnectionId;
|
|
673
|
-
if (
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
745
|
+
if (registration.sessionId !== undefined) {
|
|
746
|
+
provider.sessionRegistrations.set(registration.sessionId, registration);
|
|
747
|
+
}
|
|
748
|
+
else {
|
|
749
|
+
if (previousConnectionId && previousConnectionId !== context.connectionId) {
|
|
750
|
+
const previousConnection = this.#connections.get(previousConnectionId);
|
|
751
|
+
if (previousConnection)
|
|
752
|
+
previousConnection.superseded = true;
|
|
753
|
+
for (const retired of provider.registrations.values()) {
|
|
754
|
+
if (retired.sessionId === undefined && retired.connectionId === previousConnectionId) {
|
|
755
|
+
void this.#invocations.releaseRegistration(retired);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
provider.activeConnectionId = context.connectionId;
|
|
760
|
+
provider.current = registration;
|
|
678
761
|
}
|
|
679
|
-
provider.activeConnectionId = context.connectionId;
|
|
680
|
-
provider.current = registration;
|
|
681
762
|
const currentContracts = new Set(registration.offersByContract.keys());
|
|
763
|
+
if (registration.sessionId !== undefined) {
|
|
764
|
+
// Discovery may have changed while this scope's connection was away.
|
|
765
|
+
// Republishing is a complete replacement, including previously lost
|
|
766
|
+
// contracts; keep other Sessions and legacy provider leases untouched.
|
|
767
|
+
const previousBindings = this.#sessions.get(registration.sessionId)?.sessionBindings;
|
|
768
|
+
const removed = new Set([...(previousBindings ?? [])]
|
|
769
|
+
.filter(([contractId, binding]) => binding.providerId === provider.providerId &&
|
|
770
|
+
binding.sessionId === registration.sessionId &&
|
|
771
|
+
!currentContracts.has(contractId))
|
|
772
|
+
.map(([contractId]) => contractId));
|
|
773
|
+
this.#retireBindings(provider.providerId, removed, registration.sessionId);
|
|
774
|
+
}
|
|
682
775
|
if (previous) {
|
|
683
|
-
this.#retireBindings(provider.providerId, new Set([...previous.offersByContract.keys()].filter((contractId) => !currentContracts.has(contractId))));
|
|
684
|
-
this.#removeTurnBindings(provider.providerId, new Set([...previous.offersByContract.keys()].filter((contractId) => !currentContracts.has(contractId))));
|
|
776
|
+
this.#retireBindings(provider.providerId, new Set([...previous.offersByContract.keys()].filter((contractId) => !currentContracts.has(contractId))), registration.sessionId);
|
|
777
|
+
this.#removeTurnBindings(provider.providerId, new Set([...previous.offersByContract.keys()].filter((contractId) => !currentContracts.has(contractId))), registration.sessionId);
|
|
685
778
|
}
|
|
686
|
-
this.#restoreBindings(provider.providerId, currentContracts);
|
|
779
|
+
this.#restoreBindings(provider.providerId, currentContracts, registration.sessionId);
|
|
687
780
|
provider.registrations.set(registration.registrationId, registration);
|
|
688
781
|
this.#revision += 1;
|
|
689
782
|
if (hasModelToolOffers(previous) || hasModelToolOffers(registration)) {
|
|
@@ -704,9 +797,11 @@ export class HostClientCapabilityCoordinator {
|
|
|
704
797
|
#unregister(input, context) {
|
|
705
798
|
return this.#activation.runMutation(async () => {
|
|
706
799
|
const provider = this.#connections.get(context.connectionId)?.provider;
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
800
|
+
const registration = provider?.registrations.get(input.registrationId);
|
|
801
|
+
if (!provider ||
|
|
802
|
+
!registration ||
|
|
803
|
+
registration.connectionId !== context.connectionId ||
|
|
804
|
+
this.#currentRegistration(provider, registration.sessionId) !== registration) {
|
|
710
805
|
return {
|
|
711
806
|
ok: false,
|
|
712
807
|
error: {
|
|
@@ -715,10 +810,12 @@ export class HostClientCapabilityCoordinator {
|
|
|
715
810
|
},
|
|
716
811
|
};
|
|
717
812
|
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
813
|
+
if (registration.sessionId === undefined)
|
|
814
|
+
provider.current = undefined;
|
|
815
|
+
else
|
|
816
|
+
provider.sessionRegistrations.delete(registration.sessionId);
|
|
817
|
+
this.#retireBindings(provider.providerId, new Set(registration.offersByContract.keys()), registration.sessionId);
|
|
818
|
+
this.#removeTurnBindings(provider.providerId, new Set(registration.offersByContract.keys()), registration.sessionId);
|
|
722
819
|
this.#revision += 1;
|
|
723
820
|
if (hasModelToolOffers(registration))
|
|
724
821
|
this.#onModelToolsChanged();
|
|
@@ -772,6 +869,7 @@ export class HostClientCapabilityCoordinator {
|
|
|
772
869
|
toolSnapshot: () => snapshot,
|
|
773
870
|
prepareTool: async (binding, args, options) => {
|
|
774
871
|
const { selectedBinding, dynamicBinding } = resolveBinding(binding);
|
|
872
|
+
assertRegistrationSession(dynamicBinding.registration, options.context.sessionId);
|
|
775
873
|
const prepared = this.#invocations.prepare(dynamicBinding.registration, dynamicBinding.tool, args, options.context, options.signal, options.timeoutMs ?? DEFAULT_CALL_TIMEOUT_MS);
|
|
776
874
|
try {
|
|
777
875
|
const evidence = await prepared.waitUntilAccepted();
|
|
@@ -782,7 +880,7 @@ export class HostClientCapabilityCoordinator {
|
|
|
782
880
|
if (options.context.permissionMode !== 'ask' || !options.context.runId) {
|
|
783
881
|
throw new Error('Client Capability is unavailable in the current permission mode');
|
|
784
882
|
}
|
|
785
|
-
const target = managedClientCapabilityGrantTarget(selectedBinding.contractId, dynamicBinding.registration, dynamicBinding.tool, evidence);
|
|
883
|
+
const target = managedClientCapabilityGrantTarget(selectedBinding.contractId, dynamicBinding.registration, dynamicBinding.tool, evidence, options.context.sessionId);
|
|
786
884
|
if (!target) {
|
|
787
885
|
return {
|
|
788
886
|
execute: ({ emitProgress, requestInteraction } = {}) => prepared.admit(emitProgress, requestInteraction),
|
|
@@ -818,6 +916,7 @@ export class HostClientCapabilityCoordinator {
|
|
|
818
916
|
},
|
|
819
917
|
callTool: (binding, args, options) => {
|
|
820
918
|
const { dynamicBinding } = resolveBinding(binding);
|
|
919
|
+
assertRegistrationSession(dynamicBinding.registration, options.context.sessionId);
|
|
821
920
|
return this.#invocations.invoke(dynamicBinding.registration, dynamicBinding.tool, args, options.context, options.signal, options.timeoutMs ?? DEFAULT_CALL_TIMEOUT_MS, options.emitProgress, options.requestInteraction);
|
|
822
921
|
},
|
|
823
922
|
};
|
|
@@ -879,6 +978,7 @@ export class HostClientCapabilityCoordinator {
|
|
|
879
978
|
? { capabilityOwner: Object.freeze({ ...identity.capabilityOwner }) }
|
|
880
979
|
: {}),
|
|
881
980
|
registrations: new Map(),
|
|
981
|
+
sessionRegistrations: new Map(),
|
|
882
982
|
};
|
|
883
983
|
this.#providers.set(providerId, provider);
|
|
884
984
|
}
|
|
@@ -892,16 +992,21 @@ export class HostClientCapabilityCoordinator {
|
|
|
892
992
|
}
|
|
893
993
|
return provider;
|
|
894
994
|
}
|
|
895
|
-
#eligibleOffersByContract() {
|
|
995
|
+
#eligibleOffersByContract(sessionId) {
|
|
896
996
|
const eligible = new Map();
|
|
897
997
|
for (const provider of this.#providers.values()) {
|
|
898
|
-
const
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
998
|
+
const registrations = [
|
|
999
|
+
provider.current,
|
|
1000
|
+
sessionId === undefined ? undefined : provider.sessionRegistrations.get(sessionId),
|
|
1001
|
+
];
|
|
1002
|
+
for (const registration of registrations) {
|
|
1003
|
+
if (!registration || !this.#registrationConnection(registration))
|
|
1004
|
+
continue;
|
|
1005
|
+
for (const offer of registration.offersByContract.values()) {
|
|
1006
|
+
const candidates = eligible.get(offer.contractId) ?? [];
|
|
1007
|
+
candidates.push({ registration, offer });
|
|
1008
|
+
eligible.set(offer.contractId, candidates);
|
|
1009
|
+
}
|
|
905
1010
|
}
|
|
906
1011
|
}
|
|
907
1012
|
for (const candidates of eligible.values()) {
|
|
@@ -925,31 +1030,33 @@ export class HostClientCapabilityCoordinator {
|
|
|
925
1030
|
}
|
|
926
1031
|
return { registration: candidate.registration, tool };
|
|
927
1032
|
}
|
|
928
|
-
#markBindingsLost(providerId, contracts) {
|
|
1033
|
+
#markBindingsLost(providerId, contracts, scopeSessionId) {
|
|
929
1034
|
for (const [sessionId, state] of this.#sessions) {
|
|
930
1035
|
const bindings = state.sessionBindings;
|
|
931
1036
|
let next;
|
|
932
1037
|
for (const [contractId, binding] of bindings) {
|
|
933
1038
|
if (binding.kind !== 'bound' ||
|
|
934
1039
|
binding.providerId !== providerId ||
|
|
1040
|
+
binding.sessionId !== scopeSessionId ||
|
|
935
1041
|
(contracts && !contracts.has(contractId))) {
|
|
936
1042
|
continue;
|
|
937
1043
|
}
|
|
938
1044
|
next ??= new Map(bindings);
|
|
939
|
-
next.set(contractId, { kind: 'lost'
|
|
1045
|
+
next.set(contractId, { ...binding, kind: 'lost' });
|
|
940
1046
|
}
|
|
941
1047
|
if (next)
|
|
942
1048
|
this.#storeSessionState(sessionId, { ...state, sessionBindings: next });
|
|
943
1049
|
}
|
|
944
1050
|
}
|
|
945
|
-
#restoreBindings(providerId, contracts) {
|
|
1051
|
+
#restoreBindings(providerId, contracts, scopeSessionId) {
|
|
946
1052
|
for (const [sessionId, state] of this.#sessions) {
|
|
947
1053
|
const next = new Map(state.sessionBindings);
|
|
948
1054
|
for (const [contractId, binding] of state.sessionBindings) {
|
|
949
1055
|
if (binding.kind === 'lost' &&
|
|
950
1056
|
binding.providerId === providerId &&
|
|
1057
|
+
binding.sessionId === scopeSessionId &&
|
|
951
1058
|
contracts.has(contractId)) {
|
|
952
|
-
next.set(contractId, { kind: 'bound'
|
|
1059
|
+
next.set(contractId, { ...binding, kind: 'bound' });
|
|
953
1060
|
}
|
|
954
1061
|
}
|
|
955
1062
|
if (bindingMapsEqual(state.sessionBindings, next))
|
|
@@ -957,13 +1064,14 @@ export class HostClientCapabilityCoordinator {
|
|
|
957
1064
|
this.#storeSessionState(sessionId, { ...state, sessionBindings: next });
|
|
958
1065
|
}
|
|
959
1066
|
}
|
|
960
|
-
#retireBindings(providerId, contracts) {
|
|
1067
|
+
#retireBindings(providerId, contracts, scopeSessionId) {
|
|
961
1068
|
for (const [sessionId, state] of this.#sessions) {
|
|
962
1069
|
const bindings = state.sessionBindings;
|
|
963
1070
|
const next = new Map(bindings);
|
|
964
1071
|
for (const [contractId, binding] of bindings) {
|
|
965
|
-
if (binding.kind === 'bound' &&
|
|
1072
|
+
if ((binding.kind === 'bound' || scopeSessionId !== undefined) &&
|
|
966
1073
|
binding.providerId === providerId &&
|
|
1074
|
+
binding.sessionId === scopeSessionId &&
|
|
967
1075
|
contracts.has(contractId)) {
|
|
968
1076
|
next.delete(contractId);
|
|
969
1077
|
}
|
|
@@ -973,13 +1081,14 @@ export class HostClientCapabilityCoordinator {
|
|
|
973
1081
|
this.#storeSessionState(sessionId, { ...state, sessionBindings: next });
|
|
974
1082
|
}
|
|
975
1083
|
}
|
|
976
|
-
#removeTurnBindings(providerId, contracts) {
|
|
1084
|
+
#removeTurnBindings(providerId, contracts, scopeSessionId) {
|
|
977
1085
|
for (const [sessionId, state] of this.#sessions) {
|
|
978
1086
|
const bindings = state.turnBindings;
|
|
979
1087
|
const next = new Map(bindings);
|
|
980
1088
|
for (const [contractId, binding] of bindings) {
|
|
981
1089
|
if (binding.kind === 'bound' &&
|
|
982
1090
|
binding.providerId === providerId &&
|
|
1091
|
+
binding.sessionId === scopeSessionId &&
|
|
983
1092
|
(!contracts || contracts.has(contractId))) {
|
|
984
1093
|
next.delete(contractId);
|
|
985
1094
|
}
|
|
@@ -1015,7 +1124,7 @@ export class HostClientCapabilityCoordinator {
|
|
|
1015
1124
|
}
|
|
1016
1125
|
#releaseRegistrationIfUnused(registration) {
|
|
1017
1126
|
const provider = this.#providers.get(registration.providerId);
|
|
1018
|
-
if (provider
|
|
1127
|
+
if ((provider && this.#currentRegistration(provider, registration.sessionId) === registration) ||
|
|
1019
1128
|
registration.snapshotRefs !== 0 ||
|
|
1020
1129
|
this.#invocations.holdsRegistration(registration)) {
|
|
1021
1130
|
return;
|
|
@@ -1037,8 +1146,28 @@ export class HostClientCapabilityCoordinator {
|
|
|
1037
1146
|
: undefined;
|
|
1038
1147
|
return connection?.provider === provider && !connection.superseded ? connection : undefined;
|
|
1039
1148
|
}
|
|
1149
|
+
#currentRegistration(provider, sessionId) {
|
|
1150
|
+
return sessionId === undefined
|
|
1151
|
+
? provider.current
|
|
1152
|
+
: provider.sessionRegistrations.get(sessionId);
|
|
1153
|
+
}
|
|
1154
|
+
#registrationConnection(registration) {
|
|
1155
|
+
const connection = this.#connections.get(registration.connectionId);
|
|
1156
|
+
if (!connection || connection.provider.providerId !== registration.providerId)
|
|
1157
|
+
return;
|
|
1158
|
+
// Session publications have their own owning connection. A replacement of
|
|
1159
|
+
// the legacy connection-wide publication cannot supersede those channels.
|
|
1160
|
+
if (registration.sessionId !== undefined)
|
|
1161
|
+
return connection;
|
|
1162
|
+
return this.#activeConnection(connection.provider) === connection ? connection : undefined;
|
|
1163
|
+
}
|
|
1164
|
+
#bindingRegistration(binding) {
|
|
1165
|
+
const provider = this.#providers.get(binding.providerId);
|
|
1166
|
+
return provider ? this.#currentRegistration(provider, binding.sessionId) : undefined;
|
|
1167
|
+
}
|
|
1040
1168
|
#deleteProviderIfUnused(provider) {
|
|
1041
1169
|
if (provider.current ||
|
|
1170
|
+
provider.sessionRegistrations.size > 0 ||
|
|
1042
1171
|
provider.activeConnectionId ||
|
|
1043
1172
|
provider.registrations.size > 0 ||
|
|
1044
1173
|
[...this.#connections.values()].some((connection) => connection.provider === provider) ||
|
|
@@ -1060,7 +1189,23 @@ export class HostClientCapabilityCoordinator {
|
|
|
1060
1189
|
function trustedClientToolActivityKind(descriptor) {
|
|
1061
1190
|
return descriptor.activityKind;
|
|
1062
1191
|
}
|
|
1192
|
+
function assertRegistrationSession(registration, sessionId) {
|
|
1193
|
+
if (registration.sessionId !== undefined && registration.sessionId !== sessionId) {
|
|
1194
|
+
throw new ClientCapabilityInvocationError('capability_lost', 'Client Capability belongs to another Session');
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
function assertRegistrationToolsDoNotOverlap(registration, overlapping) {
|
|
1198
|
+
const names = new Set([...registration.offersByContract.values()].flatMap(({ offer }) => offer.tools.map((tool) => mcpProxyToolName(tool.serverId, tool.name))));
|
|
1199
|
+
for (const candidate of overlapping) {
|
|
1200
|
+
for (const { offer } of candidate.offersByContract.values()) {
|
|
1201
|
+
if (offer.tools.some((tool) => names.has(mcpProxyToolName(tool.serverId, tool.name)))) {
|
|
1202
|
+
throw new Error('Connection and Session capabilities expose conflicting model tool identities');
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1063
1207
|
function freezeRegistration(providerId, connectionId, input, trustedProvider) {
|
|
1208
|
+
input = decodeClientCapabilityReplaceInput(input);
|
|
1064
1209
|
const offers = input.offers.map((offer) => Object.freeze({
|
|
1065
1210
|
...offer,
|
|
1066
1211
|
tools: Object.freeze(offer.tools.map((tool) => Object.freeze({
|
|
@@ -1085,6 +1230,7 @@ function freezeRegistration(providerId, connectionId, input, trustedProvider) {
|
|
|
1085
1230
|
toolsByIdentity.set(identity, {
|
|
1086
1231
|
offerId: offer.offerId,
|
|
1087
1232
|
hostPathAccess: offer.hostPathAccess,
|
|
1233
|
+
...(offer.admission === undefined ? {} : { admission: offer.admission }),
|
|
1088
1234
|
descriptor,
|
|
1089
1235
|
});
|
|
1090
1236
|
}
|
|
@@ -1105,14 +1251,30 @@ function freezeRegistration(providerId, connectionId, input, trustedProvider) {
|
|
|
1105
1251
|
providerId,
|
|
1106
1252
|
connectionId,
|
|
1107
1253
|
registrationId: input.registrationId,
|
|
1254
|
+
...(input.sessionId === undefined ? {} : { sessionId: input.sessionId }),
|
|
1108
1255
|
trustedProvider,
|
|
1109
1256
|
offersByContract,
|
|
1110
1257
|
servicesByContract,
|
|
1111
1258
|
snapshotRefs: 0,
|
|
1112
1259
|
};
|
|
1113
1260
|
}
|
|
1114
|
-
function managedClientCapabilityGrantTarget(contractId, registration, tool, evidence) {
|
|
1261
|
+
function managedClientCapabilityGrantTarget(contractId, registration, tool, evidence, sessionId) {
|
|
1115
1262
|
const { serverId, name: toolName } = tool.descriptor;
|
|
1263
|
+
if (tool.admission === 'mcp') {
|
|
1264
|
+
if (registration.sessionId !== sessionId ||
|
|
1265
|
+
tool.hostPathAccess !== 'none' ||
|
|
1266
|
+
evidence.kind !== 'none') {
|
|
1267
|
+
throw new Error('MCP admission requires its target Session and path-independent evidence');
|
|
1268
|
+
}
|
|
1269
|
+
return Object.freeze({
|
|
1270
|
+
providerId: registration.providerId,
|
|
1271
|
+
contractId,
|
|
1272
|
+
serverId,
|
|
1273
|
+
toolName,
|
|
1274
|
+
capability: 'mcp',
|
|
1275
|
+
scope: Object.freeze({ kind: 'mcp_tool', serverId, toolName }),
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1116
1278
|
if (!registration.trustedProvider) {
|
|
1117
1279
|
throw new Error('Managed Client Capability requires a trusted Desktop provider');
|
|
1118
1280
|
}
|
|
@@ -1203,6 +1365,7 @@ function capabilityGroupId(offer) {
|
|
|
1203
1365
|
version: offer.version,
|
|
1204
1366
|
affinity: offer.affinity,
|
|
1205
1367
|
hostPathAccess: offer.hostPathAccess,
|
|
1368
|
+
admission: offer.admission,
|
|
1206
1369
|
tools,
|
|
1207
1370
|
}))
|
|
1208
1371
|
.digest('hex')
|
|
@@ -1303,7 +1466,8 @@ function bindingMapsEqual(left, right) {
|
|
|
1303
1466
|
const candidate = right.get(contractId);
|
|
1304
1467
|
if (!candidate ||
|
|
1305
1468
|
candidate.kind !== binding.kind ||
|
|
1306
|
-
candidate.providerId !== binding.providerId
|
|
1469
|
+
candidate.providerId !== binding.providerId ||
|
|
1470
|
+
candidate.sessionId !== binding.sessionId) {
|
|
1307
1471
|
return false;
|
|
1308
1472
|
}
|
|
1309
1473
|
}
|