Skip to content

Commit aca1279

Browse files
dmitrivMSCopilot
andcommitted
sessions: validate remote targets and queued message trust
Reuse authoritative workspace trust and model-availability checks, guard target replacement, retain uncertain deliveries until idle, and distinguish acknowledgement timeouts from cancellation. Refs #336682 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 22512d0 commit aca1279

9 files changed

Lines changed: 749 additions & 154 deletions

File tree

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { URI } from '../../../base/common/uri.js';
7+
import { IWorkspaceTrustManagementService } from '../../workspace/common/workspaceTrust.js';
8+
import { SessionConfigKey } from './sessionConfigKeys.js';
9+
import { readSessionWorkspaceless, SessionState } from './state/sessionState.js';
10+
import { isWorktreeUnderRepository } from './worktreePaths.js';
11+
12+
/** Resolves persisted trust roots, inheriting trust only for individual VS Code-created worktrees. */
13+
export async function resolveAgentHostSessionTrustFolders(
14+
state: SessionState,
15+
workspaceTrustService: IWorkspaceTrustManagementService,
16+
mapResource: (resource: URI) => URI = resource => resource,
17+
): Promise<readonly URI[] | undefined> {
18+
if (readSessionWorkspaceless(state._meta)) {
19+
return undefined;
20+
}
21+
const folders = state.workingDirectories?.map(directory => typeof directory === 'string' ? URI.parse(directory) : directory);
22+
if (folders === undefined) {
23+
return undefined;
24+
}
25+
if (state.config?.values[SessionConfigKey.Isolation] === 'worktree' && state.project?.uri) {
26+
const repositoryRoot = typeof state.project.uri === 'string' ? URI.parse(state.project.uri) : state.project.uri;
27+
const worktrees = folders.filter(folder => isWorktreeUnderRepository(folder, repositoryRoot)).map(mapResource);
28+
if (worktrees.length > 0) {
29+
const [repoTrust, ...folderTrusts] = await Promise.all([
30+
workspaceTrustService.getUriTrustInfo(mapResource(repositoryRoot)),
31+
...worktrees.map(folder => workspaceTrustService.getUriTrustInfo(folder)),
32+
]);
33+
if (repoTrust.trusted) {
34+
const untrusted = worktrees.filter((_, index) => !folderTrusts[index].trusted);
35+
if (untrusted.length > 0) {
36+
await workspaceTrustService.setUrisTrust(untrusted, true);
37+
}
38+
}
39+
}
40+
}
41+
return folders.map(mapResource);
42+
}

‎src/vs/sessions/contrib/providers/agentHost/browser/baseAgentHostSessionsProvider.ts‎

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ import { assertAutomationSessionTemplate, IAutomationSessionTemplate } from '../
5757
import { AutomationModelConfiguration } from '../../../automations/browser/automationModelConfiguration.js';
5858
import { ChatAgentLocation, ChatConfiguration, ChatModeKind, ChatPermissionLevel, getChatPermissionLevelFromDefaultConfiguration, isChatPermissionLevel, type IChatDefaultConfiguration } from '../../../../../workbench/contrib/chat/common/constants.js';
5959
import { isAutoApprovePolicyRestricted, normalizeSessionConfigValue } from '../../../../../workbench/contrib/chat/common/agentHostConfigPolicy.js';
60-
import { ILanguageModelChatMetadata, ILanguageModelsService } from '../../../../../workbench/contrib/chat/common/languageModels.js';
61-
import { getRegisteredLanguageModels, resolveConfiguredModel, resolveModelIdentifier, resolveModelIdentifierFromLanguageModels } from '../../../../../workbench/contrib/chat/common/modelSelection.js';
60+
import { ILanguageModelsService } from '../../../../../workbench/contrib/chat/common/languageModels.js';
61+
import { getRegisteredLanguageModels, getVisibleLanguageModelsForTarget, resolveConfiguredModel, resolveModelIdentifier, resolveModelIdentifierFromLanguageModels } from '../../../../../workbench/contrib/chat/common/modelSelection.js';
6262
import { buildMutableConfigSchema, IAgentHostMcpServer, IAgentHostSessionsProvider, IAgentMergeClientState, resolvedConfigsEqual } from '../../../../common/agentHostSessionsProvider.js';
6363
import { agentHostSessionWorkspaceKey } from '../../../../common/agentHostSessionWorkspace.js';
6464
import { USE_WORKTREE_SETTING, isSessionConfigComplete } from '../../../../common/sessionConfig.js';
@@ -4450,16 +4450,7 @@ export abstract class BaseAgentHostSessionsProvider extends Disposable implement
44504450
};
44514451
}
44524452
const allModels = getRegisteredLanguageModels(this._languageModelsService);
4453-
const models = allModels.filter(model => {
4454-
if (model.metadata.targetChatSessionType !== resourceScheme) {
4455-
return false;
4456-
}
4457-
if (this._languageModelsService.isModelHidden(model.identifier)) {
4458-
return false;
4459-
}
4460-
const manageModelsIdentifier = ILanguageModelChatMetadata.getAgentHostByokManageModelsIdentifier(model.metadata);
4461-
return manageModelsIdentifier === undefined || !this._languageModelsService.isModelHidden(manageModelsIdentifier);
4462-
});
4453+
const models = getVisibleLanguageModelsForTarget(allModels, resourceScheme, this._languageModelsService);
44634454
const desiredModel = desiredModelId ? this._languageModelsService.lookupLanguageModel(desiredModelId) : undefined;
44644455
const resolvedDesiredModelId = desiredModel?.targetChatSessionType && this.resourceSchemeForProvider(desiredModel.targetChatSessionType) === resourceScheme
44654456
? `${resourceScheme}:${desiredModel.id}`

‎src/vs/sessions/contrib/remoteSessions/browser/remoteSessionMessageRouter.ts‎

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@ import { URI } from '../../../../base/common/uri.js';
1313
import { generateUuid } from '../../../../base/common/uuid.js';
1414
import { localize } from '../../../../nls.js';
1515
import { IAgentHostConnectionsService } from '../../../../platform/agentHost/common/agentHostConnectionsService.js';
16+
import { resolveAgentHostSessionTrustFolders } from '../../../../platform/agentHost/common/agentHostWorkspaceTrust.js';
1617
import { toRemoteSessionMessageMetadata } from '../../../../platform/agentHost/common/meta/agentRemoteSessionMeta.js';
1718
import { buildOpenSessionLinkUri } from '../../../../platform/agentHost/common/openSessionLink.js';
1819
import { RemoteAgentHostsEnabledSettingId } from '../../../../platform/agentHost/common/remoteAgentHostService.js';
1920
import { ChatInteractivity as ProtocolChatInteractivity } from '../../../../platform/agentHost/common/state/protocol/state.js';
2021
import { ActionType } from '../../../../platform/agentHost/common/state/sessionActions.js';
21-
import { DEFAULT_CHAT_ID, effectiveChatInteractivity, getSessionChatResource, isSessionStatusArchived, MessageKind, parseChatUri, PendingMessageKind, StateComponents } from '../../../../platform/agentHost/common/state/sessionState.js';
22+
import { DEFAULT_CHAT_ID, effectiveChatInteractivity, getSessionChatResource, isSessionStatusArchived, MessageKind, parseChatUri, PendingMessageKind, readSessionWorkspaceless, SessionState, StateComponents } from '../../../../platform/agentHost/common/state/sessionState.js';
2223
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
24+
import { IWorkspaceTrustManagementService } from '../../../../platform/workspace/common/workspaceTrust.js';
2325
import { isAgentHostProvider } from '../../../common/agentHostSessionsProvider.js';
2426
import { ISessionsProvidersService } from '../../../services/sessions/browser/sessionsProvidersService.js';
2527
import { ChatInteractivity } from '../../../services/sessions/common/session.js';
@@ -73,6 +75,7 @@ export class RemoteSessionMessageRouter {
7375
@IAgentHostConnectionsService private readonly connectionsService: IAgentHostConnectionsService,
7476
@IConfigurationService private readonly configurationService: IConfigurationService,
7577
@IRemoteSessionChatService private readonly backgroundChats: IRemoteSessionChatService,
78+
@IWorkspaceTrustManagementService private readonly workspaceTrustService: IWorkspaceTrustManagementService,
7679
) { }
7780

7881
async prepareTarget(source: URI, target: string, token: CancellationToken): Promise<IRemoteMessageTarget> {
@@ -170,45 +173,70 @@ export class RemoteSessionMessageRouter {
170173
};
171174
}
172175

176+
private async checkTargetTrust(target: IResolvedRemoteChat, state: SessionState, token: CancellationToken): Promise<void> {
177+
if (!readSessionWorkspaceless(state._meta) && state.workingDirectories === undefined) {
178+
throw new Error(localize('remoteMessage.unknownWorkspace', "The target session's working directories are unavailable. Open the session before sending a remote message."));
179+
}
180+
const folders = await raceCancellationError(resolveAgentHostSessionTrustFolders(
181+
state, this.workspaceTrustService, resource => target.host.connection.resourceUris.fromAgentHost(resource),
182+
), token);
183+
if (folders === undefined) {
184+
return;
185+
}
186+
const trusted = folders.length === 0
187+
? this.workspaceTrustService.isWorkspaceTrusted()
188+
: (await raceCancellationError(Promise.all(folders.map(folder => this.workspaceTrustService.getUriTrustInfo(folder))), token)).every(info => info.trusted);
189+
if (!trusted) {
190+
throw new Error(localize('remoteMessage.untrustedWorkspace', "The target session's workspace is not trusted. Trust it before sending a remote message."));
191+
}
192+
}
193+
173194
private async doSend(source: IResolvedRemoteChat, target: IResolvedRemoteChat, options: ISendRemoteMessageOptions, token: CancellationToken): Promise<ISendRemoteMessageResult> {
174195
const store = new DisposableStore();
175196
let dispatched = false;
176197
let rejected = false;
177198
let confirmed = false;
178199
let started = false;
179-
let connectionError: Error | undefined;
200+
let operationError: Error | undefined;
180201
let background: IRemoteSessionChatReference | undefined;
181202
try {
182203
const cancellation = store.add(new CancellationTokenSource(token));
183-
store.add(disposableTimeout(() => cancellation.cancel(), 10_000));
204+
store.add(disposableTimeout(() => {
205+
operationError = new Error(dispatched
206+
? localize('remoteMessage.acknowledgementTimeout', "Timed out waiting for remote message acknowledgement.")
207+
: localize('remoteMessage.preparationTimeout', "Timed out preparing the remote message."));
208+
cancellation.cancel();
209+
}, 10_000));
184210
const checkConnections = () => {
185211
try {
186212
this.checkConnected(source);
187213
this.checkConnected(target);
188214
} catch (error) {
189-
connectionError = error instanceof Error ? error : new Error(toErrorMessage(error));
215+
operationError = error instanceof Error ? error : new Error(toErrorMessage(error));
190216
cancellation.cancel();
191217
}
192218
};
193219
store.add(this.connectionsService.onDidChangeConnections(checkConnections));
194220
checkConnections();
195221
const connection = target.host.connection;
196222
const session = store.add(connection.getSubscription(StateComponents.Session, target.host.backendSession, 'RemoteSessionMessageRouter'));
197-
const sessionState = await readRemoteSessionState(session.object, cancellation.token);
223+
const sessionState = await readRemoteSessionState(session.object, cancellation.token, true);
198224
const chatResource = getSessionChatResource(sessionState, target.chat.fragment || DEFAULT_CHAT_ID);
199225
const identity = chatResource ? parseChatUri(chatResource) : undefined;
200226
if (!chatResource || !identity || !isEqual(URI.parse(identity.session), target.host.backendSession)) {
201227
throw new Error(localize('remoteMessage.missingChat', "The exact target chat no longer exists on its agent host."));
202228
}
203229
const chat = store.add(connection.getSubscription(StateComponents.Chat, URI.parse(chatResource), 'RemoteSessionMessageRouter'));
204-
const chatState = await readRemoteSessionState(chat.object, cancellation.token);
230+
const chatState = await readRemoteSessionState(chat.object, cancellation.token, true);
205231
if (!isEqual(URI.parse(chatState.resource), URI.parse(chatResource))) {
206232
throw new Error(localize('remoteMessage.chatChanged', "The target chat identity changed while preparing the message."));
207233
}
208234
if (effectiveChatInteractivity(chatState.interactivity, isSessionStatusArchived(sessionState.status)) !== ProtocolChatInteractivity.Full) {
209235
throw new Error(localize('remoteMessage.readOnly', "The target chat is archived or read-only."));
210236
}
237+
await this.checkTargetTrust(target, sessionState, cancellation.token);
211238
background = await this.backgroundChats.acquire(target.chat, cancellation.token, true);
239+
await this.checkTargetTrust(target, await readRemoteSessionState(session.object, cancellation.token, true), cancellation.token);
212240
this.checkEnabled();
213241
this.checkConnected(source);
214242
this.checkConnected(target);
@@ -254,13 +282,13 @@ export class RemoteSessionMessageRouter {
254282
await raceCancellationError(accepted, cancellation.token);
255283
} catch (error) {
256284
if (!confirmed && dispatched && !rejected) {
257-
throw new Error(localize('remoteMessage.unconfirmed', "Remote message delivery was not confirmed. It may already be queued; do not retry automatically. {0}", toErrorMessage(connectionError ?? error)));
285+
throw new Error(localize('remoteMessage.unconfirmed', "Remote message delivery was not confirmed. It may already be queued; do not retry automatically. {0}", toErrorMessage(operationError ?? error)));
258286
}
259287
if (!confirmed) {
260-
throw connectionError ?? error;
288+
throw operationError ?? error;
261289
}
262290
} finally {
263-
if (confirmed) {
291+
if (confirmed || (dispatched && !rejected)) {
264292
background?.releaseWhenIdle();
265293
} else {
266294
background?.dispose();

0 commit comments

Comments
 (0)