Skip to content

Repository files navigation

CreateOS C# SDK

Launch an isolated cloud sandbox, run real commands, stream output, move files, open a preview URL, and tear everything down from .NET.

The SDK targets .NET 8 and provides asynchronous APIs with cancellation, request-level timeouts, retries, typed models, and inspectable errors.

Your first sandbox

Install the published CreateOS.Sandbox package from nuget.org. No GitHub package source or token is needed:

dotnet add package CreateOS.Sandbox

To develop against the SDK source instead of a published package:

dotnet add reference path/to/createos-csharp-sdk/src/CreateOS.Sandbox/CreateOS.Sandbox.csproj
using CreateOS.Sandbox;

var client = new SandboxClient(new SandboxClientOptions
{
    ApiKey = "your-api-key"
});

var sandbox = await client.CreateSandboxAsync(new CreateSandboxRequest
{
    Name = "hello-csharp",
    Shape = "s-4vcpu-4gb",
    RootFileSystem = "devbox:1"
});

try
{
    var response = await sandbox.RunCommandAsync(new RunCommandRequest
    {
        Command = "sh",
        Arguments = ["-c", "printf 'C# says hello from '; uname -m"]
    });

    Console.Write(response.Result.StandardOutput);
}
finally
{
    await sandbox.DestroyAsync(CancellationToken.None);
}
C# says hello from x86_64

Do not commit a real API key to source control. Inject it through your secret manager or the CREATEOS_API_KEY environment variable. Explicit options take precedence over environment variables:

var client = new SandboxClient(new SandboxClientOptions
{
    ApiKey = apiKey,
    BaseUri = new Uri("http://localhost:8080"),
    Timeout = TimeSpan.FromSeconds(30),
    MaxRetries = 2
});

CREATEOS_SANDBOX_BASE_URL optionally configures the endpoint when BaseUri is not supplied.

Delegate access to one sandbox

An owner can create one delegated token for a sandbox. Plaintext is returned only on creation or rotation; inspection provides a redacted hint.

var created = await sandbox.CreateAccessTokenAsync();
var worker = sandbox.WithAccessToken(created.Token);
var result = await worker.RunCommandAsync(new RunCommandRequest { Command = "echo", Arguments = ["hello"] });
var metadata = await sandbox.GetAccessTokenAsync();
var replacement = await sandbox.RotateAccessTokenAsync();
await sandbox.DisableAccessTokenAsync();

Use the owner's handle for token management. The delegated handle can operate its bound sandbox, including commands, files, processes, computer use, pause, resume, and destroy; it cannot manage tokens or account resources. Creating another enabled token returns HTTP 409; rotation requires an existing token. Disabling is idempotent. Revocation is immediate in the home region and propagates asynchronously to peer regions.

Documentation

SDKs

Stream output as it happens

Long-running commands do not need to disappear behind a buffered HTTP call:

await foreach (var item in sandbox.StreamCommandAsync(new RunCommandRequest
{
    Command = "sh",
    Arguments = ["-c", "for n in 1 2 3; do echo step-$n; sleep 1; done"]
}, token: cancellationToken))
{
    switch (item.Type)
    {
        case ExecStreamEventType.Stdout:
            Console.Write(item.Data);
            break;
        case ExecStreamEventType.Stderr:
            Console.Error.Write(item.Data);
            break;
        case ExecStreamEventType.Exit:
            Console.WriteLine($"exit code: {item.ExitCode}");
            break;
        case ExecStreamEventType.Error:
            Console.Error.WriteLine(item.ErrorMessage);
            break;
    }
}

Breaking out of the await foreach loop disposes the HTTP response and closes the underlying stream. Pass a CancellationToken to stop from another task.

Move files without shell escaping

await using (var source = File.OpenRead("config.json"))
{
    await sandbox.Files.UploadAsync(
        "/workspace/config.json",
        source,
        token: cancellationToken);
}

await using var downloaded = await sandbox.Files.DownloadAsync(
    "/workspace/config.json",
    token: cancellationToken);

using var reader = new StreamReader(downloaded);
var contents = await reader.ReadToEndAsync(cancellationToken);

Override transport settings for one large transfer without changing the client defaults:

var transferOptions = new RequestOptions
{
    Timeout = TimeSpan.FromMinutes(30),
    DisableRetry = true
};

await sandbox.Files.UploadAsync(remotePath, source, transferOptions, cancellationToken);
await using var file = await sandbox.Files.DownloadAsync(
    remotePath,
    transferOptions,
    cancellationToken);

The timeout remains active until a download reaches EOF or its stream is disposed. Uploads are not automatically replayed because an arbitrary Stream may not be safe to read again after a partial write.

Keep a process alive after disconnecting

Managed processes are durable resources rather than fragile terminal sessions. Start one, reconnect from an output sequence, send input or signals, and wait for either its leader or complete process tree:

var process = await sandbox.Processes.CreateAsync(new ManagedProcessCreateRequest
{
    Command = "python3",
    Arguments = ["-m", "http.server", "8080"]
});

process = await sandbox.Processes.WaitAsync(
    process.ProcessId,
    new ManagedProcessWaitOptions
    {
        Scope = ManagedProcessWaitScope.Tree,
        WaitTimeout = TimeSpan.FromSeconds(30)
    });

Connect returns an asynchronous event stream with retained output replay:

await foreach (var item in sandbox.Processes.ConnectAsync(
    process.ProcessId,
    new ManagedProcessConnectOptions { AfterSequence = lastSequence },
    cancellationToken))
{
    if (item.Type == ManagedProcessConnectEventType.Data && item.Data is not null)
        Console.Write(Encoding.UTF8.GetString(item.Data));
}

Use PtySize when creating a process to request a terminal-backed session. The managed-process example covers PTY input, resize, replay, waiting, signals, and forced process-tree termination.

Turn a service into a URL

Create with ingress enabled, wait for the server to listen, then ask the sandbox for its public URL:

var sandbox = await client.CreateSandboxAsync(new CreateSandboxRequest
{
    Shape = "s-4vcpu-4gb",
    RootFileSystem = "devbox:1",
    IngressEnabled = true
});

await sandbox.Processes.CreateAsync(new ManagedProcessCreateRequest
{
    Command = "python3",
    Arguments = ["-m", "http.server", "8080", "--bind", "0.0.0.0"]
});

await sandbox.WaitForPortAsync(
    8080,
    host: "127.0.0.1",
    timeout: TimeSpan.FromSeconds(15),
    token: cancellationToken);

Console.WriteLine(sandbox.GetPreviewUri(8080));

Everything is already connected

Account-level services are initialized with SandboxClient:

var templates = client.Templates;
var networks = client.Networks;
var disks = client.Disks;

var customTemplates = await templates.ListAsync();
Console.WriteLine($"{customTemplates.Count} templates ready");

Instance-level services are initialized when a sandbox handle is created or retrieved:

var files = sandbox.Files;
var processes = sandbox.Processes;
var mouse = sandbox.Computer.Mouse;
var keyboard = sandbox.Computer.Keyboard;
var windows = sandbox.Computer.Windows;
var screens = sandbox.Computer.Screens;

Connect sandboxes on a private network

Create an overlay network, attach a running sandbox, and inspect its membership. Cleanup runs in reverse order so the sandbox disconnects before the network is deleted:

var network = await client.Networks.CreateAsync(
    new NetworkCreateRequest("agent-mesh"));

try
{
    await sandbox.AttachNetworkAsync(network.Id);
    try
    {
        var connected = await client.Networks.GetAsync(network.Id);
        foreach (var member in connected.Members ?? [])
        {
            Console.WriteLine(
                $"sandbox={member.SandboxId} private-ip={member.IpAddress} status={member.Status}");
        }
    }
    finally
    {
        await sandbox.DetachNetworkAsync(network.Id, CancellationToken.None);
    }
}
finally
{
    await client.Networks.DeleteAsync(network.Id, CancellationToken.None);
}

Lifecycle reads like the domain

await sandbox.PauseAsync(cancellationToken);
await sandbox.WaitUntilPausedAsync(token: cancellationToken);

var clone = await sandbox.ForkAsync(
    new ForkSandboxRequest { StartPaused = true },
    cancellationToken);

try
{
    await clone.ResumeAsync(cancellationToken);
    await clone.WaitUntilRunningAsync(token: cancellationToken);
}
finally
{
    await clone.DestroyAsync(CancellationToken.None);
}

SandboxInstance caches the latest server projection safely. Lifecycle mutations and RefreshAsync update it, while Id, Name, Status, IpAddress, and Data provide synchronized reads.

Set or disable automatic idle pausing with the same handle:

await sandbox.SetAutoPauseAsync(TimeSpan.FromMinutes(30), cancellationToken);
await sandbox.SetAutoPauseAsync(null, cancellationToken);

Build reusable templates

Templates turn Dockerfiles into reusable sandbox root filesystems:

var template = await client.Templates.CreateAsync(new TemplateCreateRequest(
    Name: $"tools-{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}",
    Dockerfile: """
        FROM nodeops/sandbox:debian
        RUN apt-get update && apt-get install -y git curl
        """));

await foreach (var item in client.Templates.FollowLogsAsync(
    template.Id,
    new TemplateLogsOptions { Timeout = TimeSpan.FromMinutes(10) },
    cancellationToken))
{
    if (!string.IsNullOrEmpty(item.Line)) Console.WriteLine(item.Line);
    if (item.Final == true) break;
}

Poll Templates.GetAsync until the status is TemplateStatus.Ready, then use the template ID as CreateSandboxRequest.RootFileSystem. Delete the template when it is no longer needed.

Automate a desktop

The desktop:1 root filesystem exposes screenshots, mouse and keyboard control, clipboard access, window management, multiple screens, and temporary noVNC connections:

var options = new ComputerScreenOptions
{
    ScreenId = ComputerScreenId.Screen0
};

var geometry = await sandbox.Computer.GetScreenAsync(options, cancellationToken);
await sandbox.Computer.Mouse.MoveAsync(
    new ComputerPoint(geometry.Width / 2, geometry.Height / 2),
    options,
    cancellationToken);

await sandbox.Computer.SetClipboardAsync("hello desktop", options, cancellationToken);
var connection = await sandbox.Computer.Screens.ConnectAsync(
    ComputerScreenId.Screen0,
    cancellationToken);

Console.WriteLine(connection.Url);

See the desktop example for PNG capture and cropping, cursor verification, clipboard round-tripping, browser launch, and noVNC connection creation.

Errors stay inspectable

Non-successful HTTP responses throw CreateOSApiException:

try
{
    await client.GetSandboxAsync("missing-sandbox", cancellationToken);
}
catch (CreateOSApiException exception)
{
    Console.WriteLine(
        $"HTTP {(int)exception.StatusCode}, code={exception.ApiCode}, request={exception.RequestId}");
}
catch (TimeoutException)
{
    // A lifecycle or readiness wait exhausted its budget.
}

A successful HTTP response containing a JSend fail or error envelope throws CreateOSEnvelopeException, preserving its status, code, and response data. Cancellation continues to surface as OperationCanceledException.

The transport retries rate limits and temporary service-unavailable responses. Network failures and other retryable status codes are replayed only for idempotent methods. Use RequestOptions.DisableRetry or Retry for a per-request override:

var requestOptions = new RequestOptions
{
    Retry = new RetryOptions
    {
        MaxRetries = 3,
        BaseDelay = TimeSpan.FromMilliseconds(250),
        MaxDelay = TimeSpan.FromSeconds(10)
    }
};

Examples

Runnable examples live under examples/:

Run any example with the API key in the environment:

export CREATEOS_API_KEY=your-api-key
dotnet run --project examples/HelloWorld/HelloWorld.csproj
dotnet run --project examples/CommandStreaming/CommandStreaming.csproj

Every example cleans up its resources in a finally block. Template, desktop, and snapshot examples may take several minutes. The complete set has been validated end-to-end against the live CreateOS API.

Development

Build every project, including all examples:

dotnet restore CreateOS.Sandbox.sln
dotnet format CreateOS.Sandbox.sln --verify-no-changes --no-restore
dotnet build CreateOS.Sandbox.sln --configuration Release --no-restore
dotnet test CreateOS.Sandbox.sln --configuration Release --no-build

Create a local NuGet package:

dotnet pack src/CreateOS.Sandbox/CreateOS.Sandbox.csproj \
  --configuration Release \
  --output artifacts

The repository enables the built-in .NET analyzers, enforces code style during builds, treats warnings as errors, and pins the .NET 8 SDK feature band. GitHub Actions verifies formatting, builds the complete solution, runs the test suite, and packs the SDK on every pull request and push to main.

For custom proxy, certificate, or connection settings, provide a fresh HttpClientHandler through SandboxClientOptions.HttpClientHandler. The SDK always disables automatic redirects on that handler so credentials cannot be forwarded to another origin.

Package layout

src/CreateOS.Sandbox/          client, resource services, models, and transport
src/CreateOS.Sandbox/Internal/ JSend, enum conversion, retries, and HTTP details
examples/                      independently runnable .NET programs

About CreateOS

CreateOS is an execution and governance platform for AI agents and applications. Learn more about isolated Firecracker-based workloads on the CreateOS Sandbox product page.

License

This SDK is available under the MIT License.

About

CreateOS C# SDK

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages