Skip to content

Repository files navigation

openshellctl

Lightweight Go client and CLI for NVIDIA OpenShell gateways. A pure-Go reimplementation of the upstream Rust CLI that is a full standalone replacement — it does not depend on or shell out to the upstream openshell binary. Built for embedding in operators and CI pipelines as a single static binary with zero external dependencies.

Installation

go install github.com/openshift-online/openshellctl/cmd/openshellctl@latest

Or build from source:

git clone https://github.com/openshift-online/openshellctl.git
cd openshellctl
make build          # binary at ./openshellctl

Quick start

# Point at a gateway (env vars or config file)
export OPENSHELL_GATEWAY_ENDPOINT=https://gateway.example.com
export OPENSHELL_TOKEN=$(cat /path/to/token)

# Create a sandbox and connect
openshellctl sandbox create --from python
openshellctl sandbox connect my-sandbox

# Run a one-shot command
openshellctl sandbox exec --name my-sandbox -- python3 -c "print('hello')"

# Upload code, run it
openshellctl sandbox upload my-sandbox ./myproject /workspace
openshellctl sandbox exec --name my-sandbox -- python3 /workspace/main.py

# Clean up
openshellctl sandbox delete my-sandbox

Authentication

openshellctl resolves authentication in priority order — the first match wins:

1. Personal login (interactive use)

If you already have an openshell gateway login session, openshellctl reads the same token file — no separate login required:

openshell gateway login <gateway-name>   # one-time setup (upstream CLI)
openshellctl sandbox list                # uses your login token automatically

openshellctl reads oidc_token.json from $XDG_CONFIG_HOME/openshell/gateways/<name>/. If the access token is expired and a refresh token is present, openshellctl exchanges it for a new one automatically. The upstream openshell binary is only needed for the initial gateway login — after that, openshellctl is fully standalone.

2. Service account (CI / fire-and-forget)

Set a client secret and openshellctl mints tokens via client_credentials — no login required, and tokens are re-minted automatically when they expire (even during long-running sessions):

export OPENSHELL_OIDC_CLIENT_SECRET=<secret>
# Optional overrides (defaults come from gateway metadata):
# export OPENSHELL_OIDC_CLIENT_ID=<client-id>
# export OPENSHELL_OIDC_ISSUER=<issuer-url>

openshellctl sandbox create -f job.yaml --no-keep
# Token re-mints transparently — the --no-keep cleanup always has a valid token

This is the recommended method for automation, CI pipelines, and any non-interactive use. Unlike the upstream Rust CLI (which bakes the token once at startup), openshellctl refreshes per-request, so long-running jobs never fail with ExpiredSignature.

3. Static bearer token

For quick testing or external token management:

openshellctl --token <JWT> sandbox list
# or
export OPENSHELL_TOKEN=<JWT>

This overrides all other auth methods. The token is used as-is with no refresh — if it expires, commands will fail.

How auth retry works

Every API call goes through an automatic retry: if the gateway returns Unauthenticated, openshellctl invalidates its cached token and retries once with a fresh one. For service accounts this means a full re-mint; for personal logins it means a refresh-token exchange. This is why --no-keep cleanup works reliably even after hour-long sessions.

Token management

openshellctl token show               # display token subject, expiry, roles
openshellctl token refresh             # force a token refresh
openshellctl token refresh --write     # refresh and persist to oidc_token.json
openshellctl token inspect <JWT>       # decode a JWT's claims (no sig verify)

If a command fails with an expired or invalid token, openshellctl prints:

Error: invalid token: ExpiredSignature
Hint: try `openshellctl token refresh` to obtain a new token.

Configuration

Configuration is resolved in priority order:

  1. CLI flags (highest)
  2. Environment variables (OPENSHELL_*)
  3. Config file ($XDG_CONFIG_HOME/openshellctl/config.yaml, override with --config)
  4. Gateway config dirs ($XDG_CONFIG_HOME/openshell/gateways/<name>/)

Global flags

These flags apply to every command:

Flag Env var Description
--gateway, -g OPENSHELL_GATEWAY Gateway name (selects config directory)
--gateway-endpoint OPENSHELL_GATEWAY_ENDPOINT Gateway gRPC endpoint URL
--gateway-insecure OPENSHELL_GATEWAY_INSECURE Skip TLS verification
--workspace OPENSHELL_WORKSPACE Workspace name (default: default)
--token OPENSHELL_TOKEN Bearer token (overrides OIDC)
--config Config file path
--verbose, -v Increase verbosity (repeat for more)
--no-color NO_COLOR Disable coloured output
--oidc-issuer OIDC issuer URL override
--oidc-client-id OIDC client ID override
--oidc-audience OIDC audience override
--oidc-scopes OIDC scopes (space-separated)
--client-secret-file File containing the OIDC client secret
--token-leeway Token expiry leeway (default: 30s)
--write-token Write refreshed tokens to disk (Rust CLI schema)

Commands

sandbox create

Create a sandbox and optionally connect to it.

# Create from a community image
openshellctl sandbox create --from python
openshellctl sandbox create --from python --name my-sandbox

# Create with GPU
openshellctl sandbox create --from pytorch --gpu       # driver default count
openshellctl sandbox create --from pytorch --gpu 2     # explicit count

# Create from a full image reference
openshellctl sandbox create --from ghcr.io/org/image:tag

# Create from a manifest file (everything in one file)
openshellctl sandbox create -f sandbox.yaml
cat sandbox.yaml | openshellctl sandbox create -f -

# Create with uploads and port forwarding
openshellctl sandbox create --from python \
  --upload ./src:/workspace \
  --upload ./data:/data \
  --forward 8080 \
  --no-git-ignore

# Create and run a command
openshellctl sandbox create --from python -- python3 main.py

# Create without attaching
openshellctl sandbox create --from python --detach

# Output as JSON/YAML instead of table
openshellctl sandbox create --from python -o json

Create flags

Flag Description
--from Image reference or community name (e.g. python, pytorch)
--name Sandbox name (auto-generated if omitted; max 19 chars)
-f, --file Manifest YAML file (- for stdin)
--gpu [COUNT] Request GPU resources (bare = driver default, or specify count)
--cpu CPU request (e.g. 500m, 2)
--memory Memory request (e.g. 1Gi, 512Mi)
--env Environment variable KEY=VALUE (repeatable)
--label Label key=value (repeatable)
--tty Force pseudo-terminal allocation
--no-tty Disable pseudo-terminal (last-wins with --tty)
--upload Upload local[:dest] after create (repeatable)
--no-git-ignore Disable .gitignore filtering for uploads
--forward Forward [bind:]port to sandbox
--detach Return after create without attaching
--provider Provider to attach (repeatable)
--auto-providers Enable auto provider detection
--no-auto-providers Disable auto provider detection
--no-credential-warnings Suppress credential exposure warnings
--approval-mode Proposal approval mode: manual (default) or auto
--no-keep Delete the sandbox after the session ends
--driver-config-json Driver config as inline JSON
--policy Sandbox policy file (env: OPENSHELL_SANDBOX_POLICY)
-o, --output Output format: table (default), json, yaml

Manifest format

A manifest file (-f) lets you put everything — sandbox spec, command, and session behavior — into one YAML file:

apiVersion: openshell.managed.openshift.io/v1alpha1
kind: Sandbox
metadata:
  name: sop-improve
  labels:
    team: sre-platform
spec:
  image: quay.io/redhat-services-prod/rosa-tenant/rosa-agent/rosa-agent:latest
  command: ["claude", "/job-sop-improve"]
  providerRefs:
    - name: rosa-general-vertex
    - name: rosa-agent-github
    - name: rosa-agent-jira
  env:
    ANTHROPIC_BASE_URL: https://inference.local
    ANTHROPIC_API_KEY: unused
    JIRA_EMAIL: sd-sre-platform+rosa-agent@redhat.com
    JIRA_BASE_URL: https://redhat.atlassian.net
  resources:
    cpu: "2"
    memory: 4Gi
    gpu:
      count: 1
  upload:
    - local: ./src
      dest: /workspace
  sessionOpts:
    noKeep: true
    forward: "8080"
    approvalMode: auto
    output: table

Then create and delete are both one-liners:

openshellctl sandbox create -f sop-improve.yaml
openshellctl sandbox delete -f sop-improve.yaml

spec.command works like a Kubernetes pod spec — it's the command (with arguments) to run inside the sandbox. If omitted, the sandbox's default entrypoint (/bin/bash -l) is used.

spec.sessionOpts groups client-side session behavior:

Field Type Description
noKeep bool Delete the sandbox when the session ends
detach bool Return after create, do not attach
forward string [bind:]port to forward to the sandbox
approvalMode string manual (default) or auto
output string table (default), json, or yaml

These fields are ignored by the operator — they control what the CLI does after creation. CLI flags always override manifest values.

spec.policy (inline map) and spec.policyFile (path string) are mutually exclusive. spec.policyFile is resolved relative to the manifest file's directory; when the manifest is read from stdin, policyFile must be an absolute path.

sandbox get

Show details of a single sandbox.

openshellctl sandbox get my-sandbox
openshellctl sandbox get my-sandbox -o json
openshellctl sandbox get my-sandbox -o yaml
openshellctl sandbox get -f sandbox.yaml       # read name from manifest
openshellctl sandbox get                       # uses last-used sandbox
Flag Description
-f, --file Manifest file to read sandbox name from (- for stdin)
-o, --output Output format: table (default), json, yaml

sandbox list

List sandboxes in the current workspace.

openshellctl sandbox list
openshellctl sandbox list -o json
openshellctl sandbox list --all-workspaces
openshellctl sandbox list --names          # one name per line
openshellctl sandbox list --ids            # one ID per line
openshellctl sandbox list --selector "gpu=true"
openshellctl sandbox list --limit 10 --offset 20
Flag Description
-o, --output Output format: table (default), json, yaml
--all-workspaces List across all workspaces
--names Print names only (one per line)
--ids Print IDs only (one per line)
--selector Label selector filter
--limit Max results (default: 100)
--offset Result offset for pagination

sandbox delete

Delete one or more sandboxes. Supports -f to read the sandbox name from a manifest, like kubectl delete -f.

openshellctl sandbox delete my-sandbox
openshellctl sandbox delete sb-1 sb-2 sb-3
openshellctl sandbox delete -f sandbox.yaml          # reads name from manifest
openshellctl sandbox delete --all
openshellctl sandbox delete my-sandbox --wait
openshellctl sandbox delete my-sandbox --wait --wait-timeout 2m
Flag Description
-f, --file Manifest file to read sandbox name from (- for stdin)
--all Delete all sandboxes in the workspace
--wait Wait for deletion to complete
--wait-timeout Wait timeout (default: 5m)

sandbox stop / sandbox start

Stop or start a sandbox.

openshellctl sandbox stop my-sandbox
openshellctl sandbox start my-sandbox
openshellctl sandbox stop -f sandbox.yaml    # read name from manifest
openshellctl sandbox start -f sandbox.yaml
openshellctl sandbox stop                    # uses last-used sandbox
openshellctl sandbox start                   # uses last-used sandbox
Flag Description
-f, --file Manifest file to read sandbox name from (- for stdin)

sandbox exec

Run a command inside a sandbox. The command must follow --.

openshellctl sandbox exec --name my-sandbox -- ls -la
openshellctl sandbox exec --name my-sandbox -- python3 -c "print('hello')"

# With environment variables
openshellctl sandbox exec --name my-sandbox --env FOO=bar -- env

# With working directory
openshellctl sandbox exec --name my-sandbox --workdir /workspace -- make build

# With timeout (seconds)
openshellctl sandbox exec --name my-sandbox --timeout 60 -- long-running-task

# Force/disable TTY
openshellctl sandbox exec --name my-sandbox --tty -- bash
openshellctl sandbox exec --name my-sandbox --no-tty -- cat /etc/os-release

# Interactive mode (stdin is forwarded when --tty + terminal detected)
openshellctl sandbox exec --name my-sandbox --tty -- bash
Flag Description
-n, --name Sandbox name (defaults to last-used)
-f, --file Manifest file to read sandbox name from (- for stdin)
--env Environment variable KEY=VALUE (repeatable)
--workdir Working directory inside the sandbox
--timeout Timeout in seconds (0 = none)
--tty Force pseudo-terminal allocation
--no-tty Disable pseudo-terminal (last-wins with --tty)

The process exit code matches the remote command's exit code.

sandbox connect

Open an interactive shell session to a sandbox.

openshellctl sandbox connect my-sandbox
openshellctl sandbox connect -f sandbox.yaml   # read name from manifest
openshellctl sandbox connect                   # uses last-used sandbox
Flag Description
-f, --file Manifest file to read sandbox name from (- for stdin)

Detach from the session with Ctrl-P Ctrl-Q (the connection closes cleanly and exits 0).

sandbox upload

Upload files or directories to a sandbox.

openshellctl sandbox upload my-sandbox ./myproject
openshellctl sandbox upload my-sandbox ./myproject /workspace
openshellctl sandbox upload my-sandbox ./file.txt /remote/path/file.txt
openshellctl sandbox upload -f sandbox.yaml ./src /workspace

Arguments are NAME LOCAL_PATH [DEST]. If DEST is omitted, files are placed in the sandbox's home directory. Directory uploads use scp-like semantics (./myproject~/myproject/).

By default, .gitignore rules are respected for directory uploads. Use --no-git-ignore to include all files.

Flag Description
-f, --file Manifest file to read sandbox name from (- for stdin)
--no-git-ignore Disable .gitignore filtering for directory uploads

sandbox download

Download files or directories from a sandbox.

openshellctl sandbox download my-sandbox /workspace/output
openshellctl sandbox download my-sandbox /workspace/output ./local-dir
openshellctl sandbox download -f sandbox.yaml /workspace/output

Arguments are NAME SANDBOX_PATH [DEST]. If DEST is omitted, files are placed in the current directory.

Flag Description
-f, --file Manifest file to read sandbox name from (- for stdin)

sandbox ssh-config

Print an SSH configuration block for use with the upstream openshell binary.

openshellctl sandbox ssh-config my-sandbox
openshellctl sandbox ssh-config -f sandbox.yaml   # read name from manifest
openshellctl sandbox ssh-config                   # uses last-used sandbox
Flag Description
-f, --file Manifest file to read sandbox name from (- for stdin)

This outputs a block suitable for ~/.ssh/config, using openshellctl ssh-proxy as the ProxyCommand.

sandbox provider

Manage providers attached to a sandbox.

# List providers
openshellctl sandbox provider list my-sandbox
openshellctl sandbox provider list -f sandbox.yaml

# Attach a provider
openshellctl sandbox provider attach my-sandbox github

# Detach a provider
openshellctl sandbox provider detach my-sandbox github

Attach and detach use optimistic concurrency — if another operation modifies the sandbox concurrently, you will see:

Failed to attach provider: sandbox was modified by another operation.
Please retry the command.

logs

View sandbox logs. This is a top-level command (alias: lg).

openshellctl logs my-sandbox
openshellctl logs -f sandbox.yaml              # read name from manifest
openshellctl logs my-sandbox --tail            # stream live
openshellctl logs my-sandbox -n 50             # last 50 lines
openshellctl logs my-sandbox --since 5m        # last 5 minutes
openshellctl logs my-sandbox --level error     # errors only
openshellctl logs my-sandbox --source gateway  # gateway logs only
Flag Description
-f, --file Manifest file to read sandbox name from (- for stdin)
--tail Stream live logs
-n Number of log lines (default: 200)
--since Show logs from this duration ago (e.g. 5m, 1h, 30s)
--level Minimum level: error, warn, info, debug, trace
--source Filter by source: gateway, sandbox, or all (default)

token

Inspect and manage the gateway authentication token.

openshellctl token show                # show token metadata
openshellctl token refresh             # force refresh
openshellctl token refresh --write     # refresh and persist
openshellctl token inspect <JWT>       # decode JWT claims

policy lint

Client-side validation of a sandbox policy file.

openshellctl policy lint policy.yaml

Checks cover process uid/gid ranges, filesystem path constraints, middleware selectors, endpoint ports, and TCP access rules. The gateway's server-side validation remains authoritative.

version

Print build information.

openshellctl version

Shows: version, commit, OpenShell pin version, and SDK version.

completion

Generate shell completion scripts.

openshellctl completion bash
openshellctl completion zsh
openshellctl completion fish
openshellctl completion powershell

Aliases

Command Alias
sandbox sb
logs lg
openshellctl sb list          # same as: openshellctl sandbox list
openshellctl lg my-sandbox    # same as: openshellctl logs my-sandbox

Last-used sandbox

Commands that take a sandbox name remember the last-used sandbox per gateway and workspace. If you omit the name argument, the last-used sandbox is used automatically:

openshellctl sandbox exec --name my-sandbox -- whoami   # saves my-sandbox
openshellctl sandbox exec -- whoami                     # reuses my-sandbox
openshellctl sandbox connect                            # reuses my-sandbox
openshellctl logs                                       # reuses my-sandbox

Exit codes

Code Meaning
0 Success
1 Generic / RPC error
2 Usage error (bad flags, validation failure, invalid argument rejected by gateway)
3 Authentication failure (expired/invalid token, missing credentials)
4 Not found (sandbox, gateway)
5 Conflict (already exists, concurrent modification)
6 Provisioning failure or timeout
N Remote process exit code (from exec/connect/create with attached command)

Environment variables

Variable Description
OPENSHELL_GATEWAY Default gateway name
OPENSHELL_GATEWAY_ENDPOINT Gateway endpoint URL
OPENSHELL_GATEWAY_INSECURE Skip TLS verification (true/1)
OPENSHELL_WORKSPACE Default workspace
OPENSHELL_TOKEN Bearer token (overrides all other auth)
OPENSHELL_OIDC_CLIENT_SECRET OIDC client secret (enables service account auth)
OPENSHELL_OIDC_CLIENT_ID OIDC client ID override (default from gateway metadata)
OPENSHELL_OIDC_ISSUER OIDC issuer URL override (default from gateway metadata)
OPENSHELL_SANDBOX_POLICY Default sandbox policy file path
NO_COLOR Disable coloured output (any value)

Differences from the upstream Rust CLI

openshellctl is a compatible reimplementation with these intentional differences:

  • Pure Go, zero dependencies: no shell-out to ssh, tar, git, or any other binary — including the upstream openshell CLI. All SSH transfer, tar streaming, and gitignore filtering are implemented in-process. Single static binary.
  • Per-request auth with automatic retry: tokens are resolved per-RPC (not baked in at startup). On Unauthenticated, the token is refreshed (or re-minted for service accounts) and the request retried. Long-running sessions and --no-keep cleanup work reliably even after token expiry.
  • Embeddable: pkg/gateway, pkg/transfer, pkg/sandbox, and pkg/auth are importable Go packages with interface-driven I/O seams, suitable for use in operators and controllers.
  • No interactive spinner: provisioning progress uses plain text output instead of terminal spinners.
  • policy lint is a separate top-level command (not under sandbox).
  • Gitignore filtering: uses compiled .gitignore rules instead of shelling out to git ls-files. Tracked-but-ignored files are excluded (matching untracked behaviour) rather than re-included.

License

See LICENSE.

About

Lightweight, CI-friendly Go client for OpenShell

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages