Table of Contents

Sandboxes

client.Sandboxes (ISandboxClient) creates and manages sandboxes in the client's workspace. A Sandbox is an immutable snapshot: read the current state with GetAsync, a wait, or a watch.

Lifecycle

Sandbox.Phase is a SandboxPhase:

Phase Meaning Reached by
Provisioning Resources are being created. CreateAsync
Ready Running and usable. provisioning, StartAsync, ResumeAsync
Stopping, Stopped Being stopped; stopped and can be started again. StopAsync
Starting Being started. StartAsync, ResumeAsync
Suspending, Suspended Being frozen; frozen with its memory retained. SuspendAsync
Completed The main process finished and will not restart. the workload
Error Failed. the workload or the driver
Deleting Being deleted. DeleteAsync
Unknown, Unspecified The gateway cannot tell, or did not report.

Create

CreateAsync returns as soon as the gateway accepts the sandbox, usually in Provisioning. SandboxSpec describes the workload (image, command, environment, providers, policy, restart policy, services to expose); CreateSandboxOptions adds labels, annotations, and an idempotency key.

var sandbox = await client.Sandboxes.CreateAsync(
    "agent-1",
    new SandboxSpec
    {
        Image = "registry.example.com/team/python-agent:1.0",
        Environment = new Dictionary<string, string> { ["MODE"] = "dev" },
        RestartPolicy = SandboxRestartPolicy.OnFailure,
        ServiceExposures = [new ServiceExposure("web", 8080)],
    },
    new CreateSandboxOptions
    {
        Labels = new Dictionary<string, string> { ["team"] = "platform" },
        RequestId = "create-agent-1",   // a retry with the same id returns the original result
    });

Console.WriteLine($"{sandbox.Name} ({sandbox.Id}) is {sandbox.Phase}");

Pass null as the name to let the gateway generate one, and read it from the returned Sandbox.Name. SandboxSpec.Template (SandboxTemplateSpec) sets driver-level details such as the runtime class, resources, and driver configuration; SandboxSpec.Policy is the generated SandboxPolicy wire type (see Policy); SandboxSpec.RawSpec adjusts the wire request for fields the curated shape does not cover (see Raw gRPC access).

Wait

The wait helpers poll GetAsync with backoff until the sandbox reaches a phase, and throw OpenShellException with DeadlineExceeded when the WaitOptions timeout (two minutes by default) runs out.

// Polls with backoff until the sandbox is Ready (or Completed). Error, Stopped, and Suspending or Suspended
// fail the wait at once; running out of time throws OpenShellException with DeadlineExceeded.
var ready = await client.Sandboxes.WaitReadyAsync("agent-1", new WaitOptions
{
    Timeout = TimeSpan.FromMinutes(5),
    PollInterval = TimeSpan.FromMilliseconds(500),
    MaxPollInterval = TimeSpan.FromSeconds(5),
});
Wait Ends when Fails at once when
WaitReadyAsync Ready or Completed Error, Stopped, Suspending, or Suspended
WaitStoppedAsync Stopped Error, Suspending, or Suspended
WaitSuspendedAsync Suspended Error, or any phase other than Suspending (the suspension did not happen or was undone)
WaitDeletedAsync the sandbox is gone, or its id differs from ExpectedSandboxId

Cancelling a wait through its CancellationToken throws OperationCanceledException.

Read and list

var sandbox = await client.Sandboxes.GetAsync("agent-1");
foreach (var condition in sandbox.Conditions)
{
    Console.WriteLine($"{condition.Type}={condition.Status}: {condition.Reason} {condition.Message}");
}

await foreach (var s in client.Sandboxes.List(new ListOptions { LabelSelector = "team=platform" }))
{
    Console.WriteLine($"{s.Name}: {s.Phase}");
}

IReadOnlyList<string> ids = await client.Sandboxes.ListIdsAsync();

List returns a lazy Pager<T>; see Workspaces and pagination.

Stop and start

await client.Sandboxes.StopAsync("agent-1");          // Ready -> Stopping
await client.Sandboxes.WaitStoppedAsync("agent-1");   // -> Stopped

await client.Sandboxes.StartAsync("agent-1");         // -> Starting
await client.Sandboxes.WaitReadyAsync("agent-1");     // -> Ready

Suspend and resume

A ready sandbox can be suspended: it is frozen with its memory retained and resumed later from that state. The compute driver must support it; otherwise both calls fail with OpenShellErrorKind.Conflict (FAILED_PRECONDITION).

await client.Sandboxes.SuspendAsync("agent-1");         // Ready -> Suspending
await client.Sandboxes.WaitSuspendedAsync("agent-1");   // -> Suspended, memory retained

await client.Sandboxes.ResumeAsync(
    "agent-1",
    new ResumeSandboxOptions { RestartIfRestoreFails = true });   // -> Starting, with the retained state
await client.Sandboxes.WaitReadyAsync("agent-1");       // -> Ready

Repeating a suspend while suspending or suspended, or a resume while it is starting, is safe. ResumeSandboxOptions.RestartIfRestoreFails boots the workload cleanly when the memory state cannot be restored, instead of failing. StopAsync and StartAsync also work on a suspended sandbox, but they discard the retained state. WaitReadyAsync and WaitStoppedAsync fail fast on a suspended sandbox instead of waiting for a phase it will not reach on its own.

Update a stopped sandbox

A gateway with [openshell.gateway] enable_sandbox_updates = true can change a stopped sandbox in place. Pass the resource version you read (compare-and-swap), and set only what changes: a null property in SandboxUpdate is left alone, while an empty collection replaces the value with nothing. The update does not start the sandbox.

var sandbox = await client.Sandboxes.GetAsync("agent-1");   // must be Stopped

// Compare-and-swap on the version you read. Null properties stay as they are; an empty collection
// replaces the value with nothing.
sandbox = await client.Sandboxes.UpdateAsync(
    "agent-1",
    sandbox.ResourceVersion,
    new SandboxUpdate
    {
        Image = "registry.example.com/team/python-agent:1.1",
        Environment = new Dictionary<string, string> { ["MODE"] = "prod" },
    },
    new UpdateSandboxOptions { RequestId = "update-agent-1-to-1.1" });

await client.Sandboxes.StartAsync("agent-1");   // the update does not start the sandbox
Failure Kind
The resource version is stale Conflict (ABORTED)
The sandbox is running, or the gateway has the feature off Conflict (FAILED_PRECONDITION)
Image, command, or environment change on a compute driver that does not support it (only Kubernetes at first) Unimplemented

Label and annotation changes work on every driver; gateway-owned keys are preserved and system keys are rejected. An empty Command runs the image's default shell, and an empty Environment clears the sandbox-level overrides (template defaults remain). With UpdateSandboxOptions.RequestId the gateway admits a retried update at most once. If an update fails part-way, it stays pending and blocks start and stop; finish it with ResumePendingUpdateAsync:

// An update that failed part-way stays pending and blocks start and stop until it is finished.
var current = await client.Sandboxes.GetAsync("agent-1");
await client.Sandboxes.ResumePendingUpdateAsync("agent-1", current.ResourceVersion);

Endpoint statuses

Sandbox.EndpointStatuses reports what the sandbox supervisor last observed for each declared endpoint: host, ports, path, the latest EndpointResult, and when it was reported. When a gateway-managed token grant fails, EndpointStatus.LastFailure and Sandbox.LastTokenGrantFailure carry the classified cause (EndpointFailure): a stable Code and Stage (lower_snake_case), the RecoveryAction to take, the provider and credential key, the upstream HTTP status, and the x-openshell-request-id the workload saw, for correlating with gateway logs.

var sandbox = await client.Sandboxes.GetAsync("agent-1");
foreach (var endpoint in sandbox.EndpointStatuses)
{
    Console.WriteLine($"{endpoint.Host}:{string.Join(',', endpoint.Ports)}{endpoint.Path} {endpoint.LastResult} at {endpoint.LastReportedAt}");
    if (endpoint.LastFailure is { } failure)
    {
        Console.WriteLine(
            $"  {failure.Stage}/{failure.Code}: {failure.RecoveryAction} " +
            $"(provider {failure.Provider}, key {failure.CredentialKey}, HTTP {failure.UpstreamHttpStatus}, request {failure.RequestId})");
    }
}

if (sandbox.LastTokenGrantFailure is { } grant)
{
    Console.WriteLine($"last token grant failure for {grant.DestinationHost}:{grant.DestinationPort}: {grant.Failure?.Code}");
}

Delete

DeleteAsync returns a typed DeletionResult, not a boolean:

Outcome Meaning IsComplete
Completed Deleted. true
AlreadyAbsent It did not exist, and DeleteOptions.AllowMissing was set. true
Accepted Cleanup is still pending; wait with WaitDeletedAsync. false
Unspecified The gateway did not say; it does not establish completion. false

A missing sandbox is a NotFound error unless you pass AllowMissing = true. The same result type is returned by every delete in the SDK.

var deletion = await client.Sandboxes.DeleteAsync("agent-1", new DeleteOptions { AllowMissing = true });
switch (deletion.Outcome)
{
    case DeletionOutcome.Completed:
    case DeletionOutcome.AlreadyAbsent:
        break;   // deletion.IsComplete is true

    case DeletionOutcome.Accepted:
        // Cleanup is still running. ExpectedSandboxId makes sure a new sandbox created under the
        // same name does not keep the wait going.
        await client.Sandboxes.WaitDeletedAsync(
            "agent-1",
            new WaitOptions { ExpectedSandboxId = deletion.ResourceId });
        break;

    default:
        // Unspecified: the gateway did not say; it does not establish completion.
        await client.Sandboxes.WaitDeletedAsync("agent-1");
        break;
}

Templates

client.Templates (ISandboxTemplateClient) stores reusable workload templates (SandboxWorkloadTemplate: image, environment, resources, driver configuration, startup service level). CreateFromTemplateAsync creates a sandbox from one, and an optional spec overrides its fields.

await client.Templates.CreateAsync(new SandboxWorkloadTemplate
{
    Name = "python-small",
    Image = "registry.example.com/team/python-agent:1.0",
    Resources = new WorkloadResources { Cpu = "1", Memory = "2Gi" },
    Environment = new Dictionary<string, string> { ["PYTHONUNBUFFERED"] = "1" },
});

// The optional spec overrides fields of the template.
var sandbox = await client.Sandboxes.CreateFromTemplateAsync(
    "agent-2",
    "python-small",
    new SandboxSpec { Environment = new Dictionary<string, string> { ["MODE"] = "dev" } });