Add P7 management-control tools behind a single McpManage flag (plus
McpReadonly=0): kill_process, operate_window, control_service and
power_action. The write tools run a ready->confirm lifecycle on their
sub-connection, using a new followUp/followUpSent field to tell the two
arrivals of the same TOKEN_*LIST apart; the write command and the follow-up
refresh command go out as two separate Send2Client calls, with a new
WaitSendIdle() (context.h / Server.h) draining the outbound buffer in between
so the same buffer is never WSASend'd concurrently.
McpSettingsDlg gains a "management operations" checkbox (IDC_MCP_MANAGE), the
main toolbar gains an MCP entry button, and en/de/zh-TW strings are synced.
power_action is fire-and-forget over the main connection (no confirmation).
Verified end-to-end against a real Windows 10/VMware client: kill_process
returns remaining:[] and operate_window/control_service return ok:true with
their confirm packet arriving after an HTTP split. Verification surfaced a
prerequisite parser bug, fixed separately in a72e6eb, where HeaderParser
latched m_bShouldUnmask FALSE on HTTP continuation chunks and dropped the
confirm packet.
Co-Authored-By: deepseek-v4-pro
13 KiB
MCP P7 设计:设备管理控制(kill / window / service / power)
延续
Mcp_Design.md→Mcp_Phase2_Design.md(P2)→ P3/P4/P5/P6 的阶段演进。 本文定义 P7:把 YAMA 服务端高频「控制类」操作 MCP 化,补上从「观测」到「管控」的短板, 服务「自然语言即可操作服务端、对受管设备全方位管控」的最终目标。安全门结论(已确认):新增单一
McpManage开关统一门控本阶段全部控制类工具, 并要求McpReadonly=0;不再为每个工具单设开关。
1. 目标与范围
P7 交付 4 个控制类工具,覆盖服务端日常最常用的写操作:
| 工具 | 动作 | 命令 | 连接 | 确认方式 |
|---|---|---|---|---|
kill_process |
结束进程(多 pid) | COMMAND_KILLPROCESS |
系统子连接 | 补发 COMMAND_PSLIST 收新列表 |
operate_window |
关闭/显示/隐藏/最小化/最大化/还原 | CMD_WINDOW_CLOSE / CMD_WINDOW_TEST |
窗口子连接 | close 自动回 TOKEN_WSLIST;test 补发 COMMAND_WSLIST |
control_service |
启动/停止/设为自动/设为手动 | COMMAND_SERVICECONFIG |
服务子连接 | 客户端每项 Sleep(500) 后自动 SendServicesList |
power_action |
注销/关机/重启 | TOKEN_MACHINE_MANAGE |
主连接 | 发完即走(无响应) |
仅 Windows 客户端支持(clientType 为 LNX/MAC/APK 提前返回 -32005;注意 list_services
只排除了 LNX/MAC、漏掉 APK,P7 需把 APK 一并排除)。
2. 复用现状
P7 不改客户端。客户端已全部实现下列处理(MFC 界面在跑),MCP 侧只需补齐 「发命令 → 接管子连接 → 发写命令 → 收确认 → 关闭」这条链:
- 系统子连接:
COMMAND_SYSTEM(进程)/COMMAND_WSLIST(窗口)→ 客户端开CSystemManager子连接(client/KernelManager.cpp1169/1182,client/SystemManager.cpp)。 COMMAND_KILLPROCESS:CSystemManager::KillProcess(TerminateProcess,无回包)。CMD_WINDOW_CLOSE:PostMessage(hWnd, WM_CLOSE)后自动SendWindowsList。CMD_WINDOW_TEST:ShowWindow(hwnd, dHow)(不自动回包)。COMMAND_SERVICECONFIG:CServicesManager::ServicesConfig(client/ServicesManager.cpp213), bCmd=1 启动 / 2 停止 / 3 自动 / 4 手动,每项后Sleep(500)+SendServicesList。TOKEN_MACHINE_MANAGE:KernelManager.cpp872,ExitWindowsEx(注销/关机/重启)。
复用现有 MCP 基建(server/2015Remote/McpServer.{h,cpp}):
ParseHostIdArg/FindMainContext:入参解析 + 主连接定位。BeginPending/WaitPending/ClearPending/TakeMainResponse:单飞行挂起 + 响应回收。OnRegeditReady:已示范「接管子连接 → 发后续命令」的范式(P3 list_registry 三阶段),P7 直接泛化。
3. 子连接生命周期(P7 核心新增)
写工具与只读工具的关键差异:只读工具「子连接回一包即完成」,写工具需要
「接管子连接 → 发写命令 → 收确认 → 关闭」。在现有 PendingRequest 上增加
followUp(接管后要发的写命令 payload,二进制)+ followUpSent(是否已发写命令)两个字段:
followUpSent == false → 等子连接就绪(首包 TOKEN_*LIST 到达 = 就绪)
followUpSent == true → 已发写命令,等确认包
关键事实(已核实):三个工具各自的「就绪 token」与「确认 token」是同一个 token—— 客户端子连接构造即回一包(就绪),写操作后再回一包(确认)。因此必须用
followUpSent区分这两次到达,而不能靠 token 区分。这也是它与list_registry(PATH/KEY 两个不同 token) 的差别。
流程(以 kill_process 为例):
- 工具线程
BeginPending(devId, "kill_process")登记单飞行(false = 设备忙),并预填followUp(COMMAND_KILLPROCESS多 pid 包)。 - 主连接发
COMMAND_SYSTEM。 - 客户端开系统子连接,首包
TOKEN_PSLIST到达 →MessageHandle调新增的OnSystemReady(devId, subCtx)(类比OnRegeditReady):- 命中挂起且
followUpSent==false→ 保存subCtx、置followUpSent=true、向子连接发followUp,随后补发COMMAND_PSLIST索取新列表。
- 命中挂起且
- 客户端杀进程后回第二个
TOKEN_PSLIST→TakeMainResponse取走(followUpSent==true命中)→ 关闭子连接(CancelIO)→ 唤醒工具线程。 - 工具线程解析新进程列表返回;超时/失败走
ClearPending(含subCtx->CancelIO(),防孤儿子连接)。
注意:
followUp(写命令)与「补发的COMMAND_PSLIST/COMMAND_WSLIST」必须是两次独立的Send2Client调用,不能拼成一个 buffer——客户端OnReceive按szBuffer[0]单命令分发, 拼包会把后续字节当成 pid/hwnd 解析。
接管函数(进程/窗口共用同一个 OnSystemReady——两者都在同一个 CSystemManager 子连接上,
客户端 OnReceive 统一处理 COMMAND_PSLIST/COMMAND_KILLPROCESS/COMMAND_WSLIST/
CMD_WINDOW_CLOSE/CMD_WINDOW_TEST):
| 工具 | 开子连接命令 | 就绪包(首包) | 接管后写命令 | 确认包(第二包) |
|---|---|---|---|---|
kill_process |
COMMAND_SYSTEM |
TOKEN_PSLIST |
COMMAND_KILLPROCESS + 补 COMMAND_PSLIST |
TOKEN_PSLIST |
operate_window |
COMMAND_WSLIST |
TOKEN_WSLIST |
CMD_WINDOW_CLOSE(close 自动回);CMD_WINDOW_TEST(test 补 COMMAND_WSLIST) |
TOKEN_WSLIST |
control_service |
COMMAND_SERVICES |
TOKEN_SERVERLIST |
COMMAND_SERVICECONFIG(客户端写后自动 SendServicesList) |
TOKEN_SERVERLIST |
MessageHandle 现有分派点(均已核实):
TOKEN_PSLIST/TOKEN_WSLIST分支:2015RemoteDlg.cpp:6540(当前TakeMainResponse+CancelIO)TOKEN_SERVERLIST分支:2015RemoteDlg.cpp:6585(同上)
接入方式:在这两个分支的 IsPending(devId) 命中后,先按 pending.tool 判断是否为
写工具(kill/operate/control_service);是则调 OnSystemReady/OnServiceReady(返回 true
= 已接管、不 CancelIO),否则沿用现有 TakeMainResponse+CancelIO。与 TOKEN_REGEDIT
分支调用 OnRegeditReady(2015RemoteDlg.cpp:6561)的写法一致,避免与人工界面抢子连接。
4. 工具 schema
4.1 kill_process
// inputSchema
{ "id": "string(必填,主机 id)",
"pids": ["integer 数组(必填,至少 1 个进程 PID)"] }
// outputSchema
{ "remaining": ["integer 数组(杀完后仍在运行的 PID)"] }
- payload:
[COMMAND_KILLPROCESS:1][pid:4]*N(DWORD 小端,与SystemDlg::OnPlistKill一致)。 - 成功判定:返回
remaining(发COMMAND_PSLIST后进程列表里已不含目标 pid 即视为成功)。 - 描述需写明:
TerminateProcess强制结束、不保存进程状态,可能造成数据丢失。
4.2 operate_window
// inputSchema
{ "id": "string(必填)",
"hwnd": "integer(必填,窗口句柄,取 list_windows 返回的 hwnd)",
"action": "string(必填)close|show|hide|minimize|maximize|restore" }
// outputSchema
{ "ok": "boolean" }
- payload(与
SystemDlg::OnWlistClose/OnWlistHide/OnWlistRecover/OnWlistMax/OnWlistMin一致, 固定 20 字节零填充BYTE lpMsgBuf[20],hwnd 一律按DWORD(4 字节)小端写入 offset 1):close→[CMD_WINDOW_CLOSE:1][hwnd:4](客户端PostMessage WM_CLOSE)。- 其余 →
[CMD_WINDOW_TEST:1][hwnd:4][dHow:4],dHow 映射(MFC 实测值):hide=SW_HIDE(0)/show=SW_NORMAL(1)/restore=SW_NORMAL(1)/maximize=SW_MAXIMIZE(3)/minimize=SW_MINIMIZE(6)。
- 缓冲器必须补零到 20 字节:客户端
close分支读*((HWND*)(szBuffer+1)),在 x64 是 8 字节, 而服务端只写了 4 字节 hwnd——正是 20 字节零填充补齐了高 4 字节,读出的 HWND 才是正确值; 若 MCP 发最小 5 字节包,会越界读。test分支(TestWindow)读[hwnd:4][dHow:4]共 9 字节, 20 字节缓冲也足够。MFC 固定发lpMsgBuf[20]正是为此,MCP 照搬。
4.3 control_service
// inputSchema
{ "id": "string(必填)",
"service_name": "string(必填,服务名,取 list_services 返回的 service_name)",
"action": "string(必填)start|stop|set_auto|set_manual" }
// outputSchema
{ "ok": "boolean" }
- payload:
COMMAND_SERVICECONFIG [cmd:1][bCmd:1][serviceName\0], bCmd:start=1/stop=2/set_auto=3/set_manual=4。 - 边界:客户端
ServicesManager仅实现这 4 项;无 pause / continue / delete (它们属于MachineDlg机器管理的另一条链路,留待 P8 评估)。
4.4 power_action
// inputSchema
{ "id": "string(必填)",
"action": "string(必填)logout|shutdown|reboot" }
// outputSchema
{ "ok": "boolean" }
- payload:
TOKEN_MACHINE_MANAGE [token:1][type:1],type:logout=0/shutdown=1/reboot=2(enum MachineCommand顺序)。MFC 发的是 32 字节缓冲(2015RemoteDlg::MachineManage), 但客户端只校验ulLength>1并读szBuffer[1],故 2 字节即可。 - 走主连接、发完即走、无响应;主机将立即离线。描述需明确「不可撤销、主机下线」。
- 需客户端管理员权限(
EnableShutdownPrivilege),无权限时客户端静默忽略(同 MFC 现状)。 - 仅 Windows 客户端:Linux 侧文档写的是
COMMAND_SESSION(linux/README.md),但代码中 未发现对应 handler,即 Linux 实际未实现电源操作——power_action对 LNX/MAC/APK 一律返回-32005,不尝试发送。
5. 安全门(单一 McpManage 开关)
- 新增配置键
settings/McpManage(默认0,关闭)。 - 生效条件:
McpManage=1且McpReadonly=0;二者任一不满足即隐藏 4 个工具。 - 双保险:
tools/list隐藏 + 每个BuildXxx入口二次校验(与exec_command的IsReadonly()双保险一致)。 McpSettingsDlg增加「管理操作(进程/窗口/服务/电源)」复选框,写入THIS_CFG。- 审计:每次写操作写入服务端审计日志(
get_audit_log可见),记录 tool + host id + 参数摘要 + 结果。
6. 单飞行与并发约束
- 沿用现有约束:同 host 同一时刻只允许一个 MCP 挂起请求(
BeginPending保证)。 - 写工具与只读工具共用
m_Pending,天然互斥;与 MFC 对话框并存(各用独立子连接,互不干扰)。 - 超时沿用
kMcpToolTimeoutMs = 20000;power_action因无响应、发完即返回成功。
7. 边界与风险
- 杀进程/关机/重启不可逆:描述与审计都需显著标注;不提供「确认对话框」, 确认责任在调用方(MCP 客户端/LLM agent)。
- kill 无客户端回执:成功与否靠「新进程列表不含目标 pid」推断,
TerminateProcess失败(权限不足等)时进程仍在remaining,由调用方判断。 - 服务操作仅 4 项:pause/continue/delete 缺省,如需需评估
MachineDlg链路。 - 窗口句柄时效:hwnd 可能已失效(窗口已关闭),客户端
PostMessage/ShowWindow对失效句柄静默无操作,operate_window的ok=true只表示「已下发」。 - 子连接竞态:写工具接管子连接后若客户端断线,
OfflineProc调OnManageSubClosed清理挂起(置 done 并清subCtx、唤醒等待线程;不擦表,由WaitPendingWrite自行擦除, 防迭代器失效)。清subCtx是关键——context 对象被连接池回收复用,若超时路径仍对旧指针CancelIO会误伤新连接(与OnTerminalClosed/OnScreenControlClosed同理)。
8. P8 / P9 路线图(后续)
- P8 文件与信息面:
delete_file/delete_directory、create_folder、rename_file、open_file;list_network_connections、list_software、get/set_hosts、list_scheduled_tasks(+增删启停)、get_keylog。 - P9 进阶与 NL 增强:
set/delete_registry、screen_blank/lock_input/get_clipboard、open_url/send_message;批量操作(ids[]/group维度);离线主机枚举list_all_hosts。
9. 实现与验证记录(2026-10-01)
已实现并端到端验证(真实客户端 DESKTOP-A55C0OI / Windows 10 / VMware,id=3441387693643366974):
kill_process→remaining:[];operate_window(restore) →ok:true;control_service(set_manual AJRouter) →ok:true;power_action(reboot) →ok:true(发完即走)。 三个写工具的确认包(第二个TOKEN_*LIST)均正常到达,列表均 >1460 字节触发 HTTP 分片。- 落地:
McpManage开关 +McpReadonly=0双保险;McpSettingsDlg增「管理操作」复选框; 主工具栏增MCP入口按钮(ID_MCP_SETTINGS);en/de/zh-TW 三语文案同步。 - 前置解析 bug(验证中暴露,已单独提交
a72e6eb):HeaderParser::Parse的m_bShouldUnmask在 HTTP 分片续包上被误置FALSE,导致写工具的确认包被PARSER_FAILED丢弃(「首包能到、后续包到不了」);修复为else if (m_bShouldUnmask != TRUE)。