| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091 |
- <!doctype html><html lang="zh"><head><meta charset="utf-8">
- <meta name="viewport" content="width=device-width,initial-scale=1">
- <title>硬沙箱:把'写文件'这一 effect 限制在能力边界内</title><style>
- :root{--fg:#1a1a1a;--bg:#fff;--mut:#666;--line:#e3e3e3;--accent:#3b5bdb;
- --codebg:#f6f8fa;--warn:#b00020;--okbg:#eef4ff;--formalbg:#f3f0ff;--conceptbg:#fbfbf9;}
- *{box-sizing:border-box}
- body{font:15px/1.65 -apple-system,"PingFang SC",Segoe UI,Roboto,sans-serif;
- color:var(--fg);background:var(--bg);margin:0;padding:0}
- a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline}
- .wrap{max-width:1180px;margin:0 auto;padding:28px 22px 80px}
- header.top{border-bottom:1px solid var(--line);padding-bottom:14px;margin-bottom:22px}
- header.top h1{margin:0 0 4px;font-size:20px}
- .sub{color:var(--mut);font-size:13px}
- .crumbs{font-size:13px;margin-bottom:18px}.crumbs a{color:var(--mut)}
- .badge{display:inline-block;font-size:12px;background:var(--okbg);color:var(--accent);
- border-radius:4px;padding:1px 8px;margin-right:6px}
- h2.ctitle{font-size:19px;margin:6px 0 4px}
- .keypoint{background:#fff8e6;border-left:3px solid #e6a700;padding:10px 14px;
- border-radius:4px;margin:14px 0;font-size:14px}
- .cols{display:grid;grid-template-columns:1fr 1.5fr 1fr;gap:16px;margin:22px 0}
- .col{border:1px solid var(--line);border-radius:8px;padding:14px 16px;min-width:0}
- .col h3{margin:0 0 10px;font-size:13px;letter-spacing:.04em;text-transform:uppercase;color:var(--mut)}
- .col.concept{background:var(--conceptbg)}
- .col.formal{background:var(--formalbg)}
- .col p{margin:0 0 10px;white-space:pre-wrap;font-size:14px}
- .anchor{margin:0 0 16px}
- .anchor .meta{font-size:12px;color:var(--mut);margin-bottom:4px}
- .anchor .meta code{background:var(--codebg);padding:1px 5px;border-radius:3px}
- pre{background:var(--codebg);border:1px solid var(--line);border-radius:6px;
- overflow:auto;margin:0 0 6px;padding:10px 0;font-size:12px;line-height:1.5}
- pre code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;display:block}
- .ln{display:flex}.ln .n{color:#aaa;text-align:right;width:46px;padding:0 10px;
- user-select:none;flex:none}.ln .c{white-space:pre;padding-right:14px}
- .note{font-size:12.5px;color:var(--mut);margin:0 0 14px}
- .warn{color:var(--warn);background:#fff0f0;border:1px solid #ffd0d0;border-radius:6px;
- padding:8px 12px;font-size:13px}
- .kv{font-size:13px}.kv code{background:var(--codebg);padding:1px 5px;border-radius:3px}
- .formal .term{font-family:ui-monospace,Menlo,monospace;background:#fff;border:1px solid var(--line);
- border-radius:5px;padding:8px 10px;font-size:13px;margin-bottom:8px;word-break:break-word}
- .effect{font-family:ui-monospace,Menlo,monospace;color:#7048e8;font-size:13px;margin-bottom:8px}
- .trace{border:1px solid var(--line);border-radius:8px;padding:14px 16px;margin:22px 0;background:#fafafa}
- .trace h3{margin:0 0 10px;font-size:14px}
- .stat{display:inline-block;margin:0 18px 6px 0;font-size:13px}
- .stat b{font-size:17px;color:var(--accent)}
- .toolchip{display:inline-block;background:#eef;border:1px solid #dde;border-radius:12px;
- padding:1px 9px;margin:0 6px 6px 0;font-size:12px}
- .takeaway{background:var(--okbg);border-radius:8px;padding:14px 18px;margin:22px 0;
- white-space:pre-wrap;font-size:14px}
- .rel a{margin-right:14px;font-size:13px}
- table.idx{border-collapse:collapse;width:100%;margin-top:8px}
- table.idx td{border-bottom:1px solid var(--line);padding:9px 8px;vertical-align:top}
- table.idx .wk{color:var(--mut);font-size:13px;width:64px;white-space:nowrap}
- .modhdr{margin:26px 0 4px;font-size:15px;color:var(--accent)}
- footer{margin-top:50px;color:var(--mut);font-size:12px;border-top:1px solid var(--line);padding-top:14px}
- @media(max-width:900px){.cols{grid-template-columns:1fr}}
- </style></head><body><div class="wrap">
- <div class="crumbs"><a href="index.html">← 对照卡索引</a></div>
- <header class="top">
- <span class="badge">模块 C</span><span class="badge">第 8 周</span>
- <span class="badge">sandbox-effect-cap</span>
- <h2 class="ctitle">硬沙箱:把'写文件'这一 effect 限制在能力边界内</h2></header>
- <div class="keypoint">🎯 prompt 约束挡不住模型(实测它把 5 次 WriteFile 全写到 F 之外、还跑偏续跑别人的研究); 必须工具层硬拦。只拦写不拦读;Bash 靠 CWD + dangerousCommandBlock(shell 无法工具层硬沙箱)。
- </div>
- <div class="cols">
- <div class="col concept"><h3>📖 教科书概念</h3><p>智能体一旦能写文件、跑命令,就具备了改变环境的副作用(side effect)。仅靠
- prompt 约束("请只在 F 目录内操作")是不可靠的——模型会越界。工程上必须在
- **工具层**做硬性能力限制(capability confinement):无论模型怎么想,越界的写
- 操作在执行边界被直接拒绝。
- 这对应操作系统里的沙箱/权限模型:能力(capability)显式授予且不可逾越,
- realpath 解析符号链接以防 `..` 或 symlink 逃逸。
- </p></div>
- <div class="col code"><h3>💻 平台源码落点</h3><div class="anchor"><div class="meta"><code>lambdagent/src/lambdagent/builtin_tools/_sandbox.py</code> · <b>check_write_allowed</b> · L38-56</div><pre><code><div class="ln"><span class="n">38</span><span class="c">def check_write_allowed(target_path: str) -> Tuple[bool, str]:</span></div><div class="ln"><span class="n">39</span><span class="c"> """目标路径是否允许写。未设 root → 总允许;设了 → 必须落在 root 内。</span></div><div class="ln"><span class="n">40</span><span class="c"></span></div><div class="ln"><span class="n">41</span><span class="c"> 返回 (ok, error_msg)。realpath 解析符号链接,防 symlink 逃逸。</span></div><div class="ln"><span class="n">42</span><span class="c"> """</span></div><div class="ln"><span class="n">43</span><span class="c"> root = get_sandbox_root()</span></div><div class="ln"><span class="n">44</span><span class="c"> if not root:</span></div><div class="ln"><span class="n">45</span><span class="c"> return True, ""</span></div><div class="ln"><span class="n">46</span><span class="c"> try:</span></div><div class="ln"><span class="n">47</span><span class="c"> rp = os.path.realpath(target_path)</span></div><div class="ln"><span class="n">48</span><span class="c"> except Exception:</span></div><div class="ln"><span class="n">49</span><span class="c"> rp = os.path.abspath(target_path)</span></div><div class="ln"><span class="n">50</span><span class="c"> if rp == root or rp.startswith(root + os.sep):</span></div><div class="ln"><span class="n">51</span><span class="c"> return True, ""</span></div><div class="ln"><span class="n">52</span><span class="c"> return False, (</span></div><div class="ln"><span class="n">53</span><span class="c"> f"[SANDBOX_DENIED] 拒绝写到工作文件夹之外:{target_path}\n"</span></div><div class="ln"><span class="n">54</span><span class="c"> f"本次会话只允许在工作文件夹内读写:{root}\n"</span></div><div class="ln"><span class="n">55</span><span class="c"> f"请把目标路径改到该文件夹内(用其下的绝对路径)后重试。"</span></div><div class="ln"><span class="n">56</span><span class="c"> )</span></div></code></pre><div class="note">模块级 root + 锁。未设 root → 总允许;设了 → realpath 后必须 == root 或落在 root/ 之下,否则返回 [SANDBOX_DENIED]。realpath 防 symlink/.. 逃逸。
- </div></div><div class="anchor"><div class="meta"><code>lambdagent/src/lambdagent/builtin_tools/file_tools.py</code> · <b>_resolve</b> · L21-35</div><pre><code><div class="ln"><span class="n">21</span><span class="c">def _resolve(path: str) -> str:</span></div><div class="ln"><span class="n">22</span><span class="c"> """把(可能相对的)路径解析为绝对路径,基准 = Bash 会话当前目录(_session_cwd),</span></div><div class="ln"><span class="n">23</span><span class="c"> 与 Bash/沙箱一致。原实现用 os.path.abspath()/os.getcwd() 即**服务进程 CWD**</span></div><div class="ln"><span class="n">24</span><span class="c"> (=启动目录 lambdagentpaas),导致工作区对话里 ListFiles/相对路径落到错目录——</span></div><div class="ln"><span class="n">25</span><span class="c"> agent 用绝对路径读到了对的论文,却 ListFiles 列出 lambdagentpaas 源码、自我迷惑、</span></div><div class="ln"><span class="n">26</span><span class="c"> 放弃落盘(commit 见此修复)。绝对路径原样规整。"""</span></div><div class="ln"><span class="n">27</span><span class="c"> try:</span></div><div class="ln"><span class="n">28</span><span class="c"> from lambdagent.builtin_tools.shell_tools import _get_cwd</span></div><div class="ln"><span class="n">29</span><span class="c"> base = _get_cwd()</span></div><div class="ln"><span class="n">30</span><span class="c"> except Exception:</span></div><div class="ln"><span class="n">31</span><span class="c"> base = os.getcwd()</span></div><div class="ln"><span class="n">32</span><span class="c"> if not path:</span></div><div class="ln"><span class="n">33</span><span class="c"> return base</span></div><div class="ln"><span class="n">34</span><span class="c"> if os.path.isabs(path):</span></div><div class="ln"><span class="n">35</span><span class="c"> return os.path.abspath(path)</span></div></code></pre><div class="note">相对路径必须按会话 CWD(shell_tools._get_cwd)解析,而非服务进程 CWD。 配套坑:加任何新文件工具都要走 _resolve,否则读侧目录会和写侧对不上。
- </div></div></div>
- <div class="col formal"><h3>∑ 形式化对象</h3><div class="term">Guard(WriteFile, check_write_allowed) ; root = realpath(F)</div><div class="effect">effect: State[fs] ↾ Cap(root) (effect 被限制到能力 root 内)</div><p>沙箱在形式上是一个 effect 限制算子:把 WriteFile 的 State[fs] effect 投影 (restrict, ↾)到能力集 Cap(root) 之内。越界写不是"运行期再补救",而是被 Guard 直接拒绝并把 [SANDBOX_DENIED] 反馈给模型纠正——这正是"形式化提前拒绝" 在安全维度的兑现,和范式③的能力授权(Cap)是同一套语言。
- </p></div>
- </div>
- <div class="trace"><h3>🎞 Live Demo</h3><p class="note">回归测试 test_sandbox_blocks_writes_outside_root 验证越界写被拒。 教学演示:在工作区模式下令 agent 尝试写 F 之外,观察 trace 里的 [SANDBOX_DENIED] 与模型随后的自我纠正。
- </p></div>
- <div class="takeaway"><b>学习目标 / Takeaway</b>
- 学生应能把三件事连起来:
- 1. 副作用(effect)是危险的来源,所以需要边界;
- 2. 边界必须在工具层强制(prompt 不够),代码落点是 check_write_allowed + _resolve;
- 3. 形式上这是 effect 到能力集的限制(↾ Cap),与范式③的外包能力授权是同一抽象。
- 这张卡把"安全"从一句口号变成一个可指认的 effect 限制算子。
- </div>
- <p class="rel"><b>相关对照卡:</b> <a href="tool-claude-code-native.html">工具调用范式③:claude-code 原生车道(把工具调用外包给运行时)</a> <a href="tool-native-fc.html">工具调用范式②:原生 function-calling(结构化 tool_calls)</a></p>
- <footer>对照卡源文件: <code>teaching/cards/04-sandbox-effect-cap.yml</code> ·
- 源码 span 由 gen.py 在生成时实时读自仓库</footer>
- </div></body></html>
|