@code-yeongyu/senpi 2026.10.10-4 → 2026.10.10-6
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/CHANGELOG.md +46 -0
- package/dist/bundle/chunks/{anthropic-messages-OE3QBX4P.js → anthropic-messages-DUULMQBF.js} +1 -1
- package/dist/bundle/chunks/{app-server-command-N3L2LNP7.js → app-server-command-GJ5NXQSY.js} +1 -1
- package/dist/bundle/chunks/{chunk-3A3C5VVO.js → chunk-3OGEJMZ7.js} +1 -1
- package/dist/bundle/chunks/{chunk-J5MUMVWB.js → chunk-64ZVBCTL.js} +8 -7
- package/dist/bundle/chunks/{chunk-MP2DMTI7.js → chunk-C7LEZMWS.js} +124 -124
- package/dist/bundle/chunks/{chunk-ZSYRWQWG.js → chunk-JMI5OCIJ.js} +1 -1
- package/dist/bundle/chunks/{chunk-ODYQK5XJ.js → chunk-TALS4X7S.js} +1 -1
- package/dist/bundle/chunks/{chunk-7GVD54RM.js → chunk-U3IL5N76.js} +1 -1
- package/dist/bundle/chunks/{chunk-H67EVDWG.js → chunk-URQSNRHZ.js} +1 -1
- package/dist/bundle/chunks/{chunk-H4UIUFU5.js → chunk-VGYIPYOK.js} +1 -1
- package/dist/bundle/chunks/{chunk-M2IHGL3M.js → chunk-VW74BGIC.js} +1 -1
- package/dist/bundle/chunks/{chunk-4TK7VCZZ.js → chunk-WKMUAG23.js} +1 -1
- package/dist/bundle/chunks/chunk-YG5UO3E4.js +7 -0
- package/dist/bundle/chunks/chunk-YV2XQFXG.js +14 -0
- package/dist/bundle/chunks/{chunk-Z7MMKKIY.js → chunk-ZYHIRFGT.js} +1 -1
- package/dist/bundle/chunks/{cli-main-CHDR33FZ.js → cli-main-SZ54SQLA.js} +1 -1
- package/dist/bundle/chunks/{help-fast-path-EHRZEBRO.js → help-fast-path-W5W3FYYM.js} +1 -1
- package/dist/bundle/chunks/{host-command-VATE7HRG.js → host-command-YG2FIDPN.js} +1 -1
- package/dist/bundle/chunks/{host-lifecycle-GC3GIR2N.js → host-lifecycle-5AV4JXDQ.js} +1 -1
- package/dist/bundle/chunks/{interactive-mode-RVEM62WX.js → interactive-mode-SCNDQ4R6.js} +1 -1
- package/dist/bundle/chunks/{list-tips-IIRFXBTO.js → list-tips-BXAA4FG5.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-CFND7CXH.js → multi-session-host-ZDZMPNP5.js} +7 -6
- package/dist/bundle/chunks/{package-manager-cli-NWNECBF7.js → package-manager-cli-4LMYHHKH.js} +1 -1
- package/dist/bundle/chunks/{rotation-stream-C5BM7TMM.js → rotation-stream-E3XKSST7.js} +1 -1
- package/dist/bundle/chunks/{rpc-mode-JQFUDLNB.js → rpc-mode-ZFNFREN5.js} +1 -1
- package/dist/bundle/chunks/{schedule-command-EQ4PQCRP.js → schedule-command-K6CTAB4T.js} +1 -1
- package/dist/bundle/chunks/{session-control-endpoint-HMB5T5VD.js → session-control-endpoint-MVSYBGM3.js} +1 -1
- package/dist/bundle/chunks/{session-picker-O7OESJWU.js → session-picker-TNXWY5QY.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +224 -222
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/bundle/runtime-manifest.json +1 -1
- package/dist/core/agent-session-runtime.d.ts +6 -0
- package/dist/core/agent-session-runtime.js +18 -0
- package/dist/core/agent-session.js +5 -2
- package/dist/core/extensions/builtin/anthropic-subscription/auth-lane.js +3 -2
- package/dist/core/extensions/builtin/anthropic-subscription/options.js +11 -2
- package/dist/core/extensions/builtin/anthropic-subscription/session-reattach.js +4 -1
- package/dist/core/extensions/builtin/anthropic-subscription/session-sync.js +3 -0
- package/dist/core/extensions/builtin/anthropic-subscription/settings.d.ts +5 -0
- package/dist/core/extensions/builtin/anthropic-subscription/settings.js +11 -0
- package/dist/core/extensions/builtin/anthropic-subscription/stream.d.ts +1 -0
- package/dist/core/extensions/builtin/anthropic-subscription/stream.js +17 -1
- package/dist/core/extensions/builtin/compaction/lane-policy.d.ts +20 -12
- package/dist/core/extensions/builtin/compaction/lane-policy.js +39 -34
- package/dist/core/extensions/builtin/html-render/bootstrap.d.ts +15 -0
- package/dist/core/extensions/builtin/html-render/bootstrap.js +123 -0
- package/dist/core/extensions/builtin/html-render/images.d.ts +16 -0
- package/dist/core/extensions/builtin/html-render/images.js +155 -0
- package/dist/core/extensions/builtin/html-render/index.d.ts +3 -0
- package/dist/core/extensions/builtin/html-render/index.js +5 -0
- package/dist/core/extensions/builtin/html-render/tool.d.ts +19 -0
- package/dist/core/extensions/builtin/html-render/tool.js +74 -0
- package/dist/core/extensions/builtin/index.js +4 -0
- package/dist/core/extensions/builtin/mcp/auth/catalog-identity.d.ts +4 -0
- package/dist/core/extensions/builtin/mcp/auth/catalog-identity.js +41 -0
- package/dist/core/extensions/builtin/mcp/auth/legacy-lock.d.ts +3 -0
- package/dist/core/extensions/builtin/mcp/auth/legacy-lock.js +39 -0
- package/dist/core/extensions/builtin/mcp/auth/legacy-migration.d.ts +14 -0
- package/dist/core/extensions/builtin/mcp/auth/legacy-migration.js +91 -0
- package/dist/core/extensions/builtin/mcp/auth/oauth-refresh.js +1 -1
- package/dist/core/extensions/builtin/mcp/auth/token-store.d.ts +1 -0
- package/dist/core/extensions/builtin/mcp/auth/token-store.js +19 -90
- package/dist/core/extensions/builtin/mcp/catalog-cache.d.ts +7 -3
- package/dist/core/extensions/builtin/mcp/catalog-cache.js +61 -12
- package/dist/core/extensions/builtin/mcp/catalog.d.ts +3 -1
- package/dist/core/extensions/builtin/mcp/catalog.js +2 -0
- package/dist/core/extensions/builtin/mcp/connection-types.d.ts +2 -0
- package/dist/core/extensions/builtin/mcp/expose/call.d.ts +12 -0
- package/dist/core/extensions/builtin/mcp/expose/call.js +60 -0
- package/dist/core/extensions/builtin/mcp/expose/proxy.js +16 -3
- package/dist/core/extensions/builtin/mcp/expose/register.d.ts +3 -1
- package/dist/core/extensions/builtin/mcp/expose/register.js +41 -50
- package/dist/core/extensions/builtin/mcp/expose/session.d.ts +4 -0
- package/dist/core/extensions/builtin/mcp/expose/session.js +5 -1
- package/dist/core/extensions/builtin/mcp/expose/tier-b.js +15 -4
- package/dist/core/extensions/builtin/mcp/index.js +5 -26
- package/dist/core/extensions/builtin/mcp/invocation.d.ts +42 -0
- package/dist/core/extensions/builtin/mcp/invocation.js +118 -0
- package/dist/core/extensions/builtin/mcp/service-connection.d.ts +3 -0
- package/dist/core/extensions/builtin/mcp/service-connection.js +6 -0
- package/dist/core/extensions/builtin/mcp/service-register.d.ts +4 -0
- package/dist/core/extensions/builtin/mcp/service-register.js +45 -1
- package/dist/core/extensions/builtin/mcp/service-tools-changed.js +26 -3
- package/dist/core/extensions/builtin/mcp/service-types.d.ts +6 -0
- package/dist/core/extensions/builtin/mcp/service.js +83 -49
- package/dist/core/extensions/builtin/mcp/shared-connection.js +17 -4
- package/dist/core/extensions/builtin/mcp/sharing-policy.js +1 -0
- package/dist/core/extensions/builtin/mcp/startup-race.js +47 -9
- package/dist/core/extensions/builtin/permission-system/dispatch-metadata.d.ts +8 -0
- package/dist/core/extensions/builtin/permission-system/dispatch-metadata.js +23 -0
- package/dist/core/extensions/builtin/permission-system/dispatch-policy.d.ts +14 -0
- package/dist/core/extensions/builtin/permission-system/dispatch-policy.js +53 -0
- package/dist/core/extensions/builtin/permission-system/dispatch.d.ts +33 -0
- package/dist/core/extensions/builtin/permission-system/dispatch.js +106 -0
- package/dist/core/extensions/builtin/permission-system/index.js +64 -17
- package/dist/core/extensions/builtin/permission-system/prompt.d.ts +1 -1
- package/dist/core/extensions/builtin/permission-system/prompt.js +3 -1
- package/dist/core/extensions/builtin/permission-system/service.d.ts +11 -1
- package/dist/core/extensions/builtin/permission-system/service.js +14 -4
- package/dist/core/extensions/builtin/ttsr/detectors/collapse-near-duplicates.d.ts +1 -0
- package/dist/core/extensions/builtin/ttsr/detectors/collapse-near-duplicates.js +24 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/modes/interactive/interactive-mode.js +21 -4
- package/dist/modes/rpc/host-reservations.d.ts +1 -1
- package/dist/modes/rpc/host-reservations.js +9 -3
- package/dist/modes/rpc/rpc-client.js +14 -2
- package/dist/modes/rpc/rpc-request-deadline.d.ts +8 -0
- package/dist/modes/rpc/rpc-request-deadline.js +10 -0
- package/dist/modes/rpc/rpc-types.d.ts +7 -4
- package/dist/modes/rpc/session-binding.js +15 -1
- package/dist/modes/rpc/session-command-router.d.ts +1 -0
- package/dist/modes/rpc/session-command-router.js +16 -0
- package/dist/modes/rpc/session-registry-attach.js +8 -2
- package/dist/modes/rpc/session-registry.js +5 -7
- package/dist/modes/rpc/session-teardown.d.ts +6 -1
- package/dist/modes/rpc/session-teardown.js +26 -7
- package/dist/modes/rpc/session-worker-client.d.ts +2 -0
- package/dist/modes/rpc/session-worker-client.js +6 -0
- package/dist/modes/rpc/session-worker-protocol.d.ts +4 -0
- package/dist/modes/rpc/session-worker.js +6 -0
- package/dist/modes/rpc/worker-session-registry.js +6 -1
- package/docs/providers.md +3 -2
- package/docs/rpc.md +26 -10
- package/package.json +6 -6
- package/dist/bundle/chunks/chunk-RADHRQ4K.js +0 -7
- package/dist/bundle/chunks/chunk-USRAHAW4.js +0 -14
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { getAgentDir } from "../../../../config.js";
|
|
2
2
|
import { getToolSearchService, getToolSearchServiceForExtension, resetToolSearchServiceForTests, ToolSearchService, } from "../tool-search/service.js";
|
|
3
|
+
import { mcpCredentialIdentity } from "./auth/catalog-identity.js";
|
|
3
4
|
import { resolveAuthMode } from "./auth/context.js";
|
|
4
|
-
import { getValidCachedServer, readMcpCatalogCache } from "./catalog-cache.js";
|
|
5
|
+
import { cachedCatalogNeedsRefresh, getValidCachedServer, readMcpCatalogCache } from "./catalog-cache.js";
|
|
5
6
|
import { loadMcpConfig, mergeExtensionMcpServers, visitSpawnableMcpServers } from "./config.js";
|
|
6
|
-
import { collectAllPages } from "./expose/pagination.js";
|
|
7
7
|
import { cleanupMcpOutputArtifacts, McpOutputArtifacts } from "./guard/output-guard.js";
|
|
8
8
|
import { HostMcpRegistry } from "./host-registry.js";
|
|
9
9
|
import { refreshMcpInstructionsForSession } from "./instructions.js";
|
|
@@ -15,7 +15,7 @@ import { registerMcpServiceDirectTools } from "./service-register.js";
|
|
|
15
15
|
import { buildMcpServerSnapshot } from "./service-snapshot.js";
|
|
16
16
|
import { refreshMcpToolsOnListChanged, subscribeMcpToolsChanged } from "./service-tools-changed.js";
|
|
17
17
|
import { resolveSkillMcpServer } from "./skill-server.js";
|
|
18
|
-
import { MCP_ATTACH_SETTLE_TIMEOUT_MS, McpDeferredAttach, raceMcpStartupConnect,
|
|
18
|
+
import { MCP_ATTACH_SETTLE_TIMEOUT_MS, McpDeferredAttach, raceMcpStartupConnect, shouldRaceMcpStartup, } from "./startup-race.js";
|
|
19
19
|
import { safeTimer } from "./wrap.js";
|
|
20
20
|
export { registerToolsPreservingActiveSet } from "./active-set.js";
|
|
21
21
|
export class McpService {
|
|
@@ -86,7 +86,8 @@ export class McpService {
|
|
|
86
86
|
projectTrusted: sessionOptions.projectTrusted ?? ctx.isProjectTrusted(),
|
|
87
87
|
});
|
|
88
88
|
mergeExtensionMcpServers(config, ctx.getRegisteredMcpServers?.() ?? []);
|
|
89
|
-
|
|
89
|
+
// Equivalent peer attachment must not retire another session's configuration fence.
|
|
90
|
+
this.#config = JSON.stringify(this.#config) === JSON.stringify(config) ? this.#config : config;
|
|
90
91
|
const binding = _pi === undefined || this.#releasedSessions.has(_pi) ? undefined : this.#bind(_pi, ctx);
|
|
91
92
|
this.#authAgentDir = sessionOptions.agentDir;
|
|
92
93
|
this.#authEnv = sessionOptions.env;
|
|
@@ -373,7 +374,8 @@ export class McpService {
|
|
|
373
374
|
}
|
|
374
375
|
getConnection(name) {
|
|
375
376
|
const key = this.#connectionKeysByName.get(name);
|
|
376
|
-
|
|
377
|
+
const entry = key === undefined ? undefined : this.#connections.get(key);
|
|
378
|
+
return entry?.credentialsCurrent?.() === false ? undefined : entry?.connection;
|
|
377
379
|
}
|
|
378
380
|
async reconnectServer(name) {
|
|
379
381
|
const entry = this.#entryForName(name);
|
|
@@ -431,13 +433,19 @@ export class McpService {
|
|
|
431
433
|
const hadConnectionsBeforeSync = this.#connections.size > 0;
|
|
432
434
|
this.#refreshActiveSetWhenNoTools = Object.keys(config.servers).length > 0 || hadConnectionsBeforeSync;
|
|
433
435
|
const wanted = new Map();
|
|
436
|
+
const credentialIdentities = new Map();
|
|
434
437
|
visitSpawnableMcpServers(config, (name, server) => {
|
|
435
438
|
wanted.set(name, server);
|
|
439
|
+
if (server.config !== undefined) {
|
|
440
|
+
credentialIdentities.set(name, mcpCredentialIdentity(server.config, name, options.agentDir, options.env));
|
|
441
|
+
}
|
|
436
442
|
});
|
|
437
443
|
const disposals = [];
|
|
438
444
|
for (const entry of this.#connections.values()) {
|
|
439
445
|
const server = wanted.get(entry.name);
|
|
440
|
-
const key = server?.configHash === undefined
|
|
446
|
+
const key = server?.configHash === undefined
|
|
447
|
+
? undefined
|
|
448
|
+
: `${entry.name}\0${server.configHash}\0${credentialIdentities.get(entry.name) ?? "unknown"}`;
|
|
441
449
|
if (key === entry.key)
|
|
442
450
|
continue;
|
|
443
451
|
this.#connections.delete(entry.key);
|
|
@@ -449,7 +457,9 @@ export class McpService {
|
|
|
449
457
|
for (const [name, server] of wanted) {
|
|
450
458
|
if (server.config === undefined || server.configHash === undefined)
|
|
451
459
|
continue;
|
|
452
|
-
const
|
|
460
|
+
const serverConfig = server.config;
|
|
461
|
+
const credentialIdentity = credentialIdentities.get(name);
|
|
462
|
+
const key = `${name}\0${server.configHash}\0${credentialIdentity ?? "unknown"}`;
|
|
453
463
|
if (this.#connections.has(key))
|
|
454
464
|
continue;
|
|
455
465
|
const entry = createMcpSessionConnection({
|
|
@@ -460,13 +470,27 @@ export class McpService {
|
|
|
460
470
|
name,
|
|
461
471
|
configHash: server.configHash,
|
|
462
472
|
config: server.config,
|
|
473
|
+
credentialIdentity,
|
|
474
|
+
credentialsCurrent: () => mcpCredentialIdentity(serverConfig, name, options.agentDir, options.env) === credentialIdentity,
|
|
475
|
+
onCredentialsChanged: async () => {
|
|
476
|
+
if (this.#disposed || this.#config !== config || this.#entryForName(name) !== entry)
|
|
477
|
+
return;
|
|
478
|
+
await this.#syncFromConfig(config, options, true, binding);
|
|
479
|
+
for (const live of this.#liveBindings())
|
|
480
|
+
await this.#registerDirectTools(live);
|
|
481
|
+
},
|
|
463
482
|
session: options,
|
|
464
483
|
cwd: this.#sessionContext?.cwd ?? process.cwd(),
|
|
465
484
|
ui: () => this.getMcpElicitationUi(),
|
|
466
485
|
artifacts: this.#outputArtifacts,
|
|
467
|
-
shouldReconnect: (current) => !this.#disposed &&
|
|
486
|
+
shouldReconnect: (current) => !this.#disposed &&
|
|
487
|
+
this.#entryForName(name) === current &&
|
|
488
|
+
this.#config?.servers[name]?.state === "enabled" &&
|
|
489
|
+
this.#config.servers[name]?.configHash === current.configHash,
|
|
468
490
|
});
|
|
469
|
-
const cachedCatalog = useCache
|
|
491
|
+
const cachedCatalog = useCache && credentialIdentity !== undefined
|
|
492
|
+
? getValidCachedServer(cache, name, server.configHash, credentialIdentity)
|
|
493
|
+
: undefined;
|
|
470
494
|
entry.cachedCatalog = cachedCatalog;
|
|
471
495
|
this.#connections.set(key, entry);
|
|
472
496
|
this.#connectionKeysByName.set(name, key);
|
|
@@ -475,9 +499,11 @@ export class McpService {
|
|
|
475
499
|
// the background past the deadline (eager/keep-alive always; a cold lazy
|
|
476
500
|
// server that has no cached catalog also races so a slow/wedged server
|
|
477
501
|
// never gates attachSession -> before_agent_start -> the first turn).
|
|
478
|
-
// A cached lazy server
|
|
479
|
-
//
|
|
480
|
-
if (shouldRaceMcpStartup(server.config.lifecycle) ||
|
|
502
|
+
// A fresh cached lazy server connects only on demand. An aged matching
|
|
503
|
+
// catalog remains usable while its background refresh runs.
|
|
504
|
+
if (shouldRaceMcpStartup(server.config.lifecycle) ||
|
|
505
|
+
cachedCatalog === undefined ||
|
|
506
|
+
cachedCatalogNeedsRefresh(cachedCatalog)) {
|
|
481
507
|
connects.push(raceMcpStartupConnect({
|
|
482
508
|
entry,
|
|
483
509
|
pi: binding?.pi,
|
|
@@ -510,7 +536,7 @@ export class McpService {
|
|
|
510
536
|
// A later attach does not supersede this catalog: it registers in every live session. Skip it only
|
|
511
537
|
// once the service is gone or this connection was replaced (#2524 review).
|
|
512
538
|
shouldRefreshTools: () => !this.#disposed && this.#entryForName(name) === entry,
|
|
513
|
-
deadlineMs:
|
|
539
|
+
deadlineMs: 0,
|
|
514
540
|
onDeferred: (settled) => this.#deferredAttach.track(settled),
|
|
515
541
|
}));
|
|
516
542
|
}
|
|
@@ -553,6 +579,16 @@ export class McpService {
|
|
|
553
579
|
binding.registration = await registerMcpServiceDirectTools(binding.pi, config, this.#connections.values(), toolSearchService, {
|
|
554
580
|
refreshActiveSetWhenEmpty: this.#refreshActiveSetWhenNoTools,
|
|
555
581
|
onRegistered: (entry, identity) => binding.registeredIdentities.set(entry.key, identity),
|
|
582
|
+
contextRequired: binding.context.mode !== undefined,
|
|
583
|
+
sessionManager: binding.context.sessionManager,
|
|
584
|
+
isCurrent: (entry) => !this.#disposed &&
|
|
585
|
+
this.#config === config &&
|
|
586
|
+
this.#bindings.get(binding.pi) === binding &&
|
|
587
|
+
this.#entryForName(entry.name) === entry,
|
|
588
|
+
publishCurrent: async () => {
|
|
589
|
+
if (this.#bindings.get(binding.pi) === binding)
|
|
590
|
+
await this.#registerDirectTools(binding);
|
|
591
|
+
},
|
|
556
592
|
});
|
|
557
593
|
const ctx = binding.context;
|
|
558
594
|
if (ctx.mode === "rpc") {
|
|
@@ -571,9 +607,17 @@ export class McpService {
|
|
|
571
607
|
return ((binding === undefined ? undefined : this.#toolSearchFor(binding)?.maybeRehydrateFromHistory(messages)) ?? []);
|
|
572
608
|
}
|
|
573
609
|
#serverSnapshot(name) {
|
|
610
|
+
const entry = this.#entryForName(name);
|
|
611
|
+
const config = this.#config?.servers[name]?.config;
|
|
574
612
|
const connection = this.getConnection(name);
|
|
575
613
|
connection?.refreshCapturedDiagnostics();
|
|
576
|
-
|
|
614
|
+
const snapshot = buildMcpServerSnapshot(name, this.#config?.servers[name], connection, entry);
|
|
615
|
+
if (config !== undefined &&
|
|
616
|
+
entry?.connection.state === "needs_auth" &&
|
|
617
|
+
mcpCredentialIdentity(config, name, entry.agentDir, this.#authEnv) === undefined) {
|
|
618
|
+
return { ...snapshot, lifecycleState: "needs_auth", lastError: entry.connection.lastError?.message ?? null };
|
|
619
|
+
}
|
|
620
|
+
return snapshot;
|
|
577
621
|
}
|
|
578
622
|
async #captureWireStatus(sessionId) {
|
|
579
623
|
const config = this.#config;
|
|
@@ -594,42 +638,19 @@ export class McpService {
|
|
|
594
638
|
}
|
|
595
639
|
async #captureWireStatusServer(name, server) {
|
|
596
640
|
const entry = this.#entryForName(name);
|
|
597
|
-
const connection =
|
|
641
|
+
const connection = this.getConnection(name);
|
|
598
642
|
const connected = connection?.state === "connected";
|
|
599
|
-
const
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
643
|
+
const needsAuth = !connected && this.#serverSnapshot(name).lifecycleState === "needs_auth";
|
|
644
|
+
const cached = entry?.credentialsCurrent?.() === false ? undefined : entry?.cachedCatalog;
|
|
645
|
+
const tools = cached?.tools ?? [];
|
|
646
|
+
const resources = cached?.resources ?? [];
|
|
647
|
+
const resourceTemplates = cached?.resourceTemplates ?? [];
|
|
603
648
|
let serverInfo = null;
|
|
604
649
|
if (connected && connection !== undefined) {
|
|
605
650
|
const client = connection.client;
|
|
606
651
|
const version = client.getServerVersion();
|
|
607
652
|
if (version !== undefined)
|
|
608
653
|
serverInfo = mapWireServerInfo(version);
|
|
609
|
-
try {
|
|
610
|
-
tools = (await collectAllPages((cursor) => client.listTools(cursor === undefined ? {} : { cursor }))).items;
|
|
611
|
-
}
|
|
612
|
-
catch (error) {
|
|
613
|
-
if (!(error instanceof Error))
|
|
614
|
-
throw error;
|
|
615
|
-
tools = cached?.tools ?? [];
|
|
616
|
-
}
|
|
617
|
-
try {
|
|
618
|
-
resources = (await collectAllPages((cursor) => client.listResources(cursor === undefined ? {} : { cursor }))).items;
|
|
619
|
-
}
|
|
620
|
-
catch (error) {
|
|
621
|
-
if (!(error instanceof Error))
|
|
622
|
-
throw error;
|
|
623
|
-
resources = cached?.resources ?? [];
|
|
624
|
-
}
|
|
625
|
-
try {
|
|
626
|
-
resourceTemplates = (await collectAllPages((cursor) => client.listResourceTemplates(cursor === undefined ? {} : { cursor }))).items;
|
|
627
|
-
}
|
|
628
|
-
catch (error) {
|
|
629
|
-
if (!(error instanceof Error))
|
|
630
|
-
throw error;
|
|
631
|
-
resourceTemplates = [];
|
|
632
|
-
}
|
|
633
654
|
}
|
|
634
655
|
return {
|
|
635
656
|
name,
|
|
@@ -637,10 +658,16 @@ export class McpService {
|
|
|
637
658
|
tools: tools.map(mapWireTool),
|
|
638
659
|
resources: resources.map(mapWireResource),
|
|
639
660
|
resourceTemplates: resourceTemplates.map(mapWireResourceTemplate),
|
|
640
|
-
authStatus: wireAuthStatus(entry, server),
|
|
661
|
+
authStatus: wireAuthStatus(entry, server, this.#authEnv),
|
|
641
662
|
...(connection?.state === undefined && server?.state === undefined
|
|
642
663
|
? {}
|
|
643
|
-
: {
|
|
664
|
+
: {
|
|
665
|
+
status: needsAuth
|
|
666
|
+
? "needs_auth"
|
|
667
|
+
: entry?.startupCatalogClaim?.ownsRegistration()
|
|
668
|
+
? "connecting"
|
|
669
|
+
: (connection?.state ?? server?.state),
|
|
670
|
+
}),
|
|
644
671
|
};
|
|
645
672
|
}
|
|
646
673
|
#entryForName(name) {
|
|
@@ -651,6 +678,9 @@ export class McpService {
|
|
|
651
678
|
this.#mcpInstructions = instructions;
|
|
652
679
|
}
|
|
653
680
|
getMcpInstructions() {
|
|
681
|
+
if ([...this.#connections.values()].some((entry) => entry.credentialsCurrent?.() === false)) {
|
|
682
|
+
refreshMcpInstructionsForSession(this);
|
|
683
|
+
}
|
|
654
684
|
return this.#mcpInstructions;
|
|
655
685
|
}
|
|
656
686
|
setMcpElicitationUiProvider(provider) {
|
|
@@ -690,10 +720,11 @@ export class McpService {
|
|
|
690
720
|
}
|
|
691
721
|
/** Live auth status without fetching catalogs or exposing credentials. */
|
|
692
722
|
getServerAuthStatus(name) {
|
|
693
|
-
return wireAuthStatus(this.#entryForName(name), this.#config?.servers[name]);
|
|
723
|
+
return wireAuthStatus(this.#entryForName(name), this.#config?.servers[name], this.#authEnv);
|
|
694
724
|
}
|
|
695
725
|
getCachedInstructions(name) {
|
|
696
|
-
|
|
726
|
+
const entry = this.#entryForName(name);
|
|
727
|
+
return entry?.credentialsCurrent?.() === false ? undefined : entry?.cachedCatalog?.instructions;
|
|
697
728
|
}
|
|
698
729
|
/** Resolved `settings.nativeToolSearch` (auto | true | false | undefined).
|
|
699
730
|
* Drives the native provider tool-search adapter gate. */
|
|
@@ -701,7 +732,7 @@ export class McpService {
|
|
|
701
732
|
return this.#config?.settings.nativeToolSearch;
|
|
702
733
|
}
|
|
703
734
|
}
|
|
704
|
-
function wireAuthStatus(entry, server) {
|
|
735
|
+
function wireAuthStatus(entry, server, env) {
|
|
705
736
|
const mode = entry?.authPlan?.mode ?? (server?.config === undefined ? "none" : resolveAuthMode(server.config));
|
|
706
737
|
switch (mode) {
|
|
707
738
|
case "none":
|
|
@@ -709,7 +740,10 @@ function wireAuthStatus(entry, server) {
|
|
|
709
740
|
case "bearer":
|
|
710
741
|
return entry?.connection.state === "needs_auth" ? "notLoggedIn" : "bearerToken";
|
|
711
742
|
case "oauth":
|
|
712
|
-
return
|
|
743
|
+
return server?.config === undefined ||
|
|
744
|
+
mcpCredentialIdentity(server.config, server.name, entry?.agentDir, env) === undefined
|
|
745
|
+
? "notLoggedIn"
|
|
746
|
+
: "oAuth";
|
|
713
747
|
default:
|
|
714
748
|
return assertNever(mode);
|
|
715
749
|
}
|
|
@@ -149,14 +149,27 @@ export class SharedMcpConnection {
|
|
|
149
149
|
}
|
|
150
150
|
async #collectCatalog() {
|
|
151
151
|
const generation = this.connection.generation;
|
|
152
|
-
const
|
|
153
|
-
|
|
152
|
+
const ownsCatalog = () => generation === this.connection.generation &&
|
|
153
|
+
!this.#disposed &&
|
|
154
|
+
[...this.leases].some((lease) => lease.options.credentialsCurrent?.() !== false);
|
|
155
|
+
const catalog = await collectServerCatalogForCache(this.connection, this.#options.config, this.#options.configHash, this.#options.credentialIdentity);
|
|
156
|
+
if (!ownsCatalog())
|
|
154
157
|
return catalog;
|
|
155
158
|
if (this.#cacheGeneration !== generation) {
|
|
156
159
|
this.#cacheGeneration = generation;
|
|
157
|
-
|
|
160
|
+
try {
|
|
161
|
+
await writeMcpCachedServer(this.#options.agentDir, this.#options.serverName, catalog, ownsCatalog);
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
for (const lease of this.leases) {
|
|
165
|
+
lease.options.logger.warn("Failed to persist MCP catalog cache", {
|
|
166
|
+
error: error instanceof Error ? error.message : String(error),
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
158
170
|
}
|
|
159
|
-
|
|
171
|
+
if (ownsCatalog())
|
|
172
|
+
await ensureMcpResourceSubscriptions(this.connection.client, catalog.resources);
|
|
160
173
|
return catalog;
|
|
161
174
|
}
|
|
162
175
|
async dispose() {
|
|
@@ -21,6 +21,7 @@ export function sharedMcpKey(options, agentDir) {
|
|
|
21
21
|
const { config, env } = options;
|
|
22
22
|
const bearer = config.bearerTokenEnv;
|
|
23
23
|
const identity = {
|
|
24
|
+
credentialIdentity: options.credentialIdentity,
|
|
24
25
|
agentDir,
|
|
25
26
|
name: options.serverName,
|
|
26
27
|
type: config.type,
|
|
@@ -64,6 +64,7 @@ export async function raceMcpStartupConnect(options) {
|
|
|
64
64
|
const claim = {
|
|
65
65
|
cachedCatalog: entry.cachedCatalog,
|
|
66
66
|
ownsRegistration: () => pi !== undefined && options.shouldRefreshTools(),
|
|
67
|
+
settled: () => connect,
|
|
67
68
|
};
|
|
68
69
|
entry.startupCatalogClaim = claim;
|
|
69
70
|
const releaseClaim = () => {
|
|
@@ -76,8 +77,22 @@ export async function raceMcpStartupConnect(options) {
|
|
|
76
77
|
return;
|
|
77
78
|
options.onDeferred(connect.then(() => refreshMcpToolsAfterStartupRace(options)));
|
|
78
79
|
}
|
|
79
|
-
|
|
80
|
-
|
|
80
|
+
const catalogRefreshes = new WeakMap();
|
|
81
|
+
export function connectAndRefreshMcpCatalog(entry, serverConfig) {
|
|
82
|
+
const pending = catalogRefreshes.get(entry);
|
|
83
|
+
if (pending !== undefined)
|
|
84
|
+
return pending;
|
|
85
|
+
const refresh = refreshConnectedMcpCatalog(entry, serverConfig);
|
|
86
|
+
catalogRefreshes.set(entry, refresh);
|
|
87
|
+
const finish = () => {
|
|
88
|
+
if (catalogRefreshes.get(entry) === refresh)
|
|
89
|
+
catalogRefreshes.delete(entry);
|
|
90
|
+
};
|
|
91
|
+
void refresh.then(finish, finish);
|
|
92
|
+
return refresh;
|
|
93
|
+
}
|
|
94
|
+
async function refreshConnectedMcpCatalog(entry, serverConfig) {
|
|
95
|
+
if (serverConfig === undefined || entry.isCurrent?.() === false)
|
|
81
96
|
return;
|
|
82
97
|
try {
|
|
83
98
|
await entry.authPlan?.refresh?.ensureFresh();
|
|
@@ -90,24 +105,47 @@ export async function connectAndRefreshMcpCatalog(entry, serverConfig) {
|
|
|
90
105
|
}
|
|
91
106
|
throw error;
|
|
92
107
|
}
|
|
108
|
+
if (entry.credentialsCurrent?.() === false) {
|
|
109
|
+
await entry.onCredentialsChanged?.();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
93
112
|
await connectMcpServer(entry.connection, entry.logger);
|
|
94
|
-
if (entry.connection.state !== "connected")
|
|
113
|
+
if (entry.connection.state !== "connected" || entry.isCurrent?.() === false)
|
|
95
114
|
return;
|
|
115
|
+
const generation = entry.connection.generation;
|
|
116
|
+
const ownsCatalog = () => entry.isCurrent?.() !== false &&
|
|
117
|
+
entry.credentialsCurrent?.() !== false &&
|
|
118
|
+
entry.connection.state === "connected" &&
|
|
119
|
+
entry.connection.generation === generation;
|
|
96
120
|
if (entry.connection instanceof SharedMcpLease) {
|
|
97
|
-
|
|
121
|
+
const catalog = await entry.connection.catalog();
|
|
122
|
+
if (!ownsCatalog()) {
|
|
123
|
+
if (entry.credentialsCurrent?.() === false)
|
|
124
|
+
await entry.onCredentialsChanged?.();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
entry.cachedCatalog = catalog;
|
|
128
|
+
entry.catalogGeneration = generation;
|
|
98
129
|
entry.cacheRefreshedAfterConnect = true;
|
|
99
130
|
return;
|
|
100
131
|
}
|
|
101
|
-
if (entry.cacheRefreshedAfterConnect)
|
|
132
|
+
if (entry.cacheRefreshedAfterConnect && entry.catalogGeneration === generation)
|
|
102
133
|
return;
|
|
103
|
-
entry.cacheRefreshedAfterConnect = true;
|
|
104
134
|
try {
|
|
105
|
-
const catalog = await collectServerCatalogForCache(entry.connection, serverConfig, entry.configHash);
|
|
135
|
+
const catalog = await collectServerCatalogForCache(entry.connection, serverConfig, entry.configHash, entry.credentialIdentity);
|
|
136
|
+
if (!ownsCatalog()) {
|
|
137
|
+
if (entry.credentialsCurrent?.() === false)
|
|
138
|
+
await entry.onCredentialsChanged?.();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
106
141
|
entry.cachedCatalog = catalog;
|
|
107
|
-
|
|
142
|
+
entry.catalogGeneration = generation;
|
|
143
|
+
entry.cacheRefreshedAfterConnect = true;
|
|
144
|
+
await writeMcpCachedServer(entry.agentDir, entry.name, catalog, ownsCatalog);
|
|
108
145
|
// Per-resource subscriptions (todo 39): only when the server declares
|
|
109
146
|
// resources.subscribe; best-effort, failures are non-fatal.
|
|
110
|
-
|
|
147
|
+
if (ownsCatalog())
|
|
148
|
+
await ensureMcpResourceSubscriptions(entry.connection.client, catalog.resources ?? []);
|
|
111
149
|
}
|
|
112
150
|
catch (error) {
|
|
113
151
|
entry.logger.warn("Failed to refresh MCP catalog cache", {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext, ToolCallEvent } from "../../types.ts";
|
|
2
|
+
import { type DispatchAuthorizer, type DispatchIdentity } from "./dispatch.ts";
|
|
3
|
+
type IdentityForInput = (input: Record<string, unknown>) => DispatchIdentity | undefined;
|
|
4
|
+
export declare function registerDispatchIdentity(parameters: object, identity: IdentityForInput): void;
|
|
5
|
+
export declare function getDispatchIdentity(parameters: object, input: Record<string, unknown>): DispatchIdentity | undefined;
|
|
6
|
+
export declare function prepareMcpDispatchApproval(pi: Pick<ExtensionAPI, "getAllTools">, event: ToolCallEvent, ctx: ExtensionContext, authorizer: DispatchAuthorizer): (() => void) | undefined;
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=dispatch-metadata.d.ts.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { prepareDispatchApproval } from "./dispatch.js";
|
|
2
|
+
// ToolInfo retains the registered schema object. Its identity scopes metadata
|
|
3
|
+
// to the offered definition, without adding private fields to provider schemas.
|
|
4
|
+
const identities = new WeakMap();
|
|
5
|
+
export function registerDispatchIdentity(parameters, identity) {
|
|
6
|
+
identities.set(parameters, identity);
|
|
7
|
+
}
|
|
8
|
+
export function getDispatchIdentity(parameters, input) {
|
|
9
|
+
return identities.get(parameters)?.(input);
|
|
10
|
+
}
|
|
11
|
+
export function prepareMcpDispatchApproval(pi, event, ctx, authorizer) {
|
|
12
|
+
// Other tools retain their existing preflight path, including calls while
|
|
13
|
+
// the extension runtime is still starting and cannot list tools (#2617).
|
|
14
|
+
if (!event.toolName.startsWith("mcp_"))
|
|
15
|
+
return undefined;
|
|
16
|
+
const tool = pi.getAllTools().find((candidate) => candidate.name === event.toolName);
|
|
17
|
+
const identity = tool === undefined ? undefined : getDispatchIdentity(tool.parameters, event.input);
|
|
18
|
+
if (identity === undefined)
|
|
19
|
+
return undefined;
|
|
20
|
+
const request = { ...event, identity };
|
|
21
|
+
return prepareDispatchApproval(ctx.sessionManager, request, authorizer.policy(request, ctx));
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=dispatch-metadata.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext, ToolCallEvent, ToolCallEventResult } from "../../types.ts";
|
|
2
|
+
import type { DispatchAuthorizer } from "./dispatch.ts";
|
|
3
|
+
import { type ParserRegistry } from "./parsers.ts";
|
|
4
|
+
import type { PermissionService } from "./service.ts";
|
|
5
|
+
import type { PermissionPresetName } from "./types.ts";
|
|
6
|
+
export interface DispatchPermissionState {
|
|
7
|
+
readonly service: PermissionService | null;
|
|
8
|
+
readonly parsers: ParserRegistry | null;
|
|
9
|
+
readonly preset: PermissionPresetName | null;
|
|
10
|
+
readonly setupError: string | null;
|
|
11
|
+
}
|
|
12
|
+
/** Use the permission extension's live parser, evaluator and approval handler. */
|
|
13
|
+
export declare function createDispatchAuthorizer(pi: Pick<ExtensionAPI, "getAllTools">, stateFor: (ctx: ExtensionContext) => DispatchPermissionState, authorize: (event: ToolCallEvent, ctx: ExtensionContext, signal?: AbortSignal) => Promise<ToolCallEventResult | undefined>): DispatchAuthorizer;
|
|
14
|
+
//# sourceMappingURL=dispatch-policy.d.ts.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { toolOwnedPermissionRequests } from "./parsers.js";
|
|
2
|
+
/** Use the permission extension's live parser, evaluator and approval handler. */
|
|
3
|
+
export function createDispatchAuthorizer(pi, stateFor, authorize) {
|
|
4
|
+
return {
|
|
5
|
+
policy(request, ctx) {
|
|
6
|
+
const state = stateFor(ctx);
|
|
7
|
+
if (state.setupError !== null)
|
|
8
|
+
throw new Error(`Permission setup failed: ${state.setupError}`);
|
|
9
|
+
const { service, parsers } = state;
|
|
10
|
+
if (!service || !parsers)
|
|
11
|
+
throw new Error("Permission authorizer is not initialized");
|
|
12
|
+
const requests = parsers.has(request.toolName)
|
|
13
|
+
? parsers.parse(request.toolName, request.input, ctx.cwd)
|
|
14
|
+
: (toolOwnedPermissionRequests(pi.getAllTools(), request.toolName, request.input, ctx.cwd) ??
|
|
15
|
+
parsers.parse(request.toolName, request.input, ctx.cwd));
|
|
16
|
+
let action = "allow";
|
|
17
|
+
const decisions = requests.map((permission) => {
|
|
18
|
+
const rules = permission.patterns.map((pattern) => service.dispatchDecision(permission.permission, permission.ruleAliases ?? pattern, {
|
|
19
|
+
presetBound: state.preset === "auto",
|
|
20
|
+
}));
|
|
21
|
+
for (const decision of rules) {
|
|
22
|
+
if (decision.action === "deny")
|
|
23
|
+
action = "deny";
|
|
24
|
+
else if (decision.action === "ask" && !permission.autoApproveAsk && action !== "deny")
|
|
25
|
+
action = "ask";
|
|
26
|
+
}
|
|
27
|
+
return { permission, rules };
|
|
28
|
+
});
|
|
29
|
+
return {
|
|
30
|
+
action,
|
|
31
|
+
fingerprint: JSON.stringify([
|
|
32
|
+
ctx.sessionManager.getSessionId(),
|
|
33
|
+
ctx.cwd,
|
|
34
|
+
state.preset === "auto",
|
|
35
|
+
decisions,
|
|
36
|
+
]),
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
async ask(request, ctx, signal) {
|
|
40
|
+
signal?.throwIfAborted();
|
|
41
|
+
const result = await authorize({
|
|
42
|
+
type: "tool_call",
|
|
43
|
+
toolCallId: request.toolCallId,
|
|
44
|
+
toolName: request.toolName,
|
|
45
|
+
input: request.input,
|
|
46
|
+
...(request.parentToolCallId === undefined ? {} : { parentToolCallId: request.parentToolCallId }),
|
|
47
|
+
}, ctx, signal);
|
|
48
|
+
if (result?.block)
|
|
49
|
+
throw new Error(result.reason ?? "Permission request was rejected.");
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=dispatch-policy.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ExtensionContext } from "../../types.ts";
|
|
2
|
+
export interface DispatchIdentity {
|
|
3
|
+
readonly owner: object;
|
|
4
|
+
readonly metadata: string;
|
|
5
|
+
readonly operation?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface DispatchRequest {
|
|
8
|
+
readonly toolCallId: string;
|
|
9
|
+
readonly parentToolCallId?: string;
|
|
10
|
+
readonly toolName: string;
|
|
11
|
+
readonly input: Record<string, unknown>;
|
|
12
|
+
readonly identity: DispatchIdentity;
|
|
13
|
+
}
|
|
14
|
+
export interface DispatchPolicy {
|
|
15
|
+
readonly action: "allow" | "ask" | "deny";
|
|
16
|
+
readonly fingerprint: string;
|
|
17
|
+
}
|
|
18
|
+
export interface DispatchAuthorizer {
|
|
19
|
+
policy(request: DispatchRequest, ctx: ExtensionContext): DispatchPolicy;
|
|
20
|
+
ask(request: DispatchRequest, ctx: ExtensionContext, signal: AbortSignal | undefined): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
/** Retirement cannot remove the authorizer installed by a newer extension load. */
|
|
23
|
+
export declare function registerDispatchAuthorizer(session: object, authorizer: DispatchAuthorizer): () => void;
|
|
24
|
+
export declare function hasDispatchAuthorizer(session: object): boolean;
|
|
25
|
+
/** Keep the policy and operation that were actually presented, not their post-reply replacements. */
|
|
26
|
+
export declare function prepareDispatchApproval(session: object, request: DispatchRequest, policy: DispatchPolicy): () => void;
|
|
27
|
+
export declare function forgetDispatchApproval(input: object): void;
|
|
28
|
+
/**
|
|
29
|
+
* Resolve live permission after MCP readiness. The returned synchronous fence must
|
|
30
|
+
* pass immediately before dispatch; a false result requires preparation again.
|
|
31
|
+
*/
|
|
32
|
+
export declare function authorizeToolDispatch(session: object, request: DispatchRequest, ctx: ExtensionContext, signal: AbortSignal | undefined): Promise<() => boolean>;
|
|
33
|
+
//# sourceMappingURL=dispatch.d.ts.map
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { DeniedError } from "./types.js";
|
|
2
|
+
const authorizers = new WeakMap();
|
|
3
|
+
// AgentSession passes this validated object unchanged from preflight to execution.
|
|
4
|
+
// Identical calls, including calls with duplicate provider IDs, get distinct objects.
|
|
5
|
+
const approvals = new WeakMap();
|
|
6
|
+
/** Retirement cannot remove the authorizer installed by a newer extension load. */
|
|
7
|
+
export function registerDispatchAuthorizer(session, authorizer) {
|
|
8
|
+
const previous = authorizers.get(session);
|
|
9
|
+
const registration = { authorizer, retired: new AbortController() };
|
|
10
|
+
authorizers.set(session, registration);
|
|
11
|
+
previous?.retired.abort(new Error("Permission authorizer was retired"));
|
|
12
|
+
return () => {
|
|
13
|
+
// Retain the retired registration so required authority cannot become
|
|
14
|
+
// indistinguishable from a session where enforcement was never loaded.
|
|
15
|
+
registration.retired.abort(new Error("Permission authorizer was retired"));
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function hasDispatchAuthorizer(session) {
|
|
19
|
+
return authorizers.has(session);
|
|
20
|
+
}
|
|
21
|
+
/** Keep the policy and operation that were actually presented, not their post-reply replacements. */
|
|
22
|
+
export function prepareDispatchApproval(session, request, policy) {
|
|
23
|
+
const registration = authorizers.get(session);
|
|
24
|
+
if (!registration)
|
|
25
|
+
throw new Error("Permission authorizer is unavailable");
|
|
26
|
+
registration.retired.signal.throwIfAborted();
|
|
27
|
+
const approval = {
|
|
28
|
+
registration,
|
|
29
|
+
toolCallId: request.toolCallId,
|
|
30
|
+
toolName: request.toolName,
|
|
31
|
+
input: JSON.stringify(request.input),
|
|
32
|
+
identity: { ...request.identity },
|
|
33
|
+
policy: { ...policy },
|
|
34
|
+
};
|
|
35
|
+
return () => {
|
|
36
|
+
registration.retired.signal.throwIfAborted();
|
|
37
|
+
if (authorizers.get(session) !== registration)
|
|
38
|
+
throw new Error("Permission authorizer was retired");
|
|
39
|
+
approvals.set(request.input, approval);
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export function forgetDispatchApproval(input) {
|
|
43
|
+
approvals.delete(input);
|
|
44
|
+
}
|
|
45
|
+
function matches(approval, registration, request, policy) {
|
|
46
|
+
return (approval?.registration === registration &&
|
|
47
|
+
approval.toolCallId === request.toolCallId &&
|
|
48
|
+
approval.toolName === request.toolName &&
|
|
49
|
+
approval.input === JSON.stringify(request.input) &&
|
|
50
|
+
approval.identity.owner === request.identity.owner &&
|
|
51
|
+
approval.identity.metadata === request.identity.metadata &&
|
|
52
|
+
approval.policy.action === policy.action &&
|
|
53
|
+
approval.policy.fingerprint === policy.fingerprint);
|
|
54
|
+
}
|
|
55
|
+
/** Await only this caller's approval; aborting it does not retire another caller's authority. */
|
|
56
|
+
async function awaitApproval(pending, signal) {
|
|
57
|
+
if (!signal)
|
|
58
|
+
return pending;
|
|
59
|
+
let onAbort = () => { };
|
|
60
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
61
|
+
onAbort = () => reject(signal.reason);
|
|
62
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
63
|
+
if (signal.aborted)
|
|
64
|
+
onAbort();
|
|
65
|
+
});
|
|
66
|
+
try {
|
|
67
|
+
await Promise.race([pending, aborted]);
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
signal.removeEventListener("abort", onAbort);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Resolve live permission after MCP readiness. The returned synchronous fence must
|
|
75
|
+
* pass immediately before dispatch; a false result requires preparation again.
|
|
76
|
+
*/
|
|
77
|
+
export async function authorizeToolDispatch(session, request, ctx, signal) {
|
|
78
|
+
signal?.throwIfAborted();
|
|
79
|
+
const registration = authorizers.get(session);
|
|
80
|
+
if (!registration)
|
|
81
|
+
throw new Error("Permission authorizer is unavailable");
|
|
82
|
+
registration.retired.signal.throwIfAborted();
|
|
83
|
+
const policy = registration.authorizer.policy(request, ctx);
|
|
84
|
+
if (policy.action === "deny")
|
|
85
|
+
throw new DeniedError(["*"]);
|
|
86
|
+
if (policy.action === "ask" && !matches(approvals.get(request.input), registration, request, policy)) {
|
|
87
|
+
const approve = prepareDispatchApproval(session, request, policy);
|
|
88
|
+
const waitSignal = signal === undefined ? registration.retired.signal : AbortSignal.any([signal, registration.retired.signal]);
|
|
89
|
+
waitSignal.throwIfAborted();
|
|
90
|
+
await awaitApproval(registration.authorizer.ask(request, ctx, waitSignal), waitSignal);
|
|
91
|
+
signal?.throwIfAborted();
|
|
92
|
+
approve();
|
|
93
|
+
}
|
|
94
|
+
const approvedInput = JSON.stringify(request.input);
|
|
95
|
+
return () => {
|
|
96
|
+
if (signal?.aborted || registration.retired.signal.aborted || authorizers.get(session) !== registration)
|
|
97
|
+
return false;
|
|
98
|
+
if (JSON.stringify(request.input) !== approvedInput)
|
|
99
|
+
return false;
|
|
100
|
+
const current = registration.authorizer.policy(request, ctx);
|
|
101
|
+
if (current.action === "deny")
|
|
102
|
+
return false;
|
|
103
|
+
return current.action === "allow" || matches(approvals.get(request.input), registration, request, current);
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=dispatch.js.map
|