@@ -5,7 +5,60 @@ All notable changes to this project will be documented in this file.
55The format is based on [ Keep a Changelog] ( https://keepachangelog.com/en/1.0.0/ ) ,
66and this project adheres to [ Semantic Versioning] ( https://semver.org/spec/v2.0.0.html ) .
77
8- ## [ Unreleased]
8+ ## [ 3.0.0] - 2026-05-20
9+
10+ ### Added (Phase 8 — typed-error SDK alignment)
11+
12+ - New public ` ToolErrorKind ` enum (` #[non_exhaustive] ` , JSON-tagged on
13+ the ` type ` discriminator) carries structured tool failure reasons
14+ from the Rust core all the way to SDK callers without losing the
15+ type. Six variants: ` version_conflict ` , ` remote_git_conflict ` ,
16+ ` not_found ` , ` invalid_argument ` , ` unsupported ` , ` timeout ` .
17+ - New optional ` error_kind ` field on ` ToolOutput ` , ` ToolResult ` , and
18+ ` ToolCallResult ` , plus a matching field on ` AgentEvent::ToolEnd ` for
19+ streaming consumers.
20+ - Built-in ` edit ` and ` patch ` tools populate ` error_kind ` via
21+ ` ToolErrorKind::from_workspace_error ` whenever a ` WorkspaceError `
22+ variant maps to a typed kind. The human-readable ` output ` /
23+ ` content ` message is unchanged so the model still gets the retry
24+ hint; SDK callers now have a programmatic discriminator next to it.
25+ - Node SDK: new ` errorKindJson ` field on ` ToolResult ` and ` AgentEvent `
26+ (JSON-encoded ` ToolErrorKind ` ) plus a new ` ToolErrorKind ` TypeScript
27+ discriminated-union type in ` index.d.ts ` .
28+ - Python SDK: new ` error_kind_json ` (raw) and ` error_kind ` (parsed
29+ dict) properties on ` ToolResult ` and ` AgentEvent ` .
30+
31+ This closes the v3.0 typed-error gap: until this commit the typed
32+ ` WorkspaceError ` enum on the Rust trait surface was effectively
33+ re-stringified at the SDK boundary, forcing JS/Python callers to
34+ regex-match the output to detect e.g. concurrent-modification
35+ conflicts. They now ` switch ` / ` match ` on ` error_kind.type ` instead.
36+
37+ ### ⚠️ Breaking changes (3.0.0)
38+
39+ - ** ` WorkspaceFileSystem ` and ` WorkspaceFileSystemExt ` trait methods now
40+ return ` WorkspaceResult<T> ` instead of ` anyhow::Result<T> ` .** The new
41+ result type wraps the typed ` WorkspaceError ` enum
42+ (` #[non_exhaustive] ` ) with structured variants for ` NotFound ` ,
43+ ` VersionConflict ` , ` RemoteGitConflict ` , ` InvalidArgument ` , ` Timeout ` ,
44+ ` Unsupported ` , and a ` Backend(anyhow::Error) ` catch-all. Callers that
45+ used ` ? ` to lift errors into ` anyhow::Result ` keep working unchanged
46+ thanks to the blanket ` From<WorkspaceError> for anyhow::Error ` impl;
47+ callers that previously did ` err.downcast_ref::<WorkspaceVersionConflict>() `
48+ now ` match ` on the typed variant directly:
49+ ``` rust
50+ // before:
51+ if e . downcast_ref :: <WorkspaceVersionConflict >(). is_some () { ... }
52+ // after:
53+ if matches! (e , WorkspaceError :: VersionConflict (_ )) { ... }
54+ ```
55+ ` WorkspaceServices::read_for_edit ` , ` write_for_edit ` , and the generic
56+ ` run_with_timeout ` (now polymorphic in the error type) follow the
57+ same shape. The other 5 traits (` WorkspaceCommandRunner ` ,
58+ ` WorkspaceSearch ` , ` WorkspaceGit ` , ` WorkspaceGitStashProvider ` ,
59+ ` WorkspaceGitWorktreeProvider ` ) ** still return ` anyhow::Result ` ** —
60+ their migration to ` WorkspaceResult ` will be additive (non-breaking)
61+ in a future v3.x release.
962
1063### Added
1164
@@ -24,6 +77,113 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2477- Exposed ` S3WorkspaceBackend ` in the Node and Python SDKs alongside
2578 ` LocalWorkspaceBackend ` . Configuration uses the same option surface
2679 (` workspaceBackend ` / ` workspace_backend ` ).
80+ - ` S3WorkspaceBackend::read_text ` now enforces a configurable size ceiling
81+ (` S3BackendConfig::max_read_bytes ` , default 10 MiB) by inspecting
82+ ` Content-Length ` on the ` GetObject ` response before consuming the body.
83+ Oversized objects are rejected with a clear error and never buffered
84+ into memory. Responses without a ` Content-Length ` header are refused
85+ rather than risking OOM.
86+ - Added optional ` WorkspaceFileSystemExt ` trait for backends that expose
87+ compare-and-swap writes, plus a ` WorkspaceVersionConflict ` error type.
88+ ` S3WorkspaceBackend ` implements it via ETag + ` If-Match ` on ` PutObject ` .
89+ The ` edit ` and ` patch ` tools now capture the ETag during the read and
90+ reject the write on version mismatch (HTTP 412), surfacing a typed
91+ "Concurrent modification detected" error so the model can re-read and
92+ retry instead of silently clobbering a concurrent writer.
93+ ` WorkspaceServices::read_for_edit ` and ` write_for_edit ` are the new
94+ helpers tools should use for any read-modify-write cycle; backends
95+ without versioning (e.g. local) transparently fall through to plain
96+ ` read_text ` / ` write_text ` .
97+ - ` S3WorkspaceBackend ` now implements ` WorkspaceSearch ` (degraded ` grep ` /
98+ ` glob ` via ` LIST ` + ` GET ` + regex). Off by default; opt in via
99+ ` S3BackendConfig::enable_search(true) ` . Hard ceilings on objects scanned
100+ per call (` max_objects_scanned ` , default 500) and per-object body size
101+ for ` grep ` (` max_grep_bytes_per_object ` , default 1 MiB) bound the API
102+ cost. Hitting either ceiling sets ` WorkspaceGrepResult::truncated = true ` .
103+ Glob patterns follow the local backend's recursion convention: ` *.rs `
104+ matches the immediate level, ` **/*.rs ` recurses.
105+ - ` S3WorkspaceBackend::grep ` now downloads candidate objects in parallel
106+ via ` futures::stream::buffer_unordered ` . Concurrency defaults to 8 and
107+ is configurable via ` S3BackendConfig::search_concurrency ` (also
108+ exposed on both SDKs). Output ordering remains deterministic — results
109+ are sorted by workspace path before assembly — so callers see the same
110+ layout regardless of S3 response timing.
111+
112+ ### Added
113+
114+ - Internal ` workspace::conformance ` module (test-only) codifies the
115+ behavioural invariants every backend implementing
116+ ` WorkspaceFileSystem ` (and optionally ` WorkspaceFileSystemExt ` ) must
117+ satisfy. Two public entry points, ` assert_filesystem_conformance ` and
118+ ` assert_filesystem_ext_conformance ` , are run against
119+ ` LocalWorkspaceBackend ` and a new ` InMemoryFileSystem ` reference
120+ backend so the contract is exercised both over real I/O and an ideal
121+ HashMap-backed implementation. Future backends (GCS, container,
122+ browser) gain a regression suite for free — when the conformance set
123+ grows after a production incident, every backend running it picks up
124+ the new test automatically.
125+
126+ ### Fixed
127+
128+ - ` WorkspaceServices::with_remote_git ` previously rebuilt the services
129+ through ` WorkspaceServicesBuilder ` , which silently dropped ` local_root `
130+ (and would silently drop any future field). The decorator now goes
131+ through a new internal ` with_git_provider ` helper that uses an explicit
132+ struct literal — adding a new field to ` WorkspaceServices ` now triggers
133+ a compile error in every decorator, forcing a deliberate decision.
134+ - ` RemoteGitBackend::diff ` previously deserialised the entire response
135+ body before applying ` max_diff_bytes ` , so a misbehaving gitserver
136+ returning a multi-gigabyte JSON could exhaust client memory. The diff
137+ path now streams the body with a hard cap (` max_diff_bytes * 4 ` , floor
138+ 64 KiB), rejecting requests upfront when ` Content-Length ` advertises an
139+ oversized body and aborting the stream mid-flight when chunked encoding
140+ hides the size. The soft ` max_diff_bytes ` display truncation is
141+ unchanged.
142+
143+ ### Changed
144+
145+ - ` S3WorkspaceBackend::list_dir ` now errors with "S3 path not found" when
146+ the LIST returns zero entries on a non-root path, matching the local
147+ backend's behaviour. Previously a missing prefix silently returned
148+ ` Ok(vec![]) ` , masking typos. Paths that exist only as S3 zero-byte
149+ directory markers still return ` Ok(vec![]) ` .
150+ - Every S3 API call (` GET ` , ` PUT ` , ` LIST ` ) on ` S3WorkspaceBackend ` now
151+ emits a structured ` tracing::debug! ` event with fields ` op ` , ` bucket ` ,
152+ ` target ` , ` bytes ` , ` outcome ` , ` duration_ms ` . Hosts can meter S3 cost
153+ by subscribing to these events without the backend taking a dependency
154+ on any metrics framework.
155+ - Node and Python SDKs now expose the workspace hardening options added
156+ in this release. The Node ` JsS3BackendConfig ` and Python
157+ ` S3WorkspaceBackend ` constructor accept ` maxReadBytes ` /
158+ ` max_read_bytes ` , ` searchEnabled ` / ` search_enabled ` ,
159+ ` maxObjectsScanned ` / ` max_objects_scanned ` , and
160+ ` maxGrepBytesPerObject ` / ` max_grep_bytes_per_object ` . A new
161+ ` RemoteGitBackendConfig ` class (Python) / ` JsRemoteGitBackendConfig `
162+ shape (Node) and a top-level ` remoteGit ` / ` remote_git ` session
163+ option let SDK callers attach ` RemoteGitBackend ` on top of any
164+ workspace backend. Passing ` remoteGit ` without ` workspaceBackend `
165+ raises a clear error.
166+ - Added ` RemoteGitBackend ` — an HTTP/JSON ` WorkspaceGit ` client that
167+ brings the ` git ` tool to non-local workspaces (S3 today; future
168+ container / DFS). Implements ` WorkspaceGit ` in full and
169+ ` WorkspaceGitStashProvider ` ; deliberately omits ` WorkspaceGitWorktreeProvider `
170+ because worktrees do not map to a remote service. The protocol is
171+ specified in ` apps/docs/content/docs/en/code/rfcs/workspace-remote-git.mdx ` .
172+ - New types: ` RemoteGitBackend ` , ` RemoteGitBackendConfig ` ,
173+ ` RemoteGitConflict ` (anyhow-downcastable for recoverable 409 / 422
174+ responses such as ` WORKING_TREE_DIRTY ` and ` BRANCH_EXISTS ` ).
175+ - New factory: ` WorkspaceServices::with_remote_git(config) ` on any
176+ existing ` Arc<WorkspaceServices> ` to attach remote git on top of an
177+ S3 (or local) filesystem backend.
178+ - Client-side ceilings: ` request_timeout ` (default 30 s),
179+ ` max_log_entries ` (default 200), ` max_diff_bytes ` (default 1 MiB).
180+ - Per-call ` tracing::debug! ` event with fields ` op ` , ` repo_id ` ,
181+ ` status ` , ` bytes ` , ` outcome ` , ` duration_ms ` , mirroring the S3
182+ metering shape so a single subscriber meters both.
183+ - Authentication: bearer token (header ` Authorization: Bearer <token> ` )
184+ or mTLS via ` client_cert_pem ` + ` client_key_pem ` (PKCS #8 PEM key for
185+ the ` rustls-tls ` backend). Setting only one of the mTLS pair fails
186+ at construction.
27187
28188### Changed
29189
0 commit comments