From 945db355fcae01873767b23a0315fb1b6f6a62a3 Mon Sep 17 00:00:00 2001 From: weichen Date: Thu, 15 Oct 2026 13:29:49 +0000 Subject: [PATCH] Record the layered Python environment proposal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why: 8788 could not render docx because the sandbox hides host site-packages and there is no request path for extra packages. The proposal fixes the design before the implementation lands. Evidence: SELF — proposal written from the cnv_7936c0a799ff transcript and the existing exec-deps-dir code path. Impact: Documents only. Index lists the proposal as pending review, not approved. Refs: docs/proposals/2026-10-15-layered-python-env.md Co-authored-by: Cursor --- .../2026-10-15-layered-python-env.md | 108 ++++++++++++++++++ docs/proposals/INDEX.md | 3 +- 2 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 docs/proposals/2026-10-15-layered-python-env.md diff --git a/docs/proposals/2026-10-15-layered-python-env.md b/docs/proposals/2026-10-15-layered-python-env.md new file mode 100644 index 0000000..43ba0fe --- /dev/null +++ b/docs/proposals/2026-10-15-layered-python-env.md @@ -0,0 +1,108 @@ +# 分层 Python 环境方案 r1 + +状态:待评审(收录不是批准)。编制:2026-10-15。 +触发:dev 8788 会话 `cnv_7936c0a799ff` 中,智能体无法把已有 Markdown 摘要渲染成 docx,并向用户报告「没有 Python 对应的包」。 + +## 1. 已核实的故障(不是「缺包」本身) + +8788 的 `run_python` / `run_shell` 跑在 bwrap 沙箱里(只读根、`--tmpfs /home`、`--unshare-net`)。同一台宿主机的 `~/.local` 里其实已经有 python-docx、lxml、openpyxl、python-pptx。失败来自四件事叠在一起: + +1. **依赖层没有接线。** 代码已有 `--exec-deps-dir`(F3),但 `scripts/svc.sh` 与 8788 的启动参数都没有传入它。沙箱把 `/home` 遮掉之后,`~/.local` 不可见。 +2. **解释器解析到会被遮掉的路径。** 服务进程的 `PATH` 里 linuxbrew 在 `/usr/bin` 之前,`python3` 被解析成 `/home/linuxbrew/.linuxbrew/bin/python3`。沙箱里这条路径不存在,`run_python` 得到 `exit=127`。 +3. **工具说明指向一条走不通的安装路径。** `run_python` 描述和 `base.deliverables` 都写「缺库先 `pip install --user`」。沙箱断网、根只读、家目录是空 tmpfs,这条命令必然失败。 +4. **没有向系统申请包的通道。** 智能体只能探测、报错、让用户选择,再派一个后台任务去轮询一个永远不会出现的依赖。 + +同一次会话里还有多次模型网关 `401 invalid_api_key`。那是密钥问题,不在本方案范围内。 + +## 2. 目标 + +每个工作区和沙箱都能直接使用系统提供的 Python 能力(解释器 + 办公与数据处理基础包),不必各自新建虚拟环境。某个工作区需要额外的包时,向系统申请;系统决定装不装,并只把结果挂给这个工作区。 + +成功标准: + +- 沙箱内 `import docx, openpyxl, pptx` 成功,`python3 -m housestyle.build` 能产出通过结构自检的 docx。 +- 两个工作区要同一包的不同版本时,互不影响,基础层版本不变。 +- 沙箱仍然断网。安装只发生在宿主侧的构建器里,并且只接受带哈希的预编译 wheel。 +- 白名单内的包自动批准;白名单外、要改基础层版本、体积超限、用户上传的 wheel,弹审批卡。sdist 与黑名单直接拒绝。 + +## 3. 不采用的两条路 + +- **每个工作区一个 venv。** 贵的是重复下载、解析和安装,不是 venv 目录本身。同样的包会被装很多遍。 +- **所有包装进一个可变的系统环境。** 一个工作区的需求会改变所有工作区的行为,版本冲突无法隔离,也无法复现「当时用的是哪一套包」。 + +## 4. 采用的结构 + +所有环境层不可变,按内容哈希命名,按会话挂载。系统只新增层,不改已经发出去的层。 + +```text +/ 默认 runtime/envs/,各服务实例共享 + python/cpython-/ 系统托管的 CPython(只读) + python/current -> 上述目录 + layers/base-/site-packages + layers/x-/site-packages 扩展层:相对基础层的增量闭包 + layers/iso-/site-packages 完整隔离层:仅在审批通过「覆盖基础层版本」时使用 + wheelhouse/ 按 sha256 去重的 wheel + locks/.json + catalog.json 白名单、黑名单、单包体积上限 + current -> layers/base- + bin/pip 垫片:提示改用 python_env,不以安装成功退出 +``` + +层键 = 锁文件内容 + Python 标签 + 平台的哈希。同一份需求在任何工作区都命中同一层。 + +基础层由系统策划,首批为:python-docx、openpyxl、python-pptx、lxml、Pillow、numpy、matplotlib、pypdf、reportlab、markdown、pdfplumber,以及它们的传递依赖。名单可以在评审时增减;锁文件在第一次 `env ensure` 时生成并应入库,之后构建按锁复现。 + +解释器优先使用托管前缀(python-build-standalone 的 install_only 包)。下载失败时,退化为用系统 `python3 -m venv` 做一份唯一的托管前缀,并在 `python/SOURCE` 里写明这是退化,不是每个工作区各做一份。沙箱里的 `python3` 永远是这个前缀的绝对路径;找不到托管前缀时才用 `/usr/bin/python3`,不再走会落到 `/home` 下的 `PATH` 查找。 + +## 5. 一次执行如何看到这些包 + +沙箱参数在 `--tmpfs /home` 之后,只读挂载:托管解释器前缀(如果它位于会被 tmpfs 遮住的路径上)、基础层、该会话绑定的扩展层。`PATH` 固定为 `<托管 bin>:/bin:/usr/local/bin:/usr/bin:/bin`,不再继承宿主 `PATH`。`PYTHONPATH` 是这些层再加上 house-style。`PIP_NO_INDEX=1`。 + +扩展层与基础层来自同一次以基础层为约束的解析,每个发行名只有一个版本,所以叠放顺序不影响导入。隔离层例外:它替换基础层,而不是叠在上面,用来满足「这个工作区必须要 numpy<2」这类与基础层冲突的需求。 + +## 6. 申请(工具 `python_env`) + +动作:`status`、`request`、`wait`。 + +- `status` 返回基础层包清单、本会话已绑定的层、未完成的申请。 +- `request` 带包规格或工作区内的 `.whl`。解析以基础层锁为约束(等价于带 constraints 的 `pip install --dry-run --report`)。分级: + - 全部在白名单内、纯 wheel、未超体积:自动批准并构建。命中已有层时只是多挂一个目录。 + - 白名单外、要覆盖基础层已固定的版本、超体积、用户上传的 wheel:写入 `exec_env_requests`,发一张选择卡(批准 / 拒绝;覆盖基础层版本时多一个「建隔离层」)。系统管理员或该会话的属主可以批。决定写入 `audit_events`。 + - 只有 sdist、命中黑名单、解析失败、哈希不符:拒绝,并说明原因。 +- 构建在宿主侧进行:`--only-binary=:all:`,校验 sha256,只用解包 wheel,不执行 setup.py。这是唯一有网络的步骤,并且不在沙箱里。 +- 构建完成后把层 ID 写进 `exec_env_bindings`。下一次 `run_python` / `run_shell` 挂上这些层。`wait` 在短超时内轮询申请状态。 + +## 7. 三个顾虑如何被挡住 + +- **成本。** 基础层覆盖常见办公和数据场景。wheel 与层按哈希缓存,第二个工作区要同一套包时不再下载、不再解析。 +- **冲突。** 不存在共享的可变环境。冲突需求要么被拒绝并说明卡在基础层的哪一个版本,要么经审批得到一份只属于该会话的隔离层。基础层哈希不变。 +- **包都堆进系统环境。** 基础层只放策划过的包。扩展层按会话绑定,其他会话看不见。升级基础层生成新的 `base-` 并移动 `current`;已经绑定旧基础层的会话保持不动,直到显式 rebase。 + +## 8. 安全 + +- 沙箱断网、根只读、家目录遮蔽,这些都不放宽。 +- 不在宿主上执行第三方构建脚本。 +- 只挂当前会话绑定的层。 +- 申请记录含会话、包、wheel 哈希、批准人。 +- 任务封存时应能读到基础层哈希与层 ID(绑定表就是这份清单)。 + +## 9. 实施切片 + +- **P0** 解释器与 `PATH` 不再指向会被沙箱遮住的 linuxbrew;`svc.sh` 在环境根存在时传入 `AGENT_SYSTEM_EXEC_ENV_ROOT` 与基础层 `AGENT_SYSTEM_EXEC_DEPS_DIR`;工具说明与提示词改为 `python_env`,去掉 `pip install --user`;配置了依赖层时启动做真实导入探针。 +- **P1** 环境根、锁、层、wheelhouse、托管解释器,以及 `agent-system env ensure|lock|gc|doctor|rebase|python`。 +- **P2** 表 `exec_env_requests`、`exec_env_bindings`,工具 `python_env`,分级审批,pip 垫片,按会话挂载。 +- **P3** 管理区「Python 环境」页(层、申请、批准/拒绝、rebase、gc、上传 wheel)。 + +## 10. 验收 + +- `sandboxArgv` 覆盖多层只读挂载、固定 `PATH`、`PIP_NO_INDEX`。 +- 离线解析夹具:冲突、白名单、sdist 拒绝、哈希不符。 +- 有 bwrap 时:沙箱内能导入层里的模块,`pip` 垫片给出申请指引,外网不可达。 +- 两个会话分别要 `foo==1` 与 `foo==2`,各得一层,基础层哈希不变。 +- 在基础层可用时,`housestyle.build` 产出的 docx 通过既有 OOXML 结构自检。 + +## 11. 已定与未定 + +已定(2026-10-15 对话):白名单内自动批准,其余弹审批卡;解释器由系统托管,而不是长期绑死宿主 `/usr/bin/python3`。 + +未定,留在评审:基础层最终包名单是否增减;审批人除系统管理员和会话属主外,是否还要接项目角色。本实施先只认系统管理员与会话属主,避免在基座里发明第二套项目角色判断。 diff --git a/docs/proposals/INDEX.md b/docs/proposals/INDEX.md index f79df77..b5d3f8b 100644 --- a/docs/proposals/INDEX.md +++ b/docs/proposals/INDEX.md @@ -4,7 +4,8 @@ 本索引负责导航与差异提示,规划正文、批准范围和验收条件仍以链接的主记录及其批准版本为准。 合入授权不批准新方向、不调整里程碑范围、不启动新的业务任务。 -待评审(收录不是批准):[svc 插件组合部署(--with)方案 r1](2026-10-14-svc-plugin-compose-deploy.md)。 +待评审(收录不是批准):[svc 插件组合部署(--with)方案 r1](2026-10-14-svc-plugin-compose-deploy.md); +[分层 Python 环境方案 r1](2026-10-15-layered-python-env.md)(8788 无法生成 docx 的环境层;白名单自动批准,其余审批)。 ## 先看哪一份 -- 2.54.0