docs: organize product design by feature

Define repository and feature-scoped documentation rules for PRDs, User Stories, logic, UI, technical design, and progress evidence.

Move feature-owned documents under docs/features, add one entry point per feature, remove the redundant docs index, and update all affected cross-references. Expand the local tool environment design with explicit product, story, logic, UI, technical, and progress documents.
This commit is contained in:
mesalogo
2026-09-01 14:52:22 +08:00
parent dba3380a06
commit 2f1881a414
56 changed files with 1763 additions and 765 deletions
+13
View File
@@ -0,0 +1,13 @@
# 远程主机与远程执行
本功能负责 SSH Host 管理、Host Key、远程环境安装、GoodBuddy Agent、签名 Runtime、
远程 Workspace 和 Ask/Execute 生产路径。
## 文档导航
| 文档 | 权威职责 |
| --- | --- |
| [远程主机与 Agent 技术设计](./technical-design.md) | 当前产品语义、Agent、Runtime、Workspace、协议、生命周期和验证事实 |
| [远程环境准备技术设计](./environment-provisioning-technical-design.md) | Host 下载、GoodBuddy 传输、安装事务、更新和恢复边界 |
现有文档包含产品、功能逻辑和进度事实,后续修改远程功能时再按标准职责拆分。
@@ -0,0 +1,329 @@
# SSH Host 远程环境准备与直连下载设计
## 状态
本文记录 2026-08-28 确认的目标设计及当前实施状态。源码已经实现 Host 手动准备、
GoodBuddy 传输、Host 直连下载、control-plane one-shot installer、发布后健康 adoption,
以及项目路径只验证已安装环境。Host 负责 Agent/Runtime 的安装与更新生命周期,项目流程
只消费已经准备好的 Host 环境。
当前源码可以直接使用既有 package format v1 `.gbagent`:归档已经包含固定 Agent 和
Node,installer 由 GoodBuddy 桌面控制面打包并通过固定 SSH prepare channel 单次发送,
不要求归档包含 `agent/lib/package-installer.cjs`,签名目录也不提供或检查额外的
bootstrap 能力元数据。公开直连能力仍须完成本文末尾 GitHub/北京镜像、Linux x64/arm64、
取消和离线 GoodBuddy 传输的真实 Host 验收;本文
不表示这些验收或新的 Agent 发布已经发生。当前实现细节同时以
[SSH 远程主机与 GoodBuddy Agent 实现说明](./technical-design.md)
和源码为准。
## 设计结论
将安装和更新从项目切换移到 Host 管理是合理的,原因如下:
- Agent、固定 Node 和 OpenCode Runtime 安装在 SSH 账号 Home 下,由同一 Host/账号上的
多个项目共享,不属于某一个 Workspace。
- 大文件传输发生在项目切换时会阻塞导航,也把 Host 级故障误显示成项目故障。
- Host Key、认证、平台和架构本来就在新增 Host 时验证,此时继续只读探测并在 Host 卡片
提供手动准备入口最容易理解。
- 项目只保存 Host ID、远端路径、Runtime 选择和默认工作模式。Agent installation、
Runtime digest、Workspace identity 和 Host revision 都从当次连接取得,不写入项目;
“移出安装”不等于“跳过当前环境检查”。
目标职责是:
| 流程 | 职责 |
| --- | --- |
| 新增或重新验证 Host | 验证身份和认证,保存 Host 并探测环境,不传输安装包 |
| Host 管理 | 用户明确刷新目标 Host,并可选择安装、同版本重修、取消和重试 |
| 创建远程项目 | 只读本地 Host 验证记录,保存时连接所选 Host 并验证当前 Agent、Workspace、Runtime 和模型配置,只持久化稳定配置 |
| 切换远程项目 | 只切换本地项目配置,不连接 Host;实际使用 Workspace 或 Runtime 时按需复用 Host 当前环境 |
## 不变量
- Host 配置与远程环境准备是两个事务。SSH 验证成功后先原子保存 Host、凭据和完整 Host
Key,再开始可能耗时的下载和安装。
- 环境准备失败或取消时保留已保存 Host,显示“未就绪”并允许重试,不创建半完成项目。
- 不把“环境健康”保存为长期可信布尔值。设置页从 Host 的签名 registry 和当前 Host
identity 派生版本状态;准备完成和实际使用时再执行 bootstrap/health。合法但暂未运行的
detached Agent 仍可显示为已安装,打开设置不能为了刷新卡片隐式启动或更新它。
- 所有安装继续使用 GoodBuddy-owned staging 和 side-by-side digest 目录。失败不得覆盖
可用旧安装,也不得删除 Host 上无关文件。
- Renderer 不接收下载 URL、缓存路径、签名 key、digest、SSH 命令或凭据。Main 负责来源
选择、可信目录解析、操作编排和 IPC 输入校验。
- 产品不提供“自动安装远程环境”开关。保存、重新验证或打开 Host 都不能自动传输
`.gbagent`;只有用户点击 Host 卡片的主按钮后,Main 才开始准备。
- Host 卡片只有一个主按钮,按版本事实显示“安装远程环境”“更新远程环境”或“重新安装”。
次级 SegmentedControl 选择“自动”“Host 下载”或“GoodBuddy 传输”;默认“自动”,只影响
下一次操作,不持久化为 Host 配置。
- “自动”只在 operation/prepare 前执行一次 Host capability probe:直连明确可用时选择
Host 下载,否则选择 GoodBuddy 传输。用户显式选择的方式保持有效;一旦 acquisition
已选定,prepare、commit 或 adoption 的任何失败都停止本次操作,不跨 acquisition
自动回退。
- 直连安装只要求已验证 SSH 连接和远端系统工具,不要求现有 Agent daemon。轻量能力探测
返回确定性缺少工具、空间、权限或来源不可达时说明原因;探测本身异常时仍允许用户点击
直连安装,由实际安装流程重新解析签名候选并通过 SSH 重试。
- 项目路径不得为了“方便”回退到隐式安装。Host 未就绪时应引导到 Host 设置,不在项目
切换器中启动大文件传输。
## 新增 Host 流程
新增向导保持现有 Host Key 和认证顺序,并在系统探针之后显示一次只读环境结果:
1. 输入名称、地址、端口、用户名和认证方式。
2. 在发送认证材料前读取并确认 Host Key。
3. 完成 SSH 认证以及平台、架构、Shell、Home 有界探针。
4. 原子保存 Host、加密凭据和完整 Host Key。
5. 读取 Host 已安装的 Agent/Runtime registry,并执行一次轻量下载能力探测。
6. 如果已安装 identity 满足当前 GoodBuddy 要求,显示“版本匹配”并结束;该 badge 不代表
Agent 正在运行或环境健康。
7. 如果组件缺失或需要更新,显示目标 Agent、Node、Runtime、架构、来源和完整包大小,
告知用户保存后前往 Host 卡片安装,然后结束新增向导。
新增流程到此结束,不显示自动安装开关,也不自动开始环境安装。保存后的 Host 卡片提供
一个手动准备主按钮,并以次级 SegmentedControl 选择“自动”“Host 下载”或
“GoodBuddy 传输”。主按钮按版本事实显示“安装远程环境”“更新远程环境”或“重新安装”。
本地尚无精确包不要求用户先离开当前页面;选择 GoodBuddy 传输时,同一次操作会取得并
验证候选后继续上传。
探测不得自动下载完整 `.gbagent`。只有用户明确启动安装后才允许传输大包。
## Host 编辑
- 仅修改显示名称不要求重装远端环境。
- 地址、端口、用户名或 Host Key 变化时,旧 Host identity 立即失效,关闭旧连接和对应
Runtime 缓存。项目记录不含旧组件 identity;下一次打开直接解析新 Host 的当前环境。
保存新身份后只重新执行环境探测,所需安装由用户在 Host 卡片手动启动。
- 只替换同一目标的认证材料时仍重新认证并读取实际环境;如果签名 identity 和健康状态
未变化,不重复安装。
- 当前项目引用的 Host 被编辑时,Main 立即失效该 Host 的 identity 和连接缓存;下一次
Workspace/Runtime 操作按需使用新目标。环境未准备好时该操作失败,不回退旧连接。
## 下载能力探测
探测在 Host Key 已固定且 SSH 认证成功后执行,只检查当前选择的“关于与更新”来源:
- Host 是受支持的 Linux `x64` 或 `arm64`。
- SSH 账号 Home 可写,GoodBuddy-owned staging 可创建。
- 可用空间满足签名目录声明的包大小、解包上限和安全余量。
- Host 可以通过 HTTPS 访问所选 GitHub 或北京镜像 URL。
- 首次安装所需的下载、SHA-256 和运行归档内固定 Node 的能力可用。
- 已安装 Agent 是否报告未来的直连更新能力。
探测使用小型签名目录或有界请求,不以一次成功 `HEAD` 代替真实安装验证。结果只用于当次
展示,不持久化为永久能力;开始安装前重新检查,因为网络、代理和磁盘状态可能变化。
探测失败不能阻止保存 Host,也不能禁用显式 GoodBuddy 传输或离线导入路径。显式选择
Host 下载时由实际操作报告不可用原因;“自动”只把“直连明确可用”作为选择 Host 下载的
条件,其他结果选择 GoodBuddy 传输。
## 工件选择与来源
Main 继续读取并验证 `agent-catalog.json`、`agent-catalog.sig` 和 production 公钥
registry,按以下条件选择唯一候选:
- Host 平台和架构匹配;
- 最低桌面版本满足;
- Agent protocol major 相同,minor 不高于桌面支持值;
- 使用满足条件的最高 SemVer;
- URL、文件名、大小和 SHA-256 来自签名目录;
- 下载来源跟随“关于与更新”的 GitHub/镜像选择。
远程下载只优化大包的网络路径。Main 仍需先取得并验证小型目录,再通过经过 Host Key
验证的 SSH 控制面传递固定候选元数据。产品不接受 Renderer、项目、Host 文件或用户输入的
任意下载 URL。
## 获取与统一安装事务
首次安装不能假设 Host 已有 GoodBuddy Agent、installer 或系统 Node,也不由既有 Agent
daemon 参与。两种 acquisition 的差别只在于如何把同一个签名 compound `.gbagent` 交付到
固定 operation staging:
- **Host 下载**:控制面通过固定 SSH prepare channel 发送签名目录绑定的 URL、大小和
SHA-256;Host 有界下载并校验完整归档。
- **GoodBuddy 传输**:显式选择时,在线目录不可用或本地已验证包版本不低于在线候选,
则优先使用本地包,包括版本更高的离线导入;线上存在更新且可用时仍取得线上新包。
需要下载时,同一次用户操作会验证 SHA-256 和签名、写入缓存并取得 lease。随后以有界
流式 SFTP 上传一个 compound archive,以及从该归档中已验证的 bootstrap Node。约
294 MiB 的完整包不会一次读入 Main `Buffer`,Host 收到后仍再次校验完整包。
交付后两种 acquisition 完全共用一个 control-plane 事务:
1. Main 创建带 operation ID、总时限、输出上限和取消信号的准备操作,并通过固定
`prepare` channel 发送有界 one-shot installer 与结构化输入。
2. Host 在解包写入准备目录的同一遍处理中校验归档大小、SHA-256、外层及内层签名、
平台、架构、协议和每个 payload digest;不再先完整扫描 payload、再重复解包和哈希。
3. `commit` 只复核有界的签名 manifest、准备回执和入口 metadata,再以目录 rename 发布
已准备内容及包内认证的公钥 registry/Runtime lock;不会重读归档或遍历 payload。
4. commit 后在同一条已固定身份的 SSH lease 上执行 Agent bootstrap/readiness 和 Runtime
activate。新 Agent 最多完整验证一次,新 Runtime 最多完整验证一次;注册成功后的
health、capabilities 和 prompt 启动只读取 registry、签名 manifest 与入口 metadata。
5. Main 确认 adoption 后显式执行 cleanup。cleanup 失败不回滚已经健康的 Agent/Runtime,
也不阻塞下一次更新。
这不是两个并行安装管理器,也不存在把 Agent 与 Runtime payload 逐文件走 SFTP 安装的
第二条路径。两种 acquisition 共享相同的 prepare、commit、adoption、finalize 与 cleanup。
Host 缺少可靠的下载、SHA-256 或 ZIP 能力时,不自动安装系统软件,也不要求 root。显式
Host 下载返回明确失败;本次操作不自动切换 acquisition,用户可改选 GoodBuddy 传输后
重新点击主按钮。后续更新和同版本重装复用同一事务,不要求用户先通过项目切换完成
“过渡升级”。
## 本机传输与离线路径
现有 `.gbagent` 本地缓存、在线下载、离线导入和导出继续保留:
- GoodBuddy 路径在本地缺少精确候选时,于同一次操作完成下载、整包大小与 SHA-256、
全部签名和 payload 验证、缓存发布及 lease 获取。
- SFTP 有界流式上传单个 compound archive 与其中已验证的 bootstrap Node 到操作自己的
staging;Main 不把完整归档读入内存,Host 再次校验后进入统一 control-plane 事务。
- Host 无公网、代理/TLS 异常或缺少 bootstrap 工具时,用户可显式使用此路径;离线导入
后也走同一缓存 lease、上传和 Host 校验流程。
- 本地项目和普通桌面打包不依赖任何远端 Agent 工件。
## 完整包与发布
第一阶段继续发布和下载完整复合 `.gbagent`,不同时引入组件化增量发布:
- 远程直连和 GoodBuddy 本机下载都会传输完整包,其中包含 Agent、固定 Node 和 OpenCode
Runtime。
- 每次显式安装或重装都从本次已经校验并解包的准备目录原子替换对应 Agent 与 Runtime
digest 目录,不为节省本机磁盘写入而重新遍历旧目录,也不使用跨安装硬链接。
- 操作中断后不续写旧 operation。下一次更新尽力清理其 staging,并从签名候选重新
prepare;正常 cleanup 随后删除本次准备目录。
只有真实包体和发布频率证明网络成本仍不可接受时,才另行设计 Agent、Node 和 Runtime
独立签名组件目录;本功能不提前引入第二套发布协议。
## Host 准备状态
设置中的 Host 卡片分开表达版本事实和操作结果:
- **未验证**:Host Key、认证或系统探针尚未完成。
- **尚未安装**:Host 可连接,但 Agent 或 Runtime 缺失。
- **需要更新**:已安装 identity 与当前兼容候选不一致。
- **正在准备**:正在探测、下载、验签、安装或健康检查。
- **版本匹配**:版本 badge 只表示 Agent/Runtime identity 与当前兼容候选匹配,不等同于
进程正在运行或环境健康;设置页普通刷新不会启动已停止的 Agent。
- **准备失败**:保留 Host 和可用旧组件,展示有界错误与重试入口。
主按钮按版本事实显示“安装远程环境”“更新远程环境”或“重新安装”。重新安装失败后,
界面显示本次操作未完成,并以随后重新检查的版本卡片表达当前事实,不在提交结果不确定时
断言旧版本未被替换,也不得把“版本匹配”改称“环境健康”。统一进度以 `探测`、
`下载/传输`、`校验`、`应用环境`、`Agent 激活与健康检查`、`Runtime 激活` 和
`完成设置` 表达用户可理解的阶段。
同一 Host identity 同时只允许一个由用户启动、Main-owned 的准备操作。重复观察者加入
已有操作,不启动第二次发布;窗口关闭、显式取消、Host identity 改变或 Main 退出时执行
有界取消。Main 只为该 Host 持久化一个不含凭据的 version-1 pending operation ID,用于
下一次操作开始时尽力清理旧 staging。无论旧 cleanup 成功、目标已变化还是旧记录损坏,
都删除这条可丢弃记录并开始 fresh prepare;不保存远端 metadata 副本,不续写旧 commit,
也不跨重启恢复 adoption。当前操作中可确定的失败会立即尽力 cleanup。环境已健康后的
cleanup 失败不回滚环境,也不阻塞下一次更新。
## 项目创建与切换
- 新建托管 SSH 项目弹窗只读取本地 Host Key/连接验证记录,不逐台连接或刷新版本。只有
已完成本地验证的 Host 可选;浏览目录或保存时才连接所选 Host,并由保存流程完整检查
Agent、Workspace 和 Runtime,但不隐式安装。
- 项目保存验证 Host 当前 revision 与 Host Key generation、当前 Agent、Workspace、
Runtime、模型 profile 和 Ask/Execute 配置;SQLite 事务只写入 Host ID、规范远端路径、
Runtime 选择和默认工作模式。
- 打开或切换已有项目只提交本地项目选择,不执行 SSH、registry、Workspace 或 Runtime
检查。首次实际使用远程 Workspace 或 Runtime 时才解析 Host 管理的当前 Agent/Runtime,
并建立或复用当前 Agent 连接。已安装 Agent 暂未运行时,
固定 `attach-or-bootstrap` 命令幂等启动 registry 中的 current identity;这里的
bootstrap 只启动进程,不安装组件,也不重新读取、哈希或验签完整 payload。
Agent 进程读取 Host 管理已提交的 registry 和匹配 manifest 元数据,不重新验签或扫描
完整安装 payload。
- Workspace 和 Runtime 使用当前 Agent 连接产生的 live handle/lease。项目不保存或核对
历史 Workspace/Runtime identity;当前 registry、连接状态或 capability 无效时停止并
引导到 Host 管理,不在项目流程中下载或安装。
- 桌面更新导致 Host 组件过旧时,项目保持未切换并提示前往 Host 设置。应用启动仍进入
第一个普通本地项目,不自动连接或更新远端 Host。
- Host 环境更新成功后,Main 定向失效该 Host 的 Agent/Runtime identity、连接和 Runtime
会话缓存。所有项目在下一次实际操作时自然使用新的 current 环境;无需改写项目记录。
- 环境准备失败时不改变已有项目绑定。Host identity 编辑失败时则继续保持 fail-closed,
因为旧身份不能替代用户刚保存的新身份。
- 项目管理入口与项目选择分离。项目选择器中的浮动管理按钮直接打开目标项目设置;删除
不访问 Workspace、Agent 或 Host,因此远端目录已删除或 Host 不可达都不能阻止本地
项目记录清理。删除当前项目或 Host 级联删除当前项目时只回退到普通本地项目。
- 项目选择器把托管 SSH 项目按已保存 Host 分组,并只读取 Main 当前持有的 Agent 连接
状态显示“未连接 / 连接中 / 就绪 / 异常”;打开菜单或读取状态不得隐式连接 Host。
Host 已承担类型与连接语义,项目副标题只显示远端工作目录。
- Host 删除确认使用本地数据库列出全部引用项目,包括归档项目;确认后级联删除本地项目
及其会话、任务、成果、记忆、计划和心跳关联,再删除本地 Host 与凭据。整个流程不发起
SSH,也不删除远端内容;跨 Host 设置文件与 SQLite 的普通失败会恢复已写入的一侧。
项目切换不展示远程激活进度,也不阻塞项目导航。`Host`、`Agent`、`Workspace`、`Runtime`
和 `Saving` 进度只属于新建或显式保存项目;实际 Workspace/Runtime 操作使用正常的局部
加载和错误反馈。
## 实现结构
当前实现复用现有机制,没有新增平行的安装管理器:
- 扩展当前 Host 系统探针和远程环境 inspector,返回瞬时下载准备能力。
- 让当前 `RemoteEnvironmentUpdateService` 演进为 Host 环境准备编排器,统一用户手动
启动的首次安装和后续更新、自动 acquisition 选择、进度、取消和最终失效。
- Host 环境准备编排器直接完成一次 package prepare/commit 及 Agent/Runtime adoption。
Workspace 和执行路径只读取 current registry 并调用 Host 侧轻量 identity/health 检查,
在当前进程内复用已确认 identity 和 Agent 连接;Host 编辑或环境更新定向清空这些缓存。
这些路径不会取得安装包、发布组件或通过 SFTP 扫描 payload。
- Host 卡片即使显示相同版本也保留显式重装操作,用于修复 registry、签名或 payload
identity 异常;若同一 digest 的 GoodBuddy-owned 目录校验失败,重装会先隔离旧目录,
并在原子 commit 失败时恢复旧目录。commit 成功后的 adoption 失败保留新发布内容,
下次重装清理旧暂存并重新 prepare/adopt。重装不要求删除 Host、凭据或项目,也不会
处理所有者异常、符号链接或非 GoodBuddy 管理路径。
- 由桌面控制面打包有界 one-shot installer,并通过固定短命令 `exec sh -s` 和 stdin
发送脚本及结构化输入,避免 SSH command 长度限制;既有 package format v1 归档内固定
Node 负责解码和运行它。
- Preload 只暴露明确的 Host 探测、准备、取消和进度方法,不暴露 URL 或任意 SSH 命令。
- 现有 Agent/Runtime side-by-side、固定 SSH lease、Host 端一次完整验证、注册后轻量检查、
连接失效和项目稳定配置事务继续作为唯一实现。
## 迁移
- 已保存 Host 打开设置时只显示本地记录,不自动连接或检查版本;用户点击对应 Host 的
“刷新版本”后才执行只读状态探测。项目切换不依赖该按钮,也不建立连接。
- 数据库升级保留远程项目、Host ID、远端路径、Runtime 选择及关联会话/任务,并删除旧的
Agent/Runtime/Workspace/Host 验证列和 Runtime 验证表。下一次实际远程操作从 Host
current 环境重新取得所有 live identity。
- 如果 Host 未就绪或版本不兼容,项目仍可在本地选择,但实际远程操作失败;不再走旧的
隐式安装。
- 已下载或导入的本地 `.gbagent` 保持可用,不能因加入远程直连而迁移或删除。
- pending operation 保持 version 1,只记录 Host、目标 identity 与 operation ID,用于
尽力清理旧 staging;它不是远端环境状态或恢复日志。
- 远程直连继续使用现有 package format version 1,不要求归档携带 installer,也不要求
目录声明额外的 bootstrap 能力元数据。
## 验收标准
1. 新 Host 保存和探测完成后不会自动下载、上传或安装任何包,也不存在自动安装开关;
再次进入 Host 设置不会逐台连接,只有显式“刷新版本”才探测目标 Host。
2. Host 卡片只有一个按版本事实命名的主按钮;次级 SegmentedControl 提供默认且不持久化的
“自动”及显式“Host 下载”“GoodBuddy 传输”。没有预装 Agent、Node 或 Runtime 的 Host
可以完成准备;远程下载不可用时可手动改用 GoodBuddy 传输。
3. 探测不下载完整包,失败不阻止保存 Host;非确定性探测失败不禁用直连重试,安装失败
或取消后 Host 仍可重试。
4. 直接下载严格匹配签名目录中的 URL、大小和 SHA-256;GoodBuddy 传输可在同一次操作中
下载并验证缺失候选,以有界流式 SFTP 上传完整归档和已验证 bootstrap Node。Host 对
两者完成相同的外层与内层签名、路径、架构、协议和 payload 校验。
5. Host 对本次候选只完成一次完整 payload 校验;commit、adoption、health、capabilities
和 prompt 启动不得再次遍历或哈希完整 Agent/Runtime,显式重装仍能用本次准备内容修复
同 digest 的损坏目录。
6. 创建、打开和切换项目不会产生 `.gbagent` 下载、Agent payload SFTP 上传或安装目录发布。
7. 未就绪 Host 不能创建远程项目;已有项目仍可本地选择,但远程操作失败并引导到 Host 设置。
8. Host 编辑或更新后,引用项目使用 Host 管理的当前连接和 current registry,不读取项目
中的旧 Host/Agent/Runtime/Workspace identity;当前环境无效时才停止并要求在 Host 管理
中修复。
9. Host 地址、用户或 Host Key 在准备期间变化时停止候选发布,旧身份不会被重新启用。
10. GitHub、北京镜像、无公网 Host、取消、应用退出、磁盘不足、损坏下载和不支持架构均有
明确结果,且不触碰 GoodBuddy-owned 目录之外的文件。
11. Linux x64 和 arm64 均通过 GitHub/北京镜像、取消、离线 GoodBuddy 传输,以及项目使用
Host current 环境执行 Workspace/Runtime 操作的真实 Host 测试;这些真实 Host 验收当前
仍待完成。
12. Host 不可达或远端目录不存在时,用户仍可从项目浮动管理入口删除本地项目;删除 Host
会列出并本地清理全部关联项目,但不会连接 Host 或删除任何远端目录和内容。
13. 中断或 adoption 失败后的下一次更新只尽力清理旧 staging,然后 fresh prepare;
pending record 不包含远端 metadata 副本。已健康环境的 cleanup 失败不触发回滚,
也不阻塞下一次更新。
@@ -0,0 +1,502 @@
# SSH 远程主机与 GoodBuddy Agent 实现说明
## 状态
本文记录截至 2026-08-31 的当前代码实现,不定义额外的信任框架。“新增 Host 只探测、Host 卡片手动准备
Agent/Runtime、Host 直接从 GitHub/北京镜像下载、项目始终使用 Host current 环境”已经完成源码接线,
详细事务与验收边界见
[SSH Host 远程环境准备与直连下载设计](./environment-provisioning-technical-design.md);
控制面直连源码可直接使用既有 package format v1 包,不等待携带 installer 的新 Agent
包,也不通过额外目录元数据判断 bootstrap 能力;公开能力仍等待 GitHub/北京镜像、
Linux x64/arm64、取消和离线 GoodBuddy 传输的真实 Host 验收。
Windows 到 Linux x64 的安装、Agent-owned Prompt、Agent 本地模型 gateway、断线恢复、
同一 OpenCode Session 续接、取消和终态清理已经使用真实模型与工具验证。Agent
`0.11.10` 已通过独立 workflow 发布 Linux x64/arm64 复合包和签名累计目录;当前未发布
Agent 源码 lock 为 `0.11.14`,桌面版本仍保持已发布的 `0.11.13`,等待发布审批后再选择
新版本。现有源码显示本地与远端 OpenCode 原生 Task,并取消 GoodBuddy 对生产 Prompt 的
固定墙钟总时限。失败的 `agent-v0.11.3` 保持不可变且未发布。
## 产品语义
远程 Host 与项目能力是默认关闭的技术预览。用户在“设置 > 平台功能”的独立
“远程项目(技术预览)”页签启用开关后,Renderer 才显示 SSH Host 管理、托管 SSH 项目创建和已保存
远程项目;Main 在 IPC 边界执行同一开关校验,避免隐藏界面被绕过。关闭开关不删除 Host、
项目或凭据;当前远程项目切回第一个普通本地项目。应用启动仍固定
选择普通本地项目,不会因为保存了远程项目而自动连接 Host。
远程项目只有两种工作模式:
- **Ask**:Runtime 在操作系统边界以只读方式访问项目 Workspace。
- **Execute**:用户已授权使用所选 SSH 账号的完整权限。Runtime 可以使用该账号可访问的文件、进程、网络和工具,不再要求额外 trust tier、consent checklist、逐工具审批或“受控执行”授权。
Execute 不获得 root 或 SSH 账号本身没有的权限。托管 SSH Prompt 会把当前选中的文本模型
profile 和凭据作为有界、逐 Prompt 的控制消息交给 Agent;该凭据不进入命令参数、环境变量、
语义日志或模型调用账本,并在 Prompt 终态清除。此行为只适用于用户已明确启用的可信组织
网络和已认证 SSH Host。
## 组件
```text
Renderer
-> 窄 Preload / Main IPC
Main
-> SSH Host 与 Host Key 管理
-> SSH connection pool
-> 签名 Agent / Runtime 安装
-> 远程项目当前环境准备与稳定配置 SQLite 事务
-> 永久项目、对话、Task 与已同步事件 SQLite
SSH attach
-> 远端私有 Unix socket
Detached GoodBuddy Agent
-> Workspace / Git 协议
-> Agent-owned OpenCode ACP Prompt Promise
-> Prompt-scoped Provider gateway / model-call ledger
-> 有界语义 transcript / page / ACK
-> 有界私有诊断记录
-> 直接拥有的 Runtime 进程
```
不提供把远端路径交给本机 Runtime 的伪本地模式,也不提供 SSH stdio Runtime fallback。
## SSH Host
- Renderer 只管理脱敏 Host 快照,不接触密码、密文、SSH Client 或任意命令接口。
- Main 在认证前取得 Host Key,展示算法与 SHA-256 指纹,并保存完整 key blob。
- 首次连接需要用户确认。Host Key 变化展示旧/新值并要求显式替换。
- 密码使用 Electron `safeStorage`;系统 SSH Agent 认证不启用 Agent forwarding。
- Host 地址、用户或 Host Key generation 变化时关闭旧连接,并定向退役依赖旧 Host identity 的 Workspace 和 Runtime 会话。下一次选择使用 Host 管理的当前连接记录解析 current Agent/Runtime,不读取项目中的旧 revision 或组件 identity;当前 registry、连接或 capability 无效时才要求显式修复。
## Agent 安装与生命周期
- Agent bundle 通过 manifest、Ed25519 签名、payload digest、平台和架构校验。
- 安装使用当前 SSH 用户目录中的 GoodBuddy-owned 路径和 side-by-side digest 目录。
- Agent 代码与固定 Node Runtime 一起位于签名 compound payload。显式安装或重装从本次
已完整校验的准备目录原子发布整个 Agent,不扫描旧安装、不创建跨版本 Node 硬链接;
Agent 不匹配不会阻止升级,也不会覆盖 GoodBuddy-owned 路径以外的内容。
- 桌面包只携带版本 lock 与 production 公钥,不携带远端 payload。Host 卡片只有一个按
版本事实显示“安装远程环境”“更新远程环境”或“重新安装”的主按钮;次级
SegmentedControl 选择“自动”“Host 下载”或“GoodBuddy 传输”,默认“自动”且不持久化。
每个复合包绑定 Agent、固定 Node 与桌面维护的 OpenCode Runtime 精确工件。添加或重新
验证 Host 只保存并探测,不自动传输完整包。
- Host 管理独占 Agent/Runtime 的包准备、更新和完整 payload 验证。已有项目的打开和切换
只更新本地项目选择。Workspace 和执行路径按需读取 Host current registry,
执行固定 `attach-or-bootstrap` 并复用当前 Agent 连接;同一进程内复用已确认 identity。
它们不取得安装包、不发布组件,也不通过 SFTP 重读或哈希 payload。
- Agent 的固定 attach/按需启动命令只读取 Host 管理已提交的 installation registry 和
与其 digest 匹配的有界 manifest 元数据,不在每次项目切换时重新验签或哈希完整 payload。
完整验签、payload 扫描和 registry 写入仍只发生在显式 Host 准备/更新流程。
- 项目切换不执行远程核对,立即选择本地项目配置。Workspace 或 Runtime 首次实际使用时
才通过当前 Agent 连接核对所需能力;失败只影响该操作。新建或显式保存项目仍执行完整
准备,并只事务写入 Host、路径、Runtime 选择和工作模式。
- SSH 连接先尝试 attach;Agent 不存在或未运行时执行幂等 bootstrap,然后重新 attach。
- Agent 是按需启动的 detached process,不注册开机服务,不依赖 systemd、D-Bus 或 Linger。
- 每个模型桥 helper 都为 loopback HTTP 入口生成一次性随机路径 capability;只有写入当前 OpenCode 子进程配置的 URL 可以访问该入口,其他本机用户即使发现临时端口也不能提交模型请求。
- Agent 监听当前用户拥有的私有 Unix socket。SSH 中断只关闭 relay,不拥有 Agent 和活动 Runtime 的生命周期。
- SSH Host 设置通过固定探针和有界 SFTP 显示 Host 已登记的 Agent/OpenCode 版本,并与当前
GoodBuddy 所需版本比较。进入或切回该设置页只读取本地 Host 列表,不连接任何 Host;
用户点击“刷新版本”后才探测对应 Host,探测不安装或切换远端组件。项目切换与此按钮
解耦且不建立连接;实际远程操作才建立或复用 SSH/Agent 连接。
- “平台功能 > 远程项目”中的本地 Agent 包清单与 Host 状态分离。Main 对用户数据目录中
Linux x64/arm64 `.gbagent` 分别执行外层可信签名、桌面/协议兼容性、内部 Agent/Runtime
签名、架构与完整 payload 校验;首次打开和手动刷新还会读取“关于与更新”所选来源的
小型签名目录。Renderer 只得到本地/在线版本、是否有更新、架构、远端 Runtime 版本、
协议和本地状态,不得到缓存路径、key ID 或 digest。目录检查不下载 `.gbagent`,在线
包下载只由用户点击带目标版本的操作触发;离线导入/导出通过 Main 管理的文件对话框完成。
- “自动”只在 operation/prepare 前用 Host capability probe 选择 acquisition:直连明确
可用才选 Host 下载,否则选 GoodBuddy 传输;显式选择不被改写。prepare、commit 或
adoption 失败后都不跨 acquisition 自动 fallback。
- Host 下载由桌面控制面通过固定 SSH prepare channel 和结构化 stdin 发送候选信息,
Host 取得 compound `.gbagent` 并核对大小与 SHA-256。显式 GoodBuddy 传输在在线目录
不可用,或本地已验证包版本不低于在线候选时优先租用本地包,包括更高版本离线导入;
线上存在更新且可用时仍取得线上新包。需要下载时,同一次操作会验 SHA-256 与签名、
缓存并取得 lease,再以有界流式 SFTP 上传一个 compound archive 和归档中已验证的
bootstrap Node;约 294 MiB 完整包不会一次读入 Main `Buffer`,Host 仍再次校验完整包。
- 两种 acquisition 交付到固定 operation staging 后,完全共用 control-plane
`prepare → commit → Agent activate/health → Runtime activate → finalize → cleanup`。
Main 只保存清理 operation staging 所需的 operation ID。中断或 adoption 失败后,下次
更新尽力清理旧暂存并重新 prepare,不保存或恢复 Agent/Runtime metadata 副本。确认
adoption 后才显式 cleanup;cleanup 失败不回滚健康环境,也不阻塞下一次 fresh prepare。
Host 在 prepare 解包时
完成一次完整 payload 校验,commit 和后续注册检查不再遍历包体。首次 bootstrap 不依赖
既有 Agent daemon,也不要求 format v1 归档携带 `agent/lib/package-installer.cjs`。
- 即使版本号均为当前版本,Host 卡片仍提供同版本“重新安装”,用于修复 registry、签名
或安装 identity 异常。GoodBuddy-owned 同 digest 目录损坏时先隔离后替换,并在发布或
原子 commit 失败时恢复;commit 成功后的激活失败保留新发布内容,下次点击重新安装时
清理旧暂存并重新 prepare/adopt。不要求删除 Host、凭据或引用项目。
- Host 卡片的“版本匹配”badge 只表达版本事实,不代表 Agent 正在运行或环境健康。重新安装
失败时明确显示本次操作未完成,并以随后重新检查的版本卡片表达当前事实;提交结果
不确定时不声称旧版本未被替换。
- 更新成功后才定向清理该 Host 的 Agent 连接和 Runtime 选择缓存。引用项目再次打开时
解析新的 current registry 并建立当前连接,无需刷新项目记录;当前环境无效时要求用户
显式修复,但不在项目切换中下载或安装。失败或取消保留 Host 配置、凭据、项目、
Workspace 和旧组件。
- 显式 stop、升级、身份冲突或进程退出时清理 GoodBuddy 自己的 socket、状态和子进程;不得删除或覆盖无关 Host 文件。
### Host 级环境生命周期
- Agent、Node 和 Runtime 是 Host/SSH 账号级共享环境。新增或重新验证 Host 后先保存 Host
并只读探测,不自动安装;用户随后在 Host 卡片明确选择安装或更新方式,失败保留 Host
并允许重试。
- Host 可以按签名目录固定的 URL、大小和 SHA-256 直接从当前 GitHub/北京镜像来源下载
完整 `.gbagent`,也可使用 GoodBuddy 本机下载、流式 SFTP 传输和离线导入。自动模式只在
操作开始时择一;执行失败不会在同一次操作中切换 acquisition。
- 项目只保存 Host ID、远端路径、Runtime 选择和默认模式。创建或保存时验证 Host current
环境;打开或切换只读取本地配置。Workspace/Runtime 实际使用时从 Host current registry
和当前 Agent 连接取得 live identity,不再扫描、下载、上传或发布 Agent/Runtime。完整
事务、兼容边界和验收要求以
[Host 环境准备设计](./environment-provisioning-technical-design.md) 为准。
- 打开新建/项目设置弹窗只读取本地 Host 验证记录,不并发检查所有 Host;目录浏览或保存
才连接所选 Host。
- 项目选择器中的管理操作默认浮动隐藏,在悬停、键盘焦点或触屏环境显示。管理任意已保存
项目不先激活项目,因此 Host 不可达或远端目录已不存在时仍可删除其本地记录。
- “远程项目”在选择器中按已保存 SSH Host 轻量分组。Host 标题显示 Main 已持有的真实
Agent 连接状态(未连接、连接中、就绪或异常),读取和展示状态本身不连接 Host;
项目副标题只显示远端路径,不重复“托管 SSH”类型。
- 删除 Host 时确认框列出所有引用它的本地项目记录,包括归档项目。确认后只删除本机
Host、凭据、项目及其关联记录,不连接 Host、不删除远端目录或内容;Host 设置写入或
项目事务失败时恢复另一侧,避免只删掉其中一类本地记录。
## Workspace
Main 传入已规范化的绝对 POSIX root。Agent 返回 Workspace identity、访问模式、Git 状态和有界能力。
- 创建项目时,用户可以直接输入远端工作目录,或通过输入框右侧的文件夹按钮浏览并选择目录。
- 项目保存前的目录浏览使用 Main 管理的只读、有界 SFTP,只返回目录;每次请求重新解析并校验当前 Host revision 与 Host Key generation,不安装 Agent、不暴露任意 shell/SFTP 接口。
- 浏览从 SSH 账号 Home 或当前有效绝对路径开始,限制扫描次数、返回条目和总时限;取消、Host 变化、超时或连接失败时关闭 SFTP 并保留用户原先输入。
- 选择目录只更新项目草稿,仍需通过正常的 Agent、Workspace、Runtime 验证和项目保存事务才会持久化。
- Ask Workspace handle 不暴露写入方法。
- Execute Workspace handle 可读写,但这不是 Execute 的唯一权限面;Execute Runtime 本身使用 SSH 账号的正常权限。
- 文件预览、搜索、Git diff、目录项和显式文件传输保持字节、条目数和路径长度上限。
## Runtime
首个远程 Runtime 固定为签名 OpenCode ACP bundle。
托管 SSH 项目的 Composer Runtime 菜单只显示当前配置的 OpenCode 和管理入口,不显示
直连模型、Continue 或 DeepSeek Harness。激活旧远程会话时,若其保存了其他 Runtime
selection,Renderer 会恢复为当前 OpenCode 配置;Main 的远程请求校验仍是最终边界。
本机配置的 SKILL 包和 stdio MCP Server 不上传或分配给远程 Runtime;远程 ACP Session
继续使用空 MCP Server 列表。Node.js、Python 等本机工具环境同样不修改或同步到 Host,
完整边界见[本机工具环境](../local-tool-environment/README.md)。
### Ask
Ask 使用系统 `bwrap`:
- Workspace 和 Runtime bundle 只读 bind;
- 独立可写 scratch HOME、TMPDIR 和 XDG 目录;
- 固定环境名和固定 Runtime argv;
- 通过只读边界阻止 Workspace 修改。
- ACP 权限请求只有在工具种类为原生 `read` 且 Runtime 提供 `allow_once` 选项时才允许;
search、edit、execute、未知工具以及只提供持久授权的请求全部拒绝。
`allow_once` 只解决 OpenCode 发起原生读取前的 ACP 协商,真正的文件系统边界仍是只读
`bwrap` bind。Host 缺少 `bwrap` 时 Ask 不可用。
### Execute
Execute 不经过 Ask 的 bubblewrap profile:
- 直接启动已签名 Runtime entrypoint;
- 使用 Agent 本地模型 gateway 时,签名 Agent launcher 会 `exec` 候选 manifest 锁定的
Node Runtime;进程 owner 校验最终 Node executable,而不是已经被替换的 shell
launcher 路径;
- `cwd` 为项目 Workspace;
- 继承 SSH 账号的正常环境、文件系统、进程和网络能力;
- 不进行 T2/T3、confinement attestation、approval bridge 或逐工具批准。
两种模式都保留输入/输出字节上限、用户取消和进程组清理。生产 Prompt 不设置固定墙钟
总时限;只要 Runtime 尚未按自身协议结束,Main 和 Agent 就允许其持续运行。启动、握手、
单个控制 RPC、重连尝试和关闭清理仍使用独立的有界超时,测试可以显式注入短 Prompt
时限验证取消升级,但该覆盖值不是生产默认。无界 Prompt 继续复用已发布的必填
`deadlineAt` 字段,以 year-9999 哨兵表达;Main 与 Agent 必须同时理解该哨兵,Agent
`0.11.10` 及更早版本仍会按其 manifest 上限夹紧,因此桌面启用这一语义前必须配套更新
到 Agent `0.11.13` 或更高版本。
## ACP 与断线
- 管控面只负责有界 JSON-RPC:连接、查询、启动、取消、关闭和重连。数据面只负责
ACP/blob 数据与 Agent 本机 journal。二者共享同一个经过 Host Key 校验和认证的
SSH attach 字节流,但不共享生命周期:管控连接关闭不会隐式取消 Agent 已接受的
Runtime 动作。
- 帧只保留连接 identity、channel identity、单调 sequence、类型和长度。Main 与
Agent 都按调用顺序 FIFO 写入,输入队列达到固定字节或条目上限时暂停 SSH stream。
顺序、可靠传输、分包和网络背压全部交给 SSH;协议不实现 credit/window、优先级
调度、跨 channel 超车、批发送或第二套拥塞控制。
- Agent 在把 Main 输入交给 Runtime 前、以及把 Runtime 输出交给 Main 前,先把 ACP frame 写入本机 journal。ACK 只推进 cursor 并裁剪已确认 frame,不表示 channel 已终止。
- Main 持久化 binding identity 和单调 cursor;保留中的 connection lease 即使处于 offline/reconnecting,也可以先把新 cursor 落盘。重连提交不能覆盖 resume 过程中并发落盘的更新。
- Runtime 事件先经过单事件、单请求输出和每次 Prompt 最多 100 个不同工具调用的边界;
通过边界后,Renderer 和 Main 将已接受的工具详情保存到本地 SQLite,不再额外缩短其
输入、输出、错误或摘要。
- OpenCode 原生 Task 工具按 `subagent_type`、`description`、`prompt` 和稳定 tool call ID
解析为子 Agent 事件。本地 OpenCode SDK 与远端 ACP 增量工具事件复用同一转换;ACP
首帧缺少参数时可以先显示普通工具活动,后续参数确认其为 Task 后必须替换为子 Agent
状态卡,并持久化任务说明、终态与输出。
- 同一 detached Agent 存活时,短暂 SSH 断线依次执行 `controller/resume`、`runtime/resumeAcpChannel` 和 `runtime/replayAcpChannel`,从 Main 已确认的 cursor 后只重放 Agent 到 Main 的已记录输出。重复 frame 会被确认但不会再次交给上层。若上一代连接只留下没有活动请求的 detached binding,完成精确 controller takeover 后会先有界停止并核对遗留 Runtime process,再用新 channel epoch 重新打开同一 binding 并恢复已有 ACP session;其他 controller、未证明 takeover 或仍有活动请求的 binding 仍被拒绝。
- Main 到 Runtime 的 ACP 输入、模型请求、工具请求和 blob 不自动重放。
- 无法确认外部 Provider 是否已处理的模型调用保持结果未知,避免重复计费或重复副作用。
- 只有远端返回 identity 匹配且 `closed: true` 时,Main 才删除持久化 binding;传输失败或未确认 close 会保留 recovery identity。
- 终态 close 由 Agent 在一个事务中为两个方向记录 sequence high-water tombstone,删除剩余 frame 和 active channel,并归零对应 journal quota。tombstone 用于拒绝迟到的旧 epoch frame,因此当前不会按时间自动裁剪。
- 断线不主动终止正在运行的 Runtime。用户取消、显式关闭、输出边界或 Agent shutdown
才触发停止;连接恢复只受当前用户取消信号和单次重连控制超时约束,不把 Prompt 已运行
时长当作停止条件。
- Agent 失联期间继续把 Runtime 输出写入本机 journal;重新连接只校验 controller
identity、恢复 binding,并从 Main 最后确认的 cursor 后同步缺失数据。已接受的
指令和结果不确定的 Provider 请求都不自动重放。
### Agent-owned Prompt 与 Desktop 恢复
- 托管 SSH 的生产 Prompt 从一开始就由 Agent 持有 ACP `ClientSideConnection`、原始
Prompt Promise、Provider 轮次和 Runtime 进程。SSH relay 或 Desktop 进程不属于该
Promise 的生命周期,因此正常退出、强制结束 Desktop 或本机网络中断后,Host 仍能继续
后续模型/工具轮次。该路径要求 `runtime/acp` capability v4,即 Agent `0.11.14` 或更高。
- Agent 把 ACP session update、permission decision 和唯一 Prompt 终态写入有界语义
transcript。Main 不恢复旧 JSON-RPC Promise,也不重发 `session/prompt`;Fresh Desktop
只对同一 controller/binding/operation 执行 takeover、attach 和 ACK-exclusive page。
- 每个 Agent 语义 sequence 在 Main 映射为零到多个公开事件,最后追加内部 checkpoint。
Desktop 在同一 SQLite 事务中去重 provenance、归并原 assistant message,并在 checkpoint
时提交 Task 终态;事务成功后生成器才继续并向 Agent ACK。断电发生在同一 sequence
中间时会重读该 sequence,已写事件按 `(binding, operation, sequence, eventIndex)` 去重,
未写事件继续归并,不会重复 Provider 或工具执行。
- ACK 后 Agent 删除已确认事件,只保留小型操作终态与游标用于幂等核对;未 ACK 输出继续
保留。Main 按 transcript page 的最高连续 sequence ACK,终态仍立即 ACK。单事件、单页、
单 Prompt 总字节和事件数都有上限;Agent 始终为唯一终态预留一个事件和最大事件字节,
避免配额耗尽后无法记录终态或无法重启。已 ACK 终态操作和已交付模型 ledger 在 daemon
运行期间也按固定上限裁剪,不依赖重启。Agent 重启不能重建内存中的 Runtime/Prompt
Promise,因此启动时把遗留 nonterminal 记录变为 `outcome-unknown`,Desktop 只同步该
终态而不自动重放。
- prepare 已接受但 start RPC 未到达时,Agent 使用两分钟 prepare-to-start watchdog 清除
进程和 Prompt 凭据并记录 `outcome-unknown`,避免无界 Prompt 哨兵造成永久悬挂。
- Desktop SQLite 仍是永久可见项目/对话数据的权威;Agent transcript 只是尚未同步事件的
权威。启动后 Renderer 先订阅恢复进度,再触发每项目恢复。项目选择器依次显示网络、
Agent、Runtime、cursor、完成或失败/重试;只阻塞受影响项目的发送和队列,已有历史与
其他项目保持可用。
- 正常应用退出对已接受托管 SSH 请求执行 detach,不等待远端完成;本地请求和尚未接受的
请求仍按原行为取消。用户显式“停止”始终发送稳定 operation cancellation,并等待 Agent
语义终态。已进入 Agent-owned `run` 但 start/attach 响应仍不确定的请求继续保留为
`interrupted + remote_recoverable`,并自动走 exact attach;不会因为尚未收到第一个公开
事件而丢失恢复身份。
## Agent 本地模型 gateway
- Main 在 `runtime/preparePrompt` 中发送当前解析后的有界 profile;Agent 只在该 operation
的内存状态中持有 Provider URL、API Key 和认证信息。OpenCode 仍通过 Prompt 私有 Unix
socket 发出模型请求,但 HTTP Provider dispatch、响应交付和稳定 call ledger 全部位于
Agent,因此 Desktop 断电后可以继续下一轮。
- Agent 校验固定 model、协议和 API path,拒绝 background/store、provider web search、
provider MCP、持久 conversation、非文本 modality 和其他可独立计费能力;请求/响应 body
各限制 768 KiB,响应同时核对声明长度与短读。每个
`(binding, operation, roundIndex)` 只 dispatch 一次;已 dispatch 但未证明完整交付的调用
变为 `outcome-unknown`,不会自动重试。
- Prompt 最多 100 个模型调用,并受单次与累计输出 Token 上限保护;这些是资源边界,不是
Prompt 墙钟总时限。Provider 单次请求仍有独立超时。实际 usage 在语义 transcript 中同步
回 Desktop 的 usage/task 数据。Agent 在所有进程终止路径统一清除 Prompt token 计数。
- helper 可以接收同一 Prompt 内并发到达的模型桥请求;它在单一稳定模型桥上按到达
顺序等待并交付,不返回本地 `bridge-busy`,每个响应只有在 HTTP 完整 flush 后才
发送 delivery ACK。
- GoodBuddy 自己管理会话标题,因此传给 OpenCode 的配置禁用 title Agent;一次用户
Prompt 不会额外触发标题模型请求。
- Agent gateway 不再把 Provider 请求往返 Desktop;legacy blob bridge 只保留给旧的
非 Agent-owned 测试/兼容路径。凭据不进入 Renderer、SSH 命令参数或远端环境。
- Unix socket 接收端按 `readableLength` 中已经缓冲的字节增量排空一个声明长度的帧,
不会在部分大响应到达时反复请求尚未缓冲的完整剩余长度;短读、连接结束、错误和取消
都会使当前交换失败。原生 Linux 回归覆盖至少 256 KiB 的 broker 响应。
- Renderer 把相邻 text/reasoning delta 合并为消息 block 时始终替换最后一个 block,
不修改既有 React state 对象。这样开发环境 StrictMode 重复调用 state updater 时,
block metadata 与 canonical 消息正文保持一致。
## Agent 开发期间的真实 Host 验证
任何会改变已部署 GoodBuddy Agent 或桌面到 Agent 生产链路的源码改动,都必须在开发期间
使用共享 Linux x64 真实测试 Host 验证。适用范围包括 Agent daemon、Agent protocol
及共享 contract、Agent 组包与安装、attach/update、Runtime 启动与进程归属、Workspace、
model bridge,以及生命周期和恢复逻辑。单元测试、mock、fixture、CI 或正式发布前回归
都不能替代这一步;不得等到准备发布时才首次运行受影响的真实链路。
测试 Host 有两个网络入口:
- `192.168.0.23`:局域网入口,在本地网络可达时优先使用;
- `10.7.0.23`:VPN 入口,局域网入口不可达时使用。
两者指向同一台物理 Host,只代表两条访问路由,不能计为两个独立 Host、两种架构或两份
覆盖结果。测试只使用已固定的 Host identity 和 GoodBuddy 已有凭据存储;地址不得写入
产品行为或默认配置,用户名、密码、私钥等凭据不得写入源码、文档、命令、日志或测试输出。
开发时应在改动链路达到可运行状态后尽早执行真实 Host 验证,并在最终相关改动后重复受
影响的场景。必须使用当前源码构建或当前候选包,不能只验证 Host 上已经发布的旧 Agent。
按改动范围至少选择以下场景:
1. Agent 组包、安装或升级:验证当前候选的 attach-or-bootstrap、安装/更新、版本切换及
随后的连接。
2. Runtime profile、进程启动/归属或 Ask/Execute 边界:通过桌面生产入口启动真实 Runtime,
验证受影响模式;Ask 权限改动必须验证只读边界,Execute 权限改动必须验证所需写入、
进程和网络能力。
3. Agent protocol、RPC、stream、model bridge 或模型消息链路:执行最小有界真实模型调用,
禁用非必要工具和附件,并记录实际调用次数;大消息或工具链路改动还应覆盖对应的真实
payload/工具场景。
4. 会话、owner、取消、超时、断线或恢复逻辑:执行与改动直接相关的中断、取消、Agent/
Runtime 重启、SSH 断开或重连序列,并确认不会错误重放结果未知的操作。
5. Workspace 或 Host 文件路径:只在 GoodBuddy 专用测试目录操作,验证真实路径和权限,
不得读取、覆盖或删除无关 Host 文件。
停止或修改远端进程前必须先核对其启动身份、PID/进程组和 GoodBuddy 归属。若两个入口均
不可达,应立即把真实 Host 验证报告为开发阻塞,保持该验证未完成,不能把 Agent 改动表述
为已完成,也不能把首次真实验证推迟到发布阶段。
## 已完成的 E2E 验收记录
- 2026-08 的本地 fixture 完整验证 Linux x64 Agent `0.11.2-e2e.12`、Node `24.19.0`
和 Agent protocol `2.0`;当时没有 arm64 fixture,因此该记录不能作为当前独立发布
的双架构验收。Agent `0.11.10` 后续已由原生 workflow 发布并公开验证双架构工件;
当前源码 lock 固定为 `0.11.14`;正式发布状态以独立 Agent Release 与签名 catalog 为准。
- 2026-08-30 在共享 Linux x64 Host 的隔离测试 HOME 中验证当前 `0.11.13` 源码候选:
签名 Runtime 清单的测试墙钟上限为 1 秒,模型桥首轮故意延迟 2.515 秒后 Prompt 仍在
5.770 秒正常完成;随后 OpenCode 原生 Task 依次产生 `running`、`completed` 子 Agent
事件并完成父请求。测试结束后隔离 Agent、Runtime、socket 和 Workspace 已全部清理,
Host current Agent 仍为 `0.11.10`。
- 2026-08-31 使用隔离 Linux x64 Agent `0.11.14` 候选完成 remote-authoritative 矩阵:
15 秒短 smoke、2 分钟正常 detach、5 分钟本机 harness 强制结束、并发对话、SSH relay
丢失、显式取消、确定性 Provider 错误、2 秒 Provider 结果未知、Agent `SIGKILL`/重启,
以及 reopened Desktop SQLite 恢复。成功工具的 START/END sentinel 都各出现一次;
Provider 结果未知与 Agent 重启只提交 `outcome-unknown`,未重放 Prompt、模型调用或工具。
- 同一矩阵中的 Desktop schema v32 实际恢复保持原 project/conversation/request/
assistant-message 身份,在同一事务中提交语义 provenance、消息与 cursor;重新打开
`assistant.sqlite` 后消息和 Task 都完整,重复 provenance 返回 false,recoverable Task
归零。
- 最终源码复核后重新构建测试签名 package:
SHA-256 `39307fd1dccd638dcb723027591bcd83deab7057d550c99eff6f037e339829f4`,
installation `agent-2cad05acfebe853394807e3a11f7befcd9f47215c2cf3a33a03d5f644b45716e`,
Runtime `1.18.9` / bundle
`sha256:6c7fc975415ef2547d3729bc4772d3e78362c2e76c05d1fc24968a0468fcb7ba`。
最终 15 秒 detach/recovery 使用两个模型轮次、一个工具和一个终态,ledger 两轮均
`completed + delivered`,语义 `latest=ACK=41`,保留事件、Runtime owner、ACP channel/
frame 都为 0,START/END 各一次。最终取消另使用一个模型轮次、一个工具和一个 cancelled
终态,语义 `latest=ACK=29`,清理计数同样为 0。最终补充验证共 3 次真实文本模型调用。
采证后已按 PID/starttime/executable 停止精确隔离 daemon 并删除隔离 Host 测试根目录;
`/root/.goodbuddy` 下的共享 Agent 安装和进程未被修改。
- 2026-08-31 使用当前工作树短期测试签名 Linux x64 候选,在全新隔离 HOME 和 installation
中验证有界诊断:bootstrap、detached daemon、attach、relay 断开与同 controller 重连、
`controller/resume`、Runtime candidate 激活和 OpenCode ACP 进程启动均通过;新
`diagnostics --installation-id` 命令在 daemon 重启后读取到 23 条完整 JSONL。诊断目录
为 `0700`、文件为 `0600`,单文件 3719 bytes,未超过 64 KiB。六类测试哨兵覆盖 Prompt、
密钥、环境、SSH 参数、用户文件内容和原始错误消息,CLI 输出与磁盘均未出现哨兵。把隔离
diagnostics 目录临时设为不可写后,Attach 和 `agent/status` 仍正常;随后已恢复权限。
本次未注入模型凭据或执行 Prompt,真实文本模型调用 0 次;三文件轮转继续由聚焦测试覆盖。
测试结束前按 PID、UID、starttime、executable 和唯一 installation 核对并停止所有本次
daemon,本地及 Host 临时目录均已清理,共享 current Agent 和 `/root/.goodbuddy` 未修改。
- 正常 Host 更新路径把 Linux x64 Host 的 Agent 更新为 `0.11.2-e2e.12`,并确认
OpenCode Runtime 已安装版本与所需版本均为 `1.18.9`。
- 一条新的 Ask 用户操作只提交一次。OpenCode 先在 build 模型轮次请求一个原生
`read`,读取专用测试 Workspace 中的证据文件,再在第二个 build 模型轮次生成最终
回答;没有 title 模型轮次,也没有第二个工具调用。
- 两个模型轮次都记录已交付,远端 helper 保持休眠且 Unix socket 没有未读响应积压。
持久化的 canonical 助手正文精确为 `GOODBUDDY_REMOTE_TOOL_4C6B9D8A`。验收操作没有
重放任何结果未知或已经完成的 Provider 请求。
## 项目保存
远程项目只持久化稳定配置:
- Host ID;
- 规范远端工作目录;
- Runtime selection;
- `ask | execute` 默认模式。
Agent installation、Host revision/Host Key generation、Workspace identity、Runtime
bundle/adapter digest 和 capability generation 都属于 live connection/lease,不进入项目
domain object 或 SQLite。打开托管 SSH 项目不接受 Renderer 回传的旧项目草稿,而是从 Host
store 解析当前连接目标,通过 Agent/Runtime 安装管理器的 current/activate 读取路径取得
registry identity,再用同一个 Agent 连接验证 Workspace 路径和 Runtime capability。该流程
不会扫描完整 payload、取得安装包或发布组件。
实时 Runtime 创建只接受 OpenCode,使用当前解析后的模型 profile 准备 Agent 本地 gateway,
从当前 Agent 连接和 Runtime registry 取得会话 identity,并在该连接上打开 Workspace 和
ACP channel。Host 编辑或环境更新会定向失效 Agent 连接与 Runtime 缓存,下一次请求自然
重新取得 current 环境;无需修改项目。Ask/Execute 权限继续由每次 Prompt 的 ACP Runtime
边界执行。
Renderer 在用户主动打开已有托管 SSH 项目时立即切换,不显示远程激活状态。Host、Agent、
Workspace、Runtime 和 Saving 进度仅用于新建或显式保存项目;进行中可显式取消并禁用会
产生冲突的项目创建和设置入口。远端 RPC 拒绝只向
Renderer 暴露固定方法名、数字 RPC code 和有界 service code,不转发 Host 私有错误详情。
`runtime/preparePrompt` 在远端明确拒绝且尚未接受 Prompt 时会关闭对应 Main binding,
不会把确定性失败错误保留为 `outcome-unknown`。未确认终态的 Prompt 仍不会自动重放;
同一当前 Agent identity 上的 `prompt-running` binding 会阻止不同的新 operation,直到
原 operation 恢复或取消,避免遗留 Host 工作与新 Prompt 并行。只有 Host/Agent/Runtime
identity 已变化且旧 authority 无法恢复,或原 binding 已有 `outcome-unknown` 终态时,显式
新 Prompt 才替换旧 binding。新 ACP session 的首次 Prompt 会把 Desktop 已持久化的有界
会话历史作为不可信数据一并发送;成功加载或恢复原 ACP session 时不会重复注入历史。
Ask 模式的 Linux Runtime 进程由 `bwrap` 启动后 `exec` 到已签名 Runtime 或 Agent
model bridge helper;监督器只接受同一 PID/进程组从固定 `bwrap` 路径到预期最终可执行文件
的有界转换,再登记为运行中进程。
不持久化 T2/T3、consent、approval bridge、confinement 或组件验证结果。数据库 schema
v32 保留项目及关联用户数据,并加入 remote-recoverable Task/message/provenance/cursor;
执行空间表保持 `project_id/kind/root_path/ssh_host_id`
四列,并删除旧 Runtime 验证表。
## Agent 有界诊断
Detached Agent 不依赖 stdio 留存故障证据。每个 installation 在固定私有目录
`~/.goodbuddy/state/<installationId>/diagnostics/` 中维护
`agent-diagnostics.jsonl`、`.1` 和 `.2`,每个文件不超过 64 KiB,目录权限为 `0700`,
文件权限为 `0600`。记录覆盖 daemon 启停、detached 启动、连接、恢复和 Runtime
启动/退出;每条只包含固定事件、时间、PID、可选工作模式、固定原因和白名单错误码。
诊断写入使用最多 64 条的有界异步队列。连接、恢复和 Runtime 回调只把已经归一化的固定
记录入队,不等待磁盘;原始 `Error` 不进入队列。队列满、目录不可写或轮转失败只丢弃诊断,
不得改变 Attach、协议、Runtime 或退出结果。daemon 停止和 bootstrap 生命周期结束时执行
有界 flush。多个 Agent 进程通过 GoodBuddy 私有短期锁协调 append 和轮转,不把长期锁作为
运行前提。
已安装 Agent 提供只读固定命令:
```text
goodbuddy-agent diagnostics --installation-id <installationId>
```
该命令只能从 installation 对应的固定 state 目录读取上述有界文件,并逐行输出已验证 JSONL;
不接受任意路径,也不扩展 Agent protocol。诊断不得包含 accepted Prompt、模型输入输出、
模型密钥、完整配置、环境变量、SSH 参数、用户文件内容、路径或原始错误消息。应用内反馈的
“附加最近桌面诊断记录”只读取 Desktop 诊断,不会自动取得或上传这些远端 Agent 记录。
## 资源与发布
- 所有桌面构建(包括 `portable` 与 `release:package`)只携带
`agent-runtime-lock.json`、`remote-runtime-lock.json` 与公开的
`agent-release-keys.json`,不嵌入 `.agent-resources`、
`.remote-runtime-resources` 或任何可安装 Linux 远端 payload。
- `build/agent-package.cjs` 在对应原生 Linux 架构组装确定性 `.gbagent`,外层签名覆盖
描述符和每个内部文件身份;内部 Agent 与 Runtime 仍分别使用既有签名 manifest 和 lock
校验,但所有 production 层统一使用一组 GoodBuddy 通用发布身份并通过签名域区分用途,
不要求内部 Runtime 单独配置密钥。`build/agent-catalog.cjs` 为双架构包生成签名累计目录,
拒绝同一版本/架构改变字节。
- `.github/workflows/agents.yml` 只使用进程内临时测试 key 做分支/PR 原生验证,不发布。
`.github/workflows/agent-release.yml` 才可在 annotated `agent-v<version>` 标签和受保护
`agent-signing` Environment 中构建 production 包。Agent GitHub Release 必须
`--latest=false`;北京 OSS 先写 `agent-releases/v<version>/`,GitHub 公开后才切换
单一 `agent-releases/latest.json` 指针,使客户端始终从同一不可变版本目录读取目录与签名。
- Main 的本地缓存只发布完整验证成功的内容。在线目录和包都绑定固定 GitHub/OSS URL、
大小和 SHA-256;选取规则是桌面最低版本满足、Agent protocol major 相同且 minor 不高于
当前支持值的最高 SemVer。缺少某架构仅使该架构 Host 的托管 SSH 无法激活。
- `agent-release-keys.json` 读取时按 UTF-8 JSON 解析并严格验证 schema、唯一 key ID、
规范 Base64、有效 Ed25519 key、环境和撤销列表,不把 CRLF/LF 或普通 JSON 空白作为
信任条件。上传到 Host 前重新序列化为确定性 LF JSON。签名目录、描述符、manifest 和
payload 仍按原始签名字节及 SHA-256 验证。
## 发布前完整回归(不能替代开发验证)
以下清单只用于复核开发期间已经在真实 Host 上通过的链路,不得作为 Agent 改动的首次
真实 Host 验证:
1. `npm test`
2. `npm run typecheck`
3. `npm run lint`
4. `npm run build`
5. 用测试签名复合包验证在线下载源选择和离线导入/导出;正式 Agent 候选还需公开校验
双架构 production 包与签名目录。
6. 在已固定 Host Key 的 Linux x64 测试 Host 上验证 attach-or-bootstrap。
7. 在 GoodBuddy 专用测试目录验证 Ask 无法修改文件。
8. 验证 Execute 可以写文件、启动进程和访问网络,同时不触碰无关 Host 文件。
9. 运行一次有界的真实模型调用,确认凭据不进入 Renderer、SSH 参数、远端环境或磁盘,
只在当前 accepted operation 生命周期内进入 Agent 内存。
10. 中断并恢复 SSH,确认活动 Runtime 不被网络抖动终止。