ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Grist 的 Pyodide 沙箱:从 `make setup` 到 Deno 权限锁定的完整技术指南

Grist 的 Pyodide 沙箱:从 `make setup` 到 Deno 权限锁定的完整技术指南 Grist 的 Pyodide 沙箱从make setup到 Deno 权限锁定的完整技术指南【免费下载链接】grist-coreGrist is the evolution of spreadsheets.项目地址: https://gitcode.com/GitHub_Trending/gr/grist-core导读本文以 sandbox/pyodide/README.md 为骨架系统讲解 Grist 如何借助 PyodidePython 编译为 WebAssembly 的运行时在浏览器/Node 生态之外构建一套基于 WASM 的 Python 数据引擎沙箱包括make setup一键初始化、Makefile 的包管理与版本化发布流程、pipe.js内部管道实现以及通过 Deno 权限锁定使其成为真正意义上沙箱的安全原理。读完本文你将掌握该目录下每个脚本的职责、GRIST_PYODIDE_VERSION与PYODIDE_VERSION的版本治理机制以及服务端 SandboxPyodide.ts 是如何拉起并约束这个沙箱进程的。一、为什么 Grist 需要一个 Pyodide 沙箱Grist 的数据引擎data engine原本以 CPython 进程见 sandbox/grist/main.py形式运行负责公式求值、表格数据处理、导入解析等任务。Pyodide 沙箱提供了另一条运行路径把 Python 代码与依赖包编译成 WebAssembly用一套独立进程由 Deno 或 Node 托管运行。sandbox/pyodide/README.md开篇即点明两个关键事实这是一组用于运行 Grist 的 Pyodide 沙箱的脚本集合只有在额外边界extra boundary内运行时它才算得上是真正的沙箱例如用 Deno 并将权限锁定permissions locked down后运行。也就是说Pyodide 本身只是把 Python 装进 WASM并不能自动隔离恶意代码真正的隔离来自承载进程的权限控制。这一点在 SandboxPyodide.ts 的注释中得到印证Pyodide 经由 deno 运行虽然也提供GRIST_PYODIDE_SKIP_DENO1直连模式但官方明确说明这不是一个好的沙箱。从服务端集成代码可以看到这个沙箱支撑两类典型场景常规数据引擎计算加载 Grist 数据引擎代码sandbox/grist与全部 Python 依赖通过管道pipe接收指令导入import场景当ISandboxOptions.importDir存在时沙箱进程会获得对导入目录的只读访问--allow-read${importDir}并在pipe.js中将其挂载为/import见 pipe.js。二、快速开始make setup按 README 的说明在sandbox/pyodide目录下执行一条命令即可完成环境准备make setupsetup目标定义在 Makefile实际包含两个步骤setup: ./setup.sh make fetch_packages./setup.sh获取并校验 Pyodide 的 Node 包make fetch_packages从远端拉取 Grist 预先构建好的 Python 依赖包。setup.sh 的三重版本校验setup.sh 并非简单的安装脚本它包含一套严谨的版本一致性检查配置与锁文件一致性读取 worker/package.json 中的dependencies.pyodide与 env.sh 中的PYODIDE_VERSION比对不一致直接报错退出首次安装若worker/node_modules/pyodide不存在则执行yarn install --frozen-lockfile --cwd worker使用冻结锁文件保证可复现已装版本校验读取node_modules/pyodide/package.json中的实际版本号若与配置版本不符提示Runmake cleanand try again。最后创建_build/cache作为 Pyodide 的包缓存目录——这个目录也是后续 Deno 权限设计中先授予、后撤销的写入路径详见第五节。版本治理两套版本号各司其职该目录存在两套版本号含义不同容易混淆版本号定义位置含义GRIST_PYODIDE_VERSIONMakefile当前为4Grist 自有的 Python 包集合版本。按注释要求对 Python 包做非增量non-additive变更时必须递增它决定了远端包仓库的 URL 路径v$(GRIST_PYODIDE_VERSION)/PYODIDE_VERSIONenv.sh当前为0.28.1Pyodide 运行时本身的版本必须与worker/package.json及worker/yarn.lock同步env.sh中还记录了历史对应关系GRIST_PYODIDE_VERSION 3对应 Pyodide 版本0.23.4说明两套版本可以独立演进Pyodide 升级不一定导致 Grist 包集合版本变更反之亦然。版本变更后的清理流程env.sh头部注释给出升级 Pyodide 的标准动作更新本文件时务必同步更新worker/package.json和worker/yarn.lock然后执行make clean和make setup以拉取新版 Pyodide。make clean见 Makefile会清除三部分内容_build/packages与_build/pyodide/grist-packagesclean_packagesworker/node_modules、_build/worker旧路径与_build/pyodideclean_code。三、Makefile 的完整命令矩阵README 提示See theMakefilefor other options并强调本目录所有脚本都设计为从 Makefile 调用。make default直接执行make会打印全部可用目标目标命令作用make setup./setup.sh make fetch_packages获取 Pyodide Node 包 预构建 Python 包README 推荐入口make fetch_packagesnode ./preparePackages.js url _build/packages/从远端缓存下载此前构建好的 Python 包make build_packages./check_version_change.sh./build_packages.sh从源码重新构建全部 Python 包make save_packagesaws s3 sync _build/packages s3://grist-pynbox/pyodide/packages/v$(GRIST_PYODIDE_VERSION)将本地构建产物上传到 S3供fetch_packages拉取make cleanclean_packagesclean_code全量清理本地缓存与构建产物fetch_packages按版本拉取预构建包fetch_packages: node ./preparePackages.js https://s3.amazonaws.com/grist-pynbox/pyodide/packages/v$(GRIST_PYODIDE_VERSION)/ _build/packages/URL 中嵌入v$(GRIST_PYODIDE_VERSION)即包集合的每次非增量变更都对应一个独立版本目录旧版本可继续被旧代码引用互不干扰。build_packages从源码完整重建build_packages.sh 展示了整条构建链路克隆 Pyodide 仓库到_build/pyodide若不存在git checkout $PYODIDE_VERSION并初始化子模块调用./run_docker make利用 Pyodide 官方 Docker 环境构建 Pyodide 本体转译 Python 包将 requirements.txt 复制进容器执行pyodide build -r requirements.txt --outdir grist-packages把 Grist 所需的纯 Python 依赖编译为可在 WASM 环境加载的 wheel随后pyodide py-compile预编译字节码整理产物node ./preparePackages.js将 wheel 从_build/pyodide/grist-packages/复制到_build/packages/并生成元数据。check_version_change.sh构建前的版本守卫check_version_change.sh 接受一个版本号参数由 Makefile 传入GRIST_PYODIDE_VERSION。它维护_build/VERSION.txt记录上次构建的版本版本文件不存在直接写入当前版本号版本一致跳过清理No clean needed版本不一致自动执行make clean与./setup.sh后写入新版本号。这套机制保证构建产物缓存不会跨版本混用——在构建前强制清场避免旧 wheel 污染新版本的包集合。preparePackages.js本地与网络两种工作模式preparePackages.js 是fetch_packages与build_packages共用的整理器根据src是否以http(s):开头自动分派网络模式findOnNet先列出本地已有包仅对缺失项逐个下载name-version.json元数据与 wheel 文件以 200 状态码为成功判据本地模式findOnDisk把listLibs命中的 wheel 复制到目标目录并为每个包额外生成一份name-version.json元数据文件。两种模式最终都会把可用包的fileName列表写入 package_filenames.json这份清单即当前版本包集合的快照目前包含 19 个 wheel如openpyxl、python_dateutil、typing_extensions、friendly_traceback等全部为cp313目标。packages.js从 requirements.txt 推导包清单packages.js 负责解析 requirements.txt 并匹配磁盘上的 wheel 文件只处理包含的行忽略注释与无固定版本的行解析出name与version将包名中的-替换为_如python-dateutil→python_dateutil以匹配 wheel 命名规范在源目录中精确查找前缀为standardName-version-的文件恰好命中一个才算可用hits否则列入misses返回{ available, misses }供调用方决定下载或上报缺失。四、pipe.js沙箱进程内部管道实现pipe.js 是沙箱进程的真正入口即PyodideSettings.scriptPath指向的脚本见 SandboxPyodide.ts。它同时兼容 Deno 与 Node 两种运行时通过typeof Deno ! undefined判断并用不同的文件描述符承载通信——Deno 用标准输入/输出FD 0/1Node 用 FD 4/5因为 Node 会把普通描述符设为非阻塞Windows 下用独立 FD 最稳妥。其核心流程分四步init()loadPyodide加载 WASM 运行时。注意jsglobals中setTimeout被拦截仅在adminMode管理阶段放行进入用户代码阶段后调用即抛错——这是沙箱约束的一部分。sendFromSandbox通过fs.writeSync(OUTGOING_FD, ...)把 Python 侧消息同步写回宿主进程setStdin从INCOMING_FD同步读取宿主指令stderr 统一由[py]前缀记录日志loadCode()调用loadPackage加载全部 Python 包。这里有一段重要防御loadPackage即使什么都没加载也会正常 resolve只通过 errorCallback 报告问题因此脚本在加载后用loadedPackages集合逐一核对发现缺失立即抛错避免问题延迟到引擎深处才以 ImportError 爆发。随后把 sandbox/grist 目录挂载为/grist_src、复制为/grist再卸载copyFiles内部用自定义 Pythoncopytree而非shutil.copytree因为后者在 Windows 下会失败mountImportDirIfNeeded()若设置了IMPORTDIR环境变量将宿主导入目录以 NODEFS 挂载为/import。注释解释了为何采用每次启动时挂载同一沙箱会被复用执行后续导入若只在首次导入时复制文件后续导入的文件将不可见runCode()注入 Python 启动代码——sys.path追加/与/grist设置PIPE_MODEpyodide与IMPORTDIR/import环境变量最终调用main.main()进入 sandbox/grist/main.py 的事件循环。对于 Deno 模式在loadCode完成、执行用户代码之前脚本会主动撤销写权限并撤销对sandbox/pyodide、sandbox/grist、requirements.txt的读权限见 pipe.js打印revoked read and write permissions.日志——这正是锁定权限后才是真沙箱的落地实现。五、服务端集成与 Deno 权限模型进程启动与参数构造SandboxPyodide.ts 的getPyodideSettings()构造沙箱进程的启动参数scriptPath指向sandbox/pyodide/pipe.jscwd为sandbox目录Node 模式设置GRIST_PYODIDE_SKIP_DENO1时不带额外参数使用描述符 4/5 与stdio: [ignore, ignore, pipe, ipc, pipe, pipe]。注释说明该模式较不安全仅为桌面端等尚未充分验证 Deno 的环境提供兜底——这些场景下用户信任自建文档为安全而中断体验不合理Deno 模式默认通过findDenoBinary在sandbox/pyodide/worker下定位 deno 二进制版本固定为2.6.3见 worker/package.json并授予最小启动权限--allow-readsandbox/pyodide --allow-readsandbox/grist --allow-readsandbox/requirements.txt --allow-writesandbox/pyodide/_build/cache --allow-env [--allow-readimportDir] # 仅导入场景这套权限的授予范围与pipe.js中的撤销清单一一对应四个被授予的读写路径恰是pipe.js在进入用户代码前逐一 revoke 的对象唯一的例外是importDir——导入目录的读权限不会被撤销因为导入期间沙箱仍需访问用户上传的文件。stdio简化为[pipe, pipe, pipe]配合 Deno 下 FD 0/1 的简单管道方案。安全模型总结综合 pipe.js 与 SandboxPyodide.ts 两处实现Pyodide 沙箱的安全边界由三层构成运行时层Python 代码运行在 WASM 中Pyodide天然与宿主进程隔离进程权限层核心Deno 以最小权限启动仅开放加载期所需的读写路径pipe.js在加载完成后、执行用户代码前通过Deno.permissions.revoke撤销写权限与绝大部分读权限并拦截setTimeout等危险全局能力通信层宿主与沙箱仅通过固定文件描述符同步交换消息sendFromSandbox/setStdin无共享内存或其他逃逸通道。六、实操速查与故障排查常用操作序列# 首次初始化README 推荐 make setup # 查看所有可用目标 make # 升级 Pyodide 版本后重建环境 make clean make setup # 修改了 Python 包需求后重新构建 make build_packages # 构建完成后发布到远端缓存 make save_packages常见问题定位setup.sh报版本不匹配检查 env.sh 与 worker/package.json 的 Pyodide 版本是否一致若node_modules中版本过期按提示make clean后重试build_packages静默失败确认_build/VERSION.txt与GRIST_PYODIDE_VERSION一致check_version_change.sh会自动清理不一致状态加载包数量不足pipe.js会抛出Pyodide loaded N of M packages. Missing: ...可对照 package_filenames.json 检查_build/packages/是否完整想临时绕过 Deno 排查问题设置GRIST_PYODIDE_SKIP_DENO1走 Node 直连模式但务必清楚这会显著降低隔离强度仅建议在可信文档的桌面场景使用。七、小结sandbox/pyodide目录虽然 README 简短背后却是一套设计完整的沙箱工程make setup背后是版本双轨治理GRIST_PYODIDE_VERSION管包集合、PYODIDE_VERSION管运行时、基于 S3 的包缓存分发、Docker 内的 wheel 转译流水线以及pipe.js Deno 权限撤销共同构成的隔离边界。理解这套机制不仅能在需要定制 Python 依赖或升级 Pyodide 时得心应手也能为如何把不可信代码安全地跑进 WASM 进程这一通用命题提供一份可复用的参考答案。【免费下载链接】grist-coreGrist is the evolution of spreadsheets.项目地址: https://gitcode.com/GitHub_Trending/gr/grist-core创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进