-
Notifications
You must be signed in to change notification settings - Fork 0
Add /remote-control command to Gemini CLI #1
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
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 | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,203 @@ | ||||||||||||||||||||||
| /** | ||||||||||||||||||||||
| * @license | ||||||||||||||||||||||
| * Copyright 2025 Google LLC | ||||||||||||||||||||||
| * SPDX-License-Identifier: Apache-2.0 | ||||||||||||||||||||||
| */ | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| import WebSocket from 'ws'; | ||||||||||||||||||||||
| import { | ||||||||||||||||||||||
| ShellExecutionService, | ||||||||||||||||||||||
| coreEvents, | ||||||||||||||||||||||
| CoreEvent, | ||||||||||||||||||||||
| type OutputPayload, | ||||||||||||||||||||||
| type ConsoleLogPayload, | ||||||||||||||||||||||
| } from '@google/gemini-cli-core'; | ||||||||||||||||||||||
| import { remoteControlEmitter, REMOTE_INPUT_EVENT } from './remoteControlEmitter.js'; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| interface RemoteCommand { | ||||||||||||||||||||||
| type: 'exec' | 'stdin' | 'resize' | 'input'; | ||||||||||||||||||||||
| command?: string; | ||||||||||||||||||||||
| input?: string; | ||||||||||||||||||||||
| pid?: number; | ||||||||||||||||||||||
| cols?: number; | ||||||||||||||||||||||
| rows?: number; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| export class RemoteControlService { | ||||||||||||||||||||||
| private ws: WebSocket | null = null; | ||||||||||||||||||||||
| private url: string; | ||||||||||||||||||||||
| private connected = false; | ||||||||||||||||||||||
| private activePids = new Set<number>(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| constructor(url: string) { | ||||||||||||||||||||||
| this.url = url; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| public connect(): Promise<void> { | ||||||||||||||||||||||
| return new Promise((resolve, reject) => { | ||||||||||||||||||||||
| try { | ||||||||||||||||||||||
| this.ws = new WebSocket(this.url); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| this.ws.on('open', () => { | ||||||||||||||||||||||
| this.connected = true; | ||||||||||||||||||||||
| this.subscribeToCoreEvents(); | ||||||||||||||||||||||
| resolve(); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| this.ws.on('message', (data: WebSocket.RawData) => { | ||||||||||||||||||||||
| this.handleMessage(data); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| this.ws.on('close', () => { | ||||||||||||||||||||||
| this.connected = false; | ||||||||||||||||||||||
| this.unsubscribeFromCoreEvents(); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| this.ws.on('error', (err) => { | ||||||||||||||||||||||
| if (!this.connected) { | ||||||||||||||||||||||
| reject(err); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| // If already connected, error will likely be followed by close | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| } catch (err) { | ||||||||||||||||||||||
| reject(err); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| public disconnect(): void { | ||||||||||||||||||||||
| if (this.ws) { | ||||||||||||||||||||||
| this.ws.close(); | ||||||||||||||||||||||
| this.ws = null; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| this.connected = false; | ||||||||||||||||||||||
| this.unsubscribeFromCoreEvents(); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| public isConnected(): boolean { | ||||||||||||||||||||||
| return this.connected; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| private subscribeToCoreEvents() { | ||||||||||||||||||||||
| coreEvents.on(CoreEvent.Output, this.handleOutput); | ||||||||||||||||||||||
| coreEvents.on(CoreEvent.ConsoleLog, this.handleConsoleLog); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| private unsubscribeFromCoreEvents() { | ||||||||||||||||||||||
| coreEvents.off(CoreEvent.Output, this.handleOutput); | ||||||||||||||||||||||
| coreEvents.off(CoreEvent.ConsoleLog, this.handleConsoleLog); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| private handleOutput = (payload: OutputPayload) => { | ||||||||||||||||||||||
| // Forward global output to remote | ||||||||||||||||||||||
| this.sendMessage({ | ||||||||||||||||||||||
| type: 'output', | ||||||||||||||||||||||
| stream: payload.isStderr ? 'stderr' : 'stdout', | ||||||||||||||||||||||
| chunk: payload.chunk.toString(), | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| }; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| private handleConsoleLog = (payload: ConsoleLogPayload) => { | ||||||||||||||||||||||
| // Forward console logs to remote | ||||||||||||||||||||||
| this.sendMessage({ | ||||||||||||||||||||||
| type: 'log', | ||||||||||||||||||||||
| level: payload.type, | ||||||||||||||||||||||
| content: payload.content, | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| }; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| private handleMessage(data: WebSocket.RawData) { | ||||||||||||||||||||||
| try { | ||||||||||||||||||||||
| const message = JSON.parse(data.toString()) as RemoteCommand; | ||||||||||||||||||||||
| switch (message.type) { | ||||||||||||||||||||||
| case 'exec': | ||||||||||||||||||||||
| if (message.command) { | ||||||||||||||||||||||
| this.executeCommand(message.command); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| break; | ||||||||||||||||||||||
| case 'stdin': | ||||||||||||||||||||||
| if (message.pid && message.input) { | ||||||||||||||||||||||
| ShellExecutionService.writeToPty(message.pid, message.input); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| break; | ||||||||||||||||||||||
|
Comment on lines
+118
to
+122
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. The Tracing the data flow:
This should be restricted to only allow input to processes explicitly managed by the remote session, and only after proper authorization. |
||||||||||||||||||||||
| case 'input': | ||||||||||||||||||||||
| if (message.input) { | ||||||||||||||||||||||
| remoteControlEmitter.emit(REMOTE_INPUT_EVENT, { input: message.input }); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| break; | ||||||||||||||||||||||
| case 'resize': | ||||||||||||||||||||||
| if (message.pid && message.cols && message.rows) { | ||||||||||||||||||||||
| ShellExecutionService.resizePty( | ||||||||||||||||||||||
| message.pid, | ||||||||||||||||||||||
| message.cols, | ||||||||||||||||||||||
| message.rows, | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| break; | ||||||||||||||||||||||
| default: | ||||||||||||||||||||||
| console.warn('Unknown remote command type:', message.type); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||
| console.error('Failed to parse remote message:', e); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| private async executeCommand(command: string) { | ||||||||||||||||||||||
| const cwd = process.cwd(); | ||||||||||||||||||||||
| const abortController = new AbortController(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| try { | ||||||||||||||||||||||
| const { pid, result } = await ShellExecutionService.execute( | ||||||||||||||||||||||
| command, | ||||||||||||||||||||||
| cwd, | ||||||||||||||||||||||
| (event) => { | ||||||||||||||||||||||
| if (pid) { | ||||||||||||||||||||||
| this.sendMessage({ | ||||||||||||||||||||||
| type: 'shell_event', | ||||||||||||||||||||||
| pid, | ||||||||||||||||||||||
| event | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
| abortController.signal, | ||||||||||||||||||||||
| true, // Use PTY if available | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| sanitizationConfig: { | ||||||||||||||||||||||
| allowedEnvironmentVariables: [], | ||||||||||||||||||||||
| blockedEnvironmentVariables: [], | ||||||||||||||||||||||
| enableEnvironmentVariableRedaction: false, | ||||||||||||||||||||||
| }, | ||||||||||||||||||||||
|
Comment on lines
+165
to
+169
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. The For a remote execution feature, the environment should be heavily restricted by default. You should block all environment variables and only allow specific, safe ones if necessary.
Suggested change
|
||||||||||||||||||||||
| } | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if (pid) { | ||||||||||||||||||||||
| this.activePids.add(pid); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const res = await result; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if (pid) { | ||||||||||||||||||||||
| this.activePids.delete(pid); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| this.sendMessage({ | ||||||||||||||||||||||
| type: 'exec_result', | ||||||||||||||||||||||
| pid, | ||||||||||||||||||||||
| exitCode: res.exitCode, | ||||||||||||||||||||||
| output: res.output | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||
| this.sendMessage({ | ||||||||||||||||||||||
| type: 'exec_error', | ||||||||||||||||||||||
| error: e instanceof Error ? e.message : String(e) | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| private sendMessage(message: any) { | ||||||||||||||||||||||
| if (this.ws && this.connected) { | ||||||||||||||||||||||
| this.ws.send(JSON.stringify(message)); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| /** | ||
| * @license | ||
| * Copyright 2025 Google LLC | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| import { EventEmitter } from 'events'; | ||
|
|
||
| export const remoteControlEmitter = new EventEmitter(); | ||
| export const REMOTE_INPUT_EVENT = 'remote-input'; | ||
|
|
||
| export interface RemoteInputPayload { | ||
| input: string; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -142,6 +142,11 @@ import { NewAgentsChoice } from './components/NewAgentsNotification.js'; | |
| import { isSlashCommand } from './utils/commandUtils.js'; | ||
| import { useTerminalTheme } from './hooks/useTerminalTheme.js'; | ||
| import { isITerm2 } from './utils/terminalUtils.js'; | ||
| import { | ||
| remoteControlEmitter, | ||
| REMOTE_INPUT_EVENT, | ||
| type RemoteInputPayload, | ||
| } from '../services/remoteControlEmitter.js'; | ||
|
|
||
| function isToolExecuting(pendingHistoryItems: HistoryItemWithoutId[]) { | ||
| return pendingHistoryItems.some((item) => { | ||
|
|
@@ -1106,6 +1111,16 @@ Logging in with Google... Restarting Gemini CLI to continue. | |
| ], | ||
| ); | ||
|
|
||
| useEffect(() => { | ||
| const handleRemoteInput = (payload: RemoteInputPayload) => { | ||
| handleFinalSubmit(payload.input); | ||
| }; | ||
| remoteControlEmitter.on(REMOTE_INPUT_EVENT, handleRemoteInput); | ||
| return () => { | ||
| remoteControlEmitter.off(REMOTE_INPUT_EVENT, handleRemoteInput); | ||
| }; | ||
| }, [handleFinalSubmit]); | ||
|
Comment on lines
+1114
to
+1122
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. The Tracing the data flow:
This effectively grants full control over the CLI session to the remote server. Remote input should be strictly validated and restricted to a safe subset of operations. |
||
|
|
||
| const handleClearScreen = useCallback(() => { | ||
| historyManager.clearItems(); | ||
| clearConsoleMessagesState(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,61 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * @license | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Copyright 2025 Google LLC | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * SPDX-License-Identifier: Apache-2.0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Hoist the mock factory | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| vi.mock('../../services/RemoteControlService.js', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const RemoteControlService = vi.fn(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| RemoteControlService.prototype.connect = vi.fn().mockResolvedValue(undefined); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| RemoteControlService.prototype.disconnect = vi.fn(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| RemoteControlService.prototype.isConnected = vi.fn().mockReturnValue(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { RemoteControlService }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { remoteControlCommand } from './remoteControlCommand.js'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { createMockCommandContext } from '../../test-utils/mockCommandContext.js'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { MessageType } from '../types.js'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { RemoteControlService } from '../../services/RemoteControlService.js'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| describe('remoteControlCommand', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let mockContext: any; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const originalEnv = process.env; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| beforeEach(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| vi.clearAllMocks(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| process.env = { ...originalEnv }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mockContext = createMockCommandContext({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ui: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| addItem: vi.fn(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } as any); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| afterEach(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| process.env = originalEnv; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+25
to
+39
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. This test modifies
Suggested change
References
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| it('should start a remote control session', async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| process.env['GEMINI_REMOTE_CONTROL_URL'] = 'ws://test-url'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
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. In accordance with the repository's testing conventions for environment variables (lines 67-71), please use
Suggested change
References
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!remoteControlCommand.action) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error('Command has no action'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // @ts-ignore | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await remoteControlCommand.action(mockContext, ''); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(RemoteControlService).toHaveBeenCalledWith(expect.stringMatching(/^ws:\/\/test-url\?token=/)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(RemoteControlService.prototype.connect).toHaveBeenCalled(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect(mockContext.ui.addItem).toHaveBeenCalledWith( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| expect.objectContaining({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: MessageType.INFO, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| text: expect.stringContaining('Connected to remote control service'), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| /** | ||
| * @license | ||
| * Copyright 2025 Google LLC | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| import { RemoteControlService } from '../../services/RemoteControlService.js'; | ||
| import { CommandKind, type SlashCommand } from './types.js'; | ||
| import { MessageType } from '../types.js'; | ||
| import { randomBytes } from 'crypto'; | ||
|
|
||
| let service: RemoteControlService | null = null; | ||
|
|
||
| export const remoteControlCommand: SlashCommand = { | ||
| name: 'remote-control', | ||
| description: 'Start a remote control session via Google Chat', | ||
| kind: CommandKind.BUILT_IN, | ||
| autoExecute: true, | ||
| action: async (context) => { | ||
| if (service && service.isConnected()) { | ||
| context.ui.addItem({ | ||
| type: MessageType.INFO, | ||
| text: 'Remote control session is already active.', | ||
| }); | ||
| return; | ||
| } | ||
|
|
||
| const token = randomBytes(16).toString('hex'); | ||
| const baseUrl = process.env['GEMINI_REMOTE_CONTROL_URL'] || 'ws://localhost:8080'; | ||
| const url = `${baseUrl}?token=${token}`; | ||
|
|
||
| context.ui.addItem({ | ||
| type: MessageType.INFO, | ||
| text: `Connecting to remote control service at ${baseUrl}...`, | ||
| }); | ||
| context.ui.addItem({ | ||
| type: MessageType.INFO, | ||
| text: `Session Token: ${token}`, | ||
| }); | ||
|
|
||
| service = new RemoteControlService(url); | ||
|
|
||
| try { | ||
| await service.connect(); | ||
|
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. After a successful connection, a cleanup function should be registered to ensure await service.connect();
registerCleanup(() => service?.disconnect()); |
||
| context.ui.addItem({ | ||
| type: MessageType.INFO, | ||
| text: 'Connected to remote control service. You can now manage your session via Google Chat.', | ||
| }); | ||
| context.ui.addItem({ | ||
| type: MessageType.INFO, | ||
| text: 'Press Ctrl+C to stop the CLI (which will also stop the remote session).', | ||
| }); | ||
| } catch (err) { | ||
| context.ui.addItem({ | ||
| type: MessageType.ERROR, | ||
| text: `Failed to connect: ${err instanceof Error ? err.message : String(err)}`, | ||
| }); | ||
| service = null; | ||
| } | ||
| }, | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
RemoteControlServicedirectly executes commands received from the WebSocket server without any validation or sanitization. This allows a malicious or compromised server to achieve Remote Code Execution (RCE) on the user's machine by sending anexeccommand.Tracing the data flow:
handleMessage(line 109) receives raw data from the WebSocket.JSON.parse(line 111) extracts thecommandproperty.executeCommand(line 115) is called with the untrusted command.ShellExecutionService.execute(line 150) runs the command in a shell.This is a critical security vulnerability. You must implement a strict allow-list of permitted commands or ensure that the remote server is fully trusted and the connection is securely authenticated.