OpenShell Go SDK
The OpenShell Go SDK provides an idiomatic Go client for the OpenShell gateway API. It wraps the underlying gRPC protocol behind typed interfaces, making it straightforward to manage sandboxes, execute commands, handle providers, and more.
Key Features
- Sub-client pattern: A single
Clientprovides typed accessors for each API domain (Sandboxes, SandboxTemplates, Exec, Providers, Files, Health, SSH, TCP, Config, Policy, Services) - Clean types: SDK types are self-contained, so your code works with idiomatic Go types without extra dependencies
- Fake client for testing: An in-memory implementation of the full
ClientInterfacefor testing without a live gateway - Watch and streaming: First-class support for watching sandbox state changes and streaming command output
- Typed error handling: Functions like
IsNotFound,IsAlreadyExists, andIsConflictfor precise error classification
Where to Start
If you are new to the SDK, the Quick Start guide walks you through installation, connecting to a gateway, creating your first sandbox, running a command, and cleaning up.
For a deeper understanding of how the SDK is structured, see the Architecture overview.
API Reference
Every SDK interface has a dedicated reference page with method signatures and code examples.
Browse the full API Overview to see all 13 interfaces at a glance.
Guides
- Error Handling: StatusError, typed error checks, retry patterns
- Testing: Fake client usage, fixture seeding, watch event testing
Related Projects
- OpenShell (by NVIDIA): The upstream project that defines the gateway API and sandbox runtime this SDK wraps.
- openshell-sdk-go: This SDK’s source repository on GitHub.
- pkg.go.dev: Go package documentation with type signatures and godoc.
Quick Start
This guide walks you through installing the OpenShell Go SDK, connecting to a gateway, creating a sandbox, running a command, and cleaning up. You should be up and running in under 5 minutes.
Prerequisites
- Go 1.25.13 or later
- Access to an OpenShell gateway (address and authentication token)
Installation
Add the SDK to your Go module:
go get github.com/ai-openshell/sdk-go@latest
Connect to the Gateway
Create a client by providing the gateway address and authentication credentials:
package main
import (
"context"
"fmt"
"log"
v1 "github.com/ai-openshell/sdk-go/openshell/v1"
)
func main() {
client, err := v1.NewClient(v1.Config{
Address: "gateway.example.com:443",
Auth: v1.StaticToken("my-token"),
})
if err != nil {
log.Fatal(err)
}
defer client.Close()
For production use, load the token from an environment variable instead of hardcoding it:
v1.StaticToken(os.Getenv("OPENSHELL_TOKEN"))
The Config struct accepts optional fields for TLS configuration and retry policies. For development against a local gateway without TLS, use v1.NoAuth() and set TLS to skip verification.
Check Gateway Health
Verify the gateway is reachable:
ctx := context.Background()
health, err := client.Health().Check(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Gateway healthy: %v\n", health.Healthy)
Create a Sandbox
Create a sandbox with a Python image:
sandbox, err := client.Sandboxes().Create(ctx, "default", "my-sandbox", &v1.SandboxSpec{
Template: &v1.SandboxTemplate{Image: "python:3.12"},
Environment: map[string]string{"LANG": "en_US.UTF-8"},
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Created sandbox: %s\n", sandbox.Name)
Wait for the Sandbox to be Ready
Sandboxes take a moment to provision. Use WaitReady to block until the sandbox is ready to accept commands:
sandbox, err = client.Sandboxes().WaitReady(ctx, "default", sandbox.Name)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Sandbox is ready (phase: %s)\n", sandbox.Status.Phase)
Run a Command
Execute a command inside the sandbox:
result, err := client.Exec().Run(ctx, "default", sandbox.Name, []string{"echo", "hello from OpenShell"})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Output: %s", result.Stdout)
fmt.Printf("Exit code: %d\n", result.ExitCode)
For long-running commands, use Stream to receive output incrementally, or Interactive for terminal-like sessions.
Clean Up
Delete the sandbox when you are done:
deletion, err := client.Sandboxes().Delete(ctx, "default", sandbox.Name)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Deletion outcome: %v\n", deletion.Outcome)
}
Next Steps
- Browse the API Overview to see all available interfaces
- Learn about Error Handling for production code
- Set up Testing with the fake client for your test suites
- Explore the Architecture to understand the SDK design
Architecture
This page explains how the OpenShell Go SDK is structured internally. Understanding the design helps you navigate the API surface and write idiomatic code.
Client Hierarchy
The SDK follows the Kubernetes client-go sub-client pattern. A single Client provides typed accessors for each API domain:
Client
├── Sandboxes() → SandboxInterface
├── Exec() → ExecInterface
├── Providers() → ProviderInterface
│ ├── Profiles() → ProfileInterface
│ └── Refresh() → RefreshInterface
├── Services() → ServiceInterface
├── Files() → FileInterface
├── Health() → HealthInterface
├── SSH() → SSHInterface
├── TCP() → TCPInterface
├── Config() → ConfigInterface
└── Policy() → PolicyInterface
Each accessor returns an interface. You work with the interface, not the concrete implementation. This makes the sub-clients easy to mock and test.
Creating a Client
All interaction starts with NewClient:
client, err := v1.NewClient(v1.Config{
Address: "gateway.example.com:443",
Auth: v1.StaticToken("my-token"),
})
if err != nil {
log.Fatal(err)
}
defer client.Close()
The Config struct controls:
| Field | Purpose |
|---|---|
Address | Gateway host and port |
Auth | Authentication provider (StaticToken, NoAuth, or custom) |
TLS | TLS settings (CA cert, skip verify, client certs) |
Retry | Retry policy for transient failures |
Proto Isolation
The SDK never exposes protobuf-generated types in its public API. Instead, it defines its own Go types (in openshell/v1/) and converts to/from proto at the gRPC boundary.
This means:
- Your code imports
openshell/v1, notproto/openshellv1 - You work with plain Go structs, not proto messages
- Proto schema changes in upstream OpenShell do not break your code (the SDK adapts internally)
- You can use standard Go patterns (json.Marshal, fmt.Sprintf, reflect) on SDK types without proto constraints
The conversion layer lives in openshell/v1/internal/converter/ and is not part of the public API.
Sub-Client Pattern
Each sub-client groups methods for a specific API domain. For example, SandboxInterface provides Create, Get, List, Delete, WaitReady, Watch, and more.
Sub-clients are cheap to access. They are created once when the Client is initialized and reuse the same underlying gRPC connection:
// These return the same sub-client instance every time
sandboxes := client.Sandboxes()
exec := client.Exec()
Some sub-clients have their own sub-clients. ProviderInterface exposes Profiles() and Refresh():
profiles, err := client.Providers().Profiles().ListAll(ctx, "default")
status, err := client.Providers().Refresh().GetStatus(ctx, "default", "openai", "api-key")
Functional Options
Many SDK entry points accept variadic option parameters that configure the call:
auth, err := v1.RefreshableToken(tokenSource,
v1.WithLeeway(30*time.Second),
v1.WithLogger(logger),
)
Options are applied in the order they are passed, so later options override earlier ones when they set the same field. Nil options are silently ignored at every entry point. You can safely build option lists conditionally without filtering out nil entries:
opts := []v1.RefreshOption{
v1.WithLeeway(30 * time.Second),
maybeLogger(), // may return nil
}
auth, err := v1.RefreshableToken(tokenSource, opts...)
gRPC Layer
Underneath, the SDK communicates with the OpenShell gateway over gRPC. The single OpenShell service in proto/openshell.proto defines all RPCs. The SDK maps each interface method to one or more RPCs:
| Pattern | Example |
|---|---|
| Unary RPC | Create, Get, Delete |
| Server-streaming RPC | Watch, Stream, GetLogs |
| Client-streaming RPC | File uploads |
| Bidirectional streaming | Interactive, Forward |
The gRPC connection is managed by the Client. Calling client.Close() cleanly shuts down all active streams and the underlying connection.
Error Model
All SDK methods return standard Go errors. Errors from the gateway carry a StatusError with a typed error code. Use the Is* functions to classify errors:
_, err := client.Sandboxes().Get(ctx, "default", "missing")
if v1.IsNotFound(err) {
// sandbox does not exist
}
See the Error Handling guide for the complete list of error checks and retry patterns.
Fake Client
For testing, the SDK provides openshell/v1/fake with an in-memory implementation of ClientInterface. The fake client supports fixture seeding, watch events, and health simulation:
fc := fake.NewClient()
fc.AddSandbox(&v1.Sandbox{Name: "test-sb", Status: v1.SandboxStatus{Phase: v1.SandboxReady}})
See the Testing guide for complete examples.
API Overview
The OpenShell Go SDK exposes typed interfaces through the sub-client pattern.
You access each interface through a typed accessor on the concrete Client.
Interface Summary
Top-Level Interfaces
| Interface | Accessor | Description |
|---|---|---|
| SandboxInterface | client.Sandboxes() | Create, manage, and watch sandbox lifecycle |
| SandboxTemplateInterface | client.SandboxTemplates() | Manage reusable sandbox templates |
| ExecInterface | client.Exec() | Run commands, stream output, interactive sessions |
| ProviderInterface | client.Providers() | Manage compute providers and their lifecycle |
| ServiceInterface | client.Services() | Expose and manage HTTP services inside sandboxes |
| FileInterface | client.Files() | Upload and download files to/from sandboxes |
| HealthInterface | client.Health() | Check gateway health status |
| SSHInterface | client.SSH() | Create SSH sessions and tunnels to sandboxes |
| TCPInterface | client.TCP() | Forward TCP connections to sandbox ports |
| ConfigInterface | client.Config() | Read and update sandbox and gateway configuration |
| PolicyInterface | client.Policy() | Manage draft policy recommendations |
Convenience Packages
| Package | Entry Point | Description |
|---|---|---|
| gateway | gateway.NewClient(name) | Read CLI gateway configs and auto-wire clients |
Provider Sub-Interfaces
These are accessed through client.Providers():
| Interface | Accessor | Description |
|---|---|---|
| ProfileInterface | client.Providers().Profiles() | Manage provider type profiles |
| RefreshInterface | client.Providers().Refresh() | Configure credential refresh strategies |
Interfaces
Each interface has a reference page with method signatures and usage examples:
- Sandboxes: Create sandboxes, wait for readiness, watch state changes, manage providers, retrieve logs.
- Sandbox Templates: Create reusable workload templates and launch sandboxes from them.
- Exec: Execute commands with one-shot, streaming, or interactive modes.
- Providers: Register and manage compute providers. Includes sub-clients for profiles and credential refresh.
- Services: Expose and manage HTTP services inside sandboxes.
- Files: Upload and download files to/from sandboxes.
- Health: Check gateway health status.
- SSH: Create SSH sessions and tunnels to sandboxes.
- TCP: Forward TCP connections to sandbox ports.
- Config: Read and update sandbox and gateway configuration.
- Policy: Manage draft policy recommendations.
- Profiles: Manage provider type profiles (via
client.Providers().Profiles()). - Refresh: Configure credential refresh strategies (via
client.Providers().Refresh()).
Common Patterns
SDK methods follow these conventions:
- Network operations take
context.Context; list constructors return a pager, and eachNextPagecall accepts the request context - Methods that can fail return
(result, error) - List methods accept variadic options and
ListAllis the explicit exhaustive path - Errors from the gateway carry a
StatusErrorwith a typed code (see Error Handling)
Client
Constructor: v1.NewClient(config)
Client is the root entry point for SDK operations. It provides typed accessors
for each resource domain and manages the underlying gRPC connection. The
ClientInterface covers the stable core accessors; additive helpers such as
SandboxTemplates() are available on the concrete client.
Methods
| Accessor | Returns | Description |
|---|---|---|
Sandboxes() | SandboxInterface | Sandbox lifecycle management |
SandboxTemplates() | SandboxTemplateInterface | Reusable sandbox template management |
Providers() | ProviderInterface | Provider CRUD and idempotent ensure |
Services() | ServiceInterface | Service exposure and management |
Exec() | ExecInterface | Command execution (run, stream, interactive) |
Files() | FileInterface | File upload and download |
Health() | HealthInterface | Gateway health checking |
SSH() | SSHInterface | SSH session and tunnel management |
TCP() | TCPInterface | TCP port forwarding |
Config() | ConfigInterface | Sandbox and gateway configuration |
Policy() | PolicyInterface | Draft policy review workflow |
Close() | error | Close the gRPC connection |
Sub-client hierarchy: Providers() has two nested accessors:
client.Providers().Profiles()returnsProfileInterfaceclient.Providers().Refresh()returnsRefreshInterface
Creating a Client
import v1 "github.com/ai-openshell/sdk-go/openshell/v1"
client, err := v1.NewClient(v1.Config{
Address: "gateway.example.com:443",
Auth: v1.StaticToken("my-token"),
})
if err != nil {
log.Fatal(err)
}
defer client.Close()
Configuration
The Config struct controls connection behavior:
type Config struct {
Address string // Gateway address (host:port)
TLS *TLSConfig // TLS settings (nil uses system defaults)
Auth AuthProvider // Authentication provider
Timeout time.Duration // Default timeout for all operations (0 = no timeout)
RetryPolicy *RetryPolicy // Retry configuration (nil = no automatic retries)
Logger Logger // Custom logger (nil = no logging)
}
Authentication providers:
v1.StaticToken(token)provides a fixed bearer tokenv1.NoAuth()skips authentication (for local development)
Testing
For unit tests, use the fake client instead of a real connection:
import "github.com/ai-openshell/sdk-go/openshell/v1/fake"
client := fake.NewClient()
defer client.Close()
The fake client implements the full ClientInterface with in-memory stores.
See Testing for details.
See also: Getting Started, Architecture
Sandboxes
Accessor: client.Sandboxes()
Manage sandbox lifecycle: create, inspect, delete, attach/detach providers, wait for readiness, watch state changes, and retrieve logs.
Create
Creates a new sandbox with the given name and spec.
sb, err := client.Sandboxes().Create(ctx, "default", "my-sandbox", &v1.SandboxSpec{
Template: &v1.SandboxTemplate{
Image: "nvcr.io/nvidia/openshell:latest",
},
Providers: []string{"openai"},
}, v1.WithLabels(map[string]string{
"team": "platform",
}), v1.WithServiceExposures(v1.ServiceExposure{TargetPort: 8080}))
fmt.Println(sb.ServiceURLs[""])
Create-time service exposures register loopback HTTP endpoints with the
sandbox. Leave Service empty for the unnamed endpoint or set it to create a
named endpoint. Routing begins when the sandbox is ready.
ServiceURLs returns the routed URLs keyed by service name; the empty key is
the unnamed endpoint.
Set GPU: true to request the active driver’s default GPU assignment. Set
GPUCount when the sandbox needs a specific GPU count; a non-nil GPUCount
also implies GPU.
Create From Template
Creates a new sandbox from a reusable sandbox workload template. The template provides workload fields such as image, environment, resources, and driver config. The create request supplies governance fields such as providers, policy, command, and TTY.
sb, err := client.CreateSandboxFromTemplate(ctx,
"default",
"my-sandbox",
"gpu-kata",
&v1.SandboxSpec{
Providers: []string{"openai"},
Policy: policy,
},
v1.WithLabels(map[string]string{"team": "platform"}),
)
See Sandbox Templates for template CRUD.
Get
Retrieves a sandbox by name.
sb, err := client.Sandboxes().Get(ctx, "default", "my-sandbox")
fmt.Println(sb.Status.Phase) // "Ready", "Provisioning", etc.
Tool server connections
sb.Status.EndpointStatuses shows each configured tool server endpoint and its last accepted network result in one record. A sandbox can be Ready while a tool server connection fails; endpoint results do not change lifecycle readiness. OpenShell currently observes endpoints configured for MCP over HTTP.
for _, endpoint := range sb.Status.EndpointStatuses {
fmt.Printf("%s:%v%s: %s, reported %s\n",
endpoint.Host, endpoint.Ports, endpoint.Path,
endpoint.LastResult, endpoint.LastReportedAt)
}
LastResult is a typed v1.EndpointResult. For example, v1.EndpointTransportFailed means the transport failed before an HTTP response arrived. v1.EndpointHTTPResponseReceived means the server returned an HTTP status below 400; its body can still contain a tool error. Neither result establishes current availability.
OpenShell observes real traffic passively and does not expire idle observations. Keep LastReportedAt visible when displaying a result: it records when the gateway accepted the observation, not when the request happened. Retained evidence can be accepted after a reset. v1.EndpointNoObservedExchange has an empty timestamp and retains the configured address, so the endpoint remains identifiable before traffic is observed or after evidence is invalidated.
LastFailure explains a v1.EndpointCredentialUnavailable result when the supervisor classified the cause, and is nil otherwise. Each later observation replaces or clears it. Code and Stage come from closed sets, Provider and CredentialKey name the configured credential, and no field carries remote text. RequestID matches the x-openshell-request-id header the workload received, so you can correlate the refused request.
for _, endpoint := range sb.Status.EndpointStatuses {
if f := endpoint.LastFailure; f != nil && f.Domain == v1.EndpointFailureDomainTokenGrant {
fmt.Printf("%s/%s: %s at %s (request %s)\n",
f.Provider, f.CredentialKey, f.Code, f.Stage, f.RequestID)
if f.RecoveryAction == v1.RefreshRecoveryActionReauthorize {
// Ask the user to replace the provider's OAuth grant.
}
}
}
Token grant failures
sb.Status.LastTokenGrantFailure gives the latest classified grant refusal for any destination, including REST requests without an MCP inventory entry. It carries DestinationHost, DestinationPort, and the same bounded EndpointFailure as endpoint status.
if observation := sb.Status.LastTokenGrantFailure; observation != nil && observation.Failure != nil {
f := observation.Failure
fmt.Printf("%s (port %d): %s, recovery %s, request %s\n",
observation.DestinationHost, observation.DestinationPort,
f.Code, f.RecoveryAction, f.RequestID)
}
Check RequestID against the refused response before acting: a newer failure can replace this record. A successful grant for the same destination, provider, and credential clears it; a failure before credential selection clears on the next successful grant to that destination. Policy, provider, and supervisor-session resets also clear it. nil means no retained cause. This is one latest observation, not a request history or a server health check.
List
List constructs a lazy pager; NextPage fetches one page at a time.
PageSize controls each request, and ListAll explicitly exhausts the pager.
// List all sandboxes
sandboxes, err := client.Sandboxes().ListAll(ctx, "default")
// Process one page at a time
pages, err := client.Sandboxes().List("default", v1.ListOptions{PageSize: 10})
page, err := pages.NextPage(ctx)
// With a page size and label filtering
sandboxes, err := client.Sandboxes().ListAll(ctx, "default", v1.ListOptions{
PageSize: 10,
LabelSelector: "team=platform",
})
// Platform Admin only: list across all workspaces
allSandboxes, err := client.Sandboxes().ListAll(ctx, "", v1.ListOptions{
AllWorkspaces: true,
})
Delete
Deletes a sandbox by name.
deletion, err := client.Sandboxes().Delete(ctx, "default", "my-sandbox", v1.DeleteOptions{AllowMissing: true})
Missing targets return NotFound unless AllowMissing is true. Inspect
deletion.Outcome: DeletionAccepted means cleanup is pending, while
DeletionCompleted and DeletionAlreadyAbsent establish logical completion.
Unknown values do not establish completion. deletion.SandboxID identifies the
original sandbox; do not confuse a same-name replacement with that target.
Allowing absence does not make a retry safe if names can be reused.
AttachProvider
Attaches a provider to a sandbox. The expectedResourceVersion enables optimistic concurrency control: pass the sandbox’s current ResourceVersion to ensure no other client has modified it since your last read.
sb, _ := client.Sandboxes().Get(ctx, "default", "my-sandbox")
result, err := client.Sandboxes().AttachProvider(ctx,
"default", "my-sandbox",
"openai",
sb.ResourceVersion,
)
fmt.Println(result.Attached) // true if newly attached
DetachProvider
Detaches a provider from a sandbox. Uses the same optimistic concurrency pattern as AttachProvider.
sb, _ := client.Sandboxes().Get(ctx, "default", "my-sandbox")
result, err := client.Sandboxes().DetachProvider(ctx,
"default", "my-sandbox",
"openai",
sb.ResourceVersion,
)
fmt.Println(result.Detached) // true if actually detached
ListProviders
Lists all providers currently attached to a sandbox.
providers, err := client.Sandboxes().ListProviders(ctx, "default", "my-sandbox")
for _, p := range providers {
fmt.Printf("provider: %s (type: %s)\n", p.Name, p.Type)
}
WaitReady
Blocks until the sandbox reaches the Ready phase, returning the final sandbox state. Under the hood, WaitReady polls via Get at a configurable interval (default 500ms). Use context cancellation or deadlines to set a timeout.
If the sandbox enters the Error phase, WaitReady returns immediately with a StatusError.
// Wait with a 30-second timeout
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
sb, err := client.Sandboxes().WaitReady(ctx, "default", "my-sandbox")
if err != nil {
log.Fatal(err)
}
fmt.Println(sb.Status.Phase) // "Ready"
// Custom poll interval
sb, err := client.Sandboxes().WaitReady(ctx, "default", "my-sandbox", v1.WaitOptions{
PollInterval: 2 * time.Second,
})
There is no dedicated WaitReady RPC. The SDK implements this by polling GetSandbox until the sandbox phase is Ready or Error.
Watch
Opens a server-streaming connection to observe sandbox state changes in real time. Returns a WatchInterface[*Sandbox] that delivers events through a channel.
The WatchInterface[T] provides:
ResultChan() <-chan Event[T]returns the channel of eventsStop()closes the stream and the channel
Each Event[T] carries:
Type: one ofEventAdded,EventModified,EventDeleted, orEventErrorObject: the*Sandboxat that point in time (nilforEventError)
watcher, err := client.Sandboxes().Watch(ctx, "default", "my-sandbox")
if err != nil {
log.Fatal(err)
}
defer watcher.Stop()
for event := range watcher.ResultChan() {
switch event.Type {
case v1.EventModified:
fmt.Printf("phase: %s\n", event.Object.Status.Phase)
case v1.EventDeleted:
fmt.Println("sandbox deleted")
return
case v1.EventError:
fmt.Println("watch error")
return
}
}
Setting StopOnTerminal: true causes the watcher to close automatically once the sandbox reaches a terminal phase (Ready or Error). This is useful for provisioning flows where you only care about the outcome.
watcher, err := client.Sandboxes().Watch(ctx, "default", "my-sandbox", v1.WatchOptions{
StopOnTerminal: true,
})
if err != nil {
log.Fatal(err)
}
for event := range watcher.ResultChan() {
fmt.Printf("phase: %s\n", event.Object.Status.Phase)
}
// Channel closes after Ready or Error
GetLogs
Retrieves log entries from a sandbox. The sandbox is looked up by name (the SDK resolves the name to an internal ID automatically). Use functional options to filter results.
Available options:
| Option | Description |
|---|---|
WithLogLines(n uint32) | Maximum number of log lines to return |
WithLogSince(t time.Time) | Only include entries at or after this time |
WithLogSources(sources ...string) | Filter by source (e.g., "gateway", "sandbox") |
WithLogMinLevel(level string) | Minimum log level (e.g., "WARN", "ERROR") |
// Get the last 50 log lines
result, err := client.Sandboxes().GetLogs(ctx, "default", "my-sandbox",
v1.WithLogLines(50),
)
for _, line := range result.Lines {
fmt.Printf("[%s] %s: %s\n", line.Level, line.Source, line.Message)
}
// Filter by source and level since a specific time
result, err := client.Sandboxes().GetLogs(ctx, "default", "my-sandbox",
v1.WithLogSources("gateway"),
v1.WithLogMinLevel("WARN"),
v1.WithLogSince(time.Now().Add(-1*time.Hour)),
)
The LogResult contains:
Lines []LogLine: log entries in chronological orderBufferTotal uint32: total number of lines available in the server’s buffer
Each LogLine has Timestamp, Level, Target, Message, Source, and Fields (structured key-value data).
See also: Error Handling, Testing
Sandbox Templates
Accessor: client.SandboxTemplates()
Manage reusable, workspace-scoped sandbox workload templates. Templates own the portable workload shape and optional driver-specific config. Sandbox creation can reference a template by name while supplying only governance fields such as providers or policy.
The Go SDK names the reusable resource SandboxWorkloadTemplate because
SandboxTemplate already represents the legacy inline compute template inside
SandboxSpec.
Create
Creates a reusable template in a workspace.
gpuCount := uint32(1)
template, err := client.SandboxTemplates().Create(ctx, "default", &v1.SandboxWorkloadTemplate{
Name: "gpu-kata",
Labels: map[string]string{
"team": "platform",
},
Spec: v1.SandboxWorkloadTemplateSpec{
Workload: &v1.SandboxWorkloadConfig{
Image: "nvcr.io/nvidia/openshell:latest",
Environment: map[string]string{
"NVIDIA_VISIBLE_DEVICES": "all",
},
Resources: &v1.SandboxResources{
CPU: "2",
Memory: "8Gi",
GPU: &v1.SandboxGPURequirements{Count: &gpuCount},
},
},
DriverConfig: map[string]any{
"kubernetes": map[string]any{
"pod": map[string]any{
"runtime_class_name": "kata",
},
},
},
DesiredServiceLevel: &v1.SandboxServiceLevel{
Startup: &v1.SandboxStartup{
ReadyWithin: 45 * time.Second,
MaxBurst: 2,
},
},
},
})
Set GPU: &v1.SandboxGPURequirements{} to request the active driver’s default
GPU assignment without specifying a count.
Create a Sandbox From a Template
Use CreateSandboxFromTemplate to create a sandbox from a named reusable
template without changing the legacy Sandboxes() interface.
sb, err := client.CreateSandboxFromTemplate(
ctx,
"default",
"training-run",
"gpu-kata",
&v1.SandboxSpec{
Providers: []string{"openai"},
Policy: policy,
},
v1.WithLabels(map[string]string{"job": "training"}),
)
When creating from a template, the spec should only include governance fields:
Providers, Policy, Command, and TTY. Workload fields such as image,
environment, CPU, memory, GPU, and driver config come from the template.
Get
Retrieves a template by name.
template, err := client.SandboxTemplates().Get(ctx, "default", "gpu-kata")
fmt.Println(template.Spec.Workload.Image)
List
List returns a lazy pager over matching templates in one workspace or across
all workspaces. Use ListAll to follow every continuation token automatically.
templates, err := client.SandboxTemplates().ListAll(ctx, "default", v1.ListOptions{
PageSize: 50,
})
allTemplates, err := client.SandboxTemplates().ListAll(ctx, "", v1.ListOptions{
AllWorkspaces: true,
})
Delete
Deletes a template by name. Existing sandboxes created from the template are not deleted.
deletion, err := client.SandboxTemplates().Delete(ctx, "default", "gpu-kata")
Fake Client
The fake client includes the same template sub-client and can be pre-populated for tests.
client := fake.NewClient()
client.AddSandboxTemplate("default", &types.SandboxWorkloadTemplate{
Name: "gpu-kata",
Spec: types.SandboxWorkloadTemplateSpec{
Workload: &types.SandboxWorkloadConfig{Image: "python:3.12"},
},
})
Exec
Accessor: client.Exec()
Execute commands in a sandbox with three modes: one-shot (Run), streaming
(Stream), or interactive terminal (Interactive).
Run
Execute a command and collect all output into a single result.
result, err := client.Exec().Run(ctx, "default", "sbx-123", []string{"ls", "-la"})
if err != nil {
log.Fatal(err)
}
fmt.Println("Exit code:", result.ExitCode)
fmt.Println("Stdout:", result.Stdout)
fmt.Println("Stderr:", result.Stderr)
The SDK collects all streamed events, assembles stdout/stderr, and returns a single ExecResult.
ExecResult contains the complete output after the command finishes:
| Field | Type | Description |
|---|---|---|
Stdout | string | Captured standard output |
Stderr | string | Captured standard error |
ExitCode | int | Process exit code |
Stream
Execute a command and process output chunks as they arrive.
stream, err := client.Exec().Stream(ctx, "default", "sbx-123", []string{"tail", "-f", "/var/log/app.log"})
if err != nil {
log.Fatal(err)
}
defer stream.Close()
for {
chunk, err := stream.Next()
if err == io.EOF {
break
}
if err != nil {
log.Fatal(err)
}
if chunk.Stream == v1.StreamStdout {
fmt.Print(string(chunk.Data))
}
}
exitCode, err := stream.ExitCode()
if err != nil {
log.Fatal(err)
}
fmt.Println("Exited with:", exitCode)
Interactive
Open a bidirectional terminal session with a command.
session, err := client.Exec().Interactive(ctx, "default", "sbx-123", []string{"/bin/bash"}, 80, 24)
if err != nil {
log.Fatal(err)
}
defer session.Close()
// Send input
_, err = session.Write([]byte("echo hello\n"))
if err != nil {
log.Fatal(err)
}
// Read output
buf := make([]byte, 4096)
n, err := session.Read(buf)
if err != nil {
log.Fatal(err)
}
fmt.Print(string(buf[:n]))
// Handle terminal resize
if err := session.Resize(120, 40); err != nil {
log.Fatal(err)
}
// Get exit code after session ends
exitCode, err := session.ExitCode()
if err != nil {
log.Fatal(err)
}
fmt.Println("Exited with:", exitCode)
The SDK wraps the bidirectional stream as an InteractiveSession with Read/Write/Resize methods.
ExecStream
ExecStream provides an iterator interface over command output chunks. Call Next() repeatedly to receive output as it is produced. When the command finishes, Next() returns io.EOF.
type ExecStream interface {
Next() (*ExecChunk, error)
ExitCode() (int, error)
Close() error
}
| Method | Description |
|---|---|
Next | Returns the next output chunk. Returns io.EOF when done. |
ExitCode | Returns the process exit code. Call after Next returns io.EOF. |
Close | Releases the underlying stream resources. |
InteractiveSession
InteractiveSession implements io.Reader and io.Writer for bidirectional communication with a running process. Use it for terminal emulation, REPL interaction, or any command that requires ongoing input.
type InteractiveSession interface {
Read(p []byte) (int, error)
Write(p []byte) (int, error)
Resize(cols, rows uint32) error
ExitCode() (int, error)
Close() error
}
| Method | Description |
|---|---|
Read | Reads output from the process into the provided buffer. |
Write | Sends input to the process. |
Resize | Updates the terminal dimensions (columns and rows). |
ExitCode | Waits for final gRPC status; returns any observed exit code alongside a later stream error. |
Close | Aborts the RPC and releases resources. |
SDK sessions also implement the optional InteractiveSessionControl interface,
which embeds InteractiveSession and adds CloseWrite() error and Cancel() error.
Existing custom implementations only need the original methods above.
Call CloseInteractiveInput(session) to end stdin and resize input without
cancelling output. It invokes CloseWrite() when supported; otherwise it returns
ErrorUnimplemented without closing the session. Call CancelInteractive(session)
to invoke Cancel() when supported, falling back to Close() for legacy sessions.
The SDK’s close and cancel methods are idempotent.
Drain Read concurrently with waiting for ExitCode; bounded output buffers can
otherwise prevent completion. After input closure, writes and resizes return
io.ErrClosedPipe. Request EOF is not equivalent to a terminal Ctrl-D keystroke.
ExecChunk
Each chunk from ExecStream.Next() carries a segment of process output along with which stream it came from.
type ExecChunk struct {
Data []byte
Stream StreamType
}
| Field | Type | Description |
|---|---|---|
Data | []byte | Raw output bytes from the process. |
Stream | StreamType | Either StreamStdout or StreamStderr. |
See also: Error Handling, Testing
Providers
Accessor: client.Providers()
Register and manage compute providers (AI inference endpoints). Exposes sub-clients for Profiles and Refresh.
Create
Register a new provider with the gateway.
provider, err := client.Providers().Create(ctx, "default", &v1.Provider{
Name: "my-openai",
Type: "openai",
Spec: v1.ProviderSpec{
Credentials: map[string]string{
"api_key": "sk-...",
},
Config: map[string]string{
"base_url": "https://api.openai.com/v1",
},
},
})
if err != nil {
log.Fatal(err)
}
fmt.Println("Created provider:", provider.Name)
Get
Fetch a provider by name.
provider, err := client.Providers().Get(ctx, "default", "my-openai")
if err != nil {
log.Fatal(err)
}
fmt.Println("Provider type:", provider.Type)
List
List returns a lazy pager over registered providers. ListAll follows every
continuation token; PageSize controls each request.
// List all providers
providers, err := client.Providers().ListAll(ctx, "default")
if err != nil {
log.Fatal(err)
}
for _, p := range providers {
fmt.Println(p.Name, p.Type)
}
// With a smaller page size
providers, err = client.Providers().ListAll(ctx, "default", v1.ListOptions{
PageSize: 10,
})
// Platform Admin only: list across all workspaces
allProviders, err := client.Providers().ListAll(ctx, "", v1.ListOptions{
AllWorkspaces: true,
})
Update
Update an existing provider’s configuration or credentials.
provider, err := client.Providers().Get(ctx, "default", "my-openai")
if err != nil {
log.Fatal(err)
}
provider.Spec.Credentials["api_key"] = "sk-new-key"
updated, err := client.Providers().Update(ctx, "default", provider)
if err != nil {
log.Fatal(err)
}
fmt.Println("Updated provider:", updated.Name)
Delete
Remove a provider by name.
deletion, err := client.Providers().Delete(ctx, "default", "my-openai")
if err != nil {
log.Fatal(err)
}
Ensure
Create or update a provider in a single idempotent call. If a provider with the given name exists, it is updated; otherwise a new one is created. This is the recommended way to register providers because it avoids “already exists” errors when re-registering.
provider, err := client.Providers().Ensure(ctx, "default", &v1.Provider{
Name: "my-openai",
Type: "openai",
Spec: v1.ProviderSpec{
Credentials: map[string]string{
"api_key": "sk-...",
},
},
})
if err != nil {
log.Fatal(err)
}
fmt.Println("Provider ready:", provider.Name)
Ensure is an SDK-level convenience. It calls GetProvider first, then either CreateProvider or UpdateProvider depending on whether the provider already exists.
Sub-Clients
The ProviderInterface exposes two sub-client accessors for related operations. These are pure client-side accessors with no corresponding gRPC call.
Profiles
client.Providers().Profiles() returns a ProfileInterface for managing provider type profiles. Profiles define templates and defaults for different provider types.
Refresh
client.Providers().Refresh() returns a RefreshInterface for configuring credential refresh strategies. Use it to set up automatic credential rotation for providers with expiring credentials.
Provider
The Provider type represents a registered compute provider.
| Field | Type | Description |
|---|---|---|
ID | string | Server-assigned unique identifier |
Name | string | User-chosen name (unique per gateway) |
Type | string | Provider type (e.g., "openai", "azure") |
CreatedAt | time.Time | Timestamp of creation |
Labels | map[string]string | Key-value metadata labels |
ResourceVersion | uint64 | Optimistic concurrency version |
Spec | ProviderSpec | Configuration and credentials |
ProviderSpec
ProviderSpec holds provider-specific configuration and credentials.
| Field | Type | Description |
|---|---|---|
Credentials | map[string]string | Authentication credentials (e.g., API keys) |
Config | map[string]string | Provider-specific configuration values |
CredentialExpiresAt | map[string]time.Time | Expiration timestamps for credentials |
The curated Go API uses a zero time.Time value to clear an expiration during
an update. Consequently, it cannot represent the protobuf minimum timestamp
(0001-01-01T00:00:00Z) in this map. Use the raw protobuf API if that exact
timestamp is required. Other pointer-based timestamp fields preserve it.
See also: Profiles, Refresh, Error Handling, Testing
Profiles
Accessor: client.Providers().Profiles()
Manage provider profiles for AI model providers. Profiles define connection details, credentials, and model mappings for providers like OpenAI, Anthropic, or custom endpoints.
List
List returns a lazy pager over visible provider profiles. Use ListAll to
collect every page.
profiles, err := client.Providers().Profiles().ListAll(ctx, "default")
if err != nil {
log.Fatal(err)
}
for _, p := range profiles {
fmt.Printf("Profile: %s (%s)\n", p.ID, p.DisplayName)
}
Import
Import one or more provider profiles from configuration items.
result, err := client.Providers().Profiles().Import(ctx, "default", []v1.ProfileImportItem{
{
Profile: v1.ProviderProfile{
DisplayName: "OpenAI",
Category: v1.ProfileCategoryInference,
},
Source: "manual",
},
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Imported: %v\n", result.Imported)
Update
Update a profile using optimistic concurrency control via the resource version.
profile, err := client.Providers().Profiles().Get(ctx, "default", "profile-id")
if err != nil {
log.Fatal(err)
}
profile.DisplayName = "Updated Provider"
result, err := client.Providers().Profiles().Update(
ctx, "default",
profile.ID,
profile.ResourceVersion,
v1.ProfileImportItem{Profile: *profile, Source: "manual"},
)
if err != nil {
// See [Error Handling](../error-handling.md) for conflict errors
log.Fatal(err)
}
Lint
Validate profile configurations without persisting them. Useful for pre-flight checks.
items := []v1.ProfileImportItem{
{
Profile: v1.ProviderProfile{DisplayName: "Test"},
Source: "manual",
},
}
result, err := client.Providers().Profiles().Lint(ctx, "default", items)
if err != nil {
log.Fatal(err)
}
for _, d := range result.Diagnostics {
fmt.Printf("[%s] %s: %s\n", d.Severity, d.Field, d.Message)
}
See also: Error Handling, Testing
Refresh
Accessor: client.Providers().Refresh()
Manage credential refresh schedules for provider profiles. Configure automatic rotation of API keys and monitor refresh status.
GetStatus
Check the refresh status for a specific provider credential.
statuses, err := client.Providers().Refresh().GetStatus(ctx, "default", "openai", "default")
if err != nil {
log.Fatal(err)
}
for _, s := range statuses {
fmt.Printf("Key: %s, Last refresh: %s, Next: %s\n",
s.CredentialKey, s.LastRefreshAt, s.NextRefreshAt)
}
Configure
Set up automatic credential refresh with a defined strategy and material.
status, err := client.Providers().Refresh().Configure(ctx, "default", &v1.RefreshConfig{
Provider: "openai",
CredentialKey: "default",
Strategy: v1.RefreshStrategyOAuth2ClientCredentials,
Material: map[string]string{
"client_id": "my-client-id",
"client_secret": "my-client-secret",
"token_url": "https://oauth.example.com/token",
},
SecretMaterialKeys: []string{"client_secret"},
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Refresh configured, next rotation: %s\n", status.NextRefreshAt)
Rotate
Manually trigger an immediate credential rotation.
status, err := client.Providers().Refresh().Rotate(ctx, "default", "openai", "default")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Rotated successfully at %s\n", status.LastRefreshAt)
See also: Error Handling, Profiles
Services
Accessor: client.Services()
Expose, inspect, and manage network services attached to sandboxes. Services provide external access to ports running inside a sandbox via managed endpoints.
Expose
Expose a port from a sandbox as a named service endpoint. Set domain to true
to assign a DNS-routable domain name to the service.
endpoint, err := client.Services().Expose(ctx, "default", "my-sandbox", "web", 8080, true)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Service available at: %s\n", endpoint.URL)
List
List returns a lazy pager over exposed services. Use ListAll to collect
every page.
services, err := client.Services().ListAll(ctx, "default", "my-sandbox")
if err != nil {
log.Fatal(err)
}
for _, svc := range services {
fmt.Printf(" %s -> port %d (%s)\n", svc.Name, svc.TargetPort, svc.URL)
}
// Platform Admin only: list services across all workspaces
allServices, err := client.Services().ListAll(ctx, "", "", v1.ListOptions{
AllWorkspaces: true,
})
Delete
Remove an exposed service. The underlying sandbox port remains accessible internally but is no longer reachable through the service endpoint.
deletion, err := client.Services().Delete(ctx, "default", "my-sandbox", "web")
if err != nil {
log.Fatal(err)
}
See also: Error Handling, Testing
Files
Accessor: client.Files()
FileInterface reserves the upload and download API while keeping sandbox lookup
and SSH-session lifecycle behavior stable. The standalone SDK does not currently
ship an SSH file-transfer transport, so both operations return
v1.ErrTransportNotAvailable before performing local validation or gateway RPCs.
Upload
Detect transport availability programmatically:
err := client.Files().Upload(ctx, "default", "sandbox-123", "./data/config.yaml", "/app/config.yaml")
if errors.Is(err, v1.ErrTransportNotAvailable) {
// Use another transfer mechanism until an SSH transport is available.
} else if err != nil {
log.Fatal(err)
}
Download
Download has the same capability gate:
err := client.Files().Download(ctx, "default", "sandbox-123", "/app/output.log", "./output.log")
if errors.Is(err, v1.ErrTransportNotAvailable) {
// Use another transfer mechanism until an SSH transport is available.
} else if err != nil {
log.Fatal(err)
}
See also: Error Handling, Testing
Health
Accessor: client.Health()
Check the health status of the connected OpenShell gateway.
Check
Perform a health check against the gateway. Returns the overall status and component-level details.
result, err := client.Health().Check(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Gateway healthy: %v\n", result.Healthy)
This is useful for readiness probes or verifying connectivity before executing other operations.
// Quick connectivity check before starting work
if result, err := client.Health().Check(ctx); err != nil {
log.Fatalf("Gateway unreachable: %v", err)
} else if !result.Healthy {
log.Fatal("Gateway is not healthy")
}
See also: Error Handling
SSH
Accessor: client.SSH()
Create and manage SSH sessions for sandboxes. Supports direct SSH access and TCP tunneling through SSH connections.
CreateSession
Create a new SSH session for a sandbox. Returns connection details including host, port, and authentication credentials.
session, err := client.SSH().CreateSession(ctx, "default", "sandbox-123")
if err != nil {
log.Fatal(err)
}
fmt.Printf("SSH via %s://%s:%d\n", session.GatewayScheme, session.GatewayHost, session.GatewayPort)
RevokeSession
Revoke an active SSH session, immediately terminating any connections using it.
deletion, err := client.SSH().RevokeSession(ctx, "default", session.Token)
if err != nil {
log.Fatal(err)
}
if deletion.Outcome == v1.DeletionCompleted {
fmt.Println("Session revoked")
}
Tunnel
Create an SSH tunnel that provides a bidirectional stream to a port inside a
sandbox. This combines SSH session creation with TCP forwarding into a single
operation, returning an io.ReadWriteCloser for the tunnel.
tunnel, err := client.SSH().Tunnel(ctx, "default", "my-sandbox", 8080)
if err != nil {
log.Fatal(err)
}
defer tunnel.Close()
// Use the tunnel as a regular io.ReadWriteCloser
_, err = tunnel.Write([]byte("GET / HTTP/1.0\r\n\r\n"))
if err != nil {
log.Fatal(err)
}
See also: TCP Forwarding, Error Handling
TCP
Accessor: client.TCP()
Forward TCP connections to sandbox ports using bidirectional gRPC streaming.
Forward
Open a bidirectional TCP forwarding stream to a specific port inside a sandbox.
Returns an io.ReadWriteCloser that proxies data between the caller and the
sandbox port over gRPC streaming.
conn, err := client.TCP().Forward(ctx, "default", "sandbox-123", 8080)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
// Exchange protocol bytes with the service through the tunnel.
_, err = conn.Write([]byte("ping\n"))
if err != nil {
log.Fatal(err)
}
buf := make([]byte, 4096)
n, err := conn.Read(buf)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Response: %s\n", buf[:n])
Listen
Bind a local address that forwards every connection to a sandbox port. The
returned ForwardListener owns its accept loop and all bridge goroutines; it is
a lifecycle handle with Addr and Close, not a net.Listener.
forward, err := client.TCP().Listen(ctx, "default", "sandbox-123", 8080, 0)
if err != nil {
log.Fatal(err)
}
defer forward.Close()
conn, err := net.Dial("tcp", forward.Addr().String())
if err != nil {
log.Fatal(err)
}
defer conn.Close()
Do not pass ForwardListener to http.Serve. Dial its address with the client
for the protocol exposed by the sandbox service.
TCP forwarding is lower-level than SSH tunneling. Use TCP forwarding when you need direct port access without SSH session overhead.
See also: SSH Tunneling, Error Handling
Config
Accessor: client.Config()
Retrieve and update configuration for sandboxes and the gateway.
GetSandbox
Retrieve the current configuration for a specific sandbox.
config, err := client.Config().GetSandbox(ctx, "default", "sandbox-123")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Sandbox config: policy_version=%d, revision=%d\n",
config.PolicyVersion, config.ConfigRevision)
GetGateway
Retrieve the gateway-level configuration.
config, err := client.Config().GetGateway(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Gateway settings revision: %d\n", config.SettingsRevision)
Update
Apply a configuration update. The update is validated before being applied.
result, err := client.Config().Update(ctx, "default", &v1.ConfigUpdate{
Name: "sandbox-123",
SettingKey: "idle_timeout",
SettingValue: &v1.SettingValue{
Type: v1.SettingValueString,
StringVal: "30m",
},
})
if err != nil {
// See [Error Handling](../error-handling.md) for validation errors
log.Fatal(err)
}
fmt.Printf("Config updated: revision=%d\n", result.SettingsRevision)
See also: Error Handling
Policy
Accessor: client.Policy()
Manage network policies for sandboxes through a draft-based workflow. Policies go through a draft, review, and approval cycle before being applied.
GetDraft
Retrieve the current draft policy for a sandbox.
draft, err := client.Policy().GetDraft(ctx, "default", "my-sandbox")
if err != nil {
log.Fatal(err)
}
for _, chunk := range draft.Chunks {
fmt.Printf("Chunk %s: rule=%s, status=%s, confidence=%.1f\n",
chunk.ID, chunk.RuleName, chunk.Status, chunk.Confidence)
}
ApproveAllDraftChunks
Approve all pending chunks in a single operation.
result, err := client.Policy().ApproveAllDraftChunks(ctx, "default", "my-sandbox")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Approved %d chunks (skipped %d), policy version: %d\n",
result.ChunksApproved, result.ChunksSkipped, result.PolicyVersion)
GetStatus
Check the current policy enforcement status for a sandbox.
status, err := client.Policy().GetStatus(ctx, "default", "my-sandbox")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Active version: %d, revision status: %s\n",
status.ActiveVersion, status.Revision.Status)
List
List returns a lazy pager over policy revisions for one sandbox. Use
ListAll to collect every page.
revisions, err := client.Policy().ListAll(ctx, "default", "my-sandbox")
if err != nil {
log.Fatal(err)
}
for _, rev := range revisions {
fmt.Printf("Version %d: %s (status: %s)\n",
rev.Version, rev.CreatedAt, rev.Status)
}
RejectDraftChunk
Reject a specific draft chunk, providing a reason.
err := client.Policy().RejectDraftChunk(ctx, "default", "my-sandbox", "chunk-abc", "Too permissive")
if err != nil {
log.Fatal(err)
}
EditDraftChunk
Modify the proposed rule in a draft chunk before approval.
err := client.Policy().EditDraftChunk(ctx, "default", "my-sandbox", "chunk-abc", &v1.NetworkPolicyRule{
Name: "allow-api",
Endpoints: []v1.PolicyNetworkEndpoint{{
Host: "api.example.com",
Port: 443,
Protocol: "tcp",
}},
})
if err != nil {
log.Fatal(err)
}
Append allow or deny rules
Use client.Config().Update with AddAllowRules or AddDenyRules to append request matchers to an existing base-policy endpoint. Both operations require an L7RuleTarget naming the rule, host, every affected port, and every binary governed by that rule. The gateway rejects missing scope or a scope that differs from the current policy without applying the merge batch.
result, err := client.Config().Update(ctx, "default", &v1.ConfigUpdate{
Name: "my-sandbox",
MergeOperations: []v1.PolicyMergeOperation{{
AddAllowRules: &v1.AddAllowRules{
Target: &v1.L7RuleTarget{
RuleName: "api",
Host: "api.example.com",
Ports: []uint32{443, 8443},
Binaries: []v1.PolicyNetworkBinary{
{Path: "/usr/bin/curl"},
{Path: "/usr/bin/wget"},
},
},
Rules: []v1.L7Rule{{
Allow: &v1.L7Allow{Method: "POST", Path: "/admin"},
}},
},
}},
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Policy version: %d\n", result.Version)
This example explicitly permits both binaries to make the new request on both ports. For a rule that permits any binary, set AnyBinary: true and omit Binaries. Omitting both fields is invalid; the SDK never infers any-binary permission.
Target.Path selects the existing endpoint path and is separate from the appended request matcher’s path. Leave it nil when the rule, host, and ports identify a unique endpoint. To choose an endpoint with a specific path, pass a pointer to that path; a pointer to an empty string selects an endpoint without a path selector. Use AddDenyRules with the same target shape and a DenyRules payload to append deny matchers.
See also: Error Handling, Testing
Gateway
Package: openshell/v1/gateway
The gateway package reads on-disk gateway configurations created by the OpenShell Rust CLI and constructs fully wired SDK clients. It eliminates the boilerplate of locating config files, parsing metadata, loading tokens, and wiring auth providers.
Quick Start
import "github.com/ai-openshell/sdk-go/openshell/v1/gateway"
// Connect to a named gateway
client, err := gateway.NewClient("prod")
if err != nil {
log.Fatal(err)
}
defer client.Close()
Functions
| Function | Description |
|---|---|
NewClient(name, opts...) | Create a fully wired SDK client from gateway config |
LoadConfig(name) | Parse gateway config without connecting |
ListGateways() | Enumerate all available gateways |
NewClient
func NewClient(name string, opts ...ClientOption) (*v1.Client, error)
Creates a fully configured SDK client for the named gateway. If name
is empty, the active gateway (set via openshell gateway use) is used.
The function resolves the gateway directory, parses metadata.json,
loads tokens lazily, maps the auth mode to an SDK auth provider, and
applies any ClientOption values before delegating to v1.NewClient.
// Named gateway
client, err := gateway.NewClient("prod")
// Active gateway
client, err := gateway.NewClient("")
// With options
client, err := gateway.NewClient("staging",
gateway.WithTimeout(10 * time.Second),
gateway.WithLogger(myLogger),
)
Errors: ErrGatewayNotFound, ErrConfigParse,
ErrUnsupportedAuthMode, ErrInvalidGatewayName, ErrNoActiveGateway
LoadConfig
func LoadConfig(name string) (*Config, error)
Parses gateway configuration without creating a client connection. Returns a frozen snapshot; changes to on-disk files after the call are not reflected.
cfg, err := gateway.LoadConfig("staging")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Endpoint: %s, Auth: %s\n", cfg.Endpoint, cfg.AuthMode)
ListGateways
func ListGateways() ([]Info, error)
Enumerates all available gateways from user and system directories. User gateways appear first. Duplicate names resolve to user precedence. Returns an empty slice (not an error) when no gateways are configured.
gateways, err := gateway.ListGateways()
for _, gw := range gateways {
fmt.Printf("%s (active=%v, source=%s)\n", gw.Name, gw.Active, gw.Source)
}
Types
Config
type Config struct {
Name string // Validated gateway name
Endpoint string // Host:port of the gateway
AuthMode AuthMode // Resolved auth mode
Source ConfigSource // User or System origin
Dir string // Absolute path to gateway config directory
}
Info
type Info struct {
Name string // Gateway name from directory listing
Active bool // Whether this is the active gateway
Source ConfigSource // User or System origin
}
AuthMode
| Value | Constant | SDK AuthProvider |
|---|---|---|
"" or "none" | AuthModeNone | v1.NoAuth() |
"plaintext" | AuthModePlaintext | v1.NoAuth() + insecure TLS |
"cloudflare_jwt" | AuthModeCloudflareJWT | Lazy edge token auth |
"oidc" | AuthModeOIDC | v1.RefreshableToken that reads oidc_token.json near expiry and fails once the token has expired |
"mtls" | AuthModeMTLS | Unsupported (use WithAuth) |
ClientOption
| Constructor | Effect |
|---|---|
WithLogger(l) | Set logger on the SDK client |
WithTimeout(d) | Set connection timeout |
WithTLS(cfg) | Override TLS settings from gateway config |
WithAuth(provider) | Override auto-resolved auth provider |
WithRetryPolicy(p) | Set retry policy |
Error Handling
All errors support errors.Is for classification:
client, err := gateway.NewClient("my-gateway")
if errors.Is(err, gateway.ErrGatewayNotFound) {
fmt.Println("Gateway not configured. Run: openshell gateway add my-gateway")
}
Tokens load lazily, so NewClient does not return ErrTokenLoad. For oidc
gateways, a missing, malformed, or expired oidc_token.json surfaces on RPCs as
an error for which v1.IsUnauthenticated(err) is true, and its message includes
run `openshell gateway login <name>` .
| Error | Meaning |
|---|---|
ErrGatewayNotFound | No gateway directory in user or system paths |
ErrConfigParse | metadata.json missing or malformed |
ErrTokenLoad | Token file missing, unreadable, malformed, or expired; RPCs report it as Unauthenticated |
ErrUnsupportedAuthMode | Unrecognized auth_mode value |
ErrInvalidGatewayName | Name fails validation |
ErrNoActiveGateway | No active gateway configured |
On-Disk Layout
The package reads gateway metadata from these locations:
$XDG_CONFIG_HOME/openshell/ (user, default: ~/.config/openshell/)
├── active_gateway # Plain text: active gateway name
└── gateways/
└── <name>/
├── metadata.json # {"endpoint":"...","auth_mode":"...","name":"..."}
├── edge_token # Cloudflare edge JWT (plaintext)
├── cf_token # Legacy edge token (fallback)
└── oidc_token.json # OIDC token bundle
/etc/openshell/gateways/ (system, fallback)
User gateways take precedence over system gateways with the same name.
Thread Safety
All exported functions (NewClient, LoadConfig, ListGateways) are
safe for concurrent use from multiple goroutines. Token loading uses
internal synchronization.
OIDC Login
Package: openshell/v1/oidc
The oidc package provides OIDC authentication for OpenShell gateways. It supports four OAuth2 flows: browser-based authorization code with PKCE, keyboard fallback, device code (RFC 8628), and client credentials.
Quick Start
import "github.com/ai-openshell/sdk-go/openshell/v1/oidc"
// Gateway-aware login (reads OIDC config from gateway metadata)
token, err := oidc.Login(ctx, "my-gateway")
if err != nil {
log.Fatal(err)
}
Functions
| Function | Description |
|---|---|
Login(ctx, gatewayName, opts...) | Interactive login via browser or keyboard flow |
DeviceLogin(ctx, opts...) | Device authorization grant (RFC 8628) |
ClientCredentials(ctx, opts...) | Non-interactive client credentials grant |
Login
func Login(ctx context.Context, gatewayName string, opts ...LoginOption) (*oauth2.Token, error)
Performs an interactive OIDC login. When gatewayName is provided, the
OIDC issuer and client ID are read from the gateway’s metadata.json.
The default flow opens a browser for authorization code exchange with
PKCE. If the provider does not support PKCE (S256), the flow proceeds
without it.
Tokens are persisted to the gateway’s config directory as
oidc_token.json, in the same format as openshell gateway login, so CLI
commands against that gateway can use them. Subsequent calls reuse the stored
token only when it is valid and was issued for the gateway’s issuer and client
ID; otherwise Login runs the flow again and rewrites the file.
For standalone use (no gateway), pass an empty gatewayName with
WithIssuer and WithClientID. Combine with WithInMemory to skip
disk persistence.
DeviceLogin
func DeviceLogin(ctx context.Context, opts ...LoginOption) (*oauth2.Token, error)
Performs an OAuth2 device authorization grant (RFC 8628). The flow
requests a device code and user code from the provider, displays them
via WithDisplayFunc (or stdout), and polls the token endpoint until
the user completes authorization.
Requires WithIssuer and WithClientID, or WithGateway.
ClientCredentials
func ClientCredentials(ctx context.Context, opts ...LoginOption) (*oauth2.Token, error)
Performs a non-interactive OAuth2 client credentials grant. Requires
WithIssuer, WithClientID, and WithClientSecret (or WithGateway
combined with WithClientSecret). The client secret is never included
in error messages.
NewClientCredentialsAuth
func NewClientCredentialsAuth(opts ...LoginOption) (types.AuthProvider, error)
Returns a lazy renewable auth provider for v1.Config.Auth or
gateway.WithAuth. It caches access tokens in memory, renews within 30 seconds
of expiry, coalesces concurrent exchanges, and fails closed if renewal fails.
Use WithClientSecretProvider when the application rotates secret material.
Options
| Option | Description |
|---|---|
WithIssuer(url) | OIDC provider issuer URL |
WithClientID(id) | OAuth2 client ID |
WithClientSecret(secret) | OAuth2 client secret (for client credentials) |
WithClientSecretProvider(provider) | Resolve the client secret for each exchange |
WithAudience(audience) | Optional OAuth2 resource-server audience |
WithScopes(scopes...) | Custom scopes (client credentials has no implicit scopes) |
WithCallbackPort(port) | Fixed port for localhost callback (default: tries 8000, then 18000) |
WithTimeout(d) | Auth flow timeout (default: 2 minutes) |
WithKeyboardFlow() | Use keyboard flow instead of browser |
WithInMemory() | Skip token persistence to disk |
WithDisplayFunc(fn) | Custom display for device code flow |
WithGateway(name) | Resolve OIDC config from gateway metadata |
Error Handling
The package defines sentinel errors for errors.Is() matching:
| Error | Description |
|---|---|
ErrDiscovery | OIDC discovery document fetch failed |
ErrAuthCode | Authorization code exchange failed |
ErrDeviceCode | Device code flow failed |
ErrClientCredentials | Client credentials exchange failed |
ErrTimeout | Authentication flow timed out |
ErrCallbackServer | Localhost callback server failed |
ErrTokenPersist | Token read/write failed |
ErrOIDCConfig | Missing or invalid OIDC configuration |
token, err := oidc.Login(ctx, "my-gateway")
if errors.Is(err, oidc.ErrDiscovery) {
// Provider unreachable
}
if errors.Is(err, oidc.ErrTimeout) {
// User did not complete login in time
}
Gateway Integration
When a gateway’s metadata.json contains oidc_issuer and
oidc_client_id fields, Login and DeviceLogin can resolve
configuration automatically:
// Login reads OIDC config from the gateway
token, err := oidc.Login(ctx, "my-gateway")
// Then use the token with the SDK client
client, err := v1.NewClient(v1.Config{
Address: "gateway.example.com:443",
Auth: v1.StaticToken(token.AccessToken),
})
Flows
Browser Flow (default)
- Discovers OIDC endpoints from issuer
- Generates PKCE code verifier and S256 challenge
- Opens browser to authorization URL
- Starts localhost callback server to receive the auth code
- Exchanges code for tokens with PKCE verification
- Persists tokens to disk
Keyboard Flow
Same as browser flow, but prints the authorization URL for the user to
copy manually. The user pastes the authorization code back into the
terminal. Use WithKeyboardFlow() to enable.
Device Code Flow (RFC 8628)
- Requests a device code and user code from the provider
- Displays the verification URL and user code
- Polls the token endpoint at the provider’s requested interval
- Handles
slow_downresponses by increasing the poll interval
Client Credentials Flow
- Discovers OIDC endpoints from issuer
- Exchanges client ID and secret for an access token
- No user interaction required
Edge
Package: openshell/v1/edge
The edge package provides utilities for connecting to OpenShell gateways through edge proxies such as Cloudflare Access. It includes auth wrappers for edge proxy headers and a WebSocket tunnel proxy for gRPC transport through HTTP/1.1-only proxies.
Cloudflare Access
Wrap any AuthProvider with Cloudflare Access headers
(cf-access-jwt-assertion and CF_Authorization cookie):
import "github.com/ai-openshell/sdk-go/openshell/v1/edge"
base := v1.StaticToken("my-gateway-token")
auth, err := edge.CloudflareAccess(base, os.Getenv("CF_ACCESS_TOKEN"))
if err != nil {
log.Fatal(err)
}
client, err := v1.NewClient(v1.Config{
Address: "gateway.example.com:443",
Auth: auth,
})
CloudflareAccess composes with any auth provider, including RefreshableToken
for automatic token refresh:
tokenSource := oauth2Config.TokenSource(ctx, initialToken)
refreshAuth, err := v1.RefreshableToken(tokenSource)
if err != nil {
log.Fatal(err)
}
auth, err := edge.CloudflareAccess(refreshAuth, cfToken)
WebSocket Tunnel
TunnelProxy bridges gRPC connections over a WebSocket tunnel for edge
proxies that reject standard HTTP/2 POST requests. The tunnel carries
its own edge token for proxy authentication, independent of the
application-level auth provider.
tunnel, err := edge.NewTunnelProxy(
"wss://gateway.example.com/ws",
os.Getenv("CF_ACCESS_TOKEN"),
)
if err != nil {
log.Fatal(err)
}
defer tunnel.Close()
auth := v1.StaticToken("my-gateway-token")
client, err := v1.NewClient(v1.Config{
Address: tunnel.Addr(),
Auth: auth,
TLS: &v1.TLSConfig{Insecure: true}, // local tunnel
})
Functions
| Function | Description |
|---|---|
CloudflareAccess(base, edgeToken) | Wrap an AuthProvider with Cloudflare Access headers |
NewTunnelProxy(url, edgeToken, opts...) | Create a WebSocket tunnel proxy for gRPC-over-HTTP/1.1 |
TunnelProxy Methods
| Method | Description |
|---|---|
Addr() | Local listener address for gRPC client to dial |
Close() | Gracefully drain in-flight connections and shut down |
TunnelOption
| Constructor | Effect |
|---|---|
WithTunnelTLS(cfg) | Configure TLS for the WebSocket connection |
WithTunnelLogger(l) | Set a logger for tunnel events |
WithCloseTimeout(d) | Override the graceful shutdown timeout (default 5s) |
Thread Safety
All exported functions and methods are safe for concurrent use.
Close is idempotent and safe to call multiple times.
Fake
Package: openshell/v1/fake
The fake package provides an in-memory fake implementation of all SDK
client interfaces for use in consumer test suites. It follows the
client-go/kubernetes/fake pattern: in-memory stores, watch event
broadcasting, and matching StatusError codes for equivalent error
conditions (NotFound, AlreadyExists, Unavailable, Unimplemented).
Quick Start
import "github.com/ai-openshell/sdk-go/openshell/v1/fake"
func TestSandboxLifecycle(t *testing.T) {
client := fake.NewClient()
defer client.Close()
ctx := context.Background()
sb, err := client.Sandboxes().Create(ctx, "default", "my-sandbox", &v1.SandboxSpec{})
require.NoError(t, err)
assert.Equal(t, types.SandboxProvisioning, sb.Status.Phase)
sb, err = client.Sandboxes().WaitReady(ctx, "default", "my-sandbox")
require.NoError(t, err)
assert.Equal(t, types.SandboxReady, sb.Status.Phase)
deletion, err := client.Sandboxes().Delete(ctx, "default", "my-sandbox")
require.NoError(t, err)
assert.Equal(t, v1.DeletionCompleted, deletion.Outcome)
}
Creating a Client
func NewClient(opts ...ClientOption) *Client
Returns a fake client implementing v1.ClientInterface with all
sub-clients wired up. Default health result is healthy. Use options
to customize initial state:
client := fake.NewClient(
fake.WithHealthResult(&types.HealthResult{Healthy: false}),
fake.WithCurrentUser(&types.CurrentUser{Subject: "test-user"}),
fake.WithGatewayInfo(&types.GatewayInfo{Version: "1.0.0"}),
)
Pre-populating State
Seed objects directly into the fake stores for test setup:
client := fake.NewClient()
client.AddSandbox("default", &types.Sandbox{
Name: "pre-existing",
Status: types.SandboxStatus{Phase: types.SandboxReady},
})
client.AddSandboxTemplate("default", &types.SandboxWorkloadTemplate{
Name: "gpu-kata",
Spec: types.SandboxWorkloadTemplateSpec{
Workload: &types.SandboxWorkloadConfig{Image: "python:3.12"},
},
})
client.AddProvider("default", &types.Provider{
Name: "my-provider",
Spec: types.ProviderSpec{Type: "docker"},
})
client.AddWorkspace(&types.Workspace{Name: "staging"})
client.AddMember("staging", &types.WorkspaceMember{
PrincipalSubject: "subject-123",
Role: types.WorkspaceRoleAdmin,
})
All Add* methods deep-copy their arguments; mutating the input after
insertion does not affect the stored object.
Sub-Client Coverage
The fake client implements every stable accessor in v1.ClientInterface plus
additive concrete-client accessors such as SandboxTemplates():
| Accessor | Interface | Behavior |
|---|---|---|
Sandboxes() | SandboxInterface | Full CRUD, Watch, WaitReady |
SandboxTemplates() | SandboxTemplateInterface | Full CRUD |
Providers() | ProviderInterface | Full CRUD, Ensure |
Workspaces() | WorkspaceInterface | Full CRUD, Members |
Health() | HealthInterface | Configurable result |
Policy() | PolicyInterface | List, GetStatus (draft ops return Unimplemented) |
Exec() | ExecInterface | Returns Unimplemented |
Files() | FileInterface | Returns Unimplemented |
Services() | ServiceInterface | Returns Unimplemented |
SSH() | SSHInterface | Input validation, then Unimplemented |
TCP() | TCPInterface | Input validation, then Unimplemented |
Config() | ConfigInterface | Returns Unimplemented |
ClientOption
| Constructor | Effect |
|---|---|
WithHealthResult(r) | Set the health check return value |
WithCurrentUser(u) | Set the current user return value |
WithGatewayInfo(i) | Set the gateway info return value |
Thread Safety
All operations are safe for concurrent use from multiple goroutines.
Close is idempotent and causes all subsequent operations to return
Unavailable.
See also: Testing Guide
Error Handling
Gateway status failures and SDK validation failures use *v1.StatusError, which carries a machine-readable Code and a human-readable Message. Local I/O, transport setup, and sentinel errors may use other Go error types. The SDK provides predicate functions for classified status errors.
StatusError
When an operation returns a classified status error, inspect it directly or use the convenience predicates below. Use errors.Is for documented sentinel errors such as ErrTransportNotAvailable.
var se *v1.StatusError
if errors.As(err, &se) {
fmt.Printf("code: %s, message: %s\n", se.Code, se.Message)
// se.ErrorInfo and se.FieldViolations contain optional structured details.
}
| Field | Type | Description |
|---|---|---|
Code | ErrorCode | Machine-readable error classification |
Message | string | Human-readable error description |
GRPCCode | int32 | Original gRPC code before SDK classification |
FieldViolations | []FieldViolation | Rejected request fields and descriptions |
ErrorInfo | *ErrorInfo | Server reason, domain, and metadata |
RetryDelay | *time.Duration | Suggested minimum delay for an otherwise safe retry |
Cause | error | Original gRPC error, including unknown details |
Use status.FromError(err) to inspect the complete gRPC status through the SDK’s
error wrapper. A delay does not prove that a timed-out mutation was not applied.
Predicate Functions
Use these top-level functions to check error types. They work with wrapped errors via errors.As.
| Function | ErrorCode | When it fires |
|---|---|---|
v1.IsNotFound | ErrorNotFound | Resource does not exist (sandbox, provider, etc.) |
v1.IsAlreadyExists | ErrorAlreadyExists | Resource with that name already exists |
v1.IsConflict | ErrorConflict | Optimistic concurrency violation or invalid state transition |
v1.IsUnavailable | ErrorUnavailable | Gateway is unreachable or client is closed |
v1.IsUnimplemented | ErrorUnimplemented | Operation not supported by the gateway version |
v1.IsPermissionDenied | ErrorPermissionDenied | Insufficient permissions |
v1.IsInvalidArgument | ErrorInvalidArgument | Invalid request parameters |
v1.IsDeadlineExceeded | ErrorDeadlineExceeded | Operation timed out |
v1.IsCancelled | ErrorCancelled | Operation was cancelled (context cancellation) |
For ErrorInternal (server-side errors), no convenience predicate exists. Match it directly via the Code field:
var se *v1.StatusError
if errors.As(err, &se) && se.Code == v1.ErrorInternal {
fmt.Println("Internal server error:", se.Message)
}
Common Patterns
Not Found
Handle missing resources gracefully:
sb, err := client.Sandboxes().Get(ctx, "default", "my-sandbox")
if v1.IsNotFound(err) {
fmt.Println("Sandbox does not exist, creating...")
sb, err = client.Sandboxes().Create(ctx, "default", "my-sandbox", &v1.SandboxSpec{})
}
if err != nil {
log.Fatal(err)
}
Already Exists
Guard against duplicate creation:
_, err := client.Providers().Create(ctx, "default", &v1.Provider{
Name: "openai",
Type: "openai",
})
if v1.IsAlreadyExists(err) {
fmt.Println("Provider already registered, skipping")
} else if err != nil {
log.Fatal(err)
}
Alternatively, use Ensure for idempotent registration:
// Create-or-update in a single call
provider, err := client.Providers().Ensure(ctx, "default", &v1.Provider{
Name: "openai",
Type: "openai",
})
Conflict (Optimistic Concurrency)
When two clients modify the same resource concurrently, the second write receives a conflict error. Retry by re-reading the resource:
sb, _ := client.Sandboxes().Get(ctx, "default", "my-sandbox")
result, err := client.Sandboxes().AttachProvider(ctx,
"default", "my-sandbox", "openai", sb.ResourceVersion,
)
if v1.IsConflict(err) {
// Another client modified the sandbox — re-read and retry
sb, _ = client.Sandboxes().Get(ctx, "default", "my-sandbox")
result, err = client.Sandboxes().AttachProvider(ctx,
"default", "my-sandbox", "openai", sb.ResourceVersion,
)
}
if err != nil {
log.Fatal(err)
}
Unavailable
Handle gateway connectivity issues:
sb, err := client.Sandboxes().Get(ctx, "default", "my-sandbox")
if v1.IsUnavailable(err) {
fmt.Println("Gateway is not reachable, check connection")
// Implement retry with backoff
}
Unimplemented
Detect unsupported operations gracefully:
_, err := client.Sandboxes().GetLogs(ctx, "default", "my-sandbox")
if v1.IsUnimplemented(err) {
fmt.Println("Log retrieval not supported by this gateway version")
}
Retry with Backoff
For transient errors like Unavailable or DeadlineExceeded, use exponential backoff only when the operation is idempotent or carries an idempotency/concurrency token. A timeout does not prove that a mutating request was not applied.
func withRetry(ctx context.Context, maxAttempts int, fn func() error) error {
backoff := 100 * time.Millisecond
for attempt := 0; attempt < maxAttempts; attempt++ {
err := fn()
if err == nil {
return nil
}
// Only retry transient errors
if !v1.IsUnavailable(err) && !v1.IsDeadlineExceeded(err) {
return err
}
// Add jitter to prevent thundering herd after outages
jitter := time.Duration(rand.Int63n(int64(backoff) / 2))
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(backoff + jitter):
backoff *= 2
}
}
return fmt.Errorf("exhausted %d retry attempts", maxAttempts)
}
Usage:
var sb *v1.Sandbox
err := withRetry(ctx, 3, func() error {
var e error
sb, e = client.Sandboxes().Get(ctx, "default", "my-sandbox")
return e
})
Context Cancellation
All SDK methods accept a context.Context. Use context deadlines and cancellation to control timeouts:
// 5-second timeout for a single call
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
sb, err := client.Sandboxes().Get(ctx, "default", "my-sandbox")
if v1.IsDeadlineExceeded(err) {
fmt.Println("Request timed out")
}
See also: Testing for how the fake client returns the same error codes.
Testing
The SDK ships a fake package that provides an in-memory implementation of all client interfaces. Use it in your test suites to exercise SDK interactions without a real gateway.
import "github.com/ai-openshell/sdk-go/openshell/v1/fake"
The fake client follows the same pattern as k8s.io/client-go/kubernetes/fake: it maintains in-memory stores, supports watch event broadcasting, and returns the same StatusError codes as the real client.
Creating a Fake Client
func TestMyOperator(t *testing.T) {
client := fake.NewClient()
defer client.Close()
ctx := context.Background()
// Use client exactly like the real SDK
sb, err := client.Sandboxes().Create(ctx, "default", "test-sandbox", &v1.SandboxSpec{})
require.NoError(t, err)
assert.Equal(t, "Provisioning", string(sb.Status.Phase))
}
The returned *fake.Client satisfies v1.ClientInterface, so you can pass it anywhere your code accepts the interface.
Fixture Seeding
Pre-populate the fake client with existing resources before your test runs. Seeded resources are available immediately via Get and List without going through Create.
AddSandbox
client := fake.NewClient()
// Pre-seed a sandbox that already exists
client.AddSandbox(&types.Sandbox{
Name: "existing-sandbox",
Status: types.SandboxStatus{
Phase: types.SandboxReady,
},
ResourceVersion: 5,
})
// Now Get returns it immediately
sb, err := client.Sandboxes().Get(ctx, "default", "existing-sandbox")
// sb.Status.Phase == "Ready"
AddProvider
client := fake.NewClient()
// Pre-seed a provider
client.AddProvider(&types.Provider{
Name: "my-openai",
Type: "openai",
Spec: types.ProviderSpec{
Credentials: map[string]string{
"api_key": "sk-test-key",
},
},
})
// List returns the seeded provider
providers, _ := client.Providers().ListAll(ctx, "default")
// len(providers) == 1
Sandbox Lifecycle
The fake client implements the full sandbox lifecycle. Created sandboxes start in the Provisioning phase. Calling WaitReady transitions them to Ready synchronously.
client := fake.NewClient()
ctx := context.Background()
// Create starts in Provisioning
sb, err := client.Sandboxes().Create(ctx, "default", "my-sandbox", &v1.SandboxSpec{})
assert.Equal(t, types.SandboxProvisioning, sb.Status.Phase)
// WaitReady transitions to Ready (synchronous in fake)
sb, err = client.Sandboxes().WaitReady(ctx, "default", "my-sandbox")
assert.Equal(t, types.SandboxReady, sb.Status.Phase)
// Delete removes the sandbox
_, err = client.Sandboxes().Delete(ctx, "default", "my-sandbox")
assert.NoError(t, err)
// Get after delete returns NotFound
_, err = client.Sandboxes().Get(ctx, "default", "my-sandbox")
assert.True(t, v1.IsNotFound(err))
Watch Events
The fake client broadcasts watch events when resources change. Use watchers to test event-driven code.
client := fake.NewClient()
ctx := context.Background()
// Start watching before making changes
watcher, err := client.Sandboxes().Watch(ctx, "default", "my-sandbox")
require.NoError(t, err)
defer watcher.Stop()
// Create a sandbox — triggers an ADDED event
client.Sandboxes().Create(ctx, "default", "my-sandbox", &v1.SandboxSpec{})
// Read the event from the channel
event := <-watcher.ResultChan()
assert.Equal(t, types.EventAdded, event.Type)
assert.Equal(t, "my-sandbox", event.Object.Name)
StopOnTerminal
Setting StopOnTerminal: true causes the watcher to close automatically when the sandbox reaches a terminal phase (Ready or Error).
watcher, err := client.Sandboxes().Watch(ctx, "default", "my-sandbox", v1.WatchOptions{
StopOnTerminal: true,
})
require.NoError(t, err)
// Create and transition to Ready
client.Sandboxes().Create(ctx, "default", "my-sandbox", &v1.SandboxSpec{})
client.Sandboxes().WaitReady(ctx, "default", "my-sandbox")
// Drain events — channel closes after the Ready event
var events []types.Event[*types.Sandbox]
for ev := range watcher.ResultChan() {
events = append(events, ev)
}
// Channel is now closed
Health Simulation
Use WithHealthResult to simulate an unhealthy or degraded gateway.
// Default: healthy gateway
client := fake.NewClient()
result, _ := client.Health().Check(ctx)
// result.Healthy == true, result.Version == "fake"
// Simulate unhealthy gateway
client = fake.NewClient(fake.WithHealthResult(&types.HealthResult{
Healthy: false,
Version: "1.2.3",
}))
result, _ = client.Health().Check(ctx)
// result.Healthy == false
Error Behavior
The fake client returns the same StatusError codes as the real client:
| Scenario | Error Code |
|---|---|
Get for a non-existent resource | ErrorNotFound |
Create with a duplicate name | ErrorAlreadyExists |
Any call after Close() | ErrorUnavailable |
Unimplemented operations (e.g., GetLogs) | ErrorUnimplemented |
client := fake.NewClient()
client.Close()
_, err := client.Sandboxes().Get(ctx, "default", "anything")
assert.True(t, v1.IsUnavailable(err))
Concurrency
All fake client operations are safe for concurrent use. The internal stores use mutex-based synchronization. This means you can safely use the fake client from multiple goroutines in parallel tests.
See also: Error Handling, API Reference