执行闭环 (Remediation Execution Loop) — 档位 2#
把诊断平台从”只读诊断 + 出建议”升级成”会动手处置”。当前实现是 档位 2 (typed action): 借鉴 planner/executor 分离 —— LLM 只规划结构化动作字段 (verb/target/pid…),命令字符串由 护栏按固定顺序拼装,再复用 restricted 门 + 人工审批把它执行出去。
核心安全姿态: 诊断阶段写操作恒被拦截; 写操作只能由处置流程 (remediation) 发起, 动作字段必过护栏 (command_guard) 拼装成 argv, 且默认不执行 (
REMEDIATION_EXECUTE_ENABLED=false), 开启后仍逐条走人工审批。
为什么是 typed action (演进史)#
- 档位 0 (已删除): 处置固化成 runbook YAML,LLM 零参与、只填模板参 —— 最确定但覆盖窄、加动作贵。
- 档位 2 · freeform (旧): 借鉴 HolmesGPT
run_kubectl_command,LLM 生成整条命令字符串,靠 下游护栏 shlex 解析 + verb 白名单 + flag 黑名单 + 元字符拦截来收敛。灵活,但要写一大坨”解析不可信字符串”的防御。 - 档位 2 · typed action (当前): LLM 只填枚举字段,命令由代码拼。把”解析自由字符串”的整类风险
(元字符注入、flag 夹带、
docker…docker链式、多目标) 从源头消除 —— 这些在类型层面根本无法表达。
| 档位 0 (已废弃) | freeform (旧) | typed action (当前) | |
|---|---|---|---|
| 命令来源 | runbook 模板填 {{target}} | LLM 现场生成命令字符串 | LLM 填 typed 字段, 代码拼 argv |
| 安全收敛 | ”白名单即存在的 action” | 下游护栏解析字符串 | 护栏校验字段 (verb 枚举/target 正则/pid int) |
| 攻击面 | 无 (无 LLM) | 需拦元字符/flag/链式/多目标 | 枚举+正则即上界, 注入不可表达 |
| 执行工具入参 | docker_restart(name) | run_docker_command(command) | run_docker_command(verb, target, timeout_sec?) |
注意:
RunbookAgent(app/agents/runbook_agent.py) 是 deep 图里检索 SOP 知识的取证子 agent, 与本执行闭环无关, 一直保留。
链路总览#
... RCAJudge → RemediationPlanner → RemediationExecutor → Verify → Report
(LLM填字段+护栏拼argv) (护栏+restricted门+审批) (docker_ps复核)plaintext多域处置#
一个 planner + 一套护栏骨架覆盖三个处置域, LLM 输出的 typed JSON 里 action_type 选域, 其余是该域字段:
| action_type | 执行工具 (MCP) | LLM 填的 typed 字段 | verb 白名单 | env 开关 |
|---|---|---|---|---|
docker | run_docker_command(verb, target, timeout_sec?) | verb target timeout_sec? | restart/stop/start/kill/pause/unpause | DOCKER_ALLOW_EXEC |
host_service | run_service_command(manager, verb, unit) | manager verb unit | restart/stop/start/reload(launchctl: kickstart/stop/start) | HOST_ALLOW_SERVICE |
host_process | kill_host_process(pid, signal) | pid signal | 信号仅 TERM/HUP/INT | HOST_ALLOW_KILL |
command_guard.build_action(action_type, params) 按域分发到对应 build_* 校验并拼装 argv;
ACTION_TOOLS 把 action_type 映射到工具名。展示串 command(审批卡/审计/日志用) 由校验后的 argv " ".join 得到,不是 LLM 原文。
三层安全 (护栏是唯一收敛点, 两处硬跑)#
动作由 LLM 规划, 所以 app/runtime/command_guard.py 是唯一的安全收敛点, 在两处执行同一份逻辑:
- app 侧 fail-fast (
remediation_exec.execute_command_with_approval): 字段不过护栏直接拒, 不浪费人工审批。 - MCP 侧硬边界 (
run_docker_command/run_service_command/kill_host_process): 即便 app 逻辑被绕过, MCP 工具内部用同一build_*再校验一次。
typed 护栏规则 (校验字段, 非解析字符串):
- verb 枚举白名单 (只放行止血类, 见上表); 白名单外 verb 直接拒 ——
exec/run/rm等根本不在枚举里; - target/unit 正则: 容器/服务名须匹配字符集 —— 元字符/路径/空格/多目标进不了正则, 无需再单独拦 shell 元字符;
- pid 强制 int 且 > 0、≠ 1; signal 枚举 仅 TERM/HUP/INT ——
-9/KILL/STOP无法表达; - timeout_sec 是 int 字段 ([0,300], 仅 restart/stop),取代旧的
-t 5带值 flag,不再有解析歧义; - argv 仅由
build_*按固定顺序拼装, 交 subprocess 执行, 绝不shell=True, 调用方无从拼接。
纵深保留: 旧的字符串护栏
check_docker_command/check_service_command/check_kill_command(shlex + 元字符/flag 黑名单) 仍在, 作为”万一有字符串入口”的兜底与回归基线, 但生产主路径已不再解析字符串。
主机 kill 的额外动态保护 (在 MCP 工具里, 执行前): PID→进程名/cmdline, 命中保护名单
(postgres/redis/docker/sshd/launchd/systemd/agent 自身进程树 等) 或本进程 PID/PPID → 拒杀; 默认 SIGTERM, 禁 SIGKILL。
各节点做了什么#
| 节点 | 内容 | 关键文件 |
|---|---|---|
| RemediationPlanner | LLM 依据 RCA/现象选处置域 + 填 typed 动作字段 (输出 {action_type, verb/target/pid…, why} JSON) → build_action 分发校验并拼 argv, 派生展示串 command; none/字段非法 = 不处置 (安全) | deep_diagnosis_graph.remediation_planner_node |
| RemediationExecutor | 护栏 fail-fast → restricted 门 (remediation_context=True) → 人工审批 (ASK_DESTRUCTIVE) → 原子认领 (claim_for_execution) → 调 plan.tool 对应工具 → 回写终态 (mark_execution_result); 执行开关关时只规划不执行 | remediation_executor_node, app/runtime/remediation_exec.py |
| Verify (deep 内联路径) | 仅 docker 域用只读 docker_ps 复核目标是否 Up; host 域暂无自动复核 (交人工); 未恢复发 remediation_verify_failed 通知 (不自动回滚) | verify_node |
解耦路径 (下节) 的”成败”标准更简单: 命令执行返回不报错即 done, 报错即 failed (不为每种命令维护后置状态探测)。
复用的基建 (未变): restricted 门 (permissions.py / tool_filter.py)、审批仓 (approvals.py:
create_request / wait_for_decision / claim_for_execution + mark_execution_result 状态机)、
等审批让出分布式执行槽、任务标 awaiting_approval。
诊断与处置解耦 · 独立处置服务 (2026-07)#
上面的 RemediationExecutor 是 deep 图内联执行的路径 (跑到节点就阻塞等审批、当场执行)。为把诊断与 处置彻底解耦, 新增一条 生产者 / 审批队列 / 消费者 链路: 诊断只投递处置计划, 由独立进程执行。
[Fast 图] ─┐
├─ enqueue_action_for_approval ─► approval_requests (pending)
[Deep 图] ─┘ │ 人工在审批中心批准 (approved)
▼
remediation_worker (独立进程)
轮询 approved → claim(→executing) → 重跑护栏 → 调 MCP 写工具
→ mark(→done/failed) → 落库审计 + emitplaintext生产者: fast 与 deep 都只”投递”#
- fast 路径 (
app/agents/remediation_fast.pyfast_remediation_node): fast 图出报告后追加一步, 从报告根因让 LLM 填 typed 动作 → 护栏 → 投递审批中心。开关FAST_REMEDIATION_PLAN_ENABLED。 处置域跟根因走 (docker/host_service/host_process), 不绑诊断 skill (症状域常≠修复域)。 - deep 路径:
REMEDIATION_DECOUPLED_MODE=true时,remediation_executor_node不再内联 create+wait+执行, 改为只投递 (默认 false 保持原内联行为, 零回归)。 - 两者统一走
remediation_exec.enqueue_action_for_approval: 护栏 → restricted 门 →create_request(pending), 不 wait、不执行。
队列 = approval_requests 表 (DB 即队列, 不引消息队列)#
审批请求本就必须落 DB (审批中心要读写它), 所以直接把它当队列:
- exactly-once 由
claim_for_execution的原子UPDATE ... WHERE status='approved'保证 (多 worker 安全); - 人工审批天然低频 (分钟级), 轮询开销可忽略; 真要压低轮询延迟, 最简是 Postgres
pg_notify(不加 broker)。
消费者: 独立处置服务 remediation_worker#
python -m app.remediation_worker —— 独立进程, 与诊断解耦, 可单独部署 / 授权 (DOCKER_ALLOW_EXEC 等) / 重启:
- 轮询
list_approved_for_execution(status=‘approved’, 不再按 expires_at 过滤 —— 批准了就该执行); claim_for_execution原子认领 (approved→executing, 多 worker 只有一个抢到);- 重跑
command_guard(计划来自 DB, 不信任, 纵深防御); execute_action调 MCP 写工具 (从execute_command_with_approval抽出的”执行半段”, 内联路径共用);mark_execution_result回写终态 → 落库审计 (remediation_executions) → emit 结果事件。三者 fail-safe。
状态机 (approval_requests.status)#
pending(入库待批) ─┬─ denied / timeout / cancelled
└─ approved(已批待执行)
└─ executing(正在处置, 原子认领)
├─ done(处置完成)
└─ failed(处置失败)plaintext- “生效”标准 = 命令执行返回不报错:
execute_action按工具的[成功]/[失败](subprocess 退出码 / 异常) 给出succeeded,mark_execution_result据此写 done / failed。不为每种命令维护后置状态探测 —— 加新处置 类型无需再写复核逻辑, 工具报错即 failed。
执行审计落库#
remediation_executions 表 (app/runtime/remediation_audit.py remediation_execution_repository): worker
每次执行落一行 (approval_id / tool_name / tool_args / command / status / succeeded / result /
executed_by / created_at), 回答”谁在何时对什么执行了什么、成没成”, 供审计页与复盘。
开关 (默认全是安全值)#
APPROVALS_ENABLED=true # 审批闭环 (默认 true)
REMEDIATION_EXECUTE_ENABLED=false # 允许 deep 内联 executor 真执行 (默认 false, 只规划)
PERMISSION_MODE=normal # 诊断默认 normal; remediation 内部用 ask_destructive
# —— 解耦处置服务 (生产者/队列/消费者) ——
FAST_REMEDIATION_PLAN_ENABLED=false # fast 出报告后生成处置计划并投递审批 (默认 false)
REMEDIATION_DECOUPLED_MODE=false # deep 也只投递, 由 remediation_worker 执行 (默认 false=内联)
REMEDIATION_APPROVAL_TTL_SEC=86400 # 解耦模式下审批请求有效期秒数 (默认 24h)
REMEDIATION_WORKER_POLL_INTERVAL_SEC=3.0 # 处置服务轮询审批中心的间隔秒数
# —— 各处置域的工具级开关 (MCP 侧读, 默认全 false) ——
DOCKER_ALLOW_EXEC=false # run_docker_command (docker MCP)
HOST_ALLOW_SERVICE=false # run_service_command (system MCP)
HOST_ALLOW_KILL=false # kill_host_process (system MCP)bash改工具级开关后必须重启对应 MCP server (启动时
load_dotenv()读死一次):DOCKER_ALLOW_EXEC→ docker server (8011);HOST_ALLOW_*→ system server (8005)。bashkill $(cat .run/mcp-<name>.pid) nohup .venv/bin/python mcp_servers/<name>_server.py > logs/mcp-<name>.log 2>&1 & echo $! > .run/mcp-<name>.pid
docker_server.py/system_server.py顶部已自插项目根到sys.path(否则按脚本路径运行 import 不到app护栏)。
验证#
单测 (离线无依赖, 全 mock; 含护栏、审批状态机、解耦生产者/消费者):
.venv/bin/python -m pytest tests/test_command_guard.py tests/test_remediation_exec.py \
tests/test_approval_execution.py tests/test_remediation_decoupled.py \
tests/test_remediation_executor_wiring.py tests/test_fast_remediation.py -vbash覆盖: typed build_* + string check_* 护栏、审批状态机 (claim/mark)、enqueue_action_for_approval
投递、remediation_worker 认领→执行→回写→审计、fast 报告后处置衔接、处置域直驱写执行的接线。
真机端到端 (需 Postgres / docker MCP / 一个靶子容器, 已实测三层全通):
.env设DOCKER_ALLOW_EXEC=true+REMEDIATION_EXECUTE_ENABLED=true, 重启 docker MCP server;- 起靶子:
docker run -d --name demo-nginx nginx(或用已 Exited 的); - 触发 deep 诊断 → planner LLM 填
{action_type:"docker", verb:"restart", target:"demo-nginx"}(护栏拼成docker restart demo-nginx) → executor 落审批; - 批准:
curl -X POST http://localhost:9900/api/v1/approvals/<ID>/decide -H 'Content-Type: application/json' -d '{"decision":"approved"}'; - 真重启 demo-nginx (Exited → Up) → verify
docker_ps复核recovered=1/1→ 审批consumed。
已实测:
- docker: 护栏真拦白名单外 verb (
exec/rm); LLM 填 typed 字段拼成docker restart demo-nginx; 审批落库→批准→真执行→复核恢复。 - 主机进程 kill: 开关门 / 护栏拦
-9/PID 1/sudo/元字符 / 禁杀自身 / 保护名单命中redis拒杀 / happy-path 杀傀儡sleep。 - 主机服务: 开关门 / 护栏拦
disable/sudo/元字符 (macOS 无 systemctl, 真机服务动作未跑)。
已知边界 / 后续#
- planner 目前只出一条命令; 多步处置 (先 stop 再 start) 待做。
- host 域 (服务/进程) 暂无自动 verify, 交人工确认; docker
update、systemd 更多 verb 未进白名单 —— typed action 下扩 verb 需为其带值参数显式加类型化字段 (如--memory→memory: str)。 - 不自动回滚; 未恢复只发通知交人工升级。
- “通知”目前是 SSE 事件 + 日志; 接 Slack/钉钉 是后续。
- 处置目标当前从
remediation_target或input文本正则提取; 真实告警接入可让 IncidentManager 从 alert labels 填。