RFC: Remote Workspace Git Backend
This page specifies the HTTP/JSON protocol that RemoteGitBackend speaks and
describes how the client behaves. A gitserver that implements the endpoints
below lets the built-in git tool run on workspaces that cannot hold a .git
directory, such as S3 workspaces.
1. Motivation
Built-in tools reach the workspace through WorkspaceServices, and each tool
is registered only when the workspace backend supports it. An S3 workspace
has read and write capability but no shell and no git provider, so bash and
git are never registered for it.
Hiding git everywhere except local sessions would make cloud workspaces
second-class: many coding workflows need branch and commit state, diffs,
branch creation, and stashes. RemoteGitBackend fills that gap by delegating
git operations over HTTP to a service the host operates. That service owns a
real working tree; the client presents it to tools as an ordinary
WorkspaceGit provider.
2. Scope
The client and protocol cover what the WorkspaceGit and
WorkspaceGitStashProvider traits expose. The following are not part of the
implementation:
- A gitserver. a3s-code ships only the client. Server implementations (libgit2 wrapper, shell-out, hosted-git adapter) belong to the operator.
- Push, pull, and fetch.
list_remotesreports configured remotes; there is no operation that talks to an upstream. - Worktrees. The remote backend does not implement
WorkspaceGitWorktreeProvider. See §8. - Commits.
WorkspaceGithas no commit operation, so neither does the protocol.
A local workspace keeps its local git provider unless the host explicitly attaches a remote one.
3. Protocol Choice — HTTP/JSON
This section records the design rationale for the shipped protocol.
The operation set is small (ten endpoints), every call is request/response,
and the shapes are flat. HTTP/JSON reuses the reqwest client already in the
dependency tree, is easy to mock in tests, can be debugged with curl, and
lets a server be a thin wrapper around git. gRPC would add a proto toolchain
for little benefit at this size; streaming would only help log and diff,
which the client already bounds (§9).
Every operation uses POST. Git operations are imperative rather than
resource CRUD, and a JSON request body lets new optional fields be added
without changing URLs.
4. Repository Identity
Every request goes to:
base_url has any trailing / removed. repo_id is an opaque string agreed
between the host and the gitserver operator. The client inserts it into the
path verbatim, without percent-encoding, so a value such as
users/u1/sessions/s1 produces several path segments. The server maps
repo_id to a working tree.
5. Endpoint Reference
All endpoints are POST, and request and response bodies are
application/json with snake_case fields. Endpoints that take no input
receive an empty JSON body. Any 2xx status is success. The client ignores
unknown response fields, so servers may add fields.
The built-in git tool calls exists before every command, then:
5.1 Status — WorkspaceGit::status
branch and commit are required. is_worktree, is_dirty, and
dirty_count default to false, false, and 0.
5.2 Log — WorkspaceGit::log
The client sends max_count already capped at max_log_entries (§9). All
four commit fields are required.
5.3 List Branches — WorkspaceGit::list_branches
is_current defaults to false.
5.4 Create Branch — WorkspaceGit::create_branch
The response body of a successful call is ignored.
5.5 Checkout — WorkspaceGit::checkout
stdout defaults to an empty string. The git tool appends it to the
checkout result when it is not blank.
5.6 Diff — WorkspaceGit::diff
The client passes diff through unchanged, so the server should return
unified diff text. truncated defaults to false; when the server sets it,
the client appends ... [truncated by gitserver] to the diff. See §9 for the
client-side caps.
5.7 List Remotes — WorkspaceGit::list_remotes
direction defaults to "fetch".
5.8 Is Repository — WorkspaceGit::is_repository
is_repository defaults to false. A false result makes the git tool
return Not a git repository: ...; an error response makes it return
Failed to inspect git repository: .... Returning false lets a server serve
a repo_id that exists but is not a repository without overloading 404.
5.9 List Stashes — WorkspaceGitStashProvider::list_stashes
message defaults to an empty string.
5.10 Stash — WorkspaceGitStashProvider::stash
message is omitted from the request when the caller did not supply one. The
response body of a successful call is ignored.
6. Authentication
- Bearer token. When
bearer_tokenis set and non-empty, every request carriesAuthorization: Bearer <token>. Provisioning the token is the host's job. - mTLS. Set both
client_cert_pemandclient_key_pemto PEM file paths. The client reads both files when the backend is constructed, concatenates them, and passes the result toreqwest::Identity::from_pem. The TLS stack isrustls, which expects the key in PKCS#8 PEM. Setting only one of the pair, an unreadable file, or invalid PEM fails construction.
Both can be used together. With no token and no client certificate, the
client still works and logs a tracing::warn! at construction; use that only
against a trusted localhost gitserver.
The HTTP client is built with proxies disabled, so HTTP_PROXY,
HTTPS_PROXY, and system proxy settings do not apply to gitserver calls.
7. Error Model
The HTTP status decides the error category. The server should put the error kind in the body:
message is optional. If the body is not in this shape, the client uses
HTTP_<status> as the code and the raw body as the message.
Transport failures produce remote git call '<op>' transport error: .... The
client does not retry.
409 and 422 become one typed error:
Rust callers that use the WorkspaceGit provider directly can recover with
error.downcast_ref::<RemoteGitConflict>(), and WorkspaceError::from_anyhow
maps it to WorkspaceError::RemoteGitConflict. The built-in git tool reports
failures as text, for example
Failed to create branch: remote git conflict: BRANCH_EXISTS: ....
The client interprets only the HTTP status, never the code string. These codes are the conventional set for servers:
8. Optional Traits
RemoteGitBackend implements WorkspaceGit and WorkspaceGitStashProvider.
It does not implement WorkspaceGitWorktreeProvider: a worktree is a local
path, and the client has no view of the server's filesystem layout. For
isolated parallel work on cloud workspaces, use separate sessions with
separate repo_ids.
with_remote_git resets git_worktree to None, including on a local
workspace, so git status and worktree state cannot come from different
sources. The git tool's worktree command then returns
Worktree operations are not supported by this workspace backend.
9. Size, Time, and Cost Bounds
The client bounds every response so a misbehaving server cannot exhaust memory or model context:
For diff, a Content-Length above the hard cap is rejected before the body
is read, and a streamed body is aborted as soon as it passes the cap. A decoded
diff longer than max_diff_bytes is cut at a UTF-8 boundary and ends with
... [truncated by client max_diff_bytes].
Because log is capped, the git tool cannot page past max_log_entries
commits on a remote backend.
WorkspaceServices::operation_timeout does not apply to the git tool, so
request_timeout is the only time limit on a gitserver call.
WorkspaceServices::s3 sets a 60 second operation timeout for file and search
operations only.
10. Rust Client API
RemoteGitBackendConfig has public fields base_url, repo_id,
bearer_token, client_cert_pem, client_key_pem, request_timeout,
max_diff_bytes, and max_log_entries. The module compiles without any Cargo
feature.
Attach the backend to existing services:
It returns new services with git and git_stash set to the remote backend
and git_worktree set to None. Every other field is preserved, including
the local root, command runner, search provider, S3 compare-and-swap
extension, and operation timeout. The original services are not changed.
Hosts that assemble services by hand can use the builder:
Setting a git provider marks the workspace git-capable, so the session
registers the git tool.
11. Per-Call Observability
Every HTTP call emits one tracing::debug! event, shaped like the S3
backend's per-call event (emit_s3_call_event in core/src/workspace/s3.rs):
A subscriber that already meters S3 calls can meter gitserver calls the same way.
12. Composition Examples
S3 workspace + remote git
Requires the s3 Cargo feature of a3s-code-core.
The session registers read, write, ls, edit, patch, search
(because search is enabled), and git. bash stays hidden because object
storage cannot run a shell.
Local filesystem + remote git (mixed)
Useful when the files are local but git operations must go through a service, for example for audit or rate limiting.
SDK options
Each SDK accepts a remote git config next to a workspace backend and calls
with_remote_git on it. Setting remote git without a workspace backend is an
error.
- Node.js:
SessionOptions.remoteGit(baseUrl,repoId,bearerToken,clientCertPem,clientKeyPem,requestTimeoutMs,maxDiffBytes,maxLogEntries) withworkspaceBackend. - Python:
SessionOptions.remote_git(RemoteGitBackendConfig) withworkspace_backend. - Go:
SessionOptions.RemoteGit(RemoteGitBackendConfig) withSessionOptions.WorkspaceBackend.
The published npm and PyPI packages include the S3 backend. The published Go
bridge is built without the s3 feature, so in Go remote git attaches only to
a local workspace backend unless you build the bridge with s3 or server.
13. Server Expectations
The client leaves these behaviors to the server:
- Concurrency. Serialize operations per
repo_id. The client does not retry on conflict. - Diff format. Return unified diff text; the client does not normalize it.
- Long operations. Finish within the client's
request_timeout. The protocol has no polling or async-job mode. - Hooks. Report a hook rejection as 422 with
HOOK_FAILED, so it surfaces asRemoteGitConflict. - Versioning. Keep additive changes under
/v1/; the client ignores unknown fields. Only an incompatible request or response change needs a new path prefix.