Skip to content

feat(sandbox): add a self-hostable Docker sandbox provider - #467

Open
rycerzes wants to merge 3 commits into
truefoundry:mainfrom
rycerzes:feat/docker-sandbox-provider
Open

feat(sandbox): add a self-hostable Docker sandbox provider#467
rycerzes wants to merge 3 commits into
truefoundry:mainfrom
rycerzes:feat/docker-sandbox-provider

Conversation

@rycerzes

@rycerzes rycerzes commented Aug 27, 2026

Copy link
Copy Markdown

Summary

Adds a Docker sandbox provider, and widens the provider manifest to a discriminated union so a second provider can exist at all.

daytona is 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. gpus passes through to --gpus for workloads that need a device.

Closes #466

Changes

  • SandboxProviderManifestSchema becomes a discriminated union on type; toDaytonaSandboxProvider becomes toSandboxProvider and dispatches
  • New DockerSandboxProvider against the existing SandboxProvider interface — path-shaped sandboxId, docker exec, file transfer streamed through docker 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) #416
  • Optional gpus passed through to --gpus; omit for no GPU
  • Optional supportsCodeMode on the provider interface, so a session degrades to ordinary tool calls instead of failing when a provider has no bidirectional transport
  • One docker preset in sandbox-catalog.yaml

How was this tested?

pnpm build, pnpm test, pnpm typecheck, pnpm lint:ci, pnpm format:check all 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 your sandboxProviderContractSuite and sibling-escape/traversal hardening
  • pnpm test:docker-sandbox:gpu — 3 tests, skipped via pending() when no GPU-capable host is present, so it is safe in CI

Verified on Linux, Node 22.14, Docker 29.7.2 with the NVIDIA runtime, RTX 4090, driver 580.159.04, CUDA 13.2: nvidia-smi visible inside the sandbox, and an nvcc -arch=sm_89 compile-and-run measuring 921.2 of 1008.1 GB/s.

Checklist

  • I have read the contributing guidelines
  • pnpm build, pnpm test, pnpm typecheck, pnpm lint:ci, and pnpm format:check pass locally
  • Tests added/updated where it makes sense
  • No hand-edits to generated code
  • Docs / .env.example updated if configuration or behavior changed — not done; happy to add docs if you want this direction

Raised 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 --gpus passthrough and a configurable image (catalog preset uses python:3.12-slim).

Provider model & API: SandboxProviderManifest is now a type-discriminated union (daytona | docker). Runtime construction goes through toSandboxProvider instead 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 a docker preset.

Code Mode: Providers may set optional supportsCodeMode; when it is false, Sandbox.configureCodeMode logs 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 exec with 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) and test: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.

@changeset-bot

changeset-bot Bot commented Aug 27, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c22c327

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@truefoundry/trueforge-core Patch
@truefoundry/trueforge Patch

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) };
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 02f66d2. Configure here.

gpus: this.gpus ?? null,
});
return { sandboxId };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 02f66d2. Configure here.

} catch (error) {
await this.removeContainer(sandboxId).catch(() => undefined);
throw error;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 02f66d2. Configure here.

};
}
return { status: 'pending', reason: `image ${this.image} not present locally`, metadata: { image: this.image } };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 02f66d2. Configure here.

@rycerzes rycerzes changed the title feat(sandbox): add a Docker sandbox provider with optional GPU passthrough feat(sandbox): add a self-hostable Docker sandbox provider (optional GPU passthrough) Aug 27, 2026
@rycerzes rycerzes changed the title feat(sandbox): add a self-hostable Docker sandbox provider (optional GPU passthrough) feat(sandbox): add a self-hostable Docker sandbox provider Aug 27, 2026
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.
@rycerzes
rycerzes force-pushed the feat/docker-sandbox-provider branch from 6d34506 to c22c327 Compare August 27, 2026 05:24

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Fix All in Cursor

❌ 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])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Triggered by project rule: TrueForge review rules

Reviewed by Cursor Bugbot for commit c22c327. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Sandbox provider that can be self-hosted (and pin an image toolchain)

1 participant