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