面试知识库

执行闭环 (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 开关
dockerrun_docker_command(verb, target, timeout_sec?)verb target timeout_sec?restart/stop/start/kill/pause/unpauseDOCKER_ALLOW_EXEC
host_servicerun_service_command(manager, verb, unit)manager verb unitrestart/stop/start/reload(launchctl: kickstart/stop/start)HOST_ALLOW_SERVICE
host_processkill_host_process(pid, signal)pid signal信号仅 TERM/HUP/INTHOST_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 是唯一的安全收敛点, 在两处执行同一份逻辑:

  1. app 侧 fail-fast (remediation_exec.execute_command_with_approval): 字段不过护栏直接拒, 不浪费人工审批
  2. 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。

各节点做了什么#

节点内容关键文件
RemediationPlannerLLM 依据 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) → 落库审计 + emit
plaintext

生产者: fast 与 deep 都只”投递”#

  • fast 路径 (app/agents/remediation_fast.py fast_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 等) / 重启:

  1. 轮询 list_approved_for_execution (status=‘approved’, 不再按 expires_at 过滤 —— 批准了就该执行);
  2. claim_for_execution 原子认领 (approved→executing, 多 worker 只有一个抢到);
  3. 重跑 command_guard (计划来自 DB, 不信任, 纵深防御);
  4. execute_action 调 MCP 写工具 (从 execute_command_with_approval 抽出的”执行半段”, 内联路径共用);
  5. 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)。

kill $(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
bash

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 -v
bash

覆盖: typed build_* + string check_* 护栏、审批状态机 (claim/mark)、enqueue_action_for_approval 投递、remediation_worker 认领→执行→回写→审计、fast 报告后处置衔接、处置域直驱写执行的接线。

真机端到端 (需 Postgres / docker MCP / 一个靶子容器, 已实测三层全通):

  1. .envDOCKER_ALLOW_EXEC=true + REMEDIATION_EXECUTE_ENABLED=true, 重启 docker MCP server;
  2. 起靶子: docker run -d --name demo-nginx nginx (或用已 Exited 的);
  3. 触发 deep 诊断 → planner LLM 填 {action_type:"docker", verb:"restart", target:"demo-nginx"} (护栏拼成 docker restart demo-nginx) → executor 落审批;
  4. 批准: curl -X POST http://localhost:9900/api/v1/approvals/<ID>/decide -H 'Content-Type: application/json' -d '{"decision":"approved"}';
  5. 真重启 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 需为其带值参数显式加类型化字段 (如 --memorymemory: str)。
  • 不自动回滚; 未恢复只发通知交人工升级。
  • “通知”目前是 SSE 事件 + 日志; 接 Slack/钉钉 是后续。
  • 处置目标当前从 remediation_targetinput 文本正则提取; 真实告警接入可让 IncidentManager 从 alert labels 填。