You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 6c72537
Browse filesBrowse the repository at this point in the historyBrowse files
feat(workspaces)!: replace the backend protocol with pydantic ai workspaces (#112)
Pydantic AI 2.52 gives every tool one environment, ctx.workspace, with
its own file and command contract. Keeping BackendProtocol beside it
meant every tool, adapter and sandbox existed twice. The sandboxes are
now workspaces and the console tools work in whichever one a run has.
Adds DockerWorkspace, SandboxdWorkspace, KubernetesWorkspace,
DaytonaWorkspace and StateWorkspace. Every command-capable workspace
stops a timed out or cancelled command with its whole process group.
sandboxd serves commands only through /run, and answers 410 rather
than replacing a sandbox whose files are gone. Read-only workspaces
hide the mutating tools. Permission rules bind however a path is
spelled, including through a symlink. Fixes grep on BSD and on a
single file with GNU grep, and glob/grep reporting transport failures
as no matches.
Verified: 1313 tests at 100% coverage on Python 3.10 to 3.13; ruff,
mypy, pyright; DockerWorkspace and SandboxdWorkspace pass Pydantic AI's
WorkspaceBackendSuite on a real daemon. The Kubernetes and Daytona
suites exist but have not run against a live cluster or account.
BREAKING CHANGE: BackendProtocol, SandboxProtocol, the async adapters,
LocalBackend, CompositeBackend, BaseSandbox, RemoteSandbox,
DaytonaSandbox, ConsoleDeps, the background shell tools,
ConsoleCapability(backend=, include_background=), the sandboxd file and
exec routes, DockerSandbox file operations and the Kubernetes HTTP mode
are removed. StateBackend is a document store with directories. The
console and workspaces extras need pydantic-ai-slim>=2.52.0. See
CHANGELOG.md for what replaces each name.
Closes#113Closes#114
`glob_info`, `grep_raw`, `execute`) and `max_read_bytes`: reach files through
109
+
a workspace, run commands with `run_command`.
110
+
-**The live file browser in the `sandboxd` dashboard.** The terminal runs
111
+
through `/run`; files are browsed in the stored workspace.
112
+
113
+
### Changed
114
+
115
+
-**`StateBackend` is a document store, not a backend.** It keeps `files` and
116
+
the `directories` created — both JSON — and follows a filesystem's rules,
117
+
raising `FileNotFoundError`, `IsADirectoryError` and `NotADirectoryError`.
118
+
`StateWorkspace` serves documents from a store the application owns. A
119
+
directory a write or `make_dir` created stays when the last thing in it is
120
+
removed. A document written by an earlier version loads unchanged.
121
+
122
+
### Fixed
123
+
124
+
-**`grep` found nothing on macOS.** BSD `grep` matches `--exclude` against the
125
+
whole path (`./f.txt`), so excluding hidden files with `.*` excluded every
126
+
file. Hidden directories are excluded by the shell and hidden files filtered
127
+
afterwards.
128
+
-**`glob` and `grep` reported "no matches" when the sandbox was unreachable.**
129
+
Transport failures now surface as errors.
130
+
-**File-read tracking was lost between tool calls** when the tools ran in a
131
+
workspace. What a toolset has read is now kept per workspace, and released
132
+
with it, so a long-lived agent does not keep every workspace it worked in.
133
+
-**`grep` on a single file found nothing with GNU grep**, which leaves the file
134
+
name out for one file; the line then did not parse as a match. `-H` makes it
135
+
print the name.
136
+
10
137
## [0.2.29] - 2026-08-22
11
138
12
139
### Fixed
@@ -503,7 +630,7 @@ before upgrading is deferred.
503
630
504
631
The guard deliberately lets pydantic-ai's control-flow exceptions past — `ModelRetry`, `ApprovalRequired`, `CallDeferred` and the `Skip*` family. Those are not failures, they steer the run, and catching `ModelRetry` in particular would turn a retry into a dead end the model cannot recover from. `UserError` passes through too, because reporting a misuse of the library to the model as a failed file operation hides the bug; it subclasses `RuntimeError`, so the narrower handler this replaced was already swallowing it.
505
632
-**The shell derivation every sandbox depends on is measured.**`BaseSandbox` carried a blanket `# pragma: no cover`, so the command construction and output parsing behind Docker, Daytona, Kubernetes and any third-party sandbox contributed nothing to the 100% gate. Moving it into one module made it directly testable, and it is now covered — including the quoting of a hostile path, `ls` rows with spaces in the name, a `grep` line containing colons, and the failure branch of every operation.
506
-
-**The failure contract is written down.** What a backend must return when an operation fails was real, load-bearing and documented nowhere, so implementors were deducing it from our source. `protocol.py` now states it per method, including the asymmetry that matters most: `read` may return an `Error: ` string but `read_bytes` must return `b""`, because its caller cannot tell an error message from real file content — a probe staging a screenshot would treat `b"Error: not found"` as the image.
633
+
-**The failure contract is written down.** What a backend must return when an operation fails was real, load-bearing and documented nowhere, so implementers were deducing it from our source. `protocol.py` now states it per method, including the asymmetry that matters most: `read` may return an `Error: ` string but `read_bytes` must return `b""`, because its caller cannot tell an error message from real file content — a probe staging a screenshot would treat `b"Error: not found"` as the image.
507
634
508
635
-**`SandboxdConfig(container_ttl=...)`** removes a persisted sandbox container that has been stopped for that long, leaving its workspace untouched. It separates the two things a session accumulates: what it *installed* is rebuildable, what it *wrote* is not — so a deployment can reclaim the first on a schedule while keeping an agent's files for ever, which is what its user expects. `workspace_ttl` remains the opposite knob and stays `None` by default. The sweep finds containers by their name prefix rather than from a record of its own, because after a restart Docker is the only source of what is still lying around.
509
636
-**`SandboxdConfig(evict_idle_after=...)`** turns the session ceiling from a hard cap on how many sessions may exist into a working-set size. At the ceiling the least recently used session idle for at least that long is closed to make room, instead of the incoming request being refused — which was the wrong answer when the pool was full of sandboxes nobody was using. With `workspace_root` set the evicted session loses nothing but its container: its next request re-attaches and finds its files, for the price of a container start. A session idle for less than the threshold is never a candidate, because killing an agent's work to serve somebody else's first request is worse than making them wait, and a pool of genuinely busy sessions still answers `429`. Requires `workspace_root`, and the config refuses without it rather than silently discarding an evicted session's files.
0 commit comments