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

13 KiB
Raw Blame History

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 实现,答案是:

// 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)关闭:平台侧零改动、无需放权评估,与用户判据(不扩大用户访问边界)一致。

未记录本次是否包含上文②的"先切档位"动作;两种情形结论一致 —— 平台侧都不需要改任何东西。若属"直接派发即成功",则新会话开箱即用、② 可省。