Skip to content

Commit 0dad7e0

Browse files
authored
Use URI for workingDirectory across agent host layer (#306117)
* Use URI for workingDirectory across agent host layer - Change IAgentCreateSessionConfig.workingDirectory and IAgentSessionMetadata.workingDirectory from string to URI - Convert file paths to URI at SDK boundary (CopilotAgent) - Convert protocol strings to agenthost:// URIs at protocol client boundary - Convert agenthost:// URIs back to file:// at protocol client on outgoing - Update resolveWorkingDirectory callback to return URI - Use extUriBiasedIgnorePathCase.isEqualOrParent for path containment - Use generateUuid() instead of crypto.randomUUID() - Add CopilotAgentSession tests and SessionWrapperFactory pattern - Register all event subscription disposables properly (Written by Copilot) * Tweak * Fix test
1 parent 9a6bf81 commit 0dad7e0

15 files changed

Lines changed: 46 additions & 46 deletions

File tree

src/vs/platform/agentHost/common/agentService.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export interface IAgentSessionMetadata {
3737
readonly startTime: number;
3838
readonly modifiedTime: number;
3939
readonly summary?: string;
40-
readonly workingDirectory?: string;
40+
readonly workingDirectory?: URI;
4141
}
4242

4343
export type AgentProvider = string;
@@ -100,7 +100,7 @@ export interface IAgentCreateSessionConfig {
100100
readonly provider?: AgentProvider;
101101
readonly model?: string;
102102
readonly session?: URI;
103-
readonly workingDirectory?: string;
103+
readonly workingDirectory?: URI;
104104
}
105105

106106
/** Serializable attachment passed alongside a message to the agent host. */

src/vs/platform/agentHost/electron-browser/remoteAgentHostProtocolClient.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { URI } from '../../../base/common/uri.js';
1515
import { generateUuid } from '../../../base/common/uuid.js';
1616
import { ILogService } from '../../log/common/log.js';
1717
import { AgentSession, IAgentConnection, IAgentCreateSessionConfig, IAgentDescriptor, IAgentSessionMetadata, IAuthenticateParams, IAuthenticateResult, IResourceMetadata } from '../common/agentService.js';
18+
import { agentHostAuthority, fromAgentHostUri, toAgentHostUri } from '../common/agentHostUri.js';
1819
import type { IClientNotificationMap, ICommandMap } from '../common/state/protocol/messages.js';
1920
import type { IActionEnvelope, INotification, ISessionAction } from '../common/state/sessionActions.js';
2021
import { PROTOCOL_VERSION } from '../common/state/sessionCapabilities.js';
@@ -36,6 +37,7 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC
3637

3738
private readonly _clientId = generateUuid();
3839
private readonly _transport: WebSocketClientTransport;
40+
private readonly _connectionAuthority: string;
3941
private _serverSeq = 0;
4042
private _nextClientSeq = 1;
4143
private _defaultDirectory: string | undefined;
@@ -71,6 +73,7 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC
7173
@ILogService private readonly _logService: ILogService,
7274
) {
7375
super();
76+
this._connectionAuthority = agentHostAuthority(address);
7477
this._transport = this._register(new WebSocketClientTransport(address, connectionToken));
7578
this._register(this._transport.onMessage(msg => this._handleMessage(msg)));
7679
this._register(this._transport.onClose(() => this._onDidClose.fire()));
@@ -132,7 +135,7 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC
132135
session: session.toString(),
133136
provider,
134137
model: config?.model,
135-
workingDirectory: config?.workingDirectory,
138+
workingDirectory: config?.workingDirectory ? fromAgentHostUri(config.workingDirectory).toString() : undefined,
136139
});
137140
return session;
138141
}
@@ -189,7 +192,7 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC
189192
startTime: s.createdAt,
190193
modifiedTime: s.modifiedAt,
191194
summary: s.title,
192-
workingDirectory: typeof s.workingDirectory === 'string' ? s.workingDirectory : undefined,
195+
workingDirectory: typeof s.workingDirectory === 'string' ? toAgentHostUri(URI.parse(s.workingDirectory), this._connectionAuthority) : undefined,
193196
}));
194197
}
195198

src/vs/platform/agentHost/node/agentHostMain.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ async function startWebSocketServer(agentService: AgentService, logService: ILog
153153
await agentService.createSession({
154154
provider: command.provider,
155155
model: command.model,
156-
workingDirectory: command.workingDirectory,
156+
workingDirectory: command.workingDirectory ? URI.parse(command.workingDirectory) : undefined,
157157
session: URI.parse(command.session),
158158
});
159159
},
@@ -169,10 +169,9 @@ async function startWebSocketServer(agentService: AgentService, logService: ILog
169169
status: SessionStatus.Idle,
170170
createdAt: s.startTime,
171171
modifiedAt: s.modifiedTime,
172-
workingDirectory: s.workingDirectory,
172+
workingDirectory: s.workingDirectory?.toString(),
173173
}));
174174
},
175-
176175
handleGetResourceMetadata() {
177176
return agentService.getResourceMetadataSync();
178177
},

src/vs/platform/agentHost/node/agentService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ export class AgentService extends Disposable implements IAgentService {
156156
status: SessionStatus.Idle,
157157
createdAt: Date.now(),
158158
modifiedAt: Date.now(),
159-
workingDirectory: config?.workingDirectory,
159+
workingDirectory: config?.workingDirectory?.toString(),
160160
};
161161
this._stateManager.createSession(summary);
162162
this._stateManager.dispatchServerAction({ type: ActionType.SessionReady, session: session.toString() });

src/vs/platform/agentHost/node/agentSideEffects.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@ export class AgentSideEffects extends Disposable implements IProtocolSideEffectH
364364
await agent.createSession({
365365
provider,
366366
model: command.model,
367-
workingDirectory: command.workingDirectory,
367+
workingDirectory: command.workingDirectory ? URI.parse(command.workingDirectory) : undefined,
368368
session: URI.parse(session),
369369
});
370370
const summary: ISessionSummary = {
@@ -461,7 +461,7 @@ export class AgentSideEffects extends Disposable implements IProtocolSideEffectH
461461
status: SessionStatus.Idle,
462462
createdAt: meta.startTime,
463463
modifiedAt: meta.modifiedTime,
464-
workingDirectory: meta.workingDirectory,
464+
workingDirectory: meta.workingDirectory?.toString(),
465465
};
466466

467467
this._stateManager.restoreSession(summary, turns);

src/vs/platform/agentHost/node/copilot/copilotAgent.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { FileAccess } from '../../../../base/common/network.js';
1111
import type { IAuthorizationProtectedResourceMetadata } from '../../../../base/common/oauth.js';
1212
import { delimiter, dirname } from '../../../../base/common/path.js';
1313
import { URI } from '../../../../base/common/uri.js';
14+
import { generateUuid } from '../../../../base/common/uuid.js';
1415
import { IInstantiationService } from '../../../instantiation/common/instantiation.js';
1516
import { ILogService } from '../../../log/common/log.js';
1617
import { AgentSession, IAgent, IAgentAttachment, IAgentCreateSessionConfig, IAgentDescriptor, IAgentMessageEvent, IAgentModelInfo, IAgentProgressEvent, IAgentSessionMetadata, IAgentToolCompleteEvent, IAgentToolStartEvent } from '../../common/agentService.js';
@@ -150,7 +151,7 @@ export class CopilotAgent extends Disposable implements IAgent {
150151
startTime: s.startTime.getTime(),
151152
modifiedTime: s.modifiedTime.getTime(),
152153
summary: s.summary,
153-
workingDirectory: typeof s.context?.cwd === 'string' ? s.context.cwd : undefined,
154+
workingDirectory: typeof s.context?.cwd === 'string' ? URI.file(s.context.cwd) : undefined,
154155
}));
155156
this._logService.info(`[Copilot] Found ${result.length} sessions`);
156157
return result;
@@ -185,7 +186,7 @@ export class CopilotAgent extends Disposable implements IAgent {
185186
model: config?.model,
186187
sessionId: config?.session ? AgentSession.id(config.session) : undefined,
187188
streaming: true,
188-
workingDirectory: config?.workingDirectory,
189+
workingDirectory: config?.workingDirectory?.fsPath,
189190
onPermissionRequest: callbacks.onPermissionRequest,
190191
hooks: callbacks.hooks,
191192
});
@@ -283,8 +284,8 @@ export class CopilotAgent extends Disposable implements IAgent {
283284
* and returns it. The caller must call {@link CopilotAgentSession.initializeSession}
284285
* to wire up the SDK session.
285286
*/
286-
private _createAgentSession(wrapperFactory: SessionWrapperFactory, workingDirectory: string | undefined, sessionIdOverride?: string): CopilotAgentSession {
287-
const rawId = sessionIdOverride ?? crypto.randomUUID();
287+
private _createAgentSession(wrapperFactory: SessionWrapperFactory, workingDirectory: URI | undefined, sessionIdOverride?: string): CopilotAgentSession {
288+
const rawId = sessionIdOverride ?? generateUuid();
288289
const sessionUri = AgentSession.uri(this.id, rawId);
289290

290291
const agentSession = this._instantiationService.createInstance(

src/vs/platform/agentHost/node/copilot/copilotAgentSession.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { DeferredPromise } from '../../../../base/common/async.js';
88
import { Emitter } from '../../../../base/common/event.js';
99
import { Disposable, IReference, toDisposable } from '../../../../base/common/lifecycle.js';
1010
import { URI } from '../../../../base/common/uri.js';
11+
import { extUriBiasedIgnorePathCase, normalizePath } from '../../../../base/common/resources.js';
1112
import { IFileService } from '../../../files/common/files.js';
1213
import { ILogService } from '../../../log/common/log.js';
1314
import { localize } from '../../../../nls.js';
@@ -118,12 +119,12 @@ export class CopilotAgentSession extends Disposable {
118119
/** SDK session wrapper, set by {@link initializeSession}. */
119120
private _wrapper!: CopilotSessionWrapper;
120121

121-
private readonly _workingDirectory: string | undefined;
122+
private readonly _workingDirectory: URI | undefined;
122123

123124
constructor(
124125
sessionUri: URI,
125126
rawSessionId: string,
126-
workingDirectory: string | undefined,
127+
workingDirectory: URI | undefined,
127128
private readonly _onDidSessionProgress: Emitter<IAgentProgressEvent>,
128129
private readonly _wrapperFactory: SessionWrapperFactory,
129130
@IFileService private readonly _fileService: IFileService,
@@ -240,7 +241,7 @@ export class CopilotAgentSession extends Disposable {
240241
// Auto-approve reads inside the working directory
241242
if (request.kind === 'read') {
242243
const requestPath = typeof request.path === 'string' ? request.path : undefined;
243-
if (requestPath && this._workingDirectory && requestPath.startsWith(this._workingDirectory)) {
244+
if (requestPath && this._workingDirectory && extUriBiasedIgnorePathCase.isEqualOrParent(normalizePath(URI.file(requestPath)), this._workingDirectory)) {
244245
this._logService.trace(`[Copilot:${this.sessionId}] Auto-approving read inside working directory: ${requestPath}`);
245246
return { kind: 'approved' };
246247
}

src/vs/platform/agentHost/test/node/agentSideEffects.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -430,7 +430,7 @@ suite('AgentSideEffects', () => {
430430
});
431431

432432
test('preserves workingDirectory from agent metadata', async () => {
433-
agent.sessionMetadataOverrides = { workingDirectory: '/home/user/project' };
433+
agent.sessionMetadataOverrides = { workingDirectory: URI.file('/home/user/project') };
434434
const session = await agent.createSession();
435435
const sessions = await agent.listSessions();
436436
const sessionResource = sessions[0].session.toString();
@@ -444,7 +444,7 @@ suite('AgentSideEffects', () => {
444444

445445
const state = stateManager.getSessionState(sessionResource);
446446
assert.ok(state);
447-
assert.strictEqual(state!.summary.workingDirectory, '/home/user/project');
447+
assert.strictEqual(state!.summary.workingDirectory, URI.file('/home/user/project').toString());
448448
});
449449
});
450450

src/vs/platform/agentHost/test/node/copilotAgentSession.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ function createMockSessionDataService(): ISessionDataService {
8181
};
8282
}
8383

84-
async function createAgentSession(disposables: DisposableStore, options?: { workingDirectory?: string }): Promise<{
84+
async function createAgentSession(disposables: DisposableStore, options?: { workingDirectory?: URI }): Promise<{
8585
session: CopilotAgentSession;
8686
mockSession: MockCopilotSession;
8787
progressEvents: IAgentProgressEvent[];
@@ -129,7 +129,7 @@ suite('CopilotAgentSession', () => {
129129
suite('permission handling', () => {
130130

131131
test('auto-approves read inside working directory', async () => {
132-
const { session } = await createAgentSession(disposables, { workingDirectory: '/workspace' });
132+
const { session } = await createAgentSession(disposables, { workingDirectory: URI.file('/workspace') });
133133
const result = await session.handlePermissionRequest({
134134
kind: 'read',
135135
path: '/workspace/src/file.ts',
@@ -139,7 +139,7 @@ suite('CopilotAgentSession', () => {
139139
});
140140

141141
test('does not auto-approve read outside working directory', async () => {
142-
const { session, progressEvents } = await createAgentSession(disposables, { workingDirectory: '/workspace' });
142+
const { session, progressEvents } = await createAgentSession(disposables, { workingDirectory: URI.file('/workspace') });
143143

144144
// Kick off permission request but don't await — it will block
145145
const resultPromise = session.handlePermissionRequest({

src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHost.contribution.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { Disposable, DisposableMap, DisposableStore, toDisposable } from '../../
77
import { URI } from '../../../../base/common/uri.js';
88
import * as nls from '../../../../nls.js';
99
import { AgentHostFileSystemProvider } from '../../../../platform/agentHost/common/agentHostFileSystemProvider.js';
10-
import { AGENT_HOST_LABEL_FORMATTER, AGENT_HOST_SCHEME, agentHostAuthority, fromAgentHostUri } from '../../../../platform/agentHost/common/agentHostUri.js';
10+
import { AGENT_HOST_LABEL_FORMATTER, AGENT_HOST_SCHEME, agentHostAuthority } from '../../../../platform/agentHost/common/agentHostUri.js';
1111
import { type AgentProvider, type IAgentConnection } from '../../../../platform/agentHost/common/agentService.js';
1212
import { IRemoteAgentHostConnectionInfo, IRemoteAgentHostEntry, IRemoteAgentHostService, RemoteAgentHostsEnabledSettingId, RemoteAgentHostsSettingId } from '../../../../platform/agentHost/common/remoteAgentHostService.js';
1313
import { isSessionAction } from '../../../../platform/agentHost/common/state/sessionActions.js';
@@ -306,24 +306,20 @@ export class RemoteAgentHostContribution extends Disposable implements IWorkbenc
306306
const displayName = configuredName || `${agent.displayName} (${address})`;
307307

308308
// Per-agent working directory cache, scoped to the agent store lifetime
309-
const sessionWorkingDirs = new Map<string, string>();
309+
const sessionWorkingDirs = new Map<string, URI>();
310310
agentStore.add(toDisposable(() => sessionWorkingDirs.clear()));
311311

312312
// Capture the working directory from the active session for new sessions
313-
const resolveWorkingDirectory = (resourceKey: string): string | undefined => {
313+
const resolveWorkingDirectory = (resourceKey: string): URI | undefined => {
314314
const cached = sessionWorkingDirs.get(resourceKey);
315315
if (cached) {
316316
return cached;
317317
}
318318
const activeSession = this._sessionsManagementService.activeSession.get();
319319
const repoUri = activeSession?.workspace.get()?.repositories[0]?.uri;
320320
if (repoUri) {
321-
// The repository URI may be wrapped as a vscode-agent-host:// URI.
322-
// Unwrap to get the original filesystem path.
323-
const originalUri = repoUri.scheme === AGENT_HOST_SCHEME ? fromAgentHostUri(repoUri) : repoUri;
324-
const dir = originalUri.path;
325-
sessionWorkingDirs.set(resourceKey, dir);
326-
return dir;
321+
sessionWorkingDirs.set(resourceKey, repoUri);
322+
return repoUri;
327323
}
328324
return undefined;
329325
};

0 commit comments

Comments
 (0)