Your first sandbox

A sandbox_session gives you five tools: run, read_file, write_file, grep, and list_dir. Use them together to run code and work with files. The SDK loads your connection from environment variables or a saved profile.

Use the five tools

Save either program as sandbox_tools.py. It writes a file, runs cat, reads the file, finds a matching line, and lists the directory in the same session.

"""Use the five sandbox tools in one session."""

from contree_sdk import create_sandbox_session


def main() -> None:
    with create_sandbox_session(image="tag:alpine:latest") as sandbox_session:
        # Write a file in the sandbox.
        sandbox_session.write_file("/work/message.txt", b"Hello from ConTree!\n")

        # Run a command and print its output.
        result = sandbox_session.run("cat", args=["/work/message.txt"])
        print(result.stdout)

        # Read the file as bytes.
        content = sandbox_session.read_file("/work/message.txt")
        print(content.decode())

        # Search file contents.
        matches = sandbox_session.grep("Hello", path="/work/message.txt")
        print(matches)

        # List the directory with file metadata.
        directory = sandbox_session.list_dir("/work")
        print(directory)


main()
import asyncio

from contree_sdk import create_sandbox_session


async def main() -> None:
    async with create_sandbox_session(image="tag:alpine:latest") as sandbox_session:
        # Write a file in the sandbox.
        await sandbox_session.write_file("/work/message.txt", b"Hello from ConTree!\n")

        # Run a command and print its output.
        result = await sandbox_session.run("cat", args=["/work/message.txt"])
        print(result.stdout)

        # Read the file as bytes.
        content = await sandbox_session.read_file("/work/message.txt")
        print(content.decode())

        # Search file contents.
        matches = await sandbox_session.grep("Hello", path="/work/message.txt")
        print(matches)

        # List the directory with file metadata.
        directory = await sandbox_session.list_dir("/work")
        print(directory)


asyncio.run(main())

Both programs print the command output, file contents, search matches, and file metadata.

run() returns stdout, stderr, and process status. read_file() returns bytes; write_file() accepts bytes and creates parent directories. grep() and list_dir() return structured results that you can print directly. Use print(directory) for a detailed listing, or directory.as_text(short=True) for names only.

What the default lazy session does

Without session_type, the factory selects LazySession for Sync and AsyncLazySession for Async. Both have the same behavior. “Lazy” means the VM starts when needed, and several commands can share one snapshot.

        sequenceDiagram
    participant S as SDK
    participant V1 as MicroVM 1
    participant V2 as MicroVM 2
    S->>V1: Run commands
    Note over S,V1: 10 s idle
    S->>V1: Save and stop
    S->>V2: Start from saved files
    

Select the lazy type explicitly and call uptime twice. Both calls use the same MicroVM. These examples use your configured connection; add profile="work" or the token arguments below when needed.

from contree_sdk import LazySession, create_sandbox_session


with create_sandbox_session(
    image="tag:alpine:latest",
    session_type=LazySession,
) as session:
    print(session.run("uptime").stdout)
    print(session.run("uptime").stdout)
import asyncio

from contree_sdk import AsyncLazySession, create_sandbox_session


async def main() -> None:
    async with create_sandbox_session(
        image="tag:alpine:latest",
        session_type=AsyncLazySession,
    ) as session:
        print((await session.run("uptime")).stdout)
        print((await session.run("uptime")).stdout)


asyncio.run(main())

Creating the session or entering its context does not start a VM. The first run() or write_file() starts it. Later commands reuse that VM, so installed packages, files, and running processes remain available until a snapshot.

After the last command or file request finishes, the default idle timer waits 10 seconds. It then saves the filesystem as an image and stops the VM in the background, without another call from your code. The next command starts a new VM from that saved image automatically. Reads, searches, and directory listings can use the saved image without starting a VM.

A snapshot preserves files, including installed packages, but loses processes and memory. For example, a file remains after an idle pause; a background web server must be started again. Each VM also has a lifetime limit; see VM lifetime settings.

Normal context exit saves pending changes. An exception discards changes since the last completed snapshot. To save after every command instead, select the session type shown below.

The image tag must exist on your endpoint and provide sleep, sh, mkdir, and cat. Replace tag:alpine:latest with an available image, or use Import a Docker image to prepare one. Configure credentials in Connection sources.

Install the SDK

These guides describe the breaking 0.5 development API and require Python 3.10–3.14. Run the installation command from this repository checkout; the published 0.4 package does not provide the new API.

pip install -e ".[async]"

For synchronous programs only, pip install -e . is sufficient.

Initialize a session

Choose one connection source: pass a token and AI project, or select a saved profile. Both paths return the same session with all five tools. The SDK creates and closes the client for you; retries are enabled by default.

Create a session from a token

Replace your-token and your-ai-project with your credentials. Without url, the SDK uses https://api.tokenfactory.nebius.com/sandboxes. Pass url=... when using another deployment.

"""Create a sandbox session from a token."""

from contree_sdk import create_sandbox_session
from contree_sdk.session import IdleSnapshotPolicy


def main() -> None:
    with create_sandbox_session(
        image="tag:alpine:latest",
        token="your-token",
        project="your-ai-project",
        session_kwargs={"snapshot_policy": IdleSnapshotPolicy(60)},
    ) as sandbox_session:
        result = sandbox_session.run("echo", args=["Hello from ConTree!"])
        print(result.stdout)


main()
import asyncio

from contree_sdk import create_sandbox_session
from contree_sdk.session import IdleSnapshotPolicy


async def main() -> None:
    async with create_sandbox_session(
        image="tag:alpine:latest",
        token="your-token",
        project="your-ai-project",
        session_kwargs={"snapshot_policy": IdleSnapshotPolicy(60)},
    ) as sandbox_session:
        result = await sandbox_session.run("echo", args=["Hello from ConTree!"])
        print(result.stdout)


asyncio.run(main())

Create a session from a saved profile

Save this file as contree.ini in the directory where you run the program. Replace the token and AI project values with your credentials:

[DEFAULT]
profile = work

[profile:work]
type = iam
url = https://api.tokenfactory.nebius.com/sandboxes
token = your-token
project = your-ai-project

The program selects work from this file:

"""Create a sandbox session from a saved profile."""

from contree_sdk import create_sandbox_session
from contree_sdk.session import IdleSnapshotPolicy


def main() -> None:
    with create_sandbox_session(
        image="tag:alpine:latest",
        profile="work",
        config_path="contree.ini",
        session_kwargs={"snapshot_policy": IdleSnapshotPolicy(60)},
    ) as sandbox_session:
        result = sandbox_session.run("echo", args=["Hello from ConTree!"])
        print(result.stdout)


main()
import asyncio

from contree_sdk import create_sandbox_session
from contree_sdk.session import IdleSnapshotPolicy


async def main() -> None:
    async with create_sandbox_session(
        image="tag:alpine:latest",
        profile="work",
        config_path="contree.ini",
        session_kwargs={"snapshot_policy": IdleSnapshotPolicy(60)},
    ) as sandbox_session:
        result = await sandbox_session.run("echo", args=["Hello from ConTree!"])
        print(result.stdout)


asyncio.run(main())

If the profile already exists in the client’s standard configuration, pass only profile="work" and omit config_path. Do not combine profile with token, project, or url; put those values in the profile instead.

Session options and defaults

Both examples use session_kwargs to set a 60-second idle interval. Omit that argument to use the default of 10 seconds. Session constructor options belong inside session_kwargs; client options such as retry and timeout belong on the factory call.

The factory selects LazySession for with and AsyncLazySession for async with. Set session_type only when selecting another class or a subclass. Use create_sync_sandbox_session() or create_async_sandbox_session() if you want to select the context protocol explicitly; they accept the same arguments.

Creating the context opens no connection. Entering it prepares the session; the first command starts the VM. Normal exit saves pending file changes and closes owned resources. An exception discards changes since the last snapshot.

SDK-created clients use contree_client.RetryPolicy() by default. Pass retry=RetryPolicy(...) to customize it, or retry=None to disable retries. A client supplied through client= keeps its existing retry configuration. See Advanced usage for more session options and component ownership.

Save after every command

Select ContreeSession for Sync or ContreeAsyncSession for Async. These classes start a separate VM for each run() and save its filesystem before returning. The next command uses that saved image. You do not call snapshot() or wait for an idle timeout.

        sequenceDiagram
    participant S as SDK
    participant V1 as MicroVM 1
    participant V2 as MicroVM 2
    S->>V1: Run, save, stop
    S->>V2: Run from saved files, save, stop
    

Call uptime twice with the regular session type. Each call starts a new MicroVM; the second uses the filesystem saved by the first. Connection arguments are the same as in the lazy example.

from contree_sdk import ContreeSession, create_sandbox_session


with create_sandbox_session(
    image="tag:alpine:latest",
    session_type=ContreeSession,
) as session:
    print(session.run("uptime").stdout)
    print(session.run("uptime").stdout)
import asyncio

from contree_sdk import ContreeAsyncSession, create_sandbox_session


async def main() -> None:
    async with create_sandbox_session(
        image="tag:alpine:latest",
        session_type=ContreeAsyncSession,
    ) as session:
        print((await session.run("uptime")).stdout)
        print((await session.run("uptime")).stdout)


asyncio.run(main())

In the lazy example, both commands share a running VM. In this example, the VM restarts between commands. uptime output is rounded, so two immediate calls can print similar values in either mode. Files survive the restart; processes and memory do not.

disposable=False is the default. Passing disposable=True to a command skips saving its changes. By default, a completed operation is saved even if its process exits with a nonzero code. Failed or cancelled operations do not advance the session history. See Work with a Session for checkpoint and error behavior.

Connection sources

Configure the endpoint URL, token, and AI project for your deployment. The project identifies where the sandbox runs; omitting it is not a complete working configuration.

Setting

Environment

Profile / INI

Direct factory argument

Endpoint

CONTREE_URL

url

url

Token

CONTREE_TOKEN or NEBIUS_API_KEY

token

token

AI project

CONTREE_PROJECT or NEBIUS_AI_PROJECT

project

project

The SDK delegates environment parsing to contree-client.

Use exactly one explicit source:

Source

Arguments

Behavior

Direct credentials

url, project, token

Build a regular client Profile. Token is required. An omitted URL uses the client’s default endpoint.

Profile object

profile=profile

Use that exact profile. Do not combine it with direct credentials or config_path.

Named profile

profile="work", optional config_path

Resolve that name through contree-client.

Explicit INI

config_path="config.ini"

Resolve the file’s active profile, or the name selected by CONTREE_PROFILE.

Automatic

Omit all source arguments

Use contree_client.profiles.from_env(), then the client’s default profile resolution.

Direct credentials cannot be combined with profile or config_path. Incomplete explicit credentials raise an error rather than falling back to ENV. An explicit source takes priority over ENV. Without one, the environment needs both CONTREE_URL and CONTREE_TOKEN (or NEBIUS_API_KEY). Provide the project through CONTREE_PROJECT (or NEBIUS_AI_PROJECT).

For INI resolution, contree-client uses an explicit profile name, then CONTREE_PROFILE, then the active profile in the file. Its default files are cli.ini and auth.ini under CONTREE_HOME; auth.ini takes priority. Without CONTREE_HOME, the client uses its XDG configuration directory. A supplied config_path replaces the auth file path and can be named config.ini; the client also reads cli.ini from the same directory.

The SDK uses credentials from the selected profile; it does not acquire or refresh credentials itself. For a profile constructed in Python, pass a contree_client.profiles.Profile as profile=.... The profile_from_args() helper constructs this object without writing a configuration file.

Continue with Import a Docker image to import a Docker image for your sandbox.