Skip to content

Commit 55c1881

Browse files
authored
Merge pull request #310294 from microsoft/connor4312/tunnel-local-publish
agentHost: allow enabling tunnel access to local agent host
2 parents 3f42e69 + 07b1a72 commit 55c1881

File tree

13 files changed

+1004
-8
lines changed

13 files changed

+1004
-8
lines changed

build/lib/i18n.resources.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,10 @@
707707
{
708708
"name": "vs/sessions/contrib/chatDebug",
709709
"project": "vscode-sessions"
710+
},
711+
{
712+
"name": "vs/sessions/contrib/tunnelHost",
713+
"project": "vscode-sessions"
710714
}
711715
]
712716
}

src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,9 @@ import { IExtensionsScannerService } from '../../../platform/extensionManagement
8989
import { ExtensionsScannerService } from '../../../platform/extensionManagement/node/extensionsScannerService.js';
9090
import { ISSHRemoteAgentHostMainService, SSH_REMOTE_AGENT_HOST_CHANNEL } from '../../../platform/agentHost/common/sshRemoteAgentHost.js';
9191
import { SSHRemoteAgentHostMainService } from '../../../platform/agentHost/node/sshRemoteAgentHostService.js';
92-
import { ITunnelAgentHostMainService, TUNNEL_AGENT_HOST_CHANNEL } from '../../../platform/agentHost/common/tunnelAgentHost.js';
92+
import { ITunnelAgentHostMainService, ITunnelAgentHostHostingService, TUNNEL_AGENT_HOST_CHANNEL, TUNNEL_HOST_CHANNEL } from '../../../platform/agentHost/common/tunnelAgentHost.js';
9393
import { TunnelAgentHostMainService } from '../../../platform/agentHost/node/tunnelAgentHostService.js';
94+
import { TunnelHostMainService } from '../../../platform/agentHost/node/tunnelHostMainService.js';
9495
import { IUserDataProfilesService } from '../../../platform/userDataProfile/common/userDataProfile.js';
9596
import { IExtensionsProfileScannerService } from '../../../platform/extensionManagement/common/extensionsProfileScannerService.js';
9697
import { PolicyChannelClient } from '../../../platform/policy/common/policyIpc.js';
@@ -418,6 +419,9 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter {
418419
// Tunnel Agent Host
419420
services.set(ITunnelAgentHostMainService, new SyncDescriptor(TunnelAgentHostMainService, undefined, true));
420421

422+
// Tunnel Host (hosting local agent host for remote connections)
423+
services.set(ITunnelAgentHostHostingService, new SyncDescriptor(TunnelHostMainService, undefined, true));
424+
421425
return new InstantiationService(services);
422426
}
423427

@@ -501,6 +505,10 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter {
501505
// Tunnel Agent Host
502506
const tunnelAgentHostChannel = ProxyChannel.fromService(accessor.get(ITunnelAgentHostMainService), this._store);
503507
this.server.registerChannel(TUNNEL_AGENT_HOST_CHANNEL, tunnelAgentHostChannel);
508+
509+
// Tunnel Host
510+
const tunnelHostChannel = ProxyChannel.fromService(accessor.get(ITunnelAgentHostHostingService), this._store);
511+
this.server.registerChannel(TUNNEL_HOST_CHANNEL, tunnelHostChannel);
504512
}
505513

506514
private registerErrorHandler(logService: ILogService): void {

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,27 @@ export const AgentHostEnabledSettingId = 'chat.agentHost.enabled';
3535
/** Configuration key that controls whether per-host IPC traffic output channels are created. */
3636
export const AgentHostIpcLoggingSettingId = 'chat.agentHost.ipcLoggingEnabled';
3737

38+
/** Result of starting the agent host WebSocket server on-demand. */
39+
export interface IAgentHostSocketInfo {
40+
readonly socketPath: string;
41+
}
42+
43+
/**
44+
* IPC service exposed on the {@link AgentHostIpcChannels.ConnectionTracker}
45+
* channel. Used by the server process for lifetime management and by the
46+
* shared process to request a local WebSocket listener on-demand.
47+
*/
48+
export interface IConnectionTrackerService {
49+
readonly onDidChangeConnectionCount: Event<number>;
50+
51+
/**
52+
* Request the agent host to start a WebSocket server on a local
53+
* pipe/socket. Returns the socket path.
54+
* If a server is already running, returns the existing info.
55+
*/
56+
startWebSocketServer(): Promise<IAgentHostSocketInfo>;
57+
}
58+
3859
// ---- IPC data types (serializable across MessagePort) -----------------------
3960

4061
export interface IAgentSessionMetadata {
@@ -652,4 +673,6 @@ export interface IAgentHostService extends IAgentConnection {
652673
readonly onAgentHostStart: Event<void>;
653674

654675
restartAgentHost(): Promise<void>;
676+
677+
startWebSocketServer(): Promise<IAgentHostSocketInfo>;
655678
}

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

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import { Event } from '../../../base/common/event.js';
77
import { createDecorator } from '../../instantiation/common/instantiation.js';
8+
import type { IAgentHostSocketInfo } from './agentService.js';
89

910
export const ITunnelAgentHostService = createDecorator<ITunnelAgentHostService>('tunnelAgentHostService');
1011

@@ -213,3 +214,54 @@ export interface ITunnelAgentHostService {
213214
*/
214215
getAuthProvider(options?: { silent?: boolean }): Promise<'github' | 'microsoft' | undefined>;
215216
}
217+
218+
// ---- Tunnel hosting (exposing the local agent host to remote clients) --------
219+
220+
/** IPC channel name for the tunnel host service. */
221+
export const TUNNEL_HOST_CHANNEL = 'tunnelHost';
222+
223+
/** Output channel ID for the tunnel host logs. */
224+
export const TUNNEL_HOST_LOG_ID = 'tunnelHostService';
225+
226+
/** Information about an actively hosted tunnel. */
227+
export interface ITunnelHostInfo {
228+
readonly tunnelName: string;
229+
readonly tunnelId: string;
230+
readonly clusterId: string;
231+
readonly domain: string;
232+
}
233+
234+
/** Status of the tunnel host. */
235+
export type TunnelHostStatus =
236+
| { readonly active: false }
237+
| { readonly active: true; readonly info: ITunnelHostInfo };
238+
239+
/**
240+
* Shared-process service that hosts a dev tunnel using `TunnelRelayTunnelHost`
241+
* and pipes incoming connections to the local agent host.
242+
*/
243+
export const ITunnelAgentHostHostingService = createDecorator<ITunnelAgentHostHostingService>('tunnelAgentHostHostingService');
244+
245+
export interface ITunnelAgentHostHostingService {
246+
readonly _serviceBrand: undefined;
247+
248+
/** Fires when the hosting status changes. */
249+
readonly onDidChangeStatus: Event<TunnelHostStatus>;
250+
251+
/**
252+
* Start hosting a dev tunnel that forwards connections to the local
253+
* agent host. Creates a tunnel with the appropriate labels and port
254+
* configuration, then connects a `TunnelRelayTunnelHost`.
255+
*
256+
* @param token The user's access token.
257+
* @param authProvider The auth provider that issued the token.
258+
* @param socketInfo Socket path for the local agent host.
259+
*/
260+
startHosting(token: string, authProvider: 'github' | 'microsoft', socketInfo: IAgentHostSocketInfo): Promise<ITunnelHostInfo>;
261+
262+
/** Stop hosting and clean up the tunnel. */
263+
stopHosting(): Promise<void>;
264+
265+
/** Get the current hosting status. */
266+
getStatus(): Promise<TunnelHostStatus>;
267+
}

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { acquirePort } from '../../../base/parts/ipc/electron-browser/ipc.mp.js'
1313
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
1414
import { IConfigurationService } from '../../configuration/common/configuration.js';
1515
import { ILogService } from '../../log/common/log.js';
16-
import { AgentHostEnabledSettingId, AgentHostIpcChannels, IAgentCreateSessionConfig, IAgentHostService, IAgentResolveSessionConfigParams, IAgentService, IAgentSessionConfigCompletionsParams, IAgentSessionMetadata, IAuthenticateParams, IAuthenticateResult } from '../common/agentService.js';
16+
import { AgentHostEnabledSettingId, AgentHostIpcChannels, IAgentCreateSessionConfig, IAgentHostService, IAgentResolveSessionConfigParams, IAgentService, IAgentSessionConfigCompletionsParams, IAgentSessionMetadata, IAuthenticateParams, IAuthenticateResult, IAgentHostSocketInfo, IConnectionTrackerService } from '../common/agentService.js';
1717
import { AgentSubscriptionManager, type IAgentSubscription } from '../common/state/agentSubscription.js';
1818
import type { ICreateTerminalParams, IResolveSessionConfigResult, ISessionConfigCompletionsResult } from '../common/state/protocol/commands.js';
1919
import type { IActionEnvelope, INotification, ISessionAction, ITerminalAction } from '../common/state/sessionActions.js';
@@ -36,6 +36,7 @@ class AgentHostServiceClient extends Disposable implements IAgentHostService {
3636

3737
private readonly _clientEventually = new DeferredPromise<MessagePortClient>();
3838
private readonly _proxy: IAgentService;
39+
private readonly _connectionTracker: IConnectionTrackerService;
3940
private readonly _subscriptionManager: AgentSubscriptionManager;
4041

4142
private readonly _onAgentHostExit = this._register(new Emitter<number>());
@@ -61,6 +62,10 @@ class AgentHostServiceClient extends Disposable implements IAgentHostService {
6162
getDelayedChannel(this._clientEventually.p.then(client => client.getChannel(AgentHostIpcChannels.AgentHost)))
6263
);
6364

65+
this._connectionTracker = ProxyChannel.toService<IConnectionTrackerService>(
66+
getDelayedChannel(this._clientEventually.p.then(client => client.getChannel(AgentHostIpcChannels.ConnectionTracker)))
67+
);
68+
6469
this._subscriptionManager = this._register(new AgentSubscriptionManager(
6570
this.clientId,
6671
() => this.nextClientSeq(),
@@ -183,6 +188,10 @@ class AgentHostServiceClient extends Disposable implements IAgentHostService {
183188
async restartAgentHost(): Promise<void> {
184189
// Restart is handled by the main process side
185190
}
191+
192+
startWebSocketServer(): Promise<IAgentHostSocketInfo> {
193+
return this._connectionTracker.startWebSocketServer();
194+
}
186195
}
187196

188197
registerSingleton(IAgentHostService, AgentHostServiceClient, InstantiationType.Delayed);

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

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ import { Server as UtilityProcessServer } from '../../../base/parts/ipc/node/ipc
99
import { isUtilityProcess } from '../../../base/parts/sandbox/node/electronTypes.js';
1010
import { Emitter } from '../../../base/common/event.js';
1111
import { DisposableStore } from '../../../base/common/lifecycle.js';
12+
import { isWindows } from '../../../base/common/platform.js';
1213
import { URI } from '../../../base/common/uri.js';
14+
import { generateUuid } from '../../../base/common/uuid.js';
1315
import * as os from 'os';
14-
import { AgentHostIpcChannels } from '../common/agentService.js';
16+
import { AgentHostIpcChannels, IAgentHostSocketInfo, IConnectionTrackerService } from '../common/agentService.js';
1517
import { AgentService } from './agentService.js';
1618
import { IAgentHostTerminalManager } from './agentHostTerminalManager.js';
1719
import { CopilotAgent } from './copilot/copilotAgent.js';
@@ -42,6 +44,7 @@ import { AGENT_CLIENT_SCHEME } from '../common/agentClientUri.js';
4244
import { IAgentPluginManager } from '../common/agentPluginManager.js';
4345
import { AgentPluginManager } from './agentPluginManager.js';
4446
import { AgentHostGitService, IAgentHostGitService } from './agentHostGitService.js';
47+
import { join } from '../../../base/common/path.js';
4548

4649
// Entry point for the agent host utility process.
4750
// Sets up IPC, logging, and registers agent providers (Copilot).
@@ -105,13 +108,45 @@ function startAgentHost(): void {
105108
// This is NOT part of the agent host protocol -- it is only used by the
106109
// server process to manage the agent host process lifetime.
107110
const connectionCountEmitter = disposables.add(new Emitter<number>());
108-
const connectionTrackerChannel = ProxyChannel.fromService(
109-
{ onDidChangeConnectionCount: connectionCountEmitter.event },
110-
disposables,
111-
);
111+
let dynamicSocketInfo: IAgentHostSocketInfo | undefined;
112+
const connectionTrackerService: IConnectionTrackerService = {
113+
onDidChangeConnectionCount: connectionCountEmitter.event,
114+
async startWebSocketServer(): Promise<IAgentHostSocketInfo> {
115+
if (dynamicSocketInfo) {
116+
return dynamicSocketInfo;
117+
}
118+
119+
const socketPath = isWindows
120+
? `\\\\.\\pipe\\vscode-agent-host-${generateUuid().replace(/-/g, '')}`
121+
: join(os.tmpdir(), `vscode-agent-host-${generateUuid().replace(/-/g, '')}.sock`);
122+
123+
const wsServer = disposables.add(await WebSocketProtocolServer.create(
124+
{ socketPath },
125+
logService,
126+
));
127+
128+
const clientFileSystemProvider = disposables.add(new AgentHostClientFileSystemProvider());
129+
disposables.add(fileService.registerProvider(AGENT_CLIENT_SCHEME, clientFileSystemProvider));
130+
131+
const protocolHandler = disposables.add(new ProtocolServerHandler(
132+
agentService,
133+
agentService.stateManager,
134+
wsServer,
135+
{ defaultDirectory: URI.file(os.homedir()).toString() },
136+
clientFileSystemProvider,
137+
logService,
138+
));
139+
disposables.add(protocolHandler.onDidChangeConnectionCount(count => connectionCountEmitter.fire(count)));
140+
141+
logService.info(`[AgentHost] Dynamic WebSocket server listening on ${socketPath}`);
142+
dynamicSocketInfo = { socketPath };
143+
return dynamicSocketInfo;
144+
},
145+
};
146+
const connectionTrackerChannel = ProxyChannel.fromService(connectionTrackerService, disposables);
112147
server.registerChannel(AgentHostIpcChannels.ConnectionTracker, connectionTrackerChannel);
113148

114-
// Start WebSocket server for external clients if configured
149+
// Start WebSocket server for external clients if configured (env-var flow for CLI/server)
115150
startWebSocketServer(agentService, fileService, logService, disposables, count => connectionCountEmitter.fire(count)).catch(err => {
116151
logService.error('Failed to start WebSocket server', err);
117152
});

0 commit comments

Comments
 (0)