feat(sandbox): add a self-hostable Docker sandbox provider - #467
feat(sandbox): add a self-hostable Docker sandbox provider#467rycerzes wants to merge 3 commits into
Conversation
🦋 Changeset detectedLatest commit: c22c327 The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
| } | ||
| return { success: false, error: errorMessage(error) }; | ||
| } | ||
| } |
There was a problem hiding this comment.
Dead containers never recreate sandboxes
High Severity
exec never throws SandboxNotAvailableError when the container is missing or stopped. requireContainer only checks the id shape, and a failed docker exec comes back as a normal non-zero exit, which is wrapped in success: true. After a daemon reboot or a removed container, the session keeps the stale id and never recreates the sandbox.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 02f66d2. Configure here.
| gpus: this.gpus ?? null, | ||
| }); | ||
| return { sandboxId }; | ||
| } |
There was a problem hiding this comment.
Sandbox containers are never reaped
High Severity
Each sandbox is a detached sleep infinity container with no restart policy, idle timeout, or server-side cleanup. dispose only tracks this instance’s creates, and reapStale is unused outside tests, while the server builds a fresh provider per turn and never disposes it. Containers, disk, and GPUs accumulate until someone deletes them by hand.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 02f66d2. Configure here.
| # initialisation fails with "python3: not found" while the session still | ||
| # starts, which makes the failure easy to miss. | ||
| image: python:3.12-slim | ||
| exec_timeout_ms: 600000 |
There was a problem hiding this comment.
Catalog image cannot run skill init
Medium Severity
The shipped docker preset uses python:3.12-slim, which has neither pydantic nor git. The comment says both are required because sandbox init runs git_downloader.py with python3, not a venv bootstrap. Copying this preset into settings leaves git-backed skills failing while the session still starts.
Reviewed by Cursor Bugbot for commit 02f66d2. Configure here.
| } catch (error) { | ||
| await this.removeContainer(sandboxId).catch(() => undefined); | ||
| throw error; | ||
| } |
There was a problem hiding this comment.
Layout mkdir failures are ignored
Medium Severity
The post-create mkdir goes through execInContainer, which only throws on a wedged client timeout and otherwise returns a non-zero exitCode. That status is never checked, so a failed layout setup still returns a sandboxId. Images without GNU timeout (or any mkdir failure) leak a container whose uploads / skills / tool-results dirs were never created.
Reviewed by Cursor Bugbot for commit 02f66d2. Configure here.
| }; | ||
| } | ||
| return { status: 'pending', reason: `image ${this.image} not present locally`, metadata: { image: this.image } }; | ||
| } |
There was a problem hiding this comment.
Interrupted image pulls stay pending
Medium Severity
Pull state lives in a process-local map, and getImageBuildStatus never starts a pull. After a restart (or a killed docker pull), a stored pending row is refreshed with getImageBuildStatus, which reports the image as still missing and leaves status pending. Settings GET polling will not recover until someone PUTs again.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 02f66d2. Configure here.
SandboxProviderManifestSchema carried a TODO to widen it when a second provider shipped. Renames toDaytonaSandboxProvider to toSandboxProvider and dispatches on type.
Code Mode needs a bidirectional transport that not every provider can offer. Adds an optional supportsCodeMode flag so a session degrades to ordinary tool calls instead of failing.
daytona is the only exposed provider and is no longer self-hostable. The sandbox is an image, so a workload needing a specific toolchain gets a reproducible one; gpus passes through to --gpus. Closes truefoundry#466.
6d34506 to
c22c327
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
There are 6 total unresolved issues (including 5 from previous reviews).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit c22c327. Configure here.
| export const CatalogSandboxProviderSchema = DaytonaSandboxProviderSchema.omit({ auth: true }) | ||
| .strict() | ||
| export const CatalogSandboxProviderSchema = z | ||
| .discriminatedUnion('type', [DaytonaSandboxProviderSchema.omit({ auth: true }).strict(), DockerSandboxProviderSchema]) |
There was a problem hiding this comment.
UI adapter still assumes Daytona only
Medium Severity
The catalog and persisted manifest are now a type union that includes docker, but the settings UI adapter still treats every provider as Daytona. The catalog list will offer docker, then toHarnessManifest rejects it, and resolveApiKey reads manifest.auth which docker does not have. Schema changes also did not update the UI layer that consumes them.
Additional Locations (1)
Triggered by project rule: TrueForge review rules
Reviewed by Cursor Bugbot for commit c22c327. Configure here.


Summary
Adds a Docker sandbox provider, and widens the provider manifest to a discriminated union so a second provider can exist at all.
daytonais the only provider exposed in the catalog, and it went closed source in June 2026, so there is currently no way to run TrueForge fully self-hosted. This is also the only provider that can pin a toolchain: the sandbox is an image, so a workload needing a specific CUDA or Python version gets a reproducible one.gpuspasses through to--gpusfor workloads that need a device.Closes #466
Changes
SandboxProviderManifestSchemabecomes a discriminated union ontype;toDaytonaSandboxProviderbecomestoSandboxProviderand dispatchesDockerSandboxProvideragainst the existingSandboxProviderinterface — path-shapedsandboxId,docker exec, file transfer streamed throughdocker cp/stdin rather than base64-into-argv, so it does not reproduce TFY sandbox uploadFile fails for files larger than ~96 KiB: whole payload is base64-encoded into a single argv (MAX_ARG_STRLEN/E2BIG) #416gpuspassed through to--gpus; omit for no GPUsupportsCodeModeon the provider interface, so a session degrades to ordinary tool calls instead of failing when a provider has no bidirectional transportdockerpreset insandbox-catalog.yamlHow was this tested?
pnpm build,pnpm test,pnpm typecheck,pnpm lint:ci,pnpm format:checkall pass. Default suites unchanged at 389 and 274 tests.Two opt-in suites added, both needing a container runtime:
pnpm test:docker-sandbox:contract— 13 tests, including yoursandboxProviderContractSuiteand sibling-escape/traversal hardeningpnpm test:docker-sandbox:gpu— 3 tests, skipped viapending()when no GPU-capable host is present, so it is safe in CIVerified on Linux, Node 22.14, Docker 29.7.2 with the NVIDIA runtime, RTX 4090, driver 580.159.04, CUDA 13.2:
nvidia-smivisible inside the sandbox, and annvcc -arch=sm_89compile-and-run measuring 921.2 of 1008.1 GB/s.Checklist
pnpm build,pnpm test,pnpm typecheck,pnpm lint:ci, andpnpm format:checkpass locally.env.exampleupdated if configuration or behavior changed — not done; happy to add docs if you want this directionRaised as #466 first per the approval note in CONTRIBUTING.md. Happy to rework the shape, or to close this if a container provider is not a direction you want to take.
Note
Medium Risk
Introduces container execution via the host Docker/Podman socket with new path and symlink containment logic; misconfiguration or bugs could affect host isolation, though the PR adds explicit hardening and scoped cleanup.
Overview
Adds a self-hostable Docker sandbox backend so tenants can run agent workloads in one container per session, with optional
--gpuspassthrough and a configurable image (catalog preset usespython:3.12-slim).Provider model & API:
SandboxProviderManifestis now atype-discriminated union (daytona|docker). Runtime construction goes throughtoSandboxProviderinstead of Daytona-only helpers; settings PUT/GET redact and merge secrets only for Daytona, since Docker has no stored credentials. The shipped sandbox catalog includes adockerpreset.Code Mode: Providers may set optional
supportsCodeMode; when it isfalse,Sandbox.configureCodeModelogs and skips Code Mode instead of failing the session (Docker sets this because it has no shared-filesystem transport).Docker implementation highlights: Path-shaped sandbox ids map to derived container names (works across fresh provider instances per turn). File upload/download uses
docker execwith stdin/stdout, avoiding argv size limits. Hardening includes in-container timeouts,realpath-based path containment for file ops, scoped stale container reaping, and background image pulls with process-wide pull state.Tests: Opt-in
test:docker-sandbox:contract(shared provider contract + hardening) andtest:docker-sandbox:gpu(CUDA smoke, skipped without GPU).Reviewed by Cursor Bugbot for commit c22c327. Bugbot is set up for automated code reviews on this repo. Configure here.