Files
dsh_shenxian/dsh-server-docs/04-调整方案/42-实例共享Python运行时与会话取证复核.md
T
admin 5ad755116e chore(docs): 文档库并入代码仓(R4 选 a)+ 索引/台账跟进
1) dsh-server-docs/ 从工作区(原 E:\...\aliyun-dsh-server\dsh-server-docs)**整体并入本仓**,
   保留目录名 ⇒ 仓库内 dsh-server-docs/... 的相对引用天然继续有效;旧目录(含其 .git)已归档到
   工作区 _中间产物_待清理/,未随本提交带入。
2) .gitattributes:新增 `dsh-server-docs/** -text` —— 原文档库是 `* -text` + autocrlf=false,
   必须保持纯 LF,否则会被本仓的 CRLF 规则翻掉。
3) 活引用里的绝对路径已全部改到新位置(docs 的 INDEX / README / scripts / skills + 用户级 skills
   + ~/.workbuddy/settings.json 的 hooks);历史档案(04-调整方案/、archive/)按「只增不改」未动。
   ⚠️ hooks 路径改动需「完全重启会话」才生效(配置是会话启动快照)。
4) 交接单/T08:新增 §16「生产整体切换执行记录」(形态 / 落地动作 / **4 个只有真上线才暴露的真 bug** /
   验收证据 / 回滚命令 / 残留项);台账 T08 行 → 已完成并归档;03-路线图 §二 登记 T08 收尾项。
5) 统一称谓:**「本机」只指跑 WorkBuddy 的开发机**,47 / 106 一律写「远程服务器」。
2026-09-15 18:47:13 +08:00

149 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 42-实例共享 Python 运行时与会话取证复核(2026-09-11)
> **TL;DR**|**结论**:会话取证发现 `python3` 不存在 —— 实为**档案 39 收窄 `/etc` 引入的回归**;本轮改为实例侧使用**共享 Python 运行时**(宿主装一次、全员可用)。
> **关键**:共享路径走宿主 `/usr`(已 ro-bind),不逐用户安装;同时复核会话取证结论。
> **状态**:✅ 已修复
## 背景与动机
1. 用户要求**分析 guest「MCN短视频创作」工作区的最新对话记录**(dsh 服务侧,非 WorkBuddy 侧)。
2. 会话取证暴露「`python3` 不存在」→ 复核发现是**档案 39 我引入的回归**。
3. 用户提问:**能不能给 dsh 用户单独装一个 python,不用系统的版本?** 并要求**方便服务器迁移时整体打包**。
4. 用户补充:**python3.6 不必对用户开放,避免模型迷惑**。
## 一、会话取证(8 个会话目录的身份甄别)
| 会话 | 创建 | depth | 性质 |
|---|---|---|---|
| `session-b83ae69b` | 09-10 21:29 | **0** | **最新主会话**(末次活动今天 16:58;标题「看看有哪些功能和权限尝试所」→「代码库功能与权限探索」)|
| `session-bd54c7cc` | 09-10 04:12 | 0 | 上午取证过的另一条主会话(12 turns / 4191 行)|
| `b11581f3` `6ac89f97` `71e06296` `75a54b50` `e7f270f0` `43a2f3cb` | 09-11 16:48–16:51 | **1** | 主会话派发的**子代理**(**非我的验证所致**)|
用户一句「看看有哪些功能和权限尝试所有方式」→ agent 系统性实测 **26 个工具** + 沙箱 + 文件策略 + 网络 + 子代理/workflow/ralph,产出 `能力与权限探测报告.md`(7.5KB) + `RALPH_PROBE.md`。
**暴露的平台问题**:
1. **存量会话仍 `workspace-write` + `ask`**(本会话创建于 09-10 21:29,**早于档案 33 的 09-11 14:33 修复 17 小时**)→ 每次 bash 报 `no sandbox backend is usable on this host` → **逐条审批(本次 5 次,全部 `allowed-once`,不持久)**;用户最后一句正是「没办法执行 bash命令了吗」。
2. **子代理固定 `approval:"never"` → 永久不可用 shell** → 多代理并行实际废掉。
3. `subagent_fork` 看不到父代理 **in-flight turn**(只继承已完成 turn)→ 两个子代理都回「无继承证据」。
4. 成果落 `/tmp`(`dy_hot.json` 64KB、`board_*.json`、`c.html` 等)**不在工作区**,有丢失风险。
5. agent 一条**错误建议**「装 bubblewrap 或换 Landlock 内核」(用户侧装不上;平台已在容器层用 bwrap)。
**技术结论修正**:不是「沙箱完全不可用」,而是「**bash 执行沙箱后端缺失;文件工具的 policy 层正常**」(`write DSH_HOME` 被拒 = `[sandbox: file access denied under workspace-write mode]`;`write /tmp` 允许)。
## 二、P0 回归修复:`/etc` 白名单漏 `/etc/alternatives`(commit `e32d1bf`)
`/usr/bin/python3` 是**两跳软链**(`→ /etc/alternatives/python3 → /usr/bin/python3.6`),中间跳在 `/etc` → 沙箱内断链 → **21 个命令静默 `command not found`**:
`python3` `python` `pip3` `pip-3` `pydoc3` `pydoc-3` `python3-config` `pyvenv-3` `easy_install-3` `unversioned-python` `ld`(→ld.bfd,node-gyp 用) `pax` `print-*`(lp/lpr/lpq/lprm/lpstat/cancel) `ifup` `ifdown` `lpc`
**修法**:白名单加 `/etc/alternatives`(33 项全部指向 `/usr`/`/lib64`,无凭据与平台情报 → 不扩大实质可见面)。
实测:21 命令全恢复;`/etc` 可见项 14、可读文件 27、平台情报仍 0/0/0/0、其他用户目录仍 1 项、DB 不可读。
**通用教训**:`/etc` 白名单化**必须枚举「整条软链链条会穿过 /etc」的条目**(探测器已入 skill);这类回归**不报错、只静默失效**,验收只能靠"工具清单前后对比"或会话取证。
## 三、可移植 Python 3.12(已装)
- **为什么不用 `dnf install python3.11`**:用户要求「**方便服务器迁移时一起打包迁移,避免环境出问题**」。rpm 包分散在 `/usr` 各处、依赖系统库 → 迁移易出问题。选 **python-build-standalone(install_only, stripped)**:自包含单目录、解压即用。
- **落地**:`scripts/install-python-runtime.sh`(幂等 / 可离线 / 支持 `--tarball`)→ `/usr/local/dsh-runtime/python-3.12.14/`(109MB)+ `/usr/local/bin/{python3,python,pip3}` 软链。
- **零代码生效**:`/usr` 已 ro-bind + `/usr/local/bin` 在实例 PATH 首位 → 实例内 `python3` = **3.12.14**、`pip3` = 26.2.1,**无需重启实例**。
- **迁移打包单元 = `/usr/local/dsh-runtime/` 一个目录**:`tar czf dsh-python-runtime.tar.gz -C /usr/local dsh-runtime` → 目标机解压回原位 + 重跑脚本(只重建软链)。
- **宿主零影响**:`dnf`(4.7.0) shebang 为绝对路径 `/usr/libexec/platform-python`;BT-Panel 用自带 pyenv 绝对路径。已核实宿主**无任何东西依赖裸 `python3`**。
- **pip 落点**:默认装 runtime 的 site-packages(**只读被挡** ✅ 天然护栏);`--user` → `$HOME/.local`;**推荐 `--target <ws>/.pylibs` + `PYTHONPATH`**(实测成功,requests 2.34.2)。
## 四、平台自身 bug:root 属主污染用户工作区(存量已修,治本待做)
- 现象:guest `ws/.local`、`.local/share/pnpm` **属主 root** → 用户 `pip install --user` 报 `Permission denied`(**在自己的目录里装不了包**)。
- 根因:**平台以 root 跑 `pnpm`,且 `HOME=<userRoot>/ws`**(`src/web/routes/business-plugins.ts:260` 的 `pnpmEnv`、`scripts/ensure-biz-plugins.cjs:154`)。
- **不能简单改 HOME**:代码注释写明需与 dsh 实例共用 pnpm store,否则 `ERR_PNPM_UNEXPECTED_STORE`(已踩过)。
- 存量已修:全量扫描 + `chown -R` 回对应用户(guest **49 项 → 0**;admin 本就干净)。
- **治本待做**:pnpm 流程后把 `ws` 下 root 属主项 chown 回用户,或加入 `ws-cleanup.cjs`(cron 04:10)自愈 pass。
## 五、P0 自伤并已回滚:无法遮蔽 `/usr` 内文件
用户要求「`python3.6` 不必对用户开放,避免模型迷惑」。尝试用 `--ro-bind /dev/null <path>` 遮蔽 8 项(`platform-python*` + `python2.7`)。
**结果**:`bwrap: Can't create file at /usr/libexec/platform-python3.6m-config: Permission denied` → **实例无法启动**。
- **根因**:bwrap 需要**在 DEST 创建挂载点**,而 `/usr` 是 **ro-bind(只读)** → 创建失败。
`/etc` 之所以能自由白名单,是因为它先 `--tmpfs`(可写)再逐项 `--ro-bind-try`。
- **判据(已入 skill)**:**只有「先 tmpfs 再白名单」的目录才可自由增删**;ro-bind 的目录只能整目录决策。
- **处置**:立即用 `.bak-*` 回滚 → `grep -c platform-python` = 0、lib 同步、门户 200。
**失败窗口期零实例启动请求**(journalctl 窗口内 0 条 spawn;0 个 scope)→ **零用户影响**。
- **可行变体(未采用)**:`--tmpfs /usr/libexec` + rebind 必需子项(`git-core`/`getconf`/`gawk`/`awk`/`coreutils`…)→ 收益低(只是"看不见旧解释器")、风险中(同类静默回归)。
## 验证记录(Python 运行时)
| 检查 | 结果 |
|---|---|
| 实例内 `python3` | `/usr/local/bin/python3` = **3.12.14**(OpenSSL 3.5.8)|
| 实例内 `pip3` | pip **26.2.1** |
| 模块自检 | ssl / sqlite3 / lzma / zlib / ctypes / json / urllib **全可用** |
| `/usr/local/dsh-runtime` | 可读、**只读**(`touch` = Read-only file system)|
| `pip install`(默认) | 拒绝(runtime 只读)→ 天然护栏 |
| `pip install --target <ws>/.pylibs` + `PYTHONPATH` | **成功**(requests 2.34.2)|
| 宿主 `dnf` | 4.7.0 正常 |
| 宿主 `python3.6` | 3.6.8 仍可用(未受影响)|
| `/etc` 白名单(补 alternatives 后) | 可见项 14、平台情报 0/0/0/0 |
## 回滚 / 注意
- **Python**:删 `/usr/local/bin/{python3,python,pip3}` 软链即恢复旧行为;`/usr/local/dsh-runtime/` 可保留(迁移资产)。
- **未做(待用户决策)**:① 遮蔽 `python3.6` 的可行变体;② pnpm 污染治本;③ 子代理 `approval` 播种放开(否则多代理并行永远无 shell)。
- **存档的迁移资产**:`/opt/dsh/artifacts/cpython-3.12.14.tar.gz`(34MB)。
---
## 修正(2026-09-12):③ 子代理 `approval` 的根因与可行路径
**用户裁定**:判据 = 「**不扩大用户访问边界即可**」。
**源码级取证**(服务器 `/usr/local/lib/node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai/dsh-subagent/lib/types/child-agent.js`,官方包,只读):
| 事实 | 证据(官方注释原文) |
|---|---|
| 子代理沙箱只继承父会话的**显式 override** | `sandboxMode: parent.ctx.get('sandboxPolicy')?.overrideOf(parent.session)`;注释:*"Only the parent session's explicit sandbox override is captured — **never deployment defaults** or one-shot grants"* |
| approval 被**钉死** `'never'` | `approvalPolicy: parent.ctx.get('approval') === undefined ? undefined : 'never'`;注释:*"the approval policy is **pinned to `'never'` regardless of the parent's own policy**"* |
| `'never'` 的语义 = **需要审批的操作被自动拒绝**(不是放行) | `SUBAGENT_DELEGATION_CONTEXT`:*"operations that require approval are **rejected automatically**"*(并刻意声明 *"cannot be widened from inside this session"*) |
**闭环解释**:平台的 `DSH_PERMISSION_MODE=danger-full-access` 属**部署默认值** → 恰好被官方规则**排除** → 子代理回落内置默认 sandbox(需审批)→ 而 approval 钉死 `never` = 自动拒绝 ⇒ **子代理永远拿不到 shell**。这正解释了 §一 的实测现象(当时父会话还是 `workspace-write`)。
**路径裁决**:
| 路径 | 判定 |
|---|---|
| 用 env / 部署默认值让子代理继承完全权限 | ❌ 不可能 —— 官方**明确排除** deployment defaults |
| 走 profile patch 改预设 | ❌ 无效 —— 改的仍是"默认值"通道;approval 是硬编码常量而非配置项 |
| 改写会话种子事件造"显式 override" | ❌ 档案 45 已否决同源做法(R5 扩大 + 风险收益不成比例) |
| 改官方包 | ❌ 红线 R2 |
| **用户在会话内自行切档位(产生显式 override)** | ⚠️ 唯一合规路径;但**当前平台默认已是 `danger-full-access`,切同名档位是否产生 override 未验证** |
**边界结论(回答用户判据)**:子代理与父会话**同实例、同 uid、同 cgroup、同 bwrap 沙箱** → 让它能执行 shell **不新增任何用户可访问的路径或资源** ⇒ **「不扩大用户访问边界」成立**。它触碰的是 **dsh 官方刻意的"子代理降权"设计**,而不是平台的安全边界。
**待验证(下一步)**:在实例内新开会话派发子代理跑一次 `bash` —— 可用则本项自动闭环;不可用再评估平台侧方案。
### ✅ 追加结论(2026-09-12 源码复核):**用户自己设置权限 = 支持**
用户追问「`danger-full-access` 用户自己设置权限是不是就支持了」→ 复核 `overrideOf` 实现,答案**是**:
```js
// dsh-sandbox-policy/lib/index.js
/** Read the session override **without applying the deployment default**. */
overrideOf(session) { return this.ctx.sessionProjections.stateOf(session, "sandboxMode") ?? void 0 }
// dsh-user-approval/lib/index.js —— 倒序取会话日志里最后一条 approval/policy 事件
overrideOf(session) { for (let seq = session.seq - 1; seq >= 0; seq -= 1) { const e = session.eventAt(SessionSeq(seq)); if (e?.type === "approval/policy") return e.data.policy } }
```
`overrideOf` 读的是**会话日志里最后一条 `sandbox/mode` 事件**,**不与默认值做比较** —— 所以只要日志里存在这条事件(= 用户在会话内切过一次档位),`captureDelegatedPolicyOverrides` 就会捕获它并 append 给子代理(`source: 'delegation'`)。子代理因此拿到 `danger-full-access`,而该模式下**不存在需要审批的操作**,被钉死的 `never` 不再构成阻碍 ⇒ **子代理可用 shell**。
⇒ **平台侧零改动,完全走官方机制**;边界上子代理继承的是**用户自己选的档位**,不新增任何可见面(与用户的判据一致)。
**操作(顺序重要)**:① 新开会话(档位是会话级播种,旧会话不跟随);② 会话内执行 `/permission workspace-write` 再 `/permission danger-full-access` —— **先切走再切回**,确保日志真的产生事件;③ 派子代理跑 `bash -c 'echo ok'` 验证。
**残留不确定**:设置 UI 在"选到同一档位"时是否仍写事件未验证 → 用"先切走再切回"规避。
### 🧪 实测结果(2026-09-12,用户执行):**子代理 shell 可用**
用户在其实例的会话中派子代理执行 `bash -c 'echo subagent-ok'` → **原样返回 `subagent-ok`**。
⇒ **本项(③ 子代理 approval)关闭:平台侧零改动、无需放权评估**,与用户判据(不扩大用户访问边界)一致。
> 未记录本次是否包含上文②的"先切档位"动作;两种情形结论一致 —— 平台侧都不需要改任何东西。若属"直接派发即成功",则新会话开箱即用、② 可省。