RFC: Remote Workspace Git Backend
This document is retained as the protocol specification — clients and servers speaking this surface should match the wire shapes defined below. Subsequent revisions should land as separate amendment documents rather than editing this one in place, so the "what was shipped" history stays auditable.
This RFC proposes a protocol and a Rust client that let a3s-code run the
built-in git tool against workspaces backed by something other than a local
filesystem — most importantly S3 and future container / DFS backends, which
cannot host a .git directory directly.
1. Motivation
The workspace abstraction already lets the built-in tools talk to any
filesystem implementation via WorkspaceFileSystem. For bash, grep,
glob, and git we use capability gating: if the backend cannot
service the operation, the corresponding tool is not registered and the
model never sees it.
That works for bash (object storage really cannot run a shell) and is
acceptable for grep (we added a degraded LIST + GET + regex path). It is
painful for git: many a3s-code workflows expect to query branch /
commit state, diff working tree, create branches, and stash changes. Hiding
git everywhere except local sessions splits the user experience and
prevents cloud workspaces from being first-class.
The proposal is to introduce a remote WorkspaceGit backend that
delegates these operations over the network to an external service the host
operates. The service owns a real working tree (or a libgit2-backed
implementation) and exposes a small HTTP API. The a3s-code client speaks
that API and presents it to tools as just another WorkspaceGit provider.
2. Non-Goals
- Hosting a gitserver. This RFC defines a client and a protocol the client speaks. Implementations of the server side (libgit2 wrapper, shell-out, Gitea-API adapter, ...) are out of scope.
- Replacing local git. When
WorkspaceFsisLocalWorkspaceBackend, the existingLocalWorkspaceBackendWorkspaceGitimpl remains the default. Hosts mix and match per session. - Push / pull. The
WorkspaceGittrait surface is read-only against remotes (list_remotesreturns the configured set; nogit push/git fetch). A future RFC may add those if a tool needs them. - Worktrees. Worktrees are a local-filesystem concept; the remote
backend will not implement
WorkspaceGitWorktreeProvider. See §8.
3. Protocol Choice — HTTP/JSON
Decision: HTTP/JSON, not gRPC.
The operation set is small (~12 RPCs) and the request/response shapes are
flat. Streaming would only meaningfully help log and diff, both of
which are bounded by client-side limits already. The mocking and operator
debuggability wins of HTTP/JSON outweigh the schema benefits of gRPC at
this scale. If the operation surface grows substantially or streaming
becomes load-bearing, gRPC can be revisited.
We use POST for every operation. Git operations are imperative, not
resource-CRUD, and request bodies make versioning trivial (add optional
fields, ignore unknown). GET would force everything into query strings.
4. Repository Identity
The client knows it is talking about a specific repository; the server needs to route. We encode the repo identity in the URL path:
repo_id is an opaque, URL-safe string negotiated out of band between the
host that configures the client and the gitserver operator. Typical values:
users/{user_id}/sessions/{session_id}(1:1 with an S3 workspace prefix)- A UUID
- A path to a working tree on the gitserver
The client treats repo_id as opaque; the server is responsible for
mapping it to an actual working tree / bare repo.
5. Endpoint Reference
All endpoints are POST. All requests and responses are application/json.
Field naming is snake_case to match Rust serde defaults.
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 is true when the server clipped the body — see §9. The
client surfaces this in the git diff tool result.
5.7 List Remotes — WorkspaceGit::list_remotes
5.8 Is Repository — WorkspaceGit::is_repository
Distinct from "repo not found" (404): the server may be configured to
serve repo IDs that map to non-git directories; is_repository lets the
client probe without inferring intent from a 404.
5.9 List Stashes — WorkspaceGitStashProvider::list_stashes
5.10 Stash — WorkspaceGitStashProvider::stash
6. Authentication
The client supports two transport modes, configurable per-session:
- Bearer token (default).
Authorization: Bearer <token>. Token provisioning is the host's responsibility (e.g. short-lived JWT minted by the same identity layer that gates S3 access). - mTLS. Pass both
client_cert_pemandclient_key_pempaths via the backend config. The client reads both files at construction, concatenates them, and hands the result toreqwest::Identity::from_pem. Therustls-tlsbackend expects the key in PKCS#8 PEM format. Setting only one of the pair fails at construction with a clear error.
Both are mutually compatible — a deployment that uses both for defence in depth simply sets both.
No-auth mode (for localhost development) is supported by configuring an
empty bearer token (and no mTLS); the client emits a tracing::warn!
on construction to make this visible.
7. Error Model
HTTP status code is the transport signal. The error kind is in the JSON body:
Client mapping:
The client introduces one typed error for backwards-compatible recovery:
Tools that want to recover from BRANCH_EXISTS / WORKING_TREE_DIRTY /
NOTHING_TO_STASH downcast anyhow::Error::downcast_ref::<RemoteGitConflict>(),
the same pattern edit / patch use for WorkspaceVersionConflict on
S3 compare-and-swap writes.
Error codes documented in the public schema (extensible):
8. Optional Traits
WorkspaceGit is implemented in full.
WorkspaceGitStashProvider is implemented.
WorkspaceGitWorktreeProvider is deliberately not implemented by the
remote backend. Worktrees are a local-filesystem concept that map poorly
onto a remote service:
- "Create a worktree at path X" — the client has no path concept; the server's path layout is opaque.
- The local tool flow that uses worktrees (parallel agent runs against
isolated copies) is better served on cloud workspaces by spinning up
separate sessions with separate
repo_ids, not by emulating a filesystem feature.
Tools that depend on WorkspaceGitWorktreeProvider see None from
services.git_worktree() and emit a clear "worktrees unavailable on
remote git workspaces" error.
9. Size and Cost Bounds
Following the S3 backend's pattern, the remote git client enforces client-side ceilings so the model cannot trigger unbounded responses:
The server is expected to honour these too — the client passes the relevant
caps in the request where applicable (max_log_entries), and surfaces
truncated: bool returned by the server on diff so the tool can hint
"large diff truncated; narrow your target".
10. Rust Client Design
Composition factory mirrors the S3 path:
Or as a top-level convenience for an S3 + remote-git workspace:
Wiring follows the existing builder pattern:
Capability gating then registers the git tool automatically.
11. Per-Call Observability
Every HTTP call emits a tracing::debug! event with the same field
shape S3WorkspaceBackend uses for its per-call metering (see
emit_s3_call_event in core/src/workspace/s3.rs):
Hosts that already meter S3 cost can extend the same subscriber to meter gitserver cost; no new dependency or hook surface is needed.
12. Composition Examples
S3 workspace + remote git
Tools registered for this session: read, write, edit, patch, ls,
grep, glob, git. (bash remains hidden — object storage cannot
host a shell.)
Local filesystem + remote git (mixed)
Useful when CI runs against a local checkout but the host wants to route git operations through a sandboxed service (e.g. for audit / rate limiting).
13. Open Questions
These should be resolved before Phase 4.2 (implementation) starts.
-
Diff format pinning. The local backend returns raw
git diffstdout. Should the remote API guarantee a specific diff dialect (POSIXdiff -u? libgit2's variant?) or pass through whatever the server produces? Recommendation: pass through, document that the server should produce unified diff. -
Concurrent operations. If two clients hit the same
repo_idsimultaneously (one runningcheckout, one runningdiff), is the server expected to serialise? Recommendation: server-side serialisation perrepo_id. Document the expectation; do not retry on conflict in the client. -
Long-running operations.
checkoutagainst a large tree could exceedrequest_timeout. Should the protocol support a polling / async-job mode? Recommendation: defer. Set realistic timeouts; the working-tree sizes we target (per-session sandboxes) are small. -
Hooks. The server may have pre-commit / pre-push hooks installed. Do we surface a "hook output" channel in responses? Recommendation: include hook stderr in the
checkout/stashresponse when non-empty; document that hook failures map to HTTP 422 with aHOOK_FAILEDcode. -
Schema versioning. First-cut endpoints are under
/v1/. When should we bump to/v2/? Recommendation: only on incompatible request/response shape changes; additive fields stay under/v1/(clients ignore unknown fields). -
Reference RFC. Should a reference implementation (e.g. a minimal libgit2-backed gitserver written in Rust) live in this repo, or stay out-of-tree? Recommendation: out-of-tree. The client and protocol are sufficient; reference servers belong with operators.
14. Out of Scope (Future RFCs)
- Push / fetch from upstreams. Needed for "agent commits and pushes changes back to origin" workflows. Will need credential delegation.
- Sparse / partial checkout. Useful for huge monorepos; the current surface assumes the entire repo is materialised on the server.
- Streaming
loganddiff. Required if response bodies routinely exceedmax_diff_bytes. Would motivate revisiting the gRPC decision. - Hooks management. Listing / configuring server-side hooks from the client.
15. Implementation Notes (shipped in v2.6.x)
The original draft sketched an 8-step implementation plan; what actually shipped (Phase 4.2 plus Phase 5.x follow-ups) is:
RemoteGitBackend/RemoteGitBackendConfig/RemoteGitConflictlive incore/src/workspace/remote_git.rs. Noremote-gitfeature was added —reqwestwas already a hard dependency for the LLM clients, so the module compiles unconditionally.WorkspaceGitandWorkspaceGitStashProviderimplemented in full.WorkspaceGitWorktreeProviderdeliberately omitted (see §8).WorkspaceServices::with_remote_gitis the public attachment point. The internal helperwith_git_provider(added in v2.6.x follow-up) handles the actual struct-literal copy so future fields onWorkspaceServicescannot be silently dropped by the decorator.- mTLS is supported in addition to bearer token (
client_cert_pemclient_key_pem), shipped in a Phase 5.2 follow-up that also updated §6 above.
diffis hardened against unbounded responses: HTTP body is streamed with a hard cap ofmax_diff_bytes * 4(Phase 6.2). The softmax_diff_bytesdisplay truncation continues to apply post-decode.- Test surface: 25+ wiremock-backed unit tests in
remote_git.rs, one end-to-end test driving thegitbuilt-in tool through the remote backend, plus the workspace-wide conformance suite added in Phase 6.3. - README and CHANGELOG updated; rustls-tls backend selected to match the AWS SDK.
- SDK exposure landed in Phase 5.1 (Node + Python).