RFC:远端 Workspace Git 后端

字段取值
状态已实现 v2.6.x(crates/code/core/src/workspace/remote_git.rs)
归属crates/code workspace 子系统
关联S3 / 非本地 workspace 加固 Phase 4.1
相关S3WorkspaceBackend、WorkspaceGit* trait 族

本文档作为协议规范保留——讲此协议的客户端与服务端应当严格匹配下方的 线协议形态。后续修订应作为独立的 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。

Text
┌──────────────────┐
│ a3s-code │
模型 │ │
──────► │ git 工具 │ HTTP/JSON
│ │ │ ┌────────────────┐
│ ▼ │ │ │
│ WorkspaceGit ───┼──►│ gitserver │
│ (RemoteGit…) │ │ (libgit2 / sh) │
│ │ │ │
│ WorkspaceFs ────┼──►│ (S3, etc.) │
└──────────────────┘ └────────────────┘

2. 非目标

  • 托管 gitserver。 本 RFC 只定义客户端与协议,服务端的实现 (libgit2 封装、shell 外调、Gitea API 适配等)超出范围。
  • 替换本地 git。 当 WorkspaceFs 是 LocalWorkspaceBackend 时, 现有的 LocalWorkspaceBackend 的 WorkspaceGit 实现仍是默认。 宿主按会话自由组合。
  • push / pull。 WorkspaceGit trait 对远程是只读的 (list_remotes 返回已配置的远程;不暴露 git push / git fetch)。 如果有工具需要,未来 RFC 再加。
  • Worktrees。 Worktree 是本地文件系统概念;远端后端不实现 WorkspaceGitWorktreeProvider。详见 §8。

3. 协议选择 — HTTP/JSON

结论: HTTP/JSON,不用 gRPC。

评估维度HTTP/JSONgRPC
现有依赖reqwest 已在树中tonic + proto 工具链全是新增
schema 严格度手写 serde 类型.proto 强类型
测试 mock 难度wiremock 就够需要 gRPC mock 基础设施
运维可调试性curl 直接打grpcurl(普及度低)
流式chunked / SSE原生双向流
操作数量~12 个 op,全部请求/响应数量同上;流式罕用
服务端实现自由度直接包 git 或 gitea必须实现 gRPC server

操作面 ~12 个 RPC、请求响应都是扁平结构。流式只对 log 和 diff 有 点用,而这两个客户端已经有上限。HTTP/JSON 在 mock 与运维调试上的优势 明显压过 gRPC 的 schema 优势。如果操作面剧增或流式变成关键路径, 再回头考虑 gRPC。

我们对所有操作都用 POST:git 操作本质是命令式而非资源 CRUD, 请求体让加字段(兼容方式)变得轻松——GET 强迫所有信息塞 query string。

4. 仓库标识

客户端知道自己在操作哪个仓库,服务端需要路由。仓库标识写在 URL 路径 里:

POST /v1/repos/{repo_id}/git/<operation>

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

POST /v1/repos/{repo_id}/git/status
→ 200 {
    "branch":       "main",
    "commit":       "abc123...",
    "is_worktree":  false,
    "is_dirty":     true,
    "dirty_count":  3
  }
→ 404 {"error":{"code":"REPO_NOT_FOUND", ...}}

5.2 Log — WorkspaceGit::log

POST /v1/repos/{repo_id}/git/log
{"max_count": 10}
→ 200 {
    "commits": [
      {"id":"abc...", "message":"feat: ...", "author":"Alice <a@b>", "date":"2026-05-19T..."}
    ]
  }

5.3 List Branches — WorkspaceGit::list_branches

POST /v1/repos/{repo_id}/git/branches
→ 200 {"branches":[{"name":"main", "is_current":true}, ...]}

5.4 Create Branch — WorkspaceGit::create_branch

POST /v1/repos/{repo_id}/git/branches/create
{"name":"feat/x", "base":"main"}
→ 201 {}
→ 409 {"error":{"code":"BRANCH_EXISTS", ...}}
→ 404 {"error":{"code":"BASE_NOT_FOUND", ...}}

5.5 Checkout — WorkspaceGit::checkout

POST /v1/repos/{repo_id}/git/checkout
{"refspec":"feat/x", "force":false}
→ 200 {"stdout":"Switched to branch 'feat/x'"}
→ 409 {"error":{"code":"WORKING_TREE_DIRTY", ...}}

5.6 Diff — WorkspaceGit::diff

POST /v1/repos/{repo_id}/git/diff
{"target": null}             // null 表示工作区 vs index
{"target": "main"}            // 对指定 ref diff
→ 200 {"diff":"<unified diff text>", "truncated": false}

truncated 为 true 表示服务端截断了 body — 见 §9。客户端会在 git diff 工具结果里把这个信号透出去。

5.7 List Remotes — WorkspaceGit::list_remotes

POST /v1/repos/{repo_id}/git/remotes
→ 200 {"remotes":[{"name":"origin", "url":"git@github.com:...", "direction":"fetch"}]}

5.8 Is Repository — WorkspaceGit::is_repository

POST /v1/repos/{repo_id}/git/exists
→ 200 {"is_repository": true}

与"仓库不存在"(404)有别:服务端可能允许 repo_id 映射到非 git 目录。is_repository 让客户端不依赖 404 就能探测意图。

5.9 List Stashes — WorkspaceGitStashProvider::list_stashes

POST /v1/repos/{repo_id}/git/stashes
→ 200 {"stashes":[{"index":0, "message":"WIP on main: ..."}]}

5.10 Stash — WorkspaceGitStashProvider::stash

POST /v1/repos/{repo_id}/git/stashes/create
{"message":"wip", "include_untracked":true}
→ 201 {}
→ 409 {"error":{"code":"NOTHING_TO_STASH", ...}}

6. 认证

客户端支持两种传输认证模式,按会话配置:

  1. Bearer token(默认)。 Authorization: Bearer <token>。Token 下发是宿主的责任(例如同一身份层签发的短期 JWT,复用 S3 访问的 门禁)。
  2. 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 里:

JSON
{
"error": {
"code": "BRANCH_EXISTS",
"message": "branch 'feat/x' already exists"
}
}

客户端映射:

HTTP默认行为
200/201Ok(...)
400Err(anyhow!("bad request: {message}"))
401/403Err(anyhow!("auth failed: {message}"))
404Err(anyhow!("not found: {message}"))
409类型化 conflict — 见下
5xxErr(anyhow!("gitserver internal: {...}"))

客户端为可恢复 conflict 引入一个类型化错误:

Rust
#[derive(Debug, Clone, thiserror::Error)]
#[error("remote git conflict: {code}: {message}")]
pub struct RemoteGitConflict {
pub code: String,
pub message: String,
}

希望从 BRANCH_EXISTS / WORKING_TREE_DIRTY / NOTHING_TO_STASH 恢复的工具用 anyhow::Error::downcast_ref::<RemoteGitConflict>() 取出来——这与 edit / patch 在 S3 CAS 路径上用 WorkspaceVersionConflict 的模式相同。

协议正式定义的错误码(可扩展):

Code来源
REPO_NOT_FOUNDrepo_id 未注册
NOT_A_REPOSITORY路径存在但不是 git 仓库
BRANCH_EXISTScreate_branch 同名冲突
BRANCH_NOT_FOUNDcheckout / diff 目标缺失
BASE_NOT_FOUNDcreate_branch 的 base ref 缺失
WORKING_TREE_DIRTYcheckout 会丢工作区改动(且 force=false)
NOTHING_TO_STASH干净工作区上的 stash
RATE_LIMITED服务端限流(自定义阈值)

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_diff_bytes1 MiBdiff 响应 body
max_log_entries200log 的 max_count 上限
request_timeout30 s每次 HTTP 调用
operation_timeout (WS)60 s叠加在 WorkspaceServices 层

期望服务端也尊重这些上限——客户端会把相关上限传到请求里(例如 max_log_entries),并把服务端返回的 diff.truncated 透出去,让 工具能提示"diff 太大被截断,请缩小目标"。

10. Rust 客户端设计

Rust
// crates/code/core/src/workspace/remote_git.rs
#[derive(Debug, Clone)]
pub struct RemoteGitBackendConfig {
pub base_url: String, // https://git.example.invalid
pub repo_id: String, // path-segment 安全
pub bearer_token: Option<String>,
pub client_cert_pem: Option<PathBuf>, // mTLS
pub client_key_pem: Option<PathBuf>,
pub request_timeout: Option<Duration>, // 默认 30s
pub max_diff_bytes: Option<u64>, // 默认 1 MiB
pub max_log_entries: Option<usize>, // 默认 200
}
#[derive(Debug, Clone)]
pub struct RemoteGitBackend {
http: reqwest::Client,
base_url: String,
repo_id: String,
max_diff_bytes: u64,
max_log_entries: usize,
}
#[async_trait]
impl WorkspaceGit for RemoteGitBackend { /* 见 §5 */ }
#[async_trait]
impl WorkspaceGitStashProvider for RemoteGitBackend { /* 见 §5 */ }

组合工厂沿用 S3 的形式:

Rust
impl WorkspaceServices {
/// 在已有文件系统后端之上挂一个远端 git provider。
pub fn with_remote_git(
self: Arc<Self>,
cfg: RemoteGitBackendConfig,
) -> Arc<Self> { ... }
}

或者作为 S3 + 远端 git 工作区的顶层便利方法:

Rust
pub fn s3_with_remote_git(
s3: S3BackendConfig,
git: RemoteGitBackendConfig,
) -> Arc<WorkspaceServices> { ... }

接线沿用现有的 builder 模式:

Rust
let backend = Arc::new(RemoteGitBackend::new(cfg));
let git: Arc<dyn WorkspaceGit> = backend.clone();
let stash: Arc<dyn WorkspaceGitStashProvider> = backend;
WorkspaceServices::builder(workspace_ref, fs)
.file_system_ext(fs_ext) // S3 ETag CAS
.git(git)
.git_stash(stash)
// 不设 git_worktree — 见 §8
.operation_timeout(Duration::from_secs(60))
.build()

之后能力门控会自动注册 git 工具。

11. 每调用可观测性

每次 HTTP 调用发一个 tracing::debug! 事件,字段形态与 S3WorkspaceBackend::emit_s3_call_event 一致(见 crates/code/core/src/workspace/s3.rs):

字段示例
opgit.status、git.diff、...
repo_idsessions/example
outcomeok | error
statusHTTP 状态码
bytes响应 body 长度
duration_ms墙钟耗时

已经 meter S3 成本的宿主可以把同一个 subscriber 接上来 meter gitserver 成本——不引入新依赖,也不要新接口。

12. 组合示例

S3 workspace + 远端 git

Rust
let ws = WorkspaceServices::s3_with_remote_git(
S3BackendConfig::new("workspace", "sessions/example", access_key, secret_key)
.endpoint("https://s3.example.invalid")
.force_path_style(true)
.enable_search(true),
RemoteGitBackendConfig::new(
"https://git.example.invalid",
"sessions/example",
)
.bearer_token(token),
);
let session = agent
.session_builder("s3://workspace/sessions/example")
.options(SessionOptions::new().with_workspace_backend(ws))
.build()
.await?;

该会话注册的工具:read、write、edit、patch、ls、grep、glob、 git。(bash 仍然隐藏——对象存储跑不了 shell。)

本地文件系统 + 远端 git(混合)

CI 跑在本地 checkout 上,但宿主想把 git 操作路由进沙箱化服务 (例如做审计或限流)时有用。

Rust
let local = WorkspaceServices::local("/workspaces/repo");
let ws = local.with_remote_git(remote_cfg); // 覆盖本地的 git provider

13. 未决问题

这些应在 Phase 4.2(实现)开始前敲定。

  1. Diff 方言。 本地后端返回原始 git diff 的 stdout。远端 API 是否 应该强制具体 diff 方言(POSIX diff -u?libgit2 的变体?),还是 透传服务端产出?建议:透传,文档要求服务端必须生成 unified diff。

  2. 并发操作。 两个客户端同时访问同一 repo_id(一个跑 checkout、 一个跑 diff),服务端是否要串行?建议:服务端按 repo_id 做串行;写进文档;客户端不在冲突上重试。

  3. 长任务。 大树上的 checkout 可能超过 request_timeout。协议是 否要支持轮询 / 异步 job 模式?建议:先不做。设合理的超时;我们 面向的(每会话沙箱)工作区不大。

  4. Hooks。 服务端可能装了 pre-commit / pre-push hooks。响应里要 不要给一个"hook 输出"通道?建议:当非空时把 hook 的 stderr 塞进 checkout / stash 的响应;hook 失败映射到 HTTP 422 + HOOK_FAILED。

  5. schema 版本化。 第一版端点放 /v1/。什么时候升 /v2/? 建议:只在请求/响应 shape 出现不兼容变更时;新增字段保留在 /v1/(客户端要忽略未知字段)。

  6. 参考实现。 是否要在本仓库放一个最小参考 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)如下:

  1. RemoteGitBackend / RemoteGitBackendConfig / RemoteGitConflict 位于 crates/code/core/src/workspace/remote_git.rs。最终没有加 remote-git cargo feature——reqwest 已经在依赖里,模块无条件编译。
  2. WorkspaceGit 与 WorkspaceGitStashProvider 完整实现; WorkspaceGitWorktreeProvider 故意不实现(见 §8)。
  3. WorkspaceServices::with_remote_git 是公开挂载点。内部 helper with_git_provider(v2.6.x 一个后续提交里加上)用 struct literal 显式拷贝字段——确保 WorkspaceServices 未来加字段时装饰器不会静默丢失。
  4. 除 bearer token 外支持 mTLS(client_cert_pem + client_key_pem), 在 Phase 5.2 一次后续提交里 ship;同一个提交把上方 §6 也更新到了 已实现状态。
  5. diff 客户端侧防 OOM:HTTP body 流式累积,硬上限 max_diff_bytes * 4(Phase 6.2)。soft max_diff_bytes 显示截断 在 JSON decode 之后照旧生效。
  6. 测试面:remote_git.rs 里 25+ 个 wiremock 单元测试 + 1 个端到端 驱动内置 git 工具的集成测试;workspace 通用 conformance suite 在 Phase 6.3 加入。
  7. README 与 CHANGELOG 已更新;TLS 后端选择与 AWS SDK 一致用 rustls-tls。
  8. SDK 暴露在 Phase 5.1 完成(Node + Python)——草案曾说"trait 稳定之 后再补",结果是 Phase 4.2 完成两个提交后就上了。