From 3b8d12969314db604fdbf5118d72f291d6556e56 Mon Sep 17 00:00:00 2001 From: mesalogo Date: Thu, 3 Sep 2026 10:31:14 +0800 Subject: [PATCH] fix: install managed Python on Linux filesystems MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Managed Python installation rejected valid case-distinct terminfo symlinks in the pinned Astral archives on Linux x64 and arm64. Archive uniqueness now follows the target filesystem: Linux preserves case-distinct paths, while every target still rejects exact duplicates and unsafe entries. Move the unpublished 0.12.0 bilingual release notes to the immutable-tag recovery version 0.12.1 and record the failed tag evidence. Release note: 修复 Linux x64/ARM64 上托管 Python 因官方归档包含大小写不同路径而无法安装的问题。 --- FEATURES.md | 12 ++- FEATURES.zh-CN.md | 6 +- .../local-tool-environment/progress.md | 28 +++++- .../technical-design.md | 8 +- package-lock.json | 4 +- package.json | 2 +- resources/release-notes.json | 12 +-- .../python-artifact-extract.test.ts | 86 +++++++++++++++++-- .../python-artifact-extract.ts | 30 +++++-- src/main/release-notes-source.test.ts | 2 +- 10 files changed, 152 insertions(+), 38 deletions(-) diff --git a/FEATURES.md b/FEATURES.md index f7c94148..acd72e48 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -129,7 +129,7 @@ otherwise. database. Successful tool START/END events appear exactly once, with no Prompt, provider, or tool replay observed. The current source lock and Agent source lock is `0.11.14`, while the current Desktop release candidate - is `0.12.0`; formal publication status follows the separate Agent and Desktop + is `0.12.1`; formal publication status follows the separate Agent and Desktop release channels. - [x] **Manual SSH Host environment provisioning source path**: After Host Key, authentication, and system probes succeed, GoodBuddy saves the Host and @@ -289,14 +289,18 @@ otherwise. removal. New local Runtimes and stdio MCP processes receive immutable PATH snapshots without changing ordinary terminals, the system environment, or remote Hosts. Managed Node and native-source Python have passed real - installation validation on Windows x64. + installation validation on Windows x64. Managed Python archive validation + follows the target filesystem: Linux preserves valid case-distinct paths, + while every target still rejects exact duplicates and unsafe entries. - [ ] **Local tool-execution environment release acceptance**: All six platform/architecture OSS mirror objects have passed public byte, size, and SHA-256 verification. Managed Python remains an on-demand download and does not add license files to the Desktop package. Each standard package job uses a native target-architecture Runner and performs a real managed-Python - install, SSL, pip, and venv probe before packaging; the `0.12.0` candidate - must pass that matrix. Real Skill/MCP execution, custom interpreter + install, SSL, pip, and venv probe before packaging. The immutable `v0.12.0` + attempt passed Windows and macOS but exposed Linux TAR case handling before + publication; the corrected `0.12.1` candidate must pass all six targets. + Real Skill/MCP execution, custom interpreter execution, and coordination with affected running processes remain acceptance work. Six-platform release acceptance must not be claimed before the applicable checks pass. diff --git a/FEATURES.zh-CN.md b/FEATURES.zh-CN.md index 7b7eee1b..40b00da4 100644 --- a/FEATURES.zh-CN.md +++ b/FEATURES.zh-CN.md @@ -28,7 +28,7 @@ - [x] **直连模型 Runtime**:支持问答、知识总结、受控工具执行和图像生成。 - [x] **直连模型编程 Agent**:本机直连文本模型可在 Execute 模式运行平台 Shell,并可按父请求模式、模型、工作区和能力范围委派一层编程 Subagent;OpenCode、Continue、DeepSeek Harness 和托管 SSH 不重复注入这两个工具。Windows 本机命令与真实模型“修改、测试、修复、复核”闭环已通过,macOS 与 Linux 真机命令仍待对应平台验收。 - [x] **OpenCode 与 Continue**:使用隔离子进程、环境变量白名单、统一配置、取消、总执行时限、有界流式输出和活动记录;共享进程回收逻辑保留 Windows 完整进程树终止,并对采用独立进程组的 POSIX 子进程执行组回收。聊天状态检查使用一次性探测 Runtime 并在返回后立即回收,不进入按模型与项目隔离的执行 Runtime 缓存;执行 Runtime 仍按项目复用,因此不同项目可并行。交互提问只由前台对话回答,定时任务、远程通道和委派等后台执行遇到提问时会立即失败并提示改为前台运行,避免无限等待。本机 OpenCode 启动时直接使用已选择的绝对路径,不预先检查文件或运行 `--version`,路径失效时由实际启动返回错误;托管 Linux ARM Host 激活 Runtime 时复用准备阶段已验证的 registry 和 manifest,不重新哈希或检查完整 OpenCode 二进制。 -- [x] **托管 SSH OpenCode 闭环(技术预览)**:该能力由“设置 > 平台功能”中的独立“远程项目(技术预览)”页签控制并默认关闭;关闭时不影响任何本地项目、普通桌面功能或桌面发布。启用后可管理固定 Host Key 的 SSH Host、浏览有界远端目录并创建 Ask/Execute 项目;Ask 在 Runtime 边界通过 bubblewrap 保持 Workspace 只读,Execute 使用所选 SSH 账号的完整权限。Agent 通过私有 Unix socket 和 ACP v4 持有 accepted Prompt、Provider/工具轮次、Runtime 进程、稳定模型 ledger 与有界语义 transcript;Desktop 退出、网络中断或本机进程结束后 Host 继续执行,重启后只 attach 原 controller/binding/operation 并把 provenance 原子归并回原对话,不重发 Prompt。模型 profile 与 API Key 只在当前 accepted operation 生命周期内进入 Agent 内存,不写入 Renderer、SSH 参数、远端环境或磁盘。远端组件不嵌入桌面包,项目切换只更新本地选择且不会连接或下载;设置页只读取小型签名目录并按 Linux x64/arm64 显示本地版本、在线最新兼容版本和“有更新/已是最新”,用户点击后才下载签名 `.gbagent`,也可导入/导出离线包。每个复合包包含 Agent、固定 Node 与由桌面源码统一维护适配的 OpenCode Runtime;在线来源跟随“关于与更新”的 GitHub/北京 OSS 选择,签名累计目录绑定最低桌面版本、Agent 协议、架构、大小、SHA-256 和固定 URL。公钥 registry 接受等价 JSON 空白和换行,但仍严格校验 schema、Ed25519 key、环境与撤销;目录、包、manifest、payload 签名和流式 SHA-256 不变。缺少某架构包只使该架构托管 SSH 不可用。项目只保存 Host、远端路径、Runtime 选择和模式;首次实际使用 Workspace/Runtime 时读取 Host current identity,同一进程内其他项目复用已确认 identity 和 Agent 连接。托管 SSH 会话只显示支持的 OpenCode 选择。同一 Host 可并行运行多个项目和对话;恢复进度分别显示网络、Agent、Runtime、已提交事件 cursor、完成或失败/重试,只阻塞受影响项目。Linux x64 真实 Host 已覆盖短/长 detach、本机 harness 强制结束、并发、SSH relay 丢失、取消、确定/不确定 Provider 失败、Agent `SIGKILL`/重启和 reopened Desktop SQLite 恢复;成功工具 START/END 各一次,未发现 Prompt、Provider 或工具重放。当前 Agent 源码 lock 为 `0.11.14`,Desktop 发布候选为 `0.12.0`;正式发布状态以 Agent 与 Desktop 的独立发布渠道为准。 +- [x] **托管 SSH OpenCode 闭环(技术预览)**:该能力由“设置 > 平台功能”中的独立“远程项目(技术预览)”页签控制并默认关闭;关闭时不影响任何本地项目、普通桌面功能或桌面发布。启用后可管理固定 Host Key 的 SSH Host、浏览有界远端目录并创建 Ask/Execute 项目;Ask 在 Runtime 边界通过 bubblewrap 保持 Workspace 只读,Execute 使用所选 SSH 账号的完整权限。Agent 通过私有 Unix socket 和 ACP v4 持有 accepted Prompt、Provider/工具轮次、Runtime 进程、稳定模型 ledger 与有界语义 transcript;Desktop 退出、网络中断或本机进程结束后 Host 继续执行,重启后只 attach 原 controller/binding/operation 并把 provenance 原子归并回原对话,不重发 Prompt。模型 profile 与 API Key 只在当前 accepted operation 生命周期内进入 Agent 内存,不写入 Renderer、SSH 参数、远端环境或磁盘。远端组件不嵌入桌面包,项目切换只更新本地选择且不会连接或下载;设置页只读取小型签名目录并按 Linux x64/arm64 显示本地版本、在线最新兼容版本和“有更新/已是最新”,用户点击后才下载签名 `.gbagent`,也可导入/导出离线包。每个复合包包含 Agent、固定 Node 与由桌面源码统一维护适配的 OpenCode Runtime;在线来源跟随“关于与更新”的 GitHub/北京 OSS 选择,签名累计目录绑定最低桌面版本、Agent 协议、架构、大小、SHA-256 和固定 URL。公钥 registry 接受等价 JSON 空白和换行,但仍严格校验 schema、Ed25519 key、环境与撤销;目录、包、manifest、payload 签名和流式 SHA-256 不变。缺少某架构包只使该架构托管 SSH 不可用。项目只保存 Host、远端路径、Runtime 选择和模式;首次实际使用 Workspace/Runtime 时读取 Host current identity,同一进程内其他项目复用已确认 identity 和 Agent 连接。托管 SSH 会话只显示支持的 OpenCode 选择。同一 Host 可并行运行多个项目和对话;恢复进度分别显示网络、Agent、Runtime、已提交事件 cursor、完成或失败/重试,只阻塞受影响项目。Linux x64 真实 Host 已覆盖短/长 detach、本机 harness 强制结束、并发、SSH relay 丢失、取消、确定/不确定 Provider 失败、Agent `SIGKILL`/重启和 reopened Desktop SQLite 恢复;成功工具 START/END 各一次,未发现 Prompt、Provider 或工具重放。当前 Agent 源码 lock 为 `0.11.14`,Desktop 发布候选为 `0.12.1`;正式发布状态以 Agent 与 Desktop 的独立发布渠道为准。 - [x] **SSH Host 手动环境准备源码链路**:Host Key、认证和系统探针成功后先保存 Host,并只读探测共享 Agent/Runtime;保存 Host、打开项目都不自动安装。Host 卡片只有一个按版本事实显示“安装远程环境”“更新远程环境”或“重新安装”的主按钮,次级 SegmentedControl 选择默认且不持久化的“自动”、Host 下载或 GoodBuddy 传输;“版本匹配”badge 不等同环境健康。自动模式只在 operation/prepare 前探测并择一,显式选择保持有效,任何 prepare、commit 或 adoption 失败都不跨 acquisition 自动 fallback。两种方式把同一签名 compound `.gbagent` 交付到固定 staging 后,共用 control-plane prepare、commit、Agent activate/health、Runtime activate、finalize 与显式 cleanup。GoodBuddy 路径可在同一次操作下载并验证缺失候选、缓存并取得 lease,再有界流式 SFTP 上传一个归档和其中已验证的 bootstrap Node,不把约 294 MiB 整包读入 Main `Buffer`;Host 在解包时完成一次完整 payload 校验。未完成操作只记录暂存 cleanup 所需的 operation ID;下次更新尽力清理旧暂存后重新 prepare,不保存远端 metadata 副本,也不让 cleanup 失败阻塞新更新或回滚健康环境。已有项目在实际使用 Workspace/Runtime 时按需解析 Host current identity 并执行固定 `attach-or-bootstrap`,注册后的 health、capabilities 和 prompt 启动不扫描完整 payload。详见 [设计说明](./docs/features/remote-host/environment-provisioning-technical-design.md)。 - [ ] **SSH Host 环境准备真实 Host 验收**:源码已可使用当前 format v1 包;仍需完成 GitHub、北京镜像、Linux x64/arm64、取消和离线 GoodBuddy 传输的真实 Host 验收。当前状态不表示已发布或已完成真实 Host 测试。 - [x] **DeepSeek Harness(预览)**:使用 GoodBuddy 固定 Host 和 OpenAI 兼容模型连接;优先使用管理员提供的连接,否则跟随兼容的默认模型或首个兼容连接,无需单独重复选择。Ask 只允许调用 Host 中真实注册的 `read`、`skill` 以及 Main 管理的 Web Search/Fetch 代理,拒绝插件同名冒充,Execute 放行全部已启用内置及插件工具,并以当前用户权限运行。图像输入跟随所选模型连接的能力声明,文本模型在 Host 或模型调用前拒绝图片,图片模型通过有界内联内容和临时 Attachment Store 接收 JPEG/PNG。 @@ -49,8 +49,8 @@ ### Skills、MCP 与知识库 - [x] **Skills 按需接入**:可分配给直连模型、OpenCode、Continue 和 DeepSeek Harness,并使用有界资源和受控 Runtime 边界。 -- [x] **本机工具执行环境源码链路**:在“能力与工具 > 工具执行环境”中为本机 Skills 与 stdio MCP 选择 GoodBuddy 托管 Node.js、按需安装的托管 Python,或经过真实验证的自定义解释器;提供独立的原生地址/OSS 镜像选择、诊断、安装进度、取消和删除。新的本机 Runtime 与 stdio MCP 获得不可变 PATH 快照,不修改普通终端、系统环境或远程 Host。Windows x64 托管 Node 与原生地址 Python 已通过真实安装验证。 -- [ ] **本机工具执行环境发布验收**:六个平台/架构的 OSS 镜像对象已完成字节、大小和 SHA-256 公开回读验证。托管 Python 保持按需下载,不向 Desktop 发行包额外带入许可证文件。每个标准打包任务使用目标架构原生 Runner,并在打包前真实安装托管 Python,验证 SSL、pip 与 venv;`0.12.0` 候选仍须通过该矩阵。真实 Skill/MCP、自定义解释器和运行中进程协调仍是待完成验收;完成适用检查前不宣称六平台发布验收通过。 +- [x] **本机工具执行环境源码链路**:在“能力与工具 > 工具执行环境”中为本机 Skills 与 stdio MCP 选择 GoodBuddy 托管 Node.js、按需安装的托管 Python,或经过真实验证的自定义解释器;提供独立的原生地址/OSS 镜像选择、诊断、安装进度、取消和删除。新的本机 Runtime 与 stdio MCP 获得不可变 PATH 快照,不修改普通终端、系统环境或远程 Host。Windows x64 托管 Node 与原生地址 Python 已通过真实安装验证;托管 Python 归档按目标文件系统验证,Linux 保留大小写不同的合法路径,所有目标仍拒绝完全重复路径和不安全条目。 +- [ ] **本机工具执行环境发布验收**:六个平台/架构的 OSS 镜像对象已完成字节、大小和 SHA-256 公开回读验证。托管 Python 保持按需下载,不向 Desktop 发行包额外带入许可证文件。每个标准打包任务使用目标架构原生 Runner,并在打包前真实安装托管 Python,验证 SSL、pip 与 venv。不可变的 `v0.12.0` 尝试在发布前通过 Windows 与 macOS,但暴露了 Linux TAR 大小写处理问题;修正后的 `0.12.1` 候选仍须通过全部六个目标。真实 Skill/MCP、自定义解释器和运行中进程协调仍是待完成验收;完成适用检查前不宣称六平台发布验收通过。 - [x] **内置 MCP 按需接入**:知识库、魔法笔记与 GoodBuddy 配置 MCP 可分别启停,并可分配给直连模型、GoodBuddy 管理的 OpenCode 和 Continue;DeepSeek Harness 在设置中明确显示为暂不支持。内置 MCP 仅通过当前请求的短期本机权限提供,Ask / Execute 读写边界不受用户配置放宽。 - [x] **MCP Tools**:显式启用的自定义 MCP 可按 Runtime 分配给直连模型、GoodBuddy 管理的 OpenCode、Continue Agent Execute 和 DeepSeek Harness,并仅在 Execute 加载;Agent 子进程只获得按请求签发的本机回环权限,MCP 地址、命令和凭据保留在 Main,动态工具仍会重新发现并经过现有执行记录与权限边界。 - [x] **MCP Prompts 与 Resources 元数据**:MCP 测试仅在 Server 声明对应能力时发现有界的 Prompt、参数与 Resource 元数据,不读取 Resource 内容;Runtime 支持的 Prompt 可填入聊天草稿后继续编辑。OpenCode 可报告实验性 Resource 清单,Continue 当前版本明确不支持 Resources。 diff --git a/docs/features/local-tool-environment/progress.md b/docs/features/local-tool-environment/progress.md index e04b6203..451aa2e9 100644 --- a/docs/features/local-tool-environment/progress.md +++ b/docs/features/local-tool-environment/progress.md @@ -6,8 +6,8 @@ | --- | --- | | 文档类型 | 功能进度 | | 状态 | 已实施,发布候选 CI 验收中 | -| 版本 | 0.4 | -| 日期 | 2026-09-02 | +| 版本 | 0.5 | +| 日期 | 2026-09-03 | | 关联入口 | [工具执行环境](./README.md) | ## 当前结论 @@ -19,7 +19,9 @@ 六个 OSS 镜像对象已发布并公开回读验证。托管 Python 继续按需下载,Desktop 发行包不额外 带入许可证文件。标准六平台打包任务已改用目标架构原生 Runner,并在打包前执行托管 Python -的真实安装、SSL、pip 和 venv 探针;`0.12.0` 候选仍需通过这组 CI 才能完成对应跨平台验收。 +的真实安装、SSL、pip 和 venv 探针。不可变的 `v0.12.0` 尝试已通过 Windows 与 macOS, +但 Linux x64/ARM64 在发布前暴露 TAR 大小写冲突误判;修正后的 `0.12.1` 候选仍需通过 +全部六个目标才能完成对应跨平台验收。 ## 已完成 @@ -53,6 +55,8 @@ - [x] 用户选择的原生地址或 OSS 镜像在任务开始时冻结,同一次任务不回退。 - [x] HTTPS、重定向 Host、大小、SHA-256、取消和部分文件清理。 - [x] NuGet ZIP 与 Astral TAR 的 payload 限定、路径穿越防护、展开上限和安全链接处理。 +- [x] 归档路径唯一性遵循目标文件系统;Linux 保留大小写不同的合法 TAR 条目,所有目标 + 仍拒绝完全重复路径,Windows/macOS 仍拒绝大小写冲突。 - [x] GoodBuddy-owned 暂存安装、发布、失败清理、更新保留和删除。 - [x] `python`、`python3`、`pip` 同源,`pip` 固定使用所选 Python 的 `-m pip`。 - [x] 发布前真实验证 Python 3.13.15、架构、标准库、SSL、pip,并创建和执行临时 venv。 @@ -62,7 +66,9 @@ - [x] 发布六个平台/架构的字节完全相同 OSS 镜像对象并公开回读校验。 - [x] 确认托管 Python 保持按需下载,Desktop 发行包不额外带入许可证文件。 -- [ ] Windows ARM64、macOS x64/ARM64、Linux x64/ARM64 原生 CI 安装、SSL、pip、venv 探针;工作流已接线,等待 `0.12.0` 候选运行结果。 +- [x] Windows ARM64、macOS x64/ARM64 原生 CI 安装、SSL、pip、venv 探针。 +- [ ] Linux x64/ARM64 原生 CI 安装、SSL、pip、venv 探针;`v0.12.0` 因 TAR 大小写 + 冲突误判失败,等待 `0.12.1` 修正候选运行结果。 - [ ] 最低 Windows/macOS/glibc 支持矩阵和 Windows NuGet 签名核验。 - [ ] 真实 JavaScript/Python SKILL 成果及 Node/Python stdio MCP 调用。 - [ ] 自定义 Node/Python 在产品 UI 中的真实选择和任务执行。 @@ -75,6 +81,20 @@ ## 已验证证据 +### 2026-09-03 `v0.12.0` 失败标签恢复 + +- 不可变标签工作流 `33704483505` 的 Windows x64/ARM64 与 macOS x64/ARM64 托管 Python + 安装、SSL、pip、venv 探针和打包成功;Linux x64/ARM64 在实际官方 Astral TAR 中发现 + `share/terminfo/2/2621A` 与 `share/terminfo/2/2621a` 后被大小写无关去重误判为冲突。 +- 最终 GitHub/OSS 发布任务跳过,GitHub 未创建 `v0.12.0` Release,Latest 仍为 + `v0.11.14`;恢复版本按不可变标签规则提升为 `0.12.1`。 +- 提取器改为按工件目标平台判断路径唯一性,并继续保留完全重复路径、路径穿越、安全链接 + 和展开上限校验。锁定的 Linux x64 上游工件重新下载后大小与 SHA-256 匹配目录,并确认 + 冲突项确为大小写不同的两个合法 symlink;临时审计工件已清理。 +- Release Notes 校验通过;工具环境、工作流和 Release Notes 聚焦测试 `40/40` 通过, + live 安装项按本地发布规则未启用。最终 `npm test` 为 `3478` 通过、`51` 跳过, + typecheck 与 lint 通过;未运行本地 production build、package 或安装探针。 + ### 2026-09-02 完整实现 - 发布准备确认不把按需下载的托管 Python 许可证文件加入 Desktop `extraResources`、解包 diff --git a/docs/features/local-tool-environment/technical-design.md b/docs/features/local-tool-environment/technical-design.md index e1bcd6d1..342f02db 100644 --- a/docs/features/local-tool-environment/technical-design.md +++ b/docs/features/local-tool-environment/technical-design.md @@ -5,8 +5,8 @@ | 项目 | 内容 | | --- | --- | | 状态 | 已实施,跨平台验收中 | -| 版本 | 0.2 | -| 日期 | 2026-09-02 | +| 版本 | 0.3 | +| 日期 | 2026-09-03 | | 关联 PRD | [工具执行环境 PRD](./prd.md) | | 功能逻辑 | [工具执行环境功能逻辑设计](./logic-design.md) | | 相关架构 | [DeepSeek Harness Runtime 设计](../deepseek-harness/technical-design.md)、[SSH 远程主机与 GoodBuddy Agent 实现说明](../remote-host/technical-design.md) | @@ -122,6 +122,8 @@ ELECTRON_RUN_AS_NODE=1 - 目录包含版本、平台、架构、字节数、SHA-256、原生 Target 和 OSS Target。 - OSS 对象直接复制原生工件字节,不重新压缩或转换。 - 安装包不无条件携带完整 Python。 +- 归档条目唯一性按目标文件系统判断:Linux TAR 区分大小写,Windows 与 macOS 拒绝 + 大小写冲突,所有目标都拒绝完全相同的重复路径。 具体发行包和版本必须在实现前完成六平台工件、维护状态和 pip/venv 可用性核验。 @@ -349,7 +351,7 @@ Renderer 不能提交 PATH、环境变量、下载 URL、摘要、安装目录 ## 15. 验证 单元测试覆盖设置迁移、探测、版本解析、shim 转义、PATH 顺序、失效路径、下载摘要和暂存 -清理,以及原生/OSS 选择冻结、Target 缺失和禁止回退。 +清理,以及原生/OSS 选择冻结、Target 缺失、禁止回退和目标文件系统的归档大小写语义。 Renderer 测试覆盖统一一级分类、三个外层 Tab、默认 Skills、Skills/MCP 定向入口、会话内 状态保留、MCP 内外层 tablist 隔离、键盘切换和窄屏单行滚动。 diff --git a/package-lock.json b/package-lock.json index 71fcf221..41583d71 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "goodbuddy", - "version": "0.12.0", + "version": "0.12.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "goodbuddy", - "version": "0.12.0", + "version": "0.12.1", "license": "0BSD", "dependencies": { "@agentclientprotocol/sdk": "0.25.1", diff --git a/package.json b/package.json index 96179ca2..2ff10b55 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "goodbuddy", - "version": "0.12.0", + "version": "0.12.1", "private": true, "description": "Secure desktop AI workspace with controlled Agent Runtimes", "desktopName": "GoodBuddy", diff --git a/resources/release-notes.json b/resources/release-notes.json index 47ee7be3..2a302228 100644 --- a/resources/release-notes.json +++ b/resources/release-notes.json @@ -2,12 +2,12 @@ "formatVersion": 1, "releases": [ { - "version": "0.12.0", - "releasedAt": "2026-09-02", + "version": "0.12.1", + "releasedAt": "2026-09-03", "notes": { "zh-CN": { "highlights": [ - "GoodBuddy 0.12.0 新增多终端助手工作栏、直连模型编程 Agent 和本机工具执行环境,并完善浏览器导航、回复复制、Skills 组织与 DeepSeek Harness 模型选择。" + "GoodBuddy 0.12.1 新增多终端助手工作栏、直连模型编程 Agent 和本机工具执行环境,并完善浏览器导航、回复复制、Skills 组织与 DeepSeek Harness 模型选择。" ], "features": [ "**助手工作栏与多终端。** 在本机或托管 SSH 项目中开发时,可以从右侧工作栏的“+”同时打开多个终端,分别运行开发服务、测试或其他 Shell 任务。每个终端拥有独立目录、输出和生命周期;关闭页签会结束对应 Shell,应用重启后由用户明确选择是否重连。主侧栏与魔法笔记列表也支持拖拽和键盘调宽,魔法笔记列表可隐藏并记住布局。", @@ -27,13 +27,13 @@ ], "notices": [ "工具执行环境只作用于本机 Skills 和 stdio MCP,不修改系统环境、普通终端或远程 Host。托管 Python 在首次使用时按需下载。", - "远程项目仍为默认关闭的技术预览,继续使用单独发布的 Agent `0.11.14`;Desktop `0.12.0` 不内置 Agent 或远程 Runtime。", + "远程项目仍为默认关闭的技术预览,继续使用单独发布的 Agent `0.11.14`;Desktop `0.12.1` 不内置 Agent 或远程 Runtime。", "本次使用标准 Windows、macOS、Linux `x64`/`arm64` 发布矩阵,不构建 LoongArch 实验预览。" ] }, "en-US": { "highlights": [ - "GoodBuddy 0.12.0 adds a multi-terminal assistant workbar, direct-model programming agents, and local tool environments, together with improved browser navigation, response copy, Skills organization, and DeepSeek Harness model selection." + "GoodBuddy 0.12.1 adds a multi-terminal assistant workbar, direct-model programming agents, and local tool environments, together with improved browser navigation, response copy, Skills organization, and DeepSeek Harness model selection." ], "features": [ "**Assistant workbar and multiple terminals.** While developing in a local or managed SSH project, use the “+” in the right workbar to open separate terminals for a development server, tests, or other Shell tasks. Each terminal has its own directory, output, and lifecycle; closing its tab ends that Shell, and an app restart lets the user explicitly choose whether to reconnect. The main sidebar and Magic Notes list also support pointer and keyboard resizing, while the notes list can be hidden and remembers its layout.", @@ -53,7 +53,7 @@ ], "notices": [ "Tool environments apply only to local Skills and stdio MCP. They do not modify the system environment, ordinary terminals, or remote Hosts. Managed Python is downloaded on demand when first used.", - "Remote Projects remain an off-by-default technical preview and continue to use the separately published Agent `0.11.14`; Desktop `0.12.0` does not embed the Agent or Remote Runtime.", + "Remote Projects remain an off-by-default technical preview and continue to use the separately published Agent `0.11.14`; Desktop `0.12.1` does not embed the Agent or Remote Runtime.", "This release uses the standard Windows, macOS, and Linux `x64`/`arm64` matrix and does not build an experimental LoongArch preview." ] } diff --git a/src/main/local-tool-environment/python-artifact-extract.test.ts b/src/main/local-tool-environment/python-artifact-extract.test.ts index 2bdbdd22..93683f4b 100644 --- a/src/main/local-tool-environment/python-artifact-extract.test.ts +++ b/src/main/local-tool-environment/python-artifact-extract.test.ts @@ -1,4 +1,4 @@ -import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises' +import { lstat, mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { gzipSync, zipSync } from 'fflate' @@ -29,7 +29,7 @@ function tarField(value: string | number, length: number): Buffer { function tarArchive(entries: { path: string - type?: '0' | '2' + type?: '0' | '2' | '5' content?: string linkPath?: string mode?: number @@ -77,7 +77,11 @@ describe('Managed Python archive extraction', () => { })) const destinationDirectory = join(directory, 'install') await extractPythonArtifact({ - artifact: { archiveFormat: 'nuget-zip', payloadRoot: 'tools' }, + artifact: { + platform: 'win32', + archiveFormat: 'nuget-zip', + payloadRoot: 'tools' + }, archivePath, destinationDirectory }) @@ -98,7 +102,11 @@ describe('Managed Python archive extraction', () => { 'tools/../escaped': Buffer.from('bad') })) await expect(extractPythonArtifact({ - artifact: { archiveFormat: 'nuget-zip', payloadRoot: 'tools' }, + artifact: { + platform: 'win32', + archiveFormat: 'nuget-zip', + payloadRoot: 'tools' + }, archivePath, destinationDirectory: join(directory, 'install') })).rejects.toThrow(/Unsafe archive path/u) @@ -121,7 +129,11 @@ describe('Managed Python archive extraction', () => { }, ['python']) const destinationDirectory = join(directory, 'install') await extractPythonArtifact({ - artifact: { archiveFormat: 'tar.gz', payloadRoot: 'python' }, + artifact: { + platform: 'linux', + archiveFormat: 'tar.gz', + payloadRoot: 'python' + }, archivePath, destinationDirectory }) @@ -129,6 +141,58 @@ describe('Managed Python archive extraction', () => { .toBe('python') }) + it('uses target filesystem case semantics for TAR paths', async () => { + const directory = await temporaryDirectory() + const archivePath = join(directory, 'case-sensitive.tar.gz') + await writeFile(archivePath, gzipSync(tarArchive([ + { path: 'python/share/terminfo/2/2621A', type: '5' }, + { path: 'python/share/terminfo/2/2621a', type: '5' } + ]))) + const destinationDirectory = join(directory, 'linux-install') + await extractPythonArtifact({ + artifact: { + platform: 'linux', + archiveFormat: 'tar.gz', + payloadRoot: 'python' + }, + archivePath, + destinationDirectory + }) + expect( + (await lstat(join(destinationDirectory, 'share/terminfo/2/2621A'))) + .isDirectory() + ).toBe(true) + expect( + (await lstat(join(destinationDirectory, 'share/terminfo/2/2621a'))) + .isDirectory() + ).toBe(true) + + await expect(extractPythonArtifact({ + artifact: { + platform: 'darwin', + archiveFormat: 'tar.gz', + payloadRoot: 'python' + }, + archivePath, + destinationDirectory: join(directory, 'darwin-install') + })).rejects.toThrow(/duplicate path/u) + + const duplicateArchivePath = join(directory, 'duplicate.tar.gz') + await writeFile(duplicateArchivePath, gzipSync(tarArchive([ + { path: 'python/lib/duplicate', content: 'first' }, + { path: 'python/lib/duplicate', content: 'second' } + ]))) + await expect(extractPythonArtifact({ + artifact: { + platform: 'linux', + archiveFormat: 'tar.gz', + payloadRoot: 'python' + }, + archivePath: duplicateArchivePath, + destinationDirectory: join(directory, 'duplicate-install') + })).rejects.toThrow(/duplicate path/u) + }) + it('validates relative symlinks and rejects escaping links', async () => { const directory = await temporaryDirectory() if (process.platform !== 'win32') { @@ -143,7 +207,11 @@ describe('Managed Python archive extraction', () => { ]))) const destinationDirectory = join(directory, 'safe-install') await extractPythonArtifact({ - artifact: { archiveFormat: 'tar.gz', payloadRoot: 'python' }, + artifact: { + platform: 'linux', + archiveFormat: 'tar.gz', + payloadRoot: 'python' + }, archivePath: safeArchive, destinationDirectory }) @@ -160,7 +228,11 @@ describe('Managed Python archive extraction', () => { } ]))) await expect(extractPythonArtifact({ - artifact: { archiveFormat: 'tar.gz', payloadRoot: 'python' }, + artifact: { + platform: 'linux', + archiveFormat: 'tar.gz', + payloadRoot: 'python' + }, archivePath: unsafeArchive, destinationDirectory: join(directory, 'unsafe-install') })).rejects.toThrow(/escapes payload root/u) diff --git a/src/main/local-tool-environment/python-artifact-extract.ts b/src/main/local-tool-environment/python-artifact-extract.ts index b183c366..1737a74c 100644 --- a/src/main/local-tool-environment/python-artifact-extract.ts +++ b/src/main/local-tool-environment/python-artifact-extract.ts @@ -88,14 +88,19 @@ function ensureSafeSymlink(relative: string, target: string): void { } } -function verifyEntrySet(entries: SafeEntry[]): void { +function verifyEntrySet( + entries: SafeEntry[], + platform: PythonArtifact['platform'] +): void { if (entries.length === 0 || entries.length > MAX_ENTRIES) { throw new Error('Archive has an invalid number of payload entries') } let expanded = 0 const seen = new Set() for (const entry of entries) { - const key = entry.outputPath.toLowerCase() + const key = platform === 'linux' + ? entry.outputPath + : entry.outputPath.toLowerCase() if (seen.has(key)) { throw new Error(`Archive contains duplicate path: ${entry.outputPath}`) } @@ -107,7 +112,11 @@ function verifyEntrySet(entries: SafeEntry[]): void { } } -function readZipEntries(data: Buffer, root: string): SafeEntry[] { +function readZipEntries( + data: Buffer, + root: string, + platform: PythonArtifact['platform'] +): SafeEntry[] { const entries: SafeEntry[] = [] for (let offset = 0; offset + 46 <= data.length;) { if (data.readUInt32LE(offset) !== zipSignature) { @@ -146,7 +155,7 @@ function readZipEntries(data: Buffer, root: string): SafeEntry[] { } offset = end } - verifyEntrySet(entries) + verifyEntrySet(entries, platform) return entries } @@ -154,11 +163,12 @@ async function extractNuget( archivePath: string, destination: string, root: string, + platform: PythonArtifact['platform'], signal?: AbortSignal ): Promise { ensureNotAborted(signal) const data = await readFile(archivePath) - const entries = readZipEntries(data, root) + const entries = readZipEntries(data, root, platform) const payloadPaths = new Set(entries.map((entry) => entry.archivePath)) const unzipped = unzipSync(data, { filter: ({ name }) => payloadPaths.has(name) @@ -217,6 +227,7 @@ async function extractTar( archivePath: string, destination: string, root: string, + platform: PythonArtifact['platform'], signal?: AbortSignal ): Promise { const entries: SafeEntry[] = [] @@ -243,7 +254,7 @@ async function extractTar( if (validationError) { throw validationError } - verifyEntrySet(entries) + verifyEntrySet(entries, platform) const allowed = new Map(entries.map((entry) => [entry.archivePath, entry])) await extract({ file: archivePath, @@ -278,7 +289,10 @@ async function extractTar( } export async function extractPythonArtifact(options: { - artifact: Pick + artifact: Pick< + PythonArtifact, + 'platform' | 'archiveFormat' | 'payloadRoot' + > archivePath: string destinationDirectory: string signal?: AbortSignal @@ -289,6 +303,7 @@ export async function extractPythonArtifact(options: { options.archivePath, options.destinationDirectory, options.artifact.payloadRoot, + options.artifact.platform, options.signal ) } else { @@ -296,6 +311,7 @@ export async function extractPythonArtifact(options: { options.archivePath, options.destinationDirectory, options.artifact.payloadRoot, + options.artifact.platform, options.signal ) } diff --git a/src/main/release-notes-source.test.ts b/src/main/release-notes-source.test.ts index 4aab4e90..1616b616 100644 --- a/src/main/release-notes-source.test.ts +++ b/src/main/release-notes-source.test.ts @@ -36,7 +36,7 @@ describe('packaged release notes', () => { } } const releaseWithLongScenario = parsed.releases.find( - (release) => release.version === '0.12.0' + (release) => release.version === packageSource.version ) expect( releaseWithLongScenario?.notes['en-US'].features.some(