Repository navigation
fix(server): switching providers no longer revokes the other provider's T3 tool access #36801
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,6 @@ | ||
| // @effect-diagnostics nodeBuiltinImport:off -- Pure synchronous hashing keeps page matching bounded without a runtime dependency. | ||
| import * as NodeCrypto from "node:crypto"; | ||
| import type { ThreadId } from "@t3tools/contracts"; | ||
| import type { ProviderInstanceId, ThreadId } from "@t3tools/contracts"; | ||
| import type { HtmlRenderReference } from "@t3tools/shared/htmlRender"; | ||
| import * as Context from "effect/Context"; | ||
| import * as Effect from "effect/Effect"; | ||
|
|
@@ -32,8 +32,11 @@ export class McpProviderSessions extends Context.Service< | |
| McpProviderSessions, | ||
| { | ||
| readonly set: (config: McpProviderSessionConfig) => Effect.Effect<void>; | ||
| readonly read: (threadId: ThreadId) => Effect.Effect<McpProviderSessionConfig | undefined>; | ||
| readonly clear: (threadId: ThreadId) => Effect.Effect<void>; | ||
| readonly read: ( | ||
| threadId: ThreadId, | ||
| providerInstanceId: ProviderInstanceId, | ||
| ) => Effect.Effect<McpProviderSessionConfig | undefined>; | ||
| readonly clear: (threadId: ThreadId, providerSessionId?: string) => Effect.Effect<void>; | ||
| readonly recordHtmlRender: ( | ||
| threadId: ThreadId, | ||
| input: { | ||
|
|
@@ -100,37 +103,57 @@ const matchingPageIndex = (kept: ReadonlyArray<KeptHtmlRender>, input: PageInput | |
| }; | ||
|
|
||
| const make = Effect.gen(function* () { | ||
| const sessions = yield* Ref.make(new Map<ThreadId, McpProviderSessionConfig>()); | ||
| const sessions = yield* Ref.make( | ||
| new Map<ThreadId, ReadonlyMap<ProviderInstanceId, McpProviderSessionConfig>>(), | ||
| ); | ||
| const htmlRenders = yield* Ref.make(new Map<ThreadId, ReadonlyArray<KeptHtmlRender>>()); | ||
| return McpProviderSessions.of({ | ||
| set: Effect.fn("McpProviderSessions.set")(function* (config) { | ||
| const current = yield* Ref.get(sessions); | ||
| if (current.get(config.threadId)?.providerSessionId !== config.providerSessionId) { | ||
| const previous = current.get(config.threadId)?.get(config.providerInstanceId); | ||
| if (previous?.providerSessionId !== config.providerSessionId) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Preserve thread renders when adding another instance. If A has a credential and a recorded render, adding B makes Make
|
||
| yield* Ref.update(htmlRenders, (kept) => { | ||
| const next = new Map(kept); | ||
| next.delete(config.threadId); | ||
| return next; | ||
| }); | ||
| } | ||
| yield* Ref.update(sessions, (current) => new Map(current).set(config.threadId, config)); | ||
| yield* Ref.update(sessions, (current) => | ||
| new Map(current).set( | ||
| config.threadId, | ||
| new Map(current.get(config.threadId)).set(config.providerInstanceId, config), | ||
| ), | ||
| ); | ||
| }), | ||
| read: Effect.fn("McpProviderSessions.read")(function (threadId) { | ||
| return Ref.get(sessions).pipe(Effect.map((current) => current.get(threadId))); | ||
| read: Effect.fn("McpProviderSessions.read")(function (threadId, providerInstanceId) { | ||
| return Ref.get(sessions).pipe( | ||
| Effect.map((current) => current.get(threadId)?.get(providerInstanceId)), | ||
| ); | ||
| }), | ||
| clear: Effect.fn("McpProviderSessions.clear")(function (threadId) { | ||
| return Ref.update(sessions, (current) => { | ||
| clear: Effect.fn("McpProviderSessions.clear")(function* (threadId, providerSessionId) { | ||
| const threadCleared = yield* Ref.modify(sessions, (current) => { | ||
| const next = new Map(current); | ||
| const byInstance = current.get(threadId); | ||
| if (providerSessionId === undefined || byInstance === undefined) { | ||
| next.delete(threadId); | ||
| return [true, next] as const; | ||
| } | ||
| const remaining = new Map( | ||
| Array.from(byInstance).filter( | ||
| ([, config]) => config.providerSessionId !== providerSessionId, | ||
| ), | ||
| ); | ||
| if (remaining.size === 0) next.delete(threadId); | ||
| else next.set(threadId, remaining); | ||
| return [remaining.size === 0, next] as const; | ||
| }); | ||
| // Kept renders belong to the thread, so another instance's live credential keeps them. | ||
| if (!threadCleared) return; | ||
| yield* Ref.update(htmlRenders, (current) => { | ||
| const next = new Map(current); | ||
| next.delete(threadId); | ||
| return next; | ||
| }).pipe( | ||
| Effect.andThen( | ||
| Ref.update(htmlRenders, (current) => { | ||
| const next = new Map(current); | ||
| next.delete(threadId); | ||
| return next; | ||
| }), | ||
| ), | ||
| ); | ||
| }); | ||
| }), | ||
| recordHtmlRender: Effect.fn("McpProviderSessions.recordHtmlRender")( | ||
| function (threadId, input, reference) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Revoke retained preview and device grants when agent access is turned off.
Switching from A to B with browser or device access disabled does not invalidate A's retained credential. The replacement revocation at
ProviderSessionManager.ts:601runs only for the entry of the same provider instance. MCP authorization trusts the grants stored at issuance (McpSessionRegistry.ts:138-142,166-181), andrequireMcpCapabilitychecks only that stored set. A's credential therefore keepspreviewordeviceuntil it is next replaced or revoked, or the thread is archived or deleted. The user's opt-out does not apply to that session in the meantime.Ensure the opt-out takes effect for every retained credential of the thread. Either revoke the thread's credentials that hold
previewordevicewhen those settings change, or check the effective settings when a tool call is authorized.