Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/vs/code/electron-utility/sharedProcess/sharedProcessMain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,9 @@ import { IExtensionsScannerService } from '../../../platform/extensionManagement
import { ExtensionsScannerService } from '../../../platform/extensionManagement/node/extensionsScannerService.js';
import { ISSHRemoteAgentHostMainService, SSH_REMOTE_AGENT_HOST_CHANNEL } from '../../../platform/agentHost/common/sshRemoteAgentHost.js';
import { SSHRemoteAgentHostMainService } from '../../../platform/agentHost/node/sshRemoteAgentHostService.js';
import { ITunnelAgentHostMainService, TUNNEL_AGENT_HOST_CHANNEL } from '../../../platform/agentHost/common/tunnelAgentHost.js';
import { ITunnelAgentHostMainService, ITunnelAgentHostHostingService, TUNNEL_AGENT_HOST_CHANNEL, TUNNEL_HOST_CHANNEL } from '../../../platform/agentHost/common/tunnelAgentHost.js';
import { TunnelAgentHostMainService } from '../../../platform/agentHost/node/tunnelAgentHostService.js';
import { TunnelHostMainService } from '../../../platform/agentHost/node/tunnelHostMainService.js';
import { IUserDataProfilesService } from '../../../platform/userDataProfile/common/userDataProfile.js';
import { IExtensionsProfileScannerService } from '../../../platform/extensionManagement/common/extensionsProfileScannerService.js';
import { PolicyChannelClient } from '../../../platform/policy/common/policyIpc.js';
Expand Down Expand Up @@ -418,6 +419,9 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter {
// Tunnel Agent Host
services.set(ITunnelAgentHostMainService, new SyncDescriptor(TunnelAgentHostMainService, undefined, true));

// Tunnel Host (hosting local agent host for remote connections)
services.set(ITunnelAgentHostHostingService, new SyncDescriptor(TunnelHostMainService, undefined, true));

return new InstantiationService(services);
}

Expand Down Expand Up @@ -501,6 +505,10 @@ class SharedProcessMain extends Disposable implements IClientConnectionFilter {
// Tunnel Agent Host
const tunnelAgentHostChannel = ProxyChannel.fromService(accessor.get(ITunnelAgentHostMainService), this._store);
this.server.registerChannel(TUNNEL_AGENT_HOST_CHANNEL, tunnelAgentHostChannel);

// Tunnel Host
const tunnelHostChannel = ProxyChannel.fromService(accessor.get(ITunnelAgentHostHostingService), this._store);
this.server.registerChannel(TUNNEL_HOST_CHANNEL, tunnelHostChannel);
}

private registerErrorHandler(logService: ILogService): void {
Expand Down
23 changes: 23 additions & 0 deletions src/vs/platform/agentHost/common/agentService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,27 @@ export const AgentHostEnabledSettingId = 'chat.agentHost.enabled';
/** Configuration key that controls whether per-host IPC traffic output channels are created. */
export const AgentHostIpcLoggingSettingId = 'chat.agentHost.ipcLoggingEnabled';

/** Result of starting the agent host WebSocket server on-demand. */
export interface IAgentHostSocketInfo {
readonly socketPath: string;
}

/**
* IPC service exposed on the {@link AgentHostIpcChannels.ConnectionTracker}
* channel. Used by the server process for lifetime management and by the
* shared process to request a local WebSocket listener on-demand.
*/
export interface IConnectionTrackerService {
readonly onDidChangeConnectionCount: Event<number>;

/**
* Request the agent host to start a WebSocket server on a local
* pipe/socket. Returns the socket path.
* If a server is already running, returns the existing info.
*/
startWebSocketServer(): Promise<IAgentHostSocketInfo>;
}

// ---- IPC data types (serializable across MessagePort) -----------------------

export interface IAgentSessionMetadata {
Expand Down Expand Up @@ -642,4 +663,6 @@ export interface IAgentHostService extends IAgentConnection {
readonly onAgentHostStart: Event<void>;

restartAgentHost(): Promise<void>;

startWebSocketServer(): Promise<IAgentHostSocketInfo>;
}
52 changes: 52 additions & 0 deletions src/vs/platform/agentHost/common/tunnelAgentHost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import { Event } from '../../../base/common/event.js';
import { createDecorator } from '../../instantiation/common/instantiation.js';
import type { IAgentHostSocketInfo } from './agentService.js';

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

Expand Down Expand Up @@ -213,3 +214,54 @@ export interface ITunnelAgentHostService {
*/
getAuthProvider(options?: { silent?: boolean }): Promise<'github' | 'microsoft' | undefined>;
}

// ---- Tunnel hosting (exposing the local agent host to remote clients) --------

/** IPC channel name for the tunnel host service. */
export const TUNNEL_HOST_CHANNEL = 'tunnelHost';

/** Output channel ID for the tunnel host logs. */
export const TUNNEL_HOST_LOG_ID = 'tunnelHostService';

/** Information about an actively hosted tunnel. */
export interface ITunnelHostInfo {
readonly tunnelName: string;
readonly tunnelId: string;
readonly clusterId: string;
readonly domain: string;
}

/** Status of the tunnel host. */
export type TunnelHostStatus =
| { readonly active: false }
| { readonly active: true; readonly info: ITunnelHostInfo };

/**
* Shared-process service that hosts a dev tunnel using `TunnelRelayTunnelHost`
* and pipes incoming connections to the local agent host.
*/
export const ITunnelAgentHostHostingService = createDecorator<ITunnelAgentHostHostingService>('tunnelAgentHostHostingService');

export interface ITunnelAgentHostHostingService {
readonly _serviceBrand: undefined;

/** Fires when the hosting status changes. */
readonly onDidChangeStatus: Event<TunnelHostStatus>;

/**
* Start hosting a dev tunnel that forwards connections to the local
* agent host. Creates a tunnel with the appropriate labels and port
* configuration, then connects a `TunnelRelayTunnelHost`.
*
* @param token The user's access token.
* @param authProvider The auth provider that issued the token.
* @param socketInfo Socket path for the local agent host.
*/
startHosting(token: string, authProvider: 'github' | 'microsoft', socketInfo: IAgentHostSocketInfo): Promise<ITunnelHostInfo>;

/** Stop hosting and clean up the tunnel. */
stopHosting(): Promise<void>;

/** Get the current hosting status. */
getStatus(): Promise<TunnelHostStatus>;
}
11 changes: 10 additions & 1 deletion src/vs/platform/agentHost/electron-browser/agentHostService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { acquirePort } from '../../../base/parts/ipc/electron-browser/ipc.mp.js'
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
import { IConfigurationService } from '../../configuration/common/configuration.js';
import { ILogService } from '../../log/common/log.js';
import { AgentHostEnabledSettingId, AgentHostIpcChannels, IAgentCreateSessionConfig, IAgentHostService, IAgentResolveSessionConfigParams, IAgentService, IAgentSessionConfigCompletionsParams, IAgentSessionMetadata, IAuthenticateParams, IAuthenticateResult } from '../common/agentService.js';
import { AgentHostEnabledSettingId, AgentHostIpcChannels, IAgentCreateSessionConfig, IAgentHostService, IAgentResolveSessionConfigParams, IAgentService, IAgentSessionConfigCompletionsParams, IAgentSessionMetadata, IAuthenticateParams, IAuthenticateResult, IAgentHostSocketInfo, IConnectionTrackerService } from '../common/agentService.js';
import { AgentSubscriptionManager, type IAgentSubscription } from '../common/state/agentSubscription.js';
import type { ICreateTerminalParams, IResolveSessionConfigResult, ISessionConfigCompletionsResult } from '../common/state/protocol/commands.js';
import type { IActionEnvelope, INotification, ISessionAction, ITerminalAction } from '../common/state/sessionActions.js';
Expand All @@ -36,6 +36,7 @@ class AgentHostServiceClient extends Disposable implements IAgentHostService {

private readonly _clientEventually = new DeferredPromise<MessagePortClient>();
private readonly _proxy: IAgentService;
private readonly _connectionTracker: IConnectionTrackerService;
private readonly _subscriptionManager: AgentSubscriptionManager;

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

this._connectionTracker = ProxyChannel.toService<IConnectionTrackerService>(
getDelayedChannel(this._clientEventually.p.then(client => client.getChannel(AgentHostIpcChannels.ConnectionTracker)))
);

this._subscriptionManager = this._register(new AgentSubscriptionManager(
this.clientId,
() => this.nextClientSeq(),
Expand Down Expand Up @@ -183,6 +188,10 @@ class AgentHostServiceClient extends Disposable implements IAgentHostService {
async restartAgentHost(): Promise<void> {
// Restart is handled by the main process side
}

startWebSocketServer(): Promise<IAgentHostSocketInfo> {
return this._connectionTracker.startWebSocketServer();
}
}

registerSingleton(IAgentHostService, AgentHostServiceClient, InstantiationType.Delayed);
47 changes: 41 additions & 6 deletions src/vs/platform/agentHost/node/agentHostMain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import { Server as UtilityProcessServer } from '../../../base/parts/ipc/node/ipc
import { isUtilityProcess } from '../../../base/parts/sandbox/node/electronTypes.js';
import { Emitter } from '../../../base/common/event.js';
import { DisposableStore } from '../../../base/common/lifecycle.js';
import { isWindows } from '../../../base/common/platform.js';
import { URI } from '../../../base/common/uri.js';
import { generateUuid } from '../../../base/common/uuid.js';
import * as os from 'os';
import { AgentHostIpcChannels } from '../common/agentService.js';
import { AgentHostIpcChannels, IAgentHostSocketInfo, IConnectionTrackerService } from '../common/agentService.js';
import { AgentService } from './agentService.js';
import { IAgentHostTerminalManager } from './agentHostTerminalManager.js';
import { CopilotAgent } from './copilot/copilotAgent.js';
Expand Down Expand Up @@ -42,6 +44,7 @@ import { AGENT_CLIENT_SCHEME } from '../common/agentClientUri.js';
import { IAgentPluginManager } from '../common/agentPluginManager.js';
import { AgentPluginManager } from './agentPluginManager.js';
import { AgentHostGitService, IAgentHostGitService } from './agentHostGitService.js';
import { join } from '../../../base/common/path.js';

// Entry point for the agent host utility process.
// Sets up IPC, logging, and registers agent providers (Copilot).
Expand Down Expand Up @@ -105,13 +108,45 @@ function startAgentHost(): void {
// This is NOT part of the agent host protocol -- it is only used by the
// server process to manage the agent host process lifetime.
const connectionCountEmitter = disposables.add(new Emitter<number>());
const connectionTrackerChannel = ProxyChannel.fromService(
{ onDidChangeConnectionCount: connectionCountEmitter.event },
disposables,
);
let dynamicSocketInfo: IAgentHostSocketInfo | undefined;
const connectionTrackerService: IConnectionTrackerService = {
onDidChangeConnectionCount: connectionCountEmitter.event,
async startWebSocketServer(): Promise<IAgentHostSocketInfo> {
if (dynamicSocketInfo) {
return dynamicSocketInfo;
}

const socketPath = isWindows
? `\\\\.\\pipe\\vscode-agent-host-${generateUuid().replace(/-/g, '')}`
: join(os.tmpdir(), `vscode-agent-host-${generateUuid().replace(/-/g, '')}.sock`);

const wsServer = disposables.add(await WebSocketProtocolServer.create(
{ socketPath },
logService,
));

const clientFileSystemProvider = disposables.add(new AgentHostClientFileSystemProvider());
disposables.add(fileService.registerProvider(AGENT_CLIENT_SCHEME, clientFileSystemProvider));
Comment thread
connor4312 marked this conversation as resolved.

const protocolHandler = disposables.add(new ProtocolServerHandler(
agentService,
agentService.stateManager,
wsServer,
{ defaultDirectory: URI.file(os.homedir()).toString() },
clientFileSystemProvider,
logService,
));
disposables.add(protocolHandler.onDidChangeConnectionCount(count => connectionCountEmitter.fire(count)));

logService.info(`[AgentHost] Dynamic WebSocket server listening on ${socketPath}`);
dynamicSocketInfo = { socketPath };
return dynamicSocketInfo;
},
};
const connectionTrackerChannel = ProxyChannel.fromService(connectionTrackerService, disposables);
server.registerChannel(AgentHostIpcChannels.ConnectionTracker, connectionTrackerChannel);

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