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, 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.23 or later
- Access to an OpenShell gateway (address and authentication token)
Installation
Add the SDK to your Go module:
go get github.com/rhuss/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/rhuss/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, "my-sandbox", &v1.SandboxSpec{
Template: &v1.SandboxTemplate{Image: "python:3.12"},
Environment: map[string]string{"LANG": "en_US.UTF-8"},
}, nil)
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, 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, 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:
err = client.Sandboxes().Delete(ctx, sandbox.Name)
if err != nil {
log.Fatal(err)
}
fmt.Println("Sandbox deleted")
}
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().List(ctx)
status, err := client.Providers().Refresh().GetStatus(ctx, "openai", "api-key")
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, "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 12 interfaces through the sub-client pattern. You access each interface through a typed accessor on the Client.
Interface Summary
Top-Level Interfaces
| Interface | Accessor | Description |
|---|---|---|
| SandboxInterface | client.Sandboxes() | Create, manage, and watch sandbox lifecycle |
| 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.
- 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
All SDK methods follow these conventions:
- Every method takes
context.Contextas its first argument - Methods that can fail return
(result, error) - List methods accept variadic option arguments
- Errors from the gateway carry a
StatusErrorwith a typed code (see Error Handling)
Client
Constructor: v1.NewClient(config)
The ClientInterface is the root entry point for all SDK operations. It provides
typed accessors for each resource domain and manages the underlying gRPC connection.
Methods
| Accessor | Returns | Description |
|---|---|---|
Sandboxes() | SandboxInterface | Sandbox lifecycle 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/rhuss/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/rhuss/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, spec, and labels.
sb, err := client.Sandboxes().Create(ctx, "my-sandbox", &v1.SandboxSpec{
Template: &v1.SandboxTemplate{
Image: "nvcr.io/nvidia/openshell:latest",
},
Providers: []string{"openai"},
}, map[string]string{
"team": "platform",
})
Get
Retrieves a sandbox by name.
sb, err := client.Sandboxes().Get(ctx, "my-sandbox")
fmt.Println(sb.Status.Phase) // "Ready", "Provisioning", etc.
List
Lists sandboxes with optional pagination and label filtering.
// List all sandboxes
sandboxes, err := client.Sandboxes().List(ctx)
// With pagination and label filtering
sandboxes, err := client.Sandboxes().List(ctx, v1.ListOptions{
Limit: 10,
Offset: 0,
LabelSelector: "team=platform",
})
Delete
Deletes a sandbox by name.
err := client.Sandboxes().Delete(ctx, "my-sandbox")
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, "my-sandbox")
result, err := client.Sandboxes().AttachProvider(ctx,
"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, "my-sandbox")
result, err := client.Sandboxes().DetachProvider(ctx,
"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, "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, "my-sandbox")
if err != nil {
log.Fatal(err)
}
fmt.Println(sb.Status.Phase) // "Ready"
// Custom poll interval
sb, err := client.Sandboxes().WaitReady(ctx, "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, "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, "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, "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, "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
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, "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, "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, "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 | Returns the process exit code after the session ends. |
Close | Closes the session and releases resources. |
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, &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, "my-openai")
if err != nil {
log.Fatal(err)
}
fmt.Println("Provider type:", provider.Type)
List
List all registered providers, with optional pagination.
// List all providers
providers, err := client.Providers().List(ctx)
if err != nil {
log.Fatal(err)
}
for _, p := range providers {
fmt.Println(p.Name, p.Type)
}
// With pagination
providers, err = client.Providers().List(ctx, v1.ListOptions{
Limit: 10,
Offset: 0,
})
Update
Update an existing provider’s configuration or credentials.
provider, err := client.Providers().Get(ctx, "my-openai")
if err != nil {
log.Fatal(err)
}
provider.Spec.Credentials["api_key"] = "sk-new-key"
updated, err := client.Providers().Update(ctx, provider)
if err != nil {
log.Fatal(err)
}
fmt.Println("Updated provider:", updated.Name)
Delete
Remove a provider by name.
err := client.Providers().Delete(ctx, "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, &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 |
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 all provider profiles visible to the current user.
profiles, err := client.Providers().Profiles().List(ctx)
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, []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, "profile-id")
if err != nil {
log.Fatal(err)
}
profile.DisplayName = "Updated Provider"
result, err := client.Providers().Profiles().Update(
ctx,
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, 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, "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, &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, "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, "my-sandbox", "web", 8080, true)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Service available at: %s\n", endpoint.URL)
List
List all exposed services for a sandbox.
services, err := client.Services().List(ctx, "my-sandbox")
if err != nil {
log.Fatal(err)
}
for _, svc := range services {
fmt.Printf(" %s -> port %d (%s)\n", svc.ServiceName, svc.TargetPort, svc.URL)
}
Delete
Remove an exposed service. The underlying sandbox port remains accessible internally but is no longer reachable through the service endpoint.
err := client.Services().Delete(ctx, "my-sandbox", "web")
if err != nil {
log.Fatal(err)
}
See also: Error Handling, Testing
Files
Accessor: client.Files()
Upload and download files to and from sandboxes. Uses gRPC streaming for efficient transfer of large files.
Upload
Upload a file from the local filesystem to a path inside a sandbox. The file is streamed in chunks using client-side gRPC streaming.
err := client.Files().Upload(ctx, "sandbox-123", "./data/config.yaml", "/app/config.yaml")
if err != nil {
log.Fatal(err)
}
fmt.Println("File uploaded successfully")
Download
Download a file from a sandbox to the local filesystem. The file is received in chunks using server-side gRPC streaming.
err := client.Files().Download(ctx, "sandbox-123", "/app/output.log", "./output.log")
if err != nil {
log.Fatal(err)
}
fmt.Println("File downloaded successfully")
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, "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.
revoked, err := client.SSH().RevokeSession(ctx, session.Token)
if err != nil {
log.Fatal(err)
}
if revoked {
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, "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, "sandbox-123", 5432)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
// Example: proxy a PostgreSQL connection through the tunnel
// Write and read data as with any net.Conn-like stream
_, err = conn.Write([]byte("SELECT 1;\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])
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, "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, &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, "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, "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, "my-sandbox")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Active version: %d, revision status: %s\n",
status.ActiveVersion, status.Revision.Status)
List
List all policy revisions for a sandbox.
revisions, err := client.Policy().List(ctx, "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, "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, "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)
}
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/rhuss/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, ErrTokenLoad,
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 with disk source |
"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")
}
if errors.Is(err, gateway.ErrTokenLoad) {
fmt.Println("Token expired or missing. Run: openshell gateway login my-gateway")
}
| Error | Meaning |
|---|---|
ErrGatewayNotFound | No gateway directory in user or system paths |
ErrConfigParse | metadata.json missing or malformed |
ErrTokenLoad | Token file missing or unreadable |
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/rhuss/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. Subsequent calls reuse a valid cached token.
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.
Options
| Option | Description |
|---|---|
WithIssuer(url) | OIDC provider issuer URL |
WithClientID(id) | OAuth2 client ID |
WithClientSecret(secret) | OAuth2 client secret (for client credentials) |
WithScopes(scopes...) | Custom scopes (default: openid, profile, email) |
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
Error Handling
All SDK operations return errors as *v1.StatusError, a typed error that carries a machine-readable Code and a human-readable Message. The SDK provides predicate functions to classify errors without importing internal packages.
StatusError
Every error returned by the SDK is a *StatusError (or wraps one). You can inspect it directly or use the convenience predicates below.
var se *v1.StatusError
if errors.As(err, &se) {
fmt.Printf("code: %s, message: %s\n", se.Code, se.Message)
// se.Details contains optional structured metadata
}
| Field | Type | Description |
|---|---|---|
Code | ErrorCode | Machine-readable error classification |
Message | string | Human-readable error description |
Details | map[string]string | Optional structured metadata |
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, "my-sandbox")
if v1.IsNotFound(err) {
fmt.Println("Sandbox does not exist, creating...")
sb, err = client.Sandboxes().Create(ctx, "my-sandbox", &v1.SandboxSpec{}, nil)
}
if err != nil {
log.Fatal(err)
}
Already Exists
Guard against duplicate creation:
_, err := client.Providers().Create(ctx, &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, &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, "my-sandbox")
result, err := client.Sandboxes().AttachProvider(ctx,
"my-sandbox", "openai", sb.ResourceVersion,
)
if v1.IsConflict(err) {
// Another client modified the sandbox — re-read and retry
sb, _ = client.Sandboxes().Get(ctx, "my-sandbox")
result, err = client.Sandboxes().AttachProvider(ctx,
"my-sandbox", "openai", sb.ResourceVersion,
)
}
if err != nil {
log.Fatal(err)
}
Unavailable
Handle gateway connectivity issues:
sb, err := client.Sandboxes().Get(ctx, "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, "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:
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, "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, "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/rhuss/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, "test-sandbox", &v1.SandboxSpec{}, nil)
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, "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().List(ctx)
// 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, "my-sandbox", &v1.SandboxSpec{}, nil)
assert.Equal(t, types.SandboxProvisioning, sb.Status.Phase)
// WaitReady transitions to Ready (synchronous in fake)
sb, err = client.Sandboxes().WaitReady(ctx, "my-sandbox")
assert.Equal(t, types.SandboxReady, sb.Status.Phase)
// Delete removes the sandbox
err = client.Sandboxes().Delete(ctx, "my-sandbox")
assert.NoError(t, err)
// Get after delete returns NotFound
_, err = client.Sandboxes().Get(ctx, "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, "my-sandbox")
require.NoError(t, err)
defer watcher.Stop()
// Create a sandbox — triggers an ADDED event
client.Sandboxes().Create(ctx, "my-sandbox", &v1.SandboxSpec{}, nil)
// 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, "my-sandbox", v1.WatchOptions{
StopOnTerminal: true,
})
require.NoError(t, err)
// Create and transition to Ready
client.Sandboxes().Create(ctx, "my-sandbox", &v1.SandboxSpec{}, nil)
client.Sandboxes().WaitReady(ctx, "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, "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