RFC:远端 Workspace Git 后端
本文档作为协议规范保留——讲此协议的客户端与服务端应当严格匹配下方的 线协议形态。后续修订应作为独立的 amendment 文档发布,而非原地修改这 份 RFC,以便"实际 ship 出去的是什么"这条历史线索可审计。
本 RFC 提出一套协议和 Rust 客户端,让 a3s-code 的内置 git 工具能够
跑在非本地文件系统的 workspace 之上——首先服务 S3,将来覆盖容器 / DFS
等无法承载 .git 目录的后端。
1. 动机
Workspace 抽象层已经通过 WorkspaceFileSystem 把内置工具与具体文件系统
解耦。对 bash、grep、glob、git 这四个工具,我们使用能力门控:
后端无法提供的能力对应的工具就不会被注册,模型根本看不到它。
这对 bash 是合理的(对象存储确实没法跑 shell),对 grep 是可接受的
(我们后来加了 LIST + GET + regex 的降级路径)。但对 git 是痛点:
很多 a3s-code 工作流期望查询分支/提交状态、diff 工作区、创建分支、stash
变更。如果只对本地会话开放 git,云端 workspace 就成了二等公民。
本方案是引入一个远端 WorkspaceGit 后端,把这些操作通过网络委托
给宿主运维的外部服务。该服务持有真实的工作区(或基于 libgit2 的实现),
对外暴露一小套 HTTP API。a3s-code 客户端讲这套 API,对工具层来说它
就是另一个 WorkspaceGit provider。
2. 非目标
- 托管 gitserver。 本 RFC 只定义客户端与协议,服务端的实现 (libgit2 封装、shell 外调、Gitea API 适配等)超出范围。
- 替换本地 git。 当
WorkspaceFs是LocalWorkspaceBackend时, 现有的LocalWorkspaceBackend的WorkspaceGit实现仍是默认。 宿主按会话自由组合。 - push / pull。
WorkspaceGittrait 对远程是只读的 (list_remotes返回已配置的远程;不暴露git push/git fetch)。 如果有工具需要,未来 RFC 再加。 - Worktrees。 Worktree 是本地文件系统概念;远端后端不实现
WorkspaceGitWorktreeProvider。详见 §8。
3. 协议选择 — HTTP/JSON
结论: HTTP/JSON,不用 gRPC。
操作面 ~12 个 RPC、请求响应都是扁平结构。流式只对 log 和 diff 有
点用,而这两个客户端已经有上限。HTTP/JSON 在 mock 与运维调试上的优势
明显压过 gRPC 的 schema 优势。如果操作面剧增或流式变成关键路径,
再回头考虑 gRPC。
我们对所有操作都用 POST:git 操作本质是命令式而非资源 CRUD,
请求体让加字段(兼容方式)变得轻松——GET 强迫所有信息塞 query string。
4. 仓库标识
客户端知道自己在操作哪个仓库,服务端需要路由。仓库标识写在 URL 路径 里:
repo_id 是宿主与 gitserver 运维方协商的、不透明的、URL 安全字符串。
典型值有:
users/{user_id}/sessions/{session_id}(与 S3 workspace prefix 1:1)- UUID
- 服务端的工作区路径
客户端把 repo_id 当不透明字符串;服务端负责映射到实际的工作区/裸仓。
5. 端点参考
全部 POST,全部 application/json 收发。字段命名 snake_case,匹配
Rust serde 默认值。
5.1 Status — WorkspaceGit::status
5.2 Log — WorkspaceGit::log
5.3 List Branches — WorkspaceGit::list_branches
5.4 Create Branch — WorkspaceGit::create_branch
5.5 Checkout — WorkspaceGit::checkout
5.6 Diff — WorkspaceGit::diff
truncated 为 true 表示服务端截断了 body — 见 §9。客户端会在
git diff 工具结果里把这个信号透出去。
5.7 List Remotes — WorkspaceGit::list_remotes
5.8 Is Repository — WorkspaceGit::is_repository
与"仓库不存在"(404)有别:服务端可能允许 repo_id 映射到非 git
目录。is_repository 让客户端不依赖 404 就能探测意图。
5.9 List Stashes — WorkspaceGitStashProvider::list_stashes
5.10 Stash — WorkspaceGitStashProvider::stash
6. 认证
客户端支持两种传输认证模式,按会话配置:
- Bearer token(默认)。
Authorization: Bearer <token>。Token 下发是宿主的责任(例如同一身份层签发的短期 JWT,复用 S3 访问的 门禁)。 - mTLS。 在 backend config 上设置
client_cert_pem和client_key_pem两个路径。客户端在构造时读两个文件,拼接后交给reqwest::Identity::from_pem。rustls-tls后端要求密钥是 PKCS#8 PEM 格式。只设一边会在构造期 fail-closed 报错。
两者可以同时启用——深度防御的部署直接两个都设。
无认证模式(本地开发场景)通过把 token 设为空、不设 mTLS 来开启;
客户端在构造时会 tracing::warn! 一条以让这个状态可见。
7. 错误模型
HTTP 状态码是传输信号。错误类别在 JSON body 里:
客户端映射:
客户端为可恢复 conflict 引入一个类型化错误:
希望从 BRANCH_EXISTS / WORKING_TREE_DIRTY / NOTHING_TO_STASH
恢复的工具用 anyhow::Error::downcast_ref::<RemoteGitConflict>()
取出来——这与 edit / patch 在 S3 CAS 路径上用 WorkspaceVersionConflict
的模式相同。
协议正式定义的错误码(可扩展):
8. 可选 Trait
WorkspaceGit 完整实现。
WorkspaceGitStashProvider 实现。
WorkspaceGitWorktreeProvider 故意不实现。Worktree 是本地文件系统
概念,对远端服务映射不干净:
- "在路径 X 创建一个 worktree"——客户端没有路径概念;服务端的路径 布局对客户端是不透明的。
- 工具里用 worktree 隔离的工作流(多个 agent 并行跑在隔离副本上),
在云端的更优形式是多个会话,每个会话有自己的
repo_id,而不是 在客户端模拟一个本地文件系统特性。
依赖 WorkspaceGitWorktreeProvider 的工具会从 services.git_worktree()
拿到 None,并报"worktrees unavailable on remote git workspaces"。
9. 大小与成本上限
沿用 S3 后端的设防方式,远端 git 客户端强制几个客户端侧上限,避免 模型触发无界响应:
期望服务端也尊重这些上限——客户端会把相关上限传到请求里(例如
max_log_entries),并把服务端返回的 diff.truncated 透出去,让
工具能提示"diff 太大被截断,请缩小目标"。
10. Rust 客户端设计
组合工厂沿用 S3 的形式:
或者作为 S3 + 远端 git 工作区的顶层便利方法:
接线沿用现有的 builder 模式:
之后能力门控会自动注册 git 工具。
11. 每调用可观测性
每次 HTTP 调用发一个 tracing::debug! 事件,字段形态与
S3WorkspaceBackend::emit_s3_call_event 一致(见
crates/code/core/src/workspace/s3.rs):
已经 meter S3 成本的宿主可以把同一个 subscriber 接上来 meter gitserver 成本——不引入新依赖,也不要新接口。
12. 组合示例
S3 workspace + 远端 git
该会话注册的工具:read、write、edit、patch、ls、grep、glob、
git。(bash 仍然隐藏——对象存储跑不了 shell。)
本地文件系统 + 远端 git(混合)
CI 跑在本地 checkout 上,但宿主想把 git 操作路由进沙箱化服务 (例如做审计或限流)时有用。
13. 未决问题
这些应在 Phase 4.2(实现)开始前敲定。
-
Diff 方言。 本地后端返回原始
git diff的 stdout。远端 API 是否 应该强制具体 diff 方言(POSIXdiff -u?libgit2 的变体?),还是 透传服务端产出?建议:透传,文档要求服务端必须生成 unified diff。 -
并发操作。 两个客户端同时访问同一
repo_id(一个跑checkout、 一个跑diff),服务端是否要串行?建议:服务端按repo_id做串行;写进文档;客户端不在冲突上重试。 -
长任务。 大树上的
checkout可能超过request_timeout。协议是 否要支持轮询 / 异步 job 模式?建议:先不做。设合理的超时;我们 面向的(每会话沙箱)工作区不大。 -
Hooks。 服务端可能装了 pre-commit / pre-push hooks。响应里要 不要给一个"hook 输出"通道?建议:当非空时把 hook 的 stderr 塞进
checkout/stash的响应;hook 失败映射到 HTTP 422 +HOOK_FAILED。 -
schema 版本化。 第一版端点放
/v1/。什么时候升/v2/? 建议:只在请求/响应 shape 出现不兼容变更时;新增字段保留在/v1/(客户端要忽略未知字段)。 -
参考实现。 是否要在本仓库放一个最小参考 gitserver (比如 libgit2 + Rust)?建议:仓库外。客户端 + 协议足够;参考 服务归运维方。
14. 范围外(未来 RFC)
- push / fetch 到上游。 让"agent 改完代码 push 回去"的工作流成立, 涉及凭证下发。
- 稀疏 / 局部 checkout。 大型 monorepo 必要;当前面假定整个仓库 全量落地到服务端。
- 流式
log/diff。 当响应稳定超出max_diff_bytes时必要, 会让 gRPC 的判断重新被讨论。 - Hooks 管理。 通过客户端列举 / 配置服务端 hook。
15. 实现笔记(v2.6.x 已发布)
最初草案给的是 8 步实施提纲;实际发布出去的(Phase 4.2 + 后续 Phase 5.x)如下:
RemoteGitBackend/RemoteGitBackendConfig/RemoteGitConflict位于crates/code/core/src/workspace/remote_git.rs。最终没有加remote-gitcargo feature——reqwest已经在依赖里,模块无条件编译。WorkspaceGit与WorkspaceGitStashProvider完整实现;WorkspaceGitWorktreeProvider故意不实现(见 §8)。WorkspaceServices::with_remote_git是公开挂载点。内部 helperwith_git_provider(v2.6.x 一个后续提交里加上)用 struct literal 显式拷贝字段——确保WorkspaceServices未来加字段时装饰器不会静默丢失。- 除 bearer token 外支持 mTLS(
client_cert_pem+client_key_pem), 在 Phase 5.2 一次后续提交里 ship;同一个提交把上方 §6 也更新到了 已实现状态。 diff客户端侧防 OOM:HTTP body 流式累积,硬上限max_diff_bytes * 4(Phase 6.2)。softmax_diff_bytes显示截断 在 JSON decode 之后照旧生效。- 测试面:
remote_git.rs里 25+ 个 wiremock 单元测试 + 1 个端到端 驱动内置git工具的集成测试;workspace 通用 conformance suite 在 Phase 6.3 加入。 - README 与 CHANGELOG 已更新;TLS 后端选择与 AWS SDK 一致用 rustls-tls。
- SDK 暴露在 Phase 5.1 完成(Node + Python)——草案曾说"trait 稳定之 后再补",结果是 Phase 4.2 完成两个提交后就上了。