Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 Client provides 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 ClientInterface for 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, and IsConflict for 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
  • 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

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:

FieldPurpose
AddressGateway host and port
AuthAuthentication provider (StaticToken, NoAuth, or custom)
TLSTLS settings (CA cert, skip verify, client certs)
RetryRetry 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, not proto/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:

PatternExample
Unary RPCCreate, Get, Delete
Server-streaming RPCWatch, Stream, GetLogs
Client-streaming RPCFile uploads
Bidirectional streamingInteractive, 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

InterfaceAccessorDescription
SandboxInterfaceclient.Sandboxes()Create, manage, and watch sandbox lifecycle
SandboxTemplateInterfaceclient.SandboxTemplates()Manage reusable sandbox templates
ExecInterfaceclient.Exec()Run commands, stream output, interactive sessions
ProviderInterfaceclient.Providers()Manage compute providers and their lifecycle
ServiceInterfaceclient.Services()Expose and manage HTTP services inside sandboxes
FileInterfaceclient.Files()Upload and download files to/from sandboxes
HealthInterfaceclient.Health()Check gateway health status
SSHInterfaceclient.SSH()Create SSH sessions and tunnels to sandboxes
TCPInterfaceclient.TCP()Forward TCP connections to sandbox ports
ConfigInterfaceclient.Config()Read and update sandbox and gateway configuration
PolicyInterfaceclient.Policy()Manage draft policy recommendations

Convenience Packages

PackageEntry PointDescription
gatewaygateway.NewClient(name)Read CLI gateway configs and auto-wire clients

Provider Sub-Interfaces

These are accessed through client.Providers():

InterfaceAccessorDescription
ProfileInterfaceclient.Providers().Profiles()Manage provider type profiles
RefreshInterfaceclient.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 each NextPage call accepts the request context
  • Methods that can fail return (result, error)
  • List methods accept variadic options and ListAll is the explicit exhaustive path
  • Errors from the gateway carry a StatusError with 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

AccessorReturnsDescription
Sandboxes()SandboxInterfaceSandbox lifecycle management
SandboxTemplates()SandboxTemplateInterfaceReusable sandbox template management
Providers()ProviderInterfaceProvider CRUD and idempotent ensure
Services()ServiceInterfaceService exposure and management
Exec()ExecInterfaceCommand execution (run, stream, interactive)
Files()FileInterfaceFile upload and download
Health()HealthInterfaceGateway health checking
SSH()SSHInterfaceSSH session and tunnel management
TCP()TCPInterfaceTCP port forwarding
Config()ConfigInterfaceSandbox and gateway configuration
Policy()PolicyInterfaceDraft policy review workflow
Close()errorClose the gRPC connection

Sub-client hierarchy: Providers() has two nested accessors:

  • client.Providers().Profiles() returns ProfileInterface
  • client.Providers().Refresh() returns RefreshInterface

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 token
  • v1.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 events
  • Stop() closes the stream and the channel

Each Event[T] carries:

  • Type: one of EventAdded, EventModified, EventDeleted, or EventError
  • Object: the *Sandbox at that point in time (nil for EventError)
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:

OptionDescription
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 order
  • BufferTotal 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:

FieldTypeDescription
StdoutstringCaptured standard output
StderrstringCaptured standard error
ExitCodeintProcess 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
}
MethodDescription
NextReturns the next output chunk. Returns io.EOF when done.
ExitCodeReturns the process exit code. Call after Next returns io.EOF.
CloseReleases 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
}
MethodDescription
ReadReads output from the process into the provided buffer.
WriteSends input to the process.
ResizeUpdates the terminal dimensions (columns and rows).
ExitCodeWaits for final gRPC status; returns any observed exit code alongside a later stream error.
CloseAborts 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
}
FieldTypeDescription
Data[]byteRaw output bytes from the process.
StreamStreamTypeEither 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.

FieldTypeDescription
IDstringServer-assigned unique identifier
NamestringUser-chosen name (unique per gateway)
TypestringProvider type (e.g., "openai", "azure")
CreatedAttime.TimeTimestamp of creation
Labelsmap[string]stringKey-value metadata labels
ResourceVersionuint64Optimistic concurrency version
SpecProviderSpecConfiguration and credentials

ProviderSpec

ProviderSpec holds provider-specific configuration and credentials.

FieldTypeDescription
Credentialsmap[string]stringAuthentication credentials (e.g., API keys)
Configmap[string]stringProvider-specific configuration values
CredentialExpiresAtmap[string]time.TimeExpiration 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

FunctionDescription
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

ValueConstantSDK AuthProvider
"" or "none"AuthModeNonev1.NoAuth()
"plaintext"AuthModePlaintextv1.NoAuth() + insecure TLS
"cloudflare_jwt"AuthModeCloudflareJWTLazy edge token auth
"oidc"AuthModeOIDCv1.RefreshableToken that reads oidc_token.json near expiry and fails once the token has expired
"mtls"AuthModeMTLSUnsupported (use WithAuth)

ClientOption

ConstructorEffect
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>` .

ErrorMeaning
ErrGatewayNotFoundNo gateway directory in user or system paths
ErrConfigParsemetadata.json missing or malformed
ErrTokenLoadToken file missing, unreadable, malformed, or expired; RPCs report it as Unauthenticated
ErrUnsupportedAuthModeUnrecognized auth_mode value
ErrInvalidGatewayNameName fails validation
ErrNoActiveGatewayNo 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

FunctionDescription
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

OptionDescription
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:

ErrorDescription
ErrDiscoveryOIDC discovery document fetch failed
ErrAuthCodeAuthorization code exchange failed
ErrDeviceCodeDevice code flow failed
ErrClientCredentialsClient credentials exchange failed
ErrTimeoutAuthentication flow timed out
ErrCallbackServerLocalhost callback server failed
ErrTokenPersistToken read/write failed
ErrOIDCConfigMissing 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)

  1. Discovers OIDC endpoints from issuer
  2. Generates PKCE code verifier and S256 challenge
  3. Opens browser to authorization URL
  4. Starts localhost callback server to receive the auth code
  5. Exchanges code for tokens with PKCE verification
  6. 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)

  1. Requests a device code and user code from the provider
  2. Displays the verification URL and user code
  3. Polls the token endpoint at the provider’s requested interval
  4. Handles slow_down responses by increasing the poll interval

Client Credentials Flow

  1. Discovers OIDC endpoints from issuer
  2. Exchanges client ID and secret for an access token
  3. 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

FunctionDescription
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

MethodDescription
Addr()Local listener address for gRPC client to dial
Close()Gracefully drain in-flight connections and shut down

TunnelOption

ConstructorEffect
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():

AccessorInterfaceBehavior
Sandboxes()SandboxInterfaceFull CRUD, Watch, WaitReady
SandboxTemplates()SandboxTemplateInterfaceFull CRUD
Providers()ProviderInterfaceFull CRUD, Ensure
Workspaces()WorkspaceInterfaceFull CRUD, Members
Health()HealthInterfaceConfigurable result
Policy()PolicyInterfaceList, GetStatus (draft ops return Unimplemented)
Exec()ExecInterfaceReturns Unimplemented
Files()FileInterfaceReturns Unimplemented
Services()ServiceInterfaceReturns Unimplemented
SSH()SSHInterfaceInput validation, then Unimplemented
TCP()TCPInterfaceInput validation, then Unimplemented
Config()ConfigInterfaceReturns Unimplemented

ClientOption

ConstructorEffect
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.
}
FieldTypeDescription
CodeErrorCodeMachine-readable error classification
MessagestringHuman-readable error description
GRPCCodeint32Original gRPC code before SDK classification
FieldViolations[]FieldViolationRejected request fields and descriptions
ErrorInfo*ErrorInfoServer reason, domain, and metadata
RetryDelay*time.DurationSuggested minimum delay for an otherwise safe retry
CauseerrorOriginal 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.

FunctionErrorCodeWhen it fires
v1.IsNotFoundErrorNotFoundResource does not exist (sandbox, provider, etc.)
v1.IsAlreadyExistsErrorAlreadyExistsResource with that name already exists
v1.IsConflictErrorConflictOptimistic concurrency violation or invalid state transition
v1.IsUnavailableErrorUnavailableGateway is unreachable or client is closed
v1.IsUnimplementedErrorUnimplementedOperation not supported by the gateway version
v1.IsPermissionDeniedErrorPermissionDeniedInsufficient permissions
v1.IsInvalidArgumentErrorInvalidArgumentInvalid request parameters
v1.IsDeadlineExceededErrorDeadlineExceededOperation timed out
v1.IsCancelledErrorCancelledOperation 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:

ScenarioError Code
Get for a non-existent resourceErrorNotFound
Create with a duplicate nameErrorAlreadyExists
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