Repository navigation
fix(server): hand-offs no longer revoke a retained provider's T3 tool access #37005
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
base: main
Are you sure you want to change the base?
Changes from all commits
485a26c
a08d422
f72a503
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -547,6 +547,8 @@ export const layerWithOptions = ( | |
| const isMcpCredentialReserved = (threadId: ThreadId, mcpCredentialId: string) => | ||
| (mcpCredentialReservations.get(mcpReservationKey(threadId, mcpCredentialId)) ?? 0) > 0; | ||
| const mcpPrepareLock = yield* KeyedLock.make<ThreadId>(); | ||
| /** The thread's MCP slot holds one config; sessions still attached get theirs back on reuse. */ | ||
| const mcpConfigByCredentialId = new Map<string, Parameters<typeof mcpSessions.set>[0]>(); | ||
| /** Serialize prepares per thread so concurrent rotations cannot revoke each other's credentials. */ | ||
| const prepareMcpSession = ( | ||
| threadId: ThreadId, | ||
|
|
@@ -599,42 +601,96 @@ export const layerWithOptions = ( | |
| } | ||
| dropMcpCredentialReservation(threadId, existing.providerSessionId); | ||
| } | ||
| yield* mcpSessionRegistry.revokeThread(threadId); | ||
| // A hand-off can keep a busy session attached; its process still uses its token | ||
| // unless agent access was narrowed since it was issued. | ||
| const held = new Set<string>(); | ||
| for (const entry of (yield* Ref.get(sessions)).values()) { | ||
| const heldId = entry.mcpCredentialIdByThread.get(threadId); | ||
| const heldCapabilities = | ||
| heldId === undefined | ||
| ? undefined | ||
| : mcpConfigByCredentialId.get(heldId)?.capabilities; | ||
| if ( | ||
| heldId !== undefined && | ||
| entry.attachedThreadIds.has(threadId) && | ||
| heldCapabilities?.has("preview") === browserToolsAvailable && | ||
| heldCapabilities.has("device") === deviceToolsAvailable | ||
| ) { | ||
| held.add(heldId); | ||
| } | ||
| } | ||
| yield* mcpSessionRegistry.revokeThread(threadId, held); | ||
|
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. Retained sessions keep device access after the setting is turned off. At prepare time, device access comes from current settings ( Keep an attached credential in
|
||
| for (const [id, config] of mcpConfigByCredentialId) { | ||
| if (config.threadId === threadId && !held.has(id)) { | ||
| mcpConfigByCredentialId.delete(id); | ||
| } | ||
| } | ||
| const credential = yield* mcpSessionRegistry.issue({ | ||
| threadId, | ||
| providerInstanceId, | ||
| browserToolsAvailable, | ||
| capabilities, | ||
| subagent, | ||
| }); | ||
| yield* mcpSessions.set({ | ||
| const config = { | ||
| ...credential.config, | ||
| hasPendingWake: projectionStore | ||
| .getThreadRecords(threadId, ["runs", "subagents"]) | ||
| .pipe( | ||
| Effect.map(delegatedTaskWillWake), | ||
| Effect.orElseSucceed(() => false), | ||
| ), | ||
| }); | ||
| }; | ||
| mcpConfigByCredentialId.set(credential.config.providerSessionId, config); | ||
| yield* mcpSessions.set(config); | ||
| reserveMcpCredential(threadId, credential.config.providerSessionId); | ||
| return { mcpCredentialId: credential.config.providerSessionId, issued: true }; | ||
| }), | ||
| ); | ||
| const clearMcpSession = (threadId: ThreadId, mcpCredentialId?: string) => | ||
| mcpCredentialId === undefined | ||
| ? mcpSessionRegistry | ||
| .revokeThread(threadId) | ||
| .pipe(Effect.tap(() => mcpSessions.clear(threadId))) | ||
| ? mcpSessionRegistry.revokeThread(threadId).pipe( | ||
| Effect.tap(() => | ||
| Effect.sync(() => { | ||
| for (const [id, config] of mcpConfigByCredentialId) { | ||
| if (config.threadId === threadId) mcpConfigByCredentialId.delete(id); | ||
| } | ||
| }), | ||
| ), | ||
| Effect.tap(() => mcpSessions.clear(threadId)), | ||
| ) | ||
| : mcpSessionRegistry.revokeProviderSession(mcpCredentialId).pipe( | ||
| Effect.tap(() => | ||
| Effect.gen(function* () { | ||
| mcpConfigByCredentialId.delete(mcpCredentialId); | ||
| if ((yield* mcpSessions.read(threadId))?.providerSessionId === mcpCredentialId) { | ||
| yield* mcpSessions.clear(threadId); | ||
| } | ||
| }), | ||
| ), | ||
| ); | ||
|
|
||
| const restoreHeldMcpSession = (providerSessionId: ProviderSessionId, threadId: ThreadId) => | ||
| mcpPrepareLock.withLock( | ||
| threadId, | ||
| Effect.gen(function* () { | ||
| const mcpCredentialId = (yield* Ref.get(sessions)) | ||
| .get(sessionKey(providerSessionId)) | ||
| ?.mcpCredentialIdByThread.get(threadId); | ||
| const config = | ||
| mcpCredentialId === undefined | ||
| ? undefined | ||
| : mcpConfigByCredentialId.get(mcpCredentialId); | ||
| if ( | ||
| config === undefined || | ||
| (yield* mcpSessions.read(threadId))?.providerSessionId === mcpCredentialId | ||
| ) { | ||
| return; | ||
| } | ||
| yield* mcpSessions.set(config); | ||
| }), | ||
| ); | ||
|
|
||
| const subscriberReleaseCleanup = new WeakMap< | ||
| Queue.Queue<ProviderSessionEventSignal, Cause.Done>, | ||
| Effect.Effect<void> | ||
|
|
@@ -1767,6 +1823,8 @@ export const layerWithOptions = ( | |
| }), | ||
| ); | ||
| } | ||
| } else { | ||
| yield* restoreHeldMcpSession(input.providerSessionId, input.threadId); | ||
| } | ||
| }).pipe( | ||
| Effect.onError(() => | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.