# Daytona Documentation
# https://www.daytona.io/docs
# Generated on: 2026-07-18
# Daytona Documentation
Daytona is a secure and elastic infrastructure for running AI-generated code.
Daytona provides **full composable computers** — [sandboxes](https://www.daytona.io/docs/en/sandboxes.md) — for AI agents with complete isolation, a dedicated kernel, filesystem, network stack, and allocated vCPU, RAM, and disk.
Sandboxes are the core component of the Daytona platform, spinning up in under 90ms from code to execution and running any code in Python, TypeScript, and JavaScript. Built on OCI/Docker compatibility, massive parallelization, and unlimited persistence, sandboxes deliver consistent, predictable environments for agent workflows.
Agents and developers interact with sandboxes programmatically using the Daytona SDKs, API, and CLI. Operations span sandbox lifecycle management, filesystem operations, process and code execution, and runtime configuration. Our stateful environment snapshots enable persistent agent operations across sessions, making Daytona the ideal foundation for AI agent architectures.
## Get started
1. Create an account → [app.daytona.io](https://app.daytona.io)
2. Get an API key → [app.daytona.io/dashboard/keys](https://app.daytona.io/dashboard/keys)
3. Install the Python SDK
```bash
pip install daytona
```
4. Create a sandbox and run code
```python
# Import the Daytona SDK
from daytona import Daytona, DaytonaConfig
# Define the configuration
config = DaytonaConfig(api_key="YOUR_API_KEY") # Replace with your API key
# Initialize the Daytona client
daytona = Daytona(config)
# Create the Sandbox instance
sandbox = daytona.create()
# Run code
response = sandbox.process.code_run('print("Hello World")')
print(response.result)
```
3. Install the TypeScript SDK
```bash
npm install @daytona/sdk
```
4. Create a sandbox and run code
```typescript
// Import the Daytona SDK
import { Daytona } from '@daytona/sdk'
// Initialize the Daytona client
const daytona = new Daytona({ apiKey: 'YOUR_API_KEY' }) // Replace with your API key
// Create the Sandbox instance
const sandbox = await daytona.create()
// Run code
const response = await sandbox.process.codeRun('print("Hello World")')
console.log(response.result)
```
3. Install the Ruby SDK
```bash
gem install daytona
```
4. Create a sandbox and run code
```ruby
require 'daytona'
# Initialize the Daytona client
config = Daytona::Config.new(api_key: 'YOUR_API_KEY') # Replace with your API key
# Create the Daytona client
daytona = Daytona::Daytona.new(config)
# Create the Sandbox instance
sandbox = daytona.create
# Run code
response = sandbox.process.code_run(code: 'print("Hello World")')
puts response.result
```
3. Install the Go SDK
```bash
go get github.com/daytona/clients/sdk-go
```
4. Create a sandbox and run code
```go
package main
import (
"context"
"fmt"
"github.com/daytona/clients/sdk-go/pkg/daytona"
"github.com/daytona/clients/sdk-go/pkg/types"
)
func main() {
config := &types.DaytonaConfig{
APIKey: "YOUR_API_KEY", // Replace with your API key
}
client, _ := daytona.NewClientWithConfig(config)
ctx := context.Background()
sandbox, _ := client.Create(ctx, nil)
// Run code
result, _ := sandbox.Process.CodeRun(ctx, `print("Hello World")`)
fmt.Println(result.Result)
}
```
3. Install the Java SDK
**Gradle**
Add the Daytona SDK dependency to your `build.gradle.kts`:
```kotlin
dependencies {
implementation("io.daytona:sdk-java:0.1.0")
}
```
**Maven**
Add the Daytona SDK dependency to your `pom.xml`:
```xml
io.daytona
sdk-java
0.1.0
```
4. Create a sandbox and run code
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.DaytonaConfig;
import io.daytona.sdk.model.ExecuteResponse;
public class Main {
public static void main(String[] args) {
DaytonaConfig config = new DaytonaConfig.Builder()
.apiKey("YOUR_API_KEY")
.build();
try (Daytona daytona = new Daytona(config)) {
Sandbox sandbox = daytona.create();
// Run code
ExecuteResponse response = sandbox.getProcess().codeRun("print(\"Hello World\")");
System.out.println(response.getResult());
}
}
}
```
3. Create a sandbox and run code
```bash
# Create sandbox
curl https://app.daytona.io/api/sandbox \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{}'
# Run code
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/code-run' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"code": "print(\"Hello World\")"
}'
```
3. [Install the CLI](https://www.daytona.io/docs/en/tools/cli.md)
4. Create a sandbox and run code
```shell
daytona create --name hello
daytona exec hello -- python3 -c 'print("Hello World")'
```
## References
- **Daytona SDKs**: [TypeScript](https://www.daytona.io/docs/en/typescript-sdk.md), [Python](https://www.daytona.io/docs/en/python-sdk.md), [Ruby](https://www.daytona.io/docs/en/ruby-sdk.md), [Go](https://www.daytona.io/docs/en/go-sdk.md), [Java](https://www.daytona.io/docs/en/java-sdk.md)
- **Daytona APIs**:
- [Platform API](https://www.daytona.io/docs/en/tools/api.md#daytona) ([OpenAPI spec](https://www.daytona.io/docs/openapi.json))
- [Toolbox API](https://www.daytona.io/docs/en/tools/api.md#daytona-toolbox) ([OpenAPI spec](https://www.daytona.io/docs/toolbox-openapi.json))
- [Analytics API](https://www.daytona.io/docs/en/tools/api.md#daytona-analytics) ([OpenAPI spec](https://www.daytona.io/docs/analytics-openapi.json))
- **Daytona CLI**: [Mac/Linux/Windows](https://www.daytona.io/docs/en/tools/cli.md)
- **Daytona LLMs**: [llms.txt](https://www.daytona.io/docs/llms.txt), [llms-full.txt](https://www.daytona.io/docs/llms-full.txt)
- **Daytona Sitemap**: [sitemap-0.xml](https://www.daytona.io/docs/sitemap-0.xml)
- **Daytona Skills**: [agent skills](https://www.daytona.io/docs/en/agent-skills.md)
```text
npx skills add https://github.com/daytona/skills --skill daytona
```
# Architecture
Daytona platform is organized into multiple plane components, each serving a specific purpose:
- [Interface plane](#interface-plane) provides client interfaces for interacting with Daytona
- [Control plane](#control-plane) orchestrates all sandbox operations
- [Compute plane](#compute-plane) runs and manages sandbox instances
### Interface plane
The interface plane provides client interfaces for users and agents to interact with Daytona. The following components are part of the interface plane and available to all users and agents:
- **SDK**: [Python](https://www.daytona.io/docs/en/python-sdk.md), [TypeScript](https://www.daytona.io/docs/en/typescript-sdk.md), [Ruby](https://www.daytona.io/docs/en/ruby-sdk.md), [Go](https://www.daytona.io/docs/en/go-sdk.md), and [Java](https://www.daytona.io/docs/en/java-sdk.md) SDKs for programmatic access
- [CLI](https://www.daytona.io/docs/en/tools/cli.md): command-line interface for direct sandbox operations
- [Dashboard](https://app.daytona.io/dashboard/): web interface for visual sandbox management and monitoring
- [MCP](https://www.daytona.io/docs/en/mcp.md): Model Context Protocol server for AI tool integrations
- [SSH](https://www.daytona.io/docs/en/ssh-access.md): secure shell access to running sandboxes
### Control plane
The control plane is the central coordination layer of the Daytona platform. It receives all client requests, manages the full sandbox lifecycle, schedules sandboxes onto runners, and continuously reconciles states across the infrastructure. The control plane includes the following components:
- [API](#api) handles authentication, sandbox lifecycle management, and resource allocation
- [Proxy](#proxy) routes external traffic to sandboxes, enabling direct access to services
- [Snapshot builder](#snapshot-builder) builds and manages sandbox [snapshots](https://www.daytona.io/docs/en/snapshots.md)
- [Sandbox manager](#sandbox-manager) handles sandbox lifecycle management and state reconciliation
#### API
The API is a NestJS-based RESTful service that serves as the primary entry point for all platform operations, managing authentication, sandbox lifecycle, snapshots, volumes, and resource allocation. The [snapshot builder](#snapshot-builder) and [sandbox manager](#sandbox-manager) run as internal processes within the API. The API integrates the following internal services and components:
- **Redis** provides caching, session management, and distributed locking
- **PostgreSQL** serves as the primary persistent store for metadata and configuration
- **Auth0/OIDC provider** authenticates users and services via OpenID Connect. The API enforces organization-level multi-tenancy, where each sandbox, snapshot, and volume belongs to an organization, and access control is applied at the organization boundary
- **SMTP server** handles email delivery for organization invitations, account notifications, and alert messages
- [Sandbox manager](#sandbox-manager) schedules sandboxes onto runners, reconciles states, and enforces sandbox lifecycle management policies
- **PostHog** collects platform analytics and usage metrics for monitoring and improvement
To interact with sandboxes from the API, see the [API](https://www.daytona.io/docs/en/tools/api.md) and [Toolbox API](https://www.daytona.io/docs/en/tools/api.md#daytona-toolbox) references.
#### Proxy
The proxy is a dedicated HTTP proxy that routes external traffic to the correct sandbox using host-based routing. Each sandbox is reachable at `{port}-{sandboxId}.{proxy-domain}`, where the port maps to a service running inside the sandbox. The proxy resolves the target runner for a given sandbox, injects authentication headers, and forwards the request. It supports both HTTP and WebSocket protocols.
#### Snapshot builder
The snapshot builder is part of the API process and orchestrates the creation of sandbox [snapshots](https://www.daytona.io/docs/en/snapshots.md) from a Dockerfile or a pre-built image from a [container registry](#container-registry). It coordinates with runners to build or pull images, which are then pushed to an internal snapshot registry that implements the OCI distribution specification.
#### Sandbox manager
The sandbox manager is part of the API process and schedules sandboxes onto runners, reconciles states, and enforces [sandbox lifecycle management](https://www.daytona.io/docs/en/sandboxes.md#sandbox-lifecycle) policies.
### Compute plane
The compute plane is the infrastructure layer where sandboxes run. Sandboxes run on [runners](#sandbox-runners), compute nodes that host multiple sandboxes with dedicated resources and scale horizontally across shared or dedicated [regions](https://www.daytona.io/docs/en/regions.md). The compute plane consists of the following components:
- [Sandbox runners](#sandbox-runners) host sandboxes with dedicated resources
- [Sandbox daemon](#sandbox-daemon) provides code execution and environment access inside each sandbox
- [Snapshot store](#snapshot-store) stores sandbox snapshot images
- [Volumes](#volumes) provides persistent storage shared across sandboxes
#### Sandbox runners
Runners are compute nodes that power Daytona's compute plane, providing the underlying infrastructure for running sandbox workloads. Each runner polls the control plane API for jobs and executes sandbox operations: creating, starting, stopping, destroying, resizing, and backing up sandboxes. Runners interact with S3-compatible object storage for snapshot and volume data, and with the internal snapshot registry.
Each sandbox runs as an isolated instance with its own Linux namespaces for processes, network, filesystem mounts, and inter-process communication. Each runner allocates dedicated vCPU, RAM, and disk resources per sandbox.
#### Sandbox daemon
The sandbox daemon is a code execution agent that runs inside each sandbox. It exposes the [Toolbox API](https://www.daytona.io/docs/en/tools/api.md#daytona-toolbox), providing direct access to the sandbox environment: file system and Git operations, process and code execution, computer use, log streaming, and terminal sessions.
#### Snapshot store
The snapshot store is an internal OCI-compliant registry that stores sandbox snapshot images using the OCI distribution specification. Runners pull snapshot images from this store when creating new sandboxes. The store uses S3-compatible object storage as its backend.
#### Volumes
[Volumes](https://www.daytona.io/docs/en/volumes.md) provide persistent storage that can be shared across sandboxes. Each volume is backed by S3-compatible object storage and mounted into sandboxes as a read-write directory. Multiple sandboxes can mount the same volume simultaneously, allowing data to be shared across sandboxes and persist independently of the sandbox lifecycle.
### Container registry
Container registries serve as the source for sandbox base images. When creating a [snapshot](https://www.daytona.io/docs/en/snapshots.md), the snapshot builder pulls the specified image from an external registry, and pushes it to the internal snapshot registry for use by runners. For Dockerfile-based snapshots, parent images referenced in `FROM` directives are also pulled from the configured source registries during the build. Daytona supports any OCI-compatible registry:
- [Docker Hub](https://www.daytona.io/docs/en/snapshots.md#docker-hub)
- [Google Artifact Registry](https://www.daytona.io/docs/en/snapshots.md#google-artifact-registry)
- [GitHub Container Registry (GHCR)](https://www.daytona.io/docs/en/snapshots.md#github-container-registry)
- [Private registries](https://www.daytona.io/docs/en/snapshots.md#using-images-from-private-registries): any registry that implements the OCI distribution specification
# Sandboxes
Daytona provides **full composable computers** — **sandboxes** — for AI agents.
Sandboxes are isolated runtime environments you can manage programmatically to run code. Each sandbox runs in isolation, giving it a dedicated kernel, filesystem, network stack, and allocated vCPU, RAM, and disk. Agents and developers get access to a full composable computer where they can install packages, run servers, compile code, and manage processes.
Sandboxes run as **Linux containers** by default. Daytona also provides [VM sandboxes](#vm-sandboxes) with a dedicated **Linux VM** or **Windows** operating system, and [GPU sandboxes](#gpu-sandboxes) with **NVIDIA GPU** acceleration for model inference, fine-tuning, and CUDA-accelerated compute.
## Create sandboxes
Create a sandbox.
1. Go to [Daytona Sandboxes ↗](https://app.daytona.io/dashboard/sandboxes)
2. Click **Create Sandbox**
3. Click **Create**
```python
from daytona import Daytona
daytona = Daytona()
sandbox = daytona.create()
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
const sandbox = await daytona.create()
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
sandbox = daytona.create
```
```go
package main
import (
"context"
"github.com/daytona/clients/sdk-go/pkg/daytona"
)
func main() {
client, _ := daytona.NewClient()
ctx := context.Background()
_, _ = client.Create(ctx, nil)
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
Sandbox sandbox = daytona.create();
}
}
}
```
```bash
daytona create [flags]
```
```bash
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{}'
```
### Snapshots
Create a sandbox from a [default snapshot](https://www.daytona.io/docs/en/snapshots.md#default-snapshots).
[Snapshots](https://www.daytona.io/docs/en/snapshots.md) are persistent captures of sandbox state, including the filesystem, installed packages, and settings. They serve as pre-configured environments for creating sandboxes, and you can [capture them from an existing sandbox](#create-snapshot-from-sandbox) to save its state and restore it later.
| **Snapshot** | **vCPU** | **Memory** | **Storage** | **GPU** | **Sandbox Class** |
| ----------------------- | -------- | ---------- | ----------- | ------- | ----------------- |
| **`daytona-small`** | 1 | 1GiB | 3GiB | | Container |
| **`daytona-medium`** | 2 | 4GiB | 8GiB | | Container |
| **`daytona-large`** | 4 | 8GiB | 10GiB | | Container |
| **`daytona-gpu`** | 1 | 1GiB | 1GiB | 1 | GPU |
| **`daytona-vm-small`** | 1 | 1GiB | 3GiB | | Linux VM |
| **`daytona-vm-medium`** | 2 | 4GiB | 8GiB | | Linux VM |
| **`daytona-vm-large`** | 4 | 8GiB | 10GiB | | Linux VM |
| **`windows-small`** | 1 | 4GiB | 30GiB | | Windows |
| **`windows-medium`** | 2 | 8GiB | 50GiB | | Windows |
| **`windows-large`** | 4 | 16GiB | 50GiB | | Windows |
1. Go to [Daytona Sandboxes ↗](https://app.daytona.io/dashboard/sandboxes)
2. Click **Create Sandbox**
3. Select a **`snapshot`**
4. Click **Create**
```python
from daytona import Daytona, CreateSandboxFromSnapshotParams
daytona = Daytona()
sandbox = daytona.create(
CreateSandboxFromSnapshotParams(
snapshot="daytona-small",
)
)
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
const sandbox = await daytona.create({
snapshot: 'daytona-small',
})
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
sandbox = daytona.create(
Daytona::CreateSandboxFromSnapshotParams.new(
snapshot: 'daytona-small'
)
)
```
```go
package main
import (
"context"
"github.com/daytona/clients/sdk-go/pkg/daytona"
"github.com/daytona/clients/sdk-go/pkg/types"
)
func main() {
client, _ := daytona.NewClient()
ctx := context.Background()
params := types.SnapshotParams{
Snapshot: "daytona-small",
}
_, _ = client.Create(ctx, params)
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setSnapshot("daytona-small");
Sandbox sandbox = daytona.create(params);
}
}
}
```
```bash
daytona create --snapshot daytona-small
```
```bash
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
"snapshot": "daytona-small"
}'
```
### Resources
Create a sandbox with custom resources.
Sandboxes have **1 vCPU**, **1GB RAM**, and **3GiB disk** by default. Organizations get a maximum sandbox resource limit of **4 vCPUs**, **8GB RAM**, and **10GB disk**.
| **Resource** | **Unit** | **Default** | **Minimum** | **Maximum** |
| ------------ | -------- | ----------- | ----------- | ----------- |
| CPU | vCPU | **`1`** | **`1`** | **`4`** |
| Memory | GiB | **`1`** | **`1`** | **`8`** |
| Disk | GiB | **`3`** | **`1`** | **`10`** |
1. Go to [Daytona Sandboxes ↗](https://app.daytona.io/dashboard/sandboxes)
2. Click **Create Sandbox**
3. Enter a base **`image`**
4. Set **`resources`** (**`cpu`**, **`memory`**, **`disk`**) to the values within your organization's limits
5. Click **Create**
```python
from daytona import Daytona, CreateSandboxFromImageParams, Image, Resources
daytona = Daytona()
sandbox = daytona.create(
CreateSandboxFromImageParams(
image="ubuntu:22.04",
resources=Resources(cpu=2, memory=4, disk=8),
)
)
```
```typescript
import { Daytona, Image } from '@daytona/sdk'
const daytona = new Daytona()
const sandbox = await daytona.create({
image: Image.base('ubuntu:22.04'),
resources: { cpu: 2, memory: 4, disk: 8 },
})
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
sandbox = daytona.create(
Daytona::CreateSandboxFromImageParams.new(
image: Daytona::Image.base('ubuntu:22.04'),
resources: Daytona::Resources.new(
cpu: 2,
memory: 4,
disk: 8
)
)
)
```
```go
package main
import (
"context"
"github.com/daytona/clients/sdk-go/pkg/daytona"
"github.com/daytona/clients/sdk-go/pkg/types"
)
func main() {
client, _ := daytona.NewClient()
ctx := context.Background()
_, _ = client.Create(ctx, types.ImageParams{
Image: "ubuntu:22.04",
Resources: &types.Resources{
CPU: 2,
Memory: 4,
Disk: 8,
},
})
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromImageParams;
import io.daytona.sdk.model.Resources;
final class CreateSandboxResources {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
CreateSandboxFromImageParams params = new CreateSandboxFromImageParams();
params.setImage("ubuntu:22.04");
Resources resources = new Resources();
resources.setCpu(2);
resources.setMemory(4);
resources.setDisk(8);
params.setResources(resources);
Sandbox sandbox = daytona.create(params);
}
}
}
```
```bash
daytona create --cpu 2 --memory 4 --disk 8
```
```bash
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
"image": "ubuntu:22.04",
"cpu": 2,
"memory": 4,
"disk": 8
}'
```
### Languages
Create a sandbox with a specific language runtime.
Daytona sandboxes support **Python**, **TypeScript**, and **JavaScript** programming language runtimes for direct code execution inside the sandbox. The `language` parameter controls which programming language runtime is used for the sandbox. If omitted, it defaults to `python`.
- **`python`**
- **`typescript`**
- **`javascript`**
```python
from daytona import Daytona, CreateSandboxFromSnapshotParams
daytona = Daytona()
# Python runtime (default)
sandbox = daytona.create(CreateSandboxFromSnapshotParams(language="python"))
response = sandbox.process.code_run('print("Hello from Python")')
print(response.result)
# TypeScript runtime
sandbox = daytona.create(CreateSandboxFromSnapshotParams(language="typescript"))
response = sandbox.process.code_run('console.log("Hello from TypeScript")')
print(response.result)
# JavaScript runtime
sandbox = daytona.create(CreateSandboxFromSnapshotParams(language="javascript"))
response = sandbox.process.code_run('console.log("Hello from JavaScript")')
print(response.result)
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
// Python runtime (default)
let sandbox = await daytona.create({ language: 'python' })
let response = await sandbox.process.codeRun('print("Hello from Python")')
console.log(response.result)
// TypeScript runtime
sandbox = await daytona.create({ language: 'typescript' })
response = await sandbox.process.codeRun('console.log("Hello from TypeScript")')
console.log(response.result)
// JavaScript runtime
sandbox = await daytona.create({ language: 'javascript' })
response = await sandbox.process.codeRun('console.log("Hello from JavaScript")')
console.log(response.result)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
# Python runtime (default)
sandbox = daytona.create(Daytona::CreateSandboxFromSnapshotParams.new(
language: Daytona::CodeLanguage::PYTHON
))
response = sandbox.process.code_run(code: 'print("Hello from Python")')
puts response.result
# TypeScript runtime
sandbox = daytona.create(Daytona::CreateSandboxFromSnapshotParams.new(
language: Daytona::CodeLanguage::TYPESCRIPT
))
response = sandbox.process.code_run(code: 'console.log("Hello from TypeScript")')
puts response.result
# JavaScript runtime
sandbox = daytona.create(Daytona::CreateSandboxFromSnapshotParams.new(
language: Daytona::CodeLanguage::JAVASCRIPT
))
response = sandbox.process.code_run(code: 'console.log("Hello from JavaScript")')
puts response.result
```
```go
package main
import (
"context"
"fmt"
"log"
"github.com/daytona/clients/sdk-go/pkg/daytona"
"github.com/daytona/clients/sdk-go/pkg/types"
)
func main() {
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
// Python runtime (default)
sandbox, err := client.Create(ctx, types.SnapshotParams{
SandboxBaseParams: types.SandboxBaseParams{Language: types.CodeLanguagePython},
})
if err != nil {
log.Fatal(err)
}
result, err := sandbox.Process.CodeRun(ctx, `print("Hello from Python")`)
if err != nil {
log.Fatal(err)
}
fmt.Println(result.Result)
// TypeScript runtime
sandbox, err = client.Create(ctx, types.SnapshotParams{
SandboxBaseParams: types.SandboxBaseParams{Language: types.CodeLanguageTypeScript},
})
if err != nil {
log.Fatal(err)
}
result, err = sandbox.Process.CodeRun(ctx, `console.log("Hello from TypeScript")`)
if err != nil {
log.Fatal(err)
}
fmt.Println(result.Result)
// JavaScript runtime
sandbox, err = client.Create(ctx, types.SnapshotParams{
SandboxBaseParams: types.SandboxBaseParams{Language: types.CodeLanguageJavaScript},
})
if err != nil {
log.Fatal(err)
}
result, err = sandbox.Process.CodeRun(ctx, `console.log("Hello from JavaScript")`)
if err != nil {
log.Fatal(err)
}
fmt.Println(result.Result)
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
import io.daytona.sdk.model.ExecuteResponse;
Daytona daytona = new Daytona();
// Python runtime (default)
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setLanguage("python");
Sandbox sandbox = daytona.create(params);
ExecuteResponse response = sandbox.process.codeRun("print(\"Hello from Python\")");
System.out.println(response.getResult());
// TypeScript runtime
params = new CreateSandboxFromSnapshotParams();
params.setLanguage("typescript");
sandbox = daytona.create(params);
response = sandbox.process.codeRun("console.log(\"Hello from TypeScript\")");
System.out.println(response.getResult());
// JavaScript runtime
params = new CreateSandboxFromSnapshotParams();
params.setLanguage("javascript");
sandbox = daytona.create(params);
response = sandbox.process.codeRun("console.log(\"Hello from JavaScript\")");
System.out.println(response.getResult());
```
```bash
# Python runtime (default)
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
"language": "python"
}'
# TypeScript runtime
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
"language": "typescript"
}'
# JavaScript runtime
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
"language": "javascript"
}'
```
## GPU sandboxes
Daytona provides **GPU sandboxes** for workloads that require NVIDIA GPU acceleration, such as model inference, fine-tuning, and CUDA-accelerated compute. GPU sandboxes are ephemeral and support up to **16 vCPUs**, **192GB RAM**, and **512GB disk**. Supported GPU types:
- **NVIDIA H100**
- **NVIDIA H200**
- **NVIDIA RTX Pro 6000**
- **NVIDIA RTX 4090**
- **NVIDIA RTX 5090**
> Due to possible events of temporary GPU scarcity, the target/region requested for GPU sandboxes is ignored by default. If you need access to a specific geographical location, contact us at support@daytona.io.
Create a GPU sandbox from a default snapshot.
1. Go to [Daytona Sandboxes ↗](https://app.daytona.io/dashboard/sandboxes)
2. Click **Create Sandbox**
3. Select a **`daytona-gpu`** snapshot
4. Select **`ephemeral`** or set **`auto-delete interval`** to **`0`**
5. Click **Create**
```python
from daytona import Daytona, CreateSandboxFromSnapshotParams
daytona = Daytona()
sandbox = daytona.create(
CreateSandboxFromSnapshotParams(
snapshot="daytona-gpu",
auto_delete_interval=0,
),
)
```
```typescript
import { Daytona } from "@daytona/sdk";
const daytona = new Daytona()
const sandbox = await daytona.create({
snapshot: "daytona-gpu",
ephemeral: true,
});
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
sandbox = daytona.create(
Daytona::CreateSandboxFromSnapshotParams.new(
snapshot: 'daytona-gpu',
ephemeral: true
)
)
```
```go
package main
import (
"context"
"github.com/daytona/clients/sdk-go/pkg/daytona"
"github.com/daytona/clients/sdk-go/pkg/types"
)
func main() {
client, _ := daytona.NewClient()
ctx := context.Background()
params := types.SnapshotParams{
Snapshot: "daytona-gpu",
SandboxBaseParams: types.SandboxBaseParams{
Ephemeral: true,
},
}
_, _ = client.Create(ctx, params)
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setSnapshot("daytona-gpu");
params.setAutoDeleteInterval(0);
Sandbox sandbox = daytona.create(params);
}
}
}
```
```bash
daytona create --snapshot daytona-gpu --auto-delete 0
```
```bash
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
"snapshot": "daytona-gpu",
"autoDeleteInterval": 0
}'
```
Create a GPU sandbox with custom GPU resources: units and types.
1. Create a sandbox from an **`image`**
2. Set the **`auto-delete interval`** to **`0`** (ephemeral)
3. Set the **`GPU`** to the number of GPU units
4. Specify the **`GPU type`**(s):
The GPU type field accepts a single value or an ordered list of preferred types.
Daytona uses the first available type in the order you provide. This lets you fall back from a preferred GPU to an alternative when the first choice is not available.
- **`H100`**
- **`H200`**
- **`RTX-PRO-6000`**
- **`RTX-4090`**
- **`RTX-5090`**
```python
from daytona import Daytona, CreateSandboxFromImageParams, Image, Resources, GpuType
daytona = Daytona()
sandbox = daytona.create(
CreateSandboxFromImageParams(
image=Image.debian_slim("3.12"),
auto_delete_interval=0,
resources=Resources(
gpu=1,
gpu_type=[GpuType.H100, GpuType.RTX_PRO_6000],
),
)
)
```
```typescript
import { Daytona, GpuType, Image } from "@daytona/sdk";
const daytona = new Daytona()
const sandbox = await daytona.create({
image: Image.debianSlim("3.12"),
autoDeleteInterval: 0,
resources: {
gpu: 1,
gpuType: [GpuType.H100, GpuType.RTX_PRO_6000],
},
});
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
sandbox = daytona.create(
Daytona::CreateSandboxFromImageParams.new(
image: Daytona::Image.debian_slim('3.12'),
auto_delete_interval: 0,
resources: Daytona::Resources.new(
gpu: 1,
gpu_type: [Daytona::GpuType::H100, Daytona::GpuType::RTX_PRO_6000]
)
)
)
```
```go
package main
import (
"context"
"github.com/daytona/clients/sdk-go/pkg/daytona"
"github.com/daytona/clients/sdk-go/pkg/types"
)
func main() {
client, _ := daytona.NewClient()
ctx := context.Background()
autoDelete := 0
_, _ = client.Create(ctx, types.ImageParams{
Image: "python:3.12",
SandboxBaseParams: types.SandboxBaseParams{
AutoDeleteInterval: &autoDelete,
},
Resources: &types.Resources{
GPU: 1,
GpuType: []types.GpuType{types.GpuTypeH100, types.GpuTypeRtxPro6000},
},
})
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromImageParams;
import io.daytona.sdk.model.Resources;
import io.daytona.api.client.model.GpuType;
import java.util.List;
final class CreateGpuSandbox {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
CreateSandboxFromImageParams params = new CreateSandboxFromImageParams();
params.setImage("python:3.12");
params.setAutoDeleteInterval(0);
Resources resources = new Resources();
resources.setGpu(1);
resources.setGpuType(List.of(GpuType.H100, GpuType.RTX_PRO_6000));
params.setResources(resources);
Sandbox sandbox = daytona.create(params);
}
}
}
```
```bash
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
"image": "python:3.12",
"autoDeleteInterval": 0,
"gpu": 1,
"gpuType": ["H100", "RTX-PRO-6000"]
}'
```
## VM sandboxes
Daytona provides **VM sandboxes** for workloads that require a full virtual machine with a dedicated **Linux** or **Windows** operating system. VM sandboxes are distinct from container sandboxes and support VM-only capabilities:
- [Fork sandboxes](#fork-sandboxes)
- [Pause/resume sandboxes](#pause--resume-sandboxes)
- [Create snapshot from sandbox](#create-snapshot-from-sandbox)
:::note[Limitations]
VM sandboxes can currently only be created from existing VM snapshots. Dynamic builds through the declarative builder are supported for container sandboxes only.
:::
### Linux VM
Create a Linux VM sandbox.
Create a Linux VM sandbox from a default snapshot.
1. Go to [Daytona Sandboxes ↗](https://app.daytona.io/dashboard/sandboxes)
2. Click **Create Sandbox**
3. Select a Linux VM snapshot:
- **`daytona-vm-small`**
- **`daytona-vm-medium`**
- **`daytona-vm-large`**
4. Click **Create**
```python
from daytona import Daytona, CreateSandboxFromSnapshotParams
daytona = Daytona()
sandbox = daytona.create(CreateSandboxFromSnapshotParams(snapshot="daytona-vm-small"))
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
const sandbox = await daytona.create({ snapshot: 'daytona-vm-small' })
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
sandbox = daytona.create(
Daytona::CreateSandboxFromSnapshotParams.new(
snapshot: 'daytona-vm-small'
)
)
```
```go
package main
import (
"context"
"github.com/daytona/clients/sdk-go/pkg/daytona"
"github.com/daytona/clients/sdk-go/pkg/types"
)
func main() {
client, _ := daytona.NewClient()
ctx := context.Background()
params := types.SnapshotParams{
Snapshot: "daytona-vm-small",
}
_, _ = client.Create(ctx, params)
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setSnapshot("daytona-vm-small");
Sandbox sandbox = daytona.create(params);
}
}
}
```
```bash
daytona create --snapshot daytona-vm-small
```
```bash
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
"snapshot": "daytona-vm-small"
}'
```
Create a Linux VM sandbox from a custom snapshot.
1. Create a snapshot from a base **`image`**
2. Set sandbox class to **`LINUX_VM`**
3. Create a Linux VM sandbox from the snapshot
```python
from daytona import (
Daytona,
CreateSnapshotParams,
CreateSandboxFromSnapshotParams,
SandboxClass,
)
daytona = Daytona()
# 1. Create a VM snapshot (linux-vm class)
daytona.snapshot.create(
CreateSnapshotParams(
name="my-vm-snapshot",
image="ubuntu:22.04",
sandbox_class=SandboxClass.LINUX_VM,
)
)
# 2. Create a VM sandbox from the snapshot
sandbox = daytona.create(CreateSandboxFromSnapshotParams(snapshot="my-vm-snapshot"))
```
```typescript
import { Daytona, SandboxClass } from "@daytona/sdk";
const daytona = new Daytona();
// 1. Create a VM snapshot (linux-vm class)
await daytona.snapshot.create({
name: "my-vm-snapshot",
image: "ubuntu:22.04",
sandboxClass: SandboxClass.LINUX_VM,
});
// 2. Create a VM sandbox from the snapshot
const sandbox = await daytona.create({ snapshot: "my-vm-snapshot" });
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
# 1. Create a VM snapshot (linux-vm class)
daytona.snapshot.create(
Daytona::CreateSnapshotParams.new(
name: 'my-vm-snapshot',
image: 'ubuntu:22.04',
sandbox_class: DaytonaApiClient::SandboxClass::LINUX_VM
)
)
# 2. Create a VM sandbox from the snapshot
sandbox = daytona.create(Daytona::CreateSandboxFromSnapshotParams.new(snapshot: 'my-vm-snapshot'))
```
```go
package main
import (
"context"
"github.com/daytona/clients/sdk-go/pkg/daytona"
"github.com/daytona/clients/sdk-go/pkg/types"
)
func main() {
client, _ := daytona.NewClient()
ctx := context.Background()
// 1. Create a VM snapshot (linux-vm class)
sandboxClass := types.SandboxClassLinuxVM
_, logCh, _ := client.Snapshot.Create(ctx, &types.CreateSnapshotParams{
Name: "my-vm-snapshot",
Image: "ubuntu:22.04",
SandboxClass: &sandboxClass,
})
for range logCh {
}
// 2. Create a VM sandbox from the snapshot
_, _ = client.Create(ctx, types.SnapshotParams{
Snapshot: "my-vm-snapshot",
})
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.api.client.model.SandboxClass;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
// 1. Create a VM snapshot (linux-vm class)
daytona.snapshot().create("my-vm-snapshot", "ubuntu:22.04", SandboxClass.LINUX_VM);
// 2. Create a VM sandbox from the snapshot
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setSnapshot("my-vm-snapshot");
Sandbox sandbox = daytona.create(params);
}
}
}
```
```bash
# 1. Create a VM snapshot (linux-vm class)
daytona snapshot create my-vm-snapshot --image ubuntu:22.04 --sandbox-class linux-vm
# 2. Create a VM sandbox from the snapshot
daytona create --snapshot my-vm-snapshot
```
```bash
# 1. Create a VM snapshot (linux-vm class)
curl https://app.daytona.io/api/snapshots \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
"name": "my-vm-snapshot",
"imageName": "ubuntu:22.04",
"sandboxClass": "linux-vm"
}'
# 2. Create a VM sandbox from the snapshot
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
"snapshot": "my-vm-snapshot"
}'
```
### Windows
Create a Windows sandbox.
1. Go to [Daytona Sandboxes ↗](https://app.daytona.io/dashboard/sandboxes)
2. Click **Create Sandbox**
3. Select a Windows snapshot:
- **`windows-small`**
- **`windows-medium`**
- **`windows-large`**
4. Click **Create**
```python
from daytona import Daytona, CreateSandboxFromSnapshotParams
daytona = Daytona()
sandbox = daytona.create(
CreateSandboxFromSnapshotParams(
snapshot="windows-small",
)
)
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
const sandbox = await daytona.create({
snapshot: 'windows-small',
})
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
sandbox = daytona.create(
Daytona::CreateSandboxFromSnapshotParams.new(
snapshot: 'windows-small'
)
)
```
```go
package main
import (
"context"
"github.com/daytona/clients/sdk-go/pkg/daytona"
"github.com/daytona/clients/sdk-go/pkg/types"
)
func main() {
client, _ := daytona.NewClient()
ctx := context.Background()
params := types.SnapshotParams{
Snapshot: "windows-small",
}
_, _ = client.Create(ctx, params)
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setSnapshot("windows-small");
Sandbox sandbox = daytona.create(params);
}
}
}
```
```bash
daytona create --snapshot windows-small
```
```bash
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
"snapshot": "windows-small"
}'
```
## Ephemeral sandboxes
Create an ephemeral sandbox.
Ephemeral sandboxes are automatically deleted once they are stopped.
1. Go to [Daytona Sandboxes ↗](https://app.daytona.io/dashboard/sandboxes)
2. Click **Create Sandbox**
3. Set **Ephemeral** to **`True`** or set the [auto-delete interval](#auto-delete-interval) to **`0`**
4. Click **Create**
```python
from daytona import Daytona, CreateSandboxFromSnapshotParams
daytona = Daytona()
params = CreateSandboxFromSnapshotParams(
ephemeral=True,
auto_stop_interval=5,
)
sandbox = daytona.create(params)
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
const sandbox = await daytona.create({
ephemeral: true,
autoStopInterval: 5,
})
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
params = Daytona::CreateSandboxFromSnapshotParams.new(
ephemeral: true,
auto_stop_interval: 5
)
sandbox = daytona.create(params)
```
```go
package main
import (
"context"
"github.com/daytona/clients/sdk-go/pkg/daytona"
"github.com/daytona/clients/sdk-go/pkg/types"
)
func main() {
client, _ := daytona.NewClient()
ctx := context.Background()
autoStopInterval := 5
params := types.SnapshotParams{
SandboxBaseParams: types.SandboxBaseParams{
Language: types.CodeLanguagePython,
Ephemeral: true,
AutoStopInterval: &autoStopInterval,
},
}
_, _ = client.Create(ctx, params)
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setAutoDeleteInterval(0);
params.setAutoStopInterval(5);
Sandbox sandbox = daytona.create(params);
}
}
}
```
```bash
daytona create --auto-delete 0 --auto-stop 5
```
```bash
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
"autoDeleteInterval": 0,
"autoStopInterval": 5
}'
```
## Linked sandboxes
Create a linked sandbox.
Linked sandboxes are attached to an existing parent sandbox at creation time.
- **Lifecycle**
Linked sandboxes are always ephemeral and cannot be persisted or resumed after stop. The [auto-delete interval](#auto-delete-interval) must be exactly `0` on create; this is enforced, not a default. The [auto-stop interval](#auto-stop-interval) sets the idle period in minutes after which the child sandbox stops. Once stopped, linked children are auto-deleted. Deleting the parent deletes all of its linked children (cascade). One parent may have many linked children (1:N).
- **Networking**
Linked sandboxes share an internal link network. Connections work in both directions: the parent can reach each child and each child can reach the parent. Every sandbox on the link network is registered under its sandbox name and ID as DNS aliases, so either works as the host. For example: `telnet LINKED_SANDBOX_ID 5555` from the parent reaches port `5555` on the linked child sandbox.
1. Create a parent sandbox
2. Create one or more child sandboxes that reference the parent's sandbox ID. This records the relationship on the child sandbox as the linked sandbox ID. Omitting the linked sandbox parameter yields an unlinked sandbox.
```python
from daytona import CreateSandboxFromSnapshotParams, Daytona
daytona = Daytona()
parent = daytona.create()
child = daytona.create(
CreateSandboxFromSnapshotParams(
linked_sandbox=parent.id,
ephemeral=True,
)
)
# The link network registers each sandbox under its name as a DNS alias
response = child.process.exec(f"curl http://{parent.name}:3000/")
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
const parent = await daytona.create()
const child = await daytona.create({
linkedSandbox: parent.id,
ephemeral: true,
})
// The link network registers each sandbox under its name as a DNS alias
const response = await child.process.executeCommand(
`curl http://${parent.name}:3000/`
)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
parent = daytona.create
child = daytona.create(
Daytona::CreateSandboxFromSnapshotParams.new(
linked_sandbox: parent.id,
ephemeral: true
)
)
# The link network registers each sandbox under its name and ID as DNS aliases.
# The Ruby SDK does not expose the sandbox name, so address the parent by ID.
response = child.process.exec(command: "curl http://#{parent.id}:3000/")
```
```go
package main
import (
"context"
"fmt"
"github.com/daytona/clients/sdk-go/pkg/daytona"
"github.com/daytona/clients/sdk-go/pkg/types"
)
func main() {
client, _ := daytona.NewClient()
ctx := context.Background()
parent, _ := client.Create(ctx, types.SnapshotParams{})
child, _ := client.Create(ctx, types.SnapshotParams{
SandboxBaseParams: types.SandboxBaseParams{
LinkedSandbox: parent.ID,
Ephemeral: true,
},
})
// The link network registers each sandbox under its name as a DNS alias
response, _ := child.Process.ExecuteCommand(ctx, fmt.Sprintf("curl http://%s:3000/", parent.Name))
fmt.Println(response.Result)
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
import io.daytona.sdk.model.ExecuteResponse;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
Sandbox parent = daytona.create();
CreateSandboxFromSnapshotParams childParams = new CreateSandboxFromSnapshotParams();
childParams.setLinkedSandbox(parent.getId());
childParams.setAutoDeleteInterval(0); // linked sandboxes must be ephemeral
Sandbox child = daytona.create(childParams);
// The link network registers each sandbox under its name as a DNS alias
ExecuteResponse response = child.getProcess()
.executeCommand("curl http://" + parent.getName() + ":3000/");
}
}
}
```
```bash
# Create parent sandbox
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{}'
# Create linked child sandbox (replace PARENT_SANDBOX_ID with the id from the first response)
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
"linkedSandbox": "PARENT_SANDBOX_ID",
"autoDeleteInterval": 0
}'
```
## Start sandboxes
Start a sandbox.
1. Go to [Daytona Sandboxes ↗](https://app.daytona.io/dashboard/sandboxes)
2. Click the start icon (**▶**) next to the sandbox you want to start
```python
sandbox.start()
```
```typescript
await sandbox.start()
```
```ruby
sandbox.start
```
```go
sandbox.Start(ctx)
```
```java
sandbox.start();
```
```bash
daytona start [SANDBOX_ID] | [SANDBOX_NAME] [flags]
```
```bash
curl 'https://app.daytona.io/api/sandbox/{sandboxIdOrName}/start' \
--request POST \
--header 'Authorization: Bearer YOUR_API_KEY'
```
## Get sandbox
Get a sandbox by ID or name.
```python
sandbox = daytona.get("my-sandbox-id-or-name")
```
```typescript
const sandbox = await daytona.get('my-sandbox-id-or-name')
```
```ruby
sandbox = daytona.get('my-sandbox-id-or-name')
```
```go
sandbox, err := client.Get(ctx, "my-sandbox-id-or-name")
```
```java
Sandbox sandbox = daytona.get("my-sandbox-id-or-name");
```
```bash
daytona info [SANDBOX_ID] | [SANDBOX_NAME] [flags]
```
```bash
curl 'https://app.daytona.io/api/sandbox/{sandboxIdOrName}' \
--header 'Authorization: Bearer YOUR_API_KEY'
```
## List sandboxes
List sandboxes.
```python
for sandbox in daytona.list():
print(sandbox.id)
```
```typescript
for await (const sandbox of daytona.list()) {
console.log(sandbox.id)
}
```
```ruby
daytona.list.each { |sandbox| puts sandbox.id }
```
```go
iter := client.List(ctx, nil)
defer iter.Close()
for iter.Next() {
sandbox := iter.Value()
fmt.Println(sandbox.ID)
}
if err := iter.Err(); err != nil {
log.Fatal(err)
}
```
```java
Iterator> iter = daytona.list();
while (iter.hasNext()) {
Map sandbox = iter.next();
System.out.println(sandbox.get("id"));
}
```
```bash
daytona list [flags]
```
```bash
curl 'https://app.daytona.io/api/sandbox' \
--header 'Authorization: Bearer YOUR_API_KEY'
```
## Stop sandboxes
For [container sandboxes](#create-sandboxes), stopping terminates the running container. The filesystem is preserved, but memory state is not. Container sandboxes do not support pause; stop is the way to shut down a container sandbox when it is not in use.
For [VM sandboxes](#vm-sandboxes) (Linux and Windows), stopping shuts down the virtual machine while preserving the filesystem, and memory state is cleared. To preserve running process state without consuming CPU, use [pause / resume](#pause--resume-sandboxes) instead.
The sandbox moves to the **stopped** state when shutdown completes. While a stop is in progress, the sandbox is in the **stopping** state and does not accept new requests.
1. Go to [Daytona Sandboxes ↗](https://app.daytona.io/dashboard/sandboxes)
2. Click the stop icon (**⏹**) next to the sandbox you want to stop
```python
sandbox.stop()
```
```typescript
await sandbox.stop()
```
```ruby
sandbox.stop
```
```go
sandbox.Stop(ctx)
```
```java
sandbox.stop();
```
```bash
daytona stop [SANDBOX_ID] | [SANDBOX_NAME] [flags]
```
```bash
curl 'https://app.daytona.io/api/sandbox/{sandboxIdOrName}/stop' \
--request POST \
--header 'Authorization: Bearer YOUR_API_KEY'
```
:::note[Force stop]
If you need a faster shutdown, use force stop (`force=true` / `--force`) to terminate the sandbox immediately. Force stop is ungraceful and should be used when quick termination is more important than process cleanup. Avoid force stop for normal shutdowns where the process should flush buffers, write final state, or run cleanup.
:::
## Archive sandboxes
Archive moves a stopped container sandbox's filesystem to object storage and free disk quota. Archive is supported for [container sandboxes](#create-sandboxes) only.
[VM sandboxes](#vm-sandboxes) (Linux and Windows) do not support archive. Stopping a VM sandbox already offloads filesystem state and releases disk quota, so a separate archive step is not needed.
1. Ensure the sandbox is **stopped**
2. **Archive** the sandbox
3. Wait for the sandbox to reach the **archived** state
4. **Start** the sandbox again when you need to use it
```python
sandbox.archive()
```
```typescript
await sandbox.archive()
```
```ruby
sandbox.archive
```
```go
sandbox.Archive(ctx)
```
```bash
daytona archive [SANDBOX_ID] | [SANDBOX_NAME] [flags]
```
```bash
curl 'https://app.daytona.io/api/sandbox/{sandboxIdOrName}/archive' \
--request POST \
--header 'Authorization: Bearer YOUR_API_KEY'
```
## Pause / resume sandboxes
For [container sandboxes](#create-sandboxes), pause is not supported. The filesystem can be preserved on [stop](#stop-sandboxes), but memory state is not. Use stop to shut down a container sandbox when it is not in use.
For [VM sandboxes](#vm-sandboxes) (Linux and Windows), pausing freezes the virtual machine. The filesystem and memory state are preserved, and CPU is no longer consumed.
1. Ensure the VM sandbox is **started**
2. **Pause** the VM sandbox
3. Wait for the VM sandbox to reach the **paused** state
4. **Resume** (start) the VM sandbox again when you need to resume it
```python
sandbox.pause()
```
```typescript
await sandbox.pause()
```
```ruby
sandbox.pause
```
```go
sandbox.Pause(ctx)
```
```java
sandbox.pause();
```
```bash
daytona pause [SANDBOX_ID] | [SANDBOX_NAME]
```
```bash
curl 'https://app.daytona.io/api/sandbox/{sandboxIdOrName}/pause' \
--request POST \
--header 'Authorization: Bearer YOUR_API_KEY'
```
## Recover sandboxes
Recover a sandbox.
1. Ensure the sandbox is in **error** state
2. Check that the sandbox is **recoverable**
3. Resolve any underlying issue that requires user intervention
4. **Recover** the sandbox and wait for it to be ready
```python
# Check if the sandbox is recoverable
if sandbox.recoverable:
sandbox.recover()
```
```python
sandbox.recover()
```
```typescript
// Check if the sandbox is recoverable
if (sandbox.recoverable) {
await sandbox.recover()
}
```
```typescript
await sandbox.recover()
```
```ruby
# Check if the sandbox is in an error state before recovering
if sandbox.state == 'error'
sandbox.recover
end
```
```ruby
sandbox.recover
```
```bash
curl 'https://app.daytona.io/api/sandbox/{sandboxIdOrName}/recover' \
--request POST \
--header 'Authorization: Bearer YOUR_API_KEY'
```
```bash
curl 'https://app.daytona.io/api/sandbox/{sandboxIdOrName}/recover' \
--request POST \
--header 'Authorization: Bearer YOUR_API_KEY'
```
## Resize sandboxes
Resizing updates the sandbox resource allocation (`cpu`, `memory`, and `disk`) for that sandbox. CPU and memory control compute capacity for running workloads, while disk controls persistent filesystem capacity.
On a running sandbox, you can increase CPU and memory without interruption. To decrease CPU or memory, or to increase disk capacity, stop the sandbox first. Disk size can only be increased and cannot be decreased.
1. Choose the new **CPU**, **memory**, and **disk** values within your organization's limits
2. Ensure the sandbox is **stopped** if you need to decrease CPU or memory, or increase disk
3. **Resize** the sandbox with the new resource values
4. **Start** the sandbox
```python
# Resize a started sandbox (CPU and memory can be increased)
sandbox.resize(Resources(cpu=2, memory=4))
# Resize a stopped sandbox (CPU and memory can change, disk can only increase)
sandbox.stop()
sandbox.resize(Resources(cpu=4, memory=8, disk=20))
sandbox.start()
```
```typescript
// Resize a started sandbox (CPU and memory can be increased)
await sandbox.resize({ cpu: 2, memory: 4 })
// Resize a stopped sandbox (CPU and memory can change, disk can only increase)
await sandbox.stop()
await sandbox.resize({ cpu: 4, memory: 8, disk: 20 })
await sandbox.start()
```
```ruby
# Resize a started sandbox (CPU and memory can be increased)
sandbox.resize(Daytona::Resources.new(cpu: 2, memory: 4))
# Resize a stopped sandbox (CPU and memory can change, disk can only increase)
sandbox.stop
sandbox.resize(Daytona::Resources.new(cpu: 4, memory: 8, disk: 20))
sandbox.start
```
```go
// Resize a started sandbox (CPU and memory can be increased)
err := sandbox.Resize(ctx, &types.Resources{CPU: 2, Memory: 4})
// Resize a stopped sandbox (CPU and memory can change, disk can only increase)
err = sandbox.Stop(ctx)
err = sandbox.Resize(ctx, &types.Resources{CPU: 4, Memory: 8, Disk: 20})
err = sandbox.Start(ctx)
```
```bash
curl 'https://app.daytona.io/api/sandbox/{sandboxIdOrName}/resize' \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
"cpu": 2,
"memory": 4,
"disk": 20
}'
```
To verify CPU and memory limits inside the sandbox after resizing, read `cgroup` values directly. Tools such as `nproc`, `free`, `top`, `htop`, `/proc/cpuinfo`, and `/proc/meminfo` read host-level values and do not reflect sandbox resource limits.
```bash
cat /sys/fs/cgroup/cpu.max # " " (cores = quota / period)
cat /sys/fs/cgroup/memory.max # bytes
df -h / # disk
```
## Label sandboxes
Set sandbox labels.
1. Go to [Daytona Sandboxes ↗](https://app.daytona.io/dashboard/sandboxes)
2. Click **Create Sandbox**
3. Click **Add Labels**
4. Enter the labels in key-value pairs
```python
sandbox.set_labels({
"team": "platform",
"env": "staging",
})
```
```typescript
await sandbox.setLabels({
team: 'platform',
env: 'staging',
})
```
```ruby
sandbox.labels = {
team: 'platform',
env: 'staging'
}
```
```go
err := sandbox.SetLabels(ctx, map[string]string{
"team": "platform",
"env": "staging",
})
```
```java
Map labels = new HashMap<>();
labels.put("team", "platform");
labels.put("env", "staging");
sandbox.setLabels(labels);
```
```bash
curl 'https://app.daytona.io/api/sandbox/{sandboxIdOrName}/labels' \
--request PUT \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
"labels": {
"team": "platform",
"env": "staging"
}
}'
```
## Delete sandboxes
Delete a sandbox.
By default `delete` is fire-and-forget: it returns as soon as the API accepts the deletion request, without waiting for the sandbox to be destroyed. Pass the `wait` flag to block until the sandbox reaches the destroyed state.
1. Go to [Daytona Sandboxes ↗](https://app.daytona.io/dashboard/sandboxes)
2. Click the **Delete** button next to the sandbox you want to delete.
```python
sandbox.delete()
# Block until the sandbox is destroyed
sandbox.delete(timeout=60, wait=True)
```
```typescript
await sandbox.delete()
// Block until the sandbox is destroyed
await sandbox.delete(60, true)
```
```ruby
sandbox.delete
# Block until the sandbox is destroyed
sandbox.delete(60, wait: true)
```
```go
err = sandbox.Delete(ctx)
// Block until the sandbox is destroyed
err = sandbox.DeleteAndWait(ctx, 60*time.Second)
```
```java
sandbox.delete();
// Block until the sandbox is destroyed
sandbox.delete(60, true);
```
```bash
daytona delete [SANDBOX_ID] | [SANDBOX_NAME] [flags]
```
```bash
curl 'https://app.daytona.io/api/sandbox/{sandboxIdOrName}' \
--request DELETE \
--header 'Authorization: Bearer YOUR_API_KEY'
```
## Create snapshot from sandbox
Container sandboxes capture filesystem state only (**cold snapshot**). VM sandboxes capture filesystem and memory state (**hot snapshot**) through the `includeMemory` parameter:
| **Snapshot type** | **Include memory** | **Snapshot contents** | **Required sandbox state** |
| ----------------- | --------------------- | --------------------- | -------------------------- |
| Cold | **`false`** (default) | Filesystem only | Stopped |
| Hot | **`true`** | Filesystem and memory | Started |
```python
# Cold snapshot (filesystem only, sandbox stopped)
sandbox._experimental_create_snapshot("my-snapshot")
# Hot snapshot (filesystem and memory, sandbox running)
sandbox._experimental_create_snapshot("my-vm-snapshot", include_memory=True)
```
```typescript
// Cold snapshot (filesystem only, sandbox stopped)
await sandbox._experimental_createSnapshot('my-snapshot')
// Hot snapshot (filesystem and memory, sandbox running)
await sandbox._experimental_createSnapshot('my-vm-snapshot', 60, true)
```
```ruby
# Cold snapshot (filesystem only, sandbox stopped)
sandbox.experimental_create_snapshot(name: 'my-snapshot')
# Hot snapshot (filesystem and memory, sandbox running)
sandbox.experimental_create_snapshot(name: 'my-vm-snapshot', include_memory: true)
```
```go
// Cold snapshot (filesystem only, sandbox stopped)
err := sandbox.ExperimentalCreateSnapshot(ctx, "my-snapshot")
if err != nil {
return err
}
// Hot snapshot (filesystem and memory, sandbox running)
err = sandbox.ExperimentalCreateSnapshotWithMemory(ctx, "my-vm-snapshot", 60*time.Second)
if err != nil {
return err
}
```
```java
// Cold snapshot (filesystem only, sandbox stopped)
sandbox.experimentalCreateSnapshot("my-snapshot");
// Hot snapshot (filesystem and memory, sandbox running)
sandbox.experimentalCreateSnapshot("my-vm-snapshot", 60, true);
```
```bash
# Cold snapshot (filesystem only, sandbox stopped)
curl 'https://app.daytona.io/api/sandbox/{sandboxIdOrName}/snapshot' \
--request POST \
--header 'X-Daytona-Organization-ID: YOUR_ORGANIZATION_ID' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
"name": "my-snapshot",
"includeMemory": false
}'
# Hot snapshot (filesystem and memory, sandbox running)
curl 'https://app.daytona.io/api/sandbox/{sandboxIdOrName}/snapshot' \
--request POST \
--header 'X-Daytona-Organization-ID: YOUR_ORGANIZATION_ID' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
"name": "my-vm-snapshot",
"includeMemory": true
}'
```
## Fork sandboxes
Forking is supported for [VM sandboxes](#vm-sandboxes) only. Forking creates a duplicate of a sandbox's filesystem and memory state in a new sandbox. The forked sandbox is fully independent: it can be started, stopped, and deleted without affecting the original.
Daytona tracks the parent-child relationship in a fork tree, so you can trace a fork's lineage back to the sandbox it was created from. You can fork a fork to build branches. The parent sandbox cannot be deleted while it has active fork children.
1. Go to [Daytona Sandboxes ↗](https://app.daytona.io/dashboard/sandboxes)
2. Click the three-dot menu (**⋮**) next to the started VM sandbox you want to fork
3. Select **Fork**
```python
# Fork sandbox through the Sandbox instance
forked = sandbox._experimental_fork(name="my-forked-sandbox")
```
```typescript
// Fork sandbox through the Sandbox instance
const forkedSandbox = await sandbox._experimental_fork({ name: "my-forked-sandbox" });
// Or use the Daytona helper method
const forkedSandbox = await daytona._experimental_fork(sandbox, { name: "my-forked-sandbox" });
```
```ruby
# Fork sandbox through the Sandbox instance
forkedSandbox = sandbox.experimental_fork(name: "my-forked-sandbox")
```
```go
// Fork sandbox through the Sandbox instance
name := "my-forked-sandbox"
forkedSandbox, err := sandbox.ExperimentalFork(ctx, &name)
if err != nil {
return err
}
```
```java
// Fork sandbox through the Sandbox instance
Sandbox forkedSandbox = sandbox.experimentalFork("my-forked-sandbox", 60);
```
```bash
curl 'https://app.daytona.io/api/sandbox/{sandboxIdOrName}/fork' \
--request POST \
--header 'X-Daytona-Organization-ID: YOUR_ORGANIZATION_ID' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
"name": "my-forked-sandbox"
}'
```
Query fork relationships:
```bash
# List direct fork children of a sandbox
curl 'https://app.daytona.io/api/sandbox/{sandboxIdOrName}/forks' \
--header 'Authorization: Bearer YOUR_API_KEY'
# Get the parent sandbox of a fork
curl 'https://app.daytona.io/api/sandbox/{sandboxIdOrName}/parent' \
--header 'Authorization: Bearer YOUR_API_KEY'
# Get the full ancestor chain (parent, grandparent, and so on)
curl 'https://app.daytona.io/api/sandbox/{sandboxIdOrName}/ancestors' \
--header 'Authorization: Bearer YOUR_API_KEY'
```
## Sandbox lifecycle
| **Lifecycle feature** | **Container** | **Linux VM** | **Windows** | **GPU** |
| ------------------------------------------------- | ------------- | ------------ | ----------- | ------- |
| Start sandboxes | ✓ | ✓ | ✓ | ✓ |
| Stop sandboxes | ✓ | ✓ | ✓ | ✓ |
| Pause / resume sandboxes | ✗ | ✓ | ✓ | ✗ |
| Archive sandboxes | ✓ | ✗ | ✗ | ✗ |
| Fork sandboxes | ✗ | ✓ | ✓ | ✗ |
| Snapshot from sandbox (filesystem only) | ✓ | ✓ | ✓ | ✓ |
| Snapshot from sandbox (filesystem + memory) | ✗ | ✓ | ✓ | ✗ |
A sandbox can have several different states. Each state reflects the status of your sandbox.
| **State** | **Description** |
| ----------------- | ------------------------------------------------------------------------------------------- |
| Creating | The sandbox is provisioning and will be ready to use. |
| Pulling Snapshot | The sandbox is pulling a [**snapshot**](https://www.daytona.io/docs/en/snapshots.md) to provide a base environment. |
| Building Snapshot | The sandbox is building a [**snapshot**](https://www.daytona.io/docs/en/snapshots.md) to provide a base environment. |
| Pending Build | The sandbox build is pending and will start shortly. |
| Build Failed | The sandbox build failed and needs to be retried. |
| Starting | The sandbox is starting and will be ready to use. |
| Started | The sandbox has started and is ready to use. |
| Stopping | The sandbox is stopping and will no longer accept requests. |
| Stopped | The sandbox has stopped and is no longer running. Container sandboxes keep their filesystem on the runner. VM sandboxes offload filesystem state to nearby storage. |
| Pausing | The VM sandbox is pausing while its filesystem and memory state are preserved. |
| Paused | The VM sandbox is paused with filesystem and memory state preserved. State is offloaded to nearby storage. |
| Resuming | The VM sandbox is resuming from a paused state and will be ready to use. |
| Archiving | The container sandbox filesystem is being moved to object storage. |
| Archived | The container sandbox filesystem is stored in object storage. |
| Restoring | The sandbox is being restored and will be ready to use shortly. |
| Resizing | The sandbox is being resized to a new set of resources. |
| Snapshotting | The sandbox is creating a [**snapshot**](https://www.daytona.io/docs/en/snapshots.md) of its filesystem and memory. |
| Forking | The sandbox is being forked into a new independent sandbox. |
| Deleting | The sandbox is deleting and will be removed. |
| Deleted | The sandbox has been deleted and no longer exists. |
| Error | The sandbox is in an error state and needs to be recovered. |
| Unknown | The default sandbox state before it is created. |
The diagram demonstrates the states and possible transitions between them.
##### State transitions
A sandbox can transition between states in response to various actions. The following table lists the initial state, target state, and trigger for the transition.
| **Initial state** | **Target state** | **Trigger** |
| ----------------- | ----------------- | --------------------------------------------------------------------------------- |
| Unknown | Pulling Snapshot | The base snapshot is being pulled to provide the sandbox environment. |
| Unknown | Building Snapshot | The sandbox uses a declarative image build, which begins building. |
| Pending Build | Building Snapshot | The queued image build starts. |
| Building Snapshot | Build Failed | The image build fails or times out. |
| Pulling Snapshot | Creating | The snapshot is available and the sandbox container is created. |
| Building Snapshot | Creating | The snapshot finishes building and the sandbox container is created. |
| Creating | Started | The sandbox container finishes initializing and is running. |
| Stopped | Starting | A start is requested and the sandbox boots. |
| Stopped | Restoring | A start is requested and the sandbox is restored from a backup. |
| Archived | Restoring | A start is requested and the archived filesystem is restored from object storage. |
| Restoring | Started | The restore completes and the sandbox is running. |
| Starting | Started | The sandbox is running and ready to accept requests. |
| Started | Stopping | A stop is requested, or the auto-stop interval is exceeded. |
| Stopping | Stopped | The sandbox process exits and its memory state is cleared. |
| Started | Pausing | A pause is requested, or the auto-pause interval is exceeded. |
| Pausing | Paused | The filesystem and memory state are preserved. |
| Paused | Resuming | A start is requested on a paused sandbox. |
| Paused | Stopping | A stop is requested on a paused sandbox. |
| Resuming | Started | The sandbox resumes from memory and is running. |
| Stopped | Archiving | An archive is requested, or the auto-archive interval is exceeded. |
| Archiving | Archived | The backup completes and the filesystem is moved to object storage. |
| Started | Resizing | CPU or memory is increased on a running sandbox. |
| Stopped | Resizing | Resources are changed on a stopped sandbox. |
| Resizing | Started | The running sandbox returns to service after resizing. |
| Resizing | Stopped | The stopped sandbox completes resizing. |
| Started | Snapshotting | A snapshot of the filesystem and memory is created. |
| Stopped | Snapshotting | A snapshot of the filesystem is created. |
| Snapshotting | Started | The snapshot completes and the sandbox returns to service. |
| Snapshotting | Stopped | The snapshot completes and the sandbox remains stopped. |
| Started | Forking | The sandbox is forked into a new independent sandbox. |
| Forking | Started | The fork completes and the sandbox returns to service. |
| Started | Deleting | A delete is requested, or the auto-delete interval is exceeded. |
| Stopped | Deleting | A delete is requested. |
| Archived | Deleted | An archived sandbox is deleted directly without being restored. |
| Deleting | Deleted | The sandbox is removed and its resources are released. |
| Started | Error | An operation fails or times out. |
| Error | Restoring | A recover is requested for a recoverable error and the sandbox is restored. |
| Error | Archiving | An errored sandbox with a completed backup is archived to preserve its state. |
## Automated lifecycle management
Sandboxes can be managed automatically based on user-defined intervals. The intervals act as a TTL (time-to-live) mechanism for the sandbox.
- **[Auto-stop interval](#auto-stop-interval)**: stop a sandbox after a specified period of inactivity
- **[Auto-pause interval](#auto-pause-interval)**: pause a VM sandbox after a specified period of inactivity
- **[Auto-archive interval](#auto-archive-interval)**: archive a sandbox after a specified period of inactivity
- **[Auto-delete interval](#auto-delete-interval)**: delete a sandbox after a specified period of inactivity
- **[Update sandbox last activity](#update-sandbox-last-activity)**: signal activity to reset the inactivity timer
- **[Running indefinitely](#running-indefinitely)**: run a sandbox indefinitely
### Auto-stop interval
The auto-stop interval sets the amount of time after which a running sandbox is automatically stopped. The auto-stop triggers even if there are internal processes running in the sandbox.
1. Go to [Daytona Sandboxes ↗](https://app.daytona.io/dashboard/sandboxes)
2. Click **Create Sandbox**
3. Set **`auto-stop`** interval to the desired value in minutes
- **`0`**: disables the auto-stop functionality, allowing the sandbox to run indefinitely
- if not set, the default interval of 15 minutes is used
4. Click **Create**
```python
sandbox = daytona.create(CreateSandboxFromSnapshotParams(
snapshot="my-snapshot-name",
# Disables the auto-stop feature - default is 15 minutes
auto_stop_interval=0,
))
```
```typescript
const sandbox = await daytona.create({
snapshot: 'my-snapshot-name',
// Disables the auto-stop feature - default is 15 minutes
autoStopInterval: 0,
})
```
```ruby
sandbox = daytona.create(
Daytona::CreateSandboxFromSnapshotParams.new(
snapshot: 'my-snapshot-name',
# Disables the auto-stop feature - default is 15 minutes
auto_stop_interval: 0
)
)
```
```go
// Create a sandbox with auto-stop disabled
autoStopInterval := 0
params := types.SnapshotParams{
Snapshot: "my-snapshot-name",
SandboxBaseParams: types.SandboxBaseParams{
AutoStopInterval: &autoStopInterval,
},
}
sandbox, err := client.Create(ctx, params)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setSnapshot("my-snapshot-name");
// Disables the auto-stop feature - default is 15 minutes
params.setAutoStopInterval(0);
Sandbox sandbox = daytona.create(params);
}
}
}
```
```bash
curl 'https://app.daytona.io/api/sandbox/{sandboxIdOrName}/autostop/{interval}' \
--request POST \
--header 'Authorization: Bearer YOUR_API_KEY'
```
The system differentiates between "internal processes" and "active user interaction". Merely having a script or background task running is not sufficient to keep the sandbox alive.
##### What resets the timer
The inactivity timer resets only for specific external interactions:
- Updates to [sandbox lifecycle states](#sandbox-lifecycle)
- Network requests through [sandbox previews](https://www.daytona.io/docs/en/preview.md)
- Active [SSH connections](https://www.daytona.io/docs/en/ssh-access.md)
- API requests to the [Daytona Toolbox SDK](https://www.daytona.io/docs/en/tools/api.md#daytona-toolbox)
##### What does not reset the timer
The following do not reset the timer:
- SDK requests that are not toolbox actions
- Background scripts (e.g., `npm run dev` run as a fire-and-forget command)
- Long-running tasks without external interaction
- Processes that don't involve active monitoring
If you run a long-running task like LLM inference that takes more than 15 minutes to complete without any external interaction, the sandbox may auto-stop mid-process because the process itself doesn't count as "activity", therefore the timer is not reset.
### Auto-pause interval
The auto-pause interval sets the amount of time after which an idle VM sandbox is automatically [paused](#pause--resume-sandboxes). Auto-pause applies only to [VM sandboxes](#vm-sandboxes) (Linux and Windows) and is mutually exclusive with the [auto-stop interval](#auto-stop-interval): at most one of the two intervals may be non-zero. Ephemeral sandboxes cannot have auto-pause enabled.
The interval is set in minutes:
- **`0`**: disables the auto-pause functionality
- if neither auto-pause nor auto-stop is set, non-ephemeral sandbox classes that support pausing default to an auto-pause interval of 60 minutes with auto-stop disabled
The sandbox pauses after no new events occur for the specified interval. Events include sandbox state changes and interactions with the sandbox through the SDK. Interactions through [sandbox previews](https://www.daytona.io/docs/en/preview.md) do not reset the timer.
```python
sandbox = daytona.create(CreateSandboxFromSnapshotParams(
snapshot="daytona-vm-small",
# Auto-pause after 1 hour of inactivity
auto_pause_interval=60,
))
# Update the auto-pause interval on an existing sandbox
sandbox.set_auto_pause_interval(60)
# Disable auto-pause
sandbox.set_auto_pause_interval(0)
```
```typescript
const sandbox = await daytona.create({
snapshot: 'daytona-vm-small',
// Auto-pause after 1 hour of inactivity
autoPauseInterval: 60,
})
// Update the auto-pause interval on an existing sandbox
await sandbox.setAutoPauseInterval(60)
// Disable auto-pause
await sandbox.setAutoPauseInterval(0)
```
```ruby
sandbox = daytona.create(
Daytona::CreateSandboxFromSnapshotParams.new(
snapshot: 'daytona-vm-small',
# Auto-pause after 1 hour of inactivity
auto_pause_interval: 60
)
)
# Update the auto-pause interval on an existing sandbox
sandbox.auto_pause_interval = 60
# Disable auto-pause
sandbox.auto_pause_interval = 0
```
```go
// Auto-pause after 1 hour of inactivity
autoPauseInterval := 60
params := types.SnapshotParams{
Snapshot: "daytona-vm-small",
SandboxBaseParams: types.SandboxBaseParams{
AutoPauseInterval: &autoPauseInterval,
},
}
sandbox, err := client.Create(ctx, params)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setSnapshot("daytona-vm-small");
// Auto-pause after 1 hour of inactivity
params.setAutoPauseInterval(60);
Sandbox sandbox = daytona.create(params);
// Update the auto-pause interval on an existing sandbox
sandbox.setAutoPauseInterval(60);
// Disable auto-pause
sandbox.setAutoPauseInterval(0);
}
}
}
```
```bash
curl 'https://app.daytona.io/api/sandbox/{sandboxIdOrName}/autopause/{interval}' \
--request POST \
--header 'Authorization: Bearer YOUR_API_KEY'
```
### Auto-archive interval
The auto-archive interval sets the amount of time after which a continuously stopped sandbox is automatically archived. Auto-archive applies only to container sandboxes. VM sandboxes are excluded.
1. Go to [Daytona Sandboxes ↗](https://app.daytona.io/dashboard/sandboxes)
2. Click **Create Sandbox**
3. Set **`auto-archive`** interval to the desired value in minutes
- **`0`**: the maximum interval of 30 days is used
- if not set, the default interval of 7 days is used
4. Click **Create**
```python
sandbox = daytona.create(CreateSandboxFromSnapshotParams(
snapshot="my-snapshot-name",
# Auto-archive after a sandbox has been stopped for 1 hour
auto_archive_interval=60,
))
```
```typescript
const sandbox = await daytona.create({
snapshot: 'my-snapshot-name',
// Auto-archive after a sandbox has been stopped for 1 hour
autoArchiveInterval: 60,
})
```
```ruby
sandbox = daytona.create(
Daytona::CreateSandboxFromSnapshotParams.new(
snapshot: 'my-snapshot-name',
# Auto-archive after a sandbox has been stopped for 1 hour
auto_archive_interval: 60
)
)
```
```go
// Create a sandbox with auto-archive after 1 hour
autoArchiveInterval := 60
params := types.SnapshotParams{
Snapshot: "my-snapshot-name",
SandboxBaseParams: types.SandboxBaseParams{
AutoArchiveInterval: &autoArchiveInterval,
},
}
sandbox, err := client.Create(ctx, params)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setSnapshot("my-snapshot-name");
// Auto-archive after a sandbox has been stopped for 1 hour
params.setAutoArchiveInterval(60);
Sandbox sandbox = daytona.create(params);
}
}
}
```
```bash
curl 'https://app.daytona.io/api/sandbox/{sandboxIdOrName}/autoarchive/{interval}' \
--request POST \
--header 'Authorization: Bearer YOUR_API_KEY'
```
### Auto-delete interval
The auto-delete interval sets the amount of time after which a continuously stopped sandbox is automatically deleted.
1. Go to [Daytona Sandboxes ↗](https://app.daytona.io/dashboard/sandboxes)
2. Click **Create Sandbox**
3. Set **`auto-delete`** to the desired value in minutes
- `-1`: disables the auto-delete functionality
- `0`: the sandbox is deleted immediately after it is stopped
- if not set, the sandbox is not deleted automatically
4. Click **Create**
```python
sandbox = daytona.create(CreateSandboxFromSnapshotParams(
snapshot="my-snapshot-name",
# Auto-delete after a sandbox has been stopped for 1 hour
auto_delete_interval=60,
))
# Delete the sandbox immediately after it has been stopped
sandbox.set_auto_delete_interval(0)
# Disable auto-deletion
sandbox.set_auto_delete_interval(-1)
```
```typescript
const sandbox = await daytona.create({
snapshot: 'my-snapshot-name',
// Auto-delete after a sandbox has been stopped for 1 hour
autoDeleteInterval: 60,
})
// Delete the sandbox immediately after it has been stopped
await sandbox.setAutoDeleteInterval(0)
// Disable auto-deletion
await sandbox.setAutoDeleteInterval(-1)
```
```ruby
sandbox = daytona.create(
Daytona::CreateSandboxFromSnapshotParams.new(
snapshot: 'my-snapshot-name',
# Auto-delete after a sandbox has been stopped for 1 hour
auto_delete_interval: 60
)
)
# Delete the sandbox immediately after it has been stopped
sandbox.auto_delete_interval = 0
# Disable auto-deletion
sandbox.auto_delete_interval = -1
```
```go
// Create a sandbox with auto-delete after 1 hour
autoDeleteInterval := 60
params := types.SnapshotParams{
Snapshot: "my-snapshot-name",
SandboxBaseParams: types.SandboxBaseParams{
AutoDeleteInterval: &autoDeleteInterval,
},
}
sandbox, err := client.Create(ctx, params)
// Delete the sandbox immediately after it has been stopped
zeroInterval := 0
err = sandbox.SetAutoDeleteInterval(ctx, &zeroInterval)
// Disable auto-deletion
disableInterval := -1
err = sandbox.SetAutoDeleteInterval(ctx, &disableInterval)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setSnapshot("my-snapshot-name");
// Auto-delete after a sandbox has been stopped for 1 hour
params.setAutoDeleteInterval(60);
Sandbox sandbox = daytona.create(params);
// Delete the sandbox immediately after it has been stopped
sandbox.setAutoDeleteInterval(0);
// Disable auto-deletion
sandbox.setAutoDeleteInterval(-1);
}
}
}
```
```bash
curl 'https://app.daytona.io/api/sandbox/{sandboxIdOrName}/autodelete/{interval}' \
--request POST \
--header 'Authorization: Bearer YOUR_API_KEY'
```
### Update sandbox last activity
Update a sandbox's last activity timestamp.
This updates the sandbox's recorded activity time without changing its runtime state. It is useful when your workflow is driven by external systems or background orchestration that may not reset inactivity tracking.
```python
sandbox.refresh_activity()
```
```typescript
await sandbox.refreshActivity()
```
```ruby
sandbox.refresh_activity
```
```bash
curl 'https://app.daytona.io/api/sandbox/{sandboxId}/last-activity' \
--request POST \
--header 'Authorization: Bearer YOUR_API_KEY'
```
### Running indefinitely
Run sandboxes indefinitely.
By default, Daytona sandboxes auto-stop after 15 minutes of inactivity. To keep a sandbox running without interruption, set the auto-stop interval to `0` when creating a new sandbox.
1. Go to [Daytona Sandboxes ↗](https://app.daytona.io/dashboard/sandboxes)
2. Click **Create Sandbox**
3. Set **`auto-stop`** to **`0`**
4. Click **Create**
```python
sandbox = daytona.create(CreateSandboxFromSnapshotParams(
snapshot="my_awesome_snapshot",
# Disables the auto-stop feature - default is 15 minutes
auto_stop_interval=0,
))
```
```typescript
const sandbox = await daytona.create({
snapshot: 'my_awesome_snapshot',
// Disables the auto-stop feature - default is 15 minutes
autoStopInterval: 0,
})
```
```ruby
sandbox = daytona.create(
Daytona::CreateSandboxFromSnapshotParams.new(
snapshot: 'my_awesome_snapshot',
# Disables the auto-stop feature - default is 15 minutes
auto_stop_interval: 0
)
)
```
```go
// Disables the auto-stop feature - default is 15 minutes
autoStopInterval := 0
params := types.SnapshotParams{
Snapshot: "my_awesome_snapshot",
SandboxBaseParams: types.SandboxBaseParams{
AutoStopInterval: &autoStopInterval,
},
}
sandbox, err := client.Create(ctx, params)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setSnapshot("my_awesome_snapshot");
// Disables the auto-stop feature - default is 15 minutes
params.setAutoStopInterval(0);
Sandbox sandbox = daytona.create(params);
}
}
}
```
# Persistence
Daytona sandboxes are persistent by default. Stopping a sandbox does not destroy it: the sandbox keeps its identity, its filesystem, and its configuration, and can be started again at any point with all files, installed packages, and repositories intact. A sandbox is only deleted when you delete it, mark it as [ephemeral](https://www.daytona.io/docs/en/sandboxes.md#ephemeral-sandboxes), or set an [auto-delete interval](https://www.daytona.io/docs/en/sandboxes.md#auto-delete-interval).
Persistence operates at three layers:
| **Layer** | **What is preserved** | **Mechanisms** |
| ---------------- | --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Filesystem | Files, installed packages, cloned repositories, build artifacts | • [**Stop / start**](https://www.daytona.io/docs/en/sandboxes.md#stop-sandboxes) • [**Archive**](https://www.daytona.io/docs/en/sandboxes.md#archive-sandboxes) • [**Cold snapshots**](https://www.daytona.io/docs/en/snapshots.md#create-snapshot-from-sandbox) |
| Memory | Running processes, open connections, loaded application state | • [**Pause / resume**](https://www.daytona.io/docs/en/sandboxes.md#pause--resume-sandboxes) • [**Hot snapshots**](https://www.daytona.io/docs/en/snapshots.md#create-snapshot-from-sandbox) • [**Fork**](https://www.daytona.io/docs/en/sandboxes.md#fork-sandboxes) |
| External storage | Data that outlives any single sandbox | • [**Volumes**](https://www.daytona.io/docs/en/volumes.md) • [**Mount external storage**](https://www.daytona.io/docs/en/mount-external-storage.md) |
## Filesystem persistence
Filesystem persistence keeps the contents of a sandbox's disk: files, installed packages, cloned repositories, and build artifacts. A sandbox with a preserved filesystem starts with its environment intact, with nothing to reinstall or rebuild.
Sandboxes preserve their filesystem across **stop and start** cycles, **archive**, and **snapshots created from a sandbox (cold snapshots)**. Sandbox classes differ in where and how the preserved filesystem is stored while the sandbox is stopped:
| **Sandbox class** | **Filesystem persistence** |
| ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Container | Filesystem is preserved through [**stop / start**](https://www.daytona.io/docs/en/sandboxes.md#stop-sandboxes) : it stays on the runner and counts against disk quota while stopped. [**Archive**](https://www.daytona.io/docs/en/sandboxes.md#archive-sandboxes) moves it to object storage and frees the quota; starting an archived sandbox restores it. Use [**cold snapshots**](https://www.daytona.io/docs/en/snapshots.md#create-snapshot-from-sandbox) to capture the filesystem. |
| VM sandboxes (Linux VM and Windows) | Filesystem is preserved through [**stop / start**](https://www.daytona.io/docs/en/sandboxes.md#stop-sandboxes) : stopping offloads it to nearby storage and releases disk quota, starting restores it. Archive is not needed. Use [**cold snapshots**](https://www.daytona.io/docs/en/snapshots.md#create-snapshot-from-sandbox) to capture the filesystem. |
| GPU | Ephemeral by design: deleted on stop, filesystem is not preserved. Use [**volumes**](https://www.daytona.io/docs/en/volumes.md) to persist results. |
```python
from daytona import Daytona
daytona = Daytona()
sandbox = daytona.create()
# Write state to the filesystem
sandbox.fs.upload_file(b"intermediate results", "results.txt")
# Stop clears memory; the filesystem is preserved on the runner
sandbox.stop()
# Start returns the filesystem exactly as it was
sandbox.start()
response = sandbox.process.exec("cat results.txt")
print(response.result) # intermediate results
```
```python
from daytona import Daytona
daytona = Daytona()
sandbox = daytona.get("my-sandbox")
# Archive a stopped sandbox: the filesystem moves to object storage
# and no longer counts against disk quota
sandbox.stop()
sandbox.archive()
# Start restores the filesystem from object storage
sandbox.start()
```
```python
from daytona import Daytona, CreateSandboxFromSnapshotParams
daytona = Daytona()
sandbox = daytona.create()
# Prepare the environment to capture
sandbox.process.exec("pip install requests")
# Cold snapshot captures the filesystem of a stopped sandbox
sandbox.stop()
sandbox._experimental_create_snapshot("my-env-snapshot")
# Any number of new sandboxes can start from the same snapshot
clone = daytona.create(CreateSandboxFromSnapshotParams(snapshot="my-env-snapshot"))
response = clone.process.exec("pip show requests")
print(response.result) # requests is already installed
```
```python
from daytona import Daytona, CreateSandboxFromSnapshotParams
daytona = Daytona()
sandbox = daytona.create(CreateSandboxFromSnapshotParams(snapshot="daytona-vm-small"))
# Write state to the filesystem
sandbox.fs.upload_file(b"intermediate results", "results.txt")
# Stop clears memory and offloads the filesystem to nearby storage
sandbox.stop()
# Start restores the filesystem exactly as it was
sandbox.start()
response = sandbox.process.exec("cat results.txt")
print(response.result) # intermediate results
```
```python
from daytona import Daytona, CreateSandboxFromSnapshotParams
daytona = Daytona()
sandbox = daytona.create(CreateSandboxFromSnapshotParams(snapshot="daytona-vm-small"))
# Prepare the environment to capture
sandbox.process.exec("pip install requests")
# Cold snapshot captures the filesystem of a stopped sandbox
sandbox.stop()
sandbox._experimental_create_snapshot("my-vm-env-snapshot")
# Any number of new sandboxes can start from the same snapshot
clone = daytona.create(CreateSandboxFromSnapshotParams(snapshot="my-vm-env-snapshot"))
response = clone.process.exec("pip show requests")
print(response.result) # requests is already installed
```
```python
from daytona import Daytona, CreateSandboxFromSnapshotParams
daytona = Daytona()
sandbox = daytona.create(CreateSandboxFromSnapshotParams(snapshot="windows-small"))
# Write state to the filesystem
sandbox.fs.upload_file(b"intermediate results", "results.txt")
# Stop clears memory and offloads the filesystem to nearby storage
sandbox.stop()
# Start restores the filesystem exactly as it was
sandbox.start()
```
```python
from daytona import Daytona, CreateSandboxFromSnapshotParams
daytona = Daytona()
sandbox = daytona.create(CreateSandboxFromSnapshotParams(snapshot="windows-small"))
# Cold snapshot captures the filesystem of a stopped sandbox
sandbox.stop()
sandbox._experimental_create_snapshot("my-windows-env-snapshot")
# Any number of new sandboxes can start from the same snapshot
clone = daytona.create(CreateSandboxFromSnapshotParams(snapshot="my-windows-env-snapshot"))
```
```python
from daytona import CreateSandboxFromSnapshotParams, Daytona, VolumeMount
daytona = Daytona()
volume = daytona.volume.get("gpu-results", create=True)
# GPU sandboxes are ephemeral: mount a volume to keep results
sandbox = daytona.create(CreateSandboxFromSnapshotParams(
snapshot="daytona-gpu",
ephemeral=True,
volumes=[VolumeMount(volume_id=volume.id, mount_path="/home/daytona/results")],
))
# Write results to the volume before the sandbox stops
sandbox.process.exec("cp model-output.json /home/daytona/results/")
# The sandbox is deleted on stop; the volume and its data persist
sandbox.stop()
```
## Memory persistence
Memory persistence keeps the runtime state of a sandbox: running processes, open connections, and everything loaded in RAM. A sandbox with preserved memory continues from the point it was frozen, with processes still running, instead of starting from a clean boot.
Sandboxes preserve their memory across **pause and resume** cycles, **snapshots created from a sandbox (hot snapshots)**, and **forks**. Memory state is cleared on every stop. Sandbox classes differ in whether memory can be preserved without keeping the sandbox running:
| **Sandbox class** | **Memory persistence** |
| ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Container | Memory persists only while the sandbox is running. Set the [**auto-stop interval**](https://www.daytona.io/docs/en/sandboxes.md#auto-stop-interval) to `0` to run indefinitely, or relaunch processes after each start. Pause is not supported. |
| VM sandboxes (Linux VM and Windows) | Memory is preserved through [**pause / resume**](https://www.daytona.io/docs/en/sandboxes.md#pause--resume-sandboxes) : pausing freezes the VM with memory intact, resuming continues all processes from the point they were frozen. Use [**hot snapshots**](https://www.daytona.io/docs/en/snapshots.md#create-snapshot-from-sandbox) or [**forks**](https://www.daytona.io/docs/en/sandboxes.md#fork-sandboxes) to capture memory. |
| GPU | Ephemeral by design: deleted on stop, and memory is not preserved. Use [**volumes**](https://www.daytona.io/docs/en/volumes.md) to persist results of a GPU sandbox. |
```python
from daytona import Daytona, CreateSandboxFromSnapshotParams, SessionExecuteRequest
daytona = Daytona()
sandbox = daytona.create(CreateSandboxFromSnapshotParams(snapshot="daytona-vm-small"))
# Launch a process that holds state in memory
sandbox.process.create_session("server")
sandbox.process.execute_session_command("server", SessionExecuteRequest(
command="python3 -m http.server 8000",
run_async=True,
))
# Pause freezes the VM with filesystem and memory intact
sandbox.pause()
# Resume continues all processes from the point they were frozen
sandbox.start()
response = sandbox.process.exec("curl -s http://localhost:8000")
print(response.result) # the server is still running
```
```python
from daytona import Daytona, CreateSandboxFromSnapshotParams, SessionExecuteRequest
daytona = Daytona()
sandbox = daytona.create(CreateSandboxFromSnapshotParams(snapshot="daytona-vm-small"))
# Launch a process that holds state in memory
sandbox.process.create_session("server")
sandbox.process.execute_session_command("server", SessionExecuteRequest(
command="python3 -m http.server 8000",
run_async=True,
))
# Hot snapshot captures the filesystem and memory of the running VM
sandbox._experimental_create_snapshot("my-vm-snapshot", include_memory=True)
# Sandboxes created from the hot snapshot start with the process already running
clone = daytona.create(CreateSandboxFromSnapshotParams(snapshot="my-vm-snapshot"))
response = clone.process.exec("curl -s http://localhost:8000")
print(response.result)
```
```python
from daytona import Daytona, CreateSandboxFromSnapshotParams, SessionExecuteRequest
daytona = Daytona()
sandbox = daytona.create(CreateSandboxFromSnapshotParams(snapshot="daytona-vm-small"))
# Launch a process that holds state in memory
sandbox.process.create_session("server")
sandbox.process.execute_session_command("server", SessionExecuteRequest(
command="python3 -m http.server 8000",
run_async=True,
))
# Fork duplicates the filesystem and memory into an independent sandbox
fork = sandbox._experimental_fork(name="my-forked-sandbox")
response = fork.process.exec("curl -s http://localhost:8000")
print(response.result) # the process is running in the fork as well
```
```python
from daytona import Daytona, CreateSandboxFromSnapshotParams
daytona = Daytona()
sandbox = daytona.create(CreateSandboxFromSnapshotParams(snapshot="windows-small"))
# Pause freezes the VM with filesystem and memory intact
sandbox.pause()
# Resume continues all processes from the point they were frozen
sandbox.start()
```
```python
from daytona import Daytona, CreateSandboxFromSnapshotParams
daytona = Daytona()
sandbox = daytona.create(CreateSandboxFromSnapshotParams(snapshot="windows-small"))
# Hot snapshot captures the filesystem and memory of the running VM
sandbox._experimental_create_snapshot("my-windows-snapshot", include_memory=True)
# Sandboxes created from the hot snapshot start with processes already running
clone = daytona.create(CreateSandboxFromSnapshotParams(snapshot="my-windows-snapshot"))
```
```python
from daytona import Daytona, CreateSandboxFromSnapshotParams
daytona = Daytona()
sandbox = daytona.create(CreateSandboxFromSnapshotParams(snapshot="windows-small"))
# Fork duplicates the filesystem and memory into an independent sandbox
fork = sandbox._experimental_fork(name="my-forked-sandbox")
```
```python
from daytona import CreateSandboxFromSnapshotParams, Daytona, VolumeMount
daytona = Daytona()
volume = daytona.volume.get("gpu-results", create=True)
# GPU sandboxes are ephemeral: mount a volume to keep results
sandbox = daytona.create(CreateSandboxFromSnapshotParams(
snapshot="daytona-gpu",
ephemeral=True,
volumes=[VolumeMount(volume_id=volume.id, mount_path="/home/daytona/results")],
))
# Write results to the volume before the sandbox stops
sandbox.process.exec("cp model-output.json /home/daytona/results/")
# The sandbox is deleted on stop; the volume and its data persist
sandbox.stop()
```
## Persistence beyond a sandbox
Filesystem and memory persistence are tied to a single sandbox: deleting the sandbox deletes its state. To persist state beyond the sandbox itself, capture it as a **snapshot**, duplicate it into an independent sandbox with a **fork**, or store data outside any sandbox in a **volume**.
### Snapshots from a sandbox
[Snapshots created from a sandbox](https://www.daytona.io/docs/en/snapshots.md#create-snapshot-from-sandbox) capture and persist the sandbox's current state, including the filesystem, installed packages, dependencies, and settings. The snapshot saves the state so you can restore it later, and any number of new sandboxes can start from the same snapshot. For snapshots built from an image or Dockerfile, which define a base environment rather than persist an existing sandbox, see [snapshots](https://www.daytona.io/docs/en/snapshots.md).
A snapshot created from a sandbox captures the filesystem (**cold snapshot**) or the filesystem and memory (**hot snapshot**), depending on the sandbox class:
- **Container sandboxes** can capture the filesystem only (**cold snapshot**)
- **VM sandboxes** (**Linux VM** and **Windows**) can capture the filesystem only (**cold snapshot**) or the filesystem and memory (**hot snapshot**): sandboxes created from a hot snapshot start with processes already running
Snapshots persist independently of the source sandbox: deleting the sandbox does not delete snapshots created from it.
### Forks
[Forking](https://www.daytona.io/docs/en/sandboxes.md#fork-sandboxes) duplicates a VM sandbox's filesystem and memory into a new, fully independent sandbox. Use forks to branch a live environment: run divergent experiments from the same state, test a risky change without touching the original, or hand each agent in a fleet an identical starting point.
The fork persists independently of the original: it can be started, stopped, and deleted without affecting it. Daytona tracks the fork lineage, forks can be forked again, and a parent cannot be deleted while it has active fork children.
### Volumes
[Volumes](https://www.daytona.io/docs/en/volumes.md) are S3-backed mounts that persist independently of any sandbox. Data written to a mounted volume survives sandbox deletion and is readable from other sandboxes that mount the same volume. Use volumes for datasets, model weights, build caches, and per-user or per-tenant data scoped with a `subpath`. For data already in your own object storage, [mount external storage](https://www.daytona.io/docs/en/mount-external-storage.md) directly instead of copying it in.
## Retention and lifecycle
Sandboxes in stopped, paused, and archived states are retained until you delete them. Retention costs are driven by the lifecycle state: see [billing](https://www.daytona.io/docs/en/billing.md) and [limits](https://www.daytona.io/docs/en/limits.md) for details.
Auto-stop and auto-pause are mutually exclusive: at most one of the two intervals may be non-zero. VM sandboxes (Linux VM and Windows) default to auto-pause with auto-stop disabled; container and GPU sandboxes default to auto-stop.
| **Interval** | **Applies to** | **Default** | **Effect** |
| ------------------------------------------------------------------- | ------------------- | ---------------------------------------- | ------------------------------------------------------- |
| [**Auto-stop**](https://www.daytona.io/docs/en/sandboxes.md#auto-stop-interval) | Container and GPU sandboxes | **`15`** minutes of inactivity | Stops the sandbox; container sandboxes preserve the filesystem, while GPU sandboxes are deleted |
| [**Auto-pause**](https://www.daytona.io/docs/en/sandboxes.md#auto-pause-interval) | VM sandboxes (Linux VM, Windows) | **`60`** minutes of inactivity | Pauses the VM sandbox, preserving filesystem and memory |
| [**Auto-archive**](https://www.daytona.io/docs/en/sandboxes.md#auto-archive-interval) | Container sandboxes | **`7`** days stopped (**`30`** days max) | Moves the filesystem to object storage |
| [**Auto-delete**](https://www.daytona.io/docs/en/sandboxes.md#auto-delete-interval) | All sandboxes | Disabled | Deletes the sandbox after it has been stopped |
## Opting out of persistence
Persistence is the default, not a requirement: a sandbox can opt out of it entirely. For workloads where state is not needed after the run, create an [ephemeral sandbox](https://www.daytona.io/docs/en/sandboxes.md#ephemeral-sandboxes). Ephemeral sandboxes are deleted as soon as they stop, discarding all state, and accrue no stopped-state disk billing.
```python
from daytona import Daytona, CreateSandboxFromSnapshotParams
daytona = Daytona()
# Ephemeral sandboxes are deleted as soon as they stop
sandbox = daytona.create(CreateSandboxFromSnapshotParams(ephemeral=True))
# Run the workload and read the results before the sandbox stops
response = sandbox.process.exec("python3 run.py")
print(response.result)
# Stop deletes the sandbox and all of its state
sandbox.stop()
```
```python
from daytona import Daytona, CreateSandboxFromSnapshotParams
daytona = Daytona()
# Auto-delete removes the sandbox after it has been stopped for 1 hour
sandbox = daytona.create(CreateSandboxFromSnapshotParams(auto_delete_interval=60))
# Set the interval to 0 to delete immediately on stop, same as ephemeral
sandbox.set_auto_delete_interval(0)
# Set the interval to -1 to disable auto-deletion
sandbox.set_auto_delete_interval(-1)
```
# Snapshots
Snapshots are persistent, point-in-time captures of sandbox state, including the filesystem, installed packages, dependencies, and settings. A snapshot saves a sandbox's state so you can restore it later, and any number of new sandboxes can start from the same snapshot.
Daytona provides default snapshots for creating sandboxes. You can also create snapshots from your own images or capture the state of existing sandboxes to create snapshots:
- [**Create snapshots from an image**](#create-snapshots): define the base operating system, language runtimes, packages, and project-level setup in an image or Dockerfile, and Daytona builds it into a snapshot you can use to create sandboxes
- [**Create snapshots from a sandbox**](#create-snapshot-from-sandbox): captures and persists a sandbox's current state; container sandboxes capture filesystem state only (**cold snapshots**), and VM sandboxes (Linux VM and Windows) capture filesystem and memory state (**hot snapshots**)
## Default snapshots
| **Snapshot** | **vCPU** | **Memory** | **Storage** | **GPU** | **Sandbox Class** |
| ----------------------- | -------- | ---------- | ----------- | ------- | ----------------- |
| **`daytona-small`** | 1 | 1GiB | 3GiB | | Container |
| **`daytona-medium`** | 2 | 4GiB | 8GiB | | Container |
| **`daytona-large`** | 4 | 8GiB | 10GiB | | Container |
| **`daytona-gpu`** | 1 | 1GiB | 1GiB | 1 | GPU |
| **`daytona-vm-small`** | 1 | 1GiB | 3GiB | | Linux VM |
| **`daytona-vm-medium`** | 2 | 4GiB | 8GiB | | Linux VM |
| **`daytona-vm-large`** | 4 | 8GiB | 10GiB | | Linux VM |
| **`windows-small`** | 1 | 4GiB | 30GiB | | Windows |
| **`windows-medium`** | 2 | 8GiB | 50GiB | | Windows |
| **`windows-large`** | 4 | 16GiB | 50GiB | | Windows |
1. Go to [Daytona Sandboxes ↗](https://app.daytona.io/dashboard/sandboxes)
2. Click **Create Sandbox**
3. Select a **`snapshot`**
4. Click **Create**
```python
from daytona import Daytona, CreateSandboxFromSnapshotParams
daytona = Daytona()
sandbox = daytona.create(
CreateSandboxFromSnapshotParams(
snapshot="daytona-small",
)
)
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
const sandbox = await daytona.create({
snapshot: 'daytona-small',
})
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
sandbox = daytona.create(
Daytona::CreateSandboxFromSnapshotParams.new(
snapshot: 'daytona-small'
)
)
```
```go
package main
import (
"context"
"github.com/daytona/clients/sdk-go/pkg/daytona"
"github.com/daytona/clients/sdk-go/pkg/types"
)
func main() {
client, _ := daytona.NewClient()
ctx := context.Background()
params := types.SnapshotParams{
Snapshot: "daytona-small",
}
_, _ = client.Create(ctx, params)
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setSnapshot("daytona-small");
Sandbox sandbox = daytona.create(params);
}
}
}
```
```bash
daytona create --snapshot daytona-small
```
```bash
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
"snapshot": "daytona-small"
}'
```
## Create snapshots
Create a snapshot.
1. Go to [Daytona Snapshots ↗](https://app.daytona.io/dashboard/snapshots)
2. Click **Create Snapshot**
3. Enter the snapshot **`name`** and **`image`** of any publicly accessible image or container registry
- **Snapshot name**: identifier used to reference the snapshot
- **Snapshot image**: base image for the snapshot, must include either a tag or a digest (e.g., **`ubuntu:22.04`**); the **`latest`**/**`lts`**/**`stable`** tags are not supported
4. Click **Create**
```python
from daytona import Daytona, CreateSnapshotParams
daytona = Daytona()
snapshot = daytona.snapshot.create(
CreateSnapshotParams(name="my-awesome-snapshot", image="ubuntu:22.04"),
)
```
```typescript
import { Daytona } from "@daytona/sdk";
const daytona = new Daytona();
const snapshot = await daytona.snapshot.create({
name: "my-awesome-snapshot",
image: "python:3.12",
});
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
snapshot = daytona.snapshot.create(
Daytona::CreateSnapshotParams.new(name: 'my-awesome-snapshot', image: 'python:3.12')
)
```
```go
package main
import (
"context"
"github.com/daytona/clients/sdk-go/pkg/daytona"
"github.com/daytona/clients/sdk-go/pkg/types"
)
func main() {
client, _ := daytona.NewClient()
ctx := context.Background()
snapshot, logCh, _ := client.Snapshot.Create(ctx, &types.CreateSnapshotParams{
Name: "my-awesome-snapshot",
Image: "python:3.12",
})
for range logCh {
}
_ = snapshot
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.model.Snapshot;
final class CreateSnapshot {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
Snapshot snapshot = daytona.snapshot().create("my-awesome-snapshot", "python:3.12");
}
}
}
```
```bash
daytona snapshot create my-awesome-snapshot --image python:3.11-slim --cpu 2 --memory 4
```
```bash
curl https://app.daytona.io/api/snapshots \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_SECRET_TOKEN' \
--data '{
"name": "my-awesome-snapshot",
"imageName": "python:3.11-slim",
"cpu": 2,
"memory": 4
}'
```
## VM snapshots
Daytona provides methods to create VM snapshots for **Linux VM** and **Windows**.
VM snapshots are used to create [VM sandboxes](https://www.daytona.io/docs/en/sandboxes.md#vm-sandboxes). VM snapshots are distinct from container snapshots and cannot be used to create container sandboxes. VM snapshots support VM-only capabilities such as [creating a snapshot from a sandbox](#create-snapshot-from-sandbox).
Create a Linux VM snapshot.
1. Create a snapshot from a base **`image`**
2. Set the snapshot's sandbox class to **`LINUX_VM`**
```python
from daytona import Daytona, CreateSnapshotParams, SandboxClass
daytona = Daytona()
snapshot = daytona.snapshot.create(
CreateSnapshotParams(
name="my-vm-snapshot",
image="ubuntu:22.04",
sandbox_class=SandboxClass.LINUX_VM,
)
)
```
```typescript
import { Daytona, SandboxClass } from "@daytona/sdk";
const daytona = new Daytona();
const snapshot = await daytona.snapshot.create({
name: "my-vm-snapshot",
image: "ubuntu:22.04",
sandboxClass: SandboxClass.LINUX_VM,
});
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
snapshot = daytona.snapshot.create(
Daytona::CreateSnapshotParams.new(
name: 'my-vm-snapshot',
image: 'ubuntu:22.04',
sandbox_class: DaytonaApiClient::SandboxClass::LINUX_VM
)
)
```
```go
package main
import (
"context"
"github.com/daytona/clients/sdk-go/pkg/daytona"
"github.com/daytona/clients/sdk-go/pkg/types"
)
func main() {
client, _ := daytona.NewClient()
ctx := context.Background()
sandboxClass := types.SandboxClassLinuxVM
snapshot, logCh, _ := client.Snapshot.Create(ctx, &types.CreateSnapshotParams{
Name: "my-vm-snapshot",
Image: "ubuntu:22.04",
SandboxClass: &sandboxClass,
})
for range logCh {
}
_ = snapshot
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.api.client.model.SandboxClass;
import io.daytona.sdk.model.Snapshot;
final class CreateVmSnapshot {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
Snapshot snapshot = daytona.snapshot().create("my-vm-snapshot", "ubuntu:22.04", SandboxClass.LINUX_VM);
}
}
}
```
```bash
daytona snapshot create my-vm-snapshot --image ubuntu:22.04 --sandbox-class linux-vm
```
```bash
curl https://app.daytona.io/api/snapshots \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_SECRET_TOKEN' \
--data '{
"name": "my-vm-snapshot",
"imageName": "ubuntu:22.04",
"sandboxClass": "linux-vm"
}'
```
Windows snapshots are used to create [Windows sandboxes](https://www.daytona.io/docs/en/sandboxes.md#windows). They cannot be created from a base image. They are produced only through the [snapshot from sandbox](#create-snapshot-from-sandbox) by starting from an existing Windows sandbox and capturing its current state as a snapshot.
## GPU snapshots
Create a GPU snapshot. GPU snapshots are used to create [GPU sandboxes](https://www.daytona.io/docs/en/sandboxes.md#gpu-sandboxes).
1. Go to [Daytona Snapshots ↗](https://app.daytona.io/dashboard/snapshots)
2. Click **Create Snapshot**
3. Enter the snapshot **`name`** and **`image`**
4. Select the **`Allocate GPU`** checkbox
5. Specify the **`GPU type`**(s):
- **`NVIDIA H100`**
- **`NVIDIA H200`**
- **`NVIDIA RTX PRO 6000`**
- **`NVIDIA RTX 4090`**
- **`NVIDIA RTX 5090`**
6. Click **Create**
```python
from daytona import CreateSnapshotParams, Daytona, Image, Resources
daytona = Daytona()
snapshot = daytona.snapshot.create(
CreateSnapshotParams(
name="my-gpu-snapshot",
image=Image.base("python:3.12"),
resources=Resources(cpu=1, memory=1, disk=1, gpu=1),
),
)
```
```typescript
import { Daytona } from "@daytona/sdk";
const daytona = new Daytona();
const snapshot = await daytona.snapshot.create({
name: "my-gpu-snapshot",
image: "python:3.12",
resources: { cpu: 1, memory: 1, disk: 1, gpu: 1 },
});
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
snapshot = daytona.snapshot.create(
Daytona::CreateSnapshotParams.new(
name: 'my-gpu-snapshot',
image: 'python:3.12',
resources: Daytona::Resources.new(cpu: 1, memory: 1, disk: 1, gpu: 1)
)
)
```
```go
package main
import (
"context"
"github.com/daytona/clients/sdk-go/pkg/daytona"
"github.com/daytona/clients/sdk-go/pkg/types"
)
func main() {
client, _ := daytona.NewClient()
ctx := context.Background()
snapshot, logCh, _ := client.Snapshot.Create(ctx, &types.CreateSnapshotParams{
Name: "my-gpu-snapshot",
Image: "python:3.12",
Resources: &types.Resources{
CPU: 1,
Memory: 1,
Disk: 1,
GPU: 1,
},
})
for range logCh {
}
_ = snapshot
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Image;
import io.daytona.sdk.model.Resources;
import io.daytona.sdk.model.Snapshot;
final class CreateGpuSnapshot {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
Resources resources = new Resources();
resources.setCpu(1);
resources.setMemory(1);
resources.setDisk(1);
resources.setGpu(1);
Snapshot snapshot = daytona.snapshot().create(
"my-gpu-snapshot",
Image.base("python:3.12"),
resources,
null
);
}
}
}
```
```bash
curl https://app.daytona.io/api/snapshots \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_SECRET_TOKEN' \
--data '{
"name": "my-gpu-snapshot",
"imageName": "python:3.12",
"cpu": 1,
"memory": 1,
"disk": 1,
"gpu": 1
}'
```
## Create snapshot from sandbox
Container sandboxes capture filesystem state only (**cold snapshot**). VM sandboxes capture filesystem and memory state (**hot snapshot**) through the `includeMemory` parameter:
| **Snapshot type** | **Include memory** | **Snapshot contents** | **Required sandbox state** |
| ----------------- | --------------------- | --------------------- | -------------------------- |
| Cold | **`false`** (default) | Filesystem only | Stopped |
| Hot | **`true`** | Filesystem and memory | Started |
```python
# Cold snapshot (filesystem only, sandbox stopped)
sandbox._experimental_create_snapshot("my-snapshot")
# Hot snapshot (filesystem and memory, sandbox running)
sandbox._experimental_create_snapshot("my-vm-snapshot", include_memory=True)
```
```typescript
// Cold snapshot (filesystem only, sandbox stopped)
await sandbox._experimental_createSnapshot('my-snapshot')
// Hot snapshot (filesystem and memory, sandbox running)
await sandbox._experimental_createSnapshot('my-vm-snapshot', 60, true)
```
```ruby
# Cold snapshot (filesystem only, sandbox stopped)
sandbox.experimental_create_snapshot(name: 'my-snapshot')
# Hot snapshot (filesystem and memory, sandbox running)
sandbox.experimental_create_snapshot(name: 'my-vm-snapshot', include_memory: true)
```
```go
// Cold snapshot (filesystem only, sandbox stopped)
err := sandbox.ExperimentalCreateSnapshot(ctx, "my-snapshot")
if err != nil {
return err
}
// Hot snapshot (filesystem and memory, sandbox running)
err = sandbox.ExperimentalCreateSnapshotWithMemory(ctx, "my-vm-snapshot", 60*time.Second)
if err != nil {
return err
}
```
```java
// Cold snapshot (filesystem only, sandbox stopped)
sandbox.experimentalCreateSnapshot("my-snapshot");
// Hot snapshot (filesystem and memory, sandbox running)
sandbox.experimentalCreateSnapshot("my-vm-snapshot", 60, true);
```
```bash
# Cold snapshot (filesystem only, sandbox stopped)
curl 'https://app.daytona.io/api/sandbox/{sandboxIdOrName}/snapshot' \
--request POST \
--header 'X-Daytona-Organization-ID: YOUR_ORGANIZATION_ID' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
"name": "my-snapshot",
"includeMemory": false
}'
# Hot snapshot (filesystem and memory, sandbox running)
curl 'https://app.daytona.io/api/sandbox/{sandboxIdOrName}/snapshot' \
--request POST \
--header 'X-Daytona-Organization-ID: YOUR_ORGANIZATION_ID' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
"name": "my-vm-snapshot",
"includeMemory": true
}'
```
## Snapshots from private registries
Create a snapshot from images from private container registries.
1. Go to [Daytona Registries ↗](https://app.daytona.io/dashboard/registries)
2. Click **Add Registry** and select your provider:
- [Docker Hub](#docker-hub)
- [Google Artifact Registry](#google-artifact-registry)
- [GitHub Container Registry](#github-container-registry)
- [Amazon ECR](#amazon-elastic-container-registry)
3. Enter the required fields
4. Go to [Daytona Snapshots ↗](https://app.daytona.io/dashboard/snapshots)
5. Click **Create Snapshot**
6. Enter the snapshot **`name`** and the full **`image`** reference, including the registry host and repository (e.g. **`my-registry.com//custom-alpine:3.21`**)
#### Docker Hub
Create a snapshot from Docker Hub images.
1. Go to [Daytona Registries ↗](https://app.daytona.io/dashboard/registries)
2. Click **Add Registry** and select the **Docker Hub** tab
3. Input the following fields:
- **Username**: your Docker Hub username (the account with access to the image)
- **Personal Access Token**: a [Docker Hub PAT](https://docs.docker.com/security/access-tokens/); not your account password
- **Registry URL**: auto-filled with **`docker.io`** and not shown in the form
4. Create the snapshot using the full image reference
**`docker.io//:`**
#### Google Artifact Registry
Create a snapshot from images from Google Artifact Registry.
1. Go to [Daytona Registries ↗](https://app.daytona.io/dashboard/registries),
2. Click **Add Registry** and select the **Google** tab
2. Input the following fields:
- **Registry URL**: the base URL for your region
**`https://-docker.pkg.dev`**
- **Service Account JSON Key**: the contents of your service account key JSON file
- **Google Cloud Project ID**: your GCP project ID
- **Username**: auto-filled with **`_json_key`** (required by Google for service-account auth)
3. Create the snapshot using the full image reference
**`-docker.pkg.dev///:`**
#### GitHub Container Registry
Create a snapshot from images from GitHub Container Registry.
1. Go to [Daytona Registries ↗](https://app.daytona.io/dashboard/registries),
2. Click **Add Registry** and select the **GitHub** tab
2. Input the following fields:
- **GitHub Username**: the account with access to the image
- **Personal Access Token**: a [GitHub PAT](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) with **`read:packages`** scope (and **`write:packages`** / **`delete:packages`** for pushing or deleting)
- **Registry URL**: auto-filled with **`ghcr.io`** and not shown in the form
3. Create the snapshot using the full image reference
**`ghcr.io//:`**
#### Amazon ECR
Create a snapshot from images from Amazon Elastic Container Registry.
Daytona pulls private ECR images via cross-account IAM role assumption. You create a role in your AWS account that trusts Daytona's broker principal, and Daytona assumes it on every pull to fetch a short-lived ECR token.
- **Daytona Broker ARN**
The IAM principal Daytona uses to assume into your role. Self-hosted: substitute the IAM role your API pods assume (e.g. via IRSA).
`arn:aws:iam::967657494466:role/DaytonaEcrCredentialBroker`
- **External ID**
Your Daytona organization ID, visible in the dashboard URL (`/dashboard//...`) and on your organization settings page.
1. Create an IAM role in your AWS account
- **Trust policy**
```json
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::967657494466:role/DaytonaEcrCredentialBroker" },
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"sts:ExternalId": ""
}
}
}]
}
```
- **Permissions policy (read-only on ECR)**
```json
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": [
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage"
],
"Resource": "*"
}]
}
```
2. Go to [Daytona Registries ↗](https://app.daytona.io/dashboard/registries)
3. Click **Add Registry** and select the **Amazon ECR** tab
4. Input the following fields:
- **Registry URL**: **`.dkr.ecr..amazonaws.com`**
- **Role ARN**: the role you created in step 1
Password is not used for ECR. Daytona resolves credentials server-side by assuming the role you created in step 1, using your organization ID as the **`AssumeRole ExternalId`**.
5. Go to [Daytona Snapshots ↗](https://app.daytona.io/dashboard/snapshots)
6. Click **Create Snapshot**
7. Enter the snapshot **`name`** and the full **`image`** reference
**`.dkr.ecr..amazonaws.com//:`**
8. (Optional) Harden the trust policy
Daytona sends a `daytona--pull` session name on every AssumeRole call. You can require it in your trust policy for CloudTrail audit visibility. Add inside `Condition`:
```json
"StringLike": {
"sts:RoleSessionName": "daytona--*"
}
```
## Snapshots from local images
Create a snapshot from local images or from local Dockerfiles.
Daytona expects the local image to be built for AMD64 architecture. Therefore, the `--platform=linux/amd64` flag is required when building the Docker image if your machine is running on a different architecture.
1. Ensure the image and tag you want to use is available
```bash
docker images
```
2. Create a snapshot and push it to Daytona:
```bash
daytona snapshot push custom-alpine:3.21 --name alpine-minimal
```
Alternatively, use the `--dockerfile` flag under `create` to pass the path to the Dockerfile you want to use and Daytona will build the snapshot for you. The `COPY`/`ADD` commands will be automatically parsed and added to the context. To manually add files to the context, use the `--context` flag.
```bash
daytona snapshot create my-awesome-snapshot --dockerfile ./Dockerfile
```
## Get a snapshot by name
Get a snapshot by name.
```python
daytona.snapshot.get("my-awesome-snapshot")
```
```typescript
await daytona.snapshot.get('my-awesome-snapshot')
```
```ruby
daytona.snapshot.get('my-awesome-snapshot')
```
```go
_, err := client.Snapshots.Get(ctx, "my-awesome-snapshot")
```
```java
daytona.snapshot().get("my-awesome-snapshot");
```
```bash
curl https://app.daytona.io/api/snapshots/my-awesome-snapshot \
--header 'Authorization: Bearer YOUR_SECRET_TOKEN'
```
## List snapshots
List snapshots and view their details.
```python
daytona.snapshot.list(page=2, limit=10)
```
```typescript
await daytona.snapshot.list(2, 10)
```
```ruby
daytona.snapshot.list(page: 2, limit: 10)
```
```go
page, limit := 2, 10
_, err := client.Snapshots.List(ctx, &page, &limit)
```
```java
daytona.snapshot().list(2, 10);
```
```bash
# List snapshots with pagination
daytona snapshot list --page 2 --limit 10
```
```bash
curl 'https://app.daytona.io/api/snapshots?page=2&limit=10' \
--header 'Authorization: Bearer YOUR_SECRET_TOKEN'
```
## Activate snapshots
Activate an inactive snapshot.
Snapshots automatically become inactive after 2 weeks of not being used.
1. Go to [Daytona Snapshots ↗](https://app.daytona.io/dashboard/snapshots)
2. Click the three dots at the end of the row for the snapshot you want to activate
3. Click the **Activate** button
```python
daytona.snapshot.activate("my-awesome-snapshot")
```
```typescript
await daytona.snapshot.activate("my-awesome-snapshot")
```
```ruby
daytona.snapshot.activate('my-awesome-snapshot')
```
```bash
curl https://app.daytona.io/api/snapshots/my-inactive-snapshot/activate \
--request POST \
--header 'Authorization: Bearer YOUR_SECRET_TOKEN'
```
## Deactivate snapshots
Deactivate a snapshot.
Deactivated snapshots are not available for new sandboxes.
1. Go to [Daytona Snapshots ↗](https://app.daytona.io/dashboard/snapshots)
2. Click the three dots at the end of the row for the snapshot you want to deactivate
3. Click the **Deactivate** button
## Delete snapshots
Delete a snapshot.
Deleted snapshots cannot be recovered.
1. Go to [Daytona Snapshots ↗](https://app.daytona.io/dashboard/snapshots)
2. Click the three dots at the end of the row for the snapshot you want to delete
3. Click the **Delete** button
```python
daytona.snapshot.delete(daytona.snapshot.get("my-awesome-snapshot"))
```
```typescript
await daytona.snapshot.delete(await daytona.snapshot.get("my-awesome-snapshot"))
```
```ruby
daytona.snapshot.delete(daytona.snapshot.get('my-awesome-snapshot'))
```
```go
snapshot, err := client.Snapshots.Get(ctx, "my-awesome-snapshot")
err = client.Snapshots.Delete(ctx, snapshot)
```
```java
daytona.snapshot().delete(daytona.snapshot().get("my-awesome-snapshot").getId());
```
```bash
daytona snapshot delete my-awesome-snapshot
```
```bash
curl https://app.daytona.io/api/snapshots/my-awesome-snapshot \
--request DELETE \
--header 'Authorization: Bearer YOUR_SECRET_TOKEN'
```
## Snapshot lifecycle
A snapshot can have several different states. Each state reflects the snapshot's current status.
- **Pending**: the snapshot creation has been requested
- **Building**: the snapshot is being built
- **Pulling**: the snapshot image is being pulled from a registry
- **Active**: the snapshot is ready to use for creating sandboxes
- **Inactive**: the snapshot is deactivated; must be explicitly [activated](#activate-snapshots) before use
- **Error**: the snapshot creation failed
- **Build Failed**: the snapshot build process failed
- **Removing**: the snapshot is being deleted
Default snapshots include pre-installed Python and Node.js packages.
| **Package** | **Version** |
| ---------------------- | ----------- |
| **`anthropic`** | v0.76.0 |
| **`beautifulsoup4`** | v4.14.3 |
| **`claude-agent-sdk`** | v0.1.22 |
| **`openai-agents`** | v0.15.1 |
| **`daytona`** | v0.134.0 |
| **`django`** | v6.0.1 |
| **`flask`** | v3.1.2 |
| **`huggingface-hub`** | v0.36.0 |
| **`instructor`** | v1.14.4 |
| **`keras`** | v3.13.0 |
| **`langchain`** | v1.2.7 |
| **`llama-index`** | v0.14.13 |
| **`matplotlib`** | v3.10.8 |
| **`numpy`** | v2.4.1 |
| **`ollama`** | v0.6.1 |
| **`openai`** | v2.33.0 |
| **`opencv-python`** | v4.13.0.90 |
| **`pandas`** | v2.3.3 |
| **`pillow`** | v12.1.0 |
| **`pipx`** | v1.8.0 |
| **`pydantic-ai`** | v1.47.0 |
| **`python-lsp-server`** | v1.14.0 |
| **`requests`** | v2.32.5 |
| **`scikit-learn`** | v1.8.0 |
| **`scipy`** | v1.17.0 |
| **`seaborn`** | v0.13.2 |
| **`sqlalchemy`** | v2.0.46 |
| **`torch`** | v2.10.0 |
| **`transformers`** | v4.57.6 |
| **`uv`** | v0.9.26 |
| **Package** | **Version** |
| -------------------------------- | ----------- |
| **`@anthropic-ai/claude-code`** | v2.1.19 |
| **`@openai/codex`** | v0.128.0 |
| **`bun`** | v1.3.6 |
| **`openclaw`** | v2026.2.1 |
| **`opencode-ai`** | v1.1.35 |
| **`ts-node`** | v10.9.2 |
| **`typescript`** | v5.9.3 |
| **`typescript-language-server`** | v5.1.3 |
## Run Docker in a sandbox
Sandboxes can run Docker containers inside them (**Docker-in-Docker**), enabling you to build, test, and deploy containerized applications.
Agents can interact with these services since they run within the same sandbox environment, providing better isolation and security compared to external service dependencies.
- Run databases (PostgreSQL, Redis, MySQL) and other services
- Build and test containerized applications
- Deploy microservices and their dependencies
- Create isolated development environments with full container orchestration
:::note
Docker-in-Docker sandboxes require additional resources due to the Docker daemon overhead. Consider allocating at least 2 vCPU and 4GiB of memory for optimal performance.
:::
##### Create a Docker-in-Docker snapshot
Daytona provides an option to create a snapshot with Docker support using pre-built Docker-in-Docker images as a base or by manually installing Docker in a custom image.
###### Using pre-built images
The following base images are widely used for creating Docker-in-Docker snapshots or can be used as a base for a custom Dockerfile:
- **`docker:28.3.3-dind`**: official Docker-in-Docker image (Alpine-based, lightweight)
- **`docker:28.3.3-dind-rootless`**: rootless Docker-in-Docker for enhanced security
- **`docker:28.3.2-dind-alpine3.22`**: Docker-in-Docker image with Alpine 3.22
**Manual installation**
Alternatively, install Docker manually in a custom Dockerfile:
```dockerfile
FROM ubuntu:22.04
# Install Docker using the official install script
RUN curl -fsSL https://get.docker.com | VERSION=28.3.3 sh -
```
##### Run Docker Compose in a sandbox
Define and run multi-container applications. With Docker-in-Docker enabled in a Daytona sandbox, you can use Docker Compose to orchestrate services like databases, caches, and application containers.
1. Create a Docker-in-Docker snapshot with one of the [pre-built images](#using-pre-built-images)
2. Run Docker Compose services inside a sandbox
```python
from daytona import Daytona, CreateSandboxFromSnapshotParams
# Initialize the Daytona client
daytona = Daytona()
# Create a sandbox from a Docker-in-Docker snapshot
sandbox = daytona.create(CreateSandboxFromSnapshotParams(snapshot='docker-dind'))
# Create a docker-compose.yml file
compose_content = '''
services:
web:
image: nginx:alpine
ports:
- "8080:80"
'''
sandbox.fs.upload_file(compose_content.encode(), 'docker-compose.yml')
# Start Docker Compose services
result = sandbox.process.exec('docker compose -p demo up -d')
print(result.result)
# Check running services
result = sandbox.process.exec('docker compose -p demo ps')
print(result.result)
# Clean up
sandbox.process.exec('docker compose -p demo down')
```
```typescript
import { Daytona } from '@daytona/sdk'
// Initialize the Daytona client
const daytona = new Daytona()
// Create a sandbox from a Docker-in-Docker snapshot
const sandbox = await daytona.create({ snapshot: 'docker-dind' })
// Create a docker-compose.yml file
const composeContent = `
services:
web:
image: nginx:alpine
ports:
- "8080:80"
`
await sandbox.fs.uploadFile(Buffer.from(composeContent), 'docker-compose.yml')
// Start Docker Compose services
let result = await sandbox.process.executeCommand('docker compose -p demo up -d')
console.log(result.result)
// Check running services
result = await sandbox.process.executeCommand('docker compose -p demo ps')
console.log(result.result)
// Clean up
await sandbox.process.executeCommand('docker compose -p demo down')
```
```ruby
require 'daytona'
# Initialize the Daytona client
daytona = Daytona::Daytona.new
# Create a sandbox from a Docker-in-Docker snapshot
sandbox = daytona.create(Daytona::CreateSandboxFromSnapshotParams.new(snapshot: 'docker-dind'))
# Create a docker-compose.yml file
compose_content = <<~YAML
services:
web:
image: nginx:alpine
ports:
- "8080:80"
YAML
sandbox.fs.upload_file(compose_content, 'docker-compose.yml')
# Start Docker Compose services
result = sandbox.process.exec(command: 'docker compose -p demo up -d')
puts result.result
# Check running services
result = sandbox.process.exec(command: 'docker compose -p demo ps')
puts result.result
# Clean up
sandbox.process.exec(command: 'docker compose -p demo down')
```
```go
package main
import (
"context"
"fmt"
"github.com/daytonaio/sdk-go/daytona"
"github.com/daytonaio/sdk-go/types"
)
func main() {
ctx := context.Background()
// Initialize the Daytona client
client, _ := daytona.NewDaytona(nil)
// Create a sandbox from a Docker-in-Docker snapshot
sandbox, _ := client.Create(ctx, &types.CreateSandboxFromSnapshotParams{
Snapshot: daytona.Ptr("docker-dind"),
}, nil)
// Create a docker-compose.yml file
composeContent := `
services:
web:
image: nginx:alpine
ports:
- "8080:80"
`
sandbox.Fs.UploadFile(ctx, []byte(composeContent), "docker-compose.yml")
// Start Docker Compose services
result, _ := sandbox.Process.ExecuteCommand(ctx, "docker compose -p demo up -d", nil)
fmt.Println(result.Result)
// Check running services
result, _ = sandbox.Process.ExecuteCommand(ctx, "docker compose -p demo ps", nil)
fmt.Println(result.Result)
// Clean up
sandbox.Process.ExecuteCommand(ctx, "docker compose -p demo down", nil)
}
```
## Run Kubernetes in a sandbox
Sandboxes can run a Kubernetes cluster inside the sandbox. Kubernetes runs entirely inside the sandbox and is removed when the sandbox is deleted, keeping environments secure and reproducible.
The snippet installs and starts a k3s cluster inside a sandbox and lists all running pods:
```typescript
import { Daytona } from '@daytona/sdk'
import { setTimeout } from 'timers/promises'
// Initialize the Daytona client
const daytona = new Daytona()
// Create the sandbox instance
const sandbox = await daytona.create()
// Run the k3s installation script
const response = await sandbox.process.executeCommand(
'curl -sfL https://get.k3s.io | sh -'
)
// Run k3s
const sessionName = 'k3s-server'
await sandbox.process.createSession(sessionName)
const k3s = await sandbox.process.executeSessionCommand(sessionName, {
command: 'sudo /usr/local/bin/k3s server',
async: true,
})
// Give time to k3s to fully start
await setTimeout(30000)
// Get all pods
const pods = await sandbox.process.executeCommand(
'sudo /usr/local/bin/kubectl get pod -A'
)
console.log(pods.result)
```
# Declarative Builder
Declarative Builder provides a powerful, code-first approach to defining dependencies for Daytona sandboxes. Instead of importing images from a container registry, you can programmatically define them using the Daytona SDK.
The declarative builder system supports two primary workflows:
- [**Declarative images**](#build-declarative-images): build images on demand when creating sandboxes
- [**Pre-built snapshots**](#create-pre-built-snapshots): create and register ready-to-use [snapshots](https://www.daytona.io/docs/snapshots.md)
## Build declarative images
Create a declarative image by defining the dependencies for the sandbox.
Declarative images are cached for 24 hours, and are automatically reused when running the same script. Thus, subsequent runs on the same runner will be almost instantaneous.
```python
# Define a declarative image with python packages
declarative_image = (
Image.debian_slim("3.12")
.pip_install(["requests", "pytest"])
.workdir("/home/daytona")
)
# Create a new sandbox with the declarative image and stream the build logs
sandbox = daytona.create(
CreateSandboxFromImageParams(image=declarative_image),
timeout=0,
on_snapshot_create_logs=print,
)
```
```typescript
// Define a declarative image with python packages
const declarativeImage = Image.debianSlim('3.12')
.pipInstall(['requests', 'pytest'])
.workdir('/home/daytona')
// Create a new sandbox with the declarative image and stream the build logs
const sandbox = await daytona.create(
{
image: declarativeImage,
},
{
timeout: 0,
onSnapshotCreateLogs: console.log,
}
)
```
```ruby
# Define a simple declarative image with Python packages
declarative_image = Daytona::Image
.debian_slim('3.12')
.pip_install(['requests', 'pytest'])
.workdir('/home/daytona')
# Create a new Sandbox with the declarative image and stream the build logs
sandbox = daytona.create(
Daytona::CreateSandboxFromImageParams.new(image: declarative_image),
on_snapshot_create_logs: proc { |chunk| puts chunk }
)
```
```go
// Define a declarative image with python packages
version := "3.12"
declarativeImage := daytona.DebianSlim(&version).
PipInstall([]string{"requests", "pytest"}).
Workdir("/home/daytona")
// Create a new sandbox with the declarative image and stream the build logs
logChan := make(chan string)
go func() {
for log := range logChan {
fmt.Print(log)
}
}()
sandbox, err := client.Create(ctx, types.ImageParams{
Image: declarativeImage,
}, options.WithTimeout(0), options.WithLogChannel(logChan))
if err != nil {
// handle error
}
```
```java
// Define a declarative image with python packages
Image declarativeImage = Image.debianSlim("3.12")
.pipInstall("requests", "pytest")
.workdir("/home/daytona");
// Create a new sandbox with the declarative image and stream the build logs
CreateSandboxFromImageParams params = new CreateSandboxFromImageParams();
params.setImage(declarativeImage);
Sandbox sandbox = daytona.create(params, 0L, System.out::println);
```
## Create pre-built snapshots
Create a pre-built snapshot by building a declarative image and registering it as a [snapshot](https://www.daytona.io/docs/en/snapshots.md).
```python
# Define the declarative image for the snapshot
image = (
Image.debian_slim("3.12")
.pip_install(["numpy", "pandas"])
.workdir("/home/daytona")
)
# Create and register the snapshot, streaming the build logs
daytona.snapshot.create(
CreateSnapshotParams(name="my-snapshot", image=image),
on_logs=print,
)
# Create a new sandbox from the pre-built snapshot
sandbox = daytona.create(CreateSandboxFromSnapshotParams(snapshot="my-snapshot"))
```
```typescript
// Define the declarative image for the snapshot
const image = Image.debianSlim('3.12')
.pipInstall(['numpy', 'pandas'])
.workdir('/home/daytona')
// Create and register the snapshot, streaming the build logs
await daytona.snapshot.create(
{
name: 'my-snapshot',
image,
},
{
onLogs: console.log,
}
)
// Create a new sandbox from the pre-built snapshot
const sandbox = await daytona.create({ snapshot: 'my-snapshot' })
```
```ruby
# Define the declarative image for the snapshot
image = Daytona::Image
.debian_slim('3.12')
.pip_install(['numpy', 'pandas'])
.workdir('/home/daytona')
# Create and register the snapshot, streaming the build logs
daytona.snapshot.create(
Daytona::CreateSnapshotParams.new(name: 'my-snapshot', image: image),
on_logs: proc { |chunk| print chunk }
)
# Create a new sandbox from the pre-built snapshot
sandbox = daytona.create(Daytona::CreateSandboxFromSnapshotParams.new(snapshot: 'my-snapshot'))
```
```go
// Define the declarative image for the snapshot
version := "3.12"
image := daytona.DebianSlim(&version).
PipInstall([]string{"numpy", "pandas"}).
Workdir("/home/daytona")
// Create and register the snapshot, streaming the build logs
snapshot, logChan, err := client.Snapshot.Create(ctx, &types.CreateSnapshotParams{
Name: "my-snapshot",
Image: image,
})
if err != nil {
// handle error
}
for log := range logChan {
fmt.Print(log)
}
// Create a new sandbox from the pre-built snapshot
sandbox, err := client.Create(ctx, types.SnapshotParams{
Snapshot: snapshot.Name,
})
if err != nil {
// handle error
}
```
```java
// Define the declarative image for the snapshot
Image image = Image.debianSlim("3.12")
.pipInstall("numpy", "pandas")
.workdir("/home/daytona");
// Create and register the snapshot, streaming the build logs
Snapshot snapshot = daytona.snapshot().create("my-snapshot", image, System.out::println);
// Create a new sandbox from the pre-built snapshot
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setSnapshot("my-snapshot");
Sandbox sandbox = daytona.create(params);
```
## Image configuration
Daytona provides an option to define images programmatically. Chain the methods below to build a complete image definition in a single fluent call.
1. **Select a base image**
Start from any registry image with `Image.base()`, or use `Image.debian_slim()` for a Python-ready Debian image.
2. **Install Python packages**
Add packages with `pip_install()`, or install from `requirements.txt` or `pyproject.toml` using `pip_install_from_requirements()` and `pip_install_from_pyproject()`.
3. **Add files and directories**
Copy local files into the image with `add_local_file()` and `add_local_dir()`.
4. **Configure environment**
Set environment variables and the working directory with `env()` and `workdir()`.
5. **Install system packages**
Use `run_commands()` to install OS-level CLI tools and libraries not available through `pip`. Chain `apt-get update`, install, and cache cleanup with `&&` in a single command to minimize Docker layers.
6. **Add additional runtimes**
Install secondary language runtimes in a single chained `RUN` instruction. The example below adds Node.js 20 alongside Python.
7. **Set up a non-root user**
Run all installation steps as `root` first, then create the user, fix ownership of the working directory, and switch with the `USER` directive. Commands that write to system locations after switching users will fail with permission errors.
8. **Configure startup**
Set the container entrypoint and default command with `entrypoint()` and `cmd()`.
```python
image = (
# 1. Base image
Image.debian_slim("3.12")
# 2. Python packages
.pip_install(["requests", "pandas"])
# 3. Local files
.add_local_file("package.json", "/home/daytona/package.json")
.add_local_dir("src", "/home/daytona/src")
# 4. Environment
.env({"PROJECT_ROOT": "/home/daytona"})
.workdir("/home/daytona")
# 5. System packages
.run_commands(
"apt-get update "
"&& apt-get install -y --no-install-recommends git curl ffmpeg jq "
"&& rm -rf /var/lib/apt/lists/*"
)
# 6. Additional runtime
.run_commands(
"apt-get update "
"&& apt-get install -y --no-install-recommends curl ca-certificates "
"&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - "
"&& apt-get install -y nodejs "
"&& rm -rf /var/lib/apt/lists/*"
)
# 7. Non-root user
.run_commands(
"groupadd -r daytona && useradd -r -g daytona -m -d /home/daytona daytona",
"chown -R daytona:daytona /home/daytona",
)
.dockerfile_commands(["USER daytona"])
# 8. Startup
.entrypoint(["/bin/bash"])
.cmd(["/bin/bash"])
)
```
```typescript
// 1. Base image
const image = Image.debianSlim('3.12')
// 2. Python packages
.pipInstall(['requests', 'pandas'])
// 3. Local files
.addLocalFile('package.json', '/home/daytona/package.json')
.addLocalDir('src', '/home/daytona/src')
// 4. Environment
.env({ PROJECT_ROOT: '/home/daytona' })
.workdir('/home/daytona')
// 5. System packages
.runCommands(
'apt-get update ' +
'&& apt-get install -y --no-install-recommends git curl ffmpeg jq ' +
'&& rm -rf /var/lib/apt/lists/*',
)
// 6. Additional runtime
.runCommands(
'apt-get update ' +
'&& apt-get install -y --no-install-recommends curl ca-certificates ' +
'&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - ' +
'&& apt-get install -y nodejs ' +
'&& rm -rf /var/lib/apt/lists/*',
)
// 7. Non-root user
.runCommands(
'groupadd -r daytona && useradd -r -g daytona -m -d /home/daytona daytona',
'chown -R daytona:daytona /home/daytona',
)
.dockerfileCommands(['USER daytona'])
// 8. Startup
.entrypoint(['/bin/bash'])
.cmd(['/bin/bash'])
```
```ruby
image = Daytona::Image
# 1. Base image
.debian_slim('3.12')
# 2. Python packages
.pip_install(['requests', 'pandas'])
# 3. Local files
.add_local_file('package.json', '/home/daytona/package.json')
.add_local_dir('src', '/home/daytona/src')
# 4. Environment
.env({ 'PROJECT_ROOT' => '/home/daytona' })
.workdir('/home/daytona')
# 5. System packages
.run_commands(
'apt-get update ' \
'&& apt-get install -y --no-install-recommends git curl ffmpeg jq ' \
'&& rm -rf /var/lib/apt/lists/*'
)
# 6. Additional runtime
.run_commands(
'apt-get update ' \
'&& apt-get install -y --no-install-recommends curl ca-certificates ' \
'&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - ' \
'&& apt-get install -y nodejs ' \
'&& rm -rf /var/lib/apt/lists/*'
)
# 7. Non-root user
.run_commands(
'groupadd -r daytona && useradd -r -g daytona -m -d /home/daytona daytona',
'chown -R daytona:daytona /home/daytona'
)
.dockerfile_commands(['USER daytona'])
# 8. Startup
.entrypoint(['/bin/bash'])
.cmd(['/bin/bash'])
```
```go
version := "3.12"
// 1. Base image
image := daytona.DebianSlim(&version).
// 2. Python packages
PipInstall([]string{"requests", "pandas"}).
// 3. Local files
AddLocalFile("package.json", "/home/daytona/package.json").
AddLocalDir("src", "/home/daytona/src").
// 4. Environment
Env("PROJECT_ROOT", "/home/daytona").
Workdir("/home/daytona").
// 5. System packages
AptGet([]string{"git", "curl", "ffmpeg", "jq"}).
// 6. Additional runtime
Run("apt-get update " +
"&& apt-get install -y --no-install-recommends curl ca-certificates " +
"&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - " +
"&& apt-get install -y nodejs " +
"&& rm -rf /var/lib/apt/lists/*").
// 7. Non-root user
Run("groupadd -r daytona && useradd -r -g daytona -m -d /home/daytona daytona").
Run("chown -R daytona:daytona /home/daytona").
User("daytona").
// 8. Startup
Entrypoint([]string{"/bin/bash"}).
Cmd([]string{"/bin/bash"})
```
```java
// 1. Base image
Image image = Image.debianSlim("3.12")
// 2. Python packages
.pipInstall("requests", "pandas")
// 3. Local files
.addLocalFile("package.json", "/home/daytona/package.json")
.addLocalDir("src", "/home/daytona/src")
// 4. Environment
.env(java.util.Map.of("PROJECT_ROOT", "/home/daytona"))
.workdir("/home/daytona")
// 5. System packages
.runCommands(
"apt-get update "
+ "&& apt-get install -y --no-install-recommends git curl ffmpeg jq "
+ "&& rm -rf /var/lib/apt/lists/*"
)
// 6. Additional runtime
.runCommands(
"apt-get update "
+ "&& apt-get install -y --no-install-recommends curl ca-certificates "
+ "&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - "
+ "&& apt-get install -y nodejs "
+ "&& rm -rf /var/lib/apt/lists/*"
)
// 7. Non-root user
.runCommands(
"groupadd -r daytona && useradd -r -g daytona -m -d /home/daytona daytona",
"chown -R daytona:daytona /home/daytona"
)
.dockerfileCommands("USER daytona")
// 8. Startup
.entrypoint("/bin/bash")
.cmd("/bin/bash");
```
### Dockerfile integration
Integrate Dockerfiles and custom Dockerfile commands.
```python
# Add custom Dockerfile commands
image = Image.debian_slim("3.12").dockerfile_commands(["RUN echo 'Hello, world!'"])
# Use an existing Dockerfile
image = Image.from_dockerfile("Dockerfile")
# Extend an existing Dockerfile
image = Image.from_dockerfile("app/Dockerfile").pip_install(["numpy"])
```
```typescript
// Add custom Dockerfile commands
const image = Image.debianSlim('3.12').dockerfileCommands(['RUN echo "Hello, world!"'])
// Use an existing Dockerfile
const image = Image.fromDockerfile('Dockerfile')
// Extend an existing Dockerfile
const image = Image.fromDockerfile("app/Dockerfile").pipInstall(['numpy'])
```
```ruby
# Add custom Dockerfile commands
image = Daytona::Image.debian_slim('3.12').dockerfile_commands(['RUN echo "Hello, world!"'])
# Use an existing Dockerfile
image = Daytona::Image.from_dockerfile('Dockerfile')
# Extend an existing Dockerfile
image = Daytona::Image.from_dockerfile('app/Dockerfile').pip_install(['numpy'])
```
```go
// Note: In Go, FromDockerfile takes the Dockerfile content as a string
content, err := os.ReadFile("Dockerfile")
if err != nil {
// handle error
}
image := daytona.FromDockerfile(string(content))
// Extend an existing Dockerfile with additional commands
content, err = os.ReadFile("app/Dockerfile")
if err != nil {
// handle error
}
image := daytona.FromDockerfile(string(content)).
PipInstall([]string{"numpy"})
```
# Volumes
Volumes are FUSE-based mounts that provide shared file access across Daytona sandboxes. They enable sandboxes to read from large files instantly - no need to upload files manually to each sandbox. Volume data is stored in an S3-compatible object store.
A sandbox reads and writes a mounted volume like any local directory, and the contents persist independently of the sandbox lifecycle. Use volumes to share datasets, model weights, build caches, or application state between sandboxes, scope per-user or per-tenant data with a `subpath`, and combine multiple volumes in the same sandbox at different mount paths.
- multiple volumes can be mounted to a single sandbox
- a single volume can be mounted to multiple sandboxes
## Create volumes
Create a volume.
For persistent per-user, per-tenant, or per-workspace storage, use one shared volume per use case, environment, or project (for example a volume for staging and another for production), and set a dedicated `subpath` when you create each sandbox. The sandbox sees only that prefix inside the volume; it cannot access sibling subpaths.
This is the default pattern we recommend because it:
- stays within the per-organization volume [limits](#pricing--limits)
- avoids mounting a separate volume for every user or sandbox
- continues to provide strong isolation at the mount boundary
1. Go to [Daytona Volumes ↗](https://app.daytona.io/dashboard/volumes)
2. Click the **Create Volume** button
3. Enter the volume name
```python
from daytona import Daytona
daytona = Daytona()
volume = daytona.volume.create("my-awesome-volume")
```
```typescript
import { Daytona } from "@daytona/sdk";
const daytona = new Daytona();
const volume = await daytona.volume.create("my-awesome-volume");
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
volume = daytona.volume.create("my-awesome-volume")
```
```go
package main
import (
"context"
"fmt"
"log"
"github.com/daytona/clients/sdk-go/pkg/daytona"
)
func main() {
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
volume, err := client.Volume.Create(context.Background(), "my-awesome-volume")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Volume ID: %s\n", volume.ID)
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.model.Volume;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
Volume volume = daytona.volume().create("my-awesome-volume");
}
}
}
```
```shell
daytona volume create my-awesome-volume
```
```bash
curl 'https://app.daytona.io/api/volumes' \
--request POST \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"name": "my-awesome-volume"
}'
```
## Wait for a volume to be ready
Wait until a newly created volume reaches the `ready` state before mounting it to a sandbox. Mounting a volume that is still provisioning returns an error.
```python
import time
from daytona import Daytona
daytona = Daytona()
volume = daytona.volume.create("my-awesome-volume")
while volume.state != "ready":
time.sleep(1)
volume = daytona.volume.get("my-awesome-volume")
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
let volume = await daytona.volume.create('my-awesome-volume')
while (volume.state !== 'ready') {
await new Promise((resolve) => setTimeout(resolve, 1000))
volume = await daytona.volume.get('my-awesome-volume')
}
```
```go
import (
"context"
"log"
"time"
"github.com/daytona/clients/sdk-go/pkg/daytona"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
volume, err := client.Volume.Create(ctx, "my-awesome-volume")
if err != nil {
log.Fatal(err)
}
volume, err = client.Volume.WaitForReady(ctx, volume, 2*time.Minute)
if err != nil {
log.Fatal(err)
}
```
## Mount volumes
Mount a volume to a sandbox at sandbox creation time.
For per-user or multi-tenant data, pass `subpath` so only the specified folder inside the volume is visible at `mount_path`. Mount the entire volume (omit `subpath`) when every sandbox that uses that volume should see the same tree. The volume must be in the `ready` state.
Volume mount paths must meet the following requirements:
- **Must be absolute paths**: mount paths must start with `/` (e.g., `/home/daytona/volume`)
- **Cannot be root directory**: cannot mount to `/` or `//`
- **No relative path components**: cannot contain `/../`, `/./`, or end with `/..` or `/.`
- **No consecutive slashes**: cannot contain multiple consecutive slashes like `//` (except at the beginning)
- **Cannot mount to system directories**: the following system directories are prohibited: `/proc`, `/sys`, `/dev`, `/boot`, `/etc`, `/bin`, `/sbin`, `/lib`, `/lib64`
When you set `subpath`, the value must meet the following requirements:
- **No leading slash**: subpaths are S3 key prefixes and must not start with `/`
- **No path traversal**: cannot contain `..`
- **No consecutive slashes**: cannot contain `//`
```python
from daytona import CreateSandboxFromSnapshotParams, Daytona, VolumeMount
daytona = Daytona()
# Create a new volume or get an existing one
volume = daytona.volume.get("my-awesome-volume", create=True)
mount_dir = "/home/daytona/volume"
# Recommended for per-user / per-tenant data: one volume, unique subpath per sandbox
params = CreateSandboxFromSnapshotParams(
language="python",
volumes=[VolumeMount(volume_id=volume.id, mount_path=mount_dir, subpath="users/alice")],
)
sandbox = daytona.create(params)
# Entire volume at mount path (omit subpath) when all sandboxes should share the same tree
params_full = CreateSandboxFromSnapshotParams(
language="python",
volumes=[VolumeMount(volume_id=volume.id, mount_path=mount_dir)],
)
sandbox_shared = daytona.create(params_full)
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
const volume = await daytona.volume.get('my-awesome-volume', true)
const mountDir = '/home/daytona/volume'
// Recommended for per-user / per-tenant data: one volume, unique subpath per sandbox
const sandbox = await daytona.create({
language: 'typescript',
volumes: [
{ volumeId: volume.id, mountPath: mountDir, subpath: 'users/alice' },
],
})
// Entire volume at mount path (omit subpath) when all sandboxes should share the same tree
const sandboxShared = await daytona.create({
language: 'typescript',
volumes: [{ volumeId: volume.id, mountPath: mountDir }],
})
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
volume = daytona.volume.get('my-awesome-volume', create: true)
mount_dir = '/home/daytona/volume'
# Recommended for per-user / per-tenant data: one volume, unique subpath per sandbox
params = Daytona::CreateSandboxFromSnapshotParams.new(
language: Daytona::CodeLanguage::PYTHON,
volumes: [DaytonaApiClient::SandboxVolume.new(
volume_id: volume.id,
mount_path: mount_dir,
subpath: 'users/alice'
)]
)
sandbox = daytona.create(params)
# Entire volume at mount path (omit subpath) when all sandboxes should share the same tree
params_shared = Daytona::CreateSandboxFromSnapshotParams.new(
language: Daytona::CodeLanguage::PYTHON,
volumes: [DaytonaApiClient::SandboxVolume.new(volume_id: volume.id, mount_path: mount_dir)]
)
sandbox_shared = daytona.create(params_shared)
```
```go
import (
"context"
"log"
"github.com/daytona/clients/sdk-go/pkg/daytona"
"github.com/daytona/clients/sdk-go/pkg/types"
)
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
// Create a new volume or get an existing one
volume, err := client.Volume.Get(context.Background(), "my-awesome-volume")
if err != nil {
// If volume doesn't exist, create it
volume, err = client.Volume.Create(context.Background(), "my-awesome-volume")
if err != nil {
log.Fatal(err)
}
}
mountDir := "/home/daytona/volume"
// Recommended for per-user / per-tenant data: one volume, unique subpath per sandbox
subpath := "users/alice"
sandbox, err := client.Create(context.Background(), types.SnapshotParams{
SandboxBaseParams: types.SandboxBaseParams{
Language: types.CodeLanguagePython,
Volumes: []types.VolumeMount{
{VolumeID: volume.ID, MountPath: mountDir, Subpath: &subpath},
},
},
})
if err != nil {
log.Fatal(err)
}
// Entire volume at mount path (omit Subpath) when all sandboxes should share the same tree
_, err = client.Create(context.Background(), types.SnapshotParams{
SandboxBaseParams: types.SandboxBaseParams{
Language: types.CodeLanguagePython,
Volumes: []types.VolumeMount{
{VolumeID: volume.ID, MountPath: mountDir},
},
},
})
if err != nil {
log.Fatal(err)
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.exception.DaytonaNotFoundException;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
import io.daytona.sdk.model.Volume;
import io.daytona.sdk.model.VolumeMount;
import java.util.Collections;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
Volume volume;
try {
volume = daytona.volume().getByName("my-awesome-volume");
} catch (DaytonaNotFoundException e) {
volume = daytona.volume().create("my-awesome-volume");
}
String mountDir = "/home/daytona/volume";
// io.daytona.sdk.model.VolumeMount has no subpath field; use the API for subpath mounts.
// Mount the entire volume at mountPath:
CreateSandboxFromSnapshotParams paramsFull = new CreateSandboxFromSnapshotParams();
paramsFull.setLanguage("python");
VolumeMount mountFull = new VolumeMount();
mountFull.setVolumeId(volume.getId());
mountFull.setMountPath(mountDir);
paramsFull.setVolumes(Collections.singletonList(mountFull));
Sandbox sandboxShared = daytona.create(paramsFull);
}
}
}
```
```shell
daytona volume create my-awesome-volume
daytona create --volume my-awesome-volume:/home/daytona/volume
```
The `--volume` flag accepts `VOLUME_ID_OR_NAME:MOUNT_PATH` only. For a **subpath** mount, use a [SDK](#mount-volumes) or the [API](#mount-volumes) and set `subpath` on the volume entry.
```bash
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"volumes": [
{
"volumeId": "",
"mountPath": "/home/daytona/volume",
"subpath": "users/alice"
}
]
}'
```
Omit `subpath` to mount the full volume at `mountPath`.
## Work with volumes
Read from and write to a volume just like any other directory in the sandbox file system. Files written to the volume persist beyond the lifecycle of any individual sandbox.
```python
# Write to a file in the mounted volume using the Sandbox file system API
sandbox.fs.upload_file(b"Hello from Daytona volume!", "/home/daytona/volume/example.txt")
# When you're done with the sandbox, you can remove it
# The volume will persist even after the sandbox is removed
sandbox.delete()
```
```typescript
// Write to a file in the mounted volume using the Sandbox file system API
await sandbox.fs.uploadFile(
Buffer.from('Hello from Daytona volume!'),
'/home/daytona/volume/example.txt'
)
// When you're done with the sandbox, you can remove it
// The volume will persist even after the sandbox is removed
await daytona.delete(sandbox)
```
```ruby
# Write to a file in the mounted volume using the Sandbox file system API
sandbox.fs.upload_file('Hello from Daytona volume!', '/home/daytona/volume/example.txt')
# When you're done with the sandbox, you can remove it
# The volume will persist even after the sandbox is removed
daytona.delete(sandbox)
```
```go
import (
"context"
"log"
)
// Write to a file in the mounted volume
err := sandbox.FileSystem.UploadFile(context.Background(), []byte("Hello from Daytona volume!"), "/home/daytona/volume/example.txt")
if err != nil {
log.Fatal(err)
}
// When you're done with the sandbox, you can remove it
// The volume will persist even after the sandbox is removed
err = sandbox.Delete(context.Background())
if err != nil {
log.Fatal(err)
}
```
```java
import java.nio.charset.StandardCharsets;
// Write to a file in the mounted volume using the Sandbox file system API
sandbox.fs.uploadFile(
"Hello from Daytona volume!".getBytes(StandardCharsets.UTF_8),
"/home/daytona/volume/example.txt");
// When you're done with the sandbox, you can remove it
// The volume will persist even after the sandbox is removed
sandbox.delete();
```
## Get a volume by name
Get a volume by its name.
```python
daytona.volume.get("my-awesome-volume", create=True)
```
```typescript
await daytona.volume.get('my-awesome-volume', true)
```
```ruby
daytona.volume.get('my-awesome-volume', create: true)
```
```go
volume, err := client.Volume.Get(ctx, "my-awesome-volume")
```
```java
daytona.volume().getByName("my-awesome-volume");
```
```shell
daytona volume get my-awesome-volume
```
```bash
curl 'https://app.daytona.io/api/volumes/by-name/my-awesome-volume' \
--header 'Authorization: Bearer '
```
## Get a volume by ID
Get a volume by its ID.
```bash
curl 'https://app.daytona.io/api/volumes/' \
--header 'Authorization: Bearer '
```
## List volumes
List all volumes.
```python
daytona.volume.list()
```
```typescript
await daytona.volume.list()
```
```ruby
daytona.volume.list
```
```go
volumes, err := client.Volume.List(ctx)
```
```java
daytona.volume().list();
```
```shell
daytona volume list
```
```bash
curl 'https://app.daytona.io/api/volumes' \
--header 'Authorization: Bearer '
```
## Delete volumes
Delete a volume. Deletion is asynchronous: the volume moves through `pending_delete` and `deleting` before it is removed. Deleted volumes cannot be recovered.
A volume can be deleted only when it is in the `ready` or `error` state and is not mounted by any active sandbox. Attempting to delete a volume that is still in use returns a `409` error.
```python
daytona.volume.delete(volume)
```
```typescript
await daytona.volume.delete(volume)
```
```ruby
daytona.volume.delete(volume)
```
```go
err := client.Volume.Delete(ctx, volume)
```
```java
daytona.volume().delete(volume.getId());
```
```shell
daytona volume delete
```
```bash
curl 'https://app.daytona.io/api/volumes/' \
--request DELETE \
--header 'Authorization: Bearer '
```
## Share data between sandboxes
Share data across sandboxes by mounting the same volume in each one.
A producer sandbox writes to the volume and is then deleted; a separately created consumer sandbox mounts the same volume by ID and reads the data. Volume contents persist independently of any individual sandbox.
Sandboxes that mount the same volume see writes immediately, but FUSE-backed volumes are not transactional. If two sandboxes write to the same path concurrently, the last write wins. Coordinate access in your application when ordering matters.
```python
from daytona import CreateSandboxFromSnapshotParams, Daytona, VolumeMount
daytona = Daytona()
volume = daytona.volume.get("shared-data", create=True)
mount_dir = "/home/daytona/volume"
# Producer: write data into the volume, then delete the sandbox
producer = daytona.create(CreateSandboxFromSnapshotParams(
language="python",
volumes=[VolumeMount(volume_id=volume.id, mount_path=mount_dir)],
))
producer.fs.upload_file(b"shared payload", f"{mount_dir}/payload.bin")
producer.delete()
# Consumer: a separate sandbox mounts the same volume by ID and reads the data
consumer = daytona.create(CreateSandboxFromSnapshotParams(
language="python",
volumes=[VolumeMount(volume_id=volume.id, mount_path=mount_dir)],
))
data = consumer.fs.download_file(f"{mount_dir}/payload.bin")
print(data.decode())
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
const volume = await daytona.volume.get('shared-data', true)
const mountDir = '/home/daytona/volume'
// Producer: write data into the volume, then delete the sandbox
const producer = await daytona.create({
language: 'typescript',
volumes: [{ volumeId: volume.id, mountPath: mountDir }],
})
await producer.fs.uploadFile(Buffer.from('shared payload'), `${mountDir}/payload.bin`)
await daytona.delete(producer)
// Consumer: a separate sandbox mounts the same volume by ID and reads the data
const consumer = await daytona.create({
language: 'typescript',
volumes: [{ volumeId: volume.id, mountPath: mountDir }],
})
const data = await consumer.fs.downloadFile(`${mountDir}/payload.bin`)
console.log(data.toString())
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
volume = daytona.volume.get('shared-data', create: true)
mount_dir = '/home/daytona/volume'
mount = DaytonaApiClient::SandboxVolume.new(volume_id: volume.id, mount_path: mount_dir)
# Producer: write data into the volume, then delete the sandbox
producer = daytona.create(Daytona::CreateSandboxFromSnapshotParams.new(
language: Daytona::CodeLanguage::PYTHON,
volumes: [mount]
))
producer.fs.upload_file('shared payload', "#{mount_dir}/payload.bin")
daytona.delete(producer)
# Consumer: a separate sandbox mounts the same volume by ID and reads the data
consumer = daytona.create(Daytona::CreateSandboxFromSnapshotParams.new(
language: Daytona::CodeLanguage::PYTHON,
volumes: [mount]
))
data = consumer.fs.download_file("#{mount_dir}/payload.bin")
puts data
```
```go
import (
"context"
"fmt"
"log"
"github.com/daytona/clients/sdk-go/pkg/daytona"
"github.com/daytona/clients/sdk-go/pkg/types"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
volume, err := client.Volume.Get(ctx, "shared-data")
if err != nil {
volume, err = client.Volume.Create(ctx, "shared-data")
if err != nil {
log.Fatal(err)
}
}
mountDir := "/home/daytona/volume"
mount := types.VolumeMount{VolumeID: volume.ID, MountPath: mountDir}
// Producer: write data into the volume, then delete the sandbox
producer, err := client.Create(ctx, types.SnapshotParams{
SandboxBaseParams: types.SandboxBaseParams{
Language: types.CodeLanguagePython,
Volumes: []types.VolumeMount{mount},
},
})
if err != nil {
log.Fatal(err)
}
if err := producer.FileSystem.UploadFile(ctx, []byte("shared payload"), mountDir+"/payload.bin"); err != nil {
log.Fatal(err)
}
if err := producer.Delete(ctx); err != nil {
log.Fatal(err)
}
// Consumer: a separate sandbox mounts the same volume by ID and reads the data
consumer, err := client.Create(ctx, types.SnapshotParams{
SandboxBaseParams: types.SandboxBaseParams{
Language: types.CodeLanguagePython,
Volumes: []types.VolumeMount{mount},
},
})
if err != nil {
log.Fatal(err)
}
data, err := consumer.FileSystem.DownloadFile(ctx, mountDir+"/payload.bin", nil)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(data))
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.exception.DaytonaNotFoundException;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
import io.daytona.sdk.model.Volume;
import io.daytona.sdk.model.VolumeMount;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
Volume volume;
try {
volume = daytona.volume().getByName("shared-data");
} catch (DaytonaNotFoundException e) {
volume = daytona.volume().create("shared-data");
}
String mountDir = "/home/daytona/volume";
VolumeMount mount = new VolumeMount();
mount.setVolumeId(volume.getId());
mount.setMountPath(mountDir);
// Producer: write data into the volume, then delete the sandbox
CreateSandboxFromSnapshotParams producerParams = new CreateSandboxFromSnapshotParams();
producerParams.setLanguage("python");
producerParams.setVolumes(Collections.singletonList(mount));
Sandbox producer = daytona.create(producerParams);
producer.fs.uploadFile(
"shared payload".getBytes(StandardCharsets.UTF_8),
mountDir + "/payload.bin");
producer.delete();
// Consumer: a separate sandbox mounts the same volume by ID and reads the data
CreateSandboxFromSnapshotParams consumerParams = new CreateSandboxFromSnapshotParams();
consumerParams.setLanguage("python");
consumerParams.setVolumes(Collections.singletonList(mount));
Sandbox consumer = daytona.create(consumerParams);
byte[] data = consumer.fs.downloadFile(mountDir + "/payload.bin");
System.out.println(new String(data, StandardCharsets.UTF_8));
}
}
}
```
## Mount multiple volumes to one sandbox
Mount more than one volume to a single sandbox by passing entries in the `volumes` list. Use this pattern to combine shared assets, models, or datasets in one volume with separate per-application or per-user state in another, exposed at distinct mount paths.
```python
from daytona import CreateSandboxFromSnapshotParams, Daytona, VolumeMount
daytona = Daytona()
shared_assets = daytona.volume.get("shared-assets", create=True)
logs = daytona.volume.get("logs", create=True)
sandbox = daytona.create(CreateSandboxFromSnapshotParams(
language="python",
volumes=[
VolumeMount(volume_id=shared_assets.id, mount_path="/home/daytona/assets"),
VolumeMount(volume_id=logs.id, mount_path="/home/daytona/logs"),
],
))
```
```typescript
const sharedAssets = await daytona.volume.get('shared-assets', true)
const logs = await daytona.volume.get('logs', true)
const sandbox = await daytona.create({
language: 'typescript',
volumes: [
{ volumeId: sharedAssets.id, mountPath: '/home/daytona/assets' },
{ volumeId: logs.id, mountPath: '/home/daytona/logs' },
],
})
```
```ruby
shared_assets = daytona.volume.get('shared-assets', create: true)
logs = daytona.volume.get('logs', create: true)
params = Daytona::CreateSandboxFromSnapshotParams.new(
language: Daytona::CodeLanguage::PYTHON,
volumes: [
DaytonaApiClient::SandboxVolume.new(volume_id: shared_assets.id, mount_path: '/home/daytona/assets'),
DaytonaApiClient::SandboxVolume.new(volume_id: logs.id, mount_path: '/home/daytona/logs')
]
)
sandbox = daytona.create(params)
```
```go
sharedAssets, err := client.Volume.Get(ctx, "shared-assets")
if err != nil {
sharedAssets, err = client.Volume.Create(ctx, "shared-assets")
if err != nil {
log.Fatal(err)
}
}
logs, err := client.Volume.Get(ctx, "logs")
if err != nil {
logs, err = client.Volume.Create(ctx, "logs")
if err != nil {
log.Fatal(err)
}
}
sandbox, err := client.Create(ctx, types.SnapshotParams{
SandboxBaseParams: types.SandboxBaseParams{
Language: types.CodeLanguagePython,
Volumes: []types.VolumeMount{
{VolumeID: sharedAssets.ID, MountPath: "/home/daytona/assets"},
{VolumeID: logs.ID, MountPath: "/home/daytona/logs"},
},
},
})
if err != nil {
log.Fatal(err)
}
```
```java
Volume sharedAssets;
try {
sharedAssets = daytona.volume().getByName("shared-assets");
} catch (DaytonaNotFoundException e) {
sharedAssets = daytona.volume().create("shared-assets");
}
Volume logs;
try {
logs = daytona.volume().getByName("logs");
} catch (DaytonaNotFoundException e) {
logs = daytona.volume().create("logs");
}
VolumeMount assetsMount = new VolumeMount();
assetsMount.setVolumeId(sharedAssets.getId());
assetsMount.setMountPath("/home/daytona/assets");
VolumeMount logsMount = new VolumeMount();
logsMount.setVolumeId(logs.getId());
logsMount.setMountPath("/home/daytona/logs");
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setLanguage("python");
params.setVolumes(java.util.Arrays.asList(assetsMount, logsMount));
Sandbox sandbox = daytona.create(params);
```
## Multi-tenant isolation with subpaths
Isolate per-tenant or per-user data inside a single shared volume by setting a unique `subpath` on each sandbox's volume mount. Each sandbox sees only files under its assigned subpath at `mount_path` and cannot read or write sibling subpaths within the same volume. This is the recommended pattern for multi-tenant workloads because it stays within the [per-organization volume limit](#pricing--limits) instead of creating one volume per tenant.
Isolation is enforced at the FUSE mount boundary. Each sandbox sees its assigned subpath as the volume root, so a sandbox mounted at `users/alice` cannot reach `users/bob` through relative paths such as `../bob`.
```python
from daytona import CreateSandboxFromSnapshotParams, Daytona, VolumeMount
daytona = Daytona()
volume = daytona.volume.get("tenants", create=True)
mount_dir = "/home/daytona/data"
# Tenant A
alice_sandbox = daytona.create(CreateSandboxFromSnapshotParams(
language="python",
volumes=[VolumeMount(volume_id=volume.id, mount_path=mount_dir, subpath="users/alice")],
))
alice_sandbox.fs.upload_file(b"alice's data", f"{mount_dir}/notes.txt")
# Tenant B sees only its own subpath; alice's notes.txt is invisible
bob_sandbox = daytona.create(CreateSandboxFromSnapshotParams(
language="python",
volumes=[VolumeMount(volume_id=volume.id, mount_path=mount_dir, subpath="users/bob")],
))
bob_sandbox.fs.upload_file(b"bob's data", f"{mount_dir}/notes.txt")
```
```typescript
const volume = await daytona.volume.get('tenants', true)
const mountDir = '/home/daytona/data'
// Tenant A
const aliceSandbox = await daytona.create({
language: 'typescript',
volumes: [{ volumeId: volume.id, mountPath: mountDir, subpath: 'users/alice' }],
})
await aliceSandbox.fs.uploadFile(Buffer.from("alice's data"), `${mountDir}/notes.txt`)
// Tenant B sees only its own subpath; alice's notes.txt is invisible
const bobSandbox = await daytona.create({
language: 'typescript',
volumes: [{ volumeId: volume.id, mountPath: mountDir, subpath: 'users/bob' }],
})
await bobSandbox.fs.uploadFile(Buffer.from("bob's data"), `${mountDir}/notes.txt`)
```
```ruby
volume = daytona.volume.get('tenants', create: true)
mount_dir = '/home/daytona/data'
# Tenant A
alice_params = Daytona::CreateSandboxFromSnapshotParams.new(
language: Daytona::CodeLanguage::PYTHON,
volumes: [DaytonaApiClient::SandboxVolume.new(
volume_id: volume.id, mount_path: mount_dir, subpath: 'users/alice'
)]
)
alice_sandbox = daytona.create(alice_params)
alice_sandbox.fs.upload_file("alice's data", "#{mount_dir}/notes.txt")
# Tenant B sees only its own subpath; alice's notes.txt is invisible
bob_params = Daytona::CreateSandboxFromSnapshotParams.new(
language: Daytona::CodeLanguage::PYTHON,
volumes: [DaytonaApiClient::SandboxVolume.new(
volume_id: volume.id, mount_path: mount_dir, subpath: 'users/bob'
)]
)
bob_sandbox = daytona.create(bob_params)
bob_sandbox.fs.upload_file("bob's data", "#{mount_dir}/notes.txt")
```
```go
volume, err := client.Volume.Get(ctx, "tenants")
if err != nil {
volume, err = client.Volume.Create(ctx, "tenants")
if err != nil {
log.Fatal(err)
}
}
mountDir := "/home/daytona/data"
// Tenant A
aliceSubpath := "users/alice"
aliceSandbox, err := client.Create(ctx, types.SnapshotParams{
SandboxBaseParams: types.SandboxBaseParams{
Language: types.CodeLanguagePython,
Volumes: []types.VolumeMount{
{VolumeID: volume.ID, MountPath: mountDir, Subpath: &aliceSubpath},
},
},
})
if err != nil {
log.Fatal(err)
}
if err := aliceSandbox.FileSystem.UploadFile(ctx, []byte("alice's data"), mountDir+"/notes.txt"); err != nil {
log.Fatal(err)
}
// Tenant B sees only its own subpath; alice's notes.txt is invisible
bobSubpath := "users/bob"
bobSandbox, err := client.Create(ctx, types.SnapshotParams{
SandboxBaseParams: types.SandboxBaseParams{
Language: types.CodeLanguagePython,
Volumes: []types.VolumeMount{
{VolumeID: volume.ID, MountPath: mountDir, Subpath: &bobSubpath},
},
},
})
if err != nil {
log.Fatal(err)
}
if err := bobSandbox.FileSystem.UploadFile(ctx, []byte("bob's data"), mountDir+"/notes.txt"); err != nil {
log.Fatal(err)
}
```
The Java SDK and CLI do not currently expose `subpath`. Use the REST API directly when you need subpath mounts from those clients.
```bash
# Tenant A
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"volumes": [
{
"volumeId": "",
"mountPath": "/home/daytona/data",
"subpath": "users/alice"
}
]
}'
# Tenant B
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
"volumes": [
{
"volumeId": "",
"mountPath": "/home/daytona/data",
"subpath": "users/bob"
}
]
}'
```
## Limitations
Since volumes are FUSE-based mounts, they can not be used for applications that require block storage access (like database tables). Volumes are generally slower for both read and write operations compared to the local sandbox file system.
## Pricing & Limits
Daytona volumes are included at no additional cost. Each organization can create up to 100 volumes, and volume data does not count against your storage quota.
You can view your current volume usage in the [Daytona Volumes ↗](https://app.daytona.io/dashboard/volumes).
# Mount External Storage
Mount object storage (Amazon S3, Cloudflare R2, Google Cloud Storage, Azure Blob) and filesystems like Archil and MesaFS into a Daytona sandbox as a regular directory. The sandbox reads from and writes to the bucket as if it were a local directory, so existing tools, scripts, and agents work without changes. This is useful for bringing in datasets, model weights, or build artifacts that already live in your own cloud account.
External storage mounts and [Daytona Volumes](https://www.daytona.io/docs/en/volumes.md) are complementary FUSE-based mechanisms — both expose remote object storage as a regular sandbox directory, both can be shared across sandboxes, and both persist beyond any individual sandbox's lifetime. The main distinction is **where the data physically lives**: Daytona Volumes are hosted on Daytona's own S3-compatible object store, while external mounts connect to a bucket or filesystem hosted on another provider (Amazon S3, Cloudflare R2, GCS, Azure Blob, Archil, MesaFS).
External storage is mounted using FUSE. Daytona supports two approaches, and each provider section below shows both — pick whichever fits your workflow:
- **Pre-built snapshot** — build a [snapshot](https://www.daytona.io/docs/en/snapshots.md) once with the FUSE tool (`mount-s3`, `gcsfuse`, `blobfuse2`) built-in, then launch every sandbox from that snapshot. Cold starts are fast and predictable. Best for production.
- **Runtime install** — launch a default sandbox and `apt-get install` the FUSE tool when the sandbox starts. Adds time to sandbox startup, but you don't manage snapshots. Best for quick experiments.
Both approaches end with the same mount command and the same usage — the only difference is when the FUSE tool gets installed.
## Mount an Amazon S3 bucket
Mount an S3 bucket using [Mountpoint for Amazon S3 ↗](https://github.com/awslabs/mountpoint-s3) — AWS's official FUSE client, optimized for high throughput on S3.
**Credentials** — set `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` in your local environment. The snippets below pass them into the sandbox via `envVars`, and `mount-s3` reads them from there.
### Pre-built snapshot
Build a snapshot with `mount-s3` preinstalled, then launch all S3-enabled sandboxes from that snapshot. This removes per-sandbox package install work, keeps cold starts predictable, and gives you a reusable baseline image for production workloads.
#### Build a snapshot
Create a reusable snapshot that installs `mount-s3` and its system dependencies. After it finishes, every sandbox launched from `fuse-s3` already has the mount binary available.
```python
from daytona import CreateSnapshotParams, Daytona, Image
daytona = Daytona()
image = (
Image.base("daytonaio/sandbox")
.run_commands(
"sudo apt-get update "
"&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget",
'arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" '
'&& wget -O /tmp/mount-s3.deb '
'"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" '
"&& sudo apt-get install -y /tmp/mount-s3.deb "
"&& rm /tmp/mount-s3.deb",
)
)
daytona.snapshot.create(
CreateSnapshotParams(name="fuse-s3", image=image),
on_logs=print,
)
```
```typescript
import { Daytona, Image } from '@daytona/sdk'
const daytona = new Daytona()
const image = Image.base('daytonaio/sandbox').runCommands(
'sudo apt-get update ' +
'&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget',
'arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" ' +
'&& wget -O /tmp/mount-s3.deb ' +
'"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" ' +
'&& sudo apt-get install -y /tmp/mount-s3.deb ' +
'&& rm /tmp/mount-s3.deb',
)
await daytona.snapshot.create(
{ name: 'fuse-s3', image },
{ onLogs: console.log },
)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
image = Daytona::Image
.base('daytonaio/sandbox')
.run_commands(
'sudo apt-get update ' \
'&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget',
'arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" ' \
'&& wget -O /tmp/mount-s3.deb ' \
'"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" ' \
'&& sudo apt-get install -y /tmp/mount-s3.deb ' \
'&& rm /tmp/mount-s3.deb'
)
daytona.snapshot.create(
Daytona::CreateSnapshotParams.new(name: 'fuse-s3', image: image),
on_logs: proc { |chunk| print(chunk) }
)
```
```go
import (
"context"
"fmt"
"log"
"github.com/daytona/clients/sdk-go/pkg/daytona"
"github.com/daytona/clients/sdk-go/pkg/types"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
image := daytona.Base("daytonaio/sandbox").
Run("sudo apt-get update && sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget").
Run(`arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" && ` +
`wget -O /tmp/mount-s3.deb "https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" && ` +
`sudo apt-get install -y /tmp/mount-s3.deb && rm /tmp/mount-s3.deb`)
_, logChan, err := client.Snapshot.Create(ctx, &types.CreateSnapshotParams{
Name: "fuse-s3",
Image: image,
})
if err != nil {
log.Fatal(err)
}
for line := range logChan {
fmt.Print(line)
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Image;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
Image image = Image.base("daytonaio/sandbox")
.runCommands(
"sudo apt-get update "
+ "&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget",
"arch=\"$(dpkg --print-architecture | sed s/amd64/x86_64/)\" "
+ "&& wget -O /tmp/mount-s3.deb "
+ "\"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb\" "
+ "&& sudo apt-get install -y /tmp/mount-s3.deb "
+ "&& rm /tmp/mount-s3.deb"
);
daytona.snapshot().create("fuse-s3", image, System.out::println);
}
}
}
```
#### Launch and mount
Pass AWS credentials as environment variables on sandbox creation. `mount-s3` reads them automatically.
```python
import os
from daytona import CreateSandboxFromSnapshotParams, Daytona
daytona = Daytona()
sandbox = daytona.create(
CreateSandboxFromSnapshotParams(
snapshot="fuse-s3",
env_vars={
"AWS_ACCESS_KEY_ID": os.environ["AWS_ACCESS_KEY_ID"],
"AWS_SECRET_ACCESS_KEY": os.environ["AWS_SECRET_ACCESS_KEY"],
},
)
)
mount_path = "/home/daytona/s3"
# mount-s3 daemonizes by default and reads AWS_* from the environment
sandbox.process.exec(f"mkdir -p {mount_path}")
sandbox.process.exec(f"mount-s3 my-bucket {mount_path}")
# Read and write through the mount as if it were a local directory
sandbox.process.exec(f"echo 'hello from Daytona' > {mount_path}/hello.txt")
response = sandbox.process.exec(f"cat {mount_path}/hello.txt")
print(response.result)
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
const sandbox = await daytona.create({
snapshot: 'fuse-s3',
envVars: {
AWS_ACCESS_KEY_ID: process.env.AWS_ACCESS_KEY_ID!,
AWS_SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY!,
},
})
const mountPath = '/home/daytona/s3'
// mount-s3 daemonizes by default and reads AWS_* from the environment
await sandbox.process.executeCommand(`mkdir -p ${mountPath}`)
await sandbox.process.executeCommand(`mount-s3 my-bucket ${mountPath}`)
// Read and write through the mount as if it were a local directory
await sandbox.process.executeCommand(`echo 'hello from Daytona' > ${mountPath}/hello.txt`)
const response = await sandbox.process.executeCommand(`cat ${mountPath}/hello.txt`)
console.log(response.result)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
sandbox = daytona.create(
Daytona::CreateSandboxFromSnapshotParams.new(
snapshot: 'fuse-s3',
env_vars: {
'AWS_ACCESS_KEY_ID' => ENV.fetch('AWS_ACCESS_KEY_ID'),
'AWS_SECRET_ACCESS_KEY' => ENV.fetch('AWS_SECRET_ACCESS_KEY')
}
)
)
mount_path = '/home/daytona/s3'
# mount-s3 daemonizes by default and reads AWS_* from the environment
sandbox.process.exec(command: "mkdir -p #{mount_path}")
sandbox.process.exec(command: "mount-s3 my-bucket #{mount_path}")
# Read and write through the mount as if it were a local directory
sandbox.process.exec(command: "echo 'hello from Daytona' > #{mount_path}/hello.txt")
response = sandbox.process.exec(command: "cat #{mount_path}/hello.txt")
puts response.result
```
```go
import (
"context"
"fmt"
"log"
"os"
"github.com/daytona/clients/sdk-go/pkg/daytona"
"github.com/daytona/clients/sdk-go/pkg/types"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
sandbox, err := client.Create(ctx, types.SnapshotParams{
Snapshot: "fuse-s3",
SandboxBaseParams: types.SandboxBaseParams{
EnvVars: map[string]string{
"AWS_ACCESS_KEY_ID": os.Getenv("AWS_ACCESS_KEY_ID"),
"AWS_SECRET_ACCESS_KEY": os.Getenv("AWS_SECRET_ACCESS_KEY"),
},
},
})
if err != nil {
log.Fatal(err)
}
mountPath := "/home/daytona/s3"
// mount-s3 daemonizes by default and reads AWS_* from the environment
if _, err := sandbox.Process.ExecuteCommand(ctx, "mkdir -p "+mountPath); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx, "mount-s3 my-bucket "+mountPath); err != nil {
log.Fatal(err)
}
// Read and write through the mount as if it were a local directory
if _, err := sandbox.Process.ExecuteCommand(ctx, "echo 'hello from Daytona' > "+mountPath+"/hello.txt"); err != nil {
log.Fatal(err)
}
response, err := sandbox.Process.ExecuteCommand(ctx, "cat "+mountPath+"/hello.txt")
if err != nil {
log.Fatal(err)
}
fmt.Println(response.Result)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
import io.daytona.sdk.model.ExecuteResponse;
import java.util.Map;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setSnapshot("fuse-s3");
params.setEnvVars(Map.of(
"AWS_ACCESS_KEY_ID", System.getenv("AWS_ACCESS_KEY_ID"),
"AWS_SECRET_ACCESS_KEY", System.getenv("AWS_SECRET_ACCESS_KEY")
));
Sandbox sandbox = daytona.create(params);
String mountPath = "/home/daytona/s3";
// mount-s3 daemonizes by default and reads AWS_* from the environment
sandbox.getProcess().executeCommand("mkdir -p " + mountPath);
sandbox.getProcess().executeCommand("mount-s3 my-bucket " + mountPath);
// Read and write through the mount as if it were a local directory
sandbox.getProcess().executeCommand(
"echo 'hello from Daytona' > " + mountPath + "/hello.txt");
ExecuteResponse response = sandbox.getProcess().executeCommand(
"cat " + mountPath + "/hello.txt");
System.out.println(response.getResult());
}
}
}
```
### Runtime install
Start from a default sandbox and install `mount-s3` during startup before running the mount command. This is useful for quick testing and temporary environments where you do not want to maintain a custom snapshot, with the tradeoff of slower cold starts.
```python
import os
from daytona import CreateSandboxBaseParams, Daytona
daytona = Daytona()
sandbox = daytona.create(
CreateSandboxBaseParams(
env_vars={
"AWS_ACCESS_KEY_ID": os.environ["AWS_ACCESS_KEY_ID"],
"AWS_SECRET_ACCESS_KEY": os.environ["AWS_SECRET_ACCESS_KEY"],
},
)
)
# Install mount-s3 at runtime
sandbox.process.exec(
"sudo apt-get update "
"&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget"
)
sandbox.process.exec(
'arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" '
'&& wget -O /tmp/mount-s3.deb '
'"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" '
"&& sudo apt-get install -y /tmp/mount-s3.deb"
)
# Mount and use
mount_path = "/home/daytona/s3"
sandbox.process.exec(f"mkdir -p {mount_path} && mount-s3 my-bucket {mount_path}")
response = sandbox.process.exec(f"ls {mount_path}")
print(response.result)
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
const sandbox = await daytona.create({
envVars: {
AWS_ACCESS_KEY_ID: process.env.AWS_ACCESS_KEY_ID!,
AWS_SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY!,
},
})
// Install mount-s3 at runtime
await sandbox.process.executeCommand(
'sudo apt-get update ' +
'&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget',
)
await sandbox.process.executeCommand(
'arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" ' +
'&& wget -O /tmp/mount-s3.deb ' +
'"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" ' +
'&& sudo apt-get install -y /tmp/mount-s3.deb',
)
// Mount and use
const mountPath = '/home/daytona/s3'
await sandbox.process.executeCommand(`mkdir -p ${mountPath} && mount-s3 my-bucket ${mountPath}`)
const response = await sandbox.process.executeCommand(`ls ${mountPath}`)
console.log(response.result)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
sandbox = daytona.create(
Daytona::CreateSandboxBaseParams.new(
env_vars: {
'AWS_ACCESS_KEY_ID' => ENV.fetch('AWS_ACCESS_KEY_ID'),
'AWS_SECRET_ACCESS_KEY' => ENV.fetch('AWS_SECRET_ACCESS_KEY')
}
)
)
# Install mount-s3 at runtime
sandbox.process.exec(
command: 'sudo apt-get update ' \
'&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget'
)
sandbox.process.exec(
command: 'arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" ' \
'&& wget -O /tmp/mount-s3.deb ' \
'"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" ' \
'&& sudo apt-get install -y /tmp/mount-s3.deb'
)
# Mount and use
mount_path = '/home/daytona/s3'
sandbox.process.exec(command: "mkdir -p #{mount_path} && mount-s3 my-bucket #{mount_path}")
response = sandbox.process.exec(command: "ls #{mount_path}")
puts response.result
```
```go
import (
"context"
"fmt"
"log"
"os"
"github.com/daytona/clients/sdk-go/pkg/daytona"
"github.com/daytona/clients/sdk-go/pkg/types"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
sandbox, err := client.Create(ctx, types.SnapshotParams{
SandboxBaseParams: types.SandboxBaseParams{
EnvVars: map[string]string{
"AWS_ACCESS_KEY_ID": os.Getenv("AWS_ACCESS_KEY_ID"),
"AWS_SECRET_ACCESS_KEY": os.Getenv("AWS_SECRET_ACCESS_KEY"),
},
},
})
if err != nil {
log.Fatal(err)
}
// Install mount-s3 at runtime
if _, err := sandbox.Process.ExecuteCommand(ctx,
"sudo apt-get update && sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget"); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx,
`arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" && `+
`wget -O /tmp/mount-s3.deb "https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" && `+
`sudo apt-get install -y /tmp/mount-s3.deb`); err != nil {
log.Fatal(err)
}
// Mount and use
mountPath := "/home/daytona/s3"
if _, err := sandbox.Process.ExecuteCommand(ctx,
"mkdir -p "+mountPath+" && mount-s3 my-bucket "+mountPath); err != nil {
log.Fatal(err)
}
response, err := sandbox.Process.ExecuteCommand(ctx, "ls "+mountPath)
if err != nil {
log.Fatal(err)
}
fmt.Println(response.Result)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
import io.daytona.sdk.model.ExecuteResponse;
import java.util.Map;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setEnvVars(Map.of(
"AWS_ACCESS_KEY_ID", System.getenv("AWS_ACCESS_KEY_ID"),
"AWS_SECRET_ACCESS_KEY", System.getenv("AWS_SECRET_ACCESS_KEY")
));
Sandbox sandbox = daytona.create(params);
// Install mount-s3 at runtime
sandbox.getProcess().executeCommand(
"sudo apt-get update "
+ "&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget");
sandbox.getProcess().executeCommand(
"arch=\"$(dpkg --print-architecture | sed s/amd64/x86_64/)\" "
+ "&& wget -O /tmp/mount-s3.deb "
+ "\"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb\" "
+ "&& sudo apt-get install -y /tmp/mount-s3.deb");
// Mount and use
String mountPath = "/home/daytona/s3";
sandbox.getProcess().executeCommand(
"mkdir -p " + mountPath + " && mount-s3 my-bucket " + mountPath);
ExecuteResponse response = sandbox.getProcess().executeCommand("ls " + mountPath);
System.out.println(response.getResult());
}
}
}
```
## Mount a Cloudflare R2 bucket
Cloudflare R2 is S3-compatible, so the same `mount-s3` tool works. Pass an explicit `--endpoint-url` pointing at your R2 account.
**Credentials** — set `R2_ACCOUNT_ID`, `R2_ACCESS_KEY_ID`, and `R2_SECRET_ACCESS_KEY` in your local environment. R2 is S3-compatible, so the snippets below pass your R2 keys into the sandbox via `envVars` under the `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` names that `mount-s3` expects.
### Pre-built snapshot
Build a snapshot with `mount-s3` preinstalled, then launch all R2-enabled sandboxes from that snapshot. The mount flow stays identical to S3 except for the R2 `--endpoint-url`, and startup remains fast because installation is done once at snapshot build time.
#### Build a snapshot
Create a reusable snapshot that installs the same `mount-s3` tool used for S3. R2 remains S3-compatible, so this snapshot is identical to S3 setup and only the runtime mount command changes.
```python
from daytona import CreateSnapshotParams, Daytona, Image
daytona = Daytona()
image = (
Image.base("daytonaio/sandbox")
.run_commands(
"sudo apt-get update "
"&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget",
'arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" '
'&& wget -O /tmp/mount-s3.deb '
'"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" '
"&& sudo apt-get install -y /tmp/mount-s3.deb "
"&& rm /tmp/mount-s3.deb",
)
)
daytona.snapshot.create(
CreateSnapshotParams(name="fuse-r2", image=image),
on_logs=print,
)
```
```typescript
import { Daytona, Image } from '@daytona/sdk'
const daytona = new Daytona()
const image = Image.base('daytonaio/sandbox').runCommands(
'sudo apt-get update ' +
'&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget',
'arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" ' +
'&& wget -O /tmp/mount-s3.deb ' +
'"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" ' +
'&& sudo apt-get install -y /tmp/mount-s3.deb ' +
'&& rm /tmp/mount-s3.deb',
)
await daytona.snapshot.create(
{ name: 'fuse-r2', image },
{ onLogs: console.log },
)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
image = Daytona::Image
.base('daytonaio/sandbox')
.run_commands(
'sudo apt-get update ' \
'&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget',
'arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" ' \
'&& wget -O /tmp/mount-s3.deb ' \
'"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" ' \
'&& sudo apt-get install -y /tmp/mount-s3.deb ' \
'&& rm /tmp/mount-s3.deb'
)
daytona.snapshot.create(
Daytona::CreateSnapshotParams.new(name: 'fuse-r2', image: image),
on_logs: proc { |chunk| print(chunk) }
)
```
```go
import (
"context"
"fmt"
"log"
"github.com/daytona/clients/sdk-go/pkg/daytona"
"github.com/daytona/clients/sdk-go/pkg/types"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
image := daytona.Base("daytonaio/sandbox").
Run("sudo apt-get update && sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget").
Run(`arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" && ` +
`wget -O /tmp/mount-s3.deb "https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" && ` +
`sudo apt-get install -y /tmp/mount-s3.deb && rm /tmp/mount-s3.deb`)
_, logChan, err := client.Snapshot.Create(ctx, &types.CreateSnapshotParams{
Name: "fuse-r2",
Image: image,
})
if err != nil {
log.Fatal(err)
}
for line := range logChan {
fmt.Print(line)
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Image;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
Image image = Image.base("daytonaio/sandbox")
.runCommands(
"sudo apt-get update "
+ "&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget",
"arch=\"$(dpkg --print-architecture | sed s/amd64/x86_64/)\" "
+ "&& wget -O /tmp/mount-s3.deb "
+ "\"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb\" "
+ "&& sudo apt-get install -y /tmp/mount-s3.deb "
+ "&& rm /tmp/mount-s3.deb"
);
daytona.snapshot().create("fuse-r2", image, System.out::println);
}
}
}
```
#### Launch and mount
Pass your R2 credentials into the sandbox as `AWS_*` environment variables and mount with the R2 endpoint URL. This keeps the authentication flow compatible with `mount-s3` while targeting your Cloudflare account.
```python
import os
from daytona import CreateSandboxFromSnapshotParams, Daytona
daytona = Daytona()
# R2 credentials live in your Cloudflare dashboard under R2 > Manage API Tokens
account_id = os.environ["R2_ACCOUNT_ID"]
sandbox = daytona.create(
CreateSandboxFromSnapshotParams(
snapshot="fuse-r2",
env_vars={
"AWS_ACCESS_KEY_ID": os.environ["R2_ACCESS_KEY_ID"],
"AWS_SECRET_ACCESS_KEY": os.environ["R2_SECRET_ACCESS_KEY"],
},
)
)
mount_path = "/home/daytona/r2"
sandbox.process.exec(f"mkdir -p {mount_path}")
sandbox.process.exec(
f"mount-s3 --endpoint-url https://{account_id}.r2.cloudflarestorage.com "
f"my-r2-bucket {mount_path}"
)
response = sandbox.process.exec(f"ls {mount_path}")
print(response.result)
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
// R2 credentials live in your Cloudflare dashboard under R2 > Manage API Tokens
const accountId = process.env.R2_ACCOUNT_ID!
const sandbox = await daytona.create({
snapshot: 'fuse-r2',
envVars: {
AWS_ACCESS_KEY_ID: process.env.R2_ACCESS_KEY_ID!,
AWS_SECRET_ACCESS_KEY: process.env.R2_SECRET_ACCESS_KEY!,
},
})
const mountPath = '/home/daytona/r2'
await sandbox.process.executeCommand(`mkdir -p ${mountPath}`)
await sandbox.process.executeCommand(
`mount-s3 --endpoint-url https://${accountId}.r2.cloudflarestorage.com ` +
`my-r2-bucket ${mountPath}`,
)
const response = await sandbox.process.executeCommand(`ls ${mountPath}`)
console.log(response.result)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
# R2 credentials live in your Cloudflare dashboard under R2 > Manage API Tokens
account_id = ENV.fetch('R2_ACCOUNT_ID')
sandbox = daytona.create(
Daytona::CreateSandboxFromSnapshotParams.new(
snapshot: 'fuse-r2',
env_vars: {
'AWS_ACCESS_KEY_ID' => ENV.fetch('R2_ACCESS_KEY_ID'),
'AWS_SECRET_ACCESS_KEY' => ENV.fetch('R2_SECRET_ACCESS_KEY')
}
)
)
mount_path = '/home/daytona/r2'
sandbox.process.exec(command: "mkdir -p #{mount_path}")
sandbox.process.exec(
command: "mount-s3 --endpoint-url https://#{account_id}.r2.cloudflarestorage.com " \
"my-r2-bucket #{mount_path}"
)
response = sandbox.process.exec(command: "ls #{mount_path}")
puts response.result
```
```go
import (
"context"
"fmt"
"log"
"os"
"github.com/daytona/clients/sdk-go/pkg/daytona"
"github.com/daytona/clients/sdk-go/pkg/types"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
// R2 credentials live in your Cloudflare dashboard under R2 > Manage API Tokens
accountID := os.Getenv("R2_ACCOUNT_ID")
sandbox, err := client.Create(ctx, types.SnapshotParams{
Snapshot: "fuse-r2",
SandboxBaseParams: types.SandboxBaseParams{
EnvVars: map[string]string{
"AWS_ACCESS_KEY_ID": os.Getenv("R2_ACCESS_KEY_ID"),
"AWS_SECRET_ACCESS_KEY": os.Getenv("R2_SECRET_ACCESS_KEY"),
},
},
})
if err != nil {
log.Fatal(err)
}
mountPath := "/home/daytona/r2"
if _, err := sandbox.Process.ExecuteCommand(ctx, "mkdir -p "+mountPath); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx,
"mount-s3 --endpoint-url https://"+accountID+".r2.cloudflarestorage.com "+
"my-r2-bucket "+mountPath); err != nil {
log.Fatal(err)
}
response, err := sandbox.Process.ExecuteCommand(ctx, "ls "+mountPath)
if err != nil {
log.Fatal(err)
}
fmt.Println(response.Result)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
import io.daytona.sdk.model.ExecuteResponse;
import java.util.Map;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
// R2 credentials live in your Cloudflare dashboard under R2 > Manage API Tokens
String accountId = System.getenv("R2_ACCOUNT_ID");
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setSnapshot("fuse-r2");
params.setEnvVars(Map.of(
"AWS_ACCESS_KEY_ID", System.getenv("R2_ACCESS_KEY_ID"),
"AWS_SECRET_ACCESS_KEY", System.getenv("R2_SECRET_ACCESS_KEY")
));
Sandbox sandbox = daytona.create(params);
String mountPath = "/home/daytona/r2";
sandbox.getProcess().executeCommand("mkdir -p " + mountPath);
sandbox.getProcess().executeCommand(
"mount-s3 --endpoint-url https://" + accountId + ".r2.cloudflarestorage.com "
+ "my-r2-bucket " + mountPath);
ExecuteResponse response = sandbox.getProcess().executeCommand("ls " + mountPath);
System.out.println(response.getResult());
}
}
}
```
### Runtime install
Start from a default sandbox and install `mount-s3` during startup, then mount your bucket with the R2 `--endpoint-url`. This path is convenient for prototyping or one-off tasks, but each new sandbox pays the package installation cost.
```python
import os
from daytona import CreateSandboxBaseParams, Daytona
daytona = Daytona()
account_id = os.environ["R2_ACCOUNT_ID"]
sandbox = daytona.create(
CreateSandboxBaseParams(
env_vars={
"AWS_ACCESS_KEY_ID": os.environ["R2_ACCESS_KEY_ID"],
"AWS_SECRET_ACCESS_KEY": os.environ["R2_SECRET_ACCESS_KEY"],
},
)
)
# Install mount-s3
sandbox.process.exec(
"sudo apt-get update "
"&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget"
)
sandbox.process.exec(
'arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" '
'&& wget -O /tmp/mount-s3.deb '
'"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" '
"&& sudo apt-get install -y /tmp/mount-s3.deb"
)
# Mount with R2 endpoint
mount_path = "/home/daytona/r2"
sandbox.process.exec(
f"mkdir -p {mount_path} && "
f"mount-s3 --endpoint-url https://{account_id}.r2.cloudflarestorage.com "
f"my-r2-bucket {mount_path}"
)
response = sandbox.process.exec(f"ls {mount_path}")
print(response.result)
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
const accountId = process.env.R2_ACCOUNT_ID!
const sandbox = await daytona.create({
envVars: {
AWS_ACCESS_KEY_ID: process.env.R2_ACCESS_KEY_ID!,
AWS_SECRET_ACCESS_KEY: process.env.R2_SECRET_ACCESS_KEY!,
},
})
// Install mount-s3
await sandbox.process.executeCommand(
'sudo apt-get update ' +
'&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget',
)
await sandbox.process.executeCommand(
'arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" ' +
'&& wget -O /tmp/mount-s3.deb ' +
'"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" ' +
'&& sudo apt-get install -y /tmp/mount-s3.deb',
)
// Mount with R2 endpoint
const mountPath = '/home/daytona/r2'
await sandbox.process.executeCommand(
`mkdir -p ${mountPath} && ` +
`mount-s3 --endpoint-url https://${accountId}.r2.cloudflarestorage.com ` +
`my-r2-bucket ${mountPath}`,
)
const response = await sandbox.process.executeCommand(`ls ${mountPath}`)
console.log(response.result)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
account_id = ENV.fetch('R2_ACCOUNT_ID')
sandbox = daytona.create(
Daytona::CreateSandboxBaseParams.new(
env_vars: {
'AWS_ACCESS_KEY_ID' => ENV.fetch('R2_ACCESS_KEY_ID'),
'AWS_SECRET_ACCESS_KEY' => ENV.fetch('R2_SECRET_ACCESS_KEY')
}
)
)
# Install mount-s3
sandbox.process.exec(
command: 'sudo apt-get update ' \
'&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget'
)
sandbox.process.exec(
command: 'arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" ' \
'&& wget -O /tmp/mount-s3.deb ' \
'"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" ' \
'&& sudo apt-get install -y /tmp/mount-s3.deb'
)
# Mount with R2 endpoint
mount_path = '/home/daytona/r2'
sandbox.process.exec(
command: "mkdir -p #{mount_path} && " \
"mount-s3 --endpoint-url https://#{account_id}.r2.cloudflarestorage.com " \
"my-r2-bucket #{mount_path}"
)
response = sandbox.process.exec(command: "ls #{mount_path}")
puts response.result
```
```go
import (
"context"
"fmt"
"log"
"os"
"github.com/daytona/clients/sdk-go/pkg/daytona"
"github.com/daytona/clients/sdk-go/pkg/types"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
accountID := os.Getenv("R2_ACCOUNT_ID")
sandbox, err := client.Create(ctx, types.SnapshotParams{
SandboxBaseParams: types.SandboxBaseParams{
EnvVars: map[string]string{
"AWS_ACCESS_KEY_ID": os.Getenv("R2_ACCESS_KEY_ID"),
"AWS_SECRET_ACCESS_KEY": os.Getenv("R2_SECRET_ACCESS_KEY"),
},
},
})
if err != nil {
log.Fatal(err)
}
// Install mount-s3
if _, err := sandbox.Process.ExecuteCommand(ctx,
"sudo apt-get update && sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget"); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx,
`arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" && `+
`wget -O /tmp/mount-s3.deb "https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" && `+
`sudo apt-get install -y /tmp/mount-s3.deb`); err != nil {
log.Fatal(err)
}
// Mount with R2 endpoint
mountPath := "/home/daytona/r2"
if _, err := sandbox.Process.ExecuteCommand(ctx,
"mkdir -p "+mountPath+" && "+
"mount-s3 --endpoint-url https://"+accountID+".r2.cloudflarestorage.com "+
"my-r2-bucket "+mountPath); err != nil {
log.Fatal(err)
}
response, err := sandbox.Process.ExecuteCommand(ctx, "ls "+mountPath)
if err != nil {
log.Fatal(err)
}
fmt.Println(response.Result)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
import io.daytona.sdk.model.ExecuteResponse;
import java.util.Map;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
String accountId = System.getenv("R2_ACCOUNT_ID");
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setEnvVars(Map.of(
"AWS_ACCESS_KEY_ID", System.getenv("R2_ACCESS_KEY_ID"),
"AWS_SECRET_ACCESS_KEY", System.getenv("R2_SECRET_ACCESS_KEY")
));
Sandbox sandbox = daytona.create(params);
// Install mount-s3
sandbox.getProcess().executeCommand(
"sudo apt-get update "
+ "&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget");
sandbox.getProcess().executeCommand(
"arch=\"$(dpkg --print-architecture | sed s/amd64/x86_64/)\" "
+ "&& wget -O /tmp/mount-s3.deb "
+ "\"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb\" "
+ "&& sudo apt-get install -y /tmp/mount-s3.deb");
// Mount with R2 endpoint
String mountPath = "/home/daytona/r2";
sandbox.getProcess().executeCommand(
"mkdir -p " + mountPath + " && "
+ "mount-s3 --endpoint-url https://" + accountId + ".r2.cloudflarestorage.com "
+ "my-r2-bucket " + mountPath);
ExecuteResponse response = sandbox.getProcess().executeCommand("ls " + mountPath);
System.out.println(response.getResult());
}
}
}
```
## Mount a Tigris bucket
Mount a Tigris bucket with the same `mount-s3` tool used for S3. Pass `--endpoint-url https://t3.storage.dev`, because Tigris uses one global endpoint with no per-account subdomain. Tigris also supports bucket snapshots and copy-on-write forks through request headers, so each sandbox can use an isolated writable bucket without duplicating source data.
**Credentials** — set `TIGRIS_STORAGE_ACCESS_KEY_ID` and `TIGRIS_STORAGE_SECRET_ACCESS_KEY` in your local environment. The snippets below pass these into the sandbox via `envVars` under the `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` names that `mount-s3` expects.
### Pre-built snapshot
Build a snapshot with `mount-s3` preinstalled, then launch Tigris sandboxes from that snapshot. The mount flow matches S3 except for the Tigris `--endpoint-url`, and startup stays fast because installation happens once during snapshot build.
#### Build a snapshot
Create a reusable snapshot that installs `mount-s3`. Because Tigris is S3-compatible, this setup matches S3 and only the runtime mount command changes.
```python
from daytona import CreateSnapshotParams, Daytona, Image
daytona = Daytona()
image = (
Image.base("daytonaio/sandbox")
.run_commands(
"sudo apt-get update "
"&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget",
'arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" '
'&& wget -O /tmp/mount-s3.deb '
'"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" '
"&& sudo apt-get install -y /tmp/mount-s3.deb "
"&& rm /tmp/mount-s3.deb",
)
)
daytona.snapshot.create(
CreateSnapshotParams(name="fuse-tigris", image=image),
on_logs=print,
)
```
```typescript
import { Daytona, Image } from '@daytona/sdk'
const daytona = new Daytona()
const image = Image.base('daytonaio/sandbox').runCommands(
'sudo apt-get update ' +
'&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget',
'arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" ' +
'&& wget -O /tmp/mount-s3.deb ' +
'"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" ' +
'&& sudo apt-get install -y /tmp/mount-s3.deb ' +
'&& rm /tmp/mount-s3.deb',
)
await daytona.snapshot.create(
{ name: 'fuse-tigris', image },
{ onLogs: console.log },
)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
image = Daytona::Image
.base('daytonaio/sandbox')
.run_commands(
'sudo apt-get update ' \
'&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget',
'arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" ' \
'&& wget -O /tmp/mount-s3.deb ' \
'"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" ' \
'&& sudo apt-get install -y /tmp/mount-s3.deb ' \
'&& rm /tmp/mount-s3.deb'
)
daytona.snapshot.create(
Daytona::CreateSnapshotParams.new(name: 'fuse-tigris', image: image),
on_logs: proc { |chunk| print(chunk) }
)
```
```go
import (
"context"
"fmt"
"log"
"github.com/daytona/clients/sdk-go/pkg/daytona"
"github.com/daytona/clients/sdk-go/pkg/types"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
image := daytona.Base("daytonaio/sandbox").
Run("sudo apt-get update && sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget").
Run(`arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" && ` +
`wget -O /tmp/mount-s3.deb "https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" && ` +
`sudo apt-get install -y /tmp/mount-s3.deb && rm /tmp/mount-s3.deb`)
_, logChan, err := client.Snapshot.Create(ctx, &types.CreateSnapshotParams{
Name: "fuse-tigris",
Image: image,
})
if err != nil {
log.Fatal(err)
}
for line := range logChan {
fmt.Print(line)
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Image;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
Image image = Image.base("daytonaio/sandbox")
.runCommands(
"sudo apt-get update "
+ "&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget",
"arch=\"$(dpkg --print-architecture | sed s/amd64/x86_64/)\" "
+ "&& wget -O /tmp/mount-s3.deb "
+ "\"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb\" "
+ "&& sudo apt-get install -y /tmp/mount-s3.deb "
+ "&& rm /tmp/mount-s3.deb"
);
daytona.snapshot().create("fuse-tigris", image, System.out::println);
}
}
}
```
#### Launch and mount
Pass Tigris credentials into the sandbox as `AWS_*` environment variables, then mount with the Tigris endpoint URL. This keeps authentication compatible with `mount-s3` while targeting your Tigris account.
```python
import os
from daytona import CreateSandboxFromSnapshotParams, Daytona
daytona = Daytona()
sandbox = daytona.create(
CreateSandboxFromSnapshotParams(
snapshot="fuse-tigris",
env_vars={
"AWS_ACCESS_KEY_ID": os.environ["TIGRIS_STORAGE_ACCESS_KEY_ID"],
"AWS_SECRET_ACCESS_KEY": os.environ["TIGRIS_STORAGE_SECRET_ACCESS_KEY"],
},
)
)
mount_path = "/home/daytona/tigris"
sandbox.process.exec(f"mkdir -p {mount_path}")
sandbox.process.exec(
f"mount-s3 --endpoint-url https://t3.storage.dev "
f"my-tigris-bucket {mount_path}"
)
response = sandbox.process.exec(f"ls {mount_path}")
print(response.result)
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
const sandbox = await daytona.create({
snapshot: 'fuse-tigris',
envVars: {
AWS_ACCESS_KEY_ID: process.env.TIGRIS_STORAGE_ACCESS_KEY_ID!,
AWS_SECRET_ACCESS_KEY: process.env.TIGRIS_STORAGE_SECRET_ACCESS_KEY!,
},
})
const mountPath = '/home/daytona/tigris'
await sandbox.process.executeCommand(`mkdir -p ${mountPath}`)
await sandbox.process.executeCommand(
`mount-s3 --endpoint-url https://t3.storage.dev ` +
`my-tigris-bucket ${mountPath}`,
)
const response = await sandbox.process.executeCommand(`ls ${mountPath}`)
console.log(response.result)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
sandbox = daytona.create(
Daytona::CreateSandboxFromSnapshotParams.new(
snapshot: 'fuse-tigris',
env_vars: {
'AWS_ACCESS_KEY_ID' => ENV.fetch('TIGRIS_STORAGE_ACCESS_KEY_ID'),
'AWS_SECRET_ACCESS_KEY' => ENV.fetch('TIGRIS_STORAGE_SECRET_ACCESS_KEY')
}
)
)
mount_path = '/home/daytona/tigris'
sandbox.process.exec(command: "mkdir -p #{mount_path}")
sandbox.process.exec(
command: 'mount-s3 --endpoint-url https://t3.storage.dev ' \
"my-tigris-bucket #{mount_path}"
)
response = sandbox.process.exec(command: "ls #{mount_path}")
puts response.result
```
```go
import (
"context"
"fmt"
"log"
"os"
"github.com/daytona/clients/sdk-go/pkg/daytona"
"github.com/daytona/clients/sdk-go/pkg/types"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
sandbox, err := client.Create(ctx, types.SnapshotParams{
Snapshot: "fuse-tigris",
SandboxBaseParams: types.SandboxBaseParams{
EnvVars: map[string]string{
"AWS_ACCESS_KEY_ID": os.Getenv("TIGRIS_STORAGE_ACCESS_KEY_ID"),
"AWS_SECRET_ACCESS_KEY": os.Getenv("TIGRIS_STORAGE_SECRET_ACCESS_KEY"),
},
},
})
if err != nil {
log.Fatal(err)
}
mountPath := "/home/daytona/tigris"
if _, err := sandbox.Process.ExecuteCommand(ctx, "mkdir -p "+mountPath); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx,
"mount-s3 --endpoint-url https://t3.storage.dev "+
"my-tigris-bucket "+mountPath); err != nil {
log.Fatal(err)
}
response, err := sandbox.Process.ExecuteCommand(ctx, "ls "+mountPath)
if err != nil {
log.Fatal(err)
}
fmt.Println(response.Result)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
import io.daytona.sdk.model.ExecuteResponse;
import java.util.Map;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setSnapshot("fuse-tigris");
params.setEnvVars(Map.of(
"AWS_ACCESS_KEY_ID", System.getenv("TIGRIS_STORAGE_ACCESS_KEY_ID"),
"AWS_SECRET_ACCESS_KEY", System.getenv("TIGRIS_STORAGE_SECRET_ACCESS_KEY")
));
Sandbox sandbox = daytona.create(params);
String mountPath = "/home/daytona/tigris";
sandbox.getProcess().executeCommand("mkdir -p " + mountPath);
sandbox.getProcess().executeCommand(
"mount-s3 --endpoint-url https://t3.storage.dev "
+ "my-tigris-bucket " + mountPath);
ExecuteResponse response = sandbox.getProcess().executeCommand("ls " + mountPath);
System.out.println(response.getResult());
}
}
}
```
### Runtime install
Start from a default sandbox, install `mount-s3` during startup, then mount with the Tigris `--endpoint-url`. This path is convenient for prototyping or one-off tasks, but each new sandbox repeats package installation.
```python
import os
from daytona import CreateSandboxBaseParams, Daytona
daytona = Daytona()
sandbox = daytona.create(
CreateSandboxBaseParams(
env_vars={
"AWS_ACCESS_KEY_ID": os.environ["TIGRIS_STORAGE_ACCESS_KEY_ID"],
"AWS_SECRET_ACCESS_KEY": os.environ["TIGRIS_STORAGE_SECRET_ACCESS_KEY"],
},
)
)
# Install mount-s3
sandbox.process.exec(
"sudo apt-get update "
"&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget"
)
sandbox.process.exec(
'arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" '
'&& wget -O /tmp/mount-s3.deb '
'"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" '
"&& sudo apt-get install -y /tmp/mount-s3.deb"
)
# Mount with Tigris endpoint
mount_path = "/home/daytona/tigris"
sandbox.process.exec(
f"mkdir -p {mount_path} && "
f"mount-s3 --endpoint-url https://t3.storage.dev "
f"my-tigris-bucket {mount_path}"
)
response = sandbox.process.exec(f"ls {mount_path}")
print(response.result)
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
const sandbox = await daytona.create({
envVars: {
AWS_ACCESS_KEY_ID: process.env.TIGRIS_STORAGE_ACCESS_KEY_ID!,
AWS_SECRET_ACCESS_KEY: process.env.TIGRIS_STORAGE_SECRET_ACCESS_KEY!,
},
})
// Install mount-s3
await sandbox.process.executeCommand(
'sudo apt-get update ' +
'&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget',
)
await sandbox.process.executeCommand(
'arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" ' +
'&& wget -O /tmp/mount-s3.deb ' +
'"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" ' +
'&& sudo apt-get install -y /tmp/mount-s3.deb',
)
// Mount with Tigris endpoint
const mountPath = '/home/daytona/tigris'
await sandbox.process.executeCommand(
`mkdir -p ${mountPath} && ` +
`mount-s3 --endpoint-url https://t3.storage.dev ` +
`my-tigris-bucket ${mountPath}`,
)
const response = await sandbox.process.executeCommand(`ls ${mountPath}`)
console.log(response.result)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
sandbox = daytona.create(
Daytona::CreateSandboxBaseParams.new(
env_vars: {
'AWS_ACCESS_KEY_ID' => ENV.fetch('TIGRIS_STORAGE_ACCESS_KEY_ID'),
'AWS_SECRET_ACCESS_KEY' => ENV.fetch('TIGRIS_STORAGE_SECRET_ACCESS_KEY')
}
)
)
# Install mount-s3
sandbox.process.exec(
command: 'sudo apt-get update ' \
'&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget'
)
sandbox.process.exec(
command: 'arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" ' \
'&& wget -O /tmp/mount-s3.deb ' \
'"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" ' \
'&& sudo apt-get install -y /tmp/mount-s3.deb'
)
# Mount with Tigris endpoint
mount_path = '/home/daytona/tigris'
sandbox.process.exec(
command: "mkdir -p #{mount_path} && " \
'mount-s3 --endpoint-url https://t3.storage.dev ' \
"my-tigris-bucket #{mount_path}"
)
response = sandbox.process.exec(command: "ls #{mount_path}")
puts response.result
```
```go
import (
"context"
"fmt"
"log"
"os"
"github.com/daytona/clients/sdk-go/pkg/daytona"
"github.com/daytona/clients/sdk-go/pkg/types"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
sandbox, err := client.Create(ctx, types.SnapshotParams{
SandboxBaseParams: types.SandboxBaseParams{
EnvVars: map[string]string{
"AWS_ACCESS_KEY_ID": os.Getenv("TIGRIS_STORAGE_ACCESS_KEY_ID"),
"AWS_SECRET_ACCESS_KEY": os.Getenv("TIGRIS_STORAGE_SECRET_ACCESS_KEY"),
},
},
})
if err != nil {
log.Fatal(err)
}
// Install mount-s3
if _, err := sandbox.Process.ExecuteCommand(ctx,
"sudo apt-get update && sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget"); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx,
`arch="$(dpkg --print-architecture | sed s/amd64/x86_64/)" && `+
`wget -O /tmp/mount-s3.deb "https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb" && `+
`sudo apt-get install -y /tmp/mount-s3.deb`); err != nil {
log.Fatal(err)
}
// Mount with Tigris endpoint
mountPath := "/home/daytona/tigris"
if _, err := sandbox.Process.ExecuteCommand(ctx,
"mkdir -p "+mountPath+" && "+
"mount-s3 --endpoint-url https://t3.storage.dev "+
"my-tigris-bucket "+mountPath); err != nil {
log.Fatal(err)
}
response, err := sandbox.Process.ExecuteCommand(ctx, "ls "+mountPath)
if err != nil {
log.Fatal(err)
}
fmt.Println(response.Result)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
import io.daytona.sdk.model.ExecuteResponse;
import java.util.Map;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setEnvVars(Map.of(
"AWS_ACCESS_KEY_ID", System.getenv("TIGRIS_STORAGE_ACCESS_KEY_ID"),
"AWS_SECRET_ACCESS_KEY", System.getenv("TIGRIS_STORAGE_SECRET_ACCESS_KEY")
));
Sandbox sandbox = daytona.create(params);
// Install mount-s3
sandbox.getProcess().executeCommand(
"sudo apt-get update "
+ "&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates wget");
sandbox.getProcess().executeCommand(
"arch=\"$(dpkg --print-architecture | sed s/amd64/x86_64/)\" "
+ "&& wget -O /tmp/mount-s3.deb "
+ "\"https://s3.amazonaws.com/mountpoint-s3-release/latest/${arch}/mount-s3.deb\" "
+ "&& sudo apt-get install -y /tmp/mount-s3.deb");
// Mount with Tigris endpoint
String mountPath = "/home/daytona/tigris";
sandbox.getProcess().executeCommand(
"mkdir -p " + mountPath + " && "
+ "mount-s3 --endpoint-url https://t3.storage.dev "
+ "my-tigris-bucket " + mountPath);
ExecuteResponse response = sandbox.getProcess().executeCommand("ls " + mountPath);
System.out.println(response.getResult());
}
}
}
```
### Mount a copy-on-write fork per sandbox
A Tigris bucket fork is a new bucket created from a snapshot of a source bucket. The fork shares underlying storage with the source until written to — new writes go only to the fork, and the source bucket and other forks are unaffected. Fork creation is constant-time regardless of source bucket size.
This pattern fits Daytona sandboxes when each sandbox needs a writable copy of a shared dataset (model weights, fixtures, golden data) without duplicating it on every launch.
**Prerequisite** — the source bucket must be created with snapshots enabled. This is a one-time setup, done outside the per-sandbox flow:
```typescript
import { createBucket } from '@tigrisdata/storage'
await createBucket('my-source-bucket', { enableSnapshot: true })
```
In any S3 SDK, send a `CreateBucket` request with the header `X-Tigris-Enable-Snapshot: true`.
The `@tigrisdata/agent-kit` package wraps this workflow as `createForks()`. It snapshots the source bucket and creates one or more forks in a single call. Passing `credentials: { role: 'Editor' }` also creates a scoped access key per fork, so each sandbox can read and write only its own fork bucket instead of the full account.
```typescript
import { createForks, teardownForks } from '@tigrisdata/agent-kit'
import { Daytona } from '@daytona/sdk'
const SOURCE_BUCKET = 'my-source-bucket'
// 1. Snapshot the source and create a fork with a scoped access key
const { data: forkSet, error } = await createForks(SOURCE_BUCKET, 1, {
credentials: { role: 'Editor' },
})
if (error) throw error
const fork = forkSet.forks[0]
// 2. Launch the sandbox with the fork's scoped credentials
const daytona = new Daytona()
const sandbox = await daytona.create({
snapshot: 'fuse-tigris',
envVars: {
AWS_ACCESS_KEY_ID: fork.credentials!.accessKeyId,
AWS_SECRET_ACCESS_KEY: fork.credentials!.secretAccessKey,
},
})
// 3. Mount the fork bucket
const mountPath = '/home/daytona/tigris'
await sandbox.process.executeCommand(`mkdir -p ${mountPath}`)
await sandbox.process.executeCommand(
`mount-s3 --endpoint-url https://t3.storage.dev ${fork.bucket} ${mountPath}`,
)
try {
const response = await sandbox.process.executeCommand(`ls ${mountPath}`)
console.log(response.result)
} finally {
await daytona.delete(sandbox)
await teardownForks(forkSet) // revokes the scoped key and deletes the fork bucket
}
```
To run the same workflow from a language without an `agent-kit` equivalent, use any S3 SDK and send the headers documented below. Send `X-Tigris-Snapshot: true` on `CreateBucket` for the source name, then capture `X-Tigris-Snapshot-Version` from the response. Next, send `CreateBucket` for the fork name with `X-Tigris-Fork-Source-Bucket` and `X-Tigris-Fork-Source-Bucket-Snapshot`. Mount the fork with the same `mount-s3` snippets shown above, replacing the bucket name with the fork bucket.
#### Reference: Tigris-specific headers
These headers drive snapshot and fork operations over the S3 API. Use them with any AWS SDK (boto3, aws-sdk-go-v2, aws-sdk-java-v2, aws-sdk-ruby) by attaching a request interceptor.
| Header | Sent on | Purpose |
| ------------------------------------------ | ----------------------------------------- | ------------------------------------------------------------------------------ |
| **`X-Tigris-Enable-Snapshot: true`** | **`CreateBucket`** (new source) | Enable snapshots on a new bucket. Required before snapshotting it. |
| **`X-Tigris-Snapshot: true`** | **`CreateBucket`** (existing source name) | Take a snapshot of the bucket. Optional **`; name=`** suffix labels it. |
| **`X-Tigris-Snapshot-Version`** | Response to snapshot create | Snapshot version ID returned to the caller — pass to fork creation. |
| **`X-Tigris-Fork-Source-Bucket`** | **`CreateBucket`** (new fork name) | Source bucket to fork from. |
| **`X-Tigris-Fork-Source-Bucket-Snapshot`** | **`CreateBucket`** (new fork name) | Source snapshot version to fork from. |
Source: [**`tigrisdata/storage`**](https://github.com/tigrisdata/storage). See **`shared/headers.ts`** for the full header set.
## Mount a Google Cloud Storage bucket
Mount a GCS bucket using [gcsfuse ↗](https://github.com/GoogleCloudPlatform/gcsfuse) — Google's official FUSE client.
**Credentials** — `gcsfuse` reads a service account JSON key file. The snippets below read the key from a local path on your host and upload it into the sandbox via `sandbox.fs`.
:::note
Daytona's base image is Debian Trixie, but Google has not published a Trixie-specific gcsfuse repository yet. The `gcsfuse-bookworm` repo packages run cleanly on Trixie, so we use them in the snippets below.
:::
### Pre-built snapshot
Build a snapshot with `gcsfuse` preinstalled, then launch all GCS-enabled sandboxes from that snapshot. This avoids repeating apt repository setup and package installation for every sandbox, which makes startup behavior more consistent.
#### Build a snapshot
Create a reusable snapshot that installs `gcsfuse` plus its apt repository configuration. After this step, GCS-enabled sandboxes can mount immediately without repeating package setup.
```python
from daytona import CreateSnapshotParams, Daytona, Image
daytona = Daytona()
image = (
Image.base("daytonaio/sandbox")
.run_commands(
"sudo apt-get update "
"&& sudo apt-get install -y --no-install-recommends ca-certificates curl gnupg",
"sudo mkdir -p /etc/apt/keyrings "
"&& curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg "
"| sudo gpg --dearmor -o /etc/apt/keyrings/gcsfuse.gpg",
'echo "deb [signed-by=/etc/apt/keyrings/gcsfuse.gpg] '
'https://packages.cloud.google.com/apt gcsfuse-bookworm main" '
"| sudo tee /etc/apt/sources.list.d/gcsfuse.list",
"sudo apt-get update && sudo apt-get install -y gcsfuse",
)
)
daytona.snapshot.create(
CreateSnapshotParams(name="fuse-gcs", image=image),
on_logs=print,
)
```
```typescript
import { Daytona, Image } from '@daytona/sdk'
const daytona = new Daytona()
const image = Image.base('daytonaio/sandbox').runCommands(
'sudo apt-get update ' +
'&& sudo apt-get install -y --no-install-recommends ca-certificates curl gnupg',
'sudo mkdir -p /etc/apt/keyrings ' +
'&& curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg ' +
'| sudo gpg --dearmor -o /etc/apt/keyrings/gcsfuse.gpg',
'echo "deb [signed-by=/etc/apt/keyrings/gcsfuse.gpg] ' +
'https://packages.cloud.google.com/apt gcsfuse-bookworm main" ' +
'| sudo tee /etc/apt/sources.list.d/gcsfuse.list',
'sudo apt-get update && sudo apt-get install -y gcsfuse',
)
await daytona.snapshot.create(
{ name: 'fuse-gcs', image },
{ onLogs: console.log },
)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
image = Daytona::Image
.base('daytonaio/sandbox')
.run_commands(
'sudo apt-get update ' \
'&& sudo apt-get install -y --no-install-recommends ca-certificates curl gnupg',
'sudo mkdir -p /etc/apt/keyrings ' \
'&& curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg ' \
'| sudo gpg --dearmor -o /etc/apt/keyrings/gcsfuse.gpg',
'echo "deb [signed-by=/etc/apt/keyrings/gcsfuse.gpg] ' \
'https://packages.cloud.google.com/apt gcsfuse-bookworm main" ' \
'| sudo tee /etc/apt/sources.list.d/gcsfuse.list',
'sudo apt-get update && sudo apt-get install -y gcsfuse'
)
daytona.snapshot.create(
Daytona::CreateSnapshotParams.new(name: 'fuse-gcs', image: image),
on_logs: proc { |chunk| print(chunk) }
)
```
```go
import (
"context"
"fmt"
"log"
"github.com/daytona/clients/sdk-go/pkg/daytona"
"github.com/daytona/clients/sdk-go/pkg/types"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
image := daytona.Base("daytonaio/sandbox").
Run("sudo apt-get update && sudo apt-get install -y --no-install-recommends ca-certificates curl gnupg").
Run("sudo mkdir -p /etc/apt/keyrings && " +
"curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | " +
"sudo gpg --dearmor -o /etc/apt/keyrings/gcsfuse.gpg").
Run(`echo "deb [signed-by=/etc/apt/keyrings/gcsfuse.gpg] ` +
`https://packages.cloud.google.com/apt gcsfuse-bookworm main" | ` +
`sudo tee /etc/apt/sources.list.d/gcsfuse.list`).
Run("sudo apt-get update && sudo apt-get install -y gcsfuse")
_, logChan, err := client.Snapshot.Create(ctx, &types.CreateSnapshotParams{
Name: "fuse-gcs",
Image: image,
})
if err != nil {
log.Fatal(err)
}
for line := range logChan {
fmt.Print(line)
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Image;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
Image image = Image.base("daytonaio/sandbox")
.runCommands(
"sudo apt-get update "
+ "&& sudo apt-get install -y --no-install-recommends ca-certificates curl gnupg",
"sudo mkdir -p /etc/apt/keyrings "
+ "&& curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg "
+ "| sudo gpg --dearmor -o /etc/apt/keyrings/gcsfuse.gpg",
"echo \"deb [signed-by=/etc/apt/keyrings/gcsfuse.gpg] "
+ "https://packages.cloud.google.com/apt gcsfuse-bookworm main\" "
+ "| sudo tee /etc/apt/sources.list.d/gcsfuse.list",
"sudo apt-get update && sudo apt-get install -y gcsfuse"
);
daytona.snapshot().create("fuse-gcs", image, System.out::println);
}
}
}
```
#### Launch and mount
`gcsfuse` authenticates to GCS with a service account JSON key. Upload it into the sandbox via `sandbox.fs` and point `gcsfuse` at it with `--key-file`.
```python
import os
from daytona import CreateSandboxFromSnapshotParams, Daytona
daytona = Daytona()
# GCS_SERVICE_ACCOUNT_KEY holds the full service account JSON as a string
service_account_key = os.environ["GCS_SERVICE_ACCOUNT_KEY"].encode()
sandbox = daytona.create(CreateSandboxFromSnapshotParams(snapshot="fuse-gcs"))
mount_path = "/home/daytona/gcs"
key_path = "/home/daytona/.gcs-key.json"
# Upload the key file into the sandbox
sandbox.fs.upload_file(service_account_key, key_path)
sandbox.process.exec(f"chmod 600 {key_path}")
# Mount the bucket
sandbox.process.exec(f"mkdir -p {mount_path}")
sandbox.process.exec(f"gcsfuse --key-file={key_path} my-gcs-bucket {mount_path}")
# Use the mount
response = sandbox.process.exec(f"ls {mount_path}")
print(response.result)
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
// GCS_SERVICE_ACCOUNT_KEY holds the full service account JSON as a string
const serviceAccountKey = Buffer.from(process.env.GCS_SERVICE_ACCOUNT_KEY!)
const sandbox = await daytona.create({ snapshot: 'fuse-gcs' })
const mountPath = '/home/daytona/gcs'
const keyPath = '/home/daytona/.gcs-key.json'
// Upload the key file into the sandbox
await sandbox.fs.uploadFile(serviceAccountKey, keyPath)
await sandbox.process.executeCommand(`chmod 600 ${keyPath}`)
// Mount the bucket
await sandbox.process.executeCommand(`mkdir -p ${mountPath}`)
await sandbox.process.executeCommand(`gcsfuse --key-file=${keyPath} my-gcs-bucket ${mountPath}`)
// Use the mount
const response = await sandbox.process.executeCommand(`ls ${mountPath}`)
console.log(response.result)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
# GCS_SERVICE_ACCOUNT_KEY holds the full service account JSON as a string
service_account_key = ENV.fetch('GCS_SERVICE_ACCOUNT_KEY')
sandbox = daytona.create(
Daytona::CreateSandboxFromSnapshotParams.new(snapshot: 'fuse-gcs')
)
mount_path = '/home/daytona/gcs'
key_path = '/home/daytona/.gcs-key.json'
# Upload the key file into the sandbox
sandbox.fs.upload_file(service_account_key, key_path)
sandbox.process.exec(command: "chmod 600 #{key_path}")
# Mount the bucket
sandbox.process.exec(command: "mkdir -p #{mount_path}")
sandbox.process.exec(command: "gcsfuse --key-file=#{key_path} my-gcs-bucket #{mount_path}")
# Use the mount
response = sandbox.process.exec(command: "ls #{mount_path}")
puts response.result
```
```go
import (
"context"
"fmt"
"log"
"os"
"github.com/daytona/clients/sdk-go/pkg/daytona"
"github.com/daytona/clients/sdk-go/pkg/types"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
// GCS_SERVICE_ACCOUNT_KEY holds the full service account JSON as a string
serviceAccountKey := []byte(os.Getenv("GCS_SERVICE_ACCOUNT_KEY"))
sandbox, err := client.Create(ctx, types.SnapshotParams{
Snapshot: "fuse-gcs",
})
if err != nil {
log.Fatal(err)
}
mountPath := "/home/daytona/gcs"
keyPath := "/home/daytona/.gcs-key.json"
// Upload the key file into the sandbox
if err := sandbox.FileSystem.UploadFile(ctx, serviceAccountKey, keyPath); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx, "chmod 600 "+keyPath); err != nil {
log.Fatal(err)
}
// Mount the bucket
if _, err := sandbox.Process.ExecuteCommand(ctx, "mkdir -p "+mountPath); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx,
"gcsfuse --key-file="+keyPath+" my-gcs-bucket "+mountPath); err != nil {
log.Fatal(err)
}
// Use the mount
response, err := sandbox.Process.ExecuteCommand(ctx, "ls "+mountPath)
if err != nil {
log.Fatal(err)
}
fmt.Println(response.Result)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
import io.daytona.sdk.model.ExecuteResponse;
import java.nio.charset.StandardCharsets;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
// GCS_SERVICE_ACCOUNT_KEY holds the full service account JSON as a string
byte[] serviceAccountKey = System.getenv("GCS_SERVICE_ACCOUNT_KEY")
.getBytes(StandardCharsets.UTF_8);
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setSnapshot("fuse-gcs");
Sandbox sandbox = daytona.create(params);
String mountPath = "/home/daytona/gcs";
String keyPath = "/home/daytona/.gcs-key.json";
// Upload the key file into the sandbox
sandbox.fs.uploadFile(serviceAccountKey, keyPath);
sandbox.getProcess().executeCommand("chmod 600 " + keyPath);
// Mount the bucket
sandbox.getProcess().executeCommand("mkdir -p " + mountPath);
sandbox.getProcess().executeCommand(
"gcsfuse --key-file=" + keyPath + " my-gcs-bucket " + mountPath);
// Use the mount
ExecuteResponse response = sandbox.getProcess().executeCommand("ls " + mountPath);
System.out.println(response.getResult());
}
}
}
```
### Runtime install
Start from a default sandbox and install `gcsfuse` when the sandbox starts, then upload the service account key and mount the bucket. This is the fastest way to iterate on setup, but every sandbox repeats install and key staging steps.
```python
import os
from daytona import CreateSandboxBaseParams, Daytona
daytona = Daytona()
# GCS_SERVICE_ACCOUNT_KEY holds the full service account JSON as a string
service_account_key = os.environ["GCS_SERVICE_ACCOUNT_KEY"].encode()
sandbox = daytona.create(CreateSandboxBaseParams())
# Install gcsfuse from the bookworm repo (works on Trixie)
sandbox.process.exec(
"sudo apt-get update "
"&& sudo apt-get install -y --no-install-recommends ca-certificates curl gnupg"
)
sandbox.process.exec(
"sudo mkdir -p /etc/apt/keyrings "
"&& curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg "
"| sudo gpg --dearmor -o /etc/apt/keyrings/gcsfuse.gpg"
)
sandbox.process.exec(
'echo "deb [signed-by=/etc/apt/keyrings/gcsfuse.gpg] '
'https://packages.cloud.google.com/apt gcsfuse-bookworm main" '
"| sudo tee /etc/apt/sources.list.d/gcsfuse.list "
"&& sudo apt-get update && sudo apt-get install -y gcsfuse"
)
# Upload the key and mount
mount_path = "/home/daytona/gcs"
key_path = "/home/daytona/.gcs-key.json"
sandbox.fs.upload_file(service_account_key, key_path)
sandbox.process.exec(f"chmod 600 {key_path}")
sandbox.process.exec(f"mkdir -p {mount_path}")
sandbox.process.exec(f"gcsfuse --key-file={key_path} my-gcs-bucket {mount_path}")
response = sandbox.process.exec(f"ls {mount_path}")
print(response.result)
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
// GCS_SERVICE_ACCOUNT_KEY holds the full service account JSON as a string
const serviceAccountKey = Buffer.from(process.env.GCS_SERVICE_ACCOUNT_KEY!)
const sandbox = await daytona.create()
// Install gcsfuse from the bookworm repo (works on Trixie)
await sandbox.process.executeCommand(
'sudo apt-get update ' +
'&& sudo apt-get install -y --no-install-recommends ca-certificates curl gnupg',
)
await sandbox.process.executeCommand(
'sudo mkdir -p /etc/apt/keyrings ' +
'&& curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg ' +
'| sudo gpg --dearmor -o /etc/apt/keyrings/gcsfuse.gpg',
)
await sandbox.process.executeCommand(
'echo "deb [signed-by=/etc/apt/keyrings/gcsfuse.gpg] ' +
'https://packages.cloud.google.com/apt gcsfuse-bookworm main" ' +
'| sudo tee /etc/apt/sources.list.d/gcsfuse.list ' +
'&& sudo apt-get update && sudo apt-get install -y gcsfuse',
)
// Upload the key and mount
const mountPath = '/home/daytona/gcs'
const keyPath = '/home/daytona/.gcs-key.json'
await sandbox.fs.uploadFile(serviceAccountKey, keyPath)
await sandbox.process.executeCommand(`chmod 600 ${keyPath}`)
await sandbox.process.executeCommand(`mkdir -p ${mountPath}`)
await sandbox.process.executeCommand(`gcsfuse --key-file=${keyPath} my-gcs-bucket ${mountPath}`)
const response = await sandbox.process.executeCommand(`ls ${mountPath}`)
console.log(response.result)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
# GCS_SERVICE_ACCOUNT_KEY holds the full service account JSON as a string
service_account_key = ENV.fetch('GCS_SERVICE_ACCOUNT_KEY')
sandbox = daytona.create(Daytona::CreateSandboxBaseParams.new)
# Install gcsfuse from the bookworm repo (works on Trixie)
sandbox.process.exec(
command: 'sudo apt-get update ' \
'&& sudo apt-get install -y --no-install-recommends ca-certificates curl gnupg'
)
sandbox.process.exec(
command: 'sudo mkdir -p /etc/apt/keyrings ' \
'&& curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg ' \
'| sudo gpg --dearmor -o /etc/apt/keyrings/gcsfuse.gpg'
)
sandbox.process.exec(
command: 'echo "deb [signed-by=/etc/apt/keyrings/gcsfuse.gpg] ' \
'https://packages.cloud.google.com/apt gcsfuse-bookworm main" ' \
'| sudo tee /etc/apt/sources.list.d/gcsfuse.list ' \
'&& sudo apt-get update && sudo apt-get install -y gcsfuse'
)
# Upload the key and mount
mount_path = '/home/daytona/gcs'
key_path = '/home/daytona/.gcs-key.json'
sandbox.fs.upload_file(service_account_key, key_path)
sandbox.process.exec(command: "chmod 600 #{key_path}")
sandbox.process.exec(command: "mkdir -p #{mount_path}")
sandbox.process.exec(command: "gcsfuse --key-file=#{key_path} my-gcs-bucket #{mount_path}")
response = sandbox.process.exec(command: "ls #{mount_path}")
puts response.result
```
```go
import (
"context"
"fmt"
"log"
"os"
"github.com/daytona/clients/sdk-go/pkg/daytona"
"github.com/daytona/clients/sdk-go/pkg/types"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
// GCS_SERVICE_ACCOUNT_KEY holds the full service account JSON as a string
serviceAccountKey := []byte(os.Getenv("GCS_SERVICE_ACCOUNT_KEY"))
sandbox, err := client.Create(ctx, types.SnapshotParams{})
if err != nil {
log.Fatal(err)
}
// Install gcsfuse from the bookworm repo (works on Trixie)
if _, err := sandbox.Process.ExecuteCommand(ctx,
"sudo apt-get update && sudo apt-get install -y --no-install-recommends ca-certificates curl gnupg"); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx,
"sudo mkdir -p /etc/apt/keyrings && "+
"curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | "+
"sudo gpg --dearmor -o /etc/apt/keyrings/gcsfuse.gpg"); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx,
`echo "deb [signed-by=/etc/apt/keyrings/gcsfuse.gpg] `+
`https://packages.cloud.google.com/apt gcsfuse-bookworm main" | `+
`sudo tee /etc/apt/sources.list.d/gcsfuse.list && `+
`sudo apt-get update && sudo apt-get install -y gcsfuse`); err != nil {
log.Fatal(err)
}
// Upload the key and mount
mountPath := "/home/daytona/gcs"
keyPath := "/home/daytona/.gcs-key.json"
if err := sandbox.FileSystem.UploadFile(ctx, serviceAccountKey, keyPath); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx, "chmod 600 "+keyPath); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx, "mkdir -p "+mountPath); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx,
"gcsfuse --key-file="+keyPath+" my-gcs-bucket "+mountPath); err != nil {
log.Fatal(err)
}
response, err := sandbox.Process.ExecuteCommand(ctx, "ls "+mountPath)
if err != nil {
log.Fatal(err)
}
fmt.Println(response.Result)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
import io.daytona.sdk.model.ExecuteResponse;
import java.nio.charset.StandardCharsets;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
// GCS_SERVICE_ACCOUNT_KEY holds the full service account JSON as a string
byte[] serviceAccountKey = System.getenv("GCS_SERVICE_ACCOUNT_KEY")
.getBytes(StandardCharsets.UTF_8);
Sandbox sandbox = daytona.create(new CreateSandboxFromSnapshotParams());
// Install gcsfuse from the bookworm repo (works on Trixie)
sandbox.getProcess().executeCommand(
"sudo apt-get update "
+ "&& sudo apt-get install -y --no-install-recommends ca-certificates curl gnupg");
sandbox.getProcess().executeCommand(
"sudo mkdir -p /etc/apt/keyrings "
+ "&& curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg "
+ "| sudo gpg --dearmor -o /etc/apt/keyrings/gcsfuse.gpg");
sandbox.getProcess().executeCommand(
"echo \"deb [signed-by=/etc/apt/keyrings/gcsfuse.gpg] "
+ "https://packages.cloud.google.com/apt gcsfuse-bookworm main\" "
+ "| sudo tee /etc/apt/sources.list.d/gcsfuse.list "
+ "&& sudo apt-get update && sudo apt-get install -y gcsfuse");
// Upload the key and mount
String mountPath = "/home/daytona/gcs";
String keyPath = "/home/daytona/.gcs-key.json";
sandbox.fs.uploadFile(serviceAccountKey, keyPath);
sandbox.getProcess().executeCommand("chmod 600 " + keyPath);
sandbox.getProcess().executeCommand("mkdir -p " + mountPath);
sandbox.getProcess().executeCommand(
"gcsfuse --key-file=" + keyPath + " my-gcs-bucket " + mountPath);
ExecuteResponse response = sandbox.getProcess().executeCommand("ls " + mountPath);
System.out.println(response.getResult());
}
}
}
```
## Mount an Azure Blob container
Mount an Azure Blob container using [blobfuse2 ↗](https://github.com/Azure/azure-storage-fuse) — Microsoft's official FUSE client.
**Credentials** — set `AZURE_STORAGE_ACCOUNT`, `AZURE_STORAGE_CONTAINER`, and `AZURE_STORAGE_ACCOUNT_KEY` in your local environment. The snippets below pass them into the sandbox via `envVars`, and `blobfuse2` reads them from its YAML config.
:::caution
Daytona's base image is Debian Trixie. Microsoft's `blobfuse2` Bookworm binary links against `libfuse3.so.3`, but Trixie's `fuse3` package bumped the SONAME to `.4`, so `libfuse3.so.3` is missing on disk. The snapshot recipe below creates a compatibility symlink. Without it, `blobfuse2` fails with `libfuse3.so.3: cannot open shared object file`.
:::
### Pre-built snapshot
Build a snapshot with `blobfuse2` and required FUSE compatibility setup preinstalled, then launch all Azure-enabled sandboxes from that snapshot. This is the recommended path for stable environments because dependency and compatibility work runs once during snapshot creation.
#### Build a snapshot
Create a reusable snapshot that installs `blobfuse2`, configures required FUSE dependencies, and applies the Trixie compatibility steps. This ensures Azure mounts work out of the box in sandboxes launched from `fuse-azure`.
```python
from daytona import CreateSnapshotParams, Daytona, Image
daytona = Daytona()
image = (
Image.base("daytonaio/sandbox")
.run_commands(
"sudo apt-get update "
"&& sudo apt-get install -y --no-install-recommends ca-certificates curl gnupg wget",
# Microsoft's apt repo (use bookworm packages on Trixie)
"wget -qO- https://packages.microsoft.com/keys/microsoft.asc "
"| sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/microsoft.gpg",
'echo "deb [arch=$(dpkg --print-architecture) '
'signed-by=/etc/apt/trusted.gpg.d/microsoft.gpg] '
'https://packages.microsoft.com/debian/12/prod bookworm main" '
"| sudo tee /etc/apt/sources.list.d/microsoft-prod.list",
"sudo apt-get update && sudo apt-get install -y blobfuse2 fuse3",
# libfuse3.so.3 compat symlink for Trixie (see :::caution above)
'src=$(find /usr/lib /lib -name "libfuse3.so.3.*" -type f 2>/dev/null '
"| sort -V | tail -1) "
'&& sudo ln -sfn "$src" "$(dirname "$src")/libfuse3.so.3" '
"&& sudo ldconfig",
"sudo touch /etc/fuse.conf "
'&& grep -qxF "user_allow_other" /etc/fuse.conf '
'|| echo "user_allow_other" | sudo tee -a /etc/fuse.conf',
)
)
daytona.snapshot.create(
CreateSnapshotParams(name="fuse-azure", image=image),
on_logs=print,
)
```
```typescript
import { Daytona, Image } from '@daytona/sdk'
const daytona = new Daytona()
const image = Image.base('daytonaio/sandbox').runCommands(
'sudo apt-get update ' +
'&& sudo apt-get install -y --no-install-recommends ca-certificates curl gnupg wget',
// Microsoft's apt repo (use bookworm packages on Trixie)
'wget -qO- https://packages.microsoft.com/keys/microsoft.asc ' +
'| sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/microsoft.gpg',
'echo "deb [arch=$(dpkg --print-architecture) ' +
'signed-by=/etc/apt/trusted.gpg.d/microsoft.gpg] ' +
'https://packages.microsoft.com/debian/12/prod bookworm main" ' +
'| sudo tee /etc/apt/sources.list.d/microsoft-prod.list',
'sudo apt-get update && sudo apt-get install -y blobfuse2 fuse3',
// libfuse3.so.3 compat symlink for Trixie (see :::caution above)
'src=$(find /usr/lib /lib -name "libfuse3.so.3.*" -type f 2>/dev/null ' +
'| sort -V | tail -1) ' +
'&& sudo ln -sfn "$src" "$(dirname "$src")/libfuse3.so.3" ' +
'&& sudo ldconfig',
'sudo touch /etc/fuse.conf ' +
'&& grep -qxF "user_allow_other" /etc/fuse.conf ' +
'|| echo "user_allow_other" | sudo tee -a /etc/fuse.conf',
)
await daytona.snapshot.create(
{ name: 'fuse-azure', image },
{ onLogs: console.log },
)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
image = Daytona::Image
.base('daytonaio/sandbox')
.run_commands(
'sudo apt-get update ' \
'&& sudo apt-get install -y --no-install-recommends ca-certificates curl gnupg wget',
# Microsoft's apt repo (use bookworm packages on Trixie)
'wget -qO- https://packages.microsoft.com/keys/microsoft.asc ' \
'| sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/microsoft.gpg',
'echo "deb [arch=$(dpkg --print-architecture) ' \
'signed-by=/etc/apt/trusted.gpg.d/microsoft.gpg] ' \
'https://packages.microsoft.com/debian/12/prod bookworm main" ' \
'| sudo tee /etc/apt/sources.list.d/microsoft-prod.list',
'sudo apt-get update && sudo apt-get install -y blobfuse2 fuse3',
# libfuse3.so.3 compat symlink for Trixie
'src=$(find /usr/lib /lib -name "libfuse3.so.3.*" -type f 2>/dev/null ' \
'| sort -V | tail -1) ' \
'&& sudo ln -sfn "$src" "$(dirname "$src")/libfuse3.so.3" ' \
'&& sudo ldconfig',
'sudo touch /etc/fuse.conf ' \
'&& grep -qxF "user_allow_other" /etc/fuse.conf ' \
'|| echo "user_allow_other" | sudo tee -a /etc/fuse.conf'
)
daytona.snapshot.create(
Daytona::CreateSnapshotParams.new(name: 'fuse-azure', image: image),
on_logs: proc { |chunk| print(chunk) }
)
```
```go
import (
"context"
"fmt"
"log"
"github.com/daytona/clients/sdk-go/pkg/daytona"
"github.com/daytona/clients/sdk-go/pkg/types"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
image := daytona.Base("daytonaio/sandbox").
Run("sudo apt-get update && sudo apt-get install -y --no-install-recommends ca-certificates curl gnupg wget").
// Microsoft's apt repo (use bookworm packages on Trixie)
Run("wget -qO- https://packages.microsoft.com/keys/microsoft.asc | " +
"sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/microsoft.gpg").
Run(`echo "deb [arch=$(dpkg --print-architecture) ` +
`signed-by=/etc/apt/trusted.gpg.d/microsoft.gpg] ` +
`https://packages.microsoft.com/debian/12/prod bookworm main" | ` +
`sudo tee /etc/apt/sources.list.d/microsoft-prod.list`).
Run("sudo apt-get update && sudo apt-get install -y blobfuse2 fuse3").
// libfuse3.so.3 compat symlink for Trixie
Run(`src=$(find /usr/lib /lib -name "libfuse3.so.3.*" -type f 2>/dev/null | sort -V | tail -1) && ` +
`sudo ln -sfn "$src" "$(dirname "$src")/libfuse3.so.3" && sudo ldconfig`).
Run(`sudo touch /etc/fuse.conf && grep -qxF "user_allow_other" /etc/fuse.conf || ` +
`echo "user_allow_other" | sudo tee -a /etc/fuse.conf`)
_, logChan, err := client.Snapshot.Create(ctx, &types.CreateSnapshotParams{
Name: "fuse-azure",
Image: image,
})
if err != nil {
log.Fatal(err)
}
for line := range logChan {
fmt.Print(line)
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Image;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
Image image = Image.base("daytonaio/sandbox")
.runCommands(
"sudo apt-get update "
+ "&& sudo apt-get install -y --no-install-recommends ca-certificates curl gnupg wget",
// Microsoft's apt repo (use bookworm packages on Trixie)
"wget -qO- https://packages.microsoft.com/keys/microsoft.asc "
+ "| sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/microsoft.gpg",
"echo \"deb [arch=$(dpkg --print-architecture) "
+ "signed-by=/etc/apt/trusted.gpg.d/microsoft.gpg] "
+ "https://packages.microsoft.com/debian/12/prod bookworm main\" "
+ "| sudo tee /etc/apt/sources.list.d/microsoft-prod.list",
"sudo apt-get update && sudo apt-get install -y blobfuse2 fuse3",
// libfuse3.so.3 compat symlink for Trixie
"src=$(find /usr/lib /lib -name \"libfuse3.so.3.*\" -type f 2>/dev/null "
+ "| sort -V | tail -1) "
+ "&& sudo ln -sfn \"$src\" \"$(dirname \"$src\")/libfuse3.so.3\" "
+ "&& sudo ldconfig",
"sudo touch /etc/fuse.conf "
+ "&& grep -qxF \"user_allow_other\" /etc/fuse.conf "
+ "|| echo \"user_allow_other\" | sudo tee -a /etc/fuse.conf"
);
daytona.snapshot().create("fuse-azure", image, System.out::println);
}
}
}
```
#### Launch and mount
`blobfuse2` reads its configuration from a YAML file. Build it with your account credentials and upload it into the sandbox.
The YAML below tells `blobfuse2` three things: **where to connect** (the `azstorage:` block — your storage account, the container you want to mount, the endpoint URL, and the auth method), **what to enable** (the `components:` list — the FUSE interface itself, a content cache, a metadata cache, and the Azure backend), and **how to log**. The cache components use sensible defaults when listed without their own top-level config blocks; add explicit `block_cache:` / `attr_cache:` blocks later if you need to tune cache sizes or timeouts. Note that in Azure terminology, a "container" is the equivalent of an S3 bucket — it's specified inside the YAML rather than passed as a command-line argument.
```python
import os
from daytona import CreateSandboxFromSnapshotParams, Daytona
daytona = Daytona()
sandbox = daytona.create(CreateSandboxFromSnapshotParams(snapshot="fuse-azure"))
mount_path = "/home/daytona/azure"
config_path = "/home/daytona/.blobfuse2.yaml"
account = os.environ["AZURE_STORAGE_ACCOUNT"]
container = os.environ["AZURE_STORAGE_CONTAINER"]
account_key = os.environ["AZURE_STORAGE_ACCOUNT_KEY"]
config = f"""\
allow-other: true
logging:
type: syslog
level: log_warning
components:
- libfuse
- block_cache
- attr_cache
- azstorage
azstorage:
type: block
account-name: {account}
container: {container}
endpoint: https://{account}.blob.core.windows.net
auth-type: key
account-key: {account_key}
"""
sandbox.fs.upload_file(config.encode(), config_path)
sandbox.process.exec(f"chmod 600 {config_path}")
# Mount the container
sandbox.process.exec(f"mkdir -p {mount_path}")
sandbox.process.exec(f"blobfuse2 mount --config-file={config_path} {mount_path}")
# Use the mount
response = sandbox.process.exec(f"ls {mount_path}")
print(response.result)
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
const sandbox = await daytona.create({ snapshot: 'fuse-azure' })
const mountPath = '/home/daytona/azure'
const configPath = '/home/daytona/.blobfuse2.yaml'
const account = process.env.AZURE_STORAGE_ACCOUNT!
const container = process.env.AZURE_STORAGE_CONTAINER!
const accountKey = process.env.AZURE_STORAGE_ACCOUNT_KEY!
const config = `allow-other: true
logging:
type: syslog
level: log_warning
components:
- libfuse
- block_cache
- attr_cache
- azstorage
azstorage:
type: block
account-name: ${account}
container: ${container}
endpoint: https://${account}.blob.core.windows.net
auth-type: key
account-key: ${accountKey}
`
await sandbox.fs.uploadFile(Buffer.from(config), configPath)
await sandbox.process.executeCommand(`chmod 600 ${configPath}`)
// Mount the container
await sandbox.process.executeCommand(`mkdir -p ${mountPath}`)
await sandbox.process.executeCommand(`blobfuse2 mount --config-file=${configPath} ${mountPath}`)
// Use the mount
const response = await sandbox.process.executeCommand(`ls ${mountPath}`)
console.log(response.result)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
sandbox = daytona.create(
Daytona::CreateSandboxFromSnapshotParams.new(snapshot: 'fuse-azure')
)
mount_path = '/home/daytona/azure'
config_path = '/home/daytona/.blobfuse2.yaml'
account = ENV.fetch('AZURE_STORAGE_ACCOUNT')
container = ENV.fetch('AZURE_STORAGE_CONTAINER')
account_key = ENV.fetch('AZURE_STORAGE_ACCOUNT_KEY')
config = <<~YAML
allow-other: true
logging:
type: syslog
level: log_warning
components:
- libfuse
- block_cache
- attr_cache
- azstorage
azstorage:
type: block
account-name: #{account}
container: #{container}
endpoint: https://#{account}.blob.core.windows.net
auth-type: key
account-key: #{account_key}
YAML
sandbox.fs.upload_file(config, config_path)
sandbox.process.exec(command: "chmod 600 #{config_path}")
# Mount the container
sandbox.process.exec(command: "mkdir -p #{mount_path}")
sandbox.process.exec(command: "blobfuse2 mount --config-file=#{config_path} #{mount_path}")
# Use the mount
response = sandbox.process.exec(command: "ls #{mount_path}")
puts response.result
```
```go
import (
"context"
"fmt"
"log"
"os"
"github.com/daytona/clients/sdk-go/pkg/daytona"
"github.com/daytona/clients/sdk-go/pkg/types"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
sandbox, err := client.Create(ctx, types.SnapshotParams{
Snapshot: "fuse-azure",
})
if err != nil {
log.Fatal(err)
}
mountPath := "/home/daytona/azure"
configPath := "/home/daytona/.blobfuse2.yaml"
account := os.Getenv("AZURE_STORAGE_ACCOUNT")
container := os.Getenv("AZURE_STORAGE_CONTAINER")
accountKey := os.Getenv("AZURE_STORAGE_ACCOUNT_KEY")
config := fmt.Sprintf(`allow-other: true
logging:
type: syslog
level: log_warning
components:
- libfuse
- block_cache
- attr_cache
- azstorage
azstorage:
type: block
account-name: %s
container: %s
endpoint: https://%s.blob.core.windows.net
auth-type: key
account-key: %s
`, account, container, account, accountKey)
if err := sandbox.FileSystem.UploadFile(ctx, []byte(config), configPath); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx, "chmod 600 "+configPath); err != nil {
log.Fatal(err)
}
// Mount the container
if _, err := sandbox.Process.ExecuteCommand(ctx, "mkdir -p "+mountPath); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx,
"blobfuse2 mount --config-file="+configPath+" "+mountPath); err != nil {
log.Fatal(err)
}
// Use the mount
response, err := sandbox.Process.ExecuteCommand(ctx, "ls "+mountPath)
if err != nil {
log.Fatal(err)
}
fmt.Println(response.Result)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
import io.daytona.sdk.model.ExecuteResponse;
import java.nio.charset.StandardCharsets;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setSnapshot("fuse-azure");
Sandbox sandbox = daytona.create(params);
String mountPath = "/home/daytona/azure";
String configPath = "/home/daytona/.blobfuse2.yaml";
String account = System.getenv("AZURE_STORAGE_ACCOUNT");
String container = System.getenv("AZURE_STORAGE_CONTAINER");
String accountKey = System.getenv("AZURE_STORAGE_ACCOUNT_KEY");
String config = "allow-other: true\n"
+ "logging:\n"
+ " type: syslog\n"
+ " level: log_warning\n"
+ "components:\n"
+ " - libfuse\n"
+ " - block_cache\n"
+ " - attr_cache\n"
+ " - azstorage\n"
+ "azstorage:\n"
+ " type: block\n"
+ " account-name: " + account + "\n"
+ " container: " + container + "\n"
+ " endpoint: https://" + account + ".blob.core.windows.net\n"
+ " auth-type: key\n"
+ " account-key: " + accountKey + "\n";
sandbox.fs.uploadFile(config.getBytes(StandardCharsets.UTF_8), configPath);
sandbox.getProcess().executeCommand("chmod 600 " + configPath);
// Mount the container
sandbox.getProcess().executeCommand("mkdir -p " + mountPath);
sandbox.getProcess().executeCommand(
"blobfuse2 mount --config-file=" + configPath + " " + mountPath);
// Use the mount
ExecuteResponse response = sandbox.getProcess().executeCommand("ls " + mountPath);
System.out.println(response.getResult());
}
}
}
```
### Runtime install
Start from a default sandbox and install `blobfuse2` during startup before writing the config and mounting the container. This is useful for quick validation and experiments, with the tradeoff of slower cold starts and repeated setup on each sandbox launch.
```python
import os
from daytona import CreateSandboxBaseParams, Daytona
daytona = Daytona()
sandbox = daytona.create(CreateSandboxBaseParams())
# Install blobfuse2
sandbox.process.exec(
"sudo apt-get update "
"&& sudo apt-get install -y --no-install-recommends ca-certificates curl gnupg wget"
)
sandbox.process.exec(
"wget -qO- https://packages.microsoft.com/keys/microsoft.asc "
"| sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/microsoft.gpg"
)
sandbox.process.exec(
'echo "deb [arch=$(dpkg --print-architecture) '
'signed-by=/etc/apt/trusted.gpg.d/microsoft.gpg] '
'https://packages.microsoft.com/debian/12/prod bookworm main" '
"| sudo tee /etc/apt/sources.list.d/microsoft-prod.list "
"&& sudo apt-get update && sudo apt-get install -y blobfuse2 fuse3"
)
# libfuse3.so.3 compat symlink for Trixie
sandbox.process.exec(
'src=$(find /usr/lib /lib -name "libfuse3.so.3.*" -type f 2>/dev/null '
"| sort -V | tail -1) "
'&& sudo ln -sfn "$src" "$(dirname "$src")/libfuse3.so.3" '
"&& sudo ldconfig"
)
# Build config and mount
mount_path = "/home/daytona/azure"
config_path = "/home/daytona/.blobfuse2.yaml"
account = os.environ["AZURE_STORAGE_ACCOUNT"]
container = os.environ["AZURE_STORAGE_CONTAINER"]
account_key = os.environ["AZURE_STORAGE_ACCOUNT_KEY"]
config = f"""\
allow-other: true
components:
- libfuse
- block_cache
- attr_cache
- azstorage
azstorage:
type: block
account-name: {account}
container: {container}
endpoint: https://{account}.blob.core.windows.net
auth-type: key
account-key: {account_key}
"""
sandbox.fs.upload_file(config.encode(), config_path)
sandbox.process.exec(f"chmod 600 {config_path}")
sandbox.process.exec(f"mkdir -p {mount_path}")
sandbox.process.exec(f"blobfuse2 mount --config-file={config_path} {mount_path}")
response = sandbox.process.exec(f"ls {mount_path}")
print(response.result)
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
const sandbox = await daytona.create()
// Install blobfuse2
await sandbox.process.executeCommand(
'sudo apt-get update ' +
'&& sudo apt-get install -y --no-install-recommends ca-certificates curl gnupg wget',
)
await sandbox.process.executeCommand(
'wget -qO- https://packages.microsoft.com/keys/microsoft.asc ' +
'| sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/microsoft.gpg',
)
await sandbox.process.executeCommand(
'echo "deb [arch=$(dpkg --print-architecture) ' +
'signed-by=/etc/apt/trusted.gpg.d/microsoft.gpg] ' +
'https://packages.microsoft.com/debian/12/prod bookworm main" ' +
'| sudo tee /etc/apt/sources.list.d/microsoft-prod.list ' +
'&& sudo apt-get update && sudo apt-get install -y blobfuse2 fuse3',
)
// libfuse3.so.3 compat symlink for Trixie
await sandbox.process.executeCommand(
'src=$(find /usr/lib /lib -name "libfuse3.so.3.*" -type f 2>/dev/null ' +
'| sort -V | tail -1) ' +
'&& sudo ln -sfn "$src" "$(dirname "$src")/libfuse3.so.3" ' +
'&& sudo ldconfig',
)
// Build config and mount
const mountPath = '/home/daytona/azure'
const configPath = '/home/daytona/.blobfuse2.yaml'
const account = process.env.AZURE_STORAGE_ACCOUNT!
const container = process.env.AZURE_STORAGE_CONTAINER!
const accountKey = process.env.AZURE_STORAGE_ACCOUNT_KEY!
const config = `allow-other: true
components:
- libfuse
- block_cache
- attr_cache
- azstorage
azstorage:
type: block
account-name: ${account}
container: ${container}
endpoint: https://${account}.blob.core.windows.net
auth-type: key
account-key: ${accountKey}
`
await sandbox.fs.uploadFile(Buffer.from(config), configPath)
await sandbox.process.executeCommand(`chmod 600 ${configPath}`)
await sandbox.process.executeCommand(`mkdir -p ${mountPath}`)
await sandbox.process.executeCommand(`blobfuse2 mount --config-file=${configPath} ${mountPath}`)
const response = await sandbox.process.executeCommand(`ls ${mountPath}`)
console.log(response.result)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
sandbox = daytona.create(Daytona::CreateSandboxBaseParams.new)
# Install blobfuse2
sandbox.process.exec(
command: 'sudo apt-get update ' \
'&& sudo apt-get install -y --no-install-recommends ca-certificates curl gnupg wget'
)
sandbox.process.exec(
command: 'wget -qO- https://packages.microsoft.com/keys/microsoft.asc ' \
'| sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/microsoft.gpg'
)
sandbox.process.exec(
command: 'echo "deb [arch=$(dpkg --print-architecture) ' \
'signed-by=/etc/apt/trusted.gpg.d/microsoft.gpg] ' \
'https://packages.microsoft.com/debian/12/prod bookworm main" ' \
'| sudo tee /etc/apt/sources.list.d/microsoft-prod.list ' \
'&& sudo apt-get update && sudo apt-get install -y blobfuse2 fuse3'
)
# libfuse3.so.3 compat symlink for Trixie
sandbox.process.exec(
command: 'src=$(find /usr/lib /lib -name "libfuse3.so.3.*" -type f 2>/dev/null ' \
'| sort -V | tail -1) ' \
'&& sudo ln -sfn "$src" "$(dirname "$src")/libfuse3.so.3" ' \
'&& sudo ldconfig'
)
# Build config and mount
mount_path = '/home/daytona/azure'
config_path = '/home/daytona/.blobfuse2.yaml'
account = ENV.fetch('AZURE_STORAGE_ACCOUNT')
container = ENV.fetch('AZURE_STORAGE_CONTAINER')
account_key = ENV.fetch('AZURE_STORAGE_ACCOUNT_KEY')
config = <<~YAML
allow-other: true
components:
- libfuse
- block_cache
- attr_cache
- azstorage
azstorage:
type: block
account-name: #{account}
container: #{container}
endpoint: https://#{account}.blob.core.windows.net
auth-type: key
account-key: #{account_key}
YAML
sandbox.fs.upload_file(config, config_path)
sandbox.process.exec(command: "chmod 600 #{config_path}")
sandbox.process.exec(command: "mkdir -p #{mount_path}")
sandbox.process.exec(command: "blobfuse2 mount --config-file=#{config_path} #{mount_path}")
response = sandbox.process.exec(command: "ls #{mount_path}")
puts response.result
```
```go
import (
"context"
"fmt"
"log"
"os"
"github.com/daytona/clients/sdk-go/pkg/daytona"
"github.com/daytona/clients/sdk-go/pkg/types"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
sandbox, err := client.Create(ctx, types.SnapshotParams{})
if err != nil {
log.Fatal(err)
}
// Install blobfuse2
if _, err := sandbox.Process.ExecuteCommand(ctx,
"sudo apt-get update && sudo apt-get install -y --no-install-recommends ca-certificates curl gnupg wget"); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx,
"wget -qO- https://packages.microsoft.com/keys/microsoft.asc | "+
"sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/microsoft.gpg"); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx,
`echo "deb [arch=$(dpkg --print-architecture) `+
`signed-by=/etc/apt/trusted.gpg.d/microsoft.gpg] `+
`https://packages.microsoft.com/debian/12/prod bookworm main" | `+
`sudo tee /etc/apt/sources.list.d/microsoft-prod.list && `+
`sudo apt-get update && sudo apt-get install -y blobfuse2 fuse3`); err != nil {
log.Fatal(err)
}
// libfuse3.so.3 compat symlink for Trixie
if _, err := sandbox.Process.ExecuteCommand(ctx,
`src=$(find /usr/lib /lib -name "libfuse3.so.3.*" -type f 2>/dev/null | sort -V | tail -1) && `+
`sudo ln -sfn "$src" "$(dirname "$src")/libfuse3.so.3" && sudo ldconfig`); err != nil {
log.Fatal(err)
}
// Build config and mount
mountPath := "/home/daytona/azure"
configPath := "/home/daytona/.blobfuse2.yaml"
account := os.Getenv("AZURE_STORAGE_ACCOUNT")
container := os.Getenv("AZURE_STORAGE_CONTAINER")
accountKey := os.Getenv("AZURE_STORAGE_ACCOUNT_KEY")
config := fmt.Sprintf(`allow-other: true
components:
- libfuse
- block_cache
- attr_cache
- azstorage
azstorage:
type: block
account-name: %s
container: %s
endpoint: https://%s.blob.core.windows.net
auth-type: key
account-key: %s
`, account, container, account, accountKey)
if err := sandbox.FileSystem.UploadFile(ctx, []byte(config), configPath); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx, "chmod 600 "+configPath); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx, "mkdir -p "+mountPath); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx,
"blobfuse2 mount --config-file="+configPath+" "+mountPath); err != nil {
log.Fatal(err)
}
response, err := sandbox.Process.ExecuteCommand(ctx, "ls "+mountPath)
if err != nil {
log.Fatal(err)
}
fmt.Println(response.Result)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
import io.daytona.sdk.model.ExecuteResponse;
import java.nio.charset.StandardCharsets;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
Sandbox sandbox = daytona.create(new CreateSandboxFromSnapshotParams());
// Install blobfuse2
sandbox.getProcess().executeCommand(
"sudo apt-get update "
+ "&& sudo apt-get install -y --no-install-recommends ca-certificates curl gnupg wget");
sandbox.getProcess().executeCommand(
"wget -qO- https://packages.microsoft.com/keys/microsoft.asc "
+ "| sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/microsoft.gpg");
sandbox.getProcess().executeCommand(
"echo \"deb [arch=$(dpkg --print-architecture) "
+ "signed-by=/etc/apt/trusted.gpg.d/microsoft.gpg] "
+ "https://packages.microsoft.com/debian/12/prod bookworm main\" "
+ "| sudo tee /etc/apt/sources.list.d/microsoft-prod.list "
+ "&& sudo apt-get update && sudo apt-get install -y blobfuse2 fuse3");
// libfuse3.so.3 compat symlink for Trixie
sandbox.getProcess().executeCommand(
"src=$(find /usr/lib /lib -name \"libfuse3.so.3.*\" -type f 2>/dev/null "
+ "| sort -V | tail -1) "
+ "&& sudo ln -sfn \"$src\" \"$(dirname \"$src\")/libfuse3.so.3\" "
+ "&& sudo ldconfig");
// Build config and mount
String mountPath = "/home/daytona/azure";
String configPath = "/home/daytona/.blobfuse2.yaml";
String account = System.getenv("AZURE_STORAGE_ACCOUNT");
String container = System.getenv("AZURE_STORAGE_CONTAINER");
String accountKey = System.getenv("AZURE_STORAGE_ACCOUNT_KEY");
String config = "allow-other: true\n"
+ "components:\n"
+ " - libfuse\n"
+ " - block_cache\n"
+ " - attr_cache\n"
+ " - azstorage\n"
+ "azstorage:\n"
+ " type: block\n"
+ " account-name: " + account + "\n"
+ " container: " + container + "\n"
+ " endpoint: https://" + account + ".blob.core.windows.net\n"
+ " auth-type: key\n"
+ " account-key: " + accountKey + "\n";
sandbox.fs.uploadFile(config.getBytes(StandardCharsets.UTF_8), configPath);
sandbox.getProcess().executeCommand("chmod 600 " + configPath);
sandbox.getProcess().executeCommand("mkdir -p " + mountPath);
sandbox.getProcess().executeCommand(
"blobfuse2 mount --config-file=" + configPath + " " + mountPath);
ExecuteResponse response = sandbox.getProcess().executeCommand("ls " + mountPath);
System.out.println(response.getResult());
}
}
}
```
## Mount an Archil disk
[Archil ↗](https://archil.com) is an infinite, elastic, POSIX file system that automatically synchronizes to object storage like S3, R2, and Azure Blob. You should use Archil when you need to mount a bucket to your Daytona sandbox, but need higher out-of-the-box performance than traditional FUSE mounts. Archil achieves higher performance by using shared SSD read and write caching in front of your object storage bucket. Archil disks are mounted as regular directories, scale to whatever your sandbox writes (you pay only for what you use), and can be [mounted by many sandboxes at once ↗](https://docs.archil.com/concepts/shared-disks), making them a natural fit for parallel agents that share state.
**Credentials** — set `ARCHIL_MOUNT_TOKEN` (a disk-scoped [mount token ↗](https://docs.archil.com/concepts/disk-users#disk-token-authorization) generated from the disk's **Details** page in the [Archil console ↗](https://console.archil.com)), `ARCHIL_REGION` (the disk's region, e.g. `aws-us-east-1`), and `ARCHIL_DISK` (the owner-qualified disk name, e.g. `myorg/my-disk`, or disk ID like `dsk-0123456789abcdef`) in your local environment. The snippets below pass them into the sandbox via `envVars`, and the `archil` CLI reads them from there.
### Pre-built snapshot
Build a snapshot with the `archil` CLI preinstalled, then launch Archil-enabled sandboxes from that snapshot. You still authenticate and mount at runtime, but installation is no longer part of each sandbox startup sequence.
#### Build a snapshot
Create a reusable snapshot that installs the `archil` CLI. The `daytonaio/sandbox` base image ships Debian Trixie which doesn't include `libfuse2`, so we apt-install it first; the Archil installer reads the `.deb` it downloads and links against `libfuse2`.
```python
from daytona import CreateSnapshotParams, Daytona, Image
daytona = Daytona()
image = (
Image.base("daytonaio/sandbox")
.run_commands(
"sudo apt-get update "
"&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates",
"curl -fsSL https://archil.com/install | sh",
)
)
daytona.snapshot.create(
CreateSnapshotParams(name="fuse-archil", image=image),
on_logs=lambda chunk: print(chunk, end="", flush=True),
)
```
```typescript
import { Daytona, Image } from '@daytona/sdk'
const daytona = new Daytona()
const image = Image.base('daytonaio/sandbox').runCommands(
'sudo apt-get update ' +
'&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates',
'curl -fsSL https://archil.com/install | sh',
)
await daytona.snapshot.create(
{ name: 'fuse-archil', image },
{ onLogs: console.log },
)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
image = Daytona::Image
.base('daytonaio/sandbox')
.run_commands(
'sudo apt-get update ' \
'&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates',
'curl -fsSL https://archil.com/install | sh'
)
daytona.snapshot.create(
Daytona::CreateSnapshotParams.new(name: 'fuse-archil', image: image),
on_logs: proc { |chunk| print(chunk) }
)
```
```go
import (
"context"
"fmt"
"log"
"github.com/daytona/clients/sdk-go/pkg/daytona"
"github.com/daytona/clients/sdk-go/pkg/types"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
image := daytona.Base("daytonaio/sandbox").
Run("sudo apt-get update && sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates").
Run("curl -fsSL https://archil.com/install | sh")
_, logChan, err := client.Snapshot.Create(ctx, &types.CreateSnapshotParams{
Name: "fuse-archil",
Image: image,
})
if err != nil {
log.Fatal(err)
}
for line := range logChan {
fmt.Print(line)
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Image;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
Image image = Image.base("daytonaio/sandbox")
.runCommands(
"sudo apt-get update "
+ "&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates",
"curl -fsSL https://archil.com/install | sh"
);
daytona.snapshot().create("fuse-archil", image, System.out::println);
}
}
}
```
#### Launch and mount
Pass `ARCHIL_MOUNT_TOKEN`, `ARCHIL_REGION`, and `ARCHIL_DISK` to the sandbox via `envVars`. Your code then mounts the disk at `/home/daytona/archil` and hands ownership to the `daytona` user so non-root processes can read and write through the mount. The `chown` step works on exclusive (non-shared) mounts only; on mounts created with `--shared` or `--read-only` it fails with `Read-only file system`. See [Shared and read-only mounts](#shared-and-read-only-mounts).
```python
import os
from daytona import CreateSandboxFromSnapshotParams, Daytona
daytona = Daytona()
mount_path = "/home/daytona/archil"
sandbox = daytona.create(
CreateSandboxFromSnapshotParams(
snapshot="fuse-archil",
env_vars={
"ARCHIL_MOUNT_TOKEN": os.environ["ARCHIL_MOUNT_TOKEN"],
"ARCHIL_REGION": os.environ["ARCHIL_REGION"],
"ARCHIL_DISK": os.environ["ARCHIL_DISK"],
},
)
)
sandbox.process.exec(f"sudo mkdir -p {mount_path}")
sandbox.process.exec(
f"sudo --preserve-env=ARCHIL_MOUNT_TOKEN archil mount "
f"$ARCHIL_DISK {mount_path} --region $ARCHIL_REGION"
)
# Exclusive mounts only; on --shared or --read-only mounts this fails
# with "Read-only file system" (see "Shared and read-only mounts" below)
sandbox.process.exec(f"sudo chown daytona:daytona {mount_path}")
# Read and write through the mount as if it were a local directory
response = sandbox.process.exec(f"ls {mount_path}")
print(response.result)
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
const mountPath = '/home/daytona/archil'
const sandbox = await daytona.create({
snapshot: 'fuse-archil',
envVars: {
ARCHIL_MOUNT_TOKEN: process.env.ARCHIL_MOUNT_TOKEN!,
ARCHIL_REGION: process.env.ARCHIL_REGION!,
ARCHIL_DISK: process.env.ARCHIL_DISK!,
},
})
await sandbox.process.executeCommand(`sudo mkdir -p ${mountPath}`)
await sandbox.process.executeCommand(
`sudo --preserve-env=ARCHIL_MOUNT_TOKEN archil mount ` +
`$ARCHIL_DISK ${mountPath} --region $ARCHIL_REGION`,
)
// Exclusive mounts only; on --shared or --read-only mounts this fails
// with "Read-only file system" (see "Shared and read-only mounts" below)
await sandbox.process.executeCommand(`sudo chown daytona:daytona ${mountPath}`)
// Read and write through the mount as if it were a local directory
const response = await sandbox.process.executeCommand(`ls ${mountPath}`)
console.log(response.result)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
mount_path = '/home/daytona/archil'
sandbox = daytona.create(
Daytona::CreateSandboxFromSnapshotParams.new(
snapshot: 'fuse-archil',
env_vars: {
'ARCHIL_MOUNT_TOKEN' => ENV.fetch('ARCHIL_MOUNT_TOKEN'),
'ARCHIL_REGION' => ENV.fetch('ARCHIL_REGION'),
'ARCHIL_DISK' => ENV.fetch('ARCHIL_DISK')
}
)
)
sandbox.process.exec(command: "sudo mkdir -p #{mount_path}")
sandbox.process.exec(
command: "sudo --preserve-env=ARCHIL_MOUNT_TOKEN archil mount " \
"$ARCHIL_DISK #{mount_path} --region $ARCHIL_REGION"
)
# Exclusive mounts only; on --shared or --read-only mounts this fails
# with "Read-only file system" (see "Shared and read-only mounts" below)
sandbox.process.exec(command: "sudo chown daytona:daytona #{mount_path}")
# Read and write through the mount as if it were a local directory
response = sandbox.process.exec(command: "ls #{mount_path}")
puts response.result
```
```go
import (
"context"
"fmt"
"log"
"os"
"github.com/daytona/clients/sdk-go/pkg/daytona"
"github.com/daytona/clients/sdk-go/pkg/types"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
mountPath := "/home/daytona/archil"
sandbox, err := client.Create(ctx, types.SnapshotParams{
Snapshot: "fuse-archil",
SandboxBaseParams: types.SandboxBaseParams{
EnvVars: map[string]string{
"ARCHIL_MOUNT_TOKEN": os.Getenv("ARCHIL_MOUNT_TOKEN"),
"ARCHIL_REGION": os.Getenv("ARCHIL_REGION"),
"ARCHIL_DISK": os.Getenv("ARCHIL_DISK"),
},
},
})
if err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx, "sudo mkdir -p "+mountPath); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx,
"sudo --preserve-env=ARCHIL_MOUNT_TOKEN archil mount "+
"$ARCHIL_DISK "+mountPath+" --region $ARCHIL_REGION"); err != nil {
log.Fatal(err)
}
// Exclusive mounts only; on --shared or --read-only mounts this fails
// with "Read-only file system" (see "Shared and read-only mounts" below)
if _, err := sandbox.Process.ExecuteCommand(ctx,
"sudo chown daytona:daytona "+mountPath); err != nil {
log.Fatal(err)
}
// Read and write through the mount as if it were a local directory
response, err := sandbox.Process.ExecuteCommand(ctx, "ls "+mountPath)
if err != nil {
log.Fatal(err)
}
fmt.Println(response.Result)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
import io.daytona.sdk.model.ExecuteResponse;
import java.util.Map;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
String mountPath = "/home/daytona/archil";
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setSnapshot("fuse-archil");
params.setEnvVars(Map.of(
"ARCHIL_MOUNT_TOKEN", System.getenv("ARCHIL_MOUNT_TOKEN"),
"ARCHIL_REGION", System.getenv("ARCHIL_REGION"),
"ARCHIL_DISK", System.getenv("ARCHIL_DISK")
));
Sandbox sandbox = daytona.create(params);
sandbox.getProcess().executeCommand("sudo mkdir -p " + mountPath);
sandbox.getProcess().executeCommand(
"sudo --preserve-env=ARCHIL_MOUNT_TOKEN archil mount "
+ "$ARCHIL_DISK " + mountPath + " --region $ARCHIL_REGION");
// Exclusive mounts only; on --shared or --read-only mounts this fails
// with "Read-only file system" (see "Shared and read-only mounts" below)
sandbox.getProcess().executeCommand("sudo chown daytona:daytona " + mountPath);
// Read and write through the mount as if it were a local directory
ExecuteResponse response = sandbox.getProcess().executeCommand("ls " + mountPath);
System.out.println(response.getResult());
}
}
}
```
To let multiple sandboxes mount the **same** disk concurrently, add `--shared` to `archil mount`. Shared mounts change how permissions and writes behave; see [Shared and read-only mounts](#shared-and-read-only-mounts).
### Runtime install
Start from a default sandbox and install the `archil` CLI during startup before mounting the disk. This is useful when iterating quickly on mount behavior, with the tradeoff of slower cold starts for each sandbox.
```python
import os
from daytona import CreateSandboxBaseParams, Daytona
daytona = Daytona()
mount_path = "/home/daytona/archil"
sandbox = daytona.create(
CreateSandboxBaseParams(
env_vars={
"ARCHIL_MOUNT_TOKEN": os.environ["ARCHIL_MOUNT_TOKEN"],
"ARCHIL_REGION": os.environ["ARCHIL_REGION"],
"ARCHIL_DISK": os.environ["ARCHIL_DISK"],
},
)
)
sandbox.process.exec(
"sudo apt-get update "
"&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates"
)
sandbox.process.exec("curl -fsSL https://archil.com/install | sh")
sandbox.process.exec(f"sudo mkdir -p {mount_path}")
sandbox.process.exec(
f"sudo --preserve-env=ARCHIL_MOUNT_TOKEN archil mount "
f"$ARCHIL_DISK {mount_path} --region $ARCHIL_REGION"
)
# Exclusive mounts only; on --shared or --read-only mounts this fails
# with "Read-only file system" (see "Shared and read-only mounts" below)
sandbox.process.exec(f"sudo chown daytona:daytona {mount_path}")
response = sandbox.process.exec(f"ls {mount_path}")
print(response.result)
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
const mountPath = '/home/daytona/archil'
const sandbox = await daytona.create({
envVars: {
ARCHIL_MOUNT_TOKEN: process.env.ARCHIL_MOUNT_TOKEN!,
ARCHIL_REGION: process.env.ARCHIL_REGION!,
ARCHIL_DISK: process.env.ARCHIL_DISK!,
},
})
await sandbox.process.executeCommand(
'sudo apt-get update ' +
'&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates',
)
await sandbox.process.executeCommand('curl -fsSL https://archil.com/install | sh')
await sandbox.process.executeCommand(`sudo mkdir -p ${mountPath}`)
await sandbox.process.executeCommand(
`sudo --preserve-env=ARCHIL_MOUNT_TOKEN archil mount ` +
`$ARCHIL_DISK ${mountPath} --region $ARCHIL_REGION`,
)
// Exclusive mounts only; on --shared or --read-only mounts this fails
// with "Read-only file system" (see "Shared and read-only mounts" below)
await sandbox.process.executeCommand(`sudo chown daytona:daytona ${mountPath}`)
const response = await sandbox.process.executeCommand(`ls ${mountPath}`)
console.log(response.result)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
mount_path = '/home/daytona/archil'
sandbox = daytona.create(
Daytona::CreateSandboxBaseParams.new(
env_vars: {
'ARCHIL_MOUNT_TOKEN' => ENV.fetch('ARCHIL_MOUNT_TOKEN'),
'ARCHIL_REGION' => ENV.fetch('ARCHIL_REGION'),
'ARCHIL_DISK' => ENV.fetch('ARCHIL_DISK')
}
)
)
sandbox.process.exec(
command: 'sudo apt-get update ' \
'&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates'
)
sandbox.process.exec(command: 'curl -fsSL https://archil.com/install | sh')
sandbox.process.exec(command: "sudo mkdir -p #{mount_path}")
sandbox.process.exec(
command: "sudo --preserve-env=ARCHIL_MOUNT_TOKEN archil mount " \
"$ARCHIL_DISK #{mount_path} --region $ARCHIL_REGION"
)
# Exclusive mounts only; on --shared or --read-only mounts this fails
# with "Read-only file system" (see "Shared and read-only mounts" below)
sandbox.process.exec(command: "sudo chown daytona:daytona #{mount_path}")
response = sandbox.process.exec(command: "ls #{mount_path}")
puts response.result
```
```go
import (
"context"
"fmt"
"log"
"os"
"github.com/daytona/clients/sdk-go/pkg/daytona"
"github.com/daytona/clients/sdk-go/pkg/types"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
mountPath := "/home/daytona/archil"
sandbox, err := client.Create(ctx, types.SnapshotParams{
SandboxBaseParams: types.SandboxBaseParams{
EnvVars: map[string]string{
"ARCHIL_MOUNT_TOKEN": os.Getenv("ARCHIL_MOUNT_TOKEN"),
"ARCHIL_REGION": os.Getenv("ARCHIL_REGION"),
"ARCHIL_DISK": os.Getenv("ARCHIL_DISK"),
},
},
})
if err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx,
"sudo apt-get update && sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates"); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx,
"curl -fsSL https://archil.com/install | sh"); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx, "sudo mkdir -p "+mountPath); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx,
"sudo --preserve-env=ARCHIL_MOUNT_TOKEN archil mount "+
"$ARCHIL_DISK "+mountPath+" --region $ARCHIL_REGION"); err != nil {
log.Fatal(err)
}
// Exclusive mounts only; on --shared or --read-only mounts this fails
// with "Read-only file system" (see "Shared and read-only mounts" below)
if _, err := sandbox.Process.ExecuteCommand(ctx,
"sudo chown daytona:daytona "+mountPath); err != nil {
log.Fatal(err)
}
response, err := sandbox.Process.ExecuteCommand(ctx, "ls "+mountPath)
if err != nil {
log.Fatal(err)
}
fmt.Println(response.Result)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxBaseParams;
import io.daytona.sdk.model.ExecuteResponse;
import java.util.Map;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
String mountPath = "/home/daytona/archil";
CreateSandboxBaseParams params = new CreateSandboxBaseParams();
params.setEnvVars(Map.of(
"ARCHIL_MOUNT_TOKEN", System.getenv("ARCHIL_MOUNT_TOKEN"),
"ARCHIL_REGION", System.getenv("ARCHIL_REGION"),
"ARCHIL_DISK", System.getenv("ARCHIL_DISK")
));
Sandbox sandbox = daytona.create(params);
sandbox.getProcess().executeCommand(
"sudo apt-get update "
+ "&& sudo apt-get install -y --no-install-recommends libfuse2 ca-certificates");
sandbox.getProcess().executeCommand(
"curl -fsSL https://archil.com/install | sh");
sandbox.getProcess().executeCommand("sudo mkdir -p " + mountPath);
sandbox.getProcess().executeCommand(
"sudo --preserve-env=ARCHIL_MOUNT_TOKEN archil mount "
+ "$ARCHIL_DISK " + mountPath + " --region $ARCHIL_REGION");
// Exclusive mounts only; on --shared or --read-only mounts this fails
// with "Read-only file system" (see "Shared and read-only mounts" below)
sandbox.getProcess().executeCommand("sudo chown daytona:daytona " + mountPath);
ExecuteResponse response = sandbox.getProcess().executeCommand("ls " + mountPath);
System.out.println(response.getResult());
}
}
}
```
### Shared and read-only mounts
The `--shared` and `--read-only` flags produce the same mount behavior: the entire mount is read-only, including `chmod` and `chown`, until `archil checkout ` grants a write delegation for that path. This has two consequences for the snippets above:
- **`sudo chown daytona:daytona `** fails with "Read-only file system" on any **`--shared`** or **`--read-only`** mount. On a writable shared mount:
1. Run **`sudo archil checkout `**
2. Run **`chown`**
3. Run **`sudo archil checkin `**
- Ownership and permissions come from the metadata stored on the Archil disk, not from the mount. If a root process wrote files with a restrictive umask, every mount shows them as **`root:root`** with mode **`600`**, and the **`daytona`** user cannot read them.
1. **Fix permissions at the source (recommended)**
Write with `umask 022` or run `chmod -R a+rX` after writing, so files are readable from the start. For existing data, apply a one-time fix from any client with a writable shared mount:
```bash
sudo --preserve-env=ARCHIL_MOUNT_TOKEN archil mount "$ARCHIL_DISK" /mnt/fix --region "$ARCHIL_REGION" --shared
sudo archil checkout /mnt/fix/data
sudo chmod -R a+rX /mnt/fix/data
sudo archil checkin /mnt/fix/data
```
The permission bits persist on the disk, so every subsequent read-only mount shows the corrected modes.
2. **Re-export with `bindfs` (when the source data cannot be changed)**
Inside the sandbox, mount a read-only view of the Archil mount that remaps ownership to the **`daytona`** user:
```bash
sudo apt-get install -y bindfs
mkdir -p /home/daytona/archil-view
sudo bindfs --force-user=daytona --force-group=daytona --perms=a+rX \
/home/daytona/archil /home/daytona/archil-view
```
The **`archil`** CLI requires root to mount and has no uid, gid, or umask override flags, so there is no way to remap ownership at mount time.
## Mount a MesaFS filesystem
[MesaFS ↗](https://mesa.dev) is an agent-native versioned filesystem from Mesa, purpose-built for the same workloads Daytona sandboxes run — parallel agent swarms, shared working memory, structured artifacts, and long-lived state across runs. With MesaFS, instead of mounting a cloud bucket, you mount a Mesa **repository**: a Git-compatible versioned working directory with sub-50ms reads/writes, instant fork/branch operations, and unlimited concurrent writers.
The Mesa setup follows the same pattern as the bucket providers but uses the Mesa CLI rather than a FUSE-specific tool: install the CLI in your sandbox, authenticate with your API key, and run `mesa mount --daemonize` to mount your repos at `/home/daytona/mesa/mnt//`.
**Credentials** — set `MESA_API_KEY` and `MESA_ORG` (your Mesa organization slug) in your local environment. The snippets below pass them into the sandbox via `envVars`, and the Mesa CLI reads them from there.
### Pre-built snapshot
Build a snapshot with the Mesa CLI preinstalled, then launch Mesa-enabled sandboxes from that snapshot. You still authenticate and mount at runtime, but installation is no longer part of each sandbox startup sequence.
#### Build a snapshot
Create a reusable snapshot that installs the Mesa CLI and enables the FUSE `user_allow_other` setting. Sandboxes launched from `fuse-mesa` can then authenticate and mount repos without repeating install work.
```python
from daytona import CreateSnapshotParams, Daytona, Image
daytona = Daytona()
image = (
Image.base("daytonaio/sandbox")
.run_commands(
"curl -fsSL https://mesa.dev/install.sh | sh",
"sudo sed -i 's/^#user_allow_other/user_allow_other/' /etc/fuse.conf",
)
)
daytona.snapshot.create(
CreateSnapshotParams(name="fuse-mesa", image=image),
on_logs=lambda chunk: print(chunk, end="", flush=True),
)
```
```typescript
import { Daytona, Image } from '@daytona/sdk'
const daytona = new Daytona()
const image = Image.base('daytonaio/sandbox').runCommands(
'curl -fsSL https://mesa.dev/install.sh | sh',
"sudo sed -i 's/^#user_allow_other/user_allow_other/' /etc/fuse.conf",
)
await daytona.snapshot.create(
{ name: 'fuse-mesa', image },
{ onLogs: console.log },
)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
image = Daytona::Image
.base('daytonaio/sandbox')
.run_commands(
'curl -fsSL https://mesa.dev/install.sh | sh',
"sudo sed -i 's/^#user_allow_other/user_allow_other/' /etc/fuse.conf"
)
daytona.snapshot.create(
Daytona::CreateSnapshotParams.new(name: 'fuse-mesa', image: image),
on_logs: proc { |chunk| print(chunk) }
)
```
```go
import (
"context"
"fmt"
"log"
"github.com/daytona/clients/sdk-go/pkg/daytona"
"github.com/daytona/clients/sdk-go/pkg/types"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
image := daytona.Base("daytonaio/sandbox").
Run("curl -fsSL https://mesa.dev/install.sh | sh").
Run(`sudo sed -i 's/^#user_allow_other/user_allow_other/' /etc/fuse.conf`)
_, logChan, err := client.Snapshot.Create(ctx, &types.CreateSnapshotParams{
Name: "fuse-mesa",
Image: image,
})
if err != nil {
log.Fatal(err)
}
for line := range logChan {
fmt.Print(line)
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Image;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
Image image = Image.base("daytonaio/sandbox")
.runCommands(
"curl -fsSL https://mesa.dev/install.sh | sh",
"sudo sed -i 's/^#user_allow_other/user_allow_other/' /etc/fuse.conf"
);
daytona.snapshot().create("fuse-mesa", image, System.out::println);
}
}
}
```
#### Launch and mount
Pass `MESA_API_KEY` and your Mesa organization slug to the sandbox via `envVars`. Your code then writes a TOML config into the sandbox, authenticates the Mesa CLI, and mounts your repos at `/home/daytona/mesa/mnt//`.
```python
import os
from daytona import CreateSandboxFromSnapshotParams, Daytona
daytona = Daytona()
org = os.environ["MESA_ORG"]
repo = "my-workspace"
mount_path = f"/home/daytona/mesa/mnt/{org}/{repo}"
config_path = "/home/daytona/.config/mesa/config.toml"
sandbox = daytona.create(
CreateSandboxFromSnapshotParams(
snapshot="fuse-mesa",
env_vars={
"MESA_API_KEY": os.environ["MESA_API_KEY"],
"MESA_ORG": org,
},
)
)
config = f'''mount-point = "/home/daytona/mesa/mnt"
[secrets]
backend = "plaintext-file"
[organizations.{org}]
'''
sandbox.process.exec(f"mkdir -p $(dirname {config_path})")
sandbox.fs.upload_file(config.encode(), config_path)
sandbox.process.exec("mesa auth set-key --org $MESA_ORG $MESA_API_KEY")
sandbox.process.exec("mesa mount --daemonize")
response = sandbox.process.exec(f"ls {mount_path}")
print(response.result)
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
const org = process.env.MESA_ORG!
const repo = 'my-workspace'
const mountPath = `/home/daytona/mesa/mnt/${org}/${repo}`
const configPath = '/home/daytona/.config/mesa/config.toml'
const sandbox = await daytona.create({
snapshot: 'fuse-mesa',
envVars: {
MESA_API_KEY: process.env.MESA_API_KEY!,
MESA_ORG: org,
},
})
const config = `mount-point = "/home/daytona/mesa/mnt"
[secrets]
backend = "plaintext-file"
[organizations.${org}]
`
await sandbox.process.executeCommand(`mkdir -p $(dirname ${configPath})`)
await sandbox.fs.uploadFile(Buffer.from(config), configPath)
await sandbox.process.executeCommand('mesa auth set-key --org $MESA_ORG $MESA_API_KEY')
await sandbox.process.executeCommand('mesa mount --daemonize')
const response = await sandbox.process.executeCommand(`ls ${mountPath}`)
console.log(response.result)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
org = ENV.fetch('MESA_ORG')
repo = 'my-workspace'
mount_path = "/home/daytona/mesa/mnt/#{org}/#{repo}"
config_path = '/home/daytona/.config/mesa/config.toml'
sandbox = daytona.create(
Daytona::CreateSandboxFromSnapshotParams.new(
snapshot: 'fuse-mesa',
env_vars: {
'MESA_API_KEY' => ENV.fetch('MESA_API_KEY'),
'MESA_ORG' => org
}
)
)
config = <<~TOML
mount-point = "/home/daytona/mesa/mnt"
[secrets]
backend = "plaintext-file"
[organizations.#{org}]
TOML
sandbox.process.exec(command: "mkdir -p $(dirname #{config_path})")
sandbox.fs.upload_file(config, config_path)
sandbox.process.exec(command: 'mesa auth set-key --org $MESA_ORG $MESA_API_KEY')
sandbox.process.exec(command: 'mesa mount --daemonize')
response = sandbox.process.exec(command: "ls #{mount_path}")
puts response.result
```
```go
import (
"context"
"fmt"
"log"
"os"
"github.com/daytona/clients/sdk-go/pkg/daytona"
"github.com/daytona/clients/sdk-go/pkg/types"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
org := os.Getenv("MESA_ORG")
repo := "my-workspace"
mountPath := fmt.Sprintf("/home/daytona/mesa/mnt/%s/%s", org, repo)
configPath := "/home/daytona/.config/mesa/config.toml"
sandbox, err := client.Create(ctx, types.SnapshotParams{
Snapshot: "fuse-mesa",
SandboxBaseParams: types.SandboxBaseParams{
EnvVars: map[string]string{
"MESA_API_KEY": os.Getenv("MESA_API_KEY"),
"MESA_ORG": org,
},
},
})
if err != nil {
log.Fatal(err)
}
config := fmt.Sprintf(`mount-point = "/home/daytona/mesa/mnt"
[secrets]
backend = "plaintext-file"
[organizations.%s]
`, org)
if _, err := sandbox.Process.ExecuteCommand(ctx, "mkdir -p $(dirname "+configPath+")"); err != nil {
log.Fatal(err)
}
if err := sandbox.FileSystem.UploadFile(ctx, []byte(config), configPath); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx, "mesa auth set-key --org $MESA_ORG $MESA_API_KEY"); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx, "mesa mount --daemonize"); err != nil {
log.Fatal(err)
}
response, err := sandbox.Process.ExecuteCommand(ctx, "ls "+mountPath)
if err != nil {
log.Fatal(err)
}
fmt.Println(response.Result)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
import io.daytona.sdk.model.ExecuteResponse;
import java.nio.charset.StandardCharsets;
import java.util.Map;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
String org = System.getenv("MESA_ORG");
String repo = "my-workspace";
String mountPath = "/home/daytona/mesa/mnt/" + org + "/" + repo;
String configPath = "/home/daytona/.config/mesa/config.toml";
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setSnapshot("fuse-mesa");
params.setEnvVars(Map.of(
"MESA_API_KEY", System.getenv("MESA_API_KEY"),
"MESA_ORG", org
));
Sandbox sandbox = daytona.create(params);
String config = "mount-point = \"/home/daytona/mesa/mnt\"\n\n"
+ "[secrets]\n"
+ "backend = \"plaintext-file\"\n\n"
+ "[organizations." + org + "]\n";
sandbox.getProcess().executeCommand("mkdir -p $(dirname " + configPath + ")");
sandbox.fs.uploadFile(config.getBytes(StandardCharsets.UTF_8), configPath);
sandbox.getProcess().executeCommand(
"mesa auth set-key --org $MESA_ORG $MESA_API_KEY");
sandbox.getProcess().executeCommand("mesa mount --daemonize");
ExecuteResponse response = sandbox.getProcess().executeCommand("ls " + mountPath);
System.out.println(response.getResult());
}
}
}
```
### Runtime install
Start from a default sandbox and install the Mesa CLI during startup before configuring auth and running `mesa mount --daemonize`. This is useful when iterating quickly on mount behavior, with the tradeoff of slower cold starts for each sandbox.
```python
import os
from daytona import CreateSandboxBaseParams, Daytona
daytona = Daytona()
org = os.environ["MESA_ORG"]
repo = "my-workspace"
mount_path = f"/home/daytona/mesa/mnt/{org}/{repo}"
config_path = "/home/daytona/.config/mesa/config.toml"
sandbox = daytona.create(
CreateSandboxBaseParams(
env_vars={
"MESA_API_KEY": os.environ["MESA_API_KEY"],
"MESA_ORG": org,
},
)
)
sandbox.process.exec("curl -fsSL https://mesa.dev/install.sh | sh")
sandbox.process.exec(
"sudo sed -i 's/^#user_allow_other/user_allow_other/' /etc/fuse.conf"
)
config = f'''mount-point = "/home/daytona/mesa/mnt"
[secrets]
backend = "plaintext-file"
[organizations.{org}]
'''
sandbox.process.exec(f"mkdir -p $(dirname {config_path})")
sandbox.fs.upload_file(config.encode(), config_path)
sandbox.process.exec("mesa auth set-key --org $MESA_ORG $MESA_API_KEY")
sandbox.process.exec("mesa mount --daemonize")
response = sandbox.process.exec(f"ls {mount_path}")
print(response.result)
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
const org = process.env.MESA_ORG!
const repo = 'my-workspace'
const mountPath = `/home/daytona/mesa/mnt/${org}/${repo}`
const configPath = '/home/daytona/.config/mesa/config.toml'
const sandbox = await daytona.create({
envVars: {
MESA_API_KEY: process.env.MESA_API_KEY!,
MESA_ORG: org,
},
})
await sandbox.process.executeCommand('curl -fsSL https://mesa.dev/install.sh | sh')
await sandbox.process.executeCommand(
"sudo sed -i 's/^#user_allow_other/user_allow_other/' /etc/fuse.conf",
)
const config = `mount-point = "/home/daytona/mesa/mnt"
[secrets]
backend = "plaintext-file"
[organizations.${org}]
`
await sandbox.process.executeCommand(`mkdir -p $(dirname ${configPath})`)
await sandbox.fs.uploadFile(Buffer.from(config), configPath)
await sandbox.process.executeCommand('mesa auth set-key --org $MESA_ORG $MESA_API_KEY')
await sandbox.process.executeCommand('mesa mount --daemonize')
const response = await sandbox.process.executeCommand(`ls ${mountPath}`)
console.log(response.result)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
org = ENV.fetch('MESA_ORG')
repo = 'my-workspace'
mount_path = "/home/daytona/mesa/mnt/#{org}/#{repo}"
config_path = '/home/daytona/.config/mesa/config.toml'
sandbox = daytona.create(
Daytona::CreateSandboxBaseParams.new(
env_vars: {
'MESA_API_KEY' => ENV.fetch('MESA_API_KEY'),
'MESA_ORG' => org
}
)
)
sandbox.process.exec(command: 'curl -fsSL https://mesa.dev/install.sh | sh')
sandbox.process.exec(
command: "sudo sed -i 's/^#user_allow_other/user_allow_other/' /etc/fuse.conf"
)
config = <<~TOML
mount-point = "/home/daytona/mesa/mnt"
[secrets]
backend = "plaintext-file"
[organizations.#{org}]
TOML
sandbox.process.exec(command: "mkdir -p $(dirname #{config_path})")
sandbox.fs.upload_file(config, config_path)
sandbox.process.exec(command: 'mesa auth set-key --org $MESA_ORG $MESA_API_KEY')
sandbox.process.exec(command: 'mesa mount --daemonize')
response = sandbox.process.exec(command: "ls #{mount_path}")
puts response.result
```
```go
import (
"context"
"fmt"
"log"
"os"
"github.com/daytona/clients/sdk-go/pkg/daytona"
"github.com/daytona/clients/sdk-go/pkg/types"
)
ctx := context.Background()
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
org := os.Getenv("MESA_ORG")
repo := "my-workspace"
mountPath := fmt.Sprintf("/home/daytona/mesa/mnt/%s/%s", org, repo)
configPath := "/home/daytona/.config/mesa/config.toml"
sandbox, err := client.Create(ctx, types.SnapshotParams{
SandboxBaseParams: types.SandboxBaseParams{
EnvVars: map[string]string{
"MESA_API_KEY": os.Getenv("MESA_API_KEY"),
"MESA_ORG": org,
},
},
})
if err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx,
"curl -fsSL https://mesa.dev/install.sh | sh"); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx,
`sudo sed -i 's/^#user_allow_other/user_allow_other/' /etc/fuse.conf`); err != nil {
log.Fatal(err)
}
config := fmt.Sprintf(`mount-point = "/home/daytona/mesa/mnt"
[secrets]
backend = "plaintext-file"
[organizations.%s]
`, org)
if _, err := sandbox.Process.ExecuteCommand(ctx, "mkdir -p $(dirname "+configPath+")"); err != nil {
log.Fatal(err)
}
if err := sandbox.FileSystem.UploadFile(ctx, []byte(config), configPath); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx, "mesa auth set-key --org $MESA_ORG $MESA_API_KEY"); err != nil {
log.Fatal(err)
}
if _, err := sandbox.Process.ExecuteCommand(ctx, "mesa mount --daemonize"); err != nil {
log.Fatal(err)
}
response, err := sandbox.Process.ExecuteCommand(ctx, "ls "+mountPath)
if err != nil {
log.Fatal(err)
}
fmt.Println(response.Result)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
import io.daytona.sdk.model.ExecuteResponse;
import java.nio.charset.StandardCharsets;
import java.util.Map;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
String org = System.getenv("MESA_ORG");
String repo = "my-workspace";
String mountPath = "/home/daytona/mesa/mnt/" + org + "/" + repo;
String configPath = "/home/daytona/.config/mesa/config.toml";
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setEnvVars(Map.of(
"MESA_API_KEY", System.getenv("MESA_API_KEY"),
"MESA_ORG", org
));
Sandbox sandbox = daytona.create(params);
sandbox.getProcess().executeCommand(
"curl -fsSL https://mesa.dev/install.sh | sh");
sandbox.getProcess().executeCommand(
"sudo sed -i 's/^#user_allow_other/user_allow_other/' /etc/fuse.conf");
String config = "mount-point = \"/home/daytona/mesa/mnt\"\n\n"
+ "[secrets]\n"
+ "backend = \"plaintext-file\"\n\n"
+ "[organizations." + org + "]\n";
sandbox.getProcess().executeCommand("mkdir -p $(dirname " + configPath + ")");
sandbox.fs.uploadFile(config.getBytes(StandardCharsets.UTF_8), configPath);
sandbox.getProcess().executeCommand(
"mesa auth set-key --org $MESA_ORG $MESA_API_KEY");
sandbox.getProcess().executeCommand("mesa mount --daemonize");
ExecuteResponse response = sandbox.getProcess().executeCommand("ls " + mountPath);
System.out.println(response.getResult());
}
}
}
```
### Production: scoped ephemeral keys
For non-test workloads, Mesa recommends minting a **short-lived, scoped API key per sandbox session** rather than passing your long-lived `MESA_API_KEY` into the sandbox. Use the [Mesa SDK ↗](https://docs.mesa.dev) on your trusted host to derive an ephemeral key from your long-lived one — the long-lived key never leaves your host process. Mesa SDKs are available for TypeScript, Python, and Rust; for other languages, use the [Mesa REST API ↗](https://docs.mesa.dev/content/api-reference/overview) directly.
```python
import asyncio
import os
from daytona import CreateSandboxFromSnapshotParams, Daytona
from mesa_sdk import Mesa
async def mint_ephemeral_key() -> str:
async with Mesa(api_key=os.environ["MESA_API_KEY"], org=os.environ["MESA_ORG"]) as mesa:
key = await mesa.api_keys.create(
name="sandbox-session",
scopes=["read", "write"],
expires_in_seconds=3600,
)
return key.key
ephemeral_key = asyncio.run(mint_ephemeral_key())
daytona = Daytona()
sandbox = daytona.create(
CreateSandboxFromSnapshotParams(
snapshot="fuse-mesa",
env_vars={
"MESA_API_KEY": ephemeral_key,
"MESA_ORG": os.environ["MESA_ORG"],
},
)
)
```
```typescript
import { Daytona } from '@daytona/sdk'
import { Mesa } from '@mesadev/sdk'
const mesa = new Mesa({ apiKey: process.env.MESA_API_KEY!, org: process.env.MESA_ORG! })
const ephemeralKey = await mesa.apiKeys.create({
name: 'sandbox-session',
scopes: ['read', 'write'],
expires_in_seconds: 3600,
})
const daytona = new Daytona()
const sandbox = await daytona.create({
snapshot: 'fuse-mesa',
envVars: {
MESA_API_KEY: ephemeralKey.key,
MESA_ORG: process.env.MESA_ORG!,
},
})
```
The rest of the launch flow (writing the TOML config, `mesa auth set-key`, `mesa mount --daemonize`) is unchanged — the sandbox doesn't know whether the key it received is long-lived or ephemeral.
For repo-scoped or path-scoped keys, see Mesa's [auth and permissions guide ↗](https://docs.mesa.dev/content/getting-started/auth-and-permissions). For the full integration recipe, see Mesa's [Daytona guide ↗](https://docs.mesa.dev/content/integration-guides/daytona).
## Unmount
When a sandbox is deleted via `daytona.delete(sandbox)`, the container teardown automatically removes any active FUSE mounts and shuts down their daemons. For normal cleanup, this is all you need — no manual unmount required.
To free a mount path **during** a sandbox's lifetime (for example, to remount with different credentials or before persisting a workspace archive), relocate the mount onto a throwaway path:
```bash
sudo mkdir -p /tmp/.fuse-defunct-$$
sudo mount --move /tmp/.fuse-defunct-$$
```
After this, your original mount path is free for remounting. The FUSE daemon stays alive serving the mount at the new path; both the relocated mount and the daemon are cleaned up automatically when the sandbox is deleted.
This works for any FUSE-based mount — verified against `mount-s3`, `gcsfuse`, and `blobfuse2`.
# Regions
Sandboxes are isolated runtime environments that run on [runners](https://www.daytona.io/docs/en/bring-your-own-compute.md) — machines that form Daytona's compute plane.
Runners are organized into **regions**, which are geographic or logical groupings of compute infrastructure. When creating a sandbox, you can target a specific region, and Daytona will schedule your workload on an available runner within that region.
As a result, you're able to:
- Choose specific geographic locations for reduced latency
- Comply with data residency requirements
- Use your own runner machines for custom regions
- Scale compute resources independently within each custom region
Regions are geographic or logical groupings of runners that execute sandbox workloads. The sandbox region is specified by setting the `target` parameter on initialization:
```python
from daytona import Daytona, DaytonaConfig
# Configure Daytona to use the US region
config = DaytonaConfig(
target="us"
)
# Initialize the Daytona client with the specified configuration
daytona = Daytona(config)
```
```typescript
import { Daytona } from '@daytona/sdk';
// Configure Daytona to use the EU region
const daytona: Daytona = new Daytona({
target: "eu"
});
```
```go
package main
import (
"github.com/daytona/clients/sdk-go/pkg/daytona"
"github.com/daytona/clients/sdk-go/pkg/types"
)
// Configure Daytona to use the US region
client, _ := daytona.NewClientWithConfig(&types.DaytonaConfig{
Target: "us",
})
```
```ruby
require 'daytona'
# Configure Daytona to use the EU region
config = Daytona::Config.new(
target: "eu"
)
# Initialize the Daytona client with the specified configuration
daytona = Daytona::Daytona.new(config)
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.DaytonaConfig;
// Configure Daytona to use the US region
DaytonaConfig config = new DaytonaConfig.Builder()
.apiKey(System.getenv("DAYTONA_API_KEY"))
.target("us")
.build();
Daytona daytona = new Daytona(config);
```
### Shared regions
Shared regions are managed by Daytona and available to all organizations. These regions provide immediate access to Daytona's infrastructure without any setup required.
Limits are applied to your organization's default region. For access to a different shared region, contact [sales@daytona.io](mailto:sales@daytona.io).
| Region | Target |
| ------------- | -------- |
| United States | **`us`** |
| Europe | **`eu`** |
### Dedicated regions
Dedicated regions are managed by Daytona and provisioned exclusively for individual organizations. These regions deliver dedicated infrastructure with the operational simplicity of a managed service.
:::note
Contact [sales@daytona.io](mailto:sales@daytona.io) to set up a dedicated region for your organization.
:::
### Custom regions
Custom regions are created and managed by your organization, allowing you to use your own runner machines and scale compute resources independently within each region. This provides maximum control over data locality, compliance, and infrastructure configuration.
Additionally, custom regions have no limits applied for concurrent resource usage, giving you full control over capacity and performance.
For more information, see the [bring your own compute (BYOC)](https://www.daytona.io/docs/en/bring-your-own-compute.md) guide.
# File System Operations
Daytona provides comprehensive file system operations through the `fs` module in sandboxes.
## Basic operations
Daytona provides methods to interact with the file system in sandboxes. You can perform various operations like listing files, creating directories, reading and writing files, and more.
File operations assume you are operating in the sandbox user's home directory (e.g. `workspace` implies `/home/[username]/workspace`). Use a leading `/` when providing absolute paths.
### List files and directories
List files and directories in a sandbox by providing the path to the directory.
```python
# List files in a directory
files = sandbox.fs.list_files("workspace")
for file in files:
print(f"Name: {file.name}")
print(f"Is directory: {file.is_dir}")
print(f"Size: {file.size}")
print(f"Modified: {file.mod_time}")
```
```typescript
// List files in a directory
const files = await sandbox.fs.listFiles('workspace')
files.forEach(file => {
console.log(`Name: ${file.name}`)
console.log(`Is directory: ${file.isDir}`)
console.log(`Size: ${file.size}`)
console.log(`Modified: ${file.modTime}`)
})
```
```ruby
# List directory contents
files = sandbox.fs.list_files("workspace/data")
# Print files and their sizes
files.each do |file|
puts "#{file.name}: #{file.size} bytes" unless file.is_dir
end
# List only directories
dirs = files.select(&:is_dir)
puts "Subdirectories: #{dirs.map(&:name).join(', ')}"
```
```go
// List files in a directory
files, err := sandbox.FileSystem.ListFiles(ctx, "workspace")
if err != nil {
log.Fatal(err)
}
for _, file := range files {
fmt.Printf("Name: %s\n", file.Name)
fmt.Printf("Is directory: %t\n", file.IsDirectory)
fmt.Printf("Size: %d\n", file.Size)
fmt.Printf("Modified: %s\n", file.ModifiedTime)
}
```
```java
import io.daytona.sdk.model.FileInfo;
import java.util.List;
List files = sandbox.fs.listFiles("workspace");
for (FileInfo file : files) {
System.out.println("Name: " + file.getName());
System.out.println("Is directory: " + file.getIsDir());
System.out.println("Size: " + file.getSize());
System.out.println("Modified: " + file.getModTime());
}
```
```bash
# List one directory level (default depth=1)
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/files?path=workspace'
# List recursively: depth=2 includes immediate children
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/files?path=workspace&depth=2'
```
### Get directory or file information
Get directory or file information by providing the path to the directory or file.
```python
# Get file metadata
info = sandbox.fs.get_file_info("workspace/data/file.txt")
print(f"Size: {info.size} bytes")
print(f"Modified: {info.mod_time}")
print(f"Mode: {info.mode}")
# Check if path is a directory
info = sandbox.fs.get_file_info("workspace/data")
if info.is_dir:
print("Path is a directory")
```
```typescript
// Get file details
const info = await sandbox.fs.getFileDetails('app/config.json')
console.log(`Size: ${info.size}, Modified: ${info.modifiedAt ?? info.modTime}`)
```
```ruby
# Get file metadata
info = sandbox.fs.get_file_info("workspace/data/file.txt")
puts "Size: #{info.size} bytes"
puts "Modified: #{info.mod_time}"
puts "Mode: #{info.mode}"
# Check if path is a directory
info = sandbox.fs.get_file_info("workspace/data")
puts "Path is a directory" if info.is_dir
```
```go
// Get file metadata
info, err := sandbox.FileSystem.GetFileInfo(ctx, "workspace/data/file.txt")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Size: %d bytes\n", info.Size)
fmt.Printf("Modified: %s\n", info.ModifiedTime)
fmt.Printf("Mode: %s\n", info.Mode)
// Check if path is a directory
info, err = sandbox.FileSystem.GetFileInfo(ctx, "workspace/data")
if err != nil {
log.Fatal(err)
}
if info.IsDirectory {
fmt.Println("Path is a directory")
}
```
```java
import io.daytona.sdk.model.FileInfo;
FileInfo info = sandbox.fs.getFileDetails("workspace/data/file.txt");
System.out.println("Size: " + info.getSize() + " bytes");
System.out.println("Modified: " + info.getModTime());
System.out.println("Mode: " + info.getMode());
info = sandbox.fs.getFileDetails("workspace/data");
if (Boolean.TRUE.equals(info.getIsDir())) {
System.out.println("Path is a directory");
}
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/files/info?path='
```
### Create directories
Create a directory by providing the path and permissions to set on the directory.
```python
# Create with specific permissions
sandbox.fs.create_folder("workspace/new-dir", "755")
```
```typescript
// Create with specific permissions
await sandbox.fs.createFolder('workspace/new-dir', '755')
```
```ruby
# Create a directory with standard permissions
sandbox.fs.create_folder("workspace/data", "755")
# Create a private directory
sandbox.fs.create_folder("workspace/secrets", "700")
```
```go
// Create with specific permissions
err := sandbox.FileSystem.CreateFolder(ctx, "workspace/new-dir",
options.WithMode("755"),
)
if err != nil {
log.Fatal(err)
}
```
```java
sandbox.fs.createFolder("workspace/new-dir", "755");
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/files/folder?path=&mode=' \
--request POST
```
### Upload files
Daytona provides methods to upload a single or multiple files in sandboxes.
#### Upload a single file
Upload a single file by providing the content to upload and the path to the file to upload it to.
```python
# Upload from memory (small files)
content = b"Hello, World!"
sandbox.fs.upload_file(content, "remote_file.txt")
# Upload from a local file path (streams large files)
sandbox.fs.upload_file("local_file.txt", "remote_file.txt")
```
```typescript
// Upload a single file
const fileContent = Buffer.from('Hello, World!')
await sandbox.fs.uploadFile(fileContent, 'data.txt')
```
```ruby
# Upload a text file from string content
content = "Hello, World!"
sandbox.fs.upload_file(content, "tmp/hello.txt")
# Upload a local file
sandbox.fs.upload_file("local_file.txt", "tmp/file.txt")
# Upload binary data
data = { key: "value" }.to_json
sandbox.fs.upload_file(data, "tmp/config.json")
```
```go
// Upload from a local file path
err := sandbox.FileSystem.UploadFile(ctx, "local_file.txt", "remote_file.txt")
if err != nil {
log.Fatal(err)
}
// Or upload from byte content
content := []byte("Hello, World!")
err = sandbox.FileSystem.UploadFile(ctx, content, "hello.txt")
if err != nil {
log.Fatal(err)
}
```
```java
import java.nio.charset.StandardCharsets;
byte[] fileContent = "Hello, World!".getBytes(StandardCharsets.UTF_8);
sandbox.fs.uploadFile(fileContent, "data.txt");
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/files/upload?path=' \
--request POST \
--header 'Content-Type: multipart/form-data' \
--form 'file='
```
#### Upload multiple files
Upload multiple files by providing the content to upload and their destination paths.
```python
# Upload multiple files at once
files_to_upload = []
with open("file1.txt", "rb") as f1:
files_to_upload.append(FileUpload(
source=f1.read(),
destination="data/file1.txt",
))
with open("file2.txt", "rb") as f2:
files_to_upload.append(FileUpload(
source=f2.read(),
destination="data/file2.txt",
))
with open("settings.json", "rb") as f3:
files_to_upload.append(FileUpload(
source=f3.read(),
destination="config/settings.json",
))
sandbox.fs.upload_files(files_to_upload)
```
```typescript
// Upload multiple files at once
const files = [
{
source: Buffer.from('Content of file 1'),
destination: 'data/file1.txt',
},
{
source: Buffer.from('Content of file 2'),
destination: 'data/file2.txt',
},
{
source: Buffer.from('{"key": "value"}'),
destination: 'config/settings.json',
},
]
await sandbox.fs.uploadFiles(files)
```
```ruby
# Upload multiple files
files = [
FileUpload.new("Content of file 1", "/tmp/file1.txt"),
FileUpload.new("workspace/data/file2.txt", "/tmp/file2.txt"),
FileUpload.new('{"key": "value"}', "/tmp/config.json")
]
sandbox.fs.upload_files(files)
```
```go
// Upload multiple files by calling UploadFile for each
filesToUpload := []struct {
source string
destination string
}{
{"file1.txt", "data/file1.txt"},
{"file2.txt", "data/file2.txt"},
{"settings.json", "config/settings.json"},
}
for _, f := range filesToUpload {
err := sandbox.FileSystem.UploadFile(ctx, f.source, f.destination)
if err != nil {
log.Fatal(err)
}
}
```
```java
import java.nio.charset.StandardCharsets;
sandbox.fs.uploadFile("Hello, World!".getBytes(StandardCharsets.UTF_8), "data/file1.txt");
sandbox.fs.uploadFile("More content".getBytes(StandardCharsets.UTF_8), "data/file2.txt");
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/files/bulk-upload' \
--request POST
```
#### Stream uploads
For large files, use streaming upload methods to avoid loading the entire file into memory.
```python
with open("large_dataset.csv", "rb") as f:
sandbox.fs.upload_file_stream(f, "workspace/dataset.csv")
```
```typescript
import { createReadStream } from 'node:fs'
await sandbox.fs.uploadFileStream(
createReadStream('large_dataset.csv'),
'workspace/dataset.csv'
)
```
```ruby
File.open("large_dataset.csv", "rb") do |f|
sandbox.fs.upload_file_stream(f, "workspace/dataset.csv")
end
```
```go
f, err := os.Open("large_dataset.csv")
if err != nil {
log.Fatal(err)
}
defer f.Close()
err = sandbox.FileSystem.UploadFileStream(ctx, f, "workspace/dataset.csv")
if err != nil {
log.Fatal(err)
}
```
```java
import java.io.FileInputStream;
import java.nio.file.Path;
try (var in = new FileInputStream(Path.of("large_dataset.csv").toFile())) {
sandbox.fs.uploadFileStream(in, "workspace/dataset.csv");
}
```
### Download files
Daytona provides methods to download files from sandboxes.
#### Download a single file
Download a single file by providing the path to the file to download.
```python
from daytona import DaytonaNotFoundError
try:
content = sandbox.fs.download_file("file1.txt")
except DaytonaNotFoundError as error:
print(f"Missing file: {error}")
else:
with open("local_file.txt", "wb") as f:
f.write(content)
print(content.decode("utf-8"))
# Stream a large file to disk without loading it into memory
sandbox.fs.download_file("workspace/large-file.bin", "local_copy.bin")
```
```typescript
import { DaytonaNotFoundError } from '@daytona/sdk'
try {
const downloadedFile = await sandbox.fs.downloadFile('file1.txt')
console.log('File content:', downloadedFile.toString())
} catch (error) {
if (error instanceof DaytonaNotFoundError) {
console.error(`Missing file: ${error.message}`)
} else {
throw error
}
}
```
```ruby
# Download and get file content
content = sandbox.fs.download_file("workspace/data/file.txt")
puts content
# Download and save a file locally
sandbox.fs.download_file("workspace/data/file.txt", "local_copy.txt")
size_mb = File.size("local_copy.txt") / 1024.0 / 1024.0
puts "Size of the downloaded file: #{size_mb} MB"
```
```go
// Download and get contents in memory
content, err := sandbox.FileSystem.DownloadFile(ctx, "file1.txt", nil)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(content))
// Download and save to a local file
localPath := "local_file.txt"
content, err = sandbox.FileSystem.DownloadFile(ctx, "file1.txt", &localPath)
if err != nil {
log.Fatal(err)
}
```
```java
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
byte[] content = sandbox.fs.downloadFile("file1.txt");
System.out.println(new String(content, StandardCharsets.UTF_8));
Files.write(Path.of("local_file.txt"), content);
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/files/download?path='
```
#### Download multiple files
Download multiple files by providing the paths to the files to download.
```python
# Download multiple files at once
files_to_download = [
FileDownloadRequest(source="data/file1.txt"), # No destination - download to memory
FileDownloadRequest(source="data/file2.txt", destination="local_file2.txt"), # Download to local file
]
results = sandbox.fs.download_files(files_to_download)
for result in results:
if result.error:
print(f"Error downloading {result.source}: {result.error}")
if result.error_details:
print(
f" status={result.error_details.status_code} "
f"code={result.error_details.error_code}"
)
elif result.result:
print(f"Downloaded {result.source} to {result.result}")
```
```typescript
// Download multiple files at once
const files = [
{ source: 'data/file1.txt' }, // No destination - download to memory
{ source: 'data/file2.txt', destination: 'local_file2.txt' }, // Download to local file
]
const results = await sandbox.fs.downloadFiles(files)
results.forEach(result => {
if (result.error) {
console.error(`Error downloading ${result.source}: ${result.error}`)
if (result.errorDetails) {
console.error(
` status=${result.errorDetails.statusCode} code=${result.errorDetails.errorCode}`
)
}
} else if (result.result) {
console.log(`Downloaded ${result.source} to ${result.result}`)
}
})
```
```ruby
# Download multiple files by calling download_file for each
files_to_download = [
{ remote: "data/file1.txt", local: nil }, # Download to memory
{ remote: "data/file2.txt", local: "local_file2.txt" } # Download to local file
]
files_to_download.each do |f|
if f[:local]
sandbox.fs.download_file(f[:remote], f[:local])
puts "Downloaded #{f[:remote]} to #{f[:local]}"
else
content = sandbox.fs.download_file(f[:remote])
puts "Downloaded #{f[:remote]} to memory (#{content.size} bytes)"
end
end
```
```go
// Download multiple files by calling DownloadFile for each
filesToDownload := []struct {
remotePath string
localPath *string
}{
{"data/file1.txt", nil}, // Download to memory
{"data/file2.txt", ptrString("local_file2.txt")}, // Download to local file
}
for _, f := range filesToDownload {
content, err := sandbox.FileSystem.DownloadFile(ctx, f.remotePath, f.localPath)
if err != nil {
fmt.Printf("Error downloading %s: %v\n", f.remotePath, err)
continue
}
if f.localPath == nil {
fmt.Printf("Downloaded %s to memory (%d bytes)\n", f.remotePath, len(content))
} else {
fmt.Printf("Downloaded %s to %s\n", f.remotePath, *f.localPath)
}
}
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/files/bulk-download' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"paths": [
""
]
}'
```
#### Stream downloads
For large files, use streaming download methods to avoid loading the entire file into memory.
```python
with open("local_copy.bin", "wb") as f:
for chunk in sandbox.fs.download_file_stream("workspace/large-file.bin"):
f.write(chunk)
```
```typescript
import { createWriteStream } from 'node:fs'
import { pipeline } from 'node:stream/promises'
const stream = await sandbox.fs.downloadFileStream('workspace/large-file.bin')
await pipeline(stream, createWriteStream('local_copy.bin'))
```
```ruby
File.open("local_copy.bin", "wb") do |f|
sandbox.fs.download_file_stream("workspace/large-file.bin") { |chunk| f.write(chunk) }
end
```
```go
stream, err := sandbox.FileSystem.DownloadFileStream(ctx, "workspace/large-file.bin")
if err != nil {
log.Fatal(err)
}
defer stream.Close()
out, err := os.Create("local_copy.bin")
if err != nil {
log.Fatal(err)
}
defer out.Close()
_, err = io.Copy(out, stream)
if err != nil {
log.Fatal(err)
}
```
```java
import java.io.FileOutputStream;
import java.io.InputStream;
import java.nio.file.Path;
try (InputStream in = sandbox.fs.downloadFileStream("workspace/large-file.bin");
var out = new FileOutputStream(Path.of("local_copy.bin").toFile())) {
in.transferTo(out);
}
```
### Delete files
Delete a file or directory by providing the path to the file or directory to delete.
Pass `recursive: true` to delete a directory recursively.
```python
sandbox.fs.delete_file("workspace/file.txt")
# Delete a directory recursively
sandbox.fs.delete_file("workspace/old_dir", recursive=True)
```
```typescript
await sandbox.fs.deleteFile('workspace/file.txt')
// Delete a directory recursively
await sandbox.fs.deleteFile('workspace/old_dir', true)
```
```ruby
# Delete a file
sandbox.fs.delete_file("workspace/data/old_file.txt")
# Delete a directory recursively
sandbox.fs.delete_file("workspace/old_dir", recursive: true)
```
```go
// Delete a file
err := sandbox.FileSystem.DeleteFile(ctx, "workspace/file.txt", false)
if err != nil {
log.Fatal(err)
}
// Delete a directory recursively
err = sandbox.FileSystem.DeleteFile(ctx, "workspace/old_dir", true)
if err != nil {
log.Fatal(err)
}
```
```java
sandbox.fs.deleteFile("workspace/file.txt");
```
```bash
# Delete a file
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/files?path=workspace/file.txt' \
--request DELETE
# Delete a directory recursively
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/files?path=workspace/old_dir&recursive=true' \
--request DELETE
```
## Advanced operations
Daytona provides advanced file system operations such as file permissions, search by file name, content search and replace, and move files.
### File permissions
Set file permissions, ownership, and group for a file or directory by providing the path to the file or directory and the permissions to set.
```python
# Set file permissions
sandbox.fs.set_file_permissions("workspace/file.txt", "644")
# Get file permissions
file_info = sandbox.fs.get_file_info("workspace/file.txt")
print(f"Permissions: {file_info.permissions}")
```
```typescript
// Set file permissions
await sandbox.fs.setFilePermissions('workspace/file.txt', { mode: '644' })
// Get file permissions
const fileInfo = await sandbox.fs.getFileDetails('workspace/file.txt')
console.log(`Permissions: ${fileInfo.permissions}`)
```
```ruby
# Make a file executable
sandbox.fs.set_file_permissions(
path: "workspace/scripts/run.sh",
mode: "755" # rwxr-xr-x
)
# Change file owner
sandbox.fs.set_file_permissions(
path: "workspace/data/file.txt",
owner: "daytona",
group: "daytona"
)
```
```go
// Set file permissions
err := sandbox.FileSystem.SetFilePermissions(ctx, "workspace/file.txt",
options.WithPermissionMode("644"),
)
if err != nil {
log.Fatal(err)
}
// Set owner and group
err = sandbox.FileSystem.SetFilePermissions(ctx, "workspace/file.txt",
options.WithOwner("daytona"),
options.WithGroup("daytona"),
)
if err != nil {
log.Fatal(err)
}
// Get file info to check permissions
fileInfo, err := sandbox.FileSystem.GetFileInfo(ctx, "workspace/file.txt")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Mode: %s\n", fileInfo.Mode)
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/files/permissions?path=' \
--request POST
```
### Search files by pattern
Search for files and directories by name using glob patterns (for example `*.py`). This is distinct from `find_files` / `findFiles`, which searches file contents.
```python
result = sandbox.fs.search_files("workspace", "*.py")
for file in result.files:
print(file)
```
```typescript
const result = await sandbox.fs.searchFiles('workspace', '*.ts')
result.files.forEach(file => console.log(file))
```
```ruby
result = sandbox.fs.search_files("workspace", "*.rb")
result.files.each { |file| puts file }
```
```go
result, err := sandbox.FileSystem.SearchFiles(ctx, "workspace", "*.go")
if err != nil {
log.Fatal(err)
}
files := result.(map[string]any)["files"].([]string)
for _, file := range files {
fmt.Println(file)
}
```
```java
import java.util.List;
import java.util.Map;
Map result = sandbox.fs.searchFiles("workspace", "*.java");
@SuppressWarnings("unchecked")
List files = (List) result.get("files");
for (String file : files) {
System.out.println(file);
}
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/files/search?path=workspace&pattern=*.py'
```
### Find and replace text in files
Find and replace text in files by providing the path to the directory to search in and the pattern to search for.
```python
# Search for text in files by providing the path to the directory to search in and the pattern to search for
results = sandbox.fs.find_files(
path="workspace/src",
pattern="text-of-interest"
)
for match in results:
print(f"Absolute file path: {match.file}")
print(f"Line number: {match.line}")
print(f"Line content: {match.content}")
print("\n")
# Replace text in files
sandbox.fs.replace_in_files(
files=["workspace/file1.txt", "workspace/file2.txt"],
pattern="old_text",
new_value="new_text"
)
```
```typescript
// Search for text in files; if a folder is specified, the search is recursive
const results = await sandbox.fs.findFiles('workspace/src', 'text-of-interest')
results.forEach(match => {
console.log('Absolute file path:', match.file)
console.log('Line number:', match.line)
console.log('Line content:', match.content)
})
// Replace text in files
await sandbox.fs.replaceInFiles(
["workspace/file1.txt", "workspace/file2.txt"],
"old_text",
"new_text"
)
```
```ruby
# Search for TODOs in Ruby files
matches = sandbox.fs.find_files("workspace/src", "TODO:")
matches.each do |match|
puts "#{match.file}:#{match.line}: #{match.content.strip}"
end
# Replace in specific files
results = sandbox.fs.replace_in_files(
files: ["workspace/src/file1.rb", "workspace/src/file2.rb"],
pattern: "old_function",
new_value: "new_function"
)
# Print results
results.each do |result|
if result.success
puts "#{result.file}: #{result.success}"
else
puts "#{result.file}: #{result.error}"
end
end
```
```go
// Search for text in files
result, err := sandbox.FileSystem.FindFiles(ctx, "workspace/src", "text-of-interest")
if err != nil {
log.Fatal(err)
}
matches := result.([]map[string]any)
for _, match := range matches {
fmt.Printf("Absolute file path: %s\n", match["file"])
fmt.Printf("Line number: %v\n", match["line"])
fmt.Printf("Line content: %s\n\n", match["content"])
}
// Replace text in files
_, err = sandbox.FileSystem.ReplaceInFiles(ctx,
[]string{"workspace/file1.txt", "workspace/file2.txt"},
"old_text",
"new_text",
)
if err != nil {
log.Fatal(err)
}
```
```java
import java.util.Arrays;
import java.util.List;
import java.util.Map;
List> results = sandbox.fs.findFiles("workspace/src", "text-of-interest");
for (Map match : results) {
System.out.println("Absolute file path: " + match.get("file"));
System.out.println("Line number: " + match.get("line"));
System.out.println("Line content: " + match.get("content"));
System.out.println();
}
sandbox.fs.replaceInFiles(
Arrays.asList("workspace/file1.txt", "workspace/file2.txt"),
"old_text",
"new_text"
);
```
Find text in files:
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/files/find?path=&pattern='
```
Replace text in files:
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/files/replace' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"files": [
""
],
"newValue": "",
"pattern": ""
}'
```
### Move or rename directory or file
Move or rename a directory or file by providing the path to the file or directory (source) and the new path to the file or directory (destination).
```python
# Rename a file
sandbox.fs.move_files(
"workspace/data/old_name.txt",
"workspace/data/new_name.txt"
)
# Move a file to a different directory
sandbox.fs.move_files(
"workspace/data/file.txt",
"workspace/archive/file.txt"
)
# Move a directory
sandbox.fs.move_files(
"workspace/old_dir",
"workspace/new_dir"
)
```
```typescript
// Move a file to a new location
await sandbox.fs.moveFiles('app/temp/data.json', 'app/data/data.json')
```
```ruby
# Rename a file
sandbox.fs.move_files(
"workspace/data/old_name.txt",
"workspace/data/new_name.txt"
)
# Move a file to a different directory
sandbox.fs.move_files(
"workspace/data/file.txt",
"workspace/archive/file.txt"
)
# Move a directory
sandbox.fs.move_files(
"workspace/old_dir",
"workspace/new_dir"
)
```
```go
// Rename a file
err := sandbox.FileSystem.MoveFiles(ctx, "workspace/data/old_name.txt", "workspace/data/new_name.txt")
if err != nil {
log.Fatal(err)
}
// Move a file to a different directory
err = sandbox.FileSystem.MoveFiles(ctx, "workspace/data/file.txt", "workspace/archive/file.txt")
if err != nil {
log.Fatal(err)
}
// Move a directory
err = sandbox.FileSystem.MoveFiles(ctx, "workspace/old_dir", "workspace/new_dir")
if err != nil {
log.Fatal(err)
}
```
```java
sandbox.fs.moveFiles("workspace/data/old_name.txt", "workspace/data/new_name.txt");
sandbox.fs.moveFiles("workspace/data/file.txt", "workspace/archive/file.txt");
sandbox.fs.moveFiles("workspace/old_dir", "workspace/new_dir");
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/files/move?source=&destination=' \
--request POST
```
# Git Operations
Daytona provides built-in Git support through the `git` module in sandboxes.
## Basic operations
Daytona provides methods to clone, check status, and manage Git repositories in sandboxes.
Git operations assume you are operating in the sandbox user's home directory (e.g. `workspace` implies `/home/[username]/workspace`). Use a leading `/` when providing absolute paths.
### Clone repositories
Clone a Git repository into a sandbox by providing the URL and path to clone it to. You can clone public or private repositories, specific branches or commits, and authenticate using personal access tokens.
```python
# Basic clone
sandbox.git.clone(
url="https://github.com/user/repo.git",
path="workspace/repo"
)
# Clone with authentication
sandbox.git.clone(
url="https://github.com/user/repo.git",
path="workspace/repo",
username="git",
password="personal_access_token"
)
# Clone specific branch
sandbox.git.clone(
url="https://github.com/user/repo.git",
path="workspace/repo",
branch="develop"
)
# Clone a specific commit (detached HEAD)
sandbox.git.clone(
url="https://github.com/user/repo.git",
path="workspace/repo-old",
commit_id="abc123def456"
)
# Clone from a self-signed internal Git server (insecure)
sandbox.git.clone(
url="https://internal-git.example.com/org/repo.git",
path="workspace/repo",
insecure_skip_tls=True
)
```
```typescript
// Basic clone
await sandbox.git.clone(
"https://github.com/user/repo.git",
"workspace/repo"
);
// Clone with authentication
await sandbox.git.clone(
"https://github.com/user/repo.git",
"workspace/repo",
undefined,
undefined,
"git",
"personal_access_token"
);
// Clone specific branch
await sandbox.git.clone(
"https://github.com/user/repo.git",
"workspace/repo",
"develop"
);
// Clone a specific commit (detached HEAD)
await sandbox.git.clone(
"https://github.com/user/repo.git",
"workspace/repo-old",
undefined,
"abc123def456"
);
// Clone from a self-signed internal Git server (insecure)
await sandbox.git.clone(
"https://internal-git.example.com/org/repo.git",
"workspace/repo",
undefined,
undefined,
undefined,
undefined,
true
);
```
```ruby
# Basic clone
sandbox.git.clone(
url: 'https://github.com/user/repo.git',
path: 'workspace/repo'
)
# Clone with authentication
sandbox.git.clone(
url: 'https://github.com/user/repo.git',
path: 'workspace/repo',
username: 'git',
password: 'personal_access_token'
)
# Clone specific branch
sandbox.git.clone(
url: 'https://github.com/user/repo.git',
path: 'workspace/repo',
branch: 'develop'
)
# Clone a specific commit (detached HEAD)
sandbox.git.clone(
url: 'https://github.com/user/repo.git',
path: 'workspace/repo-old',
commit_id: 'abc123def456'
)
# Clone from a self-signed internal Git server (insecure)
sandbox.git.clone(
url: 'https://internal-git.example.com/org/repo.git',
path: 'workspace/repo',
insecure_skip_tls: true
)
```
```go
// Basic clone
err := sandbox.Git.Clone(ctx, "https://github.com/user/repo.git", "workspace/repo")
if err != nil {
log.Fatal(err)
}
// Clone with authentication
err = sandbox.Git.Clone(ctx, "https://github.com/user/repo.git", "workspace/repo",
options.WithUsername("git"),
options.WithPassword("personal_access_token"),
)
if err != nil {
log.Fatal(err)
}
// Clone specific branch
err = sandbox.Git.Clone(ctx, "https://github.com/user/repo.git", "workspace/repo",
options.WithBranch("develop"),
)
if err != nil {
log.Fatal(err)
}
// Clone a specific commit (detached HEAD)
err = sandbox.Git.Clone(ctx, "https://github.com/user/repo.git", "workspace/repo-old",
options.WithCommitId("abc123def456"),
)
if err != nil {
log.Fatal(err)
}
// Clone from a self-signed internal Git server (insecure)
err = sandbox.Git.Clone(ctx, "https://internal-git.example.com/org/repo.git", "workspace/repo",
options.WithInsecureSkipTLS(true),
)
if err != nil {
log.Fatal(err)
}
```
```java
// Basic clone
sandbox.git.clone("https://github.com/user/repo.git", "workspace/repo");
// Clone with authentication
sandbox.git.clone(
"https://github.com/user/repo.git",
"workspace/repo",
null,
null,
"git",
"personal_access_token"
);
// Clone specific branch
sandbox.git.clone(
"https://github.com/user/repo.git",
"workspace/repo",
"develop",
null,
null,
null
);
// Clone a specific commit (detached HEAD)
sandbox.git.clone(
"https://github.com/user/repo.git",
"workspace/repo-old",
null,
"abc123def456",
null,
null
);
// Clone from a self-signed internal Git server (insecure)
sandbox.git.clone(
"https://internal-git.example.com/org/repo.git",
"workspace/repo",
null,
null,
null,
null,
true
);
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/git/clone' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"branch": "",
"commit_id": "",
"depth": 0,
"insecure_skip_tls": false,
"password": "",
"path": "",
"url": "",
"username": ""
}'
```
### Get repository status
Get the status of a Git repository by providing the path to the repository.
You can get the current branch, modified files, and the number of commits ahead and behind the upstream tracking branch. When no upstream is configured, `ahead` and `behind` are zero and `branch_published` is false. The response also includes `upstream` (for example `origin/main`) and `detached` when HEAD is not on a branch.
```python
# Get repository status
status = sandbox.git.status("workspace/repo")
print(f"Current branch: {status.current_branch}")
print(f"Upstream: {status.upstream}")
print(f"Detached HEAD: {status.detached}")
print(f"Commits ahead: {status.ahead}")
print(f"Commits behind: {status.behind}")
for file in status.file_status:
print(f"File: {file.name}")
# List branches
response = sandbox.git.branches("workspace/repo")
print(f"Checked out branch: {response.current}")
for branch in response.branches:
print(f"Branch: {branch}")
```
```typescript
// Get repository status
const status = await sandbox.git.status("workspace/repo");
console.log(`Current branch: ${status.currentBranch}`);
console.log(`Upstream: ${status.upstream}`);
console.log(`Detached HEAD: ${status.detached}`);
console.log(`Commits ahead: ${status.ahead}`);
console.log(`Commits behind: ${status.behind}`);
status.fileStatus.forEach(file => {
console.log(`File: ${file.name}`);
});
// List branches
const response = await sandbox.git.branches("workspace/repo");
console.log(`Checked out branch: ${response.current}`);
response.branches.forEach(branch => {
console.log(`Branch: ${branch}`);
});
```
```ruby
# Get repository status
status = sandbox.git.status('workspace/repo')
puts "Current branch: #{status.current_branch}"
puts "Upstream: #{status.upstream}"
puts "Detached HEAD: #{status.detached}"
puts "Commits ahead: #{status.ahead}"
puts "Commits behind: #{status.behind}"
status.file_status.each do |file|
puts "File: #{file.name}"
end
# List branches
response = sandbox.git.branches('workspace/repo')
puts "Checked out branch: #{response.current}"
response.branches.each do |branch|
puts "Branch: #{branch}"
end
```
```go
// Get repository status
status, err := sandbox.Git.Status(ctx, "workspace/repo")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Current branch: %s\n", status.CurrentBranch)
fmt.Printf("Commits ahead: %d\n", status.Ahead)
fmt.Printf("Commits behind: %d\n", status.Behind)
for _, file := range status.FileStatus {
fmt.Printf("File: %s\n", file.Path)
}
// List branches
branches, err := sandbox.Git.Branches(ctx, "workspace/repo")
if err != nil {
log.Fatal(err)
}
for _, branch := range branches {
fmt.Printf("Branch: %s\n", branch)
}
```
```java
import io.daytona.sdk.model.GitStatus;
import java.util.List;
// Get repository status
GitStatus status = sandbox.git.status("workspace/repo");
System.out.println("Current branch: " + status.getCurrentBranch());
System.out.println("Commits ahead: " + status.getAhead());
System.out.println("Commits behind: " + status.getBehind());
for (GitStatus.FileStatus file : status.getFileStatus()) {
System.out.println("File: " + file.getName());
}
// List branches
Object rawBranches = sandbox.git.branches("workspace/repo").get("branches");
if (rawBranches instanceof List> branchList) {
for (Object branch : branchList) {
System.out.println("Branch: " + branch);
}
}
```
```bash
# Get repository status
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/git/status?path=workspace/repo'
# List branches
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/git/branches?path=workspace/repo'
```
## Branch operations
Daytona provides methods to manage branches in Git repositories. You can create, switch, and delete branches. Checkout accepts a branch name or a commit SHA.
### Create branches
Create a new branch by providing the path to the repository and the name of the new branch.
```python
# Create a new branch
sandbox.git.create_branch("workspace/repo", "new-feature")
```
```typescript
// Create new branch
await sandbox.git.createBranch('workspace/repo', 'new-feature');
```
```ruby
# Create a new branch
sandbox.git.create_branch('workspace/repo', 'new-feature')
```
```go
// Create a new branch
err := sandbox.Git.CreateBranch(ctx, "workspace/repo", "new-feature")
if err != nil {
log.Fatal(err)
}
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/git/branches' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"name": "",
"path": ""
}'
```
### Checkout branches or commits
Checkout a branch or commit by providing the path to the repository and the name of the branch or commit SHA. Pass a commit SHA to enter detached HEAD state.
```python
# Checkout a branch
sandbox.git.checkout_branch("workspace/repo", "feature-branch")
# Checkout a commit (detached HEAD)
sandbox.git.checkout_branch("workspace/repo", "abc123def456")
```
```typescript
// Checkout a branch
await sandbox.git.checkoutBranch('workspace/repo', 'feature-branch');
// Checkout a commit (detached HEAD)
await sandbox.git.checkoutBranch('workspace/repo', 'abc123def456');
```
```ruby
# Checkout a branch
sandbox.git.checkout_branch('workspace/repo', 'feature-branch')
# Checkout a commit (detached HEAD)
sandbox.git.checkout_branch('workspace/repo', 'abc123def456')
```
```go
// Checkout a branch
err := sandbox.Git.Checkout(ctx, "workspace/repo", "feature-branch")
if err != nil {
log.Fatal(err)
}
// Checkout a commit (detached HEAD)
err = sandbox.Git.Checkout(ctx, "workspace/repo", "abc123def456")
if err != nil {
log.Fatal(err)
}
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/git/checkout' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"branch": "",
"path": ""
}'
```
### Delete branches
Delete a branch by providing the path to the repository and the name of the branch.
```python
# Delete a branch
sandbox.git.delete_branch("workspace/repo", "old-feature")
```
```typescript
// Delete a branch
await sandbox.git.deleteBranch('workspace/repo', 'old-feature');
```
```ruby
# Delete a branch
sandbox.git.delete_branch('workspace/repo', 'old-feature')
```
```go
// Delete a branch
err := sandbox.Git.DeleteBranch(ctx, "workspace/repo", "old-feature")
if err != nil {
log.Fatal(err)
}
// Force delete an unmerged branch
err = sandbox.Git.DeleteBranch(ctx, "workspace/repo", "old-feature",
options.WithForce(true),
)
if err != nil {
log.Fatal(err)
}
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/git/branches' \
--request DELETE \
--header 'Content-Type: application/json' \
--data '{
"name": "",
"path": ""
}'
```
## Stage changes
Stage specific files, all changes, or the whole repository by providing the path to the repository and the files to stage.
```python
# Stage a single file
sandbox.git.add("workspace/repo", ["file.txt"])
# Stage multiple files
sandbox.git.add("workspace/repo", [
"src/main.py",
"tests/test_main.py",
"README.md"
])
```
```typescript
// Stage a single file
await sandbox.git.add('workspace/repo', ['file.txt']);
// Stage multiple files
await sandbox.git.add('workspace/repo', [
'src/main.ts',
'tests/main.test.ts',
'README.md',
]);
// Stage whole repository
await sandbox.git.add('workspace/repo', ['.']);
```
```ruby
# Stage a single file
sandbox.git.add('workspace/repo', ['file.txt'])
```
```go
// Stage a single file
err := sandbox.Git.Add(ctx, "workspace/repo", []string{"file.txt"})
if err != nil {
log.Fatal(err)
}
// Stage multiple files
err = sandbox.Git.Add(ctx, "workspace/repo", []string{
"src/main.py",
"tests/test_main.py",
"README.md",
})
if err != nil {
log.Fatal(err)
}
// Stage whole repository
err = sandbox.Git.Add(ctx, "workspace/repo", []string{"."})
if err != nil {
log.Fatal(err)
}
```
```java
import java.util.List;
// Stage a single file
sandbox.git.add("workspace/repo", List.of("file.txt"));
// Stage multiple files
sandbox.git.add(
"workspace/repo",
List.of("src/main.py", "tests/test_main.py", "README.md")
);
// Stage whole repository
sandbox.git.add("workspace/repo", List.of("."));
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/git/add' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"files": [
""
],
"path": ""
}'
```
## Commit changes
Commit changes by providing the path to the repository, the message, author, and email.
```python
# Stage and commit changes
sandbox.git.add("workspace/repo", ["README.md"])
sandbox.git.commit(
path="workspace/repo",
message="Update documentation",
author="John Doe",
email="john@example.com",
allow_empty=True
)
```
```typescript
// Stage and commit changes
await sandbox.git.add('workspace/repo', ['README.md']);
await sandbox.git.commit(
'workspace/repo',
'Update documentation',
'John Doe',
'john@example.com',
true
);
```
```ruby
# Stage and commit changes
sandbox.git.add('workspace/repo', ['README.md'])
sandbox.git.commit('workspace/repo', 'Update documentation', 'John Doe', 'john@example.com', true)
```
```go
// Stage and commit changes
err := sandbox.Git.Add(ctx, "workspace/repo", []string{"README.md"})
if err != nil {
log.Fatal(err)
}
response, err := sandbox.Git.Commit(ctx, "workspace/repo",
"Update documentation",
"John Doe",
"john@example.com",
options.WithAllowEmpty(true),
)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Commit SHA: %s\n", response.SHA)
```
```java
import io.daytona.sdk.model.GitCommitResponse;
import java.util.List;
// Stage and commit changes
sandbox.git.add("workspace/repo", List.of("README.md"));
GitCommitResponse response = sandbox.git.commit(
"workspace/repo",
"Update documentation",
"John Doe",
"john@example.com"
);
System.out.println("Commit hash: " + response.getHash());
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/git/commit' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"allow_empty": true,
"author": "",
"email": "",
"message": "",
"path": ""
}'
```
## Remote operations
Daytona provides methods to work with remote repositories in Git. You can push and pull changes from remote repositories.
### Push changes
Push changes to a remote repository by providing the path to the repository and the username and password to authenticate.
```python
# Push without authentication (for public repos or SSH)
sandbox.git.push("workspace/repo")
# Push with authentication
sandbox.git.push(
path="workspace/repo",
username="user",
password="github_token"
)
```
```typescript
// Push to a public repository
await sandbox.git.push('workspace/repo');
// Push to a private repository
await sandbox.git.push(
'workspace/repo',
'user',
'token'
);
```
```ruby
# Push without authentication (for public repos or SSH)
sandbox.git.push('workspace/repo')
# Push with authentication
sandbox.git.push(
path: 'workspace/repo',
username: 'user',
password: 'github_token'
)
```
```go
// Push without authentication (for public repos or SSH)
err := sandbox.Git.Push(ctx, "workspace/repo")
if err != nil {
log.Fatal(err)
}
// Push with authentication
err = sandbox.Git.Push(ctx, "workspace/repo",
options.WithPushUsername("user"),
options.WithPushPassword("github_token"),
)
if err != nil {
log.Fatal(err)
}
```
```java
// Push without authentication (for public repos or SSH)
sandbox.git.push("workspace/repo");
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/git/push' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"password": "",
"path": "",
"username": ""
}'
```
The `branch`, `remote`, and `set_upstream` parameters are available on the toolbox API only.
### Pull changes
Pull changes from a remote repository by providing the path to the repository and the username and password to authenticate.
```python
# Pull without authentication
sandbox.git.pull("workspace/repo")
# Pull with authentication
sandbox.git.pull(
path="workspace/repo",
username="user",
password="github_token"
)
```
```typescript
// Pull from a public repository
await sandbox.git.pull('workspace/repo');
// Pull from a private repository
await sandbox.git.pull(
'workspace/repo',
'user',
'token'
);
```
```ruby
# Pull without authentication
sandbox.git.pull('workspace/repo')
# Pull with authentication
sandbox.git.pull(
path: 'workspace/repo',
username: 'user',
password: 'github_token'
)
```
```go
// Pull without authentication
err := sandbox.Git.Pull(ctx, "workspace/repo")
if err != nil {
log.Fatal(err)
}
// Pull with authentication
err = sandbox.Git.Pull(ctx, "workspace/repo",
options.WithPullUsername("user"),
options.WithPullPassword("github_token"),
)
if err != nil {
log.Fatal(err)
}
```
```java
// Pull without authentication
sandbox.git.pull("workspace/repo");
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/git/pull' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"password": "",
"path": "",
"username": ""
}'
```
The `branch` and `remote` parameters are available on the toolbox API only.
## Advanced operations
Daytona provides additional Git operations through the [Toolbox API](https://www.daytona.io/docs/en/tools/api.md#daytona-toolbox).
### Initialize a repository
Initialize a new Git repository by providing the path to the repository and the name of the first branch. Set `bare` to create a repository without a working tree.
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/git/init' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"bare": false,
"initial_branch": "main",
"path": "workspace/repo"
}'
```
### Reset changes
Reset the current HEAD to the specified state by providing the path to the repository, the mode and the target revision to reset to. Pass `files` to constrain the reset to specific paths.
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/git/reset' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"files": [],
"mode": "mixed",
"path": "workspace/repo",
"target": "HEAD~1"
}'
```
### Restore files
Restore working tree files or unstage changes by providing the path to the repository, the files to restore, the source revision, and whether to restore from the staged index or working tree.
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/git/restore' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"files": ["src/main.py"],
"path": "workspace/repo",
"source": "",
"staged": false,
"worktree": true
}'
```
### Get commit history
Return the commit log for a repository.
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/git/history?path=workspace/repo'
```
### Manage remotes
Add a remote or get the URL of a remote by providing the path to the repository, the name of the remote, and the URL of the remote.
1. Set **`fetch`** to **`true`** to fetch from the remote immediately after adding it
2. Set **`overwrite`** to **`true`** to replace an existing remote with the same name
```python
# Add a remote
sandbox.git.remote_add("workspace/repo", "origin", "https://github.com/user/repo.git")
# Add a remote, fetch from it, and replace an existing remote with the same name
sandbox.git.remote_add(
path="workspace/repo",
name="upstream",
url="https://github.com/other/repo.git",
fetch=True,
overwrite=True
)
# Get the URL of a remote (None when it does not exist)
url = sandbox.git.remote_get("workspace/repo", "origin")
```
```typescript
// Add a remote
await sandbox.git.remoteAdd('workspace/repo', 'origin', 'https://github.com/user/repo.git');
// Add a remote, fetch from it, and replace an existing remote with the same name
await sandbox.git.remoteAdd(
'workspace/repo',
'upstream',
'https://github.com/other/repo.git',
true,
true
);
// Get the URL of a remote (undefined when it does not exist)
const url = await sandbox.git.remoteGet('workspace/repo', 'origin');
```
```ruby
# Add a remote
sandbox.git.remote_add('workspace/repo', 'origin', 'https://github.com/user/repo.git')
# Add a remote, fetch from it, and replace an existing remote with the same name
sandbox.git.remote_add(
'workspace/repo',
'upstream',
'https://github.com/other/repo.git',
fetch: true,
overwrite: true
)
# Get the URL of a remote (nil when it does not exist)
url = sandbox.git.remote_get('workspace/repo', 'origin')
```
```go
// Add a remote
err := sandbox.Git.RemoteAdd(ctx, "workspace/repo", "origin", "https://github.com/user/repo.git")
if err != nil {
log.Fatal(err)
}
// Add a remote, fetch from it, and replace an existing remote with the same name
err = sandbox.Git.RemoteAdd(ctx, "workspace/repo", "upstream", "https://github.com/other/repo.git",
options.WithRemoteFetch(true),
options.WithRemoteOverwrite(true),
)
if err != nil {
log.Fatal(err)
}
// Get the URL of a remote (empty string when it does not exist)
url, err := sandbox.Git.RemoteGet(ctx, "workspace/repo", "origin")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Remote URL: %s\n", url)
```
```java
// Add a remote
sandbox.git.remoteAdd("workspace/repo", "origin", "https://github.com/user/repo.git");
// Add a remote, fetch from it, and replace an existing remote with the same name
sandbox.git.remoteAdd(
"workspace/repo",
"upstream",
"https://github.com/other/repo.git",
true,
true
);
// Get the URL of a remote (null when it does not exist)
String url = sandbox.git.remoteGet("workspace/repo", "origin");
```
```bash
# List remotes
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/git/remotes?path=workspace/repo'
# Add a remote
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/git/remotes' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"fetch": false,
"name": "origin",
"overwrite": false,
"path": "workspace/repo",
"url": "https://github.com/user/repo.git"
}'
```
### Configure Git
Read or write Git config values by providing the key and the value. Set **`scope`** to **`local`** together with the repository **`path`** to configure a single repository.
- **Scope**: **`global`** (default), **`local`**, or **`system`**
```python
# Set a config value at the global scope
sandbox.git.set_config("core.editor", "vim")
# Set a config value for a single repository
sandbox.git.set_config(
key="core.editor",
value="vim",
scope="local",
path="workspace/repo"
)
# Get a config value (None when unset)
editor = sandbox.git.get_config("core.editor")
```
```typescript
// Set a config value at the global scope
await sandbox.git.setConfig('core.editor', 'vim');
// Set a config value for a single repository
await sandbox.git.setConfig('core.editor', 'vim', 'local', 'workspace/repo');
// Get a config value (undefined when unset)
const editor = await sandbox.git.getConfig('core.editor');
```
```ruby
# Set a config value at the global scope
sandbox.git.set_config('core.editor', 'vim')
# Set a config value for a single repository
sandbox.git.set_config('core.editor', 'vim', scope: 'local', path: 'workspace/repo')
# Get a config value (nil when unset)
editor = sandbox.git.get_config('core.editor')
```
```go
// Set a config value at the global scope
err := sandbox.Git.SetConfig(ctx, "core.editor", "vim")
if err != nil {
log.Fatal(err)
}
// Set a config value for a single repository
err = sandbox.Git.SetConfig(ctx, "core.editor", "vim",
options.WithConfigScope("local"),
options.WithConfigPath("workspace/repo"),
)
if err != nil {
log.Fatal(err)
}
// Get a config value
editor, err := sandbox.Git.GetConfig(ctx, "core.editor")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Editor: %s\n", editor)
```
```java
// Set a config value at the global scope
sandbox.git.setConfig("core.editor", "vim");
// Set a config value for a single repository
sandbox.git.setConfig("core.editor", "vim", "local", "workspace/repo");
// Get a config value (null when unset)
String editor = sandbox.git.getConfig("core.editor");
```
```bash
# Get a config value
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/git/config?key=user.name&scope=global'
# Set a config value
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/git/config' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"key": "core.editor",
"path": "",
"scope": "global",
"value": "vim"
}'
```
### Configure user
Configure the Git user name and email by providing the `name` and `email` values. Set `scope` to `local` together with the repository `path` to configure the user for a single repository.
- **Scope**: **`global`** (default), **`local`**
```python
# Configure the global Git user
sandbox.git.configure_user("John Doe", "john@example.com")
# Configure the user for a single repository
sandbox.git.configure_user(
name="John Doe",
email="john@example.com",
scope="local",
path="workspace/repo"
)
```
```typescript
// Configure the global Git user
await sandbox.git.configureUser('John Doe', 'john@example.com');
// Configure the user for a single repository
await sandbox.git.configureUser(
'John Doe',
'john@example.com',
'local',
'workspace/repo'
);
```
```ruby
# Configure the global Git user
sandbox.git.configure_user('John Doe', 'john@example.com')
# Configure the user for a single repository
sandbox.git.configure_user(
'John Doe',
'john@example.com',
scope: 'local',
path: 'workspace/repo'
)
```
```go
// Configure the global Git user
err := sandbox.Git.ConfigureUser(ctx, "John Doe", "john@example.com")
if err != nil {
log.Fatal(err)
}
// Configure the user for a single repository
err = sandbox.Git.ConfigureUser(ctx, "John Doe", "john@example.com",
options.WithConfigScope("local"),
options.WithConfigPath("workspace/repo"),
)
if err != nil {
log.Fatal(err)
}
```
```java
// Configure the global Git user
sandbox.git.configureUser("John Doe", "john@example.com");
// Configure the user for a single repository
sandbox.git.configureUser(
"John Doe",
"john@example.com",
"local",
"workspace/repo"
);
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/git/config/user' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"email": "john@example.com",
"name": "John Doe",
"path": "",
"scope": "global"
}'
```
### Authenticate credentials
Persist Git credentials globally via the credential store by providing the host, protocol, username, and password. Credentials are stored in plaintext on disk.
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/git/credentials' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"host": "github.com",
"password": "personal_access_token",
"protocol": "https",
"username": "git"
}'
```
# Language Server Protocol
Daytona provides Language Server Protocol (LSP) support through sandbox instances. This enables code completion, document symbols, and workspace symbol search inside sandboxes.
## Workflow
Follow this order when using LSP in a sandbox:
1. Create an LSP server instance with **`create_lsp_server`** or **`createLspServer`**
2. Call **`start()`** to initialize the language server; LSP methods fail until the server is started
3. Call **`didOpen()`** on a file before requesting completions or document symbols for that file
4. Use completions, document symbols, or sandbox symbols
5. Call **`didClose()`** when you finish with a file
6. Call **`stop()`** when the LSP server is no longer needed
## Create LSP servers
Create an LSP server instance by providing the language ID and the path to the project.
- **Python**: **`LspLanguageId.PYTHON`**
- **TypeScript and JavaScript**: **`LspLanguageId.TYPESCRIPT`**
- **Custom**: Install the language server for your target language
```python
from daytona import Daytona, LspLanguageId
# Create Sandbox
daytona = Daytona()
sandbox = daytona.create()
# Create LSP server for Python
lsp_server = sandbox.create_lsp_server(
language_id=LspLanguageId.PYTHON,
path_to_project="workspace/project"
)
```
```typescript
import { Daytona, LspLanguageId } from '@daytona/sdk'
// Create sandbox
const daytona = new Daytona()
const sandbox = await daytona.create()
// Create LSP server for TypeScript
const lspServer = await sandbox.createLspServer(
LspLanguageId.TYPESCRIPT,
'workspace/project'
)
```
```ruby
require 'daytona'
# Create Sandbox
daytona = Daytona::Daytona.new
sandbox = daytona.create
# Create LSP server for Python
lsp_server = sandbox.create_lsp_server(
language_id: Daytona::LspServer::Language::PYTHON,
path_to_project: 'workspace/project'
)
```
```go
// Create sandbox
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
sandbox, err := client.Create(ctx, nil)
if err != nil {
log.Fatal(err)
}
// Create LSP server for Python
lsp := sandbox.CreateLspServer(types.LspLanguagePython, "workspace/project")
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.LspServer;
import io.daytona.sdk.Sandbox;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
Sandbox sandbox = daytona.create();
LspServer lspServer = sandbox.createLspServer(
"python",
"workspace/project");
}
}
}
```
## Start LSP servers
Start an LSP server by calling **`start()`** before any other LSP operation.
```python
lsp = sandbox.create_lsp_server(
language_id=LspLanguageId.PYTHON,
path_to_project="workspace/project"
)
lsp.start() # Initialize the server
# Now ready for LSP operations
```
```typescript
const lsp = await sandbox.createLspServer(
LspLanguageId.TYPESCRIPT,
'workspace/project'
)
await lsp.start() // Initialize the server
// Now ready for LSP operations
```
```ruby
lsp = sandbox.create_lsp_server(
language_id: Daytona::LspServer::Language::PYTHON,
path_to_project: 'workspace/project'
)
lsp.start # Initialize the server
# Now ready for LSP operations
```
```go
lsp := sandbox.CreateLspServer(types.LspLanguagePython, "workspace/project")
err := lsp.Start(ctx) // Initialize the server
if err != nil {
log.Fatal(err)
}
// Now ready for LSP operations
```
```java
LspServer lsp = sandbox.createLspServer("typescript", "workspace/project");
lsp.start("typescript", "workspace/project");
// Now ready for LSP operations
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/lsp/start' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"languageId": "python",
"pathToProject": "workspace/project"
}'
```
## Stop LSP servers
Stop an LSP server by calling **`stop()`** when the LSP server is no longer needed.
```python
# When done with LSP features
lsp.stop() # Clean up resources
```
```typescript
// When done with LSP features
await lsp.stop() // Clean up resources
```
```ruby
# When done with LSP features
lsp.stop # Clean up resources
```
```go
// When done with LSP features
err := lsp.Stop(ctx) // Clean up resources
if err != nil {
log.Fatal(err)
}
```
```java
// When done with LSP features
lsp.stop("typescript", "workspace/project"); // Clean up resources
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/lsp/stop' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"languageId": "python",
"pathToProject": "workspace/project"
}'
```
## Code completions
Get code completions for a specific position in a file by providing the file path and position.
- Position values are zero-based (`line: 0` is the first line)
- Call **`didOpen()`** on the file before requesting completions
```python
completions = lsp_server.completions(
path="workspace/project/main.py",
position={"line": 10, "character": 15}
)
print(f"Completions: {completions}")
```
```typescript
const completions = await lspServer.completions('workspace/project/main.ts', {
line: 10,
character: 15,
})
console.log('Completions:', completions)
```
```ruby
completions = lsp_server.completions(
path: 'workspace/project/main.py',
position: { line: 10, character: 15 }
)
puts "Completions: #{completions}"
```
```go
completions, err := lsp.Completions(ctx, "workspace/project/main.py",
types.Position{Line: 10, Character: 15},
)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Completions: %v\n", completions)
```
```java
var completions = lsp.completions(
"typescript",
"workspace/project",
"workspace/project/Main.java",
10,
15);
System.out.println("Completions: " + completions);
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/lsp/completions' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"context": {
"triggerCharacter": "",
"triggerKind": 1
},
"languageId": "python",
"pathToProject": "workspace/project",
"position": {
"character": 15,
"line": 10
},
"uri": "workspace/project/main.py"
}'
```
## File notifications
Daytona provides methods to notify the LSP server when files are opened or closed. This enables completion and symbol tracking for the specified files.
### Open file
Notify the language server that a file has been opened for editing by providing the path to the file. The server reads the file contents from disk at open time.
```python
# Notify server that a file is open
lsp_server.did_open("workspace/project/main.py")
```
```typescript
// Notify server that a file is open
await lspServer.didOpen('workspace/project/main.ts')
```
```ruby
# Notify server that a file is open
lsp_server.did_open('workspace/project/main.py')
```
```go
// Notify server that a file is open
err := lsp.DidOpen(ctx, "workspace/project/main.py")
if err != nil {
log.Fatal(err)
}
```
```java
// Notify server that a file is open
lsp.didOpen("typescript", "workspace/project", "workspace/project/Main.java");
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/lsp/did-open' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"languageId": "python",
"pathToProject": "workspace/project",
"uri": "workspace/project/main.py"
}'
```
### Close file
Notify the language server that a file has been closed by providing the path to the file. This allows the server to clean up resources associated with that file.
```python
# Notify server that a file is closed
lsp_server.did_close("workspace/project/main.py")
```
```typescript
// Notify server that a file is closed
await lspServer.didClose('workspace/project/main.ts')
```
```ruby
# Notify server that a file is closed
lsp_server.did_close('workspace/project/main.py')
```
```go
// Notify server that a file is closed
err := lsp.DidClose(ctx, "workspace/project/main.py")
if err != nil {
log.Fatal(err)
}
```
```java
// Notify server that a file is closed
lsp.didClose("typescript", "workspace/project", "workspace/project/Main.java");
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/lsp/did-close' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"languageId": "python",
"pathToProject": "workspace/project",
"uri": "workspace/project/main.py"
}'
```
## Document symbols
Retrieve symbols (functions, classes, variables, etc.) from a document by providing the path to the file.
```python
symbols = lsp_server.document_symbols("workspace/project/main.py")
for symbol in symbols:
print(f"Symbol: {symbol.name}, Kind: {symbol.kind}")
```
```typescript
const symbols = await lspServer.documentSymbols('workspace/project/main.ts')
symbols.forEach((symbol) => {
console.log(`Symbol: ${symbol.name}, Kind: ${symbol.kind}`)
})
```
```ruby
symbols = lsp_server.document_symbols('workspace/project/main.py')
symbols.each do |symbol|
puts "Symbol: #{symbol.name}, Kind: #{symbol.kind}"
end
```
```go
symbols, err := lsp.DocumentSymbols(ctx, "workspace/project/main.py")
if err != nil {
log.Fatal(err)
}
for _, symbol := range symbols {
fmt.Printf("Symbol: %v\n", symbol)
}
```
```java
var symbols = lsp.documentSymbols(
"typescript",
"workspace/project",
"workspace/project/Main.java");
for (var symbol : symbols) {
System.out.println("Symbol: " + symbol.getName() + ", Kind: " + symbol.getKind());
}
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/lsp/document-symbols?languageId=python&pathToProject=workspace/project&uri=workspace/project/main.py'
```
## Sandbox symbols
Search for symbols across all files in the sandbox by providing the query and the language ID. The Java SDK uses **`workspaceSymbols()`** instead of **`sandboxSymbols()`**.
```python
symbols = lsp_server.sandbox_symbols("MyClass")
for symbol in symbols:
print(f"Found: {symbol.name} at {symbol.location}")
```
```typescript
const symbols = await lspServer.sandboxSymbols('MyClass')
symbols.forEach((symbol) => {
console.log(`Found: ${symbol.name} at ${symbol.location}`)
})
```
```ruby
symbols = lsp_server.sandbox_symbols('MyClass')
symbols.each do |symbol|
puts "Found: #{symbol.name} at #{symbol.location}"
end
```
```go
symbols, err := lsp.SandboxSymbols(ctx, "MyClass")
if err != nil {
log.Fatal(err)
}
for _, symbol := range symbols {
fmt.Printf("Found: %v\n", symbol)
}
```
```java
var symbols = lsp.workspaceSymbols("MyClass", "typescript", "workspace/project");
for (var symbol : symbols) {
System.out.println("Found: " + symbol.getName() + " at " + symbol.getLocation());
}
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/lsp/workspacesymbols?query=MyClass&languageId=python&pathToProject=workspace/project'
```
# Process and Code Execution
Daytona provides process and code execution capabilities through the `process` module in sandboxes. For interactive terminal sessions, see [Pseudo Terminal (PTY)](https://www.daytona.io/docs/en/pty.md). For real-time log streaming from long-running session commands, see [Log Streaming](https://www.daytona.io/docs/en/log-streaming.md).
## Code execution
Daytona provides methods to execute code in sandboxes. You can run code snippets in multiple languages with support for both stateless execution and stateful interpretation with persistent contexts.
- [Run code (stateless)](#run-code-stateless): run independent code snippets where each execution starts from a clean interpreter state; inherits the sandbox language that you choose at [sandbox creation](https://www.daytona.io/docs/en/sandboxes.md#create-sandboxes). Supports Python, JavaScript, and TypeScript.
- [Run code (stateful)](#run-code-stateful): run Python code in a persistent interpreter context with variables, imports, and state to carry across executions; available in every SDK.
### Run code (stateless)
Run code snippets in sandboxes using stateless execution. Each invocation starts from a clean interpreter, making it ideal for independent code snippets.
```python
from daytona import CodeRunParams
# Run Python code
response = sandbox.process.code_run('''
def greet(name):
return f"Hello, {name}!"
print(greet("Daytona"))
''')
print(response.result)
# Run code with argv and environment variables
response = sandbox.process.code_run(
'import sys; print(f"Hello, {sys.argv[1]}!")',
params=CodeRunParams(argv=["Daytona"], env={"FOO": "BAR"}),
timeout=5,
)
print(response.result)
```
```typescript
// Run TypeScript code
let response = await sandbox.process.codeRun(`
function greet(name: string): string {
return \`Hello, \${name}!\`;
}
console.log(greet("Daytona"));
`);
console.log(response.result);
// Run code with argv and environment variables
response = await sandbox.process.codeRun(
`
console.log(\`Hello, \${process.argv[2]}!\`);
console.log(\`FOO: \${process.env.FOO}\`);
`,
{
argv: ["Daytona"],
env: { FOO: "BAR" }
}
);
console.log(response.result);
// Run code with timeout (5 seconds)
response = await sandbox.process.codeRun(
'setTimeout(() => console.log("Done"), 2000);',
undefined,
5
);
console.log(response.result);
```
```ruby
# Run Python code
response = sandbox.process.code_run(code: <<~PYTHON)
def greet(name):
return f"Hello, {name}!"
print(greet("Daytona"))
PYTHON
puts response.result
```
```go
// Run Python code (language defaults to sandbox language)
result, err := sandbox.Process.CodeRun(ctx, `
def greet(name):
return f"Hello, {name}!"
print(greet("Daytona"))
`)
if err != nil {
log.Fatal(err)
}
fmt.Println(result.Result)
// Run code with environment variables
result, err = sandbox.Process.CodeRun(ctx, `import os; print(os.environ.get("FOO"))`,
options.WithCodeRunParams(types.CodeRunParams{
Env: map[string]string{"FOO": "BAR"},
}),
)
if err != nil {
log.Fatal(err)
}
fmt.Println(result.Result)
// Run code with timeout (5 seconds)
result, err = sandbox.Process.CodeRun(ctx, `import time; time.sleep(2); print("Done")`,
options.WithCodeRunTimeout(5*time.Second),
)
if err != nil {
log.Fatal(err)
}
fmt.Println(result.Result)
```
```java
import io.daytona.sdk.model.ExecuteResponse;
import java.util.Map;
// Run code (stateless; language matches the sandbox image)
ExecuteResponse response = sandbox.process.codeRun(
"""
def greet(name):
return f"Hello, {name}!"
print(greet("Daytona"))
"""
);
System.out.println(response.getResult());
// Run code with environment variables and timeout (seconds)
response = sandbox.process.codeRun(
"import os; print('FOO:', os.environ.get('FOO'))",
Map.of("FOO", "BAR"),
null
);
System.out.println(response.getResult());
response = sandbox.process.codeRun(
"import time; time.sleep(2); print(\"Done\")",
null,
5
);
System.out.println(response.getResult());
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/code-run' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"code": "def greet(name):\n return f\"Hello, {name}!\"\n\nprint(greet(\"Daytona\"))",
"language": "python",
"envs": {
"FOO": "BAR"
},
"timeout": 5
}'
```
#### Artifacts
Stateless `code_run` responses can include an `artifacts` field. When your code produces matplotlib charts, the SDK strips chart metadata from `result` and returns it in the `artifacts.charts` field.
```python
code = '''
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 10, 30)
plt.plot(x, np.sin(x))
plt.title("Sine wave")
plt.show()
'''
response = sandbox.process.code_run(code)
if response.artifacts and response.artifacts.charts:
chart = response.artifacts.charts[0]
print(chart.type, chart.title)
```
```typescript
const response = await sandbox.process.codeRun(`
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 10, 30)
plt.plot(x, np.sin(x))
plt.title("Sine wave")
plt.show()
`)
if (response.artifacts?.charts?.length) {
const chart = response.artifacts.charts[0]
console.log(chart.type, chart.title)
}
```
### Run code (stateful)
Run Python code with persistent state using the code interpreter. You can maintain variables and imports between calls, create isolated contexts with optional working directories, list active contexts, and stream stdout, stderr, and errors via callbacks.
```python
from daytona import Daytona, OutputMessage, ExecutionError
def handle_stdout(message: OutputMessage):
print(f"[STDOUT] {message.output}")
def handle_stderr(message: OutputMessage):
print(f"[STDERR] {message.output}")
def handle_error(error: ExecutionError):
print(f"[ERROR] {error.name}: {error.value}")
daytona = Daytona()
sandbox = daytona.create()
# Shared default context
sandbox.code_interpreter.run_code(
"counter = 1\nprint(f'Counter initialized at {counter}')",
on_stdout=handle_stdout,
on_stderr=handle_stderr,
on_error=handle_error,
timeout=60,
)
# Isolated context with working directory
ctx = sandbox.code_interpreter.create_context(cwd="workspace/src")
try:
sandbox.code_interpreter.run_code(
"value = 'stored in ctx'",
context=ctx,
envs={"DEBUG": "1"},
)
sandbox.code_interpreter.run_code(
"print(value)",
context=ctx,
on_stdout=handle_stdout,
)
finally:
sandbox.code_interpreter.delete_context(ctx)
# List user-created contexts
for context in sandbox.code_interpreter.list_contexts():
print(context.id, context.cwd)
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
async function main() {
const sandbox = await daytona.create()
// Shared default context
await sandbox.codeInterpreter.runCode(
`
counter = 1
print(f'Counter initialized at {counter}')
`,
{
onStdout: (msg) => process.stdout.write(`[STDOUT] ${msg.output}`),
onStderr: (msg) => process.stdout.write(`[STDERR] ${msg.output}`),
timeout: 60,
},
)
// Isolated context with working directory
const ctx = await sandbox.codeInterpreter.createContext('workspace/src')
try {
await sandbox.codeInterpreter.runCode(
`value = 'stored in ctx'`,
{ context: ctx, envs: { DEBUG: '1' } },
)
await sandbox.codeInterpreter.runCode(
`print(value)`,
{ context: ctx, onStdout: (msg) => process.stdout.write(`[STDOUT] ${msg.output}`) },
)
} finally {
await sandbox.codeInterpreter.deleteContext(ctx)
}
// List user-created contexts
const contexts = await sandbox.codeInterpreter.listContexts()
for (const context of contexts) {
console.log(context.id, context.cwd)
}
}
main()
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
sandbox = daytona.create
# Shared default context
sandbox.code_interpreter.run_code(
<<~PYTHON,
counter = 1
print(f'Counter initialized at {counter}')
PYTHON
on_stdout: ->(msg) { print "[STDOUT] #{msg.output}" }
)
# Isolated context
ctx = sandbox.code_interpreter.create_context
begin
sandbox.code_interpreter.run_code("value = 'stored in ctx'", context: ctx)
sandbox.code_interpreter.run_code(
"print(value)",
context: ctx,
on_stdout: ->(msg) { print "[STDOUT] #{msg.output}" }
)
ensure
sandbox.code_interpreter.delete_context(ctx)
end
```
```go
// Shared default context
channels, err := sandbox.CodeInterpreter.RunCode(ctx,
"counter = 1\nprint(f'Counter initialized at {counter}')",
)
if err != nil {
log.Fatal(err)
}
for msg := range channels.Stdout {
fmt.Printf("[STDOUT] %s\n", msg.Text)
}
// Isolated context
cwd := "workspace/src"
ctxInfo, err := sandbox.CodeInterpreter.CreateContext(ctx, &cwd)
if err != nil {
log.Fatal(err)
}
contextID := ctxInfo["id"].(string)
channels, err = sandbox.CodeInterpreter.RunCode(ctx,
"value = 'stored in ctx'",
options.WithCustomContext(contextID),
options.WithEnv(map[string]string{"DEBUG": "1"}),
)
if err != nil {
log.Fatal(err)
}
for msg := range channels.Stdout {
fmt.Printf("[STDOUT] %s\n", msg.Text)
}
// List user-created contexts
contexts, err := sandbox.CodeInterpreter.ListContexts(ctx)
if err != nil {
log.Fatal(err)
}
for _, context := range contexts {
fmt.Println(context["id"], context["cwd"])
}
// Clean up context
err = sandbox.CodeInterpreter.DeleteContext(ctx, contextID)
if err != nil {
log.Fatal(err)
}
```
```java
import io.daytona.sdk.RunCodeOptions;
import io.daytona.toolbox.client.model.InterpreterContext;
// Default interpreter context (Python)
sandbox.codeInterpreter.runCode(
"""
counter = 1
print(f'Counter initialized at {counter}')
""",
new RunCodeOptions()
.setOnStdout(chunk -> System.out.print("[STDOUT] " + chunk))
.setOnStderr(chunk -> System.out.print("[STDERR] " + chunk))
.setTimeout(60)
);
// Context management
InterpreterContext ctx = sandbox.codeInterpreter.createContext("workspace/src");
sandbox.codeInterpreter.deleteContext(ctx.getId());
for (InterpreterContext context : sandbox.codeInterpreter.listContexts()) {
System.out.println(context.getId() + " " + context.getCwd());
}
```
```bash
# Create context
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/interpreter/context' \
--request POST \
--header 'Content-Type: application/json' \
--data '{"cwd": "workspace/src"}'
# List user-created contexts
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/interpreter/context'
# Run code in a context (ExecuteInterpreterCode, WebSocket endpoint)
# The GET request is upgraded to a WebSocket connection:
# wss://proxy.app.daytona.io/toolbox/{sandboxId}/process/interpreter/execute
# Send a JSON message with the code to run. "contextId" defaults to the default
# context, "timeout" is in seconds (default 600, 0 disables it), and "envs" sets
# environment variables for the execution:
# {
# "code": "counter = 1\nprint(f\"Counter initialized at {counter}\")",
# "contextId": "your-context-id",
# "timeout": 600,
# "envs": {"MY_VAR": "value"}
# }
# Delete context
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/interpreter/context/{contextId}' \
--request DELETE
```
## Command execution
Daytona provides methods to execute shell commands in sandboxes. You can run commands with working directory, timeout, and environment variable options. The default timeout is 10 seconds when not specified.
Git operations assume you are operating in the sandbox user's home directory (e.g. **`workspace`** implies **`/home/[username]/workspace`**). Use a leading **`/`** when providing absolute paths.
### Execute commands
Execute shell commands in sandboxes by providing the command string and optional parameters for working directory, timeout, and environment variables.
You can also use the `daytona exec` CLI command for quick command execution.
```python
# Execute any shell command
response = sandbox.process.exec("ls -la")
print(response.result)
# Setting a working directory and a timeout
response = sandbox.process.exec("sleep 3", cwd="workspace/src", timeout=5)
print(response.result)
# Passing environment variables
response = sandbox.process.exec("echo $CUSTOM_SECRET", env={
"CUSTOM_SECRET": "DAYTONA"
}
)
print(response.result)
```
```typescript
// Execute any shell command
const response = await sandbox.process.executeCommand("ls -la");
console.log(response.result);
// Setting a working directory and a timeout
const response2 = await sandbox.process.executeCommand("sleep 3", "workspace/src", undefined, 5);
console.log(response2.result);
// Passing environment variables
const response3 = await sandbox.process.executeCommand("echo $CUSTOM_SECRET", ".", {
"CUSTOM_SECRET": "DAYTONA"
}
);
console.log(response3.result);
```
```ruby
# Execute any shell command
response = sandbox.process.exec(command: 'ls -la')
puts response.result
# Setting a working directory and a timeout
response = sandbox.process.exec(command: 'sleep 3', cwd: 'workspace/src', timeout: 5)
puts response.result
# Passing environment variables
response = sandbox.process.exec(
command: 'echo $CUSTOM_SECRET',
env: { 'CUSTOM_SECRET' => 'DAYTONA' }
)
puts response.result
```
```go
// Execute any shell command
response, err := sandbox.Process.ExecuteCommand(ctx, "ls -la")
if err != nil {
log.Fatal(err)
}
fmt.Println(response.Result)
// Setting a working directory and a timeout
response, err = sandbox.Process.ExecuteCommand(ctx, "sleep 3",
options.WithCwd("workspace/src"),
options.WithExecuteTimeout(5*time.Second),
)
if err != nil {
log.Fatal(err)
}
fmt.Println(response.Result)
// Passing environment variables
response, err = sandbox.Process.ExecuteCommand(ctx, "echo $CUSTOM_SECRET",
options.WithCommandEnv(map[string]string{"CUSTOM_SECRET": "DAYTONA"}),
)
if err != nil {
log.Fatal(err)
}
fmt.Println(response.Result)
```
```java
import io.daytona.sdk.model.ExecuteResponse;
import java.util.Map;
// Execute any shell command
ExecuteResponse response = sandbox.process.executeCommand("ls -la");
System.out.println(response.getResult());
// Working directory and timeout (seconds)
response = sandbox.process.executeCommand("sleep 3", "workspace/src", null, 5);
System.out.println(response.getResult());
// Environment variables
response = sandbox.process.executeCommand(
"echo $CUSTOM_SECRET",
".",
Map.of("CUSTOM_SECRET", "DAYTONA"),
null
);
System.out.println(response.getResult());
```
```bash
# Execute any shell command
daytona exec my-sandbox -- ls -la
# Setting a working directory and a timeout
daytona exec my-sandbox --cwd workspace/src --timeout 5 -- sleep 3
# Passing environment variables (use shell syntax)
daytona exec my-sandbox -- sh -c 'CUSTOM_SECRET=DAYTONA echo $CUSTOM_SECRET'
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/execute' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"command": "ls -la",
"cwd": "workspace",
"timeout": 5
}'
```
## Session operations
Daytona provides methods to manage background process sessions in sandboxes. You can create sessions, execute commands, monitor status, and manage long-running processes.
### Get session status
Get session status and list all sessions in a sandbox by providing the session ID.
```python
# Check session's executed commands
session = sandbox.process.get_session(session_id)
print(f"Session {session_id}:")
for command in session.commands:
print(f"Command: {command.command}, Exit Code: {command.exit_code}")
# List all running sessions
sessions = sandbox.process.list_sessions()
for session in sessions:
print(f"Session: {session.session_id}, Commands: {session.commands}")
```
```typescript
// Check session's executed commands
const session = await sandbox.process.getSession(sessionId);
console.log(`Session ${sessionId}:`);
for (const command of session.commands) {
console.log(`Command: ${command.command}, Exit Code: ${command.exitCode}`);
}
// List all running sessions
const sessions = await sandbox.process.listSessions();
for (const session of sessions) {
console.log(`Session: ${session.sessionId}, Commands: ${session.commands}`);
}
```
```ruby
# Check session's executed commands
session = sandbox.process.get_session(session_id)
puts "Session #{session_id}:"
session.commands.each do |command|
puts "Command: #{command.command}, Exit Code: #{command.exit_code}"
end
# List all running sessions
sessions = sandbox.process.list_sessions
sessions.each do |session|
puts "Session: #{session.session_id}, Commands: #{session.commands}"
end
```
```go
// Check session's executed commands
session, err := sandbox.Process.GetSession(ctx, sessionID)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Session %s:\n", sessionID)
commands := session["commands"].([]any)
for _, cmd := range commands {
cmdMap := cmd.(map[string]any)
fmt.Printf("Command: %s, Exit Code: %v\n", cmdMap["command"], cmdMap["exitCode"])
}
// List all running sessions
sessions, err := sandbox.Process.ListSessions(ctx)
if err != nil {
log.Fatal(err)
}
for _, sess := range sessions {
fmt.Printf("Session: %s, Commands: %v\n", sess["sessionId"], sess["commands"])
}
```
```java
import io.daytona.sdk.model.Command;
import io.daytona.sdk.model.Session;
// Check session's executed commands
Session session = sandbox.process.getSession(sessionId);
System.out.println("Session " + sessionId + ":");
for (Command command : session.getCommands()) {
System.out.println("Command: " + command.getCommand() + ", Exit Code: " + command.getExitCode());
}
// List all running sessions
for (Session s : sandbox.process.listSessions()) {
System.out.println("Session: " + s.getSessionId() + ", Commands: " + s.getCommands());
}
```
```bash
# Get session info
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/session/{sessionId}'
# List all sessions
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/session'
```
### Get session command
Get the status of a specific command within a session, including its exit code when execution has finished. Use this to poll asynchronous session commands.
```python
command = sandbox.process.get_session_command(session_id, command_id)
print(f"Command: {command.command}, Exit Code: {command.exit_code}")
```
```typescript
const command = await sandbox.process.getSessionCommand(sessionId, commandId);
console.log(`Command: ${command.command}, Exit Code: ${command.exitCode}`);
```
```ruby
command = sandbox.process.get_session_command(session_id: session_id, command_id: command_id)
puts "Command: #{command.command}, Exit Code: #{command.exit_code}"
```
```go
command, err := sandbox.Process.GetSessionCommand(ctx, sessionID, commandID)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Command: %s, Exit Code: %v\n", command["command"], command["exitCode"])
```
```java
import io.daytona.sdk.model.Command;
Command command = sandbox.process.getSessionCommand(sessionId, commandId);
System.out.println("Command: " + command.getCommand() + ", Exit Code: " + command.getExitCode());
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/session/{sessionId}/command/{commandId}'
```
### Entrypoint session
Retrieve information about the internal entrypoint session in sandboxes. In each sandbox, the configured entrypoint command is executed inside a dedicated internal session, and you can fetch the session details (including the commands) and read its logs.
```python
# Entrypoint session details
session = sandbox.process.get_entrypoint_session()
print(f"Entrypoint session: {session.session_id}")
cmd = session.commands[0]
print(f"Entrypoint command id: {cmd.id}")
print(f"Command: {cmd.command}")
# Entrypoint logs (HTTP)
logs = sandbox.process.get_entrypoint_logs()
print(f"[STDOUT]: {logs.stdout}")
print(f"[STDERR]: {logs.stderr}")
# Stream entrypoint logs (WebSocket)
async def stream_entrypoint_logs():
await sandbox.process.get_entrypoint_logs_async(
lambda log: print(f"[STDOUT]: {log}"),
lambda log: print(f"[STDERR]: {log}"),
)
# Use asyncio.run in scripts; in notebooks or async apps, await stream_entrypoint_logs() instead.
asyncio.run(stream_entrypoint_logs())
```
```typescript
// Entrypoint session details
const session = await sandbox.process.getEntrypointSession();
console.log(`Entrypoint session: ${session.sessionId}`);
const cmd = session.commands[0]
console.log(`Entrypoint command id: ${cmd.id}`);
console.log(`Command: ${cmd.command}`);
// Entrypoint logs (HTTP)
const logs = await sandbox.process.getEntrypointLogs();
console.log('[STDOUT]:', logs.stdout);
console.log('[STDERR]:', logs.stderr);
// Stream entrypoint logs (WebSocket)
await sandbox.process.getEntrypointLogs(
(chunk) => console.log('[STDOUT]:', chunk),
(chunk) => console.log('[STDERR]:', chunk),
);
```
```ruby
# Entrypoint session details
session = sandbox.process.get_entrypoint_session
puts "Entrypoint session: #{session.session_id}"
cmd = session.commands.first
puts "Entrypoint command id: #{cmd.id}"
puts "Command: #{cmd.command}"
# Entrypoint logs (HTTP)
logs = sandbox.process.get_entrypoint_logs
puts "[STDOUT]: #{logs.stdout}"
puts "[STDERR]: #{logs.stderr}"
# Stream entrypoint logs (WebSocket)
sandbox.process.get_entrypoint_logs_async(
on_stdout: ->(log) { puts "[STDOUT]: #{log}" },
on_stderr: ->(log) { puts "[STDERR]: #{log}" }
)
```
```go
// Entrypoint session details
info, err := sandbox.Process.GetEntrypointSession(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Entrypoint session: %s\n", info.GetSessionId())
cmds := info.GetCommands()
cmd := cmds[0]
fmt.Printf("Entrypoint command id: %s\n", cmd.GetId())
fmt.Printf("Command: %s\n", cmd.GetCommand())
// Entrypoint logs (HTTP)
logs, err := sandbox.Process.GetEntrypointLogs(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Println(logs)
// Stream entrypoint logs (WebSocket)
stdout := make(chan string, 100)
stderr := make(chan string, 100)
go func() {
for msg := range stderr {
log.Printf("[STDERR]: %s", msg)
}
}()
go func() {
if err := sandbox.Process.GetEntrypointLogsStream(ctx, stdout, stderr); err != nil {
log.Println("Entrypoint log stream error:", err)
}
}()
for msg := range stdout {
fmt.Printf("[STDOUT]: %s\n", msg)
}
```
```java
import io.daytona.sdk.model.Command;
import io.daytona.sdk.model.Session;
import io.daytona.sdk.model.SessionCommandLogsResponse;
// Entrypoint session details
Session session = sandbox.process.getEntrypointSession();
System.out.println("Entrypoint session: " + session.getSessionId());
Command cmd = session.getCommands().get(0);
System.out.println("Entrypoint command id: " + cmd.getId());
System.out.println("Command: " + cmd.getCommand());
// Entrypoint logs (HTTP)
SessionCommandLogsResponse logs = sandbox.process.getEntrypointLogs();
System.out.println("[STDOUT]: " + logs.getStdout());
System.out.println("[STDERR]: " + logs.getStderr());
// Stream entrypoint logs (WebSocket)
sandbox.process.getEntrypointLogs(
chunk -> System.out.println("[STDOUT]: " + chunk),
chunk -> System.out.println("[STDERR]: " + chunk)
);
```
```bash
# Get entrypoint session details
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/session/entrypoint'
# Get entrypoint logs (HTTP)
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/session/entrypoint/logs'
# Follow entrypoint logs in real-time (WebSocket)
# wss://proxy.app.daytona.io/toolbox/{sandboxId}/process/session/entrypoint/logs?follow=true
```
### Execute interactive commands
Execute interactive commands in sessions. You can send input to running commands that expect user interaction, such as confirmations or interactive tools like database CLIs and package managers.
```python
import asyncio
from daytona import SessionExecuteRequest
session_id = "interactive-session"
sandbox.process.create_session(session_id)
# Execute command that requires confirmation
command = sandbox.process.execute_session_command(
session_id,
SessionExecuteRequest(
command='pip uninstall requests',
run_async=True,
),
)
# Stream logs asynchronously
logs_task = asyncio.create_task(
sandbox.process.get_session_command_logs_async(
session_id,
command.cmd_id,
lambda log: print(f"[STDOUT]: {log}"),
lambda log: print(f"[STDERR]: {log}"),
)
)
await asyncio.sleep(1)
# Send input to the command
sandbox.process.send_session_command_input(session_id, command.cmd_id, "y")
# Wait for logs to complete
await logs_task
```
```typescript
const sessionId = 'interactive-session'
await sandbox.process.createSession(sessionId)
// Execute command that requires confirmation
const command = await sandbox.process.executeSessionCommand(sessionId, {
command: 'pip uninstall requests',
runAsync: true,
})
// Stream logs asynchronously
const logPromise = sandbox.process.getSessionCommandLogs(
sessionId,
command.cmdId!,
(stdout) => console.log('[STDOUT]:', stdout),
(stderr) => console.log('[STDERR]:', stderr),
)
await new Promise((resolve) => setTimeout(resolve, 1000))
// Send input to the command
await sandbox.process.sendSessionCommandInput(sessionId, command.cmdId!, 'y')
// Wait for logs to complete
await logPromise
```
```ruby
session_id = "interactive-session"
sandbox.process.create_session(session_id)
# Execute command that requires confirmation
interactive_command = sandbox.process.execute_session_command(
session_id: session_id,
req: Daytona::SessionExecuteRequest.new(
command: 'pip uninstall requests',
run_async: true
)
)
# Wait a moment for the command to start
sleep 1
# Send input to the command
sandbox.process.send_session_command_input(
session_id: session_id,
command_id: interactive_command.cmd_id,
data: "y"
)
# Get logs for the interactive command asynchronously
sandbox.process.get_session_command_logs_async(
session_id: session_id,
command_id: interactive_command.cmd_id,
on_stdout: ->(log) { puts "[STDOUT]: #{log}" },
on_stderr: ->(log) { puts "[STDERR]: #{log}" }
)
```
```go
sessionID := "interactive-session"
err := sandbox.Process.CreateSession(ctx, sessionID)
if err != nil {
log.Fatal(err)
}
// Execute command that requires confirmation
result, err := sandbox.Process.ExecuteSessionCommand(ctx, sessionID, "pip uninstall requests", true, false)
if err != nil {
log.Fatal(err)
}
cmdID := result["cmdId"].(string)
// Stream logs asynchronously
stdout := make(chan string)
stderr := make(chan string)
go func() {
err := sandbox.Process.GetSessionCommandLogsStream(ctx, sessionID, cmdID, stdout, stderr)
if err != nil {
log.Println("Log stream error:", err)
}
}()
time.Sleep(1 * time.Second)
// Note: SendSessionCommandInput is not available in Go SDK
// Use the API endpoint directly for sending input
// Read logs
for msg := range stdout {
fmt.Printf("[STDOUT]: %s\n", msg)
}
```
```java
import io.daytona.sdk.model.SessionExecuteRequest;
import io.daytona.sdk.model.SessionExecuteResponse;
String sessionId = "interactive-session";
sandbox.process.createSession(sessionId);
SessionExecuteResponse command = sandbox.process.executeSessionCommand(
sessionId,
new SessionExecuteRequest("pip uninstall requests", true)
);
String cmdId = command.getCmdId();
Thread logThread = new Thread(() -> sandbox.process.getSessionCommandLogs(
sessionId,
cmdId,
log -> System.out.println("[STDOUT]: " + log),
log -> System.out.println("[STDERR]: " + log)
));
logThread.start();
Thread.sleep(1000);
sandbox.process.sendSessionCommandInput(sessionId, cmdId, "y");
logThread.join();
```
```bash
# Create session
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/session' \
--request POST \
--header 'Content-Type: application/json' \
--data '{"sessionId": "interactive-session"}'
# Execute session command
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/session/{sessionId}/exec' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"command": "pip uninstall requests",
"runAsync": true
}'
# Send input to command
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/session/{sessionId}/command/{commandId}/input' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"data": "y"
}'
# Get command logs (append ?follow=true for WebSocket streaming)
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/session/{sessionId}/command/{commandId}/logs'
```
## Resource management
Use sessions for long-running operations, clean up sessions after execution, and handle exceptions properly.
```python
# Python - Clean up session
session_id = "long-running-cmd"
try:
sandbox.process.create_session(session_id)
session = sandbox.process.get_session(session_id)
# Do work...
finally:
sandbox.process.delete_session(session.session_id)
```
```typescript
// TypeScript - Clean up session
const sessionId = "long-running-cmd";
try {
await sandbox.process.createSession(sessionId);
const session = await sandbox.process.getSession(sessionId);
// Do work...
} finally {
await sandbox.process.deleteSession(session.sessionId);
}
```
```ruby
# Ruby - Clean up session
session_id = 'long-running-cmd'
begin
sandbox.process.create_session(session_id)
session = sandbox.process.get_session(session_id)
# Do work...
ensure
sandbox.process.delete_session(session.session_id)
end
```
```go
// Go - Clean up session
sessionID := "long-running-cmd"
err := sandbox.Process.CreateSession(ctx, sessionID)
if err != nil {
log.Fatal(err)
}
defer sandbox.Process.DeleteSession(ctx, sessionID)
session, err := sandbox.Process.GetSession(ctx, sessionID)
if err != nil {
log.Fatal(err)
}
// Do work...
```
```java
import io.daytona.sdk.model.Session;
// Clean up session
String sessionId = "long-running-cmd";
try {
sandbox.process.createSession(sessionId);
Session session = sandbox.process.getSession(sessionId);
// Do work...
} finally {
sandbox.process.deleteSession(sessionId);
}
```
```bash
# Create session
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/session' \
--request POST \
--header 'Content-Type: application/json' \
--data '{"sessionId": "long-running-cmd"}'
# Delete session when done
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/session/{sessionId}' \
--request DELETE
```
## Error handling
Handle process exceptions properly, log error details for debugging, and use try-catch blocks for error handling.
```python
from daytona import DaytonaError
try:
response = sandbox.process.code_run("invalid python code")
if response.exit_code != 0:
print(f"Exit code: {response.exit_code}")
print(f"Error output: {response.result}")
except DaytonaError as e:
print(f"Execution failed: {e}")
```
```typescript
import { DaytonaError } from '@daytona/sdk'
try {
const response = await sandbox.process.codeRun("invalid typescript code");
if (response.exitCode !== 0) {
console.error("Exit code:", response.exitCode);
console.error("Error output:", response.result);
}
} catch (e) {
if (e instanceof DaytonaError) {
console.error("Execution failed:", e);
}
}
```
```ruby
begin
response = sandbox.process.code_run(code: 'invalid python code')
if response.exit_code != 0
puts "Exit code: #{response.exit_code}"
puts "Error output: #{response.result}"
end
rescue StandardError => e
puts "Execution failed: #{e}"
end
```
```go
result, err := sandbox.Process.CodeRun(ctx, "invalid python code")
if err != nil {
fmt.Println("Execution failed:", err)
}
if result != nil && result.ExitCode != 0 {
fmt.Println("Exit code:", result.ExitCode)
fmt.Println("Error output:", result.Result)
}
```
```java
import io.daytona.sdk.exception.DaytonaException;
import io.daytona.sdk.model.ExecuteResponse;
try {
ExecuteResponse response = sandbox.process.codeRun("invalid python code");
if (response.getExitCode() != null && response.getExitCode() != 0) {
System.out.println("Exit code: " + response.getExitCode());
System.out.println("Error output: " + response.getResult());
}
} catch (DaytonaException e) {
System.out.println("Execution failed: " + e.getMessage());
}
```
```bash
# API responses include exitCode field for error handling
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/execute' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"command": "python3 -c \"invalid python code\""
}'
# Response includes:
# {
# "result": "",
# "exitCode": 1
# }
```
# Pseudo Terminal (PTY)
Daytona provides powerful pseudo terminal (PTY) capabilities through the `process` module in sandboxes. PTY sessions allow you to create interactive terminal sessions that can execute commands, handle user input, and manage terminal operations.
A PTY (Pseudo Terminal) is a virtual terminal interface that allows programs to interact with a shell as if they were connected to a real terminal. PTY sessions in Daytona enable:
- **Interactive Development**: REPLs, debuggers, and development tools
- **Build Processes**: Running and monitoring compilation, testing, or deployment
- **System Administration**: Remote server management and configuration
- **User Interfaces**: Terminal-based applications requiring user interaction
## Create PTY session
Create an interactive terminal session that can execute commands and handle user input.
```python
from daytona.common.pty import PtySize
pty_handle = sandbox.process.create_pty_session(
id="my-session",
cwd="/workspace",
envs={"TERM": "xterm-256color"},
pty_size=PtySize(cols=120, rows=30)
)
```
```typescript
// Create a PTY session with custom configuration
const ptyHandle = await sandbox.process.createPty({
id: 'my-interactive-session',
cwd: '/workspace',
envs: { TERM: 'xterm-256color', LANG: 'en_US.UTF-8' },
cols: 120,
rows: 30,
onData: (data) => {
// Handle terminal output
const text = new TextDecoder().decode(data)
process.stdout.write(text)
},
})
// Wait for connection to be established
await ptyHandle.waitForConnection()
// Send commands to the terminal
await ptyHandle.sendInput('ls -la\n')
await ptyHandle.sendInput('echo "Hello, PTY!"\n')
await ptyHandle.sendInput('exit\n')
// Wait for completion and get result
const result = await ptyHandle.wait()
console.log(`PTY session completed with exit code: ${result.exitCode}`)
// Clean up
await ptyHandle.disconnect()
```
```ruby
pty_size = Daytona::PtySize.new(rows: 30, cols: 120)
pty_handle = sandbox.process.create_pty_session(
id: 'my-interactive-session',
cwd: '/workspace',
envs: { 'TERM' => 'xterm-256color' },
pty_size: pty_size
)
# Use the PTY session
pty_handle.send_input("ls -la\n")
pty_handle.send_input("echo 'Hello, PTY!'\n")
pty_handle.send_input("exit\n")
# Handle output
pty_handle.each { |data| print data }
puts "PTY session completed with exit code: #{pty_handle.exit_code}"
```
```go
// Create a PTY session with custom configuration
handle, err := sandbox.Process.CreatePty(ctx, "my-interactive-session",
options.WithCreatePtySize(types.PtySize{Cols: 120, Rows: 30}),
options.WithCreatePtyEnv(map[string]string{"TERM": "xterm-256color"}),
)
if err != nil {
log.Fatal(err)
}
defer handle.Disconnect()
// Wait for connection to be established
if err := handle.WaitForConnection(ctx); err != nil {
log.Fatal(err)
}
// Send commands to the terminal
handle.SendInput([]byte("ls -la\n"))
handle.SendInput([]byte("echo 'Hello, PTY!'\n"))
handle.SendInput([]byte("exit\n"))
// Read output from channel
for data := range handle.DataChan() {
fmt.Print(string(data))
}
// Wait for completion and get result
result, err := handle.Wait(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("PTY session completed with exit code: %d\n", *result.ExitCode)
```
```java
import io.daytona.sdk.PtyCreateOptions;
import io.daytona.sdk.PtyHandle;
import io.daytona.sdk.PtyResult;
import java.nio.charset.StandardCharsets;
PtyCreateOptions options = new PtyCreateOptions()
.setId("my-interactive-session")
.setCols(120)
.setRows(30)
.setOnData(chunk -> System.out.print(new String(chunk, StandardCharsets.UTF_8)));
PtyHandle ptyHandle = sandbox.process.createPty(options);
ptyHandle.waitForConnection(30);
ptyHandle.sendInput("ls -la\n");
ptyHandle.sendInput("echo \"Hello, PTY!\"\n");
ptyHandle.sendInput("exit\n");
PtyResult result = ptyHandle.waitForExit();
System.out.println("PTY session completed with exit code: " + result.getExitCode());
ptyHandle.disconnect();
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/pty' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"id": "my-session",
"cwd": "/workspace",
"cols": 120,
"rows": 30,
"envs": {
"TERM": "xterm-256color"
},
"lazyStart": false
}'
```
## Connect to PTY session
Establish a connection to an existing PTY session.
```python
pty_handle = sandbox.process.connect_pty_session("my-session")
```
```typescript
// Connect to an existing PTY session
const handle = await sandbox.process.connectPty('my-session', {
onData: (data) => {
// Handle terminal output
const text = new TextDecoder().decode(data)
process.stdout.write(text)
},
})
// Wait for connection to be established
await handle.waitForConnection()
// Send commands to the existing session
await handle.sendInput('pwd\n')
await handle.sendInput('ls -la\n')
await handle.sendInput('exit\n')
// Wait for completion
const result = await handle.wait()
console.log(`Session exited with code: ${result.exitCode}`)
// Clean up
await handle.disconnect()
```
```ruby
# Connect to an existing PTY session
pty_handle = sandbox.process.connect_pty_session('my-session')
pty_handle.send_input("echo 'Hello World'\n")
pty_handle.send_input("exit\n")
# Handle output
pty_handle.each { |data| print data }
puts "Session exited with code: #{pty_handle.exit_code}"
```
```go
// Connect to an existing PTY session
handle, err := sandbox.Process.ConnectPty(ctx, "my-session")
if err != nil {
log.Fatal(err)
}
defer handle.Disconnect()
// Wait for connection to be established
if err := handle.WaitForConnection(ctx); err != nil {
log.Fatal(err)
}
// Send commands to the existing session
handle.SendInput([]byte("pwd\n"))
handle.SendInput([]byte("ls -la\n"))
handle.SendInput([]byte("exit\n"))
// Read output
for data := range handle.DataChan() {
fmt.Print(string(data))
}
// Wait for completion
result, err := handle.Wait(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Session exited with code: %d\n", *result.ExitCode)
```
```java
import io.daytona.sdk.PtyCreateOptions;
import io.daytona.sdk.PtyHandle;
import io.daytona.sdk.PtyResult;
import java.nio.charset.StandardCharsets;
PtyCreateOptions options = new PtyCreateOptions()
.setOnData(chunk -> System.out.print(new String(chunk, StandardCharsets.UTF_8)));
PtyHandle handle = sandbox.process.connectPty("my-session", options);
handle.waitForConnection(30);
handle.sendInput("pwd\n");
handle.sendInput("ls -la\n");
handle.sendInput("exit\n");
PtyResult result = handle.waitForExit();
System.out.println("Session exited with code: " + result.getExitCode());
handle.disconnect();
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/pty/{sessionId}/connect'
```
## List PTY sessions
List PTY sessions currently registered in the sandbox.
```python
# List all PTY sessions
sessions = sandbox.process.list_pty_sessions()
for session in sessions:
print(f"Session ID: {session.id}")
print(f"Active: {session.active}")
print(f"Lazy start: {session.lazy_start}")
print(f"Created: {session.created_at}")
```
```typescript
// List all PTY sessions
const sessions = await sandbox.process.listPtySessions()
for (const session of sessions) {
console.log(`Session ID: ${session.id}`)
console.log(`Active: ${session.active}`)
console.log(`Lazy start: ${session.lazyStart}`)
console.log(`Created: ${session.createdAt}`)
console.log('---')
}
```
```ruby
# List all PTY sessions
sessions = sandbox.process.list_pty_sessions
sessions.each do |session|
puts "Session ID: #{session.id}"
puts "Active: #{session.active}"
puts "Terminal Size: #{session.cols}x#{session.rows}"
puts '---'
end
```
```go
// List all PTY sessions
sessions, err := sandbox.Process.ListPtySessions(ctx)
if err != nil {
log.Fatal(err)
}
for _, session := range sessions {
fmt.Printf("Session ID: %s\n", session.Id)
fmt.Printf("Active: %t\n", session.Active)
fmt.Printf("Terminal Size: %dx%d\n", session.Cols, session.Rows)
fmt.Println("---")
}
```
```java
import io.daytona.toolbox.client.model.PtySessionInfo;
import java.util.List;
List sessions = sandbox.process.listPtySessions();
for (PtySessionInfo session : sessions) {
System.out.println("Session ID: " + session.getId());
System.out.println("Active: " + session.getActive());
System.out.println("Lazy start: " + session.getLazyStart());
System.out.println("Created: " + session.getCreatedAt());
System.out.println("---");
}
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/pty'
```
## Get PTY session info
Get details about a specific PTY session.
```python
# Get details about a specific PTY session
session_info = sandbox.process.get_pty_session_info("my-session")
print(f"Session ID: {session_info.id}")
print(f"Active: {session_info.active}")
print(f"Lazy start: {session_info.lazy_start}")
print(f"Working Directory: {session_info.cwd}")
print(f"Terminal Size: {session_info.cols}x{session_info.rows}")
```
```typescript
// Get details about a specific PTY session
const session = await sandbox.process.getPtySessionInfo('my-session')
console.log(`Session ID: ${session.id}`)
console.log(`Active: ${session.active}`)
console.log(`Lazy start: ${session.lazyStart}`)
console.log(`Working Directory: ${session.cwd}`)
console.log(`Terminal Size: ${session.cols}x${session.rows}`)
```
```ruby
# Get details about a specific PTY session
session_info = sandbox.process.get_pty_session_info('my-session')
puts "Session ID: #{session_info.id}"
puts "Active: #{session_info.active}"
puts "Lazy start: #{session_info.lazy_start}"
puts "Working Directory: #{session_info.cwd}"
puts "Terminal Size: #{session_info.cols}x#{session_info.rows}"
```
```go
// Get details about a specific PTY session
session, err := sandbox.Process.GetPtySessionInfo(ctx, "my-session")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Session ID: %s\n", session.Id)
fmt.Printf("Active: %t\n", session.Active)
fmt.Printf("Working Directory: %s\n", session.Cwd)
fmt.Printf("Terminal Size: %dx%d\n", session.Cols, session.Rows)
```
```java
import io.daytona.toolbox.client.model.PtySessionInfo;
PtySessionInfo session = sandbox.process.getPtySessionInfo("my-session");
System.out.println("Session ID: " + session.getId());
System.out.println("Active: " + session.getActive());
System.out.println("Lazy start: " + session.getLazyStart());
System.out.println("Working Directory: " + session.getCwd());
System.out.println("Terminal Size: " + session.getCols() + "x" + session.getRows());
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/pty/{sessionId}'
```
## Kill PTY session
Kill a PTY session, terminating the shell process and removing the session from the sandbox.
```python
# Kill a specific PTY session
sandbox.process.kill_pty_session("my-session")
# Verify the session no longer exists
pty_sessions = sandbox.process.list_pty_sessions()
for pty_session in pty_sessions:
print(f"PTY session: {pty_session.id}")
```
```typescript
// Kill a specific PTY session
await sandbox.process.killPtySession('my-session')
// Verify the session is no longer active
try {
const info = await sandbox.process.getPtySessionInfo('my-session')
console.log(`Session still exists but active: ${info.active}`)
} catch (error) {
console.log('Session has been completely removed')
}
```
```ruby
# Delete a specific PTY session
sandbox.process.delete_pty_session('my-session')
# Verify the session no longer exists
sessions = sandbox.process.list_pty_sessions
sessions.each do |session|
puts "PTY session: #{session.id}"
end
```
```go
// Kill a specific PTY session
err := sandbox.Process.KillPtySession(ctx, "my-session")
if err != nil {
log.Fatal(err)
}
// Verify the session is no longer active
sessions, err := sandbox.Process.ListPtySessions(ctx)
if err != nil {
log.Fatal(err)
}
for _, session := range sessions {
fmt.Printf("PTY session: %s\n", session.Id)
}
```
```java
import io.daytona.toolbox.client.model.PtySessionInfo;
import java.util.List;
sandbox.process.killPtySession("my-session");
List sessions = sandbox.process.listPtySessions();
for (PtySessionInfo session : sessions) {
System.out.println("PTY session: " + session.getId());
}
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/pty/{sessionId}' \
--request DELETE
```
## Resize PTY session
Resize a PTY session, allowing you to change the terminal dimensions of an active PTY session.
```python
from daytona.common.pty import PtySize
# Resize a PTY session to a larger terminal
new_size = PtySize(rows=40, cols=150)
updated_info = sandbox.process.resize_pty_session("my-session", new_size)
print(f"Terminal resized to {updated_info.cols}x{updated_info.rows}")
# You can also use the PtyHandle's resize method
pty_handle.resize(new_size)
```
```typescript
// Resize a PTY session to a larger terminal
const updatedInfo = await sandbox.process.resizePtySession('my-session', 150, 40)
console.log(`Terminal resized to ${updatedInfo.cols}x${updatedInfo.rows}`)
// You can also use the PtyHandle's resize method
await ptyHandle.resize(150, 40) // cols, rows
```
```ruby
# Resize a PTY session to a larger terminal
pty_size = Daytona::PtySize.new(rows: 40, cols: 150)
session_info = sandbox.process.resize_pty_session('my-session', pty_size)
puts "Terminal resized to #{session_info.cols}x#{session_info.rows}"
```
```go
// Resize a PTY session to a larger terminal
updatedInfo, err := sandbox.Process.ResizePtySession(ctx, "my-session", types.PtySize{
Cols: 150,
Rows: 40,
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Terminal resized to %dx%d\n", updatedInfo.Cols, updatedInfo.Rows)
// You can also use the PtyHandle's Resize method
info, err := handle.Resize(ctx, 150, 40)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Terminal resized to %dx%d\n", info.Cols, info.Rows)
```
```java
sandbox.process.resizePtySession("my-session", 150, 40);
// Or resize using the handle (after createPty or connectPty)
handle.resize(150, 40);
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/pty/{sessionId}/resize' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"cols": 150,
"rows": 40
}'
```
## Interactive commands
Handle interactive commands with PTY sessions, allowing you to handle interactive commands that require user input and can be resized during execution.
```python
import time
import threading
from daytona.common.pty import PtySize
def handle_pty_data(data: bytes):
text = data.decode("utf-8", errors="replace")
print(text, end="")
# Create PTY session
pty_handle = sandbox.process.create_pty_session(
id="interactive-session",
pty_size=PtySize(cols=300, rows=100)
)
# Handle output while sending interactive input
thread = threading.Thread(target=pty_handle.wait, args=(handle_pty_data,))
thread.start()
# Send interactive command
pty_handle.send_input('printf "Are you accepting the terms and conditions? (y/n): " && read confirm && if [ "$confirm" = "y" ]; then echo "You accepted"; else echo "You did not accept"; fi\n')
time.sleep(1)
pty_handle.send_input("y\n")
# Resize terminal
pty_session_info = pty_handle.resize(PtySize(cols=210, rows=110))
print(f"PTY session resized to {pty_session_info.cols}x{pty_session_info.rows}")
# Exit the session
pty_handle.send_input('exit\n')
thread.join()
print(f"Session completed with exit code: {pty_handle.exit_code}")
```
```typescript
import { Daytona, Sandbox } from '@daytona/sdk'
// Create PTY session
const ptyHandle = await sandbox.process.createPty({
id: 'interactive-session',
cols: 300,
rows: 100,
onData: data => {
const text = new TextDecoder().decode(data)
process.stdout.write(text)
},
})
await ptyHandle.waitForConnection()
// Send interactive command
await ptyHandle.sendInput(
'printf "Are you accepting the terms and conditions? (y/n): " && read confirm && if [ "$confirm" = "y" ]; then echo "You accepted"; else echo "You did not accept"; fi\n'
)
await new Promise(resolve => setTimeout(resolve, 1000))
await ptyHandle.sendInput('y\n')
// Resize terminal
const ptySessionInfo = await sandbox.process.resizePtySession(
'interactive-session',
210,
110
)
console.log(
`\nPTY session resized to ${ptySessionInfo.cols}x${ptySessionInfo.rows}`
)
// Exit the session
await ptyHandle.sendInput('exit\n')
// Wait for completion
const result = await ptyHandle.wait()
console.log(`Session completed with exit code: ${result.exitCode}`)
```
```ruby
require 'daytona'
# Create PTY session
pty_handle = sandbox.process.create_pty_session(
id: 'interactive-session',
pty_size: Daytona::PtySize.new(cols: 300, rows: 100)
)
# Handle output in a separate thread
thread = Thread.new do
pty_handle.each { |data| print data }
end
# Send interactive command
pty_handle.send_input('printf "Are you accepting the terms and conditions? (y/n): " && read confirm && if [ "$confirm" = "y" ]; then echo "You accepted"; else echo "You did not accept"; fi' + "\n")
sleep(1)
pty_handle.send_input("y\n")
# Resize terminal
pty_handle.resize(Daytona::PtySize.new(cols: 210, rows: 110))
puts "\nPTY session resized"
# Exit the session
pty_handle.send_input("exit\n")
# Wait for the thread to finish
thread.join
puts "Session completed with exit code: #{pty_handle.exit_code}"
```
```go
// Create PTY session
handle, err := sandbox.Process.CreatePty(ctx, "interactive-session",
options.WithCreatePtySize(types.PtySize{Cols: 300, Rows: 100}),
)
if err != nil {
log.Fatal(err)
}
defer handle.Disconnect()
if err := handle.WaitForConnection(ctx); err != nil {
log.Fatal(err)
}
// Handle output in a goroutine
go func() {
for data := range handle.DataChan() {
fmt.Print(string(data))
}
}()
// Send interactive command
handle.SendInput([]byte(`printf "Are you accepting the terms and conditions? (y/n): " && read confirm && if [ "$confirm" = "y" ]; then echo "You accepted"; else echo "You did not accept"; fi` + "\n"))
time.Sleep(1 * time.Second)
handle.SendInput([]byte("y\n"))
// Resize terminal
info, err := handle.Resize(ctx, 210, 110)
if err != nil {
log.Fatal(err)
}
fmt.Printf("\nPTY session resized to %dx%d\n", info.Cols, info.Rows)
// Exit the session
handle.SendInput([]byte("exit\n"))
// Wait for completion
result, err := handle.Wait(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Session completed with exit code: %d\n", *result.ExitCode)
```
```java
import io.daytona.sdk.PtyCreateOptions;
import io.daytona.sdk.PtyHandle;
import io.daytona.sdk.PtyResult;
import java.nio.charset.StandardCharsets;
PtyCreateOptions options = new PtyCreateOptions()
.setId("interactive-session")
.setCols(300)
.setRows(100)
.setOnData(chunk -> System.out.print(new String(chunk, StandardCharsets.UTF_8)));
PtyHandle handle = sandbox.process.createPty(options);
handle.waitForConnection(30);
handle.sendInput(
"printf \"Are you accepting the terms and conditions? (y/n): \" && read confirm && if [ \"$confirm\" = \"y\" ]; then echo \"You accepted\"; else echo \"You did not accept\"; fi\n");
Thread.sleep(1000);
handle.sendInput("y\n");
sandbox.process.resizePtySession("interactive-session", 210, 110);
System.out.println("\nPTY session resized");
handle.sendInput("exit\n");
PtyResult result = handle.waitForExit();
System.out.println("Session completed with exit code: " + result.getExitCode());
```
## Long-running processes
Manage long-running processes with PTY sessions, allowing you to manage long-running processes that need to be monitored or terminated.
```python
import time
import threading
from daytona import Daytona, Sandbox
from daytona.common.pty import PtySize
def handle_pty_data(data: bytes):
text = data.decode("utf-8", errors="replace")
print(text, end="")
# Create PTY session
pty_handle = sandbox.process.create_pty_session(
id="long-running-session",
pty_size=PtySize(cols=120, rows=30)
)
# Start a long-running process
pty_handle.send_input('while true; do echo "Running... $(date)"; sleep 1; done\n')
# Using thread and wait() method to handle PTY output
thread = threading.Thread(target=pty_handle.wait, args=(handle_pty_data, 10))
thread.start()
time.sleep(3) # Let it run for a bit
print("Killing long-running process...")
pty_handle.kill()
thread.join()
print(f"\nProcess terminated with exit code: {pty_handle.exit_code}")
if pty_handle.error:
print(f"Termination reason: {pty_handle.error}")
```
```typescript
import { Daytona, Sandbox } from '@daytona/sdk'
// Create PTY session
const ptyHandle = await sandbox.process.createPty({
id: 'long-running-session',
cols: 120,
rows: 30,
onData: (data) => {
const text = new TextDecoder().decode(data)
process.stdout.write(text)
},
})
await ptyHandle.waitForConnection()
// Start a long-running process
await ptyHandle.sendInput('while true; do echo "Running... $(date)"; sleep 1; done\n')
await new Promise(resolve => setTimeout(resolve, 3000)) // Let it run for a bit
console.log('Killing long-running process...')
await ptyHandle.kill()
// Wait for termination
const result = await ptyHandle.wait()
console.log(`\nProcess terminated with exit code: ${result.exitCode}`)
if (result.error) {
console.log(`Termination reason: ${result.error}`)
}
```
```ruby
require 'daytona'
# Create PTY session
pty_handle = sandbox.process.create_pty_session(
id: 'long-running-session',
pty_size: Daytona::PtySize.new(cols: 120, rows: 30)
)
# Handle output in a separate thread
thread = Thread.new do
pty_handle.each { |data| print data }
end
# Start a long-running process
pty_handle.send_input("while true; do echo \"Running... $(date)\"; sleep 1; done\n")
sleep(3) # Let it run for a bit
puts "Killing long-running process..."
pty_handle.kill
thread.join
puts "\nProcess terminated with exit code: #{pty_handle.exit_code}"
puts "Termination reason: #{pty_handle.error}" if pty_handle.error
```
```go
// Create PTY session
handle, err := sandbox.Process.CreatePty(ctx, "long-running-session",
options.WithCreatePtySize(types.PtySize{Cols: 120, Rows: 30}),
)
if err != nil {
log.Fatal(err)
}
defer handle.Disconnect()
if err := handle.WaitForConnection(ctx); err != nil {
log.Fatal(err)
}
// Handle output in a goroutine
go func() {
for data := range handle.DataChan() {
fmt.Print(string(data))
}
}()
// Start a long-running process
handle.SendInput([]byte(`while true; do echo "Running... $(date)"; sleep 1; done` + "\n"))
time.Sleep(3 * time.Second) // Let it run for a bit
fmt.Println("Killing long-running process...")
if err := handle.Kill(ctx); err != nil {
log.Fatal(err)
}
// Wait for termination
result, err := handle.Wait(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("\nProcess terminated with exit code: %d\n", *result.ExitCode)
if result.Error != nil {
fmt.Printf("Termination reason: %s\n", *result.Error)
}
```
```java
import io.daytona.sdk.PtyCreateOptions;
import io.daytona.sdk.PtyHandle;
import io.daytona.sdk.PtyResult;
import java.nio.charset.StandardCharsets;
PtyCreateOptions options = new PtyCreateOptions()
.setId("long-running-session")
.setCols(120)
.setRows(30)
.setOnData(chunk -> System.out.print(new String(chunk, StandardCharsets.UTF_8)));
PtyHandle handle = sandbox.process.createPty(options);
handle.waitForConnection(30);
handle.sendInput("while true; do echo \"Running... $(date)\"; sleep 1; done\n");
Thread.sleep(3000);
System.out.println("Killing long-running process...");
handle.kill();
PtyResult result = handle.waitForExit();
System.out.println("\nProcess terminated with exit code: " + result.getExitCode());
if (result.getError() != null) {
System.out.println("Termination reason: " + result.getError());
}
```
## Resource management
Manage resource leaks with PTY sessions, allowing you to always clean up PTY sessions to prevent resource leaks.
```python
# Python: Use try/finally
pty_handle = None
try:
pty_handle = sandbox.process.create_pty_session(id="session", pty_size=PtySize(cols=120, rows=30))
# Do work...
finally:
if pty_handle:
pty_handle.kill()
```
```typescript
// TypeScript: Use try/finally
let ptyHandle
try {
ptyHandle = await sandbox.process.createPty({
id: 'session',
cols: 120,
rows: 30,
})
// Do work...
} finally {
if (ptyHandle) await ptyHandle.kill()
}
```
```ruby
# Ruby: Use begin/ensure
pty_handle = nil
begin
pty_handle = sandbox.process.create_pty_session(
id: 'session',
pty_size: Daytona::PtySize.new(cols: 120, rows: 30)
)
# Do work...
ensure
pty_handle&.disconnect
end
```
```go
// Go: defer Disconnect to close the WebSocket without killing the shell
handle, err := sandbox.Process.CreatePty(ctx, "session",
options.WithCreatePtySize(types.PtySize{Cols: 120, Rows: 30}),
)
if err != nil {
log.Fatal(err)
}
defer handle.Disconnect()
// Do work...
// Call Kill to terminate the shell process when needed
// handle.Kill(ctx)
```
```java
import io.daytona.sdk.PtyCreateOptions;
import io.daytona.sdk.PtyHandle;
PtyHandle handle = null;
try {
handle = sandbox.process.createPty(
new PtyCreateOptions().setId("session").setCols(120).setRows(30));
handle.waitForConnection(30);
// Do work...
} finally {
if (handle != null) {
handle.kill();
handle.disconnect();
}
}
```
## PtyHandle methods
Daytona provides methods to interact with PTY sessions, allowing you to send input, resize the terminal, wait for completion, and manage the WebSocket connection to a PTY session.
### Send input
Send input to a PTY session, allowing you to send input data (keystrokes or commands) to the PTY session.
```python
# Send a command
pty_handle.send_input("ls -la\n")
# Send user input
pty_handle.send_input("y\n")
```
```typescript
// Send a command
await ptyHandle.sendInput('ls -la\n')
// Send raw bytes
await ptyHandle.sendInput(new Uint8Array([3])) // Ctrl+C
```
```ruby
# Send a command
pty_handle.send_input("ls -la\n")
# Send user input
pty_handle.send_input("y\n")
```
```go
// Send a command
handle.SendInput([]byte("ls -la\n"))
// Send Ctrl+C
handle.SendInput([]byte{0x03})
```
```java
// Send a command
ptyHandle.sendInput("ls -la\n");
// Send raw bytes (Ctrl+C)
ptyHandle.sendInput(new byte[] { 0x03 });
```
### Wait for completion
Wait for a PTY process to exit and return the result.
```python
# Wait with a callback for output data
def handle_data(data: bytes):
print(data.decode("utf-8", errors="replace"), end="")
result = pty_handle.wait(on_data=handle_data, timeout=30)
print(f"Exit code: {result.exit_code}")
```
```typescript
// Wait for process to complete
const result = await ptyHandle.wait()
if (result.exitCode === 0) {
console.log('Process completed successfully')
} else {
console.log(`Process failed with code: ${result.exitCode}`)
if (result.error) {
console.log(`Error: ${result.error}`)
}
}
```
```ruby
# Wait by iterating over output (blocks until PTY session ends)
pty_handle.each { |data| print data }
if pty_handle.exit_code == 0
puts 'Process completed successfully'
else
puts "Process failed with code: #{pty_handle.exit_code}"
puts "Error: #{pty_handle.error}" if pty_handle.error
end
```
```go
// Wait for process to complete
result, err := handle.Wait(ctx)
if err != nil {
log.Fatal(err)
}
if result.ExitCode != nil && *result.ExitCode == 0 {
fmt.Println("Process completed successfully")
} else {
fmt.Printf("Process failed with code: %d\n", *result.ExitCode)
if result.Error != nil {
fmt.Printf("Error: %s\n", *result.Error)
}
}
```
```java
import io.daytona.sdk.PtyResult;
PtyResult result = ptyHandle.waitForExit();
if (result.getExitCode() == 0) {
System.out.println("Process completed successfully");
} else {
System.out.println("Process failed with code: " + result.getExitCode());
if (result.getError() != null) {
System.out.println("Error: " + result.getError());
}
}
```
### Wait for connection
Wait for the WebSocket connection to be established before sending input.
```python
# Python handles connection internally during creation
# No explicit wait needed
```
```typescript
// Wait for connection to be established
await ptyHandle.waitForConnection()
// Now safe to send input
await ptyHandle.sendInput('echo "Connected!"\n')
```
```ruby
pty_handle.wait_for_connection
pty_handle.send_input("echo 'Connected!'\n")
```
```go
// Wait for connection to be established
if err := handle.WaitForConnection(ctx); err != nil {
log.Fatal(err)
}
// Now safe to send input
handle.SendInput([]byte("echo 'Connected!'\n"))
```
```java
// Wait for connection (timeout in seconds)
ptyHandle.waitForConnection(30);
ptyHandle.sendInput("echo \"Connected!\"\n");
```
### Kill PTY process
Kill a PTY process and terminate the session from the handle.
```python
pty_handle.kill()
```
```typescript
// Kill a long-running process
await ptyHandle.kill()
// Wait to confirm termination
const result = await ptyHandle.wait()
console.log(`Process terminated with exit code: ${result.exitCode}`)
```
```ruby
# Kill a long-running process
pty_handle.kill
puts "Process terminated with exit code: #{pty_handle.exit_code}"
```
```go
// Kill a long-running process
if err := handle.Kill(ctx); err != nil {
log.Fatal(err)
}
// Wait to confirm termination
result, err := handle.Wait(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Process terminated with exit code: %d\n", *result.ExitCode)
```
```java
import io.daytona.sdk.PtyResult;
ptyHandle.kill();
PtyResult result = ptyHandle.waitForExit();
System.out.println("Process terminated with exit code: " + result.getExitCode());
```
### Resize from handle
Resize the PTY terminal dimensions directly from the handle.
```python
from daytona.common.pty import PtySize
pty_handle.resize(PtySize(cols=120, rows=30))
```
```typescript
// Resize to 120x30
await ptyHandle.resize(120, 30)
```
```ruby
# Resize to 120x30
pty_handle.resize(Daytona::PtySize.new(cols: 120, rows: 30))
```
```go
// Resize to 120x30
info, err := handle.Resize(ctx, 120, 30)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Resized to %dx%d\n", info.Cols, info.Rows)
```
```java
// Resize to 120x30
ptyHandle.resize(120, 30);
```
### Disconnect
Disconnect from a PTY session and clean up resources without killing the process.
```python
# Python: Use kill() to terminate, or let the handle go out of scope
```
```typescript
// Always clean up when done
try {
// ... use PTY session
} finally {
await ptyHandle.disconnect()
}
```
```ruby
begin
# ... use PTY session
ensure
pty_handle.disconnect
end
```
```go
// Always clean up when done using defer
handle, err := sandbox.Process.CreatePty(ctx, "session")
if err != nil {
log.Fatal(err)
}
defer handle.Disconnect()
// ... use PTY session
```
```java
try {
// ... use PTY session
} finally {
ptyHandle.disconnect();
}
```
### Check connection status
Check if a PTY session is still connected.
```python
# Python: Check by attempting operations or using session info
session_info = sandbox.process.get_pty_session_info("my-session")
print(f"Session active: {session_info.active}")
```
```typescript
if (ptyHandle.isConnected()) {
console.log('PTY session is active')
}
```
```ruby
# Ruby: Check by using session info
session_info = sandbox.process.get_pty_session_info('my-session')
puts 'PTY session is active' if session_info.active
```
```go
if handle.IsConnected() {
fmt.Println("PTY session is active")
}
```
```java
if (ptyHandle.isConnected()) {
System.out.println("PTY session is active");
}
```
### Exit code and error
Access the exit code and error message after a PTY process terminates.
```python
# After iteration or wait completes
print(f"Exit code: {pty_handle.exit_code}")
if pty_handle.error:
print(f"Error: {pty_handle.error}")
```
```typescript
// Access via getters after process terminates
console.log(`Exit code: ${ptyHandle.exitCode}`)
if (ptyHandle.error) {
console.log(`Error: ${ptyHandle.error}`)
}
```
```ruby
# Access after process terminates
puts "Exit code: #{pty_handle.exit_code}"
puts "Error: #{pty_handle.error}" if pty_handle.error
```
```go
// Access via methods after process terminates
if exitCode := handle.ExitCode(); exitCode != nil {
fmt.Printf("Exit code: %d\n", *exitCode)
}
if errMsg := handle.Error(); errMsg != nil {
fmt.Printf("Error: %s\n", *errMsg)
}
```
```java
import io.daytona.sdk.PtyResult;
PtyResult result = ptyHandle.waitForExit();
System.out.println("Exit code: " + result.getExitCode());
if (result.getError() != null) {
System.out.println("Error: " + result.getError());
}
```
### Iterate over output (Python)
Iterate over a PTY handle to receive output data.
```python
# Iterate over PTY output
for data in pty_handle:
text = data.decode("utf-8", errors="replace")
print(text, end="")
print(f"Session ended with exit code: {pty_handle.exit_code}")
```
```typescript
// TypeScript uses the onData callback instead
const ptyHandle = await sandbox.process.createPty({
id: 'my-session',
onData: (data) => {
const text = new TextDecoder().decode(data)
process.stdout.write(text)
},
})
```
```ruby
# Iterate over PTY output
pty_handle.each do |data|
print data
end
puts "Session ended with exit code: #{pty_handle.exit_code}"
```
```go
// Go uses a channel to receive output data
for data := range handle.DataChan() {
fmt.Print(string(data))
}
// Or use as io.Reader
io.Copy(os.Stdout, handle)
fmt.Printf("Session ended with exit code: %d\n", *handle.ExitCode())
```
```java
import io.daytona.sdk.PtyCreateOptions;
import java.nio.charset.StandardCharsets;
PtyHandle ptyHandle = sandbox.process.createPty(
new PtyCreateOptions()
.setId("my-session")
.setOnData(chunk -> System.out.print(new String(chunk, StandardCharsets.UTF_8))));
```
## Error handling
Monitor exit codes and handle errors appropriately with PTY sessions.
```python
# Python: Check exit codes
result = pty_handle.wait()
if result.exit_code != 0:
print(f"Command failed: {result.exit_code}")
print(f"Error: {result.error}")
```
```typescript
// TypeScript: Check exit codes
const result = await ptyHandle.wait()
if (result.exitCode !== 0) {
console.log(`Command failed: ${result.exitCode}`)
console.log(`Error: ${result.error}`)
}
```
```ruby
# Ruby: Check exit codes
# The handle blocks until the PTY session completes
pty_handle.each { |data| print data }
if pty_handle.exit_code != 0
puts "Command failed: #{pty_handle.exit_code}"
puts "Error: #{pty_handle.error}"
end
```
```go
// Go: Check exit codes
result, err := handle.Wait(ctx)
if err != nil {
log.Fatal(err)
}
if result.ExitCode != nil && *result.ExitCode != 0 {
fmt.Printf("Command failed: %d\n", *result.ExitCode)
if result.Error != nil {
fmt.Printf("Error: %s\n", *result.Error)
}
}
```
```java
import io.daytona.sdk.PtyResult;
PtyResult result = ptyHandle.waitForExit();
if (result.getExitCode() != 0) {
System.out.println("Command failed: " + result.getExitCode());
if (result.getError() != null) {
System.out.println("Error: " + result.getError());
}
}
```
# Log Streaming
Log streaming allows you to access and process logs as they are being produced, while the process is still running. When executing long-running processes in a sandbox, you often want to access and process their logs in **real-time**.
Real-time streaming is especially useful for **debugging**, **monitoring**, or integrating with **observability tools**.
- [**Log streaming**](#stream-logs-with-callbacks): stream logs as they are being produced, while the process is still running.
- [**Fetching log snapshot**](#retrieve-all-existing-logs): retrieve all logs up to a certain point.
This guide covers how to use log streaming with callbacks and fetching log snapshots in both asynchronous and synchronous modes.
For entrypoint log streaming, see [Process & Code Execution](https://www.daytona.io/docs/en/process-code-execution.md#entrypoint-session). To stream logs while sending input to a running command, see [Execute interactive commands](https://www.daytona.io/docs/en/process-code-execution.md#execute-interactive-commands).
## Stream logs with callbacks
If your sandboxed process is part of a larger system and is expected to run for an extended period (or indefinitely),
you can process logs asynchronously **in the background**, while the rest of your system continues executing.
This is ideal for:
- Continuous monitoring
- Debugging long-running jobs
- Live log forwarding or visualizations
:::tip[Python callbacks]
When streaming with the Python SDK, avoid blocking operations inside stdout/stderr callbacks. Blocking synchronous callbacks can cause WebSocket disconnections. Use async callbacks where possible.
:::
```python
import asyncio
from daytona import Daytona, SessionExecuteRequest
async def main():
daytona = Daytona()
sandbox = daytona.create()
session_id = "streaming-session"
sandbox.process.create_session(session_id)
command = sandbox.process.execute_session_command(
session_id,
SessionExecuteRequest(
command='for i in {1..5}; do echo "Step $i"; echo "Error $i" >&2; sleep 1; done',
run_async=True,
),
)
# Stream logs with separate callbacks
logs_task = asyncio.create_task(
sandbox.process.get_session_command_logs_async(
session_id,
command.cmd_id,
lambda stdout: print(f"[STDOUT]: {stdout}"),
lambda stderr: print(f"[STDERR]: {stderr}"),
)
)
print("Continuing execution while logs are streaming...")
await asyncio.sleep(3)
print("Other operations completed!")
# Wait for the logs to complete
await logs_task
sandbox.delete()
if __name__ == "__main__":
asyncio.run(main())
```
```typescript
import { Daytona } from '@daytona/sdk'
async function main() {
const daytona = new Daytona()
const sandbox = await daytona.create()
const sessionId = "exec-session-1"
await sandbox.process.createSession(sessionId)
const command = await sandbox.process.executeSessionCommand(
sessionId,
{
command: 'for i in {1..5}; do echo "Step $i"; echo "Error $i" >&2; sleep 1; done',
runAsync: true,
},
)
// Stream logs with separate callbacks
const logsTask = sandbox.process.getSessionCommandLogs(
sessionId,
command.cmdId!,
(stdout) => console.log('[STDOUT]:', stdout),
(stderr) => console.log('[STDERR]:', stderr),
)
console.log('Continuing execution while logs are streaming...')
await new Promise((resolve) => setTimeout(resolve, 3000))
console.log('Other operations completed!')
// Wait for the logs to complete
await logsTask
await sandbox.delete()
}
main()
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
sandbox = daytona.create
session_id = 'streaming-session'
sandbox.process.create_session(session_id)
command = sandbox.process.execute_session_command(
session_id: session_id,
req: Daytona::SessionExecuteRequest.new(
command: 'for i in {1..5}; do echo "Step $i"; echo "Error $i" >&2; sleep 1; done',
run_async: true
)
)
# Stream logs using a thread
log_thread = Thread.new do
sandbox.process.get_session_command_logs_async(
session_id: session_id,
command_id: command.cmd_id,
on_stdout: ->(stdout) { puts "[STDOUT]: #{stdout}" },
on_stderr: ->(stderr) { puts "[STDERR]: #{stderr}" }
)
end
puts 'Continuing execution while logs are streaming...'
sleep(3)
puts 'Other operations completed!'
# Wait for the logs to complete
log_thread.join
daytona.delete(sandbox)
```
```go
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/daytona/clients/sdk-go/pkg/daytona"
)
func main() {
client, _ := daytona.NewClient()
ctx := context.Background()
sandbox, _ := client.Create(ctx, nil)
sessionID := "streaming-session"
sandbox.Process.CreateSession(ctx, sessionID)
// Execute async command that outputs to stdout and stderr
cmd := `for i in 1 2 3 4 5; do echo "Step $i"; echo "Error $i" >&2; sleep 1; done`
cmdResult, _ := sandbox.Process.ExecuteSessionCommand(ctx, sessionID, cmd, true)
cmdID, _ := cmdResult["id"].(string)
// Create channels for stdout and stderr
stdout := make(chan string, 100)
stderr := make(chan string, 100)
// Stream logs in a goroutine
go func() {
err := sandbox.Process.GetSessionCommandLogsStream(ctx, sessionID, cmdID, stdout, stderr)
if err != nil {
log.Printf("Stream error: %v", err)
}
}()
fmt.Println("Continuing execution while logs are streaming...")
// Read from channels until both are closed
stdoutOpen, stderrOpen := true, true
for stdoutOpen || stderrOpen {
select {
case chunk, ok := <-stdout:
if !ok {
stdoutOpen = false
} else {
fmt.Fprintf(os.Stdout, "[STDOUT]: %s", chunk)
}
case chunk, ok := <-stderr:
if !ok {
stderrOpen = false
} else {
fmt.Fprintf(os.Stderr, "[STDERR]: %s", chunk)
}
}
}
fmt.Println("Streaming completed!")
sandbox.Delete(ctx)
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.SessionExecuteRequest;
import io.daytona.sdk.model.SessionExecuteResponse;
public class App {
public static void main(String[] args) throws InterruptedException {
try (Daytona daytona = new Daytona()) {
Sandbox sandbox = daytona.create();
String sessionId = "streaming-session";
sandbox.getProcess().createSession(sessionId);
SessionExecuteResponse command = sandbox.getProcess().executeSessionCommand(
sessionId,
new SessionExecuteRequest(
"for i in {1..5}; do echo \"Step $i\"; echo \"Error $i\" >&2; sleep 1; done",
true));
Thread logThread = new Thread(() -> sandbox.getProcess().getSessionCommandLogs(
sessionId,
command.getCmdId(),
stdout -> System.out.print("[STDOUT]: " + stdout),
stderr -> System.err.print("[STDERR]: " + stderr)));
logThread.start();
System.out.println("Continuing execution while logs are streaming...");
Thread.sleep(3000);
System.out.println("Other operations completed!");
logThread.join();
sandbox.delete();
}
}
}
```
```bash
# Real-time streaming uses WebSocket. Connect to:
# wss://proxy.app.daytona.io/toolbox/{sandboxId}/process/session/{sessionId}/command/{commandId}/logs?follow=true
#
# Without an SDK, set X-Daytona-Split-Output: true to receive separate stdout and stderr streams.
```
## Retrieve all existing logs
If the command has a predictable duration, or if you don't need to run it in the background but want to
periodically check all existing logs, you can use the following example to get the logs up to the current point in time.
```python
import time
from daytona import Daytona, SessionExecuteRequest
daytona = Daytona()
sandbox = daytona.create()
session_id = "exec-session-1"
sandbox.process.create_session(session_id)
# Execute a blocking command and wait for the result
command = sandbox.process.execute_session_command(
session_id, SessionExecuteRequest(command="echo 'Hello from stdout' && echo 'Hello from stderr' >&2")
)
print(f"[STDOUT]: {command.stdout}")
print(f"[STDERR]: {command.stderr}")
print(f"[OUTPUT]: {command.output}")
# Or execute command in the background and get the logs later
command = sandbox.process.execute_session_command(
session_id,
SessionExecuteRequest(
command='while true; do if (( RANDOM % 2 )); then echo "All good at $(date)"; else echo "Oops, an error at $(date)" >&2; fi; sleep 1; done',
run_async=True
)
)
time.sleep(5)
# Get the logs up to the current point in time
logs = sandbox.process.get_session_command_logs(session_id, command.cmd_id)
print(f"[STDOUT]: {logs.stdout}")
print(f"[STDERR]: {logs.stderr}")
print(f"[OUTPUT]: {logs.output}")
sandbox.delete()
```
```typescript
import { Daytona } from '@daytona/sdk'
async function main() {
const daytona = new Daytona()
const sandbox = await daytona.create()
const sessionId = "exec-session-1"
await sandbox.process.createSession(sessionId)
// Execute a blocking command and wait for the result
const command = await sandbox.process.executeSessionCommand(
sessionId,
{
command: 'echo "Hello from stdout" && echo "Hello from stderr" >&2',
},
)
console.log(`[STDOUT]: ${command.stdout}`)
console.log(`[STDERR]: ${command.stderr}`)
console.log(`[OUTPUT]: ${command.output}`)
// Or execute command in the background and get the logs later
const command2 = await sandbox.process.executeSessionCommand(
sessionId,
{
command: 'while true; do if (( RANDOM % 2 )); then echo "All good at $(date)"; else echo "Oops, an error at $(date)" >&2; fi; sleep 1; done',
runAsync: true,
},
)
await new Promise((resolve) => setTimeout(resolve, 5000))
// Get the logs up to the current point in time
const logs = await sandbox.process.getSessionCommandLogs(sessionId, command2.cmdId!)
console.log(`[STDOUT]: ${logs.stdout}`)
console.log(`[STDERR]: ${logs.stderr}`)
console.log(`[OUTPUT]: ${logs.output}`)
await sandbox.delete()
}
main()
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
sandbox = daytona.create
session_id = 'exec-session-1'
sandbox.process.create_session(session_id)
# Execute a blocking command and wait for the result
command = sandbox.process.execute_session_command(
session_id: session_id,
req: Daytona::SessionExecuteRequest.new(
command: 'echo "Hello from stdout" && echo "Hello from stderr" >&2'
)
)
puts "[STDOUT]: #{command.stdout}"
puts "[STDERR]: #{command.stderr}"
puts "[OUTPUT]: #{command.output}"
# Or execute command in the background and get the logs later
command = sandbox.process.execute_session_command(
session_id: session_id,
req: Daytona::SessionExecuteRequest.new(
command: 'while true; do if (( RANDOM % 2 )); then echo "All good at $(date)"; else echo "Oops, an error at $(date)" >&2; fi; sleep 1; done',
run_async: true
)
)
sleep(5)
# Get the logs up to the current point in time
logs = sandbox.process.get_session_command_logs(
session_id: session_id,
command_id: command.cmd_id
)
puts "[STDOUT]: #{logs.stdout}"
puts "[STDERR]: #{logs.stderr}"
puts "[OUTPUT]: #{logs.output}"
daytona.delete(sandbox)
```
```go
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/daytona/clients/sdk-go/pkg/daytona"
)
func main() {
client, _ := daytona.NewClient()
ctx := context.Background()
sandbox, _ := client.Create(ctx, nil)
sessionID := "exec-session-1"
sandbox.Process.CreateSession(ctx, sessionID)
// Execute a blocking command and wait for the result
cmd1, _ := sandbox.Process.ExecuteSessionCommand(ctx, sessionID,
`echo "Hello from stdout" && echo "Hello from stderr" >&2`, false)
if stdout, ok := cmd1["stdout"].(string); ok {
fmt.Printf("[STDOUT]: %s\n", stdout)
}
if stderr, ok := cmd1["stderr"].(string); ok {
fmt.Printf("[STDERR]: %s\n", stderr)
}
// Or execute command in the background and get the logs later
cmd := `counter=1; while (( counter <= 5 )); do echo "Count: $counter"; ((counter++)); sleep 1; done`
cmdResult, _ := sandbox.Process.ExecuteSessionCommand(ctx, sessionID, cmd, true)
cmdID, _ := cmdResult["id"].(string)
time.Sleep(5 * time.Second)
// Get the logs up to the current point in time
logs, err := sandbox.Process.GetSessionCommandLogs(ctx, sessionID, cmdID)
if err != nil {
log.Fatalf("Failed to get logs: %v", err)
}
fmt.Printf("[STDOUT]: %s\n", logs.Stdout)
fmt.Printf("[STDERR]: %s\n", logs.Stderr)
fmt.Printf("[OUTPUT]: %s\n", logs.Output)
sandbox.Delete(ctx)
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.SessionCommandLogsResponse;
import io.daytona.sdk.model.SessionExecuteRequest;
import io.daytona.sdk.model.SessionExecuteResponse;
public class App {
public static void main(String[] args) throws InterruptedException {
try (Daytona daytona = new Daytona()) {
Sandbox sandbox = daytona.create();
String sessionId = "exec-session-1";
sandbox.getProcess().createSession(sessionId);
SessionExecuteResponse command = sandbox.getProcess().executeSessionCommand(
sessionId,
new SessionExecuteRequest(
"echo 'Hello from stdout' && echo 'Hello from stderr' >&2",
false));
System.out.println("[STDOUT]: " + command.getStdout());
System.out.println("[STDERR]: " + command.getStderr());
System.out.println("[OUTPUT]: " + command.getOutput());
SessionExecuteResponse asyncCmd = sandbox.getProcess().executeSessionCommand(
sessionId,
new SessionExecuteRequest(
"while true; do if (( RANDOM % 2 )); then echo \"All good at $(date)\"; else echo \"Oops, an error at $(date)\" >&2; fi; sleep 1; done",
true));
Thread.sleep(5000);
SessionCommandLogsResponse logs = sandbox.getProcess().getSessionCommandLogs(sessionId, asyncCmd.getCmdId());
System.out.println("[STDOUT]: " + logs.getStdout());
System.out.println("[STDERR]: " + logs.getStderr());
System.out.println("[OUTPUT]: " + logs.getOutput());
sandbox.delete();
}
}
}
```
```bash
# Retrieve all logs up to the current point (HTTP GET)
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/process/session/{sessionId}/command/{commandId}/logs'
# Response (JSON, SDK >= 0.167.0):
# {
# "output": "...",
# "stdout": "...",
# "stderr": "..."
# }
```
# Daytona MCP Server
Daytona Model Context Protocol (MCP) server enables AI agents to interact with [Daytona Sandboxes](https://www.daytona.io/docs/en/sandboxes.md) programmatically. This guide covers how to set up and use the MCP server with various AI agents.
## Install Daytona CLI
Install the Daytona CLI to manage the MCP server.
```bash
brew install daytonaio/cli/daytona
```
```bash
powershell -Command "irm https://get.daytona.io/windows | iex"
```
## Authenticate with Daytona
Authenticate with Daytona to enable MCP server access.
```bash
daytona login
```
## Initialize MCP server
Initialize the MCP server with your preferred AI agent. Supported agents include Claude, Cursor, and Windsurf.
```bash
# Initialize with Claude
daytona mcp init claude
# Initialize with Cursor
daytona mcp init cursor
# Initialize with Windsurf
daytona mcp init windsurf
```
After initialization, open your AI agent application to begin using Daytona features.
## Configure MCP server
Generate MCP configuration for integration with other AI agents.
```bash
daytona mcp config
```
This command outputs a JSON configuration that you can copy into your agent's settings:
```json
{
"mcpServers": {
"daytona-mcp": {
"command": "daytona",
"args": ["mcp", "start"],
"env": {
"HOME": "${HOME}",
"PATH": "${HOME}:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin"
},
"logFile": "${HOME}/Library/Logs/daytona/daytona-mcp-server.log"
}
}
}
```
:::note
For Windows users, add the following to the `env` field:
```json
"APPDATA": "${APPDATA}"
```
:::
## Start MCP server
Manually start the MCP server.
```bash
daytona mcp start
```
## Available tools
MCP server provides the following tools for interacting with Daytona Sandboxes:
- [Sandbox management](https://www.daytona.io/docs/en/sandboxes.md)
- [File system operations](https://www.daytona.io/docs/en/file-system-operations.md)
- [Git operations](https://www.daytona.io/docs/en/git-operations.md)
- [Process and code execution](https://www.daytona.io/docs/en/process-code-execution.md)
- [Computer use](https://www.daytona.io/docs/en/computer-use.md)
- [Preview](https://www.daytona.io/docs/en/preview.md)
# Computer Use
Computer Use enables programmatic control of desktop environments within sandboxes. It provides mouse, keyboard, screenshot, screen recording, and display operations for automating GUI interactions and testing desktop applications.
Computer Use and [VNC](https://www.daytona.io/docs/en/vnc-access.md) work together to enable both manual and automated desktop interactions. VNC provides the visual interface for users to manually interact with the desktop, while Computer Use provides the programmatic API for AI agents to automate operations.
Computer Use is available for **Linux** and **Windows**. **macOS** support is currently in private alpha.
:::note[macOS access]
Computer Use for macOS is currently in private alpha and requires access. To request access, fill out the [macOS access request form](https://docs.google.com/forms/d/e/1FAIpQLSc9xlGZ49OjWNkyzDPC9Ip3InMRR0ZXY3tcoD-PFQj3ck6gzQ/viewform?usp=sharing&ouid=103304973264148733944). Our team will review your request and reach out with setup instructions.
:::
- **GUI application testing**: automate interactions with native applications, click buttons, fill forms, and validate UI behavior
- **Visual testing & screenshots**: capture screenshots of applications, compare UI states, and perform visual regression testing
- **Desktop automation**: automate repetitive desktop tasks, file management through GUI, and complex workflows
## Start Computer Use
Start all computer use processes (Xvfb, xfce4, x11vnc, novnc) in the Sandbox.
```python
result = sandbox.computer_use.start()
print("Computer use processes started:", result.message)
```
```typescript
const result = await sandbox.computerUse.start();
console.log('Computer use processes started:', result.message);
```
```ruby
result = sandbox.computer_use.start
puts "Computer use processes started: #{result.message}"
```
```go
err := sandbox.ComputerUse.Start(ctx)
if err != nil {
log.Fatal(err)
}
defer sandbox.ComputerUse.Stop(ctx)
fmt.Println("Computer use processes started")
```
```java
var result = sandbox.computerUse.start();
System.out.println("Computer use processes started: " + result.getMessage());
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/start' \
--request POST
```
## Stop Computer Use
Stop all computer use processes in the Sandbox.
```python
result = sandbox.computer_use.stop()
print("Computer use processes stopped:", result.message)
```
```typescript
const result = await sandbox.computerUse.stop();
console.log('Computer use processes stopped:', result.message);
```
```ruby
result = sandbox.computer_use.stop
puts "Computer use processes stopped: #{result.message}"
```
```go
err := sandbox.ComputerUse.Stop(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Println("Computer use processes stopped")
```
```java
var result = sandbox.computerUse.stop();
System.out.println("Computer use processes stopped: " + result.getMessage());
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/stop' \
--request POST
```
## Get status
Get the status of all computer use processes.
```python
response = sandbox.computer_use.get_status()
print("Computer use status:", response.status)
```
```typescript
const status = await sandbox.computerUse.getStatus();
console.log('Computer use status:', status.status);
```
```ruby
response = sandbox.computer_use.status
puts "Computer use status: #{response.status}"
```
```go
status, err := sandbox.ComputerUse.GetStatus(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Computer use status: %v\n", status["status"])
```
```java
var response = sandbox.computerUse.getStatus();
System.out.println("Computer use status: " + response.getStatus());
```
```bash
# Status of the computer use system
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/status'
# Status of all computer use processes
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/process-status'
```
## Get process status
Get the status of a specific VNC process.
```python
xvfb_status = sandbox.computer_use.get_process_status("xvfb")
novnc_status = sandbox.computer_use.get_process_status("novnc")
```
```typescript
const xvfbStatus = await sandbox.computerUse.getProcessStatus('xvfb');
const noVncStatus = await sandbox.computerUse.getProcessStatus('novnc');
```
```ruby
xvfb_status = sandbox.computer_use.get_process_status("xvfb")
no_vnc_status = sandbox.computer_use.get_process_status("novnc")
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/process/{processName}/status'
```
## Restart process
Restart a specific VNC process.
```python
result = sandbox.computer_use.restart_process("xfce4")
print("XFCE4 process restarted:", result.message)
```
```typescript
const result = await sandbox.computerUse.restartProcess('xfce4');
console.log('XFCE4 process restarted:', result.message);
```
```ruby
result = sandbox.computer_use.restart_process("xfce4")
puts "XFCE4 process restarted: #{result.message}"
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/process/{processName}/restart' \
--request POST
```
## Get process logs
Get logs for a specific VNC process.
```python
logs = sandbox.computer_use.get_process_logs("novnc")
print("NoVNC logs:", logs)
```
```typescript
const logsResp = await sandbox.computerUse.getProcessLogs('novnc');
console.log('NoVNC logs:', logsResp.logs);
```
```ruby
logs = sandbox.computer_use.get_process_logs("novnc")
puts "NoVNC logs: #{logs}"
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/process/{processName}/logs'
```
## Get process errors
Get error logs for a specific VNC process.
```python
errors = sandbox.computer_use.get_process_errors("x11vnc")
print("X11VNC errors:", errors)
```
```typescript
const errorsResp = await sandbox.computerUse.getProcessErrors('x11vnc');
console.log('X11VNC errors:', errorsResp.errors);
```
```ruby
errors = sandbox.computer_use.get_process_errors("x11vnc")
puts "X11VNC errors: #{errors}"
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/process/{processName}/errors'
```
## Mouse operations
### Click
Click the mouse at the specified coordinates. `button` is one of `left`, `right`, or `middle` (case-insensitive; defaults to `left`); other values return an error.
```python
# Single left click
result = sandbox.computer_use.mouse.click(100, 200)
# Double click
double_click = sandbox.computer_use.mouse.click(100, 200, "left", True)
# Right click
right_click = sandbox.computer_use.mouse.click(100, 200, "right")
```
```typescript
// Single left click
const result = await sandbox.computerUse.mouse.click(100, 200);
// Double click
const doubleClick = await sandbox.computerUse.mouse.click(100, 200, 'left', true);
// Right click
const rightClick = await sandbox.computerUse.mouse.click(100, 200, 'right');
```
```ruby
# Single left click
result = sandbox.computer_use.mouse.click(x: 100, y: 200)
# Double click
double_click = sandbox.computer_use.mouse.click(x: 100, y: 200, button: 'left', double: true)
# Right click
right_click = sandbox.computer_use.mouse.click(x: 100, y: 200, button: 'right')
```
```go
// Single left click
result, err := sandbox.ComputerUse.Mouse().Click(ctx, 100, 200, nil, nil)
if err != nil {
log.Fatal(err)
}
// Double click
doubleClick := true
result, err = sandbox.ComputerUse.Mouse().Click(ctx, 100, 200, nil, &doubleClick)
// Right click
rightButton := "right"
result, err = sandbox.ComputerUse.Mouse().Click(ctx, 100, 200, &rightButton, nil)
```
```java
// Single left click
sandbox.computerUse.click(100, 200);
// Double click
sandbox.computerUse.doubleClick(100, 200);
// Right click
sandbox.computerUse.click(100, 200, "right");
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/mouse/click' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"button": "left",
"double": true,
"x": 100,
"y": 200
}'
```
### Move
Move the mouse cursor to the specified coordinates.
```python
result = sandbox.computer_use.mouse.move(100, 200)
print(f"Mouse moved to: {result.x}, {result.y}")
```
```typescript
const result = await sandbox.computerUse.mouse.move(100, 200);
console.log(`Mouse moved to: ${result.x}, ${result.y}`);
```
```ruby
result = sandbox.computer_use.mouse.move(x: 100, y: 200)
puts "Mouse moved to: #{result.x}, #{result.y}"
```
```go
result, err := sandbox.ComputerUse.Mouse().Move(ctx, 100, 200)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Mouse moved to: %v, %v\n", result["x"], result["y"])
```
```java
var result = sandbox.computerUse.moveMouse(100, 200);
System.out.println("Mouse moved to: " + result.getX() + ", " + result.getY());
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/mouse/move' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"x": 1,
"y": 1
}'
```
### Drag
Drag the mouse from start coordinates to end coordinates.
```python
result = sandbox.computer_use.mouse.drag(50, 50, 150, 150)
print(f"Drag ended at {result.x}, {result.y}")
```
```typescript
const result = await sandbox.computerUse.mouse.drag(50, 50, 150, 150);
console.log(`Drag ended at ${result.x}, ${result.y}`);
```
```ruby
result = sandbox.computer_use.mouse.drag(start_x: 50, start_y: 50, end_x: 150, end_y: 150)
puts "Drag ended at #{result.x}, #{result.y}"
```
```go
result, err := sandbox.ComputerUse.Mouse().Drag(ctx, 50, 50, 150, 150, nil)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Dragged to %v, %v\n", result["x"], result["y"])
```
```java
var result = sandbox.computerUse.drag(50, 50, 150, 150);
System.out.println("Drag ended at: " + result.getX() + ", " + result.getY());
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/mouse/drag' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"button": "left",
"endX": 200,
"endY": 300,
"startX": 100,
"startY": 100
}'
```
### Scroll
Scroll the mouse wheel at the specified coordinates. `direction` is `up` or `down` (other values return an error). `amount` is the number of scroll wheel ticks to send — one tick is roughly one notch of a physical mouse wheel, which moves a few lines in most apps. Defaults to 1 if omitted.
```python
# Scroll up
scroll_up = sandbox.computer_use.mouse.scroll(100, 200, "up", 3)
# Scroll down
scroll_down = sandbox.computer_use.mouse.scroll(100, 200, "down", 5)
```
```typescript
// Scroll up
const scrollUp = await sandbox.computerUse.mouse.scroll(100, 200, 'up', 3);
// Scroll down
const scrollDown = await sandbox.computerUse.mouse.scroll(100, 200, 'down', 5);
```
```ruby
# Scroll up
scroll_up = sandbox.computer_use.mouse.scroll(x: 100, y: 200, direction: 'up', amount: 3)
# Scroll down
scroll_down = sandbox.computer_use.mouse.scroll(x: 100, y: 200, direction: 'down', amount: 5)
```
```go
// Scroll up
amount := 3
success, err := sandbox.ComputerUse.Mouse().Scroll(ctx, 100, 200, "up", &amount)
if err != nil {
log.Fatal(err)
}
// Scroll down
amount = 5
success, err = sandbox.ComputerUse.Mouse().Scroll(ctx, 100, 200, "down", &amount)
```
```java
// Scroll up (negative vertical delta maps to "up")
sandbox.computerUse.scroll(100, 200, 0, -3);
// Scroll down
sandbox.computerUse.scroll(100, 200, 0, 5);
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/mouse/scroll' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"amount": 3,
"direction": "down",
"x": 100,
"y": 200
}'
```
### Get position
Get the current mouse cursor position.
```python
position = sandbox.computer_use.mouse.get_position()
print(f"Mouse is at: {position.x}, {position.y}")
```
```typescript
const position = await sandbox.computerUse.mouse.getPosition();
console.log(`Mouse is at: ${position.x}, ${position.y}`);
```
```ruby
position = sandbox.computer_use.mouse.position
puts "Mouse is at: #{position.x}, #{position.y}"
```
```go
position, err := sandbox.ComputerUse.Mouse().GetPosition(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Mouse is at: %v, %v\n", position["x"], position["y"])
```
```java
var position = sandbox.computerUse.getMousePosition();
System.out.println("Mouse is at: " + position.getX() + ", " + position.getY());
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/mouse/position'
```
## Keyboard operations
### Type
Types arbitrary text, including uppercase letters, symbols, and non-ASCII characters. Newlines (`\n`, `\r`, `\r\n`) are translated into Enter key presses; literal tab and other control characters are rejected.
```python
sandbox.computer_use.keyboard.type("Hello, World!")
# With delay between characters
sandbox.computer_use.keyboard.type("Slow typing", 100)
```
```typescript
await sandbox.computerUse.keyboard.type('Hello, World!');
// With delay between characters
await sandbox.computerUse.keyboard.type('Slow typing', 100);
```
```ruby
sandbox.computer_use.keyboard.type(text: "Hello, World!")
# With delay between characters
sandbox.computer_use.keyboard.type(text: "Slow typing", delay: 100)
```
```go
err := sandbox.ComputerUse.Keyboard().Type(ctx, "Hello, World!", nil)
if err != nil {
log.Fatal(err)
}
// With delay between characters
delay := 100
err = sandbox.ComputerUse.Keyboard().Type(ctx, "Slow typing", &delay)
```
```java
sandbox.computerUse.typeText("Hello, World!");
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/keyboard/type' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"delay": 1,
"text": ""
}'
```
### Press
Press a key with optional modifiers.
```python
# Press Enter
sandbox.computer_use.keyboard.press("enter")
# Press Ctrl+C
sandbox.computer_use.keyboard.press("c", ["ctrl"])
# Press Ctrl+Shift+T
sandbox.computer_use.keyboard.press("t", ["ctrl", "shift"])
```
```typescript
// Press Enter
await sandbox.computerUse.keyboard.press('enter');
// Press Ctrl+C
await sandbox.computerUse.keyboard.press('c', ['ctrl']);
// Press Ctrl+Shift+T
await sandbox.computerUse.keyboard.press('t', ['ctrl', 'shift']);
```
```ruby
# Press Enter
sandbox.computer_use.keyboard.press(key: "enter")
# Press Ctrl+C
sandbox.computer_use.keyboard.press(key: "c", modifiers: ["ctrl"])
# Press Ctrl+Shift+T
sandbox.computer_use.keyboard.press(key: "t", modifiers: ["ctrl", "shift"])
```
```go
// Press Enter
err := sandbox.ComputerUse.Keyboard().Press(ctx, "enter", nil)
if err != nil {
log.Fatal(err)
}
// Press Ctrl+C
err = sandbox.ComputerUse.Keyboard().Press(ctx, "c", []string{"ctrl"})
// Press Ctrl+Shift+T
err = sandbox.ComputerUse.Keyboard().Press(ctx, "t", []string{"ctrl", "shift"})
```
```java
// Press Enter
sandbox.computerUse.pressKey("enter");
// Press Ctrl+C
sandbox.computerUse.pressHotkey("ctrl", "c");
// Press Ctrl+Shift+T
sandbox.computerUse.pressHotkey("ctrl", "shift", "t");
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/keyboard/key' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"key": "enter",
"modifiers": []
}'
```
### Hotkey
Press a hotkey combination.
```python
# Copy
sandbox.computer_use.keyboard.hotkey("ctrl+c")
# Paste
sandbox.computer_use.keyboard.hotkey("ctrl+v")
# Alt+Tab
sandbox.computer_use.keyboard.hotkey("alt+tab")
```
```typescript
// Copy
await sandbox.computerUse.keyboard.hotkey('ctrl+c');
// Paste
await sandbox.computerUse.keyboard.hotkey('ctrl+v');
// Alt+Tab
await sandbox.computerUse.keyboard.hotkey('alt+tab');
```
```ruby
# Copy
sandbox.computer_use.keyboard.hotkey(keys: "ctrl+c")
# Paste
sandbox.computer_use.keyboard.hotkey(keys: "ctrl+v")
# Alt+Tab
sandbox.computer_use.keyboard.hotkey(keys: "alt+tab")
```
```go
// Copy
err := sandbox.ComputerUse.Keyboard().Hotkey(ctx, "ctrl+c")
if err != nil {
log.Fatal(err)
}
// Paste
err = sandbox.ComputerUse.Keyboard().Hotkey(ctx, "ctrl+v")
// Alt+Tab
err = sandbox.ComputerUse.Keyboard().Hotkey(ctx, "alt+tab")
```
```java
// Copy
sandbox.computerUse.pressHotkey("ctrl", "c");
// Paste
sandbox.computerUse.pressHotkey("ctrl", "v");
// Alt+Tab
sandbox.computerUse.pressHotkey("alt", "tab");
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/keyboard/hotkey' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"keys": "ctrl+c"
}'
```
### Supported keys
`keyboard.press()` and `keyboard.hotkey()` are case-insensitive for named keys. The following are supported:
| Category | Keys |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------- |
| Modifiers | `ctrl`, `alt`, `shift`, `cmd` |
| Editing | `enter`, `escape`, `tab`, `backspace`, `delete`, `space` |
| Navigation | `home`, `end`, `pageup`, `pagedown`, `insert`, arrow keys (`up`, `down`, `left`, `right`) |
| Function keys | `f1` through `f24` |
| Numpad | `num0`–`num9`, `num_plus`, `num_minus`, `num_asterisk`, `num_slash`, `num_decimal`, `num_enter`, `num_equal`, `num_lock` |
| Letters and digits | `a`–`z` (case-insensitive), `0`–`9` |
| Punctuation | `` ` `` `-` `=` `[` `]` `\` `;` `'` `,` `.` `/` |
| Other | `capslock`, `menu` |
Common aliases like `Return` → `enter`, `control` → `ctrl`, `command` / `meta` / `win` → `cmd`, and `option` → `alt` are normalized automatically. Unsupported or malformed inputs return an error, sometimes with a suggested alternative.
## Accessibility operations
Use Linux accessibility operations to inspect the AT-SPI tree and interact with UI elements by node ID. Start Computer Use before calling accessibility methods.
:::note[App accessibility support]
Accessibility operations read the semantic UI information that applications expose over AT-SPI. Apps or custom widgets that do not expose accessibility objects may return sparse nodes, generic roles, or no actionable nodes; mouse, keyboard, and screenshot operations remain available for those cases.
:::
### Get tree
Read an accessibility tree for the focused app, a specific process, or all apps.
```python
# Focused app
focused_tree = sandbox.computer_use.accessibility.get_tree(scope="focused", max_depth=2)
# Specific process
process_tree = sandbox.computer_use.accessibility.get_tree(
scope="pid",
pid=1234,
max_depth=2,
)
# All apps
desktop_tree = sandbox.computer_use.accessibility.get_tree(scope="all", max_depth=2)
```
```typescript
// Focused app
const focusedTree = await sandbox.computerUse.accessibility.getTree({
scope: 'focused',
maxDepth: 2,
});
// Specific process
const processTree = await sandbox.computerUse.accessibility.getTree({
scope: 'pid',
pid: 1234,
maxDepth: 2,
});
// All apps
const desktopTree = await sandbox.computerUse.accessibility.getTree({
scope: 'all',
maxDepth: 2,
});
```
```ruby
# Focused app
focused_tree = sandbox.computer_use.accessibility.get_tree(scope: "focused", max_depth: 2)
# Specific process
process_tree = sandbox.computer_use.accessibility.get_tree(
scope: "pid",
pid: 1234,
max_depth: 2
)
# All apps
desktop_tree = sandbox.computer_use.accessibility.get_tree(scope: "all", max_depth: 2)
```
```go
maxDepth := 2
// Focused app
focusedScope := "focused"
focusedTree, err := sandbox.ComputerUse.Accessibility().GetTree(ctx, &daytona.AccessibilityTreeOptions{
Scope: &focusedScope,
MaxDepth: &maxDepth,
})
if err != nil {
log.Fatal(err)
}
// Specific process
processScope := "pid"
pid := 1234
processTree, err := sandbox.ComputerUse.Accessibility().GetTree(ctx, &daytona.AccessibilityTreeOptions{
Scope: &processScope,
PID: &pid,
MaxDepth: &maxDepth,
})
if err != nil {
log.Fatal(err)
}
// All apps
allScope := "all"
desktopTree, err := sandbox.ComputerUse.Accessibility().GetTree(ctx, &daytona.AccessibilityTreeOptions{
Scope: &allScope,
MaxDepth: &maxDepth,
})
if err != nil {
log.Fatal(err)
}
```
```java
// Focused app
var focusedTree = sandbox.computerUse.getAccessibilityTree("focused", null, 2);
// Specific process
var processTree = sandbox.computerUse.getAccessibilityTree("pid", 1234, 2);
// All apps
var desktopTree = sandbox.computerUse.getAccessibilityTree("all", null, 2);
```
```bash
# Focused app
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/a11y/tree?scope=focused&maxDepth=2'
# Specific process
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/a11y/tree?scope=pid&pid=1234&maxDepth=2'
# All apps
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/a11y/tree?scope=all&maxDepth=2'
```
### Find nodes
Search the accessibility tree by role, accessible name, state, and scope.
```python
# Find buttons by accessible name
buttons = sandbox.computer_use.accessibility.find_nodes(
scope="focused",
role="button",
name="Submit",
name_match="substring",
limit=10,
)
# Find text entries in a process
entries = sandbox.computer_use.accessibility.find_nodes(
scope="pid",
pid=1234,
role="entry",
states=["enabled", "focusable"],
limit=10,
)
# Find visible nodes across all apps
visible_nodes = sandbox.computer_use.accessibility.find_nodes(
scope="all",
states=["visible"],
limit=20,
)
```
```typescript
// Find buttons by accessible name
const buttons = await sandbox.computerUse.accessibility.findNodes({
scope: 'focused',
role: 'button',
name: 'Submit',
nameMatch: 'substring',
limit: 10,
});
// Find text entries in a process
const entries = await sandbox.computerUse.accessibility.findNodes({
scope: 'pid',
pid: 1234,
role: 'entry',
states: ['enabled', 'focusable'],
limit: 10,
});
// Find visible nodes across all apps
const visibleNodes = await sandbox.computerUse.accessibility.findNodes({
scope: 'all',
states: ['visible'],
limit: 20,
});
```
```ruby
# Find buttons by accessible name
buttons = sandbox.computer_use.accessibility.find_nodes(
scope: "focused",
role: "button",
name: "Submit",
name_match: "substring",
limit: 10
)
# Find text entries in a process
entries = sandbox.computer_use.accessibility.find_nodes(
scope: "pid",
pid: 1234,
role: "entry",
states: ["enabled", "focusable"],
limit: 10
)
# Find visible nodes across all apps
visible_nodes = sandbox.computer_use.accessibility.find_nodes(
scope: "all",
states: ["visible"],
limit: 20
)
```
```go
limit := 10
// Find buttons by accessible name
focusedScope := "focused"
buttonRole := "button"
submitName := "Submit"
substringMatch := "substring"
buttons, err := sandbox.ComputerUse.Accessibility().FindNodes(ctx, &daytona.AccessibilityFindOptions{
Scope: &focusedScope,
Role: &buttonRole,
Name: &submitName,
NameMatch: &substringMatch,
Limit: &limit,
})
if err != nil {
log.Fatal(err)
}
// Find text entries in a process
processScope := "pid"
pid := 1234
entryRole := "entry"
entries, err := sandbox.ComputerUse.Accessibility().FindNodes(ctx, &daytona.AccessibilityFindOptions{
Scope: &processScope,
PID: &pid,
Role: &entryRole,
States: []string{"enabled", "focusable"},
Limit: &limit,
})
if err != nil {
log.Fatal(err)
}
// Find visible nodes across all apps
allScope := "all"
visibleLimit := 20
visibleNodes, err := sandbox.ComputerUse.Accessibility().FindNodes(ctx, &daytona.AccessibilityFindOptions{
Scope: &allScope,
States: []string{"visible"},
Limit: &visibleLimit,
})
if err != nil {
log.Fatal(err)
}
```
```java
// Find buttons by accessible name
var buttons = sandbox.computerUse.findAccessibilityNodes(
new FindAccessibilityNodesRequest()
.scope("focused")
.role("button")
.name("Submit")
.nameMatch("substring")
.limit(10)
);
// Find text entries in a process
var entries = sandbox.computerUse.findAccessibilityNodes(
new FindAccessibilityNodesRequest()
.scope("pid")
.pid(1234)
.role("entry")
.states(java.util.List.of("enabled", "focusable"))
.limit(10)
);
// Find visible nodes across all apps
var visibleNodes = sandbox.computerUse.findAccessibilityNodes(
new FindAccessibilityNodesRequest()
.scope("all")
.states(java.util.List.of("visible"))
.limit(20)
);
```
```bash
# Find buttons by accessible name
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/a11y/find' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"scope": "focused",
"role": "button",
"name": "Submit",
"nameMatch": "substring",
"limit": 10
}'
# Find text entries in a process
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/a11y/find' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"scope": "pid",
"pid": 1234,
"role": "entry",
"states": ["enabled", "focusable"],
"limit": 10
}'
# Find visible nodes across all apps
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/a11y/find' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"scope": "all",
"states": ["visible"],
"limit": 20
}'
```
### Focus node
Move keyboard focus to a node returned by `get_tree` or `find_nodes`.
```python
sandbox.computer_use.accessibility.focus_node("node-id")
```
```typescript
await sandbox.computerUse.accessibility.focusNode('node-id');
```
```ruby
sandbox.computer_use.accessibility.focus_node(id: "node-id")
```
```go
if err := sandbox.ComputerUse.Accessibility().FocusNode(ctx, "node-id"); err != nil {
log.Fatal(err)
}
```
```java
sandbox.computerUse.focusAccessibilityNode("node-id");
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/a11y/node/focus' \
--request POST \
--header 'Content-Type: application/json' \
--data '{"id":"node-id"}'
```
### Invoke node
Run a node action, such as pressing a button.
```python
# Invoke the primary action
sandbox.computer_use.accessibility.invoke_node("node-id")
# Invoke a named action
sandbox.computer_use.accessibility.invoke_node("node-id", action="click")
```
```typescript
// Invoke the primary action
await sandbox.computerUse.accessibility.invokeNode('node-id');
// Invoke a named action
await sandbox.computerUse.accessibility.invokeNode('node-id', 'click');
```
```ruby
# Invoke the primary action
sandbox.computer_use.accessibility.invoke_node(id: "node-id")
# Invoke a named action
sandbox.computer_use.accessibility.invoke_node(id: "node-id", action: "click")
```
```go
// Invoke the primary action
if err := sandbox.ComputerUse.Accessibility().InvokeNode(ctx, "node-id", nil); err != nil {
log.Fatal(err)
}
// Invoke a named action
action := "click"
if err := sandbox.ComputerUse.Accessibility().InvokeNode(ctx, "node-id", &action); err != nil {
log.Fatal(err)
}
```
```java
// Invoke the primary action
sandbox.computerUse.invokeAccessibilityNode("node-id");
// Invoke a named action
sandbox.computerUse.invokeAccessibilityNode("node-id", "click");
```
```bash
# Invoke the primary action
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/a11y/node/invoke' \
--request POST \
--header 'Content-Type: application/json' \
--data '{"id":"node-id"}'
# Invoke a named action
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/a11y/node/invoke' \
--request POST \
--header 'Content-Type: application/json' \
--data '{"id":"node-id","action":"click"}'
```
### Set node value
Write text or value content to nodes that support value changes.
```python
sandbox.computer_use.accessibility.set_node_value("node-id", "hello")
```
```typescript
await sandbox.computerUse.accessibility.setNodeValue('node-id', 'hello');
```
```ruby
sandbox.computer_use.accessibility.set_node_value(id: "node-id", value: "hello")
```
```go
if err := sandbox.ComputerUse.Accessibility().SetNodeValue(ctx, "node-id", "hello"); err != nil {
log.Fatal(err)
}
```
```java
sandbox.computerUse.setAccessibilityNodeValue("node-id", "hello");
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/a11y/node/value' \
--request POST \
--header 'Content-Type: application/json' \
--data '{"id":"node-id","value":"hello"}'
```
## Screenshot operations
### Take full screen
Take a screenshot of the entire screen.
```python
screenshot = sandbox.computer_use.screenshot.take_full_screen()
print(f"Screenshot size: {screenshot.width}x{screenshot.height}")
# With cursor visible
with_cursor = sandbox.computer_use.screenshot.take_full_screen(True)
```
```typescript
const screenshot = await sandbox.computerUse.screenshot.takeFullScreen();
console.log(`Screenshot size: ${screenshot.width}x${screenshot.height}`);
// With cursor visible
const withCursor = await sandbox.computerUse.screenshot.takeFullScreen(true);
```
```ruby
screenshot = sandbox.computer_use.screenshot.take_full_screen
puts "Screenshot size: #{screenshot.width}x#{screenshot.height}"
# With cursor visible
with_cursor = sandbox.computer_use.screenshot.take_full_screen(show_cursor: true)
```
```go
screenshot, err := sandbox.ComputerUse.Screenshot().TakeFullScreen(ctx, nil)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Screenshot captured, size: %d bytes\n", *screenshot.SizeBytes)
// With cursor visible
showCursor := true
withCursor, err := sandbox.ComputerUse.Screenshot().TakeFullScreen(ctx, &showCursor)
```
```java
var screenshot = sandbox.computerUse.takeScreenshot();
Integer sizeBytes = screenshot.getSizeBytes();
System.out.println("Screenshot payload size: " + (sizeBytes != null ? sizeBytes + " bytes" : "n/a"));
// With cursor visible
var withCursor = sandbox.computerUse.takeScreenshot(true);
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/screenshot'
```
### Take region
Take a screenshot of a specific region.
```python
from daytona import ScreenshotRegion
region = ScreenshotRegion(x=100, y=100, width=300, height=200)
screenshot = sandbox.computer_use.screenshot.take_region(region)
print(f"Captured region: {screenshot.region.width}x{screenshot.region.height}")
```
```typescript
const region = { x: 100, y: 100, width: 300, height: 200 };
const screenshot = await sandbox.computerUse.screenshot.takeRegion(region);
console.log(`Captured region: ${screenshot.region.width}x${screenshot.region.height}`);
```
```ruby
region = Daytona::ComputerUse::ScreenshotRegion.new(x: 100, y: 100, width: 300, height: 200)
screenshot = sandbox.computer_use.screenshot.take_region(region: region)
puts "Captured region: #{screenshot.region.width}x#{screenshot.region.height}"
```
```go
region := types.ScreenshotRegion{X: 100, Y: 100, Width: 300, Height: 200}
screenshot, err := sandbox.ComputerUse.Screenshot().TakeRegion(ctx, region, nil)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Captured region: %dx%d\n", screenshot.Width, screenshot.Height)
```
```java
var screenshot = sandbox.computerUse.takeRegionScreenshot(100, 100, 300, 200);
Integer sizeBytes = screenshot.getSizeBytes();
System.out.println("Captured region, payload size: " + (sizeBytes != null ? sizeBytes + " bytes" : "n/a"));
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/screenshot/region?x=1&y=1&width=1&height=1'
```
### Take compressed
Take a compressed screenshot of the entire screen.
```python
from daytona import ScreenshotOptions
# Default compression
screenshot = sandbox.computer_use.screenshot.take_compressed()
# High quality JPEG
jpeg = sandbox.computer_use.screenshot.take_compressed(
ScreenshotOptions(format="jpeg", quality=95, show_cursor=True)
)
# Scaled down PNG
scaled = sandbox.computer_use.screenshot.take_compressed(
ScreenshotOptions(format="png", scale=0.5)
)
```
```typescript
// Default compression
const screenshot = await sandbox.computerUse.screenshot.takeCompressed();
// High quality JPEG
const jpeg = await sandbox.computerUse.screenshot.takeCompressed({
format: 'jpeg',
quality: 95,
showCursor: true
});
// Scaled down PNG
const scaled = await sandbox.computerUse.screenshot.takeCompressed({
format: 'png',
scale: 0.5
});
```
```ruby
# Default compression
screenshot = sandbox.computer_use.screenshot.take_compressed
# High quality JPEG
jpeg = sandbox.computer_use.screenshot.take_compressed(
options: Daytona::ComputerUse::ScreenshotOptions.new(format: "jpeg", quality: 95, show_cursor: true)
)
# Scaled down PNG
scaled = sandbox.computer_use.screenshot.take_compressed(
options: Daytona::ComputerUse::ScreenshotOptions.new(format: "png", scale: 0.5)
)
```
```java
// Compressed full screen (format, quality 1-100, scale factor)
var screenshot = sandbox.computerUse.takeCompressedScreenshot("png", 80, 1.0);
// High quality JPEG at full scale
var jpeg = sandbox.computerUse.takeCompressedScreenshot("jpeg", 95, 1.0);
// Scaled down PNG
var scaled = sandbox.computerUse.takeCompressedScreenshot("png", 80, 0.5);
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/screenshot/compressed'
```
### Take compressed region
Take a compressed screenshot of a specific region.
```python
from daytona import ScreenshotRegion, ScreenshotOptions
region = ScreenshotRegion(x=0, y=0, width=800, height=600)
screenshot = sandbox.computer_use.screenshot.take_compressed_region(
region,
ScreenshotOptions(format="jpeg", quality=80, show_cursor=True)
)
print(f"Compressed size: {screenshot.size_bytes} bytes")
```
```typescript
const region = { x: 0, y: 0, width: 800, height: 600 };
const screenshot = await sandbox.computerUse.screenshot.takeCompressedRegion(region, {
format: 'jpeg',
quality: 80,
showCursor: true
});
console.log(`Compressed size: ${screenshot.size_bytes} bytes`);
```
```ruby
region = Daytona::ComputerUse::ScreenshotRegion.new(x: 0, y: 0, width: 800, height: 600)
screenshot = sandbox.computer_use.screenshot.take_compressed_region(
region: region,
options: Daytona::ComputerUse::ScreenshotOptions.new(format: "jpeg", quality: 80, show_cursor: true)
)
puts "Compressed size: #{screenshot.size_bytes} bytes"
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/screenshot/region/compressed?x=1&y=1&width=1&height=1'
```
## Screen Recording
Computer Use supports screen recording capabilities, allowing you to capture desktop sessions for debugging, documentation, or automation workflows.
### Configure Recording Directory
By default, recordings are saved to `~/.daytona/recordings`. You can specify a custom directory by passing the `DAYTONA_RECORDINGS_DIR` environment variable when creating a sandbox:
```python
from daytona import Daytona, CreateSandboxFromSnapshotParams
daytona = Daytona()
sandbox = daytona.create(
CreateSandboxFromSnapshotParams(
snapshot="daytonaio/sandbox:0.6.0",
name="my-sandbox",
env_vars={"DAYTONA_RECORDINGS_DIR": "/home/daytona/my-recordings"}
)
)
```
```typescript
import { Daytona } from '@daytona/sdk';
const daytona = new Daytona();
const sandbox = await daytona.create({
snapshot: 'daytonaio/sandbox:0.6.0',
name: 'my-sandbox',
envVars: { DAYTONA_RECORDINGS_DIR: '/home/daytona/my-recordings' }
});
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
sandbox = daytona.create(Daytona::CreateSandboxFromSnapshotParams.new(
snapshot: 'daytonaio/sandbox:0.6.0',
env_vars: { 'DAYTONA_RECORDINGS_DIR' => '/home/daytona/my-recordings' }
))
```
```go
import (
"github.com/daytona/clients/sdk-go/pkg/daytona"
"github.com/daytona/clients/sdk-go/pkg/types"
)
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
sandbox, err := client.Create(ctx, types.SnapshotParams{
SandboxBaseParams: types.SandboxBaseParams{
Name: "my-sandbox",
EnvVars: map[string]string{
"DAYTONA_RECORDINGS_DIR": "/home/daytona/my-recordings",
},
},
Snapshot: "daytonaio/sandbox:0.6.0",
})
if err != nil {
log.Fatal(err)
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
import java.util.Map;
try (Daytona daytona = new Daytona()) {
CreateSandboxFromSnapshotParams params = new CreateSandboxFromSnapshotParams();
params.setSnapshot("daytonaio/sandbox:0.6.0");
params.setName("my-sandbox");
params.setEnvVars(Map.of("DAYTONA_RECORDINGS_DIR", "/home/daytona/my-recordings"));
Sandbox sandbox = daytona.create(params);
}
```
### Start Recording
Start a new screen recording session with an optional name identifier:
```python
# Start recording with a custom name
recording = sandbox.computer_use.recording.start("test-1")
print(f"Recording started: {recording.id}")
print(f"File path: {recording.file_path}")
```
```typescript
// Start recording with a custom name
const recording = await sandbox.computerUse.recording.start('test-1');
console.log(`Recording started: ${recording.id}`);
console.log(`File path: ${recording.file_path}`);
```
```ruby
# Start recording with a custom label
recording = sandbox.computer_use.recording.start(label: 'test-1')
puts "Recording started: #{recording.id}"
puts "File path: #{recording.file_path}"
```
```go
// Start recording with a custom name
name := "test-1"
recording, err := sandbox.ComputerUse.Recording().Start(ctx, &name)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Recording started: %s\n", *recording.Id)
fmt.Printf("File path: %s\n", *recording.FilePath)
```
```java
// Start recording with a custom label
var recording = sandbox.computerUse.startRecording("test-1");
System.out.println("Recording started: " + recording.getId());
System.out.println("File path: " + recording.getFilePath());
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/recordings/start' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"name": "test-1"
}'
```
### Stop Recording
Stop an active recording session by providing the recording ID:
```python
# Stop the recording
stopped_recording = sandbox.computer_use.recording.stop(recording.id)
print(f"Recording stopped: {stopped_recording.duration_seconds} seconds")
print(f"Saved to: {stopped_recording.file_path}")
```
```typescript
// Stop the recording
const stoppedRecording = await sandbox.computerUse.recording.stop(recording.id);
console.log(`Recording stopped: ${stoppedRecording.duration_seconds} seconds`);
console.log(`Saved to: ${stoppedRecording.file_path}`);
```
```ruby
# Stop the recording
stopped_recording = sandbox.computer_use.recording.stop(id: recording.id)
puts "Recording stopped: #{stopped_recording.duration_seconds} seconds"
puts "Saved to: #{stopped_recording.file_path}"
```
```go
// Stop the recording
stoppedRecording, err := sandbox.ComputerUse.Recording().Stop(ctx, *recording.Id)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Recording stopped: %f seconds\n", *stoppedRecording.DurationSeconds)
fmt.Printf("Saved to: %s\n", *stoppedRecording.FilePath)
```
```java
// Stop the recording
var stoppedRecording = sandbox.computerUse.stopRecording(recording.getId());
System.out.println("Recording stopped: " + stoppedRecording.getDurationSeconds() + " seconds");
System.out.println("Saved to: " + stoppedRecording.getFilePath());
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/recordings/stop' \
--request POST \
--header 'Content-Type: application/json' \
--data '{
"id": "recording-id"
}'
```
### List Recordings
Get a list of all recordings in the sandbox:
```python
recordings_list = sandbox.computer_use.recording.list()
print(f"Total recordings: {len(recordings_list.recordings)}")
for rec in recordings_list.recordings:
print(f"- {rec.name}: {rec.duration_seconds}s ({rec.file_size_bytes} bytes)")
```
```typescript
const recordingsList = await sandbox.computerUse.recording.list();
console.log(`Total recordings: ${recordingsList.recordings.length}`);
recordingsList.recordings.forEach(rec => {
console.log(`- ${rec.name}: ${rec.duration_seconds}s (${rec.file_size_bytes} bytes)`);
});
```
```ruby
recordings_list = sandbox.computer_use.recording.list
puts "Total recordings: #{recordings_list.recordings.length}"
recordings_list.recordings.each do |rec|
puts "- #{rec.name}: #{rec.duration_seconds}s (#{rec.file_size_bytes} bytes)"
end
```
```go
recordingsList, err := sandbox.ComputerUse.Recording().List(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Total recordings: %d\n", len(recordingsList.Recordings))
for _, rec := range recordingsList.Recordings {
fmt.Printf("- %s: %.2fs (%d bytes)\n", *rec.Name, *rec.DurationSeconds, *rec.FileSizeBytes)
}
```
```java
var recordingsList = sandbox.computerUse.listRecordings();
System.out.println("Total recordings: " + recordingsList.getRecordings().size());
for (var rec : recordingsList.getRecordings()) {
System.out.println(
"- " + rec.getFileName() + ": " + rec.getDurationSeconds() + "s (" + rec.getSizeBytes() + " bytes)"
);
}
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/recordings'
```
### Get Recording
Get details about a specific recording:
```python
recording_detail = sandbox.computer_use.recording.get("recording-id")
print(f"Recording: {recording_detail.name}")
print(f"Status: {recording_detail.status}")
print(f"Duration: {recording_detail.duration_seconds}s")
```
```typescript
const recordingDetail = await sandbox.computerUse.recording.get('recording-id');
console.log(`Recording: ${recordingDetail.name}`);
console.log(`Status: ${recordingDetail.status}`);
console.log(`Duration: ${recordingDetail.duration_seconds}s`);
```
```ruby
recording_detail = sandbox.computer_use.recording.get(id: 'recording-id')
puts "Recording: #{recording_detail.name}"
puts "Status: #{recording_detail.status}"
puts "Duration: #{recording_detail.duration_seconds}s"
```
```go
recordingDetail, err := sandbox.ComputerUse.Recording().Get(ctx, "recording-id")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Recording: %s\n", *recordingDetail.Name)
fmt.Printf("Status: %s\n", *recordingDetail.Status)
fmt.Printf("Duration: %.2fs\n", *recordingDetail.DurationSeconds)
```
```java
var recordingDetail = sandbox.computerUse.getRecording("recording-id");
System.out.println("Recording: " + recordingDetail.getFileName());
System.out.println("Status: " + recordingDetail.getStatus());
System.out.println("Duration: " + recordingDetail.getDurationSeconds() + "s");
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/recordings/{id}'
```
### Delete Recording
Delete a recording by ID:
```python
sandbox.computer_use.recording.delete("recording-id")
print("Recording deleted successfully")
```
```typescript
await sandbox.computerUse.recording.delete('recording-id');
console.log('Recording deleted successfully');
```
```ruby
sandbox.computer_use.recording.delete(id: 'recording-id')
puts 'Recording deleted successfully'
```
```go
err := sandbox.ComputerUse.Recording().Delete(ctx, "recording-id")
if err != nil {
log.Fatal(err)
}
fmt.Println("Recording deleted successfully")
```
```java
sandbox.computerUse.deleteRecording("recording-id");
System.out.println("Recording deleted successfully");
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/recordings/{id}' \
--request DELETE
```
### Download Recording
Download a recording file from the sandbox to your local machine. The file is streamed efficiently without loading the entire content into memory, making it suitable for large recordings.
```python
# Download recording to local file
sandbox.computer_use.recording.download(recording.id, "local_recording.mp4")
print("Recording downloaded successfully")
# Or with custom path
import os
download_path = os.path.join("recordings", f"recording_{recording.id}.mp4")
sandbox.computer_use.recording.download(recording.id, download_path)
```
```typescript
// Download recording to local file
await sandbox.computerUse.recording.download(recording.id, 'local_recording.mp4');
console.log('Recording downloaded successfully');
// Or with custom path
const downloadPath = `recordings/recording_${recording.id}.mp4`;
await sandbox.computerUse.recording.download(recording.id, downloadPath);
```
```ruby
# Download recording to local file
sandbox.computer_use.recording.download(id: recording.id, local_path: 'local_recording.mp4')
puts 'Recording downloaded successfully'
# Or with custom path
download_path = "recordings/recording_#{recording.id}.mp4"
sandbox.computer_use.recording.download(id: recording.id, local_path: download_path)
```
```go
// Download recording to local file
err := sandbox.ComputerUse.Recording().Download(ctx, recording.GetId(), "local_recording.mp4")
if err != nil {
log.Fatal(err)
}
fmt.Println("Recording downloaded successfully")
// Or with custom path
downloadPath := fmt.Sprintf("recordings/recording_%s.mp4", recording.GetId())
err = sandbox.ComputerUse.Recording().Download(ctx, recording.GetId(), downloadPath)
```
```java
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
// Download returns a temp file from the API client; copy it to a stable path
var tempFile = sandbox.computerUse.downloadRecording(recording.getId());
Files.copy(tempFile.toPath(), Path.of("local_recording.mp4"), StandardCopyOption.REPLACE_EXISTING);
System.out.println("Recording saved to local_recording.mp4");
var downloadPath = Path.of("recordings", "recording_" + recording.getId() + ".mp4");
Files.createDirectories(downloadPath.getParent());
Files.copy(tempFile.toPath(), downloadPath, StandardCopyOption.REPLACE_EXISTING);
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/recordings/{id}/download' \
--output local_recording.mp4
```
:::tip[Streaming Downloads]
All SDK implementations stream the recording file directly to disk without loading the entire content into memory. This allows you to download large recordings (hundreds of MB or even GB) efficiently without running out of memory.
- **Python**: Streams in 64KB chunks using `httpx`
- **TypeScript**: Uses Node.js `pipeline()` with backpressure handling
- **Ruby**: Uses Typhoeus streaming with `on_body` callbacks
- **Go**: Uses `io.Copy()` with 32KB internal buffer
- **Java**: The OpenAPI client streams the response body into a temporary file via OkHttp
:::
### Recording Dashboard
Every sandbox includes a built-in recording dashboard for managing screen recordings through a web interface. The dashboard allows you to view, download, and delete recordings without writing code.
To access the recording dashboard:
1. Navigate to your sandboxes in the Daytona Dashboard
2. Click the action menu (three dots) for your sandbox
3. Select **Screen Recordings** from the dropdown menu
The recording dashboard provides:
- List of all recordings with metadata (name, duration, file size, creation time)
- Playback controls for reviewing recordings
- Download functionality to save recordings locally
- Delete options for managing storage
:::tip
The recording dashboard runs on a private port and is automatically secured. No additional authentication is required once you access it through the Daytona Dashboard.
:::
## Display operations
### Get info
Get information about the displays.
```python
info = sandbox.computer_use.display.get_info()
print(f"Primary display: {info.primary_display.width}x{info.primary_display.height}")
print(f"Total displays: {info.total_displays}")
for i, display in enumerate(info.displays):
print(f"Display {i}: {display.width}x{display.height} at {display.x},{display.y}")
```
```typescript
const info = await sandbox.computerUse.display.getInfo();
console.log(`Primary display: ${info.primary_display.width}x${info.primary_display.height}`);
console.log(`Total displays: ${info.total_displays}`);
info.displays.forEach((display, index) => {
console.log(`Display ${index}: ${display.width}x${display.height} at ${display.x},${display.y}`);
});
```
```ruby
info = sandbox.computer_use.display.info
puts "Primary display: #{info.primary_display.width}x#{info.primary_display.height}"
puts "Total displays: #{info.total_displays}"
info.displays.each_with_index do |display, i|
puts "Display #{i}: #{display.width}x#{display.height} at #{display.x},#{display.y}"
end
```
```go
info, err := sandbox.ComputerUse.Display().GetInfo(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Displays: %v\n", info["displays"])
```
```java
var info = sandbox.computerUse.getDisplayInfo();
if (info.getDisplays() != null) {
for (var display : info.getDisplays()) {
System.out.println(
"Display " + display.getId() + ": " + display.getWidth() + "x" + display.getHeight()
+ " at " + display.getX() + "," + display.getY()
);
}
}
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/display/info'
```
### Get windows
Get the list of open windows.
```python
windows = sandbox.computer_use.display.get_windows()
print(f"Found {windows.count} open windows:")
for window in windows.windows:
print(f"- {window.title} (ID: {window.id})")
```
```typescript
const windows = await sandbox.computerUse.display.getWindows();
console.log(`Found ${windows.count} open windows:`);
windows.windows.forEach(window => {
console.log(`- ${window.title} (ID: ${window.id})`);
});
```
```ruby
windows = sandbox.computer_use.display.windows
puts "Found #{windows.count} open windows:"
windows.windows.each do |window|
puts "- #{window.title} (ID: #{window.id})"
end
```
```go
result, err := sandbox.ComputerUse.Display().GetWindows(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Open windows: %v\n", result["windows"])
```
```java
var windows = sandbox.computerUse.getWindows();
var list = windows.getWindows();
if (list != null) {
System.out.println("Found " + list.size() + " open windows:");
for (var window : list) {
System.out.println("- " + window.getTitle() + " (ID: " + window.getId() + ")");
}
}
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/display/windows'
```
# Agent Skills
An agent skill for creating and running sandboxes on the Daytona platform. Includes references and practical patterns for the Daytona API, CLI, Python, TypeScript, Go, and Ruby SDKs.
## Installation
Daytona provides multiple options to install the agent skill.
### Skills
```bash
npx skills add https://github.com/daytona/skills --skill daytona
```
### Claude Code (Plugin)
```bash
claude plugin marketplace add daytona/skills
claude plugin install daytona@daytona --scope user
```
Scope options: `user` (all projects), `project` (shared via git), `local` (gitignored).
### Manual
Clone the Daytona skills repository:
```bash
git clone https://github.com/daytona/skills.git
```
Copy the `daytona` subdirectory into your agent's `skills` directory:
| **Agent** | **Skill directory** |
| ------------ | -------------------------------- |
| Claude Code | **`~/.claude/skills/`** |
| Cursor | **`~/.cursor/skills/`** |
| OpenCode | **`~/.config/opencode/skills/`** |
| OpenAI Codex | **`~/.codex/skills/`** |
| Windsurf | **`~/.windsurf/skills/`** |
## Usage
The agent skill loads automatically when a request involves Daytona platform or sandbox management features. To use it manually, reference the skill directly in your prompt or code.
## Structure
```text
skills/daytona/
├── SKILL.md # Main entrypoint
└── references/ # Reference directories
├── api/ # API reference documentation
├── platform/ # Platform reference documentation
├── python-sdk/ # Python SDK reference documentation
│ ├── sync/ # Sync Python SDK reference documentation
│ └── async/ # Async Python SDK reference documentation
├── typescript-sdk/ # TypeScript SDK reference documentation
├── go-sdk/ # Go SDK reference documentation
├── ruby-sdk/ # Ruby SDK reference documentation
└── cli.md # CLI reference documentation
```
# Web Terminal
Daytona provides a browser-based web terminal for interacting with your sandboxes. The web terminal allows you to run commands, view files, and debug directly from your browser without installing any local tools.
- **Remote command execution**: run shell commands directly in your sandbox
- **File management**: navigate the file system, view and edit files
- **Debugging**: inspect logs, monitor processes, and troubleshoot issues
- **Package management**: install dependencies and configure your environment
## Access from Dashboard
Access the web terminal directly from the [Daytona Dashboard ↗](https://app.daytona.io/dashboard/sandboxes).
1. Go to [Sandboxes ↗](https://app.daytona.io/dashboard/sandboxes)
2. Locate the running sandbox you want to access
3. Click the terminal icon **`>_`**
This opens the web terminal in a new browser tab, providing a full terminal session connected to your sandbox. The web terminal is available only for sandboxes in the `STARTED` state. If your sandbox is stopped, start it before attempting to access the terminal.
## Access via CLI
When you create a sandbox using the Daytona CLI, the web terminal URL is displayed automatically in the output.
```shell
daytona create
```
The CLI output includes the terminal URL:
```text
Sandbox '' created successfully
Connect via SSH: daytona ssh
Open the Web Terminal: https://22222-.proxy.daytona.work
```
## Access via URL
The web terminal runs on port `22222` inside each sandbox. You can obtain the terminal URL programmatically using [Preview URLs](https://www.daytona.io/docs/en/preview.md).
Pass port `22222` to the preview URL method:
```python
terminal_info = sandbox.get_preview_link(22222)
print(f"Web Terminal URL: {terminal_info.url}")
```
```typescript
const terminalInfo = await sandbox.getPreviewLink(22222);
console.log(`Web Terminal URL: ${terminalInfo.url}`);
```
```ruby
terminal_info = sandbox.preview_url(22222)
puts "Web Terminal URL: #{terminal_info.url}"
```
```go
url, err := sandbox.GetPreviewLink(ctx, 22222)
```
```bash
daytona preview-url --port 22222
```
```bash
curl 'https://app.daytona.io/api/sandbox/{sandboxId}/ports/22222/preview-url' \
--header 'Authorization: Bearer '
```
## Security
Terminal access is restricted to authenticated members of your [Organization](https://www.daytona.io/docs/en/organizations.md). Even when a sandbox has its `public` parameter set to `true`, the web terminal remains accessible only to organization members.
:::warning
The web terminal provides full shell access to your sandbox. Treat terminal URLs with the same care as SSH credentials. Do not share terminal URLs with untrusted parties.
:::
## Related
- [SSH Access](https://www.daytona.io/docs/en/ssh-access.md): connect to your sandbox from a local terminal or IDE
- [Pseudo Terminal (PTY)](https://www.daytona.io/docs/en/pty.md): programmatic terminal sessions for automated workflows
- [Process and Code Execution](https://www.daytona.io/docs/en/process-code-execution.md): execute commands and run code in sandboxes
- [Preview](https://www.daytona.io/docs/en/preview.md): generate preview URLs for accessing sandbox services
# SSH Access
Daytona provides SSH access to your sandboxes using token-based authentication. This allows you to connect from local terminals, IDEs, and development tools without installing additional software.
## Access from Dashboard
Create an SSH access token directly from the [Daytona Dashboard ↗](https://app.daytona.io/dashboard/sandboxes).
1. Go to [Sandboxes ↗](https://app.daytona.io/dashboard/sandboxes)
2. Locate the sandbox you want to create an SSH access token for
3. Click the sandbox options menu (**⋮**)
4. Select **Create SSH Access**
5. Set the expiration time (defaults to 60 minutes)
6. Click **Create**
Daytona generates a token and displays it in the modal. Copy the token and use it to connect to your sandbox.
## Access via CLI
Daytona provides a CLI command to create an SSH access token for a sandbox:
```shell
daytona create
```
When you create a sandbox, Daytona displays the SSH command automatically in the output:
```text
Sandbox '' created successfully
Connect via SSH: daytona ssh
Open the Web Terminal: https://22222-.proxy.daytona.work
```
To SSH into an existing sandbox, use the following command:
```bash
daytona ssh --expires 60
```
## Access via token
You can create SSH access tokens programmatically. The token can then be used to connect manually:
```python
from daytona import Daytona
daytona = Daytona()
sandbox = daytona.get("sandbox-abc123")
# Create SSH access token
ssh_access = sandbox.create_ssh_access(expires_in_minutes=60)
print(f"SSH Token: {ssh_access.token}")
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
const sandbox = await daytona.get('sandbox-abc123')
// Create SSH access token
const sshAccess = await sandbox.createSshAccess(60)
console.log(`SSH Token: ${sshAccess.token}`)
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
sandbox = daytona.get('sandbox-abc123')
# Create SSH access token
ssh_access = sandbox.create_ssh_access(expires_in_minutes: 60)
puts "SSH Token: #{ssh_access.token}"
```
```bash
curl 'https://app.daytona.io/api/sandbox/{sandboxId}/ssh-access?expiresInMinutes=60' \
--request POST \
--header 'Authorization: Bearer '
```
To connect to your sandbox, use the following command:
```bash
ssh @ssh.app.daytona.io
```
## Connect with VS Code
You can connect VS Code directly to your sandbox using the Remote SSH extension.
1. Install the [Remote Explorer extension ↗](https://marketplace.visualstudio.com/items?itemName=ms-vscode.remote-explorer)
2. Add a new SSH connection
3. When prompted for the SSH connection URL, paste the SSH command from above
For more information, see the [VS Code Remote SSH documentation ↗](https://code.visualstudio.com/docs/remote/ssh).
## Connect with JetBrains IDEs
JetBrains Gateway provides remote development support for connecting to your sandbox.
1. Download [JetBrains Gateway ↗](https://www.jetbrains.com/remote-development/gateway/)
2. Add a new connection
3. When prompted for the SSH connection URL, paste the SSH command from above
4. Select the IDE to install in your sandbox
## Token management
### Expiration
SSH access tokens expire automatically after 60 minutes. You can specify a custom expiration time when creating the token using the `expires_in_minutes` parameter.
### Revoke token
Revoke SSH access tokens before expiry:
```python
# Revoke specific SSH access token for the sandbox
sandbox.revoke_ssh_access(token="specific-token")
```
```typescript
// Revoke specific SSH access token for the sandbox
await sandbox.revokeSshAccess('specific-token')
```
```ruby
# Revoke specific SSH access token for the sandbox
sandbox.revoke_ssh_access(token: 'specific-token')
```
```bash
# Revoke specific SSH access token
curl 'https://app.daytona.io/api/sandbox/{sandboxId}/ssh-access?token=specific-token' \
--request DELETE \
--header 'Authorization: Bearer '
# Revoke all SSH access for the sandbox
curl 'https://app.daytona.io/api/sandbox/{sandboxId}/ssh-access' \
--request DELETE \
--header 'Authorization: Bearer '
```
## Related
- [Web Terminal](https://www.daytona.io/docs/en/web-terminal.md): browser-based terminal access to sandboxes
- [Preview](https://www.daytona.io/docs/en/preview.md): generate preview URLs for accessing sandbox services
# VNC Access
VNC (Virtual Network Computing) access provides a graphical desktop environment for your Daytona Sandbox directly in the browser. This allows you to interact with GUI applications, desktop tools, and visual interfaces running inside your sandbox.
VNC and [Computer Use](https://www.daytona.io/docs/en/computer-use.md) work together to enable both manual and automated desktop interactions. VNC provides the visual interface for users to manually interact with the desktop, while Computer Use provides the programmatic API for AI agents to automate mouse, keyboard, and screenshot operations. Through VNC, you can observe AI agents performing automated tasks via Computer Use in real-time.
:::note[Sandbox image requirement]
VNC and Computer Use require a sandbox with the default image. Sandboxes created with custom images do not include VNC support unless you install the [required packages](#required-packages).
:::
## Access VNC from Dashboard
Access the VNC desktop environment directly from the [Daytona Dashboard ↗](https://app.daytona.io/dashboard/sandboxes).
1. Go to [Daytona Sandboxes ↗](https://app.daytona.io/dashboard/sandboxes)
2. Locate the sandbox you want to access via VNC
3. Click the options menu (**⋮**) next to the sandbox
4. Select **VNC** from the dropdown menu
This opens a VNC viewer in your browser with a **Connect** button.
5. Click **Connect** to establish the VNC session
Once connected, a full desktop environment loads in your browser, providing mouse and keyboard control over the sandbox's graphical interface.
:::note
VNC sessions remain active as long as the sandbox is running. If the sandbox auto-stops due to inactivity, you need to start the sandbox again before reconnecting via VNC.
:::
## Programmatic VNC management
Daytona provides methods to [start](#start-vnc), [stop](#stop-vnc), and [monitor](#get-vnc-status) VNC sessions and processes programmatically using the [Computer Use](https://www.daytona.io/docs/en/computer-use.md) references as part of automated workflows.
### Start VNC
Start all VNC processes (Xvfb, xfce4, x11vnc, novnc) in the sandbox to enable desktop access.
```python
result = sandbox.computer_use.start()
print("VNC processes started:", result.message)
```
```typescript
const result = await sandbox.computerUse.start();
console.log('VNC processes started:', result.message);
```
```ruby
result = sandbox.computer_use.start
puts "VNC processes started: #{result.message}"
```
```go
err := sandbox.ComputerUse.Start(ctx)
if err != nil {
log.Fatal(err)
}
defer sandbox.ComputerUse.Stop(ctx)
fmt.Println("VNC processes started")
```
```java
var result = sandbox.computerUse.start();
System.out.println("VNC processes started: " + result.getMessage());
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/start' \
--request POST
```
### Stop VNC
Stop all VNC processes in the sandbox.
```python
result = sandbox.computer_use.stop()
print("VNC processes stopped:", result.message)
```
```typescript
const result = await sandbox.computerUse.stop();
console.log('VNC processes stopped:', result.message);
```
```ruby
result = sandbox.computer_use.stop
puts "VNC processes stopped: #{result.message}"
```
```go
err := sandbox.ComputerUse.Stop(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Println("VNC processes stopped")
```
```java
var result = sandbox.computerUse.stop();
System.out.println("VNC processes stopped: " + result.getMessage());
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/stop' \
--request POST
```
### Get VNC status
Check the status of VNC processes to verify they are running.
```python
response = sandbox.computer_use.get_status()
print("VNC status:", response.status)
```
```typescript
const status = await sandbox.computerUse.getStatus();
console.log('VNC status:', status.status);
```
```ruby
response = sandbox.computer_use.status
puts "VNC status: #{response.status}"
```
```go
status, err := sandbox.ComputerUse.GetStatus(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("VNC status: %v\n", status["status"])
```
```java
var response = sandbox.computerUse.getStatus();
System.out.println("VNC status: " + response.getStatus());
```
```bash
curl 'https://proxy.app.daytona.io/toolbox/{sandboxId}/computeruse/status'
```
For additional process management operations including restarting individual processes and viewing logs, see the [Computer Use](https://www.daytona.io/docs/en/computer-use.md) reference.
## Automating desktop interactions
Once VNC is running, you can automate desktop interactions using Computer Use. This enables AI agents to programmatically control the mouse, keyboard, and capture screenshots within the VNC session.
**Available operations:**
- **Mouse**: click, move, drag, scroll, and get cursor position
- **Keyboard**: type text, press keys, and execute hotkey combinations
- **Screenshot**: capture full screen, regions, or compressed images
- **Display**: get display information and list open windows
For complete documentation on automating desktop interactions, see [Computer Use](https://www.daytona.io/docs/en/computer-use.md).
> **Example**: Automated browser interaction
```python
# Start VNC processes
sandbox.computer_use.start()
# Click to open browser
sandbox.computer_use.mouse.click(50, 50)
# Type a URL
sandbox.computer_use.keyboard.type("https://www.daytona.io/docs/")
sandbox.computer_use.keyboard.press("enter")
# Take a screenshot
screenshot = sandbox.computer_use.screenshot.take_full_screen()
```
```typescript
// Start VNC processes
await sandbox.computerUse.start();
// Click to open browser
await sandbox.computerUse.mouse.click(50, 50);
// Type a URL
await sandbox.computerUse.keyboard.type('https://www.daytona.io/docs/');
await sandbox.computerUse.keyboard.press('enter');
// Take a screenshot
const screenshot = await sandbox.computerUse.screenshot.takeFullScreen();
```
```java
// Start VNC processes
sandbox.computerUse.start();
// Click to open browser
sandbox.computerUse.click(50, 50);
// Type a URL
sandbox.computerUse.typeText("https://www.daytona.io/docs/");
sandbox.computerUse.pressKey("enter");
// Take a screenshot
var screenshot = sandbox.computerUse.takeScreenshot();
```
## Required packages
The default sandbox image includes all packages required for VNC and Computer Use. If you are using a custom image, you need to install the following packages.
### VNC and desktop environment
| Package | Description |
| -------------------- | ------------------------------------------ |
| **`xvfb`** | X Virtual Framebuffer for headless display |
| **`xfce4`** | Desktop environment |
| **`xfce4-terminal`** | Terminal emulator |
| **`x11vnc`** | VNC server |
| **`novnc`** | Web-based VNC client |
| **`dbus-x11`** | D-Bus session support |
### X11 libraries
| Library | Description |
| ----------------- | ------------------------------------------- |
| **`libx11-6`** | X11 client library |
| **`libxrandr2`** | X11 RandR extension (display configuration) |
| **`libxext6`** | X11 extensions library |
| **`libxrender1`** | X11 rendering extension |
| **`libxfixes3`** | X11 fixes extension |
| **`libxss1`** | X11 screen saver extension |
| **`libxtst6`** | X11 testing extension (input simulation) |
| **`libxi6`** | X11 input extension |
# VPN Connections
VPN connections are a way to connect your Daytona sandboxes to private networks. By establishing a VPN connection, your sandbox can access network resources using private IP addresses and can be accessed by other devices on the same VPN network.
This integration enables communication between your development environment and existing infrastructure, allowing you to test applications against services within the private network, access shared development resources, and collaborate with team members.
Daytona supports the following VPN network providers:
- [Tailscale](#tailscale)
- [OpenVPN](#openvpn)
- [Netbird](#netbird)
:::note
For connecting to a VPN network, you need to [create or access your Daytona sandbox](https://www.daytona.io/docs/en/sandboxes.md), **have access to your VPN network provider credentials**, and be on [**Tier 3** or higher](https://www.daytona.io/docs/en/limits.md#resources).
:::
## Tailscale
Daytona provides multiple ways to connect to a Daytona sandbox with a Tailscale network:
- [Connect with browser login](#tailscale-browser-login)
- [Connect with auth key](#tailscale-auth-key)
- [Connect with web terminal](#tailscale-web-terminal)
When you connect a Daytona sandbox to a Tailscale network, the sandbox becomes part of your private Tailscale network, allowing you to access resources that are available within the network and enabling other devices on the network to access the sandbox.
This integration makes your sandbox appear as a device within your Tailscale network, with its own Tailscale IP address and access to other devices and services on the network.
### Tailscale browser login
The browser login method initiates an interactive authentication flow where Tailscale generates a unique login URL that you visit in your web browser to authenticate the Dayona sandbox.
The process involves installing Tailscale, starting the daemon, initiating the login process, and then polling for the authentication status until the connection is established.
The following snippet demonstrates connecting to a Tailscale network using a browser login.
```python
from daytona import Daytona, DaytonaConfig
import time
import re
# Configuration
DAYTONA_API_KEY = "YOUR_API_KEY" # Replace with your API key
# Initialize the Daytona client
config = DaytonaConfig(api_key=DAYTONA_API_KEY)
daytona = Daytona(config)
def setup_tailscale_vpn_interactive():
"""
Connect a Daytona sandbox to a Tailscale network using the Python SDK.
Uses interactive login via browser URL (no auth key required).
"""
# Create the sandbox
print("Creating sandbox...")
sandbox = daytona.create()
print(f"Sandbox created: {sandbox.id}")
# Step 1: Install Tailscale
print("\nInstalling Tailscale (this may take a few minutes)...")
response = sandbox.process.exec(
"curl -fsSL https://tailscale.com/install.sh | sh",
timeout=300
)
if response.exit_code != 0:
print(f"Error installing Tailscale: {response.result}")
return sandbox
print("Tailscale installed successfully.")
# Step 2: Start tailscaled daemon in background
print("\nStarting tailscaled daemon...")
sandbox.process.exec("nohup sudo tailscaled > /dev/null 2>&1 &", timeout=10)
# Wait for daemon to initialize
time.sleep(3)
# Step 3: Run tailscale up in background and capture output to a file
print("\nInitiating Tailscale login...")
sandbox.process.exec(
"sudo tailscale up > /tmp/tailscale-login.txt 2>&1 &",
timeout=10
)
# Wait for the login URL to be written to the file
time.sleep(3)
# Read the login URL from the output file
response = sandbox.process.exec("cat /tmp/tailscale-login.txt", timeout=10)
output = response.result
url_match = re.search(r'https://login\.tailscale\.com/a/[^\s]+', output)
if url_match:
login_url = url_match.group(0)
print(f"\n{'='*60}")
print("To authenticate, visit this URL in your browser:")
print(f"\n {login_url}")
print(f"\n{'='*60}")
print("\nWaiting for authentication...")
# Poll for connection status
max_wait = 300
poll_interval = 5
waited = 0
while waited < max_wait:
time.sleep(poll_interval)
waited += poll_interval
status_response = sandbox.process.exec("tailscale status 2>&1", timeout=30)
status_output = status_response.result
# Check if connected
if status_response.exit_code == 0 and "logged out" not in status_output.lower():
# Verify IP is assigned
ip_response = sandbox.process.exec("tailscale ip -4 2>&1", timeout=10)
if ip_response.exit_code == 0 and ip_response.result.strip():
print(f"\nConnected to Tailscale network!")
print(f"Tailscale IP: {ip_response.result.strip()}")
break
print(f" Still waiting... ({waited}s)")
else:
print("\nTimeout waiting for authentication. Please try again.")
return sandbox
else:
# Already connected or different output
print(f"Output from tailscale up:\n{output}")
# Check if already connected
status_response = sandbox.process.exec("tailscale status", timeout=30)
if status_response.exit_code == 0:
print("\nTailscale status:")
print(status_response.result)
# Final status check
print("\nFinal Tailscale status:")
response = sandbox.process.exec("tailscale status", timeout=30)
print(response.result)
return sandbox
if __name__ == "__main__":
sandbox = setup_tailscale_vpn_interactive()
```
```typescript
import { Daytona } from '@daytona/sdk'
// Configuration
const DAYTONA_API_KEY = 'YOUR_API_KEY' // Replace with your API key
// Initialize the Daytona client
const daytona = new Daytona({
apiKey: DAYTONA_API_KEY,
})
function sleep(ms: number): Promise {
return new Promise(resolve => setTimeout(resolve, ms))
}
async function setupTailscaleVpnInteractive(): Promise {
/**
* Connect a Daytona sandbox to a Tailscale network using the TypeScript SDK.
* Uses interactive login via browser URL (no auth key required).
*/
// Create the sandbox
console.log('Creating sandbox...')
const sandbox = await daytona.create()
console.log(`Sandbox created: ${sandbox.id}`)
// Step 1: Install Tailscale
console.log('\nInstalling Tailscale (this may take a few minutes)...')
let response = await sandbox.process.executeCommand(
'curl -fsSL https://tailscale.com/install.sh | sh',
undefined, // cwd
undefined, // env
300 // timeout
)
if (response.exitCode !== 0) {
console.log(`Error installing Tailscale: ${response.result}`)
return
}
console.log('Tailscale installed successfully.')
// Step 2: Start tailscaled daemon in background
console.log('\nStarting tailscaled daemon...')
await sandbox.process.executeCommand(
'nohup sudo tailscaled > /dev/null 2>&1 &',
undefined, // cwd
undefined, // env
10 // timeout
)
// Wait for daemon to initialize
await sleep(3000)
// Step 3: Run tailscale up in background and capture output to a file
console.log('\nInitiating Tailscale login...')
await sandbox.process.executeCommand(
'sudo tailscale up > /tmp/tailscale-login.txt 2>&1 &',
undefined, // cwd
undefined, // env
10 // timeout
)
// Wait for the login URL to be written to the file
await sleep(3000)
// Read the login URL from the output file
response = await sandbox.process.executeCommand(
'cat /tmp/tailscale-login.txt',
undefined, // cwd
undefined, // env
10 // timeout
)
const output = response.result || ''
const urlMatch = output.match(/https:\/\/login\.tailscale\.com\/a\/[^\s]+/)
if (urlMatch) {
const loginUrl = urlMatch[0]
console.log('\n' + '='.repeat(60))
console.log('To authenticate, visit this URL in your browser:')
console.log(`\n ${loginUrl}`)
console.log('\n' + '='.repeat(60))
console.log('\nWaiting for authentication...')
// Poll for connection status
const maxWait = 300 // 5 minutes max wait
const pollInterval = 5
let waited = 0
while (waited < maxWait) {
await sleep(pollInterval * 1000)
waited += pollInterval
const statusResponse = await sandbox.process.executeCommand(
'tailscale status 2>&1',
undefined, // cwd
undefined, // env
30 // timeout
)
const statusOutput = statusResponse.result || ''
// Check if we're connected (status shows our machine without login prompt)
if (
statusResponse.exitCode === 0 &&
!statusOutput.toLowerCase().includes('logged out')
) {
// Verify we have an IP assigned
const ipResponse = await sandbox.process.executeCommand(
'tailscale ip -4 2>&1',
undefined, // cwd
undefined, // env
10 // timeout
)
if (ipResponse.exitCode === 0 && ipResponse.result?.trim()) {
console.log('\nConnected to Tailscale network!')
console.log(`Tailscale IP: ${ipResponse.result.trim()}`)
break
}
}
console.log(` Still waiting... (${waited}s)`)
}
if (waited >= maxWait) {
console.log('\nTimeout waiting for authentication. Please try again.')
return
}
} else {
// Maybe already connected or different output
console.log(`Output from tailscale up:\n${output}`)
// Check if already connected
const statusResponse = await sandbox.process.executeCommand(
'tailscale status',
undefined, // cwd
undefined, // env
30 // timeout
)
if (statusResponse.exitCode === 0) {
console.log('\nTailscale status:')
console.log(statusResponse.result)
}
}
// Final status check
console.log('\nFinal Tailscale status:')
response = await sandbox.process.executeCommand(
'tailscale status',
undefined, // cwd
undefined, // env
30 // timeout
)
console.log(response.result)
}
// Run the main function
setupTailscaleVpnInteractive().catch(console.error)
```
Once the connection is established and authentication is complete, the sandbox will maintain its connection as long as the service is running.
### Tailscale auth key
Using an auth key provides a non-interactive way to connect your Daytona sandbox to Tailscale, making it suitable for automated scripts, CI/CD pipelines, or any scenario where manual browser interaction is not available.
1. Access your [Tailscale admin console ↗](https://login.tailscale.com/admin/machines)
2. Click **Add device** and select **Linux server**
3. Apply the configuration and click **Generate install script**
This will generate a script that you can use to install Tailscale and connect to the Tailscale network.
```bash
curl -fsSL https://tailscale.com/install.sh | sh && sudo tailscale up --auth-key=tskey-auth-
```
Copy the auth key from the generated script and use it to connect your Daytona sandbox to Tailscale:
```python
from daytona import Daytona, DaytonaConfig
import time
# Configuration
DAYTONA_API_KEY = "YOUR_API_KEY" # Replace with your API key
TAILSCALE_AUTH_KEY = "YOUR_AUTH_KEY" # Replace with your auth key
# Initialize the Daytona client
config = DaytonaConfig(api_key=DAYTONA_API_KEY)
daytona = Daytona(config)
def setup_tailscale_vpn(auth_key: str):
"""
Connect a Daytona sandbox to a Tailscale network using the Python SDK.
Uses auth-key for non-interactive authentication.
"""
# Create the sandbox
print("Creating sandbox...")
sandbox = daytona.create()
print(f"Sandbox created: {sandbox.id}")
# Step 1: Install Tailscale
print("\nInstalling Tailscale (this may take a few minutes)...")
response = sandbox.process.exec(
"curl -fsSL https://tailscale.com/install.sh | sh",
timeout=300
)
if response.exit_code != 0:
print(f"Error installing Tailscale: {response.result}")
return sandbox
print("Tailscale installed successfully.")
# Step 2: Start tailscaled daemon manually (systemd doesn't auto-start in sandboxes)
print("\nStarting tailscaled daemon...")
sandbox.process.exec("nohup sudo tailscaled > /dev/null 2>&1 &", timeout=10)
# Wait for daemon to initialize
time.sleep(3)
# Step 3: Connect with auth key
print("\nConnecting to Tailscale network...")
response = sandbox.process.exec(
f"sudo tailscale up --auth-key={auth_key}",
timeout=60
)
if response.exit_code != 0:
print(f"Error connecting: {response.result}")
return sandbox
print("Connected to Tailscale network.")
# Verify connection status
print("\nChecking Tailscale status...")
response = sandbox.process.exec("tailscale status", timeout=30)
print(f"Status:\n{response.result}")
return sandbox
if __name__ == "__main__":
sandbox = setup_tailscale_vpn(TAILSCALE_AUTH_KEY)
```
```typescript
import { Daytona } from '@daytona/sdk'
// Configuration
const DAYTONA_API_KEY = 'YOUR_API_KEY' // Replace with your API key
const TAILSCALE_AUTH_KEY = 'YOUR_AUTH_KEY' // Replace with your auth key
// Initialize the Daytona client
const daytona = new Daytona({
apiKey: DAYTONA_API_KEY,
})
function sleep(ms: number): Promise {
return new Promise(resolve => setTimeout(resolve, ms))
}
async function setupTailscaleVpn(authKey: string): Promise {
/**
* Connect a Daytona sandbox to a Tailscale network using the TypeScript SDK.
* Uses auth-key for non-interactive authentication.
*/
// Create the sandbox
console.log('Creating sandbox...')
const sandbox = await daytona.create()
console.log(`Sandbox created: ${sandbox.id}`)
// Step 1: Install Tailscale
console.log('\nInstalling Tailscale (this may take a few minutes)...')
let response = await sandbox.process.executeCommand(
'curl -fsSL https://tailscale.com/install.sh | sh',
undefined, // cwd
undefined, // env
300 // timeout
)
if (response.exitCode !== 0) {
console.log(`Error installing Tailscale: ${response.result}`)
return
}
console.log('Tailscale installed successfully.')
// Step 2: Start tailscaled daemon manually (systemd doesn't auto-start in sandboxes)
console.log('\nStarting tailscaled daemon...')
await sandbox.process.executeCommand(
'nohup sudo tailscaled > /dev/null 2>&1 &',
undefined, // cwd
undefined, // env
10 // timeout
)
// Wait for daemon to initialize
await sleep(3000)
// Step 3: Connect with auth key
console.log('\nConnecting to Tailscale network...')
response = await sandbox.process.executeCommand(
`sudo tailscale up --auth-key=${authKey}`,
undefined, // cwd
undefined, // env
60 // timeout
)
if (response.exitCode !== 0) {
console.log(`Error connecting: ${response.result}`)
return
}
console.log('Connected to Tailscale network.')
// Verify connection status
console.log('\nChecking Tailscale status...')
response = await sandbox.process.executeCommand(
'tailscale status',
undefined, // cwd
undefined, // env
30 // timeout
)
console.log(`Status:\n${response.result}`)
}
// Run the main function
setupTailscaleVpn(TAILSCALE_AUTH_KEY).catch(console.error)
```
Once the connection is established and authentication is complete, the sandbox will maintain its connection as long as the service is running.
### Tailscale web terminal
For working directly in the terminal or for more control over the Tailscale connection process, you can set up Tailscale manually through the Daytona [web terminal](https://www.daytona.io/docs/en/web-terminal.md) or [SSH](https://www.daytona.io/docs/en/ssh-access.md).
This approach provides visibility into each step of the installation and connection process, and allows you to customize the setup if needed. The process involves installing Tailscale, starting the daemon in a persistent session using tmux, and then authenticating through the interactive login flow.
1. Navigate to your sandbox [web terminal](https://www.daytona.io/docs/en/web-terminal.md) in [Daytona Dashboard ↗](https://app.daytona.io/), or [access it via SSH](https://www.daytona.io/docs/en/ssh-access.md)
2. Install Tailscale using the official installation script:
```bash
curl -fsSL https://tailscale.com/install.sh | sh
```
This begins the Tailscale installation process and initializes the Tailscale CLI inside the sandbox. Daytona requires the Tailscale daemon to be running in the background to connect the sandbox to the Tailscale network.
The recommended approach is to run it in a detached tmux (or similar) session to ensure the daemon is running in the background:
3. Install tmux
```bash
sudo apt install tmux
```
4. Start the Tailscale daemon in a detached tmux session
```bash
tmux new -d -s tailscale 'sudo tailscaled'
```
5. Connect and authenticate your sandbox with the Tailscale network
```bash
sudo tailscale up
```
6. Visit the authentication URL in the web browser and follow the instructions to authenticate
```txt
To authenticate, visit:
https://login.tailscale.com/a/
```
Once authenticated, you will see the following confirmation message:
```txt
Your device is logged in to the tailnet.
```
You've now successfully connected your Daytona sandbox to your Tailscale network. The sandbox should appear in your [Tailscale dashboard](https://login.tailscale.com/admin/machines).
## OpenVPN
Daytona provides multiple ways to connect to a Daytona sandbox with an OpenVPN network:
- [OpenVPN client configuration](#openvpn-client-configuration)
- [Connect with web terminal](#openvpn-web-terminal)
OpenVPN uses a client-server model where your Daytona sandbox acts as a client connecting to an OpenVPN server. This approach is suitable for connecting to existing corporate VPNs, accessing resources behind firewalls, or integrating with infrastructure that already uses OpenVPN.
:::note
Connecting a Daytona sandbox to OpenVPN network requires a [client configuration file](#openvpn-client-configuration-file).
:::
### OpenVPN client configuration file
Client configuration file contains the connection parameters, certificates, and keys required to establish a secure connection to your OpenVPN server. This file is typically provided by your network administrator or generated from your OpenVPN server setup.
The configuration file should be named `client.ovpn` or similar, and it must contain all the required connection settings, including server address, port, protocol, encryption settings, and authentication credentials. To create this file, you can use a text editor such as nano or vim, or upload it to your sandbox if you have it prepared elsewhere.
The following snippet is an example of a client configuration file. Replace the placeholders with the actual values provided by your OpenVPN server.
```bash
client
proto udp
explicit-exit-notify
remote
dev tun
resolv-retry infinite
nobind
persist-key
persist-tun
remote-cert-tls server
verify-x509-name name
auth SHA256
auth-nocache
cipher AES-128-GCM
ignore-unknown-option data-ciphers
data-ciphers AES-128-GCM
ncp-ciphers AES-128-GCM
tls-client
tls-version-min 1.2
tls-cipher TLS-ECDHE-ECDSA-WITH-AES-128-GCM-SHA256
tls-ciphersuites TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256
ignore-unknown-option block-outside-dns
setenv opt block-outside-dns # Prevent Windows 10 DNS leak
verb 3
-----BEGIN CERTIFICATE-----
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
-----END CERTIFICATE-----
-----BEGIN PRIVATE KEY-----
-----END PRIVATE KEY-----
-----BEGIN OpenVPN tls-crypt-v2 client key-----
-----END OpenVPN tls-crypt-v2 client key-----
```
### OpenVPN client configuration
The following snippets demonstrate connecting to an OpenVPN network using a client configuration file.
```python
from daytona import Daytona, DaytonaConfig
import time
# Configuration
DAYTONA_API_KEY = "YOUR_API_KEY" # Replace with your API key
# OpenVPN client configuration (paste your .ovpn config here)
OPENVPN_CONFIG = """
""".strip()
# Initialize the Daytona client
config = DaytonaConfig(api_key=DAYTONA_API_KEY)
daytona = Daytona(config)
def setup_openvpn(ovpn_config: str):
"""
Connect a Daytona sandbox to an OpenVPN network using the Python SDK.
"""
# Create the sandbox
print("Creating sandbox...")
sandbox = daytona.create()
print(f"Sandbox created: {sandbox.id}")
# Step 1: Install OpenVPN
print("\nInstalling OpenVPN...")
response = sandbox.process.exec(
"sudo apt update && sudo apt install -y openvpn",
timeout=120
)
if response.exit_code != 0:
print(f"Error installing OpenVPN: {response.result}")
return sandbox
print("OpenVPN installed successfully.")
# Step 2: Write the OpenVPN config file
print("\nWriting OpenVPN configuration...")
sandbox.fs.upload_file(ovpn_config.encode(), "/home/daytona/client.ovpn")
print("Configuration written to /home/daytona/client.ovpn")
# Step 3: Start OpenVPN in background
print("\nStarting OpenVPN tunnel...")
sandbox.process.exec(
"nohup sudo openvpn /home/daytona/client.ovpn > /tmp/openvpn.log 2>&1 &",
timeout=10
)
# Wait for connection to establish
print("Waiting for VPN connection to establish...")
time.sleep(10)
# Step 4: Verify connection
print("\nVerifying OpenVPN connection...")
# Check if tun interface exists
response = sandbox.process.exec("ip addr show tun0", timeout=10)
if response.exit_code == 0:
print("VPN tunnel interface (tun0) is up:")
print(response.result)
else:
print("Warning: tun0 interface not found. Checking OpenVPN logs...")
log_response = sandbox.process.exec("cat /tmp/openvpn.log", timeout=10)
print(f"OpenVPN log:\n{log_response.result}")
return sandbox
# Get public IP through VPN
print("\nChecking public IP (should be VPN server IP)...")
response = sandbox.process.exec("curl -s ifconfig.me", timeout=30)
if response.exit_code == 0:
print(f"Public IP: {response.result}")
else:
print(f"Could not determine public IP: {response.result}")
print("\nOpenVPN connection established successfully.")
return sandbox
if __name__ == "__main__":
sandbox = setup_openvpn(OPENVPN_CONFIG)
```
```typescript
import { Daytona } from '@daytona/sdk';
// Configuration
const DAYTONA_API_KEY = "YOUR_API_KEY"; // Replace with your API key
// OpenVPN client configuration (paste your .ovpn config here)
const OPENVPN_CONFIG = `
`.trim();
// Initialize the Daytona client
const daytona = new Daytona({
apiKey: DAYTONA_API_KEY,
});
function sleep(ms: number): Promise {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function setupOpenvpn(ovpnConfig: string): Promise {
/**
* Connect a Daytona sandbox to an OpenVPN network using the TypeScript SDK.
*/
// Create the sandbox
console.log("Creating sandbox...");
const sandbox = await daytona.create();
console.log(`Sandbox created: ${sandbox.id}`);
// Step 1: Install OpenVPN
console.log("\nInstalling OpenVPN...");
let response = await sandbox.process.executeCommand(
"sudo apt update && sudo apt install -y openvpn",
undefined, // cwd
undefined, // env
120 // timeout
);
if (response.exitCode !== 0) {
console.log(`Error installing OpenVPN: ${response.result}`);
return;
}
console.log("OpenVPN installed successfully.");
// Step 2: Write the OpenVPN config file
console.log("\nWriting OpenVPN configuration...");
// Use heredoc to write the config file
await sandbox.process.executeCommand(
`cat << 'OVPNEOF' > /home/daytona/client.ovpn
${ovpnConfig}
OVPNEOF`,
undefined,
undefined,
30
);
console.log("Configuration written to /home/daytona/client.ovpn");
// Step 3: Start OpenVPN in background
console.log("\nStarting OpenVPN tunnel...");
await sandbox.process.executeCommand(
"nohup sudo openvpn /home/daytona/client.ovpn > /tmp/openvpn.log 2>&1 &",
undefined, // cwd
undefined, // env
10 // timeout
);
// Wait for connection to establish
console.log("Waiting for VPN connection to establish...");
await sleep(10000);
// Step 4: Verify connection
console.log("\nVerifying OpenVPN connection...");
// Check if tun interface exists
response = await sandbox.process.executeCommand(
"ip addr show tun0",
undefined, // cwd
undefined, // env
10 // timeout
);
if (response.exitCode === 0) {
console.log("VPN tunnel interface (tun0) is up:");
console.log(response.result);
} else {
console.log("Warning: tun0 interface not found. Checking OpenVPN logs...");
const logResponse = await sandbox.process.executeCommand(
"cat /tmp/openvpn.log",
undefined, // cwd
undefined, // env
10 // timeout
);
console.log(`OpenVPN log:\n${logResponse.result}`);
return;
}
// Get public IP through VPN
console.log("\nChecking public IP (should be VPN server IP)...");
response = await sandbox.process.executeCommand(
"curl -s ifconfig.me",
undefined, // cwd
undefined, // env
30 // timeout
);
if (response.exitCode === 0) {
console.log(`Public IP: ${response.result}`);
} else {
console.log(`Could not determine public IP: ${response.result}`);
}
console.log("\nOpenVPN connection established successfully.");
}
// Run the main function
setupOpenvpn(OPENVPN_CONFIG).catch(console.error);
```
### OpenVPN web terminal
Daytona provides a [web terminal](https://www.daytona.io/docs/en/web-terminal.md) for interacting with your sandboxes, allowing you to install OpenVPN and connect to your OpenVPN network.
1. Navigate to your sandbox [web terminal](https://www.daytona.io/docs/en/web-terminal.md) in [Daytona Dashboard ↗](https://app.daytona.io/) by clicking on the Terminal icon `>_`, or [access it via SSH](https://www.daytona.io/docs/en/ssh-access.md)
2. Install OpenVPN and tmux
```bash
sudo apt update && sudo apt install -y openvpn tmux
```
3. Create the OpenVPN [client configuration file](#openvpn-client-configuration-file) for your Daytona sandbox
```bash
sudo nano client.ovpn
```
4. Save the file by pressing `Ctrl+O`, then `Enter`, and exit by pressing `Ctrl+X`
Daytona requires the OpenVPN tunnel to be running in the background to connect the sandbox to the OpenVPN network. The recommended approach is to run it in a detached tmux (or similar) session:
5. Start OpenVPN tunnel in a background tmux session
```bash
tmux new -d -s openvpn 'sudo openvpn client.ovpn'
```
This starts the OpenVPN tunnel in a background tmux session that persists even if you disconnect from the sandbox.
6. Verify the OpenVPN connection by running the following command
```bash
curl ifconfig.me
```
This will return the IP address of the sandbox connected to the OpenVPN network.
You've now successfully connected your Daytona sandbox to your OpenVPN network.
## Netbird
Daytona provides multiple ways to connect to a Daytona sandbox with a Netbird network:
- [Connect with setup key](#netbird-setup-key)
- [Connect with browser login](#netbird-browser-login)
When you connect a Daytona sandbox to a Netbird network, the sandbox becomes part of your private Netbird network, allowing you to access resources that are available within the network and enabling other devices on the network to access the sandbox.
This integration makes your sandbox appear as a device within your Netbird network, with its own Netbird IP address and access to other devices and services on the network.
### Netbird setup key
Using a Netbird setup key provides a non-interactive way to connect your Daytona sandbox to Netbird, making it suitable for automated scripts, CI/CD pipelines, or any scenario where manual browser interaction is not available.
1. Access your [Netbird dashboard ↗](https://app.netbird.io/)
2. Click **Setup Keys**
3. Create a new setup key or use an existing one
The following snippets demonstrate connecting to a Netbird network using a setup key.
```python
from daytona import Daytona, DaytonaConfig
# Configuration
DAYTONA_API_KEY = "YOUR_API_KEY"
NETBIRD_SETUP_KEY = "YOUR_SETUP_KEY"
# Initialize the Daytona client
config = DaytonaConfig(api_key=DAYTONA_API_KEY)
daytona = Daytona(config)
def setup_netbird():
"""
Connect a Daytona sandbox to a NetBird network using the Python SDK.
Uses the official NetBird install script and a Setup Key for non-interactive auth.
See: https://docs.netbird.io/manage/peers/access-infrastructure/setup-keys-add-servers-to-network
"""
if NETBIRD_SETUP_KEY == "YOUR_SETUP_KEY":
print("Error: please set NETBIRD_SETUP_KEY to a valid setup key.")
return None
# Create the sandbox
print("Creating sandbox...")
sandbox = daytona.create()
print(f"Sandbox created: {sandbox.id}")
print("\nInstalling NetBird via the official install script...")
install_cmd = (
"sudo rm -f /etc/apt/sources.list.d/yarn.list && "
"curl -fsSL https://pkgs.netbird.io/install.sh | sudo sh"
)
response = sandbox.process.exec(install_cmd, timeout=300)
if response.exit_code != 0:
print(f"Error installing NetBird: {response.result}")
return sandbox
print("NetBird installed successfully.")
# Connect to the NetBird network using the setup key
print("\nConnecting to NetBird network with setup key...")
up_cmd = f"sudo netbird up --setup-key {NETBIRD_SETUP_KEY}"
response = sandbox.process.exec(up_cmd, timeout=120)
if response.exit_code != 0:
print(f"Error running 'netbird up': {response.result}")
return sandbox
print(response.result)
# Final status check
print("\nFinal NetBird status:")
response = sandbox.process.exec("netbird status", timeout=30)
print(response.result)
return sandbox
if __name__ == "__main__":
sandbox = setup_netbird()
```
```typescript
import { Daytona } from '@daytona/sdk';
// Configuration
const DAYTONA_API_KEY = "YOUR_API_KEY";
const NETBIRD_SETUP_KEY = "YOUR_SETUP_KEY";
// Initialize the Daytona client
const daytona = new Daytona({
apiKey: DAYTONA_API_KEY,
});
async function setupNetbird(): Promise {
/**
* Connect a Daytona sandbox to a NetBird network using the TypeScript SDK.
* Uses the official NetBird install script and a Setup Key for non-interactive auth.
* See: https://docs.netbird.io/manage/peers/access-infrastructure/setup-keys-add-servers-to-network
*/
// Create the sandbox
console.log("Creating sandbox...");
const sandbox = await daytona.create();
console.log(`Sandbox created: ${sandbox.id}`);
// Install NetBird via the official install script
console.log("\nInstalling NetBird via the official install script...");
const installCmd = [
"sudo rm -f /etc/apt/sources.list.d/yarn.list",
"curl -fsSL https://pkgs.netbird.io/install.sh | sudo sh",
].join(" && ");
let response = await sandbox.process.executeCommand(
installCmd,
undefined, // cwd
undefined, // env
300 // timeout
);
if (response.exitCode !== 0) {
console.log(`Error installing NetBird: ${response.result}`);
return;
}
console.log("NetBird installed successfully.");
// Connect to the NetBird network using the setup key
console.log("\nConnecting to NetBird network with setup key...");
const upCmd = `sudo netbird up --setup-key ${NETBIRD_SETUP_KEY}`;
response = await sandbox.process.executeCommand(
upCmd,
undefined, // cwd
undefined, // env
120 // timeout
);
if (response.exitCode !== 0) {
console.log(`Error running 'netbird up': ${response.result}`);
return;
}
console.log(response.result);
// Final status check
console.log("\nFinal NetBird status:");
response = await sandbox.process.executeCommand(
"netbird status",
undefined, // cwd
undefined, // env
30 // timeout
);
console.log(response.result);
}
// Run the main function
setupNetbird().catch(console.error);
```
### Netbird browser login
The browser login method initiates an interactive authentication flow where Netbird generates a unique login URL that you visit in your web browser to authenticate the Daytona sandbox.
The following snippets demonstrate connecting to a Netbird network using a browser login.
```python
from daytona import Daytona, DaytonaConfig
import time
import re
# Configuration
DAYTONA_API_KEY = "YOUR_API_KEY"
# Initialize the Daytona client
config = DaytonaConfig(api_key=DAYTONA_API_KEY)
daytona = Daytona(config)
def setup_netbird():
"""
Connect a Daytona sandbox to a NetBird network using the Python SDK.
Uses the official NetBird install script and interactive browser login.
"""
# Create the sandbox
print("Creating sandbox...")
sandbox = daytona.create()
print(f"Sandbox created: {sandbox.id}")
# Install NetBird via the official install script
print("\nInstalling NetBird via the official install script...")
install_cmd = (
"sudo rm -f /etc/apt/sources.list.d/yarn.list && "
"curl -fsSL https://pkgs.netbird.io/install.sh | sudo sh"
)
response = sandbox.process.exec(install_cmd, timeout=300)
if response.exit_code != 0:
print(f"Error installing NetBird: {response.result}")
return sandbox
print("NetBird installed successfully.")
# Run netbird up in background and capture output to a file
print("\nInitiating NetBird login...")
sandbox.process.exec(
"netbird up > /tmp/netbird-login.txt 2>&1 &",
timeout=10
)
# Wait for the login URL to be written to the file
time.sleep(5)
# Read the login URL from the output file
response = sandbox.process.exec("cat /tmp/netbird-login.txt", timeout=10)
output = response.result
url_match = re.search(r'https?://[^\s]+', output)
if url_match:
login_url = url_match.group(0)
print(f"\n{'='*60}")
print("To authenticate, visit this URL in your browser:")
print(f"\n {login_url}")
print(f"\n{'='*60}")
print("\nWaiting for authentication...")
# Poll for connection status
max_wait = 300 # 5 minutes max wait
poll_interval = 5
waited = 0
while waited < max_wait:
time.sleep(poll_interval)
waited += poll_interval
status_response = sandbox.process.exec("netbird status 2>&1", timeout=30)
status_output = status_response.result
# Check if we're connected
if status_response.exit_code == 0 and "connected" in status_output.lower():
print(f"\nConnected to NetBird network!")
print(f"NetBird status:\n{status_output}")
break
print(f" Still waiting... ({waited}s)")
else:
print("\nTimeout waiting for authentication. Please try again.")
return sandbox
else:
# Maybe already connected or different output
print(f"Output from netbird up:\n{output}")
# Check if already connected
status_response = sandbox.process.exec("netbird status", timeout=30)
if status_response.exit_code == 0:
print("\nNetBird status:")
print(status_response.result)
# Final status check
print("\nFinal NetBird status:")
response = sandbox.process.exec("netbird status", timeout=30)
print(response.result)
return sandbox
if __name__ == "__main__":
sandbox = setup_netbird()
```
```typescript
import { Daytona } from '@daytona/sdk';
// Configuration
const DAYTONA_API_KEY = "YOUR_API_KEY";
// Initialize the Daytona client
const daytona = new Daytona({
apiKey: DAYTONA_API_KEY,
});
function sleep(ms: number): Promise {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function setupNetbirdVpnInteractive(): Promise {
/**
* Connect a Daytona sandbox to a NetBird network using the TypeScript SDK.
* Uses the official NetBird install script and interactive browser login.
*/
// Create the sandbox
console.log("Creating sandbox...");
const sandbox = await daytona.create();
console.log(`Sandbox created: ${sandbox.id}`);
// Remove the broken Yarn list so apt-get update inside the installer succeeds,
// then run the official NetBird install script. The script adds the NetBird
// APT repo, imports the signing key, installs the package, and registers the
// systemd service.
console.log("\nInstalling NetBird via the official install script...");
const installCmd = [
"sudo rm -f /etc/apt/sources.list.d/yarn.list",
"curl -fsSL https://pkgs.netbird.io/install.sh | sudo sh",
].join(" && ");
let response = await sandbox.process.executeCommand(
installCmd,
undefined, // cwd
undefined, // env
300 // timeout
);
if (response.exitCode !== 0) {
console.log(`Error installing NetBird: ${response.result}`);
return;
}
console.log("NetBird installed successfully.");
// Run netbird up in background and capture output to a file
console.log("\nInitiating NetBird login...");
await sandbox.process.executeCommand(
"netbird up > /tmp/netbird-login.txt 2>&1 &",
undefined, // cwd
undefined, // env
10 // timeout
);
// Wait for the login URL to be written to the file
await sleep(5000);
// Read the login URL from the output file
response = await sandbox.process.executeCommand(
"cat /tmp/netbird-login.txt",
undefined, // cwd
undefined, // env
10 // timeout
);
const output = response.result || "";
const urlMatch = output.match(/https?:\/\/[^\s]+/);
if (urlMatch) {
const loginUrl = urlMatch[0];
console.log("\n" + "=".repeat(60));
console.log("To authenticate, visit this URL in your browser:");
console.log(`\n ${loginUrl}`);
console.log("\n" + "=".repeat(60));
console.log("\nWaiting for authentication...");
// Poll for connection status
const maxWait = 300; // 5 minutes max wait
const pollInterval = 5;
let waited = 0;
while (waited < maxWait) {
await sleep(pollInterval * 1000);
waited += pollInterval;
const statusResponse = await sandbox.process.executeCommand(
"netbird status 2>&1",
undefined, // cwd
undefined, // env
30 // timeout
);
const statusOutput = statusResponse.result || "";
// Check if we're connected
if (statusResponse.exitCode === 0 && statusOutput.toLowerCase().includes("connected")) {
console.log("\nConnected to NetBird network!");
console.log(`NetBird status:\n${statusOutput}`);
break;
}
console.log(` Still waiting... (${waited}s)`);
}
if (waited >= maxWait) {
console.log("\nTimeout waiting for authentication. Please try again.");
return;
}
} else {
// Maybe already connected or different output
console.log(`Output from netbird up:\n${output}`);
// Check if already connected
const statusResponse = await sandbox.process.executeCommand(
"netbird status",
undefined, // cwd
undefined, // env
30 // timeout
);
if (statusResponse.exitCode === 0) {
console.log("\nNetBird status:");
console.log(statusResponse.result);
}
}
// Final status check
console.log("\nFinal NetBird status:");
response = await sandbox.process.executeCommand(
"netbird status",
undefined, // cwd
undefined, // env
30 // timeout
);
console.log(response.result);
}
// Run the main function
setupNetbirdVpnInteractive().catch(console.error);
```
# Preview
Daytona provides preview URLs for accessing services running in your sandboxes. Any process listening for HTTP traffic on ports `1` - `65535` can be previewed through a generated URL.
Daytona supports two types of preview URLs, each with a different authentication mechanism:
- [Standard preview URL](#standard-preview-url) uses the sandbox ID in the URL and requires a separate token for authentication
- [Signed preview URL](#signed-preview-url) embeds the authentication token directly in the URL, requiring no headers
## Authentication
If a sandbox has its `public` property set to `true`, preview links are publicly accessible without authentication. Otherwise, authentication is required. The authentication mechanism depends on the preview URL type.
:::note
Standard and signed preview tokens are not interchangeable. The token from `get_preview_link()` is used as a preview access token (sent via the `x-daytona-preview-token` header). The token from `create_signed_preview_url()` is embedded in the URL itself: it cannot be used as a standard preview token, and vice versa.
:::
## Standard preview URL
The standard preview URL includes your sandbox ID in the URL and provides a separate token for authentication.
URL structure: `https://{port}-{sandboxId}.{daytonaProxyDomain}`
The token resets automatically when the sandbox restarts. Any previously issued standard preview tokens become invalid. Call the `get_preview_link()` method again after starting the sandbox to obtain a fresh token. Use standard preview URLs for programmatic access and API integrations where you control the HTTP headers.
```python
preview_info = sandbox.get_preview_link(3000)
print(f"URL: {preview_info.url}")
print(f"Token: {preview_info.token}")
# Use with requests
import requests
response = requests.get(
preview_info.url,
headers={"x-daytona-preview-token": preview_info.token}
)
```
```typescript
const previewInfo = await sandbox.getPreviewLink(3000);
console.log(`URL: ${previewInfo.url}`);
console.log(`Token: ${previewInfo.token}`);
// Use with fetch
const response = await fetch(previewInfo.url, {
headers: { 'x-daytona-preview-token': previewInfo.token }
});
```
```ruby
preview_info = sandbox.preview_url(3000)
puts "Preview link url: #{preview_info.url}"
puts "Preview link token: #{preview_info.token}"
```
```go
preview, err := sandbox.GetPreviewLink(ctx, 3000)
if err != nil {
log.Fatal(err)
}
fmt.Printf("URL: %s\n", preview.URL)
fmt.Printf("Token: %s\n", preview.Token)
```
```bash
curl 'https://app.daytona.io/api/sandbox/{sandboxId}/ports/3000/preview-url' \
--header 'Authorization: Bearer '
```
### Authentication
Authenticate by sending the token in the `x-daytona-preview-token` header:
```bash
curl -H "x-daytona-preview-token: vg5c0ylmcimr8b_v1ne0u6mdnvit6gc0" \
https://3000-sandbox-123456.proxy.daytona.work
```
## Signed preview URL
The signed preview URL embeds the authentication token directly in the URL, eliminating the need for separate headers. The token persists across sandbox restarts until it expires, or is revoked manually before expiry. Set a custom expiry time for the token:
- Default: `60` seconds
- Minimum: `1` second
- Maximum: `86,400` seconds (24 hours)
- Recommended: `3600` seconds (1 hour)
:::tip
Always set the `expires_in_seconds` parameter explicitly. The default of 60 seconds is short due to security considerations. Most use cases should use at least 3600 (1 hour).
:::
URL structure: `https://{port}-{token}.{daytonaProxyDomain}`
Use signed preview URLs when sharing links with users who cannot set custom headers, embedding previews in iframes or emails, or creating time-limited shareable links.
```python
# Create a signed preview URL that expires in 3600 seconds (1 hour)
signed_url = sandbox.create_signed_preview_url(3000, expires_in_seconds=3600)
print(f"URL: {signed_url.url}") # Token is embedded in the URL
print(f"Token: {signed_url.token}") # Can be used to revoke access
# Use directly - no headers needed
import requests
response = requests.get(signed_url.url)
# Revoke the token before expiry if needed
sandbox.expire_signed_preview_url(3000, signed_url.token)
```
```typescript
// Create a signed preview URL that expires in 3600 seconds (1 hour)
const signedUrl = await sandbox.getSignedPreviewUrl(3000, 3600);
console.log(`URL: ${signedUrl.url}`); // Token is embedded in the URL
console.log(`Token: ${signedUrl.token}`); // Can be used to revoke access
// Use directly - no headers needed
const response = await fetch(signedUrl.url);
// Revoke the token before expiry if needed
await sandbox.expireSignedPreviewUrl(3000, signedUrl.token);
```
```ruby
# Create a signed preview URL that expires in 3600 seconds (1 hour)
signed_url = sandbox.create_signed_preview_url(3000, 3600)
puts "URL: #{signed_url.url}"
puts "Token: #{signed_url.token}"
```
```go
// Create a signed preview URL that expires in 3600 seconds (1 hour)
signedPreview, err := sandbox.GetSignedPreviewLink(ctx, 3000, 3600)
if err != nil {
log.Fatal(err)
}
fmt.Printf("URL: %s\n", signedPreview.URL) // Token is embedded in the URL
fmt.Printf("Token: %s\n", signedPreview.Token) // Can be used to revoke access
// Revoke the token before expiry if needed
if err := sandbox.ExpireSignedPreviewLink(ctx, 3000, signedPreview.Token); err != nil {
log.Fatal(err)
}
```
```bash
daytona preview-url --port 3000 --expires 3600
```
```bash
curl 'https://app.daytona.io/api/sandbox/{sandboxId}/ports/3000/signed-preview-url?expiresInSeconds=3600' \
--header 'Authorization: Bearer '
```
### Authentication
The token is embedded in the URL itself, so no additional headers are required:
```bash
curl https://3000-.proxy.daytona.work
```
:::tip
Port `22222` is used by the [web terminal](https://www.daytona.io/docs/en/web-terminal.md) to access the terminal using preview URLs.
:::
## Warning page
When opening a preview link in a browser for the first time, Daytona displays a warning page. This warning informs users about potential risks of visiting the preview URL and only appears when loading the link in a browser.
To skip the warning page:
- Send the `X-Daytona-Skip-Preview-Warning: true` header
- Upgrade to [Tier 3](https://www.daytona.io/docs/en/limits.md)
- Deploy a [custom preview proxy](https://www.daytona.io/docs/en/custom-preview-proxy.md)
# Custom Preview Proxy
Daytona provides a preview proxy service that can be used to handle [preview URLs](https://www.daytona.io/docs/en/preview.md) for sandboxes. This gives you full control over the preview experience, including custom domains, authentication, error handling, and styling.
- **Custom domain**: host your proxy under your own domain (e.g., `preview.yourcompany.com`)
- **User authentication**: implement custom authentication logic for private previews
- **Sandbox management**: automatically start stopped sandboxes before forwarding users
- **Custom error pages**: style error pages to match your brand
- **Preview warning control**: disable Daytona's preview warning
- **CORS management**: override Daytona's default CORS settings
## How it works
When a user visits a preview URL, your custom proxy receives the request and can:
1. Authenticate the user using custom logic
2. Check sandbox status and start it if needed
3. Forward the request to the actual sandbox
4. Handle responses with custom styling and error pages
5. Send custom headers to control Daytona's behavior
Your proxy should forward the `X-Forwarded-Host` header with the original request host when proxying requests to Daytona.
## WebSocket support
The preview proxy fully supports WebSocket connections. WebSocket upgrade requests (`Upgrade: websocket`) are automatically detected and proxied. WebSocket connections skip the preview warning page.
## Reserved ports
The following ports are reserved for internal services and always require authentication, even on public sandboxes:
| Port | Service |
| ----------- | ----------------------------------------- |
| **`22222`** | [**Web** terminal](https://www.daytona.io/docs/en/web-terminal.md) |
| **`2280`** | Toolbox (IDE/development interface) |
| **`33333`** | Recording dashboard |
Your custom proxy should avoid exposing these ports unless you explicitly need access to these services.
## Proxy headers
Your proxy can send special headers to control Daytona's behavior.
### Disable preview warning
To disable Daytona's preview warning page, send:
```
X-Daytona-Skip-Preview-Warning: true
```
The warning page is only shown to browser requests. It sets a `daytona-preview-page-accepted` cookie that persists for 24 hours after acceptance.
### Disable CORS
Daytona's default CORS policy allows all origins with credentials. To override this and use your own CORS settings, send:
```
X-Daytona-Disable-CORS: true
```
### Disable last activity update
To prevent sandbox last activity updates when previewing, set the `X-Daytona-Skip-Last-Activity-Update` header to `true`. This prevents Daytona from keeping sandboxes that have [auto-stop enabled](https://www.daytona.io/docs/en/sandboxes.md#auto-stop-interval) in a started state:
```bash
curl -H "X-Daytona-Skip-Last-Activity-Update: true" \
https://3000-sandbox-123456.proxy.daytona.work
```
### Authentication
For private preview links, send:
```
X-Daytona-Preview-Token: {sandboxToken}
```
The `sandboxToken` can be fetched through the Daytona SDK or API using the [standard preview URL](https://www.daytona.io/docs/en/preview.md#standard-preview-url) methods.
## Examples
Examples of custom preview proxies are available on [GitHub](https://github.com/daytonaio/daytona-proxy-samples).
# Playground
Daytona provides an interactive environment ("Playground") for creating sandboxes, running SDK operations, and exploring Daytona features directly from your browser.
- **Interactive sandbox creation**: configure and create sandboxes with custom resources
- **Code snippet generation**: automatically generate SDK code samples based on the values configured in the [management](#management) section
- [Web terminal](https://www.daytona.io/docs/en/web-terminal.md): run commands directly in your sandbox through a built-in web terminal
- [VNC access](https://www.daytona.io/docs/en/vnc-access.md): interact with GUI applications using [Computer Use](https://www.daytona.io/docs/en/computer-use.md) features
## Access from Dashboard
Go to [Daytona Dashboard ↗](https://app.daytona.io/dashboard/playground) to access the playground.
Playground consists of three tabs: [Sandbox](#sandbox), [Terminal](#terminal), and [VNC](#vnc). All three tabs operate on the same active sandbox.
## Sandbox
The sandbox tab provides an interactive interface to configure sandboxes and perform SDK operations. The left panel contains configurable parameters organized into collapsible sections. The right panel displays auto-generated code snippets that you can inspect, copy, and run.
### Management
The management section contains the parameters used to configure a sandbox. You can edit these parameters to customize your sandbox and its resources.
- [sandbox language](https://www.daytona.io/docs/en/sandboxes.md#multiple-runtime-support): select a programming language runtime for the sandbox
- [sandbox resources](https://www.daytona.io/docs/en/sandboxes.md#resources): configure resources allocated to the sandbox
- [sandbox lifecycle](https://www.daytona.io/docs/en/sandboxes.md#sandbox-lifecycle): set the sandbox lifecycle policies
:::note
In the management section, sandboxes are only created when you explicitly click **Run**, replacing any existing playground sandbox. In the [Terminal](#terminal) or [VNC](#vnc) tabs, if there are no active sandboxes, the playground automatically creates one using the configured parameters.
:::
### File system
The file system section provides operations to manage files and directories in the sandbox. You can modify files and directories in the sandbox using the following operations:
- **Create a new directory**: set the directory location and its permissions
- **List files and directories**: set the directory location
- **Delete files**: set the file location and delete directory checkbox
For more information, see [file system operations](https://www.daytona.io/docs/en/file-system-operations.md).
### Git operations
The Git operations section provides operations to manage Git repositories in the sandbox. You can clone, get repository status, and list branches using the following operations:
- **Clone a Git repository**: set the repository URL, destination, branch, commit ID, and credentials
- **Get repository status**: retrieve the repository status
- **List branches**: retrieve the list of branches in the repository
For more information, see [Git operations](https://www.daytona.io/docs/en/git-operations.md).
### Process and code execution
The process and code execution section provides operations to run code snippets and shell commands directly in the sandbox. Shell commands are fixed, while code snippets change automatically based on the selected [sandbox language](https://www.daytona.io/docs/en/sandboxes.md#multiple-runtime-support) parameter.
For more information, see [process and code execution](https://www.daytona.io/docs/en/process-code-execution.md).
## Terminal
The terminal tab provides a web-based terminal connected to the sandbox. This gives you direct command-line access to the sandbox environment for running commands, viewing files, and debugging.
The terminal runs on port `22222` and remains active as long as the sandbox is running. If the sandbox stops due to inactivity, start it again to restore terminal access.
:::note
Terminal access is restricted to authenticated members of your [organization](https://www.daytona.io/docs/en/organizations.md).
:::
For more information, see [web terminal](https://www.daytona.io/docs/en/web-terminal.md).
## VNC
The VNC tab provides graphical desktop access to the sandbox, enabling you to interact with GUI applications and test [Computer Use](https://www.daytona.io/docs/en/computer-use.md) features. The left panel contains interaction controls organized into sections. The right panel displays the desktop view.
For more information, see [VNC access](https://www.daytona.io/docs/en/vnc-access.md).
### Display
The display section provides options to inspect the sandbox desktop environment.
- **Get display information**: retrieve information about the available displays
- **List open windows**: retrieve a list of currently open windows
For more information, see [display operations](https://www.daytona.io/docs/en/computer-use.md#display-operations).
### Keyboard
The keyboard section provides options to send keyboard input to the sandbox.
- **Press a hotkey combination**: send a hotkey combination to the sandbox
- **Press a key**: press a key with optional modifiers
- **Type text**: type text into the active window
For more information, see [keyboard operations](https://www.daytona.io/docs/en/computer-use.md#keyboard-operations).
### Mouse
The mouse section provides options to control mouse input in the sandbox.
- **Click**: click at a specified position
- **Drag**: drag from one position to another
- **Move**: move the cursor to a specified position
- **Get cursor position**: retrieve the current cursor position
For more information, see [mouse operations](https://www.daytona.io/docs/en/computer-use.md#mouse-operations).
### Screenshot
The screenshot section provides options to capture screenshots of the sandbox desktop.
- **Take a screenshot**: select format, scale, and quality, show cursor, and capture the entire screen or specific regions
For more information, see [screenshot operations](https://www.daytona.io/docs/en/computer-use.md#screenshot-operations).
## Related
- [Web Terminal](https://www.daytona.io/docs/en/web-terminal.md): browser-based terminal access for sandboxes
- [VNC Access](https://www.daytona.io/docs/en/vnc-access.md): graphical desktop access for sandboxes
- [Computer Use](https://www.daytona.io/docs/en/computer-use.md): programmatic desktop automation
- [File System Operations](https://www.daytona.io/docs/en/file-system-operations.md): manage files and directories in sandboxes
- [Git Operations](https://www.daytona.io/docs/en/git-operations.md): manage Git repositories in sandboxes
- [Process & Code Execution](https://www.daytona.io/docs/en/process-code-execution.md): execute commands and run code in sandboxes
# Webhooks
Webhooks are HTTP callbacks that Daytona sends to your specified endpoints when specific events occur.
Think of them as "reverse API calls" - instead of your application asking Daytona for updates, Daytona
proactively notifies your application when something important happens.
Webhooks enable powerful automation and integration scenarios:
- **Real-time notifications**: get instant alerts when sandboxes are created, started, or stopped
- **Automated workflows**: trigger deployment pipelines when snapshots are created
- **Monitoring & analytics**: track usage patterns and resource utilization across organizations
- **Integration**: connect Daytona with existing tools like Slack, Discord, or custom applications
- **Audit & compliance**: maintain detailed logs of all important changes
:::note
Webhooks are available to organization admins and members with appropriate permissions. If you don't see **Webhooks** in [Daytona Dashboard ↗](https://app.daytona.io/dashboard), contact [support@daytona.io](mailto:support@daytona.io) to enable webhooks for your organization.
:::
## Accessing webhooks
Daytona provides a webhook management interface to access and manage webhook endpoints.
1. Navigate to [Daytona Dashboard ↗](https://app.daytona.io/dashboard)
2. Click **Webhooks** in the sidebar
The webhooks management interface contains two tabs: [Endpoints](#endpoints) and [Messages](#messages).
### Endpoints
The endpoints tab lists all the webhook endpoints for the organization. Each endpoint has the following details:
- **Name**: the name of the webhook endpoint
- **URL**: the URL of the webhook endpoint
- **Status**: the status of the webhook endpoint
- **Created**: the timestamp when the webhook endpoint was created
Clicking on an endpoint opens the endpoint details page with additional endpoint configuration details: **name**, **URL**, **events**, **status**, **delivery statistics**, and **event history**.
### Messages
The messages tab lists all the webhook messages for the organization. Each message has the following details:
- **Message ID**: the ID of the webhook message
- **Event type**: the event type of the webhook message
- **Timestamp**: the timestamp when the webhook message was created
Clicking on a message opens the message details dialog with additional message details: **message ID**, **event type**, **timestamp**, **payload**, and **delivery attempts**.
Delivery attempts are shown in a separate table with the following columns:
- **Status**: the status of the delivery attempt
- **URL**: the URL of the delivery attempt
- **Timestamp**: the timestamp when the delivery attempt was created
Expanding a delivery attempt displays additional delivery attempt details: **status code**, **duration**, **trigger**, **endpoint ID**, and **response body**.
## Create webhook endpoints
Daytona provides a webhook management interface to create webhook endpoints.
1. Navigate to [Daytona Dashboard ↗](https://app.daytona.io/dashboard)
2. Click **Webhooks** in the sidebar
3. Click **Add Endpoint**
4. Configure your endpoint:
- **Endpoint name**: a name for the endpoint
- **Endpoint URL**: HTTPS endpoint where you want to receive events
- [Events](#webhook-events): select which events to subscribe to
5. Click **Create**
## Edit webhook endpoints
Daytona provides a webhook management interface to edit webhook endpoints.
1. Navigate to [Daytona Dashboard ↗](https://app.daytona.io/dashboard)
2. Click **Webhooks** in the sidebar
3. Select a webhook endpoint from the [Endpoints](#endpoints) tab
4. Click the three dots menu (**⋮**) and select **Edit**
5. Update the endpoint details
6. Click **Save**
## Delete webhook endpoints
Daytona provides a webhook management interface to delete webhook endpoints.
1. Navigate to [Daytona Dashboard ↗](https://app.daytona.io/dashboard)
2. Click **Webhooks** in the sidebar
3. Select a webhook endpoint from the [Endpoints](#endpoints) tab
4. Click the three dots menu (**⋮**) and select **Delete**
5. Confirm the deletion
## Webhook events
Daytona sends webhooks for lifecycle events across your infrastructure resources. You can subscribe to specific event types or receive all events and filter them in your application.
### Sandbox events
| Event Type | Description |
| --------------------------- | ------------------------------ |
| **`sandbox.created`** | A new sandbox has been created |
| **`sandbox.state.updated`** | A sandbox's state has changed |
### Snapshot events
| Event Type | Description |
| ---------------------------- | ------------------------------- |
| **`snapshot.created`** | A new snapshot has been created |
| **`snapshot.state.updated`** | A snapshot's state has changed |
| **`snapshot.removed`** | A snapshot has been removed |
### Volume events
| Event Type | Description |
| -------------------------- | ----------------------------- |
| **`volume.created`** | A new volume has been created |
| **`volume.state.updated`** | A volume's state has changed |
## Webhook payload format
All webhook payloads are JSON objects following a consistent format with common fields and event-specific data.
**Common Fields:**
| Field | Type | Description |
| --------------- | ------ | ----------------------------------------------- |
| **`event`** | string | Event type identifier (e.g., `sandbox.created`) |
| **`timestamp`** | string | ISO 8601 timestamp when the event occurred |
### **`sandbox.created`**
Sent when a new sandbox is created.
```json
{
"event": "sandbox.created",
"timestamp": "2025-12-19T10:30:00.000Z",
"id": "sandbox123",
"organizationId": "org123",
"state": "started",
"sandboxClass": "container",
"createdAt": "2025-12-19T10:30:00.000Z"
}
```
| Field | Type | Description |
| -------------------- | ------ | ----------------------------------------------- |
| **`id`** | string | Sandbox ID |
| **`organizationId`** | string | Organization ID |
| **`state`** | string | Sandbox state |
| **`sandboxClass`** | string | Sandbox class |
| **`createdAt`** | string | ISO 8601 timestamp when the sandbox was created |
### **`sandbox.state.updated`**
Sent when a sandbox's state changes.
```json
{
"event": "sandbox.state.updated",
"timestamp": "2025-12-19T10:30:00.000Z",
"id": "sandbox123",
"organizationId": "org123",
"oldState": "started",
"newState": "stopped",
"updatedAt": "2025-12-19T10:30:00.000Z"
}
```
| Field | Type | Description |
| -------------------- | ------ | ---------------------------------------------------- |
| **`id`** | string | Sandbox ID |
| **`organizationId`** | string | Organization ID |
| **`oldState`** | string | Previous sandbox state |
| **`newState`** | string | New sandbox state |
| **`updatedAt`** | string | ISO 8601 timestamp when the sandbox was last updated |
### **`snapshot.created`**
Sent when a new snapshot is created.
```json
{
"event": "snapshot.created",
"timestamp": "2025-12-19T10:30:00.000Z",
"id": "snapshot123",
"name": "my-snapshot",
"organizationId": "org123",
"state": "active",
"createdAt": "2025-12-19T10:30:00.000Z"
}
```
| Field | Type | Description |
| -------------------- | ------ | ------------------------------------------------ |
| **`id`** | string | Snapshot ID |
| **`name`** | string | Snapshot name |
| **`organizationId`** | string | Organization ID |
| **`state`** | string | Snapshot state |
| **`createdAt`** | string | ISO 8601 timestamp when the snapshot was created |
### **`snapshot.state.updated`**
Sent when a snapshot's state changes.
```json
{
"event": "snapshot.state.updated",
"timestamp": "2025-12-19T10:30:00.000Z",
"id": "snapshot123",
"name": "my-snapshot",
"organizationId": "org123",
"oldState": "building",
"newState": "active",
"updatedAt": "2025-12-19T10:30:00.000Z"
}
```
| Field | Type | Description |
| -------------------- | ------ | ----------------------------------------------------- |
| **`id`** | string | Snapshot ID |
| **`name`** | string | Snapshot name |
| **`organizationId`** | string | Organization ID |
| **`oldState`** | string | Previous snapshot state |
| **`newState`** | string | New snapshot state |
| **`updatedAt`** | string | ISO 8601 timestamp when the snapshot was last updated |
### **`snapshot.removed`**
Sent when a snapshot is removed.
```json
{
"event": "snapshot.removed",
"timestamp": "2025-12-19T10:30:00.000Z",
"id": "snapshot123",
"name": "my-snapshot",
"organizationId": "org123",
"removedAt": "2025-12-19T10:30:00.000Z"
}
```
| Field | Type | Description |
| -------------------- | ------ | ------------------------------------------------ |
| **`id`** | string | Snapshot ID |
| **`name`** | string | Snapshot name |
| **`organizationId`** | string | Organization ID |
| **`removedAt`** | string | ISO 8601 timestamp when the snapshot was removed |
### **`volume.created`**
Sent when a new volume is created.
```json
{
"event": "volume.created",
"timestamp": "2025-12-19T10:30:00.000Z",
"id": "vol-12345678",
"name": "my-volume",
"organizationId": "org123",
"state": "ready",
"createdAt": "2025-12-19T10:30:00.000Z"
}
```
| Field | Type | Description |
| -------------------- | ------ | ---------------------------------------------- |
| **`id`** | string | Volume ID |
| **`name`** | string | Volume name |
| **`organizationId`** | string | Organization ID |
| **`state`** | string | Volume state |
| **`createdAt`** | string | ISO 8601 timestamp when the volume was created |
### **`volume.state.updated`**
Sent when a volume's state changes.
```json
{
"event": "volume.state.updated",
"timestamp": "2025-12-19T10:30:00.000Z",
"id": "vol-12345678",
"name": "my-volume",
"organizationId": "org123",
"oldState": "creating",
"newState": "ready",
"updatedAt": "2025-12-19T10:30:00.000Z"
}
```
| Field | Type | Description |
| -------------------- | ------ | --------------------------------------------------- |
| **`id`** | string | Volume ID |
| **`name`** | string | Volume name |
| **`organizationId`** | string | Organization ID |
| **`oldState`** | string | Previous volume state |
| **`newState`** | string | New volume state |
| **`updatedAt`** | string | ISO 8601 timestamp when the volume was last updated |
# Network Limits (Firewall)
Network limits control outbound internet access from sandboxes. Each sandbox runs behind a firewall that restricts which external IP addresses and domains it can reach, preventing untrusted code from exfiltrating data or contacting arbitrary hosts.
Default network policies are applied automatically based on your organization's tier. You can also configure access per sandbox using three parameters:
- **`networkAllowList`** for IPv4 CIDR ranges
- **`domainAllowList`** for domains and wildcard domains
- **`networkBlockAll`** to block all outbound traffic
Set these parameters when [creating a sandbox](#create-sandboxes-with-network-restrictions) or [update them while the sandbox is running](#update-network-settings-while-a-sandbox-is-running).
## Tier-based network restrictions
Network limits are automatically applied to sandboxes based on your organization's billing tier. This provides secure and controlled internet access for development environments:
- **Tier 1 & Tier 2**: Network access is restricted and cannot be overridden at the sandbox level. Organization-level network restrictions take precedence over sandbox-level settings. Even with [`networkAllowList`](#create-sandboxes-with-network-restrictions) or [`domainAllowList`](#create-sandboxes-with-network-restrictions) specified when creating a sandbox, the organization's network restrictions still apply
- **Tier 3 & Tier 4**: Full internet access is available by default, with the ability to configure custom network settings
[Essential services](#essential-services) are available on all tiers and include services essential for development: package registries, container registries, Git repositories, CDN services, platform services, system package managers, and more
## Create sandboxes with network restrictions
Create a sandbox with network restrictions to control outbound internet access.
The options are mutually exclusive. Set at most one non-empty value. Sending a conflicting combination returns a `400` error. Empty-string allow lists count as unset and never conflict.
```python
from daytona import CreateSandboxFromSnapshotParams, Daytona
daytona = Daytona()
# Allow access to specific IP addresses (Wikipedia, X/Twitter, private network)
sandbox = daytona.create(CreateSandboxFromSnapshotParams(
network_allow_list='208.80.154.232/32,199.16.156.103/32,192.168.1.0/24'
))
# Allow access to specific domains
sandbox = daytona.create(CreateSandboxFromSnapshotParams(
domain_allow_list='example.com,*.daytona.io'
))
# Or block all network access
sandbox = daytona.create(CreateSandboxFromSnapshotParams(
network_block_all=True
))
```
```typescript
import { Daytona } from '@daytona/sdk'
const daytona = new Daytona()
// Allow access to specific IP addresses (Wikipedia, X/Twitter, private network)
const sandboxWithCidrAllowList = await daytona.create({
networkAllowList: '208.80.154.232/32,199.16.156.103/32,192.168.1.0/24'
})
// Allow access to specific domains
const sandboxWithDomainAllowList = await daytona.create({
domainAllowList: 'example.com,*.daytona.io'
})
// Or block all network access
const blockedSandbox = await daytona.create({
networkBlockAll: true
})
```
```ruby
require 'daytona'
daytona = Daytona::Daytona.new
# Allow access to specific IP addresses (Wikipedia, X/Twitter, private network)
sandbox = daytona.create(
Daytona::CreateSandboxFromSnapshotParams.new(
network_allow_list: '208.80.154.232/32,199.16.156.103/32,192.168.1.0/24'
)
)
# Allow access to specific domains
sandbox = daytona.create(
Daytona::CreateSandboxFromSnapshotParams.new(
domain_allow_list: 'example.com,*.daytona.io'
)
)
# Or block all network access
sandbox = daytona.create(
Daytona::CreateSandboxFromSnapshotParams.new(
network_block_all: true
)
)
```
```go
package main
import (
"context"
"log"
"github.com/daytona/clients/sdk-go/pkg/daytona"
"github.com/daytona/clients/sdk-go/pkg/types"
)
func main() {
client, err := daytona.NewClient()
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
// Allow access to specific IP addresses (Wikipedia, X/Twitter, private network)
allowList := "208.80.154.232/32,199.16.156.103/32,192.168.1.0/24"
_, err = client.Create(ctx, types.SnapshotParams{
SandboxBaseParams: types.SandboxBaseParams{
NetworkAllowList: &allowList,
},
})
if err != nil {
log.Fatal(err)
}
// Allow access to specific domains
domainAllowList := "example.com,*.daytona.io"
_, err = client.Create(ctx, types.SnapshotParams{
SandboxBaseParams: types.SandboxBaseParams{
DomainAllowList: &domainAllowList,
},
})
if err != nil {
log.Fatal(err)
}
// Or block all network access
_, err = client.Create(ctx, types.SnapshotParams{
SandboxBaseParams: types.SandboxBaseParams{
NetworkBlockAll: true,
},
})
if err != nil {
log.Fatal(err)
}
}
```
```java
import io.daytona.sdk.Daytona;
import io.daytona.sdk.Sandbox;
import io.daytona.sdk.model.CreateSandboxFromSnapshotParams;
public class App {
public static void main(String[] args) {
try (Daytona daytona = new Daytona()) {
// Allow access to specific domains
CreateSandboxFromSnapshotParams domainParams = new CreateSandboxFromSnapshotParams();
domainParams.setDomainAllowList("example.com,*.daytona.io");
Sandbox domainRestrictedSandbox = daytona.create(domainParams);
// Or block all network access
CreateSandboxFromSnapshotParams blockedParams = new CreateSandboxFromSnapshotParams();
blockedParams.setNetworkBlockAll(true);
Sandbox blockedSandbox = daytona.create(blockedParams);
}
}
}
```
```bash
# Allow access to specific IP addresses (Wikipedia, X/Twitter, private network)
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
"networkAllowList": "208.80.154.232/32,199.16.156.103/32,192.168.1.0/24"
}'
# Allow access to specific domains
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
"domainAllowList": "example.com,*.daytona.io"
}'
# Or block all network access
curl 'https://app.daytona.io/api/sandbox' \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
"networkBlockAll": true
}'
```
```bash
# Allow access to specific IP addresses (Wikipedia, X/Twitter, private network)
daytona create --network-allow-list '208.80.154.232/32,199.16.156.103/32,192.168.1.0/24'
# Or block all network access
daytona create --network-block-all
```
## Update network settings while a sandbox is running
Update network settings for running sandboxes.
This operation requires the `WRITE_SANDBOXES` permission. Organizations on [Tier 3 and Tier 4](#tier-based-network-restrictions) can change outbound firewall policy on a running sandbox. The API applies the new rules and persists them on the sandbox. The sandbox keeps running; stop or start are not required.
Organizations on Tier 1 or Tier 2 cannot override network policy at the sandbox level, and the API returns an error in that case.
- Sending `networkAllowList` as an empty string clears a stored CIDR allow list
- Sending `domainAllowList` as an empty string clears a stored domain allow list
- Sending `networkBlockAll: true` blocks all outbound traffic and clears both the stored CIDR and domain allow lists
- Sending only `networkBlockAll: false` removes the block-all rule and clears both the stored CIDR and domain allow lists
```python
# Block all outbound traffic (clears the CIDR allow list)
sandbox.update_network_settings(network_block_all=True)
# Remove the block-all rule and clear the CIDR allow list
sandbox.update_network_settings(network_block_all=False)
# Apply or replace a CIDR allow list (implies not blocking all)
sandbox.update_network_settings(
network_allow_list='208.80.154.232/32,192.168.1.0/24'
)
# Apply or replace a domain allow list
sandbox.update_network_settings(
domain_allow_list='example.com,*.daytona.io'
)
# Clear a stored CIDR allow list (empty string). Outbound traffic still follows `network_block_all`.
sandbox.update_network_settings(network_allow_list='')
# Clear a stored domain allow list
sandbox.update_network_settings(domain_allow_list='')
```
```typescript
// Block all outbound traffic (clears the CIDR allow list)
await sandbox.updateNetworkSettings({ networkBlockAll: true })
// Remove the block-all rule and clear the CIDR allow list
await sandbox.updateNetworkSettings({ networkBlockAll: false })
// Apply or replace a CIDR allow list (implies not blocking all)
await sandbox.updateNetworkSettings({
networkAllowList: '208.80.154.232/32,192.168.1.0/24',
})
// Apply or replace a domain allow list
await sandbox.updateNetworkSettings({
domainAllowList: 'example.com,*.daytona.io',
})
// Clear a stored CIDR allow list (empty string). Outbound traffic still follows `networkBlockAll`.
await sandbox.updateNetworkSettings({ networkAllowList: '' })
// Clear a stored domain allow list
await sandbox.updateNetworkSettings({ domainAllowList: '' })
```
```ruby
# Block all outbound traffic (clears the CIDR allow list)
sandbox.update_network_settings(network_block_all: true)
# Remove the block-all rule and clear the CIDR allow list
sandbox.update_network_settings(network_block_all: false)
# Apply or replace a CIDR allow list (implies not blocking all)
sandbox.update_network_settings(
network_allow_list: '208.80.154.232/32,192.168.1.0/24'
)
# Apply or replace a domain allow list
sandbox.update_network_settings(
domain_allow_list: 'example.com,*.daytona.io'
)
# Clear the CIDR allow list (empty string)
sandbox.update_network_settings(network_allow_list: '')
# Clear the domain allow list
sandbox.update_network_settings(domain_allow_list: '')
```
```go
import apiclient "github.com/daytona/clients/api-client-go"
settings := apiclient.NewUpdateSandboxNetworkSettings()
settings.SetNetworkBlockAll(true)
if err := sandbox.UpdateNetworkSettings(ctx, *settings); err != nil {
log.Fatal(err)
}
restore := apiclient.NewUpdateSandboxNetworkSettings()
restore.SetNetworkBlockAll(false)
if err := sandbox.UpdateNetworkSettings(ctx, *restore); err != nil {
log.Fatal(err)
}
allow := apiclient.NewUpdateSandboxNetworkSettings()
allow.SetNetworkAllowList("208.80.154.232/32,192.168.1.0/24")
if err := sandbox.UpdateNetworkSettings(ctx, *allow); err != nil {
log.Fatal(err)
}
domainAllow := apiclient.NewUpdateSandboxNetworkSettings()
domainAllow.SetDomainAllowList("example.com,*.daytona.io")
if err := sandbox.UpdateNetworkSettings(ctx, *domainAllow); err != nil {
log.Fatal(err)
}
clearDomainAllow := apiclient.NewUpdateSandboxNetworkSettings()
clearDomainAllow.SetDomainAllowList("")
if err := sandbox.UpdateNetworkSettings(ctx, *clearDomainAllow); err != nil {
log.Fatal(err)
}
```
```java
import io.daytona.api.client.model.UpdateSandboxNetworkSettings;
// Block all outbound traffic (clears the CIDR allow list)
sandbox.updateNetworkSettings(new UpdateSandboxNetworkSettings().networkBlockAll(true));
// Remove the block-all rule and clear the CIDR allow list
sandbox.updateNetworkSettings(new UpdateSandboxNetworkSettings().networkBlockAll(false));
// Apply or replace a CIDR allow list
sandbox.updateNetworkSettings(
new UpdateSandboxNetworkSettings().networkAllowList("208.80.154.232/32,192.168.1.0/24"));
// Apply or replace a domain allow list
sandbox.updateNetworkSettings(
new UpdateSandboxNetworkSettings().domainAllowList("example.com,*.daytona.io"));
// Clear a stored domain allow list
sandbox.updateNetworkSettings(new UpdateSandboxNetworkSettings().domainAllowList(""));
```
```bash
curl 'https://app.daytona.io/api/sandbox/SANDBOX_ID_OR_NAME/network-settings' \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{"networkBlockAll": true}'
# Remove the block-all rule and clear the CIDR allow list
curl 'https://app.daytona.io/api/sandbox/SANDBOX_ID_OR_NAME/network-settings' \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{"networkBlockAll": false}'
# Apply or replace a domain allow list
curl 'https://app.daytona.io/api/sandbox/SANDBOX_ID_OR_NAME/network-settings' \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{"domainAllowList": "example.com,*.daytona.io"}'
# Clear a stored domain allow list
curl 'https://app.daytona.io/api/sandbox/SANDBOX_ID_OR_NAME/network-settings' \
--request POST \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{"domainAllowList": ""}'
```
## Network allow list format
The network allow list is a comma-separated list of IPv4 CIDR blocks.
- **IPv4 only**: hostnames, domains, and IPv6 are not supported
- **CIDR required**: every entry must include a `/` prefix length integer in the range `0` to `32` (inclusive), for example: `/32`
- **CIDR format**: use standard CIDR notation (`A.B.C.D/N`). Do not include extra `/` segments
- **Max 10 entries**: the list cannot contain more than 10 comma-separated items
- **Whitespace is ignored**: entries are trimmed, so spaces around commas are ok
Examples:
- **Single IP**: `208.80.154.232/32` (Wikipedia)
- **Subnet**: `192.168.1.0/24` (Private network)
- **Multiple networks**: `208.80.154.232/32,199.16.156.103/32,10.0.0.0/8`
## Domain allow list format
The domain allow list is a comma-separated list of DNS domains. When a domain allow list is set, outbound traffic is limited to the listed domains and other external domains are blocked.
- **Domains only**: use hostnames such as `example.com` or `api.openai.com`. Do not include protocols, paths, ports, or query strings
- **Wildcards supported**: prefix a domain with `*.` to allow the base domain and its subdomains, for example `*.daytona.io`
- **Max 20 entries**: the list cannot contain more than 20 comma-separated items
- **Whitespace is ignored**: entries are trimmed, so spaces around commas are ok
- **Clear on update**: send `domainAllowList` as an empty string when updating network settings to clear a stored domain allow list
Examples:
- **Single domain**: `example.com`
- **Wildcard domain**: `*.daytona.io`
- **Multiple domains**: `example.com,*.daytona.io,api.openai.com`
## Test network access
To test network connectivity from your sandbox:
```bash
# Test HTTP connectivity to allowed addresses
curl -I https://208.80.154.232
# Test HTTP connectivity to allowed domains
curl -I https://example.com
# Test package manager access (allowed on all tiers)
apt update # For Ubuntu/Debian
npm ping # For Node.js
pip install --dry-run requests # For Python
```
## Security benefits
Network limits provide several security advantages:
- **Prevents data exfiltration** from sandboxes
- **Reduces attack surface** by limiting external connections
- **Complies with security policies** for development environments
- **Enables fine-grained control** over network access
:::caution
Enabling unrestricted network access may pose security risks when executing untrusted code. It is recommended to allow only the network addresses or domains you need, or block all network access. Test network connectivity before starting critical development work and consider upgrading your tier if you need access to many external services.
:::
## Essential services
Essential services are available on all tiers and include services essential for development.
### NPM registry and package managers
| **Service** | **Domains** |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| NPM Registry | **`registry.npmjs.org`**, **`registry.npmjs.com`**, **`nodejs.org`**, **`nodesource.com`**, **`deb.nodesource.com`**, **`npm.pkg.github.com`** |
| Yarn Packages | **`yarnpkg.com`**, **`*.yarnpkg.com`**, **`yarn.npmjs.org`**, **`yarnpkg.netlify.com`** |
| Bun | **`bun.sh`**, **`*.bun.sh`** |
### Nix package manager
| **Service** | **Domains** |
| ----------- | ------------------------------------------------------------------------- |
| Nix | **`cache.nixos.org`**, **`channels.nixos.org`**, **`releases.nixos.org`** |
### Git hosting and version control
| **Service** | **Domains** |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| GitHub | **`github.com`**, **`*.github.com`**, **`*.githubusercontent.com`**, **`gh.io`**, **`ghcr.io`** |
| GitLab | **`gitlab.com`**, **`*.gitlab.com`** |
| Bitbucket | **`bitbucket.org`** |
| Code Storage | **`code.storage`**, **`*.code.storage`** |
| Azure DevOps | **`dev.azure.com`**, **`*.dev.azure.com`**, **`login.microsoftonline.com`**, **`visualstudio.com`**, **`*.visualstudio.com`**, **`ssh.dev.azure.com`**, **`vs-ssh.visualstudio.com`** |
### Python package managers
| **Service** | **Domains** |
| ----------- | -------------------------------------------------------------------------------------------------------------------------------- |
| PyPI | **`pypi.org`**, **`pypi.python.org`**, **`files.pythonhosted.org`**, **`bootstrap.pypa.io`**, **`astral.sh`**, **`*.astral.sh`** |
| Conda | **`repo.anaconda.com`** |
### Rust package manager and toolchain
| **Service** | **Domains** |
| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Rust | **`crates.io`**, **`static.crates.io`**, **`index.crates.io`**, **`static.rust-lang.org`**, **`rustup.rs`**, **`sh.rustup.rs`**, **`doc.rust-lang.org`** |
### Go module proxy and toolchain
| **Service** | **Domains** |
| ----------- | ------------------------------------------------------------------------------------------------------------------------ |
| Go | **`proxy.golang.org`**, **`sum.golang.org`**, **`index.golang.org`**, **`go.dev`**, **`golang.org`**, **`*.golang.org`** |
### C/C++ build tools
| **Service** | **Domains** |
| ----------- | --------------- |
| CMake | **`cmake.org`** |
### Composer packages
| **Service** | **Domains** |
| ----------- | --------------------------------------------------------------- |
| Composer | **`packagist.org`**, **`*.packagist.org`**, **`packagist.com`** |
### NuGet packages
| **Service** | **Domains** |
| ----------- | ---------------------------------- |
| NuGet | **`nuget.org`**, **`*.nuget.org`** |
### Elixir/Erlang packages
| **Service** | **Domains** |
| ----------- | ---------------------------- |
| Hex | **`hex.pm`**, **`*.hex.pm`** |
### Ruby packages
| **Service** | **Domains** |
| ----------- | ---------------------------------------- |
| RubyGems | **`rubygems.org`**, **`*.rubygems.org`** |
### Ubuntu/Debian package repositories
| **Service** | **Domains** |
| ------------ | --------------------------------------------------- |
| Ubuntu Repos | **`*.ubuntu.com`** |
| Debian Repos | **`*.debian.org`**, **`cdn-fastly.deb.debian.org`** |
### CDN and content delivery
| **Service** | **Domains** |
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| CDN Services | **`fastly.com`**, **`cloudflare.com`**, **`gateway.ai.cloudflare.com`**, **`*.workers.dev`**, **`r2.cloudflarestorage.com`**, **`*.r2.cloudflarestorage.com`** |
| JavaScript CDNs | **`unpkg.com`**, **`jsdelivr.net`** |
### AI/ML services
| **Service** | **Domains** |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Anthropic | **`*.anthropic.com`**, **`claude.ai`**, **`*.claude.ai`**, **`platform.claude.com`** |
| OpenAI | **`openai.com`**, **`*.openai.com`**, **`chatgpt.com`** |
| Google AI | **`generativelanguage.googleapis.com`**, **`gemini.google.com`**, **`aistudio.google.com`**, **`ai.google.dev`**, **`models.dev`** |
| Perplexity | **`api.perplexity.ai`** |
| DeepSeek | **`api.deepseek.com`** |
| Groq | **`api.groq.com`** |
| Expo | **`api.expo.dev`** |
| OpenRouter | **`openrouter.ai`** |
| Qwen | **`chat.qwen.ai`**, **`dashscope.aliyuncs.com`**, **`dashscope-intl.aliyuncs.com`** |
| Cursor | **`cursor.com`**, **`*.cursor.com`**, **`*.cursor.sh`** |
| OpenCode | **`opencode.ai`**, **`*.opencode.ai`** |
| Aider | **`aider.chat`** |
| Hugging Face | **`huggingface.co`**, **`*.huggingface.co`**, **`hf.co`**, **`*.hf.co`**, **`*.xethub.hf.co`**, **`*.cdn.hf.co`**, **`*.aws.cdn.hf.co`**, **`*.gcp.cdn.hf.co`** |
| Other AI Services | **`api.letta.com`**, **`api.fireworks.ai`**, **`open.bigmodel.cn`**, **`*.z.ai`**, **`*.moonshot.ai`**, **`*.minimax.io`**, **`*.kimi.com`**, **`ai-gateway.vercel.sh`**, **`api.elevenlabs.io`**, **`api.featherless.ai`**, **`ampcode.com`**, **`*.ampcode.com`**, **`*.openai.azure.com`**, **`*.services.ai.azure.com`**, **`trynia.ai`**, **`*.trynia.ai`**, **`api.x.ai`**, **`copass.id`**, **`*.copass.id`**, **`zenmux.ai`**, **`*.devin.ai`** |
### Docker registries and container services
| **Service** | **Domains** |
| ---------------------------- | ------------------------------------------------------------------------------ |
| Docker Registries | **`docker.io`**, **`*.docker.io`**, **`*.docker.com`** |
| Microsoft Container Registry | **`mcr.microsoft.com`** |
| Kubernetes Registry | **`registry.k8s.io`** |
| Google Container Registry | **`gcr.io`**, **`*.gcr.io`**, **`*.pkg.dev`**, **`registry.cloud.google.com`** |
| Quay | **`quay.io`**, **`quay-registry.s3.amazonaws.com`** |
| AWS ECR | **`public.ecr.aws`**, **`*.ecr.aws`** |
### Maven repositories
| **Service** | **Domains** |
| ----------- | -------------------------------------------------- |
| Maven Repos | **`repo1.maven.org`**, **`repo.maven.apache.org`** |
### Google Fonts
| **Service** | **Domains** |
| ------------ | --------------------------------------------------- |
| Google Fonts | **`fonts.googleapis.com`**, **`fonts.gstatic.com`** |
### AWS endpoints
| **Region** | **Domains** |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| US East | **`*.us-east-1.amazonaws.com`**, **`*.us-east-2.amazonaws.com`** |
| US West | **`*.us-west-1.amazonaws.com`**, **`*.us-west-2.amazonaws.com`** |
| EU | **`*.eu-central-1.amazonaws.com`**, **`*.eu-central-2.amazonaws.com`**, **`*.eu-north-1.amazonaws.com`**, **`*.eu-south-1.amazonaws.com`**, **`*.eu-south-2.amazonaws.com`**, **`*.eu-west-1.amazonaws.com`**, **`*.eu-west-2.amazonaws.com`**, **`*.eu-west-3.amazonaws.com`** |
| Asia Pacific | **`*.ap-south-1.amazonaws.com`** |
### Google Cloud
| **Service** | **Domains** |
| ----------------------- | ------------------------------------------------------------------------------------------------------ |
| Google Cloud Platform | **`accounts.google.com`**, **`*.googleapis.com`**, **`*.storage.googleapis.com`**, **`*.gstatic.com`** |
| Google Downloads | **`dl.google.com`** |
| Google Package Registry | **`packages.cloud.google.com`** |
### Cloud storage
| **Service** | **Domains** |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| Azure Blob Storage | **`*.blob.core.windows.net`** |
| Box | **`api.box.com`**, **`app.box.com`**, **`*.app.box.com`**, **`upload.box.com`**, **`account.box.com`**, **`*.ent.box.com`**, **`*.boxcloud.com`** |
### Daytona
| **Service** | **Domains** |
| ----------- | -------------------- |
| Daytona | **`app.daytona.io`** |
### Developer tools and services
| **Service** | **Domains** |
| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Convex | **`convex.dev`**, **`*.convex.dev`**, **`*.convex.cloud`**, **`*.convex.site`** |
| Heroku | **`herokuapp.com`**, **`*.herokuapp.com`** |
| Vercel | **`vercel.com`**, **`*.vercel.com`**, **`*.vercel.app`** |
| Supabase | **`supabase.com`**, **`*.supabase.com`**, **`supabase.co`**, **`*.supabase.co`** |
| Clerk | **`clerk.com`**, **`*.clerk.com`**, **`clerk.dev`**, **`*.clerk.dev`**, **`accounts.dev`**, **`*.accounts.dev`**, **`clerk.accounts.dev`**, **`*.clerk.accounts.dev`** |
| WorkOS | **`workos.com`**, **`*.workos.com`**, **`authkit.app`**, **`*.authkit.app`** |
| Inngest | **`inngest.com`**, **`*.inngest.com`** |
| PostHog | **`posthog.com`**, **`*.posthog.com`** |
| Sentry | **`sentry.io`**, **`*.sentry.io`**, **`sentry-cdn.com`**, **`*.sentry-cdn.com`** |
| Linear | **`linear.app`**, **`*.linear.app`** |
| Figma | **`figma.com`**, **`*.figma.com`**, **`*.figmafiles.com`** |
| ClickUp | **`clickup.com`**, **`*.clickup.com`** |
| Atlassian | **`acli.atlassian.com`** |
| Railway | **`railway.app`**, **`*.railway.app`**, **`railway.com`**, **`*.railway.com`** |
| Autumn | **`api.useautumn.com`** |
| Playwright | **`playwright.dev`**, **`cdn.playwright.dev`** |
| Doppler | **`doppler.com`**, **`*.doppler.com`** |
| Auth0 | **`auth0.com`**, **`*.auth0.com`** |
| Sanity | **`sanity.io`**, **`*.sanity.io`**, **`sanity.work`**, **`*.sanity.work`** |
| Shopify | **`shopify.com`**, **`*.shopify.com`**, **`*.myshopify.com`**, **`*.shopify.dev`**, **`*.shopifycdn.com`** |
| Mesa | **`mesa.dev`**, **`*.mesa.dev`** |
| Buildkite | **`buildkite.com`**, **`*.buildkite.com`** |
| Shortcut | **`api.app.shortcut.com`**, **`app.shortcut.com`** |
| USAspending | **`api.usaspending.gov`**, **`files.usaspending.gov`** |
| Logo Dev | **`img.logo.dev`**, **`logo.dev`** |
| Kiro | **`*.kiro.dev`**, **`*.us-east-1.kiro.dev`**, **`prod.download.cli.kiro.dev`** |
| Browserbase | **`browserbase.com`**, **`*.browserbase.com`**, **`connect.usw2.browserbase.com`**, **`connect.use1.browserbase.com`**, **`connect.euc1.browserbase.com`**, **`connect.apse1.browserbase.com`** |
### Messaging services
| **Service** | **Domains** |
| ----------- | -------------------------------------------- |
| Telegram | **`api.telegram.org`** |
| WhatsApp | **`web.whatsapp.com`**, **`*.whatsapp.net`** |
### LLM observability
| **Service** | **Domains** |
| ----------- | ------------------------------------------------ |
| Langfuse | **`*.langfuse.com`**, **`*.cloud.langfuse.com`** |
| LangSmith | **`api.smith.langchain.com`** |
# Guides
Daytona provides a comprehensive set of guides to help you get started.
The guides cover a wide range of topics, from basic usage to advanced topics, and showcase various types of integrations between Daytona and other tools.
# Analyze Data with AI
import { Image } from 'astro:assets'
import chartImage from '../../../../assets/docs/images/chart-0.png'
You can use Daytona Sandbox to run AI-generated code to analyze data. Here's how the AI data analysis workflow typically looks:
1. Your user has a dataset in CSV format or other formats.
2. You prompt the LLM to generate code (usually Python) based on the user's data.
3. The sandbox runs the AI-generated code and returns the results.
4. The LLM receives feedback from the execution and can iterate multiple times to refine the code if needed.
5. You display the final results to the user.
---
## Build an AI Data Analyst with Daytona
This example shows how to build an AI-powered data analyst that automatically generates insights and visualizations from CSV data using Daytona's secure sandbox environment.
**What we'll build:** A system that analyzes a vehicle valuation dataset, identifies price relation to manufacturing year, and generates professional visualizations - all through natural language prompts to Claude. The system uses an agentic loop that allows Claude to iteratively refine the code based on execution results.
### 1. Project Setup
#### 1.1 Install Dependencies
Install the Daytona SDK and Anthropic SDK to your project:
`bash pip install daytona anthropic python-dotenv `
`bash npm install @daytona/sdk @anthropic-ai/sdk dotenv `
`bash gem install daytona anthropic dotenv `
#### 1.2 Configure Environment
Get your API keys and configure your environment:
1. **Daytona API key:** Get it from [Daytona Dashboard](https://app.daytona.io/dashboard/keys)
2. **Anthropic API key:** Get it from [Anthropic Console](https://console.anthropic.com/)
Create a `.env` file in your project:
```bash
DAYTONA_API_KEY=dtn_***
ANTHROPIC_API_KEY=sk-ant-***
```
### 2. Dataset Preparation
#### 2.1 Download Dataset
We'll be using a publicly available dataset of vehicle valuation. You can download it directly from:
[https://download.daytona.io/dataset.csv](https://download.daytona.io/dataset.csv)
Download the file and save it as `dataset.csv` in your project directory.
#### 2.2 Initialize Sandbox
Now create a [Daytona sandbox](https://www.daytona.io/docs/en/sandboxes.md#create-sandboxes) and upload your dataset:
```python
from dotenv import load_dotenv
from daytona import Daytona
import os
load_dotenv()
# Create sandbox
daytona = Daytona() # The sandbox language is Python by default.
sandbox = daytona.create()
# Upload the dataset to the sandbox
sandbox.fs.upload_file("dataset.csv", "/home/daytona/dataset.csv")
```
```typescript
import 'dotenv/config'
import { Daytona } from '@daytona/sdk';
// Create sandbox
const daytona = new Daytona(); // The sandbox language is Python by default.
const sandbox = await daytona.create()
// Upload the dataset to the sandbox
await sandbox.fs.uploadFile('dataset.csv', '/home/daytona/dataset.csv')
```
```ruby
require 'daytona'
require 'dotenv/load'
# Create sandbox
daytona = Daytona::Daytona.new # The sandbox language is Python by default.
sandbox = daytona.create
# Upload the dataset to the sandbox
sandbox.fs.upload_file(File.read('dataset.csv'), '/home/daytona/dataset.csv')
```
### 3. Building the AI Data Analyst
Now we'll create the core functionality that connects Claude with Daytona to analyze data and generate visualizations.
#### 3.1 Code Execution Handler
First, let's create a function to handle code execution and chart extraction. This function returns execution results that can be fed back to the AI model:
```python
import base64
from typing import TypedDict
class ExecutionResult(TypedDict):
stdout: str
exit_code: int
charts: list
def run_ai_generated_code(sandbox, ai_generated_code: str) -> ExecutionResult:
execution = sandbox.process.code_run(ai_generated_code)
result = ExecutionResult(
stdout=execution.result or "",
exit_code=execution.exit_code,
charts=execution.artifacts.charts if execution.artifacts else []
)
# Save any charts that were generated
if execution.artifacts and execution.artifacts.charts:
result_idx = 0
for chart in execution.artifacts.charts:
if chart.png:
filename = f'chart-{result_idx}.png'
with open(filename, 'wb') as f:
f.write(base64.b64decode(chart.png))
print(f'✓ Chart saved to {filename}')
result_idx += 1
return result
```
```typescript
import fs from 'fs'
import { Sandbox } from '@daytona/sdk'
interface ExecutionResult {
stdout: string
exitCode: number
charts?: Array<{ png?: string }>
}
async function runAIGeneratedCode(
sandbox: Sandbox,
aiGeneratedCode: string
): Promise {
const execution = await sandbox.process.codeRun(aiGeneratedCode)
const result: ExecutionResult = {
stdout: execution.result || "",
exitCode: execution.exitCode,
charts: execution.artifacts?.charts
}
// Save any charts that were generated
if (execution.artifacts?.charts) {
let resultIdx = 0
for (const chart of execution.artifacts.charts) {
if (chart.png) {
const filename = `chart-${resultIdx}.png`
fs.writeFileSync(filename, chart.png, { encoding: 'base64' })
console.log(`✓ Chart saved to ${filename}`)
resultIdx++
}
}
}
return result
}
```
#### 3.2 Creating the Analysis Prompt
Next, we'll create the prompt that tells Claude about our dataset and what analysis we want. This prompt includes:
- Dataset schema and column descriptions
- The specific analysis request (vehicle price variation by manufacturing year)
- Instructions for code generation
```python
from anthropic import Anthropic
prompt = f"""
I have a CSV file with vehicle valuations saved in the sandbox at /home/daytona/dataset.csv.
Relevant columns:
- 'year': integer, the manufacturing year of the vehicle
- 'price_in_euro': float, the listed price of the vehicle in Euros
Analyze how price varies by manufacturing year.
Drop rows where 'year' or 'price_in_euro' is missing, non-numeric, or an outlier.
Create a line chart showing average price per year.
Write Python code that analyzes the dataset based on my request and produces a matplotlib chart accordingly.
Always finish with plt.show() to display the chart."""
anthropic = Anthropic()
```
```typescript
import Anthropic from '@anthropic-ai/sdk'
const prompt = `
I have a CSV file with vehicle valuations saved in the sandbox at /home/daytona/dataset.csv.
Relevant columns:
- 'year': integer, the manufacturing year of the vehicle
- 'price_in_euro': float, the listed price of the vehicle in Euros
Analyze how price varies by manufacturing year.
Drop rows where 'year' or 'price_in_euro' is missing, non-numeric, or an outlier.
Create a line chart showing average price per year.
Write Python code that analyzes the dataset based on my request and produces a matplotlib chart accordingly.
Always finish with plt.show() to display the chart.`
const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY })
```
#### 3.3 Tool Definition
Define the tool that allows Claude to execute Python code in the sandbox:
```python
tools = [
{
'name': 'run_python_code',
'description': 'Run Python code in the sandbox environment and get execution results',
'input_schema': {
'type': 'object',
'properties': {
'code': {
'type': 'string',
'description': 'The Python code to run',
},
},
'required': ['code'],
},
},
]
```
```typescript
import type { Tool, ToolUseBlock } from '@anthropic-ai/sdk/resources/messages.mjs'
const tools: Tool[] = [
{
name: 'run_python_code',
description: 'Run Python code in the sandbox environment and get execution results',
input_schema: {
type: 'object',
properties: {
code: {
type: 'string',
description: 'The Python code to run',
},
},
required: ['code'],
},
},
]
```
#### 3.4 Agentic Loop Implementation
Now we'll implement the agentic loop that allows Claude to iteratively refine the code based on execution feedback. This enables Claude to fix errors, handle edge cases, and improve the analysis through multiple iterations:
```python
# Initialize conversation history
messages = [{'role': 'user', 'content': prompt}]
continue_loop = True
iteration_count = 0
max_iterations = 10
print("Starting agentic loop...\n")
while continue_loop and iteration_count < max_iterations:
iteration_count += 1
print(f"\n=== Iteration {iteration_count} ===")
print("Waiting for the model response...")
# Get response from Claude
msg = anthropic.messages.create(
model='claude-sonnet-4-5',
max_tokens=64000,
messages=messages,
tools=tools
)
# Log Claude's text response
for content_block in msg.content:
if content_block.type == 'text':
print("\nClaude's response:")
print(content_block.text)
# Check if Claude wants to use any tools
tool_uses = [block for block in msg.content if block.type == 'tool_use']
if len(tool_uses) == 0:
# No more tool uses, Claude is done
print("\nTask completed - no more actions needed.")
continue_loop = False
break
# Add Claude's response to message history
messages.append({'role': 'assistant', 'content': msg.content})
# Execute all tool calls and collect results
tool_results = []
for tool_use in tool_uses:
if tool_use.name == 'run_python_code':
code = tool_use.input['code']
print("\n--- Executing Python code in sandbox ---")
print(code)
print("--- End of code ---\n")
# Execute the code in the sandbox
execution_result = run_ai_generated_code(sandbox, code)
# Format the tool result
result_content = ""
if execution_result['exit_code'] == 0:
result_content += "Execution successful!\n\n"
if execution_result['stdout']:
result_content += f"Output:\n{execution_result['stdout']}\n"
if execution_result['charts'] and len(execution_result['charts']) > 0:
result_content += f"\nGenerated {len(execution_result['charts'])} chart(s)."
else:
result_content += "\nNote: No charts were generated. Make sure to use plt.show() to display the chart."
else:
result_content += f"Execution failed with exit code {execution_result['exit_code']}\n\n"
if execution_result['stdout']:
result_content += f"Output:\n{execution_result['stdout']}\n"
tool_results.append({
'type': 'tool_result',
'tool_use_id': tool_use.id,
'content': result_content
})
print("Execution result sent back to Claude.")
# Add tool results to conversation history
messages.append({'role': 'user', 'content': tool_results})
if iteration_count >= max_iterations:
print("\n⚠️ Reached maximum iteration limit. Task may not be complete.")
print("\n=== Agentic loop completed ===")
```
```typescript
import type { MessageParam } from '@anthropic-ai/sdk/resources/messages.mjs'
interface CodeRunToolInput {
code: string
}
// Initialize conversation history
const messages: MessageParam[] = [
{ role: 'user', content: initialPrompt }
]
let continueLoop = true
let iterationCount = 0
const maxIterations = 10
console.log("Starting agentic loop...\n")
while (continueLoop && iterationCount < maxIterations) {
iterationCount++
console.log(`\n=== Iteration ${iterationCount} ===`)
console.log("Waiting for the model response...")
// Get response from Claude
const stream = anthropic.messages.stream({
model: 'claude-sonnet-4-5',
max_tokens: 64000,
messages: messages,
tools: tools
})
const message = await stream.finalMessage()
// Log Claude's text response
for (const contentBlock of message.content) {
if (contentBlock.type === 'text') {
console.log("\nClaude's response:")
console.log(contentBlock.text)
}
}
// Check if Claude wants to use any tools
const toolUses = message.content.filter(
(block): block is ToolUseBlock => block.type === 'tool_use'
)
if (toolUses.length === 0) {
// No more tool uses, Claude is done
console.log("\nTask completed - no more actions needed.")
continueLoop = false
break
}
// Add Claude's response to message history
messages.push({
role: 'assistant',
content: message.content
})
// Execute all tool calls and collect results
const toolResults = []
for (const toolUse of toolUses) {
if (toolUse.name === 'run_python_code') {
const code = (toolUse.input as CodeRunToolInput).code
console.log("\n--- Executing Python code in sandbox ---")
console.log(code)
console.log("--- End of code ---\n")
// Execute the code in the sandbox
const executionResult = await runAIGeneratedCode(sandbox, code)
// Format the tool result
let resultContent = ""
if (executionResult.exitCode === 0) {
resultContent += "Execution successful!\n\n"
if (executionResult.stdout) {
resultContent += `Output:\n${executionResult.stdout}\n`
}
if (executionResult.charts && executionResult.charts.length > 0) {
resultContent += `\nGenerated ${executionResult.charts.length} chart(s).`
} else {
resultContent += "\nNote: No charts were generated. Make sure to use plt.show() to display the chart."
}
} else {
resultContent += `Execution failed with exit code ${executionResult.exitCode}\n\n`
if (executionResult.stdout) {
resultContent += `Output:\n${executionResult.stdout}\n`
}
}
toolResults.push({
type: 'tool_result' as const,
tool_use_id: toolUse.id,
content: resultContent
})
console.log("Execution result sent back to Claude.")
}
}
// Add tool results to conversation history
messages.push({
role: 'user',
content: toolResults
})
}
if (iterationCount >= maxIterations) {
console.log("\n⚠️ Reached maximum iteration limit. Task may not be complete.")
}
console.log("\n=== Agentic loop completed ===")
```
The agentic loop works as follows:
1. **Initial Request**: Send the initial prompt to Claude with the tool definition
2. **Iteration Loop**: For each iteration (up to 10 times):
- Claude generates a response with optional tool calls
- If there are tool calls, execute the Python code in the sandbox
- Send execution results back to Claude (including errors or success messages)
- Claude can then refine the code based on the feedback
3. **Completion**: Loop ends when Claude signals no more tool calls are needed or max iterations reached
This approach allows Claude to:
- Fix errors if the initial code fails
- Iterate on the analysis if results aren't satisfactory
- Handle edge cases discovered during execution
- Improve visualizations based on the actual data
**Key advantages of this approach:**
- **Secure execution:** Code runs in isolated Daytona sandboxes
- **Automatic artifact capture:** Charts, tables, and outputs are automatically extracted
- **Error handling:** Built-in error detection and logging
- **Language agnostic:** While we used Python here, Daytona supports multiple languages
### 4. Running Your Analysis
Now you can run the complete code to see the results.
```bash
python data-analysis.py
```
```bash
npx tsx data-analysis.ts
```
You should see the chart in your project directory that will look similar to this:
### 5. Complete Implementation
Here are the complete, ready-to-run examples with the agentic loop:
```python
import base64
from dotenv import load_dotenv
from daytona import Daytona, Sandbox
from anthropic import Anthropic
from typing import TypedDict
class ExecutionResult(TypedDict):
stdout: str
exit_code: int
charts: list
def main():
load_dotenv()
# Create sandbox
daytona = Daytona()
sandbox = daytona.create()
# Upload the dataset to the sandbox
sandbox.fs.upload_file("dataset.csv", "/home/daytona/dataset.csv")
initial_prompt = """
I have a CSV file with vehicle valuations saved in the sandbox at /home/daytona/dataset.csv.
Relevant columns:
- 'year': integer, the manufacturing year of the vehicle
- 'price_in_euro': float, the listed price of the vehicle in Euros
Analyze how price varies by manufacturing year.
Drop rows where 'year' or 'price_in_euro' is missing, non-numeric, or an outlier.
Create a line chart showing average price per year.
Write Python code that analyzes the dataset based on my request and produces a matplotlib chart accordingly.
Always finish with plt.show() to display the chart."""
anthropic = Anthropic()
tools = [
{
'name': 'run_python_code',
'description': 'Run Python code in the sandbox environment and get execution results',
'input_schema': {
'type': 'object',
'properties': {
'code': {
'type': 'string',
'description': 'The Python code to run',
},
},
'required': ['code'],
},
},
]
# Initialize conversation history
messages = [{'role': 'user', 'content': initial_prompt}]
continue_loop = True
iteration_count = 0
max_iterations = 10
print("Starting agentic loop...\n")
while continue_loop and iteration_count < max_iterations:
iteration_count += 1
print(f"\n=== Iteration {iteration_count} ===")
print("Waiting for the model response...")
# Get response from Claude
msg = anthropic.messages.create(
model='claude-sonnet-4-5',
max_tokens=64000,
messages=messages,
tools=tools
)
# Log Claude's text response
for content_block in msg.content:
if content_block.type == 'text':
print("\nClaude's response:")
print(content_block.text)
# Check if Claude wants to use any tools
tool_uses = [block for block in msg.content if block.type == 'tool_use']
if len(tool_uses) == 0:
# No more tool uses, Claude is done
print("\nTask completed - no more actions needed.")
continue_loop = False
break
# Add Claude's response to message history
messages.append({'role': 'assistant', 'content': msg.content})
# Execute all tool calls and collect results
tool_results = []
for tool_use in tool_uses:
if tool_use.name == 'run_python_code':
code = tool_use.input['code']
print("\n--- Executing Python code in sandbox ---")
print(code)
print("--- End of code ---\n")
# Execute the code in the sandbox
execution_result = run_ai_generated_code(sandbox, code)
# Format the tool result
result_content = ""
if execution_result['exit_code'] == 0:
result_content += "Execution successful!\n\n"
if execution_result['stdout']:
result_content += f"Output:\n{execution_result['stdout']}\n"
if execution_result['charts'] and len(execution_result['charts']) > 0:
result_content += f"\nGenerated {len(execution_result['charts'])} chart(s)."
else:
result_content += "\nNote: No charts were generated. Make sure to use plt.show() to display the chart."
else:
result_content += f"Execution failed with exit code {execution_result['exit_code']}\n\n"
if execution_result['stdout']:
result_content += f"Output:\n{execution_result['stdout']}\n"
tool_results.append({
'type': 'tool_result',
'tool_use_id': tool_use.id,
'content': result_content
})
print("Execution result sent back to Claude.")
# Add tool results to conversation history
messages.append({'role': 'user', 'content': tool_results})
if iteration_count >= max_iterations:
print("\n⚠️ Reached maximum iteration limit. Task may not be complete.")
print("\n=== Agentic loop completed ===")
def run_ai_generated_code(sandbox: Sandbox, ai_generated_code: str) -> ExecutionResult:
execution = sandbox.process.code_run(ai_generated_code)
result = ExecutionResult(
stdout=execution.result or "",
exit_code=execution.exit_code,
charts=execution.artifacts.charts if execution.artifacts else []
)
# Save any charts that were generated
if execution.artifacts and execution.artifacts.charts:
result_idx = 0
for chart in execution.artifacts.charts:
if chart.png:
filename = f'chart-{result_idx}.png'
with open(filename, 'wb') as f:
f.write(base64.b64decode(chart.png))
print(f'✓ Chart saved to {filename}')
result_idx += 1
return result
if __name__ == "__main__":
main()
```
```typescript
import "dotenv/config";
import fs from "fs";
import Anthropic from "@anthropic-ai/sdk";
import { Daytona, Sandbox } from "@daytona/sdk";
import type {
MessageParam,
Tool,
ToolUseBlock,
} from "@anthropic-ai/sdk/resources/messages.mjs";
interface CodeRunToolInput {
code: string;
}
interface ExecutionResult {
stdout: string;
exitCode: number;
charts?: Array<{ png?: string }>;
}
async function main() {
// Create sandbox
const daytona = new Daytona();
const sandbox = await daytona.create();
// Upload the dataset to the sandbox
await sandbox.fs.uploadFile("dataset.csv", "/home/daytona/dataset.csv");
const initialPrompt = `
I have a CSV file with vehicle valuations saved in the sandbox at /home/daytona/dataset.csv.
Relevant columns:
- 'year': integer, the manufacturing year of the vehicle
- 'price_in_euro': float, the listed price of the vehicle in Euros
Analyze how price varies by manufacturing year.
Drop rows where 'year' or 'price_in_euro' is missing, non-numeric, or an outlier.
Create a line chart showing average price per year.
Write Python code that analyzes the dataset based on my request and produces a matplotlib chart accordingly.
Always finish with plt.show() to display the chart.`;
const anthropic = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
const tools: Tool[] = [
{
name: "run_python_code",
description: "Run Python code in the sandbox environment and get execution results",
input_schema: {
type: "object",
properties: {
code: {
type: "string",
description: "The Python code to run",
},
},
required: ["code"],
},
},
];
// Initialize conversation history
const messages: MessageParam[] = [
{ role: "user", content: initialPrompt },
];
let continueLoop = true;
let iterationCount = 0;
const maxIterations = 10;
console.log("Starting agentic loop...\n");
while (continueLoop && iterationCount < maxIterations) {
iterationCount++;
console.log(`\n=== Iteration ${iterationCount} ===`);
console.log("Waiting for the model response...");
// Get response from Claude
const stream = anthropic.messages.stream({
model: "claude-sonnet-4-5",
max_tokens: 64000,
messages: messages,
tools: tools,
});
const message = await stream.finalMessage();
// Log Claude's text response
for (const contentBlock of message.content) {
if (contentBlock.type === "text") {
console.log("\nClaude's response:");
console.log(contentBlock.text);
}
}
// Check if Claude wants to use any tools
const toolUses = message.content.filter(
(block): block is ToolUseBlock => block.type === "tool_use"
);
if (toolUses.length === 0) {
// No more tool uses, Claude is done
console.log("\nTask completed - no more actions needed.");
continueLoop = false;
break;
}
// Add Claude's response to message history
messages.push({
role: "assistant",
content: message.content,
});
// Execute all tool calls and collect results
const toolResults = [];
for (const toolUse of toolUses) {
if (toolUse.name === "run_python_code") {
const code = (toolUse.input as CodeRunToolInput).code;
console.log("\n--- Executing Python code in sandbox ---");
console.log(code);
console.log("--- End of code ---\n");
// Execute the code in the sandbox
const executionResult = await runAIGeneratedCode(sandbox, code);
// Format the tool result
let resultContent = "";
if (executionResult.exitCode === 0) {
resultContent += `Execution successful!\n\n`;
if (executionResult.stdout) {
resultContent += `Output:\n${executionResult.stdout}\n`;
}
if (executionResult.charts && executionResult.charts.length > 0) {
resultContent += `\nGenerated ${executionResult.charts.length} chart(s).`;
} else {
resultContent += `\nNote: No charts were generated. Make sure to use plt.show() to display the chart.`;
}
} else {
resultContent += `Execution failed with exit code ${executionResult.exitCode}\n\n`;
if (executionResult.stdout) {
resultContent += `Output:\n${executionResult.stdout}\n`;
}
}
toolResults.push({
type: "tool_result" as const,
tool_use_id: toolUse.id,
content: resultContent,
});
console.log("Execution result sent back to Claude.");
}
}
// Add tool results to conversation history
messages.push({
role: "user",
content: toolResults,
});
}
if (iterationCount >= maxIterations) {
console.log(
"\n⚠️ Reached maximum iteration limit. Task may not be complete."
);
}
console.log("\n=== Agentic loop completed ===");
}
async function runAIGeneratedCode(
sandbox: Sandbox,
aiGeneratedCode: string
): Promise {
const execution = await sandbox.process.codeRun(aiGeneratedCode);
const result: ExecutionResult = {
stdout: execution.result || "",
exitCode: execution.exitCode,
charts: execution.artifacts?.charts,
};
// Save any charts that were generated
if (execution.artifacts?.charts) {
let resultIdx = 0;
for (const chart of execution.artifacts.charts) {
if (chart.png) {
const filename = `chart-${resultIdx}.png`;
fs.writeFileSync(filename, chart.png, {
encoding: "base64",
});
console.log(`✓ Chart saved to ${filename}`);
resultIdx++;
}
}
}
return result;
}
main().catch(console.error);
```
# Build Coding Agent Using AgentKit and Daytona
import { Image } from 'astro:assets'
import notesAppResult from '../../../../../assets/docs/images/inngest-agentkit-notes-app.gif'
This guide demonstrates how to set up and use a fully autonomous coding agent that performs software development tasks in a [Daytona](https://daytona.io) sandbox environment. The agent is built using [AgentKit](https://agentkit.inngest.com/) and leverages Daytona sandboxes for secure, isolated execution. It can create web apps, run tests, execute scripts, and more; automating multi-step workflows based on user prompts.
---
### 1. Workflow Overview
You provide a natural language prompt describing the software task. The agent reasons about your request, plans the steps, and executes them securely in a Daytona sandbox; handling everything from project setup to live previews.
### 2. Project Setup
#### Clone the Repository
Clone the [repository](https://github.com/daytona/guides) and navigate to the example directory:
```bash
git clone https://github.com/daytona/guides.git
cd guides/typescript/agentkit-inngest/coding-agent/anthropic
```
#### Configure Environment
Get your API keys:
- **Daytona API key:** [Daytona Dashboard](https://app.daytona.io/dashboard/keys)
- **Anthropic API key:** [Anthropic Console](https://console.anthropic.com/)
Copy `.env.example` to `.env` and add your keys:
```bash
DAYTONA_API_KEY=your_daytona_key
ANTHROPIC_API_KEY=your_anthropic_key
```
#### Local Usage
:::note[Node.js Version]
Node.js 18 or newer is required to run the coding agent locally. Please ensure your environment meets this requirement before proceeding.
:::
Install dependencies:
```bash
npm install
```
Run the agent:
```bash
npm run start
```
#### Docker
Build the Docker image:
```bash
docker buildx build . -t coding-agent
```
Run the container:
```bash
docker run --rm -it coding-agent
```
### 3. Configuration
- **Prompt Setting:** Edit the main prompt in `network.run(...)` in `src/index.ts` to change the agent's task.
- **Debug Logs:** Set `enableDebugLogs=true` for detailed agent flow tracking.
### 4. Example Usage
Try the default prompt in `src/index.ts`:
```typescript
const result = await network.run(
`Create a minimal React app called "Notes" that lets users add, view, and delete notes. Each note should have a title and content. Use Create React App or Vite for setup. Include a simple UI with a form to add notes and a list to display them.`
)
```
**The agent will:**
- Scaffold the app
- Install dependencies
- Start the dev server
- Generate a preview link
Terminal output example:
```
✔️ App is ready!
Preview: https://5173-3a828150-1573-42e3-bf9f-9793a2c2c0c2.proxy.daytona.works
```
The image below shows the result generated in this run:
### 5. Understanding the Agent's Execution Flow
The agent operates by leveraging a large language model (LLM) under the hood and has access to a suite of specialized tools. These tools enable the agent to perform everything needed inside the Daytona sandbox environment; from project setup to app deployment. At each step, the agent reasons about the current goal, decides which tool to call, and orchestrates the workflow to achieve the desired outcome.
Below is a step-by-step breakdown of how the agent builds the demo Notes app showing each iteration, the agent's message, and the tool used:
#### Project Initialization
**Agent message:**
```
I'll create a React app for Notes using Vite, which provides a faster and more modern setup compared to Create React App. I'll break this down into steps:
- First, I'll create the project using Vite
- Install necessary dependencies
- Create the components for the Notes app
- Implement the core functionality
Let's get started:
```
**Tool used:**
`shellTool` was called with command:
```
npm create vite@latest notes -- --template react
```
#### Install Dependencies
**Agent message:**
```
Now, I'll navigate to the project directory and install dependencies:
```
**Tool used:**
`shellTool` was called with command:
```
cd notes && npm install
```
#### Create Components & Update App Structure
**Agent message:**
```
Now, I'll create the necessary components and update the app structure:
1. Update App.jsx
2. Update App.css for basic styling
```
**Tool used:**
`uploadFilesTool` was called to upload:
```
App.jsx (main component logic)
App.css (basic styling)
```
#### Start Development Server
**Agent message:**
```
Now, I'll start the development server:
```
**Tool used:**
`startDevServerTool` was called with command:
```
cd notes && npm run dev
```
#### Check Dev Server Health
**Agent message:**
```
Let's check the dev server health:
```
**Tool used:**
`checkDevServerHealthTool` was called to verify that the development server is running and accessible.
#### App Summary & Features
**Agent message:**
```
Great! I've created a minimal React Notes app with the following features:
- Add notes with a title and content
- View the list of notes
- Delete individual notes
- Basic responsive styling
Key details:
- Used Vite for project setup
- Implemented state management with React hooks (useState)
- Simple form for adding notes
- Notes are stored in local state
- Each note has a unique ID, title, and content
- Delete functionality for individual notes
DEV_SERVER_PORT=5173
TASK_COMPLETED
The app is now running and ready to use. Users can:
1. Enter a note title in the first input
2. Enter note content in the textarea
3. Click "Add Note" to save the note
4. View notes in the list below
5. Delete notes by clicking the "Delete" button next to each note
Would you like me to explain any part of the implementation or make any modifications?
```
Notice `DEV_SERVER_PORT` and `TASK_COMPLETED` fields in the agent output message.
`DEV_SERVER_PORT` value is auto-detected by the agent and used to generate the preview link for your app.
`TASK_COMPLETED` signals that the task is finished which is used inside the agent routing logic.
---
**Key advantages:**
- Secure, isolated execution
- Multi-language support
- Auto-detects dev server, starts it, and generates preview link
- Detailed debug logs for agent actions
# OpenCode Guides
Guides for running [OpenCode](https://opencode.ai/) with Daytona.
# Run OpenCode with the Daytona Plugin
import { Image } from 'astro:assets'
import opencodePluginGif from '../../../../../assets/docs/images/opencode-plugin.gif'
This guide demonstrates how to run the [Daytona OpenCode plugin](https://www.npmjs.com/package/@daytona/opencode) which integrates Daytona sandboxes and OpenCode. When the plugin is active, all agent operations occur in secure sandboxes, with one sandbox per OpenCode session. The plugin also has the ability to sync changes between sandboxes and local Git branches.
### 1. Workflow Overview
When you run OpenCode with the Daytona plugin, sandboxes are created automatically inside OpenCode sessions. Operations such as running code, installing dependencies, and starting servers occur in the sandbox.
Sandboxes are preserved until you delete the OpenCode session. If a local Git repository is detected, the plugin syncs changes between the sandbox and branches with the `opencode/` prefix.
### 2. Project Setup
#### Add the Plugin
Add the Daytona plugin to your project by creating or editing `opencode.json` in the project directory:
```json
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["@daytona/opencode"]
}
```
OpenCode downloads the plugin automatically when it starts. To install the plugin globally instead, edit `~/.config/opencode/opencode.json` and add the same `plugin` entry.
#### Configure Environment
This plugin requires a [Daytona account](https://www.daytona.io/) and [Daytona API key](https://app.daytona.io/dashboard/keys) to create sandboxes.
Set your API key:
```bash
export DAYTONA_API_KEY="your-api-key"
```
Or create a `.env` file in your project root:
```env
DAYTONA_API_KEY=your-api-key
```
#### Run OpenCode
:::note[Git required]
Ensure your project is a Git repository to enable syncing changes between the sandbox and your machine.
:::
Initialize Git if needed, then start OpenCode:
```bash
git init
opencode
```
You can now use OpenCode as usual. As you work, you will see notifications in OpenCode indicating sandboxes are being created and changes are being synced to local branches.
To confirm the plugin is working, type `pwd` in the chat and you should see a path like `/home/daytona/project`.
To view live logs from the plugin for debugging:
```bash
tail -f ~/.local/share/opencode/log/daytona.log
```
#### Version control
In your project directory, use Git to list and check out the branches OpenCode creates:
```bash
git branch
git checkout opencode/1
```
By default, new sessions start from the branch that was checked out when OpenCode was started. After this, synchronization only goes one way: from the sandbox to your local branch. To start working from a different branch, use git to check out that branch and start OpenCode again:
```bash
git checkout opencode/1
opencode
```
You can run as many OpenCode sessions in parallel as you want. Use Git to review and merge changes.
### 3. Understanding the Plugin Architecture
The Daytona plugin consists of several modules that provide custom tools, hooks for events, connections from sessions to sandboxes and system prompt transforms. These modules ensure every agent action runs in a [Daytona sandbox](https://www.daytona.io/docs/en/sandboxes.md) and that changes sync to local Git branches:
- **Custom tools:** Overrides bash, read, write, edit, etc., so they run in the sandbox.
- **System prompt transform:** Injects sandbox path and instructions into the agent's system prompt.
- **Event handlers:** Handles session lifecycle events including cleanup (deleting sandboxes) and idle auto-commit (syncing changes to local git branches).
- **Session management:** Manages the creation and deletion of sandboxes and the mapping of sessions to sandboxes.
#### Custom tools
The custom tools module registers overrides for OpenCode's built-in tools so that every file and process operation goes through the Daytona SDK. It receives the project `id` and `worktree` from the plugin context and returns a tool map:
```typescript
export async function customTools(ctx: PluginInput, sessionManager: DaytonaSessionManager) {
logger.info('OpenCode started with Daytona plugin')
const projectId = ctx.project.id
const worktree = ctx.project.worktree
return {
bash: bashTool(sessionManager, projectId, worktree, ctx),
// ... read, write, edit, multiedit, patch, ls, glob, grep, lsp, getPreviewURL
}
}
```
For example, the plugin implementation of the bash tool uses the Daytona SDK to run the command in the sandbox:
```typescript
async execute(args: { command: string; background?: boolean }, ctx: ToolContext) {
const sessionId = ctx.sessionID
const sandbox = await sessionManager.getSandbox(sessionId, projectId, worktree, pluginCtx)
if (args.background) {
// ... create or get exec session, then:
const result = await sandbox.process.executeSessionCommand(execSessionId, {
command: args.command,
runAsync: true,
})
return `Command started in background (cmdId: ${result.cmdId})`
} else {
const result = await sandbox.process.executeCommand(args.command, repoPath)
return `Exit code: ${result.exitCode}\n${result.result}`
}
}
```
All stateful tools (bash, read, write, edit, glob, grep, ls, lsp, multiedit, patch) are overridden the same way. The plugin also adds a custom tool for [preview links](https://www.daytona.io/docs/en/preview.md).
#### System prompt transform
The system prompt transform extends the system prompt to include instructions for the agent to work in the sandbox and use the background option for long-running commands:
```typescript
export async function systemPromptTransform(ctx: PluginInput, repoPath: string) {
return async (input: ExperimentalChatSystemTransformInput, output: ExperimentalChatSystemTransformOutput) => {
output.system.push(
[
'## Daytona Sandbox Integration',
'This session is integrated with a Daytona sandbox.',
`The main project repository is located at: ${repoPath}.`,
'Bash commands will run in this directory.',
'Put all projects in the project directory. Do NOT try to use the current working directory of the host system.',
"When executing long-running commands, use the 'background' option to run them asynchronously.",
'Before showing a preview URL, ensure the server is running in the sandbox on that port.',
].join('\n'),
)
}
}
```
#### Session events
The session events handler listens for OpenCode session lifecycle events and handles them appropriately. When you delete a session, the handler cleans up the corresponding sandbox. When a session becomes idle, it triggers auto-commit and sync:
```typescript
export async function eventHandlers(ctx: PluginInput, sessionManager: DaytonaSessionManager, repoPath: string) {
const projectId = ctx.project.id
const worktree = ctx.project.worktree
return async (args: any) => {
const event = args.event
if (event.type === EVENT_TYPE_SESSION_DELETED) {
const sessionId = (event as EventSessionDeleted).properties.info.id
await sessionManager.deleteSandbox(sessionId, projectId)
toast.show({ title: 'Session deleted', message: 'Sandbox deleted successfully.', variant: 'success' })
} else if (event.type === EVENT_TYPE_SESSION_IDLE) {
const sessionId = event.properties.sessionID
const sandbox = await sessionManager.getSandbox(sessionId, projectId, worktree, ctx)
const branchNumber = sessionManager.getBranchNumberForSandbox(projectId, sandbox.id)
if (!branchNumber) return
const sessionGit = new SessionGitManager(sandbox, repoPath, worktree, branchNumber)
await sessionGit.autoCommitAndPull(ctx)
}
}
}
```
#### File synchronization
While OpenCode is in use, the plugin uses Git to keep session sandboxes and your local Git repository in sync. This only occurs if a git repository is detected in the project directory. On plugin start:
1. The plugin looks for a Git repository in the local directory.
2. A parallel repository is created in the sandbox with a single `opencode` branch, mirroring your current local branch.
3. A `sandbox` remote is added to your local repo using an SSH connection to the sandbox.
4. Your current `HEAD` is pushed to `opencode`, and the sandbox repo is reset to that state.
5. On session idle, the plugin commits in the sandbox on `opencode`, then pulls into a local branch (`opencode/1`, `opencode/2`, etc.) which is unique to each sandbox. A notification is shown when the sync is complete.
For more information on how the sync is implemented, see the [SessionGitManager](https://github.com/daytona/integrations/blob/main/packages/opencode-plugin/.opencode/plugin/daytona/git/session-git-manager.ts) class.
:::caution
When the plugin syncs to local `opencode` branches, any local changes on those branches are overwritten.
:::
#### Session storage
The session manager stores which sandbox belongs to each project in JSON files (using the same base path as OpenCode via `xdg-basedir`).
- **macOS:** `~/.local/share/opencode/storage/daytona/[projectid].json`
- **Windows:** `%LOCALAPPDATA%\opencode\storage\daytona\[projectid].json`
Each file holds sandbox metadata for that project's sessions so that sandboxes are retained between OpenCode uses.
**Key advantages:**
- Secure, isolated execution: each OpenCode session runs in its own Daytona sandbox
- Sandboxes persist until you delete the OpenCode session
- Live [preview links](https://www.daytona.io/docs/en/preview.md) when a server starts in the sandbox
- Automatic git sync to local branches so you can review and merge agent changes
- No script to run: add the plugin and use OpenCode as usual
# Build a Coding Agent Using OpenCode SDK and Daytona
import { Image } from 'astro:assets'
import opencodeSdkSyncboard from '../../../../../assets/docs/images/opencode-sdk-syncboard.gif'
This guide walks you through running the [OpenCode](https://opencode.ai/) autonomous coding agent inside a secure Daytona sandbox. The OpenCode server runs fully isolated in the sandbox and the script connects to the server using the [OpenCode SDK](https://www.npmjs.com/package/@opencode-ai/sdk).
The agent can develop full-stack web apps, write code in any language, install dependencies, and run scripts. It can also start and manage dev servers, and generate [preview links](https://www.daytona.io/docs/en/preview.md) for live apps.
### 1. Workflow Overview
When you run the script, it creates a Daytona sandbox, installs OpenCode, and starts the OpenCode server inside the sandbox. You send prompts and see responses in your terminal. On exit, the sandbox is deleted.
```
$ npm run start
Creating sandbox...
Installing OpenCode in sandbox...
Preview: https://4096-0626a08c-08e7-4fbe-9799-d160bac66b08.daytonaproxy01.net
Press Ctrl+C at any time to exit.
User: Create a minimalist collaborative whiteboard app
Thinking...
🔨 ✓ Run: Check current directory structure
✓ 3 todos
📝 Add home/daytona/SPEC.md
✓ 2 todos
📝 Add home/daytona/index.html
✓ 1 todos
✓ home/daytona/SPEC.md
✓ https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&family=Outfit:wght@400;500;700&display=swap (text/css; charset=utf-8)
✓ home/daytona/index.html
🔨 ✓ Run: List files in directory
🔨 ✓ Run: Start HTTP server for whiteboard
🔨 ✓ Run: Check if server is running
✓ 0 todos
Your minimalist collaborative whiteboard app is ready!
**Preview URL:** https://8080-0626a08c-08e7-4fbe-9799-d160bac66b08.daytonaproxy01.net
## SyncBoard - Features
**Drawing Tools:**
- Pencil (freehand drawing)
- Line, Rectangle, Ellipse (shape tools with preview)
- Eraser (removes intersecting strokes)
**Customization:**
- 6 vibrant color presets
- 3 stroke widths (thin/medium/thick)
- Keyboard shortcuts: `P` `L` `R` `E` `X` for tools, `1` `2` `3` for stroke widths
**Collaboration:**
- Simulated real-time collaboration with other users
- Unique user colors for each participant
- Auto-saves to localStorage every 2 seconds
**Actions:**
- Undo/Redo (stores 20 actions, Ctrl+Z / Ctrl+Shift+Z)
- Clear canvas (with confirmation dialog)
- Coordinates display in status bar
**Design:**
- Dark theme with coral/teal accents
- Dot grid background pattern
- Smooth animations and hover effects
- Responsive layout for mobile devices
User:
Cleaning up...
```
When your task involves running or previewing a web application, the agent can host the app and generate a [preview link](https://www.daytona.io/docs/en/preview.md) for you to inspect the live result:
You can continue interacting with your agent until you are finished. When you exit the program, the sandbox will be deleted automatically.
### 2. Project Setup
#### Clone the Repository
First, clone the Daytona [repository](https://github.com/daytona/guides.git) and navigate to the OpenCode SDK example:
```bash
git clone https://github.com/daytona/guides.git
cd guides/typescript/opencode/opencode-sdk
```
#### Configure Environment
Get your API key from the [Daytona Dashboard](https://app.daytona.io/dashboard/keys).
Copy `.env.example` to `.env` and add your key:
```bash
DAYTONA_API_KEY=your_daytona_key
```
#### Local Usage
:::note[Node.js Version]
Node.js 18 or newer is required to run this example. Please ensure your environment meets this requirement before proceeding.
:::
Install dependencies and run the example:
```bash
npm install
npm run start
```
The agent will start and wait for your prompt.
### 3. Understanding the Script
OpenCode uses a client-server model: The [server](https://opencode.ai/docs/server/) manages coding agents, sessions and configuration. Clients communicate with the server [over HTTP](https://opencode.ai/docs/server/#apis) to run prompts in sessions and receive streamed responses.
#### Initialization
On startup, the script:
1. Creates a new [Daytona sandbox](https://www.daytona.io/docs/en/sandboxes.md).
2. Installs OpenCode globally in the sandbox.
3. Starts the OpenCode server in the sandbox.
4. Gets the preview URL for the server and creates a client with that base URL.
5. Enters the readline loop to send prompts to the server and receive streamed responses.
6. On Ctrl+C, deletes the sandbox and exits.
#### OpenCode Server
The OpenCode server runs inside the sandbox and handles all agent work: LLM calls, tools (bash, write, etc.), and code execution. The script starts it with:
```ts
const envVar = injectEnvVar('OPENCODE_CONFIG_CONTENT', opencodeConfig)
const command = await sandbox.process.executeSessionCommand(sessionId, {
command: `${envVar} opencode serve --port ${PORT} --hostname ${HOSTNAME}`,
runAsync: true,
})
```
A custom configuration is injected which includes a system prompt. The system prompt tells the agent it runs in a sandbox, to use `/home/daytona` for file operations, and the preview URL pattern so it can provide correct preview links.
#### OpenCode Client
The client runs on your machine and talks to the server using the [OpenCode SDK](https://opencode.ai/docs/sdk/). `Session.create` sets up the SDK client and subscribes to the event stream so the client can show tool activity (e.g. “✓ Run: …”, “📝 Add …”) as the agent works:
```ts
const client = createOpencodeClient({ baseUrl })
const sessionRes = await client.session.create({ body: { title: 'Daytona query' } })
const sessionId = sessionRes.data?.id
if (!sessionId) throw new Error('Failed to create OpenCode session:' + sessionRes.error)
const events = await client.event.subscribe()
```
To send a prompt, the client calls `session.prompt` and processes events from the existing stream to show tool activity:
```ts
const promptPromise = this.client.session.prompt({
path: { id: this.sessionId },
body: { parts: [{ type: 'text', text: query } satisfies TextPartInput] },
})
for await (const event of takeUntil(this.events.stream, promptPromise)) {
printEvent(this.sessionId, event)
}
```
#### Main loop
When the server is ready, the script creates a session and runs a readline loop:
```ts
const session = await Session.create(baseUrl)
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
while (true) {
const query = await new Promise((resolve) => rl.question('User: ', resolve))
if (!query.trim()) continue
await session.runQuery(query)
}
```
The readline loop waits for user input, sends it to the agent, and prints the response.
**Key advantages:**
- Secure, isolated execution in Daytona sandboxes
- OpenCode SDK client in your terminal; server runs in the sandbox
- Support for 75+ LLM providers
- All agent code execution happens inside the sandbox
- Automatic preview link generation for deployed services
- Custom agent configuration for Daytona-specific workflows
# Build a Coding Agent Using OpenCode Web and Daytona
import { Image } from 'astro:assets'
import opencodeResult from '../../../../../assets/docs/images/opencode-web-agent.gif'
This guide demonstrates how to run the [OpenCode](https://opencode.ai/) coding agent inside a Daytona sandbox environment using OpenCode's easy-to-use [web interface](https://opencode.ai/docs/web/).
The agent can develop web apps, write code in any language, install dependencies, and run scripts. It supports over 75 different LLM providers and can start dev servers with preview links for live apps.
---
### 1. Workflow Overview
When you launch the main script, a Daytona sandbox is created and OpenCode is installed inside it. OpenCode is configured with a custom Daytona-aware agent.
The script provides a preview link to access the web interface, where you can create, configure and interact with agent sessions:
```
$ npm run start
Creating sandbox...
Installing OpenCode...
Starting OpenCode web server...
Press Ctrl+C to stop.
▄
█▀▀█ █▀▀█ █▀▀█ █▀▀▄ █▀▀▀ █▀▀█ █▀▀█ █▀▀█
█░░█ █░░█ █▀▀▀ █░░█ █░░░ █░░█ █░░█ █▀▀▀
▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀
Web interface: https://3000-1e0f775c-c01b-40e7-8c64-062fd3dadd75.proxy.daytona.works/
```
The agent can host web apps and provide you with a preview link using the [Daytona Preview Links](https://www.daytona.io/docs/en/preview.md) feature. When your task involves running or previewing a web application, the agent automatically reasons about this need, hosts the app, and generates a preview link for you to inspect the live result:
You can continue interacting with your agent until you are finished. When you exit the program, the sandbox will be deleted automatically.
### 2. Project Setup
#### Clone the Repository
First, clone the Daytona [repository](https://github.com/daytona/guides.git) and navigate to the example directory:
```bash
git clone https://github.com/daytona/guides.git
cd guides/typescript/opencode/opencode-web
```
#### Configure Environment
Get your API key from the [Daytona Dashboard](https://app.daytona.io/dashboard/keys).
Copy `.env.example` to `.env` and add your key:
```bash
DAYTONA_API_KEY=your_daytona_key
```
#### Local Usage
:::note[Node.js Version]
Node.js 18 or newer is required to run this example. Please ensure your environment meets this requirement before proceeding.
:::
Install dependencies:
```bash
npm install
```
Run the example:
```bash
npm run start
```
The OpenCode web interface will start and wait for you to open it in your browser.
### Models and API Providers
OpenCode works with [over 75 LLM providers](https://opencode.ai/docs/providers/), with a free provider selected by default. You can change the model or provider at any time using the menu below the prompt input in the web interface. If your chosen provider needs an API key, you’ll be prompted to enter it.
#### Persisting API Keys
To persist API keys between uses of the script, you can set them as environment variables when creating the Daytona sandbox.
For example, to use an Anthropic API key, modify the `daytona.create()` call in `src/index.ts` to include your desired API key:
```typescript
sandbox = await daytona.create({
envVars: {
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY || '',
},
})
```
### 3. Understanding the Script
This example consists of a Node.js script that installs, configures and runs OpenCode inside a Daytona sandbox.
#### Initialization
On initialization, the main script:
1. Creates a new [Daytona sandbox](https://www.daytona.io/docs/en/sandboxes.md).
2. Installs OpenCode globally inside the sandbox using npm with [process execution](https://www.daytona.io/docs/en/process-code-execution.md#command-execution).
3. Creates and uploads a [custom agent configuration](https://opencode.ai/docs/agents/) with Daytona-specific system prompt.
4. Starts the OpenCode web server inside the sandbox on port 3000.
5. Substitutes the URL in OpenCode's output with a [Daytona preview link](https://www.daytona.io/docs/en/preview.md) for the web interface.
#### Main Script Code
The script creates a session and executes OpenCode as an asynchronous command, which allows it to stream output while keeping the process alive:
```typescript
const command = await sandbox.process.executeSessionCommand(sessionId, {
command: `${envVar} opencode web --port ${OPENCODE_PORT}`,
runAsync: true,
})
```
When OpenCode starts its web server, it prints a link to its web UI using a localhost address (e.g., `http://127.0.0.1:3000`). However, since the sandbox runs remotely, this localhost link is only accessible inside the sandbox itself. To solve this, the script parses OpenCode’s output, and replaces the URL with the corresponding Daytona preview link.
```typescript
const opencodePreviewLink = await sandbox.getPreviewLink(OPENCODE_PORT)
const replaceUrl = (text: string) =>
text.replace(
new RegExp(`http:\\/\\/127\\.0\\.0\\.1:${OPENCODE_PORT}`, 'g'),
opencodePreviewLink.url
)
```
#### OpenCode Agent Configuration
A custom system prompt is used to instruct the agent on how to use Daytona sandbox paths and preview links. This prompt is packaged into a JSON configuration string, which is passed to the sandbox as the `OPENCODE_CONFIG_CONTENT` environment variable:
```json
{
"$schema": "https://opencode.ai/config.json",
"default_agent": "daytona",
"agent": {
"daytona": {
"description": "Daytona sandbox-aware coding agent",
"mode": "primary",
"prompt": "You are running in a Daytona sandbox. Use the /home/daytona directory instead of /workspace for file operations. When running services on localhost, they will be accessible as: . When starting a server, always give the user the preview URL to access it. When starting a server, start it in the background with & so the command does not block further instructions."
}
}
}
```
The `` in the agent prompt is a template URL where `{PORT}` is a placeholder for the port to access on the Daytona sandbox. This template string is created by generating a [preview link](https://www.daytona.io/docs/en/preview.md) for a specific port number and then replacing the port number with `{PORT}`.
#### Clean up
When you press `Ctrl+C`, the script automatically cleans up by deleting the sandbox:
```typescript
process.once('SIGINT', async () => {
console.log('\nCleaning up...')
if (sandbox) await sandbox.delete()
process.exit(0)
})
```
**Key advantages:**
- Secure, isolated execution in Daytona sandboxes
- OpenCode Web interface accessible via browser
- Support for 75+ LLM providers
- All agent code execution happens inside the sandbox
- Automatic preview link generation for deployed services
- Custom agent configuration for Daytona-specific workflows
- Clean resource management with automatic sandbox cleanup
# Run Pi with the Daytona Extension
import piExtensionGif from '../../../../../assets/docs/images/pi-extension.gif'
This guide demonstrates how to run the [Daytona Pi extension](https://www.npmjs.com/package/@daytona/pi) which integrates Daytona sandboxes and [Pi](https://pi.dev/). When the extension is active, the agent runs on your machine while every tool call — `bash`, file I/O, and search — runs inside a secure Daytona sandbox. Each session gets its own sandbox, and your work is synced to a per-session GitHub branch.
### 1. Workflow Overview
When you launch Pi with `--daytona`, a sandbox is created for the session and the repo you launched Pi in is cloned into it. From then on, every tool call — running code, installing dependencies, starting servers — happens in the sandbox rather than on your host.
### 2. Setup
#### Install Pi
Install Pi globally with npm:
```bash
npm install -g @earendil-works/pi-coding-agent
```
See the [Pi documentation](https://pi.dev/) for other install options.
#### Add the Extension
Add the Daytona extension to Pi:
```bash
pi install npm:@daytona/pi
```
To update it later, run `pi update` — `pi install` will not refresh an existing install.
#### Configure Environment
This extension requires a [Daytona account](https://www.daytona.io/) and [Daytona API key](https://app.daytona.io/dashboard/keys) to create sandboxes.
```bash
export DAYTONA_API_KEY="your-api-key"
```
If no key is set, Pi prompts you for one once per session. To enable GitHub branch sync, also authenticate the [GitHub CLI](https://cli.github.com/) (`gh auth login`).
#### Run Pi
Run Pi from inside a git repository:
```bash
cd my-project
pi --daytona
```
The repo you're in is cloned into the sandbox and your work is synced to a GitHub branch. Point at a different repository with `--repo`, or run outside a git repo for a blank workspace.
| Flag | Description |
| ------------------- | ----------------------------------------------------- |
| `--daytona` | Run tools inside a Daytona sandbox |
| `--repo ` | Git repo to clone (defaults to the repo you're in) |
| `--branch ` | Branch to clone (defaults to your current branch) |
| `--snapshot ` | Choose a Daytona snapshot / base image |
| `--public` | Create a public sandbox so preview URLs need no token |
| `--idle-stop ` | Minutes idle before the sandbox pauses (default 15) |
#### Slash commands
While Pi is running with `--daytona`, you can manage the active sandbox:
- `/sandbox` — show the active sandbox's status: state, working directory, branch, sync status, and its GitHub branch link
- `/github` — open this session's branch on GitHub
- `/compare` — open this session's branch compare view on GitHub
- `/merge` — merge this session's branch into its base on GitHub
- `/pr` — open a GitHub pull request for this session's branch
### 3. Understanding the Extension Architecture
The extension is a single Pi extension that splits work between your machine and the sandbox:
| | On your machine (host) | In the sandbox (Linux) |
| ----------------- | ------------------------------ | ---------------------------------------------------------------------------------- |
| **Agent & tools** | The agent (LLM, TUI, sessions) | File operations and commands (`bash`, `read`, `write`, `edit`, `ls`, `find`, `grep`) |
| **Git & GitHub** | GitHub authorization with `gh`, and `/merge` via the GitHub API | Git operations (clone and push) via the [Daytona git API](https://www.daytona.io/docs/en/git-operations.md) |
A session flows through it like this:
1. **[Tool registration](#1-tool-registration)** — at startup, Pi's built-in tools are replaced with sandbox-backed versions, so every tool call runs in the sandbox.
2. **[Session start](#2-session-start)** — a sandbox is created for the session, or reattached when you resume it, and the repo is cloned in.
3. **[System prompt](#3-system-prompt)** — before the agent starts, the system prompt is pointed at the sandbox.
4. **[GitHub branch sync](#4-github-branch-sync)** — after each turn, the agent's commits are pushed to its branch.
5. **[Reconciliation](#5-reconciliation)** — after you delete a session, its orphaned sandbox is eventually reconciled and removed.
#### 1. Tool registration
Each built-in tool is re-registered so that, when a sandbox is active, it runs against it through the [Daytona SDK](https://www.npmjs.com/package/@daytona/sdk). A small `sandboxTool` helper wraps a local tool with a per-call rebuild against the active sandbox:
```typescript
// Wrap a local tool so it runs against the sandbox (rebuilt per call) when one is active.
function sandboxTool(local, makeRemote) {
return {
...local,
execute: (...args) => {
const active = requireSandbox(); // throws when --daytona is set but no sandbox; null when off
const tool = active ? makeRemote(active.cwd, active.sandbox) : local;
return tool.execute(...args);
},
};
}
pi.registerTool(sandboxTool(localBash, (cwd, sb) => createBashTool(cwd, { operations: createBashOps(sb) })));
```
`read`, `write`, `edit`, and `ls` are registered the same way with their own factories; `find` and `grep` run a dedicated search inside the sandbox.
#### 2. Session start
On `session_start` the extension reattaches the sandbox it created earlier for this session — so resuming restores your work — or creates a new one:
```typescript
pi.on("session_start", async (event, ctx) => {
const prev = lastSandboxFor(ctx); // recorded in a session entry on first run
active = prev
? await daytona.get(prev.sandboxId) // resume → reattach
: await daytona.create({
labels: { "created-by": "pi-daytona", "session-id": sessionId },
}); // new → create
});
```
With a GitHub repo, a new sandbox also gets the `pi/` branch created and cloned into it.
#### 3. System prompt
A `before_agent_start` hook rewrites the agent's working-directory line to the sandbox path and tells it to commit (but not push) its work:
```typescript
pi.on("before_agent_start", (event) => {
if (!active) return;
const cwdLine = `Current working directory: ${active.cwd} (Daytona sandbox ${shortId(active.sandbox.id)})`;
let systemPrompt = event.systemPrompt.replace(/Current working directory: .*/g, cwdLine);
systemPrompt +=
"\n\nThis project is a git repository inside a Daytona sandbox. After you finish a unit of work, " +
'commit it with git. Do not push — pushing is handled automatically.';
return { systemPrompt };
});
```
#### 4. GitHub branch sync
When you're in a github.com repo and authenticated with `gh`, each session gets its own branch. The host only uses `gh` to mint a token and call the GitHub REST API (to create the branch and merge it); all git transfer — clone, commit, push — runs **inside the sandbox** via the Daytona git API.
The agent commits its own work; after each turn the extension pushes any new commits to the branch:
```typescript
pi.on("agent_end", async () => {
if (!active?.git) return;
const token = await getGithubToken(pi);
const status = await active.sandbox.git.status(active.cwd);
if ((status.ahead ?? 0) > 0) {
await active.sandbox.git.push(active.cwd, "x-access-token", token);
}
});
```
When you're not in a github.com repo (or `gh` isn't authenticated), push is disabled — the sandbox still keeps a local git repo so the agent can commit, but nothing is pushed.
#### 5. Reconciliation
Pi has no "session deleted" event, so the extension reconciles: on `session_start` and `session_shutdown` it reaps any sandbox whose session no longer exists by comparing Daytona's labelled sandboxes against Pi's live sessions.
```typescript
async function reapOrphans(daytona) {
const live = new Set((await SessionManager.listAll()).map((s) => s.id));
for await (const sandbox of daytona.list({ labels: { "created-by": "pi-daytona" } })) {
const sessionId = sandbox.labels?.["session-id"];
if (sessionId && !live.has(sessionId)) await sandbox.delete();
}
}
```
So deleting a session from Pi's resume menu cleans up its sandbox on the next launch or exit.
**Key advantages:**
- Secure, isolated execution: every Pi tool call runs in a Daytona sandbox, and never on your host when `--daytona` is set
- One sandbox per session, reattached on resume and deleted only when you delete the session
- Optional GitHub branch sync: each session gets a `pi/` branch you can review and merge
- Live [preview links](https://www.daytona.io/docs/en/preview.md) when a server starts in the sandbox
- The agent's brain stays on your machine — only tool execution is remote
# Analyze Data With LangChain AI Agent
import { Image } from 'astro:assets'
import chartImage from '../../../../../assets/docs/images/langchain-data-analysis-chart.png'
This package provides the `DaytonaDataAnalysisTool` - LangChain tool integration that enables agents to perform secure Python data analysis in a sandboxed environment. It supports multi-step workflows, file uploads/downloads, and custom result handling, making it ideal for automating data analysis tasks with LangChain agents.
This page demonstrates the use of this tool with a basic example analyzing a vehicle valuations dataset. Our goal is to analyze how vehicle prices vary by manufacturing year and create a line chart showing average price per year.
---
### 1. Workflow Overview
You upload your dataset and provide a natural language prompt describing the analysis you want. The agent reasons about your request, determines how to use the `DaytonaDataAnalysisTool` to perform the task on your dataset, and executes the analysis securely in a Daytona sandbox.
You provide the data and describe what insights you need - the agent handles the rest.
### 2. Project Setup
#### Clone the Repository
Clone the [repository](https://github.com/daytona/guides) and navigate to the example directory:
```bash
git clone https://github.com/daytona/guides.git
cd guides/python/langchain/data-analysis/anthropic
```
#### Install Dependencies
:::note[Python Version Requirement]
This example requires **Python 3.10 or higher** because it uses LangChain 1.0+ syntax. It's recommended to use a virtual environment (e.g., `venv` or `poetry`) to isolate project dependencies.
:::
Install the required packages for this example:
```bash
pip install -U langchain langchain-anthropic langchain-daytona-data-analysis python-dotenv
```
The packages include:
- `langchain`: LangChain framework for building AI agents
- `langchain-anthropic`: Integration package connecting Claude (Anthropic) APIs and LangChain
- `langchain-daytona-data-analysis`: Provides the `DaytonaDataAnalysisTool` for LangChain agents
- `python-dotenv`: Used for loading environment variables from `.env` file
#### Configure Environment
Get your API keys and configure your environment:
1. **Daytona API key:** Get it from [Daytona Dashboard](https://app.daytona.io/dashboard/keys)
2. **Anthropic API key:** Get it from [Anthropic Console](https://console.anthropic.com/)
Create a `.env` file in your project:
```bash
DAYTONA_API_KEY=dtn_***
ANTHROPIC_API_KEY=sk-ant-***
```
### 3. Download Dataset
We'll be using a publicly available dataset of vehicle valuation. You can download it directly from:
[https://download.daytona.io/dataset.csv](https://download.daytona.io/dataset.csv)
Download the file and save it as `dataset.csv` in your project directory.
### 4. Initialize the Language Model
Models are the reasoning engine of LangChain agents - they drive decision-making, determine which tools to call, and interpret results.
In this example, we'll use Anthropic's Claude model, which excels at code generation and analytical tasks.
Configure the Claude model with the following parameters:
```python
from langchain_anthropic import ChatAnthropic
model = ChatAnthropic(
model_name="claude-sonnet-4-5-20250929",
temperature=0,
timeout=None,
max_retries=2,
stop=None
)
```
**Parameters explained:**
- `model_name`: Specifies the Claude model to use
- `temperature`: Tunes the degree of randomness in generation
- `max_retries`: Number of retries allowed for Anthropic API requests
:::tip[Learn More About Models]
For detailed information about LangChain models, different providers, and how to choose the right model for your use case, visit the [LangChain Models documentation](https://docs.langchain.com/oss/python/langchain/models).
:::
### 5. Define the Result Handler
When the agent executes Python code in the sandbox, it generates artifacts like charts and output logs. We can define a handler function to process these results.
This function will extract chart data from the execution artifacts and save them as PNG files:
```python
import base64
from daytona import ExecutionArtifacts
def process_data_analysis_result(result: ExecutionArtifacts):
# Print the standard output from code execution
print("Result stdout", result.stdout)
result_idx = 0
for chart in result.charts:
if chart.png:
# Charts are returned in base64 format
# Decode and save them as PNG files
with open(f'chart-{result_idx}.png', 'wb') as f:
f.write(base64.b64decode(chart.png))
print(f'Chart saved to chart-{result_idx}.png')
result_idx += 1
```
This handler processes execution artifacts by:
- Logging stdout output from the executed code
- Extracting chart data from the artifacts
- Decoding base64-encoded PNG charts
- Saving them to local files
### 6. Configure the Data Analysis Tool
Now we'll initialize the `DaytonaDataAnalysisTool` and upload our dataset.
```python
from langchain_daytona_data_analysis import DaytonaDataAnalysisTool
# Initialize the tool with our result handler
DataAnalysisTool = DaytonaDataAnalysisTool(
on_result=process_data_analysis_result
)
# Upload the dataset with metadata describing its structure
with open("./dataset.csv", "rb") as f:
DataAnalysisTool.upload_file(
f,
description=(
"This is a CSV file containing vehicle valuations. "
"Relevant columns:\n"
"- 'year': integer, the manufacturing year of the vehicle\n"
"- 'price_in_euro': float, the listed price of the vehicle in Euros\n"
"Drop rows where 'year' or 'price_in_euro' is missing, non-numeric, or an outlier."
)
)
```
**Key points:**
- The `on_result` parameter connects our custom result handler
- The `description` provides context about the dataset structure to the agent
- Column descriptions help the agent understand how to process the data
- Data cleaning instructions ensure quality analysis
### 7. Create and Run the Agent
Finally, we'll create the LangChain agent with our configured model and tool, then invoke it with our analysis request.
```python
from langchain.agents import create_agent
# Create the agent with the model and data analysis tool
agent = create_agent(model, tools=[DataAnalysisTool], debug=True)
# Invoke the agent with our analysis request
agent_response = agent.invoke({
"messages": [{
"role": "user",
"content": "Analyze how vehicles price varies by manufacturing year. Create a line chart showing average price per year."
}]
})
# Always close the tool to clean up sandbox resources
DataAnalysisTool.close()
```
**What happens here:**
1. The agent receives your natural language request
2. It determines it needs to use the `DaytonaDataAnalysisTool`
3. Agent generates Python code to analyze the data
4. Code executes securely in the Daytona sandbox
5. Results are processed by our handler function
6. Charts are saved to your local directory
7. Sandbox resources are cleaned up at the end
### 8. Running Your Analysis
Now you can run the complete code to see the results.
```bash
python data_analysis.py
```
#### Understanding the Agent's Execution Flow
When you run the code, the agent works through your request step by step. Here's what happens in the background:
**Step 1: Agent receives and interprets the request**
The agent acknowledges your analysis request:
```
AI Message: "I'll analyze how vehicle prices vary by manufacturing year and create a line chart showing the average price per year."
```
**Step 2: Agent generates Python code**
The agent generates Python code to explore the dataset first:
```python
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
# Load the dataset
df = pd.read_csv('/home/daytona/dataset.csv')
# Display basic info about the dataset
print("Dataset shape:", df.shape)
print("\nFirst few rows:")
print(df.head())
print("\nColumn names:")
print(df.columns.tolist())
print("\nData types:")
print(df.dtypes)
```
**Step 3: Code executes in Daytona sandbox**
The tool runs this code in a secure sandbox and returns the output:
```
Result stdout Dataset shape: (100000, 15)
First few rows:
Unnamed: 0 ... offer_description
0 75721 ... ST-Line Hybrid Adapt.LED+Head-Up-Display Klima
1 80184 ... blue Trend,Viele Extras,Top-Zustand
2 19864 ... 35 e-tron S line/Matrix/Pano/ACC/SONOS/LM 21
3 76699 ... 2.0 Lifestyle Plus Automatik Navi FAP
4 92991 ... 1.6 T 48V 2WD Spirit LED, WR
[5 rows x 15 columns]
Column names:
['Unnamed: 0', 'brand', 'model', 'color', 'registration_date', 'year',
'price_in_euro', 'power_kw', 'power_ps', 'transmission_type', 'fuel_type',
'fuel_consumption_l_100km', 'fuel_consumption_g_km', 'mileage_in_km',
'offer_description']
Data types:
Unnamed: 0 int64
brand object
model object
color object
registration_date object
year object
price_in_euro object
power_kw object
power_ps object
transmission_type object
fuel_type object
fuel_consumption_l_100km object
fuel_consumption_g_km object
mileage_in_km float64
offer_description object
dtype: object
```
**Step 4: Agent generates detailed analysis code**
Based on the initial dataset information, the agent generates more specific code to examine the key columns:
```python
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
# Load the dataset
df = pd.read_csv('/home/daytona/dataset.csv')
print("Dataset shape:", df.shape)
print("\nColumn names:")
print(df.columns.tolist())
# Check for year and price_in_euro columns
print("\nChecking 'year' column:")
print(df['year'].describe())
print("\nMissing values in 'year':", df['year'].isna().sum())
print("\nChecking 'price_in_euro' column:")
print(df['price_in_euro'].describe())
print("\nMissing values in 'price_in_euro':", df['price_in_euro'].isna().sum())
```
**Step 5: Execution results from sandbox**
The code executes and returns column statistics:
```
Result stdout Dataset shape: (100000, 15)
Column names:
['Unnamed: 0', 'brand', 'model', 'color', 'registration_date', 'year',
'price_in_euro', 'power_kw', 'power_ps', 'transmission_type', 'fuel_type',
'fuel_consumption_l_100km', 'fuel_consumption_g_km', 'mileage_in_km',
'offer_description']
Checking 'year' column:
count 100000
unique 49
top 2019
freq 12056
Name: year, dtype: object
Missing values in 'year': 0
Checking 'price_in_euro' column:
count 100000
unique 11652
top 19990
freq 665
Name: price_in_euro, dtype: object
Missing values in 'price_in_euro': 0
```
**Step 6: Agent generates final analysis and visualization code**
Now that the agent understands the data structure, it generates the complete analysis code with data cleaning, processing, and visualization:
```python
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
# Load the dataset
df = pd.read_csv('/home/daytona/dataset.csv')
print("Original dataset shape:", df.shape)
# Clean the data - remove rows with missing values in year or price_in_euro
df_clean = df.dropna(subset=['year', 'price_in_euro'])
print(f"After removing missing values: {df_clean.shape}")
# Convert to numeric and remove non-numeric values
df_clean['year'] = pd.to_numeric(df_clean['year'], errors='coerce')
df_clean['price_in_euro'] = pd.to_numeric(df_clean['price_in_euro'], errors='coerce')
# Remove rows where conversion failed
df_clean = df_clean.dropna(subset=['year', 'price_in_euro'])
print(f"After removing non-numeric values: {df_clean.shape}")
# Remove outliers using IQR method for both year and price
def remove_outliers(df, column):
Q1 = df[column].quantile(0.25)
Q3 = df[column].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
return df[(df[column] >= lower_bound) & (df[column] <= upper_bound)]
df_clean = remove_outliers(df_clean, 'year')
print(f"After removing year outliers: {df_clean.shape}")
df_clean = remove_outliers(df_clean, 'price_in_euro')
print(f"After removing price outliers: {df_clean.shape}")
print("\nCleaned data summary:")
print(df_clean[['year', 'price_in_euro']].describe())
# Calculate average price per year
avg_price_by_year = df_clean.groupby('year')['price_in_euro'].mean().sort_index()
print("\nAverage price by year:")
print(avg_price_by_year)
# Create line chart
plt.figure(figsize=(14, 7))
plt.plot(avg_price_by_year.index, avg_price_by_year.values, marker='o',
linewidth=2, markersize=6, color='#2E86AB')
plt.xlabel('Manufacturing Year', fontsize=12, fontweight='bold')
plt.ylabel('Average Price (€)', fontsize=12, fontweight='bold')
plt.title('Average Vehicle Price by Manufacturing Year', fontsize=14,
fontweight='bold', pad=20)
plt.grid(True, alpha=0.3, linestyle='--')
plt.xticks(rotation=45)
# Format y-axis to show currency
ax = plt.gca()
ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, p: f'€{x:,.0f}'))
plt.tight_layout()
plt.show()
# Additional statistics
print(f"\nTotal number of vehicles analyzed: {len(df_clean)}")
print(f"Year range: {int(df_clean['year'].min())} - {int(df_clean['year'].max())}")
print(f"Price range: €{df_clean['price_in_euro'].min():.2f} - €{df_clean['price_in_euro'].max():.2f}")
print(f"Overall average price: €{df_clean['price_in_euro'].mean():.2f}")
```
This comprehensive code performs data cleaning, outlier removal, calculates averages by year, and creates a professional visualization.
**Step 7: Final execution and chart generation**
The code executes successfully in the sandbox, processes the data, and generates the visualization:
```
Result stdout Original dataset shape: (100000, 15)
After removing missing values: (100000, 15)
After removing non-numeric values: (99946, 15)
After removing year outliers: (96598, 15)
After removing price outliers: (90095, 15)
Cleaned data summary:
year price_in_euro
count 90095.000000 90095.000000
mean 2016.698563 22422.266707
std 4.457647 12964.727116
min 2005.000000 150.000000
25% 2014.000000 12980.000000
50% 2018.000000 19900.000000
75% 2020.000000 29500.000000
max 2023.000000 62090.000000
Average price by year:
year
2005.0 5968.124319
2006.0 6870.881523
2007.0 8015.234473
2008.0 8788.644495
2009.0 8406.198576
2010.0 10378.815972
2011.0 11540.640435
2012.0 13306.642261
2013.0 14512.707025
2014.0 15997.682899
2015.0 18563.864358
2016.0 20124.556294
2017.0 22268.083322
2018.0 24241.123673
2019.0 26757.469111
2020.0 29400.163494
2021.0 30720.168646
2022.0 33861.717552
2023.0 33119.840175
Name: price_in_euro, dtype: float64
Total number of vehicles analyzed: 90095
Year range: 2005 - 2023
Price range: €150.00 - €62090.00
Overall average price: €22422.27
Chart saved to chart-0.png
```
The agent successfully completed the analysis, showing that vehicle prices generally increased from 2005 (€5,968) to 2022 (€33,862), with a slight decrease in 2023. The result handler captured the generated chart and saved it as `chart-0.png`.
You should see the chart in your project directory that will look similar to this:
### 9. Complete Implementation
Here is the complete, ready-to-run example:
```python
import base64
from dotenv import load_dotenv
from langchain.agents import create_agent
from langchain_anthropic import ChatAnthropic
from daytona import ExecutionArtifacts
from langchain_daytona_data_analysis import DaytonaDataAnalysisTool
load_dotenv()
model = ChatAnthropic(
model_name="claude-sonnet-4-5-20250929",
temperature=0,
timeout=None,
max_retries=2,
stop=None
)
def process_data_analysis_result(result: ExecutionArtifacts):
# Print the standard output from code execution
print("Result stdout", result.stdout)
result_idx = 0
for chart in result.charts:
if chart.png:
# Save the png to a file
# The png is in base64 format.
with open(f'chart-{result_idx}.png', 'wb') as f:
f.write(base64.b64decode(chart.png))
print(f'Chart saved to chart-{result_idx}.png')
result_idx += 1
def main():
DataAnalysisTool = DaytonaDataAnalysisTool(
on_result=process_data_analysis_result
)
try:
with open("./dataset.csv", "rb") as f:
DataAnalysisTool.upload_file(
f,
description=(
"This is a CSV file containing vehicle valuations. "
"Relevant columns:\n"
"- 'year': integer, the manufacturing year of the vehicle\n"
"- 'price_in_euro': float, the listed price of the vehicle in Euros\n"
"Drop rows where 'year' or 'price_in_euro' is missing, non-numeric, or an outlier."
)
)
agent = create_agent(model, tools=[DataAnalysisTool], debug=True)
agent_response = agent.invoke(
{"messages": [{"role": "user", "content": "Analyze how vehicles price varies by manufacturing year. Create a line chart showing average price per year."}]}
)
finally:
DataAnalysisTool.close()
if __name__ == "__main__":
main()
```
**Key advantages of this approach:**
- **Secure execution:** Code runs in isolated Daytona sandbox
- **Automatic artifact capture:** Charts, tables, and outputs are automatically extracted
- **Natural language interface:** Describe analysis tasks in plain English
- **Framework integration:** Seamlessly works with LangChain's agent ecosystem
### 10. API Reference
The following public methods are available on `DaytonaDataAnalysisTool`:
#### download_file
```python
def download_file(remote_path: str) -> bytes
```
Downloads a file from the sandbox by its remote path.
**Arguments**:
- `remote_path` - str: Path to the file in the sandbox.
**Returns**:
- `bytes` - File contents.
**Example**:
```python
# Download a file from the sandbox
file_bytes = tool.download_file("/home/daytona/results.csv")
```
#### upload_file
```python
def upload_file(file: IO, description: str) -> SandboxUploadedFile
```
Uploads a file to the sandbox. The file is placed in `/home/daytona/`.
**Arguments**:
- `file` - IO: File-like object to upload.
- `description` - str: Description of the file, explaining its purpose and the type of data it contains.
**Returns**:
- [`SandboxUploadedFile`](#sandboxuploadedfile) - Metadata about the uploaded file.
**Example**:
Suppose you want to analyze sales data for a retail business. You have a CSV file named `sales_q3_2025.csv` containing columns like `transaction_id`, `date`, `product`, `quantity`, and `revenue`. You want to upload this file and provide a description that gives context for the analysis.
```python
with open("sales_q3_2025.csv", "rb") as f:
uploaded = tool.upload_file(
f,
"CSV file containing Q3 2025 retail sales transactions. Columns: transaction_id, date, product, quantity, revenue."
)
```
#### remove_uploaded_file
```python
def remove_uploaded_file(uploaded_file: SandboxUploadedFile) -> None
```
Removes a previously uploaded file from the sandbox.
**Arguments**:
- `uploaded_file` - [`SandboxUploadedFile`](#sandboxuploadedfile): The file to remove.
**Returns**:
- None
**Example**:
```python
# Remove an uploaded file
tool.remove_uploaded_file(uploaded)
```
#### get_sandbox
```python
def get_sandbox() -> Sandbox
```
Gets the current sandbox instance.
This method provides access to the Daytona sandbox instance, allowing you to inspect sandbox properties and metadata, as well as perform any sandbox-related operations. For details on available attributes and methods, see the [Sandbox](#sandbox) data structure section below.
**Arguments**:
- None
**Returns**:
- [`Sandbox`](#sandbox) - Sandbox instance.
**Example**:
```python
sandbox = tool.get_sandbox()
```
#### install_python_packages
```python
def install_python_packages(package_names: str | list[str]) -> None
```
Installs one or more Python packages in the sandbox using pip.
**Arguments**:
- `package_names` - str | list[str]: Name(s) of the package(s) to install.
**Returns**:
- None
:::note
The list of preinstalled packages in a sandbox can be found at [Daytona's Default Snapshot documentation](https://www.daytona.io/docs/en/snapshots.md#default-snapshots).
:::
**Example**:
```python
# Install a single package
tool.install_python_packages("pandas")
# Install multiple packages
tool.install_python_packages(["numpy", "matplotlib"])
```
#### close
```python
def close() -> None
```
Closes and deletes the sandbox environment.
**Arguments**:
- None
**Returns**:
- None
:::note
Call this method when you are finished with all data analysis tasks to properly clean up resources and avoid unnecessary usage.
:::
**Example**:
```python
# Close the sandbox and clean up
tool.close()
```
### 11. Data Structures
#### SandboxUploadedFile
Represents metadata about a file uploaded to the sandbox.
- `name`: `str` - Name of the uploaded file in the sandbox
- `remote_path`: `str` - Full path to the file in the sandbox
- `description`: `str` - Description provided during upload
#### Sandbox
Represents a Daytona sandbox instance.
See the full structure and API in the [Daytona Python SDK Sandbox documentation](https://www.daytona.io/docs/en/python-sdk/sync/sandbox.md#sandbox).
# Build a Plan-and-Execute Data Agent With LangGraph and Daytona
import { Image } from 'astro:assets'
import workflowDiagram from '../../../../../assets/docs/images/langgraph-plan-and-execute-workflow.svg'
This guide demonstrates how to build a [LangGraph](https://langchain-ai.github.io/langgraph/) **plan-and-execute** data agent that runs an end-to-end ETL plus analytical-SQL workflow inside a [Daytona](https://daytona.io) sandbox. The graph is hand-wired as a six-node state machine. Every node and every edge is explicit, so the agent's control flow is fully inspectable.
In this example, we ask the agent to profile the maintenance health of the public [`langchain-ai/langgraph`](https://github.com/langchain-ai/langgraph) GitHub repository: extract issues and pull requests from the public GitHub REST API, transform and normalize them into a relational schema, load them into a SQLite database in the sandbox, run three analytical SQL queries, and report findings.
---
### 1. Workflow Overview
You ask the agent a natural-language data question. The planner emits an ordered list of atomic plan steps as structured output. For each step the executor generates Python code and runs it in a Daytona sandbox. A deterministic `check` node advances to the next step on success, retries the current step (with the failing code as context, up to `max_attempts`) on failure, or routes to the summarizer once the plan is complete. The summarizer produces a final natural-language answer and a `cleanup` node deletes the sandbox.
The key benefit: every node, edge, and retry decision lives in plain Python you can read, not inside a prebuilt agent loop.
### 2. Project Setup
#### Clone the Repository
Clone the [Daytona repository](https://github.com/daytona/guides) and navigate to the example directory:
```bash
git clone https://github.com/daytona/guides.git
cd guides/python/langgraph/plan-and-execute-data-agent
```
#### Create Virtual Environment
:::note[Python Version]
Python 3.10 or higher is required.
:::
```bash
python3.10 -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
```
#### Install Dependencies
```bash
pip install -U langgraph langchain-core langchain-anthropic daytona pydantic python-dotenv
```
The packages include:
- `langgraph`: Graph-structured orchestration (provides `StateGraph` and conditional-edge routing)
- `langchain-core`: Message types (`HumanMessage`, `SystemMessage`, `BaseMessage`)
- `langchain-anthropic`: `ChatAnthropic` chat model, including the Anthropic-specific `with_structured_output` implementation we use for the planner
- `daytona`: Daytona Python SDK for sandbox provisioning and code execution
- `pydantic`: Defines the structured plan schema returned by the planner LLM call
- `python-dotenv`: Loads environment variables from `.env`
#### Configure Environment
Get your API keys and configure your environment:
1. **Daytona API key:** Get it from [Daytona Dashboard](https://app.daytona.io/dashboard/keys)
2. **Anthropic API key:** Get it from [Anthropic Console](https://console.anthropic.com/)
Create a `.env` file in your project:
```bash
DAYTONA_API_KEY=dtn_***
ANTHROPIC_API_KEY=sk-ant-***
```
### 3. Understanding the Core Components
Before walking through the implementation, here are the key concepts the code relies on: the choice of agent pattern (plan-and-execute vs. ReAct), LangGraph's `StateGraph` with task-specific state, how state updates flow through nodes, structured-output planning, and the Daytona code interpreter.
#### Plan-and-Execute vs. ReAct
LangGraph supports many agent topologies; plan-and-execute is one common pattern, and ReAct is its most-cited counterpoint. Understanding what plan-and-execute changes is easiest if you first know how ReAct works.
ReAct (short for **Reasoning + Acting**) is one of the most common patterns for structuring an agent loop. In ReAct the model alternates between two kinds of output: **reasoning** (free-form thinking about what to do next, sometimes visible as text, sometimes implicit in modern function-calling implementations) and **action** (a tool call). After each action, the tool's result (the **observation**) is appended to the conversation, and the model is invoked again. The loop continues until the model emits a final natural-language answer instead of another tool call. There is no upfront plan as a separate data structure; the agent's strategy emerges incrementally and is recorded only as the running chain of messages, tool calls, and observations in the conversation history.
Plan-and-execute splits these stages apart. The planner emits a complete plan **as data** (an ordered `list[str]` of atomic steps) before any code runs, and that plan lives in graph state where you can read it, log it, modify it, or replay individual steps. A separate `execute` node then implements each plan step in sequence, while a deterministic `check` node tracks per-step retry attempts and routes between `execute` and `summarize`. The failure-recovery loop is explicit in state rather than buried inside an agent's internal reasoning, which makes the whole control flow auditable.
This guide implements plan-and-execute because the demo task (ETL into SQLite plus three analytical queries) has a roughly fixed shape the LLM can plan in advance.
#### `StateGraph` with custom state
State is a `TypedDict` with task-specific fields rather than just `messages`. This is the canonical LangGraph pattern for non-chat workflows: state carries everything the nodes need to communicate.
```python
class AgentState(TypedDict):
sandbox: Sandbox | None
user_request: str
plan: list[str]
step_idx: int
attempts: int
max_attempts: int
last_error: str | None
last_code: str | None
step_outputs: list[str]
step_codes: list[str]
final_answer: str
```
`step_codes` and `step_outputs` are append-only lists indexed by completed plan step. The executor reads them on every call so each new step has full context about what variables and files prior steps produced.
#### How nodes update state
Nodes don't mutate `state` in place. Each node has the signature `(state: AgentState) -> dict`, and the dict it returns is **merged into the state** by the framework. The default merge is **replace-per-key**: any key present in the returned dict overwrites the previous value; keys absent from the return value stay unchanged. Returning `{}` means the node read state without changing it.
That replace-per-key default is why the `execute` node's success-path return constructs the new list explicitly:
```python
return {
"step_outputs": state["step_outputs"] + [stdout], # build new list with appended item
"step_codes": state["step_codes"] + [code],
...
}
```
If we instead returned `{"step_outputs": [stdout]}`, the framework would _replace_ the whole list with one element, losing prior outputs.
LangGraph also supports **reducers** as an alternative: annotating a state field with one (for example, `Annotated[list[str], operator.add]`) tells the framework to concatenate instead of replace, so a return of `{"step_outputs": [stdout]}` would auto-append. This guide deliberately doesn't use reducers, so the merge logic lives where the data is constructed rather than hiding in the schema annotation.
One last detail worth knowing: when a conditional edge fires after a node, the routing function (e.g., `route_from_check`) sees the **post-merge** state. The node's returned dict is applied to state _before_ the router runs, so the router always reads the updated values.
#### Structured-output planning
The `plan` node uses `model.with_structured_output(Plan)` where `Plan` is a Pydantic model with one field, `steps: list[str]`. The Anthropic adapter converts this into a tool-style schema and forces the LLM to return a parseable list rather than free-form prose. This is more reliable than parsing markdown-bulleted lists by regex.
#### Daytona SDK
`Daytona().create()` provisions a sandbox. The LLM-generated code from each `execute` step is run by `sandbox.code_interpreter.run_code(code)`. We use the `code_interpreter` API specifically because it preserves the Python execution context across calls: imports, variables, and functions defined in one plan step are still in scope in the next.
### 4. Implementation
#### Step 1: State, schema, and prompts
```python
from typing import TypedDict
from pydantic import BaseModel, Field
from daytona import Sandbox
class Plan(BaseModel):
steps: list[str] = Field(description="Atomic plan steps the executor will implement, in order.")
class AgentState(TypedDict):
sandbox: Sandbox | None
user_request: str
plan: list[str]
step_idx: int
attempts: int
max_attempts: int
last_error: str | None
last_code: str | None
step_outputs: list[str]
step_codes: list[str]
final_answer: str
```
The `description` argument on `Field(...)` in `Plan` is sent to the model as the schema's natural-language hint, which steers what the LLM puts in each list entry.
The fields of `AgentState` group into five roles based on which nodes write to them:
- **Lifecycle**: `sandbox` (set by `provision`, nulled by `cleanup`), `user_request` (seeded at startup, immutable for the run).
- **Plan progress**: `plan` (the list of steps from the planner) and `step_idx` (which step is currently being executed).
- **Retry tracking**: `attempts`, `max_attempts`, plus `last_error` and `last_code` (written by `execute` on failure, read by the next attempt so the LLM sees the error and failing code).
- **Append-only history**: `step_outputs` and `step_codes`, one entry per successfully completed step.
- **Output**: `final_answer`, populated by `summarize` once the run finishes.
Three system prompts shape the three LLM calls that the graph makes. They are defined at module level alongside the schemas so all the static configuration lives in one place.
**Planner prompt.** Drives the `plan` node. The critical rule is **preserve URLs and identifiers verbatim**: without it, the planner tends to paraphrase ("fetch from the GitHub API") and the executor then has to guess. The other rules constrain the number of steps and discourage bundling tightly-coupled work across step boundaries (which would force the executor to guess variable names from one step in the next).
```python
PLAN_SYSTEM_PROMPT = """You are the planner stage of a plan-and-execute data agent.
Produce an ordered list of 4-8 atomic plan steps. Each step is one natural-language sentence describing
a single chunk of Python code that the executor stage will then write and run in a persistent Daytona sandbox.
Rules:
- Sandbox state PERSISTS across steps. Imports, variables, and files from step N are visible in step N+1.
- Step 1 should establish any package installs or imports.
- Each step is one coherent action. Group tightly-coupled work that shares variables (fetch + filter, or
create-schema + insert-data) into a SINGLE step so the executor doesn't have to guess prior variable names
across step boundaries. Keep loosely-coupled work in separate steps.
- PRESERVE any specific URLs, endpoints, file paths, table names, or identifiers from the user's request
VERBATIM inside the plan step that uses them. Do not paraphrase URLs.
- Do NOT write code in the plan. Describe what each step does.
"""
```
**Executor prompt.** Drives the `execute` node. Forces the LLM to output **only** Python in a fenced code block (the wrapper regex `extract_code()` depends on this format), to use exact URLs/paths from the user's request, and to use the exact variable names assigned by prior steps' code (which the prompt template injects into the user message). On a retry, the prompt also asks the LLM to diagnose and produce a _materially different_ fix rather than retry the same approach.
````python
EXECUTE_SYSTEM_PROMPT = """You are the executor stage of a plan-and-execute data agent.
You receive the user's original request, the full plan, and the index of the current step. You must output
ONLY Python code that accomplishes the current step. The code runs in a persistent Daytona sandbox; prior
steps' variables, imports, and files are still in scope. Always `print()` the relevant output so later
stages can see the results.
Rules:
- Use the EXACT URLs / endpoints / file paths from the user's original request. Do not invent or paraphrase.
- CRITICAL: Before referencing any variable from a prior step, scan the prior code shown below and use
EXACTLY the variable name that the prior step assigned. Never invent variable names. If you cannot find
the variable you need in the prior code, re-derive it from scratch within your current step.
- Output format: a single ```python fenced block, nothing else. No prose.
- If a previous attempt failed, you will see the error and the failing code. Diagnose the root cause and
produce a materially different fix. Do not repeat the failing approach. If the error is a NameError,
the missing variable was never defined in the shown prior code; re-derive it from raw data.
"""
````
**Summarizer prompt.** Drives the `summarize` node. The whole job of this prompt is to keep the final answer **honest**: cite numbers that appear in the stdout, do not hallucinate values that don't appear there, and report failures plainly if the agent gave up. This is the safety belt that prevents the LLM from fabricating plausible-looking results when the actual run was incomplete.
```python
SUMMARIZE_SYSTEM_PROMPT = """You are the summarizer stage of a plan-and-execute data agent.
You will be shown the user's original request and the stdout from each successfully executed plan step.
Produce a clear, factual answer in 1-3 short paragraphs. Cite specific numbers from the stdout. Do not
hallucinate values that are not present in the stdout. If a step failed, say so plainly.
"""
```
#### Step 2: The `provision` and `plan` nodes
```python
plan_llm = model.with_structured_output(Plan)
def provision(state: AgentState) -> dict:
sandbox = Daytona().create()
return {"sandbox": sandbox}
def plan_node(state: AgentState) -> dict:
result = plan_llm.invoke([
SystemMessage(content=PLAN_SYSTEM_PROMPT),
HumanMessage(content=state["user_request"]),
])
return {"plan": result.steps}
```
#### Step 3: The `execute` node
This is the node that does the real work of the agent. On every call, the executor receives the **complete context of the run so far**, assembled from graph state:
- the **original user request** (so URLs, paths, and identifiers are never lost through paraphrasing)
- the **full plan** with the current step marked, so the LLM sees what came before and what comes after
- every **prior step's generated code** (variables, imports, helper functions that prior code defined are still in scope in the sandbox, but the LLM also needs to see them to _reuse_ names correctly)
- every **prior step's stdout** (what those steps actually printed)
- and, on retry, the **previous attempt's error and failing code**
That last bullet is the key recovery mechanism. When the executor LLM has the failing code in front of it together with the traceback, it can diagnose the problem and produce a materially different fix rather than blindly retrying.
```python
def execute(state: AgentState) -> dict:
idx = state["step_idx"]
step_text = state["plan"][idx]
plan_listing = "\n".join(
f" {i + 1}. {s}{' <-- CURRENT' if i == idx else ''}" for i, s in enumerate(state["plan"])
)
prompt_parts = [
f"Original user request:\n{state['user_request']}",
f"Full plan:\n{plan_listing}",
f"Current step ({idx + 1} of {len(state['plan'])}): {step_text}",
]
if state["step_codes"]:
prompt_parts.append("Code already executed in this sandbox (variables and imports still in scope):")
for i, prior in enumerate(state["step_codes"], 1):
prompt_parts.append(f"--- step {i} code ---\n{prior}")
if state["step_outputs"]:
prompt_parts.append("Stdout from those prior steps:")
for i, output in enumerate(state["step_outputs"], 1):
prompt_parts.append(f"--- step {i} stdout ---\n{output[:1500]}")
if state["last_error"] and state["last_code"]:
prompt_parts.append(f"--- previous attempt error ---\n{state['last_error'][:1500]}")
prompt_parts.append(f"--- previous failing code ---\n{state['last_code']}")
prompt_parts.append("Diagnose and write a corrected implementation.")
response = model.invoke([
SystemMessage(content=EXECUTE_SYSTEM_PROMPT),
HumanMessage(content="\n\n".join(prompt_parts)),
])
content = response.content if isinstance(response.content, str) else str(response.content)
code = extract_code(content)
sandbox = state["sandbox"]
result = sandbox.code_interpreter.run_code(code, timeout=180)
stdout = result.stdout or ""
if result.error is not None:
err = f"{result.error.name}: {result.error.value}\n{result.error.traceback}".strip()
return {"last_error": err, "last_code": code}
return {
"last_error": None,
"last_code": code,
"step_outputs": state["step_outputs"] + [stdout],
"step_codes": state["step_codes"] + [code],
}
```
Note the `+ [stdout]` and `+ [code]` patterns in the success-path return. `step_outputs` and `step_codes` are both `list[str]` in the state schema; this idiom _appends_ to those lists by constructing a **new** list (`existing + [new_item]`) rather than mutating the original in place. Returning new values is the LangGraph convention because the framework reasons about state as a sequence of immutable snapshots, which enables checkpointing, time-travel debugging, and replay.
Two more details worth calling out:
- **Marking the current step** with `<-- CURRENT` in the plan listing nudges the LLM to focus on that step without losing sight of the surrounding ones. It can see what was already done and what remains.
- **Sandbox state vs. shown context** are two different things. The interpreter context literally still holds whatever prior steps imported or assigned, but the LLM has no introspection into that running state. Passing prior code as text gives the LLM the symbolic view it needs to reuse names correctly.
#### Step 4: The `check` node and conditional routing
`check` does **state mutation only**. It does not decide where the graph goes. The routing decision lives in `route_from_check`, a function passed to `add_conditional_edges`.
```python
def check(state: AgentState) -> dict:
if state["last_error"]:
return {"attempts": state["attempts"] + 1}
return {
"step_idx": state["step_idx"] + 1,
"attempts": 0,
"last_error": None,
"last_code": None,
}
def route_from_check(state: AgentState) -> str:
if state["last_error"]:
if state["attempts"] >= state["max_attempts"]:
return "summarize"
return "execute"
if state["step_idx"] >= len(state["plan"]):
return "summarize"
return "execute"
```
Splitting state mutation from routing keeps each function pure and the graph topology readable: `check` updates counters, `route_from_check` picks an edge.
The conditional edge itself is wired with `add_conditional_edges`:
```python
graph.add_conditional_edges("check", route_from_check, {"execute": "execute", "summarize": "summarize"})
```
This call takes three arguments: the **source node** (`"check"`), the **routing function** (`route_from_check`), and a **mapping dict** that translates the routing function's return value to a destination node name. At runtime LangGraph calls `route_from_check(state)`, gets back a string (here either `"execute"` or `"summarize"`), looks it up as a key in the mapping dict, and routes to the value (the actual node name).
In our case the dict's keys and values are identical because the routing function happens to return literal node names. That looks redundant, but the dict layer is the convention even when they match. The reason is decoupling: a routing function can return _semantic labels_ like `"high_priority"` or `"needs_retry"`, and the dict translates those labels to whatever the graph's actual node names are. This lets you reuse routing functions across graphs and rename nodes without touching the routing logic. You can also use the special `END` constant as a destination to terminate the graph from a conditional branch (for example, `{"continue": "next_node", END: END}`).
#### Step 5: `summarize` and `cleanup`
```python
def summarize(state: AgentState) -> dict:
parts = [f"Original request:\n{state['user_request']}", "Outputs from executed plan steps:"]
for i, output in enumerate(state["step_outputs"], 1):
parts.append(f"--- step {i} stdout ---\n{output}")
if state["last_error"]:
parts.append(f"NOTE: the agent gave up before finishing. Last error:\n{state['last_error']}")
response = model.invoke([
SystemMessage(content=SUMMARIZE_SYSTEM_PROMPT),
HumanMessage(content="\n\n".join(parts)),
])
content = response.content if isinstance(response.content, str) else str(response.content)
return {"final_answer": content}
def cleanup(state: AgentState) -> dict:
sandbox = state.get("sandbox")
if sandbox is not None:
sandbox.delete()
return {"sandbox": None}
```
#### Step 6: Wire the graph
```python
graph = StateGraph(AgentState)
graph.add_node("provision", provision)
graph.add_node("plan", plan_node)
graph.add_node("execute", execute)
graph.add_node("check", check)
graph.add_node("summarize", summarize)
graph.add_node("cleanup", cleanup)
graph.add_edge(START, "provision")
graph.add_edge("provision", "plan")
graph.add_edge("plan", "execute")
graph.add_edge("execute", "check")
graph.add_conditional_edges("check", route_from_check, {"execute": "execute", "summarize": "summarize"})
graph.add_edge("summarize", "cleanup")
graph.add_edge("cleanup", END)
app = graph.compile()
```
#### Step 7: The `main()` entry point
The `main()` function is the entry point that ties everything together: instantiate the chat model, build the compiled graph, seed the initial state, invoke the graph, and print the result.
```python
def main() -> None:
model = ChatAnthropic(
model_name="claude-opus-4-6",
temperature=0,
timeout=None,
max_retries=2,
stop=None,
)
app = build_graph(model)
initial_state: AgentState = {
"sandbox": None,
"user_request": USER_REQUEST,
"plan": [],
"step_idx": 0,
"attempts": 0,
"max_attempts": 3,
"last_error": None,
"last_code": None,
"step_outputs": [],
"step_codes": [],
"final_answer": "",
}
final_state = app.invoke(initial_state, config={"recursion_limit": 50})
print(final_state["final_answer"])
```
A few things worth calling out:
- **`initial_state`** is a `dict` with every `AgentState` field set to its starting value. The `provision` node writes the real `sandbox` object; the `plan` node populates `plan`; the `execute` node appends to `step_outputs` and `step_codes`; `check` mutates `step_idx` and `attempts`. Everything starts empty/zero and is filled in by the graph.
- **`config={"recursion_limit": 50}`** raises LangGraph's per-invocation **super-step budget** from its default of 25. A super-step is one node execution; LangGraph aborts the run with `GraphRecursionError` once that count is exceeded. For this guide the canonical plan has 6 steps (3 setup steps: install/import, fetch from GitHub, create + load SQLite; plus 3 analytical SQL queries), so a clean run uses about `1 (provision) + 1 (plan) + 6 × 2 (execute → check, once per plan step) + 1 (summarize) + 1 (cleanup) = 16` super-steps. Each retry adds another `execute → check` pair on top. The default of 25 is tight once retries fire; 50 leaves comfortable headroom.
- **`final_state["final_answer"]`** is the natural-language report produced by the `summarize` node. The rest of `final_state` still contains the full plan, all per-step code and outputs, the final `sandbox=None` (cleanup nulled it after deletion), etc. so you can inspect or persist any of it.
### 5. Run the Example
```bash
python main.py
```
The agent typically emits a 6-step plan, executes each step in the persistent interpreter context, runs three analytical SQL queries, and summarizes. Because variables and imports survive across steps, an `import requests` in step 1 is still in scope when step 2 calls `requests.get(...)`, so well-formed code rarely needs the retry path. The canonical run below completes all six steps on the first attempt.
#### Example Output
```text
[provision] creating Daytona sandbox...
[provision] sandbox ready (id=b9cf758d-9b93-4117-96b3-9a406c86b1b8)
[plan] asking the LLM for a multi-step plan...
[plan] 6 step(s):
1. Install needed packages and import requests, sqlite3, json, datetime
2. Fetch the 100 most recently updated issues and PRs from the
langchain-ai/langgraph /issues and /pulls endpoints; store the JSON responses
3. Create a SQLite database (langgraph.db) with two tables (issues, pull_requests),
define schemas, and insert the fetched data
4. SQL: PR merge rate among closed PRs
5. SQL: top 5 PR authors by count with personal merge rates
6. SQL: most-commented currently-open issue
[execute] step 1/6 attempt 1/3: Install needed packages...
[execute] step OK.
[check] step 1 done; advancing to step 2
[execute] step 2/6 attempt 1/3: Fetch the 100 most recently updated issues and PRs...
[execute] step OK. stdout:
Fetched 100 issues, 100 pull requests
[check] step 2 done; advancing to step 3
[execute] step 3/6 attempt 1/3: Create a SQLite database, define schemas, insert data...
[execute] step OK. stdout:
Created database with two tables: issues, pull_requests
Inserted 40 issues (after filtering out PRs from the /issues endpoint) and 100 PRs
[check] step 3 done; advancing to step 4
[execute] step 4/6 attempt 1/3: SQL: PR merge rate among closed PRs...
[execute] step OK. stdout:
SELECT COUNT(*), SUM(CASE WHEN merged_at IS NOT NULL THEN 1 ELSE 0 END),
ROUND(100.0 * SUM(CASE WHEN merged_at IS NOT NULL THEN 1 ELSE 0 END) / COUNT(*), 2)
FROM pull_requests WHERE state = 'closed'
=> 100 closed, 96 merged, 96.0% merge rate
[check] step 4 done; advancing to step 5
... (steps 5-6 succeed) ...
[summarize] asking the LLM for a final answer...
[cleanup] deleting sandbox ...
[cleanup] done
============================================================
FINAL ANSWER
============================================================
PR Merge Rate: 96/100 closed PRs merged = 96.0%.
Top 5 PR authors (total PRs, personal merge rate):
nfcampos 40 100.00%
hinthornw 18 100.00%
hwchase17 15 93.33%
rlancemartin 10 100.00%
baskaryan 4 100.00%
Most-commented open issue:
"Long tool calls (~180s+) silently re-executed from checkpoint on LangGraph Cloud"
25 comments, opened by MarioAlessandroNapoli on 2026-04-05.
```
#### What the retry path catches
In the canonical run above no step needs a retry, because the interpreter context's persistent state matches what the LLM expects: variables and imports from earlier steps are still live, so the obvious "I forgot to re-import" class of failure cannot happen. The retry path is still there, and it fires for _genuine_ code failures: a syntax error, a runtime exception (`KeyError`, `TypeError`, `IndexError`), the LLM hallucinating a method that doesn't exist on the response object, a malformed SQL query, an unhandled empty result, and so on.
When a step does fail, `sandbox.code_interpreter.run_code(code)` returns an `ExecutionResult` whose `error` field is set to an `ExecutionError` carrying `name`, `value`, and `traceback`. The `execute` node serializes those fields into `state["last_error"]` and stores the failing source in `state["last_code"]`. `check` sees the error, increments `attempts`, and `route_from_check` sends control back to `execute`. The retry call to the LLM now includes **both** the original step description, the still-in-scope prior steps' code, and the failing code plus the error and traceback, so the LLM can diagnose the problem and produce a materially different fix rather than retrying the same approach. This continues up to `max_attempts` times; past that, `route_from_check` routes to `summarize` with `state["last_error"]` still set, and the summarizer reports the failure honestly instead of fabricating a result.
This is the value the graph provides: **the failure state is explicit, persistent, and visible to every subsequent LLM call**. There's no hidden conversation context, no implicit ReAct loop, no need to trust the prebuilt agent. Every routing decision happens in code you can read.
### 6. Adapting the Example
The graph topology is task-agnostic. To profile a different repository, change the URL in `USER_REQUEST` and rerun. To use a different model, swap `ChatAnthropic` for `ChatOpenAI` (and update `.env.example`). To allow more retries per step, raise `max_attempts` in the initial state dict. To run a different analytical workflow entirely, replace `USER_REQUEST`; the plan-and-execute machinery doesn't care what task it's executing.
**Key advantages of this approach:**
- **Inspectable plan:** The list of steps lives in graph state as data (`state["plan"]`), not implicit in chat history, so you can log it, modify it, or replay any individual step.
- **Explicit retry control:** Failure handling is your code, not a prebuilt agent's black box. `max_attempts`, the routing logic in `route_from_check`, and the prompt context shown on each retry are all readable and tunable.
- **Persistent interpreter context:** Daytona's `code_interpreter.run_code` shares one Python interpreter across all `execute` calls, so imports, variables, and functions defined in earlier steps stay in scope, exactly the behavior the LLM's plan assumes.
- **Secure sandbox execution:** Every line of LLM-generated Python runs in an isolated Daytona sandbox, not on your machine.
- **Task-agnostic topology:** The same six-node graph works for any analytical workflow by swapping `USER_REQUEST`. The provision/plan/execute/check/summarize/cleanup machinery doesn't depend on the specific task.
# Use Mastra Coding Agent with Daytona
This guide demonstrates how to configure the [Mastra coding agent](https://github.com/mastra-ai/template-coding-agent) to use Daytona sandboxes, enabling you to leverage AI capabilities for any coding-related task in a secure, isolated environment.
---
### 1. Workflow Overview
Once configured, you can use Mastra Studio to interact with the coding agent through a ChatGPT-like interface. This enables human-in-the-loop workflows where you can guide the agent, review its decisions, and iterate on solutions in real-time; all while the agent executes tasks securely within Daytona sandboxes.
### 2. Project Setup
:::note[Node.js Version]
Node.js version 20 or higher is required to run the coding agent. Please ensure your environment meets this requirement before proceeding.
:::
#### Clone the Repository
Clone the Mastra coding agent template repository, which includes the agent implementation and Daytona integration:
```bash
git clone https://github.com/mastra-ai/template-coding-agent.git
cd template-coding-agent
```
#### Configure Environment
Create a `.env` file in the project root directory:
```bash
touch .env
```
The `.env` file requires the following configuration:
- **LLM Provider**: The AI model provider for your coding agent
- **Model**: The specific model to use from your chosen provider
- **Sandbox Provider**: Daytona configuration for isolated execution
For this guide, we'll use OpenAI as the LLM provider with the `gpt-4o-mini` model:
```env
OPENAI_API_KEY=your_openai_key
MODEL=openai/gpt-4o-mini
```
Next, configure Daytona as your sandbox provider by adding your API key (available from the [Daytona Dashboard](https://app.daytona.io/dashboard/keys)):
```env
DAYTONA_API_KEY=your-daytona-api-key-here
```
#### Install Dependencies
Install the required packages using pnpm:
```bash
pnpm install
```
### 3. Running the Agent
Mastra Studio provides a ChatGPT-like interface for interacting with your coding agent. This mode offers:
- **Conversation History**: Previous conversations are stored and organized in threads
- **Visual Debugging**: Inspect agent execution steps, workflow, and tool calls
- **Model Switching**: Easily switch between different AI models
- **Tool Inspection**: View which tools your agent is using in real-time
For a complete overview of all features and capabilities, visit the [Mastra Studio documentation](https://mastra.ai/docs/getting-started/studio).
Start the dev server with:
```bash
pnpm run dev
```
If the dev server starts successfully, you'll see the terminal output displaying the URLs where you can access Mastra Studio:
```bash
│ Studio: http://localhost:4111
│ API: http://localhost:4111/api
```
Once started, open the provided URL in your browser to access the interactive interface. You can interact with your agent while monitoring its workflow in the terminal, where detailed logs show execution steps and low-level parameters.
Below is an example of terminal logs generated when the agent calls the `writeFile` tool to create a JavaScript file with a basic "Hello, world!" output:
```json
{
"text": "",
"toolCalls": [
{
"type": "tool-call",
"runId": "ab2a1d08-91c6-4028-9046-3446a721527f",
"from": "AGENT",
"payload": {
"toolCallId": "call_NiLLgBmgrYLSL0MsrG54E4A5",
"toolName": "writeFile",
"args": {
"sandboxId": "2152d23b-5742-47c2-9992-4414d4144869",
"path": "hello.js",
"content": "console.log('Hello, world!');"
},
"providerMetadata": {
"openai": {
"itemId": "fc_00bba3412cd22a2b0069399fbaeef881909b0583f359cbc33c"
}
}
}
}
],
"toolResults": [
{
"type": "tool-result",
"runId": "ab2a1d08-91c6-4028-9046-3446a721527f",
"from": "AGENT",
"payload": {
"args": {
"sandboxId": "2152d23b-5742-47c2-9992-4414d4144869",
"path": "hello.js",
"content": "console.log('Hello, world!');"
},
"toolCallId": "call_NiLLgBmgrYLSL0MsrG54E4A5",
"toolName": "writeFile",
"result": {
"success": true,
"path": "/home/daytona/hello.js"
},
"providerMetadata": {
"openai": {
"itemId": "fc_00bba3412cd22a2b0069399fbaeef881909b0583f359cbc33c"
}
}
}
}
],
"finishReason": "tool-calls",
"usage": {
"inputTokens": 4243,
"outputTokens": 53,
"totalTokens": 4296,
"reasoningTokens": 0,
"cachedInputTokens": 4096
},
"runId": "ab2a1d08-91c6-4028-9046-3446a721527f"
}
```
These logs show complete visibility into tool execution including arguments, results, token usage with caching metrics, and unique identifiers for debugging.
### 4. Example Usage
Let's walk through a practical example to see the Mastra coding agent and Daytona sandbox integration in action. We'll ask the agent to complete the following task:
> _"Create a Python file with a function that reverses a string, write a test file for it, and run the tests."_
#### Agent Workflow
The agent executes this task through a series of coordinated steps, utilizing different tools to interact with the Daytona sandbox:
##### 1. Create Sandbox
The agent first provisions a new Daytona sandbox for Python development:
**Tool:** `createSandbox`
**Arguments:**
```json
{
"name": "reverse_string_project",
"labels": null,
"language": "python",
"envVars": null
}
```
**Result:**
```json
{
"sandboxId": "bdfa9456-4945-43a7-97df-b9bfbcbdc62e"
}
```
##### 2. Write Project Files
Next, the agent creates both the implementation and test files in a single operation:
**Tool:** `writeFiles`
**Arguments:**
```json
{
"sandboxId": "bdfa9456-4945-43a7-97df-b9bfbcbdc62e",
"files": [
{
"path": "reverse_string.py",
"data": "def reverse_string(s):\n \"\"\"Return the reversed version of the input string s.\"\"\"\n return s[::-1]\n"
},
{
"path": "test_reverse_string.py",
"data": "import unittest\nfrom reverse_string import reverse_string\n\nclass TestReverseString(unittest.TestCase):\n def test_regular(self):\n self.assertEqual(reverse_string(\"hello\"), \"olleh\")\n def test_empty(self):\n self.assertEqual(reverse_string(\"\"), \"\")\n def test_single_char(self):\n self.assertEqual(reverse_string(\"a\"), \"a\")\n def test_numbers(self):\n self.assertEqual(reverse_string(\"12345\"), \"54321\")\n\nif __name__ == \"__main__\":\n unittest.main()\n"
}
]
}
```
**Result:**
```json
{
"success": true,
"filesWritten": [
"/home/daytona/reverse_string.py",
"/home/daytona/test_reverse_string.py"
]
}
```
##### 3. Execute Tests
Finally, the agent runs the test suite to verify the implementation:
**Tool:** `runCommand`
**Arguments:**
```json
{
"sandboxId": "bdfa9456-4945-43a7-97df-b9bfbcbdc62e",
"command": "python3 test_reverse_string.py",
"envs": null,
"workingDirectory": null,
"timeoutSeconds": 20,
"captureOutput": true
}
```
**Result:**
```json
{
"success": true,
"exitCode": 0,
"stdout": "....\n----------------------------------------------------------------------\nRan 4 tests in 0.000s\n\nOK\n",
"command": "python3 test_reverse_string.py",
"executionTime": 218
}
```
#### Agent Response
After successfully completing all steps, the agent provides a detailed summary:
```
The project is complete! Here's what was done:
✓ Created reverse_string.py with a function to reverse a string.
✓ Created test_reverse_string.py using Python's unittest to test the function with several cases.
✓ Ran the tests—all tests passed successfully.
Test Output:
....
----------------------------------------------------------------------
Ran 4 tests in 0.000s
OK
Would you like to see the code for either file or make any changes?
```
This example demonstrates how the agent autonomously handles the entire development workflow, from environment setup to test execution, all within the secure Daytona sandbox environment.
:::tip[Sandbox Management]
After the agent completes its task, you'll see the active sandbox in your [Daytona Dashboard](https://app.daytona.io/dashboard). It's recommended to clean up resources when finished, unless your use case requires the sandbox to remain active (e.g. for preview URLs or ongoing development).
:::
**Key Advantages**
- **Secure Isolation**: All agent operations run in isolated Daytona sandboxes, protecting your local environment
- **Multi-Language Support**: Execute code across different programming languages without local setup
- **Enhanced Debugging**: Use Mastra Studio to visualize and debug agent workflows in real-time
- **Scalable Execution**: Leverage Daytona's cloud infrastructure for resource-intensive tasks
# Claude Guides
Guides for integrating Claude with Daytona.
# Run Claude Code in a Daytona Sandbox via CLI
This guide walks you through running Claude Code inside a Daytona sandbox using the Daytona CLI.
### Prerequisites
- Daytona account and API key (Get it from [Daytona Dashboard](https://app.daytona.io/dashboard/keys))
- Local terminal (macOS, Linux, or Windows)
### Install the Daytona CLI
```bash
brew install daytonaio/cli/daytona
```
```bash
powershell -Command "irm https://get.daytona.io/windows | iex"
```
:::note
Already have the CLI? Check your version with `daytona --version`. If it's below **0.135.0**, [upgrade to the latest version](https://www.daytona.io/docs/en/tools/cli.md).
:::
### Authenticate with Daytona
Log in to your Daytona account using your API key:
```bash
daytona login --api-key=YOUR_API_KEY
```
Replace `YOUR_API_KEY` with your actual Daytona API key.
### Create a Sandbox
Create a new sandbox for running Claude Code:
```bash
daytona sandbox create --name claude-sandbox
```
This creates a sandbox named `claude-sandbox`, visible in your [Dashboard](https://app.daytona.io/dashboard/sandboxes). The default Daytona snapshot includes Claude Code, so the command above is all you need.
:::tip
Need more power? Pass `--snapshot daytona-large` or `--snapshot daytona-medium` flag to increase your sandbox resources. See [default snapshots](https://www.daytona.io/docs/en/snapshots.md#default-snapshots) for resource details.
:::
### Connect to the Sandbox
SSH into your sandbox:
```bash
daytona ssh claude-sandbox
```
You now have an interactive terminal session inside the sandbox.
### Run Claude Code
Inside the SSH session, start Claude Code:
```bash
claude
```
On first run, Claude Code will prompt you to authenticate:
1. Copy the authentication URL displayed in the terminal
2. Open the URL in your local browser
3. Complete the authentication flow
4. Copy the code provided by the browser
5. Paste the code back into the terminal
Once authenticated, you're all set. Claude Code runs inside the sandbox while you control it from your terminal.
# Build a Coding Agent Using Claude Agent SDK and Daytona
import { Image } from 'astro:assets'
import claudeAgentSDKInteractiveTerminalSandboxResult from '../../../../../assets/docs/images/claude-agent-sdk-interactive-terminal-sandbox-result.gif'
This guide demonstrates how to run an autonomous coding agent based on [Claude Code](https://code.claude.com/docs/en/overview) inside a Daytona sandbox environment. The agent uses the [Claude Agent SDK](https://platform.claude.com/docs/en/agent-sdk/overview) to follow user prompts.
The agent can develop full-stack web apps, write code in any language, install dependencies, and run scripts. It can also start and manage dev servers, and generate preview links for live apps.
---
### 1. Workflow Overview
When you launch the main module, a Daytona sandbox is created and a Python agent is initialized inside it. The agent is based on the [Claude Agent SDK](https://platform.claude.com/docs/en/agent-sdk/overview).
You interact with the main program via a command line chat interface. The program sends your prompts to the agent inside the sandbox, which executes them and returns the results:
```
$ npm run start 174s
Creating sandbox...
Installing Agent SDK...
Initializing Agent SDK...
Press Ctrl+C at any time to exit.
User: Build a Zelda-like game where I can move around the screen and talk to famous programmers
Thinking...
I'll build a Zelda-like game for you! This will be a fun project with player movement and NPC interactions with famous programmers.
🔨 Write
🔨 Write
Now let me start a simple HTTP server to host the game:
🔨 Bash
Perfect! I've created a Zelda-like game called "Programmer's Quest" for you! 🎮
## Game Features:
✨ Zelda-style gameplay:
- Top-down 2D view with classic retro aesthetics
- Player character with sword and shield
- Grid-based movement system
- Environmental obstacles (trees and rocks)
👥 Famous Programmers as NPCs:
1. Linus Torvalds - Creator of Linux
2. Grace Hopper - COBOL pioneer and Admiral
3. Alan Turing - Father of computer science
4. Ada Lovelace - First computer programmer
5. Dennis Ritchie - Creator of C and UNIX
🎮 Controls:
- Arrow Keys or WASD - Move your character
- SPACE - Talk to NPCs when you're near them
🌟 Gameplay:
- Explore the grassy map and find all 5 legendary programmers
- Each NPC has multiple quotes that cycle when you talk to them
- NPCs glow when you're near them
- Dialog boxes appear with their famous quotes
- Track your progress in the HUD
## Play Now:
🎯 [Click here to play the game!](https://80-8e2c4d23-212a-4f1e-bb6c-abfa71aeed3a.proxy.daytona.works)
The game features smooth movement, collision detection with trees and rocks, and an immersive dialog system. Try to find and talk to all 5 famous programmers to learn their wisdom! Each has 3 different quotes that cycle as you keep talking to them.
Enjoy your adventure! 🗡️✨
User:
```
The agent can also host web apps and provide you with a preview link using the [Daytona Preview Links](https://www.daytona.io/docs/en/preview.md) feature. When your task involves running or previewing a web application, the agent automatically reasons about this need, hosts the app, and generates a preview link for you to inspect the live result:
You can continue interacting with your agent until you are finished. When you exit the program, the sandbox will be deleted automatically.
### 2. Project Setup
#### Clone the Repository
First, clone the daytona [repository](https://github.com/daytona/guides.git) and navigate to the example directory:
```bash
git clone https://github.com/daytona/guides.git
cd guides/typescript/anthropic/single-claude-agent-sdk
```
#### Configure Environment
Get your API keys:
- **Daytona API key:** [Daytona Dashboard](https://app.daytona.io/dashboard/keys)
- **Anthropic API key:** [Anthropic Console](https://console.anthropic.com/)
Copy `.env.example` to `.env` and add your keys:
```bash
DAYTONA_API_KEY=your_daytona_key
SANDBOX_ANTHROPIC_API_KEY=your_anthropic_key
```
:::caution[API Key Security]
Note: The `SANDBOX_ANTHROPIC_API_KEY` key is passed into the Daytona sandbox environment and is accessible to any code executed inside the sandbox.
:::
#### Local Usage
:::note[Node.js Version]
Node.js 18 or newer is required to run this example. Please ensure your environment meets this requirement before proceeding.
:::
Install dependencies:
```bash
npm install
```
Run the agent:
```bash
npm run start
```
The agent will start and wait for your prompt.
### 3. Example Usage
Example chat session:
```
$ npm run start
Creating sandbox...
Installing Agent SDK...
Initializing Agent SDK...
Press Ctrl+C at any time to exit.
User: Create a fun pet store landing page in Svelte
Thinking...
I'll create a fun pet store landing page in Svelte for you! Let me set up the project and build an engaging landing page with animations and a colorful design.
🔨 Bash
🔨 Bash
Now let me create a fun and engaging pet store landing page with animations, colorful sections, and interactive elements:
🔨 Read
> 🔨 Write
Now let me create the component files:
🔨 Write
🔨 Write
🔨 Write
🔨 Write
🔨 Write
Now let's update the main CSS file and start the development server:
🔨 Read
🔨 Write
Now let's start the development server:
🔨 Bash
🔨 TaskOutput
Perfect! 🎉 I've created a fun and colorful pet store landing page in Svelte! Here's what I built:
## Features:
✨ Hero Section
- Beautiful gradient background with animated floating cards
- Eye-catching navigation bar
- Call-to-action buttons with hover effects
- Bouncing emoji animations
🎯 Features Section
- 4 key benefits (Free Delivery, Premium Quality, Special Offers, Award Winning)
- Interactive cards with hover animations
- Icons that rotate and scale on hover
🛍️ Products Section
- 6 featured products with colorful backgrounds
- Star ratings
- Add to cart functionality
- Smooth hover animations
📧 Newsletter Section
- Vibrant gradient background with floating pet emojis
- Email subscription form
- Success message animation
- Special 20% off offer
👣 Footer
- Comprehensive navigation links
- Social media icons
- Dark gradient design
## Live Preview:
Your pet store is now running at: https://80-b3739199-d90e-4bde-ab18-450d74c950e8.proxy.daytona.works
The page includes:
- Smooth scroll behavior
- Responsive design for mobile devices
- Fun animations throughout
- Interactive elements with hover effects
- Colorful gradients and modern styling
- Emoji-based icons for a playful feel
Click the link to see your fun pet store landing page in action! 🐾
User:
```
### 4. Understanding the Agent's Architecture
This example consists of two main components:
- **Main Program:** The main program is a Node.js script (`index.ts`) that runs on your local machine. It uses the Daytona SDK to create and manage a Daytona sandbox. The main program provides a command line interface for interacting with the agent inside the sandbox.
- **Sandbox Agent:** The sandbox agent is a Python script (`coding_agent.py`) that runs inside the Daytona sandbox. It uses the Claude Agent SDK to create a customized coding agent similar to Claude Code.
#### Initialization
On initialization, the main program:
1. Creates a new [Daytona sandbox](https://www.daytona.io/docs/en/sandboxes.md) with your Anthropic API key included in the environment variables.
2. Installs the Claude Agent SDK by running `pip install` in the sandbox with [process execution](https://www.daytona.io/docs/en/process-code-execution.md#command-execution).
3. Creates a new [code interpreter context](https://www.daytona.io/docs/en/process-code-execution.md#run-code-stateful).
4. Uploads the coding agent script to the sandbox with [file uploading](https://www.daytona.io/docs/en/file-system-operations.md#upload-a-single-file).
5. Initializes the Claude Agent SDK by running `import coding_agent` in the code interpreter context.
6. Waits for user input and sends prompts to the agent in the code interpreter context as shown below.
#### Main Program Code
Once the agent is running, the program creates a readline interface to read user input and sends it to the agent.
Each user request is passed to the agent by running a Python command in the code interpreter context:
```typescript
const result = await sandbox.codeInterpreter.runCode(
`coding_agent.run_query_sync(os.environ.get('PROMPT', ''))`,
{
context: ctx,
envs: { PROMPT: prompt },
onStdout,
onStderr,
}
)
```
The `onStdout` and `onStderr` callbacks are used to pass the agent's output back to the main program. After the agent finishes responding to the prompt, the main program waits for the next user input.
#### Sandbox Agent Code
The sandbox agent uses the [Claude Agent SDK](https://platform.claude.com/docs/en/agent-sdk/overview) to create a customized coding agent based on Claude Code.
The agent is initialized with a system prompt that includes the workspace directory and an example of the [preview URL format](https://www.daytona.io/docs/en/preview.md):
```python
system_prompt = """
You are running in a Daytona sandbox.
Use the /home/daytona directory instead of /workspace for file operations.
Your public preview URL for port 80 is: {}.
""".format(preview_url)
```
It also specifies the [tools and permission mode](https://platform.claude.com/docs/en/agent-sdk/quickstart#key-concepts) of the agent:
```python
client = ClaudeSDKClient(
options=ClaudeAgentOptions(
allowed_tools=["Read", "Edit", "Glob", "Grep", "Bash"],
permission_mode="acceptEdits",
system_prompt=system_prompt
)
)
```
The code to run queries and receive responses follows the examples in Anthropic's [Claude Agent Python SDK documentation](https://platform.claude.com/docs/en/agent-sdk/python).
#### Clean up
When you exit the main program, the Daytona sandbox and all files are automatically deleted.
**Key advantages:**
- Secure, isolated execution in Daytona sandboxes
- Communicate with the agent directly in your terminal
- Automatic dev server detection and live preview links
- Multi-language and full-stack support
- Simple setup and automatic cleanup
# Build a Two-Agent Coding System with Claude and Daytona
This guide demonstrates how to run a two-agent autonomous coding system using the [Claude Agent SDK](https://platform.claude.com/docs/en/agent-sdk/overview) and Daytona sandboxes. The system consists of a **Project Manager Agent** (local) and a **Developer Agent** (in-sandbox), enabling advanced delegation, planning, and secure code execution.
The Project Manager Agent runs locally and uses the basic Anthropic interface with the `claude-sonnet-4-20250514` model for high-level planning and task delegation. The Developer Agent runs inside the Daytona sandbox and is created using the Claude Agent SDK, which leverages Claude Code for advanced coding and automation capabilities. This architecture separates high-level planning from low-level code execution for more robust automation.
A key advantage of this approach is its **extensibility**: you can easily replace the Project Manager Agent with your own custom orchestrator logic, or even another agent, making the system highly reusable and adaptable to a wide range of advanced automation and coordination use cases.
---
### 1. Workflow Overview
When the main module is launched, a Daytona sandbox is created for the Developer Agent, and a Project Manager Agent is initialized locally. Interaction with the system occurs via a command line chat interface. The Project Manager Agent receives prompts, plans the workflow, and delegates coding tasks to the Developer Agent. The Developer Agent executes tasks in the sandbox and streams results back to the Project Manager, who reviews and coordinates further actions. All logs and outputs from both agents are streamed in real time to the terminal, providing full visibility into the process as it is managed by the Project Manager Agent.
The Developer Agent can also host web apps and provide preview links using [Daytona Preview Links](https://www.daytona.io/docs/en/preview.md). The Project Manager Agent will present these links and summarize the results for you.
You can continue interacting with the system until you are finished. When you exit the program, the sandbox is deleted automatically.
---
### 2. Project Setup
#### Clone the Repository
First, clone the daytona [repository](https://github.com/daytona/guides.git) and navigate to the example directory:
```bash
git clone https://github.com/daytona/guides.git
cd guides/typescript/anthropic/multi-agent-claude-sdk
```
#### Configure Environment
To run this example, you need to set the following environment variables:
- `DAYTONA_API_KEY`: Required for access to Daytona sandboxes. Get it from [Daytona Dashboard](https://app.daytona.io/dashboard/keys)
- `ANTHROPIC_API_KEY`: Required for the **Project Manager Agent** (runs locally). Get it from [Claude Developer Platform](https://console.anthropic.com/settings/keys)
- `SANDBOX_ANTHROPIC_API_KEY`: **Optional** for the **Developer Agent** (runs in sandbox). If not provided, defaults to using `ANTHROPIC_API_KEY`. Get it from [Claude Developer Platform](https://console.anthropic.com/settings/keys)
Copy `.env.example` to `.env` and add your keys:
```bash
DAYTONA_API_KEY=your_daytona_key
ANTHROPIC_API_KEY=your_anthropic_key
SANDBOX_ANTHROPIC_API_KEY=your_anthropic_key
```
:::tip[Agent API Key Options]
You can use a single `ANTHROPIC_API_KEY` for both agents, or provide a separate `SANDBOX_ANTHROPIC_API_KEY` for billing/tracking purposes.
:::
:::caution[API Key Security]
The `SANDBOX_ANTHROPIC_API_KEY` is passed into the Daytona sandbox environment and is accessible to any code executed inside the sandbox.
:::
#### Local Usage
:::note[Node.js Version]
Node.js 18 or newer is required to run this example. Please ensure your environment meets this requirement before proceeding.
:::
Install dependencies:
```bash
npm install
```
Run the two-agent system:
```bash
npm run start
```
The system will start and wait for your prompt.
---
### 3. Example Usage
Example chat session:
```
$ npm run start
Creating Developer Agent sandbox...
Installing Developer Agent SDK...
Initializing Developer Agent...
Initializing Project Manager Agent...
=== Two-Agent System Ready ===
Project Manager Agent: Manages tasks and delegates to Developer (Green text)
Developer Agent: Executes code in Daytona sandbox (White text)
Press Ctrl+C at any time to exit.
User: make a lunar lander web app
[Project Manager] Processing your request...
[Project Manager]: I'll help you create a lunar lander web app! Let me delegate this task to the Developer Agent.
Create a lunar lander web game with the following features:
- HTML/CSS/JavaScript based
- Canvas graphics for the game
- Physics simulation (gravity, thrust, velocity)
- Keyboard controls (arrow keys)
- Landing detection (safe landing vs crash)
- Fuel management system
- Start the game on port 80 and provide the preview URL
[Delegating to Developer Agent]...
I'll help you create a lunar lander web app! Let me build a complete game with physics simulation, graphics, and controls.
🔨 Write
🔨 Write
🔨 Bash
Perfect! The web server is now running.
Your Lunar Lander game is live at:
🌐 https://80-17ac1c0f-d684-4122-93b5-8f52fd5393f8.proxy.daytona.works
[Project Manager]: Excellent! The Developer Agent has successfully created your lunar lander web app. The game is now running and accessible at the preview URL above. The implementation includes:
✓ Physics simulation with gravity and thrust
✓ Canvas-based graphics
✓ Keyboard controls for gameplay
✓ Landing detection system
✓ Fuel management
✓ Web server running on port 80
TASK_COMPLETE
[Project Manager] All tasks completed!
```
---
### 4. Understanding the Agent Architecture
This system is composed of two collaborating agents, each with a distinct role and implementation. Below is a step-by-step guide to how each agent operates and interacts within the workflow.
#### Project Manager Agent (Terminal Orchestration)
1. **User Interaction:**
- All user interaction occurs via the terminal with the Project Manager Agent.
- The Project Manager Agent is configured with a system prompt that defines its role and maintains the full conversation history.
2. **Awareness of Developer Agent:**
- The Project Manager Agent knows that a Developer Agent is available inside a Daytona sandbox and can be invoked as needed.
3. **Task Delegation:**
- When the Project Manager Agent determines that a coding task should be delegated, it encapsulates the task within `` tags in its response.
- The system parses these tags and, when present, invokes the Developer Agent with the specified task.
4. **Iterative Workflow:**
- This process can repeat multiple times, with the Project Manager Agent reasoning about progress and delegating further tasks as needed.
5. **Session Completion:**
- When the Project Manager Agent determines the overall task is complete, it outputs `TASK_COMPLETE`, which signals the system to terminate the session.
#### Developer Agent (Sandbox Execution)
1. **Provisioning:**
- The Developer Agent is provisioned inside a Daytona sandbox and is responsible for executing coding tasks.
2. **SDK Installation:**
- The system installs the Claude Agent SDK in the sandbox by running `pip install` (see [process execution](https://www.daytona.io/docs/en/process-code-execution.md#command-execution)).
3. **Interpreter Context:**
- A new [code interpreter context](https://www.daytona.io/docs/en/process-code-execution.md#run-code-stateful) is created for isolated execution.
4. **Script Upload:**
- The coding agent script is uploaded to the sandbox using [file uploading](https://www.daytona.io/docs/en/file-system-operations.md#upload-a-single-file).
5. **SDK Initialization:**
- The Claude Agent SDK is initialized in the interpreter context (e.g., `import coding_agent`).
6. **Task Execution:**
- When a `` is received, the system sends the task to the Developer Agent by running a Python command in the interpreter context:
```typescript
const result = await sandbox.codeInterpreter.runCode(
`coding_agent.run_query_sync(os.environ.get('PROMPT', ''))`,
{
context: ctx,
envs: { PROMPT: task },
onStdout,
onStderr,
}
);
```
- The Developer Agent executes the task, streams output, and returns results to the Project Manager Agent for review and further coordination.
---
### 5. Customization
You can customize the Project Manager Agent's behavior by modifying the system prompt in `src/index.ts`. The current implementation:
- Uses `` tags for delegation
- Automatically reviews Developer Agent outputs
- Says "TASK_COMPLETE" when finished
---
### 6. Cleanup
When you exit the main program, the Daytona sandbox and all files are automatically deleted.
---
**Key advantages:**
- Secure, isolated execution in Daytona sandboxes
- Hierarchical agent architecture for robust automation
- Extensible and reusable architecture
- Automatic dev server detection and live preview links
- Multi-language and full-stack support
- Simple setup and automatic cleanup
# Running Claude Code with Daytona
Claude Code allows you to automate and orchestrate tasks using natural language and code. With Daytona, you can easily run Claude Code inside isolated sandboxes, making it simple to experiment and execute tasks securely.
## Running Claude Code in a Daytona Sandbox
You can run Claude Code and execute tasks with it directly inside a Daytona sandbox. The following examples show how to set up a sandbox, install Claude Code, run tasks programmatically, and stream logs in real time.
> **Note:** While both sync and async modes support streaming PTY output, `AsyncDaytona` is recommended as it provides automatic background callbacks via `on_data`. The synchronous API requires blocking iteration or manual threading to handle output.
```python
import os
import asyncio
from daytona import AsyncDaytona
async def run_claude_code():
async with AsyncDaytona() as daytona:
sandbox = await daytona.create()
# Define the Claude Code command to be executed
claude_command = "claude --dangerously-skip-permissions -p 'write a dad joke about penguins' --output-format stream-json --verbose"
# Install Claude Code in the sandbox
await sandbox.process.exec("npm install -g @anthropic-ai/claude-code")
pty_handle = await sandbox.process.create_pty_session(
id="claude", on_data=lambda data: print(data.decode(), end="")
)
await pty_handle.wait_for_connection()
# Run the Claude Code command inside the sandbox
await pty_handle.send_input(
f"ANTHROPIC_API_KEY={os.environ['ANTHROPIC_API_KEY']} {claude_command}\n"
)
# Use this to close the terminal session if no more commands will be executed
# await pty_handle.send_input("exit\n")
await pty_handle.wait()
# If you are done and have closed the PTY terminal, it is recommended to clean up resources by deleting the sandbox
# await sandbox.delete()
if __name__ == "__main__":
asyncio.run(run_claude_code())
````
```typescript
import { Daytona } from "@daytona/sdk";
const daytona = new Daytona();
try {
const sandbox = await daytona.create();
// Define the Claude Code command to be executed
const claudeCommand =
"claude --dangerously-skip-permissions -p 'write a dad joke about penguins' --output-format stream-json --verbose";
// Install Claude Code in the sandbox
await sandbox.process.executeCommand("npm install -g @anthropic-ai/claude-code");
const ptyHandle = await sandbox.process.createPty({
id: "claude",
onData: (data) => {
process.stdout.write(data);
},
});
await ptyHandle.waitForConnection();
// Run the Claude Code command inside the sandbox
ptyHandle.sendInput(
`ANTHROPIC_API_KEY=${process.env.ANTHROPIC_API_KEY} ${claudeCommand}\n`
);
// Use this to close the terminal session if no more commands will be executed
// ptyHandle.sendInput("exit\n")
await ptyHandle.wait();
// If you are done and have closed the PTY terminal, it is recommended to clean up resources by deleting the sandbox
// await sandbox.delete();
} catch (error) {
console.error("Failed to run Claude Code in Daytona sandbox:", error);
}
````
# Run Claude Managed Agents on Daytona
import { Image } from 'astro:assets'
import claudeManagedAgentsArchitecture from '../../../../../assets/docs/images/claude-managed-agents-architecture.svg'
A guide to running Claude Managed Agents inside your own Daytona sandboxes, as a self-hosted environment.
### Introduction
Claude Managed Agents is Anthropic's configurable agent harness and infrastructure for running Claude as an autonomous agent. You define an agent (model, system prompt, tools, MCP servers), open a session, and stream events while the agent reads files, runs commands, and uses other tools to finish the task. By default, sessions run inside Anthropic-operated cloud containers.
A *self-hosted environment* moves that container layer to you. Everything else you get from Managed Agents stays unchanged: the agent loop, prompt caching, model calls, event stream, and session history all stay on Anthropic's side. The container that holds the agent's filesystem and shell runs on your infrastructure. With the Daytona integration, that container is a Daytona sandbox.
### How the pieces fit together
Three parties are involved in any session:
- **Anthropic** runs the API, the agent loop, and a per-environment work queue that signals when an agent has tools to dispatch.
- **You** run two things: an *application* that creates sessions and talks to your end users, and an *orchestrator* that manages the sandbox lifecycle (create, start, stop, clean up) and runs the agent's tool runner inside each sandbox.
- **Daytona** provides the sandbox containers in which filesystem and shell tools execute.
When the agent decides to use a tool, where the call goes depends on the tool:
- **Filesystem and shell tools** (`bash`, `read`, `write`, `edit`, `glob`, `grep`) are dispatched inside your Daytona sandbox. Your orchestrator ensures the agent's tool runner is running there; the runner executes each call against the sandbox's filesystem and shell and posts the result back to the session stream.
- **Web tools** (`web_search`, `web_fetch`) and **MCP server tools** are dispatched by Anthropic server-side. MCP calls use credentials held in Anthropic-managed vaults. The sandbox is not involved.
This split means that a self-hosted environment changes where filesystem and shell tools run, and nothing else.
Each session gets its own isolated sandbox: filesystem changes persist across tool calls within the session, and while a single runner is alive the bash shell also keeps its working directory, environment, and background processes between calls.
### What you set up
#### Reference implementation
A reference implementation of the orchestrator, the in-sandbox runner, the snapshot builder, and example agents is provided in the Daytona repo at [`python/claude/claude-managed-agents/`](https://github.com/daytona/guides/tree/main/python/claude/claude-managed-agents). Each section below points to the concrete file in that directory.
To follow along locally:
```bash
git clone https://github.com/daytona/guides.git
cd guides/python/claude/claude-managed-agents
python3.12 -m venv .venv
source .venv/bin/activate
pip install -e .
```
If you plan to run the reference webhook orchestrator, install with the `webhook` extras instead: `pip install -e ".[webhook]"`.
#### On the Anthropic side
1. **Create a self-hosted environment.** In the Claude Console, open *Workspace → Environments → New → Self-hosted*, or from code:
```python
import anthropic
env = anthropic.Anthropic().beta.environments.create(
name="my-daytona-env",
config={"type": "self_hosted"}
)
```
2. **Generate an environment key** for the environment from the Console. This key authenticates the whole worker flow (poll, ack, stop, heartbeat, session event stream, skill download) for this one environment. Keep it on the orchestrator host only.
3. **Create your agent** as you would for any Managed Agents setup. The agent does not need to know it will run on a self-hosted environment; that is decided per session. The reference's `create_agent.py ` is a one-liner for spinning up a sandbox-tools-only testing agent and printing its id.
#### On the Daytona side
Daytona provides the per-session sandbox containers and the snapshot mechanism your orchestrator uses. You'll need a Daytona account and API key.
The reference's `build_default_snapshot.py` builds a sandbox image from `Dockerfile.default` and publishes it as a snapshot in your Daytona workspace. The Dockerfile mirrors the runtimes from Claude Managed Agents' [container reference](https://platform.claude.com/docs/en/managed-agents/cloud-containers). Run the script once; from then on, the orchestrator creates a sandbox per session from that snapshot, on demand.
At its core the script wraps a single Daytona SDK call:
```python
from daytona import CreateSnapshotParams, Daytona, Image, Resources
Daytona().snapshot.create(
CreateSnapshotParams(
name="byoc-env-default",
image=Image.from_dockerfile("Dockerfile"),
resources=Resources(cpu=2, memory=8, disk=10),
),
on_logs=lambda chunk: print(chunk, end="", flush=True),
)
```
To change what's installed in the sandbox, edit `Dockerfile.default` and rerun the script. It hashes the Dockerfile, names the snapshot `byoc-env-default-`, and no-ops if a snapshot with that exact hash already exists.
### Running the orchestrator
#### The orchestrator process
You run an orchestrator as a long-lived process. Its responsibilities:
- Watch the environment's work queue, either by long-polling it or by receiving webhooks from Anthropic on each new turn.
- For each work item, ensure a Daytona sandbox is running and start the agent's tool runner inside it. The runner attaches to the session's event stream and answers `bash`, `read`, `write`, `edit`, `glob`, `grep` against the sandbox.
- Stop sandboxes that have gone idle past a configurable threshold, and start them back up on the next work item. The sandbox's filesystem survives the pause, so a session can sit quiet between bursts of activity without keeping a sandbox running. Sandboxes that stay stopped for 30 days are deleted; activity restarts the timer.
- Archive sandboxes when their session terminates. The filesystem stays in Daytona's cost-effective object storage until the same 30-day window expires, then the sandbox is deleted.
The reference includes two orchestrator variants:
- `host_orchestrator_polling.py` long-polls the work queue and only needs the environment key, so it works against environments behind any kind of NAT or firewall.
- `host_orchestrator_webhook.py` is a FastAPI receiver that drains the queue on each `session.status_run_started` delivery; it needs a publicly reachable URL and an `ANTHROPIC_WEBHOOK_SECRET`, but it avoids continuous polling.
Both share the same sandbox-lifecycle logic.
The per-work-item path looks roughly like this (the reference's `orchestrator_lib.py` adds dedupe, ack, retries, locking, and the janitor thread):
```python
from daytona import CreateSandboxFromSnapshotParams, Daytona, DaytonaNotFoundError
daytona = Daytona()
def handle_work(work):
session_id = work.data.id
name = f"byoc-{session_id}"
try:
sb = daytona.get(name)
except DaytonaNotFoundError:
sb = daytona.create(CreateSandboxFromSnapshotParams(
name=name,
snapshot="byoc-env-default",
labels={"byoc.session_id": session_id},
))
if sb.state in ("stopped", "archived"):
sb.start()
sb.fs.upload_file(open("sandbox_runner.py", "rb").read(),
"/home/daytona/sandbox_runner.py")
sb.process.exec("pip install --user anthropic")
sb.process.exec(
f"ANTHROPIC_ENVIRONMENT_KEY={environment_key} "
f"ANTHROPIC_WORK_ID={work.id} ANTHROPIC_SESSION_ID={session_id} "
f"ANTHROPIC_ENVIRONMENT_ID={environment_id} "
"nohup python3 /home/daytona/sandbox_runner.py &"
)
```
The orchestrator needs the environment key and a Daytona API key (plus an `ANTHROPIC_WEBHOOK_SECRET` if you run the webhook receiver). It does not need any user-facing credentials, and it does not need to know about your application.
#### The in-sandbox runner
The "agent's tool runner" the orchestrator launches is a small Python process. The Anthropic SDK ships an `EnvironmentWorker` that composes skill download, tool dispatch, heartbeating the work-item lease, and the force-stop on exit. All the runner has to do is call `handle_item()`, for example:
```python
import asyncio
import os
from anthropic import AsyncAnthropic
async def main():
environment_key = os.environ["ANTHROPIC_ENVIRONMENT_KEY"]
async with AsyncAnthropic(auth_token=environment_key) as client:
await client.beta.environments.work.worker(
environment_key=environment_key,
workdir="/mnt/session",
).handle_item()
asyncio.run(main())
```
`handle_item()` reads `ANTHROPIC_SESSION_ID`, `ANTHROPIC_WORK_ID`, and `ANTHROPIC_ENVIRONMENT_ID` from the environment, then attaches to the session's event stream and runs the agent's tool calls against the six filesystem and shell tools (`bash`, `read`, `write`, `edit`, `glob`, `grep`) rooted at `workdir`. It heartbeats the work-item lease while the agent runs, and force-stops the lease on exit.
The environment key (passed in by the orchestrator) is the only credential this process needs to talk to Anthropic. It's scoped to a single environment: the runner can act on sessions in that environment, and nothing else in your Anthropic account. For multi-tenant deployments, give each tenant its own environment.
### Running applications
#### Driving a session
It's the same as for a cloud environment; the only difference is `environment_id` points at your `self_hosted` environment.
```python
session = client.beta.sessions.create(
agent=agent_id,
environment_id="env_01..." # your self-hosted env
)
```
Open the events stream before sending the first user message:
```python
with client.beta.sessions.events.stream(session.id) as stream:
client.beta.sessions.events.send(
session.id,
events=[{"type": "user.message", "content": [{"type": "text", "text": "what's installed in this container? versions please"}]}],
)
for ev in stream:
# render events as they arrive;
# session.status_idle marks end of turn.
...
```
See [Events and streaming](https://platform.claude.com/docs/en/managed-agents/events-and-streaming) for the full event vocabulary and richer examples.
#### Mounting session resources
You are responsible for mounting session resources, for example files or GitHub repositories. Shared dependencies can be baked into your container image. For per-session state, the orchestrator reads two keys off `session.metadata`:
- `daytona.snapshot_name`: create this session's sandbox from a named Daytona snapshot instead of your default. Use this when different sessions need different toolchains or pre-installed packages.
- `daytona.sandbox_id`: attach an already-prepared Daytona sandbox instead of creating one. Use this when you need to seed per-session state before the agent starts, like cloning a repo, loading a dataset, or mounting a customer-specific volume.
The two keys are mutually exclusive.
```python
session = client.beta.sessions.create(
agent=agent_id,
environment_id="env_01...",
metadata={"daytona.sandbox_id": ""},
)
```
For the prepared-sandbox path, create the sandbox with the right labels before passing its id in `session.metadata`:
```python
from daytona import CreateSandboxFromSnapshotParams, Daytona
sb = Daytona().create(CreateSandboxFromSnapshotParams(
snapshot="byoc-env-default",
labels={
"byoc.environment_id": "env_01...",
"byoc.mode": "prepared",
},
))
# do any per-session prep here (clone a repo, load data, ...)
# then pass sb.id as session.metadata["daytona.sandbox_id"]
```
`sb.set_labels({...})` does the same on an already-created sandbox, if you'd rather prepare first and label last.
When the session arrives the orchestrator validates the labels, binds the sandbox by setting `byoc.session_id` and flipping `byoc.mode` to `in-sandbox`, then installs and starts its runner. The sandbox can be in any state at handoff; the orchestrator starts it first.
Custom snapshots and prepared sandboxes must include the runner prerequisites the in-sandbox worker needs. A minimal working Daytona snapshot, included in the reference as `Dockerfile.minimal`, is just:
```dockerfile
FROM python:3.12-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
util-linux procps mawk \
&& rm -rf /var/lib/apt/lists/* \
&& mkdir -p /home/daytona /mnt/session \
&& chmod 777 /home/daytona /mnt/session
WORKDIR /mnt/session
```
:::note
Claude Managed Agents memory stores are not yet supported.
:::
#### MCP servers and vaults
MCP servers and Anthropic-managed vaults work on self-hosted environments without changes. The agent declares the MCP server in its `mcp_servers` list, vault-held credentials are referenced by id, and the call is proxied by Anthropic server-side. Your sandbox is not in the path. This is what lets a single agent mix sandbox-routed tools (a `bash` against your Daytona sandbox) with MCP-proxied tools (a query against, say, Linear) on one event stream.
### What you get
- *The sandbox is yours to do whatever else you want with.* Beyond running the agent's tool runner, you can shell into the container, mount volumes, pre-install per-customer code, warm caches, run sidecar processes, attach observability. The runner is one process in a container that you own end to end.
- *Build your sandbox snapshot however you like.* From a Dockerfile or Daytona's [declarative builder](https://www.daytona.io/docs/en/declarative-builder.md): whatever language versions, system packages, or in-house tools the agent needs goes in there.
- *Switching to cloud is one line.* Point `environment_id` at a cloud environment and the `sessions.create` / `events.stream` loop is unchanged. The `session.metadata` keys are the only Daytona-specific part — drop them and use the cloud equivalents: [environment setup](https://platform.claude.com/docs/en/managed-agents/environments) for the container customization, and the [Files API](https://platform.claude.com/docs/en/managed-agents/files) for per-session inputs.
### See also
- [Claude Managed Agents overview](https://platform.claude.com/docs/en/managed-agents/overview)
- [Claude Managed Agents tools](https://platform.claude.com/docs/en/managed-agents/tools)
- [Claude Managed Agents events and streaming](https://platform.claude.com/docs/en/managed-agents/events-and-streaming)
- [Daytona documentation](https://www.daytona.io/docs/index.md)
# Run Gemini CLI Headlessly in Daytona and Stream Its Output
This guide demonstrates how to run [Google's Gemini CLI](https://geminicli.com/) as a headless coding agent inside a Daytona sandbox. The agent can write code in any language, install dependencies, run scripts, and reason over a project, all inside a secure, isolated, disposable sandbox while its output streams back to your terminal in real time.
---
### 1. Workflow Overview
When you launch the main module, a Daytona sandbox is created and the Gemini CLI is installed inside it. The agent is driven headlessly with `gemini -p "" --yolo --output-format stream-json`, and its newline-delimited JSON events are parsed and printed as the agent works.
You interact with the main program via a command line chat interface. The program sends your prompts to the Gemini CLI inside the sandbox, which writes code, runs commands, and streams the results back as it works. Each tool call surfaces as a `[tool]` line, followed by the assistant's response.
Gemini sessions are stateful, so each turn reuses the session captured from the first run, keeping context across the conversation. You can continue interacting with your agent until you are finished. When you exit the program, the sandbox is deleted automatically.
### 2. Project Setup
#### Clone the Repository
First, clone the daytona [repository](https://github.com/daytona/guides.git) and navigate to the example directory:
```bash
git clone https://github.com/daytona/guides.git
cd guides/typescript/gemini/gemini-cli
```
#### Configure Environment
Get your API keys:
- **Daytona API key:** [Daytona Dashboard](https://app.daytona.io/dashboard/keys)
- **Gemini API key:** [Google AI Studio](https://aistudio.google.com/apikey)
Copy `.env.example` to `.env` and add your keys:
```bash
DAYTONA_API_KEY=your_daytona_key
SANDBOX_GEMINI_API_KEY=your_gemini_key
```
:::caution[API Key Security]
The `SANDBOX_GEMINI_API_KEY` key is passed into the Daytona sandbox environment and is accessible to any code executed inside the sandbox.
:::
#### Local Usage
:::note[Node.js Version]
Node.js 20 or newer is required to run this example. Please ensure your environment meets this requirement before proceeding.
:::
Install dependencies:
```bash
npm install
```
Run the agent:
```bash
npm run start
```
The agent will start and wait for your prompt.
### 3. Example Usage
Ask the agent to write and run some code. Here it generates an ASCII-art Mandelbrot fractal inside the sandbox and executes it, streaming each tool call and the program output back to your terminal:
```
$ npm run start
Creating sandbox...
Installing Gemini CLI...
Starting Gemini CLI...
Agent ready. Press Ctrl+C at any time to exit.
User: Write a Python script mandelbrot.py that renders the Mandelbrot set as ASCII art roughly 40 columns by 20 rows, then run it and show the rendered output
[tool] write_file
[tool] run_shell_command
[tool] replace
[tool] run_shell_command
I have successfully created and executed the Python script mandelbrot.py to render the Mandelbrot set as ASCII art.
......-:@...
.......:%+:....
........:*@@*:....
.....+-:--=@@-:::::.
.......:@%@@@@@@@@=#+..
.........==@@@@@@@@@@@+:..
.....-::::::%@@@@@@@@@@@@@%:..
.......:-@*@%--@@@@@@@@@@@@@@%:..
.......::%@@@@@+@@@@@@@@@@@@@@#...
..-:.::+@@@@@@@@@@@@@@@@@@@@@@:...
..-:.::+@@@@@@@@@@@@@@@@@@@@@@:...
.......::%@@@@@+@@@@@@@@@@@@@@#...
.......:-@*@%--@@@@@@@@@@@@@@%:..
.....-::::::%@@@@@@@@@@@@@%:..
.........==@@@@@@@@@@@+:..
.......:@%@@@@@@@@=#+..
.....+-:--=@@-:::::.
........:*@@*:....
.......:%+:....
......-:@...
User:
```
:::note[Why the extra steps?]
The agent wrote `mandelbrot.py` and ran it, then made a small `replace` edit and ran it again to refine the rendering. A single `write_file` plus `run_shell_command` already satisfies the prompt; the extra edit-and-rerun is the agent choosing to improve its own output.
:::
### 4. Understanding the Script
This example consists of two parts: a main program (`src/index.ts`) that manages the sandbox and a command-line loop, and a session class (`src/session.ts`) that drives the Gemini CLI over a PTY and parses its streaming JSON output.
#### Initialization
On startup, the script:
1. Creates a new [Daytona sandbox](https://www.daytona.io/docs/en/sandboxes.md) with the Gemini API key injected as an environment variable.
2. Installs the Gemini CLI globally in the sandbox.
3. Creates a PTY for streaming output from the Gemini CLI.
4. Enters a readline loop to send prompts and receive streamed responses.
5. On Ctrl+C, kills the PTY session, deletes the sandbox, and exits.
#### Creating the Sandbox
The Gemini CLI defaults to interactive browser OAuth, which would hang a headless run. Passing `GEMINI_API_KEY` as a sandbox environment variable at create time lets the CLI authenticate non-interactively. `GEMINI_CLI_TRUST_WORKSPACE` bypasses the CLI's workspace-trust prompt, which otherwise blocks `--yolo` runs in a fresh sandbox directory:
```ts
sandbox = await daytona.create({
envVars: {
GEMINI_API_KEY: process.env.SANDBOX_GEMINI_API_KEY,
GEMINI_CLI_TRUST_WORKSPACE: 'true',
},
})
const install = await sandbox.process.executeCommand('npm install -g @google/gemini-cli')
if (install.exitCode !== 0) {
throw new Error('Error installing Gemini CLI: ' + install.result)
}
```
#### PTY Communication
The session uses a pseudo-terminal (PTY) for streaming output from the Gemini CLI:
```ts
async initialize(): Promise {
this.ptyHandle = await this.sandbox.process.createPty({
id: `gemini-pty-${Date.now()}`,
cols: 120,
rows: 30,
onData: (data: Uint8Array) => this.handleData(data),
})
await this.ptyHandle.waitForConnection()
}
```
#### Running Gemini Commands
Each prompt is sent as a `gemini` command in headless mode. `-p` runs a one-shot non-interactive prompt, `--yolo` auto-approves tool calls so the run never blocks on a permission prompt, and `--output-format stream-json` emits newline-delimited JSON events. When a session ID has been captured, `-r` resumes that session for multi-turn continuity:
```ts
async processPrompt(prompt: string): Promise {
const flags = ['-p', this.shellQuote(prompt), '--yolo', '--output-format', 'stream-json']
// -r resumes the existing session for multi-turn continuity.
if (this.sessionId) flags.unshift('-r', this.shellQuote(this.sessionId))
const command = ['gemini', ...flags].join(' ')
await this.ptyHandle!.sendInput(`cd ${WORK_DIR} && ${command}\n`)
await new Promise((resolve) => {
this.onResponseComplete = resolve
})
}
```
#### Streaming JSON Messages
The Gemini CLI outputs JSON lines that are parsed to track agent activity. The `handleData` method buffers incoming PTY bytes and processes each complete line, keeping any incomplete line for the next chunk. A stateful `TextDecoder` is reused across calls so partial multi-byte UTF-8 sequences split across PTY chunks are preserved instead of being corrupted:
```ts
private decoder = new TextDecoder('utf-8')
private handleData(data: Uint8Array): void {
this.buffer += this.decoder.decode(data, { stream: true })
const lines = this.buffer.split('\n')
this.buffer = lines.pop() || ''
for (const line of lines.map((l) => l.trim()).filter(Boolean)) {
try {
this.handleEvent(JSON.parse(line) as GeminiStreamEvent)
} catch {
debug('non-JSON line:', line)
}
}
}
```
Event types from the Gemini CLI's streaming JSON ([schema reference](https://geminicli.com/docs/cli/headless/)):
- **init**: Session metadata (`session_id`, model) - captured to resume the session on later turns
- **message**: User and assistant message chunks (assistant text is printed live)
- **tool_use**: Tool call requests with arguments
- **tool_result**: Output from executed tools
- **error**: Non-fatal warnings and system errors
- **result**: Final outcome with aggregated statistics - signals response completion
```ts
private handleEvent(event: GeminiStreamEvent): void {
switch (event.type) {
case 'init': {
const init = event as InitEvent
if (init.session_id && !this.sessionId) {
this.sessionId = init.session_id
debug('captured session_id:', this.sessionId)
}
return
}
case 'message': {
const msg = event as MessageEvent
if (msg.role === 'assistant' && msg.content) {
process.stdout.write(msg.content)
}
return
}
case 'tool_use': {
const tool = event as ToolUseEvent
// Skip update_topic: an internal Gemini bookkeeping tool, not a user-facing action.
if (tool.tool_name === 'update_topic') return
process.stdout.write(`\n[tool] ${tool.tool_name}\n`)
return
}
case 'tool_result': {
const result = event as ToolResultEvent
if (result.status === 'error' && result.error) {
process.stdout.write(`\n[tool error] ${result.error.message}\n`)
}
return
}
case 'error': {
const err = event as ErrorEvent
process.stderr.write(`\n[${err.severity}] ${err.message}\n`)
return
}
case 'result': {
const res = event as ResultEvent
if (res.status === 'error' && res.error) {
process.stderr.write(`\nFailed: ${res.error.message}\n`)
}
process.stdout.write('\n')
this.onResponseComplete?.()
return
}
}
}
```
When the `result` event arrives, `onResponseComplete` resolves the promise that `processPrompt` is awaiting, so the readline loop can prompt for the next turn.
**Key advantages:**
- Secure, isolated execution in Daytona sandboxes
- Fully headless operation, no browser OAuth and no permission prompts
- Streaming JSON output (`--output-format stream-json`) for real-time tool and message activity
- PTY-based communication for low-latency streaming
- Session-based conversation continuity across prompts (`-r`)
- All agent code execution happens inside the sandbox
- Automatic cleanup on exit
# Run the Devin CLI in a Daytona Sandbox
This guide runs [Cognition's Devin CLI](https://docs.devin.ai/cli) inside a Daytona sandbox. You log in to Devin once over a real terminal, then send it prompts and get its output back in your terminal. Because Devin works entirely inside the sandbox, it can edit files, install packages, and run code in an isolated, disposable environment that is thrown away when you finish. The only thing running on your own machine is a small Node.js controller that wires your terminal to the sandbox.
---
### 1. Workflow Overview
When you launch the main module, a Daytona sandbox is created and the Devin CLI is installed inside it. You then log in once over a sandbox PTY (Devin's manual-token flow works on any plan, including the free tier) and dismiss Devin's one-time onboarding wizard, after which each prompt is run headlessly as `devin -p "" --permission-mode dangerous`. Turns after the first add `--continue` so the conversation carries context from one prompt to the next.
Every phase that talks to Devin uses the same trick. Opening a PTY starts a shell in the sandbox; rather than run Devin as a child of that shell, the controller tells the shell to `exec` Devin, which makes Devin take over the shell's process. This buys two things. First, the output you see is exactly what Devin prints, with no shell prompt or echoed command around it, so it looks the same as running Devin in your own terminal. Second, because Devin replaced the shell, the PTY closes the moment Devin exits, which is how the controller knows a turn has finished and can prompt you again.
You can keep interacting with your agent until you are finished. When you exit the program, the sandbox is deleted automatically.
### 2. Project Setup
#### Clone the Repository
First, clone the daytona [repository](https://github.com/daytona/guides.git) and navigate to the example directory:
```bash
git clone https://github.com/daytona/guides.git
cd guides/typescript/cognition/devin-cli
```
#### Configure Environment
You need:
- **Daytona API key:** [Daytona Dashboard](https://app.daytona.io/dashboard/keys)
- **Devin account:** any plan, including the free tier ([Devin app](https://app.devin.ai/)). No Devin API key is required; you log in interactively when the sandbox starts.
Copy `.env.example` to `.env` and add your Daytona key:
```bash
DAYTONA_API_KEY=your_daytona_key
```
:::caution[Credentials live in the sandbox]
During login you paste a code into the sandbox PTY; Devin exchanges it for credentials written to the sandbox's filesystem (`~/.local/share/devin/credentials.toml`). The sandbox is disposable and deleted on exit, so the credentials do not persist, but treat the sandbox as you would any environment holding a live credential.
:::
#### Local Usage
:::note[Node.js Version]
Node.js 18 or newer is required to run this example.
:::
Install dependencies:
```bash
npm install
```
Run the agent:
```bash
npm run start
```
The agent will start and wait for your prompt.
### 3. Example Usage
Ask the agent to write and run some code. Here it implements Myers' diff algorithm (the line diff at the heart of `git diff`), writes a pytest suite, and runs everything inside the sandbox:
```
$ npm run start
Creating sandbox...
Installing Devin CLI...
Starting Devin CLI...
Log in to Devin to continue (any plan works, including the free tier).
Open the URL that appears below, sign in, and paste the code back here.
Visit https://app.devin.ai/auth/cli/continue?state=...&code_challenge=...&code_challenge_method=S256 to sign in, then copy the code and paste it below.
Code:
❭ Paste the code from the sign-in page
Enter submit Esc cancel
Login successful! Credentials stored.
Finishing one-time Devin setup. Use the arrow keys and choose "Skip for now" if you do not need a Git provider.
Welcome to Devin CLI!
Logged in as you@example.com.
✓ Organization: My Team
You're all set. Run devin to get started.
Agent ready. Press Ctrl+C at any time to exit.
User: Write a Python module mydiff.py implementing Myers' diff algorithm to compute the line-level diff between two text strings, producing a minimal edit script. Add a CLI that takes two file paths and prints a git-style diff with - for removed lines, + for added lines, and a space for context. Create two sample files (old.txt and new.txt) holding a short Python function before and after a refactor. Run the diff CLI on them and show the output. Add a pytest suite covering empty inputs, identical inputs, full replacement, and a known small case with a known minimal edit script. Run the tests.
I've successfully implemented Myers' diff algorithm with all requested features. Here's a summary:
## Created Files
1. **mydiff.py** - Complete implementation including:
- Myers' diff algorithm for computing minimal edit scripts
- CLI with argparse for git-style diff output
- Support for context lines (default 3, configurable with `-U` flag)
- Edit class representing operations (delete, insert, equal)
2. **old.txt** - Sample Python function before refactoring
3. **new.txt** - Refactored version with simplified logic
4. **test_mydiff.py** - Comprehensive pytest suite with 11 tests covering:
- Empty inputs
- Identical inputs
- Full replacement
- Known small case with verified minimal edit script
- Insert at beginning
- Delete at end
- Empty old/new files
- Format diff with context
- Minimal edit script property verification
- Complex refactor scenario
## Sample Diff Output
Running `python mydiff.py old.txt new.txt` produces:
@@ -1,26 +1,11 @@
def calculate_user_score(user_data):
- if user_data is None:
+ if not user_data:
return 0
- base_score = 0
- if user_data.get('is_active', False):
- base_score = base_score + 10
- else:
- base_score = base_score + 5
+ score = 0
+ score += 15 if user_data.get('is_active') else 5
+ score += 20 if user_data.get('is_premium') else 0
+ score += 15 if user_data.get('login_count', 0) > 10 else 5
+ score += 10 if user_data.get('has_profile_pic') else 0
- if user_data.get('is_premium', False):
- base_score = base_score + 20
- else:
- base_score = base_score + 0
-
- if user_data.get('login_count', 0) > 10:
- base_score = base_score + 15
- else:
- base_score = base_score + 5
-
- if user_data.get('has_profile_pic', False):
- base_score = base_score + 10
- else:
- base_score = base_score + 0
-
- return base_score
+ return score
## Test Results
All 11 tests passed successfully:
============================== 11 passed in 0.05s ==============================
The implementation correctly handles edge cases, produces minimal edit scripts, and displays git-style diffs with proper context.
User:
```
### 4. Understanding the Script
This example consists of two parts: a main program (`src/index.ts`) that manages the sandbox and a command-line loop, and a session class (`src/session.ts`) that drives each Devin invocation over its own PTY.
#### Initialization
On startup, the script:
1. Creates a new [Daytona sandbox](https://www.daytona.io/docs/en/sandboxes.md).
2. Installs the Devin CLI in the sandbox and confirms the binary with `devin --version`.
3. Logs you in via Devin's manual-token flow over a fresh PTY.
4. Runs Devin's one-time onboarding wizard (interactive) so you can skip it once.
5. Enters a readline loop where each prompt is a headless `devin -p` turn in its own PTY.
6. On Ctrl+C, restores stdin, deletes the sandbox, and exits.
#### Creating the Sandbox
That installer finishes by launching Devin's interactive onboarding wizard, which needs a terminal. The install runs without one, so the wizard bails out and the install command exits with an error code even though the `devin` binary itself installed fine. Because that exit code is unreliable, the script confirms the install by running the binary directly with `"$HOME/.local/bin/devin" --version`. It uses the full path rather than a bare `devin` because whether `~/.local/bin` is on `PATH` varies between shell types and sandbox configurations, so a full path works regardless. The install's combined stdout and stderr is surfaced on failure for diagnostics:
```ts
sandbox = await daytona.create()
const install = await sandbox.process.executeCommand(
'curl -fsSL https://cli.devin.ai/install.sh | bash 2>&1',
)
const version = await sandbox.process.executeCommand('"$HOME/.local/bin/devin" --version')
if (version.exitCode !== 0) {
throw new Error(
'Devin CLI did not install correctly.\n' +
`Install output:\n${install.result}\n` +
`Version check output:\n${version.result}`,
)
}
```
#### Per-invocation PTY with `exec`
Every phase that talks to Devin uses the same primitive: open a fresh PTY in the sandbox, then have its shell `exec` the Devin command. The `exec` is essential rather than a detail. It makes Devin replace the shell process instead of running underneath it, so there is no shell prompt or echoed command wrapping Devin's output, and the PTY closes the moment Devin exits, which is how the controller detects the turn finished:
```ts
private async attach(command: string, interactive: boolean): Promise {
// Every phase opens a fresh PTY, so reset the per-invocation stream state first: a clean
// decoder, and passthrough/launchBuffer back to their pre-marker state so this turn's
// launch-line filtering never inherits leftover state from the previous turn.
this.decoder = new TextDecoder('utf-8')
this.passthrough = false
this.launchBuffer = ''
const pty = await this.sandbox.process.createPty({
id: `devin-pty-${Date.now()}`,
cols: process.stdout.columns || 120,
rows: process.stdout.rows || 30,
onData: (data: Uint8Array) => this.forward(data),
})
await pty.waitForConnection()
await pty.sendInput(`cd ${WORK_DIR}; printf '\\n%s\\n' '${READY}'; exec ${command}\n`)
const stdin = process.stdin
const onStdin = (chunk: Buffer) => void pty.sendInput(chunk)
if (interactive) {
while (stdin.read() !== null) { /* drain buffered bytes from the prior step */ }
if (stdin.isTTY) stdin.setRawMode(true)
stdin.resume()
stdin.on('data', onStdin)
}
try {
const result = await pty.wait()
return result.exitCode
} finally {
if (interactive) {
stdin.removeListener('data', onStdin)
if (stdin.isTTY) stdin.setRawMode(false)
stdin.pause()
}
await pty.disconnect()
}
}
```
That single launch line (`cd` to the workspace, print a readiness marker, then `exec`) is the only shell command the PTY ever runs. After `exec`, Devin owns the terminal.
For interactive commands (login and setup), the controller bridges your local keyboard into the PTY in four steps:
1. **Drain stale input** with `while (stdin.read() !== null) {}`. Any bytes left buffered from a previous step, such as the trailing newline after you pasted a login code, are discarded so they are not accidentally fed into this command.
2. **Switch the terminal to raw mode** with `setRawMode(true)`. Normally the terminal collects a whole line at a time and handles editing and echo locally. Raw mode turns that off, so each keystroke is delivered immediately and is not printed twice (once by your local terminal, once by Devin echoing it back).
3. **Resume stdin** with `stdin.resume()`. Node keeps a stdin stream paused until something listens to it, so resuming is what actually starts the bytes flowing.
4. **Register the forwarder** with `stdin.on('data', ...)`, which ships every chunk you type straight into the sandbox PTY where Devin reads it.
Headless turns (`-p`) skip all of this. Devin reads its prompt from the command arguments, so it needs no keyboard input.
#### Hiding the launch line
Before `exec` runs, the sandbox shell (zsh) prints the launch command back on its own stdout. This is the same behavior any interactive shell has: it echoes the command it has just received so a human at the terminal can see what is about to run. The sandbox PTY's stdout is what we receive over `onData`, so those bytes flow back to us alongside Devin's real output. To keep the screen clean and show only what Devin prints, the data handler buffers PTY output until it sees the readiness marker, then forwards every subsequent byte untouched:
```ts
private forward(data: Uint8Array): void {
const text = this.decoder.decode(data, { stream: true })
if (this.passthrough) {
process.stdout.write(text)
return
}
this.launchBuffer += text
const m = READY_RE.exec(this.launchBuffer)
if (m) {
const rest = this.launchBuffer.slice(m.index + m[0].length)
this.passthrough = true
this.launchBuffer = ''
if (rest) process.stdout.write(rest)
} else if (this.launchBuffer.length > 8192) {
this.launchBuffer = this.launchBuffer.slice(-READY.length - 2)
}
}
```
There are two independent things to handle here, solved by two independent pieces of the function above.
The first is that the marker text `__DAYTONA_DEVIN_READY__` ends up in the stream **twice**: once inside the echoed command (where it sits in the middle of a longer line, wrapped in single quotes: `printf '\n%s\n' '__DAYTONA_DEVIN_READY__'; exec …`), and once as the actual `printf` output (where it lands on its own line surrounded by newlines: `\r\n__DAYTONA_DEVIN_READY__\r\n`). We need to ignore the echoed copy and lock onto the `printf` copy. The regex `(^|[\r\n])__DAYTONA_DEVIN_READY__[\r\n]` does that simply by requiring a line break (or buffer start) immediately before *and* immediately after the marker text. The echoed copy has single-quotes on both sides, so the regex never matches it; the `printf` copy has line breaks on both sides, so the regex matches. The character class is `[\r\n]` rather than just `\n` because a PTY rewrites every `\n` as `\r\n` on the way out, so the newlines around the real marker arrive as carriage-return-plus-line-feed pairs.
The second thing is that the real marker can be split across two reads. PTY output arrives in arbitrary chunks, so a single `forward` call may receive only the first half of the marker bytes, with the second half landing in the next call. There is no detection logic for this case; the function simply keeps appending to `launchBuffer` and re-runs the regex after every chunk, so the match will land whenever the marker becomes complete.
The `else if` branch covers the unlikely case where the marker never arrives at all. Without it, `launchBuffer` would grow unbounded. `8192` is an arbitrary safety threshold: realistic shell-echo preludes are a few hundred bytes, so this should never fire in practice; it just has to be small enough that runaway growth is impossible. When it does fire, the buffer is trimmed but the last `READY.length + 2` bytes are kept rather than thrown out completely. That covers the case where a partial marker happens to sit at the end of the buffer right when the trim runs. For example, if the buffer ends with `\r\n__DAYTONA_DEVIN_READY__` and is waiting for the closing `\r\n` from the next chunk, keeping the last `READY.length + 2` bytes ensures the partial marker is still there when the next chunk arrives, so the regex can complete the match.
#### Logging in
Devin's default login opens a browser on the same machine the CLI runs on and waits for it to redirect back. A sandbox has no browser and no way to receive that redirect, so the session uses `--force-manual-token-flow` instead: Devin prints a URL and blocks reading from its stdin. The "wait" is just Devin's `read()` blocking on the PTY, with no polling loop. The interactive stdin bridge is what makes the paste work: whatever you type into your local terminal flows raw into the sandbox PTY where Devin reads it. After the command exits, `devin auth status` is the source of truth for whether the login actually succeeded:
```ts
async login(): Promise {
await this.attach(`${DEVIN} auth login --force-manual-token-flow`, true)
const status = await this.sandbox.process.executeCommand(`${DEVIN} auth status`)
if (status.exitCode !== 0) {
throw new Error(`devin auth status failed (exit ${status.exitCode}):\n${status.result}`)
}
if (/not logged in/i.test(status.result ?? '')) {
throw new Error('Devin login did not complete. Re-run and paste a valid code when prompted.')
}
}
```
Because the terminal is in raw mode, `Ctrl+C` is not turned into a local interrupt signal. It arrives as a raw `0x03` byte and is forwarded into the PTY, so Devin receives it exactly as if you had pressed `Ctrl+C` in the sandbox terminal it is running in. Devin exits, `auth status` then reports "not logged in", and the controller throws cleanly.
#### One-time setup
The installer normally finishes by running Devin's `setup` wizard (the "Connect a Git provider" menu). That wizard needs an interactive terminal, but the install runs through `executeCommand`, which has no terminal attached, so the wizard cannot draw its menu and is skipped. Left alone, it would resurface the first time you run `devin -p` and block the turn. The controller runs it explicitly right after login so you dismiss it once, and later headless turns are not interrupted by onboarding:
```ts
async setup(): Promise {
await this.attach(`${DEVIN} setup`, true)
}
```
Pick "Skip for now" if you do not need a Git provider; Devin records `setup_complete` on disk and every later run goes straight to the task.
#### Running a turn (with conversation continuity)
Each prompt is a one-shot, non-interactive Devin invocation. `-p` runs Devin in print mode and `--permission-mode dangerous` auto-approves tool calls so the run never blocks on a permission prompt. The first turn starts a fresh session; every turn after it adds `--continue`, which resumes the most recent session from the working directory, so context carries from one prompt to the next:
```ts
async processPrompt(prompt: string): Promise {
const cont = this.resumable ? ' --continue' : ''
const exitCode = await this.attach(
`${DEVIN} -p ${this.shellQuote(prompt)} --permission-mode dangerous${cont}`,
false,
)
// Only become resumable after a turn succeeds; a failed first turn creates no session.
if (exitCode === 0) this.resumable = true
process.stdout.write('\n')
}
```
There is no stdin bridge and no raw mode here; the controller forwards Devin's output to your terminal. The turn ends when Devin exits, which resolves `pty.wait()` and lets the readline loop prompt you again. Note that `resumable` only flips to `true` after a turn exits cleanly (exit code `0`): a failed turn may never create the session on disk, so guarding on the exit code keeps the next turn from passing `--continue` against a session that does not exist. Continuity works because Devin persists each session in the sandbox keyed by the directory it ran in, and every turn runs in the same `WORK_DIR`, so the most recent session from this directory is always the previous turn.
:::note[When does output appear?]
Devin's `-p` (print) mode runs the whole task and prints its result at the end rather than emitting a token-by-token stream, so the terminal stays quiet while Devin works and then shows the output at once. Devin exposes no public structured-output format to parse for live progress, so the controller forwards exactly what Devin prints, when it prints it. Expect a longer task to sit silently for a bit before its result lands.
:::
:::tip[For production: `executeSession`]
This guide uses a PTY for every phase to keep the architecture uniform and the on-screen experience the same as running Devin in your own terminal. If you are integrating Devin into an automated pipeline and only need to capture log output (no live TUI rendering), [session-based execution](https://www.daytona.io/docs/en/process-code-execution.md) with polled log streaming is the more API-native path for the `-p` turns. Login and setup still need a PTY because they are interactive.
:::
**Key advantages:**
- The same experience as running Devin in your own terminal, because Devin owns the PTY with no shell wrapping
- Works on any Devin plan, including the free tier (interactive login, no API key required)
- No permission prompts during a task (`--permission-mode dangerous`)
- Multi-turn continuity: `--continue` carries conversation context across turns, so the agent remembers earlier prompts
- One-time onboarding handled explicitly so headless turns never get blocked by the wizard
- All agent code execution happens inside an isolated Daytona sandbox
- Automatic cleanup on exit
# Run AWS Kiro's CLI in a Daytona Sandbox and Stream Its Output
This guide runs [AWS Kiro's CLI](https://kiro.dev/docs/cli/) inside a Daytona sandbox. You log in to Kiro once over a real terminal, then send it prompts and watch its output stream straight back to you. Because Kiro works entirely inside the sandbox, it can edit files, install packages, and run code in an isolated, disposable environment that is thrown away when you finish. The only thing running on your own machine is a small Node.js controller that wires your terminal to the sandbox.
---
### 1. Workflow Overview
When you launch the main module, a Daytona sandbox is created and the Kiro CLI is installed inside it. You then log in once over a sandbox PTY (Kiro's device flow works on any plan, including the free tier), after which each prompt is run headlessly as `kiro-cli chat --no-interactive --trust-all-tools ""`. Turns after the first add `--resume` so the conversation carries context from one prompt to the next.
Every phase that talks to Kiro uses the same trick. Opening a PTY starts a shell in the sandbox; rather than run Kiro as a child of that shell, the controller tells the shell to `exec` Kiro, which makes Kiro take over the shell's process. This buys two things. First, the output you see is exactly what Kiro prints, with no shell prompt or echoed command around it, so it looks the same as running Kiro in your own terminal. Second, because Kiro replaced the shell, the PTY closes the moment Kiro exits, which is how the controller knows a turn has finished and can prompt you again.
You can keep interacting with your agent until you are finished. When you exit the program, the sandbox is deleted automatically.
### 2. Project Setup
#### Clone the Repository
First, clone the daytona [repository](https://github.com/daytona/guides.git) and navigate to the example directory:
```bash
git clone https://github.com/daytona/guides.git
cd guides/typescript/kiro/kiro-cli
```
#### Configure Environment
You need:
- **Daytona API key:** [Daytona Dashboard](https://app.daytona.io/dashboard/keys)
- **Kiro account:** any plan, including the free tier ([Kiro app](https://app.kiro.dev/)). No Kiro API key is required; you log in interactively when the sandbox starts.
Copy `.env.example` to `.env` and add your Daytona key:
```bash
DAYTONA_API_KEY=your_daytona_key
```
:::caution[Credentials live in the sandbox]
During login, Kiro exchanges your browser approval for credentials written to the sandbox's filesystem (`~/.local/share/kiro-cli/data.sqlite3`). The sandbox is disposable and deleted on exit, so the credentials do not persist, but treat the sandbox as you would any environment holding a live credential.
:::
#### Local Usage
:::note[Node.js Version]
Node.js 18 or newer is required to run this example.
:::
Install dependencies:
```bash
npm install
```
Run the agent:
```bash
npm run start
```
The agent will start and wait for your prompt.
### 3. Example Usage
Ask the agent to write and run some code. Here it signs in via device flow on the free tier, then writes a JavaScript program that renders a 3D donut as ASCII art with surface-normal shading, runs it, and streams the rendered output back to your terminal live:
:::note[Trimmed for readability]
The real `kiro-cli chat` output is more verbose - it also prints the full contents of any files the agent writes (with line numbers and diff markers) and per-step tool-call metadata. The transcript below is trimmed to the agent's responses and the program output.
:::
```
$ npm run start
Creating sandbox...
Installing Kiro CLI...
Starting Kiro CLI...
Log in to Kiro to continue (any plan works, including the free tier).
Pick a provider, open the URL that appears below, and approve the sign-in in your browser.
? Select login method ›
❯ Use with Builder ID
Use with Google
Use with GitHub
Use with Your Organization
Confirm the following code in the browser
Code: ABCD-EFGH
Open this URL: https://view.awsapps.com/start/#/device?user_code=ABCD-EFGH
Logging in... done
Agent ready. Press Ctrl+C at any time to exit.
User: Write donut.js that renders a 3D donut/torus as static ASCII art (roughly 80×24), with shading by surface normals against a fixed light direction. Use the characters .,-~:;=!*#$@ from darkest to brightest. Then run it with node donut.js and show the output.
I'll create the following file: /home/daytona/donut.js (using tool: write)
Creating: /home/daytona/donut.js
- Completed in 0.0s
I will run the following command: node donut.js (using tool: shell)
$$$$$$$$$$$
$$$$$####*****####$$$$$
$$$$$##**!=*!!!!!*=!**##$$$$$
!$$$$$$#**!!===;;:;;===!!**#$$$$$$
*$$$$$$##*=;;:--,...,--:;;=*##$$$$$$*
*#$$$$$$##*!;-,.... ....,~;!*##$$$$$$#*
;*##$$$$$$##*=:. .:=*##$$$$$$##*;
;*##$$$$$$$$##*! !*##$$$$$$$###*;
:!*##$$$$$$$$$$$$#######$$$$$$$$$$$$##*!:
~=!**###$$$$$$$$$$$$$$$$$$$$$$$$$###**!=:
:!!!**####$$$$$$$$$$$$$$$$$$$####**!=!;
~==!!!***######$$$$$$$######***!!!==:
.:;=!!*!******************!!!!!=;:,
.~:;;=!=!***********!!==!=;;:~,
.,~::;;;;=======;;;;:~~-
.,---------,.
- Completed in 0.32s
The shading uses .,-~:;=!*#$@ (dark→bright) based on the dot product of each surface normal with the light direction (0, 1, -1). The $ and # characters on the lit upper-left face the light most directly, while . and , appear on the darker lower portions.
User:
```
### 4. Understanding the Script
This example consists of two parts: a main program (`src/index.ts`) that manages the sandbox and a command-line loop, and a session class (`src/session.ts`) that drives each Kiro invocation over its own PTY.
#### Initialization
On startup, the script:
1. Creates a new [Daytona sandbox](https://www.daytona.io/docs/en/sandboxes.md).
2. Installs the Kiro CLI in the sandbox and confirms the binary with `kiro-cli --version`.
3. Logs you in via Kiro's device flow over a fresh PTY.
4. Enters a readline loop where each prompt is a headless `kiro-cli chat --no-interactive` turn in its own PTY.
5. On Ctrl+C, restores stdin, deletes the sandbox, and exits.
#### Creating the Sandbox
The installer always drops the binary at `$HOME/.local/bin/kiro-cli`, but that directory is not always on the sandbox shell's `PATH`, and the installer's exit code is not a reliable success signal. So the script confirms the install by running the binary directly with `"$HOME/.local/bin/kiro-cli" --version`. It uses the full path rather than a bare `kiro-cli` because whether `~/.local/bin` is on `PATH` varies between shell types and sandbox configurations, so a full path works regardless. The install's combined stdout and stderr is surfaced on failure for diagnostics:
```ts
sandbox = await daytona.create()
const install = await sandbox.process.executeCommand(
'curl -fsSL https://cli.kiro.dev/install | bash 2>&1',
)
const version = await sandbox.process.executeCommand('"$HOME/.local/bin/kiro-cli" --version')
if (version.exitCode !== 0) {
throw new Error(
'Kiro CLI did not install correctly.\n' +
`Install output:\n${install.result}\n` +
`Version check output:\n${version.result}`,
)
}
```
#### Per-invocation PTY with `exec`
Every phase that talks to Kiro uses the same primitive: open a fresh PTY in the sandbox, then have its shell `exec` the Kiro command. The `exec` is essential rather than a detail. It makes Kiro replace the shell process instead of running underneath it, so there is no shell prompt or echoed command wrapping Kiro's output, and the PTY closes the moment Kiro exits, which is how the controller detects the turn finished:
```ts
private async attach(command: string, interactive: boolean): Promise {
// Every phase opens a fresh PTY, so reset the per-invocation stream state first: a clean
// decoder, and passthrough/launchBuffer back to their pre-marker state so this turn's
// launch-line filtering never inherits leftover state from the previous turn.
this.decoder = new TextDecoder('utf-8')
this.passthrough = false
this.launchBuffer = ''
const pty = await this.sandbox.process.createPty({
id: `kiro-pty-${Date.now()}`,
cols: process.stdout.columns || 120,
rows: process.stdout.rows || 30,
onData: (data: Uint8Array) => this.forward(data),
})
await pty.waitForConnection()
await pty.sendInput(`cd ${WORK_DIR}; printf '\\n%s\\n' '${READY}'; exec ${command}\n`)
const stdin = process.stdin
const onStdin = (chunk: Buffer) => void pty.sendInput(chunk)
if (interactive) {
while (stdin.read() !== null) { /* drain buffered bytes from the prior step */ }
if (stdin.isTTY) stdin.setRawMode(true)
stdin.resume()
stdin.on('data', onStdin)
}
try {
const result = await pty.wait()
return result.exitCode
} finally {
if (interactive) {
stdin.removeListener('data', onStdin)
if (stdin.isTTY) stdin.setRawMode(false)
stdin.pause()
}
await pty.disconnect()
}
}
```
That single launch line (`cd` to the workspace, print a readiness marker, then `exec`) is the only shell command the PTY ever runs. After `exec`, Kiro owns the terminal.
For the interactive login, the controller bridges your local keyboard into the PTY in four steps:
1. **Drain stale input** with `while (stdin.read() !== null) {}`. Any bytes left buffered from a previous step are discarded so they are not accidentally fed into this command.
2. **Switch the terminal to raw mode** with `setRawMode(true)`. Normally the terminal collects a whole line at a time and handles editing and echo locally. Raw mode turns that off, so each keystroke is delivered immediately and is not printed twice (once by your local terminal, once by Kiro echoing it back). This is what makes Kiro's arrow-key provider selector work.
3. **Resume stdin** with `stdin.resume()`. Node keeps a stdin stream paused until something listens to it, so resuming is what actually starts the bytes flowing.
4. **Register the forwarder** with `stdin.on('data', ...)`, which ships every chunk you type straight into the sandbox PTY where Kiro reads it.
Headless turns (`--no-interactive`) skip all of this. Kiro reads its prompt from the command arguments, so it needs no keyboard input.
#### Hiding the launch line
Before `exec` runs, the sandbox shell prints the launch command back on its own stdout. This is the same behavior any interactive shell has: it echoes the command it has just received. The sandbox PTY's stdout is what we receive over `onData`, so those bytes flow back to us alongside Kiro's real output. To keep the screen clean and show only what Kiro prints, the data handler buffers PTY output until it sees the readiness marker, then forwards every subsequent byte untouched:
```ts
private forward(data: Uint8Array): void {
const text = this.decoder.decode(data, { stream: true })
if (this.passthrough) {
process.stdout.write(text)
return
}
this.launchBuffer += text
const m = READY_RE.exec(this.launchBuffer)
if (m) {
const rest = this.launchBuffer.slice(m.index + m[0].length)
this.passthrough = true
this.launchBuffer = ''
if (rest) process.stdout.write(rest)
} else if (this.launchBuffer.length > 8192) {
this.launchBuffer = this.launchBuffer.slice(-READY.length - 2)
}
}
```
The marker text ends up in the stream **twice**: once inside the echoed command (wrapped in single quotes: `printf '\n%s\n' '__DAYTONA_KIRO_READY__'; exec …`), and once as the actual `printf` output (on its own line surrounded by newlines). The regex `(^|[\r\n])__DAYTONA_KIRO_READY__[\r\n]` locks onto the real one by requiring a line break (or buffer start) immediately before *and* after the marker - the echoed copy has quotes on both sides, so it never matches. The character class is `[\r\n]` rather than `\n` because a PTY rewrites every `\n` as `\r\n` on the way out. The marker can also be split across two reads, so the handler simply keeps appending and re-runs the regex after every chunk.
The `else if` is a safety cap so the buffer cannot grow unbounded if the marker never arrives. The `8192` value is picked arbitrarily - far above the few hundred bytes of shell-echo we actually receive, so it never fires in practice, while still bounding memory if something goes wrong upstream. When the cap does fire, the buffer is trimmed but the last `READY.length + 2` bytes are kept rather than thrown out completely (`slice(-READY.length - 2)`). That keep-window is the maximum possible length of a regex match - one leading `\r` or `\n`, plus the marker, plus one trailing `\r` or `\n` - so any partial marker that happens to be sitting at the buffer's tail when the trim runs is preserved intact. For example, if the buffer ends with `\r\n__DAYTONA_KIRO_READY__` and is waiting for the closing `\r\n` from the next chunk, keeping the last `READY.length + 2` bytes ensures the partial is still there when the next chunk arrives so the regex can complete the match.
#### Logging in
Kiro's default login opens a browser on the machine the CLI runs on. A sandbox has no browser, so the session uses `--use-device-flow` instead: Kiro shows a provider selector, then prints a URL and a one-time code and polls AWS while you approve the sign-in in your own browser. The interactive stdin bridge is what makes the provider selection work; the poll afterward needs no input. After the command exits, `kiro-cli whoami` is the source of truth for whether the login actually succeeded:
```ts
async login(): Promise {
await this.attach(`${KIRO} login --use-device-flow`, true)
const status = await this.sandbox.process.executeCommand(`${KIRO} whoami`)
if (status.exitCode !== 0) {
throw new Error(
'Kiro login did not complete. Re-run and approve the sign-in when prompted.\n' +
`kiro-cli whoami (exit ${status.exitCode}):\n${status.result}`,
)
}
}
```
Kiro persists the session inside the sandbox (`~/.local/share/kiro-cli/data.sqlite3`), so this is a one-time login: every later headless turn reuses it with no further prompts.
#### Running a turn (with conversation continuity)
Each prompt is a one-shot, non-interactive Kiro invocation. `--no-interactive` prints the response and exits, and `--trust-all-tools` auto-approves tool calls so the run never blocks on a permission prompt. The first turn starts a fresh conversation; every turn after it adds `--resume`, which continues the most recent conversation from the working directory - so context carries from one prompt to the next:
```ts
async processPrompt(prompt: string): Promise {
const resume = this.resumable ? '--resume ' : ''
const exitCode = await this.attach(
`${KIRO} chat --no-interactive --trust-all-tools ${resume}${this.shellQuote(prompt)}`,
false,
)
// Only become resumable after a turn succeeds; a failed first turn creates no conversation.
if (exitCode === 0) this.resumable = true
process.stdout.write('\n')
}
```
There is no stdin bridge and no raw mode here; the controller simply streams Kiro's output as it works. The turn ends when Kiro exits, which resolves `pty.wait()` and lets the readline loop prompt you again. Note that `resumable` only flips to `true` after a turn exits cleanly (exit code `0`): a failed turn may never create the conversation on disk, so guarding on the exit code keeps the next turn from passing `--resume` against a conversation that does not exist. Continuity works because Kiro persists each conversation in the sandbox keyed by the directory it ran in, and every turn runs in the same `WORK_DIR`, so "the most recent conversation from this directory" is always the previous turn. (Tell it your name on one turn, ask for it back on the next, and it remembers.)
:::tip[For production: `executeSession`]
This guide uses a PTY for every phase to keep the architecture uniform and the on-screen experience the same as running Kiro in your own terminal. If you are integrating Kiro into an automated pipeline and only need to capture log output (no live TUI rendering), [session-based execution](https://www.daytona.io/docs/en/process-code-execution.md) with polled log streaming is the more API-native path for the `--no-interactive` turns. Login still needs a PTY because it is interactive.
:::
**Key advantages:**
- The same experience as running Kiro in your own terminal, because Kiro owns the PTY with no shell wrapping
- Works on any Kiro plan, including the free tier (interactive device-flow login, no API key required)
- No permission prompts during a task (`--no-interactive --trust-all-tools`)
- Multi-turn continuity: `--resume` carries conversation context across turns, so the agent remembers earlier prompts
- One-time login per sandbox; later headless turns reuse the stored session
- All agent code execution happens inside an isolated Daytona sandbox
- Automatic cleanup on exit
# OpenClaw Guides
Guides for running OpenClaw with Daytona.
# Run OpenClaw in a Daytona Sandbox via CLI
This guide walks you through setting up [OpenClaw](https://openclaw.ai/) inside a Daytona sandbox and configuring Telegram and WhatsApp channels.
Running OpenClaw in a Daytona sandbox keeps your AI assistant isolated from your local machine, provides a secure environment for code execution, and ensures your bot stays online 24/7 without tying up your personal computer.
### Prerequisites
- Daytona account and API key (Get it from [Daytona Dashboard](https://app.daytona.io/dashboard/keys))
- Local terminal (macOS, Linux, or Windows)
### Install the Daytona CLI
```bash
brew install daytonaio/cli/daytona
```
```bash
powershell -Command "irm https://get.daytona.io/windows | iex"
```
:::note
Already have the CLI? Check your version with `daytona --version`. If it's below **0.135.0**, [upgrade to the latest version](https://www.daytona.io/docs/en/tools/cli.md).
:::
### Authenticate with Daytona
Log in to your Daytona account using your API key:
```bash
daytona login --api-key=YOUR_API_KEY
```
Replace `YOUR_API_KEY` with your actual Daytona API key.
### Create a Sandbox
Create a sandbox for running OpenClaw:
```bash
daytona sandbox create --name openclaw --snapshot daytona-medium --auto-stop 0
```
OpenClaw comes preinstalled in the default Daytona snapshot, so the command above is all you need.
:::note
The `--auto-stop 0` flag disables automatic shutdown, keeping OpenClaw accessible until you manually stop or delete the sandbox. The `daytona-medium` snapshot is required because the OpenClaw gateway needs a minimum of 2GB memory.
:::
### Connect to the Sandbox
SSH into your sandbox:
```bash
daytona ssh openclaw
```
### Run OpenClaw Onboarding
Start the onboarding process:
```bash
openclaw onboard
```
:::note
The model provider steps below are for Anthropic. If using a different provider, follow the prompts for your chosen option.
:::
Follow the prompts:
1. **Security acknowledgment:** Accept to continue
2. **Onboarding mode:** Select **Quickstart**
3. **Model/auth provider:** Select **Anthropic**
4. **Anthropic auth method:** Select **Anthropic API key**
5. **Enter Anthropic API key:** Paste your API key
6. **Default model:** Keep current (default: `anthropic/claude-opus-4-5`)
7. **Select channel:** Choose **Skip for now** (we'll configure channels later)
8. **Configure skills:** Select **No** (configure later based on your needs)
9. **Enable hooks:** Select **Skip for now** (configure later based on your needs)
10. **Gateway service:** Select **Skip** (already installed)
When onboarding finishes, the output will display a **Dashboard ready** section with a dashboard link. Your gateway token is the value after `?token=` in the URL. Save this token - you'll need it to connect to the dashboard.
Also, OpenClaw will ask you to **Install shell completion script?** - choose whatever you prefer, this is optional and doesn't affect functionality.
### Start the Gateway
Run the gateway in the background:
```bash
nohup openclaw gateway run > /tmp/gateway.log 2>&1 &
```
The `&` runs the gateway as a background process, keeping your terminal free for other commands. The `nohup` ensures the gateway keeps running even after you close the SSH connection.
### Access the Dashboard
The OpenClaw dashboard is a web interface for managing your assistant, monitoring connections, and configuring channels. To access it, you need a [preview URL](https://www.daytona.io/docs/en/preview.md) that exposes the gateway port running inside your sandbox.
In your local terminal (not inside the sandbox SSH session), generate the preview URL:
```bash
daytona preview-url openclaw --port 18789
```
This command generates a [signed preview URL](https://www.daytona.io/docs/en/preview.md#signed-preview-url) that securely exposes the port.
Open the URL in your browser, go to the **Overview** section, paste your gateway token in the **Gateway Token** field, and click **Connect**.
:::tip
The preview URL expires after 1 hour by default (customizable with `--expires` flag). When it expires, simply run the same CLI command to generate a new one.
:::
### Pair Your Browser
OpenClaw uses device pairing as a security measure - only approved devices can connect to and control your assistant. When you first attempt to connect from the dashboard, your browser registers as a new device that needs approval.
List pending device requests:
```bash
openclaw devices list
```
Approve your device:
```bash
openclaw devices approve REQUEST_ID
```
Replace `REQUEST_ID` with the value from the **Request** column.
Click **Connect** again in the dashboard.
Once connected, you should see a green status indicator - your OpenClaw is now ready to use.
### Security
Running OpenClaw this way provides three layers of security:
1. **Preview URL:** Time-limited access to the dashboard port
2. **Gateway token:** Required to authenticate with the dashboard
3. **Device approval:** Only approved devices can connect and control your assistant
Even if someone obtains your dashboard URL, they cannot connect without the gateway token and an approved device.
:::caution
Keep your gateway token and preview URL secret. Do not share them publicly.
:::
---
### Configure Telegram
Set up a Telegram bot to chat with OpenClaw.
#### Create a Telegram Bot
1. Open Telegram and search for **@BotFather**
2. Send `/start`, then `/newbot`
3. Enter a name for your bot
4. Enter a username for your bot
5. Copy the bot token provided
#### Configure OpenClaw
Enable Telegram and set your bot token:
```bash
openclaw config set channels.telegram.enabled true
openclaw config set channels.telegram.botToken YOUR_BOT_TOKEN
```
Verify the configuration:
```bash
openclaw config get channels.telegram
```
#### Restart the Gateway
```bash
openclaw gateway stop
nohup openclaw gateway run > /tmp/gateway.log 2>&1 &
```
#### Complete Verification
1. Open your bot's chat in Telegram and click **Start**
2. A pairing code will appear. Approve the pairing request:
```bash
openclaw pairing approve telegram PAIRING_CODE
```
You can now message your OpenClaw through Telegram.
---
### Configure WhatsApp
Set up WhatsApp to chat with OpenClaw.
#### Run Configuration
```bash
openclaw config --section channels
```
When prompted:
1. Select **Local (this machine)** for gateway location
2. Choose **Configure/link**
3. Select **WhatsApp (QR link)**
4. Select **Yes** for "Link WhatsApp now (QR)?"
#### Scan the QR Code
Open WhatsApp on your phone, go to **Settings → Linked Devices → Link a Device**, and scan the QR code displayed in your terminal.
Once paired, you'll see:
```
✅ Linked after restart; web session ready.
```
#### Set Up Your Phone Number
Select **This is my personal phone number** (or choose the other option if you have a separate phone for OpenClaw) and enter your phone number when prompted.
#### Finish Configuration
When prompted to select another channel, choose **Finished**. You'll see:
```
└ Configure complete.
```
#### Start Chatting
Send a message to yourself in WhatsApp - OpenClaw will respond. You can give it instructions and information on how to behave directly in the chat.
:::tip
To allow other users to chat with OpenClaw, add their phone numbers to the **Allow From** list in **Channels → WhatsApp** inside the dashboard. When they send a message, OpenClaw will respond.
:::
# Run OpenClaw in a Daytona Sandbox via SDK
import { Image } from 'astro:assets'
import openclawSandbox from '../../../../../assets/docs/images/openclaw-sandbox.gif'
This guide shows how to run [OpenClaw](https://openclaw.ai/) inside a Daytona sandbox using the Daytona SDK. The script automatically creates and configures a sandbox with OpenClaw and provides an authenticated [preview URL](https://www.daytona.io/docs/en/preview.md) for using OpenClaw in the browser.
---
### 1. Workflow Overview
When you run the script, it creates a Daytona sandbox, starts the OpenClaw gateway inside it, and prints a preview link for the dashboard:
```
$ npm start
Creating Daytona sandbox...
Configuring OpenClaw...
Starting OpenClaw...
(Ctrl+C to shut down and delete the sandbox)
🔗 Secret link to Control UI: https://18789-xxxx.proxy.daytona.works?token=...
```
Open the provided link in your browser to connect to the OpenClaw Control UI. This link contains a configuration token, and anyone can use it to connect to OpenClaw without device approval.
You can use the Control UI to chat with your assistant, configure Telegram and WhatsApp, and manage sessions. When you exit the script (Ctrl+C), the sandbox will not be deleted unless [sandbox persistence is disabled](https://www.daytona.io/docs/en/guides/openclaw/openclaw-sdk-sandbox.md#4-key-constants).
### 2. Project Setup
#### Clone the Repository
Clone the Daytona [repository](https://github.com/daytona/guides) and go to the example directory:
```bash
git clone https://github.com/daytona/guides.git
cd guides/typescript/openclaw
```
#### Configure Environment
Get your API key from the [Daytona Dashboard](https://app.daytona.io/dashboard/keys).
Copy `.env.example` to `.env` and add your Daytona API key:
```bash
DAYTONA_API_KEY=your_daytona_key
```
A default OpenClaw configuration is stored in `openclaw.json`. You can customize it according to the [configuration reference](https://docs.openclaw.ai/gateway/configuration-reference). You can also add additional environment variables to `.env.sandbox` (e.g. `ANTHROPIC_API_KEY` for Claude) and they will be loaded into the sandbox.
#### Run the Example
:::note[Node.js]
Node.js 18 or newer is required.
:::
Install dependencies and run:
```bash
npm install
npm start
```
The script creates the sandbox, starts the OpenClaw gateway, and prints a secret link with the token in the URL.
### 3. How It Works
1. The script creates a [Daytona sandbox](https://www.daytona.io/docs/en/sandboxes.md) with `DAYTONA_SNAPSHOT` (e.g. `daytona-medium`) and loads env vars from `.env.sandbox`.
2. Your local `openclaw.json` is merged with built-in config and written to `~/.openclaw/openclaw.json` in the sandbox.
3. The OpenClaw gateway is started inside the sandbox on `OPENCLAW_PORT` via [process execution](https://www.daytona.io/docs/en/process-code-execution.md).
4. A signed [preview link](https://www.daytona.io/docs/en/preview.md) is generated and the token is appended as `?token=...`; this link is printed so you can open the Control UI.
5. On Ctrl+C, the sandbox is deleted unless `PERSIST_SANDBOX` is `true`.
### 4. Key Constants
You can change behavior by editing the constants in [`src/index.ts`](https://github.com/daytona/guides/blob/main/typescript/openclaw/src/index.ts):
| Constant | Default | Description |
|----------|---------|-------------|
| `PERSIST_SANDBOX` | true | When true, the sandbox is not deleted when the script exits |
| `MAKE_PUBLIC` | true | Allow anyone to access the sandbox instead of limiting to your Daytona organization |
| `OPENCLAW_PORT` | 18789 | OpenClaw Gateway and Control UI port |
| `SHOW_LOGS` | true | Stream OpenClaw stdout/stderr to the terminal |
**Key advantages:**
- Secure, isolated execution in a Daytona sandbox
- No device approval — token in URL and `allowInsecureAuth` skip pairing
- Control UI and channels accessible via the secret preview link
- Optional: keep the sandbox running after exit (`PERSIST_SANDBOX`)
# Build a Multi-Language Benchmark Agent with the Vercel AI SDK and Daytona
import { Image } from 'astro:assets'
import sieveBenchmarkPlot from '../../../../../assets/docs/images/vercel-ai-sdk-sieve-benchmark.png'
This guide walks through building a multi-language benchmarking agent with the [Vercel AI SDK](https://ai-sdk.dev/) and [Daytona](https://www.daytona.io/docs/en/sandboxes.md) sandboxes. The agent has five tools (`runCode`, `runCommand`, `writeFile`, `readFile`, `downloadFile`) and a concrete benchmarking task: implement the Sieve of Eratosthenes in both Python and TypeScript, time them across input sizes, render a matplotlib comparison chart, and deliver two tangible artifacts to your local disk: the chart as a PNG and the findings as a markdown report.
This is the shape of a real coding agent (multi-step, multi-language, multi-tool) producing tangible deliverables rather than just text, running entirely inside Daytona's isolated environment.
---
### 1. Workflow Overview
When you run the script, the agent receives a Sieve of Eratosthenes benchmark task, implements it in two languages, plots the result, and pulls the PNG to your local disk. The transcript below is **excerpted from an actual run** using `claude-sonnet-4-6` against a real Daytona sandbox. Source-code arguments inside `runCode` and `writeFile` calls are abridged (marked by `...`); tool-call shapes, results, file sizes, and the final answer are verbatim:
```
$ npm run start
Creating sandbox...
Prompt:
Implement the Sieve of Eratosthenes (find all primes up to N) in both Python and TypeScript.
Benchmark each across N = 1_000, 10_000, 100_000, and 1_000_000.
Produce two artifacts on my local disk: ./sieve_benchmark.png with a chart
comparing the languages, and ./findings.md with a markdown summary of your
findings.
Keep your final answer concise; the full report is in the downloaded files.
Running agent...
--- Step 1: runCode ---
{ "code": "# Python Sieve of Eratosthenes benchmark ...", "language": "python" }
--- Step 1: result ---
{"exitCode":0,"output":"N= 1,000: median=0.0040 ms\nN= 10,000: median=0.0170 ms\nN= 100,000: median=0.1329 ms\nN=1,000,000: median=1.5081 ms\n"}
--- Step 2: runCode ---
{ "code": "// TypeScript Sieve of Eratosthenes benchmark ...", "language": "typescript" }
--- Step 2: result ---
{"exitCode":0,"output":"N= 1000: median=0.0012 ms\nN= 10000: median=0.0059 ms\nN= 100000: median=0.0786 ms\nN= 1000000: median=1.0883 ms\n"}
--- Step 3: runCode ---
{ "code": "import matplotlib ... plt.savefig('/tmp/sieve_benchmark.png')", "language": "python" }
--- Step 3: result ---
{"exitCode":0,"output":"saved\n"}
--- Step 4: writeFile ---
{ "path": "/tmp/findings.md", "content": "# Sieve of Eratosthenes - Benchmark Findings ..." }
--- Step 4: result ---
{"path":"/tmp/findings.md","bytes":2546}
--- Step 5: downloadFile --- # ← parallel: chart → local disk
{ "remotePath": "/tmp/sieve_benchmark.png", "localPath": "./sieve_benchmark.png" }
--- Step 5: downloadFile --- # ← parallel: findings → local disk
{ "remotePath": "/tmp/findings.md", "localPath": "./findings.md" }
--- Step 5: result ---
{"localPath":"./sieve_benchmark.png","bytes":116848}
--- Step 5: result ---
{"localPath":"./findings.md","bytes":2546}
--- Step 6: text ---
Both files are on your disk. Here's the concise summary:
## Results at a glance
| N | Python (ms) | TypeScript (ms) | TS speed-up |
|----------:|------------:|----------------:|---------------------|
| 1,000 | 0.0040 | 0.0012 | **3.3x** |
| 10,000 | 0.0170 | 0.0059 | **2.9x** |
| 100,000 | 0.1329 | 0.0786 | **1.7x** |
| 1,000,000 | 1.5081 | 1.0883 | **1.4x** |
TypeScript/V8 wins everywhere, but the advantage shrinks from ~3x to ~1.4x
as N grows. At N = 1M both implementations hit the same RAM-bandwidth wall,
erasing most of V8's JIT advantage. Python holds its own because the inner
marking loop is replaced by a C-level bytearray slice assignment; a naive
inner loop would be 10-50x slower. Both scale as O(N log log N).
(full report in ./findings.md)
Cleaning up sandbox...
```
After the run completes, you'll find **two real files** in your project directory:
- `sieve_benchmark.png` (~117 KB): the matplotlib chart comparing both languages
- `findings.md` (~2.5 KB): the structured benchmark report (methodology + raw numbers + conclusion)
The chart the agent drew on that run looks like this:
Worth noticing in the transcript:
- **Each "Step N" label is one agent turn, not one tool call.** Within a turn the model can emit any combination of text and tool calls. Step 6 is the only text-only turn (the final answer), so it gets a clean `--- Step 6: text ---` block. The other turns each have a single tool call.
- **Step 5 batches parallel tool calls.** Both `downloadFile` calls go out in the same turn and the Vercel AI SDK runs them concurrently, so the chart and the report ship to local disk at the same time rather than back-to-back.
- **Steps 1, 2, 3 all use `runCode`** to execute Python, TypeScript, and Python (matplotlib) in turn. No `runCommand` was needed for the TypeScript step because `runCode` handles `ts-node` invocation internally. The exact step count and tool mix will vary run to run; some runs may use `runCommand` if the agent decides to `pip install` an extra package or inspect the filesystem before plotting.
### 2. Project Setup
#### Clone the Repository
Clone the Daytona [repository](https://github.com/daytona/guides) and navigate to the example directory:
```bash
git clone https://github.com/daytona/guides.git
cd guides/typescript/vercel-ai-sdk/multi-language-benchmark-agent
```
#### Configure Environment
Get your API keys:
- **Daytona API key:** [Daytona Dashboard](https://app.daytona.io/dashboard/keys)
- **Anthropic API key:** [Anthropic Console](https://console.anthropic.com/)
Copy `.env.example` to `.env` and add your keys:
```bash
DAYTONA_API_KEY=your_daytona_key
ANTHROPIC_API_KEY=your_anthropic_key
```
#### Local Usage
:::note[Node.js Version]
Node.js 18 or newer is required to run this example.
:::
Install dependencies:
```bash
npm install
```
Run the agent:
```bash
npm run start
```
### 3. How It Works
The example consists of a single Node.js script (`src/index.ts`) that runs on your local machine. It uses the Daytona SDK to manage a sandbox and the Vercel AI SDK's `ToolLoopAgent` to drive the multi-step tool-calling loop.
#### Defining the Five Tools
Each tool wraps a single Daytona primitive and has a single, non-overlapping job. The agent never has to choose between two paths for the same goal.
```typescript
const runCode = tool({
description:
'Execute Python, JavaScript, or TypeScript source code in the sandbox. Pass standalone code; no project setup is required for any language. Use print() / console.log() to surface values. Returns the exit code and combined stdout/stderr.',
inputSchema: z.object({
code: z.string().describe('Source code to execute.'),
language: z.enum(['python', 'javascript', 'typescript']).describe('Language of the code.'),
}),
execute: async ({ code, language }) => {
const ext = { python: 'py', javascript: 'js', typescript: 'ts' }[language]
const path = `/tmp/_run_${randomUUID()}.${ext}`
await sandbox!.fs.uploadFile(Buffer.from(code, 'utf-8'), path)
const cmd = {
python: `python3 ${path}`,
javascript: `node ${path}`,
typescript:
`ts-node --transpile-only --skipProject ` +
`--compilerOptions '{"module":"commonjs","moduleResolution":"node"}' ` +
path,
}[language]
const r = await sandbox!.process.executeCommand(cmd)
return { exitCode: r.exitCode, output: r.result }
},
})
const runCommand = tool({
description: 'Execute a bash shell command in the sandbox. Use for installing packages (pip, npm), running shell utilities (ls, head, wc, find), or chaining commands with pipes.',
inputSchema: z.object({
command: z.string().describe('Shell command to execute.'),
}),
execute: async ({ command }) => {
const r = await sandbox!.process.executeCommand(command)
return { exitCode: r.exitCode, output: r.result }
},
})
const writeFile = tool({
description: 'Write text content to a file in the sandbox filesystem. Use for non-code artifacts: markdown reports, JSON data, config files. Overwrites any existing file at the path.',
inputSchema: z.object({
path: z.string().describe('Absolute path of the file to write.'),
content: z.string().describe('UTF-8 text content to write.'),
}),
execute: async ({ path, content }) => {
await sandbox!.fs.uploadFile(Buffer.from(content, 'utf-8'), path)
return { path, bytes: Buffer.byteLength(content, 'utf-8') }
},
})
const readFile = tool({
description: 'Read a file from the sandbox filesystem and return its contents as text.',
inputSchema: z.object({
path: z.string().describe('Absolute path of the file to read.'),
}),
execute: async ({ path }) => {
const buf = await sandbox!.fs.downloadFile(path)
return { content: buf.toString('utf-8') }
},
})
const downloadFile = tool({
description: 'Download a file from the sandbox to the local filesystem (where this script runs). Use this to extract generated artifacts (plots, datasets, reports) so they remain available after the sandbox is destroyed.',
inputSchema: z.object({
remotePath: z.string().describe('Absolute path of the file in the sandbox.'),
localPath: z.string().describe('Path on the local filesystem to write the downloaded bytes to.'),
}),
execute: async ({ remotePath, localPath }) => {
const buf = await sandbox!.fs.downloadFile(remotePath)
writeFileSync(localPath, buf)
return { localPath, bytes: buf.length }
},
})
```
The tools split the agent's surface into three clean categories:
| Category | Tools | When to use |
|---|---|---|
| **Code execution** | `runCode` | The agent wrote application code - benchmark, algorithm, plotting script. Picks Python/JavaScript/TypeScript per call. |
| **Shell utilities** | `runCommand` | Operating the sandbox itself - `pip install`, `npm install`, `ls`, `wc`. Never used to run scripts the agent wrote. |
| **Filesystem** | `writeFile`, `readFile`, `downloadFile` | Save text artifacts to the sandbox, read them back, extract any file (binary or text) to local disk. |
#### Defining the Agent
The `ToolLoopAgent` is constructed once with the model, `instructions` (the agent's persona and methodology), the tools, and a `stopWhen` condition. The same instance can later be invoked via `.stream()` (used here for live progress output) or `.generate()` (which waits and returns the whole result at the end):
```typescript
const MODEL = anthropic('claude-sonnet-4-6')
const agent = new ToolLoopAgent({
model: MODEL,
instructions: INSTRUCTIONS,
tools: { runCode, runCommand, writeFile, readFile, downloadFile },
stopWhen: stepCountIs(25),
})
const stream = await agent.stream({ prompt: BENCHMARK_PROMPT })
```
The benefit over inline `generateText({ tools, prompt, ... })` shows up when this same agent is later exposed from an HTTP route or shared between a CLI and a UI: the configuration lives in one place, and `createAgentUIStreamResponse({ agent, uiMessages })` plugs it directly into a Next.js route handler that returns a streaming `Response`.
#### Streaming the Agent's Progress
`agent.stream(...)` returns a result whose `fullStream` is an async iterable of typed events: every tool call, every tool result, every text chunk, and any errors surface as they happen. The example iterates that stream with a `for await` loop and a `switch` on `part.type` to print each event with a `Step N` label that matches what you saw in the transcript above:
```typescript
let stepNum = 0
for await (const part of stream.fullStream) {
switch (part.type) {
case 'start-step':
stepNum++
break
case 'tool-call': {
const input = JSON.stringify(part.input, null, 2)
const preview = input.length > 600 ? input.slice(0, 600) + '\n... (truncated)' : input
console.log(`\n--- Step ${stepNum}: ${part.toolName} ---\n${preview}`)
break
}
case 'tool-result': {
const out = JSON.stringify(part.output)
const preview = out.length > 400 ? out.slice(0, 400) + '... (truncated)' : out
console.log(`--- Step ${stepNum}: result ---\n${preview}`)
break
}
case 'tool-error': {
const message = part.error instanceof Error ? part.error.message : JSON.stringify(part.error)
console.error(`\n--- Step ${stepNum}: ${part.toolName} ERROR ---\n${message}`)
break
}
case 'text-start':
console.log(`\n--- Step ${stepNum}: text ---`)
break
case 'text-delta':
// Text streams in token-sized chunks. Use process.stdout.write so the
// chunks concatenate on the same line as a continuous string, rather
// than each chunk landing on its own line (which is what console.log
// would do because it appends a newline).
process.stdout.write(part.text)
break
case 'text-end':
process.stdout.write('\n')
break
case 'error':
console.error(`\n--- STREAM ERROR ---\n${part.error}`)
break
}
}
```
Each case corresponds to a distinct kind of progress event from the agent loop:
- `start-step` fires at the beginning of each agent turn (one round of model generation), so we use it to bump the visible step counter.
- `tool-call` and `tool-result` are the model's tool invocations and the values your tool's `execute` returned. Each is JSON-stringified for the log; very long inputs and outputs are truncated so the console stays readable.
- `text-start` / `text-delta` / `text-end` bracket every text emission, whether it is mid-run commentary or the final answer. The `delta` events are token-sized chunks, which is why we write them with `process.stdout.write` to keep them on one line.
- `tool-error` and `error` surface tool-execution failures and stream-level failures so a silent crash does not look like a stuck run.
#### Clean Up
When the script exits (whether normally or on error), the Daytona sandbox is deleted in a `finally` block. The downloaded `sieve_benchmark.png` and `findings.md` remain on your local disk. Those are the artifacts the agent produced for you to inspect after the run.
### 4. Switching Providers
The Vercel AI SDK is provider-agnostic. To swap Anthropic for OpenAI, install [`@ai-sdk/openai`](https://www.npmjs.com/package/@ai-sdk/openai) and change two lines:
```typescript
import { openai } from '@ai-sdk/openai'
// ...
const MODEL = openai('gpt-4o')
```
Set `OPENAI_API_KEY` in `.env` instead of `ANTHROPIC_API_KEY`. The default is Anthropic Claude Sonnet 4.6, but any AI SDK provider that supports tool calling works.
**Key advantages:**
- Secure, isolated execution: LLM-generated code, package installs, and shell commands never touch your machine.
- Multi-language in one sandbox: the same isolated environment runs Python, Node.js, and TypeScript. Switch languages per `runCode` call.
- Real artifact delivery: the agent's output is not just text. A real PNG chart and a markdown report land on your local disk.
- Reusable agent: the `ToolLoopAgent` instance can be invoked from a CLI, a streaming chat UI, or a background job without re-defining configuration.
# Build a Coding Agent Using Codex SDK and Daytona
import { Image } from 'astro:assets'
import codexSdkLunarLanderResult from '../../../../../assets/docs/images/codex-sdk-lunar-lander-result.gif'
This guide demonstrates how to run an autonomous coding agent based on [OpenAI Codex](https://chatgpt.com/features/codex) inside a Daytona sandbox environment. The agent can develop full-stack web apps, write code in any language, install dependencies, and run scripts. It can also start and manage dev servers, and generate preview links for live apps.
---
### 1. Workflow Overview
When you launch the main module, a Daytona sandbox is created and a Node.js agent is initialized inside it. The agent is based on the [Codex SDK](https://developers.openai.com/codex/sdk/).
You interact with the main program via a command line chat interface. The program sends your prompts to the agent inside the sandbox, which executes them and returns the results:
```
$ npm run start
Creating sandbox...
Installing Codex agent in sandbox...
Press Ctrl+C at any time to exit.
User: create a 3d animated web-based, lunar lander game
Thinking...
🔨 ✓ Run: /bin/sh -lc ls
🔨 ✓ Run: /bin/sh -lc 'ls -a'
🔨 ✓ Run: /bin/sh -lc 'ls .daytona'
🔨 ✓ Run: /bin/sh -lc 'find /home/daytona -maxdepth 4 -name .git'
📝 Add /home/daytona/index.html
📝 Add /home/daytona/style.css
📝 Add /home/daytona/main.js
📝 Update /home/daytona/main.js
- Built a self-contained 3D lunar lander experience with HUD in index.html wired to main.js.
- Styled a glassy mission card, typography, and neon accents in style.css.
- Implemented the Three.js scene in main.js: starfield + noisy terrain with a flattened pad, modeled lander, thrust/fuel/rotation controls, gravity/drag physics, landing/crash checks, exhaust particles, and a chase camera. Controls: Space/↑ thrust, ←/→ yaw, W/S pitch, R restart.
Next steps:
1) Serve locally (e.g., cd /home/daytona && python3 -m http.server 8080) and open https://8080-e7c5deb5-7723-4bb8-93c6-25258d9b7c53.proxy.daytona.works.
2) Tune physics constants or terrain size if you want a harder/easier landing.
🗒️ To-do list:
- [x] Inspect workspace and set up project structure for web-based lunar lander game
- [x] Implement 3D scene, lunar lander controls, physics, and game loop
- [x] Add UI elements, polish, and quick sanity check (open file if feasible)
Usage Summary: Cached: 71936, Input: 103238, Output: 11311
User: start the server
Thinking...
🔨 ✓ Run: /bin/sh -lc 'cd /home/daytona && nohup python3 -m http.server 8080 --bind 0.0.0.0 >/home/daytona/server.log 2>&1 & echo $!'
Server started on port 8080 (pid 274). Open the game at:
https://8080-e7c5deb5-7723-4bb8-93c6-25258d9b7c53.proxy.daytona.works
If you need to stop it later: kill 274.
Usage Summary: Cached: 4096, Input: 22231, Output: 272
User:
Cleaning up...
```
The agent can also host web apps and provide you with a preview link using the [Daytona Preview Links](https://www.daytona.io/docs/en/preview.md) feature. When your task involves running or previewing a web application, the agent automatically reasons about this need, hosts the app, and generates a preview link for you to inspect the live result:
You can continue interacting with your agent until you are finished. When you exit the program, the sandbox will be deleted automatically.
### 2. Project Setup
#### Clone the Repository
First, clone the daytona [repository](https://github.com/daytona/guides.git) and navigate to the example directory:
```bash
git clone https://github.com/daytona/guides.git
cd guides/typescript/openai/codex-sdk
```
#### Configure Environment
Get your API keys:
- **Daytona API key:** [Daytona Dashboard](https://app.daytona.io/dashboard/keys)
- **OpenAI API key:** [OpenAI Developer Platform](https://platform.openai.com/api-keys)
Copy `.env.example` to `.env` and add your keys:
```bash
DAYTONA_API_KEY=your_daytona_key
SANDBOX_OPENAI_API_KEY=your_openai_key
```
:::caution[API Key Security]
Note: The `SANDBOX_OPENAI_API_KEY` key is passed into the Daytona sandbox environment and is accessible to any code executed inside the sandbox.
:::
#### Local Usage
:::note[Node.js Version]
Node.js 18 or newer is required to run this example. Please ensure your environment meets this requirement before proceeding.
:::
Install dependencies:
```bash
npm install
```
Run the agent:
```bash
npm run start
```
The agent will start and wait for your prompt.
### 3. Understanding the Agent's Architecture
This example consists of two main components:
- **Main Program:** The main program is a Node.js script (`src/index.ts`) that runs on your local machine. It uses the Daytona SDK to create and manage a Daytona sandbox. The main program provides a command line interface for interacting with the agent inside the sandbox.
- **Sandbox Agent:** The sandbox agent is a Node.js script (`agent/index.ts`) that runs inside the Daytona sandbox. It uses the Codex SDK to create a customized coding agent.
#### Initialization
On initialization, the main program:
1. Creates a new [Daytona sandbox](https://www.daytona.io/docs/en/sandboxes.md) with your OpenAI API key included in the environment variables.
2. Configures the Codex system prompt with Daytona-specific instructions and writes it to a `.codex/config.toml` file in the sandbox.
3. Uploads the agent package to the sandbox with [file uploading](https://www.daytona.io/docs/en/file-system-operations.md#upload-a-single-file).
4. Installs the agent dependencies by running `npm install` in the uploaded agent directory.
5. Waits for user input and runs the agent asynchronously for each prompt.
#### Main Program Code
Custom system prompts for Codex must be configured via a `.codex/config.toml` file, so the main program creates this file in the sandbox before starting the agent:
```typescript
const systemPrompt = [
'You are running in a Daytona sandbox.',
'Use the /home/daytona directory instead of /workspace for file operations.',
`When running services on localhost, they will be accessible as: ${previewUrlPattern}`,
].join(' ')
const config = `developer_instructions = "${systemPrompt}"`
await sandbox.fs.createFolder('.codex', '755')
await sandbox.fs.uploadFile(Buffer.from(config, 'utf8'), '.codex/config.toml')
```
This prompt instructs the agent to use the correct file paths and preview link format for Daytona sandboxes.
After installing dependencies, the main program enters a loop to read user input and send it to the agent. For each user prompt it receives, it creates a new Daytona process session to run the agent command asynchronously and stream back the output:
```typescript
// Create a session to stream the agent output
const sessionId = `codex-session-${Date.now()}`
await sandbox.process.createSession(sessionId)
// Run the agent asynchronously, passing the prompt and OpenAI API key
const command = await sandbox.process.executeSessionCommand(sessionId, {
command: `${environmentPrefix({ PROMPT: prompt })} npm exec --prefix /tmp/agent tsx -- /tmp/agent/index.ts`,
runAsync: true,
})
// Stream agent output as it arrives
if (!command.cmdId) throw new Error('Failed to start agent command in sandbox')
await sandbox.process.getSessionCommandLogs(
sessionId,
command.cmdId,
onStdout,
onStderr,
)
// Delete the session
await sandbox.process.deleteSession(sessionId)
```
The `onStdout` and `onStderr` callbacks are used to pass the agent's output back to the main program. After the agent finishes responding to the prompt, the main program waits for the next user input.
#### Sandbox Agent Code
The sandbox agent uses the [Codex SDK](https://developers.openai.com/codex/sdk/) to create a customized coding agent.
The agent is initialized with custom options that include the workspace directory:
```typescript
// Configure Codex options
const options: ThreadOptions = {
workingDirectory: '/home/daytona',
skipGitRepoCheck: true,
sandboxMode: 'danger-full-access',
}
```
The agent maintains thread state between requests by writing the thread ID to a file, allowing it to maintain context across multiple interactions:
```typescript
const threadIdPath = '/tmp/codex-thread-id'
const threadId = (await readFileIfExisting(threadIdPath))?.trim()
const thread: Thread = threadId
? codex.resumeThread(threadId, options)
: codex.startThread(options)
```
Additional code to stream agent responses follows the examples in OpenAI's [Codex SDK documentation](https://github.com/openai/codex/blob/main/sdk/typescript/README.md).
#### Clean up
When you exit the main program, the Daytona sandbox and all files are automatically deleted.
**Key advantages:**
- Secure, isolated execution in Daytona sandboxes
- Communicate with the agent directly in your terminal
- Automatic dev server detection and live preview links
- Multi-language and full-stack support
- Thread persistence across multiple requests
- Simple setup and automatic cleanup
# OpenAI Agents SDK Guides
Guides for using the [OpenAI Agents SDK](https://developers.openai.com/api/docs/guides/agents) with Daytona.
# Using the OpenAI Agents SDK with Daytona Sandboxes
This guide walks through the core patterns for running AI agents in isolated cloud sandboxes using the [OpenAI Agents SDK](https://developers.openai.com/api/docs/guides/agents) and Daytona. We start from a simple example and progressively layer on multi-agent handoffs, memory, structured outputs, and human-in-the-loop workflows.
See also the [Text-to-SQL Agent with the OpenAI Agents SDK and Daytona](https://www.daytona.io/docs/en/guides/openai-agents/text-to-sql-agent-openai-agents-sdk.md) guide for a complete project built on these patterns.
---
## Prerequisites
Install the Agents SDK with the Daytona extra:
```shell
pip install openai-agents[daytona]
```
Set your environment variables:
```shell
export OPENAI_API_KEY=...
export DAYTONA_API_KEY=... # from https://app.daytona.io/dashboard/keys
```
## 1\. Give Your Agent a Shell
The basic pattern: declare a workspace, give an agent shell access, and let it explore, write code, and run it.
```py
from openai.types.responses import ResponseTextDeltaEvent
from agents import Runner
from agents.run import RunConfig
from agents.sandbox import Manifest, SandboxAgent, SandboxRunConfig
from agents.sandbox.capabilities import Shell
from agents.sandbox.entries import File
from agents.extensions.sandbox import DaytonaSandboxClient, DaytonaSandboxClientOptions
DAYTONA_ROOT = "/home/daytona/workspace"
# Declare workspace contents declaratively
# Use Daytona's home directory as root instead of the default /workspace.
manifest = Manifest(root=DAYTONA_ROOT, entries={
"data/sales.csv": File(content=b"quarter,revenue\nQ1,3200000\nQ2,3600000\nQ3,4200000\nQ4,3900000"),
"requirements.txt": File(content=b"pandas\nmatplotlib"),
})
agent = SandboxAgent(
name="Data Analyst",
model="gpt-5.4",
instructions=(
"You're a data analyst with shell access to a sandbox. "
"Inspect the workspace, install dependencies, write and run code to answer questions."
),
default_manifest=manifest,
capabilities=[Shell()],
)
client = DaytonaSandboxClient()
run_config = RunConfig(
sandbox=SandboxRunConfig(client=client, options=DaytonaSandboxClientOptions())
)
result = Runner.run_streamed(
agent,
"Which quarter had the highest revenue? Write a script to plot the trend and save it as chart.png.",
run_config=run_config,
)
async for event in result.stream_events():
if event.type == "raw_response_event" and isinstance(event.data, ResponseTextDeltaEvent):
print(event.data.delta, end="", flush=True)
elif event.type == "run_item_stream_event":
if event.name == "tool_called":
raw = event.item.raw_item
name = raw.get("name", "") if isinstance(raw, dict) else getattr(raw, "name", "")
args = raw.get("arguments", "") if isinstance(raw, dict) else getattr(raw, "arguments", "")
print(f"\n[{name}] {args}")
elif event.name == "tool_output":
print(f" → {event.item.output[:200]}")
await client.close()
```
The agent will likely `cat` the CSV, `pip install -r requirements.txt`, write a Python script, run it, and report back, all through the shell tool. A typical run might look like:
```
[exec_command] {"cmd": "cat data/sales.csv"}
→ quarter,revenue\nQ1,3200000\nQ2,3600000\nQ3,4200000\nQ4,3900000
[exec_command] {"cmd": "pip install -r requirements.txt"}
→ Successfully installed pandas matplotlib ...
[exec_command] {"cmd": "python plot.py"}
→ Chart saved to chart.png
Q3 had the highest revenue at $4.2M. I've saved a trend chart to chart.png.
```
**What's happening:**
- **`Manifest`** describes the workspace declaratively: files, directories, and environment variables (via `environment=Environment(value={"API_KEY": "..."})`, where `Environment` is imported from `agents.sandbox.manifest`). You can also pass `Manifest(entries={})` for an empty workspace and let the agent create everything from scratch.
- **`SandboxAgent`** adds `default_manifest` and `capabilities` on top of a regular `Agent`. You can still pass `tools=` (function tools) and `mcp_servers=` alongside capabilities.
- **`Shell`** gives the model an `exec_command` tool that can run `cat`, `ls`, `find`, `grep`, `pip install`, `python script.py`, etc. inside the sandbox. The agent can read *and* write: creating files, installing packages, and running programs are all fair game.
- **`DaytonaSandboxClient`** provisions a remote cloud sandbox.
- **`Runner.run_streamed`** streams text token-by-token and emits structured events when tools are called.
The sandbox is fully isolated, so there's no risk to your host machine. The agent has full Linux access inside it.
## 2\. Multi-Turn Conversations
The previous example runs a single question and exits. In practice you'll often want an interactive session where the human asks questions, the agent responds, and conversation history carries forward. The sandbox stays alive across turns so the agent can build on previous work.
```py
client = DaytonaSandboxClient()
session = await client.create(manifest=manifest, options=DaytonaSandboxClientOptions())
await session.start()
run_config = RunConfig(sandbox=SandboxRunConfig(session=session))
conversation = []
while True:
question = input("> ")
if question.strip().lower() == "exit":
break
input_items = conversation + [{"role": "user", "content": question}]
result = Runner.run_streamed(agent, input_items, run_config=run_config)
async for event in result.stream_events():
if event.type == "raw_response_event" and isinstance(event.data, ResponseTextDeltaEvent):
print(event.data.delta, end="", flush=True)
print()
# Carry conversation history forward so the agent remembers previous turns
conversation = result.to_input_list()
await session.aclose()
await client.close()
```
This example uses `result.to_input_list()`, which serializes the full conversation (including tool calls and their results) into a format you can pass back on the next turn. The agent sees the entire history, so follow-ups like "break that down by quarter" or "now plot it" just work. The SDK also supports other state strategies: sessions, `conversation_id`, and `previous_response_id` – see the [State and conversation management](https://openai.github.io/openai-agents-python/running_agents/#state-and-conversation-management) docs for the full picture.
This pattern composes with everything else in this guide. You can add handoffs, memory, pause/resume, etc. on top of a multi-turn loop.
## 3\. Pause and Resume
By default, when a session shuts down the sandbox is deleted. Setting `pause_on_exit=True` changes this: on shutdown, the SDK calls Daytona's pause API (`sandbox.stop()`) instead of `sandbox.delete()`. The sandbox stays on Daytona's infrastructure in a paused state, preserving the filesystem (including any installed packages).
To reconnect on the next run, you need two things:
1. **Daytona keeps the sandbox alive**, paused on their side, identifiable by its sandbox ID.
2. **Your code remembers the sandbox ID**. The SDK captures this in `DaytonaSandboxSessionState`, a Pydantic model you serialize to disk.
When you call `client.resume(saved_state)`, the SDK uses the `sandbox_id` from that state to call `daytona.get(sandbox_id)`. If the sandbox is still there, it calls `sandbox.start()` to wake it. The workspace is already populated, so it skips full manifest apply but still reapplies ephemeral state (like environment variables) and restores snapshots if needed. If the sandbox has expired or been deleted, `resume()` falls through and creates a fresh one from the same config.
```py
from pathlib import Path
from agents.extensions.sandbox import (
DaytonaSandboxClient,
DaytonaSandboxClientOptions,
DaytonaSandboxSessionState,
)
STATE_FILE = Path(".session_state.json")
client = DaytonaSandboxClient()
options = DaytonaSandboxClientOptions(pause_on_exit=True)
# Try to resume a previously paused sandbox
session = None
if STATE_FILE.exists():
saved = DaytonaSandboxSessionState.model_validate_json(STATE_FILE.read_text())
old_sandbox_id = saved.sandbox_id # snapshot before resume() mutates it
try:
session = await client.resume(saved)
if session.state.sandbox_id == old_sandbox_id:
print("Reconnected to existing sandbox.")
else:
print("Previous sandbox expired. Created a new one.")
except Exception:
session = None # fall through to fresh creation
if session is None:
session = await client.create(manifest=manifest, options=options)
# Save state immediately so crashes don't orphan the sandbox
STATE_FILE.write_text(session.state.model_dump_json(indent=2))
# ... run your agent ...
# On clean exit: aclose() persists the workspace, then pauses (or deletes) the remote sandbox
await session.aclose()
await client.close()
```
The Agents SDK also has its own **workspace persistence** mechanism (`persist_workspace`/`hydrate_workspace`) that tars up workspace files and saves them externally (local disk, S3). This is useful when the sandbox itself is gone and you need to restore contents into a new one. It's distinct from **Daytona snapshots** (`sandbox_snapshot_name`), which are pre-built sandbox templates you create sandboxes *from*.
## 4\. Handoffs: Routing Work Between Agents
A `SandboxAgent` can hand off to a regular `Agent` and vice versa. Not every agent needs sandbox access: a copywriter can draft an email without a shell.
```py
from agents import Agent, Runner
from agents.run import RunConfig
from agents.sandbox import Manifest, SandboxAgent, SandboxRunConfig
from agents.sandbox.capabilities import Shell
from agents.sandbox.entries import File
from agents.extensions.sandbox import DaytonaSandboxClient, DaytonaSandboxClientOptions
manifest = Manifest(root="/home/daytona/workspace", entries={
"data/sales.csv": File(content=b"quarter,region,revenue\nQ1,NA,3200000\nQ1,EU,2100000\n..."),
})
# The copywriter receives the analyst's findings (no sandbox needed)
copywriter = Agent(
name="Client Email Drafter",
model="gpt-5.4",
instructions="Turn the analyst's findings into a short, friendly client-facing email.",
)
# The analyst has shell access to crunch data, then hands off to the copywriter
analyst = SandboxAgent(
name="Data Analyst",
model="gpt-5.4",
instructions=(
"Analyze the sales data in the workspace. Write and run code to compute trends. "
"Then hand off your findings to the Client Email Drafter."
),
default_manifest=manifest,
capabilities=[Shell()],
handoffs=[copywriter],
)
client = DaytonaSandboxClient()
result = await Runner.run(
analyst,
"Summarize Q1 performance by region for the client.",
run_config=RunConfig(sandbox=SandboxRunConfig(client=client, options=DaytonaSandboxClientOptions())),
)
await client.close()
print(result.final_output) # a polished email, written by the copywriter
```
**The flow:** Analyst (sandbox, reads CSV, runs a script) → Copywriter (no sandbox, writes the email). The final output comes from the copywriter, but it's grounded in the analyst's computed results.
Handoffs can also be **circular**: agents pass control back and forth until one decides to respond directly instead of handing off, which ends the run. In the example above, that would look like:
```py
from agents import handoff
copywriter.handoffs = [handoff(analyst)]
analyst.handoffs = [handoff(copywriter)]
```
You can also have multiple sandbox agents, each with their own isolated workspace and separate `RunConfig`, as shown in the next section.
## 5\. Sandbox Agents as Tools
Instead of handoffs (sequential), you can run sandbox agents as parallel tools under an orchestrator:
```py
import json
from pydantic import BaseModel
class PricingReview(BaseModel):
risk: str
summary: str
class RolloutReview(BaseModel):
risk: str
blockers: list[str]
# By default, Pydantic output_type results are stringified (repr) when passed back
# as tool output. This extractor ensures the orchestrator receives clean JSON instead.
async def structured_output_extractor(result) -> str:
final_output = result.final_output
if isinstance(final_output, BaseModel):
return json.dumps(final_output.model_dump(mode="json"), sort_keys=True)
return str(final_output)
# Each reviewer gets its own isolated workspace
pricing_agent = SandboxAgent(
name="Pricing Reviewer",
default_manifest=pricing_docs_manifest,
capabilities=[Shell()],
output_type=PricingReview,
...
)
rollout_agent = SandboxAgent(
name="Rollout Reviewer",
default_manifest=rollout_docs_manifest,
capabilities=[Shell()],
output_type=RolloutReview,
...
)
# Orchestrator calls them like tools, each in its own sandbox
client = DaytonaSandboxClient()
orchestrator = Agent(
name="Deal Desk Coordinator",
instructions="Use both review tools, then synthesize a recommendation.",
tools=[
pricing_agent.as_tool(
tool_name="review_pricing",
tool_description="Review the pricing packet.",
custom_output_extractor=structured_output_extractor,
run_config=RunConfig(sandbox=SandboxRunConfig(client=client, options=DaytonaSandboxClientOptions())),
),
rollout_agent.as_tool(
tool_name="review_rollout",
tool_description="Review the rollout plan.",
custom_output_extractor=structured_output_extractor,
run_config=RunConfig(sandbox=SandboxRunConfig(client=client, options=DaytonaSandboxClientOptions())),
),
],
)
result = await Runner.run(orchestrator, "Review the Acme Corp renewal deal.")
print(result.final_output)
await client.close()
```
Each sandbox agent runs in its own isolated environment. The orchestrator never sees the files; it only gets the structured output as JSON via the `custom_output_extractor`. This is great for **fan-out** patterns where you need multiple independent analyses.
## 6\. Memory Across Sessions
The `Memory` capability lets an agent learn from previous runs. It extracts durable facts and preferences from each conversation, consolidates them into structured files in the workspace, and automatically injects a summary into the agent's instructions on future runs.
```py
from agents.sandbox import LocalSnapshotSpec, SandboxRunConfig
from agents.sandbox.capabilities import ApplyPatch, Memory, Shell
agent = SandboxAgent(
name="Data Analyst",
model="gpt-5.4",
instructions="Analyze the workspace and answer questions.",
default_manifest=manifest,
capabilities=[
Shell(),
ApplyPatch(),
Memory(),
],
)
snapshot = LocalSnapshotSpec(base_path=Path("/tmp/my-agent-snapshots"))
# First run: agent learns user preferences.
# Memory artifacts are written to the workspace when the session closes.
session = await client.create(manifest=manifest, snapshot=snapshot)
async with session:
run_config = RunConfig(sandbox=SandboxRunConfig(session=session))
result1 = await Runner.run(agent, "Fix the bug. I prefer minimal patches.", run_config=run_config)
# Second run: resume the workspace so the agent sees the memory files from run 1.
resumed = await client.resume(session.state)
async with resumed:
run_config = RunConfig(sandbox=SandboxRunConfig(session=resumed))
result2 = await Runner.run(agent, "Add a test for the fix.", run_config=run_config)
```
Memory consolidation runs as a background task and flushes when the session closes, so the close/resume cycle ensures run 2 sees the artifacts from run 1\. You can also keep a single sandbox session open across runs (like section 2), though memory visibility then depends on whether the background task has finished.
`Memory()` with no arguments enables both reading and writing with live updates (the agent can repair stale memory in place). It requires `Shell` and `ApplyPatch` as sibling capabilities. You can tune the behavior:
```py
from agents.sandbox.config import MemoryReadConfig, MemoryWriteConfig
# Write-only (no auto-injection of memory into instructions):
Memory(read=None)
# Read-only (no background memory generation):
Memory(write=None)
# Custom write settings:
Memory(write=MemoryWriteConfig(
batch_size=2,
extra_prompt="Pay attention to which SQL patterns work best for this dataset.",
))
# Disable live updates (agent reads memory but won't repair stale entries):
Memory(read=MemoryReadConfig(live_update=False))
```
**How it works under the hood:**
After each `Runner.run()` completes, the SDK serializes the run (user input, tool calls, outputs, and final response, filtering out system/developer items and reasoning) into a JSONL file in `rollouts/`. A background pipeline then processes these in two phases:
1. **Phase 1 (per-rollout extraction):** A lightweight model (`gpt-5.4-mini`) reads each rollout transcript and extracts durable facts and preferences into `memory/raw_memories/` and `memory/rollout_summaries/`.
2. **Phase 2 (consolidation):** Once enough phase-1 results accumulate (controlled by `batch_size`), a stronger model (`gpt-5.4`) consolidates everything into `memory/MEMORY.md` (a structured, grep-friendly handbook) and `memory/memory_summary.md` (a compact index). A final phase-2 pass always runs on session shutdown.
Both phases run in a background `asyncio.Task`, so they don't block the agent's main work.
On subsequent runs, the `Memory` capability reads `memory/memory_summary.md` from the workspace and injects it into the agent's instructions (truncated to 15k tokens). The agent also gets guidance on when to grep `memory/MEMORY.md` for deeper context. This injection happens automatically — you don't need to wire it up yourself.
The full set of generated artifacts:
- `rollouts/`: JSONL rollout files (raw transcripts of each run)
- `memory/MEMORY.md`: detailed, grep-friendly handbook
- `memory/memory_summary.md`: compact summary, auto-injected into instructions
- `memory/raw_memories/`: individual learned facts (one file per rollout)
- `memory/raw_memories.md`: concatenated version of the above, fed into phase 2
- `memory/rollout_summaries/`: per-rollout summaries
- `memory/skills/`: optional reusable procedures the consolidation model may create
If you combine this with pause/resume (\#3), the memory files survive across sessions. The workspace persistence model includes all runtime-created files by default (only `ephemeral=True` manifest entries are excluded). So on the next run, the agent starts with full context from previous sessions — no extra wiring needed.
## 7\. Custom Capabilities
Capabilities are plugins that inject tools and instructions into a sandbox agent. The built-in ones (`Shell`, `ApplyPatch`, `Vision`) cover common cases, but you can write your own:
```py
from agents.sandbox.capabilities.capability import Capability
from agents.tool import Tool, function_tool
class ExposePort(Capability):
type: str = "expose_port"
def tools(self) -> list[Tool]:
session = self.session # bound automatically by the framework
@function_tool
async def get_app_url(port: int) -> str:
"""Get the public URL for a port running in this sandbox."""
endpoint = await session.resolve_exposed_port(port)
return endpoint.url_for("http")
return [get_app_url]
```
**Note:** `resolve_exposed_port` requires the port to be predeclared in the client options, e.g. `DaytonaSandboxClientOptions(exposed_ports=(8080,))`. Without this, the call raises `ExposedPortUnavailableError`.
Use this to expose domain-specific operations (database queries, API testing, cloud storage access) as tools the agent can call.
## Quick Reference: DaytonaSandboxClientOptions
| Option | Default | Description |
| :---- | :---- | :---- |
| `image` | `None` | OCI-compliant image to boot from |
| `env_vars` | `None` | Environment variables injected at creation |
| `exposed_ports` | `()` | Ports accessible via signed preview URLs |
| `pause_on_exit` | `False` | Pause sandbox instead of deleting on cleanup |
| `auto_stop_interval` | `0` | Seconds of inactivity before auto-pause (0 \= disabled) |
| `create_timeout` | `60` | Timeout in seconds for sandbox creation |
| `resources` | `None` | CPU/memory/disk configuration |
## Patterns at a Glance
| Pattern | When to Use | Key Concept |
| :---- | :---- | :---- |
| **Give Your Agent a Shell** (\#1) | Agent needs to read, write, or run code | `Manifest` \+ `Shell` |
| **Multi-Turn Conversations** (\#2) | Interactive sessions with a human | `result.to_input_list()` |
| **Pause/Resume** (\#3) | Long-running or iterative tasks | `pause_on_exit` \+ `client.resume(state)` |
| **Handoffs** (\#4) | Pipeline: analyze → write → review | `handoffs=[next_agent]` |
| **Agents as Tools** (\#5) | Parallel independent analyses | `agent.as_tool(run_config=...)` |
| **Memory** (\#6) | Preferences that persist across sessions | `Memory()` \+ `MemoryReadConfig`/`MemoryWriteConfig` |
| **Custom Capabilities** (\#7) | Domain-specific sandbox operations | Subclass `Capability` |
## What's Next
For a complete project that puts these patterns to work, see [**Building a Text-to-SQL Agent with OpenAI Agents SDK and Daytona**](https://www.daytona.io/docs/en/guides/openai-agents/text-to-sql-agent-openai-agents-sdk.md), a conversational agent that queries real federal spending data, combining multi-turn conversations, pause/resume, memory, and preview URLs.
# Text-to-SQL Agent with the OpenAI Agents SDK and Daytona
In this guide we use the [OpenAI Agents SDK](https://developers.openai.com/api/docs/guides/agents) and Daytona sandboxes to build a conversational agent that answers natural-language questions about NASA's federal spending. It translates questions into SQL, runs them against a SQLite database of real USAspending.gov data (FY2021-FY2025), and explains the results. The agent runs inside a Daytona sandbox, which provides isolated code execution, pause/resume across runs (so you can reconnect to the same sandbox without re-downloading data), cross-session memory (so the agent learns from previous conversations), and signed preview URLs for downloading query results.
---
### What a session looks like
```
> How much did NASA spend in FY2024?
[SQL] (limit 10)
SELECT SUM(federal_action_obligation) AS total_obligations
FROM spending
WHERE fiscal_year = 2024;
→ Result (1 rows)
| total_obligations |
|-------------------|
| 21352116106.41 |
↓ https://8080-your-sandbox-id.proxy.daytona.works/query_1775081861_281.csv
NASA obligated **$21.35 billion** in **FY2024**.
> Who are the top 5 recipients?
[SQL] (limit 10)
SELECT COALESCE(NULLIF(recipient_parent_name, ''), NULLIF(recipient_name, ''), 'UNKNOWN') AS recipient_entity,
SUM(federal_action_obligation) AS total_obligations
FROM spending
WHERE fiscal_year = 2024
GROUP BY recipient_entity
ORDER BY total_obligations DESC
LIMIT 5;
→ Result (5 rows)
| recipient_entity | total_obligations |
|--------------------------------------|-------------------|
| CALIFORNIA INSTITUTE OF TECHNOLOGY | 2247310017.56 |
| SPACE EXPLORATION TECHNOLOGIES CORP. | 1996992060.56 |
| THE BOEING COMPANY | 1540996029.73 |
| LOCKHEED MARTIN CORP | 1208072461.33 |
| NORTHROP GRUMMAN CORPORATION | 641375071.34 |
↓ https://8080-your-sandbox-id.proxy.daytona.works/query_1775081919_307.csv
In FY2024, the top 5 recipients were **California Institute of Technology ($2.25B)**,
**SpaceX ($2.00B)**, **Boeing ($1.54B)**, **Lockheed Martin ($1.21B)**,
and **Northrop Grumman ($641.38M)**.
```
The agent keeps conversation context across turns, so one can naturally follow-up on previous questions. Each result also gets a download link via the Daytona sandbox's exposed port.
## 1\. Setup
Set your environment variables:
```shell
export OPENAI_API_KEY=...
export DAYTONA_API_KEY=... # from https://app.daytona.io/dashboard/keys
```
Clone the repo, install dependencies and run:
```shell
git clone https://github.com/openai/openai-agents-python
cd openai-agents-python
uv sync --extra daytona
uv run python -m examples.sandbox.extensions.daytona.usaspending_text2sql.agent
```
On the first run, the agent fetches NASA spending data from the USAspending.gov API and builds a SQLite database inside the sandbox. This takes a few minutes. Subsequent runs reuse the paused sandbox and skip this step entirely.
## 2\. The Workspace
The manifest declares what goes into the sandbox:
```py
from agents.sandbox import Manifest
from agents.sandbox.entries import Dir, LocalDir, LocalFile
manifest = Manifest(
root=WORKSPACE_ROOT,
entries={
"setup_db.py": LocalFile(src=SETUP_DB_PATH),
"schema": LocalDir(src=SCHEMA_DIR),
"data": Dir(ephemeral=True),
},
)
```
The main entries:
- **`setup_db.py`** fetches NASA spending data from the USAspending.gov bulk download API and builds a SQLite database. It downloads contracts, grants, and IDVs for FY2021-FY2025, parses the CSVs, and creates an indexed `spending` table with \~30 columns covering amounts, recipients, locations, industry codes, and more. It's idempotent: if the DB already exists, it skips the download.
- **`schema/`** contains documentation the agent uses to understand the data:
- `overview.md` has the table schema, column descriptions, and example SQL patterns. This gets injected directly into the agent's `developer_instructions`.
- `tables/spending.md` has detailed per-column docs the agent can read via shell if needed.
- `glossary.md` has official USAspending terminology (what "obligation" vs "outlay" means, etc.), fetched from the USAspending API during setup.
- **`data/`** is where the SQLite DB lands. It's marked `ephemeral=True` so it's excluded from workspace snapshots (the DB can always be rebuilt from the API).
## 3\. The SqlCapability
The core of this example is a custom `Capability` that gives the agent a guardrailed `run_sql` tool. The agent could run `sqlite3` directly via the shell, but that would give it unrestricted access. The capability enforces safety at multiple levels:
```py
class SqlCapability(Capability):
type: Literal["sql"] = "sql"
db_path: str = "data/usaspending.db"
max_display_rows: int = 100
max_csv_rows: int = 10_000
timeout_seconds: float = 30.0
results_dir: str = "results"
```
**Guardrails (defense in depth):**
- **Read-only access**: SQLite opened with `?mode=ro` URI \+ `PRAGMA query_only = ON`
- **Statement validation**: Only `SELECT`, `WITH`, `EXPLAIN`, `PRAGMA` are allowed
- **Row limits**: 100 rows displayed to the model, up to 10,000 saved as downloadable CSV
- **Timeouts**: Queries killed after 30 seconds
The tool returns structured JSON (columns, rows, row counts, CSV filename), and the capability injects instructions telling the agent to prefer aggregations, use the schema docs, and explain its query logic.
## 4\. The Agent
The pieces come together in a `SandboxAgent` with four capabilities:
```py
from agents.sandbox import SandboxAgent
from agents.sandbox.capabilities.compaction import Compaction
from agents.sandbox.capabilities.memory import Memory
from agents.sandbox.capabilities.shell import Shell
from agents.sandbox.config import MemoryReadConfig, MemoryWriteConfig
agent = SandboxAgent(
name="NASA Spending Q&A",
default_manifest=manifest,
model="gpt-5.4",
instructions=(
"You are a helpful data analyst that answers questions about NASA federal spending "
"by writing and executing SQL queries."
),
developer_instructions=DEVELOPER_INSTRUCTIONS,
capabilities=[
SqlCapability(db_path="data/usaspending.db"),
Shell(),
Compaction(),
Memory(
read=MemoryReadConfig(live_update=False),
write=MemoryWriteConfig(batch_size=5),
),
],
)
```
- **`SqlCapability`** provides the guardrailed `run_sql` tool (section 3).
- **`Shell()`** gives the agent general shell access, mainly so it can `cat schema/tables/spending.md` or `cat schema/glossary.md` when it needs column details or term definitions beyond what's in the overview.
- **`Compaction()`** handles long sessions. After the conversation grows past a token threshold, the SDK automatically compacts earlier turns to keep context manageable. Without this, a long Q\&A session would eventually hit the model's context limit.
- **`Memory()`** gives the agent cross-session recall. The `Memory` capability is covered in detail in section 8\. The key config choices here: `live_update=False` means the agent isn't instructed to edit memory files mid-conversation (and doesn't need `ApplyPatch`), and `batch_size=5` batches the expensive consolidation step to run every 5 turns instead of every turn.
The `developer_instructions` load the full schema overview from `schema/overview.md` plus guidelines about data caveats (obligations vs outlays, masked recipients, etc.), so the agent starts every conversation already knowing the schema and quirks of the data.
## 5\. The Conversation Loop
Each turn appends the user's question to the conversation history, streams the agent's response (printing text deltas, tool calls, and formatted results), then carries the full history forward via `result.to_input_list()`. This is what lets follow-ups like "break that down by year" work, because the history includes tool calls and their results, so the agent knows what "that" refers to.
```py
input_items = conversation + [{"role": "user", "content": question}]
result = Runner.run_streamed(agent, input_items, run_config=run_config)
async for event in result.stream_events():
# print text deltas, tool calls, and tool outputs as they arrive
...
conversation = result.to_input_list()
```
The actual example includes SQL syntax highlighting and table formatting for the terminal output. See `agent.py` for the full streaming loop.
## 6\. Pause and Resume
The first run is slow because `setup_db.py` needs to download data from the USAspending API. With `pause_on_exit=True`, the sandbox pauses instead of being deleted, so subsequent runs reconnect to the existing sandbox with the database already built.
```py
# Core pattern (see agent.py for full error handling):
client = DaytonaSandboxClient()
options = DaytonaSandboxClientOptions(pause_on_exit=True, exposed_ports=(8080,))
saved_state = _load_session_state() # returns None on first run
if saved_state is not None:
session = await client.resume(saved_state)
else:
session = await client.create(manifest=agent.default_manifest, options=options)
_save_session_state(session.state)
```
The session state is serialized to a JSON file. On the next run, `client.resume()` uses the saved `sandbox_id` to find and wake the paused sandbox. If it's expired or been deleted, it falls through and creates a fresh one. Type `exit` to pause the sandbox, or `destroy` to delete it.
## 7\. Downloadable Results via Exposed Ports
Each query result is saved as a CSV inside the sandbox. To make these downloadable, a simple HTTP file server runs on port 8080:
```py
# Start a file server inside the sandbox
await session.exec("mkdir -p results", timeout=5.0)
await session.exec(
"nohup python3 -m http.server 8080 --directory results > /dev/null 2>&1 &",
timeout=5.0,
)
# Get the signed public URL
endpoint = await session.resolve_exposed_port(8080)
downloads_url = endpoint.url_for("http")
# -> https://8080-abc123.proxy.daytona.works
```
The `run_sql` tool includes the CSV filename in its response, and the agent surfaces the download link. You can see this in the session example at the top: each result has a `↓` download URL (signed Daytona preview link).
## 8\. Memory Across Sessions
Within a single REPL session, the agent has the full conversation history and remembers every question and answer. But when you type `exit` and come back later, that history is gone. The `Memory` capability bridges this gap by extracting durable learnings from each session and making them available to future sessions.
**How it works:**
After each conversational turn, the SDK serializes the full exchange (input, tool calls, outputs, response) into a rollout file. A background pipeline then processes these:
1. **Phase 1** (after every turn): A lightweight model (`gpt-5.4-mini`) reads the rollout and extracts durable facts (useful query patterns, data caveats, column quirks, user preferences) into `memory/raw_memories/` and `memory/rollout_summaries/`.
2. **Phase 2** (every 5 turns, or on shutdown): A stronger model (`gpt-5.4`) consolidates everything into `memory/MEMORY.md` (a detailed, grep-friendly handbook) and `memory/memory_summary.md` (a compact summary).
Both phases run in background `asyncio` tasks, so they don't block the conversation.
**How it persists:**
Memory files live in the sandbox filesystem and survive as long as the paused sandbox does. When you reconnect, the `Memory` capability reads them back. If the sandbox is destroyed or can't be reconnected, memory is lost and the agent starts fresh.
## Quick Reference
| Component | What it does |
| :---- | :---- |
| `setup_db.py` | Fetches NASA data from USAspending API, builds SQLite DB |
| `schema/overview.md` | Table schema and example queries, injected into agent instructions |
| `schema/glossary.md` | Official USAspending term definitions |
| `SqlCapability` | Guardrailed `run_sql` tool (read-only, row limits, timeouts) |
| `Shell()` | General shell access for reading schema docs and memory |
| `Compaction()` | Automatic context compression for long sessions |
| `Memory()` | Cross-session learning: extracts and recalls durable facts |
| `pause_on_exit=True` | Sandbox persists across runs (avoids re-downloading data) |
# Build a Coding Agent Using Amp Code and Daytona
import { Image } from 'astro:assets'
import ampSdkCodingAgentDemo from '../../../../../assets/docs/images/amp-sdk-coding-agent.gif'
This guide demonstrates how to run an autonomous coding agent based on [Amp Code](https://ampcode.com/) inside a Daytona sandbox environment. The agent can develop full-stack web apps, write code in any language, install dependencies, and run scripts. It can also start and manage dev servers, and generate preview links for live apps.
---
### 1. Workflow Overview
When you launch the main module, a Daytona sandbox is created and the Amp CLI is installed inside it. The agent uses Amp's [streaming JSON mode](https://ampcode.com/manual#cli-streaming-json) for programmatic control.
You interact with the main program via a command line chat interface. The program sends your prompts to the agent inside the sandbox, which executes them and returns the results:
```
$ npm run start
Creating sandbox...
Installing Amp CLI...
Starting Amp Code...
Thinking...
Got it! I'm ready to help. What would you like to build or work on?
Agent ready. Press Ctrl+C at any time to exit.
User: Create a Kanji flashcard app
Thinking...
> I'll create a Kanji flashcard app with flip animations, progress tracking, and multiple study modes. Here's the preview URL:
https://8000-29baaaf7-767a-4dff-8129-1e6ec2100b3e.daytonaproxy01.net
🔧 create_file /home/daytona/index.html
Successfully created file /home/daytona/index.html
🔧 create_file /home/daytona/start.sh
Running `python3 -m http.server 8000` via session command...
User:
```
The agent can also host web apps and provide you with a preview link using the [Daytona Preview Links](https://www.daytona.io/docs/en/preview.md) feature. When your task involves running or previewing a web application, the agent automatically reasons about this need, hosts the app, and generates a preview link for you to inspect the live result:
You can continue interacting with your agent until you are finished. When you exit the program, the sandbox will be deleted automatically.
### 2. Project Setup
#### Clone the Repository
First, clone the daytona [repository](https://github.com/daytona/guides.git) and navigate to the example directory:
```bash
git clone https://github.com/daytona/guides.git
cd guides/typescript/amp/amp-sdk
```
#### Configure Environment
Get your API keys:
- **Daytona API key:** [Daytona Dashboard](https://app.daytona.io/dashboard/keys)
- **Amp API key:** [Amp Settings](https://ampcode.com/settings)
:::caution[Amp Paid Credits Required]
Amp's execute mode (`-x`) requires paid credits and cannot use the free tier. [Add credits here](https://ampcode.com/pay) before running this example.
:::
Copy `.env.example` to `.env` and add your keys:
```bash
DAYTONA_API_KEY=your_daytona_key
SANDBOX_AMP_API_KEY=your_amp_key
```
#### Local Usage
:::note[Node.js Version]
Node.js 18 or newer is required to run this example. Please ensure your environment meets this requirement before proceeding.
:::
Install dependencies:
```bash
npm install
```
Run the agent:
```bash
npm run start
```
The agent will start and wait for your prompt.
### 3. Understanding the Script
This example uses Amp's `--stream-json` mode for streaming output and the `-x` (execute) flag for autonomous operation. Commands are sent via a PTY (pseudo-terminal) for real-time streaming.
#### Initialization
On startup, the script:
1. Creates a new [Daytona sandbox](https://www.daytona.io/docs/en/sandboxes.md) with the Amp API key.
2. Installs the Amp CLI globally in the sandbox.
3. Creates a PTY for streaming output from Amp.
4. Sends a Daytona-aware system prompt as the first user message (preview URL pattern + instruction to write server startup command to `/home/daytona/start.sh`).
5. Enters the readline loop to send prompts and receive streamed responses.
6. On Ctrl+C, kills the PTY session, deletes the sandbox, and exits.
#### PTY Communication
The agent uses a pseudo-terminal (PTY) for streaming output from Amp:
```ts
// Create a PTY for streaming output from Amp
this.ptyHandle = await this.sandbox.process.createPty({
id: `amp-pty-${Date.now()}`,
cols: 120,
rows: 30,
onData: (data: Uint8Array) => this.handleData(data),
})
// Wait for PTY connection
await this.ptyHandle.waitForConnection()
```
#### Running Amp Commands
Each prompt is sent as an `amp` command with the `-x` (execute) flag for autonomous operation. The agent uses Amp's thread system to maintain conversation context:
```ts
// Run an amp command via PTY and wait for completion
private async runAmpCommand(args: string[]): Promise {
const command = ['amp', '--dangerously-allow-all', '--stream-json', '-m smart', ...args].join(' ')
// Send command to the PTY
await this.ptyHandle.sendInput(`cd /home/daytona && ${command}\n`)
// Wait for the response to complete (signaled by result message)
await new Promise((resolve) => {
this.onResponseComplete = resolve
})
}
// Process a user prompt
async processPrompt(prompt: string): Promise {
if (this.threadId) {
// Continue existing thread
await this.runAmpCommand(['-x', JSON.stringify(prompt), 'threads', 'continue', this.threadId])
} else {
// Start new thread
await this.runAmpCommand(['-x', JSON.stringify(prompt)])
}
}
```
#### Streaming JSON Messages
Amp outputs JSON lines that can be parsed to track agent activity. The `handleData` method buffers incoming data and processes complete lines:
```ts
// Handle streamed data from PTY
private handleData(data: Uint8Array): void {
// Append new data to the buffer
this.buffer += new TextDecoder().decode(data)
// Split the buffer into complete lines
const lines = this.buffer.split('\n')
// Keep any incomplete line in the buffer for next time
this.buffer = lines.pop() || ''
// Process each complete line
for (const line of lines.filter((l) => l.trim())) {
this.handleJsonLine(line)
}
}
```
Message types from Amp's streaming JSON:
- **system**: Session initialization with `subtype: 'init'` and `session_id` for thread tracking
- **assistant**: AI responses with text content and tool usage blocks
- **user**: Tool results (output from executed tools)
- **result**: Final execution result (success or error) - signals response completion
```ts
private handleJsonLine(line: string): void {
const parsed = JSON.parse(line) as AmpMessage
if (parsed.type === 'system' && parsed.subtype === 'init') {
// Capture thread ID for conversation continuation
const sysMsg = parsed as { session_id?: string }
if (sysMsg.session_id) this.threadId = sysMsg.session_id
} else if (parsed.type === 'assistant') {
// Display text and tool_use blocks
const msg = parsed as AssistantMessage
for (const block of msg.message.content) {
if (block.type === 'text') { /* render text */ }
else if (block.type === 'tool_use') { /* display tool */ }
}
} else if (parsed.type === 'user') {
// Tool results: display output
} else if (parsed.type === 'result') {
// Signal response completion
this.onResponseComplete?.()
}
}
```
#### System Prompt and Main Loop
A Daytona-aware system prompt is sent as the first user message. It instructs the agent to use the preview URL pattern and to write the server start command into `/home/daytona/start.sh` (instead of executing directly in Amp), then provide the preview URL:
```ts
const defaultSystemPrompt = [
'You are running in a Daytona sandbox.',
`When running services on localhost, they will be accessible as: ${previewUrlPattern}`,
'When you need to start a server, DO NOT run it directly.',
'Instead, write only the server start command to /home/daytona/start.sh (one command, no markdown).',
'After writing the start command, provide the preview URL to the user.',
].join(' ')
const ampSession = new AmpSession(sandbox)
await ampSession.initialize({ systemPrompt: defaultSystemPrompt })
```
When Amp is ready, the script runs a readline loop:
```ts
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
while (true) {
const prompt = await new Promise((resolve) => rl.question('User: ', resolve))
if (prompt.trim()) {
await ampSession.processPrompt(prompt)
await startServerFromScript()
}
}
```
The readline loop waits for user input, sends it to the agent, and displays the streamed response. If Amp produced `/home/daytona/start.sh`, the script is then launched via Daytona's session command API so long-running/background server startup does not hang Amp turns.
**Key advantages:**
- Secure, isolated execution in Daytona sandboxes
- Streaming JSON output for real-time tool activity feedback
- PTY-based communication for streaming output
- Thread-based conversation continuity across prompts
- Uses Amp's `smart` mode for state-of-the-art model capabilities
- All agent code execution happens inside the sandbox
- Automatic preview link generation for deployed services
- Automatic cleanup on exit
# Generate Verified Code With Google ADK Agent
This guide demonstrates how to use the `DaytonaPlugin` for Google ADK to build an agent that generates, tests, and verifies code in a secure sandbox environment. The plugin enables agents to execute Python, JavaScript, and TypeScript code, run shell commands, and manage files within isolated Daytona sandboxes.
In this example, we build a code generator agent that takes a natural language description of a function, generates the implementation in TypeScript, creates test cases, executes them in the sandbox, and iterates until all tests pass before returning the verified code.
---
### 1. Workflow Overview
You describe the function you want in plain English, specifying the language (Python, JavaScript, or TypeScript). The agent generates the implementation, writes tests for it, and executes everything in a Daytona sandbox. If tests fail, the agent automatically fixes the code and re-runs until all tests pass. Only then does it return the verified, working code.
The key benefit: you receive code that has already been tested and verified, not just generated.
### 2. Project Setup
#### Clone the Repository
Clone the Daytona repository and navigate to the example directory:
```bash
git clone https://github.com/daytona/guides
cd guides/python/google-adk/code-generator-agent/gemini
```
#### Install Dependencies
:::note[Python Version Requirement]
This example requires **Python 3.10 or higher**. It's recommended to use a virtual environment (e.g., `venv` or `poetry`) to isolate project dependencies.
:::
Install the required packages for this example:
```bash
pip install -U google-adk daytona-adk python-dotenv
```
The packages include:
- `google-adk`: Google's Agent Development Kit for building AI agents
- `daytona-adk`: Provides the `DaytonaPlugin` that enables secure code execution in Daytona sandboxes
- `python-dotenv`: Used for loading environment variables from `.env` file
#### Configure Environment
Get your API keys and configure your environment:
1. **Daytona API key:** Get it from [Daytona Dashboard](https://app.daytona.io/dashboard/keys)
2. **Google API key:** Get it from [Google AI Studio](https://aistudio.google.com/apikey)
Create a `.env` file in your project:
```bash
DAYTONA_API_KEY=dtn_***
GOOGLE_API_KEY=***
```
### 3. Understanding the Core Components
Before diving into the implementation, let's understand the key components we'll use:
#### Google ADK Components
- **Agent**: The AI model wrapper that processes requests and decides which tools to use. It receives instructions, has access to tools, and generates responses.
- **App**: A top-level container that bundles agents with plugins into a single configuration unit. It provides centralized management for shared resources and defines the root agent for your workflow.
- **InMemoryRunner**: The execution engine that runs agents and manages conversation state. It orchestrates the event-driven execution loop, handles message processing, and manages services like session history and artifact storage.
:::note[Running the Agent]
There are two ways to run Google ADK agents: using the `App` class with `InMemoryRunner`, or using `InMemoryRunner` directly with just an agent. The `App` serves as a configuration container that bundles agents with plugins, while the `Runner` handles actual execution and lifecycle management. This guide uses the `App` approach for cleaner organization of agents and plugins.
:::
#### Daytona Plugin
The `DaytonaPlugin` provides tools that allow the agent to:
- Execute code in Python, JavaScript, or TypeScript
- Run shell commands
- Upload and read files
- Start long-running background processes
All operations happen in an isolated sandbox that is automatically cleaned up when done.
### 4. Initialize Environment and Imports
First, we set up our imports and load environment variables:
```python
import asyncio
import logging
from dotenv import load_dotenv
from google.adk.agents import Agent
from google.adk.apps import App
from google.adk.runners import InMemoryRunner
from daytona_adk import DaytonaPlugin
load_dotenv()
logging.basicConfig(level=logging.DEBUG)
```
**What each import does:**
- `asyncio`: Required for running the async ADK runner
- `logging`: Enables debug output to see agent reasoning
- `load_dotenv`: Loads API keys from your `.env` file
- `Agent`, `App`, `InMemoryRunner`: Core Google ADK components
- `DaytonaPlugin`: Provides sandbox execution tools to the agent
**Logging configuration:**
The `logging.basicConfig(level=logging.DEBUG)` line configures Python's logging to show detailed debug output. You can adjust the logging level by passing different values:
- `logging.DEBUG`: Most verbose, shows all internal operations including DaytonaPlugin sandbox creation and tool invocations
- `logging.INFO`: Shows informational messages about agent progress
- `logging.WARNING`: Shows only warnings and errors
- `logging.ERROR`: Shows only errors
:::tip[Behind the Scenes]
With `DEBUG` level logging enabled, you can see the DaytonaPlugin's internal operations, including when the sandbox is created, when the `execute_code_in_daytona` tool is invoked, and when cleanup occurs. The plugin's `plugin_name` (configurable, defaults to `daytona_plugin`) appears in these log messages, making it easy to trace plugin activity.
:::
### 5. Define the Response Extractor
The ADK runner returns a list of events from the agent's execution. We need a helper function to extract the final text response:
```python
def extract_final_response(response: list) -> str:
"""Extract the final text response from a list of ADK events."""
for event in reversed(response):
text_parts = []
if hasattr(event, "text") and event.text:
return event.text
if hasattr(event, "content") and event.content:
content = event.content
if hasattr(content, "parts") and content.parts:
for part in content.parts:
if hasattr(part, "text") and part.text:
text_parts.append(part.text)
if text_parts:
return "".join(text_parts)
if hasattr(content, "text") and content.text:
return content.text
if isinstance(event, dict):
text = event.get("text") or event.get("content", {}).get("text")
if text:
return text
return ""
```
This function iterates through events in reverse order to find the last text response. It handles multiple possible event structures that the ADK may return.
### 6. Define the Agent Instruction
The instruction is critical - it defines how the agent behaves. Our instruction enforces a test-driven workflow:
```python
AGENT_INSTRUCTION = """You are a code generator agent that writes verified, working code.
You support Python, JavaScript, and TypeScript.
Your workflow for every code request:
1. Write the function
2. Write tests for it
3. EXECUTE the code in the sandbox to verify it works - do not skip this step
4. If execution fails, fix and re-execute until tests pass
5. Once verified, respond with ONLY the function (no tests)
You must always execute code before responding. Never return untested code.
Only include tests in your response if the user explicitly asks for them.
"""
```
**Key aspects of this instruction:**
- **Enforces execution**: The agent must run code in the sandbox before responding
- **Iterative fixing**: If tests fail, the agent fixes and retries
- **Controlled output**: By default, the final response contains only the working function. If you want to see the tests, include an instruction to return them in your prompt.
- **Multi-language**: Supports Python, JavaScript, and TypeScript
### 7. Configure the Daytona Plugin
Initialize the plugin that provides sandbox execution capabilities:
```python
plugin = DaytonaPlugin(
labels={"example": "code-generator"},
)
```
**Configuration options:**
- `labels`: Custom metadata tags for the sandbox (useful for tracking/filtering)
- `api_key`: Daytona API key (defaults to `DAYTONA_API_KEY` env var)
- `sandbox_name`: Custom name for the sandbox
- `plugin_name`: Name displayed in logs when the plugin logs messages (defaults to `daytona_plugin`)
- `env_vars`: Environment variables to set in the sandbox
- `auto_stop_interval`: Minutes before auto-stop (default: 15)
- `auto_delete_interval`: Minutes before auto-delete (disabled by default)
### 8. Create the Agent
Create the agent with the Gemini model, our instruction, and the Daytona tools:
```python
agent = Agent(
model="gemini-2.5-pro",
name="code_generator_agent",
instruction=AGENT_INSTRUCTION,
tools=plugin.get_tools(),
)
```
**Parameters explained:**
- `model`: The Gemini model to use for reasoning and code generation
- `name`: Identifier for the agent
- `instruction`: The behavioral guidelines we defined
- `tools`: List of tools from the Daytona plugin that the agent can use
### 9. Create the App and Runner
Bundle the agent and plugin into an App, then run it:
```python
app = App(
name="code_generator_app",
root_agent=agent,
plugins=[plugin],
)
async with InMemoryRunner(app=app) as runner:
prompt = "Write a TypeScript function called 'groupBy' that takes an array and a key function, and groups array elements by the key. Use proper type annotations."
response = await runner.run_debug(prompt)
final_response = extract_final_response(response)
print(final_response)
```
**What happens here:**
1. The `App` bundles the agent with the plugin for proper lifecycle management
2. `InMemoryRunner` is used as an async context manager (the `async with` statement). A context manager in Python automatically handles setup and cleanup - when the code enters the `async with` block, the runner initializes; when it exits (either normally or due to an error), the runner cleans up resources.
3. `run_debug` sends the prompt and returns all execution events
4. The sandbox is automatically deleted when the `async with` block exits - this cleanup happens regardless of whether the code completed successfully or raised an exception
### 10. Running the Example
Run the complete example:
```bash
python main.py
```
#### Understanding the Agent's Execution Flow
When you run the code, the agent works through your request step by step. With `logging.DEBUG` enabled, you'll see detailed output including:
- **DaytonaPlugin operations**: Sandbox creation, tool invocations (`execute_code_in_daytona`), and cleanup
- **LLM requests and responses**: The prompts sent to Gemini and the responses received
- **Plugin registration**: Confirmation that the `daytona_plugin` was registered with the agent
Here's what the debug output reveals about each step:
**Step 1: Sandbox Creation**
```
DEBUG:daytona_adk.plugin:Daytona sandbox created: e38f8574-48ac-48f1-a0ff-d922d02b0fcb
INFO:google_adk.google.adk.plugins.plugin_manager:Plugin 'daytona_plugin' registered.
```
The DaytonaPlugin creates an isolated sandbox and registers itself with the agent.
**Step 2: Agent receives the request**
The agent receives your prompt and understands it needs to create a TypeScript `groupBy` function with proper type annotations.
**Step 3: Agent generates code and tests**
The agent writes both the implementation and test cases, then calls the `execute_code_in_daytona` tool:
```
DEBUG:google_adk.google.adk.models.google_llm:
LLM Response:
-----------------------------------------------------------
Function calls:
name: execute_code_in_daytona, args: {'code': "...", 'language': 'typescript'}
```
**Step 4: Code execution in sandbox**
```
DEBUG:daytona_adk.plugin:Before tool: execute_code_in_daytona
DEBUG:daytona_adk.tools:Executing typescript code (length: 1570 chars)
DEBUG:daytona_adk.tools:Code execution completed with exit_code: 0
DEBUG:daytona_adk.plugin:After tool: execute_code_in_daytona
```
The plugin executes the code in the isolated TypeScript environment and returns the result.
**Step 5: Agent iterates if needed**
If tests fail (exit_code != 0), the agent analyzes the error, fixes the code, and re-executes until all tests pass.
**Step 6: Agent returns verified code**
Once tests pass, the agent responds with only the working function. If you included an instruction to return tests in your prompt, the tests will also be included in the response.
**Step 7: Cleanup**
```
INFO:daytona_adk.plugin:Deleting Daytona sandbox...
INFO:daytona_adk.plugin:Daytona sandbox deleted.
INFO:google_adk.google.adk.runners:Runner closed.
```
When the context manager exits, the sandbox is automatically deleted.
#### Example Output
When the agent completes the task, you'll see output like:
````
AGENT RESPONSE:
------------------------------------------------------------
```typescript
function groupBy(
array: T[],
keyFn: (item: T) => K
): Record {
return array.reduce((result, item) => {
const key = keyFn(item);
if (!result[key]) {
result[key] = [];
}
result[key].push(item);
return result;
}, {} as Record);
}
```
============================================================
App closed, sandbox cleaned up. Done!
````
The agent has already tested this code in the sandbox before returning it, so you can trust that the implementation works correctly.
#### Requesting Tests in the Response
If you want to see the tests that were executed in the sandbox, include an instruction to return them in your prompt:
```python
prompt = "Write a TypeScript function called 'groupBy' that takes an array and a key function, and groups array elements by the key. Use proper type annotations. Return the tests also in a separate code block"
```
With this prompt, the agent will return both the function and the tests:
````
```typescript
function groupBy(
array: T[],
keyFn: (item: T) => K
): Record {
return array.reduce((result, item) => {
const key = keyFn(item);
if (!result[key]) {
result[key] = [];
}
result[key].push(item);
return result;
}, {} as Record);
}
```
```typescript
import { deepStrictEqual } from 'assert';
// Test case 1: Group by a property of an object
const array1 = [
{ id: 1, category: 'A' },
{ id: 2, category: 'B' },
{ id: 3, category: 'A' },
];
const result1 = groupBy(array1, (item) => item.category);
deepStrictEqual(result1, {
A: [
{ id: 1, category: 'A' },
{ id: 3, category: 'A' },
],
B: [{ id: 2, category: 'B' }],
});
// Test case 2: Group by length of strings
const array2 = ['apple', 'banana', 'cherry', 'date'];
const result2 = groupBy(array2, (item) => item.length);
deepStrictEqual(result2, {
5: ['apple'],
6: ['banana', 'cherry'],
4: ['date'],
});
console.log('All tests passed!');
```
````
### 11. Complete Implementation
Here is the complete, ready-to-run example with additional output formatting for better readability:
```python
"""Code Generator & Tester Agent Example."""
import asyncio
import logging
from dotenv import load_dotenv
from google.adk.agents import Agent
from google.adk.apps import App
from google.adk.runners import InMemoryRunner
from daytona_adk import DaytonaPlugin
load_dotenv()
logging.basicConfig(level=logging.DEBUG)
def extract_final_response(response: list) -> str:
"""Extract the final text response from a list of ADK events."""
for event in reversed(response):
text_parts = []
if hasattr(event, "text") and event.text:
return event.text
if hasattr(event, "content") and event.content:
content = event.content
if hasattr(content, "parts") and content.parts:
for part in content.parts:
if hasattr(part, "text") and part.text:
text_parts.append(part.text)
if text_parts:
return "".join(text_parts)
if hasattr(content, "text") and content.text:
return content.text
if isinstance(event, dict):
text = event.get("text") or event.get("content", {}).get("text")
if text:
return text
return ""
AGENT_INSTRUCTION = """You are a code generator agent that writes verified, working code.
You support Python, JavaScript, and TypeScript.
Your workflow for every code request:
1. Write the function
2. Write tests for it
3. EXECUTE the code in the sandbox to verify it works - do not skip this step
4. If execution fails, fix and re-execute until tests pass
5. Once verified, respond with ONLY the function (no tests)
You must always execute code before responding. Never return untested code.
Only include tests in your response if the user explicitly asks for them.
"""
async def main() -> None:
"""Run the code generator agent example."""
plugin = DaytonaPlugin(
labels={"example": "code-generator"},
)
agent = Agent(
model="gemini-2.5-pro",
name="code_generator_agent",
instruction=AGENT_INSTRUCTION,
tools=plugin.get_tools(),
)
app = App(
name="code_generator_app",
root_agent=agent,
plugins=[plugin],
)
async with InMemoryRunner(app=app) as runner:
prompt = "Write a TypeScript function called 'groupBy' that takes an array and a key function, and groups array elements by the key. Use proper type annotations."
print("\n" + "=" * 60)
print("USER PROMPT:")
print("=" * 60)
print(prompt)
print("-" * 60)
response = await runner.run_debug(prompt)
final_response = extract_final_response(response)
print("\nAGENT RESPONSE:")
print("-" * 60)
print(final_response)
print("=" * 60)
print("\nApp closed, sandbox cleaned up. Done!")
if __name__ == "__main__":
asyncio.run(main())
```
**Key advantages of this approach:**
- **Verified code:** Every response has been tested in a real execution environment
- **Secure execution:** Code runs in isolated Daytona sandboxes, not on your machine
- **Multi-language support:** Generate and test Python, JavaScript, or TypeScript
- **Automatic iteration:** Agent fixes issues until tests pass
- **Flexible output:** Returns only the working function by default, or includes tests if explicitly requested in the prompt
### 12. API Reference
For the complete API reference of the Daytona ADK plugin, including all available tools and configuration options, see the [daytona-adk documentation](https://github.com/daytona/integrations/tree/main/packages/adk-plugin#available-tools).
# Build a Coding Agent Using Letta Code and Daytona
import { Image } from 'astro:assets'
import lettaCodeAgentResult from '../../../../../assets/docs/images/letta-code-agent-result.gif'
This guide demonstrates how to run an autonomous coding agent based on [Letta Code](https://docs.letta.com/letta-code/) inside a Daytona sandbox environment. The agent can develop web apps, write code in any language, install dependencies, and run scripts. Letta Code uses stateful agents with built-in memory, allowing conversations to persist across sessions.
---
### 1. Workflow Overview
When you launch the main script, a Daytona sandbox is created and Letta Code is installed inside it. The agent is configured with a custom Daytona-aware system prompt.
The script provides an interactive CLI interface where you can chat with the agent and issue commands:
```
$ npm run start
Creating sandbox...
Installing Letta Code...
Starting Letta Code...
Initializing agent...
Agent initialized. Press Ctrl+C at any time to exit.
User: create a beautiful, professional themed app that lets me write markdown documents and render them live
Thinking...
🔧 TodoWrite
🔧 Write /home/daytona/markdown-editor/index.html
🔧 TodoWrite
🔧 Start HTTP server on port 8080
🔧 TodoWrite
Perfect! I've created a beautiful markdown editor with live preview for you! 🎉
## Access your app here:
https://8080-c157e5cb-5e11-4bb6-883d-c873169223b8.proxy.daytona.works
## Features:
✨ **Live Preview** — Real-time markdown rendering
📝 **Full Markdown Support** — Headers, text styles, lists, code blocks, tables, links, images
💾 **Auto-Save** — Persists to browser localStorage
📥 **Export** — Download as `.md` or standalone `.html`
```
The agent can host web apps and provide you with a preview link using the [Daytona Preview Links](https://www.daytona.io/docs/en/preview.md) feature. When your task involves running or previewing a web application, the agent automatically hosts the app and generates a link for you to inspect the live result:
You can continue interacting with your agent until you are finished. When you exit the program, the sandbox will be deleted automatically.
### 2. Project Setup
#### Clone the Repository
First, clone the daytona [repository](https://github.com/daytona/guides.git) and navigate to the example directory:
```bash
git clone https://github.com/daytona/guides.git
cd guides/typescript/letta-code
```
#### Configure Environment
Get your Daytona API key from the [Daytona Dashboard](https://app.daytona.io/dashboard/keys) and your Letta API key from [Letta Platform](https://app.letta.com/api-keys).
Copy `.env.example` to `.env` and add your keys:
```bash
DAYTONA_API_KEY=your_daytona_key
SANDBOX_LETTA_API_KEY=your_letta_api_key
```
:::caution[API Key Security]
In this example, your Letta API key is passed into the sandbox environment and may be accessible to any code executed within it.
:::
#### Local Usage
:::note[Node.js Version]
Node.js 18 or newer is required to run this example. Please ensure your environment meets this requirement before proceeding.
:::
Install dependencies:
```bash
npm install
```
Run the example:
```bash
npm run start
```
The Letta Code agent will initialize and present an interactive prompt where you can issue commands.
### 3. Understanding the Agent's Architecture
This example consists of two main TypeScript files:
- **index.ts**: The main program that creates the Daytona sandbox, installs Letta Code, configures the system prompt, and provides an interactive CLI interface.
- **letta-session.ts**: A helper class that manages PTY-based bidirectional communication with Letta Code, handling JSON message streaming and response parsing.
#### Initialization
On initialization, the main program:
1. Creates a new [Daytona sandbox](https://www.daytona.io/docs/en/sandboxes.md) with your Letta API key included in the environment variables.
2. Installs Letta Code globally inside the sandbox by running `npm install` with [process execution](https://www.daytona.io/docs/en/process-code-execution.md#command-execution).
3. Creates a [PTY (pseudoterminal)](https://www.daytona.io/docs/en/pty.md) session in the sandbox for bidirectional communication with Letta Code.
4. Launches Letta Code in [bidirectional headless mode](https://docs.letta.com/letta-code/headless/) with stream-json format through the PTY.
5. Waits for user input and sends prompts to the agent through the PTY session.
#### Main Program Code
The program creates a [Daytona sandbox](https://www.daytona.io/docs/en/sandboxes.md) with the Letta API key passed as an environment variable:
```typescript
sandbox = await daytona.create({
envVars: { LETTA_API_KEY: process.env.SANDBOX_LETTA_API_KEY },
})
```
#### Running Letta Code in a Pseudoterminal
A [PTY (pseudoterminal)](https://www.daytona.io/docs/en/pty.md) is created for bidirectional communication with Letta Code:
```typescript
this.ptyHandle = await this.sandbox.process.createPty({
id: `letta-pty-${Date.now()}`,
onData: (data: Uint8Array) => this.handleData(data),
})
```
Letta Code is then launched in [bidirectional headless mode](https://docs.letta.com/letta-code/headless/#bidirectional-mode) through the PTY:
```typescript
await this.ptyHandle.sendInput(
`letta --new --system-custom "${systemPrompt.replace(/"/g, '\\"')}" --input-format stream-json --output-format stream-json --yolo -p\n`,
)
```
The `stream-json` setting is used for the input and output formats, enabling our program to send and receive JSON messages to and from the agent in real-time.
The `--system-custom` prompt allows us to pass a custom system prompt to the agent. Our prompt configures the agent with Daytona-specific instructions, including a URL pattern so the agent can generate preview links.
The `--yolo` flag allows the agent to run shell commands without requiring explicit user approval for each command.
#### Message Handling
To send prompts to the agent, the main script calls the `processPrompt()` method, which formats the user input as a JSON message and sends it to the PTY using `this.ptyHandle.sendInput()` as demonstrated above.
Formatted user messages look like this:
```json
{"type": "user", "message": {"role": "user", "content": "create a simple web server"}}
```
The agent responds with streaming JSON messages. Tool calls arrive as fragments:
```json
{"type": "message", "message_type": "approval_request_message", "tool_call": {"tool_call_id": "call_123", "name": "Bash", "arguments": "{\"command\": \"python3 -m http.server 8080\"}"}}
```
These JSON fragments are parsed by the `handleParsedMessage()` method. When multiple consecutive fragments are received for the same tool call, they are combined into a single tool call object. When a tool call or message is finished, the result is formatted and displayed to the user.
#### Clean up
When you exit the main program, the Daytona sandbox and all files are automatically deleted.
**Key advantages:**
- Secure, isolated execution in Daytona sandboxes
- Stateful agents with persistent memory across sessions
- Full Letta Code capabilities including file operations and shell commands
- Agents can be viewed in [Letta's Agent Development Environment](https://app.letta.com/)
- Automatic preview link generation for hosted services
- Multi-language and full-stack support
- Simple setup and automatic cleanup
# Fix Bugs Automatically With AG2 and Daytona
This guide demonstrates how to use `DaytonaCodeExecutor` for [AG2](https://ag2.ai/) to build a multi-agent system that automatically fixes broken code in a secure sandbox environment. The executor enables agents to run Python, JavaScript, TypeScript, and Bash code within isolated Daytona sandboxes, with no risk to your local machine.
In this example, we build a bug fixer that takes broken code as input, analyzes the bug, proposes a fix, and verifies it by actually executing the code in a Daytona sandbox. If the fix fails, the agent sees the error output and retries with a different approach, continuing until the code passes or the maximum number of attempts is reached.
---
### 1. Workflow Overview
You provide broken code. The `bug_fixer` agent (LLM) analyzes it and proposes a fix wrapped in a fenced code block. The `code_executor` agent extracts the code block and runs it in a Daytona sandbox. If execution fails, the bug fixer sees the full error output and tries again. Once the code passes, the agents terminate and the sandbox is automatically deleted.
The key benefit: every fix attempt is verified by actually running the code — not just reviewed by the LLM.
### 2. Project Setup
#### Clone the Repository
Clone the Daytona repository and navigate to the example directory:
```bash
git clone https://github.com/daytona/guides
cd guides/python/ag2/bug-fixer-agent/openai
```
#### Install Dependencies
:::note[Python Version Requirement]
This example requires **Python 3.10 or higher**. It is recommended to use a virtual environment (e.g., `venv` or `poetry`) to isolate project dependencies.
:::
Install the required packages for this example:
```bash
pip install "ag2[daytona,openai]" python-dotenv
```
The packages include:
- `ag2[daytona,openai]`: AG2 with the Daytona code executor and OpenAI model support
- `python-dotenv`: Loads environment variables from a `.env` file
#### Configure Environment
Get your API keys and configure your environment:
1. **Daytona API key:** Get it from [Daytona Dashboard](https://app.daytona.io/dashboard/keys)
2. **OpenAI API key:** Get it from [OpenAI Platform](https://platform.openai.com/api-keys)
Create a `.env` file in your project directory:
```bash
DAYTONA_API_KEY=dtn_***
OPENAI_API_KEY=sk-***
```
### 3. Understanding the Core Components
Before diving into the implementation, let's understand the key components:
#### AG2 ConversableAgent
`ConversableAgent` is AG2's general-purpose agent. Each agent can be configured as either an LLM agent (with a model and system prompt) or a non-LLM agent (`llm_config=False`) that responds through registered reply handlers — in our case, code execution via `code_execution_config`. The two agents communicate by passing messages back and forth until a termination condition is met.
#### DaytonaCodeExecutor
`DaytonaCodeExecutor` implements the AG2 `CodeExecutor` protocol. When used as a context manager, it creates a Daytona sandbox on entry and automatically deletes it on exit. It reuses the same sandbox across all code executions within the session, extracting and running fenced code blocks from agent messages. The language is inferred from the code block tag (` ```python `, ` ```javascript `, ` ```typescript `).
### 4. Implementation
#### Step 1: Imports and environment
```python
import os
from autogen import ConversableAgent, LLMConfig
from autogen.coding import DaytonaCodeExecutor
from dotenv import load_dotenv
load_dotenv()
```
#### Step 2: Bug fixer system prompt
The system prompt drives the iterative fix loop. It tells the agent which languages are supported, instructs it to wrap fixes in fenced code blocks, and separates the fix message from the TERMINATE signal so the executor always runs the code before the session ends:
```python
BUG_FIXER_SYSTEM_MESSAGE = """You are an expert bug fixer. You support Python, JavaScript, and TypeScript.
If asked to fix code in any other language, refuse and explain which languages are supported.
When given broken code:
1. Analyze the bug carefully and identify the root cause
2. Write the complete fixed code in a fenced code block using the correct language tag
3. Always include assertions or print statements at the end to verify the fix works
4. If your previous fix didn't work, analyze the error output and try a different approach
5. Once the code runs successfully, reply with just the word TERMINATE — never in the same message as a code block
Always wrap your code in fenced code blocks (```python, ```javascript, or ```typescript). Never explain without providing fixed code.
Never include TERMINATE in a message that contains a code block.
"""
```
:::note[Why separate TERMINATE from the code block?]
AG2 checks `is_termination_msg` on every incoming message. If `bug_fixer` includes `TERMINATE` in the same message as a code block, the conversation ends before `code_executor` has a chance to extract and run the fix. Keeping them in separate messages ensures every proposed fix is actually executed in the sandbox before the session terminates.
The empty content check handles a second termination case: when `bug_fixer` refuses to fix code in an unsupported language, it sends a refusal message with no code block. `code_executor` has nothing to execute and sends back an empty reply. Without the empty check, the conversation would loop until `max_turns` is exhausted — checking for empty content stops it immediately.
:::
#### Step 3: Create the agents
```python
def fix_bug(broken_code: str, error_description: str = "") -> None:
llm_config = LLMConfig(
{
"model": "gpt-4o-mini",
"api_key": os.environ["OPENAI_API_KEY"],
}
)
with DaytonaCodeExecutor(timeout=60) as executor:
bug_fixer = ConversableAgent(
name="bug_fixer",
system_message=BUG_FIXER_SYSTEM_MESSAGE,
llm_config=llm_config,
code_execution_config=False,
is_termination_msg=lambda x: (
"TERMINATE" in (x.get("content") or "") or not (x.get("content") or "").strip()
),
)
code_executor = ConversableAgent(
name="code_executor",
llm_config=False,
code_execution_config={"executor": executor},
)
```
`DaytonaCodeExecutor` is used as a context manager so the sandbox is automatically cleaned up when `fix_bug` returns. `bug_fixer` owns the LLM reasoning; `code_executor` owns sandbox execution and never calls the LLM itself (`llm_config=False`).
The optional `error_description` parameter can be used to pass additional context about the failure — for example, a stack trace, a known symptom, or a hint about the cause. In the examples below we leave it empty, as the agent is capable of identifying and fixing the bugs purely from the assertion output.
#### Step 4: Start the conversation
```python
message = f"Fix this broken code:\n\n\n{broken_code}\n"
if error_description:
message += f"\n\nError: {error_description}"
code_executor.run(
recipient=bug_fixer,
message=message,
max_turns=8,
).process()
```
`code_executor` initiates the chat because it owns the problem — the broken code. `bug_fixer` receives it as its first message, proposes a fix, and waits for execution results.
:::tip[Inspecting the fix run]
Assign the return value of `run()` before calling `process()` to access more details about the session:
```python
response = code_executor.run(recipient=bug_fixer, message=message, max_turns=8)
response.process()
response.messages # full message exchange between agents
response.cost # token usage and cost breakdown per model
response.summary # conversation summary (requires summary_method to be set)
```
:::
### 5. Running the Example
The complete example ships with three broken code snippets, one per language:
**Example 1 — Python: postfix evaluator with swapped operands**
The subtraction and division operators pop two values from the stack but apply them in reverse order, producing wrong results for non-commutative operations.
```python
elif token == '-':
stack.append(b - a) # Bug: reversed — should be a - b
elif token == '/':
stack.append(b // a) # Bug: reversed — should be a // b
```
**Example 2 — JavaScript: wrong concatenation order in run-length encoder**
The character and count are concatenated in the wrong order in two places, producing `"a2b3c2"` instead of `"2a3b2c"`.
```javascript
result += str[i - 1] + count; // Bug: should be count + str[i - 1]
result += str[str.length - 1] + count; // Bug: should be count + str[str.length - 1]
```
**Example 3 — TypeScript: `Math.min` instead of `Math.max` in Kadane's algorithm**
Both calls use `Math.min` instead of `Math.max`, causing the algorithm to track the most negative subarray sum instead of the most positive.
```typescript
currentSum = Math.min(currentSum + nums[i], nums[i]); // Bug: should be Math.max
maxSum = Math.min(maxSum, currentSum); // Bug: should be Math.max
```
Run all examples:
```bash
python main.py
```
#### Expected output
The following shows the full agent conversation for Example 1 (Python postfix evaluator):
````
============================================================
Example 1: Python — Postfix Expression Evaluator Bug
============================================================
code_executor (to bug_fixer):
Fix this broken code:
def eval_postfix(expression):
stack = []
for token in expression.split():
if token.lstrip('-').isdigit():
stack.append(int(token))
else:
b = stack.pop()
a = stack.pop()
if token == '+':
stack.append(a + b)
elif token == '-':
stack.append(b - a)
elif token == '*':
stack.append(a * b)
elif token == '/':
stack.append(b // a)
return stack[0]
assert eval_postfix("3 4 +") == 7
assert eval_postfix("10 3 -") == 7, f"Got {eval_postfix('10 3 -')}"
assert eval_postfix("12 4 /") == 3, f"Got {eval_postfix('12 4 /')}"
assert eval_postfix("2 3 4 * +") == 14
print("All postfix tests passed!")
--------------------------------------------------------------------------------
>>>>>>>> USING AUTO REPLY...
bug_fixer (to code_executor):
```python
def eval_postfix(expression):
stack = []
for token in expression.split():
if token.lstrip('-').isdigit():
stack.append(int(token))
else:
b = stack.pop()
a = stack.pop()
if token == '+':
stack.append(a + b)
elif token == '-':
stack.append(a - b) # Fixed order of operands for subtraction
elif token == '*':
stack.append(a * b)
elif token == '/':
stack.append(a // b) # Fixed order of operands for division
return stack[0]
assert eval_postfix("3 4 +") == 7
assert eval_postfix("10 3 -") == 7, f"Got {eval_postfix('10 3 -')}"
assert eval_postfix("12 4 /") == 3, f"Got {eval_postfix('12 4 /')}"
assert eval_postfix("2 3 4 * +") == 14
print("All postfix tests passed!")
```
--------------------------------------------------------------------------------
>>>>>>>> USING AUTO REPLY...
>>>>>>>> EXECUTING CODE BLOCK (inferred language is python)...
code_executor (to bug_fixer):
exitcode: 0 (execution succeeded)
Code output: All postfix tests passed!
--------------------------------------------------------------------------------
>>>>>>>> USING AUTO REPLY...
bug_fixer (to code_executor):
TERMINATE
````
The agent correctly identified both reversed operand bugs from the assertion failure output alone and resolved them in a single attempt, adding its own `# Fixed order of operands` comments to the corrected lines.
#### How the message loop works
`recipient=bug_fixer` in `run()` is what connects the two agents. AG2 sets up a managed back-and-forth loop between them — after each reply, the message is automatically forwarded to the other agent. The agents have no direct reference to each other outside of that call.
Tracing the session above step by step:
1. `code_executor.run(recipient=bug_fixer, ...)` — AG2 starts the loop and `code_executor` sends the broken code as plain text to `bug_fixer`. Nothing is executed yet.
2. `bug_fixer` (LLM) analyzes the code and replies with the fix wrapped in a ` ```python ` block.
3. AG2 calls `_generate_code_execution_reply_using_executor` on `code_executor` — a reply method registered automatically when `code_execution_config` is set. It scans `bug_fixer`'s last message for fenced code blocks, extracts the block, and calls `DaytonaCodeExecutor.execute_code_blocks()`.
4. Daytona runs the code in the sandbox and returns the exit code and output.
5. AG2 forwards the result (`exitcode: 0 (execution succeeded)\nCode output: All postfix tests passed!`) back to `bug_fixer` as `code_executor`'s reply.
6. `bug_fixer` sees the successful output and replies with `TERMINATE`.
7. AG2 checks `is_termination_msg` on the incoming message — returns `True`, conversation stops, the sandbox is deleted.
Note that the original broken code is never executed — only `bug_fixer`'s proposed fix goes into Daytona.
:::note
`>>>>>>>> USING AUTO REPLY...` is printed by AG2 before each automatic agent reply to indicate no human intervention is taking place.
:::
### 6. Complete Code
````python
import os
from autogen import ConversableAgent, LLMConfig
from autogen.coding import DaytonaCodeExecutor
from dotenv import load_dotenv
load_dotenv()
BUG_FIXER_SYSTEM_MESSAGE = """You are an expert bug fixer. You support Python, JavaScript, and TypeScript.
If asked to fix code in any other language, refuse and explain which languages are supported.
When given broken code:
1. Analyze the bug carefully and identify the root cause
2. Write the complete fixed code in a fenced code block using the correct language tag
3. Always include assertions or print statements at the end to verify the fix works
4. If your previous fix didn't work, analyze the error output and try a different approach
5. Once the code runs successfully, reply with just the word TERMINATE — never in the same message as a code block
Always wrap your code in fenced code blocks (```python, ```javascript, or ```typescript). Never explain without providing fixed code.
Never include TERMINATE in a message that contains a code block.
"""
def fix_bug(broken_code: str, error_description: str = "") -> None:
llm_config = LLMConfig(
{
"model": "gpt-4o-mini",
"api_key": os.environ["OPENAI_API_KEY"],
}
)
with DaytonaCodeExecutor(timeout=60) as executor:
bug_fixer = ConversableAgent(
name="bug_fixer",
system_message=BUG_FIXER_SYSTEM_MESSAGE,
llm_config=llm_config,
code_execution_config=False,
is_termination_msg=lambda x: (
"TERMINATE" in (x.get("content") or "") or not (x.get("content") or "").strip()
),
)
code_executor = ConversableAgent(
name="code_executor",
llm_config=False,
code_execution_config={"executor": executor},
)
message = f"Fix this broken code:\n\n\n{broken_code}\n"
if error_description:
message += f"\n\nError: {error_description}"
code_executor.run(
recipient=bug_fixer,
message=message,
max_turns=8,
).process()
if __name__ == "__main__":
# Example 1: Python — swapped operands in postfix expression evaluator
broken_postfix = """\
def eval_postfix(expression):
stack = []
for token in expression.split():
if token.lstrip('-').isdigit():
stack.append(int(token))
else:
b = stack.pop()
a = stack.pop()
if token == '+':
stack.append(a + b)
elif token == '-':
stack.append(b - a)
elif token == '*':
stack.append(a * b)
elif token == '/':
stack.append(b // a)
return stack[0]
assert eval_postfix("3 4 +") == 7
assert eval_postfix("10 3 -") == 7, f"Got {eval_postfix('10 3 -')}"
assert eval_postfix("12 4 /") == 3, f"Got {eval_postfix('12 4 /')}"
assert eval_postfix("2 3 4 * +") == 14
print("All postfix tests passed!")
"""
print("=" * 60)
print("Example 1: Python — Postfix Expression Evaluator Bug")
print("=" * 60)
fix_bug(broken_postfix, "")
# Example 2: JavaScript — wrong concatenation order in run-length encoder
broken_js = """\
function encode(str) {
if (!str) return '';
let result = '';
let count = 1;
for (let i = 1; i < str.length; i++) {
if (str[i] === str[i - 1]) {
count++;
} else {
result += str[i - 1] + count;
count = 1;
}
}
result += str[str.length - 1] + count;
return result;
}
console.assert(encode("aabbbcc") === "2a3b2c", `Expected "2a3b2c", got "${encode("aabbbcc")}"`);
console.assert(encode("abcd") === "1a1b1c1d", `Expected "1a1b1c1d", got "${encode("abcd")}"`);
console.log("All encoding tests passed!");
"""
print("\n" + "=" * 60)
print("Example 2: JavaScript — Run-Length Encoder Bug")
print("=" * 60)
fix_bug(broken_js, "")
# Example 3: TypeScript — Math.min instead of Math.max in Kadane's algorithm
broken_ts = """\
function maxSubarray(nums: number[]): number {
let maxSum = nums[0];
let currentSum = nums[0];
for (let i = 1; i < nums.length; i++) {
currentSum = Math.min(currentSum + nums[i], nums[i]);
maxSum = Math.min(maxSum, currentSum);
}
return maxSum;
}
console.assert(maxSubarray([-2, 1, -3, 4, -1, 2, 1, -5, 4]) === 6,
`Expected 6, got ${maxSubarray([-2, 1, -3, 4, -1, 2, 1, -5, 4])}`);
console.assert(maxSubarray([1]) === 1,
`Expected 1, got ${maxSubarray([1])}`);
console.assert(maxSubarray([5, 4, -1, 7, 8]) === 23,
`Expected 23, got ${maxSubarray([5, 4, -1, 7, 8])}`);
console.log("All max subarray tests passed!");
"""
print("\n" + "=" * 60)
print("Example 3: TypeScript — Max Subarray Bug")
print("=" * 60)
fix_bug(broken_ts, "")
````
**Key advantages of this approach:**
- **Execution-verified fixes:** Every proposed fix is actually run in a sandbox — the agent only terminates when the code passes, not just when it looks correct
- **Secure execution:** Fix attempts run in isolated Daytona sandboxes, not on your machine
- **Multi-language support:** Python, JavaScript, TypeScript, and Bash — language is inferred automatically from the LLM's fenced code block
- **Iterative refinement:** If a fix fails, the agent sees the full error output and retries automatically
- **Automatic cleanup:** The sandbox is deleted as soon as `fix_bug` returns, regardless of outcome
### 7. API Reference
For the complete API reference of `DaytonaCodeExecutor`, including all configuration options and supported parameters, see the [DaytonaCodeExecutor documentation](https://docs.ag2.ai/latest/docs/api-reference/autogen/coding/DaytonaCodeExecutor/).
# Build an Autonomous Bug-Fix Agent with Flue and Daytona
This guide builds an autonomous bug-fix agent using [Flue](https://flueframework.com/) and [Daytona](https://www.daytona.io/) sandboxes. Given a GitHub issue, the agent reproduces the bug with a failing test, implements the minimal fix, runs the full test suite, and opens a real pull request.
A sandbox is essential for this workflow. The agent clones unknown code, installs unknown dependencies, and executes the project's test suite — operations that need strict isolation from your host. Daytona provisions a fresh isolated environment for every run and tears it down on completion, so an untrusted repository can never affect your host.
---
### 1. Workflow Overview
You point the agent at an open issue on any GitHub repository. The agent provisions a Daytona sandbox, clones the repo into it, then executes a strict **Reproduce → Fix → Verify → PR** workflow. When it's done, it returns the URL of a real pull request you can review in the GitHub UI.
A successful run against `vercel/ms` issue #284 looks like this in the `flue dev` terminal:
```
[bug-fix] target: your-username/your-fork#284 (model: anthropic/claude-sonnet-4-6)
[bug-fix] sandbox ready (id: a44a184e-cf0a-4407-bb1a-02f1b8000466)
[bug-fix] installing gh CLI in sandbox...
[bug-fix] commits will be authored as Your Name <12345+your-username@users.noreply.github.com>
[bug-fix] cloning your-username/your-fork into sandbox...
[bug-fix] detected package manager: pnpm
[bug-fix] installing pnpm...
[bug-fix] installing project dependencies...
[bug-fix] resolving issue source: vercel/ms
[bug-fix] fetching issue #284 from vercel/ms...
[bug-fix] uploading skill into sandbox + excluding it from git...
[bug-fix] running TDD workflow (reproduce → fix → PR)...
[bug-fix] PR opened: https://github.com/your-username/your-fork/pull/1
[bug-fix] branch: flue/fix-issue-284
[bug-fix] files changed: src/index.ts, src/parse.test.ts
[bug-fix] tearing down agents + sandbox...
```
The four-phase TDD work (Understand → Reproduce → Fix → Pull Request) happens entirely inside the LLM's session in the sandbox, so it doesn't surface in the dev-server log line by line. To see those events streamed live, switch to the SSE invocation shown later in [How `bug-fix.ts` is actually invoked](#how-bug-fixts-is-actually-invoked).
The HTTP response body returned to your `curl` is the structured result the agent emits:
```json
{
"result": {
"branch": "flue/fix-issue-284",
"prUrl": "https://github.com/your-username/your-fork/pull/1",
"testFile": "src/parse.test.ts",
"filesChanged": ["src/index.ts", "src/parse.test.ts"],
"summary": "The parse() regex only matched plain decimal numbers in the value group (`-?\\d*\\.?\\d+`), so when format() produced scientific notation (e.g. `5.696545792019405e+297y`) via JavaScript's default number serialisation for very large Math.round() results, parse() returned NaN; the fix extends the value capture group with an optional exponent part (`(?:e[+-]?\\d+)?`) so scientific notation is accepted transparently."
}
}
```
### 2. Project Setup
#### Clone the Repository
Clone the Daytona [repository](https://github.com/daytona/guides.git) and navigate to the example directory:
```bash
git clone https://github.com/daytona/guides.git
cd guides/typescript/flue
```
#### Fork a Demo Target
You need a target repository to demo against. We recommend [`vercel/ms`](https://github.com/vercel/ms), the well-known millisecond conversion utility. It's small (one ~244-line source file), uses Jest for tests, has an MIT license, and ships with real open issues. Fork it so the agent can push branches and open PRs against your copy:
```bash
gh repo fork vercel/ms --clone=false
```
The agent will operate on your fork (referred to in this guide as `your-username/your-fork`, where `your-fork` is whatever you named it), so any branches and pull requests it creates land on your fork, never upstream.
#### Configure Environment
Copy `.env.example` to `.env` and fill in your keys:
```bash
cp .env.example .env
```
| Variable | Required | Source |
|---|---|---|
| `DAYTONA_API_KEY` | yes | [Daytona Dashboard](https://app.daytona.io/dashboard/keys) |
| `ANTHROPIC_API_KEY` | yes | For this agent's default model, `anthropic/claude-sonnet-4-6`. Required only if you don't override `MODEL`. (Flue itself has no default; `bug-fix.ts` picks one.) |
| `GITHUB_TOKEN` | yes | A Personal Access Token with `repo` scope ([create one](https://github.com/settings/tokens)) |
| `MODEL` | no | Override this agent's default model. Any `provider/model-id` recognized by [`@mariozechner/pi-ai`](https://www.npmjs.com/package/@mariozechner/pi-ai). Examples: `anthropic/claude-opus-4-7`, `openai/gpt-5.5` |
| `DEMO_REPO` | no¹ | Default target fork in `/` form (e.g. `your-username/your-fork`). Used when the webhook payload omits `repo` |
| `DEMO_ISSUE` | no¹ | Default issue number (e.g. `284`). Used when the webhook payload omits `issueNumber` |
| `ISSUE_REPO` | no | Override the issue source, in `/` form. By default the agent auto-detects the upstream parent of `DEMO_REPO`; set this if `DEMO_REPO` is not a fork or you want to point at a different repo |
¹ Either set both `DEMO_REPO` / `DEMO_ISSUE` in `.env` and trigger with an empty body, **or** omit them and pass `repo` / `issueNumber` on every webhook call. Payload always wins over `.env`.
:::note[Why the agent reads issues from one repo and PRs against another]
GitHub forks have **issues disabled by default** and never inherit issues from the upstream. So the agent reads the issue from the fork's upstream parent (auto-detected via `gh repo view --json parent`) but pushes its branch and opens the PR against your fork. This keeps the demo isolated to your account: no spam to `vercel/ms` maintainers, no extra setup on your side. Use `ISSUE_REPO` to override the auto-detection if your `repo` isn't a fork.
:::
:::caution[Token scope]
The `GITHUB_TOKEN` is passed into the sandbox so `gh` can clone, push, and open PRs from inside it. Use a [classic PAT](https://github.com/settings/tokens/new) with the `repo` scope. The token can be revoked at any time from your GitHub settings once you're done with the demo.
:::
#### Install Dependencies
:::note[Node.js version]
Flue requires Node.js 22 or newer. Confirm your version with `node --version` before continuing.
:::
```bash
npm install
```
#### Start the Agent Server
```bash
npm run dev
```
Flue boots a webhook server on port `3583` and discovers the `bug-fix` agent automatically:
```
[flue] Starting dev server (target: node)
[flue] Target: node
[flue] Found 1 role(s): test-driven-developer
[flue] Found 1 agent(s): bug-fix
[flue] Webhook agents: bug-fix
[flue] Built: dist/server.mjs
[flue] Server: http://localhost:3583
[flue] Try: curl -X POST http://localhost:3583/agents/bug-fix/test-1 \
-H 'Content-Type: application/json' -d '{}'
[flue] Press Ctrl+C to stop
```
#### Trigger the Agent
There are three equivalent ways to trigger the agent. Pick whichever fits your workflow.
**Option A: drive everything from `.env`** (default sync mode). With `DEMO_REPO=your-username/your-fork` and `DEMO_ISSUE=` set in `.env`, fire an empty payload:
```bash
curl -X POST http://localhost:3583/agents/bug-fix/run-1 \
-H "Content-Type: application/json" \
-d '{}'
```
**Option B: pass the target per call** (default sync mode). Override `.env` (or skip it entirely) by sending the target in the payload:
```bash
curl -X POST http://localhost:3583/agents/bug-fix/run-1 \
-H "Content-Type: application/json" \
-d '{
"repo": "your-username/your-fork",
"issueNumber":
}'
```
Replace `your-username/your-fork` with your fork's slug and `` with the issue number you want to target.
The `flue dev` terminal shows the orchestrator's setup logs in real time. When the agent finishes, the response body contains the structured result.
**Option C: one-shot with live tool tracing** (recommended when iterating or debugging):
Stop `flue dev` (or leave it; doesn't matter) and run:
```bash
npm run run
```
That maps to `flue run bug-fix --target node --id run-1 --env .env --payload '{}'`. Unlike Options A and B, this **builds and spawns its own ephemeral server**, POSTs with `Accept: text/event-stream`, and decorates every agent event (`tool:start`, `tool:done`, the LLM's reasoning text) into a readable progress line so you can watch the LLM work in real time. The final structured result is printed at the end, ready to pipe into downstream tooling. See the [Example Walkthrough](#4-example-walkthrough) below for a real `npm run run` trace.
Use Options A/B for quiet production-style runs against a long-lived `flue dev` server. Use Option C when you want to watch the LLM's tool calls tool-by-tool.
:::tip[What if the issue you target no longer exists?]
If the issue you point the agent at has been closed, deleted, or never existed, the run will fail honestly instead of fabricating a fix. Two failure modes:
- **Issue not found**: the setup phase's `gh issue view` returns a non-zero exit code, and the agent throws before reaching the LLM. Pick a different issue and rerun.
- **Issue is already fixed**: the LLM proceeds through Phase 1, then in Phase 2 writes a "failing" test that actually passes. The `test-driven-developer` role's hard rule (_"a test that doesn't fail isn't a reproduction"_) makes the agent stop and return early with `prUrl: ""` and a `summary` explaining the situation. No PR is opened.
Update `DEMO_ISSUE` in `.env` (or pass `issueNumber` in the payload) and try another open issue.
:::
### 3. Understanding the Architecture
This example splits responsibility between TypeScript and Markdown, the idiomatic Flue pattern. Plumbing (sandbox lifecycle, payload validation, structured outputs) lives in `.ts`; the agent's reasoning and workflow live in `.md`.
The directory layout is **defined by Flue**, not by us. Flue's CLI looks for a `.flue/` workspace at the project root containing `agents/`, `connectors/`, and `roles/` subdirectories, and discovers skills under `.agents/skills//SKILL.md`. We just populate those well-known locations:
```
guides/typescript/flue/
├── .flue/ # Flue workspace (convention)
│ ├── agents/ # one .ts file per agent (Flue scans this dir)
│ │ └── bug-fix.ts # orchestrator
│ ├── connectors/ # connector files referenced by agents
│ │ └── daytona.ts # Daytona → Flue SandboxFactory adapter
│ └── roles/ # role markdown files (subagent personas)
│ └── test-driven-developer.md
└── .agents/ # Flue skill workspace (convention)
└── skills/
└── bug-fix/ # skill folder name = skill identifier
└── SKILL.md # actual TDD workflow logic
```
So `session.skill('bug-fix', { ... })` in our agent code maps directly to `.agents/skills/bug-fix/SKILL.md`: Flue resolves the skill by folder name by default. If you set a `name:` field in the file's frontmatter, that takes precedence over the folder name — useful when you want to keep the directory layout but rename the skill.
#### The Daytona Connector
Daytona is a first-class Flue connector. The canonical way to install it is to pipe Flue's connector registry to your AI coding agent:
```bash
flue add daytona | claude
# or: opencode | codex | cursor-agent | pi
```
This requires an AI coding-agent CLI to already be installed and authenticated locally — pick whichever one you already use (`claude`, `opencode`, `codex`, `cursor-agent`, `pi`, etc.). If you don't have any installed yet, this guide ships the resulting connector pre-built so you can skip the `flue add` step entirely.
`flue add daytona` fetches the official installation instructions from `https://flueframework.com/cli/connectors/daytona.md` and writes them to stdout. Your AI agent reads those instructions and writes the connector adapter (`.flue/connectors/daytona.ts`) into your project automatically. No manual file copying, no version drift.
This guide ships the resulting `.flue/connectors/daytona.ts` pre-built so the demo runs without an extra step, but the file is byte-identical to what `flue add daytona | ` produces. Once installed, you import the connector and pass it to `init()`:
```typescript
import { Daytona } from '@daytona/sdk'
import { daytona } from '../connectors/daytona'
const client = new Daytona({ apiKey: env.DAYTONA_API_KEY })
const sandbox = await client.create()
const agent = await init({
// cleanup: true arms sandbox.delete() to fire on agent.destroy()
// Flue does NOT auto-destroy on handler return; see Section 5.
sandbox: daytona(sandbox, { cleanup: true }),
model: 'anthropic/claude-sonnet-4-6',
})
```
The user owns the Daytona client lifecycle (you decide how the sandbox is created, reused, or cleaned up); Flue just adapts it for agent use. The `cleanup: true` option **arms** a `sandbox.delete()` callback that fires when `agent.destroy()` is called but Flue does NOT auto-destroy on handler return. The orchestrator must explicitly call `destroy()`, which our `try/finally` does (covered in [Section 5: Cleanup](#5-cleanup)).
#### The Orchestrator
The agent file (`.flue/agents/bug-fix.ts`) is small on purpose. It provisions the sandbox, prepares the environment, and hands off to a skill that does the real work. Here's the structural shape — for the full file (env validation, slug-format checks, package-manager auto-install, gh/git config, the `try/finally` cleanup), open `.flue/agents/bug-fix.ts` from the guide directory you cloned earlier:
```typescript
// First agent: setup phase. cleanup: true arms sandbox.delete() on destroy().
const setupAgent = await init({
sandbox: daytona(sandbox, { cleanup: true }),
model,
})
const setup = await setupAgent.session()
// ... installing gh, setting up git, cloning the fork, installing deps,
// fetching the issue body, uploading the SKILL.md ...
// Second agent: shares the same sandbox, but rooted in the cloned project dir
// so Flue discovers our SKILL.md from `.agents/skills/bug-fix/SKILL.md`.
const projectAgent = await init({
id: `bug-fix-${issueNumber}`,
sandbox: daytona(sandbox), // no cleanup option — setupAgent owns teardown
cwd: projectDir,
model,
})
const session = await projectAgent.session()
return await session.skill('bug-fix', {
args: { issueNumber, issueData, repo, issueRepo, packageManager },
role: 'test-driven-developer',
result: ResultSchema,
})
```
A few details worth highlighting:
- **Two agents, one sandbox.** The setup agent installs `gh`, configures auth, clones the repo, and installs dependencies. A second agent (given a different `id` and `cwd`) operates inside the cloned repo and discovers our `bug-fix` skill from `.agents/skills/bug-fix/SKILL.md`, which the orchestrator uploads into the cloned worktree just before init. Both agents share the same Daytona sandbox. The distinct `id` matters: a fresh `id` opens a fresh Flue session — see [What `id` actually means: sessions](#what-id-actually-means-sessions) for the full lifecycle.
- **No `AGENTS.md` upload.** Flue would happily read an `AGENTS.md` from the session cwd and prepend it to every system prompt, but that file lives at the cloned repo's root and uploading our own would overwrite the target's `AGENTS.md` if it has one. Every guardrail we'd put there (TDD discipline, minimal change, match host code style) is already covered by the `test-driven-developer` role and the `bug-fix` skill body, so the harness ships nothing at the worktree root.
- **`.git/info/exclude` keeps the worktree clean.** After uploading `SKILL.md` to `.agents/skills/bug-fix/`, the orchestrator appends `.agents/` to the cloned repo's `.git/info/exclude` (git's local-only ignore — does NOT modify the target's `.gitignore`). The harness scaffolding stays invisible to `git status` and never accidentally lands in a commit.
- **Two repos, one workflow.** `repo` is the user's fork (where branches and the PR land); `issueRepo` is where the issue lives (the upstream parent, auto-detected via `gh repo view --json parent` because GitHub disables issues on forks).
- **Package-manager auto-detection.** The setup phase detects the package manager from the project's lockfile, installs it if missing, and passes the name into the skill so the LLM uses the right test command.
- **Structured input and output.** The `PayloadSchema` validates the incoming HTTP body with [Valibot](https://valibot.dev/), and `ResultSchema` forces the agent to return a typed `{ branch, prUrl, testFile, filesChanged, summary }` object you can pipe into downstream automation.
- **Skills, not prompts.** Instead of cramming the TDD workflow into a string, the agent calls a named skill and supplies a role. The actual logic lives in markdown.
#### The Skill (Where the Real Logic Lives)
`.agents/skills/bug-fix/SKILL.md` defines the TDD workflow as four strict phases. The agent is required to run them in order, and it cannot proceed to the next phase without producing concrete evidence (a read, a failing test, a passing test, a commit):
```markdown
## Phase 1: Understand
Read the issue body. Identify expected vs. actual behavior.
Inspect package.json, README.md, AGENTS.md. Identify the test framework.
Read the source file(s) most likely involved. Read at least one test file.
## Phase 2: Reproduce
Create branch flue/fix-issue-{{issueNumber}}.
Write a single, focused test that asserts the expected behavior.
Run the test command. The test MUST fail.
## Phase 3: Fix
Make the minimal code change required to make the failing test pass.
Run the full test suite. All tests MUST pass.
## Phase 4: Pull Request
Commit with `fix: (#{{issueNumber}})`.
Push the branch to the user's fork.
Open a PR via `gh pr create` with reproduction + verification output.
```
Because the workflow is markdown, you can tighten it (add a "no `--force` push" rule), loosen it (allow multi-file fixes), or fork it for a different language without touching TypeScript.
#### The Role
`.flue/roles/test-driven-developer.md` defines the agent's persona: a disciplined contributor who treats the target repository as someone else's project. The role is referenced in the skill call (`role: 'test-driven-developer'`) and shapes how the agent makes tradeoffs (minimal change, match host code style, never disable existing tests).
#### How `bug-fix.ts` is actually invoked
Nothing in our code calls our agent's default export directly; Flue's CLI does. Here's the full chain from `npm run dev` to `handler(ctx)`:
**Build time (`flue dev` startup):**
1. `flue dev --target node` calls `dev()` from `@flue/sdk`, which runs `build()`.
2. `build()` does `fs.readdirSync('.flue/agents')` and keeps any entry matching `/\.(ts|js|mts|mjs)$/`. Our `bug-fix.ts` matches → agent name is `bug-fix` (filename without extension).
3. For each agent file, Flue uses the TypeScript AST to find the static `export const triggers = {...}` declaration, validating that `webhook` is `true` or `false`. Our `triggers = { webhook: true }` registers the agent for HTTP access.
4. The build generates a [Hono](https://hono.dev/) server entry that imports each agent's default export, then esbuilds it to `dist/server.mjs`.
5. The dev server spawns `node dist/server.mjs` with `PORT=3583` and `FLUE_MODE=local`.
**Request time (when you `curl`):**
The generated server mounts a single dynamic route, `POST /agents/:name/:id`. When a request arrives:
1. Validate the method, agent name, and webhook accessibility.
2. Parse the JSON body → `payload` (defaults to `{}` for empty bodies).
3. **Pick a response mode based on headers:**
| Headers sent by client | Server behavior | Status |
|---|---|---|
| `Content-Type: application/json` (default) | Wait for handler, return `{ "result": }` | `200` |
| `Accept: text/event-stream` | Stream SSE events (channel names are `tool_start`, `text_delta`, …, finally `result`) | `200` |
| `x-webhook: true` | Fire-and-forget; run handler in background | `202` |
The CLI's pretty-print form (`[flue] tool:start`, `[flue] tool:done`) you see in `flue run` output is `flue run`'s own decoration; the underlying SSE channel names use underscores (`tool_start`, `tool_end`). If you're consuming the SSE stream from your own client, listen for the underscore form.
4. Construct a `FlueContext` (`{ id, payload, env, init }`) and invoke our default export: `handler(ctx)`.
5. Return whatever the handler resolves with, in whichever mode was selected.
So when you run our `curl` example without special headers, you hit the **sync mode**: the connection stays open until the agent finishes (PR opened), then the server returns `{ "result": { branch, prUrl, ... } }`.
If you want to watch the agent's progress live, switch to SSE:
```bash
curl -N -X POST http://localhost:3583/agents/bug-fix/run-1 \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{}'
```
Or use Flue's one-shot CLI invoker, which handles SSE for you and prints the result to stdout:
```bash
npm run run
# equivalent to:
# flue run bug-fix --target node --id run-1 --env .env --payload '{}'
```
`flue run` builds, spawns the server, POSTs with `Accept: text/event-stream`, streams events to stderr, prints the final result to stdout, and shuts the server down. Perfect for CI.
#### What `id` actually means: sessions
The `` segment in `POST /agents/bug-fix/` is not just a label. It identifies a Flue **session**: the persistent message history and conversation metadata that `agent.session()` opens inside your handler.
```
POST /agents/bug-fix/run-1 ← = "run-1" → session "run-1" for the bug-fix agent
POST /agents/bug-fix/run-2 ← different → fresh session, no shared state
POST /agents/bug-fix/run-1 ← same as before → REUSES session "run-1"
```
**Same `` reused, what actually happens:**
1. Your handler function runs **from the top, every time** (it's just a function, not auto-resumed).
2. `client.create()` makes a **new** Daytona sandbox each call (because our code calls it unconditionally).
3. But `await agent.session()` inside the handler resolves to the **same Flue session object** as the previous call with that id, so the LLM sees the previous run's message history as context for this run.
So same-id reuse persists the **conversation**, not the **sandbox**. For a chat-style agent that's exactly what you want; for our one-shot bug-fix agent, it's mostly noise (the LLM might short-circuit with "I already analyzed this") and ends up confusing things.
**Practical guidance for this guide**:
- One unique `` per run (`run-1`, `run-2`, or `$(uuidgen)`). Treat each invocation as fresh.
- Pick a stable `` only if you want resumability (e.g., agent crashed mid-fix and you want the LLM to remember its prior reasoning). You'd also need to extend the orchestrator to skip sandbox setup when a sandbox already exists for that id.
### 4. Example Walkthrough
Let's trace what happens when you trigger the agent against [`vercel/ms`](https://github.com/vercel/ms) issue [#284](https://github.com/vercel/ms/issues/284). The reporter found that `ms()` violates its own roundtrip contract: `ms(Number.MAX_VALUE)` returns a string in scientific notation that `parse()` can no longer read back.
To watch the agent work tool-by-tool, use Flue's one-shot CLI invoker (already wired into our `package.json`):
```bash
npm run run
```
That runs `flue run bug-fix --target node --id run-1 --env .env --payload '{}'`, which builds, spawns an ephemeral server, POSTs with `Accept: text/event-stream`, and decorates each agent event into a readable progress line. Below is a trimmed real run against a fork of `vercel/ms`. The orchestrator's `[bug-fix] ...` lines come from our setup phase; the `[flue] tool:start/done ...` lines and inline italicized text are the LLM's tool calls and reasoning streamed back via SSE:
```
[bug-fix] target: your-username/your-fork#284 (model: anthropic/claude-sonnet-4-6)
[bug-fix] sandbox ready (id: b5e8e152-7c67-4176-8721-c37aab6aa331)
[bug-fix] installing gh CLI in sandbox...
[bug-fix] commits will be authored as Your Name <12345+your-username@users.noreply.github.com>
[bug-fix] cloning your-username/your-fork into sandbox...
[bug-fix] detected package manager: pnpm
[bug-fix] installing pnpm...
[bug-fix] installing project dependencies...
[bug-fix] resolving issue source: vercel/ms
[bug-fix] fetching issue #284 from vercel/ms...
[bug-fix] uploading skill into sandbox + excluding it from git...
[bug-fix] running TDD workflow (reproduce → fix → PR)...
[flue] Agent started
I'll work through this systematically following the test-driven approach.
[flue] tool:start read /home/daytona/project/package.json
[flue] tool:done read (1477 chars)
[flue] tool:start glob **/*.test.*
[flue] tool:done glob
[flue] tool:start read /home/daytona/project/src/index.ts
[flue] tool:start read /home/daytona/project/src/parse.test.ts
[flue] tool:done read (5864 chars)
[flue] tool:done read (5082 chars)
Now I have a clear picture. Let me verify what `format(Number.MAX_VALUE)` actually
produces, then set up the branch and write the failing test:
[flue] tool:start bash $ node -e "const out = ms(Number.MAX_VALUE); ..."
[flue] tool:done bash (218 chars)
I've confirmed the bug: format(Number.MAX_VALUE) produces "5.696545792019405e+297y"
which the regex in parse() cannot match. Setting up the branch and writing the test:
[flue] tool:start bash $ git checkout -b flue/fix-issue-284
[flue] tool:done bash
[flue] tool:start edit /home/daytona/project/src/parse.test.ts
[flue] tool:done edit
[flue] tool:start bash $ pnpm test:nodejs
[flue] tool:done bash (2102 chars)
The tests fail as expected. Now let me implement the fix:
[flue] tool:start edit /home/daytona/project/src/index.ts
[flue] tool:done edit
[flue] tool:start bash $ pnpm test
[flue] tool:done bash (1319 chars)
All 172 tests pass (167 pre-existing + 5 new). Now let's commit and push:
[flue] tool:start bash $ git add src/index.ts src/parse.test.ts && git commit -m "fix: ..."
[flue] tool:done bash
[flue] tool:start bash $ git push origin flue/fix-issue-284
[flue] tool:done bash
[flue] tool:start bash $ gh pr create --repo your-username/your-fork --base main \
--head flue/fix-issue-284 --title "fix: ..." --body "Closes #284 ..."
[flue] tool:done bash
[bug-fix] PR opened: https://github.com/your-username/your-fork/pull/1
[bug-fix] branch: flue/fix-issue-284
[bug-fix] files changed: src/index.ts, src/parse.test.ts
[bug-fix] tearing down agents + sandbox...
```
Reading top to bottom you can see the agent following our SKILL.md phases: it understands the project (multiple parallel `read` calls), confirms the bug interactively before changing anything (the `node -e` reproduction in `bash`), creates a branch + writes the failing test, runs the suite to confirm the test fails, makes the fix, reruns the suite, then commits, pushes, and opens the PR. The final `[bug-fix] tearing down agents + sandbox...` line is the orchestrator's `try/finally` doing its work — both agents get destroyed and the Daytona sandbox is deleted before the response is returned.
Note that exact wording, file paths, test counts, and PR numbers vary between runs (the LLM is non-deterministic, and the PR number depends on how many PRs your fork already has). The shape — sandbox provision → setup → four-phase TDD workflow → PR URL → cleanup — is what's deterministic.
The four phases below zoom in on each step.
#### Phase 1: Understand
The agent reads `package.json` to identify the test runner (Jest, run via `pnpm test`), then reads the single-file source at `src/index.ts` (244 lines) and an existing parse test (`src/parse.test.ts`) to learn the project's assertion style.
The relevant code is the `parse()` regex around line 77 of `src/index.ts`:
```typescript
const match = /^(?-?\d*\.?\d+) *(?...)?$/i.exec(str);
```
The `value` group only matches plain decimal numbers; it doesn't accept scientific notation (`e+297`). That's the bug.
#### Phase 2: Reproduce
The agent creates a new branch and writes a single, focused test that asserts the **roundtrip property** the reporter described:
```typescript
import { ms } from './index';
describe('issue #284: roundtrip with very large numbers', () => {
it('format() output is always parseable back to a number', () => {
const out = ms(Number.MAX_VALUE);
expect(typeof out).toBe('string');
expect(ms(out)).not.toBeNaN();
});
});
```
It runs `pnpm test` and confirms the failures with the current implementation. From a real run:
```
FAIL src/parse.test.ts
● parse(scientific notation) › should parse scientific notation values with a unit (roundtrip with format)
Expected: false
Received: true (Number.isNaN was true — parse returned NaN)
● parse(scientific notation) › should parse scientific notation with y unit
● parse(scientific notation) › should parse scientific notation with ms unit
● parse(scientific notation) › should parse scientific notation with s unit
● parse(scientific notation) › should parse negative scientific notation with a unit
```
If the tests had unexpectedly passed, the agent would refuse to continue: a test that doesn't fail isn't a reproduction.
#### Phase 3: Fix
With the bug reproduced, the agent makes a minimal, surgical change to `src/index.ts` so `parse()` accepts scientific notation in the numeric value group, then reruns the full suite:
```
PASS src/parse.test.ts
PASS src/index.test.ts
PASS src/format.test.ts
PASS src/parse-strict.test.ts
Test Suites: 4 passed, 4 total
Tests: 172 passed, 172 total
```
Both the new tests and every pre-existing test pass. The fix is a small regex extension (one optional capture group): exactly the kind of minimal change the `test-driven-developer` role rewards.
#### Phase 4: Pull Request
The agent commits, pushes, and opens a PR against your fork:
```bash
$ git commit -m "fix: support scientific notation in parse() to fix roundtrip with large numbers (#284)"
$ git push origin flue/fix-issue-284
$ gh pr create --repo your-username/your-fork \
--base main \
--head flue/fix-issue-284 \
--title "fix: support scientific notation in parse() to fix roundtrip with large numbers (#284)" \
--body "Closes vercel#284 ..."
```
The PR body the agent generated includes the failing-test output from Phase 2, a one-paragraph root-cause analysis, and the passing-test output from Phase 3 — everything a human reviewer needs to merge in under five minutes.
The HTTP response body returned to your `curl` wraps the handler's return value under a `result` key:
```json
{
"result": {
"branch": "flue/fix-issue-284",
"prUrl": "https://github.com/your-username/your-fork/pull/1",
"testFile": "src/parse.test.ts",
"filesChanged": ["src/index.ts", "src/parse.test.ts"],
"summary": "The parse() regex only matched plain decimal numbers in the value group (`-?\\d*\\.?\\d+`), so when format() produced scientific notation (e.g. `5.696545792019405e+297y`) via JavaScript's default number serialisation for very large Math.round() results, parse() returned NaN; the fix extends the value capture group with an optional exponent part (`(?:e[+-]?\\d+)?`) so scientific notation is accepted transparently."
}
}
```
Open the PR URL in your browser to review the diff and merge, exactly as you would for a human-authored contribution.
:::tip[Commit attribution]
Both the commit and the PR are authored under the GitHub account that owns your `GITHUB_TOKEN`. The agent calls `gh api user` at startup to resolve your login + numeric ID, then sets `git config user.email` to the GitHub-recommended `+@users.noreply.github.com` noreply format. GitHub recognizes that email and attaches the commit to your profile (avatar and all). The PR body still has a small `Generated by a Flue + Daytona bug-fix agent.` footer for transparency — if you'd rather drop it (some maintainers prefer not to have third-party tags on contributions), edit the PR-body template at the bottom of `.agents/skills/bug-fix/SKILL.md` and remove that line.
:::
### 5. Cleanup
Flue does **not** auto-destroy sessions when a handler returns — sessions persist for resumability via the same ``, and the `cleanup: true` callback registered on our connector only fires when `agent.destroy()` is explicitly called. The orchestrator therefore wraps the entire two-agent flow in a `try { ... } finally { ... }` block:
```ts
try {
setupAgent = await init({ sandbox: daytona(sandbox, { cleanup: true }), ... })
// ... setup work + projectAgent + skill invocation
return result
} finally {
console.log('[bug-fix] tearing down agents + sandbox...')
if (projectAgent) {
try { await projectAgent.destroy() } catch (err) { console.error(err) }
}
if (setupAgent) {
try { await setupAgent.destroy() } catch (err) { console.error(err) }
} else {
// setupAgent never armed cleanup: true; delete sandbox directly
try { await sandbox.delete() } catch (err) { console.error(err) }
}
}
```
Order matters: the **project** agent is destroyed first (closes its session, no sandbox impact since it doesn't have `cleanup: true`), then the **setup** agent's destroy fires the registered `sandbox.delete()` callback. The fallback `else` branch handles the case where `init()` itself threw before `setupAgent` was created — in that scenario nothing armed `cleanup: true`, so the orchestrator calls `sandbox.delete()` directly on the already-created sandbox. (If `client.create()` itself fails earlier, no sandbox object exists at all, so there's nothing to leak.)
This covers the common failure paths: successful runs, caught LLM errors, and exceptions thrown during the workflow. Cleanup is best-effort — if `destroy()` or `sandbox.delete()` itself throws (transient API error, network drop), the failure is logged but the sandbox is not retried. Confirm in your [Daytona Dashboard](https://app.daytona.io/dashboard) after each run, and clean up any orphans manually if you spot them.
**Key Advantages**
- **TDD by construction**: the skill's phase ordering forces a failing test before any fix lands, so every PR is reproducible.
- **Real pull requests**: the agent uses `gh` inside the sandbox to push and open PRs you can merge in the GitHub UI, not just patches you copy by hand.
- **Skill-first design**: tweak the workflow by editing markdown. No recompile, no redeploy.
- **Structured outputs**: results are validated by Valibot schemas, so downstream automation never has to parse free-form text.
- **Sandbox-isolated execution**: cloning, dependency installation, and test runs all happen inside Daytona, so your host stays clean even if the target repo is malicious or its dependencies are.
# Build a Generative-UI Coding Agent with CopilotKit and Daytona
import { Image } from 'astro:assets'
import copilotkitSnakeGame from '../../../../../assets/docs/images/copilotkit_snake_game.gif'
This guide demonstrates how to build a [CopilotKit](https://docs.showcase.copilotkit.ai/) Built-in Agent backed by a [Daytona](https://www.daytona.io/) sandbox with full shell and filesystem access. The agent handles whatever a developer might do at a terminal: build apps, debug or analyze code, run scripts, work with data, install packages.
Every tool call streams into the chat as generative UI: shell commands render as terminal cards, file edits as syntax-highlighted code, listings and grep results as structured cards, and any hosted process (dev server, static site, API) as a live `