Files
Shiju 8983642e28 fix(gateway): give image preparation its own deadline (#4038)
* feat(server): separate image preparation and admission deadlines

Give sandbox image preparation its own deadline and start the admission
deadline after preparation finishes. Persist both phases across gateway
restarts and show recovery guidance for the phase that expired.

Preserve the current service authorization schema and regenerate the Go
bindings with the preparation timestamps.

Fixes #3952
Related to #3955

Signed-off-by: Shiju <shiju@nvidia.com>

* fix(cli): simplify preparation timeout fallback selection

Use lazy Option fallbacks while preserving timeout messages and retained
sandbox behavior.

Signed-off-by: Shiju <shiju@nvidia.com>

* docs(server): clarify admission timer prerequisites

Signed-off-by: Shiju <shiju@nvidia.com>

* fix(compute): enforce deadlines during initial sandbox create

Release stalled create operations after preparation expires and preserve
the timeout diagnosis across late driver results. Keep failed-create cleanup
bound to its original attempt so another replica can retry safely.

Signed-off-by: Shiju <shiju@nvidia.com>

* test(server): satisfy deadline regression lints

Drop the create-error mutex guard before matching its cloned value and use
idiomatic iteration and timeout matching in the deadline fixtures.

Signed-off-by: Shiju <shiju@nvidia.com>

* fix(server): retain ownership of pending provisioning operations

Keep submitted create and start requests alive after caller cancellation,
monitor failure, or preparation timeout. Persist request ownership before
dispatch and retain staged uploads while the driver response is pending.

Require timeout cleanup to stop compute after driver settlement without
discarding an active cleanup claim. Fence late result handling against newer
operations, preserve failed-start recovery, and defer automatic restart while
another request owns the sandbox. Expose pending ownership in CLI JSON.

Add ordered multi-replica and cancellation regressions for the review findings.

Signed-off-by: Shiju <shiju@nvidia.com>

* fix(openshell): preserve tracing and accept ready create responses

Carry the request span into the detached provisioning worker so compute
driver calls remain attached to their parent trace after task handoff.

Update the compensation regression to require no backend DELETE when the
durable cleanup claim fails, matching the operation ownership requirement.

Accept the gateway's current Ready snapshot when a sandbox becomes ready
before CREATE returns. Do not require the client to observe an earlier
provisioning phase. Cover the Ready-only watch and command attachment.

Signed-off-by: Shiju <shiju@nvidia.com>

---------

Signed-off-by: Shiju <shiju@nvidia.com>
2026-10-03 20:48:18 +00:00
..

OpenShell SDK for Go

Go Reference License

Important

Read the full documentation for guides, API reference with gRPC mapping, and testing patterns.

A Go SDK for interacting with OpenShell servers, providing idiomatic Go bindings for shell session management, command execution, provider configuration, and service exposure.

Why a Go SDK?

Go is the language of the Kubernetes ecosystem. If you want to build an operator, controller, or any automation that manages OpenShell resources as native Kubernetes objects, you need a Go client.

This SDK is modeled after k8s.io/client-go, the standard Kubernetes client library that every Go operator developer already knows. The patterns will look familiar:

  • Typed sub-clients per resource: client.Sandboxes(), client.Providers(), client.Exec(), just like clientset.CoreV1().Pods()
  • Domain types separated from wire formats: clean Go structs in a types package, no proto leakage into the public API (like k8s.io/api)
  • Watch primitives: channel-based watchers with ResultChan() and Stop(), identical to watch.Interface in client-go
  • Functional options: variadic option patterns for list filtering, pagination, and watch configuration. Nil options are silently ignored at every entry point, so conditional option lists are safe to pass without filtering out nil entries.
  • Composable auth with token refresh: wraps oauth2.TokenSource for automatic token caching and coalesced refresh, following the k8s client-go cachingTokenSource pattern
  • Fake client for testing: an in-memory implementation of the full client interface (like k8s.io/client-go/kubernetes/fake), so operators can be tested without a real gateway

Quick Start

import v1 "github.com/NVIDIA/OpenShell/sdk/go/openshell/v1"

// Connect to a gateway
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()

// Create a sandbox and wait until it's ready
sandbox, err := client.Sandboxes().Create(ctx, "default", "my-sandbox", &v1.SandboxSpec{
    Template: &v1.SandboxTemplate{Image: "python:3.12"},
}, nil)
if err != nil {
    log.Fatal(err)
}
sandbox, err = client.Sandboxes().WaitReady(ctx, "default", sandbox.Name)
if err != nil {
    log.Fatal(err)
}

// Run a command
result, err := client.Exec().Run(ctx, "default", sandbox.Name,
    []string{"python3", "-c", "print('hello from sandbox')"},
    v1.ExecOptions{},
)
if err != nil {
    log.Fatal(err)
}
fmt.Println(string(result.Stdout))

Pagination

List methods return a lazy pager without issuing a request. NextPage fetches one page with the supplied context, while ListAll explicitly exhausts every page. PageSize is a per-request maximum and PageToken resumes a prior query.

pager, err := client.Sandboxes().List("default", v1.ListOptions{PageSize: 100})
if err != nil {
    log.Fatal(err)
}
for {
    page, err := pager.NextPage(ctx)
    if err != nil {
        log.Fatal(err)
    }
    if page == nil {
        break
    }
    for _, sandbox := range page.Items {
        fmt.Println(sandbox.Name)
    }
}

With automatic token refresh

For OIDC gateways, use RefreshableToken to wrap any oauth2.TokenSource with automatic caching and coalesced refresh:

import "golang.org/x/oauth2"

tokenSource := oauth2Config.TokenSource(ctx, initialToken)
auth, err := v1.RefreshableToken(tokenSource,
    v1.WithLeeway(30*time.Second),
)
if err != nil {
    log.Fatal(err)
}
client, err := v1.NewClient(v1.Config{
    Address: "gateway.example.com:443",
    Auth:    auth,
})
if err != nil {
    log.Fatal(err)
}
defer client.Close()

Concurrent callers share a single refresh call. If the token source fails, the SDK falls back to the cached token with a logged warning. See the Auth docs for details.

With edge proxy headers

When a gateway sits behind a zero-trust reverse proxy, use WithExtraHeaders to attach proxy-specific headers alongside standard auth:

base := v1.StaticToken("my-gateway-token")
auth, err := v1.WithExtraHeaders(base, map[string]string{
    "x-proxy-auth": "proxy-secret",
})
if err != nil {
    log.Fatal(err)
}
client, err := v1.NewClient(v1.Config{
    Address: "gateway.example.com:443",
    Auth:    auth,
})

For Cloudflare Access, use the convenience constructor in the edge package:

import "github.com/NVIDIA/OpenShell/sdk/go/openshell/v1/edge"

auth, err := edge.CloudflareAccess(base, os.Getenv("CF_ACCESS_TOKEN"))

For gRPC behind edge proxies that reject HTTP/2, use the WebSocket tunnel:

tunnel, err := edge.NewTunnelProxy(
    "wss://gateway.example.com/ws",
    os.Getenv("CF_ACCESS_TOKEN"),
)
if err != nil {
    log.Fatal(err)
}
defer tunnel.Close()

client, err := v1.NewClient(v1.Config{
    Address: tunnel.Addr(),
    Auth:    v1.StaticToken("my-token"),
    TLS:     &v1.TLSConfig{Insecure: true}, // local tunnel, no TLS
})

OIDC Login

The oidc package provides gateway-aware OIDC authentication with browser, keyboard, device code, and client credentials flows:

import "github.com/NVIDIA/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)
}

// Use the token with the SDK client
client, err := v1.NewClient(v1.Config{
    Address: "gateway.example.com:443",
    Auth:    v1.StaticToken(token.AccessToken),
})

For headless environments, use the device code flow:

token, err := oidc.DeviceLogin(ctx,
    oidc.WithIssuer("https://auth.example.com"),
    oidc.WithClientID("my-app"),
)

For a one-shot service-account token exchange, use ClientCredentials. For a long-running SDK client, attach the renewable, memory-only auth provider:

auth, err := oidc.NewClientCredentialsAuth(
    oidc.WithGateway("my-gateway"),
    oidc.WithClientSecretProvider(func(context.Context) (string, error) {
        return os.Getenv("OPENSHELL_OIDC_CLIENT_SECRET"), nil
    }),
)
if err != nil {
    log.Fatal(err)
}

client, err := v1.NewClient(v1.Config{
    Address: "gateway.example.com:443",
    Auth:    auth,
    TLS:     tlsConfig,
})
if err != nil {
    log.Fatal(err)
}
defer client.Close()

You can use explicit WithIssuer, WithClientID, WithScopes, and WithAudience options instead of WithGateway. The provider repeats the grant before expiry and never writes the secret or access token to disk.

See the oidc package docs for all options and flows.

See the Getting Started guide for the full walkthrough.

Migrating from v0.0.101

The pre-1.0 SDK intentionally includes source-incompatible API corrections:

  • TCP.Listen returns a ForwardListener lifecycle handle. The SDK owns the accept loop; callers dial Addr() and call Close() instead of calling Accept() or passing the handle to http.Serve.
  • Resource operations take an explicit workspace, and workspace-bearing domain types preserve that scope.
  • Several public struct field orders changed. Use keyed struct literals.
  • Initialisms use Go spelling, including JSONRPCMaxBodyBytes.
  • Provider profile durations use the exact RefreshBefore, MaxLifetime, and CacheTTL fields. The legacy whole-second fields were removed.

These changes are intentional while the module remains below v1. Update callers as one migration rather than relying on the v0.0.101 API shape.

Architecture

Client
  ├── Sandboxes()   → SandboxInterface    (create, get, list, delete, watch, wait, logs)
  ├── Exec()        → ExecInterface       (run, stream, interactive)
  ├── Files()       → FileInterface       (upload, download)
  ├── Health()      → HealthInterface     (health check, gateway info, current user)
  ├── Services()    → ServiceInterface    (expose, get, list, delete)
  ├── Providers()   → ProviderInterface   (CRUD + ensure)
  │     ├── Profiles() → ProfileInterface (list, get, import, update, lint, delete)
  │     └── Refresh()  → RefreshInterface (configure, status, rotate, delete)
  ├── Workspaces()  → WorkspaceInterface  (create, get, list, delete, members)
  └── Policy()      → PolicyInterface     (draft review, approve, reject, merge, status)

All domain types live in openshell/v1/types/. Proto-to-Go conversions happen in an internal converter layer. The public API surface uses type aliases so consumers import a single package. See the Architecture overview for details.

Features

Use CloseInteractiveInput(session) to close stdin and resize input while keeping output readable. SDK sessions implement the optional InteractiveSessionControl interface (CloseWrite() and Cancel()); the original InteractiveSession interface remains unchanged for existing mocks and wrappers. Input closure returns ErrorUnimplemented for sessions without that capability and leaves them open. CancelInteractive(session) uses Cancel() when available and otherwise calls Close(). SDK close/cancel operations are idempotent; writes and resizes after input closure return io.ErrClosedPipe. Drain Read concurrently with waiting for ExitCode(). ExitCode() waits for final gRPC status and returns any observed process exit code alongside a later stream error. An exit event alone does not establish successful stream completion.

Feature Interface Docs
Sandbox lifecycle (create, get, list, delete, watch, wait) SandboxInterface Sandboxes
Command execution (collected, streamed, interactive PTY) ExecInterface Exec
Provider management (CRUD + idempotent ensure) ProviderInterface Providers
Provider profiles (list, import, lint, update) ProfileInterface Profiles
Credential refresh (configure, rotate, status) RefreshInterface Refresh
Service exposure (expose, list, delete) ServiceInterface Services
File transfer API (transport capability-gated) FileInterface Files
Policy management (draft review, approve, reject, merge, global policy) PolicyInterface Policy
Sandbox logs (streaming retrieval) SandboxInterface Sandboxes
Workspace management (create, get, list, delete, members) WorkspaceInterface Workspaces
Sandbox provider attachment (attach, detach, list) SandboxInterface Sandboxes
Gateway info and current user identity HealthInterface Health
Health checking HealthInterface Health
SSH tunneling and TCP forwarding SSHInterface, TCPInterface SSH, TCP
Auth: static token, refreshable token (oauth2.TokenSource) AuthProvider Auth
Edge auth: extra headers, Cloudflare Access, WebSocket tunnel AuthProvider, edge.TunnelProxy Edge
Typed errors (IsNotFound, IsAlreadyExists, IsConflict, ...) StatusError Error Handling
Real-time watch with auto-stop on terminal phase WatchInterface[T] Sandboxes
Fake client for testing (no gRPC server needed) fake.Client Testing
OIDC login and renewable service auth oidc.Login, oidc.DeviceLogin, oidc.ClientCredentials, oidc.NewClientCredentialsAuth OIDC
Gateway config convenience (load CLI gateway configs, auto-wire auth) gateway.NewClient, gateway.LoadConfig Gateway

Prerequisites

  • Go 1.25.13 or later
  • mise (recommended for reproducible builds)

Build and Test

git clone https://github.com/NVIDIA/OpenShell.git
cd OpenShell/sdk/go

mise run test    # Run tests with coverage
mise run lint    # Run golangci-lint
mise run ci      # Full CI pipeline (lint + build + test)

Build commands use mise for reproducible tool management.

Documentation

Full API documentation is available at the OpenShell Go SDK Docs site.

To build the docs locally:

cargo install mdbook
mdbook serve docs

License

Apache-2.0. See LICENSE for details.

Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.