Feature: Add persistent remote terminal MCP tools

Add terminal_open / terminal_exec / terminal_close so an AI can hold one
shell session per Windows host and run a sequence of commands with cwd and
environment preserved, instead of the one-shot exec_command.

The persistent terminal is a separate full-command write capability gated by
McpTerminal (default off) plus McpReadonly=0, with no whitelist and full
audit. One device maps to one terminal session via the shared m_TermSessions
map; idle sessions are swept after 300s. terminal_exec rejects commands that
contain & or | (they corrupt the sentinel control-operator chain) as well as
control characters.

Also harden exec_command and terminal_exec against newline/CR injection, fix a
dangling subCtx after an abrupt shell disconnect, and refresh lastActiveAt on
command completion. Add en/zh-TW translations for the new UI strings and a
design document covering both exec_command and the persistent terminal.

Co-Authored-By: deepseek-v4-pro
This commit is contained in:
yuanyuanxiang
2026-08-24 19:11:16 +02:00
parent 0ddbc1aced
commit 70dbb6b255
8 changed files with 1099 additions and 9 deletions

View File

@@ -2193,6 +2193,8 @@ BOOL CMy2015RemoteDlg::OnInitDialog()
// 安全配置:只读默认 1exec_command 默认禁用);白名单空则用内置只读前缀。
McpServer().SetReadonly(THIS_CFG.GetInt("settings", "McpReadonly", 1) != 0);
McpServer().SetCmdWhitelist(THIS_CFG.GetStr("settings", "McpCmdWhitelist", ""));
// 持久终端开关默认关要求只读关McpReadonly=0才生效工具列表/分派双重门控)。
McpServer().SetTerminalEnabled(THIS_CFG.GetInt("settings", "McpTerminal", 0) != 0);
if (!McpServer().Start(mcpBind, mcpPort)) {
Mprintf("McpServer start failed on %s:%d\n", mcpBind.c_str(), mcpPort);
} else {
@@ -5158,6 +5160,12 @@ BOOL CALLBACK CMy2015RemoteDlg::OfflineProc(CONTEXT_OBJECT* ContextObject)
WebService().OnTerminalClosed(ContextObject);
}
// MCP 终端(一次性 exec / 持久终端)的 shell 子上下文断开:同步清理会话,避免悬空 subCtx
// 被连接池复用时误路由新连接(与 WebService 清理同理,需在 RemoveFromHostList 之前)。
if (McpServer().IsRunning() && McpServer().IsTerminalContext(ContextObject)) {
McpServer().OnTerminalClosed(ContextObject);
}
SOCKET nSocket = ContextObject->sClientSocket;
CDialogBase* p = (CDialogBase*)ContextObject->hDlg;